提交小程序合入代码

This commit is contained in:
HycJack
2026-05-09 16:50:42 +08:00
parent 83bb79a8d0
commit a7f3b4f8dd
19 changed files with 1762 additions and 30 deletions

View File

@@ -211,10 +211,7 @@ function formatImPreview(
): string {
if (item.content_type === 1) return item.content_text || '文本消息'
if (item.content_type === 2) {
if (item.ext_json?.message_kind === 'leave_message') {
return item.media_transcript_text || '[留言]'
}
return item.media_transcript_text || '[语音]'
return ''
}
if (item.content_type === 3) return '[图片]'
if (item.content_type === 4) {

View File

@@ -5,6 +5,10 @@ export interface DeviceStatus {
device_id: string
child_id?: number | null
child_name?: string | null
sleep_mode?: number | null
disable_time_start?: string | null
disable_time_end?: string | null
timezone?: string | null
power?: number | null
volume?: number | null
signal?: number | null
@@ -24,12 +28,77 @@ export interface DeviceStatus {
location_updated_at?: string | null
}
export interface DeviceAlarmItem {
alarm_id: number
device_id: string
child_id?: number | null
child_name?: string | null
source_msg_id: string
created_at: string
}
export interface DeviceAlarmListResponse {
items: DeviceAlarmItem[]
total: number
}
export interface DeviceVolumeUpdateResponse {
device_id: string
level: number
msg_id: string
}
export interface DeviceSleepScheduleUpdateResponse {
device_id: string
sleep_mode: number
start?: string | null
end?: string | null
timezone: string
msg_id?: string | null
}
export interface DeviceRemoteSleepWakeResponse {
device_id: string
switch: 'on' | 'off'
msg_id: string
}
export interface DeviceFirmwareStatus {
device_id: string
current_version?: string | null
latest_version?: string | null
update_available: boolean
can_update: boolean
update_status: string
progress: number
target_version?: string | null
updated_at?: string | null
}
export interface DeviceFirmwareUpdateResponse extends DeviceFirmwareStatus {
msg_id: string
}
function normalizeTimeValue(value?: string | null): string | null {
if (value === null || value === undefined) return null
const trimmed = String(value).trim()
if (!trimmed) return null
const matched = trimmed.match(/^(\d{1,2}):(\d{2})(?::\d{2})?:?$/)
if (!matched) return trimmed.replace(/:+$/, '')
const hour = matched[1].padStart(2, '0')
return `${hour}:${matched[2]}`
}
function normalizeDeviceStatus(status: DeviceStatus): DeviceStatus {
return {
...status,
disable_time_start: normalizeTimeValue(status.disable_time_start),
disable_time_end: normalizeTimeValue(status.disable_time_end),
}
}
async function resolveDeviceId(deviceId?: string): Promise<string> {
const normalizedDeviceId = String(deviceId || '').trim()
if (normalizedDeviceId) return normalizedDeviceId
@@ -42,7 +111,8 @@ export async function getDeviceStatus(deviceId?: string): Promise<DeviceStatus |
if (!resolvedDeviceId) return null
try {
return await request<DeviceStatus>(`/banban/devices/${resolvedDeviceId}/status`)
const result = await request<DeviceStatus>(`/banban/devices/${resolvedDeviceId}/status`)
return normalizeDeviceStatus(result)
} catch (error: any) {
if (error?.status === 404) return null
throw error
@@ -60,3 +130,76 @@ export async function setDeviceVolume(level: number, deviceId?: string): Promise
data: { level },
})
}
export async function setDeviceSleepSchedule(
start: string,
end: string,
timezone = 'Asia/Shanghai',
deviceId?: string
): Promise<DeviceSleepScheduleUpdateResponse> {
const resolvedDeviceId = await resolveDeviceId(deviceId)
if (!resolvedDeviceId) {
throw new Error('当前没有可用设备')
}
const result = await request<DeviceSleepScheduleUpdateResponse>(`/banban/devices/${resolvedDeviceId}/sleep-schedule`, {
method: 'POST',
data: { start, end, timezone },
})
return {
...result,
start: normalizeTimeValue(result.start),
end: normalizeTimeValue(result.end),
}
}
export async function setDeviceRemoteSleepWake(
switchValue: 'on' | 'off',
deviceId?: string
): Promise<DeviceRemoteSleepWakeResponse> {
const resolvedDeviceId = await resolveDeviceId(deviceId)
if (!resolvedDeviceId) {
throw new Error('当前没有可用设备')
}
return request<DeviceRemoteSleepWakeResponse>(`/banban/devices/${resolvedDeviceId}/sleep-wake`, {
method: 'POST',
data: { switch: switchValue },
})
}
export async function getDeviceFirmwareStatus(deviceId?: string): Promise<DeviceFirmwareStatus | null> {
const resolvedDeviceId = await resolveDeviceId(deviceId)
if (!resolvedDeviceId) return null
try {
return await request<DeviceFirmwareStatus>(`/banban/devices/${resolvedDeviceId}/firmware`)
} catch (error: any) {
if (error?.status === 404) return null
throw error
}
}
export async function startDeviceFirmwareUpdate(deviceId?: string): Promise<DeviceFirmwareUpdateResponse> {
const resolvedDeviceId = await resolveDeviceId(deviceId)
if (!resolvedDeviceId) {
throw new Error('当前没有可用设备')
}
return request<DeviceFirmwareUpdateResponse>(`/banban/devices/${resolvedDeviceId}/firmware/update`, {
method: 'POST',
})
}
export async function getDeviceAlarms(
deviceId?: string,
limit = 20
): Promise<DeviceAlarmListResponse> {
const resolvedDeviceId = await resolveDeviceId(deviceId)
if (!resolvedDeviceId) {
return { items: [], total: 0 }
}
return request<DeviceAlarmListResponse>(`/banban/devices/${resolvedDeviceId}/alarms?limit=${limit}`)
}

View File

@@ -0,0 +1,175 @@
import Taro from '@tarojs/taro'
import { Binding, getBindings } from './binding'
import { DeviceAlarmItem, getDeviceAlarms } from './device'
const LAST_SEEN_ALARM_MAP_KEY = 'systemBannerLastSeenAlarmMap'
const DEVICE_ALARM_FETCH_LIMIT = 20
const LAST_POLL_TIME_KEY = 'systemBannerLastPollTime'
type LastSeenAlarmMap = Record<string, number>
export interface SystemBannerNotification {
key: string
title: string
summary: string
detail: string
timeLabel: string
route: string
alarmId: number
deviceId: string
}
function readLastSeenAlarmMap(): LastSeenAlarmMap {
const storedValue = Taro.getStorageSync(LAST_SEEN_ALARM_MAP_KEY)
if (!storedValue || typeof storedValue !== 'object') return {}
return storedValue as LastSeenAlarmMap
}
function writeLastSeenAlarmMap(map: LastSeenAlarmMap) {
Taro.setStorageSync(LAST_SEEN_ALARM_MAP_KEY, map)
}
function readLastPollTime(): number {
const value = Number(Taro.getStorageSync(LAST_POLL_TIME_KEY) || 0)
return Number.isFinite(value) ? value : 0
}
function writeLastPollTime(value: number) {
Taro.setStorageSync(LAST_POLL_TIME_KEY, value)
}
function setDeviceAlarmSeen(deviceId: string, alarmId: number, map: LastSeenAlarmMap) {
map[deviceId] = alarmId
}
function hasDeviceAlarmBaseline(deviceId: string, map: LastSeenAlarmMap): boolean {
return Object.prototype.hasOwnProperty.call(map, deviceId)
}
function getAlarmChildLabel(alarm: DeviceAlarmItem, fallbackChildName?: string | null): string {
const childName = String(alarm.child_name || '').trim()
if (childName) return childName
const fallbackName = String(fallbackChildName || '').trim()
if (fallbackName) return fallbackName
if (alarm.child_id) return `儿童 ${alarm.child_id}`
return '当前孩子'
}
function getAlarmTypeLabel(sourceMsgId?: string | null): string {
const normalizedValue = String(sourceMsgId || '').trim()
if (normalizedValue === '010') return '紧急告警'
if (!normalizedValue) return '设备提醒'
return normalizedValue
}
function formatBannerTime(value?: string | null): string {
if (!value) return ''
const date = new Date(value)
if (Number.isNaN(date.getTime())) return ''
const hour = `${date.getHours()}`.padStart(2, '0')
const minute = `${date.getMinutes()}`.padStart(2, '0')
return `${hour}:${minute}`
}
function buildDeviceAlarmNotification(
alarm: DeviceAlarmItem,
fallbackChildName?: string | null
): SystemBannerNotification {
const childLabel = getAlarmChildLabel(alarm, fallbackChildName)
const alarmTypeLabel = getAlarmTypeLabel(alarm.source_msg_id)
return {
key: `device-alarm-${alarm.device_id}-${alarm.alarm_id}`,
title: '系统消息',
summary: `${childLabel} 的伴伴触发了${alarmTypeLabel}`,
detail: `设备 ${alarm.device_id}`,
timeLabel: formatBannerTime(alarm.created_at),
route: '/pages/device/index',
alarmId: alarm.alarm_id,
deviceId: alarm.device_id,
}
}
function sortAlarmsByCreatedAt(left: DeviceAlarmItem, right: DeviceAlarmItem): number {
const leftTime = new Date(left.created_at).getTime()
const rightTime = new Date(right.created_at).getTime()
return leftTime - rightTime
}
export async function pollSystemBannerNotifications(): Promise<SystemBannerNotification[]> {
const bindingResponse = await getBindings(undefined, 100)
const bindings = bindingResponse.items || []
if (bindings.length === 0) return []
const lastSeenAlarmMap = readLastSeenAlarmMap()
const previousPollTime = readLastPollTime()
const currentPollTime = Date.now()
let hasMapChanged = false
const alarmResults = await Promise.allSettled(
bindings.map(async (binding) => ({
binding,
response: await getDeviceAlarms(binding.device_id, DEVICE_ALARM_FETCH_LIMIT),
}))
)
const nextNotifications: Array<{ alarm: DeviceAlarmItem; binding: Binding }> = []
alarmResults.forEach((result) => {
if (result.status !== 'fulfilled') return
const { binding, response } = result.value
const deviceId = String(binding.device_id || '').trim()
if (!deviceId) return
const alarms = response.items || []
const latestAlarmId = alarms[0]?.alarm_id || 0
const hasBaseline = hasDeviceAlarmBaseline(deviceId, lastSeenAlarmMap)
const knownAlarmId = Number(lastSeenAlarmMap[deviceId] || 0)
// Establish a baseline on first poll so old history does not flood the user.
if (!hasBaseline) {
setDeviceAlarmSeen(deviceId, latestAlarmId, lastSeenAlarmMap)
hasMapChanged = true
return
}
if (latestAlarmId < knownAlarmId) {
setDeviceAlarmSeen(deviceId, latestAlarmId, lastSeenAlarmMap)
hasMapChanged = true
return
}
if (latestAlarmId === knownAlarmId) {
return
}
setDeviceAlarmSeen(deviceId, latestAlarmId, lastSeenAlarmMap)
hasMapChanged = true
alarms
.filter((alarm) => {
if (alarm.alarm_id <= knownAlarmId) return false
if (previousPollTime <= 0) return true
const createdAt = new Date(alarm.created_at).getTime()
if (Number.isNaN(createdAt)) return true
return createdAt >= previousPollTime
})
.forEach((alarm) => {
nextNotifications.push({ alarm, binding })
})
})
if (hasMapChanged) {
writeLastSeenAlarmMap(lastSeenAlarmMap)
}
writeLastPollTime(currentPollTime)
return nextNotifications
.sort((left, right) => sortAlarmsByCreatedAt(left.alarm, right.alarm))
.map(({ alarm, binding }) => buildDeviceAlarmNotification(alarm, binding.child_name))
}