上传小程序修改合入

This commit is contained in:
HycJack
2026-05-05 12:24:48 +08:00
parent 7e29958ffa
commit bb87bff0d3
19 changed files with 684 additions and 283 deletions

View File

@@ -13,6 +13,7 @@ class ApiError extends Error {
function getErrorDetail(data: any): string {
if (!data) return 'Request failed'
if (typeof data.errMsg === 'string') return data.errMsg
if (typeof data.detail === 'string') return data.detail
if (Array.isArray(data.detail)) {
return data.detail.map((item: any) => String(item?.msg || item)).join('; ')
@@ -30,6 +31,8 @@ async function request<T>(
} = {}
): Promise<T> {
const token = getToken()
const requestUrl = `${BASE_URL}${url}`
const requestMethod = options.method || 'GET'
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options.headers,
@@ -41,22 +44,19 @@ async function request<T>(
let response
try {
response = await Taro.request({
url: `${BASE_URL}${url}`,
method: options.method || 'GET',
url: requestUrl,
method: requestMethod,
data: options.data,
header: headers,
timeout: REQUEST_TIMEOUT_MS,
})
} catch (error: any) {
const message = String(error?.errMsg || error?.message || '')
const detail = message.includes('timeout')
? '请求超时,请检查后端服务是否可访问'
: '网络请求失败,请检查后端地址和网络连接'
throw new ApiError(0, detail)
const message = String(error?.errMsg || error?.message || 'request:fail')
throw new ApiError(0, message)
}
if (response.statusCode === 401) {
handleUnauthorized({ redirect: !url.startsWith('/auth/') })
handleUnauthorized({ redirect: !url.startsWith('/banban/auth/') })
}
if (response.statusCode >= 400) {

View File

@@ -30,7 +30,7 @@ export interface Parent {
}
export async function wechatLogin(data: WechatLoginPayload): Promise<LoginSession> {
const session = await request<LoginSession>('/auth/login', {
const session = await request<LoginSession>('/banban/auth/login', {
method: 'POST',
data,
})
@@ -39,14 +39,14 @@ export async function wechatLogin(data: WechatLoginPayload): Promise<LoginSessio
}
export async function getParent(userId: number): Promise<Parent> {
return request<Parent>(`/parents/${userId}`)
return request<Parent>(`/banban/parents/${userId}`)
}
export async function updateParent(
userId: number,
data: { nickname?: string; avatar_url?: string; phone?: string }
): Promise<Parent> {
return request<Parent>(`/parents/${userId}`, {
return request<Parent>(`/banban/parents/${userId}`, {
method: 'PATCH',
data,
})

View File

@@ -1,5 +1,7 @@
import Taro from '@tarojs/taro'
import { request } from './api'
import { Child, clearSelectedChildId, getChildren, resolveChildSelection, setSelectedChildId } from './child'
const SELECTED_BINDING_DEVICE_ID_KEY = 'selectedBindingDeviceId'
@@ -26,12 +28,38 @@ export interface BindingListResponse {
next_cursor?: number | null
}
export interface BindingChildContext {
currentChild: Child | null
currentBinding: Binding | null
}
export interface DirectBindPayload {
device_id: string
serial_number: string
child_id?: number
}
export interface NFCSessionStartPayload {
device_id: string
serial_number: string
child_id?: number
}
export interface NFCSessionStartResponse {
bind_token: string
expires_at: string
status: number
}
export interface NFCSessionStatus {
bind_token: string
device_id: string
child_id: number | null
status: number
expires_at: string
card_uuid?: string | null
}
export function getSelectedBindingDeviceId(): string | null {
const value = Taro.getStorageSync(SELECTED_BINDING_DEVICE_ID_KEY)
const deviceId = String(value || '').trim()
@@ -67,7 +95,7 @@ export function resolveBindingSelection<T extends { device_id: string }>(items:
export async function getCurrentBinding(): Promise<Binding | null> {
try {
return await request<Binding>('/bindings/current')
return await request<Binding>('/banban/bindings/current')
} catch (error: any) {
if (error?.status === 404) return null
throw error
@@ -75,16 +103,13 @@ export async function getCurrentBinding(): Promise<Binding | null> {
}
export async function getBindings(cursor?: number, limit: number = 20): Promise<BindingListResponse> {
const query = buildQuery({
cursor,
limit,
})
return request<BindingListResponse>(`/bindings?${query}`)
const query = buildQuery({ cursor, limit })
return request<BindingListResponse>(`/banban/bindings?${query}`)
}
export async function getBinding(deviceId: string): Promise<Binding | null> {
try {
return await request<Binding>(`/bindings/${deviceId}`)
return await request<Binding>(`/banban/bindings/${deviceId}`)
} catch (error: any) {
if (error?.status === 404) return null
throw error
@@ -100,7 +125,6 @@ export async function resolveActiveBinding(): Promise<Binding | null> {
setSelectedBindingDeviceId(selectedBinding.device_id)
return selectedBinding
}
clearSelectedBindingDeviceId()
}
@@ -115,24 +139,92 @@ export async function resolveActiveBinding(): Promise<Binding | null> {
}
export async function directBind(data: DirectBindPayload): Promise<{ device_id: string; child_id: number | null }> {
return request('/bindings/direct', {
return request('/banban/bindings/direct', { method: 'POST', data })
}
export async function startNFCBind(data: NFCSessionStartPayload): Promise<NFCSessionStartResponse> {
return request<NFCSessionStartResponse>('/banban/bindings/start', {
method: 'POST',
data,
})
}
export async function getNFCBindSession(bindToken: string): Promise<NFCSessionStatus> {
return request<NFCSessionStatus>(`/banban/bindings/sessions/${bindToken}`)
}
export async function setBindingChild(
deviceId: string,
data: { child_id: number }
): Promise<{ device_id: string; child_id: number | null }> {
return request(`/bindings/${deviceId}/child`, {
return request(`/banban/bindings/${deviceId}/child`, {
method: 'PATCH',
data,
})
}
export async function unbindDevice(deviceId: string): Promise<void> {
return request(`/bindings/${deviceId}`, {
return request(`/banban/bindings/${deviceId}`, {
method: 'DELETE',
})
}
export function resolveBindingForChild<T extends { child_id: number | null }>(
items: T[],
childId?: number | null
): T | null {
const normalizedChildId = Number(childId || 0)
if (!Number.isFinite(normalizedChildId) || normalizedChildId <= 0) return null
const childBindings = items.filter((item) => item.child_id === normalizedChildId)
if (childBindings.length === 0) return null
const selectedDeviceId = getSelectedBindingDeviceId()
if (selectedDeviceId) {
const selectedBinding = childBindings.find((item: any) => item.device_id === selectedDeviceId) || null
if (selectedBinding) return selectedBinding
}
return childBindings[0] || null
}
export function syncChildAndBindingSelection(children: Child[], bindings: Binding[]): BindingChildContext {
const currentChild = resolveChildSelection(children)
const currentBinding = currentChild ? resolveBindingForChild(bindings, currentChild.child_id) : null
if (currentChild) {
setSelectedChildId(currentChild.child_id)
} else {
clearSelectedChildId()
}
if (currentBinding?.device_id) {
setSelectedBindingDeviceId(currentBinding.device_id)
} else {
clearSelectedBindingDeviceId()
}
return {
currentChild,
currentBinding,
}
}
export async function loadCurrentChildBindingContext(): Promise<{
children: Child[]
bindings: Binding[]
currentChild: Child | null
currentBinding: Binding | null
}> {
const [childResponse, bindingResponse] = await Promise.all([getChildren(undefined, 100), getBindings(undefined, 100)])
const children = childResponse.items || []
const bindings = bindingResponse.items || []
const { currentChild, currentBinding } = syncChildAndBindingSelection(children, bindings)
return {
children,
bindings,
currentChild,
currentBinding,
}
}

View File

@@ -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,

View File

@@ -1,5 +1,8 @@
import Taro from '@tarojs/taro'
import { request } from './api'
const SELECTED_CHILD_ID_KEY = 'selectedChildId'
function buildQuery(params: Record<string, string | number | undefined | null>): string {
return Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
@@ -21,12 +24,44 @@ export interface ChildListResponse {
next_cursor?: number | null
}
export function getSelectedChildId(): number | null {
const value = Number(Taro.getStorageSync(SELECTED_CHILD_ID_KEY) || 0)
return Number.isFinite(value) && value > 0 ? value : null
}
export function setSelectedChildId(childId?: number | null) {
const normalizedChildId = Number(childId || 0)
if (!Number.isFinite(normalizedChildId) || normalizedChildId <= 0) {
Taro.removeStorageSync(SELECTED_CHILD_ID_KEY)
return
}
Taro.setStorageSync(SELECTED_CHILD_ID_KEY, normalizedChildId)
}
export function clearSelectedChildId() {
Taro.removeStorageSync(SELECTED_CHILD_ID_KEY)
}
export function resolveChildSelection<T extends { child_id: number }>(items: T[]): T | null {
const selectedChildId = getSelectedChildId()
const selectedChild = selectedChildId ? items.find((item) => item.child_id === selectedChildId) || null : null
const currentChild = selectedChild || items[0] || null
if (currentChild) {
setSelectedChildId(currentChild.child_id)
} else {
clearSelectedChildId()
}
return currentChild
}
export async function createChild(data: {
child_name: string
child_gender?: number
child_birthday?: string
}): Promise<Child> {
return request<Child>('/children', {
return request<Child>('/banban/children', {
method: 'POST',
data,
})
@@ -37,18 +72,18 @@ export async function getChildren(cursor?: number, limit: number = 20): Promise<
cursor,
limit,
})
return request<ChildListResponse>(`/children?${query}`)
return request<ChildListResponse>(`/banban/children?${query}`)
}
export async function getChild(childId: number): Promise<Child> {
return request<Child>(`/children/${childId}`)
return request<Child>(`/banban/children/${childId}`)
}
export async function updateChild(
childId: number,
data: { child_name?: string; child_gender?: number; child_birthday?: string }
): Promise<Child> {
return request<Child>(`/children/${childId}`, {
return request<Child>(`/banban/children/${childId}`, {
method: 'PATCH',
data,
})

View File

@@ -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