feat(fasting): 新增轻断食功能模块
新增完整的轻断食功能,包括: - 断食计划列表和详情页面,支持12-12、14-10、16-8、18-6四种计划 - 断食状态实时追踪和倒计时显示 - 自定义开始时间选择器 - 断食通知提醒功能 - Redux状态管理和数据持久化 - 新增tab导航入口和路由配置
This commit is contained in:
191
utils/fasting.ts
Normal file
191
utils/fasting.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import { FASTING_STORAGE_KEYS } from '@/constants/Fasting';
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
|
||||
export type FastingPhase = 'upcoming' | 'fasting' | 'completed';
|
||||
|
||||
export const calculateFastingWindow = (start: Date, fastingHours: number) => {
|
||||
const startDate = dayjs(start).second(0).millisecond(0);
|
||||
const endDate = startDate.add(fastingHours, 'hour');
|
||||
|
||||
return {
|
||||
start: startDate.toDate(),
|
||||
end: endDate.toDate(),
|
||||
};
|
||||
};
|
||||
|
||||
export const getFastingPhase = (start?: Date | null, end?: Date | null, now: Date = new Date()): FastingPhase => {
|
||||
if (!start || !end) {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
const nowJs = dayjs(now);
|
||||
const startJs = dayjs(start);
|
||||
const endJs = dayjs(end);
|
||||
|
||||
if (nowJs.isBefore(startJs)) {
|
||||
return 'upcoming';
|
||||
}
|
||||
if (nowJs.isSameOrAfter(startJs) && nowJs.isBefore(endJs)) {
|
||||
return 'fasting';
|
||||
}
|
||||
return 'completed';
|
||||
};
|
||||
|
||||
export const getPhaseLabel = (phase: FastingPhase) => {
|
||||
switch (phase) {
|
||||
case 'fasting':
|
||||
return '断食中';
|
||||
case 'upcoming':
|
||||
return '可饮食';
|
||||
case 'completed':
|
||||
return '可饮食';
|
||||
default:
|
||||
return '可饮食';
|
||||
}
|
||||
};
|
||||
|
||||
export const formatDayDescriptor = (date: Date | null | undefined, now: Date = new Date()) => {
|
||||
if (!date) return '--';
|
||||
|
||||
const target = dayjs(date);
|
||||
const today = dayjs(now).startOf('day');
|
||||
|
||||
if (target.isSame(today, 'day')) {
|
||||
return '今天';
|
||||
}
|
||||
if (target.isSame(today.subtract(1, 'day'), 'day')) {
|
||||
return '昨天';
|
||||
}
|
||||
if (target.isSame(today.add(1, 'day'), 'day')) {
|
||||
return '明天';
|
||||
}
|
||||
return target.format('MM-DD');
|
||||
};
|
||||
|
||||
export const formatTime = (date: Date | null | undefined) => {
|
||||
if (!date) return '--:--';
|
||||
return dayjs(date).format('HH:mm');
|
||||
};
|
||||
|
||||
export const formatCountdown = (target: Date | null | undefined, now: Date = new Date()) => {
|
||||
if (!target) return '--:--:--';
|
||||
|
||||
const diff = dayjs(target).diff(now);
|
||||
if (diff <= 0) {
|
||||
return '00:00:00';
|
||||
}
|
||||
|
||||
const dur = dayjs.duration(diff);
|
||||
const hours = String(Math.floor(dur.asHours())).padStart(2, '0');
|
||||
const minutes = String(dur.minutes()).padStart(2, '0');
|
||||
const seconds = String(dur.seconds()).padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
export const buildDisplayWindow = (start?: Date | null, end?: Date | null) => {
|
||||
return {
|
||||
startDayLabel: formatDayDescriptor(start ?? null),
|
||||
startTimeLabel: formatTime(start ?? null),
|
||||
endDayLabel: formatDayDescriptor(end ?? null),
|
||||
endTimeLabel: formatTime(end ?? null),
|
||||
};
|
||||
};
|
||||
|
||||
export const loadPreferredPlanId = async (): Promise<string | null> => {
|
||||
try {
|
||||
return await AsyncStorage.getItem(FASTING_STORAGE_KEYS.preferredPlanId);
|
||||
} catch (error) {
|
||||
console.warn('加载断食首选计划ID失败', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const savePreferredPlanId = async (planId: string) => {
|
||||
try {
|
||||
await AsyncStorage.setItem(FASTING_STORAGE_KEYS.preferredPlanId, planId);
|
||||
} catch (error) {
|
||||
console.warn('保存断食首选计划ID失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
export type FastingNotificationIds = {
|
||||
startId?: string | null;
|
||||
endId?: string | null;
|
||||
};
|
||||
|
||||
export const getFastingNotificationsRegistered = async (): Promise<boolean> => {
|
||||
try {
|
||||
const value = await AsyncStorage.getItem(FASTING_STORAGE_KEYS.notificationsRegistered);
|
||||
return value === 'true';
|
||||
} catch (error) {
|
||||
console.warn('读取断食通知注册状态失败', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const setFastingNotificationsRegistered = async (registered: boolean) => {
|
||||
try {
|
||||
if (registered) {
|
||||
await AsyncStorage.setItem(FASTING_STORAGE_KEYS.notificationsRegistered, 'true');
|
||||
} else {
|
||||
await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.notificationsRegistered);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('更新断食通知注册状态失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const loadStoredFastingNotificationIds = async (): Promise<FastingNotificationIds> => {
|
||||
try {
|
||||
const [startId, endId] = await Promise.all([
|
||||
AsyncStorage.getItem(FASTING_STORAGE_KEYS.startNotificationId),
|
||||
AsyncStorage.getItem(FASTING_STORAGE_KEYS.endNotificationId),
|
||||
]);
|
||||
|
||||
return {
|
||||
startId: startId ?? undefined,
|
||||
endId: endId ?? undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('读取断食通知ID失败', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const saveFastingNotificationIds = async (ids: FastingNotificationIds) => {
|
||||
try {
|
||||
if (ids.startId) {
|
||||
await AsyncStorage.setItem(FASTING_STORAGE_KEYS.startNotificationId, ids.startId);
|
||||
} else {
|
||||
await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.startNotificationId);
|
||||
}
|
||||
|
||||
if (ids.endId) {
|
||||
await AsyncStorage.setItem(FASTING_STORAGE_KEYS.endNotificationId, ids.endId);
|
||||
} else {
|
||||
await AsyncStorage.removeItem(FASTING_STORAGE_KEYS.endNotificationId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('保存断食通知ID失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearFastingNotificationIds = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
AsyncStorage.removeItem(FASTING_STORAGE_KEYS.startNotificationId),
|
||||
AsyncStorage.removeItem(FASTING_STORAGE_KEYS.endNotificationId),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.warn('清除断食通知ID失败', error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user