feat: 支持 healthkit
This commit is contained in:
383
components/HealthKitTest.tsx
Normal file
383
components/HealthKitTest.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user