室内植物实验室的琴叶榕显示 74%,龟背竹显示 68%。如果页面只保存一个"湿度百分比",就会遇到很奇怪的提醒:传感器一瞬间低于阈值,浇水按钮出现;下一次采样回弹 2%,提醒又消失,用户根本不知道该不该处理。
我把植物状态拆成原始读数、平滑读数、提醒阶段和最后处理时间。百分比仍然用于卡片展示,但是否提醒由独立的规则控制。
一次测量不是植物状态
ts
export interface MoistureSample {
plantId: string
capturedAt: number
value: number
}
export type CarePhase = 'healthy' | 'watching' | 'waterSoon' | 'watering'
export interface PlantState {
id: string
name: string
zoneId: string
moisture: number
rawMoisture: number
phase: CarePhase
lastWateredAt: number
lastSampleAt: number
}
rawMoisture 用于追查异常,moisture 是页面显示的平滑值。用户点击浇水后进入 watering,不能因为下一条旧样本到达又立即变回缺水。
先过滤突刺,再判断区间
单点抖动不值得触发通知。我使用最近五次读数的中位数,避免一个异常高值或低值把整条趋势拉偏。
ts
function median(values: number[]): number {
const sorted = values.slice().sort((a: number, b: number) => a - b)
const middle = Math.floor(sorted.length / 2)
return sorted.length % 2 === 0 ?
(sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]
}
export function smooth(samples: MoistureSample[], plantId: string): number | undefined {
const recent = samples.filter((item: MoistureSample) => item.plantId === plantId)
.sort((a: MoistureSample, b: MoistureSample) => b.capturedAt - a.capturedAt)
.slice(0, 5)
.map((item: MoistureSample) => item.value)
return recent.length === 0 ? undefined : median(recent)
}
琴叶榕、凤尾蕨和豆瓣绿的阈值不同,因此阈值不写死在页面组件里。
ts
export interface CareRule {
plantId: string
watchBelow: number
waterBelow: number
recoverAbove: number
}
export function nextPhase(
phase: CarePhase,
moisture: number,
rule: CareRule
): CarePhase {
if (phase === 'watering') return phase
if (moisture <= rule.waterBelow) return 'waterSoon'
if (moisture <= rule.watchBelow) return 'watching'
if (moisture >= rule.recoverAbove) return 'healthy'
return phase
}
recoverAbove 高于 waterBelow,形成一个恢复区间。湿度在 43% 和 45% 附近来回时,提醒不会每采一次就翻转。
浇水动作先进入处理中
按钮点击不能直接把湿度改成 100%。真正的传感器可能还没反馈,或者用户只浇了一小部分水。点击后记录动作时间并进入 watering,等待连续样本确认。
ts
export function startWatering(state: PlantState, now: number): PlantState {
if (state.phase !== 'waterSoon') return state
return { ...state, phase: 'watering', lastWateredAt: now }
}
export function applySample(
state: PlantState,
samples: MoistureSample[],
rule: CareRule,
next: MoistureSample
): PlantState {
const value = smooth([...samples, next], state.id)
if (value === undefined) return state
const withinWait = state.phase === 'watering' &&
next.capturedAt - state.lastWateredAt < 15 * 60 * 1000
const phase = withinWait && value < rule.recoverAbove ? 'watering' :
nextPhase(state.phase, value, rule)
return { ...state, rawMoisture: next.value, moisture: value,
phase, lastSampleAt: next.capturedAt }
}
浇水后十五分钟内,卡片显示"正在吸收水分"。这段时间不重复弹提醒,读数达到恢复线后再回到健康状态。
平板列表按植物 ID 选择
阳台、书房、客厅的筛选会改变植物卡片顺序。选择状态保存为 selectedPlantId,而不是"第几张卡片"。
ts
ForEach(this.visiblePlants, (plant: PlantState) => {
this.plantTile(plant)
.border({ width: this.selectedPlantId === plant.id ? 2 : 0,
color: '#68A87E' })
.onClick(() => { this.selectedPlantId = plant.id })
}, (plant: PlantState) => plant.id)

趋势图读取 moisture,详情页可以保留 rawMoisture 与采样时间,用户看到异常时才展开查看。系统能力接入后,传感器适配器只要不断产生 MoistureSample;区间判断、浇水确认和多区域选择仍保持同一套状态链路。