383 lines
11 KiB
TypeScript
383 lines
11 KiB
TypeScript
/**
|
||
* 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; |