From 2639ab06ab54fb473d94eba22cc10287aec2329a Mon Sep 17 00:00:00 2001 From: HycJack <772403255@qq.com> Date: Mon, 18 May 2026 20:07:28 +0800 Subject: [PATCH] =?UTF-8?q?add=20=E5=B0=8F=E7=A8=8B=E5=BA=8F=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=A7=92=E8=89=B2=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- banban-mini/src/pages/sleep/index.scss | 51 +++++++++ banban-mini/src/pages/sleep/index.tsx | 140 ++++++++++++++++++++++++- banban-mini/src/services/chat.ts | 137 +++++++++++++++++------- banban-mini/src/services/device.ts | 54 ++++++++++ 4 files changed, 346 insertions(+), 36 deletions(-) diff --git a/banban-mini/src/pages/sleep/index.scss b/banban-mini/src/pages/sleep/index.scss index 6074d2a..5dfe3b5 100644 --- a/banban-mini/src/pages/sleep/index.scss +++ b/banban-mini/src/pages/sleep/index.scss @@ -380,6 +380,10 @@ max-height: 70vh; } +.role-switch-card { + max-height: 74vh; +} + .device-switch-empty { display: block; font-size: 28px; @@ -392,6 +396,11 @@ overflow-y: auto; } +.role-switch-list { + max-height: 56vh; + overflow-y: auto; +} + .device-switch-footer { margin-top: 20px; } @@ -424,6 +433,48 @@ } } +.role-switch-item { + padding: 24px; + border-radius: 18px; + background: #F7F8FA; + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } + + &.active { + background: #FFF2E7; + border: 2px solid #FFBE94; + } + + &.disabled { + opacity: 0.58; + } +} + +.role-switch-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 10px; +} + +.role-switch-name { + min-width: 0; + font-size: 30px; + color: #1A1A1A; + font-weight: 700; +} + +.role-switch-desc { + display: block; + font-size: 25px; + line-height: 1.55; + color: #666666; +} + .device-switch-head { display: flex; align-items: center; diff --git a/banban-mini/src/pages/sleep/index.tsx b/banban-mini/src/pages/sleep/index.tsx index 62d654f..2264cb3 100644 --- a/banban-mini/src/pages/sleep/index.tsx +++ b/banban-mini/src/pages/sleep/index.tsx @@ -10,7 +10,16 @@ import { unbindDevice, } from '@/services/binding' import { Child, clearSelectedChildId, createChild, setSelectedChildId, updateChild } from '@/services/child' -import { DeviceFirmwareStatus, getDeviceFirmwareStatus, startDeviceFirmwareUpdate } from '@/services/device' +import { + DeviceCurrentRole, + DeviceFirmwareStatus, + DeviceRoleSummary, + getDeviceFirmwareStatus, + getDeviceRole, + getDeviceRoles, + startDeviceFirmwareUpdate, + updateDeviceRole, +} from '@/services/device' import { useSystemBanner } from '@/components/system-banner/use-system-banner' import './index.scss' @@ -31,10 +40,15 @@ export default function Sleep() { const [binding, setBinding] = useState(null) const [currentChild, setCurrentChild] = useState(null) const [firmwareStatus, setFirmwareStatus] = useState(null) + const [deviceRole, setDeviceRole] = useState(null) + const [roles, setRoles] = useState([]) const [isLoadingFirmware, setIsLoadingFirmware] = useState(false) const [isUpdatingFirmware, setIsUpdatingFirmware] = useState(false) + const [isLoadingRoles, setIsLoadingRoles] = useState(false) + const [isUpdatingRole, setIsUpdatingRole] = useState(false) const [showModal, setShowModal] = useState(false) const [showChildModal, setShowChildModal] = useState(false) + const [showRoleModal, setShowRoleModal] = useState(false) const [modalType, setModalType] = useState<'add' | 'edit'>('add') const [childName, setChildName] = useState('') const [editingChildId, setEditingChildId] = useState(null) @@ -60,6 +74,7 @@ export default function Sleep() { setBinding(nextBinding) setParentInfo(Taro.getStorageSync('userInfo') || {}) void loadFirmwareStatus(nextBinding?.device_id) + void loadRoleData(nextBinding?.device_id) } catch (error: any) { console.error('[manage] load failed:', error) Taro.showToast({ @@ -97,6 +112,36 @@ export default function Sleep() { } } + const loadRoleData = async (deviceId?: string | null) => { + const normalizedDeviceId = String(deviceId || '').trim() + if (!normalizedDeviceId) { + setDeviceRole(null) + setRoles([]) + setIsLoadingRoles(false) + return + } + + setIsLoadingRoles(true) + try { + const [nextRoles, nextDeviceRole] = await Promise.all([ + getDeviceRoles(), + getDeviceRole(normalizedDeviceId), + ]) + setRoles(nextRoles) + setDeviceRole(nextDeviceRole) + } catch (error: any) { + console.error('[manage] role load failed:', error) + setRoles([]) + setDeviceRole(null) + Taro.showToast({ + title: error?.message || '角色信息加载失败', + icon: 'none', + }) + } finally { + setIsLoadingRoles(false) + } + } + const handleOpenModal = (type: 'add' | 'edit', child?: Child) => { setModalType(type) setChildName(child?.child_name || '') @@ -121,6 +166,7 @@ export default function Sleep() { setCurrentChild(child) setBinding(nextBinding) void loadFirmwareStatus(nextBinding?.device_id) + void loadRoleData(nextBinding?.device_id) setShowChildModal(false) Taro.showToast({ title: '已切换当前孩子', @@ -192,6 +238,37 @@ export default function Sleep() { }) } + const handleOpenRoleModal = () => { + if (!binding?.device_id) { + Taro.showToast({ title: '当前没有可用设备', icon: 'none' }) + return + } + setShowRoleModal(true) + if (roles.length === 0) { + void loadRoleData(binding.device_id) + } + } + + const handleSelectRole = async (role: DeviceRoleSummary) => { + if (!binding?.device_id || isUpdatingRole) return + + setIsUpdatingRole(true) + try { + const language = deviceRole?.preferred_language || role.default_language || role.languages?.[0] || null + const nextRole = await updateDeviceRole(role.role_key, language, binding.device_id) + setDeviceRole(nextRole) + setShowRoleModal(false) + Taro.showToast({ title: 'AI 角色已切换', icon: 'success' }) + } catch (error: any) { + Taro.showToast({ + title: error?.message || '角色切换失败', + icon: 'none', + }) + } finally { + setIsUpdatingRole(false) + } + } + const handleUnbind = () => { if (!binding) return @@ -268,6 +345,11 @@ export default function Sleep() { return } + if (item.name === 'AI 角色') { + handleOpenRoleModal() + return + } + if (item.name === '解除设备绑定') { handleUnbind() } @@ -289,6 +371,11 @@ export default function Sleep() { const latestFirmwareLabel = firmwareStatus?.latest_version || '--' const canUpdateFirmware = Boolean(binding?.device_id && firmwareStatus?.can_update && !isLoadingFirmware && !isUpdatingFirmware) const firmwareActionText = isLoadingFirmware ? '查询中' : isUpdatingFirmware ? '发送中' : '更新' + const roleValue = !binding?.device_id + ? '未绑定' + : isLoadingRoles + ? '查询中' + : deviceRole?.name || '未设置' const firmwareSubtitle = !binding?.device_id ? '绑定设备后可查看系统版本' : isLoadingFirmware @@ -325,6 +412,14 @@ export default function Sleep() { value: binding?.device_id ? `当前: ${binding.device_id}` : '未绑定', arrow: true, }, + { + icon: require('../../assets/tab-icons/orange-robot.png'), + iconBgClass: 'green', + name: 'AI 角色', + value: roleValue, + arrow: true, + disabled: !binding, + }, { icon: require('../../assets/tab-icons/broken-rings.png'), iconBgClass: 'red', @@ -501,6 +596,49 @@ export default function Sleep() { )} + {showRoleModal && ( + setShowRoleModal(false)}> + { + event.stopPropagation() + }} + > + 切换 AI 角色 + {isLoadingRoles ? ( + 角色加载中... + ) : roles.length === 0 ? ( + 当前没有可用角色 + ) : ( + + {roles.map((item) => { + const isActive = deviceRole?.role_key === item.role_key + return ( + handleSelectRole(item)} + > + + {item.name || item.role_key} + {isActive && 当前} + + + {item.description || `角色标识:${item.role_key}`} + + + ) + })} + + )} + + setShowRoleModal(false)}> + 关闭 + + + + + )} {systemBanner} ) diff --git a/banban-mini/src/services/chat.ts b/banban-mini/src/services/chat.ts index 757da1c..65ad358 100644 --- a/banban-mini/src/services/chat.ts +++ b/banban-mini/src/services/chat.ts @@ -88,6 +88,30 @@ interface DeviceAiMessageListResponse { next_cursor?: number | null } +interface DeviceAiConversationItem { + conversation_id: number + role_key: string + role_name: string + role_description?: string | null + message_count: number + last_message_preview?: string | null + last_message_at?: string | null + created_at: string + updated_at: string +} + +interface DeviceAiConversationListResponse { + items: DeviceAiConversationItem[] + total: number + next_cursor?: number | null +} + +interface DeviceRoleSummary { + role_key: string + name: string + description?: string | null +} + interface ChildConversationItem { conversation_id: number conversation_type: number @@ -161,6 +185,8 @@ const AI_ROLE_META: Record> { + try { + const roles = await request('/banban/roles') + return roles.reduce>((result, role) => { + const roleKey = String(role.role_key || '').trim() + if (!roleKey) return result + result[roleKey] = getAiMeta(roleKey, role.name, role.description) + return result + }, {}) + } catch (error: any) { + if (error?.status === 404) return {} + throw error + } +} + function getParticipantDisplayName(type: string, id?: string, name?: string | null): string { if (name && name.trim()) return name.trim() if (type === 'child') return id ? `儿童 ${id}` : '儿童' @@ -276,6 +326,21 @@ async function getDeviceMessages(context?: BindingContext): Promise { + const { deviceId } = context || (await getBindingContext()) + if (!deviceId) return [] + + try { + const response = await request( + `/banban/devices/${deviceId}/ai-conversations?limit=100` + ) + return response.items || [] + } catch (error: any) { + if (error?.status === 404) return [] + throw error + } +} + async function getChildConversations(context?: BindingContext): Promise { const { childId } = context || (await getBindingContext()) if (!childId) return [] @@ -330,8 +395,10 @@ function toImConversation(item: ChildConversationItem, context: BindingContext): } } -function toAiConversation(item: DeviceAiMessageItem, context: BindingContext): ChatConversation { - const meta = getAiMeta(item.role_key) +function toAiConversation(item: DeviceAiConversationItem, context: BindingContext, roleMetaMap: Record): ChatConversation { + const meta = roleMetaMap[item.role_key] || getAiMeta(item.role_key, item.role_name, item.role_description) + const lastMessage = item.last_message_preview || (item.message_count > 0 ? '暂无内容' : '这个会话还没有消息') + const sortAt = item.last_message_at || item.updated_at || item.created_at return { id: item.conversation_id, @@ -342,9 +409,9 @@ function toAiConversation(item: DeviceAiMessageItem, context: BindingContext): C avatar: meta.icon, typeLabel: PEER_META.ai.label, description: meta.description, - lastMessage: item.content || '暂无消息', - time: formatListTime(item.created_at), - sortAt: item.created_at, + lastMessage, + time: formatListTime(sortAt), + sortAt, roleKey: item.role_key, conversationTypeName: 'ai', peerId: item.role_key, @@ -386,19 +453,16 @@ function createSyntheticParentConversation(context: BindingContext): ChatConvers export async function getConversations(): Promise { const context = await getBindingContext() - const [imConversationItems, aiMessages] = await Promise.all([getChildConversations(context), getDeviceMessages(context)]) + const [imConversationItems, aiConversations, roleMetaMap] = await Promise.all([ + getChildConversations(context), + getDeviceAiConversations(context), + getRoleMetaMap(), + ]) const imConversations = imConversationItems.map((item) => toImConversation(item, context)) - const aiConversationMap = new Map() - - for (const item of aiMessages) { - if (!aiConversationMap.has(item.conversation_id)) { - aiConversationMap.set(item.conversation_id, item) - } - } const conversations = [ ...imConversations, - ...Array.from(aiConversationMap.values()).map((item) => toAiConversation(item, context)), + ...aiConversations.map((item) => toAiConversation(item, context, roleMetaMap)), ] const hasParentConversation = conversations.some( @@ -429,30 +493,33 @@ export async function getMessages( const context = await getBindingContext() if (source === 'ai') { - const items = await getDeviceMessages(context) + const [items, roleMetaMap] = await Promise.all([getDeviceMessages(context), getRoleMetaMap()]) return items .filter((item) => item.conversation_id === conversationId) .slice() .reverse() - .map((item) => ({ - id: item.id, - type: item.is_user ? 'user' : 'peer', - 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', - receiverId: item.is_user ? item.role_key : context.deviceId, - senderName: item.is_user ? context.childName || '孩子设备' : getAiMeta(item.role_key).name, - receiverName: item.is_user ? getAiMeta(item.role_key).name : context.childName || '孩子设备', - channelLabel: 'AI', - })) + .map((item) => { + const meta = roleMetaMap[item.role_key] || getAiMeta(item.role_key) + return { + id: item.id, + type: item.is_user ? 'user' : 'peer', + 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', + receiverId: item.is_user ? item.role_key : context.deviceId, + senderName: item.is_user ? context.childName || '孩子设备' : meta.name, + receiverName: item.is_user ? meta.name : context.childName || '孩子设备', + channelLabel: 'AI', + } + }) } const items = await getChildConversationMessages(conversationId, context) diff --git a/banban-mini/src/services/device.ts b/banban-mini/src/services/device.ts index ff78d1e..0b0bc9f 100644 --- a/banban-mini/src/services/device.ts +++ b/banban-mini/src/services/device.ts @@ -79,6 +79,23 @@ export interface DeviceFirmwareUpdateResponse extends DeviceFirmwareStatus { msg_id: string } +export interface DeviceRoleSummary { + role_key: string + name: string + description?: string | null + default_language?: string | null + languages: string[] +} + +export interface DeviceCurrentRole { + device_id: string + role_key: string + name: string + description?: string | null + preferred_language?: string | null + languages: string[] +} + function normalizeTimeValue(value?: string | null): string | null { if (value === null || value === undefined) return null const trimmed = String(value).trim() @@ -203,3 +220,40 @@ export async function getDeviceAlarms( return request(`/banban/devices/${resolvedDeviceId}/alarms?limit=${limit}`) } + +export async function getDeviceRoles(): Promise { + return request('/banban/roles') +} + +export async function getDeviceRole(deviceId?: string): Promise { + const resolvedDeviceId = await resolveDeviceId(deviceId) + if (!resolvedDeviceId) return null + + try { + return await request(`/banban/roles/devices/${resolvedDeviceId}`) + } catch (error: any) { + if (error?.status === 404) return null + throw error + } +} + +export async function updateDeviceRole( + roleKey: string, + language?: string | null, + deviceId?: string, + playWelcome = false +): Promise { + const resolvedDeviceId = await resolveDeviceId(deviceId) + if (!resolvedDeviceId) { + throw new Error('当前没有可用设备') + } + + return request(`/banban/roles/devices/${resolvedDeviceId}`, { + method: 'PUT', + data: { + role_key: roleKey, + language: language || undefined, + play_welcome: playWelcome, + }, + }) +}