提交小程序合入代码
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
175
banban-mini/src/services/system-banner.ts
Normal file
175
banban-mini/src/services/system-banner.ts
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user