diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx
index b81c589..8a18dcf 100644
--- a/app/sleep-detail.tsx
+++ b/app/sleep-detail.tsx
@@ -736,6 +736,109 @@ export default function SleepDetailScreen() {
);
})}
+
+ {/* Raw Sleep Samples List - 显示所有原始睡眠数据 */}
+ {sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 0 && (
+
+
+
+ 原始睡眠数据 ({sleepData.rawSleepSamples.length} 条记录)
+
+
+ 查看数据间隔和可能的gap
+
+
+
+
+ {sleepData.rawSleepSamples.map((sample, index) => {
+ // 计算与前一个样本的时间间隔
+ const prevSample = index > 0 ? sleepData.rawSleepSamples[index - 1] : null;
+ let gapMinutes = 0;
+ let hasGap = false;
+
+ if (prevSample) {
+ const prevEndTime = new Date(prevSample.endDate).getTime();
+ const currentStartTime = new Date(sample.startDate).getTime();
+ gapMinutes = (currentStartTime - prevEndTime) / (1000 * 60);
+ hasGap = gapMinutes > 1; // 大于1分钟视为有间隔
+ }
+
+ const startTime = formatTime(sample.startDate);
+ const endTime = formatTime(sample.endDate);
+ const duration = Math.round((new Date(sample.endDate).getTime() - new Date(sample.startDate).getTime()) / (1000 * 60));
+
+ // 获取睡眠阶段中文名称
+ const getStageName = (value: string) => {
+ switch (value) {
+ case 'HKCategoryValueSleepAnalysisInBed': return '在床上';
+ case 'HKCategoryValueSleepAnalysisAwake': return '清醒';
+ case 'HKCategoryValueSleepAnalysisAsleepCore': return '核心睡眠';
+ case 'HKCategoryValueSleepAnalysisAsleepDeep': return '深度睡眠';
+ case 'HKCategoryValueSleepAnalysisAsleepREM': return 'REM睡眠';
+ case 'HKCategoryValueSleepAnalysisAsleepUnspecified': return '未指定睡眠';
+ default: return value;
+ }
+ };
+
+ // 获取状态颜色
+ const getStageColor = (value: string) => {
+ switch (value) {
+ case 'HKCategoryValueSleepAnalysisInBed': return '#9CA3AF';
+ case 'HKCategoryValueSleepAnalysisAwake': return '#F59E0B';
+ case 'HKCategoryValueSleepAnalysisAsleepCore': return '#8B5CF6';
+ case 'HKCategoryValueSleepAnalysisAsleepDeep': return '#3B82F6';
+ case 'HKCategoryValueSleepAnalysisAsleepREM': return '#EC4899';
+ case 'HKCategoryValueSleepAnalysisAsleepUnspecified': return '#6B7280';
+ default: return '#6B7280';
+ }
+ };
+
+ return (
+
+ {/* 显示数据间隔 */}
+ {hasGap && (
+
+
+
+ 数据间隔: {Math.round(gapMinutes)}分钟
+
+
+ )}
+
+ {/* 睡眠样本条目 */}
+
+
+
+
+
+ {getStageName(sample.value)}
+
+
+
+ {duration}分钟
+
+
+
+
+
+ {startTime} - {endTime}
+
+
+ #{index + 1}
+
+
+
+
+ );
+ })}
+
+
+ )}
{infoModal.type && (
@@ -1388,4 +1491,92 @@ const styles = StyleSheet.create({
lineHeight: 16,
marginBottom: 4,
},
+ // Raw Sleep Samples List 样式
+ rawSamplesContainer: {
+ borderRadius: 16,
+ padding: 16,
+ marginBottom: 24,
+ marginHorizontal: 4,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 3,
+ },
+ rawSamplesHeader: {
+ marginBottom: 16,
+ },
+ rawSamplesTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ rawSamplesSubtitle: {
+ fontSize: 12,
+ fontWeight: '500',
+ },
+ rawSamplesScrollView: {
+ maxHeight: 400, // 限制高度,避免列表过长
+ },
+ rawSampleItem: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderLeftWidth: 3,
+ borderLeftColor: 'transparent',
+ marginBottom: 8,
+ borderRadius: 8,
+ backgroundColor: 'rgba(248, 250, 252, 0.5)',
+ },
+ sampleHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 4,
+ },
+ sampleLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ stageDot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ marginRight: 8,
+ },
+ sampleStage: {
+ fontSize: 14,
+ fontWeight: '500',
+ flex: 1,
+ },
+ sampleDuration: {
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ sampleTimeRange: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ sampleTime: {
+ fontSize: 12,
+ },
+ sampleIndex: {
+ fontSize: 10,
+ fontWeight: '500',
+ },
+ gapIndicator: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ marginVertical: 4,
+ borderRadius: 8,
+ gap: 6,
+ },
+ gapText: {
+ fontSize: 12,
+ fontWeight: '600',
+ },
});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index bf32659..edc25d3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.2",
"dependencies": {
"@expo/vector-icons": "^14.1.0",
+ "@kingstinct/react-native-healthkit": "^10.1.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/datetimepicker": "^8.4.4",
"@react-native-masked-view/masked-view": "^0.3.2",
@@ -55,6 +56,7 @@
"react-native-image-viewing": "^0.2.2",
"react-native-markdown-display": "^7.0.2",
"react-native-modal-datetime-picker": "^18.0.0",
+ "react-native-nitro-modules": "^0.29.3",
"react-native-popover-view": "^6.1.0",
"react-native-purchases": "^9.2.2",
"react-native-reanimated": "~3.17.4",
@@ -2728,6 +2730,21 @@
"react-native": "*"
}
},
+ "node_modules/@kingstinct/react-native-healthkit": {
+ "version": "10.1.0",
+ "resolved": "https://mirrors.tencent.com/npm/@kingstinct/react-native-healthkit/-/react-native-healthkit-10.1.0.tgz",
+ "integrity": "sha512-p6f3Uf4p6GXs+8xIc5NHu8DPnNJC9kxGvI+4qmgGk5U24hVZBZFAwFT53jkQMoIHZIoQmtuXJDp8jMJ7WzeZ+Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kingstinct"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-nitro-modules": "*"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -11732,6 +11749,17 @@
"react-native": ">=0.65.0"
}
},
+ "node_modules/react-native-nitro-modules": {
+ "version": "0.29.3",
+ "resolved": "https://mirrors.tencent.com/npm/react-native-nitro-modules/-/react-native-nitro-modules-0.29.3.tgz",
+ "integrity": "sha512-gGaCueHKaZSw2rlrKrPgMZE6O6qvsnTJwNysJgk4ZEHMwnVe6Auk5hc4+sJPQLOVd6o+HMHdVhVQhZZv1u19Eg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-popover-view": {
"version": "6.1.0",
"resolved": "https://mirrors.tencent.com/npm/react-native-popover-view/-/react-native-popover-view-6.1.0.tgz",
diff --git a/package.json b/package.json
index 0647ddc..5232473 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"@expo/vector-icons": "^14.1.0",
+ "@kingstinct/react-native-healthkit": "^10.1.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/datetimepicker": "^8.4.4",
"@react-native-masked-view/masked-view": "^0.3.2",
@@ -27,6 +28,7 @@
"dayjs": "^1.11.13",
"expo": "53.0.22",
"expo-apple-authentication": "~7.2.4",
+ "expo-background-task": "~0.2.8",
"expo-blur": "~14.1.5",
"expo-camera": "^16.1.11",
"expo-constants": "~17.1.7",
@@ -58,6 +60,7 @@
"react-native-image-viewing": "^0.2.2",
"react-native-markdown-display": "^7.0.2",
"react-native-modal-datetime-picker": "^18.0.0",
+ "react-native-nitro-modules": "^0.29.3",
"react-native-popover-view": "^6.1.0",
"react-native-purchases": "^9.2.2",
"react-native-reanimated": "~3.17.4",
@@ -69,8 +72,7 @@
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5",
"react-native-wheel-picker-expo": "^0.5.4",
- "react-redux": "^9.2.0",
- "expo-background-task": "~0.2.8"
+ "react-redux": "^9.2.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -80,4 +82,4 @@
"typescript": "~5.8.3"
},
"private": true
-}
\ No newline at end of file
+}
diff --git a/services/sleepService.ts b/services/sleepService.ts
index 4adaae9..c56c624 100644
--- a/services/sleepService.ts
+++ b/services/sleepService.ts
@@ -320,37 +320,27 @@ export async function fetchSleepDetailForDate(date: Date): Promise
- sample.value !== SleepStage.InBed && sample.value !== SleepStage.Awake
- );
-
- // 入睡时间:第一个实际睡眠阶段的开始时间
- // 起床时间:最后一个实际睡眠阶段的结束时间
+ // 使用所有样本数据来确定完整的睡眠周期(包含清醒时间)
let bedtime: string;
let wakeupTime: string;
- if (actualSleepSamples.length > 0) {
+ if (sleepSamples.length > 0) {
// 按开始时间排序
- const sortedSleepSamples = actualSleepSamples.sort((a, b) =>
+ const sortedSamples = sleepSamples.sort((a, b) =>
new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
);
- bedtime = sortedSleepSamples[0].startDate;
- wakeupTime = sortedSleepSamples[sortedSleepSamples.length - 1].endDate;
+ // 入睡时间:第一个样本的开始时间(包含清醒时间,确保完整性)
+ bedtime = sortedSamples[0].startDate;
+ // 起床时间:最后一个样本的结束时间
+ wakeupTime = sortedSamples[sortedSamples.length - 1].endDate;
console.log('计算入睡和起床时间:');
console.log('- 入睡时间:', dayjs(bedtime).format('YYYY-MM-DD HH:mm:ss'));
console.log('- 起床时间:', dayjs(wakeupTime).format('YYYY-MM-DD HH:mm:ss'));
} else {
- // 如果没有实际睡眠数据,回退到使用所有样本数据
- console.warn('没有找到实际睡眠阶段数据,使用所有样本数据');
- const sortedAllSamples = sleepSamples.sort((a, b) =>
- new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
- );
-
- bedtime = sortedAllSamples[0].startDate;
- wakeupTime = sortedAllSamples[sortedAllSamples.length - 1].endDate;
+ console.warn('没有找到睡眠样本数据');
+ return null;
}
// 计算在床时间 - 使用 INBED 样本数据
@@ -380,7 +370,6 @@ export async function fetchSleepDetailForDate(date: Date): Promise total + stage.duration, 0);
@@ -401,6 +390,19 @@ export async function fetchSleepDetailForDate(date: Date): Promise {
+ console.log(` ${getSleepStageDisplayName(stage.stage)}: ${stage.duration}分钟 (${stage.percentage}%)`);
+ });
+
+ console.log('========================');
+
const sleepDetailData: SleepDetailData = {
sleepScore,
totalSleepTime,
@@ -417,7 +419,7 @@ export async function fetchSleepDetailForDate(date: Date): Promise