feat(voice): upload device audio to cos and enable playback
This commit is contained in:
@@ -174,6 +174,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.audio-bubble {
|
||||
min-width: 220px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
|
||||
&.playing {
|
||||
opacity: 0.82;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-bubble-icon {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.audio-bubble-text {
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.audio-bubble-duration {
|
||||
font-size: 24px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.empty-chat {
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Text, ScrollView, Image, Input } from '@tarojs/components'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Taro, { useRouter } from '@tarojs/taro'
|
||||
import {
|
||||
getMessages,
|
||||
@@ -47,6 +47,11 @@ function formatParticipantLabel(message: ChatMessage): string {
|
||||
return `${senderType} ${senderId}`
|
||||
}
|
||||
|
||||
function formatAudioDuration(durationMs?: number | null): string {
|
||||
const totalSeconds = Math.max(1, Math.round((durationMs || 0) / 1000))
|
||||
return `${totalSeconds}s`
|
||||
}
|
||||
|
||||
export default function ChatDetail() {
|
||||
const router = useRouter()
|
||||
const params = router.params
|
||||
@@ -85,6 +90,29 @@ export default function ChatDetail() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [inputText, setInputText] = useState('')
|
||||
const [sending, setSending] = useState(false)
|
||||
const [playingMessageId, setPlayingMessageId] = useState<number | null>(null)
|
||||
const audioContextRef = useRef<Taro.InnerAudioContext | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const audioContext = Taro.createInnerAudioContext()
|
||||
audioContextRef.current = audioContext
|
||||
|
||||
audioContext.onEnded(() => {
|
||||
setPlayingMessageId(null)
|
||||
})
|
||||
audioContext.onStop(() => {
|
||||
setPlayingMessageId(null)
|
||||
})
|
||||
audioContext.onError(() => {
|
||||
setPlayingMessageId(null)
|
||||
Taro.showToast({ title: '语音播放失败', icon: 'none' })
|
||||
})
|
||||
|
||||
return () => {
|
||||
audioContext.destroy()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void loadChatData(activeConversationId)
|
||||
@@ -157,6 +185,46 @@ export default function ChatDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlayAudio = (message: ChatMessage) => {
|
||||
if (message.contentType !== 2 || !message.mediaUrl) {
|
||||
Taro.showToast({ title: '语音地址不存在', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const audioContext = audioContextRef.current
|
||||
if (!audioContext) {
|
||||
Taro.showToast({ title: '播放器未就绪', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (playingMessageId === message.id) {
|
||||
audioContext.stop()
|
||||
setPlayingMessageId(null)
|
||||
return
|
||||
}
|
||||
|
||||
audioContext.stop()
|
||||
audioContext.src = message.mediaUrl
|
||||
audioContext.autoplay = true
|
||||
audioContext.play()
|
||||
setPlayingMessageId(message.id)
|
||||
}
|
||||
|
||||
const renderMessageBody = (msg: ChatMessage) => {
|
||||
if (msg.contentType === 2 && msg.mediaUrl) {
|
||||
const isPlaying = playingMessageId === msg.id
|
||||
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-duration'>{formatAudioDuration(msg.mediaDurationMs)}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return <Text>{msg.content}</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='chat-detail-page'>
|
||||
<View className='chat-header'>
|
||||
@@ -199,7 +267,7 @@ export default function ChatDetail() {
|
||||
<View className='message-main'>
|
||||
<Text className='message-meta'>{formatParticipantLabel(msg)}</Text>
|
||||
<View className='message-bubble'>
|
||||
<Text>{msg.content}</Text>
|
||||
{renderMessageBody(msg)}
|
||||
</View>
|
||||
</View>
|
||||
<View className='user-avatar'>
|
||||
@@ -214,7 +282,7 @@ export default function ChatDetail() {
|
||||
<View className='message-main'>
|
||||
<Text className='message-meta'>{formatParticipantLabel(msg)}</Text>
|
||||
<View className='message-bubble'>
|
||||
<Text>{msg.content}</Text>
|
||||
{renderMessageBody(msg)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function Chat() {
|
||||
</View>
|
||||
) : conversations.length === 0 ? (
|
||||
<View className='empty-card'>
|
||||
<Text className='empty-title'>暂无玩伴会话</Text>
|
||||
<Text className='empty-title'>当前孩子暂无会话</Text>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView className='conversation-list' scrollY>
|
||||
|
||||
@@ -348,6 +348,14 @@
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 26px;
|
||||
line-height: 1.6;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
margin-top: 28px;
|
||||
height: 92px;
|
||||
@@ -378,3 +386,46 @@
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.child-focus-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24px;
|
||||
margin: 0 24px 24px;
|
||||
padding: 28px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.child-focus-avatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
background: #FEF3E8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.child-focus-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.child-focus-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.child-focus-name {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.child-focus-desc {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { View, Text, Switch, Slider, Image } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { getToken } from '@/services/auth'
|
||||
import { Binding, resolveActiveBinding } from '@/services/binding'
|
||||
import { Child, getChild } from '@/services/child'
|
||||
import { Binding, loadCurrentChildBindingContext } from '@/services/binding'
|
||||
import { Child } from '@/services/child'
|
||||
import './index.scss'
|
||||
|
||||
export default function Device() {
|
||||
@@ -25,20 +25,9 @@ export default function Device() {
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const activeBinding = await resolveActiveBinding()
|
||||
setBinding(activeBinding)
|
||||
|
||||
if (activeBinding?.child_id) {
|
||||
try {
|
||||
const currentChild = await getChild(activeBinding.child_id)
|
||||
setChild(currentChild)
|
||||
} catch (error) {
|
||||
console.error('[device] load child failed:', error)
|
||||
setChild(null)
|
||||
}
|
||||
} else {
|
||||
setChild(null)
|
||||
}
|
||||
const context = await loadCurrentChildBindingContext()
|
||||
setChild(context.currentChild)
|
||||
setBinding(context.currentBinding)
|
||||
} catch (error: any) {
|
||||
console.error('[device] load failed:', error)
|
||||
Taro.showToast({
|
||||
@@ -72,7 +61,7 @@ export default function Device() {
|
||||
)
|
||||
}
|
||||
|
||||
if (!binding) {
|
||||
if (!child) {
|
||||
return (
|
||||
<View className='device-page'>
|
||||
<View className='page-header'>
|
||||
@@ -83,7 +72,39 @@ export default function Device() {
|
||||
<View className='empty-robot'>
|
||||
<Image className='robot-img' src={require('../../assets/tab-icons/robot.png')} mode='aspectFit' />
|
||||
</View>
|
||||
<Text className='empty-title'>先绑定一台伴伴设备</Text>
|
||||
<Text className='empty-title'>先添加一个儿童资料</Text>
|
||||
<Text className='empty-subtitle'>添加后才能为孩子绑定专属设备并查看聊天和定位信息</Text>
|
||||
<View className='empty-btn' onClick={() => Taro.switchTab({ url: '/pages/sleep/index' })}>
|
||||
<Text className='empty-btn-text'>去添加孩子</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!binding) {
|
||||
return (
|
||||
<View className='device-page'>
|
||||
<View className='page-header'>
|
||||
<Text className='page-title'>{child.child_name} 的伴伴</Text>
|
||||
</View>
|
||||
|
||||
<View className='child-focus-card'>
|
||||
<View className='child-focus-avatar'>
|
||||
<Text className='child-focus-icon'>🧒</Text>
|
||||
</View>
|
||||
<View className='child-focus-info'>
|
||||
<Text className='child-focus-name'>{child.child_name}</Text>
|
||||
<Text className='child-focus-desc'>当前孩子已选中,还没有绑定设备</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='empty-card'>
|
||||
<View className='empty-robot'>
|
||||
<Image className='robot-img' src={require('../../assets/tab-icons/robot.png')} mode='aspectFit' />
|
||||
</View>
|
||||
<Text className='empty-title'>给当前孩子绑定一台设备</Text>
|
||||
<Text className='empty-subtitle'>绑定后,这个孩子的聊天记录、定位和设备状态都会显示在首页</Text>
|
||||
<View className='empty-btn' onClick={() => Taro.navigateTo({ url: '/pages/bind/index' })}>
|
||||
<Text className='empty-btn-text'>去绑定设备</Text>
|
||||
</View>
|
||||
@@ -92,8 +113,8 @@ export default function Device() {
|
||||
)
|
||||
}
|
||||
|
||||
const childName = child?.child_name || '未设置'
|
||||
const pageTitle = child?.child_name ? `${child.child_name} 的伴伴` : '伴伴设备'
|
||||
const childName = child.child_name || '未设置'
|
||||
const pageTitle = `${childName} 的伴伴`
|
||||
const battery = 82
|
||||
const daysLeft = 4
|
||||
|
||||
@@ -106,7 +127,7 @@ export default function Device() {
|
||||
<View className='status-card'>
|
||||
<View className='status-badge'>
|
||||
<View className='status-dot'></View>
|
||||
<Text className='status-text'>{binding.child_id ? '已完成绑定' : '待关联儿童'}</Text>
|
||||
<Text className='status-text'>已完成绑定</Text>
|
||||
</View>
|
||||
<View className='battery-section'>
|
||||
<Text className='battery-percent'>
|
||||
@@ -124,12 +145,12 @@ export default function Device() {
|
||||
|
||||
<View className='detail-card'>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>设备号</Text>
|
||||
<Text className='detail-value'>{binding.device_id}</Text>
|
||||
<Text className='detail-label'>当前儿童</Text>
|
||||
<Text className='detail-value'>{childName}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>儿童昵称</Text>
|
||||
<Text className='detail-value'>{childName}</Text>
|
||||
<Text className='detail-label'>当前设备</Text>
|
||||
<Text className='detail-value'>{binding.device_id}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>绑定时间</Text>
|
||||
@@ -195,12 +216,6 @@ export default function Device() {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!binding.child_id && (
|
||||
<View className='helper-card' onClick={() => Taro.navigateTo({ url: '/pages/bind/index' })}>
|
||||
<Text className='helper-title'>继续完成儿童关联</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ export default function Location() {
|
||||
</View>
|
||||
) : (
|
||||
<View className='location-empty'>
|
||||
<Text className='location-empty-title'>暂无设备位置</Text>
|
||||
<Text className='location-empty-title'>当前孩子暂无设备位置</Text>
|
||||
</View>
|
||||
)
|
||||
) : trajectory.length > 0 ? (
|
||||
@@ -384,7 +384,7 @@ export default function Location() {
|
||||
<View className='location-empty'>
|
||||
<Text className='location-empty-title'>暂无轨迹</Text>
|
||||
<Text className='location-empty-desc'>
|
||||
{trajectoryMode === 'today' ? '今天还没有新的历史点位。' : '最近时间范围内没有新的历史点位。'}
|
||||
{trajectoryMode === 'today' ? '当前孩子今天还没有新的历史点位。' : '当前孩子最近没有新的历史点位。'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -5,13 +5,11 @@ import { clearToken, getCurrentUserId, getToken } from '@/services/auth'
|
||||
import {
|
||||
BindingListItem,
|
||||
clearSelectedBindingDeviceId,
|
||||
getBindings,
|
||||
resolveBindingSelection,
|
||||
setBindingChild,
|
||||
loadCurrentChildBindingContext,
|
||||
setSelectedBindingDeviceId,
|
||||
unbindDevice,
|
||||
} from '@/services/binding'
|
||||
import { Child, createChild, getChildren, updateChild } from '@/services/child'
|
||||
import { Child, clearSelectedChildId, createChild, setSelectedChildId, updateChild } from '@/services/child'
|
||||
import './index.scss'
|
||||
|
||||
interface MenuItem {
|
||||
@@ -28,8 +26,9 @@ export default function Sleep() {
|
||||
const [children, setChildren] = useState<Child[]>([])
|
||||
const [bindings, setBindings] = useState<BindingListItem[]>([])
|
||||
const [binding, setBinding] = useState<BindingListItem | null>(null)
|
||||
const [currentChild, setCurrentChild] = useState<Child | null>(null)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [showDeviceModal, setShowDeviceModal] = useState(false)
|
||||
const [showChildModal, setShowChildModal] = useState(false)
|
||||
const [modalType, setModalType] = useState<'add' | 'edit'>('add')
|
||||
const [childName, setChildName] = useState('')
|
||||
const [editingChildId, setEditingChildId] = useState<number | null>(null)
|
||||
@@ -47,13 +46,11 @@ export default function Sleep() {
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const [childResponse, bindingResponse] = await Promise.all([getChildren(), getBindings(undefined, 100)])
|
||||
const bindingItems = bindingResponse.items || []
|
||||
const activeBinding = resolveBindingSelection(bindingItems)
|
||||
|
||||
setChildren(childResponse.items || [])
|
||||
setBindings(bindingItems)
|
||||
setBinding(activeBinding)
|
||||
const context = await loadCurrentChildBindingContext()
|
||||
setChildren(context.children)
|
||||
setBindings(context.bindings as BindingListItem[])
|
||||
setCurrentChild(context.currentChild)
|
||||
setBinding((context.currentBinding as BindingListItem | null) || null)
|
||||
setParentInfo(Taro.getStorageSync('userInfo') || {})
|
||||
} catch (error: any) {
|
||||
console.error('[manage] load failed:', error)
|
||||
@@ -66,8 +63,7 @@ export default function Sleep() {
|
||||
}
|
||||
}
|
||||
|
||||
const currentChild = binding?.child_id ? children.find((item) => item.child_id === binding.child_id) || null : null
|
||||
const currentChildName = currentChild?.child_name || binding?.child_name || (binding && !binding.child_id ? '待关联' : '未设置')
|
||||
const currentChildName = currentChild?.child_name || '未设置'
|
||||
|
||||
const handleOpenModal = (type: 'add' | 'edit', child?: Child) => {
|
||||
setModalType(type)
|
||||
@@ -82,12 +78,19 @@ export default function Sleep() {
|
||||
setEditingChildId(null)
|
||||
}
|
||||
|
||||
const handleSelectBinding = (targetBinding: BindingListItem) => {
|
||||
setSelectedBindingDeviceId(targetBinding.device_id)
|
||||
setBinding(targetBinding)
|
||||
setShowDeviceModal(false)
|
||||
const handleSelectChild = (child: Child) => {
|
||||
setSelectedChildId(child.child_id)
|
||||
const nextBinding = bindings.find((item) => item.child_id === child.child_id) || null
|
||||
if (nextBinding?.device_id) {
|
||||
setSelectedBindingDeviceId(nextBinding.device_id)
|
||||
} else {
|
||||
clearSelectedBindingDeviceId()
|
||||
}
|
||||
setCurrentChild(child)
|
||||
setBinding(nextBinding)
|
||||
setShowChildModal(false)
|
||||
Taro.showToast({
|
||||
title: '已切换设备',
|
||||
title: '已切换当前孩子',
|
||||
icon: 'success',
|
||||
})
|
||||
}
|
||||
@@ -102,12 +105,8 @@ export default function Sleep() {
|
||||
try {
|
||||
if (modalType === 'add') {
|
||||
const createdChild = await createChild({ child_name: normalizedName })
|
||||
if (binding && !binding.child_id) {
|
||||
await setBindingChild(binding.device_id, { child_id: createdChild.child_id })
|
||||
Taro.showToast({ title: '已创建并关联', icon: 'success' })
|
||||
} else {
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' })
|
||||
}
|
||||
setSelectedChildId(createdChild.child_id)
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' })
|
||||
} else if (editingChildId) {
|
||||
await updateChild(editingChildId, { child_name: normalizedName })
|
||||
Taro.showToast({ title: '修改成功', icon: 'success' })
|
||||
@@ -124,54 +123,19 @@ export default function Sleep() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleChildMenu = () => {
|
||||
if (binding && !binding.child_id && children.length > 0) {
|
||||
Taro.showActionSheet({
|
||||
itemList: [...children.map((item) => item.child_name), '新建儿童资料'],
|
||||
success: async (result) => {
|
||||
if (result.tapIndex === children.length) {
|
||||
handleOpenModal('add')
|
||||
return
|
||||
}
|
||||
|
||||
const targetChild = children[result.tapIndex]
|
||||
if (!targetChild) return
|
||||
|
||||
try {
|
||||
await setBindingChild(binding.device_id, { child_id: targetChild.child_id })
|
||||
Taro.showToast({ title: '关联成功', icon: 'success' })
|
||||
await loadData()
|
||||
} catch (error: any) {
|
||||
Taro.showToast({
|
||||
title: error?.message || '关联失败,请重试',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (currentChild) {
|
||||
handleOpenModal('edit', currentChild)
|
||||
return
|
||||
}
|
||||
|
||||
handleOpenModal('add')
|
||||
}
|
||||
|
||||
const handleUnbind = () => {
|
||||
if (!binding) return
|
||||
|
||||
Taro.showModal({
|
||||
title: '解除设备绑定',
|
||||
content: '确定要解除当前设备绑定吗?',
|
||||
content: `确定要解除 ${currentChildName} 当前绑定的设备吗?`,
|
||||
confirmColor: '#FF8C42',
|
||||
success: async (result) => {
|
||||
if (!result.confirm) return
|
||||
|
||||
try {
|
||||
await unbindDevice(binding.device_id)
|
||||
clearSelectedBindingDeviceId()
|
||||
Taro.showToast({ title: '已解绑', icon: 'success' })
|
||||
await loadData()
|
||||
} catch (error: any) {
|
||||
@@ -193,6 +157,7 @@ export default function Sleep() {
|
||||
if (!result.confirm) return
|
||||
clearToken()
|
||||
clearSelectedBindingDeviceId()
|
||||
clearSelectedChildId()
|
||||
Taro.removeStorageSync('userInfo')
|
||||
Taro.reLaunch({ url: '/pages/login/index' })
|
||||
},
|
||||
@@ -202,21 +167,33 @@ export default function Sleep() {
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
if (item.disabled) return
|
||||
|
||||
if (item.name === '儿童资料 (用于称呼)') {
|
||||
handleChildMenu()
|
||||
if (item.name === '当前孩子') {
|
||||
setShowChildModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (item.name === '绑定新设备') {
|
||||
if (item.name === '编辑当前孩子') {
|
||||
if (currentChild) {
|
||||
handleOpenModal('edit', currentChild)
|
||||
} else {
|
||||
handleOpenModal('add')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (item.name === '绑定设备') {
|
||||
if (!currentChild) {
|
||||
handleOpenModal('add')
|
||||
Taro.showToast({
|
||||
title: '请先添加儿童资料',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({ url: '/pages/bind/index' })
|
||||
return
|
||||
}
|
||||
|
||||
if (item.name === '切换设备') {
|
||||
setShowDeviceModal(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (item.name === '解除设备绑定') {
|
||||
handleUnbind()
|
||||
}
|
||||
@@ -237,24 +214,24 @@ export default function Sleep() {
|
||||
{
|
||||
icon: require('../../assets/tab-icons/orange-robot.png'),
|
||||
iconBgClass: 'orange',
|
||||
name: '儿童资料 (用于称呼)',
|
||||
name: '当前孩子',
|
||||
value: currentChildName,
|
||||
arrow: true,
|
||||
},
|
||||
{
|
||||
icon: require('../../assets/tab-icons/rings.png'),
|
||||
iconBgClass: 'green',
|
||||
name: '切换设备',
|
||||
value: bindings.length > 0 ? `共 ${bindings.length} 台` : '暂无设备',
|
||||
arrow: true,
|
||||
},
|
||||
{
|
||||
icon: require('../../assets/tab-icons/orange-robot.png'),
|
||||
iconBgClass: 'orange',
|
||||
name: '绑定新设备',
|
||||
name: '编辑当前孩子',
|
||||
value: '',
|
||||
arrow: true,
|
||||
},
|
||||
{
|
||||
icon: require('../../assets/tab-icons/rings.png'),
|
||||
iconBgClass: 'green',
|
||||
name: '绑定设备',
|
||||
value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定',
|
||||
arrow: true,
|
||||
},
|
||||
{
|
||||
icon: require('../../assets/tab-icons/broken-rings.png'),
|
||||
iconBgClass: 'red',
|
||||
@@ -284,23 +261,23 @@ export default function Sleep() {
|
||||
<Text className='user-role'>用户 ID: {userId || '--'}</Text>
|
||||
</View>
|
||||
<View className='verified-badge'>
|
||||
<Text>{bindings.length > 0 ? `已绑定 ${bindings.length} 台` : '未绑定设备'}</Text>
|
||||
<Text>{currentChild ? '已选择当前孩子' : '未选择孩子'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='summary-card'>
|
||||
<View className='summary-row'>
|
||||
<Text className='summary-label'>已绑定设备数</Text>
|
||||
<Text className='summary-value'>{bindings.length} 台</Text>
|
||||
</View>
|
||||
<View className='summary-row'>
|
||||
<Text className='summary-label'>当前设备</Text>
|
||||
<Text className='summary-value'>{binding?.device_id || '未绑定'}</Text>
|
||||
<Text className='summary-label'>儿童资料数</Text>
|
||||
<Text className='summary-value'>{children.length} 个</Text>
|
||||
</View>
|
||||
<View className='summary-row'>
|
||||
<Text className='summary-label'>当前儿童</Text>
|
||||
<Text className='summary-value'>{currentChildName}</Text>
|
||||
</View>
|
||||
<View className='summary-row'>
|
||||
<Text className='summary-label'>当前设备</Text>
|
||||
<Text className='summary-value'>{binding?.device_id || '未绑定'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='menu-card'>
|
||||
@@ -333,39 +310,40 @@ export default function Sleep() {
|
||||
<Text className='version-text'>伴伴 Companion V1.1.0</Text>
|
||||
</View>
|
||||
|
||||
{showDeviceModal && (
|
||||
<View className='modal-mask' onClick={() => setShowDeviceModal(false)}>
|
||||
{showChildModal && (
|
||||
<View className='modal-mask' onClick={() => setShowChildModal(false)}>
|
||||
<View
|
||||
className='modal-card device-switch-card'
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<Text className='modal-title'>切换设备</Text>
|
||||
{bindings.length === 0 ? (
|
||||
<Text className='device-switch-empty'>当前还没有已绑定设备</Text>
|
||||
<Text className='modal-title'>切换当前孩子</Text>
|
||||
{children.length === 0 ? (
|
||||
<Text className='device-switch-empty'>当前还没有儿童资料</Text>
|
||||
) : (
|
||||
<View className='device-switch-list'>
|
||||
{bindings.map((item) => {
|
||||
const isActive = binding?.device_id === item.device_id
|
||||
{children.map((item) => {
|
||||
const isActive = currentChild?.child_id === item.child_id
|
||||
const itemBinding = bindings.find((bindingItem) => bindingItem.child_id === item.child_id) || null
|
||||
return (
|
||||
<View
|
||||
key={item.device_id}
|
||||
key={item.child_id}
|
||||
className={`device-switch-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleSelectBinding(item)}
|
||||
onClick={() => handleSelectChild(item)}
|
||||
>
|
||||
<View className='device-switch-head'>
|
||||
<Text className='device-switch-id'>{item.device_id}</Text>
|
||||
<Text className='device-switch-id'>{item.child_name}</Text>
|
||||
{isActive && <Text className='device-switch-tag'>当前</Text>}
|
||||
</View>
|
||||
<Text className='device-switch-name'>{item.child_name || '待关联儿童'}</Text>
|
||||
<Text className='device-switch-name'>{itemBinding?.device_id || '未绑定设备'}</Text>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
<View className='modal-actions'>
|
||||
<Text className='modal-action cancel' onClick={() => setShowDeviceModal(false)}>
|
||||
<Text className='modal-action cancel' onClick={() => setShowChildModal(false)}>
|
||||
关闭
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getCurrentUserId } from './auth'
|
||||
import { request } from './api'
|
||||
import { resolveActiveBinding } from './binding'
|
||||
import { getChild } from './child'
|
||||
import { loadCurrentChildBindingContext } from './binding'
|
||||
|
||||
export type ConversationSource = 'im' | 'ai'
|
||||
export type PeerKind = 'child' | 'parent' | 'ai'
|
||||
@@ -38,6 +37,11 @@ export interface ChatMessage {
|
||||
content: string
|
||||
time: string
|
||||
conversationId: number
|
||||
contentType?: number
|
||||
mediaUrl?: string | null
|
||||
mediaDurationMs?: number | null
|
||||
mediaMimeType?: string | null
|
||||
mediaTranscriptText?: string | null
|
||||
senderType?: string
|
||||
senderId?: string
|
||||
receiverType?: string
|
||||
@@ -240,21 +244,12 @@ function getConversationRank(conversation: ChatConversation): number {
|
||||
}
|
||||
|
||||
async function getBindingContext(): Promise<BindingContext> {
|
||||
const binding = await resolveActiveBinding()
|
||||
let childName = String(binding?.child_name || '').trim()
|
||||
|
||||
if (!childName && binding?.child_id) {
|
||||
try {
|
||||
const child = await getChild(binding.child_id)
|
||||
childName = String(child?.child_name || '').trim()
|
||||
} catch (error) {
|
||||
console.error('[chat] load child name failed:', error)
|
||||
}
|
||||
}
|
||||
const context = await loadCurrentChildBindingContext()
|
||||
const childName = String(context.currentChild?.child_name || context.currentBinding?.child_name || '').trim()
|
||||
|
||||
return {
|
||||
deviceId: binding?.device_id || '',
|
||||
childId: binding?.child_id || null,
|
||||
deviceId: context.currentBinding?.device_id || '',
|
||||
childId: context.currentChild?.child_id || null,
|
||||
childName,
|
||||
currentUserId: getCurrentUserId(),
|
||||
}
|
||||
@@ -265,7 +260,7 @@ async function getDeviceMessages(context?: BindingContext): Promise<DeviceAiMess
|
||||
if (!deviceId) return []
|
||||
|
||||
try {
|
||||
const response = await request<DeviceAiMessageListResponse>(`/devices/${deviceId}/messages?limit=100`)
|
||||
const response = await request<DeviceAiMessageListResponse>(`/banban/devices/${deviceId}/messages?limit=100`)
|
||||
return response.items || []
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) return []
|
||||
@@ -278,7 +273,7 @@ async function getChildConversations(context?: BindingContext): Promise<ChildCon
|
||||
if (!childId) return []
|
||||
|
||||
try {
|
||||
const response = await request<ChildConversationListResponse>(`/children/${childId}/conversations?limit=100`)
|
||||
const response = await request<ChildConversationListResponse>(`/banban/children/${childId}/conversations?limit=100`)
|
||||
return response.items || []
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) return []
|
||||
@@ -294,7 +289,7 @@ async function getChildConversationMessages(
|
||||
if (!childId || conversationId <= 0) return []
|
||||
|
||||
const response = await request<ChildConversationMessageListResponse>(
|
||||
`/children/${childId}/conversations/${conversationId}/messages?limit=100`
|
||||
`/banban/children/${childId}/conversations/${conversationId}/messages?limit=100`
|
||||
)
|
||||
return response.items || []
|
||||
}
|
||||
@@ -437,6 +432,11 @@ export async function getMessages(
|
||||
content: item.content || '暂无内容',
|
||||
time: formatDetailTime(item.created_at),
|
||||
conversationId: item.conversation_id,
|
||||
contentType: 1,
|
||||
mediaUrl: null,
|
||||
mediaDurationMs: null,
|
||||
mediaMimeType: null,
|
||||
mediaTranscriptText: null,
|
||||
senderType: item.is_user ? 'device' : 'ai',
|
||||
senderId: item.is_user ? context.deviceId : item.role_key,
|
||||
receiverType: item.is_user ? 'ai' : 'device',
|
||||
@@ -462,6 +462,11 @@ export async function getMessages(
|
||||
content: formatImPreview(item),
|
||||
time: formatDetailTime(item.created_at),
|
||||
conversationId: item.conversation_id,
|
||||
contentType: item.content_type,
|
||||
mediaUrl: item.media_file_key || null,
|
||||
mediaDurationMs: item.media_duration_ms || null,
|
||||
mediaMimeType: item.media_mime_type || null,
|
||||
mediaTranscriptText: item.media_transcript_text || null,
|
||||
senderType: item.sender_type,
|
||||
senderId: item.sender_id,
|
||||
receiverType: item.receiver_type,
|
||||
@@ -580,7 +585,7 @@ export async function sendParentMessage(childId: number, content: string): Promi
|
||||
throw new Error('请输入消息内容')
|
||||
}
|
||||
|
||||
return request<ConversationMessageCreateResponse>(`/children/${childId}/messages`, {
|
||||
return request<ConversationMessageCreateResponse>(`/banban/children/${childId}/messages`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
content_type: 1,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { request } from './api'
|
||||
import { resolveActiveBinding } from './binding'
|
||||
import { loadCurrentChildBindingContext } from './binding'
|
||||
|
||||
export interface DeviceLocation {
|
||||
child_id: number
|
||||
@@ -32,11 +32,12 @@ export interface DeviceTrajectoryResponse {
|
||||
}
|
||||
|
||||
export async function getCurrentDeviceLocation(deviceId?: string): Promise<DeviceLocation | null> {
|
||||
const resolvedDeviceId = String(deviceId || '').trim() || (await resolveActiveBinding())?.device_id || ''
|
||||
const resolvedDeviceId =
|
||||
String(deviceId || '').trim() || (await loadCurrentChildBindingContext()).currentBinding?.device_id || ''
|
||||
if (!resolvedDeviceId) return null
|
||||
|
||||
try {
|
||||
return await request<DeviceLocation>(`/devices/${resolvedDeviceId}/location`)
|
||||
return await request<DeviceLocation>(`/banban/devices/${resolvedDeviceId}/location`)
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
@@ -49,7 +50,8 @@ export async function getDeviceTrajectory(params?: {
|
||||
endAt?: string
|
||||
limit?: number
|
||||
}): Promise<DeviceTrajectoryResponse | null> {
|
||||
const resolvedDeviceId = String(params?.deviceId || '').trim() || (await resolveActiveBinding())?.device_id || ''
|
||||
const resolvedDeviceId =
|
||||
String(params?.deviceId || '').trim() || (await loadCurrentChildBindingContext()).currentBinding?.device_id || ''
|
||||
if (!resolvedDeviceId) return null
|
||||
|
||||
const query = new URLSearchParams()
|
||||
@@ -59,7 +61,7 @@ export async function getDeviceTrajectory(params?: {
|
||||
|
||||
try {
|
||||
const suffix = query.toString() ? `?${query.toString()}` : ''
|
||||
return await request<DeviceTrajectoryResponse>(`/devices/${resolvedDeviceId}/trajectory${suffix}`)
|
||||
return await request<DeviceTrajectoryResponse>(`/banban/devices/${resolvedDeviceId}/trajectory${suffix}`)
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
|
||||
Reference in New Issue
Block a user