Merge commit 'a6382d6'

# Conflicts:
#	README.md
#	src/app.config.ts
#	src/pages/index/index.scss
#	src/pages/index/index.tsx
This commit is contained in:
czz
2026-05-08 11:39:05 +08:00
27 changed files with 3708 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
export default defineAppConfig({
pages: [
"pages/index/index",
"pages/report/index",
"pages/message/index",
"pages/mine/index",
"pages/profile/index",

View File

@@ -0,0 +1,66 @@
import { Text, View } from "@tarojs/components";
import type { TrendPoint } from "../../pages/report/types";
type ReportChartCardProps = {
title: string;
subtitle?: string;
points: TrendPoint[];
selectedId: string;
onSelect: (id: string) => void;
mode?: "line" | "scatter";
};
export function ReportChartCard({
title,
subtitle,
points,
selectedId,
onSelect,
mode = "line"
}: ReportChartCardProps) {
const selectedPoint = points.find((item) => item.id === selectedId) ?? points[0];
return (
<View className="report-card">
<View className="report-card__header">
<View>
<Text className="report-card__title">{title}</Text>
{subtitle ? <Text className="report-card__subtitle">{subtitle}</Text> : null}
</View>
{selectedPoint ? (
<View className="report-card__extra">
<Text className="report-card__badge">{selectedPoint.time}</Text>
</View>
) : null}
</View>
{selectedPoint ? (
<View className="report-chart__tooltip">
<Text className="report-chart__tooltip-title">{selectedPoint.label}</Text>
<Text className="report-chart__tooltip-value">{selectedPoint.value}</Text>
{selectedPoint.meta ? <Text className="report-chart__tooltip-meta">{selectedPoint.meta}</Text> : null}
</View>
) : null}
<View className={`report-chart report-chart--${mode}`}>
<View className="report-chart__grid">
{[0, 1, 2, 3].map((item) => (
<View className="report-chart__grid-line" key={item} />
))}
</View>
{points.map((item) => (
<View className="report-chart__item" key={item.id} style={{ left: `${item.x}%` }}>
{mode === "line" ? <View className="report-chart__stem" style={{ height: `${item.y}%` }} /> : null}
<View
className={`report-chart__point report-chart__point--${item.tone} ${selectedId === item.id ? "is-active" : ""}`}
style={{ bottom: `${item.y}%` }}
onClick={() => onSelect(item.id)}
/>
<Text className="report-chart__label">{item.time}</Text>
</View>
))}
</View>
</View>
);
}

View File

@@ -0,0 +1,29 @@
import { Text, View } from "@tarojs/components";
type ChipOption = {
label: string;
value: string;
};
type ReportChipGroupProps = {
options: ChipOption[];
value: string;
onChange: (value: string) => void;
compact?: boolean;
};
export function ReportChipGroup({ options, value, onChange, compact = false }: ReportChipGroupProps) {
return (
<View className={`report-chip-group ${compact ? "report-chip-group--compact" : ""}`}>
{options.map((item) => (
<View
className={`report-chip ${item.value === value ? "report-chip--active" : ""}`}
key={item.value}
onClick={() => onChange(item.value)}
>
<Text className={`report-chip__text ${item.value === value ? "report-chip__text--active" : ""}`}>{item.label}</Text>
</View>
))}
</View>
);
}

View File

@@ -0,0 +1,28 @@
import { Text, View } from "@tarojs/components";
import type { DaytimeState } from "../../pages/report/types";
import { getStatusTone } from "../../pages/report/report-utils";
type ReportDaytimePredictionProps = {
items: DaytimeState[];
};
export function ReportDaytimePrediction({ items }: ReportDaytimePredictionProps) {
return (
<View className="report-card">
<View className="report-card__header">
<Text className="report-card__title"></Text>
</View>
<View className="daytime-list">
{items.map((item) => (
<View className="daytime-card" key={item.label}>
<View className="daytime-card__head">
<Text className="daytime-card__label">{item.label}</Text>
<Text className={`daytime-card__status daytime-card__status--${getStatusTone(item.status)}`}>{item.status}</Text>
</View>
<Text className="daytime-card__detail">{item.detail}</Text>
</View>
))}
</View>
</View>
);
}

View File

@@ -0,0 +1,35 @@
import { Text, View } from "@tarojs/components";
import type { DistributionItem } from "../../pages/report/types";
type ReportDistributionCardProps = {
title: string;
items: DistributionItem[];
};
export function ReportDistributionCard({ title, items }: ReportDistributionCardProps) {
return (
<View className="report-card">
<View className="report-card__header">
<Text className="report-card__title">{title}</Text>
</View>
<View className="distribution-card">
<View className="distribution-card__ring">
<View className="distribution-card__ring-inner">
<Text className="distribution-card__ring-text"></Text>
</View>
</View>
<View className="distribution-card__list">
{items.map((item) => (
<View className="distribution-card__row" key={item.label}>
<View className="distribution-card__legend">
<View className="distribution-card__dot" style={{ background: item.color }} />
<Text className="distribution-card__label">{item.label}</Text>
</View>
<Text className="distribution-card__value">{item.value}%</Text>
</View>
))}
</View>
</View>
</View>
);
}

View File

@@ -0,0 +1,28 @@
import { Text, View } from "@tarojs/components";
import type { InsightItem } from "../../pages/report/types";
type ReportInsightListProps = {
title: string;
items: InsightItem[];
};
export function ReportInsightList({ title, items }: ReportInsightListProps) {
return (
<View className="report-card">
<View className="report-card__header">
<Text className="report-card__title">{title}</Text>
</View>
<View className="insight-list">
{items.map((item) => (
<View className="insight-card" key={item.title}>
<View className={`insight-card__tone insight-card__tone--${item.tone}`} />
<View className="insight-card__content">
<Text className="insight-card__title">{item.title}</Text>
<Text className="insight-card__body">{item.body}</Text>
</View>
</View>
))}
</View>
</View>
);
}

View File

@@ -0,0 +1,29 @@
import { Text, View } from "@tarojs/components";
import type { KvMetric } from "../../pages/report/types";
import { getStatusTone } from "../../pages/report/report-utils";
type ReportKvListProps = {
title: string;
items: KvMetric[];
};
export function ReportKvList({ title, items }: ReportKvListProps) {
return (
<View className="report-card">
<View className="report-card__header">
<Text className="report-card__title">{title}</Text>
</View>
<View className="kv-list">
{items.map((item) => (
<View className="kv-list__row" key={item.label}>
<Text className="kv-list__label">{item.label}</Text>
<View className="kv-list__value-wrap">
<Text className="kv-list__value">{item.value}</Text>
{item.status ? <Text className={`kv-list__status kv-list__status--${getStatusTone(item.status)}`}>{item.status}</Text> : null}
</View>
</View>
))}
</View>
</View>
);
}

View File

@@ -0,0 +1,28 @@
import { Text, View } from "@tarojs/components";
import type { MetricCard } from "../../pages/report/types";
import { getStatusTone } from "../../pages/report/report-utils";
type ReportMetricGridProps = {
metrics: MetricCard[];
};
export function ReportMetricGrid({ metrics }: ReportMetricGridProps) {
return (
<View className="metric-grid">
{metrics.map((metric) => {
const tone = getStatusTone(metric.status);
return (
<View className="metric-card" key={metric.label}>
<Text className="metric-card__label">{metric.label}</Text>
<Text className="metric-card__value">
{metric.value}
<Text className="metric-card__unit">{metric.unit}</Text>
</Text>
<Text className={`metric-card__status metric-card__status--${tone}`}>{metric.status}</Text>
</View>
);
})}
</View>
);
}

View File

@@ -0,0 +1,25 @@
import { Text, View } from "@tarojs/components";
type ReportPageHeaderProps = {
title: string;
subtitle: string;
onBack: () => void;
onShare: () => void;
};
export function ReportPageHeader({ title, subtitle, onBack, onShare }: ReportPageHeaderProps) {
return (
<View className="report-header">
<View className="report-header__action" onClick={onBack}>
<Text className="report-header__action-text">&lt;</Text>
</View>
<View className="report-header__content">
<Text className="report-header__title">{title}</Text>
<Text className="report-header__subtitle">{subtitle}</Text>
</View>
<View className="report-header__action report-header__action--share" onClick={onShare}>
<Text className="report-header__action-text"></Text>
</View>
</View>
);
}

View File

@@ -0,0 +1,63 @@
import { Text, View } from "@tarojs/components";
import type { ScoreFactor } from "../../pages/report/types";
type ReportScoreOverviewProps = {
score: number;
levelLabel: string;
levelTone: string;
qualityStatus: string;
totalSleep: string;
asleepAt: string;
wakeAt: string;
factors: ScoreFactor[];
};
export function ReportScoreOverview({
score,
levelLabel,
levelTone,
qualityStatus,
totalSleep,
asleepAt,
wakeAt,
factors
}: ReportScoreOverviewProps) {
return (
<View className="score-overview">
<View className="score-overview__top">
<View className="score-overview__ring">
<View className="score-overview__ring-inner">
<Text className="score-overview__caption"></Text>
<Text className="score-overview__score">{score}</Text>
</View>
</View>
<View className="score-overview__summary">
<Text className={`score-overview__level score-overview__level--${levelTone}`}>{levelLabel}</Text>
<Text className="score-overview__status">{qualityStatus}</Text>
<View className="score-overview__meta">
<View className="score-overview__meta-item">
<Text className="score-overview__meta-label"></Text>
<Text className="score-overview__meta-value">{totalSleep}</Text>
</View>
<View className="score-overview__meta-item">
<Text className="score-overview__meta-label"></Text>
<Text className="score-overview__meta-value">{asleepAt}</Text>
</View>
<View className="score-overview__meta-item">
<Text className="score-overview__meta-label"></Text>
<Text className="score-overview__meta-value">{wakeAt}</Text>
</View>
</View>
</View>
</View>
<View className="score-overview__factors">
{factors.map((item) => (
<View className="score-overview__factor" key={item.label}>
<Text className="score-overview__factor-label">{item.label}</Text>
<Text className="score-overview__factor-value">{item.score}</Text>
</View>
))}
</View>
</View>
);
}

View File

@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import { Text, View } from "@tarojs/components";
type SectionCardProps = {
title: string;
subtitle?: string;
extra?: ReactNode;
children: ReactNode;
};
export function ReportSectionCard({ title, subtitle, extra, children }: SectionCardProps) {
return (
<View className="report-card">
<View className="report-card__header">
<View>
<Text className="report-card__title">{title}</Text>
{subtitle ? <Text className="report-card__subtitle">{subtitle}</Text> : null}
</View>
{extra ? <View className="report-card__extra">{extra}</View> : null}
</View>
{children}
</View>
);
}

View File

@@ -0,0 +1,33 @@
import { Text, View } from "@tarojs/components";
import type { SummaryBlock } from "../../pages/report/types";
import { getStatusTone } from "../../pages/report/report-utils";
type ReportSummaryBlockProps = {
block: SummaryBlock;
};
export function ReportSummaryBlock({ block }: ReportSummaryBlockProps) {
return (
<View className="report-card">
<View className="report-card__header">
<Text className="report-card__title">{block.title}</Text>
</View>
<View className="summary-grid">
{block.items.map((item) => {
const tone = item.status ? getStatusTone(item.status) : "good";
return (
<View className="summary-grid__item" key={item.label}>
<Text className="summary-grid__label">{item.label}</Text>
<Text className="summary-grid__value">
{item.value}
{item.unit ? <Text className="summary-grid__unit">{item.unit}</Text> : null}
</Text>
{item.status ? <Text className={`summary-grid__status summary-grid__status--${tone}`}>{item.status}</Text> : null}
</View>
);
})}
</View>
</View>
);
}

View File

@@ -172,3 +172,52 @@
font-size: 22rpx;
line-height: 1.7;
}
.device-report-entry {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 22rpx;
padding: 24rpx;
border-radius: 22rpx;
background:
radial-gradient(circle at top right, rgba(54, 228, 170, 0.18), transparent 42%),
rgba(42, 48, 66, 0.96);
box-shadow:
inset 0 0 0 2rpx rgba(255, 255, 255, 0.03),
0 14rpx 28rpx rgba(6, 10, 22, 0.18);
}
.device-report-entry__content {
flex: 1;
padding-right: 20rpx;
}
.device-report-entry__eyebrow {
display: block;
color: #39e6ad;
font-size: 20rpx;
}
.device-report-entry__title {
display: block;
margin-top: 8rpx;
color: #f3f7ff;
font-size: 30rpx;
font-weight: 600;
}
.device-report-entry__desc {
display: block;
margin-top: 10rpx;
color: #96a1b5;
font-size: 22rpx;
line-height: 1.6;
}
.device-report-entry__arrow {
color: #d8e1ed;
font-size: 28rpx;
}

View File

@@ -120,6 +120,11 @@ export default function Index() {
return;
}
if (item.key === "report") {
Taro.navigateTo({ url: "/pages/report/index" });
return;
}
if (item.key === "message") {
Taro.redirectTo({ url: "/pages/message/index" });
return;
@@ -294,6 +299,15 @@ export default function Index() {
<ActionButton icon="bluetooth" label="蓝牙搜附近的设备" secondary onClick={handleBluetoothBind} />
</PanelCard>
<View className="device-report-entry" onClick={() => Taro.navigateTo({ url: "/pages/report/index" })}>
<View className="device-report-entry__content">
<Text className="device-report-entry__eyebrow"></Text>
<Text className="device-report-entry__title"></Text>
<Text className="device-report-entry__desc"> AI </Text>
</View>
<Text className="device-report-entry__arrow">&gt;</Text>
</View>
<PanelCard className="device-notice-card" warning>
<View className="device-notice-card__title-row">
<View className="device-notice-card__horn" />

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: "custom"
});

754
src/pages/report/index.scss Normal file
View File

@@ -0,0 +1,754 @@
.report-page {
position: relative;
min-height: 100vh;
background: linear-gradient(180deg, #161d29 0%, #101620 100%);
color: #eef5ff;
}
.report-page__content {
position: relative;
z-index: 1;
min-height: 100vh;
padding: calc(var(--report-top-gap, 0px) + 20rpx) 24rpx 56rpx;
box-sizing: border-box;
}
.report-page__glow {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.report-page__glow--one {
top: 18rpx;
right: -54rpx;
width: 320rpx;
height: 320rpx;
background: radial-gradient(circle, rgba(43, 226, 198, 0.18) 0%, rgba(43, 226, 198, 0) 68%);
}
.report-page__glow--two {
top: 260rpx;
left: -90rpx;
width: 280rpx;
height: 280rpx;
background: radial-gradient(circle, rgba(36, 174, 255, 0.14) 0%, rgba(36, 174, 255, 0) 70%);
}
.report-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.report-header__action {
display: flex;
align-items: center;
justify-content: center;
min-width: 68rpx;
height: 52rpx;
padding: 0 16rpx;
border-radius: 18rpx;
background: rgba(32, 42, 58, 0.82);
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.04);
}
.report-header__action--share {
min-width: 96rpx;
}
.report-header__action-text {
color: #dce7f8;
font-size: 24rpx;
}
.report-header__content {
flex: 1;
padding: 0 20rpx;
}
.report-header__title {
display: block;
color: #f7fbff;
font-size: 34rpx;
font-weight: 600;
}
.report-header__subtitle {
display: block;
margin-top: 6rpx;
color: #8ea0b6;
font-size: 22rpx;
}
.report-toolbar {
margin-bottom: 24rpx;
}
.report-filters {
margin-top: 16rpx;
padding: 22rpx;
border-radius: 24rpx;
background: rgba(24, 33, 47, 0.92);
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.04);
}
.report-filter + .report-filter {
margin-top: 18rpx;
}
.report-filter__label {
display: block;
margin-bottom: 10rpx;
color: #91a3b8;
font-size: 22rpx;
}
.report-chip-group {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.report-chip-group--compact {
gap: 10rpx;
}
.report-chip {
min-width: 112rpx;
padding: 14rpx 20rpx;
border-radius: 18rpx;
background: rgba(32, 43, 60, 0.95);
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.04);
}
.report-chip--active {
background: linear-gradient(135deg, rgba(47, 224, 190, 0.24), rgba(36, 174, 255, 0.18));
box-shadow:
inset 0 0 0 2rpx rgba(52, 222, 194, 0.42),
0 10rpx 28rpx rgba(18, 129, 165, 0.24);
}
.report-chip__text {
color: #9eb0c5;
font-size: 24rpx;
text-align: center;
}
.report-chip-group--compact .report-chip {
min-width: 0;
padding: 10rpx 18rpx;
border-radius: 16rpx;
}
.report-chip-group--compact .report-chip__text {
font-size: 22rpx;
}
.report-chip__text--active {
color: #ecfffb;
font-weight: 600;
}
.score-overview,
.report-card,
.metric-grid {
margin-bottom: 24rpx;
}
.score-overview {
padding: 24rpx;
border-radius: 28rpx;
background: rgba(24, 33, 47, 0.94);
box-shadow:
inset 0 0 0 2rpx rgba(255, 255, 255, 0.04),
0 20rpx 40rpx rgba(4, 10, 20, 0.2);
}
.score-overview__top {
display: flex;
gap: 24rpx;
}
.score-overview__ring {
display: flex;
align-items: center;
justify-content: center;
width: 248rpx;
height: 248rpx;
border-radius: 50%;
background:
radial-gradient(circle at center, rgba(16, 23, 35, 0.92) 0 59%, transparent 60%),
conic-gradient(#2de1c2 0 33%, #23b6ff 33% 61%, #ffbf4d 61% 82%, #ff6b6b 82% 100%);
}
.score-overview__ring-inner {
display: flex;
flex-direction: column;
align-items: center;
}
.score-overview__caption {
color: #8da0b5;
font-size: 22rpx;
}
.score-overview__score {
margin-top: 8rpx;
color: #ff9950;
font-size: 72rpx;
font-weight: 700;
line-height: 1;
}
.score-overview__summary {
flex: 1;
}
.score-overview__level {
display: inline-flex;
align-items: center;
height: 44rpx;
padding: 0 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: 600;
}
.score-overview__level--excellent,
.metric-card__status--excellent,
.summary-grid__status--excellent,
.kv-list__status--excellent,
.daytime-card__status--excellent {
background: rgba(45, 225, 194, 0.16);
color: #4ef0cb;
}
.score-overview__level--good,
.metric-card__status--good,
.summary-grid__status--good,
.kv-list__status--good,
.daytime-card__status--good {
background: rgba(35, 182, 255, 0.16);
color: #67cbff;
}
.score-overview__level--warning,
.metric-card__status--warning,
.summary-grid__status--warning,
.kv-list__status--warning,
.daytime-card__status--warning {
background: rgba(255, 191, 77, 0.16);
color: #ffc766;
}
.score-overview__level--danger,
.metric-card__status--danger,
.summary-grid__status--danger,
.kv-list__status--danger,
.daytime-card__status--danger {
background: rgba(255, 107, 107, 0.16);
color: #ff8787;
}
.score-overview__status {
display: block;
margin-top: 18rpx;
color: #e7eef8;
font-size: 30rpx;
font-weight: 600;
line-height: 1.4;
}
.score-overview__meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12rpx;
margin-top: 20rpx;
}
.score-overview__meta-item {
padding: 16rpx;
border-radius: 18rpx;
background: rgba(18, 26, 39, 0.88);
}
.score-overview__meta-label {
display: block;
color: #90a1b5;
font-size: 20rpx;
}
.score-overview__meta-value {
display: block;
margin-top: 8rpx;
color: #f2f7ff;
font-size: 26rpx;
font-weight: 600;
}
.score-overview__factors {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12rpx;
margin-top: 22rpx;
}
.score-overview__factor {
padding: 14rpx 10rpx;
border-radius: 16rpx;
background: rgba(18, 26, 39, 0.88);
text-align: center;
}
.score-overview__factor-label {
display: block;
color: #91a3b8;
font-size: 20rpx;
line-height: 1.4;
}
.score-overview__factor-value {
display: block;
margin-top: 6rpx;
color: #f4f8ff;
font-size: 28rpx;
font-weight: 700;
}
.report-card {
padding: 24rpx;
border-radius: 28rpx;
background: rgba(24, 33, 47, 0.94);
box-shadow:
inset 0 0 0 2rpx rgba(255, 255, 255, 0.04),
0 16rpx 36rpx rgba(4, 10, 20, 0.18);
}
.report-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 18rpx;
}
.report-card__title {
color: #f4f8ff;
font-size: 28rpx;
font-weight: 600;
}
.report-card__subtitle {
display: block;
margin-top: 6rpx;
color: #90a3b8;
font-size: 22rpx;
}
.report-card__badge {
display: inline-flex;
align-items: center;
height: 40rpx;
padding: 0 14rpx;
border-radius: 999rpx;
background: rgba(35, 182, 255, 0.16);
color: #67cbff;
font-size: 20rpx;
}
.report-chart__tooltip {
margin-bottom: 16rpx;
padding: 16rpx 18rpx;
border-radius: 18rpx;
background: rgba(16, 23, 35, 0.86);
}
.report-chart__tooltip-title {
display: block;
color: #f3f8ff;
font-size: 24rpx;
font-weight: 600;
}
.report-chart__tooltip-value {
display: block;
margin-top: 8rpx;
color: #41e0c4;
font-size: 30rpx;
font-weight: 700;
}
.report-chart__tooltip-meta {
display: block;
margin-top: 8rpx;
color: #8ea1b8;
font-size: 22rpx;
line-height: 1.5;
}
.report-chart {
position: relative;
height: 260rpx;
padding: 16rpx 18rpx 48rpx;
border-radius: 24rpx;
background: rgba(16, 23, 35, 0.84);
overflow: hidden;
}
.report-chart--scatter {
height: 220rpx;
}
.report-chart__grid {
position: absolute;
inset: 16rpx 18rpx 48rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.report-chart__grid-line {
border-top: 2rpx dashed rgba(255, 255, 255, 0.06);
}
.report-chart__item {
position: absolute;
top: 16rpx;
bottom: 48rpx;
width: 44rpx;
margin-left: -22rpx;
}
.report-chart__stem {
position: absolute;
left: 50%;
bottom: 0;
width: 4rpx;
margin-left: -2rpx;
border-radius: 999rpx;
background: linear-gradient(180deg, rgba(35, 182, 255, 0.18), rgba(45, 225, 194, 0.7));
}
.report-chart__point {
position: absolute;
left: 50%;
width: 16rpx;
height: 16rpx;
margin-left: -8rpx;
border-radius: 50%;
box-shadow: 0 0 0 6rpx rgba(255, 255, 255, 0.03);
}
.report-chart__point.is-active {
width: 22rpx;
height: 22rpx;
margin-left: -11rpx;
box-shadow: 0 0 0 10rpx rgba(255, 255, 255, 0.05);
}
.report-chart__point--excellent {
background: #2de1c2;
}
.report-chart__point--good {
background: #23b6ff;
}
.report-chart__point--warning {
background: #ffbf4d;
}
.report-chart__point--danger {
background: #ff6b6b;
}
.report-chart__label {
position: absolute;
left: 50%;
bottom: -6rpx;
width: 72rpx;
margin-left: -36rpx;
color: #8193a9;
font-size: 18rpx;
text-align: center;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
}
.metric-card {
padding: 22rpx;
border-radius: 24rpx;
background: rgba(24, 33, 47, 0.94);
box-shadow:
inset 0 0 0 2rpx rgba(255, 255, 255, 0.04),
0 14rpx 30rpx rgba(4, 10, 20, 0.18);
}
.metric-card__label {
display: block;
color: #90a2b7;
font-size: 22rpx;
}
.metric-card__value {
display: block;
margin-top: 12rpx;
color: #f4f8ff;
font-size: 34rpx;
font-weight: 700;
}
.metric-card__unit {
margin-left: 8rpx;
color: #8fa3b7;
font-size: 20rpx;
font-weight: 400;
}
.metric-card__status,
.summary-grid__status,
.kv-list__status,
.daytime-card__status {
display: inline-flex;
align-items: center;
height: 38rpx;
margin-top: 14rpx;
padding: 0 14rpx;
border-radius: 999rpx;
font-size: 20rpx;
font-weight: 600;
}
.insight-list {
display: flex;
flex-direction: column;
gap: 14rpx;
}
.insight-card {
display: flex;
gap: 14rpx;
padding: 18rpx;
border-radius: 20rpx;
background: rgba(16, 23, 35, 0.84);
}
.insight-card__tone {
width: 8rpx;
border-radius: 999rpx;
}
.insight-card__tone--excellent {
background: #2de1c2;
}
.insight-card__tone--good {
background: #23b6ff;
}
.insight-card__tone--warning {
background: #ffbf4d;
}
.insight-card__tone--danger {
background: #ff6b6b;
}
.insight-card__content {
flex: 1;
}
.insight-card__title {
display: block;
color: #f3f8ff;
font-size: 24rpx;
font-weight: 600;
}
.insight-card__body {
display: block;
margin-top: 8rpx;
color: #91a3b8;
font-size: 22rpx;
line-height: 1.7;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
}
.summary-grid__item {
padding: 18rpx;
border-radius: 20rpx;
background: rgba(16, 23, 35, 0.84);
}
.summary-grid__label {
display: block;
color: #8ea1b8;
font-size: 22rpx;
}
.summary-grid__value {
display: block;
margin-top: 12rpx;
color: #f3f8ff;
font-size: 30rpx;
font-weight: 700;
}
.summary-grid__unit {
margin-left: 6rpx;
color: #8fa2b9;
font-size: 20rpx;
font-weight: 400;
}
.kv-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.kv-list__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 16rpx 0;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.04);
}
.kv-list__row:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.kv-list__label {
color: #8fa1b6;
font-size: 22rpx;
}
.kv-list__value-wrap {
display: flex;
align-items: center;
gap: 12rpx;
}
.kv-list__value {
color: #f3f8ff;
font-size: 24rpx;
font-weight: 600;
}
.distribution-card {
display: flex;
align-items: center;
gap: 24rpx;
}
.distribution-card__ring {
display: flex;
align-items: center;
justify-content: center;
width: 188rpx;
height: 188rpx;
border-radius: 50%;
background:
radial-gradient(circle at center, rgba(16, 23, 35, 0.92) 0 54%, transparent 55%),
conic-gradient(#2de1c2 0 29%, #23b6ff 29% 66%, #ffb84d 66% 88%, #ff6b6b 88% 100%);
}
.distribution-card__ring-inner {
display: flex;
align-items: center;
justify-content: center;
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: rgba(16, 23, 35, 0.94);
}
.distribution-card__ring-text {
color: #d7e3f3;
font-size: 20rpx;
text-align: center;
line-height: 1.5;
}
.distribution-card__list {
flex: 1;
display: flex;
flex-direction: column;
gap: 14rpx;
}
.distribution-card__row {
display: flex;
align-items: center;
justify-content: space-between;
}
.distribution-card__legend {
display: flex;
align-items: center;
}
.distribution-card__dot {
width: 14rpx;
height: 14rpx;
margin-right: 10rpx;
border-radius: 50%;
}
.distribution-card__label {
color: #8fa2b8;
font-size: 22rpx;
}
.distribution-card__value {
color: #f3f8ff;
font-size: 24rpx;
font-weight: 600;
}
.daytime-list {
display: flex;
flex-direction: column;
gap: 14rpx;
}
.daytime-card {
padding: 18rpx;
border-radius: 20rpx;
background: rgba(16, 23, 35, 0.84);
}
.daytime-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.daytime-card__label {
color: #f3f8ff;
font-size: 24rpx;
font-weight: 600;
}
.daytime-card__detail {
display: block;
margin-top: 10rpx;
color: #8ea1b8;
font-size: 22rpx;
line-height: 1.6;
}

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>
);
}

865
src/pages/report/mock.ts Normal file
View File

@@ -0,0 +1,865 @@
import type { ReportDimension, ReportDimensionOptions, ReportRecord } from "./types";
function makeTrendPoints(
prefix: string,
values: Array<{ time: string; label: string; value: number; y: number; tone: "excellent" | "good" | "warning" | "danger"; meta?: string }>
) {
return values.map((item, index) => ({
id: `${prefix}-${item.time}-${index}`,
time: item.time,
label: item.label,
value: item.value,
x: index * (values.length === 1 ? 0 : 100 / (values.length - 1)),
y: item.y,
tone: item.tone,
meta: item.meta
}));
}
function createRecord(config: {
babyName: string;
dateLabel: string;
roomLabel: string;
deviceLabel: string;
score: number;
qualityStatus: string;
totalSleep: string;
asleepAt: string;
wakeAt: string;
metrics: ReportRecord["metrics"];
aiSummary: ReportRecord["aiSummary"];
suggestions: ReportRecord["suggestions"];
anomalyStats: ReportRecord["anomalyStats"];
hrvMetrics: ReportRecord["hrvMetrics"];
heartRatePoints: ReportRecord["heartRatePoints"];
breathingPoints: ReportRecord["breathingPoints"];
oxygenSummary: ReportRecord["oxygenSummary"];
snoreSummary: ReportRecord["snoreSummary"];
eventPoints: ReportRecord["eventPoints"];
sleepStructure: ReportRecord["sleepStructure"];
autonomicMetrics: ReportRecord["autonomicMetrics"];
daytimePrediction: ReportRecord["daytimePrediction"];
}): ReportRecord {
return {
babyName: config.babyName,
dateLabel: config.dateLabel,
roomLabel: config.roomLabel,
deviceLabel: config.deviceLabel,
score: config.score,
qualityStatus: config.qualityStatus,
totalSleep: config.totalSleep,
asleepAt: config.asleepAt,
wakeAt: config.wakeAt,
scoreFactors: [
{ label: "入睡速度", score: 72 },
{ label: "睡眠稳定性", score: 68 },
{ label: "呼吸状态", score: 61 },
{ label: "心率状态", score: 74 },
{ label: "异常事件", score: 59 }
],
sleepTrend: makeTrendPoints("sleep-trend", [
{ time: "22:10", label: "浅睡", value: 62, y: 42, tone: "good", meta: "浅睡,呼吸平稳" },
{ time: "23:30", label: "深睡", value: 78, y: 74, tone: "excellent", meta: "深睡段,翻身较少" },
{ time: "01:00", label: "REM", value: 58, y: 56, tone: "warning", meta: "REM呼吸波动略高" },
{ time: "02:40", label: "深睡", value: 82, y: 81, tone: "excellent", meta: "深睡,血氧稳定" },
{ time: "04:20", label: "清醒", value: 38, y: 28, tone: "danger", meta: "短时清醒,伴随翻身" },
{ time: "05:50", label: "浅睡", value: 64, y: 52, tone: "good", meta: "临近醒来,波动回落" }
]),
scoreTrend: makeTrendPoints("score-trend", [
{ time: "22", label: "22点", value: 71, y: 46, tone: "good" },
{ time: "23", label: "23点", value: 79, y: 68, tone: "excellent" },
{ time: "00", label: "0点", value: 74, y: 59, tone: "good" },
{ time: "02", label: "2点", value: 66, y: 44, tone: "warning" },
{ time: "04", label: "4点", value: 58, y: 26, tone: "danger" },
{ time: "06", label: "6点", value: 69, y: 48, tone: "good" }
]),
metrics: config.metrics,
aiSummary: config.aiSummary,
suggestions: config.suggestions,
anomalyStats: config.anomalyStats,
hrvMetrics: config.hrvMetrics,
heartRatePoints: config.heartRatePoints,
breathingPoints: config.breathingPoints,
oxygenSummary: config.oxygenSummary,
snoreSummary: config.snoreSummary,
eventPoints: config.eventPoints,
sleepStructure: config.sleepStructure,
autonomicMetrics: config.autonomicMetrics,
daytimePrediction: config.daytimePrediction
};
}
const baseHeartRate = makeTrendPoints("heart", [
{ time: "22:00", label: "22:00", value: 89, y: 34, tone: "good" },
{ time: "23:10", label: "23:10", value: 92, y: 46, tone: "good" },
{ time: "00:30", label: "00:30", value: 95, y: 59, tone: "warning" },
{ time: "02:10", label: "02:10", value: 98, y: 72, tone: "danger" },
{ time: "03:50", label: "03:50", value: 90, y: 41, tone: "good" },
{ time: "05:40", label: "05:40", value: 87, y: 30, tone: "excellent" }
]);
const baseBreathing = makeTrendPoints("breath", [
{ time: "22:30", label: "22:30", value: 20, y: 45, tone: "excellent" },
{ time: "23:40", label: "23:40", value: 21, y: 53, tone: "good" },
{ time: "01:10", label: "01:10", value: 24, y: 67, tone: "warning" },
{ time: "02:50", label: "02:50", value: 26, y: 78, tone: "danger" },
{ time: "04:10", label: "04:10", value: 23, y: 58, tone: "warning" },
{ time: "05:30", label: "05:30", value: 21, y: 49, tone: "good" }
]);
const baseEvents = makeTrendPoints("event", [
{ time: "00:42", label: "暂停", value: 1, y: 64, tone: "danger", meta: "呼吸暂停 11 秒" },
{ time: "02:16", label: "哭闹", value: 1, y: 32, tone: "warning", meta: "轻微哭闹后恢复" },
{ time: "03:48", label: "心率", value: 1, y: 78, tone: "danger", meta: "心率峰值 106 bpm" },
{ time: "05:05", label: "离床", value: 1, y: 46, tone: "good", meta: "离床 2 分钟后返回" }
]);
export const reportDimensions: Array<{ label: string; value: ReportDimension }> = [
{ label: "日报", value: "daily" },
{ label: "周报", value: "weekly" },
{ label: "月报", value: "monthly" }
];
export const reportOptions: Record<ReportDimension, ReportDimensionOptions> = {
daily: {
dates: [
{ label: "05-08", value: "2026-05-08" },
{ label: "05-07", value: "2026-05-07" }
],
rooms: [
{ label: "婴儿房", value: "roomA" },
{ label: "主卧", value: "roomB" }
],
devices: [
{ label: "监测垫 A1", value: "deviceA" },
{ label: "监测垫 B2", value: "deviceB" }
]
},
weekly: {
dates: [
{ label: "本周", value: "2026-week-19" },
{ label: "上周", value: "2026-week-18" }
],
rooms: [
{ label: "婴儿房", value: "roomA" },
{ label: "主卧", value: "roomB" }
],
devices: [
{ label: "监测垫 A1", value: "deviceA" },
{ label: "监测垫 B2", value: "deviceB" }
]
},
monthly: {
dates: [
{ label: "05月", value: "2026-05" },
{ label: "04月", value: "2026-04" }
],
rooms: [
{ label: "婴儿房", value: "roomA" },
{ label: "主卧", value: "roomB" }
],
devices: [
{ label: "监测垫 A1", value: "deviceA" },
{ label: "监测垫 B2", value: "deviceB" }
]
}
};
export const reportRecords: Record<ReportDimension, Record<string, Record<string, Record<string, ReportRecord>>>> = {
daily: {
"2026-05-08": {
roomA: {
deviceA: createRecord({
babyName: "安安的睡眠报告",
dateLabel: "2026-05-08 日报",
roomLabel: "婴儿房",
deviceLabel: "监测垫 A1",
score: 65,
qualityStatus: "呼吸波动较明显",
totalSleep: "8h 12m",
asleepAt: "22:12",
wakeAt: "06:24",
metrics: [
{ label: "入睡时长", value: "21", unit: "min", status: "正常" },
{ label: "平均呼吸率", value: "22", unit: "次/分", status: "注意" },
{ label: "平均血氧", value: "95", unit: "%", status: "正常" },
{ label: "呼吸暂停次数", value: "4", unit: "次", status: "异常" },
{ label: "平均 HRV", value: "33", unit: "ms", status: "注意" },
{ label: "平均心率", value: "92", unit: "bpm", status: "正常" },
{ label: "异常翻身次数", value: "2", unit: "次", status: "注意" },
{ label: "暂停总时长", value: "2.2", unit: "min", status: "异常" },
{ label: "心率过快次数", value: "2", unit: "次", status: "异常" }
],
aiSummary: [
{ title: "睡眠稳定性", body: "整夜睡眠节律完整,但凌晨 4 点前后出现一次明显清醒,影响连续深睡时长。", tone: "warning" },
{ title: "呼吸状态", body: "呼吸波动在 REM 阶段增大,并出现 4 次短时呼吸暂停,需要继续观察。", tone: "danger" },
{ title: "心率变化", body: "心率整体处于可接受范围,凌晨阶段有 2 次短时升高。", tone: "good" }
],
suggestions: [
{ title: "睡姿建议", body: "建议保持侧卧或轻微抬高头肩,帮助改善夜间呼吸通畅度。", tone: "good" },
{ title: "环境建议", body: "卧室温度保持 24 到 26 度,减少翻身和惊醒。", tone: "excellent" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "2", unit: "次", status: "异常" },
{ label: "呼吸异常", value: "3", unit: "次", status: "注意" },
{ label: "血氧异常", value: "1", unit: "次", status: "注意" },
{ label: "呼吸暂停", value: "4", unit: "次", status: "异常" },
{ label: "离床行为", value: "1", unit: "次", status: "正常" }
]
},
hrvMetrics: [
{ label: "HRV", value: "33 ms", status: "注意" },
{ label: "RMSSD", value: "18.6 ms", status: "注意" },
{ label: "SDNN", value: "26.4 ms", status: "注意" },
{ label: "LF/HF", value: "1.82", status: "压力偏高" },
{ label: "压力指数", value: "中等偏高", status: "压力偏高" }
],
heartRatePoints: baseHeartRate,
breathingPoints: baseBreathing,
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "91", unit: "%", status: "注意" },
{ label: "低氧次数", value: "2", unit: "次", status: "注意" },
{ label: "持续时长", value: "0.8", unit: "min", status: "低氧风险" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "90", unit: "s", status: "注意" },
{ label: "打鼾频率", value: "1.7", unit: "次/h", status: "注意" },
{ label: "峰值强度", value: "61", unit: "dB", status: "异常" }
]
},
eventPoints: baseEvents,
sleepStructure: [
{ label: "深睡", value: 29, color: "#2de1c2" },
{ label: "浅睡", value: 37, color: "#23b6ff" },
{ label: "REM", value: 22, color: "#ffb84d" },
{ label: "清醒", value: 12, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "64%", status: "压力偏高" },
{ label: "副交感活跃度", value: "36%", status: "注意" },
{ label: "压力状态", value: "夜间恢复一般", status: "注意" }
],
daytimePrediction: [
{ label: "精神状态", status: "良好", detail: "上午精神尚可,午后略易疲劳" },
{ label: "疲劳程度", status: "注意", detail: "建议安排午睡" },
{ label: "情绪状态", status: "良好", detail: "总体稳定,惊醒后安抚需求略高" }
]
}),
deviceB: createRecord({
babyName: "安安的睡眠报告",
dateLabel: "2026-05-08 日报",
roomLabel: "婴儿房",
deviceLabel: "监测垫 B2",
score: 79,
qualityStatus: "整体良好",
totalSleep: "8h 34m",
asleepAt: "21:58",
wakeAt: "06:32",
metrics: [
{ label: "入睡时长", value: "18", unit: "min", status: "正常" },
{ label: "平均呼吸率", value: "21", unit: "次/分", status: "正常" },
{ label: "平均血氧", value: "96", unit: "%", status: "正常" },
{ label: "呼吸暂停次数", value: "2", unit: "次", status: "注意" },
{ label: "平均 HRV", value: "38", unit: "ms", status: "良好" },
{ label: "平均心率", value: "90", unit: "bpm", status: "正常" },
{ label: "异常翻身次数", value: "1", unit: "次", status: "正常" },
{ label: "暂停总时长", value: "0.9", unit: "min", status: "注意" },
{ label: "心率过快次数", value: "1", unit: "次", status: "注意" }
],
aiSummary: [
{ title: "睡眠稳定性", body: "整夜清醒次数少,深睡占比相对更高。", tone: "excellent" },
{ title: "呼吸状态", body: "偶发短时波动,但整体处于可接受范围。", tone: "good" },
{ title: "心率变化", body: "夜间心率波动平缓,恢复表现较好。", tone: "excellent" }
],
suggestions: [
{ title: "保持节律", body: "继续维持当前作息,建议固定 22 点前入睡。", tone: "excellent" },
{ title: "环境监测", body: "保持空气湿度,减少凌晨轻微鼻塞对呼吸的影响。", tone: "good" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "1", unit: "次", status: "注意" },
{ label: "呼吸异常", value: "2", unit: "次", status: "注意" },
{ label: "血氧异常", value: "0", unit: "次", status: "正常" },
{ label: "呼吸暂停", value: "2", unit: "次", status: "注意" },
{ label: "离床行为", value: "0", unit: "次", status: "正常" }
]
},
hrvMetrics: [
{ label: "HRV", value: "38 ms", status: "良好" },
{ label: "RMSSD", value: "22.4 ms", status: "良好" },
{ label: "SDNN", value: "28.1 ms", status: "良好" },
{ label: "LF/HF", value: "1.31", status: "正常" },
{ label: "压力指数", value: "稳定", status: "正常" }
],
heartRatePoints: baseHeartRate.map((item) => ({ ...item, value: item.value - 2, y: Math.max(item.y - 6, 18) })),
breathingPoints: baseBreathing.map((item) => ({ ...item, value: item.value - 1, y: Math.max(item.y - 8, 18), tone: item.y > 70 ? "warning" : "good" })),
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "93", unit: "%", status: "正常" },
{ label: "低氧次数", value: "1", unit: "次", status: "注意" },
{ label: "持续时长", value: "0.3", unit: "min", status: "正常" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "36", unit: "s", status: "正常" },
{ label: "打鼾频率", value: "0.8", unit: "次/h", status: "正常" },
{ label: "峰值强度", value: "54", unit: "dB", status: "注意" }
]
},
eventPoints: baseEvents.map((item, index) => ({ ...item, y: item.y - (index % 2 === 0 ? 8 : 5), tone: index === 2 ? "warning" : "good" })),
sleepStructure: [
{ label: "深睡", value: 34, color: "#2de1c2" },
{ label: "浅睡", value: 33, color: "#23b6ff" },
{ label: "REM", value: 21, color: "#ffb84d" },
{ label: "清醒", value: 12, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "52%", status: "正常" },
{ label: "副交感活跃度", value: "48%", status: "良好" },
{ label: "压力状态", value: "恢复较好", status: "良好" }
],
daytimePrediction: [
{ label: "精神状态", status: "优秀", detail: "清晨精神恢复较好" },
{ label: "疲劳程度", status: "良好", detail: "白天精力可维持较久" },
{ label: "情绪状态", status: "良好", detail: "稳定,哭闹概率较低" }
]
})
},
roomB: {
deviceA: createRecord({
babyName: "安安的睡眠报告",
dateLabel: "2026-05-08 日报",
roomLabel: "主卧",
deviceLabel: "监测垫 A1",
score: 71,
qualityStatus: "环境切换带来轻微波动",
totalSleep: "7h 58m",
asleepAt: "22:20",
wakeAt: "06:18",
metrics: [
{ label: "入睡时长", value: "28", unit: "min", status: "注意" },
{ label: "平均呼吸率", value: "22", unit: "次/分", status: "注意" },
{ label: "平均血氧", value: "95", unit: "%", status: "正常" },
{ label: "呼吸暂停次数", value: "3", unit: "次", status: "异常" },
{ label: "平均 HRV", value: "31", unit: "ms", status: "注意" },
{ label: "平均心率", value: "93", unit: "bpm", status: "注意" },
{ label: "异常翻身次数", value: "3", unit: "次", status: "注意" },
{ label: "暂停总时长", value: "1.6", unit: "min", status: "注意" },
{ label: "心率过快次数", value: "2", unit: "次", status: "异常" }
],
aiSummary: [
{ title: "睡眠稳定性", body: "入睡速度偏慢,前半夜轻睡占比偏高。", tone: "warning" },
{ title: "呼吸状态", body: "凌晨阶段存在短时波动,建议继续观察。", tone: "warning" },
{ title: "心率变化", body: "整体稳定,但清醒后恢复速度略慢。", tone: "good" }
],
suggestions: [
{ title: "环境建议", body: "尽量保持固定睡眠环境,减少换房影响。", tone: "good" },
{ title: "安抚建议", body: "入睡前降低灯光刺激,缩短入睡等待时间。", tone: "excellent" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "2", unit: "次", status: "异常" },
{ label: "呼吸异常", value: "2", unit: "次", status: "注意" },
{ label: "血氧异常", value: "1", unit: "次", status: "注意" },
{ label: "呼吸暂停", value: "3", unit: "次", status: "异常" },
{ label: "离床行为", value: "0", unit: "次", status: "正常" }
]
},
hrvMetrics: [
{ label: "HRV", value: "31 ms", status: "注意" },
{ label: "RMSSD", value: "17.2 ms", status: "注意" },
{ label: "SDNN", value: "24.3 ms", status: "注意" },
{ label: "LF/HF", value: "1.95", status: "压力偏高" },
{ label: "压力指数", value: "恢复一般", status: "注意" }
],
heartRatePoints: baseHeartRate.map((item, index) => ({ ...item, value: item.value + (index === 3 ? 5 : 1), y: Math.min(item.y + 3, 88), tone: index === 3 ? "danger" : item.tone })),
breathingPoints: baseBreathing,
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "92", unit: "%", status: "注意" },
{ label: "低氧次数", value: "2", unit: "次", status: "注意" },
{ label: "持续时长", value: "0.6", unit: "min", status: "低氧风险" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "66", unit: "s", status: "注意" },
{ label: "打鼾频率", value: "1.2", unit: "次/h", status: "注意" },
{ label: "峰值强度", value: "58", unit: "dB", status: "注意" }
]
},
eventPoints: baseEvents,
sleepStructure: [
{ label: "深睡", value: 27, color: "#2de1c2" },
{ label: "浅睡", value: 39, color: "#23b6ff" },
{ label: "REM", value: 20, color: "#ffb84d" },
{ label: "清醒", value: 14, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "61%", status: "压力偏高" },
{ label: "副交感活跃度", value: "39%", status: "注意" },
{ label: "压力状态", value: "恢复一般", status: "注意" }
],
daytimePrediction: [
{ label: "精神状态", status: "良好", detail: "上午精力尚可" },
{ label: "疲劳程度", status: "注意", detail: "中午前后易疲劳" },
{ label: "情绪状态", status: "注意", detail: "易受惊醒影响" }
]
})
}
},
"2026-05-07": {
roomA: {
deviceA: createRecord({
babyName: "安安的睡眠报告",
dateLabel: "2026-05-07 日报",
roomLabel: "婴儿房",
deviceLabel: "监测垫 A1",
score: 73,
qualityStatus: "接近良好,异常次数减少",
totalSleep: "8h 05m",
asleepAt: "22:05",
wakeAt: "06:10",
metrics: [
{ label: "入睡时长", value: "20", unit: "min", status: "正常" },
{ label: "平均呼吸率", value: "21", unit: "次/分", status: "正常" },
{ label: "平均血氧", value: "96", unit: "%", status: "正常" },
{ label: "呼吸暂停次数", value: "2", unit: "次", status: "注意" },
{ label: "平均 HRV", value: "35", unit: "ms", status: "良好" },
{ label: "平均心率", value: "91", unit: "bpm", status: "正常" },
{ label: "异常翻身次数", value: "2", unit: "次", status: "注意" },
{ label: "暂停总时长", value: "1.1", unit: "min", status: "注意" },
{ label: "心率过快次数", value: "1", unit: "次", status: "注意" }
],
aiSummary: [
{ title: "睡眠稳定性", body: "较前一日更平稳,深睡时长增加。", tone: "good" },
{ title: "呼吸状态", body: "夜间呼吸事件减少,整体向好。", tone: "good" },
{ title: "心率变化", body: "无明显高风险波动。", tone: "excellent" }
],
suggestions: [
{ title: "继续观察", body: "建议保持现有睡前流程,持续观察一周趋势。", tone: "excellent" },
{ title: "设备摆放", body: "保持设备平整贴合,提升呼吸监测稳定度。", tone: "good" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "1", unit: "次", status: "注意" },
{ label: "呼吸异常", value: "2", unit: "次", status: "注意" },
{ label: "血氧异常", value: "0", unit: "次", status: "正常" },
{ label: "呼吸暂停", value: "2", unit: "次", status: "注意" },
{ label: "离床行为", value: "0", unit: "次", status: "正常" }
]
},
hrvMetrics: [
{ label: "HRV", value: "35 ms", status: "良好" },
{ label: "RMSSD", value: "21.3 ms", status: "良好" },
{ label: "SDNN", value: "27.5 ms", status: "良好" },
{ label: "LF/HF", value: "1.48", status: "正常" },
{ label: "压力指数", value: "稳定", status: "正常" }
],
heartRatePoints: baseHeartRate.map((item) => ({ ...item, value: item.value - 1, y: Math.max(item.y - 3, 20) })),
breathingPoints: baseBreathing.map((item) => ({ ...item, y: Math.max(item.y - 4, 18), tone: item.y > 70 ? "warning" : "good" })),
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "94", unit: "%", status: "正常" },
{ label: "低氧次数", value: "1", unit: "次", status: "注意" },
{ label: "持续时长", value: "0.2", unit: "min", status: "正常" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "41", unit: "s", status: "正常" },
{ label: "打鼾频率", value: "0.9", unit: "次/h", status: "正常" },
{ label: "峰值强度", value: "52", unit: "dB", status: "注意" }
]
},
eventPoints: baseEvents.map((item, index) => ({ ...item, y: item.y - (index + 4), tone: index === 0 ? "warning" : "good" })),
sleepStructure: [
{ label: "深睡", value: 31, color: "#2de1c2" },
{ label: "浅睡", value: 36, color: "#23b6ff" },
{ label: "REM", value: 21, color: "#ffb84d" },
{ label: "清醒", value: 12, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "55%", status: "正常" },
{ label: "副交感活跃度", value: "45%", status: "良好" },
{ label: "压力状态", value: "恢复尚可", status: "良好" }
],
daytimePrediction: [
{ label: "精神状态", status: "良好", detail: "上午状态稳定" },
{ label: "疲劳程度", status: "良好", detail: "午后需要短暂休息" },
{ label: "情绪状态", status: "良好", detail: "整体平稳" }
]
})
}
}
},
weekly: {
"2026-week-19": {
roomA: {
deviceA: createRecord({
babyName: "安安的睡眠周报",
dateLabel: "第 19 周周报",
roomLabel: "婴儿房",
deviceLabel: "监测垫 A1",
score: 76,
qualityStatus: "本周整体良好,凌晨波动仍需关注",
totalSleep: "8h 18m",
asleepAt: "22:08",
wakeAt: "06:26",
metrics: [
{ label: "平均入睡", value: "19", unit: "min", status: "正常" },
{ label: "平均呼吸率", value: "21.4", unit: "次/分", status: "正常" },
{ label: "平均血氧", value: "95.8", unit: "%", status: "正常" },
{ label: "呼吸暂停次数", value: "2.4", unit: "次/晚", status: "注意" },
{ label: "平均 HRV", value: "36", unit: "ms", status: "良好" },
{ label: "平均心率", value: "91", unit: "bpm", status: "正常" },
{ label: "异常翻身次数", value: "1.6", unit: "次/晚", status: "正常" },
{ label: "暂停总时长", value: "1.1", unit: "min", status: "注意" },
{ label: "心率过快次数", value: "1.1", unit: "次/晚", status: "注意" }
],
aiSummary: [
{ title: "周趋势", body: "本周睡眠评分稳定在良好区间,后半周连续性提升。", tone: "good" },
{ title: "呼吸状态", body: "周中仍有 2 晚出现轻微呼吸事件,建议继续观察。", tone: "warning" },
{ title: "心率恢复", body: "整体恢复表现良好。", tone: "excellent" }
],
suggestions: [
{ title: "继续观察周趋势", body: "建议下周继续关注凌晨 2 点到 4 点的呼吸波动。", tone: "good" },
{ title: "保持睡前节律", body: "稳定洗漱、喂奶和安抚流程。", tone: "excellent" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "7", unit: "次", status: "注意" },
{ label: "呼吸异常", value: "11", unit: "次", status: "注意" },
{ label: "血氧异常", value: "3", unit: "次", status: "注意" },
{ label: "呼吸暂停", value: "17", unit: "次", status: "注意" },
{ label: "离床行为", value: "2", unit: "次", status: "正常" }
]
},
hrvMetrics: [
{ label: "HRV", value: "36 ms", status: "良好" },
{ label: "RMSSD", value: "20.8 ms", status: "良好" },
{ label: "SDNN", value: "29.0 ms", status: "良好" },
{ label: "LF/HF", value: "1.56", status: "正常" },
{ label: "压力指数", value: "稳定", status: "正常" }
],
heartRatePoints: baseHeartRate,
breathingPoints: baseBreathing,
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "92", unit: "%", status: "注意" },
{ label: "低氧次数", value: "5", unit: "次", status: "注意" },
{ label: "持续时长", value: "1.7", unit: "min", status: "低氧风险" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "230", unit: "s", status: "注意" },
{ label: "打鼾频率", value: "1.1", unit: "次/h", status: "注意" },
{ label: "峰值强度", value: "60", unit: "dB", status: "异常" }
]
},
eventPoints: baseEvents,
sleepStructure: [
{ label: "深睡", value: 32, color: "#2de1c2" },
{ label: "浅睡", value: 35, color: "#23b6ff" },
{ label: "REM", value: 22, color: "#ffb84d" },
{ label: "清醒", value: 11, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "56%", status: "正常" },
{ label: "副交感活跃度", value: "44%", status: "良好" },
{ label: "压力状态", value: "本周恢复较平衡", status: "良好" }
],
daytimePrediction: [
{ label: "精神状态", status: "良好", detail: "本周白天精神总体平稳" },
{ label: "疲劳程度", status: "良好", detail: "午后有轻度疲劳趋势" },
{ label: "情绪状态", status: "优秀", detail: "情绪稳定度较高" }
]
})
}
},
"2026-week-18": {
roomA: {
deviceA: createRecord({
babyName: "安安的睡眠周报",
dateLabel: "第 18 周周报",
roomLabel: "婴儿房",
deviceLabel: "监测垫 A1",
score: 69,
qualityStatus: "周内波动偏多,需关注呼吸质量",
totalSleep: "7h 56m",
asleepAt: "22:16",
wakeAt: "06:12",
metrics: [
{ label: "平均入睡", value: "24", unit: "min", status: "注意" },
{ label: "平均呼吸率", value: "22.6", unit: "次/分", status: "注意" },
{ label: "平均血氧", value: "95.1", unit: "%", status: "正常" },
{ label: "呼吸暂停次数", value: "3.8", unit: "次/晚", status: "异常" },
{ label: "平均 HRV", value: "31", unit: "ms", status: "注意" },
{ label: "平均心率", value: "93", unit: "bpm", status: "注意" },
{ label: "异常翻身次数", value: "2.5", unit: "次/晚", status: "注意" },
{ label: "暂停总时长", value: "1.9", unit: "min", status: "异常" },
{ label: "心率过快次数", value: "2.0", unit: "次/晚", status: "异常" }
],
aiSummary: [
{ title: "周趋势", body: "周内夜间清醒次数偏多,连续深睡不足。", tone: "warning" },
{ title: "呼吸状态", body: "呼吸暂停事件较上周增加。", tone: "danger" },
{ title: "恢复情况", body: "HRV 较低,夜间恢复一般。", tone: "warning" }
],
suggestions: [
{ title: "优先观察呼吸事件", body: "建议结合视频或临床建议继续观察。", tone: "danger" },
{ title: "调整环境", body: "减少卧室闷热和干燥问题。", tone: "good" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "11", unit: "次", status: "异常" },
{ label: "呼吸异常", value: "16", unit: "次", status: "异常" },
{ label: "血氧异常", value: "6", unit: "次", status: "注意" },
{ label: "呼吸暂停", value: "24", unit: "次", status: "异常" },
{ label: "离床行为", value: "3", unit: "次", status: "注意" }
]
},
hrvMetrics: [
{ label: "HRV", value: "31 ms", status: "注意" },
{ label: "RMSSD", value: "16.8 ms", status: "注意" },
{ label: "SDNN", value: "24.8 ms", status: "注意" },
{ label: "LF/HF", value: "2.04", status: "压力偏高" },
{ label: "压力指数", value: "恢复一般", status: "压力偏高" }
],
heartRatePoints: baseHeartRate.map((item) => ({ ...item, value: item.value + 2, y: Math.min(item.y + 5, 86) })),
breathingPoints: baseBreathing.map((item) => ({ ...item, value: item.value + 1, y: Math.min(item.y + 5, 84), tone: item.y > 60 ? "warning" : item.tone })),
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "90", unit: "%", status: "低氧风险" },
{ label: "低氧次数", value: "8", unit: "次", status: "低氧风险" },
{ label: "持续时长", value: "2.6", unit: "min", status: "高风险" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "320", unit: "s", status: "异常" },
{ label: "打鼾频率", value: "1.8", unit: "次/h", status: "注意" },
{ label: "峰值强度", value: "64", unit: "dB", status: "异常" }
]
},
eventPoints: baseEvents.map((item, index) => ({ ...item, y: Math.min(item.y + index * 4, 88), tone: index > 1 ? "danger" : "warning" })),
sleepStructure: [
{ label: "深睡", value: 25, color: "#2de1c2" },
{ label: "浅睡", value: 40, color: "#23b6ff" },
{ label: "REM", value: 21, color: "#ffb84d" },
{ label: "清醒", value: 14, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "63%", status: "压力偏高" },
{ label: "副交感活跃度", value: "37%", status: "注意" },
{ label: "压力状态", value: "紧张偏高", status: "压力偏高" }
],
daytimePrediction: [
{ label: "精神状态", status: "注意", detail: "白天精神波动较明显" },
{ label: "疲劳程度", status: "异常", detail: "更易困倦和烦躁" },
{ label: "情绪状态", status: "注意", detail: "需要更多安抚" }
]
})
}
}
},
monthly: {
"2026-05": {
roomA: {
deviceA: createRecord({
babyName: "安安的睡眠月报",
dateLabel: "2026 年 05 月",
roomLabel: "婴儿房",
deviceLabel: "监测垫 A1",
score: 81,
qualityStatus: "本月整体良好,趋势向稳",
totalSleep: "8h 20m",
asleepAt: "22:03",
wakeAt: "06:23",
metrics: [
{ label: "平均入睡", value: "18", unit: "min", status: "正常" },
{ label: "平均呼吸率", value: "21.2", unit: "次/分", status: "正常" },
{ label: "平均血氧", value: "96.2", unit: "%", status: "良好" },
{ label: "呼吸暂停次数", value: "1.9", unit: "次/晚", status: "注意" },
{ label: "平均 HRV", value: "39", unit: "ms", status: "良好" },
{ label: "平均心率", value: "90", unit: "bpm", status: "正常" },
{ label: "异常翻身次数", value: "1.2", unit: "次/晚", status: "正常" },
{ label: "暂停总时长", value: "0.8", unit: "min", status: "注意" },
{ label: "心率过快次数", value: "0.8", unit: "次/晚", status: "正常" }
],
aiSummary: [
{ title: "月趋势", body: "本月睡眠评分逐步上升,后半月深睡比例提升明显。", tone: "excellent" },
{ title: "呼吸状态", body: "呼吸事件较前月减少,风险总体下降。", tone: "good" },
{ title: "恢复质量", body: "HRV 和夜间恢复状态均有改善。", tone: "excellent" }
],
suggestions: [
{ title: "延续当前节律", body: "建议保持现有入睡时间和安抚流程。", tone: "excellent" },
{ title: "关注季节变化", body: "温湿度变化时继续留意夜间呼吸状态。", tone: "good" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "18", unit: "次", status: "注意" },
{ label: "呼吸异常", value: "26", unit: "次", status: "注意" },
{ label: "血氧异常", value: "7", unit: "次", status: "注意" },
{ label: "呼吸暂停", value: "39", unit: "次", status: "注意" },
{ label: "离床行为", value: "4", unit: "次", status: "正常" }
]
},
hrvMetrics: [
{ label: "HRV", value: "39 ms", status: "良好" },
{ label: "RMSSD", value: "23.1 ms", status: "良好" },
{ label: "SDNN", value: "30.4 ms", status: "良好" },
{ label: "LF/HF", value: "1.38", status: "正常" },
{ label: "压力指数", value: "稳定", status: "良好" }
],
heartRatePoints: baseHeartRate.map((item) => ({ ...item, y: Math.max(item.y - 5, 16), tone: item.y > 60 ? "good" : "excellent" })),
breathingPoints: baseBreathing.map((item) => ({ ...item, y: Math.max(item.y - 7, 20), tone: item.y > 70 ? "warning" : "good" })),
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "93", unit: "%", status: "正常" },
{ label: "低氧次数", value: "10", unit: "次", status: "注意" },
{ label: "持续时长", value: "3.4", unit: "min", status: "低氧风险" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "540", unit: "s", status: "注意" },
{ label: "打鼾频率", value: "1.0", unit: "次/h", status: "正常" },
{ label: "峰值强度", value: "58", unit: "dB", status: "注意" }
]
},
eventPoints: baseEvents.map((item, index) => ({ ...item, y: Math.max(item.y - (index * 6 + 8), 20), tone: index === 0 ? "warning" : "good" })),
sleepStructure: [
{ label: "深睡", value: 35, color: "#2de1c2" },
{ label: "浅睡", value: 32, color: "#23b6ff" },
{ label: "REM", value: 22, color: "#ffb84d" },
{ label: "清醒", value: 11, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "51%", status: "正常" },
{ label: "副交感活跃度", value: "49%", status: "良好" },
{ label: "压力状态", value: "恢复平衡", status: "良好" }
],
daytimePrediction: [
{ label: "精神状态", status: "优秀", detail: "本月白天精神恢复稳定" },
{ label: "疲劳程度", status: "良好", detail: "疲劳风险较低" },
{ label: "情绪状态", status: "优秀", detail: "整体情绪稳定" }
]
})
}
},
"2026-04": {
roomA: {
deviceA: createRecord({
babyName: "安安的睡眠月报",
dateLabel: "2026 年 04 月",
roomLabel: "婴儿房",
deviceLabel: "监测垫 A1",
score: 74,
qualityStatus: "月内波动仍在改善中",
totalSleep: "8h 02m",
asleepAt: "22:14",
wakeAt: "06:16",
metrics: [
{ label: "平均入睡", value: "22", unit: "min", status: "注意" },
{ label: "平均呼吸率", value: "22.1", unit: "次/分", status: "注意" },
{ label: "平均血氧", value: "95.4", unit: "%", status: "正常" },
{ label: "呼吸暂停次数", value: "2.8", unit: "次/晚", status: "注意" },
{ label: "平均 HRV", value: "34", unit: "ms", status: "注意" },
{ label: "平均心率", value: "92", unit: "bpm", status: "正常" },
{ label: "异常翻身次数", value: "1.9", unit: "次/晚", status: "注意" },
{ label: "暂停总时长", value: "1.4", unit: "min", status: "注意" },
{ label: "心率过快次数", value: "1.4", unit: "次/晚", status: "注意" }
],
aiSummary: [
{ title: "月趋势", body: "月初波动较大,月末开始回稳。", tone: "warning" },
{ title: "呼吸状态", body: "呼吸事件仍有下降空间。", tone: "warning" },
{ title: "恢复质量", body: "夜间恢复在逐步提升。", tone: "good" }
],
suggestions: [
{ title: "继续保持观察", body: "建议继续以周趋势方式跟踪。", tone: "good" },
{ title: "保持环境稳定", body: "夜间温湿度波动控制有助于维持呼吸平稳。", tone: "good" }
],
anomalyStats: {
title: "异常统计",
items: [
{ label: "心率异常", value: "24", unit: "次", status: "注意" },
{ label: "呼吸异常", value: "34", unit: "次", status: "注意" },
{ label: "血氧异常", value: "10", unit: "次", status: "注意" },
{ label: "呼吸暂停", value: "51", unit: "次", status: "异常" },
{ label: "离床行为", value: "6", unit: "次", status: "注意" }
]
},
hrvMetrics: [
{ label: "HRV", value: "34 ms", status: "注意" },
{ label: "RMSSD", value: "19.2 ms", status: "注意" },
{ label: "SDNN", value: "27.1 ms", status: "注意" },
{ label: "LF/HF", value: "1.74", status: "注意" },
{ label: "压力指数", value: "轻度偏高", status: "注意" }
],
heartRatePoints: baseHeartRate,
breathingPoints: baseBreathing,
oxygenSummary: {
title: "低氧统计",
items: [
{ label: "最低血氧", value: "91", unit: "%", status: "注意" },
{ label: "低氧次数", value: "14", unit: "次", status: "低氧风险" },
{ label: "持续时长", value: "4.8", unit: "min", status: "高风险" }
]
},
snoreSummary: {
title: "打鼾监测",
items: [
{ label: "打鼾时长", value: "610", unit: "s", status: "注意" },
{ label: "打鼾频率", value: "1.3", unit: "次/h", status: "注意" },
{ label: "峰值强度", value: "62", unit: "dB", status: "异常" }
]
},
eventPoints: baseEvents,
sleepStructure: [
{ label: "深睡", value: 28, color: "#2de1c2" },
{ label: "浅睡", value: 38, color: "#23b6ff" },
{ label: "REM", value: 21, color: "#ffb84d" },
{ label: "清醒", value: 13, color: "#ff6b6b" }
],
autonomicMetrics: [
{ label: "交感活跃度", value: "59%", status: "注意" },
{ label: "副交感活跃度", value: "41%", status: "注意" },
{ label: "压力状态", value: "轻度偏高", status: "注意" }
],
daytimePrediction: [
{ label: "精神状态", status: "良好", detail: "白天状态较 5 月略弱" },
{ label: "疲劳程度", status: "注意", detail: "午后更易疲劳" },
{ label: "情绪状态", status: "良好", detail: "波动可控" }
]
})
}
}
}
};

View File

@@ -0,0 +1,55 @@
export type ReportTone = "excellent" | "good" | "warning" | "danger";
export function getSleepLevel(score: number): { label: string; tone: ReportTone } {
if (score >= 90) {
return { label: "优秀", tone: "excellent" };
}
if (score >= 75) {
return { label: "良好", tone: "good" };
}
if (score >= 60) {
return { label: "合格", tone: "warning" };
}
return { label: "异常", tone: "danger" };
}
export function getStatusTone(status: string): ReportTone {
if (status === "正常" || status === "优秀") {
return "excellent";
}
if (status === "良好") {
return "good";
}
if (status === "注意" || status === "合格") {
return "warning";
}
return "danger";
}
export function pickReportRecord<T>(
records: Record<string, Record<string, Record<string, T>>>,
dateKey: string,
roomKey: string,
deviceKey: string
): T {
const rooms = records[dateKey] ?? {};
const devices = rooms[roomKey] ?? {};
if (deviceKey in devices) {
return devices[deviceKey];
}
const fallback = Object.values(devices)[0];
if (!fallback) {
throw new Error("未找到报告数据");
}
return fallback;
}

107
src/pages/report/types.ts Normal file
View File

@@ -0,0 +1,107 @@
import type { ReportTone } from "./report-utils";
export type ReportDimension = "daily" | "weekly" | "monthly";
export type ReportOption = {
label: string;
value: string;
};
export type ReportChipOption = {
label: string;
value: string;
};
export type ScoreFactor = {
label: string;
score: number;
};
export type TrendPoint = {
id: string;
time: string;
label: string;
value: number;
x: number;
y: number;
tone: ReportTone;
meta?: string;
};
export type MetricCard = {
label: string;
value: string;
unit: string;
status: string;
};
export type InsightItem = {
title: string;
body: string;
tone: ReportTone;
};
export type SummaryItem = {
label: string;
value: string;
unit?: string;
status?: string;
};
export type SummaryBlock = {
title: string;
items: SummaryItem[];
};
export type KvMetric = {
label: string;
value: string;
status?: string;
};
export type DistributionItem = {
label: string;
value: number;
color: string;
};
export type DaytimeState = {
label: string;
status: string;
detail: string;
};
export type ReportRecord = {
babyName: string;
dateLabel: string;
roomLabel: string;
deviceLabel: string;
score: number;
qualityStatus: string;
totalSleep: string;
asleepAt: string;
wakeAt: string;
scoreFactors: ScoreFactor[];
sleepTrend: TrendPoint[];
scoreTrend: TrendPoint[];
metrics: MetricCard[];
aiSummary: InsightItem[];
suggestions: InsightItem[];
anomalyStats: SummaryBlock;
hrvMetrics: KvMetric[];
heartRatePoints: TrendPoint[];
breathingPoints: TrendPoint[];
oxygenSummary: SummaryBlock;
snoreSummary: SummaryBlock;
eventPoints: TrendPoint[];
sleepStructure: DistributionItem[];
autonomicMetrics: KvMetric[];
daytimePrediction: DaytimeState[];
};
export type ReportDimensionOptions = {
dates: ReportOption[];
rooms: ReportOption[];
devices: ReportOption[];
};