mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 15:43:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89ef36711a | ||
|
|
0ba3e2c964 | ||
|
|
e5bc029d87 | ||
|
|
3fb925f13d | ||
|
|
4e958ce57c | ||
|
|
7f6ce89e39 | ||
|
|
71274cf0a3 | ||
|
|
58127cdfdc | ||
|
|
6d7bbfca95 | ||
|
|
3de39c8cdc | ||
|
|
387398a069 | ||
|
|
f17011939c | ||
|
|
9b6f564d30 | ||
|
|
e23989add0 | ||
|
|
4f64316144 | ||
|
|
68e10f42f8 | ||
|
|
3d1cae4692 | ||
|
|
31e84dadd1 | ||
|
|
ba4423684b | ||
|
|
62eaf46f34 | ||
|
|
3767bf8c81 | ||
|
|
300a33ddc2 | ||
|
|
34d7bd92ca | ||
|
|
44e7380c6b | ||
|
|
15ca08ce14 | ||
|
|
83b88eeaa0 | ||
|
|
290c92ee39 | ||
|
|
c2985c0db3 | ||
|
|
334dfc7917 | ||
|
|
03b51f73e7 | ||
|
|
af079d6c18 | ||
|
|
99008cf813 | ||
|
|
5fcb670821 | ||
|
|
3bdf8e20c7 | ||
|
|
e8d44b13cf | ||
|
|
c6a7871e5b | ||
|
|
4c9824b16f | ||
|
|
3e54a3272a | ||
|
|
8851406f84 | ||
|
|
fe85ea049a | ||
|
|
221c01e642 | ||
|
|
753aa280f6 | ||
|
|
64f3062eb2 | ||
|
|
f5143d60b7 | ||
|
|
e9fb36bf8f | ||
|
|
11e8736e66 | ||
|
|
30ce36b169 | ||
|
|
1541128e03 | ||
|
|
9d808ec733 | ||
|
|
8f526820a8 | ||
|
|
c0689c6916 | ||
|
|
893872e509 | ||
|
|
2c4b92f941 | ||
|
|
21cc1153b0 | ||
|
|
3c35380e9d | ||
|
|
2b65cfd369 | ||
|
|
8a44aaa7f9 | ||
|
|
4e0bf5542c | ||
|
|
f788e45bc1 | ||
|
|
5b4381c257 | ||
|
|
7396605844 | ||
|
|
b0308af05d | ||
|
|
98a1483032 | ||
|
|
07242dbfc0 | ||
|
|
a9b0775469 | ||
|
|
26a49ba34c | ||
|
|
f3431ea977 | ||
|
|
f0a7ee1c0c | ||
|
|
ad5f085c1f | ||
|
|
e7f5f5da34 | ||
|
|
d10b632875 | ||
|
|
8dd39c5a5b | ||
|
|
cdd1345c19 | ||
|
|
84067a1f57 | ||
|
|
26fd3110b4 | ||
|
|
5bd78cac1a | ||
|
|
0738c52d14 | ||
|
|
c93cb3bb52 | ||
|
|
eea7689c3d | ||
|
|
7b9e34c3e5 | ||
|
|
f81be33539 | ||
|
|
21a1cb65fc | ||
|
|
ef02a01439 | ||
|
|
0b22f4bbd2 | ||
|
|
9f8ae62ebb | ||
|
|
cec71a28eb | ||
|
|
7d060e9994 | ||
|
|
a3a6e889ee | ||
|
|
80283db9bd | ||
|
|
906a322675 | ||
|
|
bc829d9ef0 | ||
|
|
c5e6a46450 | ||
|
|
77f0bdc4ca | ||
|
|
8b9f13283b | ||
|
|
c3c65708e0 | ||
|
|
6bc64aa4aa | ||
|
|
eee907b1a2 | ||
|
|
ddd63f4136 | ||
|
|
b12c1efc90 | ||
|
|
01838f598c | ||
|
|
aef6d5dc1d | ||
|
|
9262963f20 | ||
|
|
d3e2aaacb9 | ||
|
|
755c81286c | ||
|
|
9dd2d998e2 | ||
|
|
abbb13161e | ||
|
|
23dc6f1ea9 | ||
|
|
8aed23f21a | ||
|
|
ae117e3579 | ||
|
|
0d19c693a9 | ||
|
|
283e57ff71 | ||
|
|
eed9503391 | ||
|
|
d66db60177 | ||
|
|
25d9e6c8cf | ||
|
|
a81f433228 | ||
|
|
257e365299 | ||
|
|
554afa3f46 | ||
|
|
d04ec9d510 | ||
|
|
4a802e00e4 | ||
|
|
a7007f5ae2 | ||
|
|
2b3cacd5b0 | ||
|
|
49565b4b3c | ||
|
|
bd8ae27f92 | ||
|
|
3fe0b8b669 | ||
|
|
f0e0b97217 | ||
|
|
67e07c916e | ||
|
|
b3a6ae2441 | ||
|
|
74f321a0fd | ||
|
|
64c841d00a | ||
|
|
5afa2fdc98 | ||
|
|
74bc5dbb7d | ||
|
|
5c67f2ecfd | ||
|
|
66f382039c | ||
|
|
3cfc1cf97a | ||
|
|
b88b102d38 | ||
|
|
fa3ddc2e2b | ||
|
|
f45bc5d9c9 | ||
|
|
79956ade97 | ||
|
|
b9df6d4ad2 | ||
|
|
fa79e2b379 | ||
|
|
5db638c075 | ||
|
|
0f0a0814b7 | ||
|
|
a842f0688f | ||
|
|
c3c2e7c28e | ||
|
|
0364d1199b | ||
|
|
c428a9ddee | ||
|
|
dba571727a | ||
|
|
d31761e78a | ||
|
|
63dfcde4c8 | ||
|
|
32e6ecf00d | ||
|
|
2481509dc5 | ||
|
|
44dd7fc58c | ||
|
|
87f50e5253 | ||
|
|
c82cf670de | ||
|
|
a3827aed6e | ||
|
|
cc7ba0872c | ||
|
|
0676b87832 | ||
|
|
5f96f56574 | ||
|
|
076aaae944 | ||
|
|
aed54bb3a6 | ||
|
|
7c03677295 | ||
|
|
3d3875f14f | ||
|
|
f6dd493997 | ||
|
|
d5e3989ed1 | ||
|
|
b7c0201ca3 |
@@ -31,6 +31,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver-opts: |
|
||||||
|
network=host
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -60,6 +63,10 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
BUILDKIT_PROGRESS=plain
|
||||||
|
|
||||||
# 构建 manager-api 镜像
|
# 构建 manager-api 镜像
|
||||||
- name: Build and push manager-web
|
- name: Build and push manager-web
|
||||||
@@ -70,4 +77,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
BUILDKIT_PROGRESS=plain
|
||||||
+9
-2
@@ -3,10 +3,17 @@ FROM python:3.10-slim AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 配置pip使用国内镜像源(阿里云)并设置超时和重试
|
||||||
|
RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
|
||||||
|
pip config set global.trusted-host mirrors.aliyun.com && \
|
||||||
|
pip config set global.timeout 120 && \
|
||||||
|
pip config set install.retries 5
|
||||||
|
|
||||||
COPY main/xiaozhi-server/requirements.txt .
|
COPY main/xiaozhi-server/requirements.txt .
|
||||||
|
|
||||||
# 安装Python依赖
|
# 安装Python依赖,使用并行下载
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt --default-timeout=120 --retries 5
|
||||||
|
|
||||||
# 第二阶段:生产镜像
|
# 第二阶段:生产镜像
|
||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
|||||||
+3
-3
@@ -18,12 +18,12 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
|
|||||||
|
|
||||||
# 安装Nginx和字体库
|
# 安装Nginx和字体库
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache nginx bash && \
|
apk add --no-cache nginx bash fontconfig ttf-dejavu && \
|
||||||
apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \
|
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ msttcorefonts-installer || true && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# 更新字体缓存
|
# 更新字体缓存
|
||||||
RUN printf 'YES\n' | update-ms-fonts && fc-cache -f -v
|
RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v
|
||||||
|
|
||||||
# 配置Nginx
|
# 配置Nginx
|
||||||
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
本项目基于人机共生智能理论和技术研发智能终端软硬件体系<br/>为开源智能硬件项目
|
本项目基于人机共生智能理论和技术研发智能终端软硬件体系<br/>为开源智能硬件项目
|
||||||
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
<a href="https://github.com/78/xiaozhi-esp32">xiaozhi-esp32</a>提供后端服务<br/>
|
||||||
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
|
根据<a href="https://ccnphfhqs21z.feishu.cn/wiki/M0XiwldO9iJwHikpXD5cEx71nKh">小智通信协议</a>使用Python、Java、Vue实现<br/>
|
||||||
支持MCP接入点和声纹识别
|
支持MQTT+UDP协议、Websocket协议、MCP接入点、声纹识别
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -241,7 +241,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||

|

|
||||||
| 功能模块 | 描述 |
|
| 功能模块 | 描述 |
|
||||||
|:---:|:---|
|
|:---:|:---|
|
||||||
| 核心架构 | 基于WebSocket和HTTP服务器,提供完整的控制台管理和认证系统 |
|
| 核心架构 | 基于MQTT+UDP网关、WebSocket、HTTP服务器,提供完整的控制台管理和认证系统 |
|
||||||
| 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 |
|
| 语音交互 | 支持流式ASR(语音识别)、流式TTS(语音合成)、VAD(语音活动检测),支持多语言识别和语音处理 |
|
||||||
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
|
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
|
||||||
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
||||||
@@ -283,6 +283,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
| dify 接口调用 | Dify | - |
|
| dify 接口调用 | Dify | - |
|
||||||
| fastgpt 接口调用 | Fastgpt | - |
|
| fastgpt 接口调用 | Fastgpt | - |
|
||||||
| coze 接口调用 | Coze | - |
|
| coze 接口调用 | Coze | - |
|
||||||
|
| xinference 接口调用 | Xinference | - |
|
||||||
|
| homeassistant 接口调用 | HomeAssistant | - |
|
||||||
|
|
||||||
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
实际上,任何支持 openai 接口调用的 LLM 均可接入使用。
|
||||||
|
|
||||||
@@ -302,8 +304,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
|
|
||||||
| 使用方式 | 支持平台 | 免费平台 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
|
| 接口调用 | EdgeTTS、火山引擎豆包TTS、腾讯云、阿里云TTS、阿里云流式TTS、CosyVoiceSiliconflow、TTS302AI、CozeCnTTS、GizwitsTTS、ACGNTTS、OpenAITTS、灵犀流式TTS、MinimaxTTS、火山双流式TTS | 灵犀流式TTS、EdgeTTS、CosyVoiceSiliconflow(部分) |
|
||||||
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、MinimaxTTS |
|
| 本地服务 | FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3、Index-TTS、PaddleSpeech | Index-TTS、PaddleSpeech、FishSpeech、GPT_SOVITS_V2、GPT_SOVITS_V3 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -320,7 +322,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
| 使用方式 | 支持平台 | 免费平台 |
|
| 使用方式 | 支持平台 | 免费平台 |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
| 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR |
|
||||||
| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer |
|
| 接口调用 | DoubaoASR、Doubao流式ASR、FunASRServer、TencentASR、AliyunASR、Aliyun流式ASR、百度ASR、OpenAI ASR | FunASRServer |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -338,6 +340,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
|:------:|:---------------:|:----:|:---------:|:--:|
|
|:------:|:---------------:|:----:|:---------:|:--:|
|
||||||
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
| Memory | mem0ai | 接口调用 | 1000次/月额度 | |
|
||||||
| Memory | mem_local_short | 本地总结 | 免费 | |
|
| Memory | mem_local_short | 本地总结 | 免费 | |
|
||||||
|
| Memory | nomem | 无记忆模式 | 免费 | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -347,6 +350,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
|:------:|:-------------:|:----:|:-------:|:---------------------:|
|
||||||
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
| Intent | intent_llm | 接口调用 | 根据LLM收费 | 通过大模型识别意图,通用性强 |
|
||||||
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
| Intent | function_call | 接口调用 | 根据LLM收费 | 通过大模型函数调用完成意图,速度快,效果好 |
|
||||||
|
| Intent | nointent | 无意图模式 | 免费 | 不进行意图识别,直接返回对话结果 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -476,7 +476,8 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
|||||||
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
|
4、[如何部署MCP接入点](./mcp-endpoint-enable.md)<br/>
|
||||||
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
|
5、[如何接入MCP接入点](./mcp-endpoint-integration.md)<br/>
|
||||||
6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
6、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||||
10、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
7、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||||
|
8、[天气插件使用指南](./weather-integration.md)<br/>
|
||||||
## 语音克隆、本地语音部署相关教程
|
## 语音克隆、本地语音部署相关教程
|
||||||
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
1、[如何部署集成index-tts本地语音](./index-stream-integration.md)<br/>
|
||||||
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
2、[如何部署集成fish-speech本地语音](./fish-speech-integration.md)<br/>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ VAD:
|
|||||||
### 7、部署相关教程
|
### 7、部署相关教程
|
||||||
1、[如何进行最简化部署](./Deployment.md)<br/>
|
1、[如何进行最简化部署](./Deployment.md)<br/>
|
||||||
2、[如何进行全模块部署](./Deployment_all.md)<br/>
|
2、[如何进行全模块部署](./Deployment_all.md)<br/>
|
||||||
|
2、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||||
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
3、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||||
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
4、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 火山双向流式TTS+声音克隆配置教程
|
||||||
|
单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。
|
||||||
|
### 1.开通火山引擎服务
|
||||||
|
访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx)
|
||||||
|
### 2.克隆音色
|
||||||
|
克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy
|
||||||
|
|
||||||
|
准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py```
|
||||||
|
|
||||||
|
在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称
|
||||||
|
```python
|
||||||
|
train(appid=appid, token=token, audio_path=r".\audios\xiaohe.wav", spk_id=spk_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
运行以下命令生成test_submit.mp3,点击播放试听克隆效果
|
||||||
|
|
||||||
|
```python
|
||||||
|
python uploadAndStatus.py
|
||||||
|
python tts_http_demo.py
|
||||||
|
```
|
||||||
|
回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。
|
||||||
|
### 3.填写配置文件
|
||||||
|
将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中
|
||||||
|
|
||||||
|
修改 resource_id的参数为``` volc.megatts.default```
|
||||||
|
(参考官方文档 https://www.volcengine.com/docs/6561/1329505)
|
||||||
|
speaker的参数填入声音ID(S_xxxxx)
|
||||||
|
|
||||||
|
启动服务,唤醒小智发出的声音是克隆的音色即成功。
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# MQTT 网关部署教程
|
||||||
|
|
||||||
|
`xiaozhi-esp32-server`项目,可结合虾哥开源的[xiaozhi-mqtt-gateway](https://github.com/78/xiaozhi-mqtt-gateway) 项目进行简单改造,即可实现小智硬件MQTT+UDP连接。
|
||||||
|
|
||||||
|
## 准备工作
|
||||||
|
|
||||||
|
查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。
|
||||||
|
|
||||||
|
准备好你的`xiaozhi-server`的`mqtt-websocket`连接地址。在你原来的`websocket地址`基础上,添加`?from=mqtt_gateway`字符,就可以得到`mqtt-websocket`连接地址
|
||||||
|
|
||||||
|
1、如果你是源码部署,你的`mqtt-websocket`地址是:
|
||||||
|
```
|
||||||
|
ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
2、如果你是docker部署,你的`mqtt-websocket`地址是
|
||||||
|
```
|
||||||
|
ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第一步 部署MQTT网关
|
||||||
|
|
||||||
|
1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git):
|
||||||
|
```bash
|
||||||
|
git clone https://ghfast.top/https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git
|
||||||
|
cd xiaozhi-mqtt-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 安装依赖:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm install -g pm2
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 配置 `config.json`:
|
||||||
|
```bash
|
||||||
|
cp config/mqtt.json.example config/mqtt.json
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 编辑配置文件 config/mqtt.json,把你在`本文准备阶段`的`mqtt-websocket`地址替换到`chat_servers`里。例如源码部署的`xiaozhi-server`就是如下配置:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"production": {
|
||||||
|
"chat_servers": [
|
||||||
|
"ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"debug": false,
|
||||||
|
"max_mqtt_payload_size": 8192,
|
||||||
|
"mcp_client": {
|
||||||
|
"capabilities": {
|
||||||
|
},
|
||||||
|
"client_info": {
|
||||||
|
"name": "xiaozhi-mqtt-client",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"max_tools_count": 128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
5. 在项目根目录创建下`.env`文件,并设置以下环境变量:
|
||||||
|
```
|
||||||
|
PUBLIC_IP=your-ip # 服务器公网IP
|
||||||
|
MQTT_PORT=1883 # MQTT服务器端口
|
||||||
|
UDP_PORT=8884 # UDP服务器端口
|
||||||
|
API_PORT=8007 # 管理API端口
|
||||||
|
MQTT_SIGNATURE_KEY=test # MQTT签名密钥
|
||||||
|
```
|
||||||
|
请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。
|
||||||
|
|
||||||
|
`MQTT_SIGNATURE_KEY` 是用于MQTT连接认证的密钥,最好设置成复杂一点的,最好是设置成8个字符以上且同时包含大小写字母,这个密钥稍后还会用到。
|
||||||
|
|
||||||
|
- 注意不要用简单的密码,比如`123456`、`test`等。
|
||||||
|
- 注意不要用简单的密码,比如`123456`、`test`等。
|
||||||
|
- 注意不要用简单的密码,比如`123456`、`test`等。
|
||||||
|
|
||||||
|
6. 启动MQTT网关
|
||||||
|
```
|
||||||
|
# 启动服务
|
||||||
|
pm2 start ecosystem.config.js
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
pm2 logs xz-mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
当你看到如下日志,说明MQTT网关启动成功:
|
||||||
|
```
|
||||||
|
0|xz-mqtt | 2025-09-11T12:14:48: MQTT 服务器正在监听端口 1883
|
||||||
|
0|xz-mqtt | 2025-09-11T12:14:48: UDP 服务器正在监听 x.x.x.x:8884
|
||||||
|
```
|
||||||
|
|
||||||
|
如果需要重启MQTT网关,执行如下命令:
|
||||||
|
```
|
||||||
|
pm2 restart xz-mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第二步 配置智控台
|
||||||
|
|
||||||
|
1. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样
|
||||||
|
```
|
||||||
|
192.168.0.7:1883
|
||||||
|
```
|
||||||
|
2. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_signature_key`,点击编辑,填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。
|
||||||
|
|
||||||
|
3. 在智控台顶部,点击`参数管理`,搜索`server.udp_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
|
||||||
|
```
|
||||||
|
192.168.0.7:8884
|
||||||
|
```
|
||||||
|
4. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_manager_api`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样
|
||||||
|
```
|
||||||
|
192.168.0.7:8007
|
||||||
|
```
|
||||||
|
|
||||||
|
上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址
|
||||||
|
```
|
||||||
|
curl 'http://localhost:8002/xiaozhi/ota/' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Client-Id: 7b94d69a-9808-4c59-9c9b-704333b38aff' \
|
||||||
|
-H 'Device-Id: 11:22:33:44:55:66' \
|
||||||
|
--data-raw $'{\n "application": {\n "version": "1.0.1",\n "elf_sha256": "1"\n },\n "board": {\n "mac": "11:22:33:44:55:66"\n }\n}'
|
||||||
|
```
|
||||||
|
|
||||||
|
如果返回的内容包含`mqtt`相关的配置,说明配置成功。类似这样
|
||||||
|
|
||||||
|
```
|
||||||
|
{"server_time":{"timestamp":1757567894012,"timeZone":"Asia/Shanghai","timezone_offset":480},"activation":{"code":"460609","message":"http://xiaozhi.server.com\n460609","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.4.23:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.0.7:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@7b94d69a-9808-4c59-9c9b-704333b38aff","username":"eyJpcCI6IjA6MDowOjA6MDowOjA6MSJ9","password":"Y8XP9xcUhVIN9OmbCHT9ETBiYNE3l3Z07Wk46wV9PE8=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 第三步 重启小智设备
|
||||||
|
由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。
|
||||||
|
|
||||||
|
唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。
|
||||||
|
```
|
||||||
|
pm2 logs xz-mqtt
|
||||||
|
```
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 天气插件使用指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
天气插件 `get_weather` 是小智ESP32语音助手的核心功能之一,支持通过语音查询全国各地的天气信息。插件基于和风天气API,提供实时天气和7天天气预报功能。
|
||||||
|
|
||||||
|
## API Key 申请指南
|
||||||
|
|
||||||
|
### 1. 注册和风天气账号
|
||||||
|
|
||||||
|
1. 访问 [和风天气控制台](https://console.qweather.com/)
|
||||||
|
2. 注册账号并完成邮箱验证
|
||||||
|
3. 登录控制台
|
||||||
|
|
||||||
|
### 2. 创建应用获取API Key
|
||||||
|
|
||||||
|
1. 进入控制台后,点击右侧["项目管理"](https://console.qweather.com/project?lang=zh) → "创建项目"
|
||||||
|
2. 填写项目信息:
|
||||||
|
- **项目名称**:如"小智语音助手"
|
||||||
|
3. 点击保存
|
||||||
|
4. 项目创建完成后,在该项目中点击"创建凭据"
|
||||||
|
5. 填写凭据信息:
|
||||||
|
- **凭据名称**:如"小智语音助手"
|
||||||
|
- **身份认证方式**:选择"API Key"
|
||||||
|
6. 点击保存
|
||||||
|
7. 在凭据中复制`API Key`,这是第一个关键的配置信息
|
||||||
|
|
||||||
|
### 3. 获取API Host
|
||||||
|
|
||||||
|
1. 在控制台中点击["设置"](https://console.qweather.com/setting?lang=zh) → "API Host"
|
||||||
|
2. 查看分配给你的专属`API Host`地址,这个是第二个关键的配置信息
|
||||||
|
|
||||||
|
以上操作,会得到两个重要的配置信息:`API Key`和`API Host`
|
||||||
|
|
||||||
|
## 配置方式(任选一种)
|
||||||
|
|
||||||
|
### 方式1. 如果你使用了智控台部署(推荐)
|
||||||
|
|
||||||
|
1. 登录智控台
|
||||||
|
2. 进入"角色配置"页面
|
||||||
|
3. 选择要配置的智能体
|
||||||
|
4. 点击"编辑功能"按钮
|
||||||
|
5. 在右侧参数配置区域找到"天气查询"插件
|
||||||
|
6. 勾选"天气查询"
|
||||||
|
7. 将复制过来的第一个关键配置`API Key`,填入到`天气插件 API 密钥`里
|
||||||
|
8. 将复制过来的第二个关键配置`API Host`,填入到`开发者 API Host`里
|
||||||
|
9. 保存配置,再保存智能体配置
|
||||||
|
|
||||||
|
### 方式2. 如果你只是单模块xiaozhi-server部署
|
||||||
|
|
||||||
|
在 `data/.config.yaml` 中配置:
|
||||||
|
|
||||||
|
1. 将复制过来的第一个关键配置`API Key`,填入到`api_key`里
|
||||||
|
2. 将复制过来的第二个关键配置`API Host`,填入到`api_host`里
|
||||||
|
3. 将你所在的城市填入到`default_location`里,例如`广州`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugins:
|
||||||
|
get_weather:
|
||||||
|
api_key: "你的和风天气API密钥"
|
||||||
|
api_host: "你的和风天气API主机地址"
|
||||||
|
default_location: "你的默认查询城市"
|
||||||
|
```
|
||||||
|
|
||||||
@@ -91,6 +91,11 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_WEBSOCKET = "server.websocket";
|
String SERVER_WEBSOCKET = "server.websocket";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mqtt gateway 配置
|
||||||
|
*/
|
||||||
|
String SERVER_MQTT_GATEWAY = "server.mqtt_gateway";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ota地址
|
* ota地址
|
||||||
*/
|
*/
|
||||||
@@ -121,6 +126,11 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_VOICE_PRINT = "server.voice_print";
|
String SERVER_VOICE_PRINT = "server.voice_print";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mqtt密钥
|
||||||
|
*/
|
||||||
|
String SERVER_MQTT_SECRET = "server.mqtt_signature_key";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无记忆
|
* 无记忆
|
||||||
*/
|
*/
|
||||||
@@ -237,7 +247,7 @@ public interface Constant {
|
|||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.7.6";
|
public static final String VERSION = "0.8.2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无效固件URL
|
* 无效固件URL
|
||||||
|
|||||||
@@ -55,4 +55,113 @@ public interface ErrorCode {
|
|||||||
|
|
||||||
int OTA_DEVICE_NOT_FOUND = 10041;
|
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||||
int OTA_DEVICE_NEED_BIND = 10042;
|
int OTA_DEVICE_NEED_BIND = 10042;
|
||||||
|
|
||||||
|
// 新增错误编码
|
||||||
|
int DELETE_DATA_FAILED = 10043;
|
||||||
|
int USER_NOT_LOGIN = 10044;
|
||||||
|
int WEB_SOCKET_CONNECT_FAILED = 10045;
|
||||||
|
int VOICE_PRINT_SAVE_ERROR = 10046;
|
||||||
|
int TODAY_SMS_LIMIT_REACHED = 10047;
|
||||||
|
int OLD_PASSWORD_ERROR = 10048;
|
||||||
|
int INVALID_LLM_TYPE = 10049;
|
||||||
|
int TOKEN_GENERATE_ERROR = 10050;
|
||||||
|
int RESOURCE_NOT_FOUND = 10051;
|
||||||
|
|
||||||
|
// 新增错误编码
|
||||||
|
int DEFAULT_AGENT_NOT_FOUND = 10052;
|
||||||
|
int AGENT_NOT_FOUND = 10053;
|
||||||
|
int VOICEPRINT_API_NOT_CONFIGURED = 10054;
|
||||||
|
int SMS_SEND_FAILED = 10055;
|
||||||
|
int SMS_CONNECTION_FAILED = 10056;
|
||||||
|
int AGENT_VOICEPRINT_CREATE_FAILED = 10057;
|
||||||
|
int AGENT_VOICEPRINT_UPDATE_FAILED = 10058;
|
||||||
|
int AGENT_VOICEPRINT_DELETE_FAILED = 10059;
|
||||||
|
int SMS_SEND_TOO_FREQUENTLY = 10060;
|
||||||
|
int ACTIVATION_CODE_EMPTY = 10061;
|
||||||
|
int ACTIVATION_CODE_ERROR = 10062;
|
||||||
|
int DEVICE_ALREADY_ACTIVATED = 10063;
|
||||||
|
// 默认模型删除错误
|
||||||
|
int DEFAULT_MODEL_DELETE_ERROR = 10064;
|
||||||
|
// 设备相关错误码
|
||||||
|
int MAC_ADDRESS_ALREADY_EXISTS = 10090; // Mac地址已存在
|
||||||
|
// 模型相关错误码
|
||||||
|
int MODEL_PROVIDER_NOT_EXIST = 10091; // 供应器不存在
|
||||||
|
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
|
||||||
|
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
|
||||||
|
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
|
||||||
|
|
||||||
|
// 登录相关错误码
|
||||||
|
int ADD_DATA_FAILED = 10065; // 新增数据失败
|
||||||
|
int UPDATE_DATA_FAILED = 10066; // 修改数据失败
|
||||||
|
int SMS_CAPTCHA_ERROR = 10067; // 短信验证码错误
|
||||||
|
int MOBILE_REGISTER_DISABLED = 10068; // 未开启手机注册
|
||||||
|
int USERNAME_NOT_PHONE = 10069; // 用户名不是手机号码
|
||||||
|
int PHONE_ALREADY_REGISTERED = 10070; // 手机号码已注册
|
||||||
|
int PHONE_NOT_REGISTERED = 10071; // 手机号码未注册
|
||||||
|
int USER_REGISTER_DISABLED = 10072; // 不允许用户注册
|
||||||
|
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
|
||||||
|
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
|
||||||
|
int SMS_CODE_ERROR = 10075; // 手机验证码错误
|
||||||
|
|
||||||
|
// 字典类型相关错误码
|
||||||
|
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
|
||||||
|
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
|
||||||
|
|
||||||
|
// 资源处理相关错误码
|
||||||
|
int RESOURCE_READ_ERROR = 10078; // 读取资源失败
|
||||||
|
|
||||||
|
// 智能体相关错误码
|
||||||
|
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
|
||||||
|
|
||||||
|
// 声纹相关错误码
|
||||||
|
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
|
||||||
|
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
|
||||||
|
int VOICEPRINT_UPDATE_NOT_ALLOWED = 10082; // 声纹修改不允许,声音已注册
|
||||||
|
int VOICEPRINT_UPDATE_ADMIN_ERROR = 10083; // 修改声纹错误,请联系管理员
|
||||||
|
int VOICEPRINT_API_URI_ERROR = 10084; // 声纹接口地址错误
|
||||||
|
int VOICEPRINT_AUDIO_NOT_BELONG_AGENT = 10085; // 音频数据不属于智能体
|
||||||
|
int VOICEPRINT_AUDIO_EMPTY = 10086; // 音频数据为空
|
||||||
|
int VOICEPRINT_REGISTER_REQUEST_ERROR = 10087; // 声纹保存请求失败
|
||||||
|
int VOICEPRINT_REGISTER_PROCESS_ERROR = 10088; // 声纹保存处理失败
|
||||||
|
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
|
||||||
|
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
|
||||||
|
int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败
|
||||||
|
|
||||||
|
// 服务端管理相关错误码
|
||||||
|
int INVALID_SERVER_ACTION = 10095; // 无效服务端操作
|
||||||
|
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
|
||||||
|
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
|
||||||
|
|
||||||
|
// 参数验证相关错误码
|
||||||
|
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
|
||||||
|
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
|
||||||
|
int WEBSOCKET_URL_FORMAT_ERROR = 10100; // WebSocket地址格式不正确
|
||||||
|
int WEBSOCKET_CONNECTION_FAILED = 10101; // WebSocket连接测试失败
|
||||||
|
int OTA_URL_EMPTY = 10102; // OTA地址不能为空
|
||||||
|
int OTA_URL_LOCALHOST = 10103; // OTA地址不能使用localhost或127.0.0.1
|
||||||
|
int OTA_URL_PROTOCOL_ERROR = 10104; // OTA地址必须以http或https开头
|
||||||
|
int OTA_URL_FORMAT_ERROR = 10105; // OTA地址必须以/ota/结尾
|
||||||
|
int OTA_INTERFACE_ACCESS_FAILED = 10106; // OTA接口访问失败
|
||||||
|
int OTA_INTERFACE_FORMAT_ERROR = 10107; // OTA接口返回内容格式不正确
|
||||||
|
int OTA_INTERFACE_VALIDATION_FAILED = 10108; // OTA接口验证失败
|
||||||
|
int MCP_URL_EMPTY = 10109; // MCP地址不能为空
|
||||||
|
int MCP_URL_LOCALHOST = 10110; // MCP地址不能使用localhost或127.0.0.1
|
||||||
|
int MCP_URL_INVALID = 10111; // 不是正确的MCP地址
|
||||||
|
int MCP_INTERFACE_ACCESS_FAILED = 10112; // MCP接口访问失败
|
||||||
|
int MCP_INTERFACE_FORMAT_ERROR = 10113; // MCP接口返回内容格式不正确
|
||||||
|
int MCP_INTERFACE_VALIDATION_FAILED = 10114; // MCP接口验证失败
|
||||||
|
int VOICEPRINT_URL_EMPTY = 10115; // 声纹接口地址不能为空
|
||||||
|
int VOICEPRINT_URL_LOCALHOST = 10116; // 声纹接口地址不能使用localhost或127.0.0.1
|
||||||
|
int VOICEPRINT_URL_INVALID = 10117; // 不是正确的声纹接口地址
|
||||||
|
int VOICEPRINT_URL_PROTOCOL_ERROR = 10118; // 声纹接口地址必须以http或https开头
|
||||||
|
int VOICEPRINT_INTERFACE_ACCESS_FAILED = 10119; // 声纹接口访问失败
|
||||||
|
int VOICEPRINT_INTERFACE_FORMAT_ERROR = 10120; // 声纹接口返回内容格式不正确
|
||||||
|
int VOICEPRINT_INTERFACE_VALIDATION_FAILED = 10121; // 声纹接口验证失败
|
||||||
|
int MQTT_SECRET_EMPTY = 10122; // mqtt密钥不能为空
|
||||||
|
int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全
|
||||||
|
int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母
|
||||||
|
int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码
|
||||||
|
|
||||||
|
// 字典相关错误码
|
||||||
|
int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.utils.MessageUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,7 +63,7 @@ public class RenExceptionHandler {
|
|||||||
@ExceptionHandler(NoResourceFoundException.class)
|
@ExceptionHandler(NoResourceFoundException.class)
|
||||||
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
|
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
|
||||||
log.warn("Resource not found: {}", ex.getMessage());
|
log.warn("Resource not found: {}", ex.getMessage());
|
||||||
return new Result<Void>().error(404, "资源不存在");
|
return new Result<Void>().error(404, MessageUtils.getMessage(ErrorCode.RESOURCE_NOT_FOUND));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
@@ -76,7 +77,7 @@ public class RenExceptionHandler {
|
|||||||
})
|
})
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse("请求参数错误!");
|
.orElse(MessageUtils.getMessage(ErrorCode.PARAM_VALUE_NULL));
|
||||||
|
|
||||||
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
|
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.springframework.core.io.ResourceLoader;
|
|||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@@ -37,7 +38,7 @@ public class ResourcesUtils {
|
|||||||
}
|
}
|
||||||
} catch (IOException e){
|
} catch (IOException e){
|
||||||
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
|
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
|
||||||
throw new RenException("读取资源失败");
|
throw new RenException(ErrorCode.RESOURCE_READ_ERROR);
|
||||||
}
|
}
|
||||||
return luaScriptBuilder.toString();
|
return luaScriptBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
|
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
|
||||||
@@ -42,7 +43,7 @@ public class AgentVoicePrintController {
|
|||||||
if (b) {
|
if (b) {
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
return new Result<Void>().error("智能体的声纹创建失败");
|
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_CREATE_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping
|
@PutMapping
|
||||||
@@ -54,7 +55,7 @@ public class AgentVoicePrintController {
|
|||||||
if (b) {
|
if (b) {
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
return new Result<Void>().error("智能体的对应声纹更新失败");
|
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_UPDATE_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@@ -67,7 +68,7 @@ public class AgentVoicePrintController {
|
|||||||
if (delete) {
|
if (delete) {
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
return new Result<Void>().error("智能体的对应声纹删除失败");
|
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_DELETE_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/list/{id}")
|
@GetMapping("/list/{id}")
|
||||||
@@ -76,7 +77,7 @@ public class AgentVoicePrintController {
|
|||||||
public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) {
|
public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) {
|
||||||
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
|
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
|
||||||
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
||||||
throw new RenException("声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)");
|
throw new RenException(ErrorCode.VOICEPRINT_API_NOT_CONFIGURED);
|
||||||
}
|
}
|
||||||
Long userId = SecurityUser.getUserId();
|
Long userId = SecurityUser.getUserId();
|
||||||
List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id);
|
List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id);
|
||||||
|
|||||||
+4
-3
@@ -20,6 +20,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -74,7 +75,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
AgentInfoVO agent = agentDao.selectAgentInfoById(id);
|
AgentInfoVO agent = agentDao.selectAgentInfoById(id);
|
||||||
|
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
throw new RenException("智能体不存在");
|
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
@@ -204,7 +205,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
// 先查询现有实体
|
// 先查询现有实体
|
||||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
AgentEntity existingEntity = this.getAgentById(agentId);
|
||||||
if (existingEntity == null) {
|
if (existingEntity == null) {
|
||||||
throw new RuntimeException("智能体不存在");
|
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只更新提供的非空字段
|
// 只更新提供的非空字段
|
||||||
@@ -328,7 +329,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||||
if (!b) {
|
if (!b) {
|
||||||
throw new RenException("LLM大模型和Intent意图识别,选择参数不匹配");
|
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||||
}
|
}
|
||||||
this.updateById(existingEntity);
|
this.updateById(existingEntity);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-13
@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
@@ -78,7 +79,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
// 根据识别出的声纹ID查询对应的用户信息
|
// 根据识别出的声纹ID查询对应的用户信息
|
||||||
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
||||||
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
||||||
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册");
|
throw new RenException(ErrorCode.VOICEPRINT_ALREADY_REGISTERED, existingUserName);
|
||||||
}
|
}
|
||||||
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
|
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
|
||||||
// 开启事务
|
// 开启事务
|
||||||
@@ -100,7 +101,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
status.setRollbackOnly(); // 标记事务回滚
|
status.setRollbackOnly(); // 标记事务回滚
|
||||||
log.error("保存声纹错误原因:{}", e.getMessage());
|
log.error("保存声纹错误原因:{}", e.getMessage());
|
||||||
throw new RenException("保存声纹错误,请联系管理员");
|
throw new RenException(ErrorCode.VOICE_PRINT_SAVE_ERROR);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -123,7 +124,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
status.setRollbackOnly(); // 标记事务回滚
|
status.setRollbackOnly(); // 标记事务回滚
|
||||||
log.error("删除声纹存在错误原因:{}", e.getMessage());
|
log.error("删除声纹存在错误原因:{}", e.getMessage());
|
||||||
throw new RenException("删除声纹出现了错误");
|
throw new RenException(ErrorCode.VOICEPRINT_DELETE_ERROR);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
// 数据库声纹数据删除成功才继续执行删除声纹服务的数据
|
// 数据库声纹数据删除成功才继续执行删除声纹服务的数据
|
||||||
@@ -179,7 +180,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
// 根据识别出的声纹ID查询对应的用户信息
|
// 根据识别出的声纹ID查询对应的用户信息
|
||||||
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
||||||
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
||||||
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + ")");
|
throw new RenException(ErrorCode.VOICEPRINT_UPDATE_NOT_ALLOWED, existingUserName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -208,7 +209,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
status.setRollbackOnly(); // 标记事务回滚
|
status.setRollbackOnly(); // 标记事务回滚
|
||||||
log.error("修改声纹错误原因:{}", e.getMessage());
|
log.error("修改声纹错误原因:{}", e.getMessage());
|
||||||
throw new RenException("修改声纹错误,请联系管理员");
|
throw new RenException(ErrorCode.VOICEPRINT_UPDATE_ADMIN_ERROR);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -225,7 +226,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
return new URI(voicePrint);
|
return new URI(voicePrint);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
log.error("路径格式不正确路径:{},\n错误信息:{}", voicePrint, e.getMessage());
|
log.error("路径格式不正确路径:{},\n错误信息:{}", voicePrint, e.getMessage());
|
||||||
throw new RuntimeException("声纹接口的地址存在错误,请进入参数管理修改声纹接口地址");
|
throw new RenException(ErrorCode.VOICEPRINT_API_URI_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,13 +271,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
// 判断这个音频是否属于当前智能体
|
// 判断这个音频是否属于当前智能体
|
||||||
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
|
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
|
||||||
if (!b) {
|
if (!b) {
|
||||||
throw new RenException("音频数据不属于这个智能体");
|
throw new RenException(ErrorCode.VOICEPRINT_AUDIO_NOT_BELONG_AGENT);
|
||||||
}
|
}
|
||||||
// 获取到音频数据
|
// 获取到音频数据
|
||||||
byte[] audio = agentChatAudioService.getAudio(audioId);
|
byte[] audio = agentChatAudioService.getAudio(audioId);
|
||||||
// 如果音频数据为空的直接报错不进行下去
|
// 如果音频数据为空的直接报错不进行下去
|
||||||
if (audio == null || audio.length == 0) {
|
if (audio == null || audio.length == 0) {
|
||||||
throw new RenException("音频数据是空的请检查上传数据");
|
throw new RenException(ErrorCode.VOICEPRINT_AUDIO_EMPTY);
|
||||||
}
|
}
|
||||||
// 将字节数组包装为资源,返回
|
// 将字节数组包装为资源,返回
|
||||||
return new ByteArrayResource(audio) {
|
return new ByteArrayResource(audio) {
|
||||||
@@ -314,13 +315,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
|
|
||||||
if (response.getStatusCode() != HttpStatus.OK) {
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
log.error("声纹注册失败,请求路径:{}", requestUrl);
|
log.error("声纹注册失败,请求路径:{}", requestUrl);
|
||||||
throw new RenException("声纹保存失败,请求不成功");
|
throw new RenException(ErrorCode.VOICEPRINT_REGISTER_REQUEST_ERROR);
|
||||||
}
|
}
|
||||||
// 检查响应内容
|
// 检查响应内容
|
||||||
String responseBody = response.getBody();
|
String responseBody = response.getBody();
|
||||||
if (responseBody == null || !responseBody.contains("true")) {
|
if (responseBody == null || !responseBody.contains("true")) {
|
||||||
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
||||||
throw new RenException("声纹保存失败,请求处理失败");
|
throw new RenException(ErrorCode.VOICEPRINT_REGISTER_PROCESS_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,13 +345,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
String.class);
|
String.class);
|
||||||
if (response.getStatusCode() != HttpStatus.OK) {
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
log.error("声纹注销失败,请求路径:{}", requestUrl);
|
log.error("声纹注销失败,请求路径:{}", requestUrl);
|
||||||
throw new RenException("声纹注销失败,请求不成功");
|
throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_REQUEST_ERROR);
|
||||||
}
|
}
|
||||||
// 检查响应内容
|
// 检查响应内容
|
||||||
String responseBody = response.getBody();
|
String responseBody = response.getBody();
|
||||||
if (responseBody == null || !responseBody.contains("true")) {
|
if (responseBody == null || !responseBody.contains("true")) {
|
||||||
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
||||||
throw new RenException("声纹注销失败,请求处理失败");
|
throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_PROCESS_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +399,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
|||||||
|
|
||||||
if (response.getStatusCode() != HttpStatus.OK) {
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
log.error("声纹识别请求失败,请求路径:{}", requestUrl);
|
log.error("声纹识别请求失败,请求路径:{}", requestUrl);
|
||||||
throw new RenException("声纹识别失败,请求不成功");
|
throw new RenException(ErrorCode.VOICEPRINT_IDENTIFY_REQUEST_ERROR);
|
||||||
}
|
}
|
||||||
// 检查响应内容
|
// 检查响应内容
|
||||||
String responseBody = response.getBody();
|
String responseBody = response.getBody();
|
||||||
|
|||||||
+16
-2
@@ -70,7 +70,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 查询默认智能体
|
// 查询默认智能体
|
||||||
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
|
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
throw new RenException("默认智能体未找到");
|
throw new RenException(ErrorCode.DEFAULT_AGENT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建模块配置
|
// 构建模块配置
|
||||||
@@ -113,7 +113,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 获取智能体信息
|
// 获取智能体信息
|
||||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
throw new RenException("智能体未找到");
|
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
// 获取音色信息
|
// 获取音色信息
|
||||||
String voice = null;
|
String voice = null;
|
||||||
@@ -298,6 +298,20 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
Map<String, Object> voiceprintConfig = new HashMap<>();
|
Map<String, Object> voiceprintConfig = new HashMap<>();
|
||||||
voiceprintConfig.put("url", voiceprintUrl);
|
voiceprintConfig.put("url", voiceprintUrl);
|
||||||
voiceprintConfig.put("speakers", speakers);
|
voiceprintConfig.put("speakers", speakers);
|
||||||
|
|
||||||
|
// 获取声纹识别相似度阈值,默认0.4
|
||||||
|
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
|
||||||
|
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
|
||||||
|
try {
|
||||||
|
double threshold = Double.parseDouble(thresholdStr);
|
||||||
|
voiceprintConfig.put("similarity_threshold", threshold);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// 如果解析失败,使用默认值0.4
|
||||||
|
voiceprintConfig.put("similarity_threshold", 0.4);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
voiceprintConfig.put("similarity_threshold", 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
result.put("voiceprint", voiceprintConfig);
|
result.put("voiceprint", voiceprintConfig);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+162
-4
@@ -5,6 +5,10 @@ import java.util.List;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -12,32 +16,45 @@ import org.springframework.web.bind.annotation.PutMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Tag(name = "设备管理")
|
@Tag(name = "设备管理")
|
||||||
@AllArgsConstructor
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/device")
|
@RequestMapping("/device")
|
||||||
public class DeviceController {
|
public class DeviceController {
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService,
|
||||||
|
RestTemplate restTemplate, ObjectMapper objectMapper) {
|
||||||
|
this.deviceService = deviceService;
|
||||||
|
this.redisUtils = redisUtils;
|
||||||
|
this.sysParamsService = sysParamsService;
|
||||||
|
this.restTemplate = restTemplate;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/bind/{agentId}/{deviceCode}")
|
@PostMapping("/bind/{agentId}/{deviceCode}")
|
||||||
@Operation(summary = "绑定设备")
|
@Operation(summary = "绑定设备")
|
||||||
@@ -75,6 +92,88 @@ public class DeviceController {
|
|||||||
return new Result<List<DeviceEntity>>().ok(devices);
|
return new Result<List<DeviceEntity>>().ok(devices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/bind/{agentId}")
|
||||||
|
@Operation(summary = "转发POST请求到MQTT网关")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
||||||
|
try {
|
||||||
|
// 从系统参数中获取MQTT网关地址
|
||||||
|
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||||
|
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||||
|
return new Result<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户的设备列表
|
||||||
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
|
||||||
|
|
||||||
|
// 构建deviceIds数组
|
||||||
|
java.util.List<String> deviceIds = new java.util.ArrayList<>();
|
||||||
|
for (DeviceEntity device : devices) {
|
||||||
|
String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown";
|
||||||
|
String groupId = device.getBoard() != null ? device.getBoard() : "GID_default";
|
||||||
|
|
||||||
|
// 替换冒号为下划线
|
||||||
|
groupId = groupId.replace(":", "_");
|
||||||
|
macAddress = macAddress.replace(":", "_");
|
||||||
|
|
||||||
|
// 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress
|
||||||
|
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
|
||||||
|
deviceIds.add(mqttClientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的URL
|
||||||
|
String url = "http://" + mqttGatewayUrl + "/api/devices/status";
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Content-Type", "application/json");
|
||||||
|
|
||||||
|
// 生成Bearer令牌
|
||||||
|
String token = generateBearerToken();
|
||||||
|
if (token == null) {
|
||||||
|
return new Result<String>().error("令牌生成失败");
|
||||||
|
}
|
||||||
|
headers.set("Authorization", "Bearer " + token);
|
||||||
|
|
||||||
|
// 构建请求体JSON
|
||||||
|
String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}";
|
||||||
|
HttpEntity<String> requestEntity = new HttpEntity<>(jsonBody, headers);
|
||||||
|
|
||||||
|
// 发送POST请求
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
||||||
|
|
||||||
|
// 返回响应
|
||||||
|
return new Result<String>().ok(response.getBody());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new Result<String>().error("转发请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateBearerToken() {
|
||||||
|
try {
|
||||||
|
// 获取当前日期,格式为yyyy-MM-dd
|
||||||
|
String dateStr = java.time.LocalDate.now()
|
||||||
|
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||||
|
|
||||||
|
// 获取MQTT签名密钥
|
||||||
|
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||||
|
if (StringUtils.isBlank(signatureKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将日期字符串与MQTT_SIGNATURE_KEY连接
|
||||||
|
String tokenContent = dateStr + signatureKey;
|
||||||
|
|
||||||
|
// 对连接后的字符串进行SHA256哈希计算
|
||||||
|
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/unbind")
|
@PostMapping("/unbind")
|
||||||
@Operation(summary = "解绑设备")
|
@Operation(summary = "解绑设备")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
@@ -109,4 +208,63 @@ public class DeviceController {
|
|||||||
deviceService.manualAddDevice(user.getId(), dto);
|
deviceService.manualAddDevice(user.getId(), dto);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/commands/{deviceId}")
|
||||||
|
@Operation(summary = "发送设备指令")
|
||||||
|
@RequiresPermissions("sys:role:normal")
|
||||||
|
public Result<String> sendDeviceCommand(@PathVariable String deviceId, @RequestBody String command) {
|
||||||
|
try {
|
||||||
|
// 从系统参数中获取MQTT网关地址
|
||||||
|
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||||
|
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||||
|
return new Result<String>().error("MQTT网关地址未配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的URL
|
||||||
|
// 获取设备信息以构建mqttClientId
|
||||||
|
DeviceEntity deviceById = deviceService.selectById(deviceId);
|
||||||
|
|
||||||
|
if (!deviceById.getUserId().equals(SecurityUser.getUser().getId())) {
|
||||||
|
return new Result<String>().error("设备不存在");
|
||||||
|
}
|
||||||
|
String macAddress = deviceById != null ? deviceById.getMacAddress() : "unknown";
|
||||||
|
String groupId = deviceById != null ? deviceById.getBoard() : null;
|
||||||
|
if (groupId == null) {
|
||||||
|
groupId = "GID_default";
|
||||||
|
}
|
||||||
|
groupId = groupId.replace(":", "_");
|
||||||
|
macAddress = macAddress.replace(":", "_");
|
||||||
|
|
||||||
|
// 拼接为groupId@@@macAddress@@@deviceId格式
|
||||||
|
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
|
||||||
|
|
||||||
|
String url = "http://" + mqttGatewayUrl + "/api/commands/" + mqttClientId;
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Content-Type", "application/json");
|
||||||
|
|
||||||
|
// 生成Bearer令牌
|
||||||
|
String dateStr = java.time.LocalDate.now()
|
||||||
|
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||||
|
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||||
|
if (StringUtils.isBlank(signatureKey)) {
|
||||||
|
return new Result<String>().error("MQTT签名密钥未配置");
|
||||||
|
}
|
||||||
|
String tokenContent = dateStr + signatureKey;
|
||||||
|
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
|
||||||
|
headers.set("Authorization", "Bearer " + token);
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
HttpEntity<String> requestEntity = new HttpEntity<>(command, headers);
|
||||||
|
|
||||||
|
// 发送POST请求
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
||||||
|
|
||||||
|
// 返回响应
|
||||||
|
return new Result<String>().ok(response.getBody());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new Result<String>().error("发送指令失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,10 @@ public class OTAController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
@Hidden
|
@Hidden
|
||||||
public ResponseEntity<String> getOTA() {
|
public ResponseEntity<String> getOTA() {
|
||||||
|
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
|
||||||
|
if (StringUtils.isBlank(mqttUdpConfig)) {
|
||||||
|
return ResponseEntity.ok("OTA接口不正常,缺少mqtt_gateway地址,请登录智控台,在参数管理找到【server.mqtt_gateway】配置");
|
||||||
|
}
|
||||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "WebSocket配置")
|
@Schema(description = "WebSocket配置")
|
||||||
private Websocket websocket;
|
private Websocket websocket;
|
||||||
|
|
||||||
|
@Schema(description = "MQTT Gateway配置")
|
||||||
|
private MQTT mqtt;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public static class Firmware {
|
public static class Firmware {
|
||||||
@@ -70,4 +73,21 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "WebSocket服务器地址")
|
@Schema(description = "WebSocket服务器地址")
|
||||||
private String url;
|
private String url;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class MQTT {
|
||||||
|
@Schema(description = "MQTT 配置网址")
|
||||||
|
private String endpoint;
|
||||||
|
@Schema(description = "MQTT 客户端唯一标识符")
|
||||||
|
private String client_id;
|
||||||
|
@Schema(description = "MQTT 认证用户名")
|
||||||
|
private String username;
|
||||||
|
@Schema(description = "MQTT 认证密码")
|
||||||
|
private String password;
|
||||||
|
@Schema(description = "ESP32 发布消息的主题")
|
||||||
|
private String publish_topic;
|
||||||
|
@Schema(description = "ESP32 订阅的主题")
|
||||||
|
private String subscribe_topic;
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
-8
@@ -1,6 +1,8 @@
|
|||||||
package xiaozhi.modules.device.service.impl;
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -8,6 +10,9 @@ import java.util.Map;
|
|||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.aop.framework.AopContext;
|
import org.springframework.aop.framework.AopContext;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
@@ -24,6 +29,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
@@ -33,6 +39,7 @@ import xiaozhi.common.user.UserDetail;
|
|||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.DateUtils;
|
import xiaozhi.common.utils.DateUtils;
|
||||||
import xiaozhi.modules.device.dao.DeviceDao;
|
import xiaozhi.modules.device.dao.DeviceDao;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
@@ -44,7 +51,6 @@ import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
|||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@@ -78,27 +84,27 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
@Override
|
@Override
|
||||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||||
if (StringUtils.isBlank(activationCode)) {
|
if (StringUtils.isBlank(activationCode)) {
|
||||||
throw new RenException("激活码不能为空");
|
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
|
||||||
}
|
}
|
||||||
String deviceKey = "ota:activation:code:" + activationCode;
|
String deviceKey = "ota:activation:code:" + activationCode;
|
||||||
Object cacheDeviceId = redisUtils.get(deviceKey);
|
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||||
if (cacheDeviceId == null) {
|
if (cacheDeviceId == null) {
|
||||||
throw new RenException("激活码错误");
|
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||||
}
|
}
|
||||||
String deviceId = (String) cacheDeviceId;
|
String deviceId = (String) cacheDeviceId;
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||||
if (cacheMap == null) {
|
if (cacheMap == null) {
|
||||||
throw new RenException("激活码错误");
|
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||||
}
|
}
|
||||||
String cachedCode = (String) cacheMap.get("activation_code");
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
if (!activationCode.equals(cachedCode)) {
|
if (!activationCode.equals(cachedCode)) {
|
||||||
throw new RenException("激活码错误");
|
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||||
}
|
}
|
||||||
// 检查设备有没有被激活
|
// 检查设备有没有被激活
|
||||||
if (selectById(deviceId) != null) {
|
if (selectById(deviceId) != null) {
|
||||||
throw new RenException("设备已激活");
|
throw new RenException(ErrorCode.DEVICE_ALREADY_ACTIVATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
String macAddress = (String) cacheMap.get("mac_address");
|
String macAddress = (String) cacheMap.get("mac_address");
|
||||||
@@ -106,7 +112,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
String appVersion = (String) cacheMap.get("app_version");
|
String appVersion = (String) cacheMap.get("app_version");
|
||||||
UserDetail user = SecurityUser.getUser();
|
UserDetail user = SecurityUser.getUser();
|
||||||
if (user.getId() == null) {
|
if (user.getId() == null) {
|
||||||
throw new RenException("用户未登录");
|
throw new RenException(ErrorCode.USER_NOT_LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
Date currentTime = new Date();
|
Date currentTime = new Date();
|
||||||
@@ -176,6 +182,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
response.setWebsocket(websocket);
|
response.setWebsocket(websocket);
|
||||||
|
|
||||||
|
// 添加MQTT UDP配置
|
||||||
|
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
|
||||||
|
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
|
||||||
|
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
|
||||||
|
try {
|
||||||
|
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
|
||||||
|
: "GID_default";
|
||||||
|
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, groupId);
|
||||||
|
if (mqtt != null) {
|
||||||
|
mqtt.setEndpoint(mqttUdpConfig);
|
||||||
|
response.setMqtt(mqtt);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成MQTT配置失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (deviceById != null) {
|
if (deviceById != null) {
|
||||||
// 如果设备存在,则异步更新上次连接时间和版本信息
|
// 如果设备存在,则异步更新上次连接时间和版本信息
|
||||||
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
||||||
@@ -419,7 +442,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
wrapper.eq("mac_address", dto.getMacAddress());
|
wrapper.eq("mac_address", dto.getMacAddress());
|
||||||
DeviceEntity exist = baseDao.selectOne(wrapper);
|
DeviceEntity exist = baseDao.selectOne(wrapper);
|
||||||
if (exist != null) {
|
if (exist != null) {
|
||||||
throw new RenException("该Mac地址已存在");
|
throw new RenException(ErrorCode.MAC_ADDRESS_ALREADY_EXISTS);
|
||||||
}
|
}
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
DeviceEntity entity = new DeviceEntity();
|
DeviceEntity entity = new DeviceEntity();
|
||||||
@@ -437,4 +460,73 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
entity.setAutoUpdate(1);
|
entity.setAutoUpdate(1);
|
||||||
baseDao.insert(entity);
|
baseDao.insert(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成MQTT密码签名
|
||||||
|
*
|
||||||
|
* @param content 签名内容 (clientId + '|' + username)
|
||||||
|
* @param secretKey 密钥
|
||||||
|
* @return Base64编码的HMAC-SHA256签名
|
||||||
|
*/
|
||||||
|
private String generatePasswordSignature(String content, String secretKey) throws Exception {
|
||||||
|
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||||
|
hmac.init(keySpec);
|
||||||
|
byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Base64.getEncoder().encodeToString(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建MQTT配置信息
|
||||||
|
*
|
||||||
|
* @param macAddress MAC地址
|
||||||
|
* @param groupId 分组ID
|
||||||
|
* @return MQTT配置对象
|
||||||
|
*/
|
||||||
|
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
|
||||||
|
throws Exception {
|
||||||
|
// 从环境变量或系统参数获取签名密钥
|
||||||
|
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||||
|
if (StringUtils.isBlank(signatureKey)) {
|
||||||
|
log.warn("缺少MQTT_SIGNATURE_KEY,跳过MQTT配置生成");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建客户端ID格式:groupId@@@macAddress@@@uuid
|
||||||
|
String groupIdSafeStr = groupId.replace(":", "_");
|
||||||
|
String deviceIdSafeStr = macAddress.replace(":", "_");
|
||||||
|
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, deviceIdSafeStr);
|
||||||
|
|
||||||
|
// 构建用户数据(包含IP等信息)
|
||||||
|
Map<String, String> userData = new HashMap<>();
|
||||||
|
// 尝试获取客户端IP
|
||||||
|
try {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
|
||||||
|
.getRequestAttributes();
|
||||||
|
if (attributes != null) {
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
String clientIp = request.getRemoteAddr();
|
||||||
|
userData.put("ip", clientIp);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
userData.put("ip", "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户数据编码为Base64 JSON
|
||||||
|
String userDataJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(userData);
|
||||||
|
String username = Base64.getEncoder().encodeToString(userDataJson.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
// 生成密码签名
|
||||||
|
String password = generatePasswordSignature(mqttClientId + "|" + username, signatureKey);
|
||||||
|
|
||||||
|
// 构建MQTT配置
|
||||||
|
DeviceReportRespDTO.MQTT mqtt = new DeviceReportRespDTO.MQTT();
|
||||||
|
mqtt.setClient_id(mqttClientId);
|
||||||
|
mqtt.setUsername(username);
|
||||||
|
mqtt.setPassword(password);
|
||||||
|
mqtt.setPublish_topic("device-server");
|
||||||
|
mqtt.setSubscribe_topic("devices/p2p/" + deviceIdSafeStr);
|
||||||
|
|
||||||
|
return mqtt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -15,6 +15,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
@@ -94,7 +95,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
}
|
}
|
||||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||||
if (CollectionUtil.isEmpty(providerList)) {
|
if (CollectionUtil.isEmpty(providerList)) {
|
||||||
throw new RenException("供应器不存在");
|
throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再保存供应器提供的模型
|
// 再保存供应器提供的模型
|
||||||
@@ -113,7 +114,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
}
|
}
|
||||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||||
if (CollectionUtil.isEmpty(providerList)) {
|
if (CollectionUtil.isEmpty(providerList)) {
|
||||||
throw new RenException("供应器不存在");
|
throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
|
||||||
}
|
}
|
||||||
if (modelConfigBodyDTO.getConfigJson().containsKey("llm")) {
|
if (modelConfigBodyDTO.getConfigJson().containsKey("llm")) {
|
||||||
String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString();
|
String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString();
|
||||||
@@ -122,12 +123,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
String selectModelType = (modelConfigEntity == null || modelConfigEntity.getModelType() == null) ? null
|
String selectModelType = (modelConfigEntity == null || modelConfigEntity.getModelType() == null) ? null
|
||||||
: modelConfigEntity.getModelType().toUpperCase();
|
: modelConfigEntity.getModelType().toUpperCase();
|
||||||
if (modelConfigEntity == null || !"LLM".equals(selectModelType)) {
|
if (modelConfigEntity == null || !"LLM".equals(selectModelType)) {
|
||||||
throw new RenException("设置的LLM不存在");
|
throw new RenException(ErrorCode.LLM_NOT_EXIST);
|
||||||
}
|
}
|
||||||
String type = modelConfigEntity.getConfigJson().get("type").toString();
|
String type = modelConfigEntity.getConfigJson().get("type").toString();
|
||||||
// 如果查询大语言模型是openai或者ollama,意图识别选参数都可以
|
// 如果查询大语言模型是openai或者ollama,意图识别选参数都可以
|
||||||
if (!"openai".equals(type) && !"ollama".equals(type)) {
|
if (!"openai".equals(type) && !"ollama".equals(type)) {
|
||||||
throw new RenException("设置的LLM不是openai和ollama");
|
throw new RenException(ErrorCode.INVALID_LLM_TYPE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
// 查看是否是默认
|
// 查看是否是默认
|
||||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
|
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
|
||||||
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
|
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
|
||||||
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
|
throw new RenException(ErrorCode.DEFAULT_MODEL_DELETE_ERROR);
|
||||||
}
|
}
|
||||||
// 验证是否有引用
|
// 验证是否有引用
|
||||||
checkAgentReference(id);
|
checkAgentReference(id);
|
||||||
@@ -180,7 +181,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
String agentNames = agents.stream()
|
String agentNames = agents.stream()
|
||||||
.map(AgentEntity::getAgentName)
|
.map(AgentEntity::getAgentName)
|
||||||
.collect(Collectors.joining("、"));
|
.collect(Collectors.joining("、"));
|
||||||
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
|
throw new RenException(ErrorCode.MODEL_REFERENCED_BY_AGENT, agentNames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
.eq("model_type", "Intent")
|
.eq("model_type", "Intent")
|
||||||
.like("config_json", "%" + modelId + "%"));
|
.like("config_json", "%" + modelId + "%"));
|
||||||
if (!intentConfigs.isEmpty()) {
|
if (!intentConfigs.isEmpty()) {
|
||||||
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
|
throw new RenException(ErrorCode.LLM_REFERENCED_BY_INTENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -16,6 +16,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
import cn.hutool.json.JSONArray;
|
import cn.hutool.json.JSONArray;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
@@ -109,7 +110,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
|||||||
modelProviderDTO.setFields(modelProviderDTO.getFields());
|
modelProviderDTO.setFields(modelProviderDTO.getFields());
|
||||||
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
|
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
|
||||||
if (modelProviderDao.insert(entity) == 0) {
|
if (modelProviderDao.insert(entity) == 0) {
|
||||||
throw new RenException("新增数据失败");
|
throw new RenException(ErrorCode.ADD_DATA_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||||
@@ -122,7 +123,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
|||||||
modelProviderDTO.setUpdateDate(new Date());
|
modelProviderDTO.setUpdateDate(new Date());
|
||||||
if (modelProviderDao
|
if (modelProviderDao
|
||||||
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||||
throw new RenException("修改数据失败");
|
throw new RenException(ErrorCode.UPDATE_DATA_FAILED);
|
||||||
}
|
}
|
||||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||||
}
|
}
|
||||||
@@ -130,14 +131,14 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
|||||||
@Override
|
@Override
|
||||||
public void delete(String id) {
|
public void delete(String id) {
|
||||||
if (modelProviderDao.deleteById(id) == 0) {
|
if (modelProviderDao.deleteById(id) == 0) {
|
||||||
throw new RenException("删除数据失败");
|
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(List<String> ids) {
|
public void delete(List<String> ids) {
|
||||||
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
||||||
throw new RenException("删除数据失败");
|
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package xiaozhi.modules.security.config;
|
|||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||||
@@ -12,8 +15,10 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
|
|||||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||||
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -95,5 +100,47 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
converter.setObjectMapper(mapper);
|
converter.setObjectMapper(mapper);
|
||||||
return converter;
|
return converter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 国际化配置 - 根据请求头中的Accept-Language设置语言环境
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public LocaleResolver localeResolver() {
|
||||||
|
return new AcceptHeaderLocaleResolver() {
|
||||||
|
@Override
|
||||||
|
public Locale resolveLocale(HttpServletRequest request) {
|
||||||
|
String acceptLanguage = request.getHeader("Accept-Language");
|
||||||
|
if (acceptLanguage == null || acceptLanguage.isEmpty()) {
|
||||||
|
return Locale.getDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析Accept-Language请求头中的首选语言
|
||||||
|
String[] languages = acceptLanguage.split(",");
|
||||||
|
if (languages.length > 0) {
|
||||||
|
// 提取第一个语言代码,去除可能的质量值(q=...)
|
||||||
|
String[] parts = languages[0].split(";" + "\\s*");
|
||||||
|
String primaryLanguage = parts[0].trim();
|
||||||
|
|
||||||
|
// 根据前端发送的语言代码直接创建Locale对象
|
||||||
|
if (primaryLanguage.equals("zh-CN")) {
|
||||||
|
return Locale.SIMPLIFIED_CHINESE;
|
||||||
|
} else if (primaryLanguage.equals("zh-TW")) {
|
||||||
|
return Locale.TRADITIONAL_CHINESE;
|
||||||
|
} else if (primaryLanguage.equals("en-US")) {
|
||||||
|
return Locale.US;
|
||||||
|
} else if (primaryLanguage.startsWith("zh")) {
|
||||||
|
// 对于其他中文变体,默认使用简体中文
|
||||||
|
return Locale.SIMPLIFIED_CHINESE;
|
||||||
|
} else if (primaryLanguage.startsWith("en")) {
|
||||||
|
// 对于其他英文变体,默认使用美式英语
|
||||||
|
return Locale.US;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有匹配的语言,使用默认语言
|
||||||
|
return Locale.getDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+14
-14
@@ -68,12 +68,12 @@ public class LoginController {
|
|||||||
// 验证图形验证码
|
// 验证图形验证码
|
||||||
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
|
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
throw new RenException("图形验证码错误");
|
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||||
}
|
}
|
||||||
Boolean isMobileRegister = sysParamsService
|
Boolean isMobileRegister = sysParamsService
|
||||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||||
if (!isMobileRegister) {
|
if (!isMobileRegister) {
|
||||||
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
|
throw new RenException(ErrorCode.MOBILE_REGISTER_DISABLED);
|
||||||
}
|
}
|
||||||
// 发送短信验证码
|
// 发送短信验证码
|
||||||
captchaService.sendSMSValidateCode(dto.getPhone());
|
captchaService.sendSMSValidateCode(dto.getPhone());
|
||||||
@@ -86,17 +86,17 @@ public class LoginController {
|
|||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
throw new RenException("图形验证码错误,请重新获取");
|
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||||
}
|
}
|
||||||
// 按照用户名获取用户
|
// 按照用户名获取用户
|
||||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||||
// 判断用户是否存在
|
// 判断用户是否存在
|
||||||
if (userDTO == null) {
|
if (userDTO == null) {
|
||||||
throw new RenException("请检测用户和密码是否输入错误");
|
throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
|
||||||
}
|
}
|
||||||
// 判断密码是否正确,不一样则进入if
|
// 判断密码是否正确,不一样则进入if
|
||||||
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
|
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
|
||||||
throw new RenException("请检测用户和密码是否输入错误");
|
throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
|
||||||
}
|
}
|
||||||
return sysUserTokenService.createToken(userDTO.getId());
|
return sysUserTokenService.createToken(userDTO.getId());
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ public class LoginController {
|
|||||||
@Operation(summary = "注册")
|
@Operation(summary = "注册")
|
||||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||||
if (!sysUserService.getAllowUserRegister()) {
|
if (!sysUserService.getAllowUserRegister()) {
|
||||||
throw new RenException("当前不允许普通用户注册");
|
throw new RenException(ErrorCode.USER_REGISTER_DISABLED);
|
||||||
}
|
}
|
||||||
// 是否开启手机注册
|
// 是否开启手机注册
|
||||||
Boolean isMobileRegister = sysParamsService
|
Boolean isMobileRegister = sysParamsService
|
||||||
@@ -115,25 +115,25 @@ public class LoginController {
|
|||||||
// 验证用户是否是手机号码
|
// 验证用户是否是手机号码
|
||||||
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
|
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
|
||||||
if (!validPhone) {
|
if (!validPhone) {
|
||||||
throw new RenException("用户名不是手机号码,请重新输入");
|
throw new RenException(ErrorCode.USERNAME_NOT_PHONE);
|
||||||
}
|
}
|
||||||
// 验证短信验证码是否正常
|
// 验证短信验证码是否正常
|
||||||
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
|
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
throw new RenException("手机验证码错误,请重新获取");
|
throw new RenException(ErrorCode.SMS_CODE_ERROR);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
throw new RenException("图形验证码错误,请重新获取");
|
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按照用户名获取用户
|
// 按照用户名获取用户
|
||||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||||
if (userDTO != null) {
|
if (userDTO != null) {
|
||||||
throw new RenException("此手机号码已经注册过");
|
throw new RenException(ErrorCode.PHONE_ALREADY_REGISTERED);
|
||||||
}
|
}
|
||||||
userDTO = new SysUserDTO();
|
userDTO = new SysUserDTO();
|
||||||
userDTO.setUsername(login.getUsername());
|
userDTO.setUsername(login.getUsername());
|
||||||
@@ -168,26 +168,26 @@ public class LoginController {
|
|||||||
Boolean isMobileRegister = sysParamsService
|
Boolean isMobileRegister = sysParamsService
|
||||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||||
if (!isMobileRegister) {
|
if (!isMobileRegister) {
|
||||||
throw new RenException("没有开启手机注册,没法使用找回密码功能");
|
throw new RenException(ErrorCode.RETRIEVE_PASSWORD_DISABLED);
|
||||||
}
|
}
|
||||||
// 判断非空
|
// 判断非空
|
||||||
ValidatorUtils.validateEntity(dto);
|
ValidatorUtils.validateEntity(dto);
|
||||||
// 验证用户是否是手机号码
|
// 验证用户是否是手机号码
|
||||||
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
|
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
|
||||||
if (!validPhone) {
|
if (!validPhone) {
|
||||||
throw new RenException("输入的手机号码格式不正确");
|
throw new RenException(ErrorCode.PHONE_FORMAT_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按照用户名获取用户
|
// 按照用户名获取用户
|
||||||
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
|
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
|
||||||
if (userDTO == null) {
|
if (userDTO == null) {
|
||||||
throw new RenException("输入的手机号码未注册");
|
throw new RenException(ErrorCode.PHONE_NOT_REGISTERED);
|
||||||
}
|
}
|
||||||
// 验证短信验证码是否正常
|
// 验证短信验证码是否正常
|
||||||
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
|
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
|
||||||
// 判断是否通过验证
|
// 判断是否通过验证
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
throw new RenException("输入的手机验证码错误");
|
throw new RenException(ErrorCode.SMS_CODE_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
|
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import xiaozhi.common.constant.Constant;
|
|||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.utils.HttpContextUtils;
|
import xiaozhi.common.utils.HttpContextUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
|
import xiaozhi.common.utils.MessageUtils;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,8 +83,8 @@ public class Oauth2Filter extends AuthenticatingFilter {
|
|||||||
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
|
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
||||||
try {
|
try {
|
||||||
Throwable throwable = e.getCause() == null ? e : e.getCause();
|
// 使用国际化消息替代直接使用异常消息
|
||||||
Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED, throwable.getMessage());
|
Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED);
|
||||||
|
|
||||||
String json = JsonUtils.toJsonString(r);
|
String json = JsonUtils.toJsonString(r);
|
||||||
httpResponse.getWriter().print(json);
|
httpResponse.getWriter().print(json);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.security.oauth2;
|
|||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,7 +39,7 @@ public class TokenGenerator {
|
|||||||
byte[] messageDigest = algorithm.digest();
|
byte[] messageDigest = algorithm.digest();
|
||||||
return toHexString(messageDigest);
|
return toHexString(messageDigest);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RenException("token invalid", e);
|
throw new RenException(ErrorCode.TOKEN_GENERATE_ERROR, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -16,6 +16,7 @@ import com.wf.captcha.base.Captcha;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -88,7 +89,7 @@ public class CaptchaServiceImpl implements CaptchaService {
|
|||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
long timeDiff = currentTime - lastSendTimeLong;
|
long timeDiff = currentTime - lastSendTimeLong;
|
||||||
if (timeDiff < 60000) {
|
if (timeDiff < 60000) {
|
||||||
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试");
|
throw new RenException(ErrorCode.SMS_SEND_TOO_FREQUENTLY, String.valueOf((60000 - timeDiff) / 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ public class CaptchaServiceImpl implements CaptchaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (todayCount >= maxSendCount) {
|
if (todayCount >= maxSendCount) {
|
||||||
throw new RenException("今日发送次数已达上限");
|
throw new RenException(ErrorCode.TODAY_SMS_LIMIT_REACHED);
|
||||||
}
|
}
|
||||||
|
|
||||||
String key = RedisKeys.getSMSValidateCodeKey(phone);
|
String key = RedisKeys.getSMSValidateCodeKey(phone);
|
||||||
|
|||||||
+3
-2
@@ -9,6 +9,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -45,7 +46,7 @@ public class ALiYunSmsService implements SmsService {
|
|||||||
redisUtils.delete(todayCountKey);
|
redisUtils.delete(todayCountKey);
|
||||||
// 错误 message
|
// 错误 message
|
||||||
log.error(e.getMessage());
|
log.error(e.getMessage());
|
||||||
throw new RenException("短信发送失败");
|
throw new RenException(ErrorCode.SMS_SEND_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ public class ALiYunSmsService implements SmsService {
|
|||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
// 错误 message
|
// 错误 message
|
||||||
log.error(e.getMessage());
|
log.error(e.getMessage());
|
||||||
throw new RenException("短信连接建立失败");
|
throw new RenException(ErrorCode.SMS_CONNECTION_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -22,6 +22,7 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.annotation.LogOperation;
|
import xiaozhi.common.annotation.LogOperation;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
|
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
|
||||||
@@ -64,17 +65,17 @@ public class ServerSideManageController {
|
|||||||
@RequiresPermissions("sys:role:superAdmin")
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
|
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
|
||||||
if (emitSeverActionDTO.getAction() == null) {
|
if (emitSeverActionDTO.getAction() == null) {
|
||||||
throw new RenException("无效服务端操作");
|
throw new RenException(ErrorCode.INVALID_SERVER_ACTION);
|
||||||
}
|
}
|
||||||
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
if (StringUtils.isBlank(wsText)) {
|
if (StringUtils.isBlank(wsText)) {
|
||||||
throw new RenException("未配置服务端WebSocket地址");
|
throw new RenException(ErrorCode.SERVER_WEBSOCKET_NOT_CONFIGURED);
|
||||||
}
|
}
|
||||||
String targetWs = emitSeverActionDTO.getTargetWs();
|
String targetWs = emitSeverActionDTO.getTargetWs();
|
||||||
String[] wsList = wsText.split(";");
|
String[] wsList = wsText.split(";");
|
||||||
// 找到需要发起的
|
// 找到需要发起的
|
||||||
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
|
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
|
||||||
throw new RenException("目标WebSocket地址不存在");
|
throw new RenException(ErrorCode.TARGET_WEBSOCKET_NOT_EXIST);
|
||||||
}
|
}
|
||||||
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
|
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,7 @@ public class ServerSideManageController {
|
|||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 捕获全部错误,由全局异常处理器返回
|
// 捕获全部错误,由全局异常处理器返回
|
||||||
throw new RenException("WebSocket连接失败或连接超时");
|
throw new RenException(ErrorCode.WEB_SOCKET_CONNECT_FAILED);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-25
@@ -25,6 +25,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import xiaozhi.common.annotation.LogOperation;
|
import xiaozhi.common.annotation.LogOperation;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
@@ -107,9 +108,12 @@ public class SysParamsController {
|
|||||||
// 验证MCP地址
|
// 验证MCP地址
|
||||||
validateMcpUrl(dto.getParamCode(), dto.getParamValue());
|
validateMcpUrl(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
//
|
// 验证声纹地址
|
||||||
validateVoicePrint(dto.getParamCode(), dto.getParamValue());
|
validateVoicePrint(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
|
// 校验mqtt密钥长度
|
||||||
|
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
sysParamsService.update(dto);
|
sysParamsService.update(dto);
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
@@ -127,23 +131,23 @@ public class SysParamsController {
|
|||||||
}
|
}
|
||||||
String[] wsUrls = urls.split("\\;");
|
String[] wsUrls = urls.split("\\;");
|
||||||
if (wsUrls.length == 0) {
|
if (wsUrls.length == 0) {
|
||||||
throw new RenException("WebSocket地址列表不能为空");
|
throw new RenException(ErrorCode.WEBSOCKET_URLS_EMPTY);
|
||||||
}
|
}
|
||||||
for (String url : wsUrls) {
|
for (String url : wsUrls) {
|
||||||
if (StringUtils.isNotBlank(url)) {
|
if (StringUtils.isNotBlank(url)) {
|
||||||
// 检查是否包含localhost或127.0.0.1
|
// 检查是否包含localhost或127.0.0.1
|
||||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
|
throw new RenException(ErrorCode.WEBSOCKET_URL_LOCALHOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证WebSocket地址格式
|
// 验证WebSocket地址格式
|
||||||
if (!WebSocketValidator.validateUrlFormat(url)) {
|
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||||
throw new RenException("WebSocket地址格式不正确: " + url);
|
throw new RenException(ErrorCode.WEBSOCKET_URL_FORMAT_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试WebSocket连接
|
// 测试WebSocket连接
|
||||||
if (!WebSocketValidator.testConnection(url)) {
|
if (!WebSocketValidator.testConnection(url)) {
|
||||||
throw new RenException("WebSocket连接测试失败: " + url);
|
throw new RenException(ErrorCode.WEBSOCKET_CONNECTION_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,35 +174,35 @@ public class SysParamsController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||||
throw new RenException("OTA地址不能为空");
|
throw new RenException(ErrorCode.OTA_URL_EMPTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否包含localhost或127.0.0.1
|
// 检查是否包含localhost或127.0.0.1
|
||||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
|
throw new RenException(ErrorCode.OTA_URL_LOCALHOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证URL格式
|
// 验证URL格式
|
||||||
if (!url.toLowerCase().startsWith("http")) {
|
if (!url.toLowerCase().startsWith("http")) {
|
||||||
throw new RenException("OTA地址必须以http或https开头");
|
throw new RenException(ErrorCode.OTA_URL_PROTOCOL_ERROR);
|
||||||
}
|
}
|
||||||
if (!url.endsWith("/ota/")) {
|
if (!url.endsWith("/ota/")) {
|
||||||
throw new RenException("OTA地址必须以/ota/结尾");
|
throw new RenException(ErrorCode.OTA_URL_FORMAT_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 发送GET请求
|
// 发送GET请求
|
||||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||||
if (response.getStatusCode() != HttpStatus.OK) {
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
|
throw new RenException(ErrorCode.OTA_INTERFACE_ACCESS_FAILED);
|
||||||
}
|
}
|
||||||
// 检查响应内容是否包含OTA相关信息
|
// 检查响应内容是否包含OTA相关信息
|
||||||
String body = response.getBody();
|
String body = response.getBody();
|
||||||
if (body == null || !body.contains("OTA")) {
|
if (body == null || !body.contains("OTA")) {
|
||||||
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
|
throw new RenException(ErrorCode.OTA_INTERFACE_FORMAT_ERROR);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
throw new RenException(ErrorCode.OTA_INTERFACE_VALIDATION_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,61 +211,86 @@ public class SysParamsController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||||
throw new RenException("MCP地址不能为空");
|
throw new RenException(ErrorCode.MCP_URL_EMPTY);
|
||||||
}
|
}
|
||||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
throw new RenException("MCP地址不能使用localhost或127.0.0.1");
|
throw new RenException(ErrorCode.MCP_URL_LOCALHOST);
|
||||||
}
|
}
|
||||||
if (!url.toLowerCase().contains("key")) {
|
if (!url.toLowerCase().contains("key")) {
|
||||||
throw new RenException("不是正确的MCP地址");
|
throw new RenException(ErrorCode.MCP_URL_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 发送GET请求
|
// 发送GET请求
|
||||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||||
if (response.getStatusCode() != HttpStatus.OK) {
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode());
|
throw new RenException(ErrorCode.MCP_INTERFACE_ACCESS_FAILED);
|
||||||
}
|
}
|
||||||
// 检查响应内容是否包含mcp相关信息
|
// 检查响应内容是否包含mcp相关信息
|
||||||
String body = response.getBody();
|
String body = response.getBody();
|
||||||
if (body == null || !body.contains("success")) {
|
if (body == null || !body.contains("success")) {
|
||||||
throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口");
|
throw new RenException(ErrorCode.MCP_INTERFACE_FORMAT_ERROR);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RenException("MCP接口验证失败:" + e.getMessage());
|
throw new RenException(ErrorCode.MCP_INTERFACE_VALIDATION_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证声纹接口地址是否正常
|
// 验证声纹接口地址是否正常
|
||||||
private void validateVoicePrint(String paramCode, String url) {
|
private void validateVoicePrint(String paramCode, String url) {
|
||||||
if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) {
|
if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||||
throw new RenException("声纹接口地址不能为空");
|
throw new RenException(ErrorCode.VOICEPRINT_URL_EMPTY);
|
||||||
}
|
}
|
||||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||||
throw new RenException("声纹接口地址不能使用localhost或127.0.0.1");
|
throw new RenException(ErrorCode.VOICEPRINT_URL_LOCALHOST);
|
||||||
}
|
}
|
||||||
if (!url.toLowerCase().contains("key")) {
|
if (!url.toLowerCase().contains("key")) {
|
||||||
throw new RenException("不是正确的声纹接口地址");
|
throw new RenException(ErrorCode.VOICEPRINT_URL_INVALID);
|
||||||
}
|
}
|
||||||
// 验证URL格式
|
// 验证URL格式
|
||||||
if (!url.toLowerCase().startsWith("http")) {
|
if (!url.toLowerCase().startsWith("http")) {
|
||||||
throw new RenException("声纹接口地址必须以http或https开头");
|
throw new RenException(ErrorCode.VOICEPRINT_URL_PROTOCOL_ERROR);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// 发送GET请求
|
// 发送GET请求
|
||||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||||
if (response.getStatusCode() != HttpStatus.OK) {
|
if (response.getStatusCode() != HttpStatus.OK) {
|
||||||
throw new RenException("声纹接口访问失败,状态码:" + response.getStatusCode());
|
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_ACCESS_FAILED);
|
||||||
}
|
}
|
||||||
// 检查响应内容
|
// 检查响应内容
|
||||||
String body = response.getBody();
|
String body = response.getBody();
|
||||||
if (body == null || !body.contains("healthy")) {
|
if (body == null || !body.contains("healthy")) {
|
||||||
throw new RenException("声纹接口返回内容格式不正确,可能不是一个真实的MCP接口");
|
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_FORMAT_ERROR);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RenException("声纹接口验证失败:" + e.getMessage());
|
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_VALIDATION_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验mqtt密钥长度和复杂度
|
||||||
|
private void validateMqttSecretLength(String paramCode, String secret) {
|
||||||
|
if (!paramCode.equals(Constant.SERVER_MQTT_SECRET)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(secret) || secret.equals("null")) {
|
||||||
|
throw new RenException(ErrorCode.MQTT_SECRET_EMPTY);
|
||||||
|
}
|
||||||
|
if (secret.length() < 8) {
|
||||||
|
throw new RenException(ErrorCode.MQTT_SECRET_LENGTH_INSECURE);
|
||||||
|
}
|
||||||
|
// 检查是否同时包含大小写字母
|
||||||
|
if (!secret.matches(".*[a-z].*") || !secret.matches(".*[A-Z].*")) {
|
||||||
|
throw new RenException(ErrorCode.MQTT_SECRET_CHARACTER_INSECURE);
|
||||||
|
}
|
||||||
|
// 不允许包含弱密码
|
||||||
|
String[] weakPasswords = { "test", "1234", "admin", "password", "qwerty", "xiaozhi" };
|
||||||
|
for (String weakPassword : weakPasswords) {
|
||||||
|
if (secret.toLowerCase().contains(weakPassword)) {
|
||||||
|
throw new RenException(ErrorCode.MQTT_SECRET_WEAK_PASSWORD);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -17,6 +17,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -153,7 +154,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
|||||||
}
|
}
|
||||||
boolean exists = baseDao.exists(queryWrapper);
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
throw new RenException("字典标签重复");
|
throw new RenException(ErrorCode.DICT_LABEL_DUPLICATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -18,6 +18,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
@@ -66,7 +67,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
public SysDictTypeVO get(Long id) {
|
public SysDictTypeVO get(Long id) {
|
||||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new RenException("字典类型不存在");
|
throw new RenException(ErrorCode.DICT_TYPE_NOT_EXIST);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
|
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
|
||||||
@@ -147,7 +148,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
|||||||
}
|
}
|
||||||
boolean exists = baseDao.exists(queryWrapper);
|
boolean exists = baseDao.exists(queryWrapper);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
throw new RenException("字典类型编码重复");
|
throw new RenException(ErrorCode.DICT_TYPE_DUPLICATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -115,7 +115,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
|
|
||||||
// 判断旧密码是否正确
|
// 判断旧密码是否正确
|
||||||
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
|
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
|
||||||
throw new RenException("旧密码输入错误");
|
throw new RenException(ErrorCode.OLD_PASSWORD_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新密码强度
|
// 新密码强度
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- VOSK ASR模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_ASR_VoskASR';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_VoskASR', 'ASR', 'vosk', 'VOSK离线语音识别', '[{"key": "model_path", "type": "string", "label": "模型路径"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]', 11, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- VOSK ASR模型配置
|
||||||
|
delete from `ai_model_config` where id = 'ASR_VoskASR';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('ASR_VoskASR', 'ASR', 'VoskASR', 'VOSK离线语音识别', 0, 1, '{\"type\": \"vosk\", \"model_path\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 更新VOSK ASR配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://alphacephei.com/vosk/',
|
||||||
|
`remark` = 'VOSK ASR配置说明:
|
||||||
|
1. VOSK是一个离线语音识别库,支持多种语言
|
||||||
|
2. 需要先下载模型文件:https://alphacephei.com/vosk/models
|
||||||
|
3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22
|
||||||
|
4. 完全离线运行,无需网络连接
|
||||||
|
5. 输出文件保存在tmp/目录
|
||||||
|
使用步骤:
|
||||||
|
1. 访问 https://alphacephei.com/vosk/models 下载中文模型
|
||||||
|
2. 解压模型文件到项目目录下的models/vosk/文件夹
|
||||||
|
3. 在配置中指定正确的模型路径
|
||||||
|
4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格
|
||||||
|
' WHERE `id` = 'ASR_VoskASR';
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- 添加 MinimaxHTTPStream 流式 TTS 供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_MinimaxStreamTTS';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_TTS_MinimaxStreamTTS', 'TTS', 'minimax_httpstream', 'Minimax流式语音合成', '[{"key":"group_id","label":"组ID","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice_id","label":"音色ID","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice_setting","label":"音色设置","type":"dict","dict_name":"voice_setting"},{"key":"pronunciation_dict","label":"发音字典","type":"dict","dict_name":"pronunciation_dict"},{"key":"audio_setting","label":"音频设置","type":"dict","dict_name":"audio_setting"},{"key":"timber_weights","label":"音色权重","type":"string"}]', 18, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- 添加Minimax流式TTS模型配置
|
||||||
|
delete from `ai_model_config` where id = 'TTS_MinimaxStreamTTS';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('TTS_MinimaxStreamTTS', 'TTS', 'MinimaxStreamTTS', 'Minimax流式语音合成', 0, 1, '{"type": "minimax_httpstream", "group_id": "", "api_key": "", "model": "speech-01-turbo", "voice_id": "female-shaonv", "output_dir": "tmp/", "voice_setting": {"speed": 1, "vol": 1, "pitch": 0, "emotion": "happy"}, "pronunciation_dict": {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}, "audio_setting": {"sample_rate": 24000, "bitrate": 128000, "format": "pcm", "channel": 1}}', NULL, NULL, 21, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 更新Minimax流式TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://platform.minimaxi.com/',
|
||||||
|
`remark` = 'Minimax流式TTS配置说明:
|
||||||
|
1. 需要先申请Minimax API Key
|
||||||
|
2. 需要填写Group ID
|
||||||
|
3. 支持多种音色设置和音频参数调整
|
||||||
|
4. 支持实时流式合成,具有较低的延迟
|
||||||
|
5. 支持自定义发音字典和音色权重
|
||||||
|
6. 隐藏参数配置:声音设定(voice_setting)、发音字典(pronunciation_dict)、音色权重(timber_weights)
|
||||||
|
- 语速(speed): 范围[0.5,2],默认1.0,取值越大语速越快
|
||||||
|
- 音量(vol): 范围(0,10],默认1.0,取值越大音量越高
|
||||||
|
- 音调(pitch): 范围[-12,12],默认0,取值需为整数
|
||||||
|
- 情绪(emotion): 控制合成语音的情绪,支持7种值:["happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm"],该参数仅对 speech-2.5-hd-preview、speech-2.5-turbo-preview、speech-02-hd、speech-02-turbo、speech-01-turbo、speech-01-hd 生效
|
||||||
|
- timbre_weights与voice_id二选一必填
|
||||||
|
- voice_id(请求的音色id,须和weight参数同步填写)
|
||||||
|
- weight(权重,最多支持4种音色混合。范围[1,100])
|
||||||
|
' WHERE `id` = 'TTS_MinimaxStreamTTS';
|
||||||
|
|
||||||
|
-- 添加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, 7, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0011', 'TTS_MinimaxStreamTTS', '淡雅学姐', 'danya_xuejie', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0012', 'TTS_MinimaxStreamTTS', 'Santa Claus', 'Santa_Claus', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0013', 'TTS_MinimaxStreamTTS', 'Grinch', 'Grinch', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
delete from `sys_params` where param_code = 'server.mqtt_gateway';
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (116, 'server.mqtt_gateway', 'null', 'string', 1, 'mqtt gateway 配置');
|
||||||
|
|
||||||
|
delete from `sys_params` where param_code = 'server.mqtt_signature_key';
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (117, 'server.mqtt_signature_key', 'null', 'string', 1, 'mqtt 密钥 配置');
|
||||||
|
|
||||||
|
delete from `sys_params` where param_code = 'server.udp_gateway';
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (118, 'server.udp_gateway', 'null', 'string', 1, 'udp gateway 配置');
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- 添加声纹识别相似度阈值参数配置
|
||||||
|
delete from `sys_params` where id = 115;
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
|
||||||
|
VALUES (115, 'server.voiceprint_similarity_threshold', '0.4', 'string', 1, '声纹识别相似度阈值,范围0.0-1.0,默认0.4,数值越高越严格');
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- 删除非流式MiniMax TTS配置,保留流式版本
|
||||||
|
|
||||||
|
-- 删除旧的非流式MiniMax TTS模型配置
|
||||||
|
DELETE FROM `ai_model_config` WHERE `id` = 'TTS_MinimaxTTS';
|
||||||
|
|
||||||
|
-- 删除旧的非流式MiniMax TTS供应器配置
|
||||||
|
DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_TTS_minimax';
|
||||||
|
|
||||||
|
-- 删除旧的非流式MiniMax TTS音色配置
|
||||||
|
DELETE FROM `ai_tts_voice` WHERE `tts_model_id` = 'TTS_MinimaxTTS';
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- 添加通义千问Qwen3-ASR-Flash语音识别服务配置
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_ASR_Qwen3Flash';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_Qwen3Flash', 'ASR', 'qwen3_asr_flash', 'Qwen3-ASR-Flash语音识别', '[{"key":"api_key","label":"API密钥","type":"password"},{"key":"base_url","label":"服务地址","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 17, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
delete from `ai_model_config` where id = 'ASR_Qwen3Flash';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('ASR_Qwen3Flash', 'ASR', 'Qwen3-ASR-Flash', '通义千问语音识别服务', 0, 1, '{"type": "qwen3_asr_flash", "api_key": "", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model_name": "qwen3-asr-flash", "output_dir": "tmp/", "enable_lid": true, "enable_itn": true}', 'https://help.aliyun.com/zh/bailian/', '支持多语言识别、歌唱识别、噪声拒识功能', 20, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 更新Qwen3-ASR-Flash模型配置的说明文档
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1&tab=doc#/doc/?type=model&url=2979031',
|
||||||
|
`remark` = '通义千问Qwen3-ASR-Flash配置说明:
|
||||||
|
1. 登录阿里云百炼平台https://bailian.console.aliyun.com/
|
||||||
|
2. 创建API-KEY https://bailian.console.aliyun.com/#/api-key
|
||||||
|
3.Qwen3-ASR-Flash基于通义千问多模态基座,支持多语言识别、歌唱识别、噪声拒识等功能
|
||||||
|
' WHERE `id` = 'ASR_Qwen3Flash';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
delete from `sys_params` where param_code = 'server.mqtt_manager_api';
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
|
||||||
|
VALUES (119, 'server.mqtt_manager_api', 'null', 'string', 1, 'MQTT网关管理API的地址');
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
-- 添加阿里百炼流式TTS 供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_AliBLStreamTTS';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_TTS_AliBLStreamTTS', 'TTS', 'alibl_stream', '阿里百炼流式语音合成', '[{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "rate", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"}]', 19, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- 添加阿里百炼流式TTS模型配置
|
||||||
|
delete from `ai_model_config` where id = 'TTS_AliBLStreamTTS';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('TTS_AliBLStreamTTS', 'TTS', 'AliBLStreamTTS', '阿里百炼流式语音合成', 0, 1, '{\"type\": \"alibl_stream\", \"appkey\": \"\", \"output_dir\": \"tmp/\", \"model\": \"cosyvoice-v2\", \"voice\": \"longcheng_v2\", \"format\": \"pcm\", \"sample_rate\": 24000, \"volume\": 50, \"rate\": 1, \"pitch\": 1}', NULL, NULL, 22, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 更新阿里百炼流式TTS配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
|
||||||
|
`remark` = '阿里百炼流式TTS说明:
|
||||||
|
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key 创建项目并获取appkey
|
||||||
|
2. 支持实时流式合成,具有较低的延迟
|
||||||
|
3. 支持多种音色设置和音频参数调整
|
||||||
|
4. 支持CosyVoice-V3大模型音色,价格实惠(0.4元/万字符)
|
||||||
|
5. 支持实时调节音量、语速、音调等参数
|
||||||
|
6. 如果需要使用CosyVoice-V3模型和一些限制类型的音色,需要联系阿里百炼客服申请
|
||||||
|
' WHERE `id` = 'TTS_AliBLStreamTTS';
|
||||||
|
|
||||||
|
-- 添加阿里百炼流式TTS音色
|
||||||
|
delete from `ai_tts_voice` where tts_model_id = 'TTS_AliBLStreamTTS';
|
||||||
|
|
||||||
|
-- 语音助手
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0001', 'TTS_AliBLStreamTTS', '龙小淳-知性积极女', 'longxiaochun_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0002', 'TTS_AliBLStreamTTS', '龙小夏-沉稳权威女', 'longxiaoxia_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 直播带货
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0003', 'TTS_AliBLStreamTTS', '龙安燃-活泼质感女', 'longanran', '中文及中英文混合', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0004', 'TTS_AliBLStreamTTS', '龙安宣-经典直播女', 'longanxuan', '中文及中英文混合', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 社交陪伴
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0005', 'TTS_AliBLStreamTTS', '龙寒-温暖痴情男', 'longhan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0006', 'TTS_AliBLStreamTTS', '龙颜-温暖春风女', 'longyan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0007', 'TTS_AliBLStreamTTS', '龙菲菲-甜美矫情女', 'longfeifei_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 方言
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0008', 'TTS_AliBLStreamTTS', '龙老铁-东北直率男', 'longlaotie_v2', '中文(东北)及中英文混合', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0009', 'TTS_AliBLStreamTTS', '龙嘉怡-知性粤语女', 'longjiayi_v2', '中文(粤语)及中英文混合', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 童声
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0010', 'TTS_AliBLStreamTTS', '龙杰力豆-阳光顽皮男', 'longjielidou_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0011', 'TTS_AliBLStreamTTS', '龙铃-稚气呆板女', 'longling_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 诗歌朗诵
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0012', 'TTS_AliBLStreamTTS', '李白-古代诗仙男', 'libai_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 出海营销
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0013', 'TTS_AliBLStreamTTS', 'loongeva-知性英文女', 'loongeva_v2', '英式英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0014', 'TTS_AliBLStreamTTS', 'loongbrian-沉稳英文男', 'loongbrian_v2', '英式英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0015', 'TTS_AliBLStreamTTS', 'loongkyong-韩语女', 'loongkyong_v2', '韩语', NULL, NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0016', 'TTS_AliBLStreamTTS', 'loongtomoka-日语女', 'loongtomoka_v2', '日语', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0017', 'TTS_AliBLStreamTTS', 'loongtomoya-日语男', 'loongtomoya_v2', '日语', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
|
||||||
@@ -303,3 +303,59 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202508131557.sql
|
path: classpath:db/changelog/202508131557.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202508271113
|
||||||
|
author: cgd
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202508271113.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202509051745
|
||||||
|
author: RanChen
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202509051745.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202509081140
|
||||||
|
author: cgd
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202509081140.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202509091042
|
||||||
|
author: cgd
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202509091042.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202509091633
|
||||||
|
author: fyb
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202509091633.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202509080922
|
||||||
|
author: fyb
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202509080922.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202509161610
|
||||||
|
author: fyb
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202509161610.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202509161701
|
||||||
|
author: RanChen
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202509161701.sql
|
||||||
|
|||||||
@@ -1,47 +1,137 @@
|
|||||||
#Default
|
#简体中文
|
||||||
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
500=服务器内部异常
|
||||||
401=\u672A\u6388\u6743
|
401=未授权
|
||||||
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
403=拒绝访问,没有权限
|
||||||
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
|
||||||
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
|
||||||
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
|
||||||
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
|
|
||||||
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
|
||||||
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
|
||||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
|
||||||
10008=\u624B\u673A\u53F7\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
|
||||||
10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21
|
|
||||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
|
||||||
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
|
||||||
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
|
||||||
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
|
||||||
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
|
||||||
10016=\u90E8\u7F72\u5931\u8D25\uFF0C\u6CA1\u6709\u6D41\u7A0B
|
|
||||||
10017=\u6A21\u578B\u56FE\u4E0D\u6B63\u786E\uFF0C\u8BF7\u68C0\u67E5
|
|
||||||
10018=\u5BFC\u51FA\u5931\u8D25\uFF0C\u6A21\u578BID\u4E3A{0}
|
|
||||||
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
|
||||||
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
|
||||||
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
|
||||||
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
|
||||||
10023=\u8BF7\u4E0A\u4F20zip\u3001bar\u3001bpmn\u3001bpmn20.xml\u683C\u5F0F\u6587\u4EF6
|
|
||||||
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
|
||||||
10025=\u53D1\u9001\u77ED\u4FE1\u5931\u8D25{0}
|
|
||||||
10026=\u90AE\u4EF6\u6A21\u677F\u4E0D\u5B58\u5728
|
|
||||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
|
||||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
|
||||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
|
||||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
|
||||||
10031=\u5F31\u5BC6\u7801
|
|
||||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
|
||||||
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
|
||||||
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
|
||||||
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
|
||||||
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
|
|
||||||
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
|
||||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
|
||||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
|
||||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
|
||||||
|
|
||||||
10041=\u8BBE\u5907\u672A\u627E\u5230
|
10001={0}不能为空
|
||||||
|
10002=数据库中已存在该记录
|
||||||
|
10003=获取参数失败
|
||||||
|
10004=账号或密码错误
|
||||||
|
10005=账号已被停用
|
||||||
|
10006=唯一标识不能为空
|
||||||
|
10007=验证码不正确
|
||||||
|
10008=先删除子菜单或按钮
|
||||||
|
10009=原密码不正确
|
||||||
|
10010=账号或密码不正确,您还有可以尝试{0}次
|
||||||
|
|
||||||
|
10011=上级部门选择错误
|
||||||
|
10012=上级菜单不能为自身
|
||||||
|
10013=数据权限接口,只能是Map类型参数
|
||||||
|
10014=请先删除下级部门
|
||||||
|
10015=请先删除部门下的用户
|
||||||
|
10016=部署失败,没有流程
|
||||||
|
10017=模型图不正确,请检查
|
||||||
|
10018=导出失败,模型ID为{0}
|
||||||
|
|
||||||
|
10019=请上传文件
|
||||||
|
10020=token不能为空
|
||||||
|
10021=token失效,请重新登录
|
||||||
|
10022=账号已被锁定
|
||||||
|
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
|
||||||
|
|
||||||
|
10024=上传文件失败{0}
|
||||||
|
10025=发送短信失败{0}
|
||||||
|
10026=邮件模板不存在
|
||||||
|
|
||||||
|
10027=Redis服务异常
|
||||||
|
10028=定时任务失败
|
||||||
|
10029=不能包含非法字符
|
||||||
|
10030=密码长度不足{0}位
|
||||||
|
10031=密码必须同时包含数字、大小写字母和特殊字符组成
|
||||||
|
10032=删除本数据异常
|
||||||
|
10033=设备验证码错误
|
||||||
|
|
||||||
|
10034=参数值不能为空
|
||||||
|
10035=参数类型不能为空
|
||||||
|
10036=不支持的参数类型
|
||||||
|
10037=参数值必须是有效的数字
|
||||||
|
10038=参数值必须是true或false
|
||||||
|
10039=参数值必须是有效的JSON数组格式
|
||||||
|
10040=参数值必须是有效的JSON格式
|
||||||
|
|
||||||
|
10041=设备未找到
|
||||||
10042={0}
|
10042={0}
|
||||||
|
10043=删除数据失败
|
||||||
|
10044=用户未登录
|
||||||
|
10045=WebSocket连接失败或连接超时
|
||||||
|
10046=保存声纹错误,请联系管理员
|
||||||
|
10047=每日发送次数已达上限
|
||||||
|
10048=旧密码输入错误
|
||||||
|
10049=设置的LLM不是openai和ollama
|
||||||
|
10050=token生成失败
|
||||||
|
10051=资源不存在
|
||||||
|
10052=默认智能体未找到
|
||||||
|
10053=智能体未找到
|
||||||
|
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
|
||||||
|
10055=短信发送失败
|
||||||
|
10056=短信连接建立失败
|
||||||
|
10057=智能体的声纹创建失败
|
||||||
|
10058=智能体的对应声纹更新失败
|
||||||
|
10059=智能体的对应声纹删除失败
|
||||||
|
10060=发送太频繁,请{0}秒后再试
|
||||||
|
10061=激活码不能为空
|
||||||
|
10062=激活码错误
|
||||||
|
10063=设备已激活
|
||||||
|
10064=该模型为默认模型,请先设置其他模型为默认模型
|
||||||
|
10065=新增数据失败
|
||||||
|
10066=修改数据失败
|
||||||
|
10067=图形验证码错误
|
||||||
|
10068=没有开启手机注册,没法使用短信验证码功能
|
||||||
|
10069=用户名不是手机号码,请重新输入
|
||||||
|
10070=此手机号码已经注册过
|
||||||
|
10071=输入的手机号码未注册
|
||||||
|
10072=当前不允许普通用户注册
|
||||||
|
10073=没有开启手机注册,没法使用找回密码功能
|
||||||
|
10074=输入的手机号码格式不正确
|
||||||
|
10075=输入的手机验证码错误
|
||||||
|
10076=字典类型不存在
|
||||||
|
10077=字典类型编码重复
|
||||||
|
10078=读取资源失败
|
||||||
|
10079=LLM大模型和Intent意图识别,选择参数不匹配
|
||||||
|
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
|
||||||
|
10081=删除声纹出现错误
|
||||||
|
10082=此次修改不允许,此声音已经注册为声纹了({0})
|
||||||
|
10083=修改声纹错误,请联系管理员
|
||||||
|
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
|
||||||
|
10085=音频数据不属于这个智能体
|
||||||
|
10086=音频数据是空的请检查上传数据
|
||||||
|
10087=声纹保存失败,请求不成功
|
||||||
|
10088=声纹保存失败,请求处理失败
|
||||||
|
10089=声纹注销失败,请求不成功
|
||||||
|
10090=声纹注销失败,请求处理失败
|
||||||
|
10091=供应器不存在
|
||||||
|
10092=设置的LLM不存在
|
||||||
|
10093=该模型配置已被智能体{0}引用,无法删除
|
||||||
|
10094=该LLM模型已被意图识别配置引用,无法删除
|
||||||
|
10095=无效服务端操作
|
||||||
|
10096=未配置服务端WebSocket地址
|
||||||
|
10097=目标WebSocket地址不存在
|
||||||
|
10098=WebSocket地址列表不能为空
|
||||||
|
10099=WebSocket地址不能使用localhost或127.0.0.1
|
||||||
|
10100=WebSocket地址格式不正确
|
||||||
|
10101=WebSocket连接测试失败
|
||||||
|
10102=OTA地址不能为空
|
||||||
|
10103=OTA地址不能使用localhost或127.0.0.1
|
||||||
|
10104=OTA地址必须以http或https开头
|
||||||
|
10105=OTA地址必须以/ota/结尾
|
||||||
|
10106=OTA接口访问失败
|
||||||
|
10107=OTA接口返回内容格式不正确
|
||||||
|
10108=OTA接口验证失败
|
||||||
|
10109=MCP地址不能为空
|
||||||
|
10110=MCP地址不能使用localhost或127.0.0.1
|
||||||
|
10111=不是正确的MCP地址
|
||||||
|
10112=MCP接口访问失败
|
||||||
|
10113=MCP接口返回内容格式不正确
|
||||||
|
10114=MCP接口验证失败
|
||||||
|
10115=声纹接口地址不能为空
|
||||||
|
10116=声纹接口地址不能使用localhost或127.0.0.1
|
||||||
|
10117=不是正确的声纹接口地址
|
||||||
|
10118=声纹接口地址必须以http或https开头
|
||||||
|
10119=声纹接口访问失败
|
||||||
|
10120=声纹接口返回内容格式不正确
|
||||||
|
10121=声纹接口验证失败
|
||||||
|
10122=mqtt密钥不能为空
|
||||||
|
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
|
||||||
|
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
|
||||||
|
10125=您的mqtt密钥包含弱密码
|
||||||
|
10128=字典标签重复
|
||||||
@@ -11,19 +11,26 @@
|
|||||||
10007=The verification code is incorrect
|
10007=The verification code is incorrect
|
||||||
10008=First delete submenu or button
|
10008=First delete submenu or button
|
||||||
10009=The original password is incorrect
|
10009=The original password is incorrect
|
||||||
|
10010=The account or password is incorrect, you can try {0} more times
|
||||||
|
|
||||||
10011=The superior department made a wrong choice
|
10011=The superior department made a wrong choice
|
||||||
10012=Upper menu cannot be for itself
|
10012=Upper menu cannot be for itself
|
||||||
10013=Data permission interface, which can only be a Map type parameter.
|
10013=Data permission interface, which can only be a Map type parameter.
|
||||||
10014=Please delete the subordinate department first
|
10014=Please delete the subordinate department first
|
||||||
10015=Please delete the user under the department first
|
10015=Please delete the user under the department first
|
||||||
|
10016=Deployment failed, no process
|
||||||
|
10017=The model diagram is incorrect, please check
|
||||||
|
10018=Export failed, model ID is {0}
|
||||||
|
|
||||||
10019=Please upload a file
|
10019=Please upload a file
|
||||||
10020=token cannot be empty
|
10020=token cannot be empty
|
||||||
10021=token is invalid, please log in again
|
10021=token is invalid, please log in again
|
||||||
10022=The account has been locked
|
10022=The account has been locked
|
||||||
|
10023=Please upload files in zip, bar, bpmn, bpmn20.xml format
|
||||||
|
|
||||||
10024=Failed to upload file {0}
|
10024=Failed to upload file {0}
|
||||||
|
10025=Failed to send SMS {0}
|
||||||
|
10026=Email template does not exist
|
||||||
|
|
||||||
10027=Redis service exception
|
10027=Redis service exception
|
||||||
10028=Timed task failed
|
10028=Timed task failed
|
||||||
@@ -43,4 +50,88 @@
|
|||||||
10040=Parameter value must be a valid JSON format
|
10040=Parameter value must be a valid JSON format
|
||||||
|
|
||||||
10041=Device not found
|
10041=Device not found
|
||||||
10042={0}
|
10042={0}
|
||||||
|
10043=Failed to delete data
|
||||||
|
10044=User not logged in
|
||||||
|
10045=WebSocket connection failed or timeout
|
||||||
|
10046=Failed to save voiceprint, please contact administrator
|
||||||
|
10047=Daily sending limit reached
|
||||||
|
10048=Old password is incorrect
|
||||||
|
10049=The set LLM is not openai or ollama
|
||||||
|
10050=Failed to generate token
|
||||||
|
10051=Resource not found
|
||||||
|
10052=Default agent not found
|
||||||
|
10053=Agent not found
|
||||||
|
10054=Voiceprint interface not configured, please configure voiceprint interface address (server.voice_print) in parameter configuration first
|
||||||
|
10055=Failed to send SMS
|
||||||
|
10056=Failed to establish SMS connection
|
||||||
|
10057=Failed to create voiceprint for agent
|
||||||
|
10058=Failed to update voiceprint for agent
|
||||||
|
10059=Failed to delete voiceprint for agent
|
||||||
|
10060=Sending too frequently, please try again after {0} seconds
|
||||||
|
10061=Activation code cannot be empty
|
||||||
|
10062=Activation code error
|
||||||
|
10063=Device already activated
|
||||||
|
10064=This model is the default model, please set another model as default first
|
||||||
|
10065=Failed to add data
|
||||||
|
10066=Failed to update data
|
||||||
|
10067=Graphic verification code error
|
||||||
|
10068=Mobile registration is not enabled, SMS verification code function cannot be used
|
||||||
|
10069=Username is not a mobile phone number, please re-enter
|
||||||
|
10070=This mobile phone number has already been registered
|
||||||
|
10071=The entered mobile phone number is not registered
|
||||||
|
10072=Ordinary user registration is not allowed currently
|
||||||
|
10073=Mobile registration is not enabled, password retrieval function cannot be used
|
||||||
|
10074=The entered mobile phone number format is incorrect
|
||||||
|
10075=The entered SMS verification code is incorrect
|
||||||
|
10076=Dictionary type does not exist
|
||||||
|
10077=Dictionary type code is duplicated
|
||||||
|
10078=Failed to read resource
|
||||||
|
10079=LLM model and Intent recognition, parameter selection does not match
|
||||||
|
10080=This voiceprint belongs to {0} who has already registered, please select another voice
|
||||||
|
10081=Error occurred while deleting voiceprint
|
||||||
|
10082=Modification not allowed, this voice has already been registered as a voiceprint ({0})
|
||||||
|
10083=Error modifying voiceprint, please contact administrator
|
||||||
|
10084=Voiceprint interface address error, please go to parameter management to modify the voiceprint interface address
|
||||||
|
10085=Audio data does not belong to this agent
|
||||||
|
10086=Audio data is empty, please check uploaded data
|
||||||
|
10087=Voiceprint registration failed, request unsuccessful
|
||||||
|
10088=Voiceprint registration failed, request processing failed
|
||||||
|
10089=Voiceprint cancellation failed, request unsuccessful
|
||||||
|
10090=Voiceprint cancellation failed, request processing failed
|
||||||
|
10091=Model provider does not exist
|
||||||
|
10092=The configured LLM does not exist
|
||||||
|
10093=This model configuration is referenced by agent {0} and cannot be deleted
|
||||||
|
10094=This LLM model is referenced by intent recognition configuration and cannot be deleted
|
||||||
|
10095=Invalid server operation
|
||||||
|
10096=Server WebSocket address not configured
|
||||||
|
10097=Target WebSocket address does not exist
|
||||||
|
10098=WebSocket address list cannot be empty
|
||||||
|
10099=WebSocket address cannot use localhost or 127.0.0.1
|
||||||
|
10100=WebSocket address format is incorrect
|
||||||
|
10101=WebSocket connection test failed
|
||||||
|
10102=OTA address cannot be empty
|
||||||
|
10103=OTA address cannot use localhost or 127.0.0.1
|
||||||
|
10104=OTA address must start with http or https
|
||||||
|
10105=OTA address must end with /ota/
|
||||||
|
10106=OTA interface access failed
|
||||||
|
10107=OTA interface return content format is incorrect
|
||||||
|
10108=OTA interface validation failed
|
||||||
|
10109=MCP address cannot be empty
|
||||||
|
10110=MCP address cannot use localhost or 127.0.0.1
|
||||||
|
10111=Not a valid MCP address
|
||||||
|
10112=MCP interface access failed
|
||||||
|
10113=MCP interface return content format is incorrect
|
||||||
|
10114=MCP interface validation failed
|
||||||
|
10115=Voiceprint interface address cannot be empty
|
||||||
|
10116=Voiceprint interface address cannot use localhost or 127.0.0.1
|
||||||
|
10117=Not a valid voiceprint interface address
|
||||||
|
10118=Voiceprint interface address must start with http or https
|
||||||
|
10119=Voiceprint interface access failed
|
||||||
|
10120=Voiceprint interface return content format is incorrect
|
||||||
|
10121=Voiceprint interface validation failed
|
||||||
|
10122=MQTT secret cannot be empty
|
||||||
|
10123=Your MQTT secret is not secure, it needs to be at least 8 characters and must contain both uppercase and lowercase letters
|
||||||
|
10124=Your MQTT secret is not secure, MQTT secret must contain both uppercase and lowercase letters
|
||||||
|
10125=Your MQTT secret contains weak password
|
||||||
|
10128=Dictionary label is duplicated
|
||||||
@@ -1,46 +1,137 @@
|
|||||||
#\u7B80\u4F53\u4E2D\u6587
|
#简体中文
|
||||||
500=\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38
|
500=服务器内部异常
|
||||||
401=\u672A\u6388\u6743
|
401=未授权
|
||||||
403=\u62D2\u7EDD\u8BBF\u95EE\uFF0C\u6CA1\u6709\u6743\u9650
|
403=拒绝访问,没有权限
|
||||||
|
|
||||||
10001={0}\u4E0D\u80FD\u4E3A\u7A7A
|
10001={0}不能为空
|
||||||
10002=\u6570\u636E\u5E93\u4E2D\u5DF2\u5B58\u5728\u8BE5\u8BB0\u5F55
|
10002=数据库中已存在该记录
|
||||||
10003=\u83B7\u53D6\u53C2\u6570\u5931\u8D25
|
10003=获取参数失败
|
||||||
10004=\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF
|
10004=账号或密码错误
|
||||||
10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528
|
10005=账号已被停用
|
||||||
10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
10006=唯一标识不能为空
|
||||||
10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E
|
10007=验证码不正确
|
||||||
10008=\u5148\u5220\u9664\u5B50\u83DC\u5355\u6216\u6309\u94AE
|
10008=先删除子菜单或按钮
|
||||||
10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E
|
10009=原密码不正确
|
||||||
|
10010=账号或密码不正确,您还有可以尝试{0}次
|
||||||
|
|
||||||
10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF
|
10011=上级部门选择错误
|
||||||
10012=\u4E0A\u7EA7\u83DC\u5355\u4E0D\u80FD\u4E3A\u81EA\u8EAB
|
10012=上级菜单不能为自身
|
||||||
10013=\u6570\u636E\u6743\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u7C7B\u578B\u53C2\u6570
|
10013=数据权限接口,只能是Map类型参数
|
||||||
10014=\u8BF7\u5148\u5220\u9664\u4E0B\u7EA7\u90E8\u95E8
|
10014=请先删除下级部门
|
||||||
10015=\u8BF7\u5148\u5220\u9664\u90E8\u95E8\u4E0B\u7684\u7528\u6237
|
10015=请先删除部门下的用户
|
||||||
|
10016=部署失败,没有流程
|
||||||
|
10017=模型图不正确,请检查
|
||||||
|
10018=导出失败,模型ID为{0}
|
||||||
|
|
||||||
10019=\u8BF7\u4E0A\u4F20\u6587\u4EF6
|
10019=请上传文件
|
||||||
10020=token\u4E0D\u80FD\u4E3A\u7A7A
|
10020=token不能为空
|
||||||
10021=token\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55
|
10021=token失效,请重新登录
|
||||||
10022=\u8D26\u53F7\u5DF2\u88AB\u9501\u5B9A
|
10022=账号已被锁定
|
||||||
|
10023=请上传zip、bar、bpmn、bpmn20.xml格式文件
|
||||||
|
|
||||||
10024=\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25{0}
|
10024=上传文件失败{0}
|
||||||
|
10025=发送短信失败{0}
|
||||||
|
10026=邮件模板不存在
|
||||||
|
|
||||||
10027=Redis\u670D\u52A1\u5F02\u5E38
|
10027=Redis服务异常
|
||||||
10028=\u5B9A\u65F6\u4EFB\u52A1\u5931\u8D25
|
10028=定时任务失败
|
||||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
10029=不能包含非法字符
|
||||||
10030=\u5BC6\u7801\u957F\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
10030=密码长度不足{0}位
|
||||||
10031=\u5BC6\u7801\u5FC5\u987B\u540C\u65F6\u5305\u542B\u6570\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7EC4\u6210
|
10031=密码必须同时包含数字、大小写字母和特殊字符组成
|
||||||
10032=\u5220\u9664\u672C\u6570\u636E\u5F02\u5E38
|
10032=删除本数据异常
|
||||||
10033=\u8BBE\u5907\u9A8C\u8BC1\u7801\u9519\u8BEF
|
10033=设备验证码错误
|
||||||
|
|
||||||
10034=\u53C2\u6570\u503C\u4E0D\u80FD\u4E3A\u7A7A
|
10034=参数值不能为空
|
||||||
10035=\u53C2\u6570\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
10035=参数类型不能为空
|
||||||
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
|
10036=不支持的参数类型
|
||||||
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
10037=参数值必须是有效的数字
|
||||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
10038=参数值必须是true或false
|
||||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
10039=参数值必须是有效的JSON数组格式
|
||||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=参数值必须是有效的JSON格式
|
||||||
|
|
||||||
10041=\u8BBE\u5907\u672A\u627E\u5230
|
10041=设备未找到
|
||||||
10042={0}
|
10042={0}
|
||||||
|
10043=删除数据失败
|
||||||
|
10044=用户未登录
|
||||||
|
10045=WebSocket连接失败或连接超时
|
||||||
|
10046=保存声纹错误,请联系管理员
|
||||||
|
10047=每日发送次数已达上限
|
||||||
|
10048=旧密码输入错误
|
||||||
|
10049=设置的LLM不是openai和ollama
|
||||||
|
10050=token生成失败
|
||||||
|
10051=资源不存在
|
||||||
|
10052=默认智能体未找到
|
||||||
|
10053=智能体未找到
|
||||||
|
10054=声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)
|
||||||
|
10055=短信发送失败
|
||||||
|
10056=短信连接建立失败
|
||||||
|
10057=智能体的声纹创建失败
|
||||||
|
10058=智能体的对应声纹更新失败
|
||||||
|
10059=智能体的对应声纹删除失败
|
||||||
|
10060=发送太频繁,请{0}秒后再试
|
||||||
|
10061=激活码不能为空
|
||||||
|
10062=激活码错误
|
||||||
|
10063=设备已激活
|
||||||
|
10064=该模型为默认模型,请先设置其他模型为默认模型
|
||||||
|
10065=新增数据失败
|
||||||
|
10066=修改数据失败
|
||||||
|
10067=图形验证码错误
|
||||||
|
10068=没有开启手机注册,没法使用短信验证码功能
|
||||||
|
10069=用户名不是手机号码,请重新输入
|
||||||
|
10070=此手机号码已经注册过
|
||||||
|
10071=输入的手机号码未注册
|
||||||
|
10072=当前不允许普通用户注册
|
||||||
|
10073=没有开启手机注册,没法使用找回密码功能
|
||||||
|
10074=输入的手机号码格式不正确
|
||||||
|
10075=输入的手机验证码错误
|
||||||
|
10076=字典类型不存在
|
||||||
|
10077=字典类型编码重复
|
||||||
|
10078=读取资源失败
|
||||||
|
10079=LLM大模型和Intent意图识别,选择参数不匹配
|
||||||
|
10080=此声音声纹对应的人({0})已经注册,请选择其他声音注册
|
||||||
|
10081=删除声纹出现错误
|
||||||
|
10082=此次修改不允许,此声音已经注册为声纹了({0})
|
||||||
|
10083=修改声纹错误,请联系管理员
|
||||||
|
10084=声纹接口地址存在错误,请进入参数管理修改声纹接口地址
|
||||||
|
10085=音频数据不属于这个智能体
|
||||||
|
10086=音频数据是空的请检查上传数据
|
||||||
|
10087=声纹保存失败,请求不成功
|
||||||
|
10088=声纹保存失败,请求处理失败
|
||||||
|
10089=声纹注销失败,请求不成功
|
||||||
|
10090=声纹注销失败,请求处理失败
|
||||||
|
10091=供应器不存在
|
||||||
|
10092=设置的LLM不存在
|
||||||
|
10093=该模型配置已被智能体{0}引用,无法删除
|
||||||
|
10094=该LLM模型已被意图识别配置引用,无法删除
|
||||||
|
10095=无效服务端操作
|
||||||
|
10096=未配置服务端WebSocket地址
|
||||||
|
10097=目标WebSocket地址不存在
|
||||||
|
10098=WebSocket地址列表不能为空
|
||||||
|
10099=WebSocket地址不能使用localhost或127.0.0.1
|
||||||
|
10100=WebSocket地址格式不正确
|
||||||
|
10101=WebSocket连接测试失败
|
||||||
|
10102=OTA地址不能为空
|
||||||
|
10103=OTA地址不能使用localhost或127.0.0.1
|
||||||
|
10104=OTA地址必须以http或https开头
|
||||||
|
10105=OTA地址必须以/ota/结尾
|
||||||
|
10106=OTA接口访问失败
|
||||||
|
10107=OTA接口返回内容格式不正确
|
||||||
|
10108=OTA接口验证失败
|
||||||
|
10109=MCP地址不能为空
|
||||||
|
10110=MCP地址不能使用localhost或127.0.0.1
|
||||||
|
10111=不是正确的MCP地址
|
||||||
|
10112=MCP接口访问失败
|
||||||
|
10113=MCP接口返回内容格式不正确
|
||||||
|
10114=MCP接口验证失败
|
||||||
|
10115=声纹接口地址不能为空
|
||||||
|
10116=声纹接口地址不能使用localhost或127.0.0.1
|
||||||
|
10117=不是正确的声纹接口地址
|
||||||
|
10118=声纹接口地址必须以http或https开头
|
||||||
|
10119=声纹接口访问失败
|
||||||
|
10120=声纹接口返回内容格式不正确
|
||||||
|
10121=声纹接口验证失败
|
||||||
|
10122=mqtt密钥不能为空
|
||||||
|
10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母
|
||||||
|
10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母
|
||||||
|
10125=您的mqtt密钥包含弱密码
|
||||||
|
10128=字典标签重复
|
||||||
@@ -1,46 +1,137 @@
|
|||||||
#\u7E41\u4F53\u4E2D\u6587
|
#繁體中文
|
||||||
500=\u670D\u52D9\u5668\u5167\u90E8\u7570\u5E38
|
500=服務器內部異常
|
||||||
401=\u672A\u6388\u6B0A
|
401=未授權
|
||||||
403=\u62D2\u7D55\u8A2A\u554F\uFF0C\u6C92\u6709\u6B0A\u9650
|
403=拒絕訪問,沒有权限
|
||||||
|
|
||||||
10001={0}\u4E0D\u80FD\u70BA\u7A7A
|
10001={0}不能為空
|
||||||
10002=\u6578\u64DA\u5EAB\u4E2D\u5DF2\u5B58\u5728\u8A72\u8A18\u9304
|
10002=數據庫中已存在該記錄
|
||||||
10003=\u7372\u53D6\u53C3\u6578\u5931\u6557
|
10003=獲取參數失敗
|
||||||
10004=\u8CEC\u865F\u6216\u5BC6\u78BC\u932F\u8AA4
|
10004=賬號或密碼錯誤
|
||||||
10005=\u8CEC\u865F\u5DF2\u88AB\u505C\u7528
|
10005=賬號已被停用
|
||||||
10006=\u552F\u4E00\u6A19\u8B58\u4E0D\u80FD\u70BA\u7A7A
|
10006=唯一標識不能為空
|
||||||
10007=\u9A57\u8B49\u78BC\u4E0D\u6B63\u78BA
|
10007=驗證碼不正確
|
||||||
10008=\u5148\u522A\u9664\u5B50\u83DC\u55AE\u6216\u6309\u9215
|
10008=先刪除子菜單或按鈕
|
||||||
10009=\u539F\u5BC6\u78BC\u4E0D\u6B63\u78BA
|
10009=原密碼不正確
|
||||||
|
10010=帳號或密碼不正確,您還有可以嘗試{0}次
|
||||||
|
|
||||||
10011=\u4E0A\u7D1A\u90E8\u9580\u9078\u64C7\u932F\u8AA4
|
10011=上級部門選擇錯誤
|
||||||
10012=\u4E0A\u7D1A\u83DC\u55AE\u4E0D\u80FD\u70BA\u81EA\u8EAB
|
10012=上級菜單不能為自身
|
||||||
10013=\u6578\u64DA\u6B0A\u9650\u63A5\u53E3\uFF0C\u53EA\u80FD\u662FMap\u985E\u578B\u53C3\u6578
|
10013=數據權限接口,只能是Map類型參數
|
||||||
10014=\u8ACB\u5148\u522A\u9664\u4E0B\u7D1A\u90E8\u9580
|
10014=請先刪除下級部門
|
||||||
10015=\u8ACB\u5148\u522A\u9664\u90E8\u9580\u4E0B\u7684\u7528\u6236
|
10015=請先刪除部門下的用戶
|
||||||
|
10016=部署失敗,沒有流程
|
||||||
|
10017=模型圖不正確,請檢查
|
||||||
|
10018=導出失敗,模型ID為{0}
|
||||||
|
|
||||||
10019=\u8ACB\u4E0A\u50B3\u6587\u4EF6
|
10019=請上傳文件
|
||||||
10020=token\u4E0D\u80FD\u70BA\u7A7A
|
10020=token不能為空
|
||||||
10021=token\u5931\u6548\uFF0C\u8ACB\u91CD\u65B0\u767B\u9304
|
10021=token失效,請重新登錄
|
||||||
10022=\u8CEC\u865F\u5DF2\u88AB\u9396\u5B9A
|
10022=賬號已被鎖定
|
||||||
|
10023=請上傳zip、bar、bpmn、bpmn20.xml格式文件
|
||||||
|
|
||||||
10024=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557{0}
|
10024=上傳文件失敗{0}
|
||||||
|
10025=發送短信失敗{0}
|
||||||
|
10026=郵件模板不存在
|
||||||
|
|
||||||
10027=Redis\u670D\u52D9\u7570\u5E38
|
10027=Redis服務異常
|
||||||
10028=\u5B9A\u6642\u4EFB\u52D9\u5931\u6557
|
10028=定時任務失敗
|
||||||
10029=\u4E0D\u80FD\u5305\u542B\u975E\u6CD5\u5B57\u7B26
|
10029=不能包含非法字符
|
||||||
10030=\u5BC6\u78BC\u9577\u5EA6\u4E0D\u8DB3{0}\u4F4D
|
10030=密碼長度不足{0}位
|
||||||
10031=\u5BC6\u78BC\u5FC5\u9808\u540C\u6642\u5305\u542B\u6578\u5B57\u3001\u5927\u5C0F\u5B57\u6BCD\u548C\u7279\u6B8A\u5B57\u7B26\u7D44\u6210
|
10031=密碼必須同時包含數字、大小寫字母和特殊字符組成
|
||||||
10032=\u522A\u9664\u6B64\u6578\u64DA\u7570\u5E38
|
10032=刪除本數據異常
|
||||||
10033=\u8A2D\u5099\u9A57\u8B49\u78BC\u932F\u8AA4
|
10033=設備驗證碼錯誤
|
||||||
|
|
||||||
10034=\u53C3\u6578\u503C\u4E0D\u80FD\u70BA\u7A7A
|
10034=參數值不能為空
|
||||||
10035=\u53C3\u6578\u985E\u578B\u4E0D\u80FD\u70BA\u7A7A
|
10035=參數類型不能為空
|
||||||
10036=\u4E0D\u652F\u63F4\u7684\u53C3\u6578\u985E\u578B
|
10036=不支持的參數類型
|
||||||
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
|
10037=參數值必須是有效的數字
|
||||||
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
|
10038=參數值必須是true或false
|
||||||
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
|
10039=參數值必須是有效的JSON數組格式
|
||||||
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=參數值必須是有效的JSON格式
|
||||||
|
|
||||||
10041=\u8A2D\u5099\u672A\u627E\u5230
|
10041=設備未找到
|
||||||
10042={0}
|
10042={0}
|
||||||
|
10043=刪除數據失敗
|
||||||
|
10044=用戶未登錄
|
||||||
|
10045=WebSocket連接失敗或連接超時
|
||||||
|
10046=保存聲紋錯誤,請聯繫管理員
|
||||||
|
10047=每日發送次數已達上限
|
||||||
|
10048=舊密碼輸入錯誤
|
||||||
|
10049=設置的LLM不是openai和ollama
|
||||||
|
10050=token生成失敗
|
||||||
|
10051=資源不存在
|
||||||
|
10052=默認智能體未找到
|
||||||
|
10053=智能體未找到
|
||||||
|
10054=聲紋接口未配置,請先在參數配置中配置聲紋接口地址(server.voice_print)
|
||||||
|
10055=短信發送失敗
|
||||||
|
10056=短信連接建立失敗
|
||||||
|
10057=智能體的聲紋創建失敗
|
||||||
|
10058=智能體的對應聲紋更新失敗
|
||||||
|
10059=智能體的對應聲紋刪除失敗
|
||||||
|
10060=發送太頻繁,請{0}秒後再試
|
||||||
|
10061=激活碼不能為空
|
||||||
|
10062=激活碼錯誤
|
||||||
|
10063=設備已激活
|
||||||
|
10064=該模型為預設模型,請先設置其他模型為預設模型
|
||||||
|
10065=新增數據失敗
|
||||||
|
10066=更新數據失敗
|
||||||
|
10067=圖形驗證碼錯誤
|
||||||
|
10068=未開啟手機註冊,無法使用短信驗證碼功能
|
||||||
|
10069=用戶名不是手機號碼,請重新輸入
|
||||||
|
10070=該手機號碼已註冊
|
||||||
|
10071=輸入的手機號碼未註冊
|
||||||
|
10072=目前不允許普通用戶註冊
|
||||||
|
10073=未開啟手機註冊,無法使用密碼找回功能
|
||||||
|
10074=輸入的手機號碼格式不正確
|
||||||
|
10075=輸入的短信驗證碼不正確
|
||||||
|
10076=字典類型不存在
|
||||||
|
10077=字典類型編碼重複
|
||||||
|
10078=讀取資源失敗
|
||||||
|
10079=LLM模型與意圖識別、參數選擇不匹配
|
||||||
|
10080=此聲紋屬於已註冊的{0},請選擇其他聲音
|
||||||
|
10081=刪除聲紋時發生錯誤
|
||||||
|
10082=不允許修改,此聲音已註冊為聲紋({0})
|
||||||
|
10083=修改聲紋錯誤,請聯繫管理員
|
||||||
|
10084=聲紋接口地址錯誤,請前往參數管理修改聲紋接口地址
|
||||||
|
10085=音頻數據不屬於此智能體
|
||||||
|
10086=音頻數據為空,請檢查上傳數據
|
||||||
|
10087=聲紋註冊失敗,請求未成功
|
||||||
|
10088=聲紋註冊失敗,請求處理失敗
|
||||||
|
10089=聲紋註銷失敗,請求未成功
|
||||||
|
10090=聲紋註銷失敗,請求處理失敗
|
||||||
|
10091=模型提供商不存在
|
||||||
|
10092=配置的LLM不存在
|
||||||
|
10093=此模型配置被智能體{0}引用,無法刪除
|
||||||
|
10094=此LLM模型被意圖識別配置引用,無法刪除
|
||||||
|
10095=無效的服務器操作
|
||||||
|
10096=未配置服務器WebSocket地址
|
||||||
|
10097=目標WebSocket地址不存在
|
||||||
|
10098=WebSocket地址列表不能為空
|
||||||
|
10099=WebSocket地址不能使用localhost或127.0.0.1
|
||||||
|
10100=WebSocket地址格式不正確
|
||||||
|
10101=WebSocket連接測試失敗
|
||||||
|
10102=OTA地址不能為空
|
||||||
|
10103=OTA地址不能使用localhost或127.0.0.1
|
||||||
|
10104=OTA地址必須以http或https開頭
|
||||||
|
10105=OTA地址必須以/ota/結尾
|
||||||
|
10106=OTA接口訪問失敗
|
||||||
|
10107=OTA接口返回內容格式不正確
|
||||||
|
10108=OTA接口驗證失敗
|
||||||
|
10109=MCP地址不能為空
|
||||||
|
10110=MCP地址不能使用localhost或127.0.0.1
|
||||||
|
10111=不是有效的MCP地址
|
||||||
|
10112=MCP接口訪問失敗
|
||||||
|
10113=MCP接口返回內容格式不正確
|
||||||
|
10114=MCP接口驗證失敗
|
||||||
|
10115=聲紋接口地址不能為空
|
||||||
|
10116=聲紋接口地址不能使用localhost或127.0.0.1
|
||||||
|
10117=不是有效的聲紋接口地址
|
||||||
|
10118=聲紋接口地址必須以http或https開頭
|
||||||
|
10119=聲紋接口訪問失敗
|
||||||
|
10120=聲紋接口返回內容格式不正確
|
||||||
|
10121=聲紋接口驗證失敗
|
||||||
|
10122=MQTT密鑰不能為空
|
||||||
|
10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母
|
||||||
|
10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母
|
||||||
|
10125=您的MQTT密鑰包含弱密碼
|
||||||
|
10128=字典標籤重複
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 159 KiB |
@@ -56,11 +56,11 @@ function getCacheInfo() {
|
|||||||
// 验证URL格式
|
// 验证URL格式
|
||||||
function validateUrl() {
|
function validateUrl() {
|
||||||
urlError.value = ''
|
urlError.value = ''
|
||||||
|
|
||||||
if (!baseUrlInput.value) {
|
if (!baseUrlInput.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
|
if (!/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
|
||||||
urlError.value = '请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)'
|
urlError.value = '请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)'
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ function validateUrl() {
|
|||||||
async function testServerBaseUrl() {
|
async function testServerBaseUrl() {
|
||||||
// 先清除错误信息
|
// 先清除错误信息
|
||||||
urlError.value = ''
|
urlError.value = ''
|
||||||
|
|
||||||
if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
|
if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -113,20 +113,20 @@ async function saveServerBaseUrl() {
|
|||||||
clearAllCacheAfterUrlChange()
|
clearAllCacheAfterUrlChange()
|
||||||
|
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '重启应用',
|
title: '重启应用',
|
||||||
content: '服务端地址已保存并清空缓存,是否立即重启生效?',
|
content: '服务端地址已保存并清空缓存,是否立即重启生效?',
|
||||||
confirmText: '立即重启',
|
confirmText: '立即重启',
|
||||||
cancelText: '稍后',
|
cancelText: '稍后',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
restartApp()
|
restartApp()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toast.success('已保存,可稍后手动重启应用')
|
toast.success('已保存,可稍后手动重启应用')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置为 env 默认
|
// 重置为 env 默认
|
||||||
function resetServerBaseUrl() {
|
function resetServerBaseUrl() {
|
||||||
@@ -222,7 +222,7 @@ function showAbout() {
|
|||||||
title: `关于${import.meta.env.VITE_APP_TITLE}`,
|
title: `关于${import.meta.env.VITE_APP_TITLE}`,
|
||||||
content: `${import.meta.env.VITE_APP_TITLE}\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server`,
|
content: `${import.meta.env.VITE_APP_TITLE}\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server`,
|
||||||
title: `关于小智智控台`,
|
title: `关于小智智控台`,
|
||||||
content: `小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.7.6`,
|
content: `小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.8.2`,
|
||||||
showCancel: false,
|
showCancel: false,
|
||||||
confirmText: '确定',
|
confirmText: '确定',
|
||||||
})
|
})
|
||||||
@@ -263,17 +263,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<view class="mb-[24rpx]">
|
<view class="mb-[24rpx]">
|
||||||
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
|
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
|
||||||
<wd-input
|
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
|
||||||
v-model="baseUrlInput"
|
|
||||||
type="text"
|
|
||||||
clearable
|
|
||||||
:maxlength="200"
|
|
||||||
placeholder="输入服务端地址,如 https://example.com/xiaozhi"
|
placeholder="输入服务端地址,如 https://example.com/xiaozhi"
|
||||||
custom-class="!border-none !bg-transparent h-[88rpx] px-[24rpx] items-center"
|
custom-class="!border-none !bg-transparent h-[88rpx] px-[24rpx] items-center"
|
||||||
input-class="text-[28rpx] text-[#232338]"
|
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
|
||||||
@input="validateUrl"
|
|
||||||
@blur="validateUrl"
|
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
|
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
|
||||||
{{ urlError }}
|
{{ urlError }}
|
||||||
@@ -371,7 +364,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<!-- 底部安全距离 -->
|
<!-- 底部安全距离 -->
|
||||||
<!-- 底部安全距离 -->
|
<!-- 底部安全距离 -->
|
||||||
<view style="height: env(safe-area-inset-bottom);" />
|
<view style="height: env(safe-area-inset-bottom);" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
VUE_APP_API_BASE_URL=/xiaozhi
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
|
#1.如需web后台适应手机浏览器,请先启动manager-api,得到服务端地址(http://局域网ip:8002/xiaozhi)
|
||||||
|
#2.在manager-mobile里的.env中,将VITE_SERVER_BASEURL修改成自己的服务端地址,然后运行pnpm dev:h5得到h5页面地址,再将其填入下方的VUE_APP_H5_URL
|
||||||
|
#3.此时手机浏览器访问web页面,即可跳转至适应手机浏览器的h5页面
|
||||||
|
VUE_APP_H5_URL=
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
VUE_APP_API_BASE_URL=/xiaozhi
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
VUE_APP_PUBLIC_PATH=/
|
VUE_APP_PUBLIC_PATH=/
|
||||||
# 是否开启CDN
|
# 是否开启CDN
|
||||||
VUE_APP_USE_CDN=false
|
VUE_APP_USE_CDN=false
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"opus-recorder": "^8.0.5",
|
"opus-recorder": "^8.0.5",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
|
"vue-i18n": "^8.28.2",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
"vuex": "^3.6.2",
|
"vuex": "^3.6.2",
|
||||||
"xiaozhi": "file:"
|
"xiaozhi": "file:"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import CacheViewer from '@/components/CacheViewer.vue';
|
|||||||
import { logCacheStatus } from '@/utils/cacheViewer';
|
import { logCacheStatus } from '@/utils/cacheViewer';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
CacheViewer
|
CacheViewer
|
||||||
},
|
},
|
||||||
@@ -60,6 +61,12 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
// 检测是否为移动设备且VUE_APP_H5_URL不为空,如果两个条件都满足则跳转到H5页面
|
||||||
|
if (this.isMobileDevice() && process.env.VUE_APP_H5_URL) {
|
||||||
|
window.location.href = process.env.VUE_APP_H5_URL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 只有在启用CDN时才添加相关事件和功能
|
// 只有在启用CDN时才添加相关事件和功能
|
||||||
if (this.isCDNEnabled) {
|
if (this.isCDNEnabled) {
|
||||||
// 添加全局快捷键Alt+C用于显示缓存查看器
|
// 添加全局快捷键Alt+C用于显示缓存查看器
|
||||||
@@ -72,7 +79,7 @@ export default {
|
|||||||
|
|
||||||
// 在控制台输出提示信息
|
// 在控制台输出提示信息
|
||||||
console.info(
|
console.info(
|
||||||
'%c[小智服务] CDN缓存检查工具已加载',
|
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.cdnEnabled'),
|
||||||
'color: #409EFF; font-weight: bold;'
|
'color: #409EFF; font-weight: bold;'
|
||||||
);
|
);
|
||||||
console.info(
|
console.info(
|
||||||
@@ -83,7 +90,7 @@ export default {
|
|||||||
this.checkServiceWorkerStatus();
|
this.checkServiceWorkerStatus();
|
||||||
} else {
|
} else {
|
||||||
console.info(
|
console.info(
|
||||||
'%c[小智服务] CDN模式已禁用,使用本地打包资源',
|
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.cdnDisabled'),
|
||||||
'color: #67C23A; font-weight: bold;'
|
'color: #67C23A; font-weight: bold;'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,6 +108,11 @@ export default {
|
|||||||
this.showCacheViewer = true;
|
this.showCacheViewer = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isMobileDevice() {
|
||||||
|
// 检测是否为移动设备的函数
|
||||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
},
|
||||||
|
|
||||||
async checkServiceWorkerStatus() {
|
async checkServiceWorkerStatus() {
|
||||||
// 检查Service Worker是否已注册
|
// 检查Service Worker是否已注册
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
@@ -108,7 +120,7 @@ export default {
|
|||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
if (registrations.length > 0) {
|
if (registrations.length > 0) {
|
||||||
console.info(
|
console.info(
|
||||||
'%c[小智服务] Service Worker已注册',
|
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.serviceWorkerRegistered'),
|
||||||
'color: #67C23A; font-weight: bold;'
|
'color: #67C23A; font-weight: bold;'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,46 +129,46 @@ export default {
|
|||||||
const hasCaches = await logCacheStatus();
|
const hasCaches = await logCacheStatus();
|
||||||
if (!hasCaches) {
|
if (!hasCaches) {
|
||||||
console.info(
|
console.info(
|
||||||
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
|
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.noCacheDetected'),
|
||||||
|
'color: #E6A23C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 开发环境下提供额外提示
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.info(
|
||||||
|
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.swDevEnvWarning'),
|
||||||
'color: #E6A23C; font-weight: bold;'
|
'color: #E6A23C; font-weight: bold;'
|
||||||
);
|
);
|
||||||
|
console.info(this.$t('cache.swCheckMethods'));
|
||||||
// 开发环境下提供额外提示
|
console.info('1. ' + this.$t('cache.swCheckMethod1'));
|
||||||
if (process.env.NODE_ENV === 'development') {
|
console.info('2. ' + this.$t('cache.swCheckMethod2'));
|
||||||
console.info(
|
console.info('3. ' + this.$t('cache.swCheckMethod3'));
|
||||||
'%c[小智服务] 在开发环境中,Service Worker可能无法正常初始化缓存',
|
}
|
||||||
'color: #E6A23C; font-weight: bold;'
|
|
||||||
);
|
|
||||||
console.info('请尝试以下方法检查Service Worker是否生效:');
|
|
||||||
console.info('1. 在开发者工具的Application/Application标签页中查看Service Worker状态');
|
|
||||||
console.info('2. 在开发者工具的Application/Cache/Cache Storage中查看缓存内容');
|
|
||||||
console.info('3. 使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
console.info(
|
console.info(
|
||||||
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
|
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.serviceWorkerNotRegistered'),
|
||||||
'color: #F56C6C; font-weight: bold;'
|
'color: #F56C6C; font-weight: bold;'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.info(
|
console.info(
|
||||||
'%c[小智服务] 在开发环境中,这是正常现象',
|
'%c[' + this.$t('system.name') + '] ' + this.$t('cache.swDevEnvNormal'),
|
||||||
'color: #E6A23C; font-weight: bold;'
|
'color: #E6A23C; font-weight: bold;'
|
||||||
);
|
);
|
||||||
console.info('Service Worker通常只在生产环境中生效');
|
console.info(this.$t('cache.swProdOnly'));
|
||||||
console.info('要测试Service Worker功能:');
|
console.info(this.$t('cache.swTestingTitle'));
|
||||||
console.info('1. 运行npm run build构建生产版本');
|
console.info('1. ' + this.$t('cache.swTestingStep1'));
|
||||||
console.info('2. 通过HTTP服务器访问构建后的页面');
|
console.info('2. ' + this.$t('cache.swTestingStep2'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查Service Worker状态失败:', error);
|
console.error('检查Service Worker状态失败:', error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('当前浏览器不支持Service Worker,CDN资源缓存功能不可用');
|
console.warn(this.$t('cache.swNotSupported'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Fly from 'flyio/dist/npm/fly';
|
|||||||
import store from '../store/index';
|
import store from '../store/index';
|
||||||
import Constant from '../utils/constant';
|
import Constant from '../utils/constant';
|
||||||
import { goToPage, isNotNull, showDanger, showWarning } from '../utils/index';
|
import { goToPage, isNotNull, showDanger, showWarning } from '../utils/index';
|
||||||
|
import i18n from '../i18n/index';
|
||||||
|
|
||||||
const fly = new Fly()
|
const fly = new Fly()
|
||||||
// 设置超时
|
// 设置超时
|
||||||
@@ -27,6 +28,16 @@ function sendRequest() {
|
|||||||
_url: '',
|
_url: '',
|
||||||
_responseType: undefined, // 新增响应类型字段
|
_responseType: undefined, // 新增响应类型字段
|
||||||
'send'() {
|
'send'() {
|
||||||
|
// 设置语言请求头
|
||||||
|
const currentLang = i18n.locale;
|
||||||
|
// 转换语言代码格式,将zh_CN转换为zh-CN
|
||||||
|
let acceptLanguage = currentLang.replace('_', '-');
|
||||||
|
// 为英语添加默认地区代码
|
||||||
|
if (acceptLanguage === 'en') {
|
||||||
|
acceptLanguage = 'en-US';
|
||||||
|
}
|
||||||
|
this._header['Accept-Language'] = acceptLanguage;
|
||||||
|
|
||||||
if (isNotNull(store.getters.getToken)) {
|
if (isNotNull(store.getters.getToken)) {
|
||||||
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
|
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
|
||||||
}
|
}
|
||||||
@@ -116,10 +127,13 @@ function httpHandlerError(info, failCallback, networkFailCallback) {
|
|||||||
goToPage(Constant.PAGE.LOGIN, true);
|
goToPage(Constant.PAGE.LOGIN, true);
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
// 直接使用后端返回的国际化消息
|
||||||
|
let errorMessage = info.data.msg;
|
||||||
|
|
||||||
if (failCallback) {
|
if (failCallback) {
|
||||||
failCallback(info)
|
failCallback(info)
|
||||||
} else {
|
} else {
|
||||||
showDanger(info.data.msg)
|
showDanger(errorMessage)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default {
|
|||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
// 修改
|
// 修改
|
||||||
updateParam(data, callback) {
|
updateParam(data, callback, failCallback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/admin/params`)
|
.url(`${getServiceUrl()}/admin/params`)
|
||||||
.method('PUT')
|
.method('PUT')
|
||||||
@@ -106,6 +106,10 @@ export default {
|
|||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
callback(res)
|
callback(res)
|
||||||
})
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
RequestService.clearRequestTime()
|
||||||
|
failCallback(err)
|
||||||
|
})
|
||||||
.networkFail((err) => {
|
.networkFail((err) => {
|
||||||
console.error('更新参数失败:', err)
|
console.error('更新参数失败:', err)
|
||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
|
|||||||
@@ -85,4 +85,38 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
|
// 获取设备状态
|
||||||
|
getDeviceStatus(agentId, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/device/bind/${agentId}`)
|
||||||
|
.method('POST')
|
||||||
|
.data({}) // 发送空对象作为请求体
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.networkFail((err) => {
|
||||||
|
console.error('获取设备状态失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDeviceStatus(agentId, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 发送设备指令
|
||||||
|
sendDeviceCommand(deviceId, mcpData, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/device/commands/${deviceId}`)
|
||||||
|
.method('POST')
|
||||||
|
.data(mcpData)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.networkFail((err) => {
|
||||||
|
console.error('发送设备指令失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.sendDeviceCommand(deviceId, mcpData, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -6,24 +6,24 @@
|
|||||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
<img src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||||
</div>
|
</div>
|
||||||
添加设备
|
{{ $t('device.dialogTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||||
<div style="color: red;display: inline-block;">*</div>
|
<div style="color: red;display: inline-block;">*</div>
|
||||||
<span style="font-size: 11px"> 验证码:</span>
|
<span style="font-size: 11px"> {{ $t('device.verificationCode') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" @keyup.enter.native="confirm" />
|
<el-input :placeholder="$t('device.verificationCodePlaceholder')" v-model="deviceCode" @keyup.enter.native="confirm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
<div class="dialog-btn" @click="confirm">
|
<div class="dialog-btn" @click="confirm">
|
||||||
确定
|
{{ $t('device.confirmButton') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||||
取消
|
{{ $t('device.cancelButton') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -47,7 +47,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
confirm() {
|
confirm() {
|
||||||
if (!/^\d{6}$/.test(this.deviceCode)) {
|
if (!/^\d{6}$/.test(this.deviceCode)) {
|
||||||
this.$message.error('请输入6位数字验证码');
|
this.$message.error(this.$t('device.input6DigitCode'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -58,13 +58,13 @@ export default {
|
|||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$emit('refresh');
|
this.$emit('refresh');
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '设备绑定成功',
|
message: this.$t('device.bindSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '绑定失败',
|
message: data.msg || this.$t('device.bindFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
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="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;">
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
添加模型
|
{{ $t('modelConfigDialog.addModel') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="custom-close-btn" @click="handleClose">
|
<button class="custom-close-btn" @click="handleClose">
|
||||||
×
|
×
|
||||||
@@ -12,14 +12,14 @@
|
|||||||
|
|
||||||
<!-- 模型信息部分 -->
|
<!-- 模型信息部分 -->
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
|
<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; gap: 20px;">
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<span style="margin-right: 8px;">是否启用</span>
|
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
|
||||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: none; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,38 +28,38 @@
|
|||||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
|
||||||
<el-input v-model="formData.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
|
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="modelCode" style="flex: 1;">
|
||||||
<el-input v-model="formData.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
|
<el-input v-model="formData.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||||
<el-select v-model="formData.supplier" placeholder="请选择" class="custom-select custom-input-bg"
|
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
|
||||||
style="width: 100%;" @focus="loadProviders" filterable>
|
style="width: 100%;" @focus="loadProviders" filterable>
|
||||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sortOrder" style="flex: 1;">
|
||||||
<el-input v-model="formData.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
<el-input v-model="formData.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
|
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
|
||||||
<el-input v-model="formData.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }"
|
<el-input v-model="formData.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
class="custom-input-bg"></el-input>
|
class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
<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>
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
|
|
||||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
class="save-btn"
|
class="save-btn"
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
:disabled="saving">
|
:disabled="saving">
|
||||||
保存
|
{{ $t('modelConfigDialog.save') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -195,7 +195,7 @@ export default {
|
|||||||
this.saving = true;
|
this.saving = true;
|
||||||
|
|
||||||
if (!this.formData.supplier) {
|
if (!this.formData.supplier) {
|
||||||
this.$message.error('请选择供应器');
|
this.$message.error(this.$t('addModelDialog.requiredSupplier'));
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,23 @@
|
|||||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||||
<img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
<img loading="lazy" src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||||
</div>
|
</div>
|
||||||
添加智能体
|
{{ $t('addAgentDialog.title') }}
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
<div style="font-weight: 400;text-align: left;color: #3d4566;">
|
<div style="font-weight: 400;text-align: left;color: #3d4566;">
|
||||||
<div style="color: red;display: inline-block;">*</div> 智能体名称:
|
<div style="color: red;display: inline-block;">*</div> {{ $t('addAgentDialog.agentName') }}:
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
<el-input ref="inputRef" :placeholder="$t('addAgentDialog.placeholder')" v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
<div class="dialog-btn" @click="confirm">
|
<div class="dialog-btn" @click="confirm">
|
||||||
确定
|
{{ $t('addAgentDialog.confirm') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||||
取消
|
{{ $t('addAgentDialog.cancel') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -50,12 +50,12 @@ export default {
|
|||||||
},
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
if (!this.wisdomBodyName.trim()) {
|
if (!this.wisdomBodyName.trim()) {
|
||||||
this.$message.error('请输入智能体名称');
|
this.$message.error(this.$t('addAgentDialog.nameRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Api.agent.addAgent(this.wisdomBodyName, (res) => {
|
Api.agent.addAgent(this.wisdomBodyName, (res) => {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '添加成功',
|
message: this.$t('addAgentDialog.addSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.$emit('confirm', res);
|
this.$emit('confirm', res);
|
||||||
|
|||||||
@@ -1,54 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="CDN资源缓存状态"
|
:title="$t('cache.dialogTitle')"
|
||||||
:visible.sync="visible"
|
:visible.sync="visible"
|
||||||
width="70%"
|
width="70%"
|
||||||
:before-close="handleClose"
|
:before-close="handleClose"
|
||||||
>
|
>
|
||||||
<div v-if="isLoading" class="loading-container">
|
<div v-if="isLoading" class="loading-container">
|
||||||
<p>正在加载缓存信息...</p>
|
<p>{{ $t('cache.loading') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-if="!cacheAvailable" class="no-cache-message">
|
<div v-if="!cacheAvailable" class="no-cache-message">
|
||||||
<i class="el-icon-warning-outline"></i>
|
<i class="el-icon-warning-outline"></i>
|
||||||
<p>您的浏览器不支持Cache API或Service Worker未安装</p>
|
<p>{{ $t('cache.notSupported') }}</p>
|
||||||
<el-button type="primary" @click="refreshPage">刷新页面</el-button>
|
<el-button type="primary" @click="refreshPage">{{ $t('cache.refreshPage') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="cacheData.totalCached === 0"
|
v-if="cacheData.totalCached === 0"
|
||||||
title="未发现缓存的CDN资源"
|
:title="$t('cache.noCachedResources')"
|
||||||
type="warning"
|
type="warning"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
>
|
>
|
||||||
<p>Service Worker可能尚未完成初始化或缓存尚未建立。请刷新页面或等待一会后再试。</p>
|
<p>{{ $t('cache.noCachedResourcesDesc') }}</p>
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-alert
|
<el-alert
|
||||||
title="CDN资源缓存状态"
|
:title="$t('cache.cdnCacheStatus')"
|
||||||
type="success"
|
type="success"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
>
|
>
|
||||||
共发现 {{ cacheData.totalCached }} 个缓存资源
|
{{ $t('cache.totalCachedResources').replace('{count}', cacheData.totalCached) }}
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<h3>JavaScript 资源 ({{ cacheData.js.length }})</h3>
|
<h3>{{ $t('cache.jsResources').replace('{count}', cacheData.js.length) }}</h3>
|
||||||
<el-table :data="cacheData.js" stripe style="width: 100%">
|
<el-table :data="cacheData.js" stripe style="width: 100%">
|
||||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||||
<el-table-column prop="cached" label="状态" width="100">
|
<el-table-column prop="cached" :label="$t('cache.status')" width="100">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
<el-tag type="success" v-if="scope.row.cached">{{ $t('cache.cached') }}</el-tag>
|
||||||
<el-tag type="danger" v-else>未缓存</el-tag>
|
<el-tag type="danger" v-else>{{ $t('cache.notCached') }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<h3>CSS 资源 ({{ cacheData.css.length }})</h3>
|
<h3>{{ $t('cache.cssResources').replace('{count}', cacheData.css.length) }}</h3>
|
||||||
<el-table :data="cacheData.css" stripe style="width: 100%">
|
<el-table :data="cacheData.css" stripe style="width: 100%">
|
||||||
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||||
<el-table-column prop="cached" label="状态" width="100">
|
<el-table-column prop="cached" label="状态" width="100">
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
<el-button @click="handleClose">关闭</el-button>
|
<el-button @click="handleClose">{{ $t('button.close') }}</el-button>
|
||||||
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button>
|
<el-button type="primary" @click="refreshCache">{{ $t('cache.refreshStatus') }}</el-button>
|
||||||
<el-button type="danger" @click="clearCache">清除缓存</el-button>
|
<el-button type="danger" @click="clearCache">{{ $t('cache.clearCache') }}</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -138,29 +138,29 @@ export default {
|
|||||||
|
|
||||||
async refreshCache() {
|
async refreshCache() {
|
||||||
this.loadCacheData();
|
this.loadCacheData();
|
||||||
this.$message.success('正在刷新缓存状态');
|
this.$message.success(this.$t('cache.refreshingStatus'));
|
||||||
},
|
},
|
||||||
|
|
||||||
async clearCache() {
|
async clearCache() {
|
||||||
this.$confirm('确定要清除所有缓存吗?', '警告', {
|
this.$confirm(this.$t('cache.confirmClear'), this.$t('message.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('button.ok'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('button.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
try {
|
try {
|
||||||
const success = await clearAllCaches();
|
const success = await clearAllCaches();
|
||||||
if (success) {
|
if (success) {
|
||||||
this.$message.success('缓存已清除');
|
this.$message.success(this.$t('cache.clearedSuccess'));
|
||||||
await this.loadCacheData();
|
await this.loadCacheData();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error('清除缓存失败');
|
this.$message.error(this.$t('cache.clearFailed'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清除缓存失败:', error);
|
console.error('清除缓存失败:', error);
|
||||||
this.$message.error('清除缓存失败');
|
this.$message.error(this.$t('cache.clearFailed'));
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message.info('已取消清除');
|
this.$message.info(this.$t('cache.clearCanceled'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -204,4 +204,4 @@ h3 {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -8,38 +8,38 @@
|
|||||||
<img loading="lazy" src="@/assets/login/shield.png" alt=""
|
<img loading="lazy" src="@/assets/login/shield.png" alt=""
|
||||||
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||||
</div>
|
</div>
|
||||||
修改密码
|
{{ $t('changePassword.title') }}
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||||
<div style="color: red;display: inline-block;">*</div>
|
<div style="color: red;display: inline-block;">*</div>
|
||||||
旧密码:
|
{{ $t('changePassword.oldPasswordLabel') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input placeholder="请输入旧密码" v-model="oldPassword" type="password" show-password />
|
<el-input :placeholder="$t('changePassword.oldPasswordPlaceholder')" v-model="oldPassword" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
<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 style="color: red;display: inline-block;">*</div>
|
||||||
新密码:
|
{{ $t('changePassword.newPasswordLabel') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password />
|
<el-input :placeholder="$t('changePassword.newPasswordPlaceholder')" v-model="newPassword" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
<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 style="color: red;display: inline-block;">*</div>
|
||||||
确认新密码:
|
{{ $t('changePassword.confirmPasswordLabel') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password />
|
<el-input :placeholder="$t('changePassword.confirmPasswordPlaceholder')" v-model="confirmNewPassword" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
<div class="dialog-btn" @click="confirm">
|
<div class="dialog-btn" @click="confirm">
|
||||||
确定
|
{{ $t('changePassword.confirmButton') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||||
取消
|
{{ $t('changePassword.cancelButton') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user';
|
import userApi from '@/apis/module/user';
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
|
import { changeLanguage } from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChangePasswordDialog',
|
name: 'ChangePasswordDialog',
|
||||||
@@ -78,15 +79,15 @@ export default {
|
|||||||
...mapActions(['logout']), // 引入Vuex的logout action
|
...mapActions(['logout']), // 引入Vuex的logout action
|
||||||
confirm() {
|
confirm() {
|
||||||
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
|
if (!this.oldPassword.trim() || !this.newPassword.trim() || !this.confirmNewPassword.trim()) {
|
||||||
this.$message.error('请填写所有字段');
|
this.$message.error(this.$t('changePassword.allFieldsRequired'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.newPassword !== this.confirmNewPassword) {
|
if (this.newPassword !== this.confirmNewPassword) {
|
||||||
this.$message.error('两次输入的新密码不一致');
|
this.$message.error(this.$t('changePassword.passwordsNotMatch'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.newPassword === this.oldPassword) {
|
if (this.newPassword === this.oldPassword) {
|
||||||
this.$message.error('新密码不能与旧密码相同');
|
this.$message.error(this.$t('changePassword.newPasswordSameAsOld'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ export default {
|
|||||||
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
|
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '密码修改成功,请重新登录',
|
message: this.$t('changePassword.passwordChangedSuccessfully'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.logout().then(() => {
|
this.logout().then(() => {
|
||||||
@@ -102,10 +103,10 @@ export default {
|
|||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.data.msg || '密码修改失败');
|
this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
|
||||||
}
|
}
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.$message.error(err.msg || '密码修改失败');
|
this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
|
||||||
});
|
});
|
||||||
this.$emit('input', false);
|
this.$emit('input', false);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="'与' + agentName + '的聊天记录' + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
|
<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">
|
:visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="session-list" @scroll="handleScroll">
|
<div class="session-list" @scroll="handleScroll">
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loading" class="loading">加载中...</div>
|
<div v-if="loading" class="loading">{{ $t('chatHistory.loading') }}</div>
|
||||||
<div v-if="!hasMore" class="no-more">没有更多记录了</div>
|
<div v-if="!hasMore" class="no-more">{{ $t('chatHistory.noMoreRecords') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-content">
|
<div class="chat-content">
|
||||||
<div v-if="currentSessionId" class="messages">
|
<div v-if="currentSessionId" class="messages">
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-session-selected">
|
<div v-else class="no-session-selected">
|
||||||
请选择会话查看聊天记录
|
{{ $t('chatHistory.selectSession') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,9 +230,9 @@ export default {
|
|||||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
if (date >= today) {
|
if (date >= today) {
|
||||||
return `今天 ${hours}:${minutes}`;
|
return `${this.$t('chatHistory.today')} ${hours}:${minutes}`;
|
||||||
} else if (date >= yesterday) {
|
} else if (date >= yesterday) {
|
||||||
return `昨天 ${hours}:${minutes}`;
|
return `${this.$t('chatHistory.yesterday')} ${hours}:${minutes}`;
|
||||||
} else {
|
} else {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
|||||||
@@ -14,36 +14,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="device-name">
|
<div class="device-name">
|
||||||
语言模型:{{ device.llmModelName }}
|
{{ $t('home.languageModel') }}:{{ device.llmModelName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="device-name">
|
<div class="device-name">
|
||||||
音色模型:{{ device.ttsModelName }} ({{ device.ttsVoiceName }})
|
{{ $t('home.voiceModel') }}:{{ device.ttsModelName }} ({{ device.ttsVoiceName }})
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;gap: 10px;align-items: center;">
|
<div style="display: flex;gap: 10px;align-items: center;">
|
||||||
<div class="settings-btn" @click="handleConfigure">
|
<div class="settings-btn" @click="handleConfigure">
|
||||||
配置角色
|
{{ $t('home.configureRole') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn" @click="handleVoicePrint">
|
<div class="settings-btn" @click="handleVoicePrint">
|
||||||
声纹识别
|
{{ $t('home.voiceprintRecognition') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn" @click="handleDeviceManage">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理({{ device.deviceCount }})
|
{{ $t('home.deviceManagement') }}({{ device.deviceCount }})
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn" @click="handleChatHistory"
|
<div :class="['settings-btn', { 'disabled-btn': device.memModelId === 'Memory_nomem' }]"
|
||||||
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
@click="handleChatHistory">
|
||||||
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="请先在“配置角色”界面开启记忆" placement="top">
|
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" :content="$t('home.enableMemory')" placement="top">
|
||||||
<span>聊天记录</span>
|
<span>{{ $t('home.chatHistory') }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<span v-else>聊天记录</span>
|
<span v-else>{{ $t('home.chatHistory') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
<div>最近对话:{{ formattedLastConnectedTime }}</div>
|
<div>{{ $t('home.lastConversation') }}:{{ formattedLastConnectedTime }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DeviceItem',
|
name: 'DeviceItem',
|
||||||
props: {
|
props: {
|
||||||
@@ -54,20 +56,20 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
formattedLastConnectedTime() {
|
formattedLastConnectedTime() {
|
||||||
if (!this.device.lastConnectedAt) return '暂未对话';
|
if (!this.device.lastConnectedAt) return this.$t('home.noConversation');
|
||||||
|
|
||||||
const lastTime = new Date(this.device.lastConnectedAt);
|
const lastTime = new Date(this.device.lastConnectedAt);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMinutes = Math.floor((now - lastTime) / (1000 * 60));
|
const diffMinutes = Math.floor((now - lastTime) / (1000 * 60));
|
||||||
|
|
||||||
if (diffMinutes <= 1) {
|
if (diffMinutes <= 1) {
|
||||||
return '刚刚';
|
return this.$t('home.justNow');
|
||||||
} else if (diffMinutes < 60) {
|
} else if (diffMinutes < 60) {
|
||||||
return `${diffMinutes}分钟前`;
|
return this.$t('home.minutesAgo', { minutes: diffMinutes });
|
||||||
} else if (diffMinutes < 24 * 60) {
|
} else if (diffMinutes < 24 * 60) {
|
||||||
const hours = Math.floor(diffMinutes / 60);
|
const hours = Math.floor(diffMinutes / 60);
|
||||||
const minutes = diffMinutes % 60;
|
const minutes = diffMinutes % 60;
|
||||||
return `${hours}小时${minutes > 0 ? minutes + '分钟' : ''}前`;
|
return this.$t('home.hoursAgo', { hours, minutes });
|
||||||
} else {
|
} else {
|
||||||
return this.device.lastConnectedAt;
|
return this.device.lastConnectedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||||
<el-form :model="form" :rules="rules" ref="form" label-width="100px">
|
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
|
||||||
<el-form-item label="字典标签" prop="dictLabel">
|
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel">
|
||||||
<el-input v-model="form.dictLabel" placeholder="请输入字典标签"></el-input>
|
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="字典值" prop="dictValue">
|
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue">
|
||||||
<el-input v-model="form.dictValue" placeholder="请输入字典值"></el-input>
|
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序" prop="sort">
|
<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-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button @click="handleClose">取 消</el-button>
|
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
|
||||||
<el-button type="primary" @click="handleSave">确 定</el-button>
|
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,7 +28,7 @@ export default {
|
|||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '新增字典数据'
|
default: () => this.$t('dictDataDialog.addDictData')
|
||||||
},
|
},
|
||||||
dictData: {
|
dictData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -50,8 +50,8 @@ export default {
|
|||||||
sort: 0
|
sort: 0
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
dictLabel: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
|
||||||
dictValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||||
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
|
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
|
||||||
<el-form-item label="字典类型名称" prop="dictName">
|
<el-form-item :label="$t('dictTypeDialog.dictName')" prop="dictName">
|
||||||
<el-input v-model="form.dictName" placeholder="请输入字典类型名称"></el-input>
|
<el-input v-model="form.dictName" :placeholder="$t('dictTypeDialog.dictNamePlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="字典类型编码" prop="dictType">
|
<el-form-item :label="$t('dictTypeDialog.dictType')" prop="dictType">
|
||||||
<el-input v-model="form.dictType" placeholder="请输入字典类型编码"></el-input>
|
<el-input v-model="form.dictType" :placeholder="$t('dictTypeDialog.dictTypePlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button @click="handleClose">取 消</el-button>
|
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
|
||||||
<el-button type="primary" @click="handleSave">确 定</el-button>
|
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,7 +25,7 @@ export default {
|
|||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '新增字典类型'
|
default: () => this.$t('dictTypeDialog.addDictType')
|
||||||
},
|
},
|
||||||
dictTypeData: {
|
dictTypeData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -41,8 +41,8 @@ export default {
|
|||||||
dictType: ''
|
dictType: ''
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
dictName: [{ required: true, message: '请输入字典类型名称', trigger: 'blur' }],
|
dictName: [{ required: true, message: this.$t('dictTypeDialog.requiredDictName'), trigger: 'blur' }],
|
||||||
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
|
dictType: [{ required: true, message: this.$t('dictTypeDialog.requiredDictType'), trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
|
<el-dialog :visible.sync="dialogVisible" width="900px" @close="handleClose" class="compact-dialog" :append-to-body="true">
|
||||||
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="80px">
|
<el-form :model="voiceForm" :rules="rules" ref="voiceForm" label-width="auto">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="音色编码" prop="voiceCode">
|
<el-form-item :label="$t('editVoiceDialog.voiceCode')" prop="voiceCode">
|
||||||
<el-input v-model="voiceForm.voiceCode" placeholder="请输入内容" class="compact-input"></el-input>
|
<el-input v-model="voiceForm.voiceCode" :placeholder="$t('editVoiceDialog.voiceCodePlaceholder')" class="compact-input"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="音色名称" prop="voiceName">
|
<el-form-item :label="$t('editVoiceDialog.voiceName')" prop="voiceName">
|
||||||
<el-input v-model="voiceForm.voiceName" placeholder="请输入内容" class="compact-input"></el-input>
|
<el-input v-model="voiceForm.voiceName" :placeholder="$t('editVoiceDialog.voiceNamePlaceholder')" class="compact-input"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="语言类型" prop="languageType">
|
<el-form-item :label="$t('editVoiceDialog.languageType')" prop="languageType">
|
||||||
<el-input v-model="voiceForm.languageType" placeholder="请输入内容" class="compact-input"></el-input>
|
<el-input v-model="voiceForm.languageType" :placeholder="$t('editVoiceDialog.languageTypePlaceholder')" class="compact-input"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="排序号" prop="sortNumber">
|
<el-form-item :label="$t('editVoiceDialog.sortNumber')" prop="sortNumber">
|
||||||
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
|
<el-input-number v-model="voiceForm.sortNumber" :min="1" :controls="false" class="compact-number"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item :label="$t('editVoiceDialog.remark')" prop="remark">
|
||||||
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" placeholder="请输入内容" class="compact-textarea"
|
<el-input v-model="voiceForm.remark" type="textarea" :rows="2" :placeholder="$t('editVoiceDialog.remarkPlaceholder')" class="compact-textarea"
|
||||||
></el-input>
|
></el-input>
|
||||||
<div class="audio-controls">
|
<div class="audio-controls">
|
||||||
<div class="audio-player">
|
<div class="audio-player">
|
||||||
@@ -38,14 +38,14 @@
|
|||||||
class="custom-audio"
|
class="custom-audio"
|
||||||
></audio>
|
></audio>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" size="mini" class="preview-btn">生成试听</el-button>
|
<el-button type="primary" size="mini" class="preview-btn">{{ $t('editVoiceDialog.generatePreview') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||||
<el-button type="primary" @click="handleClose">关闭</el-button>
|
<el-button @click="handleClose">{{ $t('button.close') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -59,8 +59,8 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
voiceCode: 'wawaxiaohe',
|
voiceCode: 'wawaxiaohe',
|
||||||
voiceName: '湾湾小何',
|
voiceName: this.$t('editVoiceDialog.defaultVoiceName'),
|
||||||
languageType: '中文',
|
languageType: this.$t('editVoiceDialog.defaultLanguageType'),
|
||||||
sortNumber: 123
|
sortNumber: 123
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -72,8 +72,8 @@ export default {
|
|||||||
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
|
audioUrl: 'http://music.163.com/song/media/outer/url?id=447925558.mp3',
|
||||||
generatedAudio: null,
|
generatedAudio: null,
|
||||||
rules: {
|
rules: {
|
||||||
voiceCode: [{ required: true, message: '请输入音色编码', trigger: 'blur' }],
|
voiceCode: [{ required: true, message: this.$t('editVoiceDialog.requiredVoiceCode'), trigger: 'blur' }],
|
||||||
voiceName: [{ required: true, message: '请输入音色名称', trigger: 'blur' }]
|
voiceName: [{ required: true, message: this.$t('editVoiceDialog.requiredVoiceName'), trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose" @open="handleOpen">
|
<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="100px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
|
||||||
<el-form-item label="固件名称" prop="firmwareName">
|
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
|
||||||
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="固件类型" prop="type">
|
<el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
|
||||||
<el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable :disabled="isTypeDisabled">
|
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;" filterable :disabled="isTypeDisabled">
|
||||||
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="版本号" prop="version">
|
<el-form-item :label="$t('firmwareDialog.version')" prop="version">
|
||||||
<el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input>
|
<el-input v-model="form.version" :placeholder="$t('firmwareDialog.versionPlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="固件文件" prop="firmwarePath">
|
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
|
||||||
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
|
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
|
||||||
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
|
:before-upload="beforeUpload" :accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true"
|
||||||
:on-remove="handleRemove">
|
:on-remove="handleRemove">
|
||||||
<el-button size="small" type="primary">点击上传</el-button>
|
<el-button size="small" type="primary">{{ $t('firmwareDialog.clickUpload') }}</el-button>
|
||||||
<div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk),且不超过100MB</div>
|
<div slot="tip" class="el-upload__tip">{{ $t('firmwareDialog.uploadTip') }}</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
||||||
:status="uploadStatus"></el-progress>
|
:status="uploadStatus"></el-progress>
|
||||||
<div class="hint-text">
|
<div class="hint-text">
|
||||||
<span>温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件</span>
|
<span>{{ $t('firmwareDialog.uploadHint') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
|
||||||
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input>
|
<el-input type="textarea" v-model="form.remark" :placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button @click="handleCancel">取 消</el-button>
|
<el-button @click="handleCancel">{{ $t('button.cancel') }}</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
<el-button type="primary" @click="handleSubmit">{{ $t('button.save') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -68,17 +68,17 @@ export default {
|
|||||||
dialogVisible: this.visible,
|
dialogVisible: this.visible,
|
||||||
rules: {
|
rules: {
|
||||||
firmwareName: [
|
firmwareName: [
|
||||||
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
|
{ required: true, message: this.$t('firmwareDialog.requiredFirmwareName'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
type: [
|
type: [
|
||||||
{ required: true, message: '请选择固件类型', trigger: 'change' }
|
{ required: true, message: this.$t('firmwareDialog.requiredFirmwareType'), trigger: 'change' }
|
||||||
],
|
],
|
||||||
version: [
|
version: [
|
||||||
{ required: true, message: '请输入版本号', trigger: 'blur' },
|
{ required: true, message: this.$t('firmwareDialog.requiredVersion'), trigger: 'blur' },
|
||||||
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式不正确,请输入x.x.x格式', trigger: 'blur' }
|
{ pattern: /^\d+\.\d+\.\d+$/, message: this.$t('firmwareDialog.versionFormatError'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
firmwarePath: [
|
firmwarePath: [
|
||||||
{ required: false, message: '请上传固件文件', trigger: 'change' }
|
{ required: false, message: this.$t('firmwareDialog.requiredFirmwareFile'), trigger: 'change' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ export default {
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
// 如果是新增模式且没有上传文件,则提示错误
|
// 如果是新增模式且没有上传文件,则提示错误
|
||||||
if (!this.form.id && !this.form.firmwarePath) {
|
if (!this.form.id && !this.form.firmwarePath) {
|
||||||
this.$message.error('请上传固件文件')
|
this.$message.error(this.$t('firmwareDialog.requiredFirmwareFile'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 提交成功后将关闭对话框的逻辑交给父组件处理
|
// 提交成功后将关闭对话框的逻辑交给父组件处理
|
||||||
@@ -128,13 +128,13 @@ export default {
|
|||||||
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||||
|
|
||||||
if (!isValidType) {
|
if (!isValidType) {
|
||||||
this.$message.error('只能上传.bin/.apk格式的固件文件!')
|
this.$message.error(this.$t('firmwareDialog.invalidFileType'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!isValidSize) {
|
if (!isValidSize) {
|
||||||
this.$message.error('固件文件大小不能超过100MB!')
|
this.$message.error(this.$t('firmwareDialog.invalidFileSize'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
handleUpload(options) {
|
handleUpload(options) {
|
||||||
@@ -158,14 +158,14 @@ export default {
|
|||||||
this.form.size = file.size
|
this.form.size = file.size
|
||||||
this.uploadProgress = 100
|
this.uploadProgress = 100
|
||||||
this.uploadStatus = 'success'
|
this.uploadStatus = 'success'
|
||||||
this.$message.success('固件文件上传成功')
|
this.$message.success(this.$t('firmwareDialog.uploadSuccess'))
|
||||||
// 延迟2秒后隐藏进度条
|
// 延迟2秒后隐藏进度条
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.isUploading = false
|
this.isUploading = false
|
||||||
}, 2000)
|
}, 2000)
|
||||||
} else {
|
} else {
|
||||||
this.uploadStatus = 'exception'
|
this.uploadStatus = 'exception'
|
||||||
this.$message.error(res.msg || '文件上传失败')
|
this.$message.error(res.msg || this.$t('firmwareDialog.uploadFailed'))
|
||||||
this.isUploading = false
|
this.isUploading = false
|
||||||
}
|
}
|
||||||
}, (progressEvent) => {
|
}, (progressEvent) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- 自定义标题区域 -->
|
<!-- 自定义标题区域 -->
|
||||||
<div class="custom-header">
|
<div class="custom-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h3 class="bold-title">功能管理</h3>
|
<h3 class="bold-title">{{ $t('functionDialog.title') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<button class="custom-close-btn" @click="closeDialog">×</button>
|
<button class="custom-close-btn" @click="closeDialog">×</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
<!-- 左侧:未选功能 -->
|
<!-- 左侧:未选功能 -->
|
||||||
<div class="function-column">
|
<div class="function-column">
|
||||||
<div class="column-header">
|
<div class="column-header">
|
||||||
<h4 class="column-title">未选功能</h4>
|
<h4 class="column-title">{{ $t('functionDialog.unselectedFunctions') }}</h4>
|
||||||
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
|
<el-button type="text" @click="selectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="function-list">
|
<div class="function-list">
|
||||||
<div v-if="unselected.length">
|
<div v-if="unselected.length">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
||||||
<el-empty description="没有更多的插件了" />
|
<el-empty :description="$t('functionDialog.noMorePlugins')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
<!-- 中间:已选功能 -->
|
<!-- 中间:已选功能 -->
|
||||||
<div class="function-column">
|
<div class="function-column">
|
||||||
<div class="column-header">
|
<div class="column-header">
|
||||||
<h4 class="column-title">已选功能</h4>
|
<h4 class="column-title">{{ $t('functionDialog.selectedFunctions') }}</h4>
|
||||||
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
|
<el-button type="text" @click="deselectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="function-list">
|
<div class="function-list">
|
||||||
<div v-if="selectedList.length > 0">
|
<div v-if="selectedList.length > 0">
|
||||||
@@ -50,19 +50,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
<div v-else style="display: flex; justify-content: center; align-items: center;">
|
||||||
<el-empty description="请选择插件功能" />
|
<el-empty :description="$t('functionDialog.pleaseSelectPlugin')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧:参数配置 -->
|
<!-- 右侧:参数配置 -->
|
||||||
<div class="params-column">
|
<div class="params-column">
|
||||||
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
|
<h4 v-if="currentFunction" class="column-title">{{ $t('functionDialog.paramConfig') }} - {{ currentFunction.name }}</h4>
|
||||||
<div v-if="currentFunction" class="params-container">
|
<div v-if="currentFunction" class="params-container">
|
||||||
<el-form :model="currentFunction" class="param-form">
|
<el-form :model="currentFunction" class="param-form">
|
||||||
<!-- 遍历 fieldsMeta,而不是 params 的 keys -->
|
<!-- 遍历 fieldsMeta,而不是 params 的 keys -->
|
||||||
<div v-if="currentFunction.fieldsMeta.length == 0">
|
<div v-if="currentFunction.fieldsMeta.length == 0">
|
||||||
<el-empty :description="currentFunction.name + ' 无需配置参数'" />
|
<el-empty :description="currentFunction.name + $t('functionDialog.noNeedToConfig')" />
|
||||||
</div>
|
</div>
|
||||||
<el-form-item v-for="field in currentFunction.fieldsMeta" :key="field.key" :label="field.label"
|
<el-form-item v-for="field in currentFunction.fieldsMeta" :key="field.key" :label="field.label"
|
||||||
class="param-item" :class="{ 'textarea-field': field.type === 'array' || field.type === 'json' }">
|
class="param-item" :class="{ 'textarea-field': field.type === 'array' || field.type === 'json' }">
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
<div v-else class="empty-tip">{{ $t('functionDialog.pleaseSelectFunctionForParam') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,22 +105,22 @@
|
|||||||
<!-- 左侧区域 -->
|
<!-- 左侧区域 -->
|
||||||
<div class="mcp-left">
|
<div class="mcp-left">
|
||||||
<div class="mcp-header">
|
<div class="mcp-header">
|
||||||
<h3 class="bold-title">MCP接入点</h3>
|
<h3 class="bold-title">{{ $t('functionDialog.mcpAccessPoint') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="url-header">
|
<div class="url-header">
|
||||||
<div class="address-desc">
|
<div class="address-desc">
|
||||||
<span>以下是智能体的MCP接入点地址。</span>
|
<span>{{ $t('functionDialog.mcpAddressDesc') }}</span>
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-enable.md"
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-enable.md"
|
||||||
target="_blank" class="doc-link">如何部署MCP接入点</a> |
|
target="_blank" class="doc-link">{{ $t('functionDialog.howToDeployMcp') }}</a> |
|
||||||
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md"
|
<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md"
|
||||||
target="_blank" class="doc-link">如何接入MCP功能</a>
|
target="_blank" class="doc-link">{{ $t('functionDialog.howToIntegrateMcp') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="mcpUrl" readonly class="url-input">
|
<el-input v-model="mcpUrl" readonly class="url-input">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
|
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
|
||||||
复制
|
{{ $t('functionDialog.copy') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,17 +128,17 @@
|
|||||||
<!-- 右侧区域 -->
|
<!-- 右侧区域 -->
|
||||||
<div class="mcp-right">
|
<div class="mcp-right">
|
||||||
<div class="mcp-header">
|
<div class="mcp-header">
|
||||||
<h3 class="bold-title">接入点状态</h3>
|
<h3 class="bold-title">{{ $t('functionDialog.accessPointStatus') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-container">
|
<div class="status-container">
|
||||||
<span class="status-indicator" :class="mcpStatus"></span>
|
<span class="status-indicator" :class="mcpStatus"></span>
|
||||||
<span class="status-text">{{
|
<span class="status-text">{{
|
||||||
mcpStatus === 'connected' ? '已连接' :
|
mcpStatus === 'connected' ? $t('functionDialog.connected') :
|
||||||
mcpStatus === 'loading' ? '加载中...' : '未连接'
|
mcpStatus === 'loading' ? $t('functionDialog.loading') : $t('functionDialog.disconnected')
|
||||||
}}</span>
|
}}</span>
|
||||||
<button class="refresh-btn" @click="refreshStatus">
|
<button class="refresh-btn" @click="refreshStatus">
|
||||||
<span class="refresh-icon">↻</span>
|
<span class="refresh-icon">↻</span>
|
||||||
<span>刷新</span>
|
<span>{{ $t('functionDialog.refresh') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mcp-tools-list">
|
<div class="mcp-tools-list">
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-tools">
|
<div v-else class="no-tools">
|
||||||
<span>暂无可用工具</span>
|
<span>{{ $t('functionDialog.noAvailableTools') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,16 +156,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
<el-button @click="closeDialog">取消</el-button>
|
<el-button @click="closeDialog">{{ $t('functionDialog.cancel') }}</el-button>
|
||||||
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
<el-button type="primary" @click="saveSelection">{{ $t('functionDialog.saveConfig') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
i18n,
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
functions: {
|
functions: {
|
||||||
@@ -264,9 +267,9 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const successful = document.execCommand('copy');
|
const successful = document.execCommand('copy');
|
||||||
if (successful) {
|
if (successful) {
|
||||||
this.$message.success('已复制到剪贴板');
|
this.$message.success(this.$t('functionDialog.copiedToClipboard'));
|
||||||
} else {
|
} else {
|
||||||
this.$message.error('复制失败,请手动复制');
|
this.$message.error(this.$t('functionDialog.copyFailed'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.$message.error('复制失败,请手动复制');
|
this.$message.error('复制失败,请手动复制');
|
||||||
@@ -327,7 +330,7 @@ export default {
|
|||||||
const obj = JSON.parse(text);
|
const obj = JSON.parse(text);
|
||||||
this.handleParamChange(this.currentFunction, key, obj);
|
this.handleParamChange(this.currentFunction, key, obj);
|
||||||
} catch {
|
} catch {
|
||||||
this.$message.error(`${this.currentFunction.name}的${key}字段格式错误:JSON格式有误`);
|
this.$message.error(`${this.currentFunction.name}${this.$t('functionDialog.jsonFormatError')}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleFunctionClick(func) {
|
handleFunctionClick(func) {
|
||||||
@@ -409,7 +412,7 @@ export default {
|
|||||||
fieldRemark(field) {
|
fieldRemark(field) {
|
||||||
let description = (field && field.label) ? field.label : '';
|
let description = (field && field.label) ? field.label : '';
|
||||||
if (field.default) {
|
if (field.default) {
|
||||||
description += `(默认值:${field.default})`;
|
description += `(${this.$t('functionDialog.defaultValue')}:${field.default})`;
|
||||||
}
|
}
|
||||||
return description;
|
return description;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,25 +14,25 @@
|
|||||||
@click="goHome">
|
@click="goHome">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
||||||
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/home' || $route.path === '/role-config' || $route.path === '/device-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
智能体管理
|
<span class="nav-text">{{ $t('header.smartManagement') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||||
@click="goModelConfig">
|
@click="goModelConfig">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
|
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
|
||||||
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
模型配置
|
<span class="nav-text">{{ $t('header.modelConfig') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuperAdmin" class="equipment-management"
|
<div v-if="isSuperAdmin" class="equipment-management"
|
||||||
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png"
|
<img loading="lazy" alt="" src="@/assets/header/user_management.png"
|
||||||
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
用户管理
|
<span class="nav-text">{{ $t('header.userManagement') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuperAdmin" class="equipment-management"
|
<div v-if="isSuperAdmin" class="equipment-management"
|
||||||
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||||
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
OTA管理
|
<span class="nav-text">{{ $t('header.otaManagement') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' }"
|
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' }"
|
||||||
@@ -40,21 +40,21 @@
|
|||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||||
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' || $route.path === '/server-side-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
参数字典
|
<span class="nav-text">{{ $t('header.paramDictionary') }}</span>
|
||||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item @click.native="goParamManagement">
|
<el-dropdown-item @click.native="goParamManagement">
|
||||||
参数管理
|
{{ $t('header.paramManagement') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="goDictManagement">
|
<el-dropdown-item @click.native="goDictManagement">
|
||||||
字典管理
|
{{ $t('header.dictManagement') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="goProviderManagement">
|
<el-dropdown-item @click.native="goProviderManagement">
|
||||||
字段管理
|
{{ $t('header.providerManagement') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click.native="goServerSideManagement">
|
<el-dropdown-item @click.native="goServerSideManagement">
|
||||||
服务端管理
|
{{ $t('header.serverSideManagement') }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@@ -63,11 +63,31 @@
|
|||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
||||||
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input"
|
||||||
@keyup.enter.native="handleSearch">
|
@keyup.enter.native="handleSearch">
|
||||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 语言切换下拉菜单 -->
|
||||||
|
<el-dropdown trigger="click" class="language-dropdown" @visible-change="handleLanguageDropdownVisibleChange">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
<span class="current-language-text">{{ currentLanguageText }}</span>
|
||||||
|
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
|
||||||
|
</span>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
|
||||||
|
{{ $t('language.zhCN') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
|
||||||
|
{{ $t('language.zhTW') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="changeLanguage('en')">
|
||||||
|
{{ $t('language.en') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
||||||
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
||||||
<span class="el-dropdown-link">
|
<span class="el-dropdown-link">
|
||||||
@@ -75,8 +95,9 @@
|
|||||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
<el-dropdown-item @click.native="showChangePasswordDialog">{{ $t('header.changePassword')
|
||||||
<el-dropdown-item @click.native="handleLogout">退出登录</el-dropdown-item>
|
}}</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="handleLogout">{{ $t('header.logout') }}</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,6 +110,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import userApi from '@/apis/module/user';
|
import userApi from '@/apis/module/user';
|
||||||
|
import i18n, { changeLanguage } from '@/i18n';
|
||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||||
|
|
||||||
@@ -108,6 +130,7 @@ export default {
|
|||||||
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||||
userDropdownVisible: false,
|
userDropdownVisible: false,
|
||||||
paramDropdownVisible: false,
|
paramDropdownVisible: false,
|
||||||
|
languageDropdownVisible: false,
|
||||||
isSmallScreen: false
|
isSmallScreen: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -115,6 +138,24 @@ export default {
|
|||||||
...mapGetters(['getIsSuperAdmin']),
|
...mapGetters(['getIsSuperAdmin']),
|
||||||
isSuperAdmin() {
|
isSuperAdmin() {
|
||||||
return this.getIsSuperAdmin;
|
return this.getIsSuperAdmin;
|
||||||
|
},
|
||||||
|
// 获取当前语言
|
||||||
|
currentLanguage() {
|
||||||
|
return i18n.locale || 'zh_CN';
|
||||||
|
},
|
||||||
|
// 获取当前语言显示文本
|
||||||
|
currentLanguageText() {
|
||||||
|
const currentLang = this.currentLanguage;
|
||||||
|
switch (currentLang) {
|
||||||
|
case 'zh_CN':
|
||||||
|
return this.$t('language.zhCN');
|
||||||
|
case 'zh_TW':
|
||||||
|
return this.$t('language.zhTW');
|
||||||
|
case 'en':
|
||||||
|
return this.$t('language.en');
|
||||||
|
default:
|
||||||
|
return this.$t('language.zhCN');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -182,7 +223,7 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('正则表达式创建失败:', error);
|
console.error('正则表达式创建失败:', error);
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '搜索关键词格式不正确',
|
message: this.$t('message.error'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -197,13 +238,13 @@ export default {
|
|||||||
// 调用 Vuex 的 logout action
|
// 调用 Vuex 的 logout action
|
||||||
await this.logout();
|
await this.logout();
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '退出登录成功',
|
message: this.$t('message.success'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('退出登录失败:', error);
|
console.error('退出登录失败:', error);
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '退出登录失败,请重试',
|
message: this.$t('message.error'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -215,6 +256,19 @@ export default {
|
|||||||
handleParamDropdownVisibleChange(visible) {
|
handleParamDropdownVisibleChange(visible) {
|
||||||
this.paramDropdownVisible = visible;
|
this.paramDropdownVisible = visible;
|
||||||
},
|
},
|
||||||
|
// 监听语言下拉菜单的可见状态变化
|
||||||
|
handleLanguageDropdownVisibleChange(visible) {
|
||||||
|
this.languageDropdownVisible = visible;
|
||||||
|
},
|
||||||
|
// 切换语言
|
||||||
|
changeLanguage(lang) {
|
||||||
|
changeLanguage(lang);
|
||||||
|
this.languageDropdownVisible = false;
|
||||||
|
this.$message.success({
|
||||||
|
message: this.$t('message.success'),
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// 使用 mapActions 引入 Vuex 的 logout action
|
// 使用 mapActions 引入 Vuex 的 logout action
|
||||||
...mapActions(['logout'])
|
...mapActions(['logout'])
|
||||||
@@ -229,7 +283,7 @@ export default {
|
|||||||
height: 63px !important;
|
height: 63px !important;
|
||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
/* 设置最小宽度防止过度压缩 */
|
/* 设置最小宽度防止过度压缩 */
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
@@ -261,7 +315,7 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 44%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,29 +358,29 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
margin-right: 15px;
|
margin-right: 5px;
|
||||||
min-width: 150px;
|
flex: 0.9;
|
||||||
flex-grow: 1;
|
min-width: 60px;
|
||||||
max-width: 220px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-search-input>>>.el-input__inner {
|
.custom-search-input>>>.el-input__inner {
|
||||||
height: 30px;
|
height: 18px;
|
||||||
border-radius: 15px;
|
border-radius: 9px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid #e4e6ef;
|
border: 1px solid #e4e6ef;
|
||||||
padding-left: 15px;
|
padding-left: 8px;
|
||||||
font-size: 12px;
|
font-size: 9px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
.search-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
margin-right: 8px;
|
margin-right: 3px;
|
||||||
font-size: 14px;
|
font-size: 9px;
|
||||||
line-height: 30px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-search-input::v-deep .el-input__suffix-inner {
|
.custom-search-input::v-deep .el-input__suffix-inner {
|
||||||
@@ -345,6 +399,18 @@ export default {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-dropdown {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-language-text {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #3d4566;
|
||||||
|
}
|
||||||
|
|
||||||
.more-dropdown {
|
.more-dropdown {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
@@ -364,6 +430,14 @@ export default {
|
|||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 导航文本样式 - 支持中英文换行 */
|
||||||
|
.nav-text {
|
||||||
|
white-space: normal;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 80px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.header-center {
|
.header-center {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog title="手动添加设备" :visible="visible" @close="handleClose" width="30%" center>
|
<el-dialog :title="$t('manualAddDeviceDialog.title')" :visible="visible" @close="handleClose" width="30%" center>
|
||||||
<div class="dialog-content">
|
<div class="dialog-content">
|
||||||
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="100px">
|
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="auto">
|
||||||
<el-form-item label="设备型号" prop="board">
|
<el-form-item :label="$t('manualAddDeviceDialog.deviceType')" prop="board">
|
||||||
<el-select v-model="deviceForm.board" placeholder="请选择设备型号" style="width: 100%">
|
<el-select v-model="deviceForm.board" :placeholder="$t('manualAddDeviceDialog.deviceTypePlaceholder')" style="width: 100%">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in firmwareTypes"
|
v-for="item in firmwareTypes"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
@@ -12,17 +12,17 @@
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="固件版本" prop="appVersion">
|
<el-form-item :label="$t('manualAddDeviceDialog.firmwareVersion')" prop="appVersion">
|
||||||
<el-input v-model="deviceForm.appVersion" placeholder="请输入固件版本"></el-input>
|
<el-input v-model="deviceForm.appVersion" :placeholder="$t('manualAddDeviceDialog.firmwareVersionPlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Mac地址" prop="macAddress">
|
<el-form-item :label="$t('manualAddDeviceDialog.macAddress')" prop="macAddress">
|
||||||
<el-input v-model="deviceForm.macAddress" placeholder="请输入Mac地址"></el-input>
|
<el-input v-model="deviceForm.macAddress" :placeholder="$t('manualAddDeviceDialog.macAddressPlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||||
<div class="dialog-btn" @click="submitForm">确定</div>
|
<div class="dialog-btn" @click="submitForm">{{ $t('manualAddDeviceDialog.confirm') }}</div>
|
||||||
<div class="dialog-btn cancel-btn" @click="cancel">取消</div>
|
<div class="dialog-btn cancel-btn" @click="cancel">{{ $t('manualAddDeviceDialog.cancel') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,9 +41,9 @@ export default {
|
|||||||
const validateMac = (rule, value, callback) => {
|
const validateMac = (rule, value, callback) => {
|
||||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||||
if (!value) {
|
if (!value) {
|
||||||
callback(new Error('请输入Mac地址'));
|
callback(new Error(this.$t('manualAddDeviceDialog.requiredMacAddress')));
|
||||||
} else if (!macRegex.test(value)) {
|
} else if (!macRegex.test(value)) {
|
||||||
callback(new Error('请输入正确的Mac地址格式,例如:00:1A:2B:3C:4D:5E'));
|
callback(new Error(this.$t('manualAddDeviceDialog.invalidMacAddress')));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,10 @@ export default {
|
|||||||
firmwareTypes: [],
|
firmwareTypes: [],
|
||||||
rules: {
|
rules: {
|
||||||
board: [
|
board: [
|
||||||
{ required: true, message: '请选择设备型号', trigger: 'change' }
|
{ required: true, message: this.$t('manualAddDeviceDialog.requiredDeviceType'), trigger: 'change' }
|
||||||
],
|
],
|
||||||
appVersion: [
|
appVersion: [
|
||||||
{ required: true, message: '请输入固件版本', trigger: 'blur' }
|
{ required: true, message: this.$t('manualAddDeviceDialog.requiredFirmwareVersion'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
macAddress: [
|
macAddress: [
|
||||||
{ required: true, validator: validateMac, trigger: 'blur' }
|
{ required: true, validator: validateMac, trigger: 'blur' }
|
||||||
@@ -79,7 +79,7 @@ export default {
|
|||||||
this.firmwareTypes = res.data;
|
this.firmwareTypes = res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取固件类型失败:', error);
|
console.error('获取固件类型失败:', error);
|
||||||
this.$message.error(error.message || '获取固件类型失败');
|
this.$message.error(error.message || this.$t('manualAddDeviceDialog.getFirmwareTypeFailed'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
@@ -97,11 +97,11 @@ export default {
|
|||||||
|
|
||||||
Api.device.manualAddDevice(params, ({ data }) => {
|
Api.device.manualAddDevice(params, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('设备添加成功');
|
this.$message.success(this.$t('manualAddDeviceDialog.addSuccess'));
|
||||||
this.$emit('refresh');
|
this.$emit('refresh');
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '添加失败');
|
this.$message.error(data.msg || this.$t('manualAddDeviceDialog.addFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -149,10 +149,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-dialog__body {
|
::v-deep .el-dialog__body {
|
||||||
padding: 20px 6px;
|
padding: 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-form-item {
|
::v-deep .el-form-item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
class="center-dialog" >
|
class="center-dialog" >
|
||||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
<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;">
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
{{ modelData.duplicateMode ? '创建副本' : '修改模型' }}
|
{{ modelData.duplicateMode ? $t('modelConfigDialog.duplicateModel') : $t('modelConfigDialog.editModel') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="custom-close-btn" @click="dialogVisible = false">
|
<button class="custom-close-btn" @click="dialogVisible = false">
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">模型信息</div>
|
<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; gap: 20px;">
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<span style="margin-right: 8px;">是否启用</span>
|
<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>
|
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: none; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<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>
|
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,39 +26,39 @@
|
|||||||
|
|
||||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
|
|
||||||
<el-form :model="form" ref="form" label-width="100px" label-position="left" class="custom-form">
|
<el-form :model="form" ref="form" label-width="auto" label-position="left" class="custom-form">
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item label="模型名称" prop="name" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1;">
|
||||||
<el-input v-model="form.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
|
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模型编码" prop="code" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1;">
|
||||||
<el-input v-model="form.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
|
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||||
<el-select v-model="form.configJson.type" placeholder="请选择" class="custom-select custom-input-bg"
|
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
|
||||||
style="width: 100%;" @focus="loadProviders" filterable>
|
style="width: 100%;" @focus="loadProviders" filterable>
|
||||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序号" prop="sort" style="flex: 1;">
|
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1;">
|
||||||
<el-input v-model.number="form.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form-item label="文档地址" prop="docUrl" style="margin-bottom: 27px;">
|
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px;">
|
||||||
<el-input v-model="form.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入模型备注" :autosize="{ minRows: 3, maxRows: 5 }"
|
<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>
|
class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
<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>
|
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||||
|
|
||||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
<template v-if="field.type === 'json-textarea'">
|
<template v-if="field.type === 'json-textarea'">
|
||||||
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3" placeholder="请输入JSON格式变量(示例:{'key':'value'})"
|
<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)"></el-input>
|
class="custom-input-bg" @change="(val) => handleJsonChange(field.prop, val)"></el-input>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
class="save-btn"
|
class="save-btn"
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
:disabled="saving">
|
:disabled="saving">
|
||||||
保存
|
{{ $t('modelConfigDialog.save') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -192,8 +192,8 @@ export default {
|
|||||||
const model = data.data;
|
const model = data.data;
|
||||||
|
|
||||||
if (this.modelData.duplicateMode) {
|
if (this.modelData.duplicateMode) {
|
||||||
model.modelName = this.modelData.modelName + '_副本';
|
model.modelName = this.modelData.modelName + this.$t('modelConfigDialog.copySuffix');
|
||||||
model.modelCode = this.modelData.modelCode + '_副本';
|
model.modelCode = this.modelData.modelCode + this.$t('modelConfigDialog.copySuffix');
|
||||||
}
|
}
|
||||||
this.pendingProviderType = model.configJson.type;
|
this.pendingProviderType = model.configJson.type;
|
||||||
this.pendingModelData = model;
|
this.pendingModelData = model;
|
||||||
|
|||||||
@@ -1,55 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title"
|
<el-dialog :title="title" :visible.sync="visible" width="520px" class="param-dialog-wrapper" :append-to-body="true"
|
||||||
:visible.sync="visible"
|
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
|
||||||
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-container">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h2 class="dialog-title">{{ title }}</h2>
|
<h2 class="dialog-title">{{ title }}</h2>
|
||||||
<button class="custom-close-btn" @click="cancel">
|
<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">
|
<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"/>
|
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form :model="form" :rules="rules" ref="form" label-width="110px" label-position="left" class="param-form">
|
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||||
<el-form-item label="参数编码" prop="paramCode" class="form-item">
|
<el-form-item :label="$t('paramDialog.paramCode')" prop="paramCode" class="form-item">
|
||||||
<el-input v-model="form.paramCode" placeholder="请输入参数编码" class="custom-input"></el-input>
|
<el-input v-model="form.paramCode" :placeholder="$t('paramDialog.paramCodePlaceholder')"
|
||||||
|
class="custom-input"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="参数值" prop="paramValue" class="form-item">
|
<el-form-item :label="$t('paramDialog.paramValue')" prop="paramValue" class="form-item">
|
||||||
<el-input v-model="form.paramValue" placeholder="请输入参数值" class="custom-input"></el-input>
|
<el-input v-model="form.paramValue" :placeholder="$t('paramDialog.paramValuePlaceholder')"
|
||||||
|
class="custom-input"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="值类型" prop="valueType" class="form-item">
|
<el-form-item :label="$t('paramDialog.valueType')" prop="valueType" class="form-item">
|
||||||
<el-select v-model="form.valueType" placeholder="请选择值类型" class="custom-select">
|
<el-select v-model="form.valueType" :placeholder="$t('paramDialog.valueTypePlaceholder')"
|
||||||
<el-option v-for="item in valueTypeOptions" :key="item.value" :label="item.label" :value="item.value"/>
|
class="custom-select">
|
||||||
|
<el-option v-for="item in valueTypeOptions" :key="item.value" :label="$t(`paramDialog.${item.value}Type`)"
|
||||||
|
:value="item.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark" class="form-item remark-item">
|
<el-form-item :label="$t('paramDialog.remark')" prop="remark" class="form-item remark-item">
|
||||||
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注" :rows="3" class="custom-textarea"></el-input>
|
<el-input type="textarea" v-model="form.remark" :placeholder="$t('paramDialog.remarkPlaceholder')" :rows="3"
|
||||||
|
class="custom-textarea"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button
|
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
|
||||||
type="primary"
|
{{ $t('paramDialog.save') }}
|
||||||
@click="submit"
|
|
||||||
class="save-btn"
|
|
||||||
:loading="saving"
|
|
||||||
:disabled="saving">
|
|
||||||
保存
|
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="cancel" class="cancel-btn">
|
<el-button @click="cancel" class="cancel-btn">
|
||||||
取消
|
{{ $t('paramDialog.cancel') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,21 +75,21 @@ export default {
|
|||||||
dialogKey: Date.now(),
|
dialogKey: Date.now(),
|
||||||
saving: false,
|
saving: false,
|
||||||
valueTypeOptions: [
|
valueTypeOptions: [
|
||||||
{ value: 'string', label: '字符串(string)' },
|
{ value: 'string' },
|
||||||
{ value: 'number', label: '数字(number)' },
|
{ value: 'number' },
|
||||||
{ value: 'boolean', label: '布尔值(boolean)' },
|
{ value: 'boolean' },
|
||||||
{ value: 'array', label: '数组(array)' },
|
{ value: 'array' },
|
||||||
{ value: 'json', label: 'JSON对象(json)' }
|
{ value: 'json' }
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
paramCode: [
|
paramCode: [
|
||||||
{ required: true, message: "请输入参数编码", trigger: "blur" }
|
{ required: true, message: this.$t('paramDialog.requiredParamCode'), trigger: "blur" }
|
||||||
],
|
],
|
||||||
paramValue: [
|
paramValue: [
|
||||||
{ required: true, message: "请输入参数值", trigger: "blur" }
|
{ required: true, message: this.$t('paramDialog.requiredParamValue'), trigger: "blur" }
|
||||||
],
|
],
|
||||||
valueType: [
|
valueType: [
|
||||||
{ required: true, message: "请选择值类型", trigger: "change" }
|
{ required: true, message: this.$t('paramDialog.requiredValueType'), trigger: "change" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -107,28 +99,27 @@ export default {
|
|||||||
this.$refs.form.validate((valid) => {
|
this.$refs.form.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.saving = true; // 开始加载
|
this.saving = true; // 开始加载
|
||||||
this.$emit('submit', {
|
this.$emit('submit', this.form);
|
||||||
form: this.form,
|
|
||||||
done: () => {
|
|
||||||
this.saving = false; // 加载完成
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.saving = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
this.saving = false; // 取消时重置状态
|
this.saving = false; // 取消时重置状态
|
||||||
this.$emit('cancel');
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 提供给父组件调用以重置saving状态
|
||||||
|
resetSaving() {
|
||||||
|
this.saving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
visible(newVal) {
|
visible(newVal) {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.dialogKey = Date.now();
|
this.dialogKey = Date.now();
|
||||||
|
} else {
|
||||||
|
// 当对话框关闭时,重置saving状态
|
||||||
|
this.saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,44 +9,44 @@
|
|||||||
|
|
||||||
<button class="custom-close-btn" @click="handleClose">×</button>
|
<button class="custom-close-btn" @click="handleClose">×</button>
|
||||||
|
|
||||||
<el-form :model="form" label-width="100px" :rules="rules" ref="form" class="custom-form">
|
<el-form :model="form" label-width="auto" :rules="rules" ref="form" class="custom-form">
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||||
<el-form-item label="类别" prop="modelType" style="flex: 1;">
|
<el-form-item :label="$t('providerDialog.category')" prop="modelType" style="flex: 1;">
|
||||||
<el-select v-model="form.modelType" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;">
|
<el-select v-model="form.modelType" :placeholder="$t('providerDialog.selectCategory')" class="custom-input-bg" style="width: 100%;">
|
||||||
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
|
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="编码" prop="providerCode" style="flex: 1;">
|
<el-form-item :label="$t('providerDialog.code')" prop="providerCode" style="flex: 1;">
|
||||||
<el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input>
|
<el-input v-model="form.providerCode" :placeholder="$t('providerDialog.inputCode')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||||
<el-form-item label="名称" prop="name" style="flex: 1;">
|
<el-form-item :label="$t('providerDialog.name')" prop="name" style="flex: 1;">
|
||||||
<el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input>
|
<el-input v-model="form.name" :placeholder="$t('providerDialog.inputName')" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序" prop="sort" style="flex: 1;">
|
<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-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
|
||||||
style="width: 100%;"></el-input-number>
|
style="width: 100%;"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
|
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
|
||||||
字段配置
|
{{ $t('providerDialog.fieldConfig') }}
|
||||||
<div style="display: inline-block; float: right;">
|
<div style="display: inline-block; float: right;">
|
||||||
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
|
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
|
||||||
:disabled="hasIncompleteFields">
|
:disabled="hasIncompleteFields">
|
||||||
添加
|
{{ $t('providerDialog.add') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" @click="toggleSelectAllFields" size="small"
|
<el-button type="primary" @click="toggleSelectAllFields" size="small"
|
||||||
style="background: #5f70f3; border: none; margin-left: 10px;">
|
style="background: #5f70f3; border: none; margin-left: 10px;">
|
||||||
{{ isAllFieldsSelected ? '取消全选' : '全选' }}
|
{{ isAllFieldsSelected ? $t('providerDialog.deselectAll') : $t('providerDialog.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" @click="batchRemoveFields" size="small"
|
<el-button type="danger" @click="batchRemoveFields" size="small"
|
||||||
style="background: red; border: none; margin-left: 10px;">
|
style="background: red; border: none; margin-left: 10px;">
|
||||||
批量删除
|
{{ $t('providerDialog.batchDelete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,40 +54,40 @@
|
|||||||
|
|
||||||
<div class="fields-container">
|
<div class="fields-container">
|
||||||
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
|
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
|
||||||
<el-table-column label="选择" align="center" width="50">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="50">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="字段key">
|
<el-table-column :label="$t('providerDialog.fieldKey')">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<template v-if="scope.row.editing">
|
<template v-if="scope.row.editing">
|
||||||
<el-input v-model="scope.row.key" placeholder="字段key"></el-input>
|
<el-input v-model="scope.row.key" :placeholder="$t('providerDialog.fieldKey')"></el-input>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ scope.row.key }}
|
{{ scope.row.key }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="字段标签">
|
<el-table-column :label="$t('providerDialog.fieldLabel')">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<template v-if="scope.row.editing">
|
<template v-if="scope.row.editing">
|
||||||
<el-input v-model="scope.row.label" placeholder="字段标签"></el-input>
|
<el-input v-model="scope.row.label" :placeholder="$t('providerDialog.fieldLabel')"></el-input>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ scope.row.label }}
|
{{ scope.row.label }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="字段类型">
|
<el-table-column :label="$t('providerDialog.fieldType')">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<template v-if="scope.row.editing">
|
<template v-if="scope.row.editing">
|
||||||
<el-select v-model="scope.row.type" placeholder="类型">
|
<el-select v-model="scope.row.type" :placeholder="$t('providerDialog.type')">
|
||||||
<el-option label="字符串" value="string"></el-option>
|
<el-option :label="$t('providerDialog.stringType')" value="string"></el-option>
|
||||||
<el-option label="数字" value="number"></el-option>
|
<el-option :label="$t('providerDialog.numberType')" value="number"></el-option>
|
||||||
<el-option label="布尔值" value="boolean"></el-option>
|
<el-option :label="$t('providerDialog.booleanType')" value="boolean"></el-option>
|
||||||
<el-option label="字典" value="dict"></el-option>
|
<el-option :label="$t('providerDialog.dictType')" value="dict"></el-option>
|
||||||
<el-option label="分号分割的列表" value="array"></el-option>
|
<el-option :label="$t('providerDialog.arrayType')" value="array"></el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -95,26 +95,26 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="默认值">
|
<el-table-column :label="$t('providerDialog.defaultValue')">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<template v-if="scope.row.editing">
|
<template v-if="scope.row.editing">
|
||||||
<el-input v-model="scope.row.default" placeholder="请输入默认值"></el-input>
|
<el-input v-model="scope.row.default" :placeholder="$t('providerDialog.inputDefaultValue')"></el-input>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ scope.row.default }}
|
{{ scope.row.default }}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="150" align="center">
|
<el-table-column :label="$t('providerDialog.operation')" width="150" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)">
|
<el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)">
|
||||||
编辑
|
{{ $t('providerDialog.edit') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-else type="success" size="mini" @click="stopEditing(scope.row)">
|
<el-button v-else type="success" size="mini" @click="stopEditing(scope.row)">
|
||||||
完成
|
{{ $t('providerDialog.complete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" size="mini" @click="removeField(scope.$index)">
|
<el-button type="danger" size="mini" @click="removeField(scope.$index)">
|
||||||
删除
|
{{ $t('providerDialog.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: center;">
|
<div style="display: flex; justify-content: center;">
|
||||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">保存</el-button>
|
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">{{ $t('providerDialog.save') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -140,16 +140,18 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
saving: false,
|
saving: false,
|
||||||
rules: {
|
|
||||||
modelType: [{ required: true, message: '请选择类别', trigger: 'change' }],
|
|
||||||
providerCode: [{ required: true, message: '请输入供应器编码', trigger: 'blur' }],
|
|
||||||
name: [{ required: true, message: '请输入供应器名称', trigger: 'blur' }]
|
|
||||||
},
|
|
||||||
isAllFieldsSelected: false,
|
isAllFieldsSelected: false,
|
||||||
tableKey: 0 // 用于强制表格重新渲染
|
tableKey: 0 // 用于强制表格重新渲染
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
rules() {
|
||||||
|
return {
|
||||||
|
modelType: [{ required: true, message: this.$t('providerDialog.requiredCategory'), trigger: 'change' }],
|
||||||
|
providerCode: [{ required: true, message: this.$t('providerDialog.requiredCode'), trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: this.$t('providerDialog.requiredName'), trigger: 'blur' }]
|
||||||
|
};
|
||||||
|
},
|
||||||
hasIncompleteFields() {
|
hasIncompleteFields() {
|
||||||
return this.form.fields && this.form.fields.some(field =>
|
return this.form.fields && this.form.fields.some(field =>
|
||||||
!field.key || !field.label || !field.type
|
!field.key || !field.label || !field.type
|
||||||
@@ -159,11 +161,11 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
getTypeLabel(type) {
|
getTypeLabel(type) {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
'string': '字符串',
|
'string': this.$t('providerDialog.stringType'),
|
||||||
'number': '数字',
|
'number': this.$t('providerDialog.numberType'),
|
||||||
'boolean': '布尔值',
|
'boolean': this.$t('providerDialog.booleanType'),
|
||||||
'dict': '字典',
|
'dict': this.$t('providerDialog.dictType'),
|
||||||
'array': '分号分割的列表'
|
'array': this.$t('providerDialog.arrayType')
|
||||||
};
|
};
|
||||||
return typeMap[type];
|
return typeMap[type];
|
||||||
},
|
},
|
||||||
@@ -212,7 +214,7 @@ export default {
|
|||||||
addField() {
|
addField() {
|
||||||
if (this.hasIncompleteFields) {
|
if (this.hasIncompleteFields) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: '请先完成当前字段的编辑',
|
message: this.$t('providerDialog.completeFieldEdit'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -230,47 +232,47 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeField(index) {
|
removeField(index) {
|
||||||
this.$confirm('确定要删除该字段吗?', '提示', {
|
this.$confirm(this.$t('providerDialog.confirmDeleteField'), this.$t('common.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.form.fields = this.form.fields.filter((_, i) => i !== index);
|
this.form.fields = this.form.fields.filter((_, i) => i !== index);
|
||||||
this.updateSelectAllStatus();
|
this.updateSelectAllStatus();
|
||||||
this.forceTableRerender();
|
this.forceTableRerender();
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '删除成功',
|
message: this.$t('common.deleteSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message.info({
|
this.$message.info({
|
||||||
message: '已取消删除',
|
message: this.$t('common.deleteCancelled'),
|
||||||
showClose: true
|
showClose: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
batchRemoveFields() {
|
batchRemoveFields() {
|
||||||
const selectedFields = this.form.fields.filter(field => field.selected);
|
const selectedFields = this.form.fields.filter(field => field.selected);
|
||||||
if (selectedFields.length === 0) {
|
if (selectedFields.length === 0) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: '请先选择要删除的字段',
|
message: this.$t('providerDialog.selectFieldsToDelete'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$confirm(`确定要删除选中的 ${selectedFields.length} 个字段吗?`, '提示', {
|
this.$confirm(this.$t('providerDialog.confirmBatchDeleteFields', { count: selectedFields.length }), this.$t('common.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.form.fields = this.form.fields.filter(field => !field.selected);
|
this.form.fields = this.form.fields.filter(field => !field.selected);
|
||||||
this.isAllFieldsSelected = false;
|
this.isAllFieldsSelected = false;
|
||||||
this.forceTableRerender();
|
this.forceTableRerender();
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功删除 ${selectedFields.length} 个字段`,
|
message: this.$t('providerDialog.batchDeleteFieldsSuccess', { count: selectedFields.length }),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message.info({
|
this.$message.info({
|
||||||
message: '已取消删除',
|
message: '已取消删除',
|
||||||
@@ -293,12 +295,12 @@ export default {
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
const editingField = this.form.fields.find(field => field.editing);
|
const editingField = this.form.fields.find(field => field.editing);
|
||||||
if (editingField) {
|
if (editingField) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: '请先完成当前字段的编辑',
|
message: this.$t('providerDialog.completeFieldEdit'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.form.fields = this.form.fields.map(field => ({
|
this.form.fields = this.form.fields.map(field => ({
|
||||||
...field,
|
...field,
|
||||||
|
|||||||
@@ -7,72 +7,72 @@
|
|||||||
<div class="scroll-wrapper">
|
<div class="scroll-wrapper">
|
||||||
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
|
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
|
||||||
<el-table v-loading="loading" :data="filteredTtsModels" style="width: 100%;" class="data-table"
|
<el-table v-loading="loading" :data="filteredTtsModels" style="width: 100%;" class="data-table"
|
||||||
header-row-class-name="table-header" :fit="true" element-loading-text="拼命加载中"
|
header-row-class-name="table-header" :fit="true" :element-loading-text="$t('voicePrint.loading')"
|
||||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)">
|
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)">
|
||||||
<el-table-column label="选择" width="50" align="center">
|
<el-table-column :label="$t('ttsModel.select')" width="50" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="音色编码" align="center">
|
<el-table-column :label="$t('ttsModel.voiceCode')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceCode"></el-input>
|
<el-input v-if="scope.row.editing" v-model="scope.row.voiceCode"></el-input>
|
||||||
<span v-else>{{ scope.row.voiceCode }}</span>
|
<span v-else>{{ scope.row.voiceCode }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="音色名称" align="center">
|
<el-table-column :label="$t('ttsModel.voiceName')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceName"></el-input>
|
<el-input v-if="scope.row.editing" v-model="scope.row.voiceName"></el-input>
|
||||||
<span v-else>{{ scope.row.voiceName }}</span>
|
<span v-else>{{ scope.row.voiceName }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="语言类型" align="center">
|
<el-table-column :label="$t('ttsModel.languageType')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-input v-if="scope.row.editing" v-model="scope.row.languageType"></el-input>
|
<el-input v-if="scope.row.editing" v-model="scope.row.languageType"></el-input>
|
||||||
<span v-else>{{ scope.row.languageType }}</span>
|
<span v-else>{{ scope.row.languageType }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!showReferenceColumns" label="试听" align="center" class-name="audio-column">
|
<el-table-column v-if="!showReferenceColumns" :label="$t('ttsModel.preview')" align="center" class-name="audio-column">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<div class="custom-audio-container">
|
<div class="custom-audio-container">
|
||||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceDemo" placeholder="请输入MP3地址"
|
<el-input v-if="scope.row.editing" v-model="scope.row.voiceDemo" :placeholder="$t('ttsModel.enterMp3Url')"
|
||||||
class="audio-input">
|
class="audio-input">
|
||||||
</el-input>
|
</el-input>
|
||||||
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo" />
|
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!showReferenceColumns" label="备注" align="center">
|
<el-table-column v-if="!showReferenceColumns" :label="$t('ttsModel.remark')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-input v-if="scope.row.editing" type="textarea" :rows="1" autosize v-model="scope.row.remark"
|
<el-input v-if="scope.row.editing" type="textarea" :rows="1" autosize v-model="scope.row.remark"
|
||||||
placeholder="这里是备注" class="remark-input"></el-input>
|
:placeholder="$t('ttsModel.enterRemark')" class="remark-input"></el-input>
|
||||||
<span v-else>{{ scope.row.remark }}</span>
|
<span v-else>{{ scope.row.remark }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="showReferenceColumns" label="克隆音频路径" align="center">
|
<el-table-column v-if="showReferenceColumns" :label="$t('ttsModel.cloneAudioPath')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-input v-if="scope.row.editing" v-model="scope.row.referenceAudio" placeholder="这里是克隆音频路径"></el-input>
|
<el-input v-if="scope.row.editing" v-model="scope.row.referenceAudio" :placeholder="$t('ttsModel.enterCloneAudioPath')"></el-input>
|
||||||
<span v-else>{{ scope.row.referenceAudio }}</span>
|
<span v-else>{{ scope.row.referenceAudio }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="showReferenceColumns" label="克隆音频文本" align="center">
|
<el-table-column v-if="showReferenceColumns" :label="$t('ttsModel.cloneAudioText')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-input v-if="scope.row.editing" v-model="scope.row.referenceText" placeholder="这里是克隆音频对应文本"></el-input>
|
<el-input v-if="scope.row.editing" v-model="scope.row.referenceText" :placeholder="$t('ttsModel.enterCloneAudioText')"></el-input>
|
||||||
<span v-else>{{ scope.row.referenceText }}</span>
|
<span v-else>{{ scope.row.referenceText }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center" width="150">
|
<el-table-column :label="$t('ttsModel.operation')" align="center" width="150">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<template v-if="!scope.row.editing">
|
<template v-if="!scope.row.editing">
|
||||||
<el-button type="text" size="mini" @click="startEdit(scope.row)" class="edit-btn">
|
<el-button type="text" size="mini" @click="startEdit(scope.row)" class="edit-btn">
|
||||||
编辑
|
{{ $t('ttsModel.edit') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn">
|
<el-button type="text" size="mini" @click="deleteRow(scope.row)" class="delete-btn">
|
||||||
删除
|
{{ $t('ttsModel.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
<el-button v-else type="success" size="mini" @click="saveEdit(scope.row)" class="save-Tts">保存
|
<el-button v-else type="success" size="mini" @click="saveEdit(scope.row)" class="save-Tts">{{ $t('ttsModel.save') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -87,13 +87,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
||||||
{{ selectAll ? '取消全选' : '全选' }}
|
{{ selectAll ? $t('ttsModel.deselectAll') : $t('ttsModel.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
|
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
|
||||||
新增
|
{{ $t('ttsModel.add') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" size="mini" @click="deleteRow(filteredTtsModels.filter(row => row.selected))"
|
<el-button type="primary" size="mini" @click="deleteRow(filteredTtsModels.filter(row => row.selected))"
|
||||||
style="background: red;border:None">删除
|
style="background: red;border:None">{{ $t('ttsModel.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -205,7 +205,7 @@ export default {
|
|||||||
.map(item => ({
|
.map(item => ({
|
||||||
id: item.id || '',
|
id: item.id || '',
|
||||||
voiceCode: item.ttsVoice || '',
|
voiceCode: item.ttsVoice || '',
|
||||||
voiceName: item.name || '未命名音色',
|
voiceName: item.name || this.$t('ttsModel.unnamedVoice'),
|
||||||
languageType: item.languages || '',
|
languageType: item.languages || '',
|
||||||
remark: item.remark || '',
|
remark: item.remark || '',
|
||||||
referenceAudio: item.referenceAudio || '',
|
referenceAudio: item.referenceAudio || '',
|
||||||
@@ -219,7 +219,7 @@ export default {
|
|||||||
this.total = data.total;
|
this.total = data.total;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '获取音色列表失败',
|
message: data.msg || this.$t('ttsModel.getVoiceListFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,7 @@ export default {
|
|||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error('加载失败:', err);
|
console.error('加载失败:', err);
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '加载音色数据失败',
|
message: this.$t('ttsModel.loadVoiceDataFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@@ -343,7 +343,7 @@ export default {
|
|||||||
saveEdit(row) {
|
saveEdit(row) {
|
||||||
if (!row.voiceCode || !row.voiceName || !row.languageType) {
|
if (!row.voiceCode || !row.voiceName || !row.languageType) {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '音色编码、音色名称和语言类型不能为空',
|
message: this.$t('ttsModel.voiceCodeNameLanguageRequired'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -390,7 +390,7 @@ export default {
|
|||||||
delete row.originalData;
|
delete row.originalData;
|
||||||
}
|
}
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '操作失败,请重试',
|
message: this.$t('ttsModel.operationFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -399,7 +399,7 @@ export default {
|
|||||||
handleResponse(res, row) {
|
handleResponse(res, row) {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: row.id ? '修改成功' : '保存成功',
|
message: row.id ? this.$t('ttsModel.updateSuccess') : this.$t('ttsModel.saveSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
row.editing = false;
|
row.editing = false;
|
||||||
@@ -413,9 +413,9 @@ export default {
|
|||||||
delete row.originalData;
|
delete row.originalData;
|
||||||
}
|
}
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: res.msg || (row.id ? '修改失败' : '保存失败'),
|
message: res.msg || (row.id ? this.$t('ttsModel.updateFailed') : this.$t('ttsModel.saveFailed')),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ export default {
|
|||||||
addNew() {
|
addNew() {
|
||||||
const hasEditing = this.ttsModels.some(row => row.editing);
|
const hasEditing = this.ttsModels.some(row => row.editing);
|
||||||
if (hasEditing) {
|
if (hasEditing) {
|
||||||
this.$message.warning('请先完成当前编辑再新增');
|
this.$message.warning(this.$t('ttsModel.finishEditingFirst'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +440,7 @@ export default {
|
|||||||
const newRow = {
|
const newRow = {
|
||||||
voiceCode: '',
|
voiceCode: '',
|
||||||
voiceName: '',
|
voiceName: '',
|
||||||
languageType: '中文',
|
languageType: this.$t('editVoiceDialog.defaultLanguageType'),
|
||||||
voiceDemo: '',
|
voiceDemo: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
referenceAudio: '',
|
referenceAudio: '',
|
||||||
@@ -458,34 +458,34 @@ export default {
|
|||||||
const voices = Array.isArray(row) ? row : [row];
|
const voices = Array.isArray(row) ? row : [row];
|
||||||
|
|
||||||
if (Array.isArray(row) && row.length === 0) {
|
if (Array.isArray(row) && row.length === 0) {
|
||||||
this.$message.warning("请先选择需要删除的音色");
|
this.$message.warning(this.$t('ttsModel.selectVoiceToDelete'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const voiceCount = voices.length;
|
const voiceCount = voices.length;
|
||||||
this.$confirm(`确定要删除选中的${voiceCount}个音色吗?`, "警告", {
|
this.$confirm(this.$t('ttsModel.confirmDeleteVoice', {count: voiceCount}), this.$t('ttsModel.warning'), {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: "warning",
|
type: "warning",
|
||||||
distinguishCancelAndClose: true
|
distinguishCancelAndClose: true
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ids = voices.map(voice => voice.id);
|
const ids = voices.map(voice => voice.id);
|
||||||
if (ids.some(id => !id)) {
|
if (ids.some(id => !id)) {
|
||||||
this.$message.error("存在无效的音色ID");
|
this.$message.error(this.$t('ttsModel.invalidVoiceId'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Api.timbre.deleteVoice(ids, ({ data }) => {
|
Api.timbre.deleteVoice(ids, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功删除${voiceCount}个参数`,
|
message: this.$t('ttsModel.deleteVoiceSuccess', {count: voiceCount}),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.loadData(); // 刷新参数列表
|
this.loadData(); // 刷新参数列表
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '删除失败,请重试',
|
message: data.msg || this.$t('ttsModel.deleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -494,13 +494,13 @@ export default {
|
|||||||
if (action === 'cancel') {
|
if (action === 'cancel') {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: '已取消删除操作',
|
message: this.$t('ttsModel.deleteCancelled'),
|
||||||
duration: 1000
|
duration: 1000
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: '操作已关闭',
|
message: this.$t('ttsModel.operationClosed'),
|
||||||
duration: 1000
|
duration: 1000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form :model="form" :rules="rules" ref="form" label-width="110px" label-position="left" class="param-form">
|
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||||
<el-form-item label="声纹向量" prop="audioId" class="form-item">
|
<el-form-item :label="$t('voicePrintDialog.voicePrintVector')" prop="audioId" class="form-item">
|
||||||
<el-select v-model="form.audioId" placeholder="请选择一条语言消息" class="custom-select">
|
<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">
|
<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: left">{{ item.content }}</span>
|
||||||
<span style="float: right; color: #8492a6; font-size: 13px">
|
<span style="float: right; color: #8492a6; font-size: 13px">
|
||||||
@@ -24,22 +24,22 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="姓名" prop="sourceName" class="form-item">
|
<el-form-item :label="$t('voicePrintDialog.name')" prop="sourceName" class="form-item">
|
||||||
<el-input v-model="form.sourceName" placeholder="请输入姓名" class="custom-input"></el-input>
|
<el-input v-model="form.sourceName" :placeholder="$t('voicePrintDialog.enterName')" class="custom-input"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="描述" prop="introduce" class="form-item remark-item">
|
<el-form-item :label="$t('voicePrintDialog.description')" prop="introduce" class="form-item remark-item">
|
||||||
<el-input type="textarea" v-model="form.introduce" placeholder="请输入描述" :rows="3" class="custom-textarea"
|
<el-input type="textarea" v-model="form.introduce" :placeholder="$t('voicePrintDialog.enterDescription')" :rows="3" class="custom-textarea"
|
||||||
maxlength="100" show-word-limit></el-input>
|
maxlength="100" show-word-limit></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
|
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
|
||||||
保存
|
{{ $t('voicePrintDialog.save') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="cancel" class="cancel-btn">
|
<el-button @click="cancel" class="cancel-btn">
|
||||||
取消
|
{{ $t('voicePrintDialog.cancel') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,13 +83,13 @@ export default {
|
|||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
introduce: [
|
introduce: [
|
||||||
{ required: true, message: "请输入描述", trigger: "blur" }
|
{ required: true, message: '请输入描述', trigger: "blur" }
|
||||||
],
|
],
|
||||||
sourceName: [
|
sourceName: [
|
||||||
{ required: true, message: "请输入姓名", trigger: "blur" }
|
{ required: true, message: '请输入名称', trigger: "blur" }
|
||||||
],
|
],
|
||||||
audioId: [
|
audioId: [
|
||||||
{ required: true, message: "请选择音频向量", trigger: "change" }
|
{ required: true, message: '请选择音频向量', trigger: "change" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import VueI18n from 'vue-i18n';
|
||||||
|
import zhCN from './zh_CN';
|
||||||
|
import zhTW from './zh_TW';
|
||||||
|
import en from './en';
|
||||||
|
|
||||||
|
Vue.use(VueI18n);
|
||||||
|
|
||||||
|
// 从本地存储获取语言设置,如果没有则使用浏览器语言或默认语言
|
||||||
|
const getDefaultLanguage = () => {
|
||||||
|
const savedLang = localStorage.getItem('userLanguage');
|
||||||
|
if (savedLang) {
|
||||||
|
return savedLang;
|
||||||
|
}
|
||||||
|
const browserLang = navigator.language || navigator.userLanguage;
|
||||||
|
if (browserLang.indexOf('zh') === 0) {
|
||||||
|
if (browserLang === 'zh-TW' || browserLang === 'zh-HK' || browserLang === 'zh-MO') {
|
||||||
|
return 'zh_TW';
|
||||||
|
}
|
||||||
|
return 'zh_CN';
|
||||||
|
}
|
||||||
|
return 'en';
|
||||||
|
};
|
||||||
|
|
||||||
|
const i18n = new VueI18n({
|
||||||
|
locale: getDefaultLanguage(),
|
||||||
|
fallbackLocale: 'zh_CN',
|
||||||
|
messages: {
|
||||||
|
'zh_CN': zhCN,
|
||||||
|
'zh_TW': zhTW,
|
||||||
|
'en': en
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
|
|
||||||
|
// 提供一个方法来切换语言
|
||||||
|
export const changeLanguage = (lang) => {
|
||||||
|
i18n.locale = lang;
|
||||||
|
localStorage.setItem('userLanguage', lang);
|
||||||
|
// 通知组件语言已更改
|
||||||
|
Vue.prototype.$eventBus.$emit('languageChanged', lang);
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,17 @@
|
|||||||
import ElementUI from 'element-ui';
|
|
||||||
import 'element-ui/lib/theme-chalk/index.css';
|
import 'element-ui/lib/theme-chalk/index.css';
|
||||||
import 'normalize.css/normalize.css'; // A modern alternative to CSS resets
|
import 'normalize.css/normalize.css'; // A modern alternative to CSS resets
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import ElementUI from 'element-ui';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import i18n from './i18n';
|
||||||
import './styles/global.scss';
|
import './styles/global.scss';
|
||||||
import { register as registerServiceWorker } from './registerServiceWorker';
|
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||||
|
|
||||||
|
// 创建事件总线,用于组件间通信
|
||||||
|
Vue.prototype.$eventBus = new Vue();
|
||||||
|
|
||||||
Vue.use(ElementUI);
|
Vue.use(ElementUI);
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
@@ -19,5 +23,6 @@ registerServiceWorker();
|
|||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
|
i18n,
|
||||||
render: function (h) { return h(App) }
|
render: function (h) { return h(App) }
|
||||||
}).$mount('#app')
|
}).$mount('#app')
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar/>
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">设备管理</h2>
|
<h2 class="page-title">{{ $t('device.management') }}</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword" class="search-input"
|
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
|
||||||
@keyup.enter.native="handleSearch" clearable/>
|
@keyup.enter.native="handleSearch" clearable />
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
<el-button class="btn-search" @click="handleSearch">{{ $t('device.search') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -16,56 +16,56 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="device-card" shadow="never">
|
<el-card class="device-card" shadow="never">
|
||||||
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
|
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
|
||||||
:header-cell-class-name="headerCellClassName" v-loading="loading"
|
:header-cell-class-name="headerCellClassName" v-loading="loading"
|
||||||
element-loading-text="拼命加载中"
|
:element-loading-text="$t('deviceManagement.loading')" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="设备型号" prop="model" align="center">
|
<el-table-column :label="$t('device.model')" prop="model" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ getFirmwareTypeName(scope.row.model) }}
|
{{ getFirmwareTypeName(scope.row.model) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="固件版本" prop="firmwareVersion" align="center"></el-table-column>
|
<el-table-column :label="$t('device.firmwareVersion')" prop="firmwareVersion"
|
||||||
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
|
align="center"></el-table-column>
|
||||||
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
<el-table-column :label="$t('device.macAddress')" prop="macAddress" align="center"></el-table-column>
|
||||||
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
|
<el-table-column :label="$t('device.bindTime')" prop="bindTime" align="center"></el-table-column>
|
||||||
<el-table-column label="备注" align="center">
|
<el-table-column :label="$t('device.lastConversation')" prop="lastConversation"
|
||||||
<template #default="{ row }">
|
align="center"></el-table-column>
|
||||||
<el-input
|
<el-table-column :label="$t('device.deviceStatus')" prop="deviceStatus" align="center">
|
||||||
v-show="row.isEdit"
|
<template slot-scope="scope">
|
||||||
v-model="row.remark"
|
<el-tag v-if="scope.row.deviceStatus === 'online'" type="success">{{ $t('device.online') }}</el-tag>
|
||||||
size="mini"
|
<el-tag v-else type="danger">{{ $t('device.offline') }}</el-tag>
|
||||||
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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="OTA升级" align="center">
|
<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">
|
<template slot-scope="scope">
|
||||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
||||||
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center">
|
<el-table-column :label="$t('device.operation')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||||
解绑
|
{{ $t('device.unbind') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="scope.row.deviceStatus === 'online'" size="mini" type="text" @click="handleMcpToolCall(scope.row.device_id)">
|
||||||
|
{{ $t('device.toolCall') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -74,29 +74,38 @@
|
|||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isCurrentPageAllSelected ? '取消全选' : '全选' }}
|
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||||
验证码绑定
|
{{ $t('device.bindWithCode') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
||||||
手动添加
|
{{ $t('device.manualAdd') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">解绑</el-button>
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">{{
|
||||||
|
$t('device.unbind')
|
||||||
|
}}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||||
|
:label="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
$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"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
|
||||||
<span class="total-text">共{{ deviceList.length }}条记录</span>
|
$t('dictManagement.nextPage') }}</button>
|
||||||
|
<span class="total-text">{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -105,9 +114,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
@refresh="fetchBindDevices(currentAgentId)"/>
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
|
<ManualAddDeviceDialog :visible.sync="manualAddDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
@refresh="fetchBindDevices(currentAgentId)"/>
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
|
<McpToolCallDialog :visible.sync="mcpToolCallDialogVisible" :device-id="selectedDeviceId" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -115,19 +125,23 @@
|
|||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||||
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
|
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
|
||||||
|
import McpToolCallDialog from "@/components/McpToolCallDialog.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
HeaderBar,
|
HeaderBar,
|
||||||
AddDeviceDialog,
|
AddDeviceDialog,
|
||||||
ManualAddDeviceDialog
|
ManualAddDeviceDialog,
|
||||||
|
McpToolCallDialog
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
manualAddDeviceDialogVisible: false,
|
manualAddDeviceDialogVisible: false,
|
||||||
|
mcpToolCallDialogVisible: false,
|
||||||
|
selectedDeviceId: '',
|
||||||
searchKeyword: "",
|
searchKeyword: "",
|
||||||
activeSearchKeyword: "",
|
activeSearchKeyword: "",
|
||||||
currentAgentId: this.$route.query.agentId || '',
|
currentAgentId: this.$route.query.agentId || '',
|
||||||
@@ -145,8 +159,8 @@ export default {
|
|||||||
const keyword = this.activeSearchKeyword.toLowerCase();
|
const keyword = this.activeSearchKeyword.toLowerCase();
|
||||||
if (!keyword) return this.deviceList;
|
if (!keyword) return this.deviceList;
|
||||||
return this.deviceList.filter(device =>
|
return this.deviceList.filter(device =>
|
||||||
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
||||||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -160,8 +174,8 @@ export default {
|
|||||||
},
|
},
|
||||||
// 计算当前页是否全选
|
// 计算当前页是否全选
|
||||||
isCurrentPageAllSelected() {
|
isCurrentPageAllSelected() {
|
||||||
return this.paginatedDeviceList.length > 0 &&
|
return this.paginatedDeviceList.length > 0 &&
|
||||||
this.paginatedDeviceList.every(device => device.selected);
|
this.paginatedDeviceList.every(device => device.selected);
|
||||||
},
|
},
|
||||||
visiblePages() {
|
visiblePages() {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
@@ -194,8 +208,8 @@ export default {
|
|||||||
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
|
const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE')
|
||||||
this.firmwareTypes = res.data
|
this.firmwareTypes = res.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取固件类型失败:', error)
|
console.error(this.$t('device.getFirmwareTypeFailed') + ':', error)
|
||||||
this.$message.error(error.message || '获取固件类型失败')
|
this.$message.error(error.message || this.$t('device.getFirmwareTypeFailed'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handlePageSizeChange(val) {
|
handlePageSizeChange(val) {
|
||||||
@@ -218,15 +232,15 @@ export default {
|
|||||||
const selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
const selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||||
if (selectedDevices.length === 0) {
|
if (selectedDevices.length === 0) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: '请至少选择一条记录',
|
message: this.$t('device.selectAtLeastOne'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$confirm(`确认要解绑选中的 ${selectedDevices.length} 台设备吗?`, '警告', {
|
this.$confirm(this.$t('device.confirmBatchUnbind').replace('{count}', selectedDevices.length), this.$t('message.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('button.ok'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('button.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const deviceIds = selectedDevices.map(device => device.device_id);
|
const deviceIds = selectedDevices.map(device => device.device_id);
|
||||||
@@ -236,29 +250,29 @@ export default {
|
|||||||
batchUnbindDevices(deviceIds) {
|
batchUnbindDevices(deviceIds) {
|
||||||
const promises = deviceIds.map(id => {
|
const promises = deviceIds.map(id => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Api.device.unbindDevice(id, ({data}) => {
|
Api.device.unbindDevice(id, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(data.msg || '解绑失败');
|
reject(data.msg || this.$t('device.bindFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功解绑 ${deviceIds.length} 台设备`,
|
message: this.$t('device.batchUnbindSuccess').replace('{count}', deviceIds.length),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
|
||||||
this.fetchBindDevices(this.currentAgentId);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.$message.error({
|
|
||||||
message: error || '批量解绑过程中出现错误',
|
|
||||||
showClose: true
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
this.fetchBindDevices(this.currentAgentId);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$message.error({
|
||||||
|
message: error || this.$t('device.batchUnbindError'),
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
handleAddDevice() {
|
handleAddDevice() {
|
||||||
this.addDeviceDialogVisible = true;
|
this.addDeviceDialogVisible = true;
|
||||||
@@ -266,12 +280,17 @@ export default {
|
|||||||
handleManualAddDevice() {
|
handleManualAddDevice() {
|
||||||
this.manualAddDeviceDialogVisible = true;
|
this.manualAddDeviceDialogVisible = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleMcpToolCall(deviceId) {
|
||||||
|
this.selectedDeviceId = deviceId;
|
||||||
|
this.mcpToolCallDialogVisible = true;
|
||||||
|
},
|
||||||
submitRemark(row) {
|
submitRemark(row) {
|
||||||
if (row._submitting) return;
|
if (row._submitting) return;
|
||||||
|
|
||||||
const text = (row.remark || '').trim();
|
const text = (row.remark || '').trim();
|
||||||
if (text.length > 64) {
|
if (text.length > 64) {
|
||||||
this.$message.warning('备注不能超过 64 字符');
|
this.$message.warning(this.$t('device.remarkTooLong'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text === row._originalRemark) {
|
if (text === row._originalRemark) {
|
||||||
@@ -282,10 +301,10 @@ export default {
|
|||||||
this.updateDeviceInfo(row.device_id, { alias: text }, (ok, resp) => {
|
this.updateDeviceInfo(row.device_id, { alias: text }, (ok, resp) => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
row._originalRemark = text;
|
row._originalRemark = text;
|
||||||
this.$message.success('备注已保存');
|
this.$message.success(this.$t('device.remarkSaved'));
|
||||||
} else {
|
} else {
|
||||||
row.remark = row._originalRemark;
|
row.remark = row._originalRemark;
|
||||||
this.$message.error(resp.msg || '备注保存失败');
|
this.$message.error(resp.msg || this.$t('device.remarkSaveFailed'));
|
||||||
}
|
}
|
||||||
row._submitting = false;
|
row._submitting = false;
|
||||||
});
|
});
|
||||||
@@ -303,21 +322,21 @@ export default {
|
|||||||
this.submitRemark(row);
|
this.submitRemark(row);
|
||||||
},
|
},
|
||||||
handleUnbind(device_id) {
|
handleUnbind(device_id) {
|
||||||
this.$confirm('确认要解绑该设备吗?', '警告', {
|
this.$confirm(this.$t('device.confirmUnbind'), this.$t('message.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('button.ok'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('button.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Api.device.unbindDevice(device_id, ({data}) => {
|
Api.device.unbindDevice(device_id, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '设备解绑成功',
|
message: this.$t('device.unbindSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchBindDevices(this.$route.query.agentId);
|
this.fetchBindDevices(this.$route.query.agentId);
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '设备解绑失败',
|
message: data.msg || this.$t('device.unbindFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -339,7 +358,7 @@ export default {
|
|||||||
|
|
||||||
fetchBindDevices(agentId) {
|
fetchBindDevices(agentId) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
Api.device.getAgentBindDevices(agentId, ({data}) => {
|
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.deviceList = data.data.map(device => {
|
this.deviceList = data.data.map(device => {
|
||||||
@@ -356,18 +375,74 @@ export default {
|
|||||||
_submitting: false,
|
_submitting: false,
|
||||||
otaSwitch: device.autoUpdate === 1,
|
otaSwitch: device.autoUpdate === 1,
|
||||||
rawBindTime: new Date(device.createDate).getTime(),
|
rawBindTime: new Date(device.createDate).getTime(),
|
||||||
selected: false
|
selected: false,
|
||||||
|
// 初始设置为离线状态
|
||||||
|
deviceStatus: 'offline'
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||||
this.activeSearchKeyword = "";
|
this.activeSearchKeyword = "";
|
||||||
this.searchKeyword = "";
|
this.searchKeyword = "";
|
||||||
|
|
||||||
|
// 获取设备列表后,立即获取设备状态
|
||||||
|
this.fetchDeviceStatus(agentId);
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取设备列表失败');
|
this.$message.error(data.msg || this.$t('device.getListFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
headerCellClassName({columnIndex}) {
|
|
||||||
|
// 获取设备状态
|
||||||
|
fetchDeviceStatus(agentId) {
|
||||||
|
Api.device.getDeviceStatus(agentId, ({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
try {
|
||||||
|
// 解析后端返回的设备状态JSON
|
||||||
|
const statusData = JSON.parse(data.data);
|
||||||
|
|
||||||
|
// 直接使用解析后的数据作为设备状态映射(不需要devices字段包装)
|
||||||
|
if (statusData && typeof statusData === 'object') {
|
||||||
|
// 更新设备状态
|
||||||
|
this.updateDeviceStatusFromResponse(statusData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// JSON解析失败,忽略状态更新
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 根据API响应更新设备状态
|
||||||
|
updateDeviceStatusFromResponse(deviceStatusMap) {
|
||||||
|
this.deviceList.forEach(device => {
|
||||||
|
// 构建设备的MQTT客户端ID
|
||||||
|
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];
|
||||||
|
|
||||||
|
let isOnline = false;
|
||||||
|
if (statusInfo.isAlive === true) {
|
||||||
|
isOnline = true;
|
||||||
|
} else if (statusInfo.isAlive === false) {
|
||||||
|
isOnline = false;
|
||||||
|
} else if (statusInfo.isAlive === null && statusInfo.exists === true) {
|
||||||
|
isOnline = true;
|
||||||
|
} else {
|
||||||
|
isOnline = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
device.deviceStatus = isOnline ? 'online' : 'offline';
|
||||||
|
} else {
|
||||||
|
// 如果没有找到对应的状态信息,默认为离线
|
||||||
|
device.deviceStatus = 'offline';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
headerCellClassName({ columnIndex }) {
|
||||||
if (columnIndex === 0) {
|
if (columnIndex === 0) {
|
||||||
return "custom-selection-header";
|
return "custom-selection-header";
|
||||||
}
|
}
|
||||||
@@ -378,18 +453,18 @@ export default {
|
|||||||
return firmwareType ? firmwareType.name : type
|
return firmwareType ? firmwareType.name : type
|
||||||
},
|
},
|
||||||
updateDeviceInfo(device_id, payload, callback) {
|
updateDeviceInfo(device_id, payload, callback) {
|
||||||
return Api.device.updateDeviceInfo(device_id, payload, ({data}) => {
|
return Api.device.updateDeviceInfo(device_id, payload, ({ data }) => {
|
||||||
callback(data.code === 0, data);
|
callback(data.code === 0, data);
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleOtaSwitchChange(row) {
|
handleOtaSwitchChange(row) {
|
||||||
this.updateDeviceInfo(row.device_id, {autoUpdate: row.otaSwitch ? 1 : 0}, (result, {msg}) => {
|
this.updateDeviceInfo(row.device_id, { autoUpdate: row.otaSwitch ? 1 : 0 }, (result, { msg }) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级');
|
this.$message.success(row.otaSwitch ? this.$t('device.autoUpdateEnabled') : this.$t('device.autoUpdateDisabled'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
row.otaSwitch = !row.otaSwitch
|
row.otaSwitch = !row.otaSwitch
|
||||||
this.$message.error(msg || '操作失败')
|
this.$message.error(msg || this.$t('message.error'))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">字典管理</h2>
|
<h2 class="page-title">{{ $t('dictManagement.pageTitle') }}</h2>
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<div class="search-group">
|
<div class="search-group">
|
||||||
<el-input placeholder="请输入字典值标签查询" v-model="search" class="search-input" clearable
|
<el-input :placeholder="$t('dictManagement.searchPlaceholder')" v-model="search" class="search-input" clearable
|
||||||
@keyup.enter.native="handleSearch" style="width: 240px" />
|
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||||
<el-button class="btn-search" @click="handleSearch">
|
<el-button class="btn-search" @click="handleSearch">
|
||||||
搜索
|
{{ $t('dictManagement.search') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,22 +21,22 @@
|
|||||||
<!-- 左侧字典类型列表 -->
|
<!-- 左侧字典类型列表 -->
|
||||||
<div class="dict-type-panel">
|
<div class="dict-type-panel">
|
||||||
<div class="dict-type-header">
|
<div class="dict-type-header">
|
||||||
<el-button type="success" size="mini" @click="showAddDictTypeDialog">新增字典类型</el-button>
|
<el-button type="success" size="mini" @click="showAddDictTypeDialog">{{ $t('dictManagement.addDictType') }}</el-button>
|
||||||
<el-button type="danger" size="mini" @click="batchDeleteDictType"
|
<el-button type="danger" size="mini" @click="batchDeleteDictType"
|
||||||
:disabled="selectedDictTypes.length === 0">
|
:disabled="selectedDictTypes.length === 0">
|
||||||
批量删除字典类型
|
{{ $t('dictManagement.batchDeleteDictType') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading"
|
<el-table ref="dictTypeTable" :data="dictTypeList" style="width: 100%" v-loading="dictTypeLoading"
|
||||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick"
|
element-loading-background="rgba(255, 255, 255, 0.7)" @row-click="handleDictTypeRowClick"
|
||||||
@selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName"
|
@selection-change="handleDictTypeSelectionChange" :row-class-name="tableRowClassName"
|
||||||
class="dict-type-table">
|
class="dict-type-table" :header-cell-class-name="headerCellClassName">
|
||||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
<el-table-column type="selection" width="70" align="center" :cell-class-name="selectionCellClassName"></el-table-column>
|
||||||
<el-table-column label="字典类型名称" prop="dictName" align="center"></el-table-column>
|
<el-table-column :label="$t('dictManagement.dictTypeName')" prop="dictName" align="center"></el-table-column>
|
||||||
<el-table-column label="操作" width="100" align="center">
|
<el-table-column :label="$t('dictManagement.operation')" width="100" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" @click.stop="editDictType(scope.row)">编辑</el-button>
|
<el-button type="text" size="mini" @click.stop="editDictType(scope.row)">{{ $t('dictManagement.edit') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -50,23 +50,23 @@
|
|||||||
element-loading-spinner="el-icon-loading"
|
element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table"
|
element-loading-background="rgba(255, 255, 255, 0.7)" class="data-table"
|
||||||
header-row-class-name="table-header">
|
header-row-class-name="table-header">
|
||||||
<el-table-column label="选择" align="center" width="55">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="70">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="字典标签" prop="dictLabel" align="center"></el-table-column>
|
<el-table-column :label="$t('dictManagement.dictLabel')" prop="dictLabel" align="center"></el-table-column>
|
||||||
<el-table-column label="字典值" prop="dictValue" align="center"></el-table-column>
|
<el-table-column :label="$t('dictManagement.dictValue')" prop="dictValue" align="center"></el-table-column>
|
||||||
<el-table-column label="排序" prop="sort" align="center"></el-table-column>
|
<el-table-column :label="$t('dictManagement.sort')" prop="sort" align="center"></el-table-column>
|
||||||
<el-table-column label="操作" align="center" width="180px">
|
<el-table-column :label="$t('dictManagement.operation')" align="center" width="180px">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" @click="editDictData(scope.row)"
|
<el-button type="text" size="mini" @click="editDictData(scope.row)"
|
||||||
class="edit-btn">
|
class="edit-btn">
|
||||||
修改
|
{{ $t('dictManagement.edit') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="text" size="mini" @click="deleteDictData(scope.row)"
|
<el-button type="text" size="mini" @click="deleteDictData(scope.row)"
|
||||||
class="delete-btn">
|
class="delete-btn">
|
||||||
删除
|
{{ $t('dictManagement.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -74,36 +74,36 @@
|
|||||||
<div class="table-footer">
|
<div class="table-footer">
|
||||||
<div class="batch-actions">
|
<div class="batch-actions">
|
||||||
<el-button size="mini" type="primary" @click="selectAllDictData">
|
<el-button size="mini" type="primary" @click="selectAllDictData">
|
||||||
{{ isAllDictDataSelected ? '取消全选' : '全选' }}
|
{{ isAllDictDataSelected ? $t('dictManagement.deselectAll') : $t('dictManagement.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn">
|
<el-button type="success" size="mini" @click="showAddDictDataDialog" class="add-btn">
|
||||||
新增字典数据
|
{{ $t('dictManagement.addDictData') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData">
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDeleteDictData">
|
||||||
批量删除字典数据
|
{{ $t('dictManagement.batchDeleteDictData') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('dictManagement.itemsPerPage', { items: item })"
|
||||||
:value="item">
|
:value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
{{ $t('dictManagement.firstPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
上一页
|
{{ $t('dictManagement.prevPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
下一页
|
{{ $t('dictManagement.nextPage') }}
|
||||||
</button>
|
</button>
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
<span class="total-text">{{ $t('dictManagement.totalRecords', { total }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -211,7 +211,7 @@ export default {
|
|||||||
return row === this.selectedDictType ? 'current-row' : ''
|
return row === this.selectedDictType ? 'current-row' : ''
|
||||||
},
|
},
|
||||||
showAddDictTypeDialog() {
|
showAddDictTypeDialog() {
|
||||||
this.dictTypeDialogTitle = '新增字典类型'
|
this.dictTypeDialogTitle = this.$t('dictManagement.addDictType')
|
||||||
this.dictTypeForm = {
|
this.dictTypeForm = {
|
||||||
id: null,
|
id: null,
|
||||||
dictName: '',
|
dictName: '',
|
||||||
@@ -220,7 +220,7 @@ export default {
|
|||||||
this.dictTypeDialogVisible = true
|
this.dictTypeDialogVisible = true
|
||||||
},
|
},
|
||||||
editDictType(row) {
|
editDictType(row) {
|
||||||
this.dictTypeDialogTitle = '编辑字典类型'
|
this.dictTypeDialogTitle = this.$t('dictManagement.editDictType')
|
||||||
this.dictTypeForm = { ...row }
|
this.dictTypeForm = { ...row }
|
||||||
this.dictTypeDialogVisible = true
|
this.dictTypeDialogVisible = true
|
||||||
},
|
},
|
||||||
@@ -228,7 +228,7 @@ export default {
|
|||||||
const api = formData.id ? dictApi.updateDictType : dictApi.addDictType
|
const api = formData.id ? dictApi.updateDictType : dictApi.addDictType
|
||||||
api(formData, ({ data }) => {
|
api(formData, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('保存成功')
|
this.$message.success(this.$t('dictManagement.saveSuccess'))
|
||||||
this.dictTypeDialogVisible = false
|
this.dictTypeDialogVisible = false
|
||||||
this.loadDictTypeList()
|
this.loadDictTypeList()
|
||||||
}
|
}
|
||||||
@@ -236,19 +236,19 @@ export default {
|
|||||||
},
|
},
|
||||||
batchDeleteDictType() {
|
batchDeleteDictType() {
|
||||||
if (this.selectedDictTypes.length === 0) {
|
if (this.selectedDictTypes.length === 0) {
|
||||||
this.$message.warning('请选择要删除的字典类型')
|
this.$message.warning(this.$t('dictManagement.selectDictTypeToDelete'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$confirm('确定要删除选中的字典类型吗?', '提示', {
|
this.$confirm(this.$t('dictManagement.confirmDeleteDictType'), this.$t('dictManagement.confirm'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('dictManagement.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('dictManagement.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ids = this.selectedDictTypes.map(item => item.id)
|
const ids = this.selectedDictTypes.map(item => item.id)
|
||||||
dictApi.deleteDictType(ids, ({ data }) => {
|
dictApi.deleteDictType(ids, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('删除成功')
|
this.$message.success(this.$t('dictManagement.deleteSuccess'))
|
||||||
this.loadDictTypeList()
|
this.loadDictTypeList()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -273,8 +273,8 @@ export default {
|
|||||||
}))
|
}))
|
||||||
this.total = data.data.total
|
this.total = data.data.total
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取字典数据失败')
|
this.$message.error(data.msg || this.$t('dictManagement.getDictDataFailed'))
|
||||||
}
|
}
|
||||||
this.dictDataLoading = false
|
this.dictDataLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -286,10 +286,10 @@ export default {
|
|||||||
},
|
},
|
||||||
showAddDictDataDialog() {
|
showAddDictDataDialog() {
|
||||||
if (!this.selectedDictType) {
|
if (!this.selectedDictType) {
|
||||||
this.$message.warning('请先选择字典类型')
|
this.$message.warning(this.$t('dictManagement.selectDictTypeFirst'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.dictDataDialogTitle = '新增字典数据'
|
this.dictDataDialogTitle = this.$t('dictManagement.addDictData')
|
||||||
this.dictDataForm = {
|
this.dictDataForm = {
|
||||||
id: null,
|
id: null,
|
||||||
dictTypeId: this.selectedDictType.id,
|
dictTypeId: this.selectedDictType.id,
|
||||||
@@ -300,7 +300,7 @@ export default {
|
|||||||
this.dictDataDialogVisible = true
|
this.dictDataDialogVisible = true
|
||||||
},
|
},
|
||||||
editDictData(row) {
|
editDictData(row) {
|
||||||
this.dictDataDialogTitle = '编辑字典数据'
|
this.dictDataDialogTitle = this.$t('dictManagement.editDictData')
|
||||||
this.dictDataForm = { ...row }
|
this.dictDataForm = { ...row }
|
||||||
this.dictDataDialogVisible = true
|
this.dictDataDialogVisible = true
|
||||||
},
|
},
|
||||||
@@ -308,21 +308,21 @@ export default {
|
|||||||
const api = formData.id ? dictApi.updateDictData : dictApi.addDictData
|
const api = formData.id ? dictApi.updateDictData : dictApi.addDictData
|
||||||
api(formData, ({ data }) => {
|
api(formData, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('保存成功')
|
this.$message.success(this.$t('dictManagement.saveSuccess'))
|
||||||
this.dictDataDialogVisible = false
|
this.dictDataDialogVisible = false
|
||||||
this.loadDictDataList(formData.dictTypeId)
|
this.loadDictDataList(formData.dictTypeId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteDictData(row) {
|
deleteDictData(row) {
|
||||||
this.$confirm('确定要删除该字典数据吗?', '提示', {
|
this.$confirm(this.$t('dictManagement.confirmDeleteDictData'), this.$t('dictManagement.confirm'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('dictManagement.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('dictManagement.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
dictApi.deleteDictData([row.id], ({ data }) => {
|
dictApi.deleteDictData([row.id], ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('删除成功')
|
this.$message.success(this.$t('dictManagement.deleteSuccess'))
|
||||||
this.loadDictDataList(row.dictTypeId)
|
this.loadDictDataList(row.dictTypeId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -331,19 +331,19 @@ export default {
|
|||||||
batchDeleteDictData() {
|
batchDeleteDictData() {
|
||||||
const selectedRows = this.dictDataList.filter(row => row.selected)
|
const selectedRows = this.dictDataList.filter(row => row.selected)
|
||||||
if (selectedRows.length === 0) {
|
if (selectedRows.length === 0) {
|
||||||
this.$message.warning('请选择要删除的字典数据')
|
this.$message.warning(this.$t('dictManagement.selectDictDataToDelete'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$confirm(`确定要删除选中的${selectedRows.length}个字典数据吗?`, '提示', {
|
this.$confirm(this.$t('dictManagement.confirmBatchDeleteDictData', { count: selectedRows.length }), this.$t('dictManagement.confirm'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('dictManagement.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('dictManagement.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ids = selectedRows.map(item => item.id)
|
const ids = selectedRows.map(item => item.id)
|
||||||
dictApi.deleteDictData(ids, ({ data }) => {
|
dictApi.deleteDictData(ids, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('删除成功')
|
this.$message.success(this.$t('dictManagement.deleteSuccess'))
|
||||||
this.loadDictDataList(this.selectedDictType.id)
|
this.loadDictDataList(this.selectedDictType.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -363,6 +363,14 @@ export default {
|
|||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.loadDictDataList(this.selectedDictType?.id);
|
this.loadDictDataList(this.selectedDictType?.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 更新选择列表头翻译文本
|
||||||
|
updateSelectionHeaderText() {
|
||||||
|
const thElement = document.querySelector(`.el-table__header th:nth-child(1) .cell`);
|
||||||
|
if (thElement) {
|
||||||
|
thElement.setAttribute('data-content', this.$t('modelConfig.select'));
|
||||||
|
}
|
||||||
|
},
|
||||||
goFirst() {
|
goFirst() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.loadDictDataList(this.selectedDictType?.id);
|
this.loadDictDataList(this.selectedDictType?.id);
|
||||||
@@ -382,8 +390,38 @@ export default {
|
|||||||
goToPage(page) {
|
goToPage(page) {
|
||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
this.loadDictDataList(this.selectedDictType?.id);
|
this.loadDictDataList(this.selectedDictType?.id);
|
||||||
|
},
|
||||||
|
// 表头单元格样式类名,用于选择列
|
||||||
|
headerCellClassName({ columnIndex }) {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
return 'custom-selection-header';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
// 单元格样式类名,用于设置选择列表头的翻译文本
|
||||||
|
selectionCellClassName({ row, column, rowIndex, columnIndex }) {
|
||||||
|
// 只对表头行设置data-content
|
||||||
|
if (rowIndex === undefined) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateSelectionHeaderText();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// 在组件挂载后确保表头翻译文本正确显示
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateSelectionHeaderText();
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
// 在组件更新后重新设置表头翻译文本
|
||||||
|
this.updateSelectionHeaderText();
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.total / this.pageSize);
|
return Math.ceil(this.total / this.pageSize);
|
||||||
@@ -823,6 +861,29 @@ export default {
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table .custom-selection-header .cell::before {
|
||||||
|
content: attr(data-content);
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 32px;
|
||||||
|
color: black;
|
||||||
|
margin-top: 0;
|
||||||
|
height: 32px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-selection-header .cell {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-table thead) {
|
:deep(.el-table thead) {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">{{ modelTypeText }}</h2>
|
<h2 class="page-title">{{ $t('modelConfig.' + activeTab) }}</h2>
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<div class="search-group">
|
<div class="search-group">
|
||||||
<el-input placeholder="请输入模型名称查询" v-model="search" class="search-input" clearable
|
<el-input :placeholder="$t('modelConfig.searchPlaceholder')" v-model="search" class="search-input" clearable
|
||||||
@keyup.enter.native="handleSearch" style="width: 240px" />
|
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||||
<el-button class="btn-search" @click="handleSearch">
|
<el-button class="btn-search" @click="handleSearch">
|
||||||
搜索
|
{{ $t('modelConfig.search') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,73 +22,74 @@
|
|||||||
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
|
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
|
||||||
style="background-size: cover; background-position: center;">
|
style="background-size: cover; background-position: center;">
|
||||||
<el-menu-item index="vad">
|
<el-menu-item index="vad">
|
||||||
<span class="menu-text">语言活动检测</span>
|
<span class="menu-text">{{ $t('modelConfig.vad') }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="asr">
|
<el-menu-item index="asr">
|
||||||
<span class="menu-text">语音识别</span>
|
<span class="menu-text">{{ $t('modelConfig.asr') }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="llm">
|
<el-menu-item index="llm">
|
||||||
<span class="menu-text">大语言模型</span>
|
<span class="menu-text">{{ $t('modelConfig.llm') }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="vllm">
|
<el-menu-item index="vllm">
|
||||||
<span class="menu-text">视觉大模型</span>
|
<span class="menu-text">{{ $t('modelConfig.vllm') }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="intent">
|
<el-menu-item index="intent">
|
||||||
<span class="menu-text">意图识别</span>
|
<span class="menu-text">{{ $t('modelConfig.intent') }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="tts">
|
<el-menu-item index="tts">
|
||||||
<span class="menu-text">语音合成</span>
|
<span class="menu-text">{{ $t('modelConfig.tts') }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="memory">
|
<el-menu-item index="memory">
|
||||||
<span class="menu-text">记忆</span>
|
<span class="menu-text">{{ $t('modelConfig.memory') }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
|
||||||
<!-- 右侧内容 -->
|
<!-- 右侧内容 -->
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="model-card" shadow="never">
|
<el-card class="model-card" shadow="never">
|
||||||
<el-table ref="modelTable" style="width: 100%" v-loading="loading" element-loading-text="拼命加载中"
|
<el-table ref="modelTable" style="width: 100%" v-loading="loading"
|
||||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)"
|
:element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
|
||||||
:header-cell-style="{ background: 'transparent' }" :data="modelList" class="data-table"
|
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-style="{ background: 'transparent' }"
|
||||||
header-row-class-name="table-header" :header-cell-class-name="headerCellClassName"
|
:data="modelList" class="data-table" header-row-class-name="table-header"
|
||||||
@selection-change="handleSelectionChange">
|
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
||||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
<el-table-column type="selection" width="55" align="center"
|
||||||
<el-table-column label="模型ID" prop="id" align="center"></el-table-column>
|
:cell-class-name="selectionCellClassName"></el-table-column>
|
||||||
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
<el-table-column :label="$t('modelConfig.modelId')" prop="id" align="center"></el-table-column>
|
||||||
<el-table-column label="提供商" align="center">
|
<el-table-column :label="$t('modelConfig.modelName')" prop="modelName" align="center"></el-table-column>
|
||||||
|
<el-table-column :label="$t('modelConfig.provider')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ scope.row.configJson.type || '未知' }}
|
{{ scope.row.configJson.type || $t('modelConfig.unknown') }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="是否启用" align="center">
|
<el-table-column :label="$t('modelConfig.isEnabled')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||||
@change="handleStatusChange(scope.row)" />
|
@change="handleStatusChange(scope.row)" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="是否默认" align="center">
|
<el-table-column :label="$t('modelConfig.isDefault')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||||
@change="handleDefaultChange(scope.row)" />
|
@change="handleDefaultChange(scope.row)" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
|
<el-table-column v-if="activeTab === 'tts'" :label="$t('modelConfig.voiceManagement')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
|
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
|
||||||
音色管理
|
{{ $t('modelConfig.voiceManagement') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center" width="180px">
|
<el-table-column :label="$t('modelConfig.action')" align="center" width="210px">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
||||||
修改
|
{{ $t('modelConfig.edit') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="text" size="mini" @click="duplicateModel(scope.row)" class="edit-btn">
|
<el-button type="text" size="mini" @click="duplicateModel(scope.row)" class="edit-btn">
|
||||||
创建副本
|
{{ $t('modelConfig.duplicate') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
||||||
删除
|
{{ $t('modelConfig.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -97,32 +98,39 @@
|
|||||||
<div class="batch-actions">
|
<div class="batch-actions">
|
||||||
<el-button size="mini" type="primary" @click="selectAll">
|
<el-button size="mini" type="primary" @click="selectAll">
|
||||||
{{ isAllSelected ?
|
{{ isAllSelected ?
|
||||||
'取消全选' : '全选' }}
|
$t('modelConfig.deselectAll') : $t('modelConfig.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
||||||
新增
|
{{ $t('modelConfig.add') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||||
删除
|
{{ $t('modelConfig.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
|
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||||
|
:label="$t('modelConfig.itemsPerPage', { items: item })" :value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">{{
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
$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"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">{{
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
$t('modelConfig.nextPage')
|
||||||
|
}}</button>
|
||||||
|
<span class="total-text">{{ $t('modelConfig.totalRecords', { total }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -174,18 +182,21 @@ export default {
|
|||||||
this.loadData();
|
this.loadData();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// 在组件挂载后确保表头翻译文本正确显示
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateSelectionHeaderText();
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
// 在组件更新后重新设置表头翻译文本
|
||||||
|
this.updateSelectionHeaderText();
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
modelTypeText() {
|
modelTypeText() {
|
||||||
const map = {
|
return this.$t('modelConfig.' + this.activeTab) || this.$t('modelConfig.modelConfig')
|
||||||
vad: '语言活动检测模型(VAD)',
|
|
||||||
asr: '语音识别模型(ASR)',
|
|
||||||
llm: '大语言模型(LLM)',
|
|
||||||
vllm: '视觉大模型(VLLM)',
|
|
||||||
intent: '意图识别模型(Intent)',
|
|
||||||
tts: '语音合成模型(TTS)',
|
|
||||||
memory: '记忆模型(Memory)'
|
|
||||||
}
|
|
||||||
return map[this.activeTab] || '模型配置'
|
|
||||||
},
|
},
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.total / this.pageSize);
|
return Math.ceil(this.total / this.pageSize);
|
||||||
@@ -208,6 +219,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
// 更新选择列表头翻译文本
|
||||||
|
updateSelectionHeaderText() {
|
||||||
|
const thElement = document.querySelector(`.el-table__header th:nth-child(1) .cell`);
|
||||||
|
if (thElement) {
|
||||||
|
thElement.setAttribute('data-content', this.$t('modelConfig.select'));
|
||||||
|
}
|
||||||
|
},
|
||||||
handlePageSizeChange(val) {
|
handlePageSizeChange(val) {
|
||||||
this.pageSize = val;
|
this.pageSize = val;
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
@@ -224,6 +242,19 @@ export default {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
selectionCellClassName({ row, column, rowIndex, columnIndex }) {
|
||||||
|
// 只对表头行设置data-content
|
||||||
|
if (rowIndex === undefined) {
|
||||||
|
// 使用setTimeout确保DOM已经渲染完成
|
||||||
|
setTimeout(() => {
|
||||||
|
const thElement = document.querySelector(`.el-table__header th:nth-child(1) .cell`);
|
||||||
|
if (thElement) {
|
||||||
|
thElement.setAttribute('data-content', this.$t('modelConfig.select'));
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
handleMenuSelect(index) {
|
handleMenuSelect(index) {
|
||||||
this.activeTab = index;
|
this.activeTab = index;
|
||||||
this.currentPage = 1; // 重置到第一页
|
this.currentPage = 1; // 重置到第一页
|
||||||
@@ -237,13 +268,13 @@ export default {
|
|||||||
// 批量删除
|
// 批量删除
|
||||||
batchDelete() {
|
batchDelete() {
|
||||||
if (this.selectedModels.length === 0) {
|
if (this.selectedModels.length === 0) {
|
||||||
this.$message.warning('请先选择要删除的模型')
|
this.$message.warning(this.$t('modelConfig.selectModelsFirst'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$confirm('确定要删除选中的模型吗?', '提示', {
|
this.$confirm(this.$t('modelConfig.confirmBatchDelete'), this.$t('message.info'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const deletePromises = this.selectedModels.map(model =>
|
const deletePromises = this.selectedModels.map(model =>
|
||||||
@@ -258,19 +289,19 @@ export default {
|
|||||||
Promise.all(deletePromises).then(results => {
|
Promise.all(deletePromises).then(results => {
|
||||||
if (results.every(Boolean)) {
|
if (results.every(Boolean)) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '批量删除成功',
|
message: this.$t('modelConfig.batchDeleteSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
this.loadData()
|
this.loadData()
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '部分删除失败',
|
message: this.$t('modelConfig.partialDeleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message.info('已取消删除')
|
this.$message.info(this.$t('modelConfig.deleteCancelled'))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addModel() {
|
addModel() {
|
||||||
@@ -287,9 +318,9 @@ export default {
|
|||||||
},
|
},
|
||||||
// 删除单个模型
|
// 删除单个模型
|
||||||
deleteModel(model) {
|
deleteModel(model) {
|
||||||
this.$confirm('确定要删除该模型吗?', '提示', {
|
this.$confirm(this.$t('modelConfig.confirmDelete'), this.$t('message.info'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Api.model.deleteModel(
|
Api.model.deleteModel(
|
||||||
@@ -297,20 +328,20 @@ export default {
|
|||||||
({ data }) => {
|
({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '删除成功',
|
message: this.$t('modelConfig.deleteSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
this.loadData()
|
this.loadData()
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '删除失败',
|
message: data.msg || this.$t('modelConfig.deleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message.info('已取消删除')
|
this.$message.info(this.$t('modelConfig.deleteCancelled'))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleCurrentChange(page) {
|
handleCurrentChange(page) {
|
||||||
@@ -322,28 +353,28 @@ export default {
|
|||||||
const id = formData.id;
|
const id = formData.id;
|
||||||
|
|
||||||
if (this.editModelData.duplicateMode) {
|
if (this.editModelData.duplicateMode) {
|
||||||
Api.model.addModel({modelType, provideCode, formData},
|
Api.model.addModel({ modelType, provideCode, formData },
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('创建副本成功');
|
this.$message.success(this.$t('modelConfig.duplicateSuccess'));
|
||||||
this.loadData();
|
this.loadData();
|
||||||
this.editDialogVisible = false;
|
this.editDialogVisible = false;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '创建副本失败');
|
this.$message.error(data.msg || this.$t('modelConfig.duplicateFailed'));
|
||||||
}
|
}
|
||||||
done && done(); // 调用done回调关闭加载状态
|
done && done(); // 调用done回调关闭加载状态
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Api.model.updateModel(
|
Api.model.updateModel(
|
||||||
{ modelType, provideCode, id, formData },
|
{ modelType, provideCode, id, formData },
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('保存成功');
|
this.$message.success(this.$t('modelConfig.saveSuccess'));
|
||||||
this.loadData();
|
this.loadData();
|
||||||
this.editDialogVisible = false;
|
this.editDialogVisible = false;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '保存失败');
|
this.$message.error(data.msg || this.$t('modelConfig.saveFailed'));
|
||||||
}
|
}
|
||||||
done && done(); // 调用done回调关闭加载状态
|
done && done(); // 调用done回调关闭加载状态
|
||||||
}
|
}
|
||||||
@@ -381,13 +412,13 @@ export default {
|
|||||||
Api.model.addModel(params, ({ data }) => {
|
Api.model.addModel(params, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '新增成功',
|
message: this.$t('modelConfig.addSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.loadData();
|
this.loadData();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '新增失败',
|
message: data.msg || this.$t('modelConfig.addFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -432,7 +463,7 @@ export default {
|
|||||||
this.modelList = data.data.list;
|
this.modelList = data.data.list;
|
||||||
this.total = data.data.total;
|
this.total = data.data.total;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取模型列表失败');
|
this.$message.error(data.msg || this.$t('modelConfig.fetchModelsFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -448,13 +479,13 @@ export default {
|
|||||||
newStatus,
|
newStatus,
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success(newStatus === 1 ? '启用成功' : '禁用成功')
|
this.$message.success(newStatus === 1 ? this.$t('modelConfig.enableSuccess') : this.$t('modelConfig.disableSuccess'))
|
||||||
// 保持新状态
|
// 保持新状态
|
||||||
model.isEnabled = newStatus
|
model.isEnabled = newStatus
|
||||||
} else {
|
} else {
|
||||||
// 操作失败时恢复原状态
|
// 操作失败时恢复原状态
|
||||||
model.isEnabled = originalStatus
|
model.isEnabled = originalStatus
|
||||||
this.$message.error(data.msg || '操作失败')
|
this.$message.error(data.msg || this.$t('modelConfig.operationFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -462,7 +493,7 @@ export default {
|
|||||||
handleDefaultChange(model) {
|
handleDefaultChange(model) {
|
||||||
Api.model.setDefaultModel(model.id, ({ data }) => {
|
Api.model.setDefaultModel(model.id, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success('设置默认模型成功')
|
this.$message.success(this.$t('modelConfig.setDefaultSuccess'))
|
||||||
this.loadData()
|
this.loadData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -764,14 +795,28 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table .custom-selection-header .cell::before {
|
::v-deep .el-table .custom-selection-header .cell::before {
|
||||||
content: '选择';
|
content: attr(data-content);
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 0;
|
line-height: 32px;
|
||||||
|
/* 设置合适的行高,确保文本完整显示 */
|
||||||
color: black;
|
color: black;
|
||||||
margin-top: 23px;
|
margin-top: 0;
|
||||||
|
/* 移除可能导致偏移的上边距 */
|
||||||
|
height: 32px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-selection-header .cell {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已移除可能影响文本显示的空伪元素 */
|
||||||
|
|
||||||
::v-deep .el-table__body .el-checkbox__inner {
|
::v-deep .el-table__body .el-checkbox__inner {
|
||||||
display: inline-block !important;
|
display: inline-block !important;
|
||||||
background: #e6edfa;
|
background: #e6edfa;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">固件管理</h2>
|
<h2 class="page-title">{{ $t('otaManagement.firmwareManagement') }}</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-input placeholder="请输入固件名称查询" v-model="searchName" class="search-input"
|
<el-input :placeholder="$t('otaManagement.searchPlaceholder')" v-model="searchName" class="search-input"
|
||||||
@keyup.enter.native="handleSearch" clearable />
|
@keyup.enter.native="handleSearch" clearable />
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
<el-button class="btn-search" @click="handleSearch">{{ $t('otaManagement.search') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -16,44 +16,44 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="params-card" shadow="never">
|
<el-card class="params-card" shadow="never">
|
||||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||||
:header-cell-class-name="headerCellClassName">
|
:header-cell-class-name="headerCellClassName">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="固件名称" prop="firmwareName" align="center"></el-table-column>
|
<el-table-column :label="$t('otaManagement.firmwareName')" prop="firmwareName" align="center"></el-table-column>
|
||||||
<el-table-column label="固件类型" prop="type" align="center">
|
<el-table-column :label="$t('otaManagement.firmwareType')" prop="type" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ getFirmwareTypeName(scope.row.type) }}
|
{{ getFirmwareTypeName(scope.row.type) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="版本号" prop="version" align="center"></el-table-column>
|
<el-table-column :label="$t('otaManagement.version')" prop="version" align="center"></el-table-column>
|
||||||
<el-table-column label="文件大小" prop="size" align="center">
|
<el-table-column :label="$t('otaManagement.fileSize')" prop="size" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ formatFileSize(scope.row.size) }}
|
{{ formatFileSize(scope.row.size) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="备注" prop="remark" align="center"
|
<el-table-column :label="$t('otaManagement.remark')" prop="remark" align="center"
|
||||||
show-overflow-tooltip></el-table-column>
|
show-overflow-tooltip></el-table-column>
|
||||||
<el-table-column label="创建时间" prop="createDate" align="center">
|
<el-table-column :label="$t('otaManagement.createTime')" prop="createDate" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ formatDate(scope.row.createDate) }}
|
{{ formatDate(scope.row.createDate) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="更新时间" prop="updateDate" align="center">
|
<el-table-column :label="$t('otaManagement.updateTime')" prop="updateDate" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ formatDate(scope.row.updateDate) }}
|
{{ formatDate(scope.row.updateDate) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center">
|
<el-table-column :label="$t('otaManagement.action')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text"
|
<el-button size="mini" type="text"
|
||||||
@click="downloadFirmware(scope.row)">下载</el-button>
|
@click="downloadFirmware(scope.row)">{{ $t('otaManagement.download') }}</el-button>
|
||||||
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</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)">删除</el-button>
|
<el-button size="mini" type="text" @click="deleteParam(scope.row)">{{ $t('otaManagement.delete') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -61,33 +61,33 @@
|
|||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
{{ isAllSelected ? $t('otaManagement.deselectAll') : $t('otaManagement.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" @click="showAddDialog"
|
<el-button size="mini" type="success" @click="showAddDialog"
|
||||||
style="background: #5bc98c;border: None;">新增</el-button>
|
style="background: #5bc98c;border: None;">{{ $t('otaManagement.addNew') }}</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||||
@click="deleteSelectedParams">删除</el-button>
|
@click="deleteSelectedParams">{{ $t('otaManagement.delete') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('otaManagement.itemsPerPage', { items: item })"
|
||||||
:value="item">
|
:value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
{{ $t('otaManagement.firstPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
上一页
|
{{ $t('otaManagement.prevPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
下一页
|
{{ $t('otaManagement.nextPage') }}
|
||||||
</button>
|
</button>
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
<span class="total-text">{{ $t('otaManagement.totalRecords', { total }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
<!-- 新增/编辑固件对话框 -->
|
<!-- 新增/编辑固件对话框 -->
|
||||||
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm"
|
<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>
|
<el-footer>
|
||||||
<version-footer />
|
<version-footer />
|
||||||
</el-footer>
|
</el-footer>
|
||||||
@@ -110,6 +110,7 @@ import FirmwareDialog from "@/components/FirmwareDialog.vue";
|
|||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import VersionFooter from "@/components/VersionFooter.vue";
|
import VersionFooter from "@/components/VersionFooter.vue";
|
||||||
import { formatDate, formatFileSize } from "@/utils/format";
|
import { formatDate, formatFileSize } from "@/utils/format";
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar, FirmwareDialog, VersionFooter },
|
components: { HeaderBar, FirmwareDialog, VersionFooter },
|
||||||
@@ -193,7 +194,7 @@ export default {
|
|||||||
this.paramsList = [];
|
this.paramsList = [];
|
||||||
this.total = 0;
|
this.total = 0;
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: res?.data?.msg || '获取固件列表失败',
|
message: res?.data?.msg || this.$t('otaManagement.getFirmwareListFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -210,7 +211,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.dialogTitle = "新增固件";
|
this.dialogTitle = this.$t('otaManagement.addFirmware');
|
||||||
// 完全重置表单数据
|
// 完全重置表单数据
|
||||||
this.firmwareForm = {
|
this.firmwareForm = {
|
||||||
id: null,
|
id: null,
|
||||||
@@ -230,7 +231,7 @@ export default {
|
|||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
editParam(row) {
|
editParam(row) {
|
||||||
this.dialogTitle = "编辑固件";
|
this.dialogTitle = this.$t('otaManagement.editFirmware');
|
||||||
this.firmwareForm = { ...row };
|
this.firmwareForm = { ...row };
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
@@ -241,14 +242,14 @@ export default {
|
|||||||
res = res.data;
|
res = res.data;
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: "修改成功",
|
message: this.$t('otaManagement.updateSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.fetchFirmwareList();
|
this.fetchFirmwareList();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: res.msg || "修改失败",
|
message: res.msg || this.$t('otaManagement.updateFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -259,14 +260,14 @@ export default {
|
|||||||
res = res.data;
|
res = res.data;
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: "新增成功",
|
message: this.$t('otaManagement.addSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.fetchFirmwareList();
|
this.fetchFirmwareList();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: res.msg || "新增失败",
|
message: res.msg || this.$t('otaManagement.addFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -278,7 +279,7 @@ export default {
|
|||||||
const selectedRows = this.firmwareList.filter(row => row.selected);
|
const selectedRows = this.firmwareList.filter(row => row.selected);
|
||||||
if (selectedRows.length === 0) {
|
if (selectedRows.length === 0) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: "请先选择需要删除的固件",
|
message: this.$t('otaManagement.selectFirmwareFirst'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -291,23 +292,23 @@ export default {
|
|||||||
|
|
||||||
if (Array.isArray(row) && row.length === 0) {
|
if (Array.isArray(row) && row.length === 0) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: "请先选择需要删除的参数",
|
message: this.$t('otaManagement.selectFirmwareFirst'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paramCount = params.length;
|
const paramCount = params.length;
|
||||||
this.$confirm(`确定要删除选中的${paramCount}个固件吗?`, '警告', {
|
this.$confirm(this.$t('otaManagement.confirmBatchDelete', { paramCount }), 'Warning', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: 'OK',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: 'Cancel',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
distinguishCancelAndClose: true
|
distinguishCancelAndClose: true
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ids = params.map(param => param.id);
|
const ids = params.map(param => param.id);
|
||||||
if (ids.some(id => !id)) {
|
if (ids.some(id => !id)) {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '存在无效的参数ID',
|
message: this.$t('otaManagement.invalidFirmwareId'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -317,13 +318,13 @@ export default {
|
|||||||
res = res.data;
|
res = res.data;
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功删除${paramCount}个固件`,
|
message: this.$t('otaManagement.batchDeleteSuccess', { paramCount }),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchFirmwareList();
|
this.fetchFirmwareList();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: res.msg || '删除失败,请重试',
|
message: res.msg || this.$t('otaManagement.deleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -332,13 +333,13 @@ export default {
|
|||||||
if (action === 'cancel') {
|
if (action === 'cancel') {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: '已取消删除操作',
|
message: this.$t('otaManagement.operationCancelled'),
|
||||||
duration: 1000
|
duration: 1000
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: '操作已关闭',
|
message: this.$t('otaManagement.operationClosed'),
|
||||||
duration: 1000
|
duration: 1000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -372,7 +373,7 @@ export default {
|
|||||||
},
|
},
|
||||||
downloadFirmware(firmware) {
|
downloadFirmware(firmware) {
|
||||||
if (!firmware || !firmware.id) {
|
if (!firmware || !firmware.id) {
|
||||||
this.$message.error('固件信息不完整');
|
this.$message.error(this.$t('otaManagement.incompleteFirmwareInfo'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 先获取下载链接
|
// 先获取下载链接
|
||||||
@@ -382,7 +383,7 @@ export default {
|
|||||||
const baseUrl = process.env.VUE_APP_API_BASE_URL || '';
|
const baseUrl = process.env.VUE_APP_API_BASE_URL || '';
|
||||||
window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`);
|
window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`);
|
||||||
} else {
|
} else {
|
||||||
this.$message.error('获取下载链接失败');
|
this.$message.error(this.$t('otaManagement.getDownloadUrlFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -394,7 +395,7 @@ export default {
|
|||||||
this.firmwareTypes = res.data
|
this.firmwareTypes = res.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取固件类型失败:', error)
|
console.error('获取固件类型失败:', error)
|
||||||
this.$message.error(error.message || '获取固件类型失败')
|
this.$message.error(error.message || this.$t('otaManagement.getFirmwareTypesFailed'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getFirmwareTypeName(type) {
|
getFirmwareTypeName(type) {
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">参数管理</h2>
|
<h2 class="page-title">{{ $t('paramManagement.pageTitle') }}</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-input placeholder="请输入参数编码或备注查询" v-model="searchCode" class="search-input"
|
<el-input :placeholder="$t('paramManagement.searchPlaceholder')" v-model="searchCode"
|
||||||
@keyup.enter.native="handleSearch" clearable />
|
class="search-input" @keyup.enter.native="handleSearch" clearable />
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
<el-button class="btn-search" @click="handleSearch">{{ $t('paramManagement.search') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -16,33 +16,39 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="params-card" shadow="never">
|
<el-card class="params-card" shadow="never">
|
||||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||||
:header-cell-class-name="headerCellClassName">
|
:header-cell-class-name="headerCellClassName">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
<el-table-column :label="$t('paramManagement.paramCode')" prop="paramCode"
|
||||||
<el-table-column label="参数值" prop="paramValue" align="center" show-overflow-tooltip>
|
align="center"></el-table-column>
|
||||||
|
<el-table-column :label="$t('paramManagement.paramValue')" prop="paramValue" align="center"
|
||||||
|
show-overflow-tooltip>
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<div v-if="isSensitiveParam(scope.row.paramCode)">
|
<div v-if="isSensitiveParam(scope.row.paramCode)">
|
||||||
<span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue)
|
<span v-if="!scope.row.showValue">{{ maskSensitiveValue(scope.row.paramValue)
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-else>{{ scope.row.paramValue }}</span>
|
<span v-else>{{ scope.row.paramValue }}</span>
|
||||||
<el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)">
|
<el-button size="mini" type="text" @click="toggleSensitiveValue(scope.row)">
|
||||||
{{ scope.row.showValue ? '隐藏' : '查看' }}
|
{{ scope.row.showValue ? $t('paramManagement.hide') :
|
||||||
|
$t('paramManagement.view') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<span v-else>{{ scope.row.paramValue }}</span>
|
<span v-else>{{ scope.row.paramValue }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
<el-table-column :label="$t('paramManagement.remark')" prop="remark"
|
||||||
<el-table-column label="操作" align="center">
|
align="center"></el-table-column>
|
||||||
|
<el-table-column :label="$t('paramManagement.operation')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</el-button>
|
<el-button size="mini" type="text" @click="editParam(scope.row)">{{
|
||||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">删除</el-button>
|
$t('paramManagement.edit') }}</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="deleteParam(scope.row)">{{
|
||||||
|
$t('paramManagement.delete') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -50,32 +56,35 @@
|
|||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
{{ isAllSelected ? $t('paramManagement.deselectAll') :
|
||||||
|
$t('paramManagement.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
<el-button size="mini" type="success" @click="showAddDialog">{{
|
||||||
|
$t('paramManagement.add') }}</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||||
@click="deleteSelectedParams">删除</el-button>
|
@click="deleteSelectedParams">{{
|
||||||
|
$t('paramManagement.delete') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||||
:value="item">
|
:label="`${item}${$t('paramManagement.itemsPerPage')}`" :value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
{{ $t('paramManagement.firstPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
上一页
|
{{ $t('paramManagement.prevPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
下一页
|
{{ $t('paramManagement.nextPage') }}
|
||||||
</button>
|
</button>
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
<span class="total-text">{{ $t('paramManagement.totalRecords', { total }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -84,8 +93,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增/编辑参数对话框 -->
|
<!-- 新增/编辑参数对话框 -->
|
||||||
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
<param-dialog ref="paramDialog" :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm"
|
||||||
@cancel="dialogVisible = false" />
|
@submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<version-footer />
|
<version-footer />
|
||||||
</el-footer>
|
</el-footer>
|
||||||
@@ -116,6 +125,7 @@ export default {
|
|||||||
id: null,
|
id: null,
|
||||||
paramCode: "",
|
paramCode: "",
|
||||||
paramValue: "",
|
paramValue: "",
|
||||||
|
valueType: "string",
|
||||||
remark: ""
|
remark: ""
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -164,13 +174,14 @@ export default {
|
|||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.paramsList = data.data.list.map(item => ({
|
this.paramsList = data.data.list.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
|
valueType: item.valueType || "string",
|
||||||
selected: false,
|
selected: false,
|
||||||
showValue: false
|
showValue: false
|
||||||
}));
|
}));
|
||||||
this.total = data.data.total;
|
this.total = data.data.total;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '获取参数列表失败',
|
message: data.msg || this.$t('paramManagement.getParamsListFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -188,129 +199,139 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.dialogTitle = "新增参数";
|
this.dialogTitle = this.$t('paramManagement.addParam');
|
||||||
this.paramForm = {
|
this.paramForm = {
|
||||||
id: null,
|
id: null,
|
||||||
paramCode: "",
|
paramCode: "",
|
||||||
paramValue: "",
|
paramValue: "",
|
||||||
|
valueType: "string", // 默认值
|
||||||
remark: ""
|
remark: ""
|
||||||
};
|
};
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
editParam(row) {
|
editParam(row) {
|
||||||
this.dialogTitle = "编辑参数";
|
this.dialogTitle = this.$t('paramManagement.editParam');
|
||||||
this.paramForm = { ...row };
|
this.paramForm = {
|
||||||
|
id: row.id,
|
||||||
|
paramCode: row.paramCode,
|
||||||
|
paramValue: row.paramValue,
|
||||||
|
valueType: row.valueType || "string", // 确保有值
|
||||||
|
remark: row.remark
|
||||||
|
};
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
|
handleSubmit(form) {
|
||||||
handleSubmit({ form, done }) {
|
|
||||||
if (form.id) {
|
if (form.id) {
|
||||||
// 编辑
|
// 更新参数
|
||||||
Api.admin.updateParam(form, ({ data }) => {
|
Api.admin.updateParam(form, ({ data }) => {
|
||||||
if (data.code === 0) {
|
this.dialogVisible = false;
|
||||||
this.$message.success({
|
this.fetchParams();
|
||||||
message: "修改成功",
|
this.$message.success({
|
||||||
showClose: true
|
message: this.$t('paramManagement.updateSuccess'),
|
||||||
});
|
|
||||||
this.dialogVisible = false;
|
|
||||||
this.fetchParams();
|
|
||||||
}
|
|
||||||
done && done();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 新增
|
|
||||||
Api.admin.addParam(form, ({ data }) => {
|
|
||||||
if (data.code === 0) {
|
|
||||||
this.$message.success({
|
|
||||||
message: "新增成功",
|
|
||||||
showClose: true
|
|
||||||
});
|
|
||||||
this.dialogVisible = false;
|
|
||||||
this.fetchParams();
|
|
||||||
}
|
|
||||||
done && done();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteSelectedParams() {
|
|
||||||
const selectedRows = this.paramsList.filter(row => row.selected);
|
|
||||||
if (selectedRows.length === 0) {
|
|
||||||
this.$message.warning({
|
|
||||||
message: "请先选择需要删除的参数",
|
|
||||||
showClose: true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.deleteParam(selectedRows);
|
|
||||||
},
|
|
||||||
deleteParam(row) {
|
|
||||||
// 处理单个参数或参数数组
|
|
||||||
const params = Array.isArray(row) ? row : [row];
|
|
||||||
|
|
||||||
if (Array.isArray(row) && row.length === 0) {
|
|
||||||
this.$message.warning({
|
|
||||||
message: "请先选择需要删除的参数",
|
|
||||||
showClose: true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const paramCount = params.length;
|
|
||||||
this.$confirm(`确定要删除选中的${paramCount}个参数吗?`, '警告', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
distinguishCancelAndClose: true
|
|
||||||
}).then(() => {
|
|
||||||
const ids = params.map(param => param.id);
|
|
||||||
if (ids.some(id => isNaN(id))) {
|
|
||||||
this.$message.error({
|
|
||||||
message: '存在无效的参数ID',
|
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
}, ({ data }) => {
|
||||||
}
|
this.$message.error({
|
||||||
|
message: data.msg || this.$t('paramManagement.updateFailed'),
|
||||||
Api.admin.deleteParam(ids, ({ data }) => {
|
showClose: true
|
||||||
|
});
|
||||||
|
// 调用ParamDialog的resetSaving方法重置保存状态
|
||||||
|
if (this.$refs.paramDialog && typeof this.$refs.paramDialog.resetSaving === 'function') {
|
||||||
|
this.$refs.paramDialog.resetSaving();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 新增参数
|
||||||
|
Api.admin.addParam(form, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.fetchParams();
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功删除${paramCount}个参数`,
|
message: this.$t('paramManagement.addSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchParams();
|
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '删除失败,请重试',
|
message: data.msg || this.$t('paramManagement.addFailed'),
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
// 调用ParamDialog的resetSaving方法重置保存状态
|
||||||
|
if (this.$refs.paramDialog && typeof this.$refs.paramDialog.resetSaving === 'function') {
|
||||||
|
this.$refs.paramDialog.resetSaving();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteSelectedParams() {
|
||||||
|
const selectedParams = this.paramsList.filter(row => row.selected);
|
||||||
|
if (selectedParams.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: this.$t('paramManagement.selectParamsFirst'),
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deleteParams(selectedParams);
|
||||||
|
},
|
||||||
|
deleteParam(row) {
|
||||||
|
if (!row.id) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: this.$t('paramManagement.selectParamsFirst'),
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deleteParams([row]);
|
||||||
|
},
|
||||||
|
deleteParams(params) {
|
||||||
|
const paramCount = params.length;
|
||||||
|
const paramIds = params.map(param => param.id).filter(id => id);
|
||||||
|
if (paramIds.length === 0) {
|
||||||
|
this.$message.error({
|
||||||
|
message: this.$t('paramManagement.invalidParamId'),
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$confirm(this.$t('paramManagement.confirmBatchDelete', { paramCount }), this.$t('message.warning'), {
|
||||||
|
confirmButtonText: this.$t('button.ok'),
|
||||||
|
cancelButtonText: this.$t('button.cancel'),
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
Api.admin.deleteParam(paramIds, ({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.fetchParams();
|
||||||
|
this.$message.success({
|
||||||
|
message: this.$t('paramManagement.batchDeleteSuccess', { paramCount }),
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: data.msg || this.$t('paramManagement.deleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch(action => {
|
}).catch(() => {
|
||||||
if (action === 'cancel') {
|
this.$message({
|
||||||
this.$message({
|
type: 'info',
|
||||||
type: 'info',
|
message: this.$t('paramManagement.operationCancelled'),
|
||||||
message: '已取消删除操作',
|
duration: 1000
|
||||||
duration: 1000
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.$message({
|
|
||||||
type: 'info',
|
|
||||||
message: '操作已关闭',
|
|
||||||
duration: 1000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
headerCellClassName({ columnIndex }) {
|
goToPage(page) {
|
||||||
if (columnIndex === 0) {
|
if (page !== this.currentPage) {
|
||||||
return "custom-selection-header";
|
this.currentPage = page;
|
||||||
|
this.fetchParams();
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
},
|
},
|
||||||
goFirst() {
|
goFirst() {
|
||||||
this.currentPage = 1;
|
if (this.currentPage !== 1) {
|
||||||
this.fetchParams();
|
this.currentPage = 1;
|
||||||
|
this.fetchParams();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
goPrev() {
|
goPrev() {
|
||||||
if (this.currentPage > 1) {
|
if (this.currentPage > 1) {
|
||||||
@@ -324,22 +345,22 @@ export default {
|
|||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goToPage(page) {
|
|
||||||
this.currentPage = page;
|
|
||||||
this.fetchParams();
|
|
||||||
},
|
|
||||||
isSensitiveParam(paramCode) {
|
isSensitiveParam(paramCode) {
|
||||||
return this.sensitive_keys.some(key => paramCode.toLowerCase().includes(key.toLowerCase()));
|
return this.sensitive_keys.some(key => paramCode.toLowerCase().includes(key));
|
||||||
},
|
},
|
||||||
maskSensitiveValue(value) {
|
maskSensitiveValue(value) {
|
||||||
if (!value) return '';
|
if (value.length <= 4) {
|
||||||
if (value.length <= 8) return '****';
|
return '****';
|
||||||
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
}
|
||||||
|
return value.substring(0, 2) + '****' + value.substring(value.length - 2);
|
||||||
},
|
},
|
||||||
toggleSensitiveValue(row) {
|
toggleSensitiveValue(row) {
|
||||||
this.$set(row, 'showValue', !row.showValue);
|
row.showValue = !row.showValue;
|
||||||
},
|
},
|
||||||
},
|
headerCellClassName() {
|
||||||
|
return 'header-cell';
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,23 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">字段管理</h2>
|
<h2 class="page-title">{{ $t('header.providerManagement') }}</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
||||||
<el-button class="category-btn">
|
<el-button class="category-btn">
|
||||||
类别筛选 {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
{{ $t('providerManagement.categoryFilter') }} {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
||||||
:class="{ 'rotate-down': DropdownVisible }"></i>
|
:class="{ 'rotate-down': DropdownVisible }"></i>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
|
||||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<el-input placeholder="请输入供应器名称查询" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
|
<el-input :placeholder="$t('providerManagement.searchPlaceholder')" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
|
||||||
clearable />
|
clearable />
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
<el-button class="btn-search" @click="handleSearch">{{ $t('common.search') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,23 +30,23 @@
|
|||||||
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
|
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
|
||||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="类别" prop="modelType" align="center" width="200">
|
<el-table-column :label="$t('providerManagement.category')" prop="modelType" align="center" width="200">
|
||||||
<template slot="header" slot-scope="scope">
|
<template slot="header" slot-scope="scope">
|
||||||
<el-dropdown trigger="click" @command="handleSelectModelType"
|
<el-dropdown trigger="click" @command="handleSelectModelType"
|
||||||
@visible-change="isDropdownOpen = $event">
|
@visible-change="isDropdownOpen = $event">
|
||||||
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
|
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
|
||||||
类别{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
|
{{ $t('providerManagement.category') }}{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
|
||||||
:class="{ 'is-active': isDropdownOpen }"></i>
|
:class="{ 'is-active': isDropdownOpen }"></i>
|
||||||
</span>
|
</span>
|
||||||
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-menu slot="dropdown">
|
||||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
|
||||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
@@ -58,25 +58,25 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="供应器编码" prop="providerCode" align="center" width="150"></el-table-column>
|
<el-table-column :label="$t('providerManagement.providerCode')" prop="providerCode" align="center" width="150"></el-table-column>
|
||||||
<el-table-column label="名称" prop="name" align="center"></el-table-column>
|
<el-table-column :label="$t('common.name')" prop="name" align="center"></el-table-column>
|
||||||
<el-table-column label="字段配置" align="center">
|
<el-table-column :label="$t('providerManagement.fieldConfig')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-popover placement="top-start" width="400" trigger="hover">
|
<el-popover placement="top-start" width="400" trigger="hover">
|
||||||
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
||||||
<span class="field-label">{{ field.label }}:</span>
|
<span class="field-label">{{ field.label }}:</span>
|
||||||
<span class="field-type">{{ field.type }}</span>
|
<span class="field-type">{{ field.type }}</span>
|
||||||
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">敏感</span>
|
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">{{ $t('common.sensitive') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button slot="reference" size="mini" type="text">查看字段</el-button>
|
<el-button slot="reference" size="mini" type="text">{{ $t('providerManagement.viewFields') }}</el-button>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="排序" prop="sort" align="center" width="80"></el-table-column>
|
<el-table-column :label="$t('common.sort')" prop="sort" align="center" width="80"></el-table-column>
|
||||||
<el-table-column label="操作" align="center" width="180">
|
<el-table-column :label="$t('common.action')" align="center" width="180">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" @click="editProvider(scope.row)">编辑</el-button>
|
<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)">删除</el-button>
|
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">{{ $t('common.delete') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -84,31 +84,31 @@
|
|||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
{{ isAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" @click="showAddDialog">新增</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">删除
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">{{ $t('common.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="$t('common.perPage', { number: item })" :value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
{{ $t('common.firstPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
上一页
|
{{ $t('common.prevPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
下一页
|
{{ $t('common.nextPage') }}
|
||||||
</button>
|
</button>
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
<span class="total-text">{{ $t('common.totalRecords', { number: total }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -140,14 +140,14 @@ export default {
|
|||||||
searchModelType: "",
|
searchModelType: "",
|
||||||
providersList: [],
|
providersList: [],
|
||||||
modelTypes: [
|
modelTypes: [
|
||||||
{ value: "ASR", label: "语音识别" },
|
{ value: "ASR", labelKey: 'providerManagement.modelType.ASR' },
|
||||||
{ value: "TTS", label: "语音合成" },
|
{ value: "TTS", labelKey: 'providerManagement.modelType.TTS' },
|
||||||
{ value: "LLM", label: "大语言模型" },
|
{ value: "LLM", labelKey: 'providerManagement.modelType.LLM' },
|
||||||
{ value: "VLLM", label: "视觉大语言模型" },
|
{ value: "VLLM", labelKey: 'providerManagement.modelType.VLLM' },
|
||||||
{ value: "Intent", label: "意图识别" },
|
{ value: "Intent", labelKey: 'providerManagement.modelType.Intent' },
|
||||||
{ value: "Memory", label: "记忆模块" },
|
{ value: "Memory", labelKey: 'providerManagement.modelType.Memory' },
|
||||||
{ value: "VAD", label: "语音活动检测" },
|
{ value: "VAD", labelKey: 'providerManagement.modelType.VAD' },
|
||||||
{ value: "Plugin", label: "插件工具" }
|
{ value: "Plugin", labelKey: 'providerManagement.modelType.Plugin' }
|
||||||
],
|
],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -174,10 +174,16 @@ export default {
|
|||||||
this.fetchProviders();
|
this.fetchProviders();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
translatedModelTypes() {
|
||||||
|
return this.modelTypes.map(type => ({
|
||||||
|
value: type.value,
|
||||||
|
label: this.$t(type.labelKey)
|
||||||
|
}));
|
||||||
|
},
|
||||||
selectedModelTypeLabel() {
|
selectedModelTypeLabel() {
|
||||||
if (!this.searchModelType) return "(全部)";
|
if (!this.searchModelType) return `(${this.$t('providerManagement.all')})`;
|
||||||
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
|
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
|
||||||
return selectedType ? `(${selectedType.label})` : "";
|
return selectedType ? `(${this.$t(selectedType.labelKey)})` : "";
|
||||||
},
|
},
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.total / this.pageSize);
|
return Math.ceil(this.total / this.pageSize);
|
||||||
@@ -259,7 +265,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.dialogTitle = "新增供应器";
|
this.dialogTitle = this.$t('common.addProvider');
|
||||||
this.providerForm = {
|
this.providerForm = {
|
||||||
id: null,
|
id: null,
|
||||||
modelType: "",
|
modelType: "",
|
||||||
@@ -271,7 +277,7 @@ export default {
|
|||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
editProvider(row) {
|
editProvider(row) {
|
||||||
this.dialogTitle = "编辑供应器";
|
this.dialogTitle = this.$t('common.editProvider');
|
||||||
this.providerForm = {
|
this.providerForm = {
|
||||||
...row,
|
...row,
|
||||||
fields: JSON.parse(JSON.stringify(row.fields))
|
fields: JSON.parse(JSON.stringify(row.fields))
|
||||||
@@ -287,9 +293,9 @@ export default {
|
|||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.fetchProviders(); // 刷新表格
|
this.fetchProviders(); // 刷新表格
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: "修改成功",
|
message: this.$t('common.updateSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -298,9 +304,9 @@ export default {
|
|||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.fetchProviders(); // 刷新表格
|
this.fetchProviders(); // 刷新表格
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: "新增成功",
|
message: this.$t('common.addSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.total += 1;
|
this.total += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -313,7 +319,7 @@ export default {
|
|||||||
const selectedRows = this.providersList.filter(row => row.selected);
|
const selectedRows = this.providersList.filter(row => row.selected);
|
||||||
if (selectedRows.length === 0) {
|
if (selectedRows.length === 0) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: "请先选择需要删除的供应器",
|
message: this.$t('providerManagement.selectToDelete'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -324,9 +330,9 @@ export default {
|
|||||||
const providers = Array.isArray(row) ? row : [row];
|
const providers = Array.isArray(row) ? row : [row];
|
||||||
const providerCount = providers.length;
|
const providerCount = providers.length;
|
||||||
|
|
||||||
this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', {
|
this.$confirm(this.$t('providerManagement.confirmDelete', { count: providerCount }), this.$t('common.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ids = providers.map(provider => provider.id);
|
const ids = providers.map(provider => provider.id);
|
||||||
@@ -337,12 +343,12 @@ export default {
|
|||||||
this.fetchProviders(); // 刷新表格
|
this.fetchProviders(); // 刷新表格
|
||||||
|
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功删除${providerCount}个参数`,
|
message: this.$t('common.deleteSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '删除失败,请重试',
|
message: data.msg || this.$t('common.deleteFailure'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -350,7 +356,7 @@ export default {
|
|||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: '已取消删除',
|
message: this.$t('common.deleteCancelled'),
|
||||||
showClose: true,
|
showClose: true,
|
||||||
duration: 1000
|
duration: 1000
|
||||||
});
|
});
|
||||||
@@ -369,7 +375,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getModelTypeLabel(type) {
|
getModelTypeLabel(type) {
|
||||||
const typeItem = this.modelTypes.find(item => item.value === type);
|
const typeItem = this.modelTypes.find(item => item.value === type);
|
||||||
return typeItem ? typeItem.label : type;
|
return typeItem ? this.$t(typeItem.labelKey) : type;
|
||||||
},
|
},
|
||||||
isSensitiveField(fieldKey) {
|
isSensitiveField(fieldKey) {
|
||||||
if (typeof fieldKey !== 'string') return false;
|
if (typeof fieldKey !== 'string') return false;
|
||||||
@@ -388,6 +394,26 @@ export default {
|
|||||||
}
|
}
|
||||||
return "";
|
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() {
|
goFirst() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.fetchProviders();
|
this.fetchProviders();
|
||||||
@@ -875,4 +901,26 @@ export default {
|
|||||||
.dropdown-trigger.active {
|
.dropdown-trigger.active {
|
||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 确保选择列标题样式正确 */
|
||||||
|
.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>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">服务端管理</h2>
|
<h2 class="page-title">{{ $t('serverSideManager.pageTitle') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
@@ -11,19 +11,19 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="params-card" shadow="never">
|
<el-card class="params-card" shadow="never">
|
||||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
<el-table ref="paramsTable" :data="paramsList" class="transparent-table" v-loading="loading"
|
||||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
:element-loading-text="$t('serverSideManager.loading')" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="ws地址" prop="address" align="center"></el-table-column>
|
<el-table-column :label="$t('serverSideManager.wsAddress')" prop="address" align="center"></el-table-column>
|
||||||
<el-table-column label="操作" prop="operator" align="center" show-overflow-tooltip>
|
<el-table-column :label="$t('serverSideManager.operation')" prop="operator" align="center" show-overflow-tooltip>
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.restart)">重启</el-button>
|
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.restart)">{{ $t('serverSideManager.restart') }}</el-button>
|
||||||
<el-button size="medium" type="text"
|
<el-button size="medium" type="text"
|
||||||
@click="emitAction(scope.row, actionMap.update_config)">更新配置</el-button>
|
@click="emitAction(scope.row, actionMap.update_config)">{{ $t('serverSideManager.updateConfig') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -44,33 +44,20 @@ import Api from "@/apis/api";
|
|||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import ParamDialog from "@/components/ParamDialog.vue";
|
import ParamDialog from "@/components/ParamDialog.vue";
|
||||||
import VersionFooter from "@/components/VersionFooter.vue";
|
import VersionFooter from "@/components/VersionFooter.vue";
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar, ParamDialog, VersionFooter },
|
components: { HeaderBar, ParamDialog, VersionFooter },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
paramsList: [],
|
paramsList: [],
|
||||||
actionMap: {
|
|
||||||
restart: {
|
|
||||||
value: 'restart',
|
|
||||||
title: "重启服务端",
|
|
||||||
message: "确定要重启服务端吗?",
|
|
||||||
confirmText: "重启",
|
|
||||||
},
|
|
||||||
update_config: {
|
|
||||||
value: 'update_config',
|
|
||||||
title: "更新配置",
|
|
||||||
message: "确定要更新配置吗?",
|
|
||||||
confirmText: "更新",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
loading: false,
|
loading: false,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
total: 0,
|
total: 0,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: "新增参数",
|
dialogTitle: '',
|
||||||
isAllSelected: false,
|
isAllSelected: false,
|
||||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||||
paramForm: {
|
paramForm: {
|
||||||
@@ -84,6 +71,9 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.dialogTitle = this.$t('paramManagement.addParam');
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pageCount() {
|
pageCount() {
|
||||||
@@ -104,6 +94,22 @@ export default {
|
|||||||
}
|
}
|
||||||
return pages;
|
return pages;
|
||||||
},
|
},
|
||||||
|
actionMap() {
|
||||||
|
return {
|
||||||
|
restart: {
|
||||||
|
value: 'restart',
|
||||||
|
title: this.$t('serverSideManager.restartServer'),
|
||||||
|
message: this.$t('serverSideManager.confirmRestart'),
|
||||||
|
confirmText: this.$t('serverSideManager.restart'),
|
||||||
|
},
|
||||||
|
update_config: {
|
||||||
|
value: 'update_config',
|
||||||
|
title: this.$t('serverSideManager.updateConfigTitle'),
|
||||||
|
message: this.$t('serverSideManager.confirmUpdateConfig'),
|
||||||
|
confirmText: this.$t('serverSideManager.updateConfig'),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handlePageSizeChange(val) {
|
handlePageSizeChange(val) {
|
||||||
@@ -122,7 +128,7 @@ export default {
|
|||||||
this.total = data.data.length;
|
this.total = data.data.length;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '获取参数列表失败',
|
message: data.msg || this.$t('serverSideManager.getServerListFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -136,6 +142,7 @@ export default {
|
|||||||
// 弹开询问框
|
// 弹开询问框
|
||||||
this.$confirm(actionItem.message, actionItem.title, {
|
this.$confirm(actionItem.message, actionItem.title, {
|
||||||
confirmButtonText: actionItem.confirmText, // 确认按钮文本
|
confirmButtonText: actionItem.confirmText, // 确认按钮文本
|
||||||
|
cancelButtonText: this.$t('common.cancel') // 取消按钮文本
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// 用户点击了确认按钮
|
// 用户点击了确认按钮
|
||||||
Api.admin.sendWsServerAction({
|
Api.admin.sendWsServerAction({
|
||||||
@@ -144,13 +151,13 @@ export default {
|
|||||||
}, ({ data }) => {
|
}, ({ data }) => {
|
||||||
if (data.code !== 0) {
|
if (data.code !== 0) {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '操作失败',
|
message: data.msg || this.$t('serverSideManager.operationFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `${actionItem.title}成功`,
|
message: actionItem.value === 'restart' ? this.$t('serverSideManager.restartSuccess') : this.$t('serverSideManager.updateConfigSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -450,26 +457,7 @@ export default {
|
|||||||
|
|
||||||
.el-table__body-wrapper {
|
.el-table__body-wrapper {
|
||||||
max-height: calc(var(--table-max-height) - 40px);
|
max-height: calc(var(--table-max-height) - 40px);
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
: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>
|
</style>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">用户管理</h2>
|
<h2 class="page-title">{{ $t('header.userManagement') }}</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-input placeholder="请输入手机号码查询" v-model="searchPhone" class="search-input" clearable
|
<el-input :placeholder="$t('user.searchPhone')" v-model="searchPhone" class="search-input" clearable
|
||||||
@keyup.enter.native="handleSearch" />
|
@keyup.enter.native="handleSearch" />
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
<el-button class="btn-search" @click="handleSearch">{{ $t('user.search') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -16,31 +16,33 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="user-card" shadow="never">
|
<el-card class="user-card" shadow="never">
|
||||||
<el-table ref="userTable" :data="userList" class="transparent-table" v-loading="loading"
|
<el-table ref="userTable" :data="userList" class="transparent-table" v-loading="loading"
|
||||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
:element-loading-text="$t('modelConfig.loading')" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column :label="$t('modelConfig.select')" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
|
<el-table-column :label="$t('user.userid')" prop="userid" align="center"></el-table-column>
|
||||||
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
<el-table-column :label="$t('user.mobile')" prop="mobile" align="center"></el-table-column>
|
||||||
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
<el-table-column :label="$t('user.deviceCount')" prop="deviceCount" align="center"></el-table-column>
|
||||||
<el-table-column label="注册时间" prop="createDate" align="center"></el-table-column>
|
<el-table-column :label="$t('user.createDate')" prop="createDate" align="center"></el-table-column>
|
||||||
<el-table-column label="状态" prop="status" align="center">
|
<el-table-column :label="$t('user.status')" prop="status" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag>
|
<el-tag v-if="scope.row.status === 1" type="success">{{ $t('user.normal') }}</el-tag>
|
||||||
<el-tag v-else type="danger">禁用</el-tag>
|
<el-tag v-else type="danger">{{ $t('user.disabled') }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center">
|
<el-table-column :label="$t('modelConfig.action')" align="center" width="300px">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" @click="resetPassword(scope.row)">重置密码</el-button>
|
<el-button size="mini" type="text" @click="resetPassword(scope.row)">{{ $t('user.resetPassword')
|
||||||
|
}}</el-button>
|
||||||
<el-button size="mini" type="text" v-if="scope.row.status === 1"
|
<el-button size="mini" type="text" v-if="scope.row.status === 1"
|
||||||
@click="handleChangeStatus(scope.row, 0)">禁用账户</el-button>
|
@click="handleChangeStatus(scope.row, 0)">{{ $t('user.disableAccount') }}</el-button>
|
||||||
<el-button size="mini" type="text" v-if="scope.row.status === 0"
|
<el-button size="mini" type="text" v-if="scope.row.status === 0"
|
||||||
@click="handleChangeStatus(scope.row, 1)">恢复账号</el-button>
|
@click="handleChangeStatus(scope.row, 1)">{{ $t('user.enableAccount') }}</el-button>
|
||||||
<el-button size="mini" type="text" @click="deleteUser(scope.row)">删除用户</el-button>
|
<el-button size="mini" type="text" @click="deleteUser(scope.row)">{{ $t('user.deleteUser')
|
||||||
|
}}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -48,32 +50,38 @@
|
|||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
{{ isAllSelected ? $t('user.deselectAll') : $t('user.selectAll') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
|
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">{{
|
||||||
|
$t('user.enable')
|
||||||
|
}}</el-button>
|
||||||
<el-button size="mini" type="warning" @click="batchDisable"><i
|
<el-button size="mini" type="warning" @click="batchDisable"><i
|
||||||
class="el-icon-remove-outline rotated-icon"></i>禁用</el-button>
|
class="el-icon-remove-outline rotated-icon"></i>{{
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
|
$t('user.disable') }}</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">{{ $t('user.delete')
|
||||||
|
}}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange"
|
||||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
:class="['page-size-select', { 'page-size-select-en': $i18n.locale === 'en' }]">
|
||||||
|
<el-option v-for="item in pageSizeOptions" :key="item"
|
||||||
|
:label="$t('modelConfig.itemsPerPage', { items: item })" :value="item">
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
{{ $t('modelConfig.firstPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
上一页
|
{{ $t('modelConfig.prevPage') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
</button> <button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
下一页
|
{{ $t('modelConfig.nextPage') }}
|
||||||
</button>
|
</button>
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
<span class="total-text">{{ $t('modelConfig.totalRecords', { total: total }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -93,6 +101,7 @@ import Api from "@/apis/api";
|
|||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import VersionFooter from "@/components/VersionFooter.vue";
|
import VersionFooter from "@/components/VersionFooter.vue";
|
||||||
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
|
import ViewPasswordDialog from "@/components/ViewPasswordDialog.vue";
|
||||||
|
import i18n from '@/i18n';
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar, ViewPasswordDialog, VersionFooter },
|
components: { HeaderBar, ViewPasswordDialog, VersionFooter },
|
||||||
data() {
|
data() {
|
||||||
@@ -130,7 +139,7 @@ export default {
|
|||||||
pages.push(i);
|
pages.push(i);
|
||||||
}
|
}
|
||||||
return pages;
|
return pages;
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handlePageSizeChange(val) {
|
handlePageSizeChange(val) {
|
||||||
@@ -172,19 +181,19 @@ export default {
|
|||||||
batchDelete() {
|
batchDelete() {
|
||||||
const selectedUsers = this.userList.filter(user => user.selected);
|
const selectedUsers = this.userList.filter(user => user.selected);
|
||||||
if (selectedUsers.length === 0) {
|
if (selectedUsers.length === 0) {
|
||||||
this.$message.warning("请先选择需要删除的用户");
|
this.$message.warning(this.$t('user.selectUsersFirst'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$confirm(`确定要删除选中的${selectedUsers.length}个用户吗?`, "警告", {
|
this.$confirm(this.$t('user.confirmDeleteSelected', { count: selectedUsers.length }), this.$t('common.warning'), {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: "warning",
|
type: "warning",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const loading = this.$loading({
|
const loading = this.$loading({
|
||||||
lock: true,
|
lock: true,
|
||||||
text: "正在删除中...",
|
text: this.$t('user.deleting'),
|
||||||
spinner: "el-icon-loading",
|
spinner: "el-icon-loading",
|
||||||
background: "rgba(0, 0, 0, 0.7)",
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
});
|
});
|
||||||
@@ -209,29 +218,29 @@ export default {
|
|||||||
|
|
||||||
if (failCount === 0) {
|
if (failCount === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功删除${successCount}个用户`,
|
message: this.$t('user.deleteSuccess', { count: successCount }),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
} else if (successCount === 0) {
|
} else if (successCount === 0) {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '删除失败,请重试',
|
message: this.$t('user.deleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message.warning(
|
this.$message.warning(
|
||||||
`成功删除${successCount}个用户,${failCount}个删除失败`
|
this.$t('user.partialDelete', { successCount: successCount, failCount: failCount })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$message.error("删除过程中发生错误");
|
this.$message.error(this.$t('user.deleteError'));
|
||||||
} finally {
|
} finally {
|
||||||
loading.close();
|
loading.close();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.$message.info("已取消删除");
|
this.$message.info(this.$t('user.deleteCancelled'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
batchEnable() {
|
batchEnable() {
|
||||||
@@ -243,45 +252,36 @@ export default {
|
|||||||
this.handleChangeStatus(selectedUsers, 0);
|
this.handleChangeStatus(selectedUsers, 0);
|
||||||
},
|
},
|
||||||
resetPassword(row) {
|
resetPassword(row) {
|
||||||
this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
|
this.$confirm(this.$t('user.confirmResetPassword'), this.$t('common.warning'), {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Api.admin.resetUserPassword(row.userid, ({ data }) => {
|
resetPassword(row.userId).then(() => {
|
||||||
if (data.code === 0) {
|
this.$message.success(this.$t('user.resetPasswordSuccess'));
|
||||||
this.currentPassword = data.data;
|
this.getList();
|
||||||
this.showViewPassword = true;
|
}).catch(() => {
|
||||||
this.$message.success({
|
this.$message.error(this.$t('user.operationFailed'));
|
||||||
message: "密码已重置,请通知用户使用新密码登录",
|
|
||||||
showClose: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.info(this.$t('common.deleteCancelled'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteUser(row) {
|
deleteUser(row) {
|
||||||
this.$confirm("确定要删除该用户吗?", "警告", {
|
this.$confirm(this.$t('user.confirmDeleteUser'), this.$t('common.warning'), {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: "warning",
|
type: 'warning'
|
||||||
})
|
}).then(() => {
|
||||||
.then(() => {
|
deleteUser(row.userId).then(() => {
|
||||||
Api.admin.deleteUser(row.userid, ({ data }) => {
|
this.$message.success(this.$t('user.deleteUserSuccess'));
|
||||||
if (data.code === 0) {
|
this.getList();
|
||||||
this.$message.success({
|
}).catch(() => {
|
||||||
message: "删除成功",
|
this.$message.error(this.$t('user.operationFailed'));
|
||||||
showClose: true
|
});
|
||||||
});
|
}).catch(() => {
|
||||||
this.fetchUsers();
|
this.$message.info(this.$t('common.deleteCancelled'));
|
||||||
} else {
|
});
|
||||||
this.$message.error({
|
|
||||||
message: data.msg || "删除失败",
|
|
||||||
showClose: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => { });
|
|
||||||
},
|
},
|
||||||
goFirst() {
|
goFirst() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
@@ -306,30 +306,30 @@ export default {
|
|||||||
handleChangeStatus(row, status) {
|
handleChangeStatus(row, status) {
|
||||||
// 处理单个用户或用户数组
|
// 处理单个用户或用户数组
|
||||||
const users = Array.isArray(row) ? row : [row];
|
const users = Array.isArray(row) ? row : [row];
|
||||||
const confirmText = status === 0 ? '禁用' : '启用';
|
const actionText = status === 0 ? this.$t('user.disable') : this.$t('user.enable');
|
||||||
const userCount = users.length;
|
const userCount = users.length;
|
||||||
|
|
||||||
this.$confirm(`确定要${confirmText}选中的${userCount}个用户吗?`, '提示', {
|
this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: userCount }), this.$t('common.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const userIds = users.map(user => user.userid);
|
const userIds = users.map(user => user.userid);
|
||||||
if (userIds.some(id => isNaN(id))) {
|
if (userIds.some(id => isNaN(id))) {
|
||||||
this.$message.error('存在无效的用户ID');
|
this.$message.error(this.$t('user.invalidUserId'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Api.user.changeUserStatus(status, userIds, ({ data }) => {
|
Api.user.changeUserStatus(status, userIds, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功${confirmText}${userCount}个用户`,
|
message: this.$t('user.statusChangeSuccess', { action: actionText, count: userCount }),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchUsers(); // 刷新用户列表
|
this.fetchUsers(); // 刷新用户列表
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '操作失败,请重试',
|
message: this.$t('user.operationFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -338,6 +338,68 @@ export default {
|
|||||||
// 用户取消操作
|
// 用户取消操作
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleBatchDelete() {
|
||||||
|
if (!this.selection || this.selection.length === 0) {
|
||||||
|
this.$message.warning(this.$t('user.selectUsersFirst'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = this.selection.map(item => item.userId);
|
||||||
|
this.$confirm(this.$t('user.confirmDeleteSelected', { count: this.selection.length }), this.$t('common.warning'), {
|
||||||
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.loading = true;
|
||||||
|
batchDeleteUsers(userIds).then(res => {
|
||||||
|
this.loading = false;
|
||||||
|
this.getList();
|
||||||
|
this.selection = [];
|
||||||
|
if (res.successCount === userIds.length) {
|
||||||
|
this.$message.success(this.$t('user.deleteSuccess', { count: res.successCount }));
|
||||||
|
} else if (res.successCount === 0) {
|
||||||
|
this.$message.error(this.$t('user.deleteFailed'));
|
||||||
|
} else {
|
||||||
|
this.$message.warning(this.$t('user.partialDelete', { successCount: res.successCount, failCount: res.failCount }));
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.$message.error(this.$t('user.deleteError'));
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.$message.info(this.$t('user.deleteCancelled'));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleBatchStatusChange(status) {
|
||||||
|
if (!this.selection || this.selection.length === 0) {
|
||||||
|
this.$message.warning(this.$t('user.selectUsersFirst'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionText = status === 1 ? this.$t('user.enable') : this.$t('user.disable');
|
||||||
|
const userIds = this.selection.map(item => item.userId);
|
||||||
|
|
||||||
|
this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: this.selection.length }), this.$t('common.warning'), {
|
||||||
|
confirmButtonText: this.$t('common.confirm'),
|
||||||
|
cancelButtonText: this.$t('common.cancel'),
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.loading = true;
|
||||||
|
updateUserStatus(userIds, status).then(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.getList();
|
||||||
|
this.selection = [];
|
||||||
|
this.$message.success(this.$t('user.statusChangeSuccess', { action: actionText, count: userIds.length }));
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.$message.error(this.$t('user.operationFailed'));
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.$message.info(this.$t('common.deleteCancelled'));
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -657,6 +719,16 @@ export default {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.page-size-select-en {
|
||||||
|
width: 130px;
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-input__suffix) {
|
:deep(.el-input__suffix) {
|
||||||
right: 6px;
|
right: 6px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">声纹识别</h2>
|
<h2 class="page-title">{{ $t('voicePrint.pageTitle') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
@@ -11,23 +11,23 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="voice-print-card" shadow="never">
|
<el-card class="voice-print-card" shadow="never">
|
||||||
<el-table ref="paramsTable" :data="voicePrintList" class="transparent-table" v-loading="loading"
|
<el-table ref="paramsTable" :data="voicePrintList" class="transparent-table" v-loading="loading"
|
||||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
:element-loading-text="$t('voicePrint.loading')" element-loading-spinner="el-icon-loading"
|
||||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
<el-table-column label="姓名" prop="sourceName" align="center"></el-table-column>
|
<el-table-column :label="$t('voicePrint.name')" prop="sourceName" align="center"></el-table-column>
|
||||||
<el-table-column label="描述" prop="introduce" align="center"></el-table-column>
|
<el-table-column :label="$t('voicePrint.description')" prop="introduce" align="center"></el-table-column>
|
||||||
<el-table-column label="创建时间" prop="createDate" align="center"></el-table-column>
|
<el-table-column :label="$t('voicePrint.createTime')" prop="createDate" align="center"></el-table-column>
|
||||||
<el-table-column label="操作" align="center">
|
<el-table-column :label="$t('voicePrint.action')" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button size="mini" type="text" @click="editVoicePrint(scope.row)">编辑</el-button>
|
<el-button size="mini" type="text" @click="editVoicePrint(scope.row)">{{ $t('voicePrint.edit') }}</el-button>
|
||||||
<el-button size="mini" type="text"
|
<el-button size="mini" type="text"
|
||||||
@click="deleteVoicePrint(scope.row.id)">删除</el-button>
|
@click="deleteVoicePrint(scope.row.id)">{{ $t('voicePrint.delete') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
<el-button size="mini" type="success" @click="showAddDialog">{{ $t('voicePrint.add') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -56,7 +56,7 @@ export default {
|
|||||||
voicePrintList: [],
|
voicePrintList: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: "添加说话人",
|
dialogTitle: this.$t('voicePrint.addSpeaker'),
|
||||||
isAllSelected: false,
|
isAllSelected: false,
|
||||||
paramForm: {
|
paramForm: {
|
||||||
id: null,
|
id: null,
|
||||||
@@ -87,7 +87,7 @@ export default {
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '获取声纹列表失败',
|
message: data.msg || this.$t('voicePrint.fetchFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.dialogTitle = "添加说话人";
|
this.dialogTitle = this.$t('voicePrint.addSpeaker');
|
||||||
this.paramForm = {
|
this.paramForm = {
|
||||||
id: null,
|
id: null,
|
||||||
audioId: '',
|
audioId: '',
|
||||||
@@ -105,7 +105,7 @@ export default {
|
|||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
editVoicePrint(row) {
|
editVoicePrint(row) {
|
||||||
this.dialogTitle = "编辑说话人";
|
this.dialogTitle = this.$t('voicePrint.editSpeaker');
|
||||||
this.paramForm = { ...row };
|
this.paramForm = { ...row };
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
@@ -116,7 +116,7 @@ export default {
|
|||||||
Api.agent.updateAgentVoicePrint(form, ({ data }) => {
|
Api.agent.updateAgentVoicePrint(form, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: "修改成功",
|
message: this.$t('voicePrint.updateSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
@@ -134,7 +134,7 @@ export default {
|
|||||||
}, ({ data }) => {
|
}, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: "新增成功",
|
message: this.$t('voicePrint.addSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
@@ -146,22 +146,22 @@ export default {
|
|||||||
},
|
},
|
||||||
// 删除按钮
|
// 删除按钮
|
||||||
deleteVoicePrint(id) {
|
deleteVoicePrint(id) {
|
||||||
this.$confirm(`确定要删除选中的此声纹吗?`, '警告', {
|
this.$confirm(this.$t('voicePrint.confirmDelete'), this.$t('voicePrint.warning'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('voicePrint.confirm'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('voicePrint.cancel'),
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
distinguishCancelAndClose: true
|
distinguishCancelAndClose: true
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Api.agent.deleteAgentVoicePrint(id, ({ data }) => {
|
Api.agent.deleteAgentVoicePrint(id, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功删除此声纹`,
|
message: this.$t('voicePrint.deleteSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchVoicePrints();
|
this.fetchVoicePrints();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '删除失败,请重试',
|
message: data.msg || this.$t('voicePrint.deleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -170,13 +170,13 @@ export default {
|
|||||||
if (action === 'cancel') {
|
if (action === 'cancel') {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: '已取消删除操作',
|
message: this.$t('voicePrint.cancelDelete'),
|
||||||
duration: 1000
|
duration: 1000
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: '操作已关闭',
|
message: this.$t('voicePrint.closeOperation'),
|
||||||
duration: 1000
|
duration: 1000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
.login-text {
|
.login-text {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 32px;
|
font-size: 25px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #3d4566;
|
color: #3d4566;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,17 @@
|
|||||||
<div class="add-device">
|
<div class="add-device">
|
||||||
<div class="add-device-bg">
|
<div class="add-device-bg">
|
||||||
<div class="hellow-text" style="margin-top: 30px;">
|
<div class="hellow-text" style="margin-top: 30px;">
|
||||||
你好,小智
|
{{ $t('home.greeting') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="hellow-text">
|
<div class="hellow-text">
|
||||||
让我们度过
|
{{ $t('home.wish') }}
|
||||||
<div style="display: inline-block;color: #5778FF;">
|
|
||||||
美好的一天!
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hi-hint">
|
<div class="hi-hint">
|
||||||
Hello, Let's have a wonderful day!
|
let's have a wonderful day!
|
||||||
</div>
|
</div>
|
||||||
<div class="add-device-btn">
|
<div class="add-device-btn">
|
||||||
<div class="left-add" @click="showAddDialog">
|
<div class="left-add" @click="showAddDialog">
|
||||||
添加智能体
|
{{ $t('home.addAgent') }}
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||||
<div class="right-add">
|
<div class="right-add">
|
||||||
@@ -152,21 +149,21 @@ export default {
|
|||||||
},
|
},
|
||||||
// 删除智能体
|
// 删除智能体
|
||||||
handleDeleteAgent(agentId) {
|
handleDeleteAgent(agentId) {
|
||||||
this.$confirm('确定要删除该智能体吗?', '提示', {
|
this.$confirm(this.$t('home.confirmDeleteAgent'), '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: this.$t('button.ok'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: this.$t('button.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Api.agent.deleteAgent(agentId, (res) => {
|
Api.agent.deleteAgent(agentId, (res) => {
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '删除成功',
|
message: this.$t('home.deleteSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchAgentList(); // 刷新列表
|
this.fetchAgentList(); // 刷新列表
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: res.data.msg || '删除失败',
|
message: res.data.msg || this.$t('home.deleteFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,139 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<el-container style="height: 100%;">
|
<el-container style="height: 100%">
|
||||||
<el-header>
|
<el-header>
|
||||||
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
<div style="
|
||||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
|
display: flex;
|
||||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
|
align-items: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-left: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px; height: 45px" />
|
||||||
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px" />
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<div class="login-person">
|
<div class="login-person">
|
||||||
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%;" />
|
<img loading="lazy" alt="" src="@/assets/login/login-person.png" style="width: 100%" />
|
||||||
</div>
|
</div>
|
||||||
<el-main style="position: relative;">
|
<el-main style="position: relative">
|
||||||
<div class="login-box" @keyup.enter="login">
|
<div class="login-box" @keyup.enter="login">
|
||||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
<div style="
|
||||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
display: flex;
|
||||||
<div class="login-text">登录</div>
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 39px;
|
||||||
|
padding: 0 30px;
|
||||||
|
">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px; height: 34px" />
|
||||||
|
<div class="login-text">{{ $t("login.title") }}</div>
|
||||||
|
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO LOGIN
|
{{ $t("login.welcome") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 语言切换下拉菜单 -->
|
||||||
|
<el-dropdown trigger="click" class="title-language-dropdown"
|
||||||
|
@visible-change="handleLanguageDropdownVisibleChange">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
<span class="current-language-text">{{ currentLanguageText }}</span>
|
||||||
|
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': languageDropdownVisible }"></i>
|
||||||
|
</span>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click.native="changeLanguage('zh_CN')">
|
||||||
|
{{ $t("language.zhCN") }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="changeLanguage('zh_TW')">
|
||||||
|
{{ $t("language.zhTW") }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.native="changeLanguage('en')">
|
||||||
|
{{ $t("language.en") }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 0 30px;">
|
<div style="padding: 0 30px">
|
||||||
<!-- 用户名登录 -->
|
<!-- 用户名登录 -->
|
||||||
<template v-if="!isMobileLogin">
|
<template v-if="!isMobileLogin">
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
<el-input v-model="form.username" :placeholder="$t('login.usernamePlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 手机号登录 -->
|
<!-- 手机号登录 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<div style="display: flex; align-items: center; width: 100%;">
|
<div style="display: flex; align-items: center; width: 100%">
|
||||||
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
|
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px">
|
||||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||||
:value="item.key" />
|
:value="item.key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
<el-input v-model="form.mobile" :placeholder="$t('login.mobilePlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.password" placeholder="请输入密码" type="password" show-password />
|
<el-input v-model="form.password" :placeholder="$t('login.passwordPlaceholder')" type="password"
|
||||||
|
show-password />
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
">
|
||||||
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
<el-input v-model="form.captcha" :placeholder="$t('login.captchaPlaceholder')" style="flex: 1" />
|
||||||
</div>
|
</div>
|
||||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
style="width: 150px; height: 40px; cursor: pointer" @click="fetchCaptcha" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div style="
|
||||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
font-weight: 400;
|
||||||
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
font-size: 14px;
|
||||||
<div style="cursor: pointer;" @click="goToForgetPassword" v-if="enableMobileRegister">忘记密码?</div>
|
text-align: left;
|
||||||
|
color: #5778ff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
">
|
||||||
|
<div v-if="allowUserRegister" style="cursor: pointer" @click="goToRegister">
|
||||||
|
{{ $t("login.register") }}
|
||||||
|
</div>
|
||||||
|
<div style="cursor: pointer" @click="goToForgetPassword" v-if="enableMobileRegister">
|
||||||
|
{{ $t("login.forgetPassword") }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-btn" @click="login">登录</div>
|
<div class="login-btn" @click="login">{{ $t("login.login") }}</div>
|
||||||
|
|
||||||
<!-- 登录方式切换按钮 -->
|
<!-- 登录方式切换按钮 -->
|
||||||
<div class="login-type-container" v-if="enableMobileRegister">
|
<div class="login-type-container" v-if="enableMobileRegister">
|
||||||
<el-tooltip content="手机号码登录" placement="bottom">
|
<div style="display: flex; gap: 10px">
|
||||||
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
|
<el-tooltip :content="$t('login.mobileLogin')" placement="bottom">
|
||||||
@click="switchLoginType('mobile')"></el-button>
|
<el-button :type="isMobileLogin ? 'primary' : 'default'" icon="el-icon-mobile" circle
|
||||||
</el-tooltip>
|
@click="switchLoginType('mobile')"></el-button>
|
||||||
<el-tooltip content="用户名登录" placement="bottom">
|
</el-tooltip>
|
||||||
<el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
|
<el-tooltip :content="$t('login.usernameLogin')" placement="bottom">
|
||||||
@click="switchLoginType('username')"></el-button>
|
<el-button :type="!isMobileLogin ? 'primary' : 'default'" icon="el-icon-user" circle
|
||||||
</el-tooltip>
|
@click="switchLoginType('username')"></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="font-size: 14px; color: #979db1">
|
||||||
<div style="font-size: 14px;color: #979db1;">
|
{{ $t("login.agreeTo") }}
|
||||||
登录即同意
|
<div style="display: inline-block; color: #5778ff; cursor: pointer">
|
||||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
{{ $t("login.userAgreement") }}
|
||||||
和
|
</div>
|
||||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
{{ $t("login.and") }}
|
||||||
|
<div style="display: inline-block; color: #5778ff; cursor: pointer">
|
||||||
|
{{ $t("login.privacyPolicy") }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
@@ -89,42 +145,62 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from "@/apis/api";
|
||||||
import VersionFooter from '@/components/VersionFooter.vue';
|
import VersionFooter from "@/components/VersionFooter.vue";
|
||||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
import i18n, { changeLanguage } from "@/i18n";
|
||||||
import { mapState } from 'vuex';
|
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from "@/utils";
|
||||||
|
import { mapState } from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: "login",
|
||||||
components: {
|
components: {
|
||||||
VersionFooter
|
VersionFooter,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
allowUserRegister: state => state.pubConfig.allowUserRegister,
|
allowUserRegister: (state) => state.pubConfig.allowUserRegister,
|
||||||
enableMobileRegister: state => state.pubConfig.enableMobileRegister,
|
enableMobileRegister: (state) => state.pubConfig.enableMobileRegister,
|
||||||
mobileAreaList: state => state.pubConfig.mobileAreaList
|
mobileAreaList: (state) => state.pubConfig.mobileAreaList,
|
||||||
})
|
}),
|
||||||
|
// 获取当前语言
|
||||||
|
currentLanguage() {
|
||||||
|
return i18n.locale || "zh_CN";
|
||||||
|
},
|
||||||
|
// 获取当前语言显示文本
|
||||||
|
currentLanguageText() {
|
||||||
|
const currentLang = this.currentLanguage;
|
||||||
|
switch (currentLang) {
|
||||||
|
case "zh_CN":
|
||||||
|
return this.$t("language.zhCN");
|
||||||
|
case "zh_TW":
|
||||||
|
return this.$t("language.zhTW");
|
||||||
|
case "en":
|
||||||
|
return this.$t("language.en");
|
||||||
|
default:
|
||||||
|
return this.$t("language.zhCN");
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeName: "username",
|
activeName: "username",
|
||||||
form: {
|
form: {
|
||||||
username: '',
|
username: "",
|
||||||
password: '',
|
password: "",
|
||||||
captcha: '',
|
captcha: "",
|
||||||
captchaId: '',
|
captchaId: "",
|
||||||
areaCode: '+86',
|
areaCode: "+86",
|
||||||
mobile: ''
|
mobile: "",
|
||||||
},
|
},
|
||||||
captchaUuid: '',
|
captchaUuid: "",
|
||||||
captchaUrl: '',
|
captchaUrl: "",
|
||||||
isMobileLogin: false
|
isMobileLogin: false,
|
||||||
}
|
languageDropdownVisible: false,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
this.$store.dispatch('fetchPubConfig').then(() => {
|
this.$store.dispatch("fetchPubConfig").then(() => {
|
||||||
// 根据配置决定默认登录方式
|
// 根据配置决定默认登录方式
|
||||||
this.isMobileLogin = this.enableMobileRegister;
|
this.isMobileLogin = this.enableMobileRegister;
|
||||||
});
|
});
|
||||||
@@ -132,8 +208,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
fetchCaptcha() {
|
fetchCaptcha() {
|
||||||
if (this.$store.getters.getToken) {
|
if (this.$store.getters.getToken) {
|
||||||
if (this.$route.path !== '/home') {
|
if (this.$route.path !== "/home") {
|
||||||
this.$router.push('/home')
|
this.$router.push("/home");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.captchaUuid = getUUID();
|
this.captchaUuid = getUUID();
|
||||||
@@ -143,27 +219,42 @@ export default {
|
|||||||
const blob = new Blob([res.data], { type: res.data.type });
|
const blob = new Blob([res.data], { type: res.data.type });
|
||||||
this.captchaUrl = URL.createObjectURL(blob);
|
this.captchaUrl = URL.createObjectURL(blob);
|
||||||
} else {
|
} else {
|
||||||
showDanger('验证码加载失败,点击刷新');
|
showDanger("验证码加载失败,点击刷新");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 切换语言下拉菜单的可见状态变化
|
||||||
|
handleLanguageDropdownVisibleChange(visible) {
|
||||||
|
this.languageDropdownVisible = visible;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换语言
|
||||||
|
changeLanguage(lang) {
|
||||||
|
changeLanguage(lang);
|
||||||
|
this.languageDropdownVisible = false;
|
||||||
|
this.$message.success({
|
||||||
|
message: this.$t("message.success"),
|
||||||
|
showClose: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// 切换登录方式
|
// 切换登录方式
|
||||||
switchLoginType(type) {
|
switchLoginType(type) {
|
||||||
this.isMobileLogin = type === 'mobile';
|
this.isMobileLogin = type === "mobile";
|
||||||
// 清空表单
|
// 清空表单
|
||||||
this.form.username = '';
|
this.form.username = "";
|
||||||
this.form.mobile = '';
|
this.form.mobile = "";
|
||||||
this.form.password = '';
|
this.form.password = "";
|
||||||
this.form.captcha = '';
|
this.form.captcha = "";
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 封装输入验证逻辑
|
// 封装输入验证逻辑
|
||||||
validateInput(input, message) {
|
validateInput(input, messageKey) {
|
||||||
if (!input.trim()) {
|
if (!input.trim()) {
|
||||||
showDanger(message);
|
showDanger(this.$t(messageKey));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -173,38 +264,49 @@ export default {
|
|||||||
if (this.isMobileLogin) {
|
if (this.isMobileLogin) {
|
||||||
// 手机号登录验证
|
// 手机号登录验证
|
||||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||||
showDanger('请输入正确的手机号码');
|
showDanger(this.$t('login.requiredMobile'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 拼接手机号作为用户名
|
// 拼接手机号作为用户名
|
||||||
this.form.username = this.form.areaCode + this.form.mobile;
|
this.form.username = this.form.areaCode + this.form.mobile;
|
||||||
} else {
|
} else {
|
||||||
// 用户名登录验证
|
// 用户名登录验证
|
||||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
if (!this.validateInput(this.form.username, 'login.requiredUsername')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证密码
|
// 验证密码
|
||||||
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
if (!this.validateInput(this.form.password, 'login.requiredPassword')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 验证验证码
|
// 验证验证码
|
||||||
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
|
if (!this.validateInput(this.form.captcha, 'login.requiredCaptcha')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.form.captchaId = this.captchaUuid
|
this.form.captchaId = this.captchaUuid;
|
||||||
Api.user.login(this.form, ({ data }) => {
|
Api.user.login(
|
||||||
showSuccess('登录成功!');
|
this.form,
|
||||||
this.$store.commit('setToken', JSON.stringify(data.data));
|
({ data }) => {
|
||||||
goToPage('/home');
|
showSuccess(this.$t('login.loginSuccess'));
|
||||||
}, (err) => {
|
this.$store.commit("setToken", JSON.stringify(data.data));
|
||||||
showDanger(err.data.msg || '登录失败')
|
goToPage("/home");
|
||||||
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
},
|
||||||
this.fetchCaptcha()
|
(err) => {
|
||||||
|
// 直接使用后端返回的国际化消息
|
||||||
|
let errorMessage = err.data.msg || "登录失败";
|
||||||
|
|
||||||
|
showDanger(errorMessage);
|
||||||
|
if (
|
||||||
|
err.data != null &&
|
||||||
|
err.data.msg != null &&
|
||||||
|
err.data.msg.indexOf("图形验证码") > -1 || err.data.msg.indexOf("Captcha") > -1
|
||||||
|
) {
|
||||||
|
this.fetchCaptcha();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
|
|
||||||
// 重新获取验证码
|
// 重新获取验证码
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -213,19 +315,46 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
goToRegister() {
|
goToRegister() {
|
||||||
goToPage('/register')
|
goToPage("/register");
|
||||||
},
|
},
|
||||||
goToForgetPassword() {
|
goToForgetPassword() {
|
||||||
goToPage('/retrieve-password')
|
goToPage("/retrieve-password");
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import './auth.scss';
|
@import "./auth.scss";
|
||||||
|
|
||||||
.login-type-container {
|
.login-type-container {
|
||||||
margin: 10px 20px;
|
margin: 10px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-language-dropdown {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-language-text {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #3d4566;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-dropdown {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-down {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon-arrow-down {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-button--primary) {
|
:deep(.el-button--primary) {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
<!-- 修改标题部分 -->
|
<!-- 修改标题部分 -->
|
||||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
||||||
<div class="login-text">注册</div>
|
<div class="login-text">{{ $t('register.title') }}</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
WELCOME TO REGISTER
|
{{ $t('register.welcome') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<!-- 用户名/手机号输入框 -->
|
<!-- 用户名/手机号输入框 -->
|
||||||
<div class="input-box" v-if="!enableMobileRegister">
|
<div class="input-box" v-if="!enableMobileRegister">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/username.png" />
|
||||||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
<el-input v-model="form.username" :placeholder="$t('register.usernamePlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 手机号注册部分 -->
|
<!-- 手机号注册部分 -->
|
||||||
@@ -38,16 +38,16 @@
|
|||||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||||
:value="item.key" />
|
:value="item.key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
<el-input v-model="form.mobile" :placeholder="$t('register.mobilePlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
<el-input v-model="form.captcha" :placeholder="$t('register.captchaPlaceholder')" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,12 +56,12 @@
|
|||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
||||||
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
|
<el-input v-model="form.mobileCaptcha" :placeholder="$t('register.mobileCaptchaPlaceholder')" style="flex: 1;" maxlength="6" />
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
||||||
@click="sendMobileCaptcha">
|
@click="sendMobileCaptcha">
|
||||||
<span>
|
<span>
|
||||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
{{ countdown > 0 ? `${countdown}${$t('register.secondsLater')}` : $t('register.sendCaptcha') }}
|
||||||
</span>
|
</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,13 +70,13 @@
|
|||||||
<!-- 密码输入框 -->
|
<!-- 密码输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.password" placeholder="请输入密码" type="password" show-password />
|
<el-input v-model="form.password" :placeholder="$t('register.passwordPlaceholder')" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增确认密码 -->
|
<!-- 新增确认密码 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" show-password />
|
<el-input v-model="form.confirmPassword" :placeholder="$t('register.confirmPasswordPlaceholder')" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 验证码部分保持相同 -->
|
<!-- 验证码部分保持相同 -->
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
<el-input v-model="form.captcha" :placeholder="$t('register.captchaPlaceholder')" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||||
@@ -92,20 +92,20 @@
|
|||||||
|
|
||||||
<!-- 修改底部链接 -->
|
<!-- 修改底部链接 -->
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||||
<div style="cursor: pointer;" @click="goToLogin">已有账号?立即登录</div>
|
<div style="cursor: pointer;" @click="goToLogin">{{ $t('register.goToLogin') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改按钮文本 -->
|
<!-- 修改按钮文本 -->
|
||||||
<div class="login-btn" @click="register">立即注册</div>
|
<div class="login-btn" @click="register">{{ $t('register.registerButton') }}</div>
|
||||||
|
|
||||||
<!-- 保持相同的协议声明 -->
|
<!-- 保持相同的协议声明 -->
|
||||||
<div style="font-size: 14px;color: #979db1;">
|
<div style="font-size: 14px;color: #979db1;">
|
||||||
注册即同意
|
{{ $t('register.agreeTo') }}
|
||||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.userAgreement') }}</div>
|
||||||
和
|
{{ $t('register.and') }}
|
||||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
@@ -124,6 +124,9 @@ import VersionFooter from '@/components/VersionFooter.vue';
|
|||||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
|
// 导入语言切换功能
|
||||||
|
import { changeLanguage } from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'register',
|
name: 'register',
|
||||||
components: {
|
components: {
|
||||||
@@ -159,7 +162,7 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('fetchPubConfig').then(() => {
|
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||||
if (!this.allowUserRegister) {
|
if (!this.allowUserRegister) {
|
||||||
showDanger('当前不允许普通用户注册');
|
showDanger(this.$t('register.notAllowRegister'));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
goToPage('/login');
|
goToPage('/login');
|
||||||
}, 1500);
|
}, 1500);
|
||||||
@@ -178,7 +181,7 @@ export default {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('验证码加载异常:', error);
|
console.error('验证码加载异常:', error);
|
||||||
showDanger('验证码加载失败,点击刷新');
|
showDanger(this.$t('register.captchaLoadFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -195,12 +198,12 @@ export default {
|
|||||||
// 发送手机验证码
|
// 发送手机验证码
|
||||||
sendMobileCaptcha() {
|
sendMobileCaptcha() {
|
||||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||||
showDanger('请输入正确的手机号码');
|
showDanger(this.$t('register.inputCorrectMobile'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证图形验证码
|
// 验证图形验证码
|
||||||
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) {
|
if (!this.validateInput(this.form.captcha, this.$t('register.inputCaptcha'))) {
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -228,9 +231,9 @@ export default {
|
|||||||
captcha: this.form.captcha,
|
captcha: this.form.captcha,
|
||||||
captchaId: this.form.captchaId
|
captchaId: this.form.captchaId
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
showSuccess('验证码发送成功');
|
showSuccess(this.$t('register.captchaSendSuccess'));
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
showDanger(err.data.msg || '验证码发送失败');
|
showDanger(err.data.msg || this.$t('register.captchaSendFailed'));
|
||||||
this.countdown = 0;
|
this.countdown = 0;
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
});
|
});
|
||||||
@@ -241,30 +244,30 @@ export default {
|
|||||||
if (this.enableMobileRegister) {
|
if (this.enableMobileRegister) {
|
||||||
// 手机号注册验证
|
// 手机号注册验证
|
||||||
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||||
showDanger('请输入正确的手机号码');
|
showDanger(this.$t('register.inputCorrectMobile'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.form.mobileCaptcha) {
|
if (!this.form.mobileCaptcha) {
|
||||||
showDanger('请输入手机验证码');
|
showDanger(this.$t('register.requiredMobileCaptcha'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 用户名注册验证
|
// 用户名注册验证
|
||||||
if (!this.validateInput(this.form.username, '用户名不能为空')) {
|
if (!this.validateInput(this.form.username, this.$t('register.requiredUsername'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证密码
|
// 验证密码
|
||||||
if (!this.validateInput(this.form.password, '密码不能为空')) {
|
if (!this.validateInput(this.form.password, this.$t('register.requiredPassword'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.form.password !== this.form.confirmPassword) {
|
if (this.form.password !== this.form.confirmPassword) {
|
||||||
showDanger('两次输入的密码不一致')
|
showDanger(this.$t('register.passwordsNotMatch'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 验证验证码
|
// 验证验证码
|
||||||
if (!this.validateInput(this.form.captcha, '验证码不能为空')) {
|
if (!this.validateInput(this.form.captcha, this.$t('register.requiredCaptcha'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,10 +276,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Api.user.register(this.form, ({ data }) => {
|
Api.user.register(this.form, ({ data }) => {
|
||||||
showSuccess('注册成功!')
|
showSuccess(this.$t('register.registerSuccess'))
|
||||||
goToPage('/login')
|
goToPage('/login')
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
showDanger(err.data.msg || '注册失败')
|
showDanger(err.data.msg || this.$t('register.registerFailed'))
|
||||||
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
if (err.data != null && err.data.msg != null && err.data.msg.indexOf('图形验证码') > -1) {
|
||||||
this.fetchCaptcha()
|
this.fetchCaptcha()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<!-- 修改标题部分 -->
|
<!-- 修改标题部分 -->
|
||||||
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
||||||
<div class="login-text">重置密码</div>
|
<div class="login-text">{{ $t('retrievePassword.title') }}</div>
|
||||||
<div class="login-welcome">
|
<div class="login-welcome">
|
||||||
PASSWORD RETRIEVE
|
{{ $t('retrievePassword.subtitle') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,14 +31,14 @@
|
|||||||
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
<el-option v-for="item in mobileAreaList" :key="item.key" :label="`${item.name} (${item.key})`"
|
||||||
:value="item.key" />
|
:value="item.key" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
<el-input v-model="form.mobile" :placeholder="$t('retrievePassword.mobilePlaceholder')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||||
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
<el-input v-model="form.captcha" :placeholder="$t('retrievePassword.captchaPlaceholder')" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||||
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||||
@@ -48,12 +48,12 @@
|
|||||||
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
||||||
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
|
<el-input v-model="form.mobileCaptcha" :placeholder="$t('retrievePassword.mobileCaptchaPlaceholder')" style="flex: 1;" maxlength="6" />
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
||||||
@click="sendMobileCaptcha">
|
@click="sendMobileCaptcha">
|
||||||
<span>
|
<span>
|
||||||
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
{{ countdown > 0 ? `${countdown}秒后重试` : $t('retrievePassword.sendCaptcha') }}
|
||||||
</span>
|
</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,30 +61,30 @@
|
|||||||
<!-- 新密码 -->
|
<!-- 新密码 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" show-password />
|
<el-input v-model="form.newPassword" :placeholder="$t('retrievePassword.newPasswordPlaceholder')" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 确认新密码 -->
|
<!-- 确认新密码 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
<el-input v-model="form.confirmPassword" placeholder="请确认新密码" type="password" show-password />
|
<el-input v-model="form.confirmPassword" :placeholder="$t('retrievePassword.confirmPasswordPlaceholder')" type="password" show-password />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改底部链接 -->
|
<!-- 修改底部链接 -->
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||||
<div style="cursor: pointer;" @click="goToLogin">返回登录</div>
|
<div style="cursor: pointer;" @click="goToLogin">{{ $t('retrievePassword.goToLogin') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 修改按钮文本 -->
|
<!-- 修改按钮文本 -->
|
||||||
<div class="login-btn" @click="retrievePassword">立即修改</div>
|
<div class="login-btn" @click="retrievePassword">{{ $t('retrievePassword.resetButton') }}</div>
|
||||||
|
|
||||||
<!-- 保持相同的协议声明 -->
|
<!-- 保持相同的协议声明 -->
|
||||||
<div style="font-size: 14px;color: #979db1;">
|
<div style="font-size: 14px;color: #979db1;">
|
||||||
同意
|
{{ $t('retrievePassword.agreeTo') }}
|
||||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.userAgreement') }}</div>
|
||||||
和
|
{{ $t('register.and') }}
|
||||||
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
<div style="display: inline-block;color: #5778FF;cursor: pointer;">{{ $t('register.privacyPolicy') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -104,6 +104,9 @@ import VersionFooter from '@/components/VersionFooter.vue';
|
|||||||
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
|
// 导入语言切换功能
|
||||||
|
import { changeLanguage } from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'retrieve',
|
name: 'retrieve',
|
||||||
components: {
|
components: {
|
||||||
@@ -125,7 +128,7 @@ export default {
|
|||||||
mobile: '',
|
mobile: '',
|
||||||
captcha: '',
|
captcha: '',
|
||||||
captchaId: '',
|
captchaId: '',
|
||||||
smsCode: '',
|
mobileCaptcha: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
confirmPassword: ''
|
confirmPassword: ''
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">角色配置</h2>
|
<h2 class="page-title">{{ $t('roleConfig.title') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
@@ -18,10 +18,12 @@
|
|||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="hint-text">
|
<div class="hint-text">
|
||||||
<img loading="lazy" src="@/assets/home/info.png" alt="">
|
<img loading="lazy" src="@/assets/home/info.png" alt="">
|
||||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
<span>{{ $t('roleConfig.restartNotice') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
<el-button type="primary" class="save-btn" @click="saveConfig">
|
||||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
{{ $t('roleConfig.saveConfig') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button class="reset-btn" @click="resetConfig">{{ $t('roleConfig.reset') }}</el-button>
|
||||||
<button class="custom-close-btn" @click="goToHome">
|
<button class="custom-close-btn" @click="goToHome">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -33,10 +35,10 @@
|
|||||||
<div class="form-content">
|
<div class="form-content">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-column">
|
<div class="form-column">
|
||||||
<el-form-item label="助手昵称:">
|
<el-form-item :label="$t('roleConfig.agentName') + ':'">
|
||||||
<el-input v-model="form.agentName" class="form-input" maxlength="10" />
|
<el-input v-model="form.agentName" class="form-input" maxlength="10" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色模版:">
|
<el-form-item :label="$t('roleConfig.roleTemplate') + ':'">
|
||||||
<div class="template-container">
|
<div class="template-container">
|
||||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
||||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
||||||
@@ -44,39 +46,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色介绍:">
|
<el-form-item :label="$t('roleConfig.roleIntroduction') + ':'">
|
||||||
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
<el-input type="textarea" rows="9" resize="none"
|
||||||
maxlength="2000" show-word-limit class="form-textarea" />
|
:placeholder="$t('roleConfig.pleaseEnterContent')" v-model="form.systemPrompt" maxlength="2000"
|
||||||
|
show-word-limit class="form-textarea" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="记忆:">
|
<el-form-item :label="$t('roleConfig.memoryHis') + ':'">
|
||||||
<el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
|
<el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
|
||||||
show-word-limit class="form-textarea"
|
show-word-limit class="form-textarea"
|
||||||
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
|
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="语言编码:" style="display: none;">
|
<el-form-item :label="$t('roleConfig.languageCode') + ':'" style="display: none;">
|
||||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
|
<el-input v-model="form.langCode" :placeholder="$t('roleConfig.pleaseEnterLangCode')"
|
||||||
class="form-input" />
|
maxlength="10" show-word-limit class="form-input" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="交互语种:" style="display: none;">
|
<el-form-item :label="$t('roleConfig.interactionLanguage') + ':'" style="display: none;">
|
||||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
|
<el-input v-model="form.language" :placeholder="$t('roleConfig.pleaseEnterLangName')"
|
||||||
class="form-input" />
|
maxlength="10" show-word-limit class="form-input" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-column">
|
<div class="form-column">
|
||||||
<div class="model-row">
|
<div class="model-row">
|
||||||
<el-form-item label="语音活动检测(VAD)" class="model-item">
|
<el-form-item :label="$t('roleConfig.vad')" class="model-item">
|
||||||
<div class="model-select-wrapper">
|
<div class="model-select-wrapper">
|
||||||
<el-select v-model="form.model.vadModelId" filterable placeholder="请选择" class="form-select"
|
<el-select v-model="form.model.vadModelId" filterable
|
||||||
|
:placeholder="$t('roleConfig.pleaseSelect')" class="form-select"
|
||||||
@change="handleModelChange('VAD', $event)">
|
@change="handleModelChange('VAD', $event)">
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions['VAD']"
|
<el-option v-for="(item, optionIndex) in modelOptions['VAD']"
|
||||||
:key="`option-vad-${optionIndex}`" :label="item.label" :value="item.value" />
|
:key="`option-vad-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="语音识别(ASR)" class="model-item">
|
<el-form-item :label="$t('roleConfig.asr')" class="model-item">
|
||||||
<div class="model-select-wrapper">
|
<div class="model-select-wrapper">
|
||||||
<el-select v-model="form.model.asrModelId" filterable placeholder="请选择" class="form-select"
|
<el-select v-model="form.model.asrModelId" filterable
|
||||||
|
:placeholder="$t('roleConfig.pleaseSelect')" class="form-select"
|
||||||
@change="handleModelChange('ASR', $event)">
|
@change="handleModelChange('ASR', $event)">
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions['ASR']"
|
<el-option v-for="(item, optionIndex) in modelOptions['ASR']"
|
||||||
:key="`option-asr-${optionIndex}`" :label="item.label" :value="item.value" />
|
:key="`option-asr-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||||
@@ -84,10 +89,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item v-for="(model, index) in models.slice(2)" :key="`model-${index}`" :label="model.label"
|
<el-form-item v-for="(model, index) in models.slice(2)" :key="`model-${index}`"
|
||||||
class="model-item">
|
:label="$t('roleConfig.' + model.type.toLowerCase())" class="model-item">
|
||||||
<div class="model-select-wrapper">
|
<div class="model-select-wrapper">
|
||||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
|
<el-select v-model="form.model[model.key]" filterable
|
||||||
|
:placeholder="$t('roleConfig.pleaseSelect')" class="form-select"
|
||||||
@change="handleModelChange(model.type, $event)">
|
@change="handleModelChange(model.type, $event)">
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]" v-if="!item.isHidden"
|
<el-option v-for="(item, optionIndex) in modelOptions[model.type]" v-if="!item.isHidden"
|
||||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||||
@@ -104,20 +110,21 @@
|
|||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-button class="edit-function-btn" @click="openFunctionDialog"
|
<el-button class="edit-function-btn" @click="openFunctionDialog"
|
||||||
:class="{ 'active-btn': showFunctionDialog }">
|
:class="{ 'active-btn': showFunctionDialog }">
|
||||||
编辑功能
|
{{ $t('roleConfig.editFunctions') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
|
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
|
||||||
class="chat-history-options">
|
class="chat-history-options">
|
||||||
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
|
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
|
||||||
<el-radio-button :label="1">上报文字</el-radio-button>
|
<el-radio-button :label="1">{{ $t('roleConfig.reportText') }}</el-radio-button>
|
||||||
<el-radio-button :label="2">上报文字+语音</el-radio-button>
|
<el-radio-button :label="2">{{ $t('roleConfig.reportTextVoice') }}</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色音色">
|
<el-form-item :label="$t('roleConfig.voiceType')">
|
||||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
<el-select v-model="form.ttsVoiceId" :placeholder="$t('roleConfig.pleaseSelect')"
|
||||||
|
class="form-select">
|
||||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
||||||
:value="item.value" />
|
:value="item.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -139,6 +146,7 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import FunctionDialog from "@/components/FunctionDialog.vue";
|
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import i18n from '@/i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RoleConfigPage',
|
name: 'RoleConfigPage',
|
||||||
@@ -166,13 +174,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
|
{ label: this.$t('roleConfig.vad'), key: 'vadModelId', type: 'VAD' },
|
||||||
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
|
{ label: this.$t('roleConfig.asr'), key: 'asrModelId', type: 'ASR' },
|
||||||
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
|
{ label: this.$t('roleConfig.llm'), key: 'llmModelId', type: 'LLM' },
|
||||||
{ label: '视觉大模型(VLLM)', key: 'vllmModelId', type: 'VLLM' },
|
{ label: this.$t('roleConfig.vllm'), key: 'vllmModelId', type: 'VLLM' },
|
||||||
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
|
{ label: this.$t('roleConfig.intent'), key: 'intentModelId', type: 'Intent' },
|
||||||
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
|
{ label: this.$t('roleConfig.memory'), key: 'memModelId', type: 'Memory' },
|
||||||
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' }
|
{ label: this.$t('roleConfig.tts'), key: 'ttsModelId', type: 'TTS' }
|
||||||
],
|
],
|
||||||
llmModeTypeMap: new Map(),
|
llmModeTypeMap: new Map(),
|
||||||
modelOptions: {},
|
modelOptions: {},
|
||||||
@@ -221,21 +229,21 @@ export default {
|
|||||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '配置保存成功',
|
message: i18n.t('roleConfig.saveSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '配置保存失败',
|
message: data.msg || i18n.t('roleConfig.saveFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetConfig() {
|
resetConfig() {
|
||||||
this.$confirm('确定要重置配置吗?', '提示', {
|
this.$confirm(i18n.t('roleConfig.confirmReset'), i18n.t('message.info'), {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: i18n.t('button.ok'),
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: i18n.t('button.cancel'),
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.form = {
|
this.form = {
|
||||||
@@ -260,7 +268,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.currentFunctions = [];
|
this.currentFunctions = [];
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '配置已重置',
|
message: i18n.t('roleConfig.resetSuccess'),
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
@@ -271,7 +279,7 @@ export default {
|
|||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.templates = data.data;
|
this.templates = data.data;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取模板列表失败');
|
this.$message.error(data.msg || i18n.t('roleConfig.fetchTemplatesFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -281,12 +289,12 @@ export default {
|
|||||||
try {
|
try {
|
||||||
this.applyTemplateData(template);
|
this.applyTemplateData(template);
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `「${template.agentName}」模板已应用`,
|
message: `${template.agentName}${i18n.t('roleConfig.templateApplied')}`,
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '应用模板失败',
|
message: i18n.t('roleConfig.applyTemplateFailed'),
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
console.error('应用模板失败:', error);
|
console.error('应用模板失败:', error);
|
||||||
@@ -361,7 +369,7 @@ export default {
|
|||||||
this.updateIntentOptionsVisibility();
|
this.updateIntentOptionsVisibility();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取配置失败');
|
this.$message.error(data.msg || i18n.t('roleConfig.fetchConfigFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -381,7 +389,7 @@ export default {
|
|||||||
this.updateIntentOptionsVisibility();
|
this.updateIntentOptionsVisibility();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取模型列表失败');
|
this.$message.error(data.msg || i18n.t('roleConfig.fetchModelsFailed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -457,7 +465,7 @@ export default {
|
|||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取插件列表失败');
|
this.$message.error(data.msg || i18n.t('roleConfig.fetchPluginsFailed'));
|
||||||
reject();
|
reject();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
|
|
||||||
<context>
|
<context>
|
||||||
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||||
|
- **设备ID:** {{device_id}}
|
||||||
- **当前时间:** {{current_time}}
|
- **当前时间:** {{current_time}}
|
||||||
- **今天日期:** {{today_date}} ({{today_weekday}})
|
- **今天日期:** {{today_date}} ({{today_weekday}})
|
||||||
- **今天农历:** {{lunar_date}}
|
- **今天农历:** {{lunar_date}}
|
||||||
|
|||||||
@@ -114,7 +114,10 @@ plugins:
|
|||||||
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
||||||
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
||||||
# 申请后通过这个链接可以找到自己的apihost:https://console.qweather.com/setting?lang=zh
|
# 申请后通过这个链接可以找到自己的apihost:https://console.qweather.com/setting?lang=zh
|
||||||
get_weather: {"api_host":"mj7p3y7naa.re.qweatherapi.com", "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
get_weather:
|
||||||
|
api_host: "mj7p3y7naa.re.qweatherapi.com"
|
||||||
|
api_key: "a861d0d5e7bf4ee1a83d9a9e4f96d4da"
|
||||||
|
default_location: "广州"
|
||||||
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
||||||
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||||
get_news_from_chinanews:
|
get_news_from_chinanews:
|
||||||
@@ -148,6 +151,9 @@ voiceprint:
|
|||||||
- "test1,张三,张三是一个程序员"
|
- "test1,张三,张三是一个程序员"
|
||||||
- "test2,李四,李四是一个产品经理"
|
- "test2,李四,李四是一个产品经理"
|
||||||
- "test3,王五,王五是一个设计师"
|
- "test3,王五,王五是一个设计师"
|
||||||
|
# 声纹识别相似度阈值,范围0.0-1.0,默认0.4
|
||||||
|
# 数值越高越严格,减少误识别但可能增加拒识率
|
||||||
|
similarity_threshold: 0.4
|
||||||
|
|
||||||
# #####################################################################################
|
# #####################################################################################
|
||||||
# ################################以下是角色模型配置######################################
|
# ################################以下是角色模型配置######################################
|
||||||
@@ -389,7 +395,38 @@ ASR:
|
|||||||
base_url: https://api.groq.com/openai/v1/audio/transcriptions
|
base_url: https://api.groq.com/openai/v1/audio/transcriptions
|
||||||
model_name: whisper-large-v3-turbo
|
model_name: whisper-large-v3-turbo
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
|
VoskASR:
|
||||||
|
# 官方网站:https://alphacephei.com/vosk/
|
||||||
|
# 配置说明:
|
||||||
|
# 1. VOSK是一个离线语音识别库,支持多种语言
|
||||||
|
# 2. 需要先下载模型文件:https://alphacephei.com/vosk/models
|
||||||
|
# 3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22
|
||||||
|
# 4. 完全离线运行,无需网络连接
|
||||||
|
# 5. 输出文件保存在tmp/目录
|
||||||
|
# 使用步骤:
|
||||||
|
# 1. 访问 https://alphacephei.com/vosk/models 下载对应的模型
|
||||||
|
# 2. 解压模型文件到项目目录下的models/vosk/文件夹
|
||||||
|
# 3. 在配置中指定正确的模型路径
|
||||||
|
# 4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格
|
||||||
|
type: vosk
|
||||||
|
model_path: 你的模型路径,如:models/vosk/vosk-model-small-cn-0.22
|
||||||
|
output_dir: tmp/
|
||||||
|
Qwen3ASRFlash:
|
||||||
|
# 通义千问Qwen3-ASR-Flash语音识别服务,需要先在阿里云百炼平台创建API密钥
|
||||||
|
# 申请步骤:
|
||||||
|
# 1.登录阿里云百炼平台。https://bailian.console.aliyun.com/
|
||||||
|
# 2.创建API-KEY https://bailian.console.aliyun.com/#/api-key
|
||||||
|
# 3.Qwen3-ASR-Flash基于通义千问多模态基座,支持多语言识别、歌唱识别、噪声拒识等功能
|
||||||
|
type: qwen3_asr_flash
|
||||||
|
api_key: 你的阿里云百炼API密钥
|
||||||
|
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||||
|
model_name: qwen3-asr-flash
|
||||||
|
output_dir: tmp/
|
||||||
|
# ASR选项配置
|
||||||
|
enable_lid: true # 自动语种检测
|
||||||
|
enable_itn: true # 逆文本归一化
|
||||||
|
#language: "zh" # 语种,支持zh、en、ja、ko等
|
||||||
|
context: "" # 上下文信息,用于提高识别准确率,不超过10000 Token
|
||||||
|
|
||||||
|
|
||||||
VAD:
|
VAD:
|
||||||
@@ -685,19 +722,13 @@ TTS:
|
|||||||
inp_refs: []
|
inp_refs: []
|
||||||
sample_steps: 32
|
sample_steps: 32
|
||||||
if_sr: false
|
if_sr: false
|
||||||
MinimaxTTS:
|
MinimaxTTSHTTPStream:
|
||||||
# Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息
|
# Minimax流式语音合成服务
|
||||||
# 平台地址:https://platform.minimaxi.com/
|
type: minimax_httpstream
|
||||||
# 充值地址:https://platform.minimaxi.com/user-center/payment/balance
|
|
||||||
# group_id地址:https://platform.minimaxi.com/user-center/basic-information
|
|
||||||
# api_key地址:https://platform.minimaxi.com/user-center/basic-information/interface-key
|
|
||||||
# 定义TTS API类型
|
|
||||||
type: minimax
|
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
group_id: 你的minimax平台groupID
|
group_id: 你的minimax平台groupID
|
||||||
api_key: 你的minimax平台接口密钥
|
api_key: 你的minimax平台接口密钥
|
||||||
model: "speech-01-turbo"
|
model: "speech-01-turbo"
|
||||||
# 此处设置将优先于voice_setting中voice_id的设置;如都不设置,默认为 female-shaonv
|
|
||||||
voice_id: "female-shaonv"
|
voice_id: "female-shaonv"
|
||||||
# 以下可不用设置,使用默认设置
|
# 以下可不用设置,使用默认设置
|
||||||
# voice_setting:
|
# voice_setting:
|
||||||
@@ -711,7 +742,7 @@ TTS:
|
|||||||
# - "处理/(chu3)(li3)"
|
# - "处理/(chu3)(li3)"
|
||||||
# - "危险/dangerous"
|
# - "危险/dangerous"
|
||||||
# audio_setting:
|
# audio_setting:
|
||||||
# sample_rate: 32000
|
# sample_rate: 24000
|
||||||
# bitrate: 128000
|
# bitrate: 128000
|
||||||
# format: "mp3"
|
# format: "mp3"
|
||||||
# channel: 1
|
# channel: 1
|
||||||
@@ -723,26 +754,6 @@ TTS:
|
|||||||
# voice_id: female-shaonv
|
# voice_id: female-shaonv
|
||||||
# weight: 1
|
# weight: 1
|
||||||
# language_boost: auto
|
# language_boost: auto
|
||||||
|
|
||||||
# MinimaxTTSHTTPStream和MinimaxTTSWebSocketStream还在测试,测试完再开放
|
|
||||||
#
|
|
||||||
# MinimaxTTSHTTPStream:
|
|
||||||
# # Minimax流式语音合成服务
|
|
||||||
# type: minimax_httpstream
|
|
||||||
# output_dir: tmp/
|
|
||||||
# group_id: 你的minimax平台groupID
|
|
||||||
# api_key: 你的minimax平台接口密钥
|
|
||||||
# model: "speech-01-turbo"
|
|
||||||
# voice_id: "female-shaonv"
|
|
||||||
#
|
|
||||||
# MinimaxTTSWebSocketStream:
|
|
||||||
# type: minimax_webSocket
|
|
||||||
# output_dir: tmp/
|
|
||||||
# group_id: 你的minimax平台groupID
|
|
||||||
# api_key: 你的minimax平台接口密钥
|
|
||||||
# model: "speech-01-turbo"
|
|
||||||
# voice_id: "female-shaonv"
|
|
||||||
|
|
||||||
AliyunTTS:
|
AliyunTTS:
|
||||||
# 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息
|
# 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息
|
||||||
# 平台地址:https://nls-portal.console.aliyun.com/
|
# 平台地址:https://nls-portal.console.aliyun.com/
|
||||||
@@ -910,4 +921,19 @@ TTS:
|
|||||||
audio_format: "pcm"
|
audio_format: "pcm"
|
||||||
# 默认音色,如需其他音色可到项目assets文件夹下注册
|
# 默认音色,如需其他音色可到项目assets文件夹下注册
|
||||||
voice: "jay_klee"
|
voice: "jay_klee"
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
|
AliBLTTS:
|
||||||
|
# 阿里百炼CosyVoice大模型流式文本语音合成
|
||||||
|
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
|
||||||
|
# cosyvoice-v3和部分音色需要申请开通
|
||||||
|
type: alibl_stream
|
||||||
|
api_key: 你的api_key
|
||||||
|
model: "cosyvoice-v2"
|
||||||
|
voice: "longcheng_v2"
|
||||||
|
output_dir: tmp/
|
||||||
|
# 以下可不用设置,使用默认设置
|
||||||
|
# format: pcm # 音频格式:pcm、wav、mp3、opus
|
||||||
|
# sample_rate: 24000 # 采样率:16000, 24000, 48000
|
||||||
|
# volume: 50 # 音量:0-100
|
||||||
|
# rate: 1 # 语速:0.5~2
|
||||||
|
# pitch: 1 # 语调:0.5~2
|
||||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
|||||||
from config.settings import check_config_file
|
from config.settings import check_config_file
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
SERVER_VERSION = "0.7.6"
|
SERVER_VERSION = "0.8.1"
|
||||||
_logger_initialized = False
|
_logger_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ class ConnectionHandler:
|
|||||||
# {"mcp":true} 表示启用MCP功能
|
# {"mcp":true} 表示启用MCP功能
|
||||||
self.features = None
|
self.features = None
|
||||||
|
|
||||||
|
# 标记连接是否来自MQTT
|
||||||
|
self.conn_from_mqtt_gateway = False
|
||||||
|
|
||||||
# 初始化提示词管理器
|
# 初始化提示词管理器
|
||||||
self.prompt_manager = PromptManager(config, self.logger)
|
self.prompt_manager = PromptManager(config, self.logger)
|
||||||
|
|
||||||
@@ -198,6 +201,12 @@ class ConnectionHandler:
|
|||||||
self.websocket = ws
|
self.websocket = ws
|
||||||
self.device_id = self.headers.get("device-id", None)
|
self.device_id = self.headers.get("device-id", None)
|
||||||
|
|
||||||
|
# 检查是否来自MQTT连接
|
||||||
|
request_path = ws.request.path
|
||||||
|
self.conn_from_mqtt_gateway = request_path.endswith("?from=mqtt_gateway")
|
||||||
|
if self.conn_from_mqtt_gateway:
|
||||||
|
self.logger.bind(tag=TAG).info("连接来自:MQTT网关")
|
||||||
|
|
||||||
# 初始化活动时间戳
|
# 初始化活动时间戳
|
||||||
self.last_activity_time = time.time() * 1000
|
self.last_activity_time = time.time() * 1000
|
||||||
|
|
||||||
@@ -277,12 +286,82 @@ class ConnectionHandler:
|
|||||||
if isinstance(message, str):
|
if isinstance(message, str):
|
||||||
await handleTextMessage(self, message)
|
await handleTextMessage(self, message)
|
||||||
elif isinstance(message, bytes):
|
elif isinstance(message, bytes):
|
||||||
if self.vad is None:
|
if self.vad is None or self.asr is None:
|
||||||
return
|
|
||||||
if self.asr is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 处理来自MQTT网关的音频包
|
||||||
|
if self.conn_from_mqtt_gateway and len(message) >= 16:
|
||||||
|
handled = await self._process_mqtt_audio_message(message)
|
||||||
|
if handled:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 不需要头部处理或没有头部时,直接处理原始消息
|
||||||
self.asr_audio_queue.put(message)
|
self.asr_audio_queue.put(message)
|
||||||
|
|
||||||
|
async def _process_mqtt_audio_message(self, message):
|
||||||
|
"""
|
||||||
|
处理来自MQTT网关的音频消息,解析16字节头部并提取音频数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: 包含头部的音频消息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功处理了消息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 提取头部信息
|
||||||
|
timestamp = int.from_bytes(message[8:12], "big")
|
||||||
|
audio_length = int.from_bytes(message[12:16], "big")
|
||||||
|
|
||||||
|
# 提取音频数据
|
||||||
|
if audio_length > 0 and len(message) >= 16 + audio_length:
|
||||||
|
# 有指定长度,提取精确的音频数据
|
||||||
|
audio_data = message[16 : 16 + audio_length]
|
||||||
|
# 基于时间戳进行排序处理
|
||||||
|
self._process_websocket_audio(audio_data, timestamp)
|
||||||
|
return True
|
||||||
|
elif len(message) > 16:
|
||||||
|
# 没有指定长度或长度无效,去掉头部后处理剩余数据
|
||||||
|
audio_data = message[16:]
|
||||||
|
self.asr_audio_queue.put(audio_data)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"解析WebSocket音频包失败: {e}")
|
||||||
|
|
||||||
|
# 处理失败,返回False表示需要继续处理
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _process_websocket_audio(self, audio_data, timestamp):
|
||||||
|
"""处理WebSocket格式的音频包"""
|
||||||
|
# 初始化时间戳序列管理
|
||||||
|
if not hasattr(self, "audio_timestamp_buffer"):
|
||||||
|
self.audio_timestamp_buffer = {}
|
||||||
|
self.last_processed_timestamp = 0
|
||||||
|
self.max_timestamp_buffer_size = 20
|
||||||
|
|
||||||
|
# 如果时间戳是递增的,直接处理
|
||||||
|
if timestamp >= self.last_processed_timestamp:
|
||||||
|
self.asr_audio_queue.put(audio_data)
|
||||||
|
self.last_processed_timestamp = timestamp
|
||||||
|
|
||||||
|
# 处理缓冲区中的后续包
|
||||||
|
processed_any = True
|
||||||
|
while processed_any:
|
||||||
|
processed_any = False
|
||||||
|
for ts in sorted(self.audio_timestamp_buffer.keys()):
|
||||||
|
if ts > self.last_processed_timestamp:
|
||||||
|
buffered_audio = self.audio_timestamp_buffer.pop(ts)
|
||||||
|
self.asr_audio_queue.put(buffered_audio)
|
||||||
|
self.last_processed_timestamp = ts
|
||||||
|
processed_any = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 乱序包,暂存
|
||||||
|
if len(self.audio_timestamp_buffer) < self.max_timestamp_buffer_size:
|
||||||
|
self.audio_timestamp_buffer[timestamp] = audio_data
|
||||||
|
else:
|
||||||
|
self.asr_audio_queue.put(audio_data)
|
||||||
|
|
||||||
async def handle_restart(self, message):
|
async def handle_restart(self, message):
|
||||||
"""处理服务器重启请求"""
|
"""处理服务器重启请求"""
|
||||||
try:
|
try:
|
||||||
@@ -432,10 +511,14 @@ class ConnectionHandler:
|
|||||||
try:
|
try:
|
||||||
voiceprint_config = self.config.get("voiceprint", {})
|
voiceprint_config = self.config.get("voiceprint", {})
|
||||||
if voiceprint_config:
|
if voiceprint_config:
|
||||||
self.voiceprint_provider = VoiceprintProvider(voiceprint_config)
|
voiceprint_provider = VoiceprintProvider(voiceprint_config)
|
||||||
self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用")
|
if voiceprint_provider is not None and voiceprint_provider.enabled:
|
||||||
|
self.voiceprint_provider = voiceprint_provider
|
||||||
|
self.logger.bind(tag=TAG).info("声纹识别功能已在连接时动态启用")
|
||||||
|
else:
|
||||||
|
self.logger.bind(tag=TAG).warning("声纹识别功能启用但配置不完整")
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).info("声纹识别功能未启用或配置不完整")
|
self.logger.bind(tag=TAG).info("声纹识别功能未启用")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).warning(f"声纹识别初始化失败: {str(e)}")
|
self.logger.bind(tag=TAG).warning(f"声纹识别初始化失败: {str(e)}")
|
||||||
|
|
||||||
@@ -853,7 +936,11 @@ class ConnectionHandler:
|
|||||||
{
|
{
|
||||||
"id": function_id,
|
"id": function_id,
|
||||||
"function": {
|
"function": {
|
||||||
"arguments": "{}" if function_arguments == "" else function_arguments,
|
"arguments": (
|
||||||
|
"{}"
|
||||||
|
if function_arguments == ""
|
||||||
|
else function_arguments
|
||||||
|
),
|
||||||
"name": function_name,
|
"name": function_name,
|
||||||
},
|
},
|
||||||
"type": "function",
|
"type": "function",
|
||||||
@@ -921,6 +1008,10 @@ class ConnectionHandler:
|
|||||||
async def close(self, ws=None):
|
async def close(self, ws=None):
|
||||||
"""资源清理方法"""
|
"""资源清理方法"""
|
||||||
try:
|
try:
|
||||||
|
# 清理音频缓冲区
|
||||||
|
if hasattr(self, "audio_buffer"):
|
||||||
|
self.audio_buffer.clear()
|
||||||
|
|
||||||
# 取消超时任务
|
# 取消超时任务
|
||||||
if self.timeout_task and not self.timeout_task.done():
|
if self.timeout_task and not self.timeout_task.done():
|
||||||
self.timeout_task.cancel()
|
self.timeout_task.cancel()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user