feat(health): 新增手腕温度监测和经期双向同步功能
新增手腕温度健康数据追踪,支持Apple Watch睡眠手腕温度数据展示和30天历史趋势分析 实现经期数据与HealthKit的完整双向同步,支持读取、写入和删除经期记录 优化经期预测算法,基于历史数据计算更准确的周期和排卵日预测 重构经期UI组件为模块化结构,提升代码可维护性 添加完整的中英文国际化支持,覆盖所有新增功能界面
This commit is contained in:
178
utils/health.ts
178
utils/health.ts
@@ -2,6 +2,7 @@ import { CompleteSleepData, fetchCompleteSleepData } from '@/utils/sleepHealthKi
|
||||
import dayjs from 'dayjs';
|
||||
import { AppState, AppStateStatus, NativeModules } from 'react-native';
|
||||
import i18n from '../i18n';
|
||||
import { logger } from './logger';
|
||||
import { SimpleEventEmitter } from './SimpleEventEmitter';
|
||||
|
||||
type HealthDataOptions = {
|
||||
@@ -343,6 +344,19 @@ export type TodayHealthData = {
|
||||
heartRate: number | null;
|
||||
};
|
||||
|
||||
export type MenstrualFlowSample = {
|
||||
id: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
value: number; // 1=unspecified, 2=light, 3=medium, 4=heavy, 5=none
|
||||
isStart: boolean;
|
||||
source: {
|
||||
name: string;
|
||||
bundleIdentifier: string;
|
||||
};
|
||||
metadata: Record<string, any>;
|
||||
};
|
||||
|
||||
// 更新:使用新的权限管理系统
|
||||
export async function ensureHealthPermissions(): Promise<boolean> {
|
||||
return await healthPermissionManager.requestPermission();
|
||||
@@ -370,15 +384,15 @@ function createDateRange(date: Date): HealthDataOptions {
|
||||
|
||||
// 通用错误处理
|
||||
function logError(operation: string, error: any): void {
|
||||
console.error(`获取${operation}失败:`, error);
|
||||
logger.error(`获取${operation}失败`, error);
|
||||
}
|
||||
|
||||
function logWarning(operation: string, message: string): void {
|
||||
console.warn(`${operation}数据${message}`);
|
||||
logger.warn(`${operation}数据${message}`);
|
||||
}
|
||||
|
||||
function logSuccess(operation: string, data: any): void {
|
||||
console.log(`${operation}数据:`, data);
|
||||
logger.info(`${operation}数据`, data);
|
||||
}
|
||||
|
||||
// 数值验证和转换
|
||||
@@ -784,6 +798,87 @@ export async function fetchOxygenSaturation(options: HealthDataOptions): Promise
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchWristTemperature(options: HealthDataOptions, targetDate?: Date): Promise<number | null> {
|
||||
try {
|
||||
const result = await HealthKitManager.getWristTemperatureSamples(options);
|
||||
|
||||
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
|
||||
logSuccess('手腕温度', result);
|
||||
|
||||
const samples = result.data as Array<{ endDate?: string; value?: number }>;
|
||||
const dayStart = targetDate ? dayjs(targetDate).startOf('day') : dayjs(options.startDate);
|
||||
const dayEnd = targetDate ? dayjs(targetDate).endOf('day') : dayjs(options.endDate);
|
||||
|
||||
const sampleForSelectedDay = samples.find((sample) => {
|
||||
const sampleEnd = dayjs(sample.endDate);
|
||||
return sampleEnd.isValid() && !sampleEnd.isBefore(dayStart) && !sampleEnd.isAfter(dayEnd);
|
||||
});
|
||||
|
||||
const sampleToUse = sampleForSelectedDay ?? samples[samples.length - 1];
|
||||
|
||||
if (sampleToUse?.value !== undefined) {
|
||||
return Number(Number(sampleToUse.value).toFixed(1));
|
||||
}
|
||||
|
||||
logWarning('手腕温度', '未找到有效的温度值');
|
||||
return null;
|
||||
} else {
|
||||
logWarning('手腕温度', '为空或格式错误');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logError('手腕温度', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface WristTemperatureHistoryPoint {
|
||||
date: string;
|
||||
endDate: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export async function fetchWristTemperatureHistory(options: HealthDataOptions): Promise<WristTemperatureHistoryPoint[]> {
|
||||
try {
|
||||
const result = await HealthKitManager.getWristTemperatureSamples(options);
|
||||
|
||||
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
|
||||
logSuccess('手腕温度历史', result);
|
||||
|
||||
const samples = result.data as Array<{ endDate?: string; value?: number }>;
|
||||
const dailyLatest: Record<string, WristTemperatureHistoryPoint> = {};
|
||||
|
||||
samples.forEach((sample) => {
|
||||
if (!sample?.endDate || sample.value === undefined) return;
|
||||
|
||||
const end = dayjs(sample.endDate);
|
||||
if (!end.isValid()) return;
|
||||
|
||||
const dayKey = end.format('YYYY-MM-DD');
|
||||
const numericValue = Number(sample.value);
|
||||
if (Number.isNaN(numericValue)) return;
|
||||
|
||||
const existing = dailyLatest[dayKey];
|
||||
if (!existing || end.isAfter(dayjs(existing.endDate))) {
|
||||
dailyLatest[dayKey] = {
|
||||
date: dayKey,
|
||||
endDate: sample.endDate,
|
||||
value: Number(numericValue.toFixed(2)),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(dailyLatest).sort((a, b) => dayjs(a.date).diff(dayjs(b.date)));
|
||||
} else {
|
||||
logWarning('手腕温度历史', '为空或格式错误');
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
logError('手腕温度历史', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchHeartRateSamplesForRange(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
@@ -1493,6 +1588,83 @@ export async function fetchHourlyStandHoursForDate(date: Date): Promise<HourlySt
|
||||
export { fetchCompleteSleepData };
|
||||
export type { CompleteSleepData };
|
||||
|
||||
// === 经期数据相关方法 ===
|
||||
|
||||
export async function fetchMenstrualFlowSamples(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
limit: number = 100
|
||||
): Promise<MenstrualFlowSample[]> {
|
||||
try {
|
||||
const options = {
|
||||
startDate: dayjs(startDate).startOf('day').toISOString(),
|
||||
endDate: dayjs(endDate).endOf('day').toISOString(),
|
||||
limit,
|
||||
};
|
||||
|
||||
const result = await HealthKitManager.getMenstrualFlowSamples(options);
|
||||
|
||||
if (result && Array.isArray(result.data)) {
|
||||
logSuccess('经期数据', result);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
logWarning('经期数据', '为空或格式错误');
|
||||
return [];
|
||||
} catch (error) {
|
||||
logError('经期数据', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveMenstrualFlow(
|
||||
date: Date,
|
||||
value: number = 1, // Default to unspecified
|
||||
isStart: boolean = false
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const options = {
|
||||
date: dayjs(date).toISOString(),
|
||||
value,
|
||||
isStart,
|
||||
};
|
||||
|
||||
const result = await HealthKitManager.saveMenstrualFlow(options);
|
||||
if (result && result.success) {
|
||||
console.log('经期数据保存成功');
|
||||
return true;
|
||||
}
|
||||
console.error('经期数据保存失败');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('保存经期数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMenstrualFlow(
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const options = {
|
||||
startDate: dayjs(startDate).startOf('day').toISOString(),
|
||||
endDate: dayjs(endDate).endOf('day').toISOString(),
|
||||
};
|
||||
|
||||
const result = await HealthKitManager.deleteMenstrualFlow(options);
|
||||
if (result && result.success) {
|
||||
console.log(`经期数据删除成功,数量: ${result.count}`);
|
||||
return true;
|
||||
}
|
||||
console.error('经期数据删除失败');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('删除经期数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 专门为活动圆环详情页获取精简的数据
|
||||
export async function fetchActivityRingsForDate(date: Date): Promise<ActivityRingsData | null> {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { MenstrualFlowSample } from './health';
|
||||
|
||||
export type MenstrualDayStatus = 'period' | 'predicted-period' | 'fertile' | 'ovulation-day';
|
||||
|
||||
@@ -54,18 +55,42 @@ const STATUS_PRIORITY: Record<MenstrualDayStatus, number> = {
|
||||
|
||||
export const DEFAULT_CYCLE_LENGTH = 28;
|
||||
export const DEFAULT_PERIOD_LENGTH = 5;
|
||||
const MIN_CYCLE_LENGTH = 21;
|
||||
const MAX_CYCLE_LENGTH = 45;
|
||||
const LOOKBACK_WINDOW = 6;
|
||||
const LUTEAL_PHASE_MEAN = 13; // 12–14 天之间较为稳定
|
||||
|
||||
export const createDefaultRecords = (): CycleRecord[] => {
|
||||
const today = dayjs();
|
||||
const latestStart = today.subtract(4, 'day'); // 默认让今天处于经期第5天
|
||||
const previousStart = latestStart.subtract(DEFAULT_CYCLE_LENGTH, 'day');
|
||||
const olderStart = previousStart.subtract(DEFAULT_CYCLE_LENGTH, 'day');
|
||||
const clampCycleLength = (value: number, fallback: number) => {
|
||||
if (!Number.isFinite(value) || value <= 0) return fallback;
|
||||
return Math.max(MIN_CYCLE_LENGTH, Math.min(MAX_CYCLE_LENGTH, Math.round(value)));
|
||||
};
|
||||
|
||||
return [
|
||||
{ startDate: olderStart.format('YYYY-MM-DD'), periodLength: DEFAULT_PERIOD_LENGTH },
|
||||
{ startDate: previousStart.format('YYYY-MM-DD'), periodLength: DEFAULT_PERIOD_LENGTH },
|
||||
{ startDate: latestStart.format('YYYY-MM-DD'), periodLength: DEFAULT_PERIOD_LENGTH },
|
||||
];
|
||||
const calcMedian = (values: number[]) => {
|
||||
if (!values.length) return undefined;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
if (sorted.length % 2 === 0) {
|
||||
return (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
}
|
||||
return sorted[mid];
|
||||
};
|
||||
|
||||
const calcTrimmedMean = (values: number[], trim = 1) => {
|
||||
if (!values.length) return undefined;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const start = Math.min(trim, sorted.length);
|
||||
const end = Math.max(sorted.length - trim, start);
|
||||
const sliced = sorted.slice(start, end);
|
||||
if (!sliced.length) return undefined;
|
||||
return sliced.reduce((sum, cur) => sum + cur, 0) / sliced.length;
|
||||
};
|
||||
|
||||
const calcStdDev = (values: number[]) => {
|
||||
if (values.length < 2) return 0;
|
||||
const mean = values.reduce((s, v) => s + v, 0) / values.length;
|
||||
const variance =
|
||||
values.reduce((s, v) => s + (v - mean) * (v - mean), 0) / (values.length - 1);
|
||||
return Math.sqrt(variance);
|
||||
};
|
||||
|
||||
const calcAverageCycleLength = (records: CycleRecord[], fallback = DEFAULT_CYCLE_LENGTH) => {
|
||||
@@ -81,8 +106,11 @@ const calcAverageCycleLength = (records: CycleRecord[], fallback = DEFAULT_CYCLE
|
||||
}
|
||||
}
|
||||
if (!intervals.length) return fallback;
|
||||
const avg = intervals.reduce((sum, cur) => sum + cur, 0) / intervals.length;
|
||||
return Math.round(avg);
|
||||
const recent = intervals.slice(-LOOKBACK_WINDOW);
|
||||
const median = calcMedian(recent);
|
||||
const trimmed = calcTrimmedMean(recent);
|
||||
const blended = median ?? trimmed ?? calcTrimmedMean(intervals) ?? fallback;
|
||||
return clampCycleLength(blended, fallback);
|
||||
};
|
||||
|
||||
const calcAveragePeriodLength = (records: CycleRecord[], fallback = DEFAULT_PERIOD_LENGTH) => {
|
||||
@@ -90,8 +118,11 @@ const calcAveragePeriodLength = (records: CycleRecord[], fallback = DEFAULT_PERI
|
||||
.map((r) => r.periodLength)
|
||||
.filter((l): l is number => typeof l === 'number' && l > 0);
|
||||
if (!lengths.length) return fallback;
|
||||
const avg = lengths.reduce((sum, cur) => sum + cur, 0) / lengths.length;
|
||||
return Math.round(avg);
|
||||
const recent = lengths.slice(-LOOKBACK_WINDOW);
|
||||
const median = calcMedian(recent);
|
||||
const trimmed = calcTrimmedMean(recent);
|
||||
const blended = median ?? trimmed ?? calcTrimmedMean(lengths) ?? fallback;
|
||||
return Math.max(3, Math.round(blended)); // 极端短周期时仍保持生理期合理下限
|
||||
};
|
||||
|
||||
const addDayInfo = (
|
||||
@@ -110,9 +141,10 @@ const addDayInfo = (
|
||||
};
|
||||
|
||||
const getOvulationDay = (cycleStart: Dayjs, cycleLength: number) => {
|
||||
// 默认排卵日位于周期的中间偏后,兼容短/长周期
|
||||
const daysFromStart = Math.max(12, Math.round(cycleLength / 2));
|
||||
return cycleStart.add(daysFromStart, 'day');
|
||||
// 排卵日约在下次月经前 12-14 天,较稳定;对极端周期做边界约束
|
||||
const daysFromStart = cycleLength - LUTEAL_PHASE_MEAN;
|
||||
const offset = Math.min(Math.max(daysFromStart, 11), 16);
|
||||
return cycleStart.add(offset, 'day');
|
||||
};
|
||||
|
||||
export const buildMenstrualTimeline = (options?: {
|
||||
@@ -136,6 +168,23 @@ export const buildMenstrualTimeline = (options?: {
|
||||
options?.defaultCycleLength ?? calcAverageCycleLength(records, DEFAULT_CYCLE_LENGTH);
|
||||
const avgPeriodLength =
|
||||
options?.defaultPeriodLength ?? calcAveragePeriodLength(records, DEFAULT_PERIOD_LENGTH);
|
||||
const sortedStarts = [...records]
|
||||
.sort((a, b) => dayjs(a.startDate).valueOf() - dayjs(b.startDate).valueOf())
|
||||
.map((r) => dayjs(r.startDate));
|
||||
const cycleIntervals: number[] = [];
|
||||
for (let i = 1; i < sortedStarts.length; i += 1) {
|
||||
const diff = sortedStarts[i].diff(sortedStarts[i - 1], 'day');
|
||||
if (diff > 0) cycleIntervals.push(diff);
|
||||
}
|
||||
const recentIntervals = cycleIntervals.slice(-LOOKBACK_WINDOW);
|
||||
const cycleVariability = calcStdDev(recentIntervals);
|
||||
const lastInterval =
|
||||
recentIntervals.length > 0 ? recentIntervals[recentIntervals.length - 1] : avgCycleLength;
|
||||
// 对未来预测使用稳健均值 + 最新趋势的折中,以避免单次异常牵动过大
|
||||
const predictedCycleLength = clampCycleLength(
|
||||
0.55 * avgCycleLength + 0.45 * lastInterval,
|
||||
avgCycleLength
|
||||
);
|
||||
|
||||
const cycles = records.map((record) => ({
|
||||
start: dayjs(record.startDate),
|
||||
@@ -144,19 +193,36 @@ export const buildMenstrualTimeline = (options?: {
|
||||
cycleLength: record.cycleLength ?? avgCycleLength,
|
||||
}));
|
||||
|
||||
// 基于真实相邻开始日期矫正已记录周期长度,便于后续计算排卵日/易孕期
|
||||
for (let i = 0; i < cycles.length; i += 1) {
|
||||
const next = cycles[i + 1];
|
||||
if (next) {
|
||||
const interval = next.start.diff(cycles[i].start, 'day');
|
||||
if (interval > 0) {
|
||||
cycles[i].cycleLength = clampCycleLength(interval, cycles[i].cycleLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只有当存在历史记录时,才进行后续预测
|
||||
if (cycles.length > 0) {
|
||||
const lastConfirmed = cycles[cycles.length - 1];
|
||||
let cursorStart = lastConfirmed.start;
|
||||
let nextCycleLength = predictedCycleLength;
|
||||
|
||||
while (cursorStart.isBefore(endMonth)) {
|
||||
cursorStart = cursorStart.add(avgCycleLength, 'day');
|
||||
cursorStart = cursorStart.add(nextCycleLength, 'day');
|
||||
cycles.push({
|
||||
start: cursorStart,
|
||||
confirmed: false,
|
||||
periodLength: avgPeriodLength,
|
||||
cycleLength: avgCycleLength,
|
||||
cycleLength: nextCycleLength,
|
||||
});
|
||||
// 趋势逐步回归稳健均值,避免长期漂移
|
||||
nextCycleLength = clampCycleLength(
|
||||
Math.round(nextCycleLength * 0.65 + avgCycleLength * 0.35),
|
||||
avgCycleLength
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +230,9 @@ export const buildMenstrualTimeline = (options?: {
|
||||
|
||||
cycles.forEach((cycle) => {
|
||||
const ovulationDay = getOvulationDay(cycle.start, cycle.cycleLength);
|
||||
const fertileStart = ovulationDay.subtract(5, 'day');
|
||||
// 经前 luteal 稳定,易孕窗口 5-7 天:排卵日前 5 天 + 排卵日当日;高波动人群额外向前扩 1 天
|
||||
const fertileWindow = Math.min(7, 6 + (cycle.confirmed ? 0 : Math.round(cycleVariability)));
|
||||
const fertileStart = ovulationDay.subtract(fertileWindow, 'day');
|
||||
|
||||
for (let i = 0; i < cycle.periodLength; i += 1) {
|
||||
const date = cycle.start.add(i, 'day');
|
||||
@@ -177,7 +245,7 @@ export const buildMenstrualTimeline = (options?: {
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
for (let i = 0; i < fertileWindow; i += 1) {
|
||||
const date = fertileStart.add(i, 'day');
|
||||
if (date.isBefore(startMonth) || date.isAfter(endMonth)) continue;
|
||||
addDayInfo(dayMap, date, {
|
||||
@@ -245,7 +313,7 @@ export const buildMenstrualTimeline = (options?: {
|
||||
return {
|
||||
months,
|
||||
dayMap,
|
||||
cycleLength: avgCycleLength,
|
||||
cycleLength: predictedCycleLength,
|
||||
periodLength: avgPeriodLength,
|
||||
todayInfo: dayMap[todayKey],
|
||||
};
|
||||
@@ -258,3 +326,62 @@ export const getMenstrualSummaryForDate = (
|
||||
const key = date.format('YYYY-MM-DD');
|
||||
return dayMap[key];
|
||||
};
|
||||
|
||||
export const convertHealthKitSamplesToCycleRecords = (
|
||||
samples: MenstrualFlowSample[]
|
||||
): CycleRecord[] => {
|
||||
if (!samples.length) return [];
|
||||
|
||||
// 1. Sort samples by date
|
||||
const sortedSamples = [...samples].sort(
|
||||
(a, b) => dayjs(a.startDate).valueOf() - dayjs(b.startDate).valueOf()
|
||||
);
|
||||
|
||||
const records: CycleRecord[] = [];
|
||||
let currentStart: Dayjs | null = null;
|
||||
let currentEnd: Dayjs | null = null;
|
||||
|
||||
// 2. Iterate and merge consecutive days
|
||||
for (const sample of sortedSamples) {
|
||||
const sampleDate = dayjs(sample.startDate);
|
||||
|
||||
// If we have no current period being tracked, start a new one
|
||||
if (!currentStart) {
|
||||
currentStart = sampleDate;
|
||||
currentEnd = sampleDate;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this sample is contiguous with the current period
|
||||
// Allow 1 day gap? Standard logic usually assumes contiguous days for a single period.
|
||||
// However, spotting might cause gaps. For now, we'll merge if gap <= 1 day.
|
||||
const diff = sampleDate.diff(currentEnd, 'day');
|
||||
|
||||
if (diff <= 1) {
|
||||
// Extend current period
|
||||
currentEnd = sampleDate;
|
||||
} else {
|
||||
// Gap is too large, finalize current period and start new one
|
||||
if (currentStart && currentEnd) {
|
||||
records.push({
|
||||
startDate: currentStart.format('YYYY-MM-DD'),
|
||||
periodLength: currentEnd.diff(currentStart, 'day') + 1,
|
||||
source: 'healthkit',
|
||||
});
|
||||
}
|
||||
currentStart = sampleDate;
|
||||
currentEnd = sampleDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Push the last record
|
||||
if (currentStart && currentEnd) {
|
||||
records.push({
|
||||
startDate: currentStart.format('YYYY-MM-DD'),
|
||||
periodLength: currentEnd.diff(currentStart, 'day') + 1,
|
||||
source: 'healthkit',
|
||||
});
|
||||
}
|
||||
|
||||
return records;
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ const PREFERENCES_KEYS = {
|
||||
SHOW_MENSTRUAL_CYCLE_CARD: 'user_preference_show_menstrual_cycle_card',
|
||||
SHOW_WEIGHT_CARD: 'user_preference_show_weight_card',
|
||||
SHOW_CIRCUMFERENCE_CARD: 'user_preference_show_circumference_card',
|
||||
SHOW_WRIST_TEMPERATURE_CARD: 'user_preference_show_wrist_temperature_card',
|
||||
|
||||
// 首页身体指标卡片排序设置
|
||||
STATISTICS_CARD_ORDER: 'user_preference_statistics_card_order',
|
||||
@@ -46,6 +47,7 @@ export interface StatisticsCardsVisibility {
|
||||
showMenstrualCycle: boolean;
|
||||
showWeight: boolean;
|
||||
showCircumference: boolean;
|
||||
showWristTemperature: boolean;
|
||||
}
|
||||
|
||||
// 默认卡片顺序
|
||||
@@ -58,6 +60,7 @@ export const DEFAULT_CARD_ORDER: string[] = [
|
||||
'water',
|
||||
'metabolism',
|
||||
'oxygen',
|
||||
'temperature',
|
||||
'menstrual',
|
||||
'weight',
|
||||
'circumference',
|
||||
@@ -109,6 +112,7 @@ const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
showMenstrualCycle: true,
|
||||
showWeight: true,
|
||||
showCircumference: true,
|
||||
showWristTemperature: true,
|
||||
|
||||
// 默认卡片顺序
|
||||
cardOrder: DEFAULT_CARD_ORDER,
|
||||
@@ -145,6 +149,7 @@ export const getUserPreferences = async (): Promise<UserPreferences> => {
|
||||
const showMenstrualCycle = await AsyncStorage.getItem(PREFERENCES_KEYS.SHOW_MENSTRUAL_CYCLE_CARD);
|
||||
const showWeight = await AsyncStorage.getItem(PREFERENCES_KEYS.SHOW_WEIGHT_CARD);
|
||||
const showCircumference = await AsyncStorage.getItem(PREFERENCES_KEYS.SHOW_CIRCUMFERENCE_CARD);
|
||||
const showWristTemperature = await AsyncStorage.getItem(PREFERENCES_KEYS.SHOW_WRIST_TEMPERATURE_CARD);
|
||||
const cardOrderStr = await AsyncStorage.getItem(PREFERENCES_KEYS.STATISTICS_CARD_ORDER);
|
||||
const cardOrder = cardOrderStr ? JSON.parse(cardOrderStr) : DEFAULT_PREFERENCES.cardOrder;
|
||||
|
||||
@@ -174,6 +179,7 @@ export const getUserPreferences = async (): Promise<UserPreferences> => {
|
||||
showMenstrualCycle: showMenstrualCycle !== null ? showMenstrualCycle === 'true' : DEFAULT_PREFERENCES.showMenstrualCycle,
|
||||
showWeight: showWeight !== null ? showWeight === 'true' : DEFAULT_PREFERENCES.showWeight,
|
||||
showCircumference: showCircumference !== null ? showCircumference === 'true' : DEFAULT_PREFERENCES.showCircumference,
|
||||
showWristTemperature: showWristTemperature !== null ? showWristTemperature === 'true' : DEFAULT_PREFERENCES.showWristTemperature,
|
||||
cardOrder,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -611,6 +617,7 @@ export const getStatisticsCardsVisibility = async (): Promise<StatisticsCardsVis
|
||||
showMenstrualCycle: userPreferences.showMenstrualCycle,
|
||||
showWeight: userPreferences.showWeight,
|
||||
showCircumference: userPreferences.showCircumference,
|
||||
showWristTemperature: userPreferences.showWristTemperature,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取首页卡片显示设置失败:', error);
|
||||
@@ -626,6 +633,7 @@ export const getStatisticsCardsVisibility = async (): Promise<StatisticsCardsVis
|
||||
showMenstrualCycle: DEFAULT_PREFERENCES.showMenstrualCycle,
|
||||
showWeight: DEFAULT_PREFERENCES.showWeight,
|
||||
showCircumference: DEFAULT_PREFERENCES.showCircumference,
|
||||
showWristTemperature: DEFAULT_PREFERENCES.showWristTemperature,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -673,6 +681,7 @@ export const setStatisticsCardVisibility = async (key: keyof StatisticsCardsVisi
|
||||
case 'showMenstrualCycle': storageKey = PREFERENCES_KEYS.SHOW_MENSTRUAL_CYCLE_CARD; break;
|
||||
case 'showWeight': storageKey = PREFERENCES_KEYS.SHOW_WEIGHT_CARD; break;
|
||||
case 'showCircumference': storageKey = PREFERENCES_KEYS.SHOW_CIRCUMFERENCE_CARD; break;
|
||||
case 'showWristTemperature': storageKey = PREFERENCES_KEYS.SHOW_WRIST_TEMPERATURE_CARD; break;
|
||||
default: return;
|
||||
}
|
||||
await AsyncStorage.setItem(storageKey, value.toString());
|
||||
|
||||
Reference in New Issue
Block a user