From dfcac5131236e6635af4359a5ec3eee322047ce7 Mon Sep 17 00:00:00 2001
From: rainv123 <2148537152@qq.com>
Date: Fri, 26 Sep 2025 14:33:06 +0800
Subject: [PATCH] =?UTF-8?q?updata:=E7=A7=BB=E5=8A=A8=E7=AB=AF=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E8=AF=AD=E8=A8=80=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/manager-mobile/src/App.vue | 55 +++
main/manager-mobile/src/i18n/en.ts | 406 ++++++++++++++++++
main/manager-mobile/src/i18n/index.ts | 70 +++
main/manager-mobile/src/i18n/zh_CN.ts | 406 ++++++++++++++++++
main/manager-mobile/src/i18n/zh_TW.ts | 406 ++++++++++++++++++
main/manager-mobile/src/main.ts | 7 +
main/manager-mobile/src/pages.json | 8 +-
main/manager-mobile/src/pages/agent/edit.vue | 78 ++--
main/manager-mobile/src/pages/agent/index.vue | 12 +-
main/manager-mobile/src/pages/agent/tools.vue | 69 +--
.../src/pages/chat-history/detail.vue | 21 +-
.../src/pages/chat-history/index.vue | 59 +--
.../components/ultrasonic-config.vue | 173 ++++----
.../device-config/components/wifi-config.vue | 43 +-
.../components/wifi-selector.vue | 171 ++++----
.../src/pages/device-config/index.vue | 35 +-
.../manager-mobile/src/pages/device/index.vue | 67 +--
main/manager-mobile/src/pages/index/index.vue | 54 +--
main/manager-mobile/src/pages/login/index.vue | 187 ++++++--
.../src/pages/register/index.vue | 140 +++---
.../src/pages/settings/index.vue | 205 ++++++---
.../src/pages/voiceprint/index.vue | 124 +++---
main/manager-mobile/src/store/lang.ts | 41 ++
23 files changed, 2248 insertions(+), 589 deletions(-)
create mode 100644 main/manager-mobile/src/i18n/en.ts
create mode 100644 main/manager-mobile/src/i18n/index.ts
create mode 100644 main/manager-mobile/src/i18n/zh_CN.ts
create mode 100644 main/manager-mobile/src/i18n/zh_TW.ts
create mode 100644 main/manager-mobile/src/store/lang.ts
diff --git a/main/manager-mobile/src/App.vue b/main/manager-mobile/src/App.vue
index a9faa6e7..d31d32d7 100644
--- a/main/manager-mobile/src/App.vue
+++ b/main/manager-mobile/src/App.vue
@@ -1,12 +1,16 @@
@@ -456,18 +457,18 @@ async function stopAudio() {
- 选中网络: {{ props.selectedNetwork.ssid }}
+ {{ t('deviceConfig.selectedNetwork') }}: {{ props.selectedNetwork.ssid }}
- 信号: {{ props.selectedNetwork.rssi }}dBm
+ {{ t('deviceConfig.signal') }}: {{ props.selectedNetwork.rssi }}dBm
- {{ props.selectedNetwork.authmode === 0 ? '开放网络' : '加密网络' }}
+ {{ props.selectedNetwork.authmode === 0 ? t('deviceConfig.openNetwork') : t('deviceConfig.encryptedNetwork') }}
- 密码: {{ '*'.repeat(props.password.length) }}
+ {{ t('deviceConfig.password') }}: {{ '*'.repeat(props.password.length) }}
@@ -482,81 +483,81 @@ async function stopAudio() {
:disabled="!canGenerate"
@click="generateAndPlay"
>
- {{ generating ? '生成中...' : '🎵 生成并播放声波' }}
-
+ {{ generating ? t('deviceConfig.generating') : '🎵 ' + t('deviceConfig.generateAndPlaySoundWave') }}
+
-
- {{ playing ? '播放中...' : '🔊 播放声波' }}
-
+
+ {{ playing ? t('deviceConfig.playing') : '🔊 ' + t('deviceConfig.playSoundWave') }}
+
-
- ⏹️ 停止播放
-
+
+ ⏹️ {{ t('deviceConfig.stopPlaying') }}
+
-
- 自动循环播放声波
-
-
+
+ {{ t('deviceConfig.autoLoopPlaySoundWave') }}
+
+
- 配网音频文件
+ {{ t('deviceConfig.configAudioFile') }}
- 时长: {{ audioLengthText }}
+ {{ t('deviceConfig.duration') }}: {{ audioLengthText }}
-
- 超声波配网说明
+
+ {{ t('deviceConfig.ultrasonicConfigInstructions') }}
+
+
+
+ 1. {{ t('deviceConfig.ensureWifiNetworkSelectedAndPasswordEntered') }}
+
+
+ 2. {{ t('deviceConfig.clickGenerateAndPlaySoundWave') }}
+
+
+ 3. {{ t('deviceConfig.bringPhoneCloseToXiaozhiDevice') }}
+
+
+ 4. {{ t('deviceConfig.duringAudioPlaybackXiaozhiWillReceive') }}
+
+
+ 5. {{ t('deviceConfig.afterConfigSuccessDeviceWillConnect') }}
+
+
+ {{ t('deviceConfig.usesAfskModulation') }}
+
+
+ {{ t('deviceConfig.ensureModeratePhoneVolume') }}
+
+
-
-
- 1. 确保已选择WiFi网络并输入密码
-
-
- 2. 点击生成并播放声波,系统会将配网信息编码为音频
-
-
- 3. 将手机靠近xiaozhi设备(距离1-2米)
-
-
- 4. 音频播放时,xiaozhi会接收并解码配网信息
-
-
- 5. 配网成功后设备会自动连接WiFi网络
-
-
- 使用AFSK调制技术,通过1800Hz和1500Hz频率传输数据
-
-
- 请确保手机音量适中,避免环境噪音干扰
-
-
-
diff --git a/main/manager-mobile/src/pages/device-config/components/wifi-config.vue b/main/manager-mobile/src/pages/device-config/components/wifi-config.vue
index ac54b6b6..e8a4d6b7 100644
--- a/main/manager-mobile/src/pages/device-config/components/wifi-config.vue
+++ b/main/manager-mobile/src/pages/device-config/components/wifi-config.vue
@@ -1,6 +1,7 @@
-
+
- 配网方式
-
+ {{ t('deviceConfig.configMethod') }}
+
- 配网方式
-
-
- {{ configType === 'wifi' ? 'WiFi配网' : '超声波配网' }}
-
+ {{ t('deviceConfig.configMethod') }}
+
+
+ {{ configType === 'wifi' ? t('deviceConfig.wifiConfig') : t('deviceConfig.ultrasonicConfig') }}
+
@@ -96,8 +105,8 @@ function onConnectionStatusChange(connected: boolean) {
- 网络配置
-
+ {{ t('deviceConfig.networkConfig') }}
+
@@ -139,7 +148,7 @@ function onConnectionStatusChange(connected: boolean) {
{
"style": {
- "navigationBarTitleText": "设备配网",
+ "navigationBarTitleText": "设备配置",
"navigationStyle": "custom"
}
}
diff --git a/main/manager-mobile/src/pages/device/index.vue b/main/manager-mobile/src/pages/device/index.vue
index c12c5607..e592f58c 100644
--- a/main/manager-mobile/src/pages/device/index.vue
+++ b/main/manager-mobile/src/pages/device/index.vue
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni'
import { bindDevice, getBindDevices, getFirmwareTypes, unbindDevice, updateDeviceAutoUpdate } from '@/api/device'
import { toast } from '@/utils/toast'
+import { t } from '@/i18n'
defineOptions({
name: 'DeviceManage',
@@ -91,19 +92,19 @@ function getDeviceTypeName(boardKey: string): string {
// 格式化时间
function formatTime(timeStr: string) {
if (!timeStr)
- return '从未连接'
+ return t('device.neverConnected')
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000)
- return '刚刚'
+ return t('device.justNow')
if (diff < 3600000)
- return `${Math.floor(diff / 60000)}分钟前`
+ return t('device.minutesAgo', { minutes: Math.floor(diff / 60000) })
if (diff < 86400000)
- return `${Math.floor(diff / 3600000)}小时前`
+ return t('device.hoursAgo', { hours: Math.floor(diff / 3600000) })
if (diff < 604800000)
- return `${Math.floor(diff / 86400000)}天前`
+ return t('device.daysAgo', { days: Math.floor(diff / 86400000) })
return date.toLocaleDateString()
}
@@ -114,11 +115,11 @@ async function toggleAutoUpdate(device: Device) {
const newStatus = device.autoUpdate === 1 ? 0 : 1
await updateDeviceAutoUpdate(device.id, newStatus)
device.autoUpdate = newStatus
- toast.success(newStatus === 1 ? 'OTA自动升级已开启' : 'OTA自动升级已关闭')
+ toast.success(newStatus === 1 ? t('device.otaAutoUpdateEnabled') : t('device.otaAutoUpdateDisabled'))
}
catch (error: any) {
console.error('更新设备OTA状态失败:', error)
- toast.error('操作失败,请重试')
+ toast.error(t('device.operationFailed'))
}
}
@@ -127,21 +128,21 @@ async function handleUnbindDevice(device: Device) {
try {
await unbindDevice(device.id)
await loadDeviceList()
- toast.success('设备已解绑')
+ toast.success(t('device.deviceUnbound'))
}
catch (error: any) {
console.error('解绑设备失败:', error)
- toast.error('解绑失败,请重试')
+ toast.error(t('device.unbindFailed'))
}
}
// 确认解绑设备
function confirmUnbindDevice(device: Device) {
message.confirm({
- title: '解绑设备',
- msg: `确定要解绑设备 "${device.macAddress}" 吗?`,
- confirmButtonText: '确定解绑',
- cancelButtonText: '取消',
+ title: t('device.unbindDevice'),
+ msg: t('device.confirmUnbindDevice', { macAddress: device.macAddress }),
+ confirmButtonText: t('device.confirmUnbind'),
+ cancelButtonText: t('device.cancel'),
}).then(() => {
handleUnbindDevice(device)
}).catch(() => {
@@ -153,18 +154,18 @@ function confirmUnbindDevice(device: Device) {
async function handleBindDevice(code: string) {
try {
if (!currentAgentId.value) {
- toast.error('请先选择智能体')
+ toast.error(t('device.pleaseSelectAgent'))
return
}
await bindDevice(currentAgentId.value, code.trim())
await loadDeviceList()
- toast.success('设备绑定成功!')
+ toast.success(t('device.deviceBindSuccess'))
}
catch (error: any) {
console.error('绑定设备失败:', error)
const errorMessage = error?.message || '绑定失败,请检查验证码是否正确'
- toast.error(errorMessage)
+ toast.error(errorMessage || t('device.bindFailed'))
}
}
@@ -172,12 +173,12 @@ async function handleBindDevice(code: string) {
function openBindDialog() {
message
.prompt({
- title: '绑定设备',
- inputPlaceholder: '请输入设备验证码',
+ title: t('device.bindDevice'),
+ inputPlaceholder: t('device.enterDeviceCode'),
inputValue: '',
inputPattern: /^\d{6}$/,
- confirmButtonText: '立即绑定',
- cancelButtonText: '取消',
+ confirmButtonText: t('device.bindNow'),
+ cancelButtonText: t('device.cancel'),
})
.then(async (result: any) => {
if (result.value && String(result.value).trim()) {
@@ -219,7 +220,7 @@ defineExpose({
- 加载中...
+ {{ t('device.loading') }}
@@ -240,19 +241,19 @@ defineExpose({
- MAC地址:{{ device.macAddress }}
-
-
- 固件版本:{{ device.appVersion }}
-
-
- 最近对话:{{ formatTime(device.lastConnectedAt) }}
-
+ {{ t('device.macAddress') }}:{{ device.macAddress }}
+
+
+ {{ t('device.firmwareVersion') }}:{{ device.appVersion }}
+
+
+ {{ t('device.lastConnection') }}:{{ formatTime(device.lastConnectedAt) }}
+
- OTA升级
+ {{ t('device.otaUpdate') }}
- 解绑
+ {{ t('device.unbind') }}
@@ -285,10 +286,10 @@ defineExpose({
- 暂无设备
+ {{ t('device.noDevice') }}
- 点击右下角 + 号绑定您的第一个设备
+ {{ t('device.clickToBindFirstDevice') }}
diff --git a/main/manager-mobile/src/pages/index/index.vue b/main/manager-mobile/src/pages/index/index.vue
index bb74ab1c..8678b80c 100644
--- a/main/manager-mobile/src/pages/index/index.vue
+++ b/main/manager-mobile/src/pages/index/index.vue
@@ -17,6 +17,7 @@ import { useMessage } from 'wot-design-uni'
import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
import { createAgent, deleteAgent, getAgentList } from '@/api/agent/agent'
import { toast } from '@/utils/toast'
+import { t } from '@/i18n'
defineOptions({
name: 'Home',
@@ -78,11 +79,11 @@ async function handleCreateAgent(agentName: string) {
await createAgent({ agentName: agentName.trim() })
// 创建成功后刷新列表
pagingRef.value.reload()
- toast.success(`智能体"${agentName}"创建成功!`)
+ toast.success(`${t('home.agentName')}"${agentName}"${t('message.saveSuccess')}`)
}
catch (error: any) {
console.error('创建智能体失败:', error)
- const errorMessage = error?.message || '创建失败,请重试'
+ const errorMessage = error?.message || t('message.saveFail')
toast.error(errorMessage)
}
}
@@ -93,11 +94,11 @@ async function handleDeleteAgent(agent: Agent) {
await deleteAgent(agent.id)
// 删除成功后刷新列表
pagingRef.value.reload()
- toast.success(`智能体"${agent.agentName}"已删除`)
+ toast.success(`${t('home.agentName')}${t('message.deleteSuccess')}`)
}
catch (error: any) {
console.error('删除智能体失败:', error)
- const errorMessage = error?.message || '删除失败,请重试'
+ const errorMessage = error?.message || t('message.deleteFail')
toast.error(errorMessage)
}
}
@@ -119,13 +120,13 @@ function handleCardClick(agent: Agent) {
function openCreateDialog() {
message
.prompt({
- title: '创建智能体',
+ title: t('home.dialogTitle'),
msg: '',
- inputPlaceholder: '例如:客服助手、语音助理、知识问答',
+ inputPlaceholder: t('home.inputPlaceholder'),
inputValue: '',
inputPattern: /^[\u4E00-\u9FA5a-z0-9\s]{1,50}$/i,
- confirmButtonText: '立即创建',
- cancelButtonText: '取消',
+ confirmButtonText: t('home.createNow'),
+ cancelButtonText: t('common.cancel'),
})
.then(async (result: any) => {
if (result.value && String(result.value).trim()) {
@@ -144,12 +145,12 @@ function formatTime(timeStr: string) {
const diff = now.getTime() - date.getTime()
if (diff < 60000)
- return '刚刚'
+ return t('home.justNow')
if (diff < 3600000)
- return `${Math.floor(diff / 60000)}分钟前`
+ return `${Math.floor(diff / 60000)}${t('home.minutesAgo')}`
if (diff < 86400000)
- return `${Math.floor(diff / 3600000)}小时前`
- return `${Math.floor(diff / 86400000)}天前`
+ return `${Math.floor(diff / 3600000)}${t('home.hoursAgo')}`
+ return `${Math.floor(diff / 86400000)}${t('home.daysAgo')}`
}
// 页面显示时刷新列表
@@ -159,12 +160,20 @@ onShow(() => {
pagingRef.value.reload()
}
})
+
+// 在组件挂载后设置导航栏标题
+import { onMounted } from 'vue'
+onMounted(() => {
+ uni.setNavigationBarTitle({
+ title: t('home.pageTitle')
+ })
+})
- 你好,小智
+ {{ t('home.greeting') }}
- 让我们度过
- 美好的一天!
+ {{ t('home.subtitle') }}
+ {{ t('home.wonderfulDay') }}
-
- Hello, Let's have a wonderful day!
-
@@ -227,13 +233,13 @@ onShow(() => {
- 设备管理({{ agent.deviceCount }})
+ {{ t('home.deviceManagement') }}({{ agent.deviceCount }})
- 最近对话:{{ formatTime(agent.lastConnectedAt) }}
+ {{ t('home.lastConversation') }}{{ formatTime(agent.lastConnectedAt) }}
@@ -247,7 +253,7 @@ onShow(() => {
- 删除
+ {{ t('home.delete') }}
@@ -260,10 +266,10 @@ onShow(() => {
- 暂无智能体
+ {{ t('home.emptyState') }}
- 点击右下角 + 号创建您的第一个智能体
+ {{ t('home.createFirstAgent') }}
diff --git a/main/manager-mobile/src/pages/login/index.vue b/main/manager-mobile/src/pages/login/index.vue
index 71f53cce..00912902 100644
--- a/main/manager-mobile/src/pages/login/index.vue
+++ b/main/manager-mobile/src/pages/login/index.vue
@@ -3,7 +3,7 @@
"layout": "default",
"style": {
"navigationStyle": "custom",
- "navigationBarTitleText": "登陆"
+ "navigationBarTitleText": "Login"
}
}
@@ -15,6 +15,9 @@ import { login } from '@/api/auth'
import { useConfigStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast'
+// 导入国际化相关功能
+import { t, changeLanguage, getSupportedLanguages, initI18n } from '@/i18n'
+import type { Language } from '@/store/lang'
// 获取屏幕边界到安全区域距离
let safeAreaInsets
@@ -136,28 +139,28 @@ async function handleLogin() {
// 表单验证
if (loginType.value === 'username') {
if (!formData.value.username) {
- toast.warning('请输入用户名')
+ toast.warning(t('login.enterUsername'))
return
}
}
else {
if (!formData.value.mobile) {
- toast.warning('请输入手机号')
+ toast.warning(t('login.enterPhone'))
return
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) {
- toast.warning('请输入正确的手机号')
+ toast.warning(t('login.enterPhone'))
return
}
}
if (!formData.value.password) {
- toast.warning('请输入密码')
+ toast.warning(t('login.enterPassword'))
return
}
if (!formData.value.captcha) {
- toast.warning('请输入验证码')
+ toast.warning(t('login.enterCaptcha'))
return
}
@@ -177,7 +180,7 @@ async function handleLogin() {
uni.setStorageSync('token', response.token)
uni.setStorageSync('expire', response.expire)
- toast.success('登录成功')
+ toast.success(t('message.loginSuccess'))
// 跳转到主页
setTimeout(() => {
@@ -189,6 +192,10 @@ async function handleLogin() {
catch (error: any) {
// 登录失败重新获取验证码
refreshCaptcha()
+ // 处理验证码错误 - 从error.message中解析错误码
+ if (error.message.includes('请求错误[10067]')) {
+ toast.warning(t('login.captchaError'))
+ }
}
finally {
loading.value = false
@@ -200,14 +207,26 @@ onLoad(() => {
refreshCaptcha()
})
+// 语言切换相关
+const showLanguageSheet = ref(false)
+const supportedLanguages = getSupportedLanguages()
+
+// 初始化国际化
+initI18n()
+
+// 切换语言
+function handleLanguageChange(lang: Language) {
+ changeLanguage(lang)
+ showLanguageSheet.value = false
+}
+
// 组件挂载时确保配置已加载
onMounted(async () => {
if (!configStore.config.name) {
try {
await configStore.fetchPublicConfig()
- }
- catch (error) {
- console.error('获取配置失败:', error)
+ } catch (error) {
+ console.error(t('login.fetchConfigError'), error)
}
}
})
@@ -219,21 +238,25 @@ onMounted(async () => {
- 欢迎回来
+ {{ t('login.welcomeBack') }}
- 请登录您的账户
+ {{ t('login.pleaseLogin') }}
-
-
-
+
+
+
+
+ 🌐
+
+
+
+
+
+
@@ -253,7 +276,7 @@ onMounted(async () => {
v-model="formData.mobile"
custom-class="styled-input"
no-border
- placeholder="请输入手机号码"
+ :placeholder="t('login.enterPhone')"
type="number"
:maxlength="11"
/>
@@ -270,7 +293,7 @@ onMounted(async () => {
v-model="formData.username"
custom-class="styled-input"
no-border
- placeholder="请输入用户名"
+ :placeholder="t('login.enterUsername')"
/>
@@ -282,7 +305,7 @@ onMounted(async () => {
v-model="formData.password"
custom-class="styled-input"
no-border
- placeholder="请输入密码"
+ :placeholder="t('login.enterPassword')"
clearable
show-password
:maxlength="20"
@@ -296,7 +319,7 @@ onMounted(async () => {
v-model="formData.captcha"
custom-class="styled-input"
no-border
- placeholder="请输入验证码"
+ :placeholder="t('login.enterCaptcha')"
:maxlength="6"
/>
@@ -307,7 +330,7 @@ onMounted(async () => {
- 忘记密码?
+ {{ t('login.forgotPassword') }}
@@ -315,15 +338,15 @@ onMounted(async () => {
class="login-btn"
@click="handleLogin"
>
- {{ loading ? '登录中...' : '登录' }}
+ {{ loading ? t('login.loggingIn') : t('login.loginButton') }}
- 还没有账户?
+ {{ t('login.noAccount') }}
- 立即注册
+ {{ t('login.registerNow') }}
@@ -352,7 +375,7 @@ onMounted(async () => {
@@ -386,11 +409,33 @@ onMounted(async () => {
custom-class="confirm-btn"
@click="closeAreaCodeSheet"
>
- 确认
+ {{ t('login.confirm') }}
+
+
+
+
+
+
+
+ {{ lang.name }}
+
+
+
+
+
@@ -793,20 +838,52 @@ onMounted(async () => {
}
}
}
-.server-btn {
+// 右上角按钮组
+.top-right-buttons {
position: absolute;
- right: 20rpx; // 距离右边距
- top: 40rpx; // 顶部稍微下移,不贴状态栏
+ right: 20rpx;
+ display: flex;
+ gap: 20rpx;
+ z-index: 999;
+}
+
+// 语言切换按钮
+.lang-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
- z-index: 999;
cursor: pointer;
- background: rgba(255, 255, 255, 0.15); // 半透明背景,更好看
- border-radius: 24rpx; // 圆形按钮
- box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2); // 阴影
+ background: rgba(255, 255, 255, 0.15);
+ border-radius: 24rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ .lang-text-icon {
+ font-size: 28rpx;
+ color: #FFFFFF;
+ }
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.25);
+ }
+}
+
+// 服务端设置按钮
+.server-btn {
+ width: 48rpx;
+ height: 48rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ background: rgba(255, 255, 255, 0.15);
+ border-radius: 24rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.2);
&:active {
transform: scale(0.95);
@@ -814,11 +891,45 @@ onMounted(async () => {
.server-icon {
font-size: 28rpx;
- color: #FFFFFF; // 白色图标
+ color: #FFFFFF;
}
&:hover {
- background: rgba(255, 255, 255, 0.25); // 悬停效果
+ background: rgba(255, 255, 255, 0.25);
+ }
+}
+
+// 语言选择弹窗样式
+.language-sheet {
+ background: #ffffff;
+ border-radius: 24rpx 24rpx 0 0;
+ overflow: hidden;
+
+ .language-list {
+ max-height: 60vh;
+ padding: 0 40rpx;
+
+ .language-item {
+ display: flex;
+ align-items: center;
+ padding: 32rpx 0;
+ border-bottom: 1rpx solid #f8f9fa;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+
+ &:hover {
+ background-color: #f8f9fa;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .language-name {
+ font-size: 32rpx;
+ color: #333333;
+ }
+ }
}
}
diff --git a/main/manager-mobile/src/pages/register/index.vue b/main/manager-mobile/src/pages/register/index.vue
index 91463e1d..b78d61a6 100644
--- a/main/manager-mobile/src/pages/register/index.vue
+++ b/main/manager-mobile/src/pages/register/index.vue
@@ -14,6 +14,8 @@ import { register, sendSmsCode } from '@/api/auth'
import { useConfigStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
import { toast } from '@/utils/toast'
+// 导入国际化相关功能
+import { t, initI18n } from '@/i18n'
// 获取屏幕边界到安全区域距离
let safeAreaInsets
@@ -134,18 +136,18 @@ async function refreshCaptcha() {
// 发送短信验证码
async function sendSmsVerification() {
if (!formData.value.mobile) {
- toast.warning('请输入手机号')
+ toast.warning(t('register.enterPhone'))
return
}
if (!formData.value.captcha) {
- toast.warning('请输入图形验证码')
+ toast.warning(t('register.enterCode'))
return
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) {
- toast.warning('请输入正确的手机号')
+ toast.warning(t('register.enterPhone'))
return
}
@@ -157,7 +159,7 @@ async function sendSmsVerification() {
captchaId: formData.value.captchaId,
})
- toast.success('验证码发送成功')
+ toast.success(t('message.registerSuccess'))
// 开始倒计时
smsCountdown.value = 60
@@ -169,6 +171,10 @@ async function sendSmsVerification() {
}, 1000)
}
catch (error: any) {
+ // 处理验证码错误 - 从error.message中解析错误码
+ if (error.message.includes('请求错误[10067]')) {
+ toast.warning(t('login.captchaError'))
+ }
// 发送失败重新获取图形验证码
refreshCaptcha()
}
@@ -182,44 +188,44 @@ async function handleRegister() {
// 表单验证
if (registerType.value === 'username') {
if (!formData.value.username) {
- toast.warning('请输入用户名')
+ toast.warning(t('register.enterUsername'))
return
}
}
else {
if (!formData.value.mobile) {
- toast.warning('请输入手机号')
+ toast.warning(t('register.enterPhone'))
return
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.value.mobile)) {
- toast.warning('请输入正确的手机号')
+ toast.warning(t('register.enterPhone'))
return
}
if (!formData.value.mobileCaptcha) {
- toast.warning('请输入短信验证码')
+ toast.warning(t('register.enterCode'))
return
}
}
if (!formData.value.password) {
- toast.warning('请输入密码')
+ toast.warning(t('register.enterPassword'))
return
}
if (!formData.value.confirmPassword) {
- toast.warning('请确认密码')
+ toast.warning(t('register.confirmPassword'))
return
}
if (formData.value.password !== formData.value.confirmPassword) {
- toast.warning('两次输入的密码不一致')
+ toast.warning(t('register.confirmPassword'))
return
}
if (!formData.value.captcha) {
- toast.warning('请输入验证码')
+ toast.warning(t('register.enterCode'))
return
}
@@ -239,7 +245,7 @@ async function handleRegister() {
}
await register(registerData)
- toast.success('注册成功')
+ toast.success(t('message.registerSuccess'))
// 跳转到登录页
setTimeout(() => {
@@ -247,6 +253,10 @@ async function handleRegister() {
}, 1000)
}
catch (error: any) {
+ // 处理验证码错误 - 从error.message中解析错误码
+ if (error.message.includes('请求错误[10067]')) {
+ toast.warning(t('login.captchaError'))
+ }
// 注册失败重新获取验证码
refreshCaptcha()
}
@@ -275,7 +285,11 @@ onMounted(async () => {
console.error('获取配置失败:', error)
}
}
+ // 初始化国际化
+ initI18n()
})
+
+
@@ -287,10 +301,10 @@ onMounted(async () => {
- 欢迎注册
+ {{ t('register.pageTitle') }}
- 创建您的新账户
+ {{ t('register.createAccount') }}
@@ -312,7 +326,7 @@ onMounted(async () => {
v-model="formData.mobile"
custom-class="styled-input"
no-border
- placeholder="请输入手机号码"
+ :placeholder="t('register.enterPhone')"
type="number"
:maxlength="11"
/>
@@ -329,7 +343,7 @@ onMounted(async () => {
v-model="formData.username"
custom-class="styled-input"
no-border
- placeholder="请输入用户名"
+ :placeholder="t('register.enterUsername')"
/>
@@ -338,41 +352,41 @@ onMounted(async () => {
-
+ v-model="formData.password"
+ custom-class="styled-input"
+ no-border
+ :placeholder="t('register.enterPassword')"
+ show-password
+ :maxlength="20"
+ />
+
-
+ v-model="formData.confirmPassword"
+ custom-class="styled-input"
+ no-border
+ :placeholder="t('register.confirmPassword')"
+ show-password
+ :maxlength="20"
+ />
+
-
-
-
+ v-model="formData.captcha"
+ custom-class="styled-input"
+ no-border
+ :placeholder="t('register.enterCode')"
+ :maxlength="6"
+ />
+
+
+
@@ -380,21 +394,21 @@ onMounted(async () => {
-
- {{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
-
+ v-model="formData.mobileCaptcha"
+ custom-class="styled-input"
+ no-border
+ :placeholder="t('register.enterCode')"
+ type="number"
+ :maxlength="6"
+ />
+
+ {{ smsCountdown > 0 ? `${smsCountdown}s` : t('register.getCode') }}
+
@@ -404,15 +418,15 @@ onMounted(async () => {
:loading="loading"
@click="handleRegister"
>
- {{ loading ? '注册中...' : '注册' }}
+ {{ loading ? t('register.registering') : t('register.registerButton') }}
- 已有账户?
+ {{ t('register.haveAccount') }}
- 立即登录
+ {{ t('register.loginNow') }}
@@ -441,7 +455,7 @@ onMounted(async () => {
@@ -475,11 +489,13 @@ onMounted(async () => {
custom-class="confirm-btn"
@click="closeAreaCodeSheet"
>
- 确认
+ {{ t('register.confirm') }}
+
+
diff --git a/main/manager-mobile/src/pages/settings/index.vue b/main/manager-mobile/src/pages/settings/index.vue
index 220d2338..5a099bf9 100644
--- a/main/manager-mobile/src/pages/settings/index.vue
+++ b/main/manager-mobile/src/pages/settings/index.vue
@@ -11,6 +11,8 @@ import { clearServerBaseUrlOverride, getEnvBaseUrl, getServerBaseUrlOverride, se
import { isMp } from '@/utils/platform'
import { computed, onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni'
+import { t, changeLanguage, getSupportedLanguages, getCurrentLanguage } from '@/i18n'
+import type { Language } from '@/store/lang'
defineOptions({
name: 'SettingsPage',
@@ -62,7 +64,7 @@ function validateUrl() {
}
if (!/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
- urlError.value = '请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)'
+ urlError.value = t('settings.validServerUrl')
}
}
@@ -85,12 +87,12 @@ async function testServerBaseUrl() {
if (response.statusCode === 200) {
return true
} else {
- toast.error('无效地址,请检查服务端是否启动或网络连接是否正常')
+ toast.error(t('message.invalidAddress'))
return false
}
} catch (error) {
console.error('测试服务端地址失败:', error)
- toast.error('无效地址,请检查服务端是否启动或网络连接是否正常')
+ toast.error(t('message.invalidAddress'))
return false
}
}
@@ -98,7 +100,7 @@ async function testServerBaseUrl() {
// 保存服务端地址
async function saveServerBaseUrl() {
if (!baseUrlInput.value || !/^https?:\/\/.+\/xiaozhi$/.test(baseUrlInput.value)) {
- toast.warning('请输入有效的服务端地址(以 http 或 https 开头,并以 /xiaozhi 结尾)')
+ toast.warning(t('settings.validServerUrl'))
return
}
@@ -113,21 +115,32 @@ async function saveServerBaseUrl() {
clearAllCacheAfterUrlChange()
uni.showModal({
- title: '重启应用',
- content: '服务端地址已保存并清空缓存,是否立即重启生效?',
- confirmText: '立即重启',
- cancelText: '稍后',
+ title: t('settings.restartApp'),
+ content: t('settings.serverUrlSavedAndCacheCleared'),
+ confirmText: t('settings.restartNow'),
+ cancelText: t('settings.restartLater'),
success: (res) => {
if (res.confirm) {
restartApp()
}
else {
- toast.success('已保存,可稍后手动重启应用')
+ toast.success(t('settings.restartSuccess'))
}
},
})
}
+// 语言切换
+const supportedLanguages = getSupportedLanguages()
+const currentLanguage = ref(getCurrentLanguage())
+const showLanguageSheet = ref(false)
+
+function handleLanguageChange(lang: Language) {
+ changeLanguage(lang)
+ showLanguageSheet.value = false
+ toast.success(t('settings.languageChanged'))
+}
+
// 重置为 env 默认
function resetServerBaseUrl() {
clearServerBaseUrlOverride()
@@ -137,16 +150,16 @@ function resetServerBaseUrl() {
clearAllCacheAfterUrlChange()
uni.showModal({
- title: '重启应用',
- content: '已重置为默认地址并清空缓存,是否立即重启生效?',
- confirmText: '立即重启',
- cancelText: '稍后',
+ title: t('settings.restartApp'),
+ content: t('settings.resetToDefaultAndCacheCleared'),
+ confirmText: t('settings.restartNow'),
+ cancelText: t('settings.restartLater'),
success: (res) => {
if (res.confirm) {
restartApp()
}
else {
- toast.success('已重置,可稍后手动重启应用')
+ toast.success(t('settings.resetSuccess'))
}
},
})
@@ -185,8 +198,7 @@ function clearAllCacheAfterUrlChange() {
// 重新获取缓存信息
getCacheInfo()
- }
- catch (error) {
+ } catch (error) {
console.error('清除缓存失败:', error)
}
}
@@ -195,12 +207,14 @@ function clearAllCacheAfterUrlChange() {
async function clearCache() {
try {
uni.showModal({
- title: '确认清除',
- content: '确定要清除所有缓存吗?这将删除所有数据包括登录状态,需要重新登录。',
+ title: t('settings.confirmClear'),
+ content: t('settings.confirmClearMessage'),
+ confirmText: t('common.confirm'),
+ cancelText: t('common.cancel'),
success: (res) => {
if (res.confirm) {
clearAllCacheAfterUrlChange()
- toast.success('缓存清除成功,即将跳转到登录页')
+ toast.success(t('settings.cacheCleared'))
// 延迟跳转到登录页
setTimeout(() => {
@@ -209,22 +223,22 @@ async function clearCache() {
}
},
})
- }
- catch (error) {
+ } catch (error) {
console.error('清除缓存失败:', error)
- toast.error('清除缓存失败')
+ toast.error(t('settings.clearCacheFailed'))
}
}
// 关于我们
function showAbout() {
uni.showModal({
- 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`,
- title: `关于小智智控台`,
- content: `小智智控台\n\n基于 Vue.js 3 + uni-app 构建的跨平台移动端管理应用,为小智ESP32智能硬件提供设备管理、智能体配置等功能。\n\n© 2025 xiaozhi-esp32-server 0.8.3`,
+ title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
+ content: t('settings.aboutContent', {
+ appName: import.meta.env.VITE_APP_TITLE,
+ version: '0.8.3'
+ }),
showCancel: false,
- confirmText: '确定',
+ confirmText: t('common.confirm'),
})
}
@@ -234,19 +248,24 @@ onMounted(async () => {
loadServerBaseUrl()
}
getCacheInfo()
+
+ // 动态设置导航栏标题为国际化文本
+ uni.setNavigationBarTitle({
+ title: t('settings.title')
+ })
})
-
+
- 网络设置
+ {{ t('settings.networkSettings') }}
@@ -254,19 +273,19 @@ onMounted(async () => {
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
- 服务端接口地址
+ {{ t('settings.serverApiUrl') }}
- 修改后将自动清空缓存并重启应用
+ {{ t('settings.modifyWillClearCache') }}
+ :placeholder="t('settings.enterServerUrl')"
+ custom-class="!border-none !bg-transparent h-[64rpx] px-[24rpx] items-center"
+ input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl" />
{{ urlError }}
@@ -277,12 +296,12 @@ onMounted(async () => {
- 保存设置
+ {{ t('settings.saveSettings') }}
- 恢复默认
+ {{ t('settings.resetDefault') }}
@@ -292,7 +311,7 @@ onMounted(async () => {
- 缓存管理
+ {{ t('settings.cacheManagement') }}
@@ -304,11 +323,11 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
- 总缓存大小
-
-
- 应用数据总大小
-
+ {{ t('settings.totalCacheSize') }}
+
+
+ {{ t('settings.appDataSize') }}
+
{{ cacheInfo.storageSize }}
@@ -320,16 +339,16 @@ onMounted(async () => {
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
- 缓存清理
-
-
- 清空所有缓存数据
-
+ {{ t('settings.cacheClear') }}
+
+
+ {{ t('settings.clearAllCache') }}
+
- 清除缓存
+ {{ t('settings.clearCache') }}
@@ -340,8 +359,8 @@ onMounted(async () => {
- 应用信息
-
+ {{ t('settings.appInfo') }}
+
{
@click="showAbout">
- 关于我们
-
-
- 应用版本与团队信息
-
+ {{ t('settings.aboutUs') }}
+
+
+ {{ t('settings.appVersion') }}
+
+
+
+
+
+ {{ t('settings.languageSettings') }}
+
+
+
+
+
+
+
+ {{ t('settings.language') }}
+
+
+ {{ t('settings.selectLanguage') }}
+
+
+
+
+ {{ supportedLanguages.find(lang => lang.code === currentLanguage.value)?.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ lang.name }}
+
+
+
+
+
+
@@ -370,4 +440,27 @@ onMounted(async () => {
+// 保持与 edit.vue 一致的风格,样式主要通过类名控制
+
+// 语言选择弹窗样式
+.language-sheet {
+ .language-list {
+ max-height: 50vh;
+ .language-item {
+ padding: 30rpx 0;
+ text-align: center;
+ border-bottom: 1rpx solid #f0f0f0;
+ .language-name {
+ font-size: 28rpx;
+ color: #333;
+ }
+ &:last-child {
+ border-bottom: none;
+ }
+ &:active {
+ background-color: #f5f7fb;
+ }
+ }
+ }
+}
+
diff --git a/main/manager-mobile/src/pages/voiceprint/index.vue b/main/manager-mobile/src/pages/voiceprint/index.vue
index 5fe8bbad..3cca4442 100644
--- a/main/manager-mobile/src/pages/voiceprint/index.vue
+++ b/main/manager-mobile/src/pages/voiceprint/index.vue
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
import { useMessage } from 'wot-design-uni'
import { useToast } from 'wot-design-uni/components/wd-toast'
import { createVoicePrint, deleteVoicePrint, getChatHistory, getVoicePrintList, updateVoicePrint } from '@/api/voiceprint'
+import { t } from '@/i18n'
defineOptions({
name: 'VoicePrintManage',
@@ -81,7 +82,7 @@ async function loadVoicePrintList() {
// 检查是否有当前选中的智能体
if (!currentAgentId.value) {
- console.warn('没有选中的智能体')
+ console.warn(t('voiceprint.noSelectedAgent'))
voicePrintList.value = []
return
}
@@ -117,7 +118,7 @@ async function refresh() {
async function loadChatHistory() {
try {
if (!currentAgentId.value) {
- toast.error('请先选择智能体')
+ toast.error(t('voiceprint.pleaseSelectAgent'))
return
}
@@ -133,24 +134,47 @@ async function loadChatHistory() {
}
catch (error) {
console.error('获取对话记录失败:', error)
- toast.error('获取对话记录失败')
+ toast.error(t('voiceprint.fetchHistoryFailed'))
}
}
// 打开添加弹窗
function openAddDialog() {
if (!currentAgentId.value) {
- toast.error('请先选择智能体')
+ toast.error(t('voiceprint.pleaseSelectAgent'))
return
}
- addForm.value = {
- agentId: currentAgentId.value,
- audioId: '',
- sourceName: '',
- introduce: '',
+ // 检查声纹接口是否配置(通过尝试获取声纹列表来检测)
+ const checkVoicePrintConfig = async () => {
+ try {
+ await getVoicePrintList(currentAgentId.value)
+ // 接口正常,继续打开添加弹窗
+ addForm.value = {
+ agentId: currentAgentId.value,
+ audioId: '',
+ sourceName: '',
+ introduce: '',
+ }
+ showAddDialog.value = true
+ } catch (error: any) {
+ // 捕捉声纹接口未配置错误
+ if (error.message && error.message.includes('请求错误[10054]')) {
+ toast.error(t('voiceprint.voiceprintInterfaceNotConfigured'))
+ } else {
+ // 其他错误,继续打开弹窗
+ addForm.value = {
+ agentId: currentAgentId.value,
+ audioId: '',
+ sourceName: '',
+ introduce: '',
+ }
+ showAddDialog.value = true
+ }
+ }
}
- showAddDialog.value = true
+
+ checkVoicePrintConfig()
}
// 打开编辑弹窗
@@ -162,7 +186,7 @@ function openEditDialog(item: VoicePrint) {
// 获取选中音频的显示内容
function getSelectedAudioContent(audioId: string) {
if (!audioId)
- return '点击选择声纹向量'
+ return t('voiceprint.clickToSelectVector')
const chatItem = chatHistoryList.value.find(item => item.audioId === audioId)
return chatItem ? chatItem.content : `已选择: ${audioId.substring(0, 8)}...`
}
@@ -181,34 +205,34 @@ function selectAudioId({ item }: { item: any }) {
// 提交添加说话人
async function submitAdd() {
if (!addForm.value.sourceName.trim()) {
- toast.error('请输入姓名')
+ toast.error(t('voiceprint.pleaseInputName'))
return
}
if (!addForm.value.audioId) {
- toast.error('请选择声纹向量')
+ toast.error(t('voiceprint.pleaseSelectVector'))
return
}
try {
await createVoicePrint(addForm.value)
- toast.success('添加成功')
+ toast.success(t('voiceprint.addSuccess'))
showAddDialog.value = false
await loadVoicePrintList()
}
catch (error) {
console.error('添加说话人失败:', error)
- toast.error('添加说话人失败')
+ toast.error(t('voiceprint.addFailed'))
}
}
// 提交编辑说话人
async function submitEdit() {
if (!editForm.value.sourceName.trim()) {
- toast.error('请输入姓名')
+ toast.error(t('voiceprint.pleaseInputName'))
return
}
if (!editForm.value.audioId) {
- toast.error('请选择声纹向量')
+ toast.error(t('voiceprint.pleaseSelectVector'))
return
}
@@ -220,13 +244,13 @@ async function submitEdit() {
introduce: editForm.value.introduce,
createDate: editForm.value.createDate,
})
- toast.success('编辑成功')
+ toast.success(t('voiceprint.editSuccess'))
showEditDialog.value = false
await loadVoicePrintList()
}
catch (error) {
console.error('编辑说话人失败:', error)
- toast.error('编辑说话人失败')
+ toast.error(t('voiceprint.editFailed'))
}
}
@@ -239,11 +263,11 @@ function handleEdit(item: VoicePrint) {
// 删除声纹
async function handleDelete(id: string) {
message.confirm({
- msg: '确定要删除这个说话人吗?',
- title: '确认删除',
+ msg: t('voiceprint.deleteConfirmMsg'),
+ title: t('voiceprint.deleteConfirmTitle'),
}).then(async () => {
await deleteVoicePrint(id)
- toast.success('删除成功')
+ toast.success(t('voiceprint.deleteSuccess'))
await loadVoicePrintList()
}).catch(() => {
console.log('点击了取消按钮')
@@ -268,7 +292,7 @@ defineExpose({
- 加载中...
+ {{ t('voiceprint.loading') }}
@@ -302,7 +326,7 @@ defineExpose({
@click="handleDelete(item.id)"
>
- 删除
+ {{ t('voiceprint.delete') }}
@@ -315,11 +339,11 @@ defineExpose({
-
- 暂无声纹数据
+
+ {{ t('voiceprint.emptyTitle') }}
- 点击右下角 + 号添加您的第一个说话人
+ {{ t('voiceprint.emptyDesc') }}
@@ -341,7 +365,7 @@ defineExpose({
- 添加说话人
+ {{ t('voiceprint.addSpeaker') }}
@@ -349,7 +373,7 @@ defineExpose({
- * 声纹向量
+ * {{ t('voiceprint.voiceVector') }}
- * 姓名
+ * {{ t('voiceprint.name') }}
- * 描述
+ * {{ t('voiceprint.description') }}
@@ -394,11 +418,11 @@ defineExpose({
- 取消
-
-
- 保存
-
+ {{ t('voiceprint.cancel') }}
+
+
+ {{ t('voiceprint.save') }}
+
@@ -411,7 +435,7 @@ defineExpose({
- 编辑说话人
+ {{ t('voiceprint.editSpeaker') }}
@@ -419,7 +443,7 @@ defineExpose({
- * 声纹向量
+ * {{ t('voiceprint.voiceVector') }}
- * 姓名
+ * {{ t('voiceprint.name') }}
- * 描述
+ * {{ t('voiceprint.description') }}
@@ -464,18 +488,18 @@ defineExpose({
- 取消
-
-
- 保存
-
+ {{ t('voiceprint.cancel') }}
+
+
+ {{ t('voiceprint.save') }}
+
diff --git a/main/manager-mobile/src/store/lang.ts b/main/manager-mobile/src/store/lang.ts
new file mode 100644
index 00000000..38744a19
--- /dev/null
+++ b/main/manager-mobile/src/store/lang.ts
@@ -0,0 +1,41 @@
+import { ref } from 'vue'
+import { ref } from 'vue'
+import { defineStore } from 'pinia'
+
+// 支持的语言类型
+export type Language = 'zh_CN' | 'en' | 'zh_TW'
+
+export interface LangStore {
+ currentLang: Language
+ changeLang: (lang: Language) => void
+}
+
+export const useLangStore = defineStore(
+ 'lang',
+ (): LangStore => {
+ // 从本地存储获取语言设置,如果没有则使用默认值
+ const savedLang = uni.getStorageSync('app_language') as Language | null
+ const currentLang = ref(savedLang || 'zh_CN')
+
+ // 切换语言
+ const changeLang = (lang: Language) => {
+ currentLang.value = lang
+ // 将语言设置保存到本地存储
+ uni.setStorageSync('app_language', lang)
+ }
+
+ return {
+ currentLang,
+ changeLang,
+ }
+ },
+ {
+ persist: {
+ key: 'lang',
+ serializer: {
+ serialize: state => JSON.stringify(state.currentLang),
+ deserialize: value => ({ currentLang: JSON.parse(value) }),
+ },
+ },
+ },
+)
\ No newline at end of file