Files
digital-pilates/components/HealthKitTest.tsx
2025-09-17 18:05:11 +08:00

383 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* HealthKit测试组件
* 用于测试和演示HealthKit native module的功能
*/
import React, { useEffect, useState } from 'react';
import {
Alert,
Platform,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import HealthKitManager, { HealthKitUtils, SleepDataSample } from '../utils/healthKit';
interface HealthKitTestState {
isAvailable: boolean;
isAuthorized: boolean;
sleepData: SleepDataSample[];
lastNightSleep: any;
loading: boolean;
error: string | null;
}
const HealthKitTest: React.FC = () => {
const [state, setState] = useState<HealthKitTestState>({
isAvailable: false,
isAuthorized: false,
sleepData: [],
lastNightSleep: null,
loading: false,
error: null,
});
useEffect(() => {
// 检查HealthKit可用性
const available = HealthKitUtils.isAvailable();
setState(prev => ({ ...prev, isAvailable: available }));
if (!available && Platform.OS === 'ios') {
Alert.alert('提示', 'HealthKit在当前设备上不可用可能是因为运行在模拟器上。');
}
}, []);
const handleRequestAuthorization = async () => {
if (!state.isAvailable) {
Alert.alert('错误', 'HealthKit不可用');
return;
}
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const result = await HealthKitManager.requestAuthorization();
if (result.success) {
const sleepPermission = result.permissions['HKCategoryTypeIdentifierSleepAnalysis'];
const authorized = sleepPermission === 'authorized';
setState(prev => ({ ...prev, isAuthorized: authorized, loading: false }));
Alert.alert(
'授权结果',
authorized ? '已获得睡眠数据访问权限' : `睡眠数据权限状态: ${sleepPermission}`,
[{ text: '确定' }]
);
} else {
setState(prev => ({ ...prev, loading: false }));
Alert.alert('授权失败', '用户拒绝了HealthKit权限请求');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
setState(prev => ({ ...prev, loading: false, error: errorMessage }));
Alert.alert('错误', `授权失败: ${errorMessage}`);
}
};
const handleGetSleepData = async () => {
if (!state.isAuthorized) {
Alert.alert('错误', '请先获取HealthKit授权');
return;
}
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - 7); // 获取最近7天的数据
const result = await HealthKitManager.getSleepData({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
limit: 50,
});
setState(prev => ({
...prev,
sleepData: result.data,
loading: false,
}));
Alert.alert('成功', `获取到 ${result.count} 条睡眠记录`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
setState(prev => ({ ...prev, loading: false, error: errorMessage }));
Alert.alert('错误', `获取睡眠数据失败: ${errorMessage}`);
}
};
const handleGetLastNightSleep = async () => {
if (!state.isAuthorized) {
Alert.alert('错误', '请先获取HealthKit授权');
return;
}
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const startDate = new Date(yesterday);
startDate.setHours(18, 0, 0, 0);
const endDate = new Date(today);
endDate.setHours(12, 0, 0, 0);
const result = await HealthKitManager.getSleepData({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
limit: 20,
});
const sleepSamples = result.data.filter(sample =>
['asleep', 'core', 'deep', 'rem'].includes(sample.categoryType)
);
if (sleepSamples.length > 0) {
const sleepStart = new Date(Math.min(...sleepSamples.map(s => new Date(s.startDate).getTime())));
const sleepEnd = new Date(Math.max(...sleepSamples.map(s => new Date(s.endDate).getTime())));
const totalDuration = sleepSamples.reduce((sum, s) => sum + s.duration, 0);
const lastNightData = {
hasData: true,
sleepStart: sleepStart.toISOString(),
sleepEnd: sleepEnd.toISOString(),
totalDuration,
totalDurationFormatted: HealthKitUtils.formatDuration(totalDuration),
samples: sleepSamples,
bedTime: sleepStart.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
wakeTime: sleepEnd.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
};
setState(prev => ({ ...prev, lastNightSleep: lastNightData, loading: false }));
Alert.alert('昨晚睡眠', `睡眠时间: ${lastNightData.bedTime} - ${lastNightData.wakeTime}\n睡眠时长: ${lastNightData.totalDurationFormatted}`);
} else {
setState(prev => ({
...prev,
lastNightSleep: { hasData: false, message: '未找到昨晚的睡眠数据' },
loading: false
}));
Alert.alert('提示', '未找到昨晚的睡眠数据');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
setState(prev => ({ ...prev, loading: false, error: errorMessage }));
Alert.alert('错误', `获取昨晚睡眠数据失败: ${errorMessage}`);
}
};
const renderSleepSample = (sample: SleepDataSample, index: number) => (
<View key={sample.id} style={styles.sampleItem}>
<Text style={styles.sampleTitle}> #{index + 1}</Text>
<Text style={styles.sampleText}>: {sample.categoryType}</Text>
<Text style={styles.sampleText}>: {HealthKitUtils.formatDuration(sample.duration)}</Text>
<Text style={styles.sampleText}>
: {new Date(sample.startDate).toLocaleString('zh-CN')} - {new Date(sample.endDate).toLocaleTimeString('zh-CN')}
</Text>
<Text style={styles.sampleText}>: {sample.source.name}</Text>
</View>
);
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>HealthKit </Text>
{/* 状态显示 */}
<View style={styles.statusContainer}>
<Text style={styles.statusTitle}></Text>
<Text style={styles.statusText}>: {Platform.OS}</Text>
<Text style={styles.statusText}>HealthKit可用: {state.isAvailable ? '是' : '否'}</Text>
<Text style={styles.statusText}>: {state.isAuthorized ? '是' : '否'}</Text>
<Text style={styles.statusText}>: {state.sleepData.length}</Text>
{state.error && <Text style={styles.errorText}>: {state.error}</Text>}
</View>
{/* 操作按钮 */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, !state.isAvailable && styles.buttonDisabled]}
onPress={handleRequestAuthorization}
disabled={!state.isAvailable || state.loading}
>
<Text style={styles.buttonText}>
{state.loading ? '请求中...' : '请求HealthKit授权'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, (!state.isAuthorized || state.loading) && styles.buttonDisabled]}
onPress={handleGetSleepData}
disabled={!state.isAuthorized || state.loading}
>
<Text style={styles.buttonText}>
{state.loading ? '获取中...' : '获取睡眠数据(7天)'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, (!state.isAuthorized || state.loading) && styles.buttonDisabled]}
onPress={handleGetLastNightSleep}
disabled={!state.isAuthorized || state.loading}
>
<Text style={styles.buttonText}>
{state.loading ? '获取中...' : '获取昨晚睡眠'}
</Text>
</TouchableOpacity>
</View>
{/* 昨晚睡眠数据 */}
{state.lastNightSleep?.hasData && (
<View style={styles.resultContainer}>
<Text style={styles.resultTitle}></Text>
<Text style={styles.resultText}>: {state.lastNightSleep.bedTime} - {state.lastNightSleep.wakeTime}</Text>
<Text style={styles.resultText}>: {state.lastNightSleep.totalDurationFormatted}</Text>
<Text style={styles.resultText}>: {state.lastNightSleep.samples.length}</Text>
</View>
)}
{/* 睡眠数据列表 */}
{state.sleepData.length > 0 && (
<View style={styles.dataContainer}>
<Text style={styles.dataTitle}> ({state.sleepData.length})</Text>
{state.sleepData.slice(0, 10).map(renderSleepSample)}
{state.sleepData.length > 10 && (
<Text style={styles.moreText}> {state.sleepData.length - 10} ...</Text>
)}
</View>
)}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
color: '#333',
},
statusContainer: {
backgroundColor: 'white',
padding: 16,
borderRadius: 8,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
statusTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8,
color: '#333',
},
statusText: {
fontSize: 14,
marginBottom: 4,
color: '#666',
},
errorText: {
fontSize: 14,
color: '#e74c3c',
marginTop: 8,
},
buttonContainer: {
marginBottom: 16,
},
button: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 8,
marginBottom: 12,
alignItems: 'center',
},
buttonDisabled: {
backgroundColor: '#ccc',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
resultContainer: {
backgroundColor: 'white',
padding: 16,
borderRadius: 8,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
resultTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8,
color: '#333',
},
resultText: {
fontSize: 14,
marginBottom: 4,
color: '#666',
},
dataContainer: {
backgroundColor: 'white',
padding: 16,
borderRadius: 8,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
dataTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 12,
color: '#333',
},
sampleItem: {
backgroundColor: '#f8f9fa',
padding: 12,
borderRadius: 6,
marginBottom: 8,
borderLeftWidth: 3,
borderLeftColor: '#007AFF',
},
sampleTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
color: '#333',
},
sampleText: {
fontSize: 12,
marginBottom: 2,
color: '#666',
},
moreText: {
textAlign: 'center',
fontSize: 14,
color: '#999',
fontStyle: 'italic',
marginTop: 8,
},
});
export default HealthKitTest;