feat: add sleep report page

This commit is contained in:
czz
2026-05-08 11:30:37 +08:00
parent 054d8e3519
commit a6382d669b
26 changed files with 3249 additions and 6 deletions

195
src/pages/report/index.tsx Normal file
View File

@@ -0,0 +1,195 @@
import { ScrollView, Text, View } from "@tarojs/components";
import Taro from "@tarojs/taro";
import type { CSSProperties } from "react";
import { useMemo, useState } from "react";
import { ReportChartCard } from "../../components/report/chart-card";
import { ReportChipGroup } from "../../components/report/chip-group";
import { ReportDaytimePrediction } from "../../components/report/daytime-prediction";
import { ReportDistributionCard } from "../../components/report/distribution-card";
import { ReportInsightList } from "../../components/report/insight-list";
import { ReportKvList } from "../../components/report/kv-list";
import { ReportMetricGrid } from "../../components/report/metric-grid";
import { ReportPageHeader } from "../../components/report/page-header";
import { ReportScoreOverview } from "../../components/report/score-overview";
import { ReportSummaryBlock } from "../../components/report/summary-block";
import { reportDimensions, reportOptions, reportRecords } from "./mock";
import { getSleepLevel, pickReportRecord } from "./report-utils";
import type { ReportDimension } from "./types";
import "./index.scss";
function getDefaultSelection(dimension: ReportDimension) {
const options = reportOptions[dimension];
return {
dateKey: options.dates[0].value,
roomKey: options.rooms[0].value,
deviceKey: options.devices[0].value
};
}
export default function ReportPage() {
const [dimension, setDimension] = useState<ReportDimension>("daily");
const initialSelection = getDefaultSelection("daily");
const [dateKey, setDateKey] = useState(initialSelection.dateKey);
const [roomKey, setRoomKey] = useState(initialSelection.roomKey);
const [deviceKey, setDeviceKey] = useState(initialSelection.deviceKey);
const currentOptions = reportOptions[dimension];
const currentRecord = useMemo(
() => pickReportRecord(reportRecords[dimension], dateKey, roomKey, deviceKey),
[dateKey, deviceKey, dimension, roomKey]
);
const sleepLevel = getSleepLevel(currentRecord.score);
const [selectedSleepTrend, setSelectedSleepTrend] = useState(currentRecord.sleepTrend[0].id);
const [selectedScoreTrend, setSelectedScoreTrend] = useState(currentRecord.scoreTrend[0].id);
const [selectedHeartRatePoint, setSelectedHeartRatePoint] = useState(currentRecord.heartRatePoints[0].id);
const [selectedBreathingPoint, setSelectedBreathingPoint] = useState(currentRecord.breathingPoints[0].id);
const [selectedEventPoint, setSelectedEventPoint] = useState(currentRecord.eventPoints[0].id);
const resetSelections = (nextDimension: ReportDimension, nextDate: string, nextRoom: string, nextDevice: string) => {
const record = pickReportRecord(reportRecords[nextDimension], nextDate, nextRoom, nextDevice);
setSelectedSleepTrend(record.sleepTrend[0].id);
setSelectedScoreTrend(record.scoreTrend[0].id);
setSelectedHeartRatePoint(record.heartRatePoints[0].id);
setSelectedBreathingPoint(record.breathingPoints[0].id);
setSelectedEventPoint(record.eventPoints[0].id);
};
const handleDimensionChange = (nextValue: string) => {
const nextDimension = nextValue as ReportDimension;
const nextSelection = getDefaultSelection(nextDimension);
setDimension(nextDimension);
setDateKey(nextSelection.dateKey);
setRoomKey(nextSelection.roomKey);
setDeviceKey(nextSelection.deviceKey);
resetSelections(nextDimension, nextSelection.dateKey, nextSelection.roomKey, nextSelection.deviceKey);
};
const handleDateChange = (nextDate: string) => {
setDateKey(nextDate);
resetSelections(dimension, nextDate, roomKey, deviceKey);
};
const handleRoomChange = (nextRoom: string) => {
setRoomKey(nextRoom);
resetSelections(dimension, dateKey, nextRoom, deviceKey);
};
const handleDeviceChange = (nextDevice: string) => {
setDeviceKey(nextDevice);
resetSelections(dimension, dateKey, roomKey, nextDevice);
};
const pageStyle = {
"--report-top-gap": `${(typeof Taro.getWindowInfo === "function" ? Taro.getWindowInfo().statusBarHeight : Taro.getSystemInfoSync().statusBarHeight) || 0}px`
} as CSSProperties;
return (
<ScrollView className="report-page" scrollY style={pageStyle}>
<View className="report-page__glow report-page__glow--one" />
<View className="report-page__glow report-page__glow--two" />
<View className="report-page__content">
<ReportPageHeader
title={currentRecord.babyName}
subtitle={currentRecord.dateLabel}
onBack={() => Taro.navigateBack({ delta: 1 })}
onShare={() =>
Taro.showToast({
title: "分享功能待接入",
icon: "none"
})
}
/>
<View className="report-toolbar">
<ReportChipGroup options={reportDimensions} value={dimension} onChange={handleDimensionChange} />
<View className="report-filters">
<View className="report-filter">
<Text className="report-filter__label"></Text>
<ReportChipGroup options={currentOptions.dates} value={dateKey} onChange={handleDateChange} compact />
</View>
<View className="report-filter">
<Text className="report-filter__label"></Text>
<ReportChipGroup options={currentOptions.rooms} value={roomKey} onChange={handleRoomChange} compact />
</View>
<View className="report-filter">
<Text className="report-filter__label"></Text>
<ReportChipGroup options={currentOptions.devices} value={deviceKey} onChange={handleDeviceChange} compact />
</View>
</View>
</View>
<ReportScoreOverview
score={currentRecord.score}
levelLabel={sleepLevel.label}
levelTone={sleepLevel.tone}
qualityStatus={currentRecord.qualityStatus}
totalSleep={currentRecord.totalSleep}
asleepAt={currentRecord.asleepAt}
wakeAt={currentRecord.wakeAt}
factors={currentRecord.scoreFactors}
/>
<ReportChartCard
title="睡眠质量趋势"
subtitle="整夜睡眠阶段与呼吸波动"
points={currentRecord.sleepTrend}
selectedId={selectedSleepTrend}
onSelect={setSelectedSleepTrend}
/>
<ReportMetricGrid metrics={currentRecord.metrics} />
<ReportInsightList title="AI 睡眠分析" items={currentRecord.aiSummary} />
<ReportInsightList title="改善建议" items={currentRecord.suggestions} />
<ReportChartCard
title="睡眠分数趋势"
subtitle="点击节点查看阶段评分"
points={currentRecord.scoreTrend}
selectedId={selectedScoreTrend}
onSelect={setSelectedScoreTrend}
/>
<ReportSummaryBlock block={currentRecord.anomalyStats} />
<ReportKvList title="心率变异性HRV" items={currentRecord.hrvMetrics} />
<ReportChartCard
title="心率散点图"
subtitle="横轴时间,纵轴心率值"
points={currentRecord.heartRatePoints}
selectedId={selectedHeartRatePoint}
onSelect={setSelectedHeartRatePoint}
mode="scatter"
/>
<ReportChartCard
title="呼吸波形图"
subtitle="呼吸波动与暂停标记"
points={currentRecord.breathingPoints}
selectedId={selectedBreathingPoint}
onSelect={setSelectedBreathingPoint}
/>
<ReportSummaryBlock block={currentRecord.oxygenSummary} />
<ReportSummaryBlock block={currentRecord.snoreSummary} />
<ReportChartCard
title="异常事件散点图"
subtitle="呼吸暂停、心率异常、哭闹、离床"
points={currentRecord.eventPoints}
selectedId={selectedEventPoint}
onSelect={setSelectedEventPoint}
mode="scatter"
/>
<ReportDistributionCard title="睡眠结构" items={currentRecord.sleepStructure} />
<ReportKvList title="自主神经分析" items={currentRecord.autonomicMetrics} />
<ReportDaytimePrediction items={currentRecord.daytimePrediction} />
</View>
</ScrollView>
);
}