Compare commits

...
102 Commits
Author SHA1 Message Date
欣南科技andGitHub 16e5dc8c78 Merge pull request #3258 from xinnan-tech/update-version
update:Bump to 0.9.5
2026-06-30 13:10:18 +08:00
hrz f77970dd7d update:Bump to 0.9.5 2026-06-30 13:10:26 +08:00
hrzandGitHub 1ebfcd1f13 Merge pull request #3256 from xinnan-tech/update-theme
解决智能体管理查询不展示的问题&删除知识库旧版废弃文件
2026-06-30 09:36:42 +08:00
zhuoqinglian 3ca322961e fix: 解决智能体管理查询不展示的问题 2026-06-30 09:22:05 +08:00
zhuoqinglian 0eb0a600a7 update: 删除知识库旧版废弃文件 2026-06-30 09:19:26 +08:00
wengzhandGitHub 3ed06a9e34 Merge pull request #3244 from xinnan-tech/base_prompt-update
feat: 将系统提示词指令逻辑英文化,中文样本保留
2026-06-30 09:06:10 +08:00
wengzhandGitHub 67dde7cc47 Merge pull request #3254 from xinnan-tech/update-theme
智能体管理搜索恢复查询历史功能
2026-06-29 16:58:52 +08:00
zhuoqinglian efedc18f02 update: 智能体管理搜索恢复查询历史功能 2026-06-29 16:01:14 +08:00
zhuoqinglian 0270de1844 update: 滚动条样式抽离复用 2026-06-29 15:42:52 +08:00
wengzhandGitHub 06b0ff139b Merge pull request #3247 from xinnan-tech/update-theme
Update theme
2026-06-26 17:58:18 +08:00
zhuoqinglian 799ea28d82 Merge branch 'main' into update-theme 2026-06-26 17:30:15 +08:00
zhuoqinglian 5a68252d22 update: 更新所有页面背景颜色 2026-06-26 17:28:06 +08:00
zhuoqinglian 97404e53fe update: 顶部导航栏搜索放到首页 2026-06-26 17:26:16 +08:00
zhuoqinglian b052158cee feat: 完成导航菜单组件样式优化 2026-06-26 17:10:49 +08:00
zhuoqinglian 979ef24b79 update: 音色克隆多语言配置 2026-06-26 15:32:48 +08:00
zhuoqinglian 0f4f0f01fd update: 音色克隆页面展现形式修改 2026-06-26 14:45:23 +08:00
wengzhandGitHub 18e1de4f45 Merge pull request #3245 from xinnan-tech/py-add-minimax-voice
Py add minimax voice
2026-06-26 11:42:59 +08:00
Sakura-RanChen aa846a096f 名称长度调整 2026-06-26 11:41:46 +08:00
Sakura-RanChen a679412230 增加试听音色 2026-06-26 11:40:52 +08:00
3030332422 cf3e14bb07 feat: 将系统提示词指令逻辑英文化,中文样本保留 2026-06-26 11:06:18 +08:00
wengzhandGitHub 3a73a17253 Merge pull request #3243 from xinnan-tech/py-add-minnimax-voice
调整minimax音色
2026-06-26 10:50:26 +08:00
Sakura-RanChen 3b27ebcd35 调整minimax音色 2026-06-26 10:29:07 +08:00
wengzhandGitHub fa6074d284 Merge pull request #3242 from xinnan-tech/update-theme
Update theme
2026-06-25 17:29:55 +08:00
zhuoqinglian d809cfcb01 Merge branch 'main' into update-theme 2026-06-25 17:27:40 +08:00
zhuoqinglian eb2c09bbdc feat: 完成聊天记录、修改密码页面样式改造 2026-06-25 17:26:52 +08:00
zhuoqinglian 2cf8f79482 update: 模型配置新增、编辑弹窗样式优化 2026-06-25 17:09:29 +08:00
zhuoqinglian 1c94844bb9 feat: 完成声纹管理页面样式改造 2026-06-25 09:18:38 +08:00
wengzhandGitHub 0c53b8f543 Merge pull request #3240 from xinnan-tech/update-theme
Update theme
2026-06-24 17:58:13 +08:00
zhuoqinglian 1518dfca30 feat: 完成设备管理页面样式改造 2026-06-24 17:37:37 +08:00
zhuoqinglian 7529c500a4 feat: 完成音色克隆页面样式改造 2026-06-24 11:43:36 +08:00
zhuoqinglian b6ed220e48 feat: 完成音色资源页面样式改造 2026-06-23 17:23:11 +08:00
wengzhandGitHub b152c5dc23 Merge pull request #3239 from xinnan-tech/update-theme
模型配置页面样式调整
2026-06-23 16:31:30 +08:00
zhuoqinglian 04bfddf457 update: 按钮样式风格统一 2026-06-23 14:54:45 +08:00
zhuoqinglian 9d78ea29c3 Merge branch 'main' into update-theme 2026-06-23 14:15:57 +08:00
zhuoqinglian f82f364189 feat: 完成模型配置页面样式改造 2026-06-23 14:15:32 +08:00
zhuoqinglian 9705149b2b feat: 按钮组件新增add类型 2026-06-23 14:14:29 +08:00
wengzhandGitHub 7986148013 Merge pull request #3238 from xinnan-tech/py-add-edge-config
补充edge相关配置
2026-06-23 10:40:40 +08:00
Sakura-RanChen 19f5cb5b4f 补充edge相关配置 2026-06-23 10:37:48 +08:00
zhuoqinglian ea48b948bf update: 模型配置新增弹窗样式调整 2026-06-22 17:58:35 +08:00
zhuoqinglian 3c5ca9f143 update: 模型配置编辑弹窗样式调整 2026-06-22 17:31:09 +08:00
wengzhandGitHub da72ecaa1b Merge pull request #3235 from xinnan-tech/py-fix-tts
fix: 超时情况不与activate_session同一判断条件导致的监听任务未清除问题
2026-06-22 11:21:03 +08:00
Sakura-RanChen f297e1b648 fix: 超时情况不与activate_session同一判断条件导致的监听任务未清除问题 2026-06-22 11:16:25 +08:00
wengzhandGitHub a1973e07b7 Merge pull request #3227 from xinnan-tech/update-theme
feat: 完成字典管理页面样式改造
2026-06-18 16:57:49 +08:00
zhuoqinglian cd58b2c3cb Merge branch 'main' into update-theme 2026-06-18 16:55:33 +08:00
zhuoqinglian a5be67d04b feat: 完成字典管理页面样式改造 2026-06-18 16:49:15 +08:00
wengzhandGitHub e6dc6187c8 Merge pull request #3226 from xinnan-tech/update-theme
页面样式改造
2026-06-18 16:39:41 +08:00
zhuoqinglian 7f4011f9de feat: 完成字段管理页面样式改造 2026-06-18 16:37:42 +08:00
zhuoqinglian 211cf891af Merge branch 'main' into update-theme 2026-06-18 10:26:57 +08:00
zhuoqinglian 89a1229b53 feat: 完成服务端管理页面样式改造 2026-06-18 10:26:06 +08:00
wengzhandGitHub 071ed99a32 Merge pull request #3224 from xinnan-tech/py-fix-bug
fix: 通讯录页面时间不显示
2026-06-17 14:36:48 +08:00
Sakura-RanChen ea14be27e6 fix: 通讯录页面时间不显示
fix: listen消息相关状态处理
2026-06-17 14:34:42 +08:00
zhuoqinglian a834f163b7 feat: 完成替换词页面样式改造 2026-06-17 14:20:48 +08:00
zhuoqinglian 7849e6b030 update: 模板保存添加必填校验 2026-06-17 10:36:02 +08:00
wengzhandGitHub 0d869baaa0 Merge pull request #3223 from xinnan-tech/update-theme
页面样式调整
2026-06-16 17:01:10 +08:00
zhuoqinglian 164eb5481c feat: 完成默认角色管理页面样式改造,并添加编辑保存弹窗 2026-06-16 16:58:44 +08:00
zhuoqinglian 969ee3cb12 feat: 完成字段管理页面样式改造 2026-06-16 16:18:12 +08:00
zhuoqinglian ee54d87383 feat: 完成固件管理页面样式改造 2026-06-16 14:55:08 +08:00
wengzhandGitHub 849026c015 Merge pull request #3222 from xinnan-tech/update-theme
Update theme
2026-06-16 10:42:24 +08:00
zhuoqinglian e5bb3e7791 update: 优化表格组件,增加代码的复用性 2026-06-16 10:40:44 +08:00
zhuoqinglian 1ffc40e71d feat: 完成用户管理页面样式改造 2026-06-16 10:39:52 +08:00
zhuoqinglian 45fd850757 update: 表格组件添加阴影效果 2026-06-16 09:46:25 +08:00
zhuoqinglian 8a5dac181e update: 知识库顶部标题合并 2026-06-15 17:37:53 +08:00
zhuoqinglian ecf20300ff update: 通讯录初始化加载默认选中第一项 2026-06-15 17:16:50 +08:00
zhuoqinglian 74def844f0 Merge branch 'main' into update-theme 2026-06-15 17:05:28 +08:00
zhuoqinglian a5a9bd1abe fix: 修复element组件多语言切换不生效的问题 2026-06-15 17:05:04 +08:00
zhuoqinglian 2d9b915d51 feat: 完成参数管理页面改造 2026-06-15 15:44:09 +08:00
hrzandGitHub 5f87fc8361 Merge pull request #3204 from xinnan-tech/py-fix-calling
优化相关逻辑
2026-06-15 14:06:51 +08:00
zhuoqinglian 6859fdc90e update: 系统功能配置图标大小优化 2026-06-15 11:27:12 +08:00
zhuoqinglian b8ae0568e9 update: 系统功能配置页面适配调整 2026-06-15 11:23:31 +08:00
wengzhandGitHub 1dc6f143f3 Merge pull request #3219 from xinnan-tech/update-theme
完成系统功能页面样式改造
2026-06-12 14:43:23 +08:00
zhuoqinglian 67532dc230 update: 系统功能配置完善多语言 2026-06-12 14:23:25 +08:00
zhuoqinglian b2876def99 Merge branch 'main' into update-theme 2026-06-12 14:14:59 +08:00
zhuoqinglian 0c73a8b6a4 feat: 完成系统功能页面样式改造 2026-06-12 14:13:21 +08:00
zhuoqinglian c857a3164d feat: 完成按钮组件抽离 2026-06-12 11:49:22 +08:00
hrzandGitHub a964192eb0 Merge pull request #3213 from xinnan-tech/fix-knowledge-base-ragflow-bug
#3212 修复知识库RAGFlow相关逻辑BUG
2026-06-12 11:15:35 +08:00
DaGou12138 32da03818e #3212 fix:
1.修改知识库前端卡片提示样式,优化警示效果
2026-06-12 10:35:40 +08:00
DaGou12138 7f17e3e08e Merge branch 'refs/heads/main' into fix-knowledge-base-ragflow-bug 2026-06-12 10:29:17 +08:00
DaGou12138 c237e1f323 Merge remote-tracking branch 'origin/fix-knowledge-base-ragflow-bug' into fix-knowledge-base-ragflow-bug 2026-06-12 09:51:03 +08:00
DaGou12138 087a202e31 #3212 fix:
1.优化知识库连接RAGFlow异常提示,修改前端卡片提示样式
2026-06-12 09:50:01 +08:00
hrz 4b1460d938 update:调整同步数据集错误日志的级别 2026-06-11 19:10:21 +08:00
hrz f2aec524d2 del:无用类 2026-06-11 18:57:30 +08:00
wengzhandGitHub 1a97ca137c Merge pull request #3215 from xinnan-tech/update-theme
Update theme
2026-06-11 17:45:58 +08:00
zhuoqinglian 2b6f457ae6 update: 知识库页面间距优化 2026-06-11 17:44:17 +08:00
DaGou12138 3f4e0b4aa0 #3212 fix:
1.修复RAGFlow连接异常导致级联删除问题
2.修复RAGFlow获取数量逻辑问题
3.远程失败不再删除本地影子记录,避免产生RAGFlow端孤儿文档
4.优化前端提示语
2026-06-11 17:17:59 +08:00
DaGou12138 e30ded4de9 fix:删除知识库rag相关非必要实体 2026-06-11 16:08:17 +08:00
hrzandGitHub 07242a84ff Merge pull request #3210 from xinnan-tech/fix-knowledge-base-web-bug
fix:修复知识库模块弹框组件样式全局覆盖导致显示异常问题
2026-06-10 15:32:08 +08:00
DaGou12138 a2074dc0d8 fix:修复知识库模块弹框组件样式全局覆盖导致显示异常问题 2026-06-10 15:00:29 +08:00
zhuoqinglian 28b6c85650 fix: 修复弹窗展示位置异常的问题 2026-06-10 10:47:52 +08:00
wengzhandGitHub af300138eb Merge pull request #3209 from xinnan-tech/update-theme
Update theme
2026-06-09 17:24:16 +08:00
zhuoqinglian 2bf5725048 update: 知识库页面复用弹窗、分页组件,同步修改样式 2026-06-09 17:22:18 +08:00
hrzandGitHub 3d73141788 Merge pull request #3206 from xinnan-tech/web_search-Document
docs:新增联网搜索插件配置文档
2026-06-09 17:12:16 +08:00
hrz 945ab29bc3 add:增加联网搜索的教程和自编译docker镜像的教程 2026-06-09 17:11:35 +08:00
zhuoqinglian fe0f7dcf75 feat: 完成分页组件抽离 2026-06-09 15:05:16 +08:00
zhuoqinglian 1a3ffbc8e2 feat: 完成弹窗组件抽离 2026-06-09 14:25:49 +08:00
3030332422 71278b1898 docs:新增联网搜索插件配置文档 2026-06-08 11:38:11 +08:00
zhuoqinglian 0173c7abe1 update: 知识库样式优化、添加多语言配置 2026-06-05 17:37:21 +08:00
zhuoqinglian 8a50eea4ce update: 知识库整体页面样式调整 2026-06-03 16:49:08 +08:00
Sakura-RanChen b9891bb795 优化相关逻辑 2026-06-03 16:13:03 +08:00
wengzhandGitHub b903a88cd5 Merge pull request #3203 from xinnan-tech/mac-address-mask
feat: mac地址添加马赛克功能
2026-06-03 14:41:34 +08:00
zhuoqinglian f3fcf2fedf feat: mac地址添加马赛克功能 2026-06-03 14:40:01 +08:00
欣南科技andGitHub a825f0874c Merge pull request #3202 from xinnan-tech/update-doc-video
update:更新演示视频
2026-06-03 13:58:14 +08:00
hrz 8a5ca46418 update:更新演示视频 2026-06-03 13:56:34 +08:00
118 changed files with 7895 additions and 13681 deletions
+68 -83
View File
@@ -53,113 +53,98 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="docs/images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="docs/images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="docs/images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="docs/images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+2
View File
@@ -65,6 +65,7 @@ VAD:
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
6、[修改代码后怎么编译自己的Docker镜像](./docker-build.md)<br/>
### 8、编译固件相关教程
1、[如何自己编译小智固件](./firmware-build.md)<br/>
@@ -85,6 +86,7 @@ VAD:
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
14、[如何开启联网搜索功能](./web-search-integration.md)<br/>
### 10、数字人相关教程
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
+66 -13
View File
@@ -1,21 +1,74 @@
# 本地编译docker镜像方法
现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的
现在本项目已经使用`github``自动编译docker镜像`功能,如果您拉取的是项目发行的镜像,您没有自己编译镜像需求,那就忽略本文档
1、安装docker
```
如果您修改了源码,然后想采用`docker`的方式部署运行,可以参照以下步骤操作:
## 1、环境准备
安装docker
```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
2、编译docker镜像
```
#进入项目根目录
# 编译server
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
# 编译web
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
# 编译完成后,可以使用docker-compose启动项目
# docker-compose.yml你需要修改成自己编译的镜像版本
## 2、编译镜像
当你修改好代码后,需要编译新的镜像时,需要按照以下步骤操作:
准备好你的`你的用户名``新的版本号`
- 这个`你的用户名`是你在`docker hub`注册的用户名,例如`xiaozhi`。当然,如果你不需要推送到`docker hub`,你可以自由定义。
- 这个`新的版本号`是你编译的镜像版本,例如`1.2.3`,你可以根据需要自定义或者使用日期格式(例如`20260609`)主要是方便和现在运行的版本号做区分,同时也方便下次回忆你是什么时候构建的,不要和现在你本机运行的版本号相同。
进入`xiaozhi-esp32-server`项目根目录,编译 server 和 web 两个镜像:
```bash
cd 项目根目录
# 编译server镜像
docker build -f Dockerfile-server -t 你的用户名/xiaozhi-esp32-server:新的版本号 .
# 编译web镜像
docker build -f Dockerfile-web -t 你的用户名/xiaozhi-esp32-server-web:新的版本号 .
```
## 3、修改docker-compose配置
```bash
cd main/xiaozhi-server
docker compose up -d
```
编辑 `docker-compose_all.yml` 文件,将镜像版本替换为你刚才编译的版本:
```yaml
services:
xiaozhi-esp32-server:
image: 你的用户名/xiaozhi-esp32-server:新的版本号 # 修改为你的镜像地址
...
xiaozhi-esp32-server-web:
image: 你的用户名/xiaozhi-esp32-server-web:新的版本号 #修改为你的镜像地址
...
```
## 4、重启服务
```bash
# 停止旧容器
docker compose -f docker-compose_all.yml down
# 启动新容器
docker compose -f docker-compose_all.yml up -d
```
## 5、验证
查看日志确认服务启动正常:
```bash
# 查看server日志
docker logs -f -n 50 xiaozhi-esp32-server
# 查看web日志
docker logs -f -n 50 xiaozhi-esp32-server-web
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 108 KiB

+68 -83
View File
@@ -52,113 +52,98 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+68 -83
View File
@@ -52,113 +52,98 @@ Want to see the usage effects? Click the videos below 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+68 -83
View File
@@ -52,113 +52,98 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="Cenário médico complexo" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="Reproduzir música" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="Plugin de clima" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="Interrupção em tempo real" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="Timbre de voz personalizado" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="Comunicação em cantonês" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="Transmissão de notícias" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+68 -83
View File
@@ -52,113 +52,98 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
<table>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="../images/demo9.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
<picture>
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="../images/demo6.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
<picture>
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="../images/demo1.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
<picture>
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="../images/demo4.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
<picture>
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="../images/demo14.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
<picture>
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="../images/demo5.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="../images/demo13.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="../images/demo11.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
<picture>
<img alt="播放音乐" src="../images/demo7.png" />
</picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
<picture>
<img alt="天气插件" src="../images/demo8.png" />
</picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
<picture>
<img alt="实时打断" src="../images/demo10.png" />
</picture>
</a>
<picture>
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
<picture>
<img alt="拍照识物品" src="../images/demo12.png" />
</picture>
</a>
<picture>
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="../images/demo2.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
<picture>
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="../images/demo3.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
<picture>
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
<picture>
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
<picture>
<img alt="播报新闻" src="../images/demo0.png" />
</picture>
</a>
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
<picture>
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
<picture>
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
<picture>
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
</a>
</td>
<td>
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
<picture>
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
</a>
</td>
</tr>
</table>
+71
View File
@@ -0,0 +1,71 @@
# 联网搜索插件使用指南
## 功能简介
联网搜索插件 `web_search` 支持在对话过程中实时联网搜索信息并返回结果。插件支持两个搜索源:秘塔(Metaso)和Tavily,用户可根据需要选择其中一个。
## API Key申请指南
目前我们适配了`秘塔搜索``Tavily搜索`
- Tavily搜索:每个月1000次免费额度。
- 秘塔搜索:拥有较为优质的国内数据源。
## API Key申请指南
### 方式一:使用秘塔搜索
- 访问 [秘塔搜索API](https://metaso.cn/search-api/api-keys),注册并登录账号
- 在API密钥管理页面,点击"创建新的Key"
- 复制生成的API Key(以 `mk-` 为前缀),这是配置所需的关键信息
### 方式二:使用Tavily搜索
- 访问 [Tavily控制台](https://app.tavily.com/home),注册并登录账号
- 在控制台中创建API Key
- 复制生成的API Key(以 `tvly-` 为前缀),这是配置所需的关键信息
## 配置方式
### 方式1. 使用智控台部署(推荐)
- 登录智控台
- 进入"配置角色"页面,选择要配置的智能体
- 点击"编辑功能"按钮,在右侧参数配置区域找到"联网搜索"插件
- 勾选"联网搜索"
- 填入搜索源(`metaso``tavily`),并将对应的`API Key`填入配置项
- 保存配置,再保存智能体配置
### 方式2. 单模块xiaozhi-server部署
`data/.config.yaml` 中配置:
- 将搜索源填入 `provider`,可选值为 `metaso``tavily`
- 将申请到的API Key填入 `api_key`
```yaml
plugins:
web_search:
provider: "metaso"
api_key: "你的API Key"
```
如需自定义返回结果数量和工具描述,可额外配置 `max_results``description`
```yaml
plugins:
web_search:
provider: "metaso"
description: "联网搜索工具。当用户明确需要联网搜索问题时使用此工具。"
max_results: 5
api_key: "你的API Key"
```
同时在 `functions` 列表中确保已启用 `web_search`
```yaml
plugins:
functions:
- web_search
```
配置完成后重启服务即可生效。
@@ -324,7 +324,7 @@ public interface Constant {
/**
* 版本号
*/
public static final String VERSION = "0.9.4";
public static final String VERSION = "0.9.5";
/**
* 无效固件URL
@@ -1,48 +0,0 @@
package xiaozhi.modules.agent.dto;
import lombok.Data;
/**
* MCP JSON-RPC 响应 DTO
*/
@Data
public class McpJsonRpcResponse {
private String jsonrpc = "2.0";
private Integer id;
private McpResult result;
private McpError error;
public McpJsonRpcResponse() {
}
@Data
public static class McpResult {
private String type;
private String message;
private String agent_id;
private McpTool[] tools;
public McpResult() {
}
}
@Data
public static class McpTool {
private String name;
private String description;
private Object inputSchema;
public McpTool() {
}
}
@Data
public static class McpError {
private Integer code;
private String message;
private Object data;
public McpError() {
}
}
}
@@ -8,6 +8,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -174,20 +175,26 @@ public class DeviceController {
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
}
@GetMapping("/address-book/lookup")
@Operation(summary = "根据昵称查找目标设备")
public Result<Map<String, String>> lookupByNickname(String callerMac, String nickname) {
Map<String, String> result = deviceAddressBookService.lookupByNickname(callerMac, nickname);
@GetMapping("/address-book/call")
@Operation(summary = "根据昵称发起呼叫")
public Result<Map<String, Object>> callByNickname(String callerMac, String nickname,
@RequestParam(required = false, defaultValue = "false") boolean answer) {
Map<String, Object> result = deviceAddressBookService.callByNickname(callerMac, nickname, answer);
if (result == null) {
return new Result<Map<String, String>>().error("未找到对应设备");
return new Result<Map<String, Object>>().error("未找到对应设备");
}
return new Result<Map<String, String>>().ok(result);
return new Result<Map<String, Object>>().ok(result);
}
@PutMapping("/address-book/alias")
@Operation(summary = "更新设备通讯录别名")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) {
UserDetail user = SecurityUser.getUser();
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
return new Result<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
return new Result<Void>();
}
@@ -196,14 +203,12 @@ public class DeviceController {
@Operation(summary = "更新设备通讯录权限")
@RequiresPermissions("sys:role:normal")
public Result<Void> updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) {
UserDetail user = SecurityUser.getUser();
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
return new Result<Void>().error("无权限操作该设备");
}
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
return new Result<Void>();
}
@GetMapping("/call/forward")
@Operation(summary = "转发呼叫请求到网关")
public Result<Map<String, Object>> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname);
return new Result<Map<String, Object>>().ok(result);
}
}
@@ -26,4 +26,9 @@ public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity
* 更新权限
*/
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
}
@@ -1,27 +0,0 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 设备绑定的DTO
*
* @author zjy
* @since 2025-3-28
*/
@Data
@AllArgsConstructor
@Schema(description = "设备连接头信息")
public class DeviceBindDTO {
@Schema(description = "mac地址")
private String macAddress;
@Schema(description = "所属用户id")
private Long userId;
@Schema(description = "智能体id")
private String agentId;
}
@@ -38,10 +38,15 @@ public interface DeviceAddressBookService {
void refreshCache();
/**
* 根据昵称查找目标设备信息
* 根据昵称发起呼叫
* @param callerMac 主叫方MAC地址
* @param nickname 被叫方昵称
* @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方}
* @param isAnswer 是否为接听模式(跳过权限检查)
*/
Map<String, String> lookupByNickname(String callerMac, String nickname);
Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer);
/**
* 批量删除设备相关的通讯录记录
*/
void deleteByMacAddresses(List<String> macAddresses);
}
@@ -138,10 +138,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
*/
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
/**
* 转发呼叫请求到网关
* @return 网关响应 {status, message}
*/
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
}
}
@@ -1,20 +1,27 @@
package xiaozhi.modules.device.service.impl;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.nio.charset.StandardCharsets;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.hutool.http.HttpRequest;
import cn.hutool.json.JSONUtil;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.sys.service.SysParamsService;
@Service
public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
@@ -22,12 +29,14 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
private final DeviceAddressBookDao deviceAddressBookDao;
private final RedisUtils redisUtils;
private final DeviceService deviceService;
private final SysParamsService sysParamsService;
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils,
DeviceService deviceService) {
@Lazy DeviceService deviceService, SysParamsService sysParamsService) {
this.deviceAddressBookDao = deviceAddressBookDao;
this.redisUtils = redisUtils;
this.deviceService = deviceService;
this.sysParamsService = sysParamsService;
}
@Override
@@ -47,33 +56,46 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
}
@Override
public Map<String, String> lookupByNickname(String callerMac, String nickname) {
public Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer) {
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
if (isAnswer) {
return postToMqtt("/api/call/accept", Map.of("mac", callerMac), "接听");
}
// 主动呼叫模式
Map<String, String> callerBook = allBooks.get(callerMac.toLowerCase());
if (callerBook == null) {
return null;
return errorResult("未找到备注为'" + nickname + "'的设备");
}
// 从缓存获取 targetMac,格式: "mac|permission"
String targetMacWithPerm = callerBook.get(nickname);
if (targetMacWithPerm == null) {
return null;
return errorResult("未找到备注为'" + nickname + "'的设备");
}
// 解析 targetMac 和 permission
String[] parts = targetMacWithPerm.split("\\|");
String targetMac = parts[0];
boolean hasPermission = parts.length > 1 && "1".equals(parts[1]);
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
if (targetBook == null) {
return null;
if (!hasPermission) {
return errorResult("呼叫失败,您没有权限呼叫该设备");
}
// 检查双向关系:目标设备是否也添加了主叫方为联系人
String callerNickname = targetBook.get(callerMac.toLowerCase());
Map<String, String> result = new HashMap<>();
result.put("targetMac", targetMac);
result.put("callerNickname", callerNickname);
result.put("hasPermission", hasPermission ? "true" : "false");
return result;
// 获取目标设备如何称呼主叫方
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
String callerNickname = null;
if (targetBook != null) {
callerNickname = targetBook.get(callerMac.toLowerCase());
}
if (StringUtils.isBlank(callerNickname)) {
callerNickname = deviceService.getDeviceByMacAddress(callerMac).getAlias();
if (StringUtils.isBlank(callerNickname)) {
callerNickname = formatMacAsDeviceName(callerMac);
}
}
return postToMqtt("/api/call/request",
Map.of("caller_mac", callerMac, "target_mac", targetMac, "caller_nickname", callerNickname),
"呼叫");
}
@Override
@@ -81,43 +103,44 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
Map<String, Map<String, String>> result = new HashMap<>();
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
Map<String, String> reverseMap = new HashMap<>();
Map<String, Boolean> permissionMap = new HashMap<>();
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String alias = entity.getAlias();
Boolean hasPermission = entity.getHasPermission();
// 构建正向映射: A对B的映射 nickname -> macB|permission
if (alias != null && !alias.isEmpty()) {
result.computeIfAbsent(macA, k -> new HashMap<>());
String permStr = (hasPermission != null && hasPermission) ? "1" : "0";
result.get(macA).put(alias, macB + "|" + permStr);
}
// 构建反向记录用于查找B对A的称呼
if (alias != null && !alias.isEmpty()) {
// 反向记录: (macB, macA) = B对A的称呼
reverseMap.put(macB + ":" + macA, alias);
}
// 记录 A 对 B 是否有权限呼叫
permissionMap.put(macA + ":" + macB, hasPermission != null && hasPermission);
}
// 构建反向映射: B对A的映射 macA -> nickname
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String aliasAtoB = entity.getAlias();
result.computeIfAbsent(macA, k -> new HashMap<>());
result.computeIfAbsent(macB, k -> new HashMap<>());
if (aliasAtoB != null && !aliasAtoB.isEmpty()) {
// A对B的映射: nickname -> macB|permission
Boolean perm = entity.getHasPermission();
String permStr = (perm != null && perm) ? "1" : "0";
result.get(macA).put(aliasAtoB, macB + "|" + permStr);
}
// B对A的映射: macA -> nickname(查反向记录)
String aliasBtoA = reverseMap.get(macA + ":" + macB);
if (aliasBtoA != null && !aliasBtoA.isEmpty()) {
result.computeIfAbsent(macB, k -> new HashMap<>());
result.get(macB).put(macA, aliasBtoA);
}
}
redisUtils.set(RedisKeys.getAddressBookKey(), result);
}
@Override
public void updateAlias(String macAddress, String targetMac, String alias) {
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
String finalAlias = generateUniqueAlias(macAddress, targetMac, alias);
deviceAddressBookDao.updateAlias(macAddress, targetMac, finalAlias);
refreshCache();
}
@@ -132,20 +155,23 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress).eq("target_mac", targetMac);
DeviceAddressBookEntity record = deviceAddressBookDao.selectOne(wrapper);
System.out.println("saveOrUpdate - mac=" + macAddress + ", targetMac=" + targetMac + ", alias=" + alias + ", record=" + (record == null ? "null" : "not null"));
if (record == null) {
DeviceAddressBookEntity entity = new DeviceAddressBookEntity();
entity.setMacAddress(macAddress);
entity.setTargetMac(targetMac);
// 如果 alias 为空,就默认使用设备名称
// 如果 alias 为空,就默认使用设备名称
if (StringUtils.isBlank(alias)) {
alias = deviceService.getDeviceByMacAddress(targetMac).getAlias();
}
// 检查重名
alias = generateUniqueAlias(macAddress, targetMac, alias);
entity.setAlias(alias);
entity.setHasPermission(hasPermission);
deviceAddressBookDao.insert(entity);
} else {
if (alias != null) {
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
updateAlias(macAddress, targetMac, alias);
}
if (hasPermission != null) {
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
@@ -153,4 +179,95 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
}
refreshCache();
}
@Override
public void deleteByMacAddresses(List<String> macAddresses) {
if (macAddresses == null || macAddresses.isEmpty()) {
return;
}
deviceAddressBookDao.deleteByMacAddresses(macAddresses);
refreshCache();
}
private Map<String, Object> errorResult(String message) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", message);
return result;
}
private Map<String, Object> postToMqtt(String path, Map<String, Object> body, String action) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)
|| StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
result.put("message", action + "失败,网关配置缺失");
return result;
}
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
String token = hexString.toString();
String url = "http://" + mqttGatewayUrl + path;
String response = HttpRequest.post(url)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(body))
.timeout(5000)
.execute()
.body();
if (StringUtils.isNotBlank(response)) {
Map<String, Object> gwResult = JSONUtil.parseObj(response);
result.put("status", gwResult.get("status"));
result.put("message", gwResult.get("message"));
}
return result;
} catch (Exception e) {
result.put("message", action + "失败,请稍后再试");
return result;
}
}
private String formatMacAsDeviceName(String mac) {
if (StringUtils.isBlank(mac) || mac.length() < 2) {
return mac;
}
String lastTwo = mac.substring(mac.length() - 2);
return "尾号为" + lastTwo + "的设备";
}
private String generateUniqueAlias(String macAddress, String targetMac, String alias) {
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress);
List<DeviceAddressBookEntity> existing = deviceAddressBookDao.selectList(wrapper);
List<String> existNames = existing.stream()
.map(DeviceAddressBookEntity::getAlias)
.filter(a -> a != null && !a.isEmpty())
.collect(Collectors.toList());
if (!existNames.contains(alias)) {
return alias;
}
int suffix = 1;
String newAlias;
while (existNames.contains(newAlias = alias + suffix)) {
suffix++;
}
return newAlias;
}
}
@@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -63,6 +64,7 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.entity.OtaEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.device.service.OtaService;
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
@@ -80,6 +82,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
private final SysParamsService sysParamsService;
private final RedisUtils redisUtils;
private final OtaService otaService;
private final DeviceAddressBookService deviceAddressBookService;
@Async
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
@@ -314,11 +317,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override
public void unbindDevice(Long userId, String deviceId) {
// 先查询设备信息,获取agentId
// 先查询设备信息,获取agentId和macAddress
DeviceEntity device = baseDao.selectById(deviceId);
if (device == null) {
return;
}
String macAddress = device.getMacAddress();
if (StringUtils.isNotBlank(device.getAgentId())) {
// 清除智能体设备数量缓存
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
@@ -328,6 +332,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
wrapper.eq("user_id", userId);
wrapper.eq("id", deviceId);
baseDao.delete(wrapper);
// 删除设备相关的通讯录权限记录
deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress));
}
@Override
@@ -346,9 +353,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
@Override
public void deleteByAgentId(String agentId) {
// 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录
QueryWrapper<DeviceEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("agent_id", agentId);
List<DeviceEntity> devices = baseDao.selectList(queryWrapper);
// 删除设备
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("agent_id", agentId);
baseDao.delete(wrapper);
// 批量删除这些设备相关的所有通讯录权限记录
if (!devices.isEmpty()) {
List<String> macAddresses = devices.stream()
.map(DeviceEntity::getMacAddress)
.collect(Collectors.toList());
deviceAddressBookService.deleteByMacAddresses(macAddresses);
}
}
@Override
@@ -891,70 +912,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
}
return null;
}
@Override
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
Map<String, Object> result = new HashMap<>();
result.put("status", "error");
result.put("message", "网关配置缺失");
// 从系统参数获取网关配置
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
log.error("MQTT网关地址未配置");
result.put("message", "MQTT网关地址未配置");
return result;
}
if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
log.error("MQTT签名密钥未配置");
result.put("message", "MQTT签名密钥未配置");
return result;
}
// 生成token: SHA256(date + secret)
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
String token = hexString.toString();
// 构建请求体
Map<String, Object> body = new HashMap<>();
body.put("caller_mac", callerMac);
body.put("target_mac", targetMac);
body.put("caller_nickname", callerNickname);
// 发送请求并获取响应
String url = "http://" + mqttGatewayUrl + "/api/call/request";
String response = cn.hutool.http.HttpRequest.post(url)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(body))
.timeout(5000)
.execute()
.body();
// 解析网关响应
if (StringUtils.isNotBlank(response)) {
Map<String, Object> gwResult = JSONUtil.parseObj(response);
result.put("status", gwResult.get("status"));
result.put("message", gwResult.get("message"));
}
return result;
} catch (Exception e) {
log.error("转发呼叫请求失败: {}", e.getMessage());
result.put("message", "呼叫请求转发失败: " + e.getMessage());
return result;
}
}
}
@@ -1,68 +0,0 @@
package xiaozhi.modules.knowledge.dto;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 文档 DTO
*/
@Data
@Schema(description = "知识库文档")
public class DocumentDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "本地ID")
private String id;
@Schema(description = "知识库ID")
private String datasetId;
@Schema(description = "RAGFlow文档ID")
private String documentId;
@Schema(description = "文档名称")
private String name;
@Schema(description = "文件大小")
private Long size;
@Schema(description = "文件类型")
private String type;
@Schema(description = "分块方法")
private String chunkMethod;
@Schema(description = "解析配置")
private Map<String, Object> parserConfig;
@Schema(description = "处理状态 (1:解析中 3:成功 4:失败)")
private Integer status;
@Schema(description = "错误信息")
private String error;
@Schema(description = "分块数量")
private Integer chunkCount;
@Schema(description = "Token数量")
private Long tokenCount;
@Schema(description = "是否启用")
private Integer enabled;
@Schema(description = "创建时间")
private Date createdAt;
@Schema(description = "更新时间")
private Date updatedAt;
@Schema(description = "上传进度 (虚拟字段)")
private Double progress;
@Schema(description = "缩略图/预览图 (虚拟字段)")
private String thumbnail;
}
@@ -67,4 +67,7 @@ public class KnowledgeBaseDTO implements Serializable {
@Schema(description = "文档数量")
private Integer documentCount;
@Schema(description = "异常提示")
private String errorMessage;
}
@@ -1,421 +0,0 @@
package xiaozhi.modules.knowledge.dto.agent;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "智能体 (Agent) 管理聚合 DTO")
public class AgentDTO {
// ========== 1. Agent 管理 (CRUD) - 对应 RAGFlow_Agent接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 创建请求")
public static class CreateReq implements Serializable {
@Schema(description = "Agent 标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "My Agent")
@NotBlank(message = "Agent 标题不能为空")
@JsonProperty("title")
private String title;
@Schema(description = "DSL 定义 (画布 JSON)", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "DSL 定义不能为空")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "描述", example = "这是一个测试 Agent")
@JsonProperty("description")
private String description;
@Schema(description = "头像 URL", example = "http://example.com/avatar.png")
@JsonProperty("avatar")
private String avatar;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 更新请求")
public static class UpdateReq implements Serializable {
@Schema(description = "Agent 标题", example = "Updated Agent")
@JsonProperty("title")
private String title;
@Schema(description = "DSL 定义 (画布 JSON)")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "描述")
@JsonProperty("description")
private String description;
@Schema(description = "头像 URL")
@JsonProperty("avatar")
private String avatar;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 列表请求")
public static class ListReq implements Serializable {
@Schema(description = "页码", defaultValue = "1")
@JsonProperty("page")
@Builder.Default
private Integer page = 1;
@Schema(description = "每页大小", defaultValue = "10")
@JsonProperty("page_size")
@Builder.Default
private Integer pageSize = 10;
@Schema(description = "排序字段", defaultValue = "update_time")
@JsonProperty("orderby")
@Builder.Default
private String orderby = "update_time";
@Schema(description = "是否降序", defaultValue = "true")
@JsonProperty("desc")
@Builder.Default
private Boolean desc = true;
@Schema(description = "Agent ID 过滤")
@JsonProperty("id")
private String id;
@Schema(description = "标题模糊搜索")
@JsonProperty("title")
private String title;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Agent 响应对象")
public static class AgentVO implements Serializable {
@Schema(description = "Agent ID")
@JsonProperty("id")
private String id;
@Schema(description = "标题")
@JsonProperty("title")
private String title;
@Schema(description = "描述")
@JsonProperty("description")
private String description;
@Schema(description = "头像")
@JsonProperty("avatar")
private String avatar;
@Schema(description = "DSL 定义")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "创建者 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "画布分类")
@JsonProperty("canvas_category")
private String canvasCategory;
@Schema(description = "创建时间 (时间戳)")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)")
@JsonProperty("update_time")
private Long updateTime;
}
// ========== 2. Webhook 调试与追踪 - 对应 RAGFlow_Agent接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 触发请求 (参数动态)")
public static class WebhookTriggerReq implements Serializable {
@Schema(description = "输入变量", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "输入变量不能为空")
@JsonProperty("inputs")
private Map<String, Object> inputs;
@Schema(description = "查询词", example = "Hello")
@JsonProperty("query")
private String query;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 追踪请求")
public static class WebhookTraceReq implements Serializable {
@Schema(description = "时间戳游标", example = "1700000000.0")
@JsonProperty("since_ts")
private Double sinceTs;
@Schema(description = "Webhook ID")
@JsonProperty("webhook_id")
private String webhookId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Webhook 追踪响应")
public static class WebhookTraceVO implements Serializable {
@Schema(description = "Webhook ID")
@JsonProperty("webhook_id")
private String webhookId;
@Schema(description = "是否结束")
@JsonProperty("finished")
private Boolean finished;
@Schema(description = "下一次查询的时间戳游标")
@JsonProperty("next_since_ts")
private Double nextSinceTs;
@Schema(description = "事件列表")
@JsonProperty("events")
private List<TraceEvent> events;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "追踪事件项")
public static class TraceEvent implements Serializable {
@Schema(description = "时间戳")
@JsonProperty("ts")
private Double ts;
@Schema(description = "事件类型")
@JsonProperty("event")
private String event;
@Schema(description = "事件数据")
@JsonProperty("data")
private Object data;
}
}
// ========== 3. Agent 会话 (Session) - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 创建请求")
public static class SessionCreateReq implements Serializable {
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 列表请求")
public static class SessionListReq implements Serializable {
@Schema(description = "页码", defaultValue = "1")
@JsonProperty("page")
@Builder.Default
private Integer page = 1;
@Schema(description = "每页大小", defaultValue = "10")
@JsonProperty("page_size")
@Builder.Default
private Integer pageSize = 10;
@Schema(description = "排序字段", defaultValue = "create_time")
@JsonProperty("orderby")
@Builder.Default
private String orderby = "create_time";
@Schema(description = "是否降序", defaultValue = "true")
@JsonProperty("desc")
@Builder.Default
private Boolean desc = true;
@Schema(description = "Session ID")
@JsonProperty("id")
private String id;
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "是否返回 DSL")
@JsonProperty("dsl")
@Builder.Default
private Boolean dsl = false;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 批量删除请求")
public static class SessionBatchDeleteReq implements Serializable {
@Schema(description = "会话 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("ids")
@NotEmpty(message = "ID列表不能为空")
private List<String> ids;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Session 响应对象")
public static class SessionVO implements Serializable {
@Schema(description = "Session ID")
@JsonProperty("id")
private String id;
@Schema(description = "Agent ID")
@JsonProperty("agent_id")
private String agentId;
@Schema(description = "用户 ID")
@JsonProperty("user_id")
private String userId;
@Schema(description = "来源")
@JsonProperty("source")
private String source;
@Schema(description = "DSL 定义")
@JsonProperty("dsl")
private Map<String, Object> dsl;
@Schema(description = "消息列表")
@JsonProperty("messages")
private List<Map<String, Object>> messages;
}
// ========== 4. Agent 对话 (Completion) - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Completion 对话请求")
public static class CompletionReq implements Serializable {
@Schema(description = "会话 ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "会话 ID 不能为空")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "用户问题")
@JsonProperty("question")
private String question;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
@Schema(description = "是否返回追踪信息", defaultValue = "false")
@JsonProperty("return_trace")
@Builder.Default
private Boolean returnTrace = false;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Completion 对话响应")
public static class CompletionVO implements Serializable {
@Schema(description = "会话 ID")
@JsonProperty("id")
private String id;
@Schema(description = "回复内容")
@JsonProperty("content")
private String content;
@Schema(description = "引用来源")
@JsonProperty("reference")
private Map<String, Object> reference;
@Schema(description = "追踪信息")
@JsonProperty("trace")
private List<Object> trace;
}
// ========== 5. Dify 兼容检索 - 对应 RAGFlow_Agent_Dify接口详解 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Dify 兼容检索请求")
public static class DifyRetrievalReq implements Serializable {
@Schema(description = "知识库 ID")
@JsonProperty("knowledge_id")
private String knowledgeId;
@Schema(description = "查询词")
@JsonProperty("query")
private String query;
@Schema(description = "检索设置")
@JsonProperty("retrieval_setting")
private Map<String, Object> retrievalSetting;
@Schema(description = "元数据过滤条件")
@JsonProperty("metadata_condition")
private Map<String, Object> metadataCondition;
@Schema(description = "是否使用知识图谱")
@JsonProperty("use_kg")
private Boolean useKg;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Dify 兼容检索响应")
public static class DifyRetrievalVO implements Serializable {
@Schema(description = "检索结果列表")
@JsonProperty("records")
private List<Record> records;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "检索记录")
public static class Record implements Serializable {
@Schema(description = "内容")
@JsonProperty("content")
private String content;
@Schema(description = "相似度分数")
@JsonProperty("score")
private Double score;
@Schema(description = "标题")
@JsonProperty("title")
private String title;
@Schema(description = "元数据")
@JsonProperty("metadata")
private Map<String, Object> metadata;
}
}
}
@@ -1,126 +0,0 @@
package xiaozhi.modules.knowledge.dto.bot;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "外部机器人 (Bot) 聚合 DTO")
public class BotDTO {
// ========== 1. SearchBot (检索机器人) ==========
// 对应 /api/v1/searchbots/ask
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "SearchBot 提问请求")
public static class SearchAskReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is RAG?")
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "是否返回引用", defaultValue = "false")
@JsonProperty("quote")
@Builder.Default
private Boolean quote = false;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "SearchBot 提问响应")
public static class SearchAskVO implements Serializable {
@Schema(description = "回答内容")
@JsonProperty("answer")
private String answer;
@Schema(description = "引用来源 (Value 结构通常对应 RetrievalDTO.HitVO)")
@JsonProperty("reference")
private Map<String, Object> reference;
}
// 对应 /api/v1/searchbots/related_questions
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "相关问题请求")
public static class RelatedQuestionReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
}
// 对应 /api/v1/searchbots/mindmap
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "思维导图请求")
public static class MindMapReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
}
// ========== 2. AgentBot (嵌入式 Agent) ==========
// 对应 /api/v1/agentbots/{id}/inputs
@Data
@Builder
@AllArgsConstructor
@Schema(description = "AgentBot 输入参数请求")
public static class AgentInputsReq implements Serializable {
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "AgentBot 输入参数定义响应")
public static class AgentInputsVO implements Serializable {
@Schema(description = "表单变量定义列表")
@JsonProperty("variables")
private List<Map<String, Object>> variables;
}
// 对应 /api/v1/agentbots/{id}/completions
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "AgentBot 对话请求")
public static class AgentCompletionReq implements Serializable {
@Schema(description = "输入参数值")
@JsonProperty("inputs")
private Map<String, Object> inputs;
@Schema(description = "用户查询", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "查询内容不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "是否流式返回", defaultValue = "true")
@JsonProperty("stream")
@Builder.Default
private Boolean stream = true;
@Schema(description = "会话 ID")
@JsonProperty("session_id")
private String sessionId;
}
}
@@ -1,50 +0,0 @@
package xiaozhi.modules.knowledge.dto.chat;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 聊天对话请求 DTO (OpenAI 兼容格式)
*/
@Data
@Schema(description = "聊天对话请求")
public class ChatCompletionRequest implements Serializable {
@Schema(description = "模型标识 (对应 agent_id 或 bot_id)", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("model")
private String model;
@Schema(description = "对话消息列表", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("messages")
private List<Message> messages;
@Schema(description = "是否流式返回", defaultValue = "false")
@JsonProperty("stream")
private Boolean stream = false;
@Schema(description = "温度系数 (0-1)", defaultValue = "0.7")
@JsonProperty("temperature")
private Double temperature;
@Schema(description = "Session ID (可选,用于延续会话)")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "其他RAGFlow特定参数 (可选)")
private Map<String, Object> extra;
@Data
public static class Message implements Serializable {
@Schema(description = "角色 (system, user, assistant)", requiredMode = Schema.RequiredMode.REQUIRED)
private String role;
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
}
}
@@ -1,523 +0,0 @@
package xiaozhi.modules.knowledge.dto.chat;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
/**
* 对话管理聚合 DTO
* <p>
* 容器类,内含对话助手、会话和消息的所有请求/响应对象。
* </p>
*/
@Schema(description = "对话管理聚合 DTO")
public class ChatDTO {
// ========== 1. 对话助手 (Assistant/Bot) 相关 ==========
/**
* 提示词配置
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "提示词配置")
public static class PromptConfig implements Serializable {
@Schema(description = "系统提示词", example = "你是一个专业的客服助手...")
@JsonProperty("prompt")
private String systemPrompt;
@Schema(description = "开场白", example = "您好,我是您的智能助手,请问有什么可以帮您?")
private String opener;
@Schema(description = "空结果回复", example = "抱歉,我没有找到相关信息。")
@JsonProperty("empty_response")
private String emptyResponse;
@Schema(description = "是否展示引用", example = "true")
@JsonProperty("show_quote")
private Boolean quote;
@Schema(description = "是否启用 TTS", example = "false")
private Boolean tts;
@Schema(description = "相似度阈值 (0.0 - 1.0)", example = "0.2")
@JsonProperty("similarity_threshold")
private Float similarityThreshold;
@Schema(description = "关键词相似度权重 (0.0 - 1.0)", example = "0.7")
@JsonProperty("keywords_similarity_weight")
private Float vectorSimilarityWeight;
@Schema(description = "检索 Top N", example = "6")
@JsonProperty("top_n")
private Integer topK;
@Schema(description = "Rerank 模型", example = "rerank_model_001")
@JsonProperty("rerank_model")
private String rerankId;
@Schema(description = "是否启用多轮对话优化", example = "false")
@JsonProperty("refine_multiturn")
private Boolean refineMultigraph;
@Schema(description = "变量列表")
private List<Map<String, Object>> variables;
}
/**
* LLM 配置
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "LLM 模型配置")
public static class LLMConfig implements Serializable {
@NotBlank(message = "模型名称不能为空")
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-4")
@JsonProperty("model_name")
private String modelName;
@Schema(description = "温度参数 (0.0 - 2.0)", example = "0.7")
private Float temperature;
@Schema(description = "Top P 采样", example = "0.9")
@JsonProperty("top_p")
private Float topP;
@Schema(description = "最大 Token 数", example = "4096")
@JsonProperty("max_tokens")
private Integer maxTokens;
@Schema(description = "存在惩罚", example = "0.0")
@JsonProperty("presence_penalty")
private Float presencePenalty;
@Schema(description = "频率惩罚", example = "0.0")
@JsonProperty("frequency_penalty")
private Float frequencyPenalty;
}
/**
* 创建助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "创建助手请求")
public static class AssistantCreateReq implements Serializable {
@NotBlank(message = "助手名称不能为空")
@Schema(description = "助手名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能客服助手")
private String name;
@Schema(description = "助手头像 (Base64 编码)", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "助手描述", example = "这是一个智能客服助手")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
}
/**
* 更新助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "更新助手请求")
public static class AssistantUpdateReq implements Serializable {
@Schema(description = "助手名称", example = "智能客服助手 V2")
private String name;
@Schema(description = "助手头像 (Base64 编码)", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "助手描述", example = "这是一个智能客服助手")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
}
/**
* 查询助手列表请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询助手列表请求")
public static class AssistantListReq implements Serializable {
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "按名称过滤 (模糊匹配)", example = "客服")
private String name;
@Schema(description = "排序字段: create_time / update_time", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
@Schema(description = "按 ID 精确筛选", example = "assistant_001")
private String id;
}
/**
* 助手详情 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "助手详情 VO")
public static class AssistantVO implements Serializable {
@Schema(description = "助手 ID", example = "assistant_001")
private String id;
@Schema(description = "租户 ID", example = "tenant_001")
@JsonProperty("tenant_id")
private String tenantId;
@Schema(description = "助手名称", example = "智能客服助手")
private String name;
@Schema(description = "助手头像", example = "")
private String avatar;
@Schema(description = "关联的知识库 ID 列表")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
@Schema(description = "关联的知识库列表 (详情)")
private List<SimpleDatasetVO> datasets;
@Schema(description = "助手描述")
private String description;
@Schema(description = "LLM 模型配置")
@JsonProperty("llm")
private LLMConfig llm;
@Schema(description = "提示词配置")
@JsonProperty("prompt")
private PromptConfig promptConfig;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
}
/**
* 删除助手请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "删除助手请求")
public static class AssistantDeleteReq implements Serializable {
@Schema(description = "要删除的助手 ID 列表", example = "[\"assistant_001\", \"assistant_002\"]")
private List<String> ids;
}
// ========== 2. 会话 (Session) 相关 ==========
/**
* 创建会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "创建会话请求")
public static class SessionCreateReq implements Serializable {
@Schema(description = "会话名称", example = "技术咨询会话")
private String name;
@Schema(description = "用户 ID", example = "user_001")
@JsonProperty("user_id")
private String userId;
}
/**
* 更新会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "更新会话请求")
public static class SessionUpdateReq implements Serializable {
@Schema(description = "会话名称", example = "技术咨询会话 - 更新")
private String name;
}
/**
* 查询会话列表请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "查询会话列表请求")
public static class SessionListReq implements Serializable {
@Schema(description = "助手 ID", example = "assistant_001")
@JsonProperty("assistant_id")
private String assistantId;
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "按名称过滤", example = "技术")
private String name;
@Schema(description = "排序字段", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
@Schema(description = "会话 ID 精确筛选", example = "session_001")
private String id;
@Schema(description = "用户标识筛选", example = "user_001")
@JsonProperty("user_id")
private String userId;
}
/**
* 会话详情 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "会话详情 VO")
public static class SessionVO implements Serializable {
@Schema(description = "会话 ID", example = "session_001")
private String id;
@Schema(description = "助手 ID", example = "assistant_001")
@JsonProperty("chat_id")
private String chatId;
@Schema(description = "助手 ID (兼容旧版)", example = "assistant_001")
@JsonProperty("assistant_id")
private String assistantId;
@Schema(description = "会话名称", example = "技术咨询会话")
private String name;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "创建日期", example = "2024-05-01 10:00:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新日期", example = "2024-05-01 10:00:00")
@JsonProperty("update_date")
private String updateDate;
@Schema(description = "用户 ID", example = "user_001")
@JsonProperty("user_id")
private String userId;
@Schema(description = "对话历史消息列表")
private List<Map<String, Object>> messages;
}
/**
* 删除会话请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "删除会话请求")
public static class SessionDeleteReq implements Serializable {
@Schema(description = "要删除的会话 ID 列表", example = "[\"session_001\", \"session_002\"]")
private List<String> ids;
}
// ========== 3. 消息/对话 (Completion) 相关 ==========
/**
* 发送消息请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "发送消息请求")
public static class CompletionReq implements Serializable {
@NotBlank(message = "问题内容不能为空")
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "请介绍一下你们的产品")
private String question;
@Schema(description = "是否使用流式响应 (SSE)", example = "true")
@Builder.Default
private Boolean stream = true;
@NotBlank(message = "会话 ID 不能为空")
@Schema(description = "会话 ID (可选,不传则创建新会话)", example = "session_001")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "是否展示引用", example = "true")
private Boolean quote;
@Schema(description = "指定检索的文档 ID 列表 (逗号分隔)", example = "doc_001,doc_002")
@JsonProperty("doc_ids")
private String docIds;
@Schema(description = "元数据过滤条件")
@JsonProperty("metadata_condition")
private Map<String, Object> metadataCondition;
}
/**
* 消息响应 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "消息响应 VO")
public static class CompletionVO implements Serializable {
@Schema(description = "AI 回答内容")
private String answer;
@Schema(description = "引用信息")
private Reference reference;
@Schema(description = "会话 ID", example = "session_001")
@JsonProperty("session_id")
private String sessionId;
@Schema(description = "任务 ID (用于流式响应追踪)", example = "task_001")
@JsonProperty("task_id")
private String taskId;
/**
* 引用信息 (检索命中结果)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "引用信息")
public static class Reference implements Serializable {
@Schema(description = "命中的文档块列表")
private List<xiaozhi.modules.knowledge.dto.document.RetrievalDTO.HitVO> chunks;
@Schema(description = "文档聚合信息")
@JsonProperty("doc_aggs")
private List<DocAgg> docAggs;
}
/**
* 文档聚合信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文档聚合信息")
public static class DocAgg implements Serializable {
@Schema(description = "文档 ID", example = "doc_001")
@JsonProperty("doc_id")
private String docId;
@Schema(description = "文档名称", example = "产品手册.pdf")
@JsonProperty("doc_name")
private String docName;
@Schema(description = "命中次数", example = "3")
private Integer count;
}
}
/**
* 简易知识库 VO (用于 Assistant 列表)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "简易知识库 VO")
public static class SimpleDatasetVO implements Serializable {
@Schema(description = "知识库 ID")
private String id;
@Schema(description = "知识库名称")
private String name;
@Schema(description = "头像")
private String avatar;
@Schema(description = "分块数量")
@JsonProperty("chunk_num")
private Integer chunkNum;
}
}
@@ -1,79 +0,0 @@
package xiaozhi.modules.knowledge.dto.common;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
@Schema(description = "通用扩展功能 DTO")
public class CommonDTO {
// ========== 1. 引用详情 (detail_share_embedded) ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "引用详情请求")
public static class ReferenceDetailReq implements Serializable {
@Schema(description = "切片 ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "切片 ID 不能为空")
@JsonProperty("chunk_id")
private String chunkId;
@Schema(description = "知识库 ID")
@JsonProperty("knowledge_id")
private String knowledgeId;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "引用详情响应")
public static class ReferenceDetailVO implements Serializable {
@Schema(description = "切片 ID")
@JsonProperty("chunk_id")
private String chunkId;
@Schema(description = "完整内容")
@JsonProperty("content_with_weight")
private String contentWithWeight;
@Schema(description = "文档名称")
@JsonProperty("doc_name")
private String docName;
@Schema(description = "图片 ID 列表")
@JsonProperty("img_id")
private String imageId; // 注意:RAGFlow 有时返回 String 有时返回 List,需根据实际情况确认,暂定 String 用于 ID
@Schema(description = "文档 ID")
@JsonProperty("doc_id")
private String docId;
}
// ========== 2. 通用问答 (ask_about) - 调试用 ==========
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "通用问答请求 (调试用)")
public static class AskAboutReq implements Serializable {
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is this dataset about?")
@NotBlank(message = "问题不能为空")
@JsonProperty("question")
private String question;
@Schema(description = "数据集 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "数据集列表不能为空")
@JsonProperty("dataset_ids")
private List<String> datasetIds;
}
// 响应通常复用 String 或者简单的 Map 结构,视具体实现而定,暂不定义专用 VO
}
@@ -1,363 +0,0 @@
package xiaozhi.modules.knowledge.dto.file;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理聚合 DTO
* <p>
* 容器类,内含文件模块所有请求/响应对象的静态内部类定义。
* </p>
*/
@Schema(description = "文件管理聚合 DTO")
public class FileDTO {
// ========== 请求类 ==========
/**
* 文件上传请求 (对应接口 1: upload)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件上传请求")
public static class UploadReq implements Serializable {
@NotNull(message = "文件不能为空")
@Schema(description = "上传的文件", requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile file;
@Schema(description = "父文件夹 ID (为空则上传到根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
}
/**
* 新建文件夹请求 (对应接口 2: create)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "新建文件夹请求")
public static class CreateReq implements Serializable {
@NotBlank(message = "文件夹名称不能为空")
@Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "新建文件夹")
private String name;
@Schema(description = "父文件夹 ID (为空则创建在根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@NotBlank(message = "类型不能为空")
@Schema(description = "类型: FOLDER", requiredMode = Schema.RequiredMode.REQUIRED, example = "FOLDER")
@Builder.Default
private String type = "FOLDER";
}
/**
* 重命名请求 (对应接口 6: rename)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "重命名请求")
public static class RenameReq implements Serializable {
@NotBlank(message = "文件 ID 不能为空")
@Schema(description = "文件/文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "file_001")
@JsonProperty("file_id")
private String fileId;
@NotBlank(message = "新名称不能为空")
@Schema(description = "新名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重命名后的文件")
private String name;
}
/**
* 移动请求 (对应接口 7: move)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "移动请求")
public static class MoveReq implements Serializable {
@NotEmpty(message = "源文件 ID 列表不能为空")
@Schema(description = "源文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("src_file_ids")
private List<String> srcFileIds;
@NotBlank(message = "目标文件夹 ID 不能为空")
@Schema(description = "目标文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "folder_002")
@JsonProperty("dest_file_id")
private String destFileId;
}
/**
* 批量删除请求 (对应接口 8: rm)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "批量删除请求")
public static class RemoveReq implements Serializable {
@NotEmpty(message = "文件 ID 列表不能为空")
@Schema(description = "文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("file_ids")
private List<String> fileIds;
}
/**
* 导入知识库请求 (对应接口 9: convert)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "导入知识库请求")
public static class ConvertReq implements Serializable {
@NotEmpty(message = "文件 ID 列表不能为空")
@Schema(description = "文件 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
@JsonProperty("file_ids")
private List<String> fileIds;
@NotEmpty(message = "知识库 ID 列表不能为空")
@Schema(description = "目标知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"kb_001\"]")
@JsonProperty("kb_ids")
private List<String> kbIds;
}
/**
* 列表查询请求 (对应接口 3: list_files)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "列表查询请求")
public static class ListReq implements Serializable {
@Schema(description = "父文件夹 ID (为空则查询根目录)", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@Schema(description = "关键词搜索", example = "文档")
private String keywords;
@Schema(description = "页码 (从 1 开始)", example = "1")
private Integer page;
@Schema(description = "每页数量", example = "30")
@JsonProperty("page_size")
private Integer pageSize;
@Schema(description = "排序字段: create_time / update_time / name / size", example = "create_time")
private String orderby;
@Schema(description = "是否降序", example = "true")
private Boolean desc;
}
// ========== 响应类 ==========
/**
* 文件/文件夹基础信息 VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件/文件夹基础信息")
public static class InfoVO implements Serializable {
@Schema(description = "文件/文件夹 ID", example = "file_001")
private String id;
@Schema(description = "父文件夹 ID", example = "folder_001")
@JsonProperty("parent_id")
private String parentId;
@Schema(description = "租户 ID", example = "tenant_001")
@JsonProperty("tenant_id")
private String tenantId;
@Schema(description = "创建者 ID", example = "user_001")
@JsonProperty("created_by")
private String createdBy;
@Schema(description = "类型: FOLDER / FILE", example = "FOLDER")
private String type;
@Schema(description = "名称", example = "我的文件夹")
private String name;
@Schema(description = "路径位置", example = "/root/folder")
private String location;
@Schema(description = "文件大小 (字节)", example = "1024")
private Long size;
@Schema(description = "来源类型", example = "local")
@JsonProperty("source_type")
private String sourceType;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
@JsonProperty("update_date")
private String updateDate;
@Schema(description = "文件扩展名", example = "pdf")
private String extension;
}
/**
* 列表响应 VO (对应接口 3: list_files)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件列表响应")
public static class ListVO implements Serializable {
@Schema(description = "总记录数", example = "100")
private Long total;
@Schema(description = "当前父文件夹信息")
@JsonProperty("parent_folder")
private InfoVO parentFolder;
@Schema(description = "文件/文件夹列表")
private List<InfoVO> files;
@Schema(description = "面包屑导航路径")
private List<InfoVO> breadcrumb;
}
/**
* 转换结果项 VO (对应接口 9: convert)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件转换结果项")
public static class ConvertVO implements Serializable {
@Schema(description = "转换记录 ID", example = "convert_001")
private String id;
@Schema(description = "源文件 ID", example = "file_001")
@JsonProperty("file_id")
private String fileId;
@Schema(description = "目标文档 ID", example = "doc_001")
@JsonProperty("document_id")
private String documentId;
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
@JsonProperty("create_time")
private Long createTime;
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
@JsonProperty("create_date")
private String createDate;
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
@JsonProperty("update_time")
private Long updateTime;
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
@JsonProperty("update_date")
private String updateDate;
}
/**
* 转换状态 VO (对应接口 10: get_convert_status)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "文件转换状态")
public static class ConvertStatusVO implements Serializable {
@Schema(description = "转换状态: pending / processing / completed / failed", example = "completed")
private String status;
@Schema(description = "转换进度 (0.0 - 1.0)", example = "1.0")
private Float progress;
@Schema(description = "状态消息", example = "转换完成")
private String message;
}
/**
* 面包屑 VO (对应接口 12: all_parent_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "面包屑导航 (所有父文件夹)")
public static class BreadcrumbVO implements Serializable {
@Schema(description = "父文件夹列表 (从根到当前的路径)")
@JsonProperty("parent_folders")
private List<InfoVO> parentFolders;
}
/**
* 根目录信息 VO (对应接口 10: get_root_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "根目录信息")
public static class RootFolderVO implements Serializable {
@Schema(description = "根文件夹信息")
@JsonProperty("root_folder")
private InfoVO rootFolder;
}
/**
* 父目录信息 VO (对应接口 11: get_parent_folder)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "父目录信息")
public static class ParentFolderVO implements Serializable {
@Schema(description = "父文件夹信息")
@JsonProperty("parent_folder")
private InfoVO parentFolder;
}
}
@@ -485,16 +485,11 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
@Override
public Integer getDocumentCount(String datasetId) {
try {
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
if (info != null && info.getDocumentCount() != null) {
return info.getDocumentCount().intValue();
}
return 0;
} catch (Exception e) {
log.warn("获取文档数量失败: {}", e.getMessage());
return 0;
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
if (info != null && info.getDocumentCount() != null) {
return info.getDocumentCount().intValue();
}
return 0;
}
@Override
@@ -518,7 +513,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
return null;
} catch (Exception e) {
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
return null;
throw convertToRenException(e);
}
}
@@ -100,9 +100,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
}
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
// getDatasetInfo 正常返回 null 表示远端确实不存在;异常时已抛出 RenException 由外层 catch 接管
if (datasetInfo == null) {
// RAGFlow 端已删除 → 本地级联清理
// RAGFlow 端已确认删除 → 本地级联清理
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
cleanupLocalDataset(dto.getDatasetId(), dto.getId());
// 标记为已删除,让上层从列表中移除
@@ -145,8 +145,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
}
} catch (Exception e) {
log.warn("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
log.error("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
dto.setDocumentCount(0);
dto.setErrorMessage(e.getMessage());
}
}
@@ -529,7 +529,8 @@ public class KnowledgeFilesServiceImpl extends BaseServiceImpl<DocumentDao, Docu
adapter.deleteDocument(datasetId, req);
log.info("远程批量删除请求成功");
} catch (Exception e) {
log.warn("远程删除请求部分或全部失败: {}", e.getMessage());
log.error("远程删除请求失败,中止本地清理以避免数据不一致: {}", e.getMessage());
throw new RenException(e.getMessage());
}
// 4. 原子化清理本地影子记录并同步统计数据
@@ -87,8 +87,7 @@ public class ShiroConfig {
filterMap.put("/user/retrieve-password", "anon");
// 将config路径使用server服务过滤器
filterMap.put("/config/**", "server");
filterMap.put("/device/address-book/lookup", "server");
filterMap.put("/device/call/forward", "server");
filterMap.put("/device/address-book/call", "server");
filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/agent/chat-history/download/**", "anon");
filterMap.put("/agent/chat-summary/**", "server");
@@ -0,0 +1,15 @@
-- 更新EdgeTTS供应器增加语速、音调、音量配置
UPDATE `ai_model_provider`
SET fields = '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"rate","label":"语速(-100~100)","type":"number"},{"key":"volume","label":"音量(0~100)","type":"number"},{"key":"pitch","label":"音调(-100~100)","type":"number"}]'
WHERE id = 'SYSTEM_TTS_edge';
UPDATE `ai_model_config` SET
`remark` = 'EdgeTTS配置说明:
1. 使用微软Edge TTS服务
2. 支持多种语言和音色
3. 免费使用,无需注册
4. 需要网络连接
5. 输出文件保存在tmp/目录
6. 语速:-100~1000为正常速度
7. 音量:0~10050为正常音量
8. 音调:-100~1000为正常音调' WHERE `id` = 'TTS_EdgeTTS';
@@ -0,0 +1,40 @@
-- 修正、添加Minimax流式TTS音色
delete from `ai_tts_voice` where tts_model_id = 'TTS_MinimaxStreamTTS';
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0001', 'TTS_MinimaxStreamTTS', '少女音', 'female-shaonv', '普通话', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0002', 'TTS_MinimaxStreamTTS', '成熟女声', 'female-chengshu', '普通话', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0003', 'TTS_MinimaxStreamTTS', '霸道少爷', 'badao_shaoye', '普通话', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0004', 'TTS_MinimaxStreamTTS', '病娇弟弟', 'bingjiao_didi', '普通话', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0005', 'TTS_MinimaxStreamTTS', '纯真学弟', 'chunzhen_xuedi', '普通话', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0006', 'TTS_MinimaxStreamTTS', '冷淡学长', 'lengdan_xiongzhang', '普通话', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0007', 'TTS_MinimaxStreamTTS', '甜美小玲', 'tianxin_xiaoling', '普通话', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0008', 'TTS_MinimaxStreamTTS', '俏皮萌妹', 'qiaopi_mengmei', '普通话', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0009', 'TTS_MinimaxStreamTTS', '妩媚御姐', 'wumei_yujie', '普通话', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0010', 'TTS_MinimaxStreamTTS', '调教学妹', 'diadia_xuemei', '普通话', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0011', 'TTS_MinimaxStreamTTS', '淡雅学姐', 'danya_xuejie', '普通话', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0012', 'TTS_MinimaxStreamTTS', 'Santa Claus', 'Santa_Claus', '普通话', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0013', 'TTS_MinimaxStreamTTS', 'Grinch', 'Grinch', '普通话', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0014', 'TTS_MinimaxStreamTTS', '专业女主持', 'Cantonese_ProfessionalHostF)', '粤语', "https://filecdn.minimax.chat/public/84589766-c651-477b-b320-36b89599f953.mp3", NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0015', 'TTS_MinimaxStreamTTS', '温柔女声', 'Cantonese_GentleLady', '粤语', "https://filecdn.minimax.chat/public/d391936d-d586-4dab-886c-a2a084a20920.mp3", NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0016', 'TTS_MinimaxStreamTTS', '专业男主持', 'Cantonese_ProfessionalHostM)', '粤语', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0017', 'TTS_MinimaxStreamTTS', '活泼男声', 'Cantonese_PlayfulMan', '粤语', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0018', 'TTS_MinimaxStreamTTS', '可爱女孩', 'Cantonese_CuteGirl', '粤语', "https://filecdn.minimax.chat/public/ff96520a-ac12-491d-945e-d6337467eb7d.mp3", NULL, NULL, NULL, 18, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0019', 'TTS_MinimaxStreamTTS', '善良女声', 'Cantonese_KindWoman', '粤语', "https://filecdn.minimax.chat/public/78239a18-5e9c-4538-94f6-2f504c080374.mp3", NULL, NULL, NULL, 19, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0020', 'TTS_MinimaxStreamTTS', 'Crisp News Anchor', 'Cantonese_crisp_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/741a5a4b-f42a-4a9b-ae83-3954d097ab73.mp3', NULL, NULL, NULL, 20, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0021', 'TTS_MinimaxStreamTTS', 'Pro Reporter', 'Cantonese_professional_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/77afb86d-2cc4-4149-8c2e-e1174fa849d4.mp3', NULL, NULL, NULL, 21, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0022', 'TTS_MinimaxStreamTTS', 'Crisp Reporter', 'Cantonese_crisp_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/4ad39a61-628e-481c-9d60-bfb7c36bceb8.mp3', NULL, NULL, NULL, 22, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0023', 'TTS_MinimaxStreamTTS', 'Articulate', 'Cantonese_Articulate_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/9346601d-3035-4f9e-9b1c-0e03f30e5eef.mp3', NULL, NULL, NULL, 23, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0024', 'TTS_MinimaxStreamTTS', 'Warm Reporter', 'Cantonese_Warm_Reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/c651050e-d6a5-4653-b09e-5177bdc8b04c.mp3', NULL, NULL, NULL, 24, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0025', 'TTS_MinimaxStreamTTS', 'Energetic', 'Cantonese_energetic_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/1fd1c461-71da-450d-91bb-45f819249b44.mp3', NULL, NULL, NULL, 25, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0026', 'TTS_MinimaxStreamTTS', 'Objective Narrator', 'Cantonese_objective_narrator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/463c5a68-a7b5-4fe9-85b6-d494dca74abb.mp3', NULL, NULL, NULL, 26, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0027', 'TTS_MinimaxStreamTTS', '沙哑男声', 'Cantonese_Narrator', '粤语', 'https://filecdn.minimax.chat/public/22e29cb0-0f5a-443a-9fa9-a32dabd55cf9.mp3', NULL, NULL, NULL, 27, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0028', 'TTS_MinimaxStreamTTS', 'News Anchor', 'Cantonese_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/59b9ba49-47d4-48f1-b207-09c0e0769a1b.mp3', NULL, NULL, NULL, 28, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0029', 'TTS_MinimaxStreamTTS', '渊博学者', 'Cantonese_WiselProfessor', '粤语', 'https://filecdn.minimax.chat/public/8de234c4-64a7-4539-a316-c3b43ee1d0a9.mp3', NULL, NULL, NULL, 29, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0030', 'TTS_MinimaxStreamTTS', 'Resonant Host', 'Cantonese_resonant_host_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/ec8fc0ce-6657-4c49-9bf0-3953a15046ec.mp3', NULL, NULL, NULL, 30, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0031', 'TTS_MinimaxStreamTTS', '冷漠员工', 'Cantonese_IndifferentStaff', '粤语', 'https://filecdn.minimax.chat/public/9686682c-3fc8-45f0-bd88-2a4692e1f63e.mp3', NULL, NULL, NULL, 31, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0032', 'TTS_MinimaxStreamTTS', 'Male News Anchor', 'Cantonese_Male_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/2055ee8b-fb25-424c-b8cb-e2baf971e520.mp3', NULL, NULL, NULL, 32, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0033', 'TTS_MinimaxStreamTTS', 'History Narrator', 'Cantonese_casual_narrator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/c170f9e4-145d-4331-9910-1c3a0e6bbc3a.mp3', NULL, NULL, NULL, 33, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0034', 'TTS_MinimaxStreamTTS', 'Steady Reporter', 'Cantonese_Steady_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/381cfee2-772d-4a73-b30c-7914aec05771.mp3', NULL, NULL, NULL, 34, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0035', 'TTS_MinimaxStreamTTS', 'Serious Commentator', 'Cantonese_serious_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/57335c8a-32ea-475f-a198-b0de7fa1b126.mp3', NULL, NULL, NULL, 35, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0036', 'TTS_MinimaxStreamTTS', 'Podcast Host', 'Cantonese_podcast_host_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/048b617f-e1f8-422c-89fb-fdbc6e303675.mp3', NULL, NULL, NULL, 36, NULL, NULL, NULL, NULL);
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0037', 'TTS_MinimaxStreamTTS', 'Obj commentator', 'Cantonese_Objective_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/a8f55e44-b726-4ba5-815a-f1b515ba57ef.mp3', NULL, NULL, NULL, 37, NULL, NULL, NULL, NULL);
@@ -676,3 +676,17 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202605251426.sql
- changeSet:
id: 202606231030
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202606231030.sql
- changeSet:
id: 202606261131
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202606261131.sql
@@ -25,4 +25,15 @@
INSERT INTO ai_device_address_book (mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date)
VALUES (#{macAddress}, #{targetMac}, #{alias}, #{hasPermission}, #{creator}, NOW(), #{updater}, NOW())
</insert>
<delete id="deleteByMacAddresses">
DELETE FROM ai_device_address_book WHERE mac_address IN
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
#{mac}
</foreach>
OR target_mac IN
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
#{mac}
</foreach>
</delete>
</mapper>
@@ -265,7 +265,7 @@ function showAbout() {
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: t('settings.aboutContent', {
appName: import.meta.env.VITE_APP_TITLE,
version: '0.9.4'
version: '0.9.5'
}),
showCancel: false,
confirmText: t('common.confirm'),
+5 -1
View File
@@ -80,7 +80,7 @@ export default {
}).send()
},
// 保存
addParam(data, callback) {
addParam(data, callback, failCallback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/admin/params`)
.method('POST')
@@ -89,6 +89,10 @@ export default {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
RequestService.clearRequestTime()
failCallback(err)
})
.networkFail((err) => {
console.error('添加参数失败:', err)
RequestService.reAjaxFun(() => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -1,39 +1,38 @@
<template>
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="57%" center
custom-class="custom-dialog" :show-close="false" class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
{{ $t('modelConfigDialog.addModel') }}
</div>
<button class="custom-close-btn" @click="handleClose">
×
</button>
<CustomDialog
:visible.sync="dialogVisible"
:title="$t('modelConfigDialog.addModel')"
width="57%"
class="add-model-dialog"
:confirmLoading="saving"
@close="handleClose"
@confirm="confirm"
>
<div class="dialog-content">
<!-- 模型信息部分 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
<div style="display: flex; align-items: center; gap: 20px;">
<div style="display: flex; align-items: center;">
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
<div class="section-header">
<div class="section-title">{{ $t('modelConfigDialog.modelInfo') }}</div>
<div class="switch-group">
<div class="switch-item">
<span>{{ $t('modelConfigDialog.enable') }}</span>
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
</div>
<div style="display: none; align-items: center;">
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
<div class="switch-item" style="display: none;">
<span>{{ $t('modelConfigDialog.setDefault') }}</span>
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
</div>
</div>
</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<div class="divider"></div>
<el-form :model="formData" label-width="auto" label-position="left" class="custom-form">
<div class="form-row">
<el-form-item :label="$t('modelConfigDialog.modelId')" prop="id" style="flex: 1;">
<el-input v-model="formData.id" :placeholder="$t('modelConfigDialog.enterModelId')" class="custom-input-bg"
maxlength="32"></el-input>
</el-form-item>
</div>
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<div class="form-row">
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
class="custom-input-bg"></el-input>
@@ -44,7 +43,7 @@
</el-form-item>
</div>
<div style="display: flex; gap: 20px; margin-bottom: 0;">
<div class="form-row">
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')"
class="custom-select custom-input-bg" style="width: 100%;" @focus="loadProviders" filterable>
@@ -57,7 +56,6 @@
</el-form-item>
</div>
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
class="custom-input-bg"></el-input>
@@ -70,13 +68,13 @@
</el-form-item>
</el-form>
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{
$t('modelConfigDialog.callInfo') }}</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<teleport v-if="chunkedCallInfoFields.length">
<div class="section-title">{{ $t('modelConfigDialog.callInfo') }}</div>
<div class="divider"></div>
</teleport>
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex"
style="display: flex; gap: 20px; margin-bottom: 0;">
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex" class="form-row">
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" style="flex: 1;">
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
@@ -85,19 +83,17 @@
</div>
</el-form>
</div>
<div style="display: flex;justify-content: center;">
<el-button type="primary" @click="confirm" class="save-btn" :loading="saving" :disabled="saving">
{{ $t('modelConfigDialog.save') }}
</el-button>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import Api from '@/apis/api';
import CustomDialog from './CustomDialog.vue';
export default {
name: 'AddModelDialog',
components: {
CustomDialog
},
props: {
visible: { type: Boolean, required: true },
modelType: { type: String, required: true }
@@ -178,16 +174,10 @@ export default {
});
this.formData.configJson = { ...defaultConfig };
},
handleVisibleChange(val) {
this.dialogVisible = val;
this.$emit('update:visible', val);
if (!val) {
this.resetForm();
}
},
handleClose() {
this.saving = false;
this.dialogVisible = false;
this.$emit('update:visible', false);
},
initDynamicConfig() {
@@ -232,7 +222,7 @@ export default {
try {
this.$emit('confirm', submitData);
this.$emit('update:visible', false);
this.handleClose();
this.resetForm();
} catch (e) {
console.error(e);
@@ -297,134 +287,71 @@ export default {
}
</script>
<style>
.custom-dialog {
position: relative;
border-radius: 20px;
overflow: hidden;
background: white;
padding-bottom: 17px;
}
<style lang="scss" scoped>
@import '@/styles/global.scss';
.custom-dialog .el-dialog__header {
padding: 0;
border-bottom: none;
::v-deep .el-dialog {
margin-top: 6vh !important;
}
.center-dialog {
display: flex;
align-items: center;
justify-content: center;
::v-deep .el-dialog__body {
max-height: 60vh;
overflow-y: auto;
@include scrollbar-style;
}
.add-model-dialog {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.center-dialog .el-dialog {
margin: 0 0 auto !important;
display: flex;
flex-direction: column;
}
.section-title {
margin-bottom: 10px;
font-size: 20px;
font-weight: bold;
color: #3d4566;
text-align: left;
}
.custom-close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid #cfcfcf;
background: none;
font-size: 30px;
font-weight: lighter;
color: #cfcfcf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
padding: 0;
outline: none;
}
.switch-group {
display: flex;
align-items: center;
gap: 20px;
}
.custom-close-btn:hover {
color: #409EFF;
border-color: #409EFF;
}
.switch-item {
display: flex;
align-items: center;
.custom-select .el-input__suffix {
background: #e6e8ea;
right: 6px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 9px;
}
span {
margin-right: 8px;
}
}
.custom-select .el-input__suffix-inner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.divider {
height: 1px;
background: #e9e9e9;
margin-bottom: 16px;
}
.custom-select .el-icon-arrow-up:before {
content: "";
display: inline-block;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid #c0c4cc;
position: relative;
top: -2px;
transform: rotate(180deg);
.form-row {
display: flex;
gap: 20px;
margin-bottom: 0;
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 20px;
padding: 16px 20px;
}
}
.custom-form .el-form-item {
margin-bottom: 20px;
margin-bottom: 10px;
}
.custom-form .el-form-item__label {
color: #3d4566;
font-weight: normal;
text-align: right;
padding-right: 20px;
}
.custom-form .el-form-item.prop-remark .el-form-item__label {
margin-top: -4px;
}
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
color: #9c9f9e;
}
.custom-input-bg .el-input__inner,
.custom-input-bg .el-textarea__inner {
background-color: #ffffff;
}
.save-btn {
background: #e6f0fd;
color: #237ff4;
border: 1px solid #b3d1ff;
width: 150px;
height: 40px;
font-size: 16px;
transition: all 0.3s ease;
}
.save-btn:hover {
background: linear-gradient(to right, #237ff4, #9c40d5);
color: white;
border: none;
}
.custom-switch .el-switch__core {
border-radius: 20px;
height: 23px;
@@ -439,7 +366,7 @@ export default {
background-color: white;
top: 3px;
left: 4px;
transition: all .3s;
transition: all 0.3s;
}
.custom-switch.is-checked .el-switch__core {
@@ -454,12 +381,10 @@ export default {
background-color: #1b47ee;
}
[style*="display: flex"] {
gap: 20px;
}
.custom-input-bg .el-input__inner {
height: 32px;
}
::v-deep .el-input__inner {
height: 32px;
}
</style>
@@ -1,55 +1,42 @@
<template>
<form>
<el-dialog :visible.sync="dialogVisible" width="24%" center>
<div
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
<div
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
<img loading="lazy" src="@/assets/login/shield.png" alt=""
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
</div>
{{ $t('changePassword.title') }}
</div>
<div style="height: 1px;background: #e8f0ff;" />
<div style="margin: 22px 15px;">
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
<div style="color: red;display: inline-block;">*</div>
<CustomDialog
:title="$t('changePassword.title')"
:visible.sync="dialogVisible"
width="600px"
@cancel="cancel"
@confirm="confirm">
<div class="password-form">
<div class="form-label">
<span class="required">*</span>
{{ $t('changePassword.oldPasswordLabel') }}
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input :placeholder="$t('changePassword.oldPasswordPlaceholder')" v-model="oldPassword" type="password" show-password />
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
<div class="form-label" style="margin-top: 12px;">
<span class="required">*</span>
{{ $t('changePassword.newPasswordLabel') }}
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input :placeholder="$t('changePassword.newPasswordPlaceholder')" v-model="newPassword" type="password" show-password />
</div>
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
<div style="color: red;display: inline-block;">*</div>
<div class="form-label" style="margin-top: 12px;">
<span class="required">*</span>
{{ $t('changePassword.confirmPasswordLabel') }}
</div>
<div class="input-46" style="margin-top: 12px;">
<el-input :placeholder="$t('changePassword.confirmPasswordPlaceholder')" v-model="confirmNewPassword" type="password" show-password />
</div>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="confirm">
{{ $t('changePassword.confirmButton') }}
</div>
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
{{ $t('changePassword.cancelButton') }}
</div>
</div>
</el-dialog>
</CustomDialog>
</form>
</template>
<script>
import userApi from '@/apis/module/user';
import { mapActions } from 'vuex';
import { changeLanguage } from '@/i18n';
import CustomDialog from '@/components/CustomDialog.vue';
export default {
name: 'ChangePasswordDialog',
@@ -67,6 +54,9 @@ export default {
confirmNewPassword: ""
}
},
components: {
CustomDialog
},
watch: {
value(val) {
this.dialogVisible = val;
@@ -100,7 +90,6 @@ export default {
});
this.logout().then(() => {
this.$router.push('/login');
this.$emit('update:visible', false);
});
} else {
this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
@@ -108,10 +97,9 @@ export default {
}, (err) => {
this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
});
this.$emit('input', false);
this.dialogVisible = false;
},
cancel() {
this.dialogVisible = false;
this.resetForm();
},
resetForm() {
@@ -124,38 +112,18 @@ export default {
</script>
<style scoped>
.form-label {
text-align: left;
}
.required {
color: red;
display: inline-block;
}
.input-46 {
background: #f6f8fb;
border-radius: 15px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__headerbtn {
display: none;
}
::v-deep .el-dialog__body {
padding: 4px 6px;
}
::v-deep .el-dialog__header {
padding: 10px;
}
</style>
@@ -1,7 +1,19 @@
<template>
<el-dialog
:title="$t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle') + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
:visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
<CustomDialog
:title="$t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle')"
:visible.sync="dialogVisible"
width="80%"
:footer="false"
:close-on-click-modal="false"
custom-class="chat-history-dialog">
<template v-slot:title>
<span style="font-size: 18px;">
{{ $t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle') }}
<template v-if="currentMacAddress">
[<MacAddressMask :macAddress="currentMacAddress" />]
</template>
</span>
</template>
<div class="chat-container">
<div class="session-list" @scroll="handleScroll">
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
@@ -69,12 +81,14 @@
{{ $t('chatHistory.downloadCurrentSession') }}
</el-button>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import { debounce } from '@/utils'
import Api from '@/apis/api';
import CustomDialog from '@/components/CustomDialog.vue';
import MacAddressMask from '@/components/MacAddressMask.vue';
export default {
name: 'ChatHistoryDialog',
@@ -110,6 +124,10 @@ export default {
expandedToolResults: {} // 跟踪工具结果的展开状态
};
},
components: {
CustomDialog,
MacAddressMask,
},
watch: {
visible(val) {
this.dialogVisible = val;
@@ -229,9 +247,6 @@ export default {
this.isFirstLoad = true;
this.expandedToolResults = {};
},
handleClose() {
this.dialogVisible = false;
},
loadSessions() {
if (this.loading || (!this.isFirstLoad && !this.hasMore)) {
return;
@@ -623,11 +638,6 @@ export default {
overflow: hidden;
}
.chat-history-dialog .el-dialog__header {
background-color: #e6f7ff;
padding: 15px 20px;
}
.chat-history-dialog .el-dialog__body {
padding: 0;
overflow: hidden;
@@ -0,0 +1,161 @@
<template>
<el-button
:class="['custom-button', `custom-button--${type}`]"
v-bind="$attrs"
@click="$emit('click', $event)"
>
<slot />
</el-button>
</template>
<script>
export default {
name: 'CustomButton',
props: {
type: {
type: String,
default: 'default',
validator: (val) => ['default', 'confirm', 'delete', 'add'].includes(val)
}
}
};
</script>
<style lang="scss" scoped>
.custom-button--default {
width: fit-content;
background: #4998ff;
color: #ffffff;
border: none;
border-radius: 4px;
transition: all 0.3s ease;
&.el-button--small {
padding: 9px 14px;
font-size: 12px;
}
&.el-button--medium {
padding: 12px 20px;
}
&:hover {
background: #62a4fc;
color: #ffffff;
border-color: transparent;
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.2);
transform: translateY(-2px);
}
&.is-disabled,
&.is-disabled:hover {
background: linear-gradient(to right, #f0f3fe, #f5f6fe);
color: rgba(74, 124, 253, 0.5);
border-color: transparent;
box-shadow: none;
transform: none;
cursor: not-allowed;
}
}
.custom-button--confirm {
width: fit-content;
background: linear-gradient(to right, #4a7cfd, #8154fc);
color: white;
border: none;
border-radius: 4px;
transition: all 0.3s ease;
&.el-button--small {
padding: 9px 14px;
font-size: 12px;
}
&.el-button--medium {
padding: 12px 20px;
}
&:hover {
color: white;
border-color: transparent;
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.3);
transform: translateY(-2px);
}
&.is-disabled,
&.is-disabled:hover {
background: linear-gradient(to right, #a0b4fd, #b8a4fd);
color: rgba(255, 255, 255, 0.6);
border-color: transparent;
box-shadow: none;
transform: none;
cursor: not-allowed;
}
}
.custom-button--delete {
width: fit-content;
background: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
transition: all 0.3s ease;
&.el-button--small {
padding: 9px 14px;
font-size: 12px;
}
&.el-button--medium {
padding: 12px 20px;
}
&:hover {
color: white;
border-color: transparent;
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3);
transform: translateY(-2px);
}
&.is-disabled,
&.is-disabled:hover {
background: linear-gradient(to right, #ffaaaa, #ffcaca);
color: rgba(255, 255, 255, 0.6);
border-color: transparent;
box-shadow: none;
transform: none;
cursor: not-allowed;
}
}
.custom-button--add {
width: fit-content;
background: #52c41a;
color: white;
border: none;
border-radius: 4px;
transition: all 0.3s ease;
&.el-button--small {
padding: 9px 14px;
font-size: 12px;
}
&.el-button--medium {
padding: 12px 20px;
}
&:hover {
color: white;
border-color: transparent;
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
transform: translateY(-2px);
}
&.is-disabled,
&.is-disabled:hover {
background: linear-gradient(to right, #a0e89a, #c4f0c0);
color: rgba(255, 255, 255, 0.6);
border-color: transparent;
box-shadow: none;
transform: none;
cursor: not-allowed;
}
}
</style>
@@ -0,0 +1,233 @@
<template>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:width="width"
:close-on-click-modal="closeOnClickModal"
:close-on-press-escape="closeOnPressEscape"
:show-close="showClose"
:destroy-on-close="destroyOnClose"
:custom-class="customClass"
class="custom-dialog"
@close="handleClose"
@open="handleOpen"
>
<template slot="title">
<slot name="title">
<div class="dialog-title">
<img src="@/assets/knowledge-base/level.png" class="title-icon" />
<span>{{ title }}</span>
</div>
</slot>
</template>
<slot></slot>
<template slot="footer">
<div v-if="footer" class="dialog-footer">
<CustomButton @click="handleCancel">{{ cancelText }}</CustomButton>
<CustomButton :loading="confirmLoading" type="confirm" @click="handleConfirm">
<span class="confirm-inner">
<img src="@/assets/knowledge-base/star.png" class="confirm-icon" />
{{ confirmText }}
</span>
</CustomButton>
</div>
</template>
</el-dialog>
</template>
<script>
import CustomButton from './CustomButton.vue';
export default {
name: "CustomDialog",
props: {
title: {
type: String,
default: ""
},
visible: {
type: Boolean,
default: false
},
confirmLoading: {
type: Boolean,
default: false
},
width: {
type: String,
default: "600px"
},
footer: {
type: Boolean,
default: true
},
closeOnClickModal: {
type: Boolean,
default: false
},
closeOnPressEscape: {
type: Boolean,
default: true
},
showClose: {
type: Boolean,
default: true
},
destroyOnClose: {
type: Boolean,
default: true
},
customClass: {
type: String,
default: ""
},
cancelText: {
type: String,
default: "取消"
},
confirmText: {
type: String,
default: "确认保存"
}
},
data() {
return {
dialogVisible: this.visible
};
},
components: {
CustomButton
},
watch: {
visible(val) {
this.dialogVisible = val;
}
},
methods: {
handleClose() {
this.dialogVisible = false;
this.$emit("update:visible", false);
this.$emit("close");
},
handleOpen() {
this.$emit("open");
},
handleCancel() {
this.dialogVisible = false;
this.$emit("update:visible", false);
this.$emit("cancel");
},
handleConfirm() {
this.$emit("confirm");
}
}
};
</script>
<style lang="scss" scoped>
.custom-dialog {
::v-deep .el-dialog {
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__header {
padding: 16px 20px 12px;
background: linear-gradient(135deg, #e2eeff, #edeafe);
text-align: left;
}
::v-deep .el-dialog__title {
font-size: 16px;
font-weight: 500;
color: #1a1a1a;
}
.dialog-title {
font-size: 18px;
display: inline-flex;
align-items: center;
> span {
line-height: 18px;
font-weight: 500;
}
}
.title-icon {
width: 24px;
height: 24px;
margin-right: 8px;
}
::v-deep .el-dialog__headerbtn {
top: 12px;
right: 16px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
display: flex;
align-items: center;
justify-content: center;
.el-dialog__close {
font-size: 18px;
color: #666;
position: static;
transform: none;
}
&:hover {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
.el-dialog__close {
color: #333;
}
}
}
::v-deep .el-dialog__body {
padding: 20px;
}
::v-deep .el-dialog__footer {
padding: 12px 20px 16px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
.el-button {
padding: 10px 20px;
display: flex;
align-items: center;
}
.el-button--primary {
background: linear-gradient(to right, #4a7cfd, #8154fc);
border: none;
&:hover,
&:focus {
background: linear-gradient(to right, #4a7cfd, #8154fc);
opacity: 0.85;
}
}
.confirm-inner {
display: inline-flex;
align-items: center;
}
.confirm-icon {
width: 16px;
height: 16px;
margin-right: 4px;
}
}
}
</style>
@@ -0,0 +1,65 @@
<template>
<div class="pagination-wrapper">
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="pageSizeOptions"
:total="total"
layout="sizes, prev, pager, next, total"
background
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</template>
<script>
export default {
name: 'Pagination',
props: {
total: {
type: Number,
default: 0
},
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
pageSizeOptions: {
type: Array,
default: () => [10, 20, 50, 100]
}
},
methods: {
handleSizeChange(val) {
this.$emit('size-change', val);
},
handlePageChange(page) {
this.$emit('page-change', page);
}
}
};
</script>
<style lang="scss" scoped>
::v-deep .el-pagination {
.btn-next,
.btn-prev,
.el-pager li {
background: transparent !important;
min-width: 36px !important;
}
.el-pager li:not(.disabled).active {
background: linear-gradient(135deg, #2a6dfe 0%, #6b8cff 50%, #2a6dfe 100%) !important;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(45, 111, 254, 0.3);
color: #fff;
font-weight: 600;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
}
}
</style>
@@ -0,0 +1,309 @@
<template>
<div class="custom-table-wrapper">
<div class="table-container" :style="{ height: tableContainerHeight }">
<el-table
ref="tableRef"
:data="data"
:class="['custom-table', tableClass]"
height="100%"
v-loading="loading"
:element-loading-text="loadingText"
:element-loading-spinner="loadingSpinner"
:element-loading-background="loadingBackground"
:header-cell-class-name="headerCellClassName"
:row-class-name="rowClassName"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<!-- 选择列 -->
<el-table-column
v-if="showSelection"
width="55"
align="center"
label="选择"
>
<template slot-scope="scope">
<slot
v-if="$scopedSlots.selection"
name="selection"
:row="scope.row"
:$index="scope.$index"
/>
<el-checkbox
v-else
:value="scope.row.selected"
@change="handleCheckboxChange(scope.row)"
/>
</template>
</el-table-column>
<!-- 动态列 -->
<el-table-column
v-for="column in columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:width="column.width"
:min-width="column.minWidth"
:align="column.align || 'center'"
:show-overflow-tooltip="column.showOverflowTooltip !== false"
>
<template slot-scope="scope">
<!-- 自定义插槽优先使用 column.slot 指定的插槽名否则用 column.prop 作为插槽名 -->
<slot
v-if="$scopedSlots[column.slot] || $scopedSlots[column.prop]"
:name="column.slot || column.prop"
:row="scope.row"
:$index="scope.$index"
:column="column"
/>
<!-- 默认显示 -->
<template v-else>
{{ scope.row[column.prop] }}
</template>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column
v-if="showOperations"
:label="operationsLabel"
align="center"
:width="operationsWidth"
>
<template slot-scope="scope">
<slot name="operations" :row="scope.row" :$index="scope.$index" />
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="table-footer">
<slot name="footer-btns"></slot>
<CustomPagination
v-if="showPagination"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@size-change="handleSizeChange"
@page-change="handlePageChange"
/>
</div>
</div>
</template>
<script>
import CustomPagination from './CustomPagination.vue';
export default {
name: 'CustomTable',
components: {
CustomPagination
},
props: {
// 表格数据
data: {
type: Array,
default: () => []
},
// 列配置
columns: {
type: Array,
default: () => []
},
// 是否显示选择框
showSelection: {
type: Boolean,
default: false
},
// 是否显示操作列
showOperations: {
type: Boolean,
default: false
},
operationsLabel: {
type: String,
default: '操作'
},
operationsWidth: {
type: [String, Number],
default: 180
},
// 分页相关
showPagination: {
type: Boolean,
default: true
},
total: {
type: Number,
default: 0
},
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
pageSizeOptions: {
type: Array,
default: () => [10, 20, 50, 100]
},
// 加载状态
loading: {
type: Boolean,
default: false
},
loadingText: {
type: String,
default: 'Loading'
},
loadingSpinner: {
type: String,
default: 'el-icon-loading'
},
loadingBackground: {
type: String,
default: 'rgba(255, 255, 255, 0.7)'
},
// 自定义类名
tableClass: {
type: String,
default: ''
},
headerCellClassName: {
type: String,
default: ''
},
rowClassName: {
type: [String, Function],
default: ''
},
},
computed: {
tableContainerHeight() {
return this.showPagination ? 'calc(100% - 48px)' : '100%';
}
},
methods: {
// 复选框变化
handleCheckboxChange(row) {
this.$set(row, 'selected', !row.selected);
},
// 分页事件
handleSizeChange(val) {
this.$emit('size-change', val);
},
handlePageChange(page) {
this.$emit('page-change', page);
},
// 选择事件
handleSelectionChange(selection) {
this.$emit('selection-change', selection);
},
// 行点击事件
handleRowClick(row, column, event) {
this.$emit('row-click', row, column, event);
},
// 清除选择
clearSelection() {
this.$refs.tableRef && this.$refs.tableRef.clearSelection();
},
// 切换选择
toggleRowSelection(row, selected) {
this.$refs.tableRef && this.$refs.tableRef.toggleRowSelection(row, selected);
}
}
};
</script>
<style scoped lang="scss">
.custom-table-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
.table-container {
width: 100%;
box-shadow: 0 2px 12px rgba(74, 124, 253, 0.12);
border-radius: 6px;
.custom-table {
width: 100%;
border: 1px solid #eef3fd;
border-bottom: none;
border-radius: 6px;
.el-table__body-wrapper {
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #a1c9fd;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: #f0f3fe;
border-radius: 3px;
}
}
.el-table__header {
th {
color: #342f45;
background: #edf2fc !important;
}
}
}
}
}
:deep(.el-table) {
.el-table__body-wrapper {
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #a1c9fd;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: #f0f3fe;
border-radius: 3px;
}
}
.el-table__header {
th {
color: #342f45;
background: #edf2fc !important;
}
}
}
.table-footer {
padding: 16px 0px 0px;
display: flex;
align-items: center;
justify-content: space-between;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .circular) {
width: 28px;
height: 28px;
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
:deep(.el-loading-text) {
color: #6b8cff !important;
font-size: 14px;
margin-top: 8px;
}
</style>
+105 -104
View File
@@ -1,116 +1,117 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel">
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')"></el-input>
</el-form-item>
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue">
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')"></el-input>
</el-form-item>
<el-form-item :label="$t('dictDataDialog.sort')" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
</div>
</el-dialog>
<CustomDialog
:title="title"
:visible.sync="dialogVisible"
width="600px"
@confirm="submit"
@close="cancel"
:confirmLoading="saving"
>
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left">
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel" class="form-item">
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')" />
</el-form-item>
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue" class="form-item">
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')" />
</el-form-item>
<el-form-item :label="$t('dictDataDialog.sort')" prop="sort" class="form-item">
<el-input-number v-model="form.sort" :min="0" :max="999" class="custom-input-number"></el-input-number>
</el-form-item>
</el-form>
</CustomDialog>
</template>
<script>
import CustomDialog from './CustomDialog.vue';
export default {
name: 'DictDataDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: () => this.$t('dictDataDialog.addDictData')
},
dictData: {
type: Object,
default: () => ({})
},
dictTypeId: {
type: [Number, String],
default: null
}
name: 'DictDataDialog',
props: {
visible: {
type: Boolean,
default: false
},
data() {
return {
dialogVisible: this.visible,
form: {
id: null,
dictTypeId: null,
dictLabel: '',
dictValue: '',
sort: 0
},
rules: {
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
}
}
title: {
type: String,
default: () => this.$t('dictDataDialog.addDictData')
},
watch: {
dictData: {
handler(val) {
if (val) {
this.form = { ...val }
}
},
immediate: true
},
dictTypeId: {
handler(val) {
if (val) {
this.form.dictTypeId = val
}
},
immediate: true
},
visible(val) {
this.dialogVisible = val;
},
dialogVisible(val) {
this.$emit('update:visible', val);
}
dictData: {
type: Object,
default: () => ({})
},
methods: {
handleClose() {
this.dialogVisible = false;
this.resetForm();
},
resetForm() {
this.form = {
id: null,
dictTypeId: this.dictTypeId,
dictLabel: '',
dictValue: '',
sort: 0
}
this.$refs.form?.resetFields()
},
handleSave() {
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('save', this.form)
}
})
}
dictTypeId: {
type: [Number, String],
default: null
}
}
},
components: {
CustomDialog
},
data() {
return {
dialogVisible: this.visible,
saving: false,
form: {
id: null,
dictTypeId: null,
dictLabel: '',
dictValue: '',
sort: 0
},
rules: {
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
}
};
},
watch: {
visible(val) {
this.dialogVisible = val;
},
dialogVisible(val) {
this.$emit('update:visible', val);
if (!val) {
this.saving = false;
}
},
dictData: {
handler(val) {
if (val) {
this.form = { ...val };
}
},
immediate: true
},
dictTypeId: {
handler(val) {
if (val) {
this.form.dictTypeId = val;
}
},
immediate: true
}
},
methods: {
submit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.saving = true;
this.$emit('save', this.form);
}
});
},
cancel() {
this.saving = false;
this.$emit('cancel');
},
resetSaving() {
this.saving = false;
}
}
};
</script>
<style scoped>
.dialog-footer {
text-align: right;
<style scoped lang="scss">
.custom-input-number {
width: 100%;
}
:deep(.el-dialog) {
border-radius: 15px;
}
</style>
</style>
@@ -1,97 +1,94 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
<el-form-item :label="$t('dictTypeDialog.dictName')" prop="dictName">
<el-input v-model="form.dictName" :placeholder="$t('dictTypeDialog.dictNamePlaceholder')"></el-input>
</el-form-item>
<el-form-item :label="$t('dictTypeDialog.dictType')" prop="dictType">
<el-input v-model="form.dictType" :placeholder="$t('dictTypeDialog.dictTypePlaceholder')"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
</div>
</el-dialog>
<CustomDialog
:title="title"
:visible.sync="dialogVisible"
width="600px"
@confirm="submit"
@close="cancel"
:confirmLoading="saving"
>
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left">
<el-form-item :label="$t('dictTypeDialog.dictName')" prop="dictName" class="form-item">
<el-input v-model="form.dictName" :placeholder="$t('dictTypeDialog.dictNamePlaceholder')" />
</el-form-item>
<el-form-item :label="$t('dictTypeDialog.dictType')" prop="dictType" class="form-item">
<el-input v-model="form.dictType" :placeholder="$t('dictTypeDialog.dictTypePlaceholder')" />
</el-form-item>
</el-form>
</CustomDialog>
</template>
<script>
import CustomDialog from './CustomDialog.vue';
export default {
name: 'DictTypeDialog',
props: {
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: () => this.$t('dictTypeDialog.addDictType')
},
dictTypeData: {
type: Object,
default: () => ({})
}
name: 'DictTypeDialog',
props: {
visible: {
type: Boolean,
default: false
},
data() {
return {
dialogVisible: this.visible,
form: {
id: null,
dictName: '',
dictType: ''
},
rules: {
dictName: [{ required: true, message: this.$t('dictTypeDialog.requiredDictName'), trigger: 'blur' }],
dictType: [{ required: true, message: this.$t('dictTypeDialog.requiredDictType'), trigger: 'blur' }]
}
}
title: {
type: String,
default: () => this.$t('dictTypeDialog.addDictType')
},
watch: {
visible(val) {
this.dialogVisible = val;
},
dialogVisible(val) {
this.$emit('update:visible', val);
},
dictTypeData: {
handler(val) {
if (val) {
this.form = { ...val }
}
},
immediate: true
}
},
methods: {
handleClose() {
this.dialogVisible = false;
this.resetForm()
},
resetForm() {
this.form = {
id: null,
dictName: '',
dictType: ''
}
this.$refs.form?.resetFields()
},
handleSave() {
this.$refs.form.validate(valid => {
if (valid) {
this.$emit('save', this.form)
}
})
}
dictTypeData: {
type: Object,
default: () => ({})
}
}
},
components: {
CustomDialog
},
data() {
return {
dialogVisible: this.visible,
saving: false,
form: {
id: null,
dictName: '',
dictType: ''
},
rules: {
dictName: [{ required: true, message: this.$t('dictTypeDialog.requiredDictName'), trigger: 'blur' }],
dictType: [{ required: true, message: this.$t('dictTypeDialog.requiredDictType'), trigger: 'blur' }]
}
};
},
watch: {
visible(val) {
this.dialogVisible = val;
},
dialogVisible(val) {
this.$emit('update:visible', val);
if (!val) {
this.saving = false;
}
},
dictTypeData: {
handler(val) {
if (val) {
this.form = { ...val };
}
},
immediate: true
}
},
methods: {
submit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.saving = true;
this.$emit('save', this.form);
}
});
},
cancel() {
this.saving = false;
this.$emit('cancel');
},
resetSaving() {
this.saving = false;
}
}
};
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
:deep(.el-dialog) {
border-radius: 15px;
}
</style>
@@ -1,18 +1,25 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
@open="handleOpen">
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
<CustomDialog
:title="title"
:visible.sync="visible"
width="800px"
@confirm="submit"
@close="cancel"
@open="handleOpen"
:confirmLoading="saving"
>
<el-form ref="form" :model="form" :rules="rules" label-width="auto" label-position="left" class="firmware-form">
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')" />
</el-form-item>
<el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;"
filterable :disabled="isTypeDisabled">
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')"
class="custom-select" filterable :disabled="isTypeDisabled">
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('firmwareDialog.version')" prop="version">
<el-input v-model="form.version" :placeholder="$t('firmwareDialog.versionPlaceholder')"></el-input>
<el-input v-model="form.version" :placeholder="$t('firmwareDialog.versionPlaceholder')" />
</el-form-item>
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
@@ -29,21 +36,21 @@
</el-form-item>
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
<el-input type="textarea" v-model="form.remark"
:placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
:placeholder="$t('firmwareDialog.remarkPlaceholder')" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel">{{ $t('button.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmit">{{ $t('button.save') }}</el-button>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import Api from '@/apis/api';
import CustomDialog from './CustomDialog.vue';
export default {
name: 'FirmwareDialog',
components: {
CustomDialog
},
props: {
visible: {
type: Boolean,
@@ -68,7 +75,7 @@ export default {
uploadProgress: 0,
uploadStatus: '',
isUploading: false,
dialogVisible: this.visible,
saving: false,
rules: {
firmwareName: [
{ required: true, message: this.$t('firmwareDialog.requiredFirmwareName'), trigger: 'blur' }
@@ -92,28 +99,8 @@ export default {
return !!this.form.id
}
},
created() {
// 移除 getDictDataByType 调用
},
watch: {
visible(val) {
this.dialogVisible = val;
},
dialogVisible(val) {
this.$emit('update:visible', val);
},
},
methods: {
// 移除 getFirmwareTypes 方法
handleClose() {
this.dialogVisible = false;
this.$emit('cancel');
},
handleCancel() {
this.$refs.form.clearValidate();
this.$emit('cancel');
},
handleSubmit() {
submit() {
this.$refs.form.validate(valid => {
if (valid) {
// 如果是新增模式且没有上传文件,则提示错误
@@ -121,11 +108,20 @@ export default {
this.$message.error(this.$t('firmwareDialog.requiredFirmwareFile'))
return
}
this.saving = true
// 提交成功后将关闭对话框的逻辑交给父组件处理
this.$emit('submit', this.form)
}
})
},
cancel() {
this.saving = false
this.$emit('cancel')
},
// 提供给父组件调用以重置saving状态
resetSaving() {
this.saving = false
},
beforeUpload(file) {
const isValidSize = file.size / 1024 / 1024 < 100
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
@@ -197,6 +193,7 @@ export default {
this.uploadProgress = 0
this.uploadStatus = ''
this.isUploading = false
this.saving = false
// 重置表单中的文件相关字段
if (!this.form.id) { // 只在新增时重置
this.form.firmwarePath = ''
@@ -213,25 +210,24 @@ export default {
}
</script>
<style lang="scss" scoped>
::v-deep .el-dialog {
border-radius: 20px;
<style scoped lang="scss">
.firmware-form {
.custom-select {
width: 100%;
}
.upload-demo {
text-align: left;
}
.el-upload__tip {
line-height: 1.2;
padding-top: 2%;
color: #909399;
}
.hint-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
}
.upload-demo {
text-align: left;
}
.el-upload__tip {
line-height: 1.2;
padding-top: 2%;
color: #909399;
}
.hint-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
</style>
</style>
+29 -272
View File
@@ -101,7 +101,8 @@
$route.path === '/agent-template-management' ||
$route.path === '/ota-management' ||
$route.path === '/user-management' ||
$route.path === '/feature-management',
$route.path === '/feature-management' ||
$route.path === '/replacement-word-management'
}" @visible-change="handleParamDropdownVisibleChange">
<span class="el-dropdown-link">
<img loading="lazy" alt="" src="@/assets/header/param_management.png" :style="{
@@ -113,7 +114,8 @@
$route.path === '/agent-template-management' ||
$route.path === '/ota-management' ||
$route.path === '/user-management' ||
$route.path === '/feature-management'
$route.path === '/feature-management' ||
$route.path === '/replacement-word-management'
? 'brightness(0) invert(1)'
: 'None',
}" />
@@ -154,32 +156,6 @@
<!-- 右侧元素 -->
<div class="header-right">
<div class="search-container" v-if="$route.path === '/home' && !(userInfo.superAdmin && isSmallScreen)">
<div class="search-wrapper">
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input"
@keyup.enter.native="handleSearch" @focus="showSearchHistory" @blur="hideSearchHistory" clearable
ref="searchInput">
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
</el-input>
<!-- 搜索历史下拉框 -->
<div v-if="showHistory && searchHistory.length > 0" class="search-history-dropdown">
<div class="search-history-header">
<span>{{ $t("header.searchHistory") }}</span>
<el-button type="text" size="small" class="clear-history-btn" @click="clearSearchHistory">
{{ $t("header.clearHistory") }}
</el-button>
</div>
<div class="search-history-list">
<div v-for="(item, index) in searchHistory" :key="index" class="search-history-item"
@click.stop="selectSearchHistory(item)">
<span class="history-text">{{ item }}</span>
<i class="el-icon-close clear-item-icon" @click.stop="removeSearchHistory(index)"></i>
</div>
</div>
</div>
</div>
</div>
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" @click="handleAvatarClick" />
<span class="el-user-dropdown" @click="handleAvatarClick">
{{ userInfo.username || "加载中..." }}
@@ -220,12 +196,6 @@ export default {
voiceCloneDropdownVisible: false,
userMenuVisible: false, // 添加用户菜单可见状态
menuVisibleTimer: null, // 菜单显示定时器,防止够快触发
isSmallScreen: false,
// 搜索历史相关
searchHistory: [],
showHistory: false,
SEARCH_HISTORY_KEY: "xiaozhi_search_history",
MAX_HISTORY_COUNT: 3,
// Cascader 配置
cascaderProps: {
expandTrigger: "click",
@@ -351,17 +321,9 @@ export default {
},
},
async mounted() {
this.checkScreenSize();
window.addEventListener("resize", this.checkScreenSize);
// 从localStorage加载搜索历史
this.loadSearchHistory();
// 等待featureManager初始化完成后再加载功能状态
await this.loadFeatureStatus();
},
//移除事件监听器
beforeDestroy() {
window.removeEventListener("resize", this.checkScreenSize);
},
methods: {
handleRouter(type) {
this.$router.push(this.routerPaths[type]);
@@ -371,104 +333,6 @@ export default {
// 等待featureManager初始化完成
await featureManager.waitForInitialization();
},
checkScreenSize() {
this.isSmallScreen = window.innerWidth <= 1386;
},
// 处理搜索
handleSearch() {
const searchValue = this.search.trim();
// 如果搜索内容为空,触发重置事件
if (!searchValue) {
this.$emit("search-reset");
return;
}
// 保存搜索历史
this.saveSearchHistory(searchValue);
// 触发搜索事件,将搜索关键词传递给父组件
this.$emit("search", searchValue);
// 搜索完成后让输入框失去焦点,从而触发blur事件隐藏搜索历史
if (this.$refs.searchInput) {
this.$refs.searchInput.blur();
}
},
// 显示搜索历史
showSearchHistory() {
this.showHistory = true;
},
// 隐藏搜索历史
hideSearchHistory() {
// 延迟隐藏,以便点击事件能够执行
setTimeout(() => {
this.showHistory = false;
}, 200);
},
// 加载搜索历史
loadSearchHistory() {
try {
const history = localStorage.getItem(this.SEARCH_HISTORY_KEY);
if (history) {
this.searchHistory = JSON.parse(history);
}
} catch (error) {
console.error("加载搜索历史失败:", error);
this.searchHistory = [];
}
},
// 保存搜索历史
saveSearchHistory(keyword) {
if (!keyword || this.searchHistory.includes(keyword)) {
return;
}
// 添加到历史记录开头
this.searchHistory.unshift(keyword);
// 限制历史记录数量
if (this.searchHistory.length > this.MAX_HISTORY_COUNT) {
this.searchHistory = this.searchHistory.slice(0, this.MAX_HISTORY_COUNT);
}
// 保存到localStorage
try {
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
} catch (error) {
console.error("保存搜索历史失败:", error);
}
},
// 选择搜索历史项
selectSearchHistory(keyword) {
this.search = keyword;
this.handleSearch();
},
// 移除单个搜索历史项
removeSearchHistory(index) {
this.searchHistory.splice(index, 1);
try {
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
} catch (error) {
console.error("更新搜索历史失败:", error);
}
},
// 清空所有搜索历史
clearSearchHistory() {
this.searchHistory = [];
try {
localStorage.removeItem(this.SEARCH_HISTORY_KEY);
} catch (error) {
console.error("清空搜索历史失败:", error);
}
},
// 显示修改密码弹窗
showChangePasswordDialog() {
this.isChangePasswordDialogVisible = true;
@@ -631,11 +495,9 @@ export default {
<style lang="scss" scoped>
.header {
background: #f6fcfe66;
border: 1px solid #fff;
background: linear-gradient(180deg, #dfeafe, #eff4ff);
height: 63px !important;
min-width: 900px;
/* 设置最小宽度防止过度压缩 */
overflow: visible;
}
@@ -651,7 +513,7 @@ export default {
display: flex;
align-items: center;
gap: 10px;
min-width: 120px;
min-width: 130px;
cursor: pointer;
}
@@ -668,42 +530,53 @@ export default {
display: flex;
align-items: center;
gap: 25px;
position: absolute;
left: 50%;
transform: translateX(calc(-50% - 80px));
background: white;
border-radius: 30px;
box-shadow: 0 0 6px 0px #cfe1fb;
padding: 4px;
}
.header-right {
display: flex;
align-items: center;
gap: 7px;
min-width: 300px;
justify-content: flex-end;
}
.equipment-management {
height: 30px;
border-radius: 15px;
background: #deeafe;
padding: 8px 16px;
border-radius: 30px;
display: flex;
justify-content: center;
font-size: 14px;
font-size: 16px;
font-weight: 500;
gap: 7px;
color: #3d4566;
color: #6c79a8;
margin-left: 1px;
align-items: center;
transition: all 0.3s ease;
cursor: pointer;
flex-shrink: 0;
/* 防止导航按钮被压缩 */
padding: 0 15px;
position: relative;
}
.equipment-management.active-tab {
background: #5778ff !important;
color: #fff !important;
background: linear-gradient(90deg, #2983fe 0%, #5251fc 100%);
box-shadow: 0 1px 8px rgba(41, 131, 254, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);
position: relative;
overflow: hidden;
}
.equipment-management.active-tab::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 100%);
pointer-events: none;
}
.equipment-management img {
@@ -711,121 +584,6 @@ export default {
height: 13px;
}
.search-container {
margin-right: 5px;
flex: 0.9;
min-width: 60px;
max-width: none;
}
.search-wrapper {
position: relative;
}
.search-history-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #e4e6ef;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 2px;
}
.search-history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
color: #909399;
}
.clear-history-btn {
color: #909399;
font-size: 11px;
padding: 0;
height: auto;
}
.clear-history-btn:hover {
color: #606266;
}
.search-history-list {
max-height: 200px;
overflow-y: auto;
}
.search-history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
color: #606266;
}
.search-history-item:hover {
background-color: #f5f7fa;
}
.clear-item-icon {
font-size: 10px;
color: #909399;
visibility: hidden;
}
.more-dropdown {
padding: 0;
}
.more-dropdown .el-dropdown-link {
display: flex;
align-items: center;
gap: 7px;
height: 100%;
padding: 0 15px;
}
.search-history-item:hover .clear-item-icon {
visibility: visible;
}
.clear-item-icon:hover {
color: #ff4949;
}
.custom-search-input>>>.el-input__inner {
height: 18px;
border-radius: 9px;
background-color: #fff;
border: 1px solid #e4e6ef;
padding-left: 8px;
font-size: 9px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
width: 100%;
}
.search-icon {
cursor: pointer;
color: #909399;
margin-right: 3px;
font-size: 9px;
line-height: 18px;
}
.custom-search-input::v-deep .el-input__suffix-inner {
display: flex;
align-items: center;
height: 100%;
}
.avatar-img {
width: 21px;
height: 21px;
@@ -841,7 +599,6 @@ export default {
.nav-text {
white-space: normal;
text-align: center;
max-width: 80px;
line-height: 1.2;
}
@@ -1,5 +1,5 @@
<template>
<el-dialog :title="title" :visible="dialogVisible" width="600px" class="knowledge-base-dialog" @close="handleClose">
<CustomDialog :title="title" :visible.sync="dialogVisible" width="600px" class="knowledge-base-dialog" @close="handleClose" @confirm="handleSubmit">
<el-form ref="knowledgeBaseForm" :model="form" :rules="rules" label-width="100px" size="medium">
<el-form-item :label="$t('knowledgeBaseDialog.name')" prop="name">
<el-input v-model="form.name" :placeholder="$t('knowledgeBaseDialog.namePlaceholder')" clearable></el-input>
@@ -16,15 +16,12 @@
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{ $t('knowledgeBaseDialog.cancel') }}</el-button>
<el-button type="primary" @click="handleSubmit">{{ $t('knowledgeBaseDialog.confirm') }}</el-button>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import Api from "@/apis/api";
import CustomDialog from "./CustomDialog.vue";
export default {
name: "KnowledgeBaseDialog",
@@ -93,6 +90,9 @@ export default {
}
};
},
components: {
CustomDialog
},
watch: {
visible(val) {
this.dialogVisible = val;
@@ -0,0 +1,43 @@
<template>
<span v-if="isValid" class="mac-address-wrapper">{{ prefix }}<span class="mac-address-mask">{{ middle }}</span>{{ suffix }}</span>
<span v-else>{{ macAddress }}</span>
</template>
<script>
export default {
props: {
macAddress: {
type: String,
default: '',
},
},
computed: {
isValid() {
return /^(?:[0-9A-Z]{2}[:-]){5}[0-9A-Z]{2}$/i.test(this.macAddress);
},
segments() {
return this.macAddress.split(':');
},
prefix() {
return this.segments.slice(0, 2).join(':');
},
middle() {
return ':' + this.segments.slice(2, 4).join(':') + ':';
},
suffix() {
return this.segments.slice(4).join(':');
},
},
};
</script>
<style lang="scss" scoped>
.mac-address-wrapper:hover .mac-address-mask {
filter: none;
}
.mac-address-mask {
filter: blur(4px);
transition: filter 0.3s;
}
</style>
@@ -1,43 +1,45 @@
<template>
<el-dialog :title="$t('manualAddDeviceDialog.title')" :visible="visible" @close="handleClose" width="30%" center>
<div class="dialog-content">
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="auto">
<el-form-item :label="$t('manualAddDeviceDialog.deviceType')" prop="board">
<el-select v-model="deviceForm.board" :placeholder="$t('manualAddDeviceDialog.deviceTypePlaceholder')" style="width: 100%">
<el-option
v-for="item in firmwareTypes"
:key="item.key"
:label="item.name"
:value="item.key">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('manualAddDeviceDialog.firmwareVersion')" prop="appVersion">
<el-input v-model="deviceForm.appVersion" :placeholder="$t('manualAddDeviceDialog.firmwareVersionPlaceholder')"></el-input>
</el-form-item>
<el-form-item :label="$t('manualAddDeviceDialog.macAddress')" prop="macAddress">
<el-input v-model="deviceForm.macAddress" :placeholder="$t('manualAddDeviceDialog.macAddressPlaceholder')"></el-input>
</el-form-item>
</el-form>
</div>
<div style="display: flex;margin: 15px 15px;gap: 7px;">
<div class="dialog-btn" @click="submitForm">{{ $t('manualAddDeviceDialog.confirm') }}</div>
<div class="dialog-btn cancel-btn" @click="cancel">{{ $t('manualAddDeviceDialog.cancel') }}</div>
</div>
</el-dialog>
<CustomDialog
:title="$t('manualAddDeviceDialog.title')"
:visible="visible"
width="600px"
:destroy-on-close="false"
@close="closeDialog"
@confirm="submitForm"
>
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="auto">
<el-form-item :label="$t('manualAddDeviceDialog.deviceType')" prop="board">
<el-select v-model="deviceForm.board" :placeholder="$t('manualAddDeviceDialog.deviceTypePlaceholder')" style="width: 100%">
<el-option
v-for="item in firmwareTypes"
:key="item.key"
:label="item.name"
:value="item.key">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('manualAddDeviceDialog.firmwareVersion')" prop="appVersion">
<el-input v-model="deviceForm.appVersion" :placeholder="$t('manualAddDeviceDialog.firmwareVersionPlaceholder')"></el-input>
</el-form-item>
<el-form-item :label="$t('manualAddDeviceDialog.macAddress')" prop="macAddress">
<el-input v-model="deviceForm.macAddress" :placeholder="$t('manualAddDeviceDialog.macAddressPlaceholder')"></el-input>
</el-form-item>
</el-form>
</CustomDialog>
</template>
<script>
import Api from '@/apis/api';
import CustomDialog from './CustomDialog.vue';
export default {
name: 'ManualAddDeviceDialog',
components: { CustomDialog },
props: {
visible: { type: Boolean, required: true },
agentId: { type: String, required: true }
},
data() {
// MAC地址验证规则
const validateMac = (rule, value, callback) => {
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
if (!value) {
@@ -67,7 +69,7 @@ export default {
{ required: true, validator: validateMac, trigger: 'blur' }
]
}
}
};
},
created() {
this.getFirmwareTypes();
@@ -94,7 +96,7 @@ export default {
agentId: this.agentId,
...this.deviceForm
};
Api.device.manualAddDevice(params, ({ data }) => {
if (data.code === 0) {
this.$message.success(this.$t('manualAddDeviceDialog.addSuccess'));
@@ -108,51 +110,7 @@ export default {
closeDialog() {
this.$emit('update:visible', false);
this.$refs.deviceForm.resetFields();
},
cancel() {
this.closeDialog();
},
handleClose() {
this.closeDialog();
}
}
}
};
</script>
<style scoped>
.dialog-content {
padding: 0 20px;
}
.dialog-btn {
cursor: pointer;
flex: 1;
border-radius: 23px;
background: #5778ff;
height: 40px;
font-weight: 500;
font-size: 12px;
color: #fff;
line-height: 40px;
text-align: center;
}
.cancel-btn {
background: #e6ebff;
border: 1px solid #adbdff;
color: #5778ff;
}
::v-deep .el-dialog {
border-radius: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
::v-deep .el-dialog__body {
padding: 20px 20px;
}
::v-deep .el-form-item {
margin-bottom: 20px;
}
</style>
@@ -0,0 +1,20 @@
<template>
<svg t="1780649285700" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="79487" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48">
<path
d="M917.333333 138.666667C917.333333 85.333333 870.4 42.666667 810.666667 42.666667H219.733333C157.866667 42.666667 106.666667 89.6 106.666667 145.066667V855.466667c0 70.4 57.6 128 128 128h576c59.733333 0 106.666667-46.933333 106.666666-106.666667V138.666667zM234.666667 213.333333h362.666666c23.466667 0 42.666667 19.2 42.666667 42.666667s-19.2 42.666667-42.666667 42.666667H234.666667c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667z m0 128h277.333333c23.466667 0 42.666667 19.2 42.666667 42.666667s-19.2 42.666667-42.666667 42.666667H234.666667c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667z m597.333333 533.333334c0 12.8-8.533333 21.333333-21.333333 21.333333H236.8c-21.333333 0-40.533333-14.933333-44.8-36.266667-4.266667-25.6 17.066667-49.066667 42.666667-49.066666h576c12.8 0 21.333333 8.533333 21.333333 21.333333v42.666667z"
:fill="color" p-id="79488"></path>
</svg>
</template>
<script>
export default {
name: 'ManualIcon',
props: {
color: {
type: String,
default: '#b3a0fa',
},
},
}
</script>
@@ -1,135 +1,109 @@
<template>
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="57%" center custom-class="custom-dialog"
:show-close="false" class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px">
<div style="
font-size: 30px;
color: #3d4566;
margin-top: -10px;
margin-bottom: 10px;
text-align: center;
">
{{
modelData.duplicateMode
? $t("modelConfigDialog.duplicateModel")
: $t("modelConfigDialog.editModel")
}}
</div>
<button class="custom-close-btn" @click="dialogVisible = false">×</button>
<div style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
">
<div style="font-size: 20px; font-weight: bold; color: #3d4566">
{{ $t("modelConfigDialog.modelInfo") }}
<CustomDialog
:title="$t('modelConfigDialog.editModel')"
:visible.sync="dialogVisible"
width="57%"
class="model-edit-dialog"
:confirmLoading="saving"
@confirm="handleSave"
@close="handleClose"
@open="handleOpen"
>
<div class="dialog-scroll-body">
<div class="header-row">
<div class="section-title">{{ $t("modelConfigDialog.modelInfo") }}</div>
<div class="switch-group">
<div class="switch-item">
<span class="switch-label">{{ $t("modelConfigDialog.enable") }}</span>
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div>
<div style="display: flex; align-items: center; gap: 20px">
<div style="display: flex; align-items: center">
<span style="margin-right: 8px">{{ $t("modelConfigDialog.enable") }}</span>
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div>
<div style="display: none; align-items: center">
<span style="margin-right: 8px">{{
$t("modelConfigDialog.setDefault")
}}</span>
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div>
<div class="switch-item hidden">
<span class="switch-label">{{ $t("modelConfigDialog.setDefault") }}</span>
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
</div>
</div>
</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px"></div>
<div class="section-divider"></div>
<el-form :model="form" ref="form" label-width="auto" label-position="left" class="custom-form">
<div style="display: flex; gap: 20px; margin-bottom: 0">
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1">
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
class="custom-input-bg"></el-input>
</el-form-item>
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1">
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')"
class="custom-input-bg"></el-input>
</el-form-item>
</div>
<div style="display: flex; gap: 20px; margin-bottom: 0">
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1">
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')"
class="custom-select custom-input-bg" style="width: 100%" @focus="loadProviders" filterable>
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1">
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')"
class="custom-input-bg"></el-input>
</el-form-item>
</div>
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px">
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
class="custom-input-bg"></el-input>
<el-form :model="form" ref="form" label-width="auto" label-position="left">
<div class="form-row">
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1">
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"></el-input>
</el-form-item>
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')"
:autosize="{ minRows: 3, maxRows: 5 }" class="custom-input-bg"></el-input>
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1">
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')"></el-input>
</el-form-item>
</el-form>
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px">
{{ $t("modelConfigDialog.callInfo") }}
</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px"></div>
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0">
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
style="flex: 1">
<template v-if="field.type === 'json-textarea'">
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3"
:placeholder="$t('modelConfigDialog.enterJsonExample')" class="custom-input-bg"
@change="(val) => handleJsonChange(field.prop, val)" @focus="
isSensitiveField(field.prop)
? handleJsonInputFocus(field.prop, fieldJsonMap[field.prop])
: undefined
" @blur="
isSensitiveField(field.prop)
? handleJsonInputBlur(field.prop)
: undefined
"></el-input>
</template>
<div class="form-row">
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1">
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')"
style="width: 100%" @focus="loadProviders" filterable>
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1">
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')"></el-input>
</el-form-item>
</div>
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
class="custom-input-bg" :show-password="field.type === 'password'" @focus="
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px">
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"></el-input>
</el-form-item>
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
<el-input v-model="form.remark" type="textarea" :rows="3" :autosize="{ minRows: 3, maxRows: 5 }" :placeholder="$t('modelConfigDialog.enterRemark')"></el-input>
</el-form-item>
</el-form>
<teleport v-if="chunkedCallInfoFields.length">
<div class="section-title">{{ $t("modelConfigDialog.callInfo") }}</div>
<div class="section-divider"></div>
</teleport>
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" label-position="left">
<template>
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex" class="form-row">
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
style="flex: 1">
<template v-if="field.type === 'json-textarea'">
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3"
:placeholder="$t('modelConfigDialog.enterJsonExample')"
@change="(val) => handleJsonChange(field.prop, val)" @focus="
isSensitiveField(field.prop)
? handleInputFocus(field.prop, form.configJson[field.prop])
? handleJsonInputFocus(field.prop, fieldJsonMap[field.prop])
: undefined
" @blur="
isSensitiveField(field.prop) ? handleInputBlur(field.prop) : undefined
isSensitiveField(field.prop)
? handleJsonInputBlur(field.prop)
: undefined
"></el-input>
</el-form-item>
</div>
</template>
</el-form>
</div>
</template>
<div style="display: flex; justify-content: center">
<el-button type="primary" @click="handleSave" class="save-btn" :loading="saving" :disabled="saving">
{{ $t("modelConfigDialog.save") }}
</el-button>
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
:show-password="field.type === 'password'" @focus="
isSensitiveField(field.prop)
? handleInputFocus(field.prop, form.configJson[field.prop])
: undefined
" @blur="
isSensitiveField(field.prop) ? handleInputBlur(field.prop) : undefined
"></el-input>
</el-form-item>
</div>
</template>
</el-form>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import CustomDialog from './CustomDialog.vue';
import Api from "@/apis/api";
export default {
name: "ModelEditDialog",
components: { CustomDialog },
props: {
visible: { type: Boolean, default: false },
modelData: {
@@ -175,6 +149,11 @@ export default {
};
},
computed: {
title() {
return this.modelData.duplicateMode
? this.$t("modelConfigDialog.duplicateModel")
: this.$t("modelConfigDialog.editModel");
},
chunkedCallInfoFields() {
const chunkSize = 2;
const result = [];
@@ -189,19 +168,11 @@ export default {
this.resetProviders();
this.loadProviders();
},
dialogVisible(val) {
this.$emit("update:visible", val);
if (!val) {
this.resetForm();
} else if (val && this.modelData.id) {
this.loadModelData();
}
},
visible(val) {
this.dialogVisible = val;
if (val) {
this.loadProviders();
}
},
dialogVisible(val) {
this.$emit("update:visible", val);
},
"form.configJson.type"(newVal) {
if (newVal && this.providersLoaded) {
@@ -210,6 +181,19 @@ export default {
},
},
methods: {
handleOpen() {
this.loadProviders();
if (this.modelData.id) {
this.loadModelData();
}
},
handleClose() {
this.saving = false;
// 处理关闭弹窗闪动问题
setTimeout(() => {
this.resetForm();
}, 200)
},
resetForm() {
this.form = {
id: "",
@@ -480,168 +464,67 @@ export default {
</script>
<style lang="scss" scoped>
.custom-dialog {
position: relative;
border-radius: 20px;
overflow: hidden;
background: white;
padding-bottom: 17px;
}
@import '@/styles/global.scss';
.custom-dialog .el-dialog__header {
padding: 0;
border-bottom: none;
::v-deep .el-dialog {
margin-top: 6vh !important;
}
.center-dialog {
display: flex;
align-items: center;
justify-content: center;
::v-deep .el-dialog__body {
max-height: 60vh;
overflow-y: auto;
@include scrollbar-style;
}
.model-edit-dialog {
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid #cfcfcf;
background: none;
font-size: 30px;
font-weight: lighter;
color: #cfcfcf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
padding: 0;
outline: none;
}
.section-title {
margin-bottom: 10px;
font-size: 20px;
font-weight: bold;
color: #3d4566;
text-align: left;
}
.custom-close-btn:hover {
color: #409eff;
border-color: #409eff;
}
.switch-group {
display: flex;
align-items: center;
gap: 20px;
}
.custom-select .el-input__suffix {
background: #e6e8ea;
right: 6px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 9px;
}
.switch-item {
display: flex;
align-items: center;
.custom-select .el-input__suffix-inner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
&.hidden {
display: none;
}
.custom-select .el-icon-arrow-up:before {
content: "";
display: inline-block;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid #c0c4cc;
position: relative;
top: -2px;
transform: rotate(180deg);
}
.switch-label {
margin-right: 8px;
}
}
.custom-form .el-form-item {
margin-bottom: 20px;
}
.section-divider {
height: 1px;
background: #e9e9e9;
margin-bottom: 16px;
}
.custom-form .el-form-item__label {
color: #3d4566;
font-weight: normal;
text-align: right;
padding-right: 20px;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 0;
}
.custom-form .el-form-item.prop-remark .el-form-item__label {
margin-top: -4px;
}
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
color: #9c9f9e;
}
.custom-input-bg .el-input__inner,
.custom-input-bg .el-textarea__inner {
background-color: #f6f8fc;
}
.save-btn {
background: #e6f0fd;
color: #237ff4;
border: 1px solid #b3d1ff;
width: 150px;
height: 40px;
font-size: 16px;
transition: all 0.3s ease;
}
.save-btn:hover {
background: linear-gradient(to right, #237ff4, #9c40d5);
color: white;
border: none;
}
.custom-switch .el-switch__core {
border-radius: 20px;
height: 23px;
background-color: #c0ccda;
width: 35px;
padding: 0 20px;
}
.custom-switch .el-switch__core:after {
width: 15px;
height: 15px;
background-color: white;
top: 3px;
left: 4px;
transition: all 0.3s;
}
.custom-switch.is-checked .el-switch__core {
border-color: #b5bcf0;
background-color: #cfd7fa;
padding: 0 20px;
}
.custom-switch.is-checked .el-switch__core:after {
left: 100%;
margin-left: -18px;
background-color: #1b47ee;
}
[style*="display: flex"] {
gap: 20px;
}
.custom-input-bg .el-input__inner {
height: 32px;
}
.custom-form .el-form-item {
margin-bottom: 20px;
}
.custom-form .el-form-item__label {
color: #3d4566;
font-weight: normal;
text-align: right;
padding-right: 20px;
::v-deep .el-input__inner {
height: 32px;
}
::v-deep .el-form-item {
margin-bottom: 10px;
}
}
</style>
+14 -133
View File
@@ -1,16 +1,14 @@
<template>
<el-dialog :title="title" :visible.sync="visible" width="520px" class="param-dialog-wrapper" :append-to-body="true"
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
<CustomDialog
:title="title"
:visible.sync="visible"
width="600px"
class="param-dialog-wrapper"
@confirm="submit"
@close="cancel"
:confirmLoading="saving"
>
<div class="dialog-container">
<div class="dialog-header">
<h2 class="dialog-title">{{ title }}</h2>
<button class="custom-close-btn" @click="cancel">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</div>
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item :label="$t('paramDialog.paramCode')" prop="paramCode" class="form-item">
<el-input v-model="form.paramCode" :placeholder="$t('paramDialog.paramCodePlaceholder')"
@@ -54,20 +52,12 @@
class="custom-textarea"></el-input>
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
{{ $t('paramDialog.save') }}
</el-button>
<el-button @click="cancel" class="cancel-btn">
{{ $t('paramDialog.cancel') }}
</el-button>
</div>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import CustomDialog from './CustomDialog.vue';
export default {
props: {
title: {
@@ -89,6 +79,9 @@ export default {
})
}
},
components: {
CustomDialog
},
data() {
return {
dialogKey: Date.now(),
@@ -210,61 +203,9 @@ export default {
<style scoped lang="scss">
.param-dialog-wrapper {
.dialog-container {
padding: 24px 32px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.dialog-header {
position: relative;
margin-bottom: 24px;
text-align: center;
}
.dialog-title {
font-size: 20px;
color: #1e293b;
margin: 0;
padding: 0;
font-weight: 600;
letter-spacing: 0.5px;
}
.custom-close-btn {
position: absolute;
top: -8px;
right: -8px;
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
color: #ffffff;
background: #ef4444;
transform: rotate(90deg);
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
}
svg {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.param-form {
.form-item {
margin-bottom: 20px;
:deep(.el-form-item__label) {
color: #475569;
font-weight: 500;
@@ -355,65 +296,5 @@ export default {
margin-top: -4px;
}
}
.dialog-footer {
display: flex;
justify-content: center;
padding: 16px 0 0;
margin-top: 16px;
.save-btn {
width: 120px;
height: 42px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #3b82f6;
color: white;
border: none;
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
&:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
}
}
.cancel-btn {
width: 120px;
height: 42px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #ffffff;
color: #64748b;
border: 1px solid #e2e8f0;
margin-left: 16px;
letter-spacing: 0.5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:hover {
background: #f8fafc;
color: #475569;
border-color: #cbd5e1;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
}
}
</style>
@@ -1,56 +1,54 @@
<template>
<el-dialog :visible="visible" :close-on-click-modal="false" @update:visible="handleVisibleChange" width="57%" center custom-class="custom-dialog"
:show-close="false" class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
<div style="font-size: 30px; color: #3d4566; margin-top: -15px; margin-bottom: 20px; text-align: center;">
{{ title }}
</div>
<button class="custom-close-btn" @click="handleClose">×</button>
<el-form :model="form" label-width="auto" :rules="rules" ref="form" class="custom-form">
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
<el-form-item :label="$t('providerDialog.category')" prop="modelType" style="flex: 1;">
<el-select v-model="form.modelType" :placeholder="$t('providerDialog.selectCategory')" class="custom-input-bg" style="width: 100%;">
<CustomDialog
:title="title"
:visible.sync="visible"
width="57%"
class="provider-dialog-wrapper"
@confirm="submit"
@close="handleClose"
:confirmLoading="saving"
>
<div class="dialog-container">
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="provider-form">
<div class="form-row">
<el-form-item :label="$t('providerDialog.category')" prop="modelType" class="form-item">
<el-select v-model="form.modelType" :placeholder="$t('providerDialog.selectCategory')" class="custom-select">
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('providerDialog.code')" prop="providerCode" style="flex: 1;">
<el-input v-model="form.providerCode" :placeholder="$t('providerDialog.inputCode')" class="custom-input-bg"></el-input>
<el-form-item :label="$t('providerDialog.code')" prop="providerCode" class="form-item">
<el-input v-model="form.providerCode" :placeholder="$t('providerDialog.inputCode')" class="custom-input"></el-input>
</el-form-item>
</div>
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
<el-form-item :label="$t('providerDialog.name')" prop="name" style="flex: 1;">
<el-input v-model="form.name" :placeholder="$t('providerDialog.inputName')" class="custom-input-bg"></el-input>
<div class="form-row">
<el-form-item :label="$t('providerDialog.name')" prop="name" class="form-item">
<el-input v-model="form.name" :placeholder="$t('providerDialog.inputName')" class="custom-input"></el-input>
</el-form-item>
<el-form-item :label="$t('providerDialog.sort')" prop="sort" style="flex: 1;">
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
<el-form-item :label="$t('providerDialog.sort')" prop="sort" class="form-item">
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-number"
style="width: 100%;"></el-input-number>
</el-form-item>
</div>
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
{{ $t('providerDialog.fieldConfig') }}
<div style="display: inline-block; float: right;">
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
<div class="field-config-header">
<span class="field-config-title">{{ $t('providerDialog.fieldConfig') }}</span>
<div class="field-config-actions">
<el-button type="primary" @click="addField" size="small" class="btn-add"
:disabled="hasIncompleteFields">
{{ $t('providerDialog.add') }}
</el-button>
<el-button type="primary" @click="toggleSelectAllFields" size="small"
style="background: #5f70f3; border: none; margin-left: 10px;">
<el-button type="primary" @click="toggleSelectAllFields" size="small" class="btn-select-all">
{{ isAllFieldsSelected ? $t('providerDialog.deselectAll') : $t('providerDialog.selectAll') }}
</el-button>
<el-button type="danger" @click="batchRemoveFields" size="small"
style="background: red; border: none; margin-left: 10px;">
<el-button type="danger" @click="batchRemoveFields" size="small" class="btn-batch-delete">
{{ $t('providerDialog.batchDelete') }}
</el-button>
</div>
</div>
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
<div class="divider"></div>
<div class="fields-container">
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
@@ -122,26 +120,27 @@
</div>
</el-form>
</div>
<div style="display: flex; justify-content: center;">
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">{{ $t('providerDialog.save') }}</el-button>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import CustomDialog from './CustomDialog.vue';
export default {
name: 'ProviderDialog',
props: {
title: String,
visible: Boolean,
form: Object,
modelTypes: Array
},
components: {
CustomDialog
},
data() {
return {
saving: false,
isAllFieldsSelected: false,
tableKey: 0 // 用于强制表格重新渲染
tableKey: 0
};
},
computed: {
@@ -199,16 +198,8 @@ export default {
}));
},
handleVisibleChange(val) {
this.$emit('update:visible', val);
if (!val) {
this.resetForm();
}
},
handleClose() {
this.resetForm();
this.$emit('update:visible', false);
this.$emit('cancel');
},
@@ -288,7 +279,7 @@ export default {
},
forceTableRerender() {
this.tableKey += 1; // 改变key值强制表格重新渲染
this.tableKey += 1;
},
submit() {
@@ -331,8 +322,7 @@ export default {
}
this.isAllFieldsSelected = false;
this.forceTableRerender();
},
}
},
watch: {
visible(val) {
@@ -344,97 +334,40 @@ export default {
};
</script>
<style lang="scss" scoped>
::v-deep .custom-dialog.el-dialog {
margin-top: 0 !important;
border-radius: 20px !important;
}
::v-deep .custom-dialog .el-dialog__header {
padding: 0;
border-bottom: none;
}
.custom-close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 35px;
height: 35px;
border-radius: 50%;
border: 2px solid #cfcfcf;
background: none;
font-size: 30px;
font-weight: lighter;
color: #cfcfcf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
padding: 0;
outline: none;
}
.custom-close-btn:hover {
color: #409EFF;
border-color: #409EFF;
}
.custom-form .el-form-item {
margin-bottom: 20px;
}
.custom-form .el-form-item__label {
color: #3d4566;
font-weight: normal;
text-align: right;
padding-right: 20px;
}
.custom-input-bg .el-input__inner {
background-color: #f6f8fc;
height: 32px;
}
.custom-input-bg .el-input__inner::-webkit-input-placeholder {
color: #9c9f9e;
}
.fields-container {
margin-bottom: 20px;
}
.save-btn {
background: #e6f0fd;
color: #237ff4;
border: 1px solid #b3d1ff;
width: 150px;
height: 40px;
font-size: 16px;
transition: all 0.3s ease;
}
.save-btn:hover {
background: linear-gradient(to right, #237ff4, #9c40d5);
color: white;
border: none;
}
.el-table {
border-radius: 4px;
}
.el-table::before {
display: none;
}
.el-table th,
.el-table td {
padding: 8px 0;
}
.el-button.is-circle {
border-radius: 2px;
<style scoped lang="scss">
.provider-dialog-wrapper {
.provider-form {
.form-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.form-item {
flex: 1;
margin-bottom: 0;
}
.custom-select {
width: 100%;
}
.custom-input-number {
width: 100%;
}
}
.field-config-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
.field-config-title {
font-size: 16px;
font-weight: 600;
color: #3d4566;
}
}
.divider {
height: 2px;
background: #e9e9e9;
margin-bottom: 22px;
}
}
</style>
@@ -1,76 +1,67 @@
<template>
<el-dialog :title="title" :visible.sync="visible" width="700px" class="param-dialog-wrapper" :append-to-body="true"
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
<div class="dialog-container">
<div class="dialog-header">
<h2 class="dialog-title">{{ title }}</h2>
<button class="custom-close-btn" @click="cancel">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</div>
<CustomDialog
:title="title"
:visible.sync="visible"
width="700px"
class="param-dialog-wrapper"
@confirm="submit"
@close="cancel"
:confirmLoading="saving"
>
<el-form :model="localForm" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item :label="$t('replacementDialog.fileName')" prop="fileName">
<el-input v-model="localForm.fileName" :placeholder="$t('replacementDialog.fileNamePlaceholder')"
@input="clearFieldError('fileName')"></el-input>
</el-form-item>
<el-form :model="localForm" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item :label="$t('replacementDialog.fileName')" prop="fileName" class="form-item">
<el-input v-model="localForm.fileName" :placeholder="$t('replacementDialog.fileNamePlaceholder')"
class="custom-input" @input="clearFieldError('fileName')"></el-input>
</el-form-item>
<el-form-item :label="$t('replacementDialog.content')" prop="content" class="form-item content-item">
<div class="content-wrapper">
<el-input
type="textarea"
v-model="localForm.content"
:placeholder="$t('replacementDialog.contentPlaceholder')"
:rows="8"
class="custom-textarea"
@input="clearFieldError('content')"
></el-input>
<p class="format-tip">{{ $t('replacementDialog.formatTip') }}</p>
<div class="upload-section">
<el-upload
class="upload-btn"
action=""
:auto-upload="false"
:show-file-list="false"
accept=".txt"
:on-change="handleFileChange"
>
<el-button size="small" type="primary" class="upload-file-btn">
<div class="upload-file-content">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
<path d="M7 1V13M1 7H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<div>
<p>{{ $t('replacementDialog.clickUploadTip') }}</p>
<p>{{ $t('replacementDialog.uploadCoverTip') }}</p>
</div>
<el-form-item :label="$t('replacementDialog.content')" prop="content" class="content-item">
<div class="content-wrapper">
<el-input
type="textarea"
v-model="localForm.content"
:placeholder="$t('replacementDialog.contentPlaceholder')"
:rows="8"
@input="clearFieldError('content')"
></el-input>
<p class="format-tip">{{ $t('replacementDialog.formatTip') }}</p>
<div class="upload-section">
<el-upload
class="upload-btn"
action=""
:auto-upload="false"
:show-file-list="false"
accept=".txt"
:on-change="handleFileChange"
>
<el-button size="small" type="primary" class="upload-file-btn">
<div class="upload-file-content">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
<path d="M7 1V13M1 7H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<div>
<p>{{ $t('replacementDialog.clickUploadTip') }}</p>
<p>{{ $t('replacementDialog.uploadCoverTip') }}</p>
</div>
</el-button>
</el-upload>
<span class="word-count" :class="{ 'over-limit': isOverLimit }">
{{ wordCountText }}{{ $t('replacementDialog.wordCountUnit') }}
</span>
</div>
</div>
</el-button>
</el-upload>
<span class="word-count" :class="{ 'over-limit': isOverLimit }">
{{ wordCountText }}{{ $t('replacementDialog.wordCountUnit') }}
</span>
</div>
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
{{ $t('replacementDialog.save') }}
</el-button>
<el-button @click="cancel" class="cancel-btn">
{{ $t('replacementDialog.cancel') }}
</el-button>
</div>
</div>
</el-dialog>
</div>
</el-form-item>
</el-form>
</CustomDialog>
</template>
<script>
import CustomDialog from './CustomDialog.vue';
export default {
components: {
CustomDialog
},
props: {
title: {
type: String,
@@ -270,155 +261,21 @@ export default {
};
</script>
<style>
.custom-param-dialog {
border-radius: 16px !important;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
border: none !important;
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0 !important;
border-radius: 16px;
}
}
</style>
<style scoped lang="scss">
.param-dialog-wrapper {
.dialog-container {
padding: 24px 32px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.dialog-header {
position: relative;
margin-bottom: 24px;
text-align: center;
}
.dialog-title {
font-size: 20px;
color: #1e293b;
margin: 0;
padding: 0;
font-weight: 600;
letter-spacing: 0.5px;
}
.custom-close-btn {
position: absolute;
top: -8px;
right: -8px;
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
color: #ffffff;
background: #ef4444;
transform: rotate(90deg);
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
}
svg {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.param-form {
.form-item {
margin-bottom: 20px;
:deep(.el-form-item__label) {
color: #475569;
font-weight: 500;
padding-right: 12px;
text-align: right;
font-size: 14px;
letter-spacing: 0.2px;
}
}
.content-item {
:deep(.el-form-item__content) {
line-height: 1;
}
}
.custom-input {
:deep(.el-input__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
height: 42px;
padding: 0 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.content-wrapper {
width: 100%;
}
.custom-textarea {
:deep(.el-textarea__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
padding: 12px 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
line-height: 1.8;
resize: none;
white-space: pre;
overflow-x: auto;
word-wrap: normal;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.format-tip {
text-align: left;
font-size: 12px;
color: #5778ff;
}
@@ -472,65 +329,5 @@ export default {
}
}
}
.dialog-footer {
display: flex;
justify-content: center;
padding: 16px 0 0;
margin-top: 16px;
.save-btn {
width: 120px;
height: 42px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #3b82f6;
color: white;
border: none;
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
&:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
}
}
.cancel-btn {
width: 120px;
height: 42px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #ffffff;
color: #64748b;
border: 1px solid #e2e8f0;
margin-left: 16px;
letter-spacing: 0.5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:hover {
background: #f8fafc;
color: #475569;
border-color: #cbd5e1;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
}
}
}
</style>
+16 -61
View File
@@ -1,9 +1,7 @@
<template>
<el-dialog :visible.sync="localVisible" width="90%" @close="handleClose" :show-close="false" :append-to-body="true"
:close-on-click-modal="true">
<button class="custom-close-btn" @click="handleClose">
×
</button>
<CustomDialog :visible.sync="localVisible" :title="$t('modelConfig.voiceManagement')" width="90%"
:close-on-click-modal="true" :destroy-on-close="false" :footer="false" :append-to-body="true"
@close="handleClose">
<div class="scroll-wrapper">
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
<el-table v-loading="loading" :data="filteredTtsModels" style="width: 100%;" class="data-table"
@@ -92,25 +90,27 @@
</div>
</div>
<div class="action-buttons">
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
<CustomButton :icon="selectAll ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" type="default" @click="toggleSelectAll">
{{ selectAll ? $t('ttsModel.deselectAll') : $t('ttsModel.selectAll') }}
</el-button>
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
</CustomButton>
<CustomButton icon="el-icon-plus" size="small" type="add" @click="addNew">
{{ $t('ttsModel.add') }}
</el-button>
<el-button type="primary" size="mini" @click="deleteRow(filteredTtsModels.filter(row => row.selected))"
style="background: red;border:None">{{ $t('ttsModel.delete') }}
</el-button>
</CustomButton>
<CustomButton icon="el-icon-delete" size="small" type="delete" @click="deleteRow(filteredTtsModels.filter(row => row.selected))">
{{ $t('ttsModel.delete') }}
</CustomButton>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import Api from "@/apis/api";
import AudioPlayer from './AudioPlayer.vue';
import CustomDialog from './CustomDialog.vue';
import CustomButton from './CustomButton.vue';
export default {
components: { AudioPlayer },
components: { AudioPlayer, CustomDialog, CustomButton },
props: {
visible: {
type: Boolean,
@@ -532,18 +532,6 @@ export default {
</script>
<style lang="scss" scoped>
::v-deep .el-dialog {
border-radius: 8px !important;
overflow: hidden;
top: 1vh !important;
}
::v-deep .el-dialog__header {
display: none !important;
padding: 0 !important;
margin: 0 !important;
}
/* 表格样式 */
::v-deep .data-table .el-table__header th {
color: black;
@@ -570,33 +558,6 @@ export default {
border: none !important;
}
/* 关闭按钮 */
.custom-close-btn {
position: absolute;
top: 15px;
right: 15px;
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #cfcfcf;
background: none;
font-size: 30px;
font-weight: lighter;
color: #cfcfcf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
padding: 0;
outline: none;
}
.custom-close-btn:hover {
color: #409EFF;
border-color: #409EFF;
}
/* 备注文本 */
::v-deep .remark-input .el-textarea__inner {
border-radius: 4px;
@@ -691,11 +652,6 @@ export default {
margin: 0 auto;
}
.action-buttons .el-button {
padding: 8px 15px;
font-size: 11px;
}
.edit-btn,
.delete-btn,
.save-btn {
@@ -727,9 +683,8 @@ export default {
/* 按钮组定位调整 */
.action-buttons {
position: static;
padding: 15px 0;
background: white;
padding-top: 10px;
text-align: left;
}
/* 输入框自适应 */
@@ -1,6 +1,11 @@
<template>
<el-dialog :title="$t('voiceClone.dialogTitle')" :visible.sync="visible" width="900px" top="10vh"
:before-close="handleClose" class="voice-clone-dialog">
<CustomDialog
:title="$t('voiceClone.dialogTitle')"
:visible.sync="innerVisible"
width="900px"
:footer="false"
@close="handleDialogClose"
>
<div class="dialog-content">
<!-- 步骤指示器 -->
<div class="steps-header">
@@ -73,22 +78,28 @@
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{ currentStep === 1 ? $t('voiceClone.cancel') : $t('voiceClone.prevStep')
}}</el-button>
<el-button type="primary" @click="handleNext" :loading="uploading">
<div class="custom-footer">
<CustomButton @click="handleCancel">
{{ currentStep === 1 ? $t('voiceClone.cancel') : $t('voiceClone.prevStep') }}
</CustomButton>
<CustomButton type="confirm" :loading="uploading" @click="handleNext">
{{ currentStep === 1 ? $t('voiceClone.nextStep') : $t('voiceClone.upload') }}
</el-button>
</span>
</el-dialog>
</CustomButton>
</div>
</CustomDialog>
</template>
<script>
import Api from "@/apis/api";
import CustomDialog from './CustomDialog.vue';
import CustomButton from './CustomButton.vue';
export default {
name: 'VoiceCloneDialog',
components: {
CustomDialog,
CustomButton
},
props: {
visible: {
type: Boolean,
@@ -122,6 +133,14 @@ export default {
};
},
computed: {
innerVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
},
selectedDuration() {
if (this.selectionStart !== null && this.selectionEnd !== null && this.audioBuffer) {
const duration = this.audioBuffer.duration;
@@ -146,14 +165,16 @@ export default {
}
},
methods: {
handleClose() {
handleCancel() {
if (this.currentStep === 2) {
this.currentStep = 1;
} else {
this.resetDialog();
this.$emit('update:visible', false);
}
},
handleDialogClose() {
this.resetDialog();
},
resetDialog() {
this.currentStep = 1;
this.audioFile = null;
@@ -509,13 +530,6 @@ export default {
</script>
<style lang="scss" scoped>
.voice-clone-dialog {
::v-deep .el-dialog__body {
padding: 20px 30px;
}
}
.steps-header {
display: flex;
justify-content: center;
@@ -578,7 +592,6 @@ export default {
background: #e4e7ed;
margin-left: 12px;
}
.step-content {
padding: 20px 0;
}
@@ -685,10 +698,7 @@ export default {
gap: 12px;
margin-top: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
.custom-footer {
text-align: right;
}
</style>
@@ -1,55 +1,43 @@
<template>
<el-dialog :title="title" :visible.sync="visible" width="520px" class="param-dialog-wrapper" :append-to-body="true"
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
<div class="dialog-container">
<div class="dialog-header">
<h2 class="dialog-title">{{ title }}</h2>
<button class="custom-close-btn" @click="cancel">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</div>
<CustomDialog
:title="title"
:visible.sync="innerVisible"
:confirm-loading="saving"
width="600px"
@confirm="submit"
@cancel="cancel"
>
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item :label="$t('voicePrintDialog.voicePrintVector')" prop="audioId" class="form-item">
<el-select v-model="form.audioId" :placeholder="$t('voicePrintDialog.selectVoiceMessage')" class="custom-select">
<el-option v-for="item in valueTypeOptions" :key="item.audioId" :label="item.content" :value="item.audioId">
<span style="float: left">{{ item.content }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">
<i :class="getAudioIconClass(item.audioId)" @click.stop="playAudio(item.audioId)"
class="audio-icon"></i>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
<el-form-item :label="$t('voicePrintDialog.voicePrintVector')" prop="audioId" class="form-item">
<el-select v-model="form.audioId" :placeholder="$t('voicePrintDialog.selectVoiceMessage')" class="custom-select">
<el-option v-for="item in valueTypeOptions" :key="item.audioId" :label="item.content" :value="item.audioId">
<span style="float: left">{{ item.content }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">
<i :class="getAudioIconClass(item.audioId)" @click.stop="playAudio(item.audioId)"
class="audio-icon"></i>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('voicePrintDialog.name')" prop="sourceName" class="form-item">
<el-input v-model="form.sourceName" :placeholder="$t('voicePrintDialog.enterName')" class="custom-input"></el-input>
</el-form-item>
<el-form-item :label="$t('voicePrintDialog.name')" prop="sourceName" class="form-item">
<el-input v-model="form.sourceName" :placeholder="$t('voicePrintDialog.enterName')" class="custom-input"></el-input>
</el-form-item>
<el-form-item :label="$t('voicePrintDialog.description')" prop="introduce" class="form-item remark-item">
<el-input type="textarea" v-model="form.introduce" :placeholder="$t('voicePrintDialog.enterDescription')" :rows="3" class="custom-textarea"
maxlength="100" show-word-limit></el-input>
</el-form-item>
</el-form>
<div class="dialog-footer">
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
{{ $t('voicePrintDialog.save') }}
</el-button>
<el-button @click="cancel" class="cancel-btn">
{{ $t('voicePrintDialog.cancel') }}
</el-button>
</div>
</div>
</el-dialog>
<el-form-item :label="$t('voicePrintDialog.description')" prop="introduce" class="form-item remark-item">
<el-input type="textarea" v-model="form.introduce" :placeholder="$t('voicePrintDialog.enterDescription')" :rows="3" class="custom-textarea"
maxlength="100" show-word-limit></el-input>
</el-form-item>
</el-form>
</CustomDialog>
</template>
<script>
import api from '@/apis/api';
import CustomDialog from './CustomDialog.vue';
export default {
components: { CustomDialog },
props: {
title: {
type: String,
@@ -74,7 +62,6 @@ export default {
},
data() {
return {
dialogKey: Date.now(),
saving: false,
playingAudioId: null,
audioElement: null,
@@ -94,6 +81,16 @@ export default {
}
};
},
computed: {
innerVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
}
},
methods: {
getAudioIconClass(audioId) {
if (this.playingAudioId === audioId) {
@@ -137,11 +134,11 @@ export default {
submit() {
this.$refs.form.validate((valid) => {
if (valid) {
this.saving = true; // 开始加载
this.saving = true;
this.$emit('submit', {
form: this.form,
done: () => {
this.saving = false; // 加载完成
this.saving = false;
}
});
@@ -152,14 +149,14 @@ export default {
});
},
cancel() {
this.saving = false; // 取消时重置状态
this.dialogKey = Date.now();
this.saving = false;
this.$emit('cancel');
}
},
watch: {
visible(newVal) {
if (newVal) {
this.saving = false;
api.agent.getRecentlyFiftyByAgentId(this.agentId, ((data) => {
this.valueTypeOptions = data.data.data.map(item => ({
...item
@@ -184,24 +181,6 @@ export default {
};
</script>
<style>
.custom-param-dialog {
border-radius: 16px !important;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
border: none !important;
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0 !important;
border-radius: 16px;
}
}
</style>
<style scoped lang="scss">
.audio-icon {
font-size: 20px;
@@ -210,211 +189,98 @@ export default {
color: #1890ff;
}
.param-dialog-wrapper {
.dialog-container {
padding: 24px 32px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.param-form {
.form-item {
margin-bottom: 20px;
.dialog-header {
position: relative;
margin-bottom: 24px;
text-align: center;
}
.dialog-title {
font-size: 20px;
color: #1e293b;
margin: 0;
padding: 0;
font-weight: 600;
letter-spacing: 0.5px;
}
.custom-close-btn {
position: absolute;
top: -8px;
right: -8px;
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
color: #ffffff;
background: #ef4444;
transform: rotate(90deg);
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
}
svg {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
.param-form {
.form-item {
margin-bottom: 20px;
:deep(.el-form-item__label) {
color: #475569;
font-weight: 500;
padding-right: 12px;
text-align: right;
font-size: 14px;
letter-spacing: 0.2px;
}
}
.custom-input {
:deep(.el-input__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
height: 42px;
padding: 0 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.custom-select {
width: 100%;
:deep(.el-input__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
height: 42px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.custom-textarea {
:deep(.el-textarea__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
padding: 12px 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
line-height: 1.5;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.remark-item :deep(.el-form-item__label) {
margin-top: -4px;
}
}
.dialog-footer {
display: flex;
justify-content: center;
padding: 16px 0 0;
margin-top: 16px;
.save-btn {
width: 120px;
height: 42px;
font-size: 14px;
:deep(.el-form-item__label) {
color: #475569;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #3b82f6;
color: white;
border: none;
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
&:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
}
}
.cancel-btn {
width: 120px;
height: 42px;
padding-right: 12px;
text-align: right;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.2px;
}
}
.custom-input {
:deep(.el-input__inner) {
background-color: #ffffff;
border-radius: 8px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #ffffff;
color: #64748b;
border: 1px solid #e2e8f0;
margin-left: 16px;
letter-spacing: 0.5px;
height: 42px;
padding: 0 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:hover {
background: #f8fafc;
color: #475569;
border-color: #cbd5e1;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.custom-select {
width: 100%;
:deep(.el-input__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
height: 42px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.custom-textarea {
:deep(.el-textarea__inner) {
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #e2e8f0;
padding: 12px 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
color: #334155;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
line-height: 1.5;
&:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
background-color: #ffffff;
}
&::placeholder {
color: #94a3b8;
font-weight: 400;
}
}
}
.remark-item :deep(.el-form-item__label) {
margin-top: -4px;
}
}
</style>
@@ -1,7 +1,15 @@
<template>
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
@open="handleOpen" width="500px">
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<CustomDialog
:title="title"
:visible.sync="dialogVisible"
:close-on-click-modal="false"
width="600px"
@close="handleClose"
@open="handleOpen"
@cancel="handleCancel"
@confirm="handleSubmit"
>
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
<el-form-item :label="$t('voiceClone.platformName')" prop="modelId">
<el-select v-model="form.modelId" :placeholder="$t('voiceClone.platformNamePlaceholder')" filterable
style="width: 100%" @change="handlePlatformChange">
@@ -31,19 +39,16 @@
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleCancel">{{ $t('voiceClone.operationCancelled') }}</el-button>
<el-button type="primary" @click="handleSubmit">{{ $t('voiceClone.addNew') }}</el-button>
</div>
</el-dialog>
</CustomDialog>
</template>
<script>
import Api from '@/apis/api';
import CustomDialog from './CustomDialog.vue';
export default {
name: 'VoiceCloneDialog',
components: { CustomDialog },
props: {
visible: {
type: Boolean,
@@ -154,9 +159,3 @@ export default {
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-dialog {
border-radius: 20px;
}
</style>
+32 -1
View File
@@ -352,6 +352,8 @@ export default {
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Ersetzungswortverwaltung',
'replacementWordManagement.searchPlaceholder': 'Bitte geben Sie den Dateinamen zur Suche ein',
'replacementWordManagement.search': 'Suchen',
'replacementWordManagement.fileName': 'Dateiname',
'replacementWordManagement.replacementWordCount': 'Anzahl der Ersetzungswörter',
'replacementWordManagement.replacementWordContent': 'Ersetzungswortinhalt',
@@ -591,6 +593,7 @@ export default {
// Dictionary management page text
'dictManagement.pageTitle': 'Wörterbuchverwaltung',
'dictManagement.dictTypeCategory': 'Wörterbuchkategorie',
'dictManagement.searchPlaceholder': 'Bitte Wörterbuchwert-Label zur Suche eingeben',
'dictManagement.search': 'Suchen',
'dictManagement.dictTypeName': 'WDT-Name',
@@ -645,6 +648,8 @@ export default {
'user.disable': 'Deaktivieren',
'user.delete': 'Löschen',
'user.selectUsersFirst': 'Bitte wählen Sie zuerst Benutzer zum Löschen aus',
'user.selectUsersToDisable': 'Bitte wählen Sie Benutzer zum Deaktivieren aus',
'user.selectUsersToEnable': 'Bitte wählen Sie Benutzer zum Aktivieren aus',
'user.confirmDeleteSelected': 'Sind Sie sicher, dass Sie {count} ausgewählte Benutzer löschen möchten?',
'user.deleting': 'Löscht...',
'user.deleteSuccess': 'Erfolgreich {count} Benutzer gelöscht',
@@ -704,6 +709,9 @@ export default {
'common.cancel': 'Abbrechen',
'common.sensitive': 'Sensibel',
'common.loading': 'Lädt',
'common.copySuccess': 'Erfolgreich kopiert',
'common.copyFailed': 'Kopieren fehlgeschlagen',
'common.copyAddress': 'Adresse kopieren',
// Language switch
'language.zhCN': '中文简体',
@@ -1116,6 +1124,8 @@ export default {
'agentTemplateManagement.action': 'Aktion',
'agentTemplateManagement.createTemplate': 'Vorlage erstellen',
'templateQuickConfig.newTemplate': 'Neue Vorlage',
'templateQuickConfig.editTemplate': 'Vorlage bearbeiten',
'templateQuickConfig.addTemplate': 'Vorlage hinzufügen',
'agentTemplateManagement.editTemplate': 'Vorlage bearbeiten',
'agentTemplateManagement.deleteTemplate': 'Vorlage löschen',
'agentTemplateManagement.deleteSuccess': 'Vorlage erfolgreich gelöscht',
@@ -1195,6 +1205,8 @@ export default {
'voiceClone.operationClosed': 'Popup geschlossen',
'voiceClone.cloneSuccess': 'Klonen erfolgreich',
'voiceClone.cloneFailed': 'Klonen fehlgeschlagen',
'voiceClone.cloneErrorTip': 'Klonen fehlgeschlagen, bitte bewegen Sie die Maus über die Fehlermeldung für Details',
'voiceClone.apiError': 'API-Aufruf Fehler',
'voiceClone.confirmClone': 'Sind Sie sicher, dass Sie diese Stimme klonen möchten?',
'voiceClone.onlySuccessCanClone': 'Nur erfolgreich trainierte Stimmen können geklont werden',
'common.insufficient': 'Unzureichend',
@@ -1212,6 +1224,7 @@ export default {
'voiceClone.training': 'Training',
'voiceClone.trainSuccess': 'Training erfolgreich',
'voiceClone.trainFailed': 'Training fehlgeschlagen',
'voiceClone.trainFailedWithError': 'Training fehlgeschlagen: {error}',
'voiceClone.itemsPerPage': '{items} Einträge/Seite',
'voiceClone.firstPage': 'Erste Seite',
'voiceClone.prevPage': 'Vorherige Seite',
@@ -1351,6 +1364,7 @@ export default {
'knowledgeFileUpload.confirmParse': 'Sind Sie sicher, dass Sie dieses Dokument parsen möchten?',
'knowledgeFileUpload.nameRequired': 'Bitte Dokumentnamen eingeben',
'knowledgeFileUpload.fileRequired': 'Bitte wählen Sie eine Datei zum Hochladen aus',
'knowledgeFileUpload.fileSizeExceeded': 'Dateigröße darf 10MB nicht überschreiten!',
'knowledgeFileUpload.getListFailed': 'Dokumentenliste konnte nicht abgerufen werden',
'knowledgeFileUpload.parseCancelled': 'Parsen abgebrochen',
'knowledgeFileUpload.deleteCancelled': 'Löschen abgebrochen',
@@ -1384,8 +1398,20 @@ export default {
'knowledgeFileUpload.comprehensiveSimilarity': 'Umfassende Ähnlichkeit',
'knowledgeFileUpload.content': 'Inhalt:',
'knowledgeFileUpload.testQuestionRequired': 'Bitte Testfrage eingeben',
'knowledgeFileUpload.sourceDocument': 'Quelldokument',
'knowledgeFileUpload.parsing': 'Analysiere...',
'knowledgeBaseDialog.descriptionRequired': 'Bitte Wissensbasis-Beschreibung eingeben',
// Knowledge Base Management page text
'knowledgeBaseManagement.loading': 'Laden...',
'knowledgeBaseManagement.noData': 'Keine Daten',
'knowledgeBaseManagement.switchKnowledgeBase': 'Wissensbasis wechseln',
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Aktuelle Wissensbasis-Dokumentassets',
'knowledgeBaseManagement.enabled': 'Aktiviert',
'knowledgeBaseManagement.disabled': 'Deaktiviert',
'knowledgeBaseManagement.totalDocuments': 'Gesamt {total} Dokumente',
'knowledgeBaseManagement.getListFailed': 'Liste konnte nicht abgerufen werden',
// Feature Management page text
'featureManagement.selectAll': 'Alle auswählen',
'featureManagement.deselectAll': 'Alle abwählen',
@@ -1401,6 +1427,12 @@ export default {
'featureManagement.cancel': 'Abbrechen',
'featureManagement.resetSuccess': 'Funktionskonfiguration erfolgreich zurückgesetzt',
'featureManagement.noChanges': 'Keine Änderungen zum Speichern',
'featureManagement.saving': 'Speichern...',
'featureManagement.groupName.featureManagement': 'Funktionsmodul-Schalter',
'featureManagement.groupName.voiceManagement': 'Agent-Sichtbarkeitskonfiguration',
'featureManagement.groupDescription.featureManagement': 'Aktivierungsstatus der Kernfunktionsmodule steuern',
'featureManagement.groupDescription.voiceManagement': 'Konfigurieren, welche Funktionen für Agenten (KI-Assistenten) sichtbar und verfügbar sind',
'featureManagement.moduleCount': '{count} Module insgesamt',
// Feature names and descriptions
'feature.voiceprintRecognition.name': 'Stimmerkennung',
@@ -1421,7 +1453,6 @@ export default {
// Adressbuch Verwaltungsseite
'addressBookManagement.title': 'Adressbuchverwaltung',
'addressBookManagement.searchPlaceholder': 'Gerätename oder MAC-Adresse suchen',
'addressBookManagement.search': 'Suchen',
'addressBookManagement.selectAgent': 'Bitte Agent auswählen',
'addressBookManagement.name': 'Adressbuchname',
'addressBookManagement.description': 'Adressbuchbeschreibung',
+32 -1
View File
@@ -352,6 +352,8 @@ export default {
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Replacement Word Management',
'replacementWordManagement.searchPlaceholder': 'Please enter file name to search',
'replacementWordManagement.search': 'Search',
'replacementWordManagement.fileName': 'File Name',
'replacementWordManagement.replacementWordCount': 'Replacement Word Count',
'replacementWordManagement.replacementWordContent': 'Replacement Word Content',
@@ -591,6 +593,7 @@ export default {
// Dictionary management page text
'dictManagement.pageTitle': 'Dictionary Management',
'dictManagement.dictTypeCategory': 'Dict Category',
'dictManagement.searchPlaceholder': 'Please enter dict value label to search',
'dictManagement.search': 'Search',
'dictManagement.dictTypeName': 'Dict Type Name',
@@ -645,6 +648,8 @@ export default {
'user.disable': 'Disable',
'user.delete': 'Delete',
'user.selectUsersFirst': 'Please select users to delete first',
'user.selectUsersToDisable': 'Please select users to disable',
'user.selectUsersToEnable': 'Please select users to enable',
'user.confirmDeleteSelected': 'Are you sure you want to delete {count} selected users?',
'user.deleting': 'Deleting...',
'user.deleteSuccess': 'Successfully deleted {count} users',
@@ -704,6 +709,9 @@ export default {
'common.cancel': 'Cancel',
'common.sensitive': 'Sensitive',
'common.loading': 'Loading',
'common.copySuccess': 'Copy successful',
'common.copyFailed': 'Copy failed',
'common.copyAddress': 'Copy address',
// Language switch
'language.zhCN': '中文简体',
@@ -1150,6 +1158,8 @@ export default {
'templateQuickConfig.cancel': 'Cancel',
'templateQuickConfig.templateNotFound': 'Template not found',
'templateQuickConfig.newTemplate': 'New Template',
'templateQuickConfig.editTemplate': 'Edit Template',
'templateQuickConfig.addTemplate': 'Add Template',
'warning': 'Warning',
'info': 'Info',
'common.networkError': 'Network request failed',
@@ -1195,6 +1205,8 @@ export default {
'voiceClone.operationClosed': 'Popup closed',
'voiceClone.cloneSuccess': 'Clone successful',
'voiceClone.cloneFailed': 'Clone failed',
'voiceClone.cloneErrorTip': 'Clone failed, please hover over the error tip to view error details',
'voiceClone.apiError': 'API call error',
'voiceClone.confirmClone': 'Are you sure you want to clone this voice?',
'voiceClone.onlySuccessCanClone': 'Only successfully trained voices can be cloned',
'common.insufficient': 'Insufficient',
@@ -1212,6 +1224,7 @@ export default {
'voiceClone.training': 'Training',
'voiceClone.trainSuccess': 'Training successful',
'voiceClone.trainFailed': 'Training failed',
'voiceClone.trainFailedWithError': 'Training failed: {error}',
'voiceClone.itemsPerPage': '{items} items/page',
'voiceClone.firstPage': 'First Page',
'voiceClone.prevPage': 'Previous Page',
@@ -1351,6 +1364,7 @@ export default {
'knowledgeFileUpload.confirmParse': 'Are you sure you want to parse this document?',
'knowledgeFileUpload.nameRequired': 'Please enter document name',
'knowledgeFileUpload.fileRequired': 'Please select a file to upload',
'knowledgeFileUpload.fileSizeExceeded': 'File size cannot exceed 10MB!',
'knowledgeFileUpload.getListFailed': 'Failed to get document list',
'knowledgeFileUpload.parseCancelled': 'Parse cancelled',
'knowledgeFileUpload.deleteCancelled': 'Delete cancelled',
@@ -1382,10 +1396,22 @@ export default {
'knowledgeFileUpload.testResult': 'Test Result:',
'knowledgeFileUpload.noRelatedSlices': 'No related slices found',
'knowledgeFileUpload.comprehensiveSimilarity': 'Comprehensive Similarity',
'knowledgeFileUpload.sourceDocument': 'Source Document',
'knowledgeFileUpload.content': 'Content:',
'knowledgeFileUpload.testQuestionRequired': 'Please enter test question',
'knowledgeFileUpload.parsing': 'Parsing...',
'knowledgeBaseDialog.descriptionRequired': 'Please enter knowledge base description',
// Knowledge Base Management page text
'knowledgeBaseManagement.loading': 'Loading...',
'knowledgeBaseManagement.noData': 'No data',
'knowledgeBaseManagement.switchKnowledgeBase': 'Switch Knowledge Base',
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Current Knowledge Base Document Assets',
'knowledgeBaseManagement.enabled': 'Enabled',
'knowledgeBaseManagement.disabled': 'Disabled',
'knowledgeBaseManagement.totalDocuments': 'Total {total} documents',
'knowledgeBaseManagement.getListFailed': 'Failed to get list',
// Feature Management page text
'featureManagement.selectAll': 'Select All',
'featureManagement.deselectAll': 'Deselect All',
@@ -1401,6 +1427,12 @@ export default {
'featureManagement.cancel': 'Cancel',
'featureManagement.resetSuccess': 'Feature configuration reset successfully',
'featureManagement.noChanges': 'No changes to save',
'featureManagement.saving': 'Saving...',
'featureManagement.groupName.featureManagement': 'Feature Module Switch',
'featureManagement.groupName.voiceManagement': 'Agent Visible Configuration',
'featureManagement.groupDescription.featureManagement': 'Control the enable status of core feature modules',
'featureManagement.groupDescription.voiceManagement': 'Configure which features are visible and available to agents (AI assistants)',
'featureManagement.moduleCount': '{count} modules in total',
// Feature names and descriptions
'feature.voiceprintRecognition.name': 'Voiceprint Recognition',
@@ -1421,7 +1453,6 @@ export default {
// Address Book Management page
'addressBookManagement.title': 'Address Book Management',
'addressBookManagement.searchPlaceholder': 'Search device name or MAC address',
'addressBookManagement.search': 'Search',
'addressBookManagement.selectAgent': 'Please select agent',
'addressBookManagement.name': 'Address Book Name',
'addressBookManagement.description': 'Address Book Description',
+14 -6
View File
@@ -7,6 +7,14 @@ import de from './de';
import vi from './vi';
import ptBR from './pt_BR';
import enLocale from 'element-ui/lib/locale/lang/en'
import zhLocale from 'element-ui/lib/locale/lang/zh-CN'
import twLocale from 'element-ui/lib/locale/lang/zh-TW'
import deLocale from 'element-ui/lib/locale/lang/de'
import viLocale from 'element-ui/lib/locale/lang/vi'
import ptBRLocale from 'element-ui/lib/locale/lang/pt-br'
Vue.use(VueI18n);
// 从本地存储获取语言设置,如果没有则使用浏览器语言或默认语言
@@ -38,12 +46,12 @@ const i18n = new VueI18n({
locale: getDefaultLanguage(),
fallbackLocale: 'zh_CN',
messages: {
'zh_CN': zhCN,
'zh_TW': zhTW,
'en': en,
'de': de,
'vi': vi,
'pt_BR': ptBR
'zh_CN': { ...zhLocale, ...zhCN },
'zh_TW': { ...twLocale, ...zhTW },
'en': { ...en, ...enLocale },
'de': { ...de, ...deLocale },
'vi': { ...vi, ...viLocale },
'pt_BR': { ...ptBR, ...ptBRLocale }
}
});
+33 -2
View File
@@ -352,6 +352,8 @@ export default {
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Gerenciamento de Palavras de Substituição',
'replacementWordManagement.searchPlaceholder': 'Digite o nome do arquivo para pesquisar',
'replacementWordManagement.search': 'Pesquisar',
'replacementWordManagement.fileName': 'Nome do Arquivo',
'replacementWordManagement.replacementWordCount': 'Quantidade de Palavras de Substituição',
'replacementWordManagement.replacementWordContent': 'Conteúdo Substituto',
@@ -591,6 +593,7 @@ export default {
// Página de gerenciamento de dicionário
'dictManagement.pageTitle': 'Gerenciamento de Dicionário',
'dictManagement.dictTypeCategory': 'Categoria do Dicionário',
'dictManagement.searchPlaceholder': 'Por favor, insira o rótulo do dicionário para pesquisar',
'dictManagement.search': 'Pesquisar',
'dictManagement.dictTypeName': 'Nome do Tipo de Dicionário',
@@ -645,6 +648,8 @@ export default {
'user.disable': 'Desativar',
'user.delete': 'Excluir',
'user.selectUsersFirst': 'Por favor, selecione usuários para excluir primeiro',
'user.selectUsersToDisable': 'Por favor, selecione usuários para desativar',
'user.selectUsersToEnable': 'Por favor, selecione usuários para ativar',
'user.confirmDeleteSelected': 'Tem certeza de que deseja excluir {count} usuários selecionados?',
'user.deleting': 'Excluindo...',
'user.deleteSuccess': '{count} usuários excluídos com sucesso',
@@ -704,6 +709,9 @@ export default {
'common.cancel': 'Cancelar',
'common.sensitive': 'Sensível',
'common.loading': 'Carregando',
'common.copySuccess': 'Cópia bem-sucedida',
'common.copyFailed': 'Falha ao copiar',
'common.copyAddress': 'Copiar endereço',
// Troca de idioma
'language.zhCN': '中文简体',
@@ -1150,6 +1158,8 @@ export default {
'templateQuickConfig.cancel': 'Cancelar',
'templateQuickConfig.templateNotFound': 'Modelo não encontrado',
'templateQuickConfig.newTemplate': 'Novo Modelo',
'templateQuickConfig.editTemplate': 'Editar Modelo',
'templateQuickConfig.addTemplate': 'Adicionar Modelo',
'warning': 'Aviso',
'info': 'Informação',
'common.networkError': 'Falha na requisição de rede',
@@ -1193,8 +1203,10 @@ export default {
'voiceClone.selectFirst': 'Por favor, selecione recursos de voz para excluir primeiro',
'voiceClone.operationCancelled': 'Cancelar',
'voiceClone.operationClosed': 'Popup fechado',
'voiceClone.cloneSuccess': 'Clonado com sucesso',
'voiceClone.cloneSuccess': 'Clonagem bem-sucedida',
'voiceClone.cloneFailed': 'Falha ao clonar',
'voiceClone.cloneErrorTip': 'Falha na clonagem, passe o mouse sobre a dica de erro para ver os detalhes',
'voiceClone.apiError': 'Erro na chamada da API',
'voiceClone.confirmClone': 'Tem certeza de que deseja clonar esta voz?',
'voiceClone.onlySuccessCanClone': 'Apenas vozes treinadas com sucesso podem ser clonadas',
'common.insufficient': 'Insuficiente',
@@ -1212,6 +1224,7 @@ export default {
'voiceClone.training': 'Treinando',
'voiceClone.trainSuccess': 'Treinamento bem-sucedido',
'voiceClone.trainFailed': 'Treinamento falhou',
'voiceClone.trainFailedWithError': 'Treinamento falhou: {error}',
'voiceClone.itemsPerPage': '{items} itens/página',
'voiceClone.firstPage': 'Primeira Página',
'voiceClone.prevPage': 'Página Anterior',
@@ -1351,6 +1364,7 @@ export default {
'knowledgeFileUpload.confirmParse': 'Tem certeza de que deseja analisar este documento?',
'knowledgeFileUpload.nameRequired': 'Por favor, insira o nome do documento',
'knowledgeFileUpload.fileRequired': 'Por favor, selecione um arquivo para enviar',
'knowledgeFileUpload.fileSizeExceeded': 'O tamanho do arquivo não pode exceder 10MB!',
'knowledgeFileUpload.getListFailed': 'Falha ao obter lista de documentos',
'knowledgeFileUpload.parseCancelled': 'Análise cancelada',
'knowledgeFileUpload.deleteCancelled': 'Exclusão cancelada',
@@ -1384,8 +1398,20 @@ export default {
'knowledgeFileUpload.comprehensiveSimilarity': 'Similaridade Geral',
'knowledgeFileUpload.content': 'Conteúdo:',
'knowledgeFileUpload.testQuestionRequired': 'Por favor, insira a pergunta de teste',
'knowledgeFileUpload.sourceDocument': 'Documento de origem',
'knowledgeFileUpload.parsing': 'Analisando...',
'knowledgeBaseDialog.descriptionRequired': 'Por favor, insira a descrição da base de conhecimento',
// Página de gerenciamento de base de conhecimento
'knowledgeBaseManagement.loading': 'Carregando...',
'knowledgeBaseManagement.noData': 'Sem dados',
'knowledgeBaseManagement.switchKnowledgeBase': 'Alternar Base de Conhecimento',
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Ativos de Documentos da Base de Conhecimento Atual',
'knowledgeBaseManagement.enabled': 'Ativado',
'knowledgeBaseManagement.disabled': 'Desativado',
'knowledgeBaseManagement.totalDocuments': 'Total de {total} documentos',
'knowledgeBaseManagement.getListFailed': 'Falha ao obter lista',
// Página de gerenciamento de funcionalidades
'featureManagement.selectAll': 'Selecionar Tudo',
'featureManagement.deselectAll': 'Desmarcar Tudo',
@@ -1401,6 +1427,12 @@ export default {
'featureManagement.cancel': 'Cancelar',
'featureManagement.resetSuccess': 'Configuração de funcionalidades redefinida com sucesso',
'featureManagement.noChanges': 'Nenhuma alteração para salvar',
'featureManagement.saving': 'Salvando...',
'featureManagement.groupName.featureManagement': 'Interruptor de Módulo de Funcionalidade',
'featureManagement.groupName.voiceManagement': 'Configuração de Visibilidade do Agente',
'featureManagement.groupDescription.featureManagement': 'Controlar o status de ativação dos módulos de funcionalidade principal',
'featureManagement.groupDescription.voiceManagement': 'Configurar quais funcionalidades são visíveis e disponíveis para agentes (assistentes de IA)',
'featureManagement.moduleCount': '{count} módulos no total',
// Nomes e descrições de funcionalidades
'feature.voiceprintRecognition.name': 'Reconhecimento de Impressão Vocal',
@@ -1421,7 +1453,6 @@ export default {
// Página de Gerenciamento de Lista de Contatos
'addressBookManagement.title': 'Gerenciamento de Lista de Contatos',
'addressBookManagement.searchPlaceholder': 'Pesquisar nome do dispositivo ou endereço MAC',
'addressBookManagement.search': 'Pesquisar',
'addressBookManagement.selectAgent': 'Selecione um agente',
'addressBookManagement.name': 'Nome da Lista de Contatos',
'addressBookManagement.description': 'Descrição da Lista de Contatos',
+32 -1
View File
@@ -352,6 +352,8 @@ export default {
// Replacement word management page related
'replacementWordManagement.pageTitle': 'Quản lý từ thay thế',
'replacementWordManagement.searchPlaceholder': 'Vui lòng nhập tên tệp để tìm kiếm',
'replacementWordManagement.search': 'Tìm kiếm',
'replacementWordManagement.fileName': 'Tên tệp',
'replacementWordManagement.replacementWordCount': 'Số lượng từ thay thế',
'replacementWordManagement.replacementWordContent': 'Nội dung từ thay thế',
@@ -591,6 +593,7 @@ export default {
// Dictionary management page text
'dictManagement.pageTitle': 'Quản lý từ điển',
'dictManagement.dictTypeCategory': 'Danh mục từ điển',
'dictManagement.searchPlaceholder': 'Vui lòng nhập nhãn giá trị từ điển để tìm kiếm',
'dictManagement.search': 'Tìm kiếm',
'dictManagement.dictTypeName': 'Tên loại từ điển',
@@ -645,6 +648,8 @@ export default {
'user.disable': 'Vô hiệu hóa',
'user.delete': 'Xóa',
'user.selectUsersFirst': 'Vui lòng chọn người dùng để xóa trước',
'user.selectUsersToDisable': 'Vui lòng chọn người dùng để vô hiệu hóa',
'user.selectUsersToEnable': 'Vui lòng chọn người dùng để kích hoạt',
'user.confirmDeleteSelected': 'Bạn có chắc chắn muốn xóa {count} người dùng đã chọn?',
'user.deleting': 'Đang xóa...',
'user.deleteSuccess': 'Đã xóa thành công {count} người dùng',
@@ -704,6 +709,9 @@ export default {
'common.cancel': 'Hủy bỏ',
'common.sensitive': 'Nhạy cảm',
'common.loading': 'Đang tải',
'common.copySuccess': 'Sao chép thành công',
'common.copyFailed': 'Sao chép thất bại',
'common.copyAddress': 'Sao chép địa chỉ',
// Language switch
'language.zhCN': '中文简体',
@@ -1116,6 +1124,8 @@ export default {
'agentTemplateManagement.action': 'Hành động',
'agentTemplateManagement.createTemplate': 'Tạo mẫu',
'templateQuickConfig.newTemplate': 'Mẫu mới',
'templateQuickConfig.editTemplate': 'Chỉnh sửa mẫu',
'templateQuickConfig.addTemplate': 'Thêm mẫu',
'agentTemplateManagement.editTemplate': 'Chỉnh sửa mẫu',
'agentTemplateManagement.deleteTemplate': 'Xóa mẫu',
'agentTemplateManagement.deleteSuccess': 'Đã xóa mẫu thành công',
@@ -1195,6 +1205,8 @@ export default {
'voiceClone.operationClosed': 'Đã đóng popup',
'voiceClone.cloneSuccess': 'Nhân bản thành công',
'voiceClone.cloneFailed': 'Nhân bản thất bại',
'voiceClone.cloneErrorTip': 'Nhân bản thất bại, hãy di chuột qua thông báo lỗi để xem chi tiết',
'voiceClone.apiError': 'Lỗi gọi API',
'voiceClone.confirmClone': 'Bạn có chắc chắn muốn nhân bản giọng này?',
'voiceClone.onlySuccessCanClone': 'Chỉ có thể nhân bản giọng đã huấn luyện thành công',
'common.insufficient': 'Không đủ',
@@ -1212,6 +1224,7 @@ export default {
'voiceClone.training': 'Đang huấn luyện',
'voiceClone.trainSuccess': 'Huấn luyện thành công',
'voiceClone.trainFailed': 'Huấn luyện thất bại',
'voiceClone.trainFailedWithError': 'Huấn luyện thất bại: {error}',
'voiceClone.itemsPerPage': '{items} mục/trang',
'voiceClone.firstPage': 'Trang đầu',
'voiceClone.prevPage': 'Trang trước',
@@ -1351,6 +1364,7 @@ export default {
'knowledgeFileUpload.confirmParse': 'Bạn có chắc chắn muốn phân tích cú pháp tài liệu này?',
'knowledgeFileUpload.nameRequired': 'Vui lòng nhập tên tài liệu',
'knowledgeFileUpload.fileRequired': 'Vui lòng chọn tệp để tải lên',
'knowledgeFileUpload.fileSizeExceeded': 'Kích thước tệp không được vượt quá 10MB!',
'knowledgeFileUpload.getListFailed': 'Không thể lấy danh sách tài liệu',
'knowledgeFileUpload.parseCancelled': 'Đã hủy phân tích cú pháp',
'knowledgeFileUpload.deleteCancelled': 'Đã hủy xóa',
@@ -1384,8 +1398,20 @@ export default {
'knowledgeFileUpload.comprehensiveSimilarity': 'Độ tương đồng toàn diện',
'knowledgeFileUpload.content': 'Nội dung:',
'knowledgeFileUpload.testQuestionRequired': 'Vui lòng nhập câu hỏi kiểm tra',
'knowledgeFileUpload.sourceDocument': 'Tài liệu nguồn',
'knowledgeFileUpload.parsing': 'Đang phân tích...',
'knowledgeBaseDialog.descriptionRequired': 'Vui lòng nhập mô tả cơ sở kiến thức',
// Knowledge Base Management page text
'knowledgeBaseManagement.loading': 'Đang tải...',
'knowledgeBaseManagement.noData': 'Không có dữ liệu',
'knowledgeBaseManagement.switchKnowledgeBase': 'Chuyển đổi cơ sở kiến thức',
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Tài sản tài liệu cơ sở kiến thức hiện tại',
'knowledgeBaseManagement.enabled': 'Đã bật',
'knowledgeBaseManagement.disabled': 'Đã tắt',
'knowledgeBaseManagement.totalDocuments': 'Tổng {total} tài liệu',
'knowledgeBaseManagement.getListFailed': 'Lấy danh sách thất bại',
// Feature Management page text
'featureManagement.selectAll': 'Chọn tất cả',
'featureManagement.deselectAll': 'Bỏ chọn tất cả',
@@ -1401,6 +1427,12 @@ export default {
'featureManagement.cancel': 'Hủy bỏ',
'featureManagement.resetSuccess': 'Cấu hình tính năng đã được đặt lại thành công',
'featureManagement.noChanges': 'Không có thay đổi nào để lưu',
'featureManagement.saving': 'Đang lưu...',
'featureManagement.groupName.featureManagement': 'Công tắc mô-đun tính năng',
'featureManagement.groupName.voiceManagement': 'Cấu hình hiển thị Agent',
'featureManagement.groupDescription.featureManagement': 'Kiểm soát trạng thái kích hoạt của các mô-đun tính năng cốt lõi',
'featureManagement.groupDescription.voiceManagement': 'Cấu hình các tính năng hiển thị và khả dụng cho agent (trợ lý AI)',
'featureManagement.moduleCount': 'Tổng cộng {count} mô-đun',
// Feature names and descriptions
'feature.voiceprintRecognition.name': 'Nhận dạng giọng nói',
@@ -1421,7 +1453,6 @@ export default {
// Trang quản lý danh bạ
'addressBookManagement.title': 'Quản lý danh bạ',
'addressBookManagement.searchPlaceholder': 'Tìm kiếm tên thiết bị hoặc địa chỉ MAC',
'addressBookManagement.search': 'Tìm kiếm',
'addressBookManagement.selectAgent': 'Vui lòng chọn tác tử',
'addressBookManagement.name': 'Tên danh bạ',
'addressBookManagement.description': 'Mô tả danh bạ',
+31 -2
View File
@@ -352,6 +352,8 @@ export default {
// 替换词对话框相关
'replacementWordManagement.pageTitle': '替换词管理',
'replacementWordManagement.searchPlaceholder': '请输入文件名称查询',
'replacementWordManagement.search': '搜索',
'replacementWordManagement.fileName': '文件名称',
'replacementWordManagement.replacementWordCount': '替换词数量',
'replacementWordManagement.replacementWordContent': '替换词内容',
@@ -591,6 +593,7 @@ export default {
// 字典管理页面文本
'dictManagement.pageTitle': '字典管理',
'dictManagement.dictTypeCategory': '字典分类',
'dictManagement.searchPlaceholder': '请输入字典值标签查询',
'dictManagement.search': '搜索',
'dictManagement.dictTypeName': '字典类型名称',
@@ -645,6 +648,8 @@ export default {
'user.disable': '禁用',
'user.delete': '删除',
'user.selectUsersFirst': '请先选择需要删除的用户',
'user.selectUsersToDisable': '请选择要禁用的用户',
'user.selectUsersToEnable': '请选择要启用的用户',
'user.confirmDeleteSelected': '确定要删除选中的{count}个用户吗?',
'user.deleting': '正在删除中...',
'user.deleteSuccess': '成功删除{count}个用户',
@@ -704,6 +709,9 @@ export default {
'common.cancel': '取消',
'common.sensitive': '敏感',
'common.loading': '加载中',
'common.copySuccess': '复制成功',
'common.copyFailed': '复制失败',
'common.copyAddress': '复制地址',
// 语言切换
'language.zhCN': '中文简体',
@@ -1150,6 +1158,8 @@ export default {
'templateQuickConfig.cancel': '取消',
'templateQuickConfig.templateNotFound': '未找到指定模板',
'templateQuickConfig.newTemplate': '新模板',
'templateQuickConfig.editTemplate': '编辑模板',
'templateQuickConfig.addTemplate': '新增模板',
'warning': '警告',
'info': '提示',
'common.networkError': '网络请求失败',
@@ -1195,6 +1205,8 @@ export default {
'voiceClone.action': '操作',
'voiceClone.cloneSuccess': '复刻成功',
'voiceClone.cloneFailed': '复刻失败',
'voiceClone.cloneErrorTip': '克隆失败,请将鼠标悬停在错误提示上,查看错误详情',
'voiceClone.apiError': '调用API时出错',
'voiceClone.confirmClone': '确定要复刻此音色吗?',
'voiceClone.onlySuccessCanClone': '只有训练成功的音色才能复刻',
'common.insufficient': '不足',
@@ -1212,6 +1224,7 @@ export default {
'voiceClone.training': '训练中',
'voiceClone.trainSuccess': '训练成功',
'voiceClone.trainFailed': '训练失败',
'voiceClone.trainFailedWithError': '训练失败:{error}',
'voiceClone.itemsPerPage': '{items}条/页',
'voiceClone.firstPage': '首页',
'voiceClone.prevPage': '上一页',
@@ -1347,10 +1360,11 @@ export default {
'knowledgeFileUpload.parseFailed': '文档解析失败',
'knowledgeFileUpload.deleteSuccess': '文档删除成功',
'knowledgeFileUpload.deleteFailed': '文档删除失败',
'knowledgeFileUpload.confirmDelete': '确定要删除此文吗?',
'knowledgeFileUpload.confirmDelete': '确定要删除此文吗?',
'knowledgeFileUpload.confirmParse': '确定要解析该文档吗?',
'knowledgeFileUpload.nameRequired': '请输入文档名称',
'knowledgeFileUpload.fileRequired': '请选择要上传的文件',
'knowledgeFileUpload.fileSizeExceeded': '文件大小不能超过10MB!',
'knowledgeFileUpload.getListFailed': '获取文档列表失败',
'knowledgeFileUpload.parseCancelled': '已取消解析',
'knowledgeFileUpload.deleteCancelled': '已取消删除',
@@ -1382,9 +1396,19 @@ export default {
'knowledgeFileUpload.testResult': '测试结果:',
'knowledgeFileUpload.noRelatedSlices': '未找到相关切片',
'knowledgeFileUpload.comprehensiveSimilarity': '综合相似度',
'knowledgeFileUpload.sourceDocument': '来源文档',
'knowledgeFileUpload.content': '内容:',
'knowledgeFileUpload.testQuestionRequired': '请输入测试问题',
'knowledgeBaseDialog.descriptionRequired': '请输入知识库描述',
'knowledgeFileUpload.parsing': '正在解析中...',
'knowledgeBaseManagement.loading': '加载中...',
'knowledgeBaseManagement.noData': '暂无数据',
'knowledgeBaseManagement.switchKnowledgeBase': '切换知识库',
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': '当前知识库文档资产',
'knowledgeBaseManagement.enabled': '已启用',
'knowledgeBaseManagement.disabled': '已禁用',
'knowledgeBaseManagement.totalDocuments': '共 {total} 个文档',
'knowledgeBaseManagement.getListFailed': '获取列表失败',
// 系统功能配置页面文本
'featureManagement.selectAll': '全选',
@@ -1401,6 +1425,12 @@ export default {
'featureManagement.cancel': '取消',
'featureManagement.resetSuccess': '功能配置重置成功',
'featureManagement.noChanges': '没有需要保存的更改',
'featureManagement.saving': '保存中...',
'featureManagement.groupName.featureManagement': '功能模块开关',
'featureManagement.groupName.voiceManagement': '智能体可见配置',
'featureManagement.groupDescription.featureManagement': '控制系统核心功能模块的启用状态',
'featureManagement.groupDescription.voiceManagement': '配置哪些功能对智能体(AI助手)可见和可用',
'featureManagement.moduleCount': '共 {count} 个模块',
// 功能名称和描述
'feature.voiceprintRecognition.name': '声纹识别',
@@ -1421,7 +1451,6 @@ export default {
// 通讯录管理页面
'addressBookManagement.title': '通讯录管理',
'addressBookManagement.searchPlaceholder': '搜索设备名称或MAC地址',
'addressBookManagement.search': '搜索',
'addressBookManagement.selectAgent': '请选择智能体',
'addressBookManagement.name': '通讯录名称',
'addressBookManagement.description': '通讯录描述',
+32 -1
View File
@@ -352,6 +352,8 @@ export default {
// 替換詞管理頁面相關
'replacementWordManagement.pageTitle': '替換詞管理',
'replacementWordManagement.searchPlaceholder': '請輸入檔案名稱查詢',
'replacementWordManagement.search': '搜尋',
'replacementWordManagement.fileName': '檔案名稱',
'replacementWordManagement.replacementWordCount': '替換詞數量',
'replacementWordManagement.replacementWordContent': '替換詞內容',
@@ -591,6 +593,7 @@ export default {
// 字典管理頁面文本
'dictManagement.pageTitle': '字典管理',
'dictManagement.dictTypeCategory': '字典分類',
'dictManagement.searchPlaceholder': '請輸入字典值標籤查詢',
'dictManagement.search': '搜索',
'dictManagement.dictTypeName': '字典類型名稱',
@@ -645,6 +648,8 @@ export default {
'user.disable': '禁用',
'user.delete': '刪除',
'user.selectUsersFirst': '請先選擇需要刪除的用戶',
'user.selectUsersToDisable': '請選擇要禁用的用戶',
'user.selectUsersToEnable': '請選擇要啟用的用戶',
'user.confirmDeleteSelected': '確定要刪除選中的{count}個用戶嗎?',
'user.deleting': '正在刪除中...',
'user.deleteSuccess': '成功刪除{count}個用戶',
@@ -704,6 +709,9 @@ export default {
'common.cancel': '取消',
'common.sensitive': '敏感',
'common.loading': '載入中',
'common.copySuccess': '複製成功',
'common.copyFailed': '複製失敗',
'common.copyAddress': '複製地址',
// 語言切換
'language.zhCN': '中文简体',
@@ -1150,6 +1158,8 @@ export default {
'templateQuickConfig.cancel': '取消',
'templateQuickConfig.templateNotFound': '未找到指定模板',
'templateQuickConfig.newTemplate': '新模板',
'templateQuickConfig.editTemplate': '編輯模板',
'templateQuickConfig.addTemplate': '新增模板',
'warning': '警告',
'info': '提示',
'common.networkError': '網路請求失敗',
@@ -1195,6 +1205,8 @@ export default {
'voiceClone.operationClosed': '已關閉彈窗',
'voiceClone.cloneSuccess': '複刻成功',
'voiceClone.cloneFailed': '複刻失敗',
'voiceClone.cloneErrorTip': '複刻失敗,請將鼠標懸停在錯誤提示上,查看錯誤詳情',
'voiceClone.apiError': '調用API時出錯',
'voiceClone.confirmClone': '確定要複刻此音色嗎?',
'voiceClone.onlySuccessCanClone': '只有訓練成功的音色才能複刻',
'common.insufficient': '不足',
@@ -1212,6 +1224,7 @@ export default {
'voiceClone.training': '訓練中',
'voiceClone.trainSuccess': '訓練成功',
'voiceClone.trainFailed': '訓練失敗',
'voiceClone.trainFailedWithError': '訓練失敗:{error}',
'voiceClone.itemsPerPage': '{items}條/頁',
'voiceClone.firstPage': '首頁',
'voiceClone.prevPage': '上一頁',
@@ -1351,6 +1364,7 @@ export default {
'knowledgeFileUpload.confirmParse': '確定要解析該文檔嗎?',
'knowledgeFileUpload.nameRequired': '請輸入文檔名稱',
'knowledgeFileUpload.fileRequired': '請選擇要上傳的文件',
'knowledgeFileUpload.fileSizeExceeded': '文件大小不能超過10MB!',
'knowledgeFileUpload.getListFailed': '獲取文檔列表失敗',
'knowledgeFileUpload.parseCancelled': '已取消解析',
'knowledgeFileUpload.deleteCancelled': '已取消删除',
@@ -1384,8 +1398,20 @@ export default {
'knowledgeFileUpload.comprehensiveSimilarity': '綜合相似度',
'knowledgeFileUpload.content': '內容:',
'knowledgeFileUpload.testQuestionRequired': '請輸入測試問題',
'knowledgeFileUpload.sourceDocument': '來源文檔',
'knowledgeFileUpload.parsing': '正在解析中...',
'knowledgeBaseDialog.descriptionRequired': '請輸入知识库描述',
// 知識庫管理頁面文本
'knowledgeBaseManagement.loading': '加載中...',
'knowledgeBaseManagement.noData': '暫無數據',
'knowledgeBaseManagement.switchKnowledgeBase': '切換知識庫',
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': '當前知識庫文檔資產',
'knowledgeBaseManagement.enabled': '已啟用',
'knowledgeBaseManagement.disabled': '已禁用',
'knowledgeBaseManagement.totalDocuments': '共 {total} 個文檔',
'knowledgeBaseManagement.getListFailed': '獲取列表失敗',
// 功能管理頁面文本
'featureManagement.selectAll': '全選',
'featureManagement.deselectAll': '取消全選',
@@ -1401,6 +1427,12 @@ export default {
'featureManagement.cancel': '取消',
'featureManagement.resetSuccess': '功能配置重置成功',
'featureManagement.noChanges': '沒有需要儲存的更改',
'featureManagement.saving': '儲存中...',
'featureManagement.groupName.featureManagement': '功能模組開關',
'featureManagement.groupName.voiceManagement': '智能體可見配置',
'featureManagement.groupDescription.featureManagement': '控制系統核心功能模組的啟用狀態',
'featureManagement.groupDescription.voiceManagement': '配置哪些功能對智能體(AI助手)可見和可用',
'featureManagement.moduleCount': '共 {count} 個模組',
// 功能名稱和描述
'feature.voiceprintRecognition.name': '聲紋識別',
@@ -1421,7 +1453,6 @@ export default {
// 通訊錄管理頁面
'addressBookManagement.title': '通訊錄',
'addressBookManagement.searchPlaceholder': '請輸入通訊錄名稱搜尋',
'addressBookManagement.search': '搜尋',
'addressBookManagement.name': '通訊錄名稱',
'addressBookManagement.description': '通訊錄描述',
'addressBookManagement.contactCount': '聯繫人數量',
+2
View File
@@ -6,6 +6,7 @@ import App from './App.vue';
import router from './router';
import store from './store';
import i18n from './i18n';
import locale from 'element-ui/lib/locale'
import './styles/global.scss';
import { register as registerServiceWorker } from './registerServiceWorker';
import featureManager from './utils/featureManager';
@@ -14,6 +15,7 @@ import featureManager from './utils/featureManager';
Vue.prototype.$eventBus = new Vue();
Vue.use(ElementUI);
locale.i18n((key, value) => i18n.t(key, value))
Vue.config.productionTip = false
-12
View File
@@ -98,18 +98,6 @@ const routes = [
title: '知识库管理'
}
},
{
path: '/knowledge-file-upload',
name: 'KnowledgeFileUpload',
component: function () {
return import('../views/KnowledgeFileUpload.vue')
},
meta: {
requiresAuth: true,
title: '文档上传管理'
}
},
{
path: '/server-side-management',
name: 'ServerSideManager',
+14
View File
@@ -29,4 +29,18 @@ select:-webkit-autofill:focus {
.popper__arrow {
border-top-color: #e4e7ed !important;
}
}
@mixin scrollbar-style {
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #a1c9fd;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: #f0f3fe;
border-radius: 3px;
}
}
@@ -15,10 +15,9 @@
:placeholder="$t('addressBookManagement.searchPlaceholder')"
v-model="searchKeyword"
class="search-input"
@keyup.enter="handleSearch"
@input="handleSearch"
clearable
/>
<el-button class="btn-search" @click="handleSearch">{{ $t('addressBookManagement.search') }}</el-button>
</div>
<!-- 智能体列表 -->
@@ -42,9 +41,13 @@
<img :src="getDeviceAvatar(device.id)" alt="avatar" />
</div>
<div class="device-content">
<div class="device-name">{{ device.name }}</div>
<div class="device-name">
<MacAddressMask :macAddress="device.name" />
</div>
<div class="device-row">
<span class="device-id">{{ device.deviceId }}</span>
<span class="device-id">
<MacAddressMask :macAddress="device.deviceId" />
</span>
<span class="device-status" :class="device.online ? 'online' : 'offline'">
{{ device.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }}
</span>
@@ -76,10 +79,15 @@
@keyup.enter="handleAliasBlur"
autofocus
/>
<span v-else class="device-name-text" @click="handleStartEdit">{{ selectedDevice.name }}</span>
<span v-else class="device-name-text" @click="handleStartEdit">
<MacAddressMask :macAddress="selectedDevice.name" />
</span>
<i v-if="!isEditingAlias" class="el-icon-edit" @click="handleStartEdit"></i>
</div>
<div class="device-mac">{{ $t('addressBookManagement.macAddress') }}{{ selectedDevice.deviceId }}</div>
<div class="device-mac">
{{ $t('addressBookManagement.macAddress') }}
<MacAddressMask :macAddress="selectedDevice.deviceId" />
</div>
<div class="device-status">
<span>{{ $t('addressBookManagement.status') }}</span>
<span :class="selectedDevice.online ? 'online' : 'offline'">
@@ -133,11 +141,9 @@
<p class="section-desc">{{ $t('addressBookManagement.setPermissionDesc', { count: selectedPermissions.length }) }}</p>
</div>
<div class="section-actions">
<el-button size="small" @click="handleCancel">{{ $t('common.cancel') }}</el-button>
<el-button size="small" @click="handleToggleSelectAll">
{{ isAllSelected ? $t('addressBookManagement.deselectAll') : $t('addressBookManagement.selectAll') }}
</el-button>
<el-button type="primary" size="small" @click="handleSavePermissions">{{ $t('addressBookManagement.save') }}</el-button>
<CustomButton size="small" @click="handleCancel">{{ $t('common.cancel') }}</CustomButton>
<CustomButton size="small" @click="handleToggleSelectAll">{{ isAllSelected ? $t('addressBookManagement.deselectAll') : $t('addressBookManagement.selectAll') }}</CustomButton>
<CustomButton type="confirm" size="small" @click="handleSavePermissions">{{ $t('addressBookManagement.save') }}</CustomButton>
</div>
</div>
@@ -166,10 +172,14 @@
@keyup.enter="handleEditPermissionBlur(device)"
autofocus
/>
<span v-else class="permission-name" @click.stop="handleStartEditPermission(device)">{{ device.addressBookAlias || device.name }}</span>
<span v-else class="permission-name" @click.stop="handleStartEditPermission(device)">
<MacAddressMask :macAddress="device.addressBookAlias || device.name" />
</span>
<i v-if="editingDeviceId !== device.id" class="el-icon-edit permission-edit-btn" @click.stop="handleStartEditPermission(device)"></i>
</div>
<div class="permission-id">{{ device.deviceId }}</div>
<div class="permission-id">
<MacAddressMask :macAddress="device.deviceId" />
</div>
<div class="permission-status" :class="device.online ? 'online' : 'offline'">
{{ device.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }}
</div>
@@ -195,10 +205,12 @@ import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import Api from "@/apis/api.js";
import AddressBookApi from "@/apis/module/addressBook.js";
import MacAddressMask from "@/components/MacAddressMask.vue";
import CustomButton from "@/components/CustomButton.vue";
export default {
name: "AddressBookManagement",
components: { HeaderBar, VersionFooter },
components: { HeaderBar, VersionFooter, MacAddressMask, CustomButton },
data() {
return {
searchKeyword: "",
@@ -269,7 +281,7 @@ export default {
remarks: device.alias || '',
online: false,
createDate: device.createDate,
lastConnectedAt: device.lastConnectedAt,
lastConnectedAt: device.lastConnectedAtTimestamp,
deviceStatus: 'offline'
}));
resolve();
@@ -277,8 +289,11 @@ export default {
});
});
Promise.all(agentPromises).then(() => {
const firstDevice = agentList[0] || {};
this.agentDeviceOptions = agentList;
this.filteredAgents = agentList;
// 默认选中第一项
this.handleDeviceClick(firstDevice.devices?.[0] || {}, firstDevice);
// 获取设备状态
this.fetchDeviceStatus();
});
@@ -334,17 +349,6 @@ export default {
}
});
},
handleAgentClick(agent) {
if (this.expandedAgentId === agent.id) {
this.expandedAgentId = null;
this.selectedAgent = null;
this.selectedDevice = null;
} else {
this.expandedAgentId = agent.id;
this.selectedAgent = agent;
this.selectedDevice = null;
}
},
handleDeviceClick(device, agent) {
this.expandedAgentId = agent.id;
this.selectedAgent = agent;
@@ -396,8 +400,8 @@ export default {
alias: newName
}, (res) => {
if (res.data?.code === 0) {
device.addressBookAlias = newName;
this.$message.success(this.$t('addressBookManagement.aliasSaved'));
this.loadAddressBookPermissions(this.selectedDevice.deviceId);
} else {
this.$message.error(res.data?.msg || this.$t('addressBookManagement.saveFailed'));
}
@@ -453,6 +457,7 @@ export default {
} else if (results.every(r => r)) {
this.$message.success(this.$t('addressBookManagement.permissionSaved'));
this.originalPermissions = [...this.selectedPermissions];
this.loadAddressBookPermissions(this.selectedDevice.deviceId);
} else {
this.$message.error(this.$t('addressBookManagement.partialSaveFailed'));
}
@@ -523,7 +528,7 @@ export default {
getTimeAgo(timestamp) {
if (!timestamp) return '-';
const now = new Date();
const date = new Date(timestamp);
const date = new Date(Number(timestamp));
const diff = now - date;
const seconds = Math.floor(diff / 1000);
@@ -581,7 +586,7 @@ export default {
display: flex;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
background: #eff4ff;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
@@ -940,8 +945,8 @@ export default {
}
.section-actions {
margin-top: 4px;
display: flex;
gap: 8px;
}
}
}
@@ -1124,14 +1129,6 @@ export default {
flex: 1;
}
.btn-search {
padding: 10px 20px;
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
width: 80px;
}
.agent-card {
width: 100%;
padding: 12px 0px;
@@ -1,141 +1,103 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t("agentTemplateManagement.title") }}</h2>
<div class="right-operations">
<el-input
:placeholder="$t('agentTemplateManagement.searchPlaceholder')"
v-model="search"
class="search-input"
clearable
@keyup.enter.native="handleSearch"
style="width: 240px"
/>
<el-button class="btn-search" @click="handleSearch">
{{ $t("agentTemplateManagement.search") }}
</el-button>
</div>
</div>
<!-- 主体内容 -->
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="template-card" shadow="never">
<el-table
ref="templateTable"
:data="templateList"
style="width: 100%"
v-loading="templateLoading"
:element-loading-text="$t('agentTemplateManagement.loading')"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
class="transparent-table"
:header-cell-style="{ padding: '10px 20px' }"
:cell-style="{ padding: '10px 20px' }"
>
<!-- 移除@row-click="handleRowClick" -->
<!-- 自定义选择列实现表头是"选择"文字数据行是小方框 -->
<el-table-column
:label="$t('agentTemplateManagement.select')"
align="center"
min-width="100"
>
<template slot-scope="scope">
<el-checkbox
v-model="scope.row.selected"
@change="handleRowSelectionChange(scope.row)"
@click.stop
></el-checkbox>
</template>
</el-table-column>
<!-- 模板名称 -->
<el-table-column
:label="$t('agentTemplateManagement.templateName')"
prop="agentName"
min-width="250"
show-overflow-tooltip
>
<template slot-scope="scope">
<span>{{ scope.row.agentName }}</span>
</template>
</el-table-column>
<!-- 修改为序号列并移动到此处 -->
<el-table-column
:label="$t('agentTemplateManagement.serialNumber')"
min-width="120"
align="center"
>
<template slot-scope="scope">
<span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column
:label="$t('agentTemplateManagement.action')"
min-width="250"
align="center"
>
<template slot-scope="scope">
<div style="display: flex; justify-content: center; gap: 15px">
<el-button type="text" @click="editTemplate(scope.row)">{{
$t("agentTemplateManagement.editTemplate")
}}</el-button>
<el-button type="text" @click="deleteTemplate(scope.row)">{{
$t("agentTemplateManagement.deleteTemplate")
}}</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 表格底部操作栏 -->
<div class="table_bottom">
<div class="ctrl_btn">
<el-button
type="primary"
@click="handleSelectAll"
size="mini"
class="select-all-btn"
>
{{
isAllSelected
? $t("agentTemplateManagement.deselectAll")
: $t("agentTemplateManagement.selectAll")
}}
</el-button>
<el-button type="success" @click="showAddTemplateDialog" size="mini">
{{ $t("agentTemplateManagement.createTemplate") }}
</el-button>
<el-button
type="danger"
@click="batchDeleteTemplate"
:disabled="!hasSelected"
size="mini"
>
{{ $t("agentTemplateManagement.batchDelete") }}
</el-button>
</div>
<!-- 分页 -->
<div class="custom-pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="pageSizeOptions"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handlePageSizeChange"
@current-change="handlePageChange"
<div class="operation-header">
<h2 class="page-title">{{ $t("agentTemplateManagement.title") }}</h2>
<div class="right-operations">
<el-input
:placeholder="$t('agentTemplateManagement.searchPlaceholder')"
v-model="search"
class="search-input"
clearable
@keyup.enter.native="handleSearch"
/>
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">
{{ $t("agentTemplateManagement.search") }}
</CustomButton>
</div>
</div>
<CustomTable
ref="templateTable"
:data="templateList"
:columns="tableColumns"
:loading="templateLoading"
:show-selection="true"
:show-operations="true"
:operations-label="$t('agentTemplateManagement.action')"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@size-change="handlePageSizeChange"
@page-change="handlePageChange"
>
<template slot="selection" slot-scope="scope">
<el-checkbox v-model="scope.row.selected" @change="handleRowSelectionChange(scope.row)"></el-checkbox>
</template>
<template slot="serialNumber" slot-scope="scope">
<span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span>
</template>
<template slot="operations" slot-scope="scope">
<el-button size="mini" type="text" @click="editTemplate(scope.row)">
{{ $t("agentTemplateManagement.editTemplate") }}
</el-button>
<el-button size="mini" type="text" @click="deleteTemplate(scope.row)">
{{ $t("agentTemplateManagement.deleteTemplate") }}
</el-button>
</template>
<template slot="footer-btns">
<div class="ctrl_btn">
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" @click="handleSelectAll">
{{ isAllSelected ? $t("agentTemplateManagement.deselectAll") : $t("agentTemplateManagement.selectAll") }}
</CustomButton>
<CustomButton type="add" icon="el-icon-plus" size="small" @click="showAddTemplateDialog">
{{ $t("agentTemplateManagement.createTemplate") }}
</CustomButton>
<CustomButton size="small" type="delete" icon="el-icon-delete" @click="batchDeleteTemplate">
{{ $t("agentTemplateManagement.batchDelete") }}
</CustomButton>
</div>
</template>
</CustomTable>
</el-card>
</div>
</div>
</div>
<!-- 新增/编辑模板弹窗 -->
<CustomDialog
:title="dialogTitle"
:visible.sync="dialogVisible"
:confirm-loading="confirmLoading"
:footer="true"
:width="'1200px'"
@confirm="handleDialogConfirm"
@cancel="dialogVisible = false"
>
<el-form ref="dialogForm" :model="form" :rules="formRules" label-width="100px">
<el-form-item :label="$t('templateQuickConfig.agentSettings.agentName')" prop="agentName">
<el-input
v-model="form.agentName"
:placeholder="$t('templateQuickConfig.agentSettings.agentNamePlaceholder')"
/>
</el-form-item>
<el-form-item :label="$t('templateQuickConfig.agentSettings.systemPrompt')" prop="systemPrompt">
<el-input
v-model="form.systemPrompt"
type="textarea"
:placeholder="$t('templateQuickConfig.agentSettings.systemPromptPlaceholder')"
show-word-limit
maxlength="2000"
:rows="16"
/>
</el-form-item>
</el-form>
</CustomDialog>
<el-footer>
<version-footer />
</el-footer>
@@ -146,136 +108,208 @@
import HeaderBar from "@/components/HeaderBar";
import agentApi from "@/apis/module/agent";
import VersionFooter from "@/components/VersionFooter.vue";
import CustomButton from "@/components/CustomButton.vue";
import CustomTable from "@/components/CustomTable.vue";
import CustomDialog from "@/components/CustomDialog.vue";
const DEFAULT_MODEL_CONFIG = {
ttsModelId: "TTS_EdgeTTS",
vadModelId: "VAD_SileroVAD",
asrModelId: "ASR_FunASR",
llmModelId: "LLM_ChatGLMLLM",
vllmModelId: "VLLM_ChatGLMVLLM",
memModelId: "Memory_nomem",
intentModelId: "Intent_function_call"
};
export default {
name: "AgentTemplateManagement",
components: {
HeaderBar,
VersionFooter
VersionFooter,
CustomButton,
CustomTable,
CustomDialog
},
data() {
return {
// 模板相关
templateList: [],
templateLoading: false,
selectedTemplates: [],
isAllSelected: false, // 添加全选状态
isAllSelected: false,
search: "",
// 分页相关数据
pageSizeOptions: [10, 20, 50, 100],
currentPage: 1,
pageSize: 10,
total: 0,
tableColumns: [],
dialogVisible: false,
dialogTitle: "",
confirmLoading: false,
form: {
id: null,
agentCode: "小智",
agentName: "",
systemPrompt: "",
sort: 0,
model: { ...DEFAULT_MODEL_CONFIG }
},
formRules: {
agentName: [
{ required: true, message: "请输入助手昵称", trigger: "blur" }
],
systemPrompt: [
{ required: true, message: "请输入角色介绍", trigger: "blur" }
]
},
originalForm: null
};
},
created() {
this.initTableColumns();
this.loadTemplateList();
},
// 在computed部分添加hasSelected属性
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
return this.getVisiblePages();
},
hasSelected() {
return this.selectedTemplates.length > 0;
},
}
},
methods: {
// 加载模板列表
// 改进loadTemplateList方法的错误处理逻辑
initTableColumns() {
this.tableColumns = [
{
prop: "agentName",
label: this.$t("agentTemplateManagement.templateName"),
},
{
prop: "serialNumber",
label: this.$t("agentTemplateManagement.serialNumber"),
}
];
},
loadTemplateList() {
this.templateLoading = true;
const params = {
page: this.currentPage,
limit: this.pageSize,
limit: this.pageSize
};
if (this.search) {
params.agentName = this.search;
}
try {
agentApi.getAgentTemplatesPage(
params,
(res) => {
// 更健壮的响应处理逻辑
if (res && typeof res === "object") {
if (res.data && res.data.code === 0) {
const responseData = res.data.data || {};
// 为每个模板添加selected属性
this.templateList = Array.isArray(responseData.list)
? responseData.list.map((item) => ({ ...item, selected: false }))
: [];
this.total =
typeof responseData.total === "number" ? responseData.total : 0;
} else {
this.templateList = [];
this.total = 0;
this.$message.error(
res?.data?.msg || this.$t("agentTemplateManagement.fetchTemplateFailed")
);
}
} else {
this.templateList = [];
this.total = 0;
this.$message.error(
this.$t("agentTemplateManagement.fetchTemplateBackendError")
);
}
this.templateLoading = false;
},
(error) => {
agentApi.getAgentTemplatesPage(
params,
(res) => {
if (res && res.data && res.data.code === 0) {
const responseData = res.data.data || {};
this.templateList = Array.isArray(responseData.list)
? responseData.list.map((item) => ({ ...item, selected: false }))
: [];
this.total = typeof responseData.total === "number" ? responseData.total : 0;
} else {
this.templateList = [];
this.total = 0;
this.templateLoading = false;
this.$message.error(this.$t("common.networkError"));
this.$message.error(res?.data?.msg || this.$t("agentTemplateManagement.fetchTemplateFailed"));
}
);
} catch (error) {
this.templateList = [];
this.total = 0;
this.templateLoading = false;
this.$message.error(this.$t("agentTemplateManagement.fetchTemplateBackendError"));
}
this.templateLoading = false;
},
(error) => {
this.templateList = [];
this.total = 0;
this.templateLoading = false;
this.$message.error(this.$t("common.networkError"));
}
);
},
// 搜索模板
handleSearch() {
if (this.search) {
const searchValue = this.search.toLowerCase();
const filteredList = this.templateList.filter((template) =>
template.agentName.toLowerCase().includes(searchValue)
);
this.templateList = filteredList;
this.total = filteredList.length;
} else {
this.loadTemplateList();
}
this.currentPage = 1;
this.loadTemplateList();
},
// 修改showAddTemplateDialog方法,使其跳转到与编辑页面相同的页面
// 显示新增模板弹窗
showAddTemplateDialog() {
// 跳转到模板快速配置页面,不传递templateId参数表示新增
this.$router.push({
path: "/template-quick-config",
});
this.dialogTitle = this.$t("templateQuickConfig.addTemplate");
this.form = {
id: null,
agentCode: "小智",
agentName: this.$t("templateQuickConfig.newTemplate"),
systemPrompt: "",
sort: 1,
model: { ...DEFAULT_MODEL_CONFIG }
};
this.originalForm = JSON.parse(JSON.stringify(this.form));
this.dialogVisible = true;
},
// 编辑模板
editTemplate(row) {
// 跳转到模板快速配置页面,并传递模板ID参数
this.$router.push({
path: "/template-quick-config",
query: { templateId: row.id },
this.dialogTitle = this.$t("templateQuickConfig.editTemplate");
this.fetchTemplateById(row.id);
},
fetchTemplateById(templateId) {
agentApi.getAgentTemplateById(templateId, (res) => {
if (res && res.data && res.data.code === 0 && res.data.data) {
const template = res.data.data;
this.form = {
id: template.id,
agentCode: template.agentCode || "小智",
agentName: template.agentName || "",
systemPrompt: template.systemPrompt || "",
sort: template.sort || 0,
model: {
ttsModelId: template.ttsModelId || DEFAULT_MODEL_CONFIG.ttsModelId,
vadModelId: template.vadModelId || DEFAULT_MODEL_CONFIG.vadModelId,
asrModelId: template.asrModelId || DEFAULT_MODEL_CONFIG.asrModelId,
llmModelId: template.llmModelId || DEFAULT_MODEL_CONFIG.llmModelId,
vllmModelId: template.vllmModelId || DEFAULT_MODEL_CONFIG.vllmModelId,
memModelId: template.memModelId || DEFAULT_MODEL_CONFIG.memModelId,
intentModelId: template.intentModelId || DEFAULT_MODEL_CONFIG.intentModelId
}
};
this.originalForm = JSON.parse(JSON.stringify(this.form));
this.dialogVisible = true;
} else {
this.$message.error(res?.data?.msg || this.$t("templateQuickConfig.templateNotFound"));
}
});
},
handleDialogConfirm() {
this.$refs.dialogForm.validate((valid) => {
if (!valid) return;
// 删除模板
const configData = {
id: this.form.id || "",
agentCode: this.form.agentCode,
agentName: this.form.agentName,
systemPrompt: this.form.systemPrompt,
sort: this.form.sort,
functions: [],
...this.form.model
};
this.confirmLoading = true;
const apiCall = this.form.id
? agentApi.updateAgentTemplate
: agentApi.addAgentTemplate;
apiCall(configData, (res) => {
this.confirmLoading = false;
if (res && res.data && res.data.code === 0) {
this.$message.success({
message: this.$t("templateQuickConfig.saveSuccess"),
showClose: true
});
this.dialogVisible = false;
this.loadTemplateList();
} else {
this.$message.error({
message: res?.data?.msg || this.$t("templateQuickConfig.saveFailed"),
showClose: true
});
}
}, (error) => {
this.confirmLoading = false;
this.$message.error(this.$t("common.networkError"));
});
});
},
deleteTemplate(row) {
this.$confirm(
this.$t("agentTemplateManagement.confirmSingleDelete"),
@@ -283,23 +317,18 @@ export default {
{
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
type: "warning"
}
)
.then(() => {
agentApi.deleteAgentTemplate(row.id, (res) => {
if (res && typeof res === "object") {
// 检查res.data是否存在且包含code=0
if (res.data && res.data.code === 0) {
this.$message.success(this.$t("agentTemplateManagement.deleteSuccess"));
this.loadTemplateList();
} else {
this.$message.error(
res?.data?.msg || this.$t("agentTemplateManagement.deleteFailed")
);
}
if (res && res.data && res.data.code === 0) {
this.$message.success(this.$t("agentTemplateManagement.deleteSuccess"));
this.selectedTemplates = [];
this.isAllSelected = false;
this.loadTemplateList();
} else {
this.$message.error(this.$t("agentTemplateManagement.deleteBackendError"));
this.$message.error(res?.data?.msg || this.$t("agentTemplateManagement.deleteFailed"));
}
});
})
@@ -307,47 +336,30 @@ export default {
this.$message.info(this.$t("common.deleteCancelled"));
});
},
// 批量删除模板
batchDeleteTemplate() {
if (this.selectedTemplates.length === 0) {
this.$message.warning(this.$t("agentTemplateManagement.selectTemplate"));
return;
}
this.$confirm(
this.$t("agentTemplateManagement.confirmBatchDelete", {
count: this.selectedTemplates.length,
}),
this.$t("agentTemplateManagement.confirmBatchDelete", { count: this.selectedTemplates.length }),
this.$t("common.warning"),
{
confirmButtonText: this.$t("common.confirm"),
cancelButtonText: this.$t("common.cancel"),
type: "warning",
type: "warning"
}
)
.then(() => {
// 确保参数格式正确 - 将id数组作为请求体
const ids = this.selectedTemplates.map((template) => template.id);
agentApi.batchDeleteAgentTemplate(ids, (res) => {
if (res && typeof res === "object") {
if (res.data && res.data.code === 0) {
this.$message.success(
this.$t("agentTemplateManagement.batchDeleteSuccess")
);
// 重新加载模板列表
this.loadTemplateList();
// 清空选中状态
this.selectedTemplates = [];
this.isAllSelected = false;
} else {
this.$message.error(
res?.data?.msg || this.$t("agentTemplateManagement.batchDeleteFailed")
);
}
if (res && res.data && res.data.code === 0) {
this.$message.success(this.$t("agentTemplateManagement.batchDeleteSuccess"));
this.loadTemplateList();
this.selectedTemplates = [];
this.isAllSelected = false;
} else {
this.$message.error(this.$t("agentTemplateManagement.deleteBackendError"));
this.$message.error(res?.data?.msg || this.$t("agentTemplateManagement.batchDeleteFailed"));
}
});
})
@@ -355,150 +367,81 @@ export default {
this.$message.info(this.$t("common.deleteCancelled"));
});
},
// 完善分页相关方法
handlePageChange(page) {
this.currentPage = page;
this.loadTemplateList();
},
handlePageSizeChange(size) {
this.pageSize = size;
this.currentPage = 1;
this.loadTemplateList();
},
goFirst() {
this.currentPage = 1;
},
goPrev() {
this.currentPage--;
},
goNext() {
this.currentPage++;
},
goToPage(page) {
this.currentPage = page;
},
getVisiblePages() {
const pages = [];
const totalPages = this.pageCount;
const currentPage = this.currentPage;
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (currentPage <= 4) {
for (let i = 1; i <= 5; i++) {
pages.push(i);
}
pages.push("...");
pages.push(totalPages);
} else if (currentPage >= totalPages - 3) {
pages.push(1);
pages.push("...");
for (let i = totalPages - 4; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push("...");
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
pages.push(i);
}
pages.push("...");
pages.push(totalPages);
}
}
return pages;
},
// 修改handleSelectAll方法
handleSelectAll() {
this.isAllSelected = !this.isAllSelected;
this.templateList.forEach((row) => {
row.selected = this.isAllSelected;
});
// 更新选中的模板列表
this.selectedTemplates = this.isAllSelected ? [...this.templateList] : [];
},
// 处理行选择变化
handleRowSelectionChange(row) {
// 查找选中的模板
this.selectedTemplates = this.templateList.filter((template) => template.selected);
// 更新全选状态
this.isAllSelected =
this.templateList.length > 0 &&
this.selectedTemplates.length === this.templateList.length;
},
},
}
}
};
</script>
<style scoped lang="scss">
/* 基础背景和布局设置 */
<style lang="scss" scoped>
.welcome {
min-height: 100vh;
min-width: 900px;
min-height: 506px;
height: 100vh;
display: flex;
flex-direction: column;
position: relative;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
flex-direction: column;
background: #eff4ff;
background-size: cover;
overflow: hidden;
width: 100%;
}
/* 操作栏样式 */
.operation-bar {
.main-wrapper {
height: calc(100vh - 63px - 35px);
padding: 20px 22px 0;
position: relative;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.operation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
justify-content: space-between;
padding: 0 0 16px 0;
}
.page-title {
font-weight: 500;
font-size: 24px;
margin: 0;
}
.right-operations {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
}
.search-input {
width: 200px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
/* 主容器样式 */
.main-wrapper {
// 顶部 63px 底部 35px 查询72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
width: 240px;
}
.content-panel {
width: 100%;
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
border-radius: 15px;
background: transparent;
border: 1px solid #fff;
@@ -506,25 +449,25 @@ export default {
.content-area {
flex: 1;
height: 100%;
min-width: 600px;
overflow-x: auto;
overflow: auto;
background-color: white;
display: flex;
flex-direction: column;
position: relative;
}
/* 模板卡片样式 */
.template-card {
border: none;
box-shadow: none;
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
:deep(.el-card__body) {
padding: 15px;
::v-deep .el-card__body {
padding: 14px 20px;
display: flex;
flex-direction: column;
flex: 1;
@@ -532,106 +475,15 @@ export default {
}
}
/* 表格样式 - 优化整合版 */
.transparent-table {
width: 100%;
flex: 1;
min-height: 0;
}
:deep(.el-table) {
height: 100%;
display: flex;
flex-direction: column;
--table-max-height: calc(100vh - 42vh);
max-height: var(--table-max-height);
/* 表格头部样式 */
.el-table__header th {
padding: 8px 0 !important;
height: 40px !important;
}
.el-table__header th .cell {
color: #303133 !important;
font-weight: 600;
}
/* 表格主体样式 */
.el-table__body {
.el-table__row td {
padding: 12px 0 !important;
border-bottom: 1px solid #ebeef5;
}
.el-table__row:hover {
background-color: #f5f7fa;
}
}
/* 表格按钮样式 */
.el-button--text {
color: #7079aa;
}
.el-button--text:hover {
color: #5a64b5;
}
/* 单元格文本样式 */
.cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
/* 表格底部操作栏 */
.table_bottom {
display: flex;
justify-content: space-between !important;
align-items: center;
margin-top: auto;
padding: 0 20px !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* 控制按钮样式 */
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 0 !important;
margin-left: 0 !important;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--success {
background: #5bc98c;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
</style>
:deep(.el-table .el-button--text) {
color: #7079aa;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
</style>
+119 -435
View File
@@ -2,114 +2,83 @@
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('device.management') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('device.search') }}</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="device-card" shadow="never">
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
:header-cell-class-name="headerCellClassName" v-loading="loading"
:element-loading-text="$t('deviceManagement.loading')" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)">
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
</el-table-column>
<el-table-column :label="$t('device.model')" prop="model" align="center">
<template slot-scope="scope">
{{ getFirmwareTypeName(scope.row.model) }}
</template>
</el-table-column>
<el-table-column :label="$t('device.firmwareVersion')" prop="firmwareVersion"
align="center"></el-table-column>
<el-table-column :label="$t('device.macAddress')" prop="macAddress" align="center"></el-table-column>
<el-table-column :label="$t('device.bindTime')" prop="bindTime" align="center"></el-table-column>
<el-table-column :label="$t('device.lastConversation')" prop="lastConversation"
align="center"></el-table-column>
<el-table-column v-if="mqttServiceAvailable" :label="$t('device.deviceStatus')" prop="deviceStatus" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.deviceStatus === 'online'" type="success">{{ $t('device.online') }}</el-tag>
<el-tag v-else type="danger">{{ $t('device.offline') }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('device.remark')" align="center">
<template #default="{ row }">
<el-input v-show="row.isEdit" v-model="row.remark" size="mini" maxlength="64" show-word-limit
@blur="onRemarkBlur(row)" @keyup.enter.native="onRemarkEnter(row)" />
<span v-show="!row.isEdit" class="remark-view">
<i class="el-icon-edit" @click="row.isEdit = true" style="cursor: pointer;"></i>
<span @click="row.isEdit = true">
{{ row.remark || '-' }}
</span>
</span>
</template>
</el-table-column>
<el-table-column :label="$t('device.autoUpdate')" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
@change="handleOtaSwitchChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column :label="$t('device.operation')" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
{{ $t('device.unbind') }}
</el-button>
<el-button v-if="isGenerate(scope.row)" size="mini" type="text" @click="handleGenertor(scope.row)">
{{ $t('device.deviceThemeGeneration') }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
{{ $t('device.bindWithCode') }}
</el-button>
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
{{ $t('device.manualAdd') }}
</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">
{{ $t('device.unbind') }}
</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item"
:label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('dictManagement.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('dictManagement.prevPage') }}
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('dictManagement.nextPage') }}
</button>
<span class="total-text">
{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}
</span>
<div class="operation-header">
<h2 class="page-title">{{ $t('device.management') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">{{ $t('device.search') }}</CustomButton>
</div>
</div>
<CustomTable
ref="deviceTable"
:data="paginatedDeviceList"
:columns="tableColumns"
:loading="loading"
:loading-text="$t('deviceManagement.loading')"
:show-selection="true"
:show-operations="true"
:operations-label="$t('device.operation')"
:total="filteredDeviceList.length"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@size-change="handlePageSizeChange"
@page-change="goToPage"
>
<template slot="model" slot-scope="scope">
{{ getFirmwareTypeName(scope.row.model) }}
</template>
<template slot="macAddress" slot-scope="scope">
<MacAddressMask :macAddress="scope.row.macAddress" />
</template>
<template slot="deviceStatus" slot-scope="scope">
<el-tag v-if="scope.row.deviceStatus === 'online'" type="success">{{ $t('device.online') }}</el-tag>
<el-tag v-else type="danger">{{ $t('device.offline') }}</el-tag>
</template>
<template slot="remark" slot-scope="scope">
<el-input v-show="scope.row.isEdit" v-model="scope.row.remark" size="mini" maxlength="64" show-word-limit
@blur="onRemarkBlur(scope.row)" @keyup.enter.native="onRemarkEnter(scope.row)" />
<span v-show="!scope.row.isEdit" class="remark-view">
<i class="el-icon-edit" @click="scope.row.isEdit = true" style="cursor: pointer;"></i>
<span @click="scope.row.isEdit = true">
{{ scope.row.remark || '-' }}
</span>
</span>
</template>
<template slot="otaSwitch" slot-scope="scope">
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
@change="handleOtaSwitchChange(scope.row)"></el-switch>
</template>
<template slot="operations" slot-scope="scope">
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
{{ $t('device.unbind') }}
</el-button>
<el-button v-if="isGenerate(scope.row)" size="mini" type="text" @click="handleGenertor(scope.row)">
{{ $t('device.deviceThemeGeneration') }}
</el-button>
</template>
<template slot="footer-btns">
<div class="ctrl_btn">
<CustomButton :icon="isCurrentPageAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" @click="handleSelectAll">
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</CustomButton>
<CustomButton icon="el-icon-plus" type="add" size="small" @click="handleAddDevice">
{{ $t('device.bindWithCode') }}
</CustomButton>
<CustomButton icon="el-icon-plus" type="add" size="small" @click="handleManualAddDevice">
{{ $t('device.manualAdd') }}
</CustomButton>
<CustomButton size="small" type="delete" icon="el-icon-delete" @click="deleteSelected">
{{ $t('device.unbind') }}
</CustomButton>
</div>
</template>
</CustomTable>
</el-card>
</div>
</div>
@@ -131,13 +100,19 @@ import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import MacAddressMask from "@/components/MacAddressMask.vue";
import CustomButton from "@/components/CustomButton.vue";
import CustomTable from "@/components/CustomTable.vue";
export default {
components: {
HeaderBar,
AddDeviceDialog,
ManualAddDeviceDialog,
VersionFooter
VersionFooter,
MacAddressMask,
CustomButton,
CustomTable,
},
data() {
return {
@@ -154,7 +129,7 @@ export default {
loading: false,
userApi: null,
firmwareTypes: [],
mqttServiceAvailable: false, // MQTT
mqttServiceAvailable: false,
};
},
computed: {
@@ -172,28 +147,24 @@ export default {
const end = start + this.pageSize;
return this.filteredDeviceList.slice(start, end);
},
pageCount() {
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
},
//
isCurrentPageAllSelected() {
return this.paginatedDeviceList.length > 0 &&
this.paginatedDeviceList.every(device => device.selected);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
tableColumns() {
const columns = [
{ prop: 'model', label: this.$t('device.model'), align: 'center' },
{ prop: 'firmwareVersion', label: this.$t('device.firmwareVersion'), align: 'center' },
{ prop: 'macAddress', label: this.$t('device.macAddress'), align: 'center' },
{ prop: 'bindTime', label: this.$t('device.bindTime'), align: 'center' },
{ prop: 'lastConversation', label: this.$t('device.lastConversation'), align: 'center' },
];
if (this.mqttServiceAvailable) {
columns.push({ prop: 'deviceStatus', label: this.$t('device.deviceStatus'), align: 'center' });
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
columns.push({ prop: 'remark', label: this.$t('device.remark'), align: 'center' });
columns.push({ prop: 'otaSwitch', label: this.$t('device.autoUpdate'), align: 'center' });
return columns;
},
},
mounted() {
@@ -307,14 +278,12 @@ export default {
row._submitting = false;
});
},
//
onRemarkBlur(row) {
row.isEdit = false;
setTimeout(() => {
this.submitRemark(row);
}, 100); // 100ms enter+blur
}, 100);
},
//
onRemarkEnter(row) {
row.isEdit = false;
this.submitRemark(row);
@@ -348,15 +317,6 @@ export default {
sessionStorage.setItem('devicePath', window.location.href);
window.location.href = url;
},
goFirst() {
this.currentPage = 1;
},
goPrev() {
if (this.currentPage > 1) this.currentPage--;
},
goNext() {
if (this.currentPage < this.pageCount) this.currentPage++;
},
goToPage(page) {
this.currentPage = page;
},
@@ -383,7 +343,6 @@ export default {
otaSwitch: device.autoUpdate === 1,
rawBindTime: new Date(device.createDate).getTime(),
selected: false,
// 线
deviceStatus: 'offline'
};
})
@@ -391,7 +350,6 @@ export default {
this.activeSearchKeyword = "";
this.searchKeyword = "";
//
this.fetchDeviceStatus(agentId);
} else {
this.$message.error(data.msg || this.$t('device.getListFailed'));
@@ -399,47 +357,35 @@ export default {
});
},
//
fetchDeviceStatus(agentId) {
// hover
this.loading = true;
Api.device.getDeviceStatus(agentId, ({ data }) => {
this.loading = false;
if (data.code === 0) {
try {
// JSON
const statusData = JSON.parse(data.data);
// 使devices
if (statusData && typeof statusData === 'object') {
//
this.mqttServiceAvailable = true;
//
this.updateDeviceStatusFromResponse(statusData);
} else {
// MQTT
this.mqttServiceAvailable = false;
}
} catch (error) {
// JSONMQTT
this.mqttServiceAvailable = false;
}
} else {
// MQTT
this.mqttServiceAvailable = false;
}
});
},
// API
updateDeviceStatusFromResponse(deviceStatusMap) {
this.deviceList.forEach(device => {
// MQTTID
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
//
if (deviceStatusMap[mqttClientId]) {
const statusInfo = deviceStatusMap[mqttClientId];
@@ -456,17 +402,10 @@ export default {
device.deviceStatus = isOnline ? 'online' : 'offline';
} else {
// 线
device.deviceStatus = 'offline';
}
});
},
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return "custom-selection-header";
}
return "";
},
getFirmwareTypeName(type) {
const firmwareType = this.firmwareTypes.find(item => item.key === type)
return firmwareType ? firmwareType.name : type
@@ -486,7 +425,6 @@ export default {
this.$message.error(msg || this.$t('message.error'))
})
},
// bin
isGenerate(row) {
const version = row.firmwareVersion.replace(/\./g, '');
return Number(version) >= 200;
@@ -497,13 +435,13 @@ export default {
if (isNaN(ts)) return '-';
const date = new Date(ts);
if (isNaN(date.getTime())) return '-';
return date.toLocaleString(); //
return date.toLocaleString();
},
}
};
</script>
<style scoped>
<style lang="scss" scoped>
.welcome {
min-width: 900px;
min-height: 506px;
@@ -511,34 +449,33 @@ export default {
display: flex;
position: relative;
flex-direction: column;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
background: #eff4ff;
background-size: cover;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
height: calc(100vh - 63px - 35px);
padding: 20px 22px 0;
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.operation-bar {
.operation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
justify-content: space-between;
padding: 0 0 16px 0;
}
.page-title {
font-weight: 500;
font-size: 24px;
margin: 0;
color: #2c3e50;
}
.right-operations {
@@ -548,74 +485,10 @@ export default {
}
.search-input {
width: 280px;
border-radius: 4px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
::v-deep .search-input .el-input__inner {
border-radius: 4px;
border: 1px solid #DCDFE6;
background-color: white;
transition: border-color 0.2s;
}
::v-deep .page-size-select {
width: 100px;
margin-right: 8px;
}
::v-deep .page-size-select .el-input__inner {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
::v-deep .page-size-select .el-input__suffix {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
::v-deep .page-size-select .el-input__suffix-inner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
::v-deep .page-size-select .el-icon-arrow-up:before {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
::v-deep .search-input .el-input__inner:focus {
border-color: #6b8cff;
outline: none;
width: 240px;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
@@ -636,161 +509,33 @@ export default {
.device-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
::v-deep .el-card__body {
padding: 15px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
::v-deep .el-card__body {
padding: 14px 20px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
/* padding-bottom: 10px; */
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
}
.ctrl_btn .el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
:deep(.el-table .el-button--text) {
color: #7079aa;
}
.ctrl_btn .el-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.ctrl_btn .el-button--primary {
background: #5f70f3;
color: white;
}
.ctrl_btn .el-button--success {
background: #5bc98c;
color: white;
}
.ctrl_btn .el-button--danger {
background: #fd5b63;
color: white;
}
.custom-pagination {
display: flex;
align-items: center;
gap: 10px;
}
.custom-pagination .el-select {
margin-right: 8px;
}
.custom-pagination .pagination-btn:first-child,
.custom-pagination .pagination-btn:nth-child(2),
.custom-pagination .pagination-btn:nth-last-child(2),
.custom-pagination .pagination-btn:nth-child(3) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-pagination .pagination-btn:first-child:hover,
.custom-pagination .pagination-btn:nth-child(2):hover,
.custom-pagination .pagination-btn:nth-last-child(2):hover,
.custom-pagination .pagination-btn:nth-child(3):hover {
background: #d7dce6;
}
.custom-pagination .pagination-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)):hover {
background: rgba(245, 247, 250, 0.3);
}
.custom-pagination .pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
}
.custom-pagination .pagination-btn.active:hover {
background: #6d7cf5 !important;
}
.custom-pagination .total-text {
color: #909399;
font-size: 14px;
margin-left: 10px;
}
:deep(.transparent-table) {
background: white;
border: none;
}
:deep(.transparent-table .el-table__header th) {
background: white !important;
color: black;
border-right: none !important;
}
:deep(.transparent-table .el-table__body tr td) {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
border-right: none !important;
}
:deep(.transparent-table .el-table__header tr th:first-child .cell),
:deep(.transparent-table .el-table__body tr td:first-child .cell) {
padding-left: 10px;
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
:deep(.el-icon-edit) {
@@ -801,65 +546,4 @@ export default {
:deep(.el-icon-edit:hover) {
color: #5a64b5;
}
:deep(.custom-selection-header .el-checkbox) {
display: none !important;
}
:deep(.el-table .el-button--text) {
color: #7079aa;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
:deep(.transparent-table) {
flex: 1;
display: flex;
flex-direction: column;
/* max-height: calc(100vh - 40vh); */
}
:deep(.el-table__body-wrapper) {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
:deep(.el-table__header-wrapper) {
flex-shrink: 0;
}
@media (min-width: 1144px) {
.table_bottom {
margin-top: 40px;
}
:deep(.transparent-table) .el-table__body tr td {
padding-top: 16px;
padding-bottom: 16px;
}
}
:deep(.el-checkbox__inner) {
background-color: #ffffff !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
::v-deep .el-table--border::after,
::v-deep .el-table--group::after,
::v-deep .el-table::before {
display: none !important;
}
</style>
File diff suppressed because it is too large Load Diff
+143 -104
View File
@@ -1,38 +1,38 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('header.featureManagement') }}</h2>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="feature-card" shadow="never">
<div class="config-header">
<div class="header-icon">
<img loading="lazy" src="@/assets/home/equipment.png" alt="" />
</div>
<h2 class="page-title">{{ $t('header.featureManagement') }}</h2>
<div class="header-actions">
<el-button @click="!isSaving && toggleSelectAll()" class="btn-select-all" :disabled="isSaving">
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="medium" @click="!isSaving && toggleSelectAll()" :disabled="isSaving">
{{ isAllSelected ? $t('featureManagement.deselectAll') : $t('featureManagement.selectAll') }}
</el-button>
<el-button type="primary" class="save-btn" @click="handleSave" :disabled="isSaving">
{{ isSaving ? $t('featureManagement.saving') : $t('featureManagement.save') }}
</el-button>
<el-button class="reset-btn" @click="handleReset" :disabled="isSaving">
</CustomButton>
<CustomButton icon="el-icon-refresh" size="medium" @click="handleReset" :disabled="isSaving">
{{ $t('featureManagement.reset') }}
</el-button>
</CustomButton>
<CustomButton icon="el-icon-setting" size="medium" type="confirm" @click="handleSave" :disabled="isSaving">
{{ isSaving ? $t('featureManagement.saving') : $t('featureManagement.save') }}
</CustomButton>
</div>
</div>
<div class="divider"></div>
<!-- 功能分组容器 - 左右布局 -->
<div class="feature-groups-container">
<!-- 功能管理分组 -->
<div v-if="featureManagementFeatures.length > 0" class="feature-group">
<h3 class="group-title">{{ $t('featureManagement.group.featureManagement') }}</h3>
<div class="group-title">
<img src="@/assets/setting/menu.png" alt="" width="28" height="28">
<div class="group-header">
<span class="group-name">{{ $t('featureManagement.groupName.featureManagement') }}</span>
<span class="module-count">{{ $t('featureManagement.moduleCount', { count: featureManagementFeatures.length }) }}</span>
</div>
</div>
<p class="group-description">{{ $t('featureManagement.groupDescription.featureManagement') }}</p>
<div class="features-grid">
<div
v-for="feature in featureManagementFeatures"
@@ -41,23 +41,35 @@
:class="{ 'feature-enabled': feature.enabled, 'feature-disabled': isSaving }"
@click="!isSaving && toggleFeature(feature)"
>
<div class="feature-header">
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
<el-checkbox
v-model="feature.enabled"
@change="!isSaving && toggleFeature(feature)"
class="feature-checkbox"
:disabled="isSaving"
/>
<img :src="featureIcons[feature.id]" alt="" width="60" height="60">
<div class="feature-content">
<div class="feature-header">
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
<div class="feature-checkbox-container">
<el-checkbox
v-model="feature.enabled"
@change="!isSaving && toggleFeature(feature)"
class="feature-checkbox"
:disabled="isSaving"
/>
</div>
</div>
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
</div>
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
</div>
</div>
</div>
<!-- 语音管理分组 -->
<div v-if="voiceManagementFeatures.length > 0" class="feature-group">
<h3 class="group-title">{{ $t('featureManagement.group.voiceManagement') }}</h3>
<div class="group-title">
<img src="@/assets/setting/agent.png" alt="" width="28" height="28">
<div class="group-header">
<span class="group-name">{{ $t('featureManagement.groupName.voiceManagement') }}</span>
<span class="module-count">{{ $t('featureManagement.moduleCount', { count: voiceManagementFeatures.length }) }}</span>
</div>
</div>
<p class="group-description">{{ $t('featureManagement.groupDescription.voiceManagement') }}</p>
<div class="features-grid">
<div
v-for="feature in voiceManagementFeatures"
@@ -66,16 +78,21 @@
:class="{ 'feature-enabled': feature.enabled, 'feature-disabled': isSaving }"
@click="!isSaving && toggleFeature(feature)"
>
<div class="feature-header">
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
<el-checkbox
v-model="feature.enabled"
@change="!isSaving && toggleFeature(feature)"
class="feature-checkbox"
:disabled="isSaving"
/>
<img :src="voiceManagementIcons[feature.id]" alt="" width="60" height="60">
<div class="feature-content">
<div class="feature-header">
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
<div class="feature-checkbox-container">
<el-checkbox
v-model="feature.enabled"
@change="!isSaving && toggleFeature(feature)"
class="feature-checkbox"
:disabled="isSaving"
/>
</div>
</div>
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
</div>
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
</div>
</div>
</div>
@@ -101,19 +118,41 @@
import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import featureManager from "@/utils/featureManager.js";
import CustomButton from "@/components/CustomButton.vue";
import voiceprintIcon from '@/assets/setting/voiceprint.png'
import voiceCloneIcon from '@/assets/setting/voiceclone.png'
import knowledgeBaseIcon from '@/assets/setting/knowledgeBase.png'
import mcpAccessPointIcon from '@/assets/setting/mcpAccessPoint.png'
import addressBookIcon from '@/assets/setting/addressBook.png'
import vadIcon from '@/assets/setting/vad.png'
import asrIcon from '@/assets/setting/asr.png'
export default {
name: "FeatureManagement",
components: {
HeaderBar,
VersionFooter
VersionFooter,
CustomButton
},
data() {
return {
pendingChanges: false,
featureManagementFeatures: [],
voiceManagementFeatures: [],
isSaving: false //
isSaving: false, //
//
featureIcons: {
'voiceprintRecognition': voiceprintIcon,
'voiceClone': voiceCloneIcon,
'knowledgeBase': knowledgeBaseIcon,
'mcpAccessPoint': mcpAccessPointIcon,
'addressBook': addressBookIcon,
},
voiceManagementIcons: {
'vad': vadIcon,
'asr': asrIcon,
},
}
},
computed: {
@@ -317,7 +356,9 @@ export default {
}
</script>
<style scoped>
<style lang="scss" scoped>
@import '@/styles/global.scss';
.welcome {
min-width: 900px;
min-height: 506px;
@@ -326,7 +367,7 @@ export default {
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
background: #eff4ff;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
@@ -336,10 +377,11 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
padding: 22px 24px;
}
.page-title {
font-weight: 500;
font-size: 24px;
margin: 0;
}
@@ -376,60 +418,18 @@ export default {
.divider {
height: 1px;
background: #e0e0e0;
background: #f3f1f1;
margin-bottom: 20px;
}
.btn-select-all {
background: #e6ebff;
color: #5778ff;
border: 1px solid #adbdff;
border-radius: 18px;
padding: 8px 16px;
height: 32px;
font-size: 14px;
}
.btn-select-all:hover {
background: #d0d8ff;
}
.save-btn {
background: #5778ff;
color: white;
border: none;
border-radius: 18px;
padding: 8px 16px;
height: 32px;
font-size: 14px;
}
.save-btn:hover {
background: #4a6ae8;
}
.reset-btn {
background: #e6ebff;
color: #5778ff;
border: 1px solid #adbdff;
border-radius: 18px;
padding: 8px 16px;
height: 32px;
}
.reset-btn:hover {
background: #d0d8ff;
}
.main-wrapper {
height: calc(100vh - 63px - 35px - 58px);
margin: 0 22px;
height: calc(100vh - 63px - 35px);
margin: 20px 22px 0;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
overflow: auto;
}
.content-panel {
@@ -463,11 +463,12 @@ export default {
}
.feature-card ::v-deep .el-card__body {
padding: 24px;
padding: 14px 20px;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
overflow: auto;
@include scrollbar-style;
}
.features-grid {
@@ -478,9 +479,8 @@ export default {
.feature-card-item {
display: flex;
flex-direction: column;
padding: 20px;
border-radius: 12px;
border-radius: 10px;
border: 2px solid #e0e0e0;
background-color: white;
cursor: pointer;
@@ -491,25 +491,37 @@ export default {
.feature-card-item:hover {
border-color: #869bf0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.feature-card-item.feature-enabled {
border-color:#5778ff;
box-shadow: 0 4px 16px rgba(95, 112, 243, 0.2);
border: 2px solid transparent;
background: linear-gradient(white, white) padding-box,
linear-gradient(to right, #4a7cfd, #8154fc) border-box;
box-shadow: 0 4px 10px rgba(95, 112, 243, 0.2);
transform: translateY(-2px);
}
.feature-content {
width: 100%;
margin-left: 16px;
}
.feature-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 6px;
}
.feature-checkbox-container {
margin-top: 4px;
display: flex;
align-items: center;
margin-bottom: 12px;
margin-left: 30px;
}
.feature-checkbox ::v-deep .el-checkbox__input {
transform: scale(1.2);
transform: scale(1.5);
}
.feature-checkbox ::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
@@ -521,26 +533,27 @@ export default {
color: #5778ff;
}
.feature-name {
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
font-size: 18px;
line-height: 1.4;
font-weight: 600;
color: #333;
margin: 0;
transition: color 0.3s ease;
}
.feature-description {
font-size: 14px;
line-height: 1.6;
color: #666;
margin: 0 0 12px 0;
margin: 0;
transition: color 0.3s ease;
text-align: left;
}
/* 功能分组容器 - 左右布局 */
.feature-groups-container {
display: flex;
@@ -571,14 +584,40 @@ export default {
}
.group-title {
margin-top: 0;
display: flex;
align-items: center;
font-size: 18px;
line-height: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
padding-left: 12px;
border-left: 4px solid #5f70f3;
margin-bottom: 0;
text-align: left;
}
.group-name {
margin-left: 10px;
}
.group-header {
display: flex;
align-items: center;
}
.module-count {
margin-left: 13px;
font-size: 12px;
background: #ebebfe;
color: #5778ff;
padding: 2px 10px;
border-radius: 10px;
}
.group-description {
margin-top: 6px;
text-align: left;
font-size: 14px;
font-weight: 500;
}
.features-grid {
display: grid;
@@ -0,0 +1,623 @@
<template>
<div class="doc-section">
<div class="doc-section-header">
<div class="doc-section-left">
<div class="doc-section-title">{{`${$t('knowledgeBaseManagement.currentKnowledgeBaseDocuments')}${kb?.name}`}}</div>
<div class="doc-section-count" v-if="kb">{{ $t('knowledgeBaseManagement.totalDocuments', { total }) }}</div>
</div>
<div class="doc-section-actions">
<el-input
:placeholder="$t('knowledgeFileUpload.documentNamePlaceholder')"
v-model="searchDocName"
class="doc-search-input"
size="small"
prefix-icon="el-icon-search"
clearable
/>
<CustomButton size="small" @click="handleSearch">{{ $t('knowledgeBaseManagement.search') }}</CustomButton>
<CustomButton type="add" size="small" @click="handleUpload">{{ $t('knowledgeBaseManagement.add') }}</CustomButton>
<CustomButton type="confirm" size="small" @click="handleRetrievalTest">{{ $t('knowledgeFileUpload.retrievalTest') }}</CustomButton>
</div>
</div>
<div class="doc-grid" v-loading="docLoading" :element-loading-text="$t('knowledgeBaseManagement.loading')">
<div v-for="doc in documents" :key="doc.id" class="doc-card">
<div class="doc-card-top">
<img class="doc-file-icon" :src="getFileIconSrc(doc.name)" />
<div class="doc-card-info">
<div class="doc-card-name" :title="doc.name">{{ doc.name }}</div>
<div class="doc-card-meta">{{ $t('knowledgeFileUpload.uploadTime') }} {{ formatDate(doc.createdAt) }}</div>
</div>
</div>
<div class="doc-card-bottom">
<div class="doc-card-progress">
<div class="progress-ring">
<svg viewBox="0 0 40 40">
<circle class="bg" cx="20" cy="20" r="15"/>
<circle class="fg" :class="getProgressClass(doc)" cx="20" cy="20" r="15"
stroke-dasharray="94.25"
:stroke-dashoffset="getProgressOffset(doc)"/>
</svg>
<span class="progress-ring-text" :style="{ color: getProgressTextColor(doc) }">{{ getProgressText(doc) }}</span>
</div>
<div class="progress-container">
<span class="progress-label" :class="getProgressClass(doc)">{{ getDocStatusText(doc) }}</span>
<span class="doc-slice-count">{{ $t('knowledgeFileUpload.sliceCount') }} <b>{{ doc.sliceCount }}</b></span>
</div>
</div>
<div class="doc-card-actions">
<el-button
v-if="doc.parseStatusCode === 3"
type="text"
@click.stop="$emit('view-slices', doc)"
>
{{ $t('knowledgeFileUpload.viewSlices') }}
</el-button>
<el-button
v-else
type="text"
:disabled="doc.parseStatusCode === 1"
:loading="doc.parsing"
@click.stop="handleParse(doc)"
>
{{ $t('knowledgeFileUpload.parse') }}
</el-button>
<el-button type="text" class="delete-btn" :loading="deleteLoadingMap[doc.id]" @click.stop="handleDelete(doc)">
{{ $t('knowledgeBaseManagement.delete') }}
</el-button>
</div>
</div>
</div>
<div v-if="documents.length === 0 && !docLoading" class="doc-empty">
<el-empty :description="$t('knowledgeBaseManagement.noData')"></el-empty>
</div>
</div>
<CustomPagination
class="pagination-wrapper"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</template>
<script>
import Api from "@/apis/api";
import CustomButton from "@/components/CustomButton.vue";
import CustomPagination from "@/components/CustomPagination.vue";
const iconMap = {
pdf: require('@/assets/knowledge-base/pdf.png'),
doc: require('@/assets/knowledge-base/docX.png'),
docx: require('@/assets/knowledge-base/docX.png'),
xls: require('@/assets/knowledge-base/xlsx.png'),
xlsx: require('@/assets/knowledge-base/xlsx.png'),
csv: require('@/assets/knowledge-base/xlsx.png'),
md: require('@/assets/knowledge-base/MD.png'),
markdown: require('@/assets/knowledge-base/MD.png'),
txt: require('@/assets/knowledge-base/txt.png'),
};
export default {
name: 'LibraryItem',
components: { CustomPagination, CustomButton },
props: {
kb: {
type: Object,
default: null
}
},
data() {
return {
documents: [],
docLoading: false,
deleteLoadingMap: {},
searchDocName: '',
total: 0,
currentPage: 1,
pageSize: 10,
pageSizeOptions: [10, 20, 50, 100],
};
},
watch: {
kb: {
handler(newKb) {
if (newKb && newKb.datasetId) {
this.currentPage = 1;
this.fetchDocuments();
} else {
this.documents = [];
this.total = 0;
}
},
immediate: true
}
},
methods: {
fetchDocuments() {
if (!this.kb || !this.kb.datasetId) return;
this.docLoading = true;
const params = {
page: this.currentPage,
page_size: this.pageSize,
name: this.searchDocName
};
Api.knowledgeBase.getDocumentList(
this.kb.datasetId,
params,
({ data }) => {
this.docLoading = false;
if (data && data.code === 0) {
this.documents = data.data.list || [];
this.total = data.data.total || 0;
this.fetchSliceCountsForDocuments();
} else {
this.$message.error(data?.msg || '获取文档列表失败');
this.documents = [];
this.total = 0;
}
},
(err) => {
this.docLoading = false;
if (err && err.data) {
this.$message.error(err.data.msg || err.msg || '获取文档列表失败');
} else {
this.$message.error('获取文档列表失败');
}
this.documents = [];
this.total = 0;
}
);
},
handleSearch() {
this.currentPage = 1;
this.fetchDocuments();
},
handlePageChange(page) {
this.currentPage = page;
this.fetchDocuments();
},
handleSizeChange(size) {
this.pageSize = size;
this.currentPage = 1;
this.fetchDocuments();
},
handleUpload() {
this.$emit('upload');
},
handleRetrievalTest() {
this.$emit('retrieval-test');
},
handleDelete(doc) {
if (!this.kb) return;
this.$confirm(
this.$t('knowledgeFileUpload.confirmDelete'),
this.$t('message.warning'),
{
confirmButtonText: this.$t('knowledgeBaseDialog.confirm'),
cancelButtonText: this.$t('knowledgeBaseDialog.cancel'),
type: 'warning'
}
).then(() => {
this.$set(this.deleteLoadingMap, doc.id, true);
Api.knowledgeBase.deleteDocument(this.kb.datasetId, doc.id, (res) => {
this.$set(this.deleteLoadingMap, doc.id, false);
if (res.data && res.data.code === 0) {
if (this.documents.length === 1 && this.currentPage > 1) {
this.currentPage--;
}
this.fetchDocuments();
this.$message.success(this.$t('knowledgeFileUpload.deleteSuccess'));
} else {
this.$message.error(res.data?.msg || this.$t('knowledgeFileUpload.deleteFailed'));
}
}, (err) => {
this.$set(this.deleteLoadingMap, doc.id, false);
this.$message.error(err?.data?.msg ||this.$t('knowledgeFileUpload.deleteFailed'));
});
}).catch(() => {});
},
handleParse(doc) {
this.$confirm(this.$t('knowledgeFileUpload.confirmParse'), this.$t('message.info'), {
confirmButtonText: this.$t('knowledgeFileUpload.confirm'),
cancelButtonText: this.$t('knowledgeFileUpload.cancel'),
type: 'info'
}).then(() => {
this.$set(doc, 'parsing', true);
doc.parseStatusCode = 1;
Api.knowledgeBase.parseDocument(
this.kb.datasetId,
doc.id,
({ data }) => {
if (data && data.code === 0) {
this.$message.success(this.$t('knowledgeFileUpload.parsing'));
this.$set(doc, 'parsing', false);
this.startParsePolling(doc);
} else {
this.$set(doc, 'parsing', false);
doc.parseStatusCode = 0;
this.$message.error(data?.msg || this.$t('knowledgeFileUpload.parseFailed'));
}
},
(err) => {
this.$set(doc, 'parsing', false);
doc.parseStatusCode = 0;
this.$message.error(err?.data?.msg || this.$t('knowledgeFileUpload.parseFailed'));
}
);
}).catch(() => {});
},
startParsePolling(doc) {
const poll = () => {
if (!this.kb) return;
Api.knowledgeBase.getDocumentList(
this.kb.datasetId,
{ page: 1, page_size: 100, name: this.searchDocName },
({ data }) => {
if (data && data.code === 0) {
const list = data.data.list || [];
const updated = list.find(d => d.id === doc.id);
if (updated) {
this.$set(doc, 'parseStatusCode', updated.parseStatusCode);
if (updated.parseStatusCode !== 1) {
this.$set(doc, 'parsing', false);
if (updated.parseStatusCode === 3) {
this.$message.success(`${doc.name}${this.$t('knowledgeFileUpload.parseSuccess')}`);
this.fetchSliceCountForSingleDocument(doc);
} else if (updated.parseStatusCode === 4) {
this.$message.error(`${doc.name}${this.$t('knowledgeFileUpload.parseFailed')}`);
}
return;
}
}
setTimeout(poll, 5000);
}
},
() => {
setTimeout(poll, 5000);
}
);
};
setTimeout(poll, 3000);
},
fetchSliceCountsForDocuments() {
if (!this.documents || this.documents.length === 0) return;
this.documents.forEach(doc => {
this.fetchSliceCountForSingleDocument(doc);
});
},
fetchSliceCountForSingleDocument(doc) {
const params = { page: 1, page_size: 1 };
Api.knowledgeBase.listChunks(
this.kb.datasetId,
doc.id,
params,
({ data }) => {
if (data && data.code === 0) {
this.$set(doc, 'sliceCount', data.data.total || 0);
}
},
() => {}
);
},
getFileIconSrc(fileName) {
if (!fileName) return require('@/assets/knowledge-base/default.png');
const ext = fileName.split('.').pop().toLowerCase();
return iconMap[ext] || require('@/assets/knowledge-base/default.png');
},
getProgressClass(doc) {
const code = doc.parseStatusCode;
if (code === 3) return 'complete';
if (code === 1) return 'processing';
return 'pending';
},
getProgressOffset(doc) {
const code = doc.parseStatusCode;
const circumference = 94.25;
if (code === 3) return 0;
if (code === 1) {
const progress = doc.chunkNum && doc.totalChunkNum
? Math.round((doc.chunkNum / doc.totalChunkNum) * 100)
: 50;
return circumference * (1 - progress / 100);
}
return circumference;
},
getProgressText(doc) {
const code = doc.parseStatusCode;
if (code === 3) return '100%';
if (code === 1) {
if (doc.chunkNum && doc.totalChunkNum) {
return Math.round((doc.chunkNum / doc.totalChunkNum) * 100) + '%';
}
return '50%';
}
return '0%';
},
getProgressTextColor(doc) {
const code = doc.parseStatusCode;
if (code === 3) return '#34c759';
if (code === 1) return '#2f5bff';
return '#7888a8';
},
getDocStatusText(doc) {
const code = doc.parseStatusCode;
if (code === 3) return this.$t('knowledgeFileUpload.statusCompleted');
if (code === 1) return this.$t('knowledgeFileUpload.statusProcessing');
if (code === 0) return this.$t('knowledgeFileUpload.statusNotStarted');
if (code === 4) return this.$t('knowledgeFileUpload.statusFailed');
if (code === 2) return this.$t('knowledgeFileUpload.statusCancelled');
return this.$t('knowledgeFileUpload.statusNotStarted');
},
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
},
}
};
</script>
<style lang="scss" scoped>
/* ========== Document Section ========== */
.doc-section {
background: #fff;
border-radius: 10px;
padding: 16px 20px;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.doc-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.doc-section-left {
display: flex;
align-items: center;
gap: 12px;
}
.doc-section-title {
font-size: 18px;
font-weight: 500;
color: #1f2a44;
}
.doc-section-count {
font-size: 18px;
color: #788ba8;
}
.doc-section-actions {
display: flex;
align-items: center;
}
.doc-search-input {
width: 200px;
margin-right: 10px;
}
/* ========== Document Grid ========== */
.doc-grid {
padding: 10px;
display: grid;
grid-template-columns: repeat(auto-fill,minmax(300px, 1fr));
gap: 16px 20px;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: thin;
}
.doc-card {
max-height: 110px;
border-radius: 8px;
border: 1px solid transparent;
padding: 18px;
background: #fff;
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
transition: all 0.25s;
position: relative;
box-shadow: 0 0 6px rgba(47, 91, 255, 0.15);
&:hover {
box-shadow: 0 0 6px rgba(47, 91, 255, 0.15);
border: 1px solid #6b80eb;
}
}
.doc-card-top {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 12px;
}
.doc-file-icon {
width: 46px;
height: 46px;
border-radius: 10px;
object-fit: contain;
flex-shrink: 0;
}
.doc-card-info {
flex: 1;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
text-align: left;
}
.doc-card-name {
font-size: 15px;
font-weight: 500;
color: #1f2a44;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.doc-card-meta {
font-size: 13px;
color: #7888a8;
}
.doc-card-bottom {
display: flex;
align-items: center;
justify-content: space-between;
}
.doc-card-progress {
display: flex;
align-items: center;
gap: 8px;
}
/* ========== Progress Ring ========== */
.progress-ring {
width: 40px;
height: 40px;
position: relative;
svg {
width: 40px;
height: 40px;
transform: rotate(-90deg);
}
.bg {
fill: none;
stroke: #e7ecf5;
stroke-width: 4;
}
.fg {
fill: none;
stroke-width: 4;
stroke-linecap: round;
transition: stroke-dashoffset 0.5s ease;
&.complete { stroke: #34c759; }
&.processing { stroke: #2f5bff; }
&.pending { stroke: #e7ecf5; }
}
}
.progress-ring-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 10px;
font-weight: 600;
color: #7888a8;
}
.progress-container {
height: 36px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
.progress-label {
font-size: 12px;
color: #7888a8;
&.complete { color: #34c759; }
&.processing { color: #2f5bff; }
}
.doc-slice-count {
font-size: 12px;
> b {
font-weight: bold;
font-size: 14px;
}
}
}
.doc-card-actions {
display: flex;
align-items: center;
gap: 4px;
:deep(.el-button--text) {
padding: 4px 8px;
font-size: 13px;
}
.delete-btn {
color: #f56c6c !important;
&:hover {
color: #f78989 !important;
}
}
}
.doc-empty {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 300px;
color: #b0bbc9;
font-size: 14px;
i {
font-size: 48px;
margin-bottom: 12px;
}
}
.pagination-wrapper {
margin-top: 20px;
}
@media (max-width: 1144px) {
.doc-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.doc-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+68 -259
View File
@@ -105,7 +105,8 @@
>
<el-switch
v-model="scope.row.isEnabled"
class="custom-switch"
active-color="#5778ff"
inactive-color="#DCDFE6"
:active-value="1"
:inactive-value="0"
disabled
@@ -115,7 +116,8 @@
<el-switch
v-else
v-model="scope.row.isEnabled"
class="custom-switch"
active-color="#5778ff"
inactive-color="#DCDFE6"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange(scope.row)"
@@ -126,7 +128,8 @@
<template slot-scope="scope">
<el-switch
v-model="scope.row.isDefault"
class="custom-switch"
active-color="#5778ff"
inactive-color="#DCDFE6"
:active-value="1"
:inactive-value="0"
@change="handleDefaultChange(scope.row)"
@@ -184,76 +187,33 @@
</el-table>
<div class="table-footer">
<div class="batch-actions">
<el-button size="mini" type="primary" @click="selectAll">
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" type="default" size="small" @click="selectAll">
{{
isAllSelected
? $t("modelConfig.deselectAll")
: $t("modelConfig.selectAll")
}}
</el-button>
<el-button type="success" size="mini" @click="addModel" class="add-btn">
</CustomButton>
<CustomButton icon="el-icon-plus" type="add" size="small" @click="addModel">
{{ $t("modelConfig.add") }}
</el-button>
<el-button
size="mini"
type="danger"
</CustomButton>
<CustomButton
type="delete"
size="small"
icon="el-icon-delete"
@click="batchDelete"
>
{{ $t("modelConfig.delete") }}
</el-button>
</div>
<div class="custom-pagination">
<el-select
v-model="pageSize"
@change="handlePageSizeChange"
class="page-size-select"
>
<el-option
v-for="item in pageSizeOptions"
:key="item"
:label="$t('modelConfig.itemsPerPage', { items: item })"
:value="item"
>
</el-option>
</el-select>
<button
class="pagination-btn"
:disabled="currentPage === 1"
@click="goFirst"
>
{{ $t("modelConfig.firstPage") }}
</button>
<button
class="pagination-btn"
:disabled="currentPage === 1"
@click="goPrev"
>
{{ $t("modelConfig.prevPage") }}
</button>
<button
v-for="page in visiblePages"
:key="page"
class="pagination-btn"
:class="{ active: page === currentPage }"
@click="goToPage(page)"
>
{{ page }}
</button>
<button
class="pagination-btn"
:disabled="currentPage === pageCount"
@click="goNext"
>
{{ $t("modelConfig.nextPage") }}
</button>
<span class="total-text">{{
$t("modelConfig.totalRecords", { total })
}}</span>
</CustomButton>
</div>
<CustomPagination
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@size-change="handlePageSizeChange"
@page-change="handlePageChange"
/>
</div>
</el-card>
</div>
@@ -288,9 +248,11 @@ import AddModelDialog from "@/components/AddModelDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import ModelEditDialog from "@/components/ModelEditDialog.vue";
import TtsModel from "@/components/TtsModel.vue";
import CustomPagination from "@/components/CustomPagination.vue";
import CustomButton from "@/components/CustomButton.vue";
import VersionFooter from "@/components/VersionFooter.vue";
export default {
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog, VersionFooter },
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog, VersionFooter, CustomPagination, CustomButton },
data() {
return {
addDialogVisible: false,
@@ -334,24 +296,6 @@ export default {
this.$t("modelConfig." + this.activeTab) || this.$t("modelConfig.modelConfig")
);
},
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
},
methods: {
@@ -481,9 +425,9 @@ export default {
this.$message.info(this.$t("modelConfig.deleteCancelled"));
});
},
handleCurrentChange(page) {
handlePageChange(page) {
this.currentPage = page;
this.$refs.modelTable.clearSelection();
this.loadData();
},
handleModelSave({ provideCode, formData, done }) {
const modelType = this.activeTab;
@@ -558,28 +502,6 @@ export default {
});
},
//
goFirst() {
this.currentPage = 1;
this.loadData();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.loadData();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.loadData();
}
},
goToPage(page) {
this.currentPage = page;
this.loadData();
},
//
loadData() {
this.loading = true; //
@@ -654,7 +576,7 @@ export default {
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
background: #eff4ff;
-webkit-background-size: cover;
-o-background-size: cover;
}
@@ -664,9 +586,7 @@ export default {
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
position: relative;
background: rgba(237, 242, 255, 0.5);
}
.operation-bar {
@@ -793,50 +713,6 @@ export default {
transition: border-color 0.2s;
}
::v-deep .page-size-select {
width: 100px;
margin-right: 8px;
}
::v-deep .page-size-select .el-input__inner {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
::v-deep .page-size-select .el-input__suffix {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
::v-deep .page-size-select .el-input__suffix-inner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
::v-deep .page-size-select .el-icon-arrow-up:before {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
::v-deep .search-input .el-input__inner:focus {
border-color: #6b8cff;
outline: none;
@@ -869,53 +745,53 @@ export default {
background: white;
}
.batch-actions {
display: flex;
gap: 8px;
}
// .batch-actions {
// display: flex;
// gap: 8px;
// }
.batch-actions .el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
// .batch-actions .el-button {
// min-width: 72px;
// height: 32px;
// padding: 7px 12px 7px 10px;
// font-size: 12px;
// border-radius: 4px;
// line-height: 1;
// font-weight: 500;
// border: none;
// transition: all 0.3s ease;
// box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
// }
.batch-actions .el-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
// .batch-actions .el-button:hover {
// transform: translateY(-1px);
// box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
// }
.batch-actions .el-button--primary {
background: #5f70f3 !important;
color: white;
}
// .batch-actions .el-button--primary {
// background: #5f70f3 !important;
// color: white;
// }
.batch-actions .el-button--success {
background: #5bc98c;
color: white;
}
// .batch-actions .el-button--success {
// background: #5bc98c;
// color: white;
// }
.batch-actions .el-button--danger {
background: #fd5b63;
color: white;
}
// .batch-actions .el-button--danger {
// background: #fd5b63;
// color: white;
// }
.batch-actions .el-button:first-child {
background: linear-gradient(135deg, #409eff, #6b8cff);
border: none;
color: white;
}
// .batch-actions .el-button:first-child {
// background: linear-gradient(135deg, #409eff, #6b8cff);
// border: none;
// color: white;
// }
.batch-actions .el-button:first-child:hover {
background: linear-gradient(135deg, #3a8ee6, #5a7cff);
}
// .batch-actions .el-button:first-child:hover {
// background: linear-gradient(135deg, #3a8ee6, #5a7cff);
// }
.el-table th ::v-deep .el-table__cell {
overflow: hidden;
@@ -1018,73 +894,6 @@ export default {
padding-right: 10px;
}
/* 分页器 */
.custom-pagination {
display: flex;
align-items: center;
gap: 8px;
/* 导航按钮样式 (首页、上一页、下一页) */
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-child(3),
.pagination-btn:nth-last-child(2) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
/* 数字按钮样式 */
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
.total-text {
color: #909399;
font-size: 14px;
margin-left: 10px;
}
}
.model-card {
background: white;
flex: 1;
+88 -416
View File
@@ -1,95 +1,67 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('otaManagement.firmwareManagement') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('otaManagement.searchPlaceholder')" v-model="searchName" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('otaManagement.search') }}</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="params-card" shadow="never">
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)"
:header-cell-class-name="headerCellClassName">
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
</el-table-column>
<el-table-column :label="$t('otaManagement.firmwareName')" prop="firmwareName" align="center"></el-table-column>
<el-table-column :label="$t('otaManagement.firmwareType')" prop="type" align="center">
<template slot-scope="scope">
{{ getFirmwareTypeName(scope.row.type) }}
</template>
</el-table-column>
<el-table-column :label="$t('otaManagement.version')" prop="version" align="center"></el-table-column>
<el-table-column :label="$t('otaManagement.fileSize')" prop="size" align="center">
<template slot-scope="scope">
{{ formatFileSize(scope.row.size) }}
</template>
</el-table-column>
<el-table-column :label="$t('otaManagement.remark')" prop="remark" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('otaManagement.createTime')" prop="createDate" align="center">
<template slot-scope="scope">
{{ formatDate(scope.row.createDate) }}
</template>
</el-table-column>
<el-table-column :label="$t('otaManagement.updateTime')" prop="updateDate" align="center">
<template slot-scope="scope">
{{ formatDate(scope.row.updateDate) }}
</template>
</el-table-column>
<el-table-column :label="$t('otaManagement.action')" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text"
@click="downloadFirmware(scope.row)">{{ $t('otaManagement.download') }}</el-button>
<el-button size="mini" type="text" @click="editParam(scope.row)">{{ $t('otaManagement.edit') }}</el-button>
<el-button size="mini" type="text" @click="deleteParam(scope.row)">{{ $t('otaManagement.delete') }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? $t('otaManagement.deselectAll') : $t('otaManagement.selectAll') }}
</el-button>
<el-button size="mini" type="success" @click="showAddDialog"
style="background: #5bc98c;border: None;">{{ $t('otaManagement.addNew') }}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete"
@click="deleteSelectedParams">{{ $t('otaManagement.delete') }}</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('otaManagement.itemsPerPage', { items: item })"
:value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('otaManagement.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('otaManagement.prevPage') }}
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('otaManagement.nextPage') }}
</button>
<span class="total-text">{{ $t('otaManagement.totalRecords', { total }) }}</span>
<div class="operation-header">
<h2 class="page-title">{{ $t('otaManagement.firmwareManagement') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('otaManagement.searchPlaceholder')" v-model="searchName"
class="search-input" @keyup.enter.native="handleSearch" clearable />
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">
{{ $t('otaManagement.search') }}
</CustomButton>
</div>
</div>
<CustomTable ref="paramsTable" :data="paramsList" :columns="tableColumns" :loading="loading"
:show-selection="true" :show-operations="true" :operations-label="$t('otaManagement.action')"
:total="total" :current-page="currentPage" :page-size="pageSize"
:page-size-options="pageSizeOptions" @size-change="handlePageSizeChange"
@page-change="goToPage">
<template slot="selection" slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
<template slot="type" slot-scope="scope">
{{ getFirmwareTypeName(scope.row.type) }}
</template>
<template slot="size" slot-scope="scope">
{{ formatFileSize(scope.row.size) }}
</template>
<template slot="createDate" slot-scope="scope">
{{ formatDate(scope.row.createDate) }}
</template>
<template slot="updateDate" slot-scope="scope">
{{ formatDate(scope.row.updateDate) }}
</template>
<template slot="operations" slot-scope="scope">
<el-button size="mini" type="text"
@click="downloadFirmware(scope.row)">{{ $t('otaManagement.download') }}</el-button>
<el-button size="mini" type="text" @click="editParam(scope.row)">
{{ $t('otaManagement.edit') }}
</el-button>
<el-button size="mini" type="text" @click="deleteParam(scope.row)">
{{ $t('otaManagement.delete') }}
</el-button>
</template>
<template slot="footer-btns">
<div class="ctrl_btn">
<CustomButton
:icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'"
size="small" @click="handleSelectAll">
{{ isAllSelected ? $t('otaManagement.deselectAll') : $t('otaManagement.selectAll') }}
</CustomButton>
<CustomButton type="add" icon="el-icon-plus" size="small" @click="showAddDialog">
{{ $t('otaManagement.addNew') }}
</CustomButton>
<CustomButton size="small" type="delete" icon="el-icon-delete"
@click="deleteSelectedParams">
{{ $t('otaManagement.delete') }}
</CustomButton>
</div>
</template>
</CustomTable>
</el-card>
</div>
</div>
@@ -97,7 +69,7 @@
<!-- 新增/编辑固件对话框 -->
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm"
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
<el-footer>
<version-footer />
</el-footer>
@@ -109,11 +81,12 @@ import Api from "@/apis/api";
import FirmwareDialog from "@/components/FirmwareDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import CustomButton from "@/components/CustomButton.vue";
import CustomTable from "@/components/CustomTable.vue";
import { formatDate, formatFileSize } from "@/utils/format";
import i18n from '@/i18n';
export default {
components: { HeaderBar, FirmwareDialog, VersionFooter },
components: { HeaderBar, FirmwareDialog, VersionFooter, CustomButton, CustomTable },
data() {
return {
searchName: "",
@@ -137,34 +110,26 @@ export default {
firmwarePath: ""
},
firmwareTypes: [],
tableColumns: []
};
},
created() {
this.initTableColumns();
this.fetchFirmwareList();
this.getFirmwareTypes();
},
computed: {
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
},
methods: {
initTableColumns() {
this.tableColumns = [
{ prop: 'firmwareName', label: this.$t('otaManagement.firmwareName'), align: 'center' },
{ prop: 'type', label: this.$t('otaManagement.firmwareType'), align: 'center' },
{ prop: 'version', label: this.$t('otaManagement.version'), align: 'center' },
{ prop: 'size', label: this.$t('otaManagement.fileSize'), align: 'center' },
{ prop: 'remark', label: this.$t('otaManagement.remark'), align: 'center' },
{ prop: 'createDate', label: this.$t('otaManagement.createTime'), align: 'center' },
{ prop: 'updateDate', label: this.$t('otaManagement.updateTime'), align: 'center' }
];
},
handlePageSizeChange(val) {
this.pageSize = val;
this.currentPage = 1;
@@ -194,7 +159,7 @@ export default {
this.paramsList = [];
this.total = 0;
this.$message.error({
message: res?.data?.msg || this.$t('otaManagement.getFirmwareListFailed'),
message: res?.data?.msg || this.$t('otaManagement.fetchFirmwareListFailed'),
showClose: true
});
}
@@ -212,7 +177,6 @@ export default {
},
showAddDialog() {
this.dialogTitle = this.$t('otaManagement.addFirmware');
//
this.firmwareForm = {
id: null,
firmwareName: "",
@@ -222,12 +186,6 @@ export default {
remark: "",
firmwarePath: ""
};
this.$nextTick(() => {
//
if (this.$refs.firmwareDialog && this.$refs.firmwareDialog.$refs.form) {
this.$refs.firmwareDialog.$refs.form.clearValidate();
}
});
this.dialogVisible = true;
},
editParam(row) {
@@ -237,7 +195,6 @@ export default {
},
handleSubmit(form) {
if (form.id) {
//
Api.ota.updateOta(form.id, form, (res) => {
res = res.data;
if (res.code === 0) {
@@ -255,7 +212,6 @@ export default {
}
});
} else {
//
Api.ota.saveOta(form, (res) => {
res = res.data;
if (res.code === 0) {
@@ -274,7 +230,6 @@ export default {
});
}
},
deleteSelectedParams() {
const selectedRows = this.firmwareList.filter(row => row.selected);
if (selectedRows.length === 0) {
@@ -287,7 +242,6 @@ export default {
this.deleteParam(selectedRows);
},
deleteParam(row) {
//
const params = Array.isArray(row) ? row : [row];
if (Array.isArray(row) && row.length === 0) {
@@ -345,38 +299,17 @@ export default {
}
});
},
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return "custom-selection-header";
}
return "";
},
goFirst() {
this.currentPage = 1;
this.fetchFirmwareList();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchFirmwareList();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
this.currentPage++;
this.fetchFirmwareList();
}
},
goToPage(page) {
this.currentPage = page;
this.fetchFirmwareList();
if (page !== this.currentPage) {
this.currentPage = page;
this.fetchFirmwareList();
}
},
downloadFirmware(firmware) {
if (!firmware || !firmware.id) {
this.$message.error(this.$t('otaManagement.incompleteFirmwareInfo'));
return;
}
//
Api.ota.getDownloadUrl(firmware.id, (res) => {
if (res.data.code === 0) {
const uuid = res.data.data;
@@ -415,32 +348,30 @@ export default {
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
background: #eff4ff;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
height: calc(100vh - 63px - 35px);
padding: 20px 22px 0;
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.operation-bar {
.operation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
justify-content: space-between;
padding: 0 0 16px 0;
}
.page-title {
font-weight: 500;
font-size: 24px;
margin: 0;
}
@@ -455,14 +386,7 @@ export default {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
@@ -491,7 +415,7 @@ export default {
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
padding: 14px 20px;
display: flex;
flex-direction: column;
flex: 1;
@@ -499,267 +423,15 @@ export default {
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
.custom-pagination {
display: flex;
align-items: center;
gap: 5px;
.el-select {
margin-right: 8px;
}
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2),
.pagination-btn:nth-child(3) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
}
.total-text {
margin-left: 10px;
color: #606266;
font-size: 14px;
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
:deep(.transparent-table) {
background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper {
flex-shrink: 0;
}
.el-table__header th {
background: white !important;
color: black;
font-weight: 600;
height: 40px;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid #e4e7ed;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
padding: 8px 0;
height: 40px;
color: #606266;
font-size: 14px;
}
}
.el-table__row:hover>td {
background-color: #f5f7fa !important;
}
&::before {
display: none;
}
}
:deep(.el-table .el-button--text) {
color: #7079aa !important;
color: #7079aa;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5 !important;
}
:deep(.el-checkbox__inner) {
background-color: #ffffff !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
.el-table {
// --table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
color: #5a64b5;
}
</style>
File diff suppressed because it is too large Load Diff
+132 -559
View File
@@ -1,49 +1,20 @@
<template>
<div class="welcome">
<HeaderBar />
<div class="operation-bar">
<h2 class="page-title">{{ $t('header.providerManagement') }}</h2>
<div class="right-operations">
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
<el-button class="category-btn">
{{ $t('providerManagement.categoryFilter') }} {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
:class="{ 'rotate-down': DropdownVisible }"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-input :placeholder="$t('providerManagement.searchPlaceholder')" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
clearable />
<el-button class="btn-search" @click="handleSearch">{{ $t('common.search') }}</el-button>
</div>
</div>
<div class="main-wrapper">
<div class="content-panel">
<div class="content-area">
<el-card class="provider-card" shadow="never">
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
</el-table-column>
<el-table-column :label="$t('providerManagement.category')" prop="modelType" align="center" width="200">
<template slot="header" slot-scope="scope">
<el-dropdown trigger="click" @command="handleSelectModelType"
@visible-change="isDropdownOpen = $event">
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
{{ $t('providerManagement.category') }}{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
:class="{ 'is-active': isDropdownOpen }"></i>
</span>
<div class="operation-header">
<h2 class="page-title">{{ $t('header.providerManagement') }}</h2>
<div class="right-operations">
<el-input :placeholder="$t('providerManagement.searchPlaceholder')" v-model="searchName" class="search-input"
@keyup.enter.native="handleSearch" clearable />
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
<CustomButton>
{{ $t('providerManagement.categoryFilter') }}{{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
:class="{ 'rotate-down': DropdownVisible }"></i>
</CustomButton>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
@@ -51,66 +22,60 @@
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<template slot-scope="scope">
<el-tag :type="getModelTypeTag(scope.row.modelType)">
{{ getModelTypeLabel(scope.row.modelType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('providerManagement.providerCode')" prop="providerCode" align="center" width="150"></el-table-column>
<el-table-column :label="$t('common.name')" prop="name" align="center"></el-table-column>
<el-table-column :label="$t('providerManagement.fieldConfig')" align="center">
<template slot-scope="scope">
<el-popover placement="top-start" width="400" trigger="hover">
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
<span class="field-label">{{ field.label }}:</span>
<span class="field-type">{{ field.type }}</span>
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">{{ $t('common.sensitive') }}</span>
</div>
<el-button slot="reference" size="mini" type="text">{{ $t('providerManagement.viewFields') }}</el-button>
</el-popover>
</template>
</el-table-column>
<el-table-column :label="$t('common.sort')" prop="sort" align="center" width="80"></el-table-column>
<el-table-column :label="$t('common.action')" align="center" width="180">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="editProvider(scope.row)">{{ $t('common.edit') }}</el-button>
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">{{ $t('common.delete') }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="table_bottom">
<div class="ctrl_btn">
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
{{ isAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</el-button>
<el-button size="mini" type="success" @click="showAddDialog">{{ $t('common.add') }}</el-button>
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">{{ $t('common.delete') }}
</el-button>
</div>
<div class="custom-pagination">
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('common.perPage', { number: item })" :value="item">
</el-option>
</el-select>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
{{ $t('common.firstPage') }}
</button>
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
{{ $t('common.prevPage') }}
</button>
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
:class="{ active: page === currentPage }" @click="goToPage(page)">
{{ page }}
</button>
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
{{ $t('common.nextPage') }}
</button>
<span class="total-text">{{ $t('common.totalRecords', { number: total }) }}</span>
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">{{ $t('common.search') }}</CustomButton>
</div>
</div>
<CustomTable
ref="providersTable"
:data="filteredProvidersList"
:columns="tableColumns"
:loading="loading"
:show-selection="true"
:show-operations="true"
:operations-label="$t('common.action')"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@size-change="handlePageSizeChange"
@page-change="goToPage"
>
<template slot="selection" slot-scope="scope">
<el-checkbox v-model="scope.row.selected"></el-checkbox>
</template>
<template slot="modelType" slot-scope="scope">
<el-tag :type="getModelTypeTag(scope.row.modelType)">
{{ getModelTypeLabel(scope.row.modelType) }}
</el-tag>
</template>
<template slot="fields" slot-scope="scope">
<el-popover placement="top-start" width="400" trigger="hover">
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
<span class="field-label">{{ field.label }}:</span>
<span class="field-type">{{ field.type }}</span>
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">{{ $t('common.sensitive') }}</span>
</div>
<el-button slot="reference" size="mini" type="text">{{ $t('providerManagement.viewFields') }}</el-button>
</el-popover>
</template>
<template slot="operations" slot-scope="scope">
<el-button size="mini" type="text" @click="editProvider(scope.row)">{{ $t('common.edit') }}</el-button>
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">{{ $t('common.delete') }}</el-button>
</template>
<template slot="footer-btns">
<div class="ctrl_btn">
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" @click="handleSelectAll">
{{ isAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
</CustomButton>
<CustomButton type="add" icon="el-icon-plus" size="small" @click="showAddDialog">
{{ $t('common.add') }}
</CustomButton>
<CustomButton size="small" type="delete" icon="el-icon-delete" @click="deleteSelectedProviders">
{{ $t('common.delete') }}
</CustomButton>
</div>
</template>
</CustomTable>
</el-card>
</div>
</div>
@@ -131,9 +96,11 @@ import Api from "@/apis/api";
import HeaderBar from "@/components/HeaderBar.vue";
import ProviderDialog from "@/components/ProviderDialog.vue";
import VersionFooter from "@/components/VersionFooter.vue";
import CustomButton from "@/components/CustomButton.vue";
import CustomTable from "@/components/CustomTable.vue";
export default {
components: { HeaderBar, ProviderDialog, VersionFooter },
components: { HeaderBar, ProviderDialog, VersionFooter, CustomButton, CustomTable },
data() {
return {
searchName: "",
@@ -158,7 +125,7 @@ export default {
dialogVisible: false,
dialogTitle: "新增供应器",
isAllSelected: false,
isDropdownOpen: false,
DropdownVisible: false,
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
providerForm: {
id: null,
@@ -168,10 +135,11 @@ export default {
fields: [],
sort: 0
},
DropdownVisible: false,
tableColumns: []
};
},
created() {
this.initTableColumns();
this.fetchProviders();
},
computed: {
@@ -186,41 +154,43 @@ export default {
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
return selectedType ? `${this.$t(selectedType.labelKey)}` : "";
},
pageCount() {
return Math.ceil(this.total / this.pageSize);
},
visiblePages() {
const pages = [];
const maxVisible = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.pageCount, start + maxVisible - 1);
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
filteredProvidersList() {
return this.providersList;
// let list = this.providersList.filter(item => {
// const nameMatch = item.name.toLowerCase().includes(this.searchName.toLowerCase());
// const typeMatch = !this.searchModelType || item.model_type === this.searchModelType;
// return nameMatch && typeMatch;
// });
// list.sort((a, b) => a.sort - b.sort);
// //
// const start = (this.currentPage - 1) * this.pageSize;
// return list.slice(start, start + this.pageSize);
}
},
methods: {
initTableColumns() {
this.tableColumns = [
{
prop: 'modelType',
label: this.$t('providerManagement.category'),
align: 'center',
width: 200
},
{
prop: 'providerCode',
label: this.$t('providerManagement.providerCode'),
align: 'center',
width: 150
},
{
prop: 'name',
label: this.$t('common.name'),
align: 'center'
},
{
prop: 'fields',
label: this.$t('providerManagement.fieldConfig'),
align: 'center'
},
{
prop: 'sort',
label: this.$t('common.sort'),
align: 'center',
width: 80
}
];
},
fetchProviders() {
this.loading = true;
@@ -255,7 +225,6 @@ export default {
this.fetchProviders();
},
handleSelectModelType(value) {
this.isDropdownOpen = false;
this.searchModelType = value;
this.handleSearch();
},
@@ -288,26 +257,23 @@ export default {
handleSubmit({ form, done }) {
this.loading = true;
if (form.id) {
//
Api.model.updateModelProvider(form, ({ data }) => {
if (data.code === 0) {
this.fetchProviders(); //
this.fetchProviders();
this.$message.success({
message: this.$t('common.updateSuccess'),
showClose: true
});
message: this.$t('common.updateSuccess'),
showClose: true
});
}
});
} else {
//
Api.model.addModelProvider(form, ({ data }) => {
if (data.code === 0) {
this.fetchProviders(); //
this.fetchProviders();
this.$message.success({
message: this.$t('common.addSuccess'),
showClose: true
});
message: this.$t('common.addSuccess'),
showClose: true
});
this.total += 1;
}
});
@@ -339,10 +305,8 @@ export default {
const ids = providers.map(provider => provider.id);
Api.model.deleteModelProviderByIds(ids, ({ data }) => {
if (data.code === 0) {
this.isAllSelected = false;
this.fetchProviders(); //
this.fetchProviders();
this.$message.success({
message: this.$t('common.deleteSuccess'),
showClose: true
@@ -390,57 +354,16 @@ export default {
this.currentPage = 1;
this.fetchProviders();
},
headerCellClassName({ columnIndex }) {
if (columnIndex === 0) {
return "custom-selection-header";
}
return "";
},
selectionCellClassName() {
return "custom-selection-cell";
},
updateSelectionHeaderText() {
//
this.$nextTick(() => {
if (this.$refs.providersTable && this.$refs.providersTable.$el) {
const headerCheckbox = this.$refs.providersTable.$el.querySelector('.custom-selection-header .el-checkbox .el-checkbox__label');
if (headerCheckbox) {
headerCheckbox.textContent = this.$t('modelConfig.select');
}
}
});
},
mounted() {
this.updateSelectionHeaderText();
},
updated() {
this.updateSelectionHeaderText();
},
goFirst() {
this.currentPage = 1;
this.fetchProviders();
},
goPrev() {
if (this.currentPage > 1) {
this.currentPage--;
this.fetchProviders();
}
},
goNext() {
if (this.currentPage < this.pageCount) {
console.log("this.currentPage", this.currentPage);
this.currentPage++;
this.fetchProviders();
}
},
goToPage(page) {
this.currentPage = page;
this.fetchProviders();
if (page !== this.currentPage) {
this.currentPage = page;
this.fetchProviders();
}
},
handleDropdownVisibleChange(visible) {
this.DropdownVisible = visible;
},
},
}
}
};
</script>
@@ -453,32 +376,31 @@ export default {
position: relative;
flex-direction: column;
background-size: cover;
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
background: #eff4ff;
-webkit-background-size: cover;
-o-background-size: cover;
overflow: hidden;
}
.main-wrapper {
// 63px 35px 72px
height: calc(100vh - 63px - 35px - 72px);
margin: 0 22px;
border-radius: 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
// 63px 35px
height: calc(100vh - 63px - 35px);
padding: 20px 22px 0;
position: relative;
background: rgba(237, 242, 255, 0.5);
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.operation-bar {
.operation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
justify-content: space-between;
padding: 0 0 16px 0;
}
.page-title {
font-weight: 500;
font-size: 24px;
margin: 0;
}
@@ -487,20 +409,14 @@ export default {
display: flex;
gap: 10px;
margin-left: auto;
align-items: center;
}
.search-input {
width: 240px;
}
.btn-search {
background: linear-gradient(135deg, #6b8cff, #a966ff);
border: none;
color: white;
}
.content-panel {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
@@ -519,20 +435,17 @@ export default {
flex-direction: column;
}
.el-card {
border: none;
}
.provider-card {
background: white;
flex: 1;
display: flex;
flex-direction: column;
border: none;
box-shadow: none;
overflow: hidden;
::v-deep .el-card__body {
padding: 15px;
padding: 14px 20px;
display: flex;
flex-direction: column;
flex: 1;
@@ -540,284 +453,8 @@ export default {
}
}
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.ctrl_btn {
display: flex;
gap: 8px;
padding-left: 26px;
.el-button {
min-width: 72px;
height: 32px;
padding: 7px 12px 7px 10px;
font-size: 12px;
border-radius: 4px;
line-height: 1;
font-weight: 500;
border: none;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
}
.el-button--primary {
background: #5f70f3;
color: white;
}
.el-button--danger {
background: #fd5b63;
color: white;
}
}
.custom-pagination {
display: flex;
align-items: center;
gap: 10px;
.el-select {
margin-right: 8px;
}
.pagination-btn:first-child,
.pagination-btn:nth-child(2),
.pagination-btn:nth-last-child(2),
.pagination-btn:nth-child(3) {
min-width: 60px;
height: 32px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #d7dce6;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
min-width: 28px;
height: 32px;
padding: 0;
border-radius: 4px;
border: 1px solid transparent;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(245, 247, 250, 0.3);
}
}
.pagination-btn.active {
background: #5f70f3 !important;
color: #ffffff !important;
border-color: #5f70f3 !important;
&:hover {
background: #6d7cf5 !important;
}
}
.total-text {
color: #909399;
font-size: 14px;
margin-left: 10px;
}
}
:deep(.transparent-table) {
background: white;
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
.el-table__body-wrapper {
flex: 1;
overflow-y: auto;
max-height: none !important;
}
.el-table__header-wrapper {
flex-shrink: 0;
}
.el-table__header th {
background: white !important;
color: black;
}
&::before {
display: none;
}
.el-table__body tr {
background-color: white;
td {
border-top: 1px solid rgba(0, 0, 0, 0.04);
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
}
}
:deep(.el-checkbox__inner) {
background-color: #ffffff !important;
border-color: #cccccc !important;
}
:deep(.el-checkbox__inner:hover) {
border-color: #cccccc !important;
}
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5f70f3 !important;
border-color: #5f70f3 !important;
}
@media (min-width: 1144px) {
.table_bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 40px;
}
:deep(.transparent-table) {
.el-table__body tr {
td {
padding-top: 16px;
padding-bottom: 16px;
}
&+tr {
margin-top: 10px;
}
}
}
}
:deep(.el-table .el-button--text) {
color: #7079aa;
}
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
.el-button--success {
background: #5bc98c;
color: white;
}
:deep(.el-table .cell) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.page-size-select {
width: 100px;
margin-right: 10px;
:deep(.el-input__inner) {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #e4e7ed;
background: #dee7ff;
color: #606266;
font-size: 14px;
}
:deep(.el-input__suffix) {
right: 6px;
width: 15px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
top: 6px;
border-radius: 4px;
}
:deep(.el-input__suffix-inner) {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
:deep(.el-icon-arrow-up:before) {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 9px solid #606266;
position: relative;
transform: rotate(0deg);
transition: transform 0.3s;
}
}
:deep(.el-table) {
.el-table__body-wrapper {
transition: height 0.3s ease;
}
}
.el-table {
// --table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height);
.el-table__body-wrapper {
max-height: calc(var(--table-max-height) - 40px);
}
}
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(2px);
}
:deep(.el-loading-spinner .circular) {
width: 28px;
height: 28px;
}
:deep(.el-loading-spinner .path) {
stroke: #6b8cff;
}
:deep(.el-loading-text) {
color: #6b8cff !important;
font-size: 14px;
margin-top: 8px;
}
.field-item {
@@ -843,40 +480,6 @@ export default {
}
}
.dropdown-trigger {
font-size: 14px;
color: #303133;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
color: #409EFF;
}
}
.dropdown-trigger.active {
color: #409EFF;
}
.dropdown-arrow {
display: inline-block;
margin-left: 5px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid black;
position: relative;
transition: transform 0.3s ease;
transform: rotate(0deg);
&.is-active {
transform: rotate(180deg);
border-top-color: #409EFF;
}
}
.rotate-down {
transform: rotate(180deg);
transition: transform 0.3s ease;
@@ -886,41 +489,11 @@ export default {
transition: transform 0.3s ease;
}
.dropdown-trigger {
font-size: 14px;
color: #303133;
cursor: pointer;
display: flex;
align-items: center;
&:hover {
color: #409EFF;
}
:deep(.el-table .el-button--text) {
color: #7079aa;
}
.dropdown-trigger.active {
color: #409EFF;
:deep(.el-table .el-button--text:hover) {
color: #5a64b5;
}
/* 确保选择列标题样式正确 */
.custom-selection-header {
position: relative;
}
:deep(.custom-selection-header .el-checkbox) {
display: flex !important;
align-items: center;
justify-content: center;
}
:deep(.custom-selection-header .el-checkbox__label) {
position: relative;
white-space: nowrap;
}
:deep(.custom-selection-cell) {
display: flex;
align-items: center;
justify-content: center;
}
</style>
</style>

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