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:
@@ -1,6 +1,7 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
"pages/index/index",
|
||||
"pages/report/index",
|
||||
"pages/message/index",
|
||||
"pages/mine/index",
|
||||
"pages/profile/index",
|
||||
|
||||
66
src/components/report/chart-card.tsx
Normal file
66
src/components/report/chart-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
src/components/report/chip-group.tsx
Normal file
29
src/components/report/chip-group.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/report/daytime-prediction.tsx
Normal file
28
src/components/report/daytime-prediction.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
src/components/report/distribution-card.tsx
Normal file
35
src/components/report/distribution-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/report/insight-list.tsx
Normal file
28
src/components/report/insight-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
src/components/report/kv-list.tsx
Normal file
29
src/components/report/kv-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/report/metric-grid.tsx
Normal file
28
src/components/report/metric-grid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
src/components/report/page-header.tsx
Normal file
25
src/components/report/page-header.tsx
Normal 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"><</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>
|
||||
);
|
||||
}
|
||||
63
src/components/report/score-overview.tsx
Normal file
63
src/components/report/score-overview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
src/components/report/section-card.tsx
Normal file
24
src/components/report/section-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
src/components/report/summary-block.tsx
Normal file
33
src/components/report/summary-block.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">></Text>
|
||||
</View>
|
||||
|
||||
<PanelCard className="device-notice-card" warning>
|
||||
<View className="device-notice-card__title-row">
|
||||
<View className="device-notice-card__horn" />
|
||||
|
||||
3
src/pages/report/index.config.ts
Normal file
3
src/pages/report/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: "custom"
|
||||
});
|
||||
754
src/pages/report/index.scss
Normal file
754
src/pages/report/index.scss
Normal 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
195
src/pages/report/index.tsx
Normal 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
865
src/pages/report/mock.ts
Normal 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: "波动可控" }
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
55
src/pages/report/report-utils.ts
Normal file
55
src/pages/report/report-utils.ts
Normal 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
107
src/pages/report/types.ts
Normal 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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user