合入小程序修改内容

This commit is contained in:
HycJack
2026-05-06 03:37:08 +08:00
parent 326e4bac28
commit 1e7e7cc05e
16 changed files with 1072 additions and 450 deletions
+145 -32
View File
@@ -1,10 +1,10 @@
import { View, Text, ScrollView, Image, Input } from '@tarojs/components'
import { View, Text, ScrollView, Image } from '@tarojs/components'
import { useEffect, useRef, useState } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import {
getMessages,
getCharacter,
sendParentMessage,
sendParentVoiceMessage,
ChatMessage,
Character,
ConversationSource,
@@ -88,10 +88,15 @@ export default function ChatDetail() {
})
const [messages, setMessages] = useState<ChatMessage[]>([])
const [loading, setLoading] = useState(true)
const [inputText, setInputText] = useState('')
const [sending, setSending] = useState(false)
const [recording, setRecording] = useState(false)
const [playingMessageId, setPlayingMessageId] = useState<number | null>(null)
const [recordHint, setRecordHint] = useState('长按开始留言')
const audioContextRef = useRef<Taro.InnerAudioContext | null>(null)
const recorderManagerRef = useRef<Taro.RecorderManager | null>(null)
const activeConversationIdRef = useRef(initialConversationId)
const latestLoadChatDataRef = useRef<(conversationId: number) => Promise<void>>(async () => {})
const recordingChildIdRef = useRef<number | null>(null)
useEffect(() => {
const audioContext = Taro.createInnerAudioContext()
@@ -114,6 +119,10 @@ export default function ChatDetail() {
}
}, [])
useEffect(() => {
activeConversationIdRef.current = activeConversationId
}, [activeConversationId])
useEffect(() => {
void loadChatData(activeConversationId)
}, [activeConversationId, conversationSource, conversationPeerKind])
@@ -152,39 +161,139 @@ export default function ChatDetail() {
}
}
latestLoadChatDataRef.current = loadChatData
useEffect(() => {
if (typeof Taro.getRecorderManager !== 'function') return
const recorderManager = Taro.getRecorderManager()
recorderManagerRef.current = recorderManager
recorderManager.onError((error) => {
recordingChildIdRef.current = null
setRecording(false)
setRecordHint('长按开始留言')
console.error('[chat-detail] recorder failed:', error)
Taro.showToast({ title: '录音失败,请重试', icon: 'none' })
})
recorderManager.onStop(async (result) => {
const currentChildId = recordingChildIdRef.current
recordingChildIdRef.current = null
setRecording(false)
setRecordHint('长按开始留言')
if (!currentChildId) return
if (!result.tempFilePath) {
Taro.showToast({ title: '录音文件不存在', icon: 'none' })
return
}
const durationMs = Math.max(0, Number(result.duration || 0))
if (durationMs < 800) {
Taro.showToast({ title: '留言太短,请重试', icon: 'none' })
return
}
setSending(true)
try {
const response = await sendParentVoiceMessage(currentChildId, {
filePath: result.tempFilePath,
durationMs,
})
if (response.conversation_id && response.conversation_id !== activeConversationIdRef.current) {
setActiveConversationId(response.conversation_id)
} else {
await latestLoadChatDataRef.current(activeConversationIdRef.current)
}
Taro.showToast({ title: '留言已发送', icon: 'success' })
} catch (error: any) {
console.error('[chat-detail] voice send failed:', error)
Taro.showToast({
title: error?.message || '留言发送失败,请重试',
icon: 'none',
})
} finally {
setSending(false)
}
})
return () => {
recorderManagerRef.current = null
}
}, [])
const handleBack = () => {
Taro.navigateBack()
}
const handleSend = async () => {
const normalizedContent = inputText.trim()
if (!normalizedContent || sending) return
const ensureRecordPermission = async (): Promise<boolean> => {
try {
const settings = await Taro.getSetting()
if (settings.authSetting['scope.record']) return true
await Taro.authorize({ scope: 'scope.record' })
return true
} catch {
const modal = await Taro.showModal({
title: '需要录音权限',
content: '请允许小程序使用麦克风后再留言',
confirmText: '去设置',
})
if (modal.confirm) {
await Taro.openSetting()
}
return false
}
}
const handleRecordStart = async () => {
if (sending || recording) return
if (!character.canSend || !character.childId) {
Taro.showToast({ title: '当前会话不支持发送', icon: 'none' })
return
}
setSending(true)
const recorderManager = recorderManagerRef.current
if (!recorderManager) {
Taro.showToast({ title: '当前环境不支持录音', icon: 'none' })
return
}
const hasPermission = await ensureRecordPermission()
if (!hasPermission) return
recordingChildIdRef.current = character.childId
setRecording(true)
setRecordHint('松开发送留言')
try {
const response = await sendParentMessage(character.childId, normalizedContent)
setInputText('')
if (response.conversation_id && response.conversation_id !== activeConversationId) {
setActiveConversationId(response.conversation_id)
} else {
await loadChatData(activeConversationId)
}
Taro.showToast({ title: '已发送', icon: 'success' })
recorderManager.start({
duration: 60000,
format: 'mp3',
numberOfChannels: 1,
sampleRate: 16000,
encodeBitRate: 96000,
})
} catch (error: any) {
console.error('[chat-detail] send failed:', error)
recordingChildIdRef.current = null
setRecording(false)
setRecordHint('长按开始留言')
console.error('[chat-detail] recorder start failed:', error)
Taro.showToast({
title: error?.message || '发送失败,请重试',
title: '无法开始录音,请重试',
icon: 'none',
})
} finally {
setSending(false)
}
}
const handleRecordStop = () => {
if (!recording) return
const recorderManager = recorderManagerRef.current
if (!recorderManager) return
setRecordHint('正在处理留言...')
recorderManager.stop()
}
const handlePlayAudio = (message: ChatMessage) => {
if (message.contentType !== 2 || !message.mediaUrl) {
Taro.showToast({ title: '语音地址不存在', icon: 'none' })
@@ -213,10 +322,13 @@ export default function ChatDetail() {
const renderMessageBody = (msg: ChatMessage) => {
if (msg.contentType === 2 && msg.mediaUrl) {
const isPlaying = playingMessageId === msg.id
const isLeaveMessage = msg.extJson?.message_kind === 'leave_message'
return (
<View className={`audio-bubble ${isPlaying ? 'playing' : ''}`} onClick={() => handlePlayAudio(msg)}>
<Text className='audio-bubble-icon'>{isPlaying ? '[]' : '>'}</Text>
<Text className='audio-bubble-text'>{msg.mediaTranscriptText || '点击播放语音'}</Text>
<Text className='audio-bubble-text'>
{msg.mediaTranscriptText || (isLeaveMessage ? '点击收听留言' : '点击播放语音')}
</Text>
<Text className='audio-bubble-duration'>{formatAudioDuration(msg.mediaDurationMs)}</Text>
</View>
)
@@ -250,7 +362,7 @@ export default function ChatDetail() {
) : messages.length === 0 ? (
<View className='empty-chat'>
<Text className='empty-chat-text'>
{character.canSend ? '还没有消息,发送第一条吧' : '这个会话还没有消息'}
{character.canSend ? '还没有留言,长按发送第一条吧' : '这个会话还没有消息'}
</Text>
</View>
) : (
@@ -294,17 +406,18 @@ export default function ChatDetail() {
{character.canSend && character.childId && (
<View className='composer'>
<Input
className='composer-input'
value={inputText}
placeholder='输入要发给孩子的消息'
maxlength={500}
confirmType='send'
onInput={(event) => setInputText(event.detail.value)}
onConfirm={handleSend}
/>
<View className={`composer-send ${sending ? 'disabled' : ''}`} onClick={handleSend}>
<Text className='composer-send-text'>{sending ? '发送中' : '发送'}</Text>
<View className='composer-tip'>
<Text>{sending ? '正在发送留言...' : recordHint}</Text>
</View>
<View
className={`composer-record ${recording ? 'recording' : ''} ${sending ? 'disabled' : ''}`}
onLongPress={handleRecordStart}
onTouchEnd={handleRecordStop}
onTouchCancel={handleRecordStop}
>
<Text className='composer-record-text'>
{sending ? '发送中...' : recording ? '松开发送' : '按住留言'}
</Text>
</View>
</View>
)}