合入小程序修改内容

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

View File

@@ -4,6 +4,10 @@ import { getCurrentUserId, getToken, handleUnauthorized } from './session'
const BASE_URL = API_BASE_URL
const REQUEST_TIMEOUT_MS = 10000
const ERROR_DETAIL_MAP: Record<string, string> = {
'Request failed': '请求失败',
'device is already bound, unbind it before binding again': '设备已绑定,请先解绑后再重新绑定',
}
class ApiError extends Error {
constructor(public status: number, message: string) {
@@ -12,14 +16,14 @@ class ApiError extends Error {
}
function getErrorDetail(data: any): string {
if (!data) return 'Request failed'
if (!data) return ERROR_DETAIL_MAP['Request failed']
if (typeof data.errMsg === 'string') return data.errMsg
if (typeof data.detail === 'string') return data.detail
if (typeof data.detail === 'string') return ERROR_DETAIL_MAP[data.detail] || data.detail
if (Array.isArray(data.detail)) {
return data.detail.map((item: any) => String(item?.msg || item)).join('; ')
}
if (typeof data.message === 'string') return data.message
return 'Request failed'
return ERROR_DETAIL_MAP['Request failed']
}
async function request<T>(

View File

@@ -17,6 +17,7 @@ export interface LoginSession {
token_type: string
expires_in: number
user_id: number
nickname?: string
}
export interface Parent {

View File

@@ -1,6 +1,8 @@
import Taro from '@tarojs/taro'
import { getCurrentUserId } from './auth'
import { request } from './api'
import { ApiError, BASE_URL, getToken, request } from './api'
import { loadCurrentChildBindingContext } from './binding'
import { handleUnauthorized } from './session'
export type ConversationSource = 'im' | 'ai'
export type PeerKind = 'child' | 'parent' | 'ai'
@@ -49,6 +51,7 @@ export interface ChatMessage {
senderName?: string
receiverName?: string
channelLabel?: string
extJson?: Record<string, any> | null
}
export interface Character {
@@ -201,10 +204,18 @@ function formatDetailTime(value?: string | null): string {
}
function formatImPreview(
item: Pick<ChildConversationMessageItem, 'content_type' | 'content_text' | 'media_transcript_text' | 'content_json'>
item: Pick<
ChildConversationMessageItem,
'content_type' | 'content_text' | 'media_transcript_text' | 'content_json' | 'ext_json'
>
): string {
if (item.content_type === 1) return item.content_text || '文本消息'
if (item.content_type === 2) return item.media_transcript_text || '[语音]'
if (item.content_type === 2) {
if (item.ext_json?.message_kind === 'leave_message') {
return item.media_transcript_text || '[留言]'
}
return item.media_transcript_text || '[语音]'
}
if (item.content_type === 3) return '[图片]'
if (item.content_type === 4) {
const title = String(item.content_json?.title || item.content_json?.type || '').trim()
@@ -361,7 +372,7 @@ function createSyntheticParentConversation(context: BindingContext): ChatConvers
avatar: PEER_META.parent.icon,
typeLabel: '家长沟通',
description: '家长通过微信小程序和孩子直接沟通',
lastMessage: '还没有消息,点进去发送第一条',
lastMessage: '还没有留言,点进去发送第一条',
time: '',
sortAt: '',
conversationTypeName: PARENT_CHILD_CONVERSATION_TYPE_NAME,
@@ -471,6 +482,7 @@ export async function getMessages(
senderId: item.sender_id,
receiverType: item.receiver_type,
receiverId: item.receiver_id,
extJson: item.ext_json || null,
senderName:
item.sender_type === 'parent' && item.sender_id === String(context.currentUserId)
? '我'
@@ -594,3 +606,68 @@ export async function sendParentMessage(childId: number, content: string): Promi
},
})
}
function parseUploadResponseData(rawData: any): any {
if (typeof rawData !== 'string') return rawData
try {
return JSON.parse(rawData)
} catch {
return { detail: rawData }
}
}
function getUploadErrorMessage(data: any): string {
if (!data) return '留言发送失败'
if (typeof data.detail === 'string') return data.detail
if (typeof data.message === 'string') return data.message
if (typeof data.errMsg === 'string') return data.errMsg
return '留言发送失败'
}
export async function sendParentVoiceMessage(
childId: number,
options: { filePath: string; durationMs: number; transcriptText?: string }
): Promise<ConversationMessageCreateResponse> {
if (!childId) {
throw new Error('当前会话不支持留言')
}
if (!options.filePath) {
throw new Error('录音文件不存在')
}
const token = getToken()
const requestUrl = `${BASE_URL}/banban/children/${childId}/voice-message`
const uploadResponse: any = await new Promise((resolve, reject) => {
Taro.uploadFile({
url: requestUrl,
filePath: options.filePath,
name: 'file',
header: token
? {
Authorization: `Bearer ${token}`,
}
: {},
formData: {
duration_ms: `${Math.max(1, Math.round(options.durationMs || 0))}`,
client_msg_id: createClientMessageId(),
transcript_text: options.transcriptText || '',
},
success: resolve,
fail: reject,
})
}).catch((error: any) => {
const message = String(error?.errMsg || error?.message || 'request:fail')
throw new ApiError(0, message)
})
if (uploadResponse.statusCode === 401) {
handleUnauthorized({ redirect: true })
}
const data = parseUploadResponseData(uploadResponse.data)
if (uploadResponse.statusCode >= 400) {
throw new ApiError(uploadResponse.statusCode, getUploadErrorMessage(data))
}
return data as ConversationMessageCreateResponse
}

View File

@@ -0,0 +1,62 @@
import { request } from './api'
import { loadCurrentChildBindingContext } from './binding'
export interface DeviceStatus {
device_id: string
child_id?: number | null
child_name?: string | null
power?: number | null
volume?: number | null
signal?: number | null
version?: string | null
settings_updated_at?: string | null
coord_type?: string | null
lat?: number | null
lng?: number | null
accuracy_m?: number | null
altitude_m?: number | null
speed_mps?: number | null
heading_deg?: number | null
source?: number | null
battery_pct?: number | null
device_time?: string | null
server_time?: string | null
location_updated_at?: string | null
}
export interface DeviceVolumeUpdateResponse {
device_id: string
level: number
msg_id: string
}
async function resolveDeviceId(deviceId?: string): Promise<string> {
const normalizedDeviceId = String(deviceId || '').trim()
if (normalizedDeviceId) return normalizedDeviceId
const context = await loadCurrentChildBindingContext()
return context.currentBinding?.device_id || ''
}
export async function getDeviceStatus(deviceId?: string): Promise<DeviceStatus | null> {
const resolvedDeviceId = await resolveDeviceId(deviceId)
if (!resolvedDeviceId) return null
try {
return await request<DeviceStatus>(`/banban/devices/${resolvedDeviceId}/status`)
} catch (error: any) {
if (error?.status === 404) return null
throw error
}
}
export async function setDeviceVolume(level: number, deviceId?: string): Promise<DeviceVolumeUpdateResponse> {
const resolvedDeviceId = await resolveDeviceId(deviceId)
if (!resolvedDeviceId) {
throw new Error('当前没有可用设备')
}
return request<DeviceVolumeUpdateResponse>(`/banban/devices/${resolvedDeviceId}/volume`, {
method: 'POST',
data: { level },
})
}