feat(小程序): 支持多设备绑定与管理

This commit is contained in:
stu2not
2026-04-22 17:25:31 +08:00
parent d7df6f565c
commit f21e0afcd0
8 changed files with 1078 additions and 670 deletions

View File

@@ -1,97 +1,126 @@
import Taro from '@tarojs/taro'
import { request } from './api'
const SELECTED_BINDING_DEVICE_ID_KEY = 'selectedBindingDeviceId'
function buildQuery(params: Record<string, string | number | undefined | null>): string {
return Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
.join('&')
}
export interface Binding {
device_id: string
child_id: number | null
child_name?: string | null
status: number
bound_at: string
}
export interface BindStartResponse {
bind_token: string
expires_at: string
}
export interface BindingListItem extends Binding {}
export interface BindHistoryItem {
device_id: string
child_id: number | null
bound_at: string
unbound_at?: string
}
export interface BindHistoryResponse {
items: BindHistoryItem[]
export interface BindingListResponse {
items: BindingListItem[]
total: number
next_cursor?: string
next_cursor?: number | null
}
export interface DirectBindPayload {
device_id: string
serial_number: string
child_id?: number
}
export function getSelectedBindingDeviceId(): string | null {
const value = Taro.getStorageSync(SELECTED_BINDING_DEVICE_ID_KEY)
const deviceId = String(value || '').trim()
return deviceId || null
}
export function setSelectedBindingDeviceId(deviceId?: string | null) {
const normalizedDeviceId = String(deviceId || '').trim()
if (!normalizedDeviceId) {
Taro.removeStorageSync(SELECTED_BINDING_DEVICE_ID_KEY)
return
}
Taro.setStorageSync(SELECTED_BINDING_DEVICE_ID_KEY, normalizedDeviceId)
}
export function clearSelectedBindingDeviceId() {
Taro.removeStorageSync(SELECTED_BINDING_DEVICE_ID_KEY)
}
export function resolveBindingSelection<T extends { device_id: string }>(items: T[]): T | null {
const selectedDeviceId = getSelectedBindingDeviceId()
const selectedBinding = selectedDeviceId ? items.find((item) => item.device_id === selectedDeviceId) || null : null
const activeBinding = selectedBinding || items[0] || null
if (activeBinding) {
setSelectedBindingDeviceId(activeBinding.device_id)
} else {
clearSelectedBindingDeviceId()
}
return activeBinding
}
// 获取当前绑定信息
export async function getCurrentBinding(): Promise<Binding | null> {
try {
return await request<Binding>('/bindings/current')
} catch (err: any) {
if (err.status === 404) {
return null
}
throw err
} catch (error: any) {
if (error?.status === 404) return null
throw error
}
}
// 开始绑定设备
export async function startBind(data: {
device_id: string
child_id?: number
}): Promise<BindStartResponse> {
return request<BindStartResponse>('/bindings/start', {
method: 'POST',
data,
export async function getBindings(cursor?: number, limit: number = 20): Promise<BindingListResponse> {
const query = buildQuery({
cursor,
limit,
})
return request<BindingListResponse>(`/bindings?${query}`)
}
// 确认绑定设备
export async function confirmBind(data: {
bind_token: string
challenge_code: string
}): Promise<{ device_id: string; child_id: number | null }> {
return request('/bindings/confirm', {
method: 'POST',
data,
})
}
// 获取设备绑定信息
export async function getBinding(deviceId: string): Promise<Binding | null> {
// If no deviceId, get the first binding for this user
if (!deviceId) {
try {
// Try to get binding list - for now return null if no specific device
return null
} catch {
return null
}
try {
return await request<Binding>(`/bindings/${deviceId}`)
} catch (error: any) {
if (error?.status === 404) return null
throw error
}
return request<Binding>(`/bindings/${deviceId}`)
}
// 解绑设备
export async function unbindDevice(deviceId: string): Promise<void> {
return request(`/bindings/${deviceId}`, {
method: 'DELETE',
})
export async function resolveActiveBinding(): Promise<Binding | null> {
const selectedDeviceId = getSelectedBindingDeviceId()
if (selectedDeviceId) {
const selectedBinding = await getBinding(selectedDeviceId)
if (selectedBinding) {
setSelectedBindingDeviceId(selectedBinding.device_id)
return selectedBinding
}
clearSelectedBindingDeviceId()
}
const currentBinding = await getCurrentBinding()
if (currentBinding?.device_id) {
setSelectedBindingDeviceId(currentBinding.device_id)
} else {
clearSelectedBindingDeviceId()
}
return currentBinding
}
// 直接绑定设备(手动输入设备码)
export async function directBind(data: {
device_id: string
child_id?: number
}): Promise<{ device_id: string; child_id: number | null }> {
export async function directBind(data: DirectBindPayload): Promise<{ device_id: string; child_id: number | null }> {
return request('/bindings/direct', {
method: 'POST',
data,
})
}
// Bind child profile after device has been bound to parent account.
export async function setBindingChild(
deviceId: string,
data: { child_id: number }
@@ -102,15 +131,8 @@ export async function setBindingChild(
})
}
// 获取绑定历史
export async function getBindHistory(
deviceId: string,
cursor?: string,
limit: number = 20
): Promise<BindHistoryResponse> {
const params = new URLSearchParams()
if (cursor) params.append('cursor', cursor)
params.append('limit', String(limit))
return request<BindHistoryResponse>(`/bindings/history/${deviceId}?${params.toString()}`)
export async function unbindDevice(deviceId: string): Promise<void> {
return request(`/bindings/${deviceId}`, {
method: 'DELETE',
})
}

View File

@@ -1,5 +1,12 @@
import { request } from './api'
function buildQuery(params: Record<string, string | number | undefined | null>): string {
return Object.entries(params)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
.join('&')
}
export interface Child {
child_id: number
child_name: string
@@ -11,10 +18,9 @@ export interface Child {
export interface ChildListResponse {
items: Child[]
total: number
next_cursor?: number
next_cursor?: number | null
}
// 创建小孩档案
export async function createChild(data: {
child_name: string
child_gender?: number
@@ -26,24 +32,18 @@ export async function createChild(data: {
})
}
// 获取小孩列表
export async function getChildren(
cursor?: number,
limit: number = 20
): Promise<ChildListResponse> {
const params = new URLSearchParams()
if (cursor) params.append('cursor', String(cursor))
params.append('limit', String(limit))
return request<ChildListResponse>(`/children?${params.toString()}`)
export async function getChildren(cursor?: number, limit: number = 20): Promise<ChildListResponse> {
const query = buildQuery({
cursor,
limit,
})
return request<ChildListResponse>(`/children?${query}`)
}
// 获取单个小孩信息
export async function getChild(childId: number): Promise<Child> {
return request<Child>(`/children/${childId}`)
}
// 更新小孩信息
export async function updateChild(
childId: number,
data: { child_name?: string; child_gender?: number; child_birthday?: string }
@@ -52,4 +52,4 @@ export async function updateChild(
method: 'PATCH',
data,
})
}
}