mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-29 11:13:55 +08:00
Merge pull request #3043 from xinnan-tech/update-manager-mobile
mobile 优化聊天记录音频播放功能
This commit is contained in:
@@ -13,9 +13,9 @@ import type { ChatMessage, UserMessageContent } from '@/api/chat-history/types'
|
|||||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { getAudioId, getChatHistory } from '@/api/chat-history/chat-history'
|
import { getAudioId, getChatHistory } from '@/api/chat-history/chat-history'
|
||||||
import { getEnvBaseUrl } from '@/utils'
|
|
||||||
import { toast } from '@/utils/toast'
|
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { debounce, getEnvBaseUrl } from '@/utils'
|
||||||
|
import { toast } from '@/utils/toast'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ChatDetail',
|
name: 'ChatDetail',
|
||||||
@@ -129,7 +129,7 @@ function formatTime(timeStr: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 播放音频
|
// 播放音频
|
||||||
async function playAudio(audioId: string) {
|
const playAudio = debounce(async (audioId: string) => {
|
||||||
if (!audioId) {
|
if (!audioId) {
|
||||||
toast.error(t('chatHistory.invalidAudioId'))
|
toast.error(t('chatHistory.invalidAudioId'))
|
||||||
return
|
return
|
||||||
@@ -139,8 +139,11 @@ async function playAudio(audioId: string) {
|
|||||||
// 如果正在播放其他音频,先停止
|
// 如果正在播放其他音频,先停止
|
||||||
if (audioContext.value) {
|
if (audioContext.value) {
|
||||||
audioContext.value.stop()
|
audioContext.value.stop()
|
||||||
audioContext.value.destroy()
|
}
|
||||||
audioContext.value = null
|
// 如果当前音频ID与请求ID相同暂停播放
|
||||||
|
if (playingAudioId.value === audioId) {
|
||||||
|
playingAudioId.value = null
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取音频下载ID
|
// 获取音频下载ID
|
||||||
@@ -151,7 +154,9 @@ async function playAudio(audioId: string) {
|
|||||||
const audioUrl = `${baseUrl}/agent/play/${downloadId}`
|
const audioUrl = `${baseUrl}/agent/play/${downloadId}`
|
||||||
|
|
||||||
// 创建音频上下文
|
// 创建音频上下文
|
||||||
audioContext.value = uni.createInnerAudioContext()
|
if (!audioContext.value) {
|
||||||
|
audioContext.value = uni.createInnerAudioContext()
|
||||||
|
}
|
||||||
audioContext.value.src = audioUrl
|
audioContext.value.src = audioUrl
|
||||||
|
|
||||||
// 设置播放状态
|
// 设置播放状态
|
||||||
@@ -185,7 +190,7 @@ async function playAudio(audioId: string) {
|
|||||||
toast.error(t('chatHistory.playAudioFailed'))
|
toast.error(t('chatHistory.playAudioFailed'))
|
||||||
playingAudioId.value = null
|
playingAudioId.value = null
|
||||||
}
|
}
|
||||||
}
|
}, 400)
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
if (options?.sessionId && options?.agentId) {
|
if (options?.sessionId && options?.agentId) {
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ async function loadChatHistory() {
|
|||||||
audioId: item.audioId,
|
audioId: item.audioId,
|
||||||
index,
|
index,
|
||||||
}))
|
}))
|
||||||
showChatHistoryDialog.value = true
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('获取对话记录失败:', error)
|
console.error('获取对话记录失败:', error)
|
||||||
@@ -368,6 +367,7 @@ onMounted(async () => {
|
|||||||
// 智能体已简化为默认
|
// 智能体已简化为默认
|
||||||
|
|
||||||
loadVoicePrintList()
|
loadVoicePrintList()
|
||||||
|
loadChatHistory()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
@@ -468,7 +468,7 @@ defineExpose({
|
|||||||
</text>
|
</text>
|
||||||
<view
|
<view
|
||||||
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
|
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
|
||||||
@click="loadChatHistory"
|
@click="showChatHistoryDialog = true"
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
class="m-r-[16rpx] flex-1 text-left text-[26rpx] text-[#232338]"
|
class="m-r-[16rpx] flex-1 text-left text-[26rpx] text-[#232338]"
|
||||||
@@ -547,7 +547,7 @@ defineExpose({
|
|||||||
</text>
|
</text>
|
||||||
<view
|
<view
|
||||||
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
|
class="flex cursor-pointer items-center justify-between border-[1rpx] border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]"
|
||||||
@click="loadChatHistory"
|
@click="showChatHistoryDialog = true"
|
||||||
>
|
>
|
||||||
<text
|
<text
|
||||||
class="m-r-[16rpx] flex-1 text-left text-[26rpx] text-[#232338]"
|
class="m-r-[16rpx] flex-1 text-left text-[26rpx] text-[#232338]"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import smCrypto from 'sm-crypto'
|
||||||
import { pages, subPackages } from '@/pages.json'
|
import { pages, subPackages } from '@/pages.json'
|
||||||
|
|
||||||
import { isMpWeixin } from './platform'
|
import { isMpWeixin } from './platform'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -197,23 +199,21 @@ export function getEnvBaseUploadUrl() {
|
|||||||
return baseUploadUrl
|
return baseUploadUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
import smCrypto from 'sm-crypto'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成SM2密钥对(十六进制格式)
|
* 生成SM2密钥对(十六进制格式)
|
||||||
* @returns {Object} 包含公钥和私钥的对象
|
* @returns {Object} 包含公钥和私钥的对象
|
||||||
*/
|
*/
|
||||||
export function generateSm2KeyPairHex() {
|
export function generateSm2KeyPairHex() {
|
||||||
// 使用sm-crypto库生成SM2密钥对
|
// 使用sm-crypto库生成SM2密钥对
|
||||||
const sm2 = smCrypto.sm2;
|
const sm2 = smCrypto.sm2
|
||||||
const keypair = sm2.generateKeyPairHex();
|
const keypair = sm2.generateKeyPairHex()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicKey: keypair.publicKey,
|
publicKey: keypair.publicKey,
|
||||||
privateKey: keypair.privateKey,
|
privateKey: keypair.privateKey,
|
||||||
clientPublicKey: keypair.publicKey, // 客户端公钥
|
clientPublicKey: keypair.publicKey, // 客户端公钥
|
||||||
clientPrivateKey: keypair.privateKey // 客户端私钥
|
clientPrivateKey: keypair.privateKey, // 客户端私钥
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,21 +223,21 @@ export function generateSm2KeyPairHex() {
|
|||||||
* @returns {string} 加密后的密文(十六进制格式)
|
* @returns {string} 加密后的密文(十六进制格式)
|
||||||
*/
|
*/
|
||||||
export function sm2Encrypt(publicKey: string, plainText: string): string {
|
export function sm2Encrypt(publicKey: string, plainText: string): string {
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
throw new Error('公钥不能为null或undefined');
|
throw new Error('公钥不能为null或undefined')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!plainText) {
|
if (!plainText) {
|
||||||
throw new Error('明文不能为空');
|
throw new Error('明文不能为空')
|
||||||
}
|
}
|
||||||
|
|
||||||
const sm2 = smCrypto.sm2;
|
const sm2 = smCrypto.sm2
|
||||||
// SM2加密,添加04前缀表示未压缩公钥
|
// SM2加密,添加04前缀表示未压缩公钥
|
||||||
const encrypted = sm2.doEncrypt(plainText, publicKey, 1);
|
const encrypted = sm2.doEncrypt(plainText, publicKey, 1)
|
||||||
// 转换为十六进制格式(与后端保持一致,添加04前缀)
|
// 转换为十六进制格式(与后端保持一致,添加04前缀)
|
||||||
const result = "04" + encrypted;
|
const result = `04${encrypted}`
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -247,9 +247,56 @@ export function sm2Encrypt(publicKey: string, plainText: string): string {
|
|||||||
* @returns {string} 解密后的明文
|
* @returns {string} 解密后的明文
|
||||||
*/
|
*/
|
||||||
export function sm2Decrypt(privateKey: string, cipherText: string): string {
|
export function sm2Decrypt(privateKey: string, cipherText: string): string {
|
||||||
const sm2 = smCrypto.sm2;
|
const sm2 = smCrypto.sm2
|
||||||
// 移除04前缀(与后端保持一致)
|
// 移除04前缀(与后端保持一致)
|
||||||
const dataWithoutPrefix = cipherText.startsWith("04") ? cipherText.substring(2) : cipherText;
|
const dataWithoutPrefix = cipherText.startsWith('04') ? cipherText.substring(2) : cipherText
|
||||||
// SM2解密
|
// SM2解密
|
||||||
return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1);
|
return sm2.doDecrypt(dataWithoutPrefix, privateKey, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyFunction = (...args: any[]) => any
|
||||||
|
|
||||||
|
interface DebouncedFunction extends AnyFunction {
|
||||||
|
cancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
* @param fn 要防抖的函数
|
||||||
|
* @param delay 延迟时间(毫秒),默认500ms
|
||||||
|
* @param immediate 是否立即执行,默认false
|
||||||
|
* @returns 防抖处理后的函数
|
||||||
|
*/
|
||||||
|
export function debounce<T extends AnyFunction>(
|
||||||
|
fn: T,
|
||||||
|
delay = 500,
|
||||||
|
immediate = false,
|
||||||
|
): DebouncedFunction {
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const debounced = function (this: any, ...args: Parameters<T>) {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediate && !timer) {
|
||||||
|
fn.apply(this, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
if (!immediate) {
|
||||||
|
fn.apply(this, args)
|
||||||
|
}
|
||||||
|
timer = null
|
||||||
|
}, delay)
|
||||||
|
} as DebouncedFunction
|
||||||
|
|
||||||
|
debounced.cancel = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return debounced
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user