mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
Merge branch 'main' into update-web-style
This commit is contained in:
@@ -131,6 +131,7 @@ export function getMcpAddress(agentId: string) {
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
isExposeError: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,3 +60,13 @@ export function updateVoicePrint(data: VoicePrint) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取音频下载ID
|
||||
export function getAudioDownloadId(audioId: string) {
|
||||
return http.Post<string>(`/agent/audio/${audioId}`, {}, {
|
||||
meta: {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
|
||||
import type { IResponse } from './types'
|
||||
import type { Language } from '@/store/lang'
|
||||
import AdapterUniapp from '@alova/adapter-uniapp'
|
||||
import { createAlova } from 'alova'
|
||||
import { createServerTokenAuthentication } from 'alova/client'
|
||||
@@ -8,6 +9,16 @@ import { getEnvBaseUrl } from '@/utils'
|
||||
import { toast } from '@/utils/toast'
|
||||
import { ContentTypeEnum, ResultEnum, ShowMessage } from './enum'
|
||||
|
||||
// 语言映射, 用于设置 Accept-language 头
|
||||
const langMap: Record<Language, string> = {
|
||||
zh_CN: 'zh-CN',
|
||||
en: 'en-US',
|
||||
zh_TW: 'zh-TW',
|
||||
de: 'de',
|
||||
vi: 'vi',
|
||||
pt_BR: 'pt-BR',
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求实例
|
||||
*/
|
||||
@@ -51,6 +62,7 @@ const alovaInstance = createAlova({
|
||||
// 检查混合内容错误(HTTPS页面请求HTTP接口)
|
||||
const currentProtocol = typeof window !== 'undefined' && window.location.protocol
|
||||
const requestProtocol = method.baseURL?.split(':')[0]
|
||||
const currentLang = langMap[uni.getStorageSync('app_language') as Language || 'zh_CN']
|
||||
if (currentProtocol === 'https:' && requestProtocol === 'http') {
|
||||
const errorMessage = '无法配置http协议地址,请检查接口地址'
|
||||
throw new Error(errorMessage)
|
||||
@@ -60,6 +72,7 @@ const alovaInstance = createAlova({
|
||||
method.config.headers = {
|
||||
'Content-Type': ContentTypeEnum.JSON,
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-language': currentLang,
|
||||
...method.config.headers,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'login.navigationTitle': 'Anmelden',
|
||||
'login.fetchConfigError': 'Konfiguration konnte nicht abgerufen werden:',
|
||||
'login.selectLanguage': 'Sprache auswählen',
|
||||
'login.selectLanguageTip': 'Vi',
|
||||
'login.selectLanguageTip': 'De',
|
||||
'login.welcomeBack': 'Willkommen zurück',
|
||||
'login.pleaseLogin': 'Bitte melden Sie sich an',
|
||||
'login.enterUsername': 'Bitte Benutzernamen eingeben',
|
||||
@@ -492,4 +492,9 @@ export default {
|
||||
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'Nach erfolgreicher Konfiguration startet Gerät automatisch neu',
|
||||
'deviceConfig.audioPlaybackError': 'Audio-Wiedergabe-Fehler',
|
||||
'deviceConfig.playbackFailed': 'Wiedergabe fehlgeschlagen',
|
||||
|
||||
// Voiceprint page
|
||||
'voiceprint.audioNotExist': 'Audio existiert nicht',
|
||||
'voiceprint.getAudioFailed': 'Audio konnte nicht abgerufen werden',
|
||||
'voiceprint.audioPlayFailed': 'Audio-Wiedergabe fehlgeschlagen',
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'login.navigationTitle': 'Login',
|
||||
'login.fetchConfigError': 'Failed to fetch configuration:',
|
||||
'login.selectLanguage': 'Select Language',
|
||||
'login.selectLanguageTip': '中文',
|
||||
'login.selectLanguageTip': 'En',
|
||||
'login.welcomeBack': 'Welcome Back',
|
||||
'login.pleaseLogin': 'Please log in to your account',
|
||||
'login.enterUsername': 'Please enter username',
|
||||
@@ -492,4 +492,9 @@ export default {
|
||||
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'After successful configuration, device will automatically restart',
|
||||
'deviceConfig.audioPlaybackError': 'Audio playback error',
|
||||
'deviceConfig.playbackFailed': 'Playback failed',
|
||||
|
||||
// Voiceprint page
|
||||
'voiceprint.audioNotExist': 'Audio does not exist',
|
||||
'voiceprint.getAudioFailed': 'Failed to get audio',
|
||||
'voiceprint.audioPlayFailed': 'Audio playback failed',
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'login.navigationTitle': 'Entrar',
|
||||
'login.fetchConfigError': 'Falha ao buscar configuração:',
|
||||
'login.selectLanguage': 'Selecionar Idioma',
|
||||
'login.selectLanguageTip': '中文',
|
||||
'login.selectLanguageTip': 'Pt',
|
||||
'login.welcomeBack': 'Bem-vindo de Volta',
|
||||
'login.pleaseLogin': 'Por favor, entre na sua conta',
|
||||
'login.enterUsername': 'Por favor, insira o nome de usuário',
|
||||
@@ -492,4 +492,9 @@ export default {
|
||||
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'Após a configuração bem-sucedida, o dispositivo reiniciará automaticamente',
|
||||
'deviceConfig.audioPlaybackError': 'Erro na reprodução de áudio',
|
||||
'deviceConfig.playbackFailed': 'Falha na reprodução',
|
||||
|
||||
// Voiceprint page
|
||||
'voiceprint.audioNotExist': 'O áudio não existe',
|
||||
'voiceprint.getAudioFailed': 'Falha ao obter áudio',
|
||||
'voiceprint.audioPlayFailed': 'Falha na reprodução de áudio',
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'login.navigationTitle': 'Đăng nhập',
|
||||
'login.fetchConfigError': 'Không thể tải cấu hình:',
|
||||
'login.selectLanguage': 'Chọn ngôn ngữ',
|
||||
'login.selectLanguageTip': 'de',
|
||||
'login.selectLanguageTip': 'Vi',
|
||||
'login.welcomeBack': 'Chào mừng trở lại',
|
||||
'login.pleaseLogin': 'Vui lòng đăng nhập vào tài khoản của bạn',
|
||||
'login.enterUsername': 'Vui lòng nhập tên đăng nhập',
|
||||
@@ -492,4 +492,9 @@ export default {
|
||||
'deviceConfig.afterConfigSuccessDeviceWillRestart': 'Sau khi cấu hình thành công, thiết bị sẽ tự động khởi động lại',
|
||||
'deviceConfig.audioPlaybackError': 'Lỗi phát âm thanh',
|
||||
'deviceConfig.playbackFailed': 'Phát thất bại',
|
||||
|
||||
// Voiceprint page
|
||||
'voiceprint.audioNotExist': 'Âm thanh không tồn tại',
|
||||
'voiceprint.getAudioFailed': 'Không thể lấy âm thanh',
|
||||
'voiceprint.audioPlayFailed': 'Phát âm thanh thất bại',
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'login.navigationTitle': '登录',
|
||||
'login.fetchConfigError': '获取配置失败:',
|
||||
'login.selectLanguage': '选择语言',
|
||||
'login.selectLanguageTip': 'En',
|
||||
'login.selectLanguageTip': '中文',
|
||||
'login.welcomeBack': '欢迎回来',
|
||||
'login.pleaseLogin': '请登录您的账户',
|
||||
'login.enterUsername': '请输入用户名',
|
||||
@@ -492,4 +492,9 @@ export default {
|
||||
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配网成功后设备将自动重启',
|
||||
'deviceConfig.audioPlaybackError': '音频播放错误',
|
||||
'deviceConfig.playbackFailed': '播放失败',
|
||||
|
||||
// Voiceprint page
|
||||
'voiceprint.audioNotExist': '该音频不存在',
|
||||
'voiceprint.getAudioFailed': '获取音频失败',
|
||||
'voiceprint.audioPlayFailed': '音频播放失败',
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
'login.navigationTitle': '登錄',
|
||||
'login.fetchConfigError': '獲取配置失敗:',
|
||||
'login.selectLanguage': '選擇語言',
|
||||
'login.selectLanguageTip': '简体',
|
||||
'login.selectLanguageTip': '繁體',
|
||||
'login.welcomeBack': '歡迎回來',
|
||||
'login.pleaseLogin': '請登錄您的賬戶',
|
||||
'login.enterUsername': '請輸入用戶名',
|
||||
@@ -492,4 +492,9 @@ export default {
|
||||
'deviceConfig.afterConfigSuccessDeviceWillRestart': '配網成功後設備將自動重啟',
|
||||
'deviceConfig.audioPlaybackError': '音頻播放錯誤',
|
||||
'deviceConfig.playbackFailed': '播放失敗',
|
||||
|
||||
// Voiceprint page
|
||||
'voiceprint.audioNotExist': '該音頻不存在',
|
||||
'voiceprint.getAudioFailed': '獲取音頻失敗',
|
||||
'voiceprint.audioPlayFailed': '音頻播放失敗',
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@ const inputVisible = ref(false)
|
||||
const languageOptions = ref([])
|
||||
const isVisibleReport = ref(false)
|
||||
|
||||
// 音频播放相关
|
||||
const audioRef = ref<UniApp.InnerAudioContext | null>(null)
|
||||
const playingVoiceId = ref<string>('')
|
||||
|
||||
// 使用插件store
|
||||
const pluginStore = usePluginStore()
|
||||
const speedPitchStore = useSpeedPitch()
|
||||
@@ -422,15 +426,24 @@ function selectRoleTemplate(templateId: string) {
|
||||
selectedTemplateId.value = templateId
|
||||
const template = roleTemplates.value.find(t => t.id === templateId)
|
||||
if (template) {
|
||||
formData.value.systemPrompt = template.systemPrompt
|
||||
formData.value.vadModelId = template.vadModelId
|
||||
formData.value.asrModelId = template.asrModelId
|
||||
formData.value.llmModelId = template.llmModelId
|
||||
formData.value.vllmModelId = template.vllmModelId
|
||||
formData.value.intentModelId = template.intentModelId
|
||||
formData.value.memModelId = template.memModelId
|
||||
formData.value.ttsModelId = template.ttsModelId
|
||||
formData.value.ttsVoiceId = template.ttsVoiceId
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
systemPrompt: template.systemPrompt || formData.value.systemPrompt,
|
||||
vadModelId: template.vadModelId || formData.value.vadModelId,
|
||||
asrModelId: template.asrModelId || formData.value.asrModelId,
|
||||
llmModelId: template.llmModelId || formData.value.llmModelId,
|
||||
vllmModelId: template.vllmModelId || formData.value.vllmModelId,
|
||||
intentModelId: template.intentModelId || formData.value.intentModelId,
|
||||
memModelId: template.memModelId || formData.value.memModelId,
|
||||
ttsModelId: template.ttsModelId || formData.value.ttsModelId,
|
||||
ttsVoiceId: template.ttsVoiceId || formData.value.ttsVoiceId,
|
||||
agentName: template.agentName || formData.value.agentName,
|
||||
chatHistoryConf: template.chatHistoryConf || formData.value.chatHistoryConf,
|
||||
summaryMemory: template.summaryMemory || formData.value.summaryMemory,
|
||||
langCode: template.langCode || formData.value.langCode,
|
||||
}
|
||||
fetchAllLanguag(template.ttsModelId || formData.value.ttsModelId)
|
||||
updateDisplayNames()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +506,57 @@ async function onPickerConfirm(type: string, value: any, name: string) {
|
||||
// 选择器取消
|
||||
function onPickerCancel(type: string) {
|
||||
pickerShow.value[type] = false
|
||||
// 关闭时停止播放
|
||||
if (type === 'voiceprint') {
|
||||
stopAudio()
|
||||
}
|
||||
}
|
||||
|
||||
// 播放音频
|
||||
function playAudio(voiceDemo: string, voiceId: string, event: Event) {
|
||||
event.stopPropagation() // 阻止事件冒泡,防止关闭下拉框
|
||||
|
||||
if (!voiceDemo) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果正在播放同一个音频,则停止
|
||||
if (playingVoiceId.value === voiceId) {
|
||||
stopAudio()
|
||||
return
|
||||
}
|
||||
|
||||
// 停止之前的音频
|
||||
stopAudio()
|
||||
|
||||
// 创建新的音频实例
|
||||
audioRef.value = uni.createInnerAudioContext()
|
||||
audioRef.value.src = voiceDemo
|
||||
playingVoiceId.value = voiceId
|
||||
|
||||
// 监听播放结束
|
||||
audioRef.value.onEnded(() => {
|
||||
playingVoiceId.value = ''
|
||||
})
|
||||
|
||||
// 监听播放错误
|
||||
audioRef.value.onError(() => {
|
||||
toast.error('音频播放失败')
|
||||
playingVoiceId.value = ''
|
||||
})
|
||||
|
||||
// 播放音频
|
||||
audioRef.value.play()
|
||||
}
|
||||
|
||||
// 停止音频
|
||||
function stopAudio() {
|
||||
if (audioRef.value) {
|
||||
audioRef.value.stop()
|
||||
audioRef.value.destroy()
|
||||
audioRef.value = null
|
||||
}
|
||||
playingVoiceId.value = ''
|
||||
}
|
||||
|
||||
// 获取模型显示名称
|
||||
@@ -946,12 +1010,30 @@ onMounted(async () => {
|
||||
@select="({ item }) => onPickerConfirm('tts', item.value, item.name)"
|
||||
/>
|
||||
|
||||
<wd-action-sheet
|
||||
v-model="pickerShow.voiceprint"
|
||||
:actions="voiceOptions"
|
||||
@close="onPickerCancel('voiceprint')"
|
||||
@select="({ item }) => onPickerConfirm('voiceprint', item.value, item.name)"
|
||||
/>
|
||||
<!-- 自定义语音选择弹出层 -->
|
||||
<wd-popup v-model="pickerShow.voiceprint" class="custom-popup" position="bottom" @close="onPickerCancel('voiceprint')">
|
||||
<view class="overflow-hidden rounded-[20rpx] bg-white pb-[20rpx] pt-[20rpx]">
|
||||
<view class="max-h-[600rpx] overflow-y-auto">
|
||||
<view
|
||||
v-for="voice in voiceOptions"
|
||||
:key="voice.value"
|
||||
class="flex items-center justify-between border-b border-[#f5f5f5] p-[32rpx] transition-all active:bg-[#f5f7fb]"
|
||||
@click="onPickerConfirm('voiceprint', voice.value, voice.name)"
|
||||
>
|
||||
<text :class="`flex-1 text-[28rpx] text-[#232338] ${(voice.voiceDemo || voice.voice_demo) ? '' : 'text-center'}`">
|
||||
{{ voice.name }}
|
||||
</text>
|
||||
<view v-if="voice.voiceDemo || voice.voice_demo" class="ml-[20rpx]" @click.stop="playAudio(voice.voiceDemo || voice.voice_demo, voice.value, $event)">
|
||||
<wd-icon
|
||||
:name="playingVoiceId === voice.value ? 'pause-circle' : 'play-circle'"
|
||||
size="24px"
|
||||
:custom-class="playingVoiceId === voice.value ? 'text-[#336cff]' : 'text-[#9d9ea3]'"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
<wd-action-sheet
|
||||
v-model="pickerShow.language"
|
||||
:actions="languageOptions"
|
||||
@@ -971,4 +1053,10 @@ onMounted(async () => {
|
||||
::v-deep .wd-tag__close {
|
||||
color: #336cff !important;
|
||||
}
|
||||
::v-deep .custom-popup {
|
||||
.wd-popup {
|
||||
padding: 20rpx !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,17 +51,18 @@ const currentTab = ref('agent-config')
|
||||
|
||||
// 刷新和加载状态
|
||||
const refreshing = ref(false)
|
||||
|
||||
// 计算是否启用下拉刷新(角色编辑页面不启用)
|
||||
const refresherEnabled = computed(() => {
|
||||
return currentTab.value !== 'agent-config'
|
||||
})
|
||||
const refresherEnabled = ref(false)
|
||||
|
||||
// 子组件引用
|
||||
const deviceRef = ref()
|
||||
const chatRef = ref()
|
||||
const voiceprintRef = ref()
|
||||
|
||||
// 更新刷新器状态
|
||||
function updateRefresherEnabled(value: boolean) {
|
||||
refresherEnabled.value = value
|
||||
}
|
||||
|
||||
// Tab 配置
|
||||
const tabList = [
|
||||
{
|
||||
@@ -144,6 +145,10 @@ async function onLoadMore() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => currentTab.value, (newTab) => {
|
||||
updateRefresherEnabled(newTab !== 'agent-config')
|
||||
})
|
||||
|
||||
// 接收页面参数
|
||||
onLoad((options) => {
|
||||
if (options?.agentId) {
|
||||
@@ -204,6 +209,7 @@ onMounted(async () => {
|
||||
v-else-if="currentTab === 'voiceprint-management'"
|
||||
ref="voiceprintRef"
|
||||
:agent-id="currentAgentId"
|
||||
@update-refresher-enabled="updateRefresherEnabled"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
@@ -70,6 +70,7 @@ async function mergeFunctions() {
|
||||
uni.setStorageSync(`cachedMcpAddress_${agentId.value}`, address)
|
||||
}
|
||||
catch (error) {
|
||||
mcpAddress.value = error
|
||||
console.error('获取MCP地址失败:', error)
|
||||
}
|
||||
|
||||
|
||||
@@ -127,10 +127,10 @@ async function saveServerBaseUrl() {
|
||||
uni.request({
|
||||
url: `${getEnvBaseUrl()}/user/pub-config`,
|
||||
method: 'GET',
|
||||
success: (res) => {
|
||||
success: (res: any) => {
|
||||
if (res.statusCode === 200) {
|
||||
configStore.setConfig(res.data.data)
|
||||
uni.setStorageSync('config', res.data.data.sm2PubKey)
|
||||
uni.setStorageSync('config', res.data.data)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
@@ -165,6 +165,7 @@ const showLanguageSheet = ref(false)
|
||||
function handleLanguageChange(lang: Language) {
|
||||
changeLanguage(lang)
|
||||
showLanguageSheet.value = false
|
||||
currentLanguage.value = lang
|
||||
toast.success(t('settings.languageChanged'))
|
||||
}
|
||||
|
||||
@@ -225,7 +226,8 @@ function clearAllCacheAfterUrlChange() {
|
||||
|
||||
// 重新获取缓存信息
|
||||
getCacheInfo()
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('清除缓存失败:', error)
|
||||
}
|
||||
}
|
||||
@@ -250,7 +252,8 @@ async function clearCache() {
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('清除缓存失败:', error)
|
||||
toast.error(t('settings.clearCacheFailed'))
|
||||
}
|
||||
@@ -278,7 +281,7 @@ onMounted(async () => {
|
||||
|
||||
// 动态设置导航栏标题为国际化文本
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('settings.title')
|
||||
title: t('settings.title'),
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -296,8 +299,10 @@ onMounted(async () => {
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx] overflow-hidden"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
|
||||
<view
|
||||
class="overflow-hidden border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
|
||||
>
|
||||
<view class="mb-[24rpx]">
|
||||
<text class="text-[28rpx] text-[#232338] font-semibold">
|
||||
{{ t('settings.serverApiUrl') }}
|
||||
@@ -308,11 +313,13 @@ onMounted(async () => {
|
||||
</view>
|
||||
|
||||
<view class="mb-[24rpx]">
|
||||
<view class="w-full rounded-[16rpx] border border-[#eeeeee] bg-[#f5f7fb] overflow-hidden">
|
||||
<wd-input v-model="baseUrlInput" type="text" clearable :maxlength="200"
|
||||
<view class="w-full overflow-hidden border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb]">
|
||||
<wd-input
|
||||
v-model="baseUrlInput" type="text" clearable :maxlength="200"
|
||||
: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" />
|
||||
input-class="text-[28rpx] text-[#232338]" @input="validateUrl" @blur="validateUrl"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="urlError" class="mt-[8rpx] block text-[24rpx] text-[#ff4d4f]">
|
||||
{{ urlError }}
|
||||
@@ -320,14 +327,18 @@ onMounted(async () => {
|
||||
</view>
|
||||
|
||||
<view class="flex gap-[16rpx]">
|
||||
<wd-button type="primary"
|
||||
<wd-button
|
||||
type="primary"
|
||||
custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-[#336cff] border-none shadow-[0_4rpx_16rpx_rgba(51,108,255,0.3)] active:shadow-[0_2rpx_8rpx_rgba(51,108,255,0.4)] active:scale-98"
|
||||
@click="saveServerBaseUrl">
|
||||
@click="saveServerBaseUrl"
|
||||
>
|
||||
{{ t('settings.saveSettings') }}
|
||||
</wd-button>
|
||||
<wd-button type="default"
|
||||
<wd-button
|
||||
type="default"
|
||||
custom-class="flex-1 h-[88rpx] rounded-[20rpx] text-[28rpx] font-semibold bg-white border-[#eeeeee] text-[#65686f] active:bg-[#f5f7fb]"
|
||||
@click="resetServerBaseUrl">
|
||||
@click="resetServerBaseUrl"
|
||||
>
|
||||
{{ t('settings.resetDefault') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -342,12 +353,15 @@ onMounted(async () => {
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
|
||||
<view
|
||||
class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
|
||||
>
|
||||
<view class="space-y-[16rpx]">
|
||||
<!-- 缓存信息展示,参考插件样式 -->
|
||||
<view
|
||||
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]">
|
||||
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
|
||||
>
|
||||
<view>
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.totalCacheSize') }}
|
||||
@@ -363,7 +377,8 @@ onMounted(async () => {
|
||||
|
||||
<!-- 清除缓存按钮,参考插件编辑按钮样式 -->
|
||||
<view
|
||||
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]">
|
||||
class="flex items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx]"
|
||||
>
|
||||
<view>
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.cacheClear') }}
|
||||
@@ -374,7 +389,8 @@ onMounted(async () => {
|
||||
</view>
|
||||
<view
|
||||
class="cursor-pointer rounded-[24rpx] bg-[rgba(255,107,107,0.1)] px-[28rpx] py-[16rpx] text-[24rpx] text-[#ff6b6b] font-semibold transition-all duration-300 active:scale-95 active:bg-[#ff6b6b] active:text-white"
|
||||
@click="clearCache">
|
||||
@click="clearCache"
|
||||
>
|
||||
{{ t('settings.clearCache') }}
|
||||
</view>
|
||||
</view>
|
||||
@@ -390,11 +406,14 @@ onMounted(async () => {
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
|
||||
<view
|
||||
class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
|
||||
>
|
||||
<view
|
||||
class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
|
||||
@click="showAbout">
|
||||
@click="showAbout"
|
||||
>
|
||||
<view>
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.aboutUs') }}
|
||||
@@ -416,11 +435,14 @@ onMounted(async () => {
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);">
|
||||
<view
|
||||
class="border border-[#eeeeee] rounded-[24rpx] bg-[#fbfbfb] p-[32rpx]"
|
||||
style="box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);"
|
||||
>
|
||||
<view
|
||||
class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[16rpx] bg-[#f5f7fb] p-[24rpx] transition-all active:bg-[#eef3ff]"
|
||||
@click="showLanguageSheet = true">
|
||||
@click="showLanguageSheet = true"
|
||||
>
|
||||
<view>
|
||||
<text class="text-[32rpx] text-[#232338] font-medium">
|
||||
{{ t('settings.language') }}
|
||||
@@ -430,8 +452,8 @@ onMounted(async () => {
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<text class="text-[32rpx] text-[#9d9ea3] font-semibold mr-[16rpx]">
|
||||
{{supportedLanguages.find(lang => lang.code === currentLanguage)?.name}}
|
||||
<text class="mr-[16rpx] text-[32rpx] text-[#9d9ea3] font-semibold">
|
||||
{{ supportedLanguages.find(lang => lang.code === currentLanguage)?.name }}
|
||||
</text>
|
||||
<wd-icon name="arrow-right" custom-class="text-[32rpx] text-[#9d9ea3]" />
|
||||
</view>
|
||||
@@ -443,8 +465,10 @@ onMounted(async () => {
|
||||
<wd-action-sheet v-model="showLanguageSheet" :title="t('settings.selectLanguage')" :close-on-click-modal="true">
|
||||
<view class="language-sheet">
|
||||
<scroll-view scroll-y class="language-list">
|
||||
<view v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
|
||||
@click="handleLanguageChange(lang.code)">
|
||||
<view
|
||||
v-for="lang in supportedLanguages" :key="lang.code" class="language-item"
|
||||
@click="handleLanguageChange(lang.code)"
|
||||
>
|
||||
<text class="language-name">
|
||||
{{ lang.name }}
|
||||
</text>
|
||||
|
||||
@@ -3,22 +3,25 @@ import type { ChatHistory, CreateSpeakerData, VoicePrint } from '@/api/voiceprin
|
||||
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 { createVoicePrint, deleteVoicePrint, getAudioDownloadId, getChatHistory, getVoicePrintList, updateVoicePrint } from '@/api/voiceprint'
|
||||
import { t } from '@/i18n'
|
||||
import { getEnvBaseUrl } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'VoicePrintManage',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
agentId: 'default',
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update-refresher-enabled'])
|
||||
|
||||
// 接收props
|
||||
interface Props {
|
||||
agentId?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
agentId: 'default'
|
||||
})
|
||||
|
||||
// 获取屏幕边界到安全区域距离
|
||||
let safeAreaInsets: any
|
||||
let systemInfo: any
|
||||
@@ -50,6 +53,10 @@ const chatHistoryActions = ref<any[]>([])
|
||||
const swipeStates = ref<Record<string, 'left' | 'close' | 'right'>>({})
|
||||
const loading = ref(false)
|
||||
|
||||
// 音频播放相关
|
||||
const audioRef = ref<UniApp.InnerAudioContext | null>(null)
|
||||
const playingAudioId = ref<string>('')
|
||||
|
||||
// 使用传入的智能体ID
|
||||
const currentAgentId = computed(() => {
|
||||
return props.agentId
|
||||
@@ -157,11 +164,13 @@ function openAddDialog() {
|
||||
introduce: '',
|
||||
}
|
||||
showAddDialog.value = true
|
||||
} catch (error: any) {
|
||||
}
|
||||
catch (error: any) {
|
||||
// 捕捉声纹接口未配置错误
|
||||
if (error.message && error.message.includes('请求错误[10054]')) {
|
||||
toast.error(t('voiceprint.voiceprintInterfaceNotConfigured'))
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// 其他错误,继续打开弹窗
|
||||
addForm.value = {
|
||||
agentId: currentAgentId.value,
|
||||
@@ -202,6 +211,11 @@ function selectAudioId({ item }: { item: any }) {
|
||||
showChatHistoryDialog.value = false
|
||||
}
|
||||
|
||||
// 点击选择
|
||||
function handleItemClick(item: any) {
|
||||
selectAudioId({ item })
|
||||
}
|
||||
|
||||
// 提交添加说话人
|
||||
async function submitAdd() {
|
||||
if (!addForm.value.sourceName.trim()) {
|
||||
@@ -274,6 +288,82 @@ async function handleDelete(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
// 播放音频
|
||||
async function playAudio(audioId: string, event: Event) {
|
||||
event.stopPropagation() // 阻止事件冒泡,防止关闭下拉框
|
||||
|
||||
if (!audioId) {
|
||||
toast.warning(t('voiceprint.audioNotExist'))
|
||||
return
|
||||
}
|
||||
|
||||
// 如果正在播放同一个音频,则停止
|
||||
if (playingAudioId.value === audioId) {
|
||||
stopAudio()
|
||||
return
|
||||
}
|
||||
|
||||
// 停止之前的音频
|
||||
stopAudio()
|
||||
|
||||
try {
|
||||
// 先获取音频下载ID
|
||||
playingAudioId.value = audioId
|
||||
const downloadId = await getAudioDownloadId(audioId)
|
||||
|
||||
if (!downloadId) {
|
||||
toast.error(t('voiceprint.getAudioFailed'))
|
||||
playingAudioId.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 获取baseURL
|
||||
const baseURL = getEnvBaseUrl()
|
||||
const audioUrl = `${baseURL}/agent/play/${downloadId}`
|
||||
|
||||
// 创建新的音频实例
|
||||
audioRef.value = uni.createInnerAudioContext()
|
||||
audioRef.value.src = audioUrl
|
||||
audioRef.value.autoplay = true
|
||||
|
||||
// 监听播放结束
|
||||
audioRef.value.onEnded(() => {
|
||||
playingAudioId.value = ''
|
||||
})
|
||||
|
||||
// 监听播放错误
|
||||
audioRef.value.onError((error) => {
|
||||
console.error('音频播放错误:', error)
|
||||
toast.error(t('voiceprint.audioPlayFailed'))
|
||||
playingAudioId.value = ''
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('播放音频失败:', error)
|
||||
toast.error(t('voiceprint.audioPlayFailed'))
|
||||
playingAudioId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 停止音频
|
||||
function stopAudio() {
|
||||
if (audioRef.value) {
|
||||
audioRef.value.stop()
|
||||
audioRef.value.destroy()
|
||||
audioRef.value = null
|
||||
}
|
||||
playingAudioId.value = ''
|
||||
}
|
||||
|
||||
watch(() => [showAddDialog.value, showEditDialog.value], (newValues) => {
|
||||
if (newValues.some((value: boolean) => value)) {
|
||||
emits('update-refresher-enabled', false)
|
||||
}
|
||||
else {
|
||||
emits('update-refresher-enabled', true)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 智能体已简化为默认
|
||||
|
||||
@@ -282,6 +372,8 @@ onMounted(async () => {
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
showAddDialog,
|
||||
showEditDialog,
|
||||
refresh,
|
||||
})
|
||||
</script>
|
||||
@@ -349,7 +441,7 @@ defineExpose({
|
||||
</view>
|
||||
|
||||
<!-- 浮动操作按钮 -->
|
||||
<wd-fab type="primary" size="small" :draggable="true" :expandable="false" @click="openAddDialog">
|
||||
<wd-fab custom-style="z-index:10" type="primary" size="small" :draggable="true" :expandable="false" @click="openAddDialog">
|
||||
<wd-icon name="add" />
|
||||
</wd-fab>
|
||||
|
||||
@@ -359,20 +451,16 @@ defineExpose({
|
||||
|
||||
<!-- 添加说话人弹窗 -->
|
||||
<wd-popup
|
||||
v-model="showAddDialog" position="center" custom-style="width: 90%; max-width: 400px; border-radius: 16px;"
|
||||
v-model="showAddDialog"
|
||||
position="center"
|
||||
custom-style="width: 90%; max-width: 400px; border-radius: 16px;"
|
||||
safe-area-inset-bottom
|
||||
>
|
||||
<view>
|
||||
<view class="w-full flex items-center justify-between border-b-[2rpx] border-[#eeeeee] p-[32rpx_32rpx_24rpx]">
|
||||
<text class="w-full text-center text-[32rpx] text-[#232338] font-semibold">
|
||||
{{ t('voiceprint.addSpeaker') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="p-[32rpx]">
|
||||
<!-- 声纹向量选择 -->
|
||||
<view class="mb-[32rpx]">
|
||||
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
|
||||
<text class="mb-[16rpx] block text-[28rpx] text-red font-medium">
|
||||
* {{ t('voiceprint.voiceVector') }}
|
||||
</text>
|
||||
<view
|
||||
@@ -391,7 +479,7 @@ defineExpose({
|
||||
|
||||
<!-- 姓名 -->
|
||||
<view class="mb-[32rpx]">
|
||||
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
|
||||
<text class="mb-[16rpx] block text-[28rpx] text-red font-medium">
|
||||
* {{ t('voiceprint.name') }}
|
||||
</text>
|
||||
<input
|
||||
@@ -403,7 +491,7 @@ defineExpose({
|
||||
|
||||
<!-- 描述 -->
|
||||
<view>
|
||||
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-medium">
|
||||
<text class="mb-[16rpx] block text-[28rpx] text-red font-medium">
|
||||
* {{ t('voiceprint.description') }}
|
||||
</text>
|
||||
<textarea
|
||||
@@ -418,11 +506,11 @@ defineExpose({
|
||||
|
||||
<view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]">
|
||||
<wd-button type="info" custom-class="flex-1" @click="showAddDialog = false">
|
||||
{{ t('voiceprint.cancel') }}
|
||||
</wd-button>
|
||||
<wd-button type="primary" custom-class="flex-1" @click="submitAdd">
|
||||
{{ t('voiceprint.save') }}
|
||||
</wd-button>
|
||||
{{ t('voiceprint.cancel') }}
|
||||
</wd-button>
|
||||
<wd-button type="primary" custom-class="flex-1" @click="submitAdd">
|
||||
{{ t('voiceprint.save') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
@@ -488,23 +576,42 @@ defineExpose({
|
||||
|
||||
<view class="flex gap-[16rpx] border-t-[2rpx] border-[#eeeeee] p-[24rpx_32rpx_32rpx]">
|
||||
<wd-button type="info" custom-class="flex-1" @click="showEditDialog = false">
|
||||
{{ t('voiceprint.cancel') }}
|
||||
</wd-button>
|
||||
<wd-button type="primary" custom-class="flex-1" @click="submitEdit">
|
||||
{{ t('voiceprint.save') }}
|
||||
</wd-button>
|
||||
{{ t('voiceprint.cancel') }}
|
||||
</wd-button>
|
||||
<wd-button type="primary" custom-class="flex-1" @click="submitEdit">
|
||||
{{ t('voiceprint.save') }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 语音对话记录选择动作面板 -->
|
||||
<wd-action-sheet
|
||||
v-model="showChatHistoryDialog" :actions="chatHistoryActions" :title="t('voiceprint.selectVector')"
|
||||
@select="selectAudioId"
|
||||
/>
|
||||
<!-- 自定义语音对话记录选择弹出层 -->
|
||||
<wd-popup v-model="showChatHistoryDialog" class="custom-popup" position="bottom" @close="stopAudio">
|
||||
<view class="rounded-[20rpx] bg-white pb-[20rpx] pt-[20rpx]">
|
||||
<view class="max-h-[600rpx] overflow-y-auto rounded-[20rpx]">
|
||||
<view
|
||||
v-for="item in chatHistoryActions"
|
||||
:key="item.audioId"
|
||||
class="flex items-center justify-between border-b border-[#f5f5f5] p-[32rpx] transition-all active:bg-[#f5f7fb]"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<text class="flex-1 text-[28rpx] text-[#232338]">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<view class="ml-[20rpx]" @click.stop="playAudio(item.audioId, $event)">
|
||||
<wd-icon
|
||||
:name="playingAudioId === item.audioId ? 'pause-circle' : 'play-circle'"
|
||||
size="24px"
|
||||
:custom-class="playingAudioId === item.audioId ? 'text-[#336cff]' : 'text-[#9d9ea3]'"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.voiceprint-container {
|
||||
position: relative;
|
||||
}
|
||||
@@ -523,14 +630,10 @@ defineExpose({
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
:deep(.wd-swipe-action) {
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
border: 1rpx solid #eeeeee;
|
||||
}
|
||||
|
||||
:deep(.flex-1) {
|
||||
flex: 1;
|
||||
::v-deep .custom-popup {
|
||||
.wd-popup {
|
||||
padding: 20rpx !important;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -670,10 +670,10 @@ export default {
|
||||
'home.wish': 'Vamos ter um dia maravilhoso!',
|
||||
'home.languageModel': 'LLM',
|
||||
'home.voiceModel': 'TTS',
|
||||
'home.configureRole': 'Configurar Papel',
|
||||
'home.voiceprintRecognition': 'Impressão Vocal',
|
||||
'home.configureRole': 'ConfRoles',
|
||||
'home.voiceprintRecognition': 'RecVoz',
|
||||
'home.deviceManagement': 'Dispositivos',
|
||||
'home.chatHistory': 'Histórico de Chat',
|
||||
'home.chatHistory': 'Conversa',
|
||||
'home.lastConversation': 'Última Conversa',
|
||||
'home.noConversation': 'Nenhuma conversa',
|
||||
'home.justNow': 'Agora mesmo',
|
||||
|
||||
Reference in New Issue
Block a user