合入小程序修改内容
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user