Merge pull request #3023 from xinnan-tech/update-manager-mobile

mobile 播放声纹弹窗样式调整,切换角色模板更新表单状态
This commit is contained in:
wengzh
2026-03-18 16:53:54 +08:00
committed by GitHub
15 changed files with 377 additions and 101 deletions
@@ -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,
}
+6 -1
View File
@@ -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',
}
+6 -1
View File
@@ -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',
}
+6 -1
View File
@@ -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',
}
+6 -1
View File
@@ -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',
}
+6 -1
View File
@@ -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': '音频播放失败',
}
+6 -1
View File
@@ -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': '音頻播放失敗',
}
+103 -15
View File
@@ -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>
+11 -5
View File
@@ -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>
+3 -3
View File
@@ -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',