Compare commits
No commits in common. "6fd1acc9993ff3a1088d3d80c0127ccdb6bacb44" and "79ef45b4ad819904bf910cec69d960dd64d22210" have entirely different histories.
6fd1acc999
...
79ef45b4ad
67
CLAUDE.md
67
CLAUDE.md
@ -1,67 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
"掐日子" (Qia Rizi) - A Chinese calendar reminder app with anniversaries, reminders, and notes. Built with React 19 + TypeScript + Supabase.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start dev server
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Run linting
|
||||
pnpm lint
|
||||
|
||||
# Preview production build
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Frontend (client/)
|
||||
- **React 19 + TypeScript + Vite** - Core framework
|
||||
- **Mantine UI v8** - Component library with custom Apple-inspired theme
|
||||
- **Zustand** - State management with localStorage persistence
|
||||
- **React Router v7** - Routing
|
||||
- **Supabase** - Backend (auth, database)
|
||||
|
||||
### Key Directories
|
||||
- `src/pages/` - Route pages (HomePage, LoginPage, RegisterPage, SettingsPage, ArchivePage)
|
||||
- `src/components/` - Reusable components, organized by feature
|
||||
- `src/stores/` - Zustand store (useAppStore for global state)
|
||||
- `src/services/` - API layer (supabase.ts, api.ts)
|
||||
- `src/types/` - TypeScript type definitions
|
||||
- `src/utils/` - Business logic utilities (repeatCalculator, countdown)
|
||||
|
||||
### Data Model
|
||||
- **Events**: Anniversary/Reminder items with repeat rules, lunar calendar support
|
||||
- **Notes**: User's personal notes (HTML content)
|
||||
- **AI Conversations**: Chat history with AI event parsing
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### State Management
|
||||
- Use `useAppStore` from `src/stores/index.ts` for global state (auth, events, notes)
|
||||
- Store persists to localStorage via zustand/middleware
|
||||
|
||||
### API Calls
|
||||
- Direct Supabase calls via `services/supabase.ts`
|
||||
- Auth token handling with automatic refresh in store's `checkAuth`
|
||||
|
||||
### Components
|
||||
- Custom date/time pickers in `src/components/common/` (FixedCalendar, WheelTimePicker)
|
||||
- Event cards use context menus from `src/stores/contextMenu.ts`
|
||||
|
||||
## Chinese Localization
|
||||
- App uses Simplified Chinese (zh-cn) throughout
|
||||
- dayjs configured with Chinese locale
|
||||
- Mantine DatesProvider set to Chinese
|
||||
@ -27,7 +27,7 @@ import { ReminderList } from '../components/reminder/ReminderList';
|
||||
import { NoteEditor } from '../components/note/NoteEditor';
|
||||
import { FloatingAIChat } from '../components/ai/FloatingAIChat';
|
||||
import type { Event, EventType, RepeatType, PriorityType } from '../types';
|
||||
import { calculateNextReminderDate, getReminderOptions, getDefaultReminderValue, calculateReminderTimes, getReminderValueFromTimes, formatReminderTimeDisplay } from '../utils/repeatCalculator';
|
||||
import { calculateNextReminderDate } from '../utils/repeatCalculator';
|
||||
|
||||
export function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
@ -54,7 +54,6 @@ export function HomePage() {
|
||||
const [formRepeatType, setFormRepeatType] = useState<RepeatType>('none');
|
||||
const [formIsHoliday, setFormIsHoliday] = useState(false);
|
||||
const [formPriority, setFormPriority] = useState<PriorityType>('none');
|
||||
const [formReminderValue, setFormReminderValue] = useState('0'); // 提醒选项值
|
||||
|
||||
// Initialize auth and data on mount
|
||||
useEffect(() => {
|
||||
@ -82,10 +81,7 @@ export function HomePage() {
|
||||
const eventDate = new Date(event.date);
|
||||
const hours = eventDate.getHours();
|
||||
const minutes = eventDate.getMinutes();
|
||||
const hasTime = hours !== 0 || minutes !== 0;
|
||||
setFormTime(hasTime ? `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}` : '');
|
||||
// 从 reminder_times 反推用户选择的提醒选项值
|
||||
setFormReminderValue(getReminderValueFromTimes(event.reminder_times, event.date, hasTime));
|
||||
setFormTime(hours === 0 && minutes === 0 ? '' : `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`);
|
||||
open();
|
||||
};
|
||||
|
||||
@ -101,7 +97,6 @@ export function HomePage() {
|
||||
setFormRepeatType('none');
|
||||
setFormIsHoliday(type === 'anniversary');
|
||||
setFormPriority('none');
|
||||
setFormReminderValue('0');
|
||||
open();
|
||||
};
|
||||
|
||||
@ -117,15 +112,13 @@ export function HomePage() {
|
||||
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||
|
||||
if (formTime) {
|
||||
// 有时间:构建 UTC 时间格式(用户选择的是本地时间,需要转换为 UTC 存储)
|
||||
const hours = parseInt(formTime.split(':')[0]);
|
||||
const minutes = parseInt(formTime.split(':')[1]);
|
||||
// 先用本地时间创建日期对象,然后获取 UTC 时间
|
||||
const localDate = new Date(year, dateObj.getMonth(), day, hours, minutes);
|
||||
dateStr = localDate.toISOString();
|
||||
// 有时间:构建本地时间格式
|
||||
const hours = String(parseInt(formTime.split(':')[0])).padStart(2, '0');
|
||||
const minutes = String(parseInt(formTime.split(':')[1])).padStart(2, '0');
|
||||
dateStr = `${year}-${month}-${day}T${hours}:${minutes}:00`;
|
||||
} else {
|
||||
// 无时间:使用 UTC 日期格式
|
||||
dateStr = `${year}-${month}-${day}T00:00:00.000Z`;
|
||||
// 无时间:只保存日期部分
|
||||
dateStr = `${year}-${month}-${day}T00:00:00`;
|
||||
}
|
||||
|
||||
// 构建事件数据,确保不包含 undefined 值
|
||||
@ -146,12 +139,6 @@ export function HomePage() {
|
||||
eventData.content = formContent;
|
||||
}
|
||||
|
||||
// 计算提醒时间点(仅对提醒事项)
|
||||
if (formType === 'reminder') {
|
||||
const hasTime = !!formTime;
|
||||
eventData.reminder_times = calculateReminderTimes(dateStr, hasTime, formReminderValue);
|
||||
}
|
||||
|
||||
if (isEdit && selectedEvent) {
|
||||
await updateEventById(selectedEvent.id, eventData);
|
||||
} else {
|
||||
@ -286,7 +273,6 @@ export function HomePage() {
|
||||
setFormRepeatType('none');
|
||||
setFormIsHoliday(false);
|
||||
setFormPriority('none');
|
||||
setFormReminderValue('0');
|
||||
setSelectedEvent(null);
|
||||
setIsEdit(false);
|
||||
};
|
||||
@ -429,13 +415,11 @@ export function HomePage() {
|
||||
]}
|
||||
value={formType}
|
||||
onChange={(value) => value && setFormType(value as EventType)}
|
||||
readOnly={isEdit}
|
||||
rightSectionPointerEvents={isEdit ? 'none' : 'auto'}
|
||||
disabled={isEdit}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 2,
|
||||
background: '#faf9f7',
|
||||
pointerEvents: isEdit ? 'none' : 'auto',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@ -443,13 +427,14 @@ export function HomePage() {
|
||||
{/* Title */}
|
||||
<TextInput
|
||||
label={
|
||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em' }}>
|
||||
标题<Box component="span" c="red">*</Box>
|
||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
||||
标题
|
||||
</Text>
|
||||
}
|
||||
placeholder="输入标题"
|
||||
value={formTitle}
|
||||
onChange={(e) => setFormTitle(e.target.value)}
|
||||
required
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 2,
|
||||
@ -506,6 +491,11 @@ export function HomePage() {
|
||||
position="bottom"
|
||||
withArrow
|
||||
shadow="md"
|
||||
onOpenChange={(opened) => {
|
||||
if (!opened && formDate) {
|
||||
// Keep the date
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
<TextInput
|
||||
@ -608,64 +598,29 @@ export function HomePage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Repeat and Reminder on same row */}
|
||||
<Group gap={12} grow>
|
||||
{/* Repeat type */}
|
||||
<Select
|
||||
label={
|
||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em' }}>
|
||||
重复
|
||||
</Text>
|
||||
}
|
||||
data={[
|
||||
{ value: 'none', label: '不重复' },
|
||||
{ value: 'daily', label: '每天' },
|
||||
{ value: 'weekly', label: '每周' },
|
||||
{ value: 'monthly', label: '每月' },
|
||||
{ value: 'yearly', label: '每年' },
|
||||
]}
|
||||
value={formRepeatType}
|
||||
onChange={(value) => value && setFormRepeatType(value as RepeatType)}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 2,
|
||||
background: '#faf9f7',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Reminder options (only for reminders) */}
|
||||
{formType === 'reminder' && (
|
||||
<Select
|
||||
label={
|
||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em' }}>
|
||||
提醒
|
||||
<Box component="span" size="xs" c="dimmed" ml={4}>
|
||||
{formDate ? formatReminderTimeDisplay(
|
||||
calculateReminderTimes(
|
||||
`${formDate.getFullYear()}-${String(formDate.getMonth() + 1).padStart(2, '0')}-${String(formDate.getDate()).padStart(2, '0')}T${formTime ? formTime : '00:00'}:00`,
|
||||
!!formTime,
|
||||
formReminderValue
|
||||
)
|
||||
) : ''}
|
||||
</Box>
|
||||
</Text>
|
||||
}
|
||||
data={getReminderOptions(!!formTime).map(opt => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
value={formReminderValue}
|
||||
onChange={(value) => value && setFormReminderValue(value)}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 2,
|
||||
background: '#faf9f7',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
{/* Repeat type */}
|
||||
<Select
|
||||
label={
|
||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
||||
重复
|
||||
</Text>
|
||||
}
|
||||
data={[
|
||||
{ value: 'none', label: '不重复' },
|
||||
{ value: 'daily', label: '每天' },
|
||||
{ value: 'weekly', label: '每周' },
|
||||
{ value: 'monthly', label: '每月' },
|
||||
{ value: 'yearly', label: '每年' },
|
||||
]}
|
||||
value={formRepeatType}
|
||||
onChange={(value) => value && setFormRepeatType(value as RepeatType)}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 2,
|
||||
background: '#faf9f7',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Color (only for reminders) */}
|
||||
{formType === 'reminder' && (
|
||||
|
||||
@ -2,7 +2,7 @@ import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import type { User, Event, Note, AIConversation, EventType, RepeatType } from '../types';
|
||||
import { api } from '../services/api';
|
||||
import { calculateNextReminderDate, findNextValidReminderDate, isDuplicateReminder, createNextRecurringEventData } from '../utils/repeatCalculator';
|
||||
import { calculateNextReminderDate, findNextValidReminderDate, isDuplicateReminder } from '../utils/repeatCalculator';
|
||||
|
||||
// 应用设置类型
|
||||
interface AppSettings {
|
||||
@ -171,11 +171,29 @@ export const useAppStore = create<AppState>()(
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
// 使用统一的重复事件创建函数,自动继承所有字段(包括 priority 和 reminder_times)
|
||||
const newEventData = createNextRecurringEventData(
|
||||
currentEvent,
|
||||
nextValidDate
|
||||
// 计算新提醒的 next_reminder_date
|
||||
const newNextReminderDate = calculateNextReminderDate(
|
||||
nextValidDate,
|
||||
currentEvent.repeat_type as RepeatType,
|
||||
currentEvent.repeat_interval
|
||||
);
|
||||
|
||||
// 创建下一周期的新事件 - 确保所有字段都不包含 undefined
|
||||
const newEventData: Record<string, any> = {
|
||||
type: currentEvent.type,
|
||||
title: currentEvent.title,
|
||||
date: nextValidDate,
|
||||
is_lunar: currentEvent.is_lunar ?? false,
|
||||
repeat_type: currentEvent.repeat_type,
|
||||
// 确保 repeat_interval 为 null 而不是 undefined
|
||||
repeat_interval: currentEvent.repeat_interval ?? null,
|
||||
next_reminder_date: newNextReminderDate,
|
||||
is_holiday: currentEvent.is_holiday ?? false,
|
||||
};
|
||||
// 只有当 content 有值时才包含
|
||||
if (currentEvent.content) {
|
||||
newEventData.content = currentEvent.content;
|
||||
}
|
||||
const newEvent = await api.events.create(newEventData);
|
||||
|
||||
// 添加新事件到本地状态
|
||||
|
||||
@ -31,7 +31,6 @@ export interface Event {
|
||||
is_holiday?: boolean; // Only for anniversaries
|
||||
is_completed?: boolean; // Only for reminders
|
||||
priority?: PriorityType; // Priority level for reminders
|
||||
reminder_times?: string[]; // 提醒时间点列表(ISO 时间戳数组)
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { Event, RepeatType, PriorityType, EventType } from '../types';
|
||||
import type { Event, RepeatType } from '../types';
|
||||
|
||||
/**
|
||||
* 计算下一个重复周期日期(单步计算)
|
||||
* @param currentDate 当前日期(ISO 格式)
|
||||
* @param repeatType 重复类型
|
||||
* @param interval 周数间隔(仅 weekly 类型使用,默认1)
|
||||
* @returns 下一次提醒日期(ISO 格式,UTC 时间)
|
||||
* @returns 下一次提醒日期(ISO 格式,本地时间)
|
||||
*/
|
||||
export function calculateNextDueDate(
|
||||
currentDate: string,
|
||||
@ -13,36 +13,36 @@ export function calculateNextDueDate(
|
||||
interval: number = 1
|
||||
): string {
|
||||
const date = new Date(currentDate);
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth();
|
||||
const day = date.getUTCDate();
|
||||
const hours = date.getUTCHours();
|
||||
const minutes = date.getUTCMinutes();
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const day = date.getDate();
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
switch (repeatType) {
|
||||
case 'daily':
|
||||
// 每天:加1天
|
||||
return new Date(Date.UTC(year, month, day + 1, hours, minutes)).toISOString();
|
||||
return new Date(year, month, day + 1, hours, minutes).toISOString();
|
||||
|
||||
case 'weekly':
|
||||
// 每周:加7天 * interval
|
||||
return new Date(Date.UTC(year, month, day + 7 * interval, hours, minutes)).toISOString();
|
||||
return new Date(year, month, day + 7 * interval, hours, minutes).toISOString();
|
||||
|
||||
case 'monthly':
|
||||
// 每月:下月同日
|
||||
const nextMonth = new Date(Date.UTC(year, month + 1, day, hours, minutes));
|
||||
const nextMonth = new Date(year, month + 1, day, hours, minutes);
|
||||
// 处理月末日期(如3月31日 -> 4月30日)
|
||||
if (nextMonth.getUTCDate() !== day) {
|
||||
return new Date(Date.UTC(year, month + 1, 0, hours, minutes)).toISOString();
|
||||
if (nextMonth.getDate() !== day) {
|
||||
return new Date(year, month + 1, 0, hours, minutes).toISOString();
|
||||
}
|
||||
return nextMonth.toISOString();
|
||||
|
||||
case 'yearly':
|
||||
// 每年:明年同日
|
||||
const nextYearDate = new Date(Date.UTC(year + 1, month, day, hours, minutes));
|
||||
const nextYearDate = new Date(year + 1, month, day, hours, minutes);
|
||||
// 处理闰年(如2月29日 -> 2月28日)
|
||||
if (nextYearDate.getUTCMonth() !== month) {
|
||||
return new Date(Date.UTC(year + 1, month, 0, hours, minutes)).toISOString();
|
||||
if (nextYearDate.getMonth() !== month) {
|
||||
return new Date(year + 1, month, 0, hours, minutes).toISOString();
|
||||
}
|
||||
return nextYearDate.toISOString();
|
||||
|
||||
@ -231,286 +231,3 @@ export function formatDateDisplay(dateStr: string, showTime: boolean = true): st
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 提醒时间计算相关类型和函数
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* 提醒选项类型(无时间时的选项)
|
||||
*/
|
||||
export type ReminderOptionNoTime = {
|
||||
type: 'no_time';
|
||||
label: string; // 显示文本,如 "当日(9:00)"
|
||||
value: string; // 值,如 "0" 表示当天
|
||||
hours: number; // 提醒小时(9)
|
||||
days: number; // 提前天数(0表示当天)
|
||||
};
|
||||
|
||||
/**
|
||||
* 提醒选项类型(有时时的选项)
|
||||
*/
|
||||
export type ReminderOptionWithTime = {
|
||||
type: 'with_time';
|
||||
label: string; // 显示文本,如 "准时"
|
||||
value: string; // 值,如 "0" 表示准时
|
||||
minutes: number; // 提前分钟数(0表示准时)
|
||||
};
|
||||
|
||||
/**
|
||||
* 无时间时的提醒选项
|
||||
*/
|
||||
export const REMINDER_OPTIONS_NO_TIME: ReminderOptionNoTime[] = [
|
||||
{ type: 'no_time', label: '当日(9:00)', value: '0', hours: 9, days: 0 },
|
||||
{ type: 'no_time', label: '提前1天(9:00)', value: '1', hours: 9, days: 1 },
|
||||
{ type: 'no_time', label: '提前2天(9:00)', value: '2', hours: 9, days: 2 },
|
||||
{ type: 'no_time', label: '提前3天(9:00)', value: '3', hours: 9, days: 3 },
|
||||
{ type: 'no_time', label: '提前1周(9:00)', value: '7', hours: 9, days: 7 },
|
||||
];
|
||||
|
||||
/**
|
||||
* 有时间时的提醒选项
|
||||
*/
|
||||
export const REMINDER_OPTIONS_WITH_TIME: ReminderOptionWithTime[] = [
|
||||
{ type: 'with_time', label: '准时', value: '0', minutes: 0 },
|
||||
{ type: 'with_time', label: '提前5分钟', value: '5', minutes: 5 },
|
||||
{ type: 'with_time', label: '提前15分钟', value: '15', minutes: 15 },
|
||||
{ type: 'with_time', label: '提前1小时', value: '60', minutes: 60 },
|
||||
{ type: 'with_time', label: '提前1天', value: '1440', minutes: 1440 },
|
||||
];
|
||||
|
||||
/**
|
||||
* 计算提醒时间点
|
||||
* @param eventDateStr 事件日期(ISO格式,包含或不包含时间)
|
||||
* @param hasTime 是否有设置时间
|
||||
* @param reminderValue 提醒选项的值
|
||||
* @returns 提醒时间点(ISO格式字符串数组)
|
||||
*/
|
||||
export function calculateReminderTimes(
|
||||
eventDateStr: string,
|
||||
hasTime: boolean,
|
||||
reminderValue: string
|
||||
): string[] {
|
||||
const eventDate = new Date(eventDateStr);
|
||||
const reminderTimes: string[] = [];
|
||||
|
||||
if (!hasTime) {
|
||||
// 无时间:根据选项计算提前天数,使用 UTC 时间避免时区问题
|
||||
const option = REMINDER_OPTIONS_NO_TIME.find(opt => opt.value === reminderValue);
|
||||
if (option) {
|
||||
const reminderDate = new Date(Date.UTC(
|
||||
eventDate.getUTCFullYear(),
|
||||
eventDate.getUTCMonth(),
|
||||
eventDate.getUTCDate() - option.days,
|
||||
option.hours, 0, 0, 0
|
||||
));
|
||||
reminderTimes.push(reminderDate.toISOString());
|
||||
}
|
||||
} else {
|
||||
// 有时间:根据选项计算提前分钟数,使用 UTC 时间避免时区问题
|
||||
const option = REMINDER_OPTIONS_WITH_TIME.find(opt => opt.value === reminderValue);
|
||||
if (option) {
|
||||
const reminderDate = new Date(Date.UTC(
|
||||
eventDate.getUTCFullYear(),
|
||||
eventDate.getUTCMonth(),
|
||||
eventDate.getUTCDate(),
|
||||
eventDate.getUTCHours(),
|
||||
eventDate.getUTCMinutes() - option.minutes,
|
||||
0, 0
|
||||
));
|
||||
reminderTimes.push(reminderDate.toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
return reminderTimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提醒选项(根据是否有时间)
|
||||
*/
|
||||
export function getReminderOptions(hasTime: boolean) {
|
||||
return hasTime ? REMINDER_OPTIONS_WITH_TIME : REMINDER_OPTIONS_NO_TIME;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认提醒选项值
|
||||
*/
|
||||
export function getDefaultReminderValue(hasTime: boolean): string {
|
||||
return hasTime ? '0' : '0'; // 都是 "0",表示当天/准时
|
||||
}
|
||||
|
||||
/**
|
||||
* 从提醒时间点反推用户选择的提醒选项值
|
||||
* @param reminderTimes 提醒时间点数组
|
||||
* @param eventDateStr 事件日期
|
||||
* @param hasTime 是否有设置时间
|
||||
* @returns 提醒选项值
|
||||
*/
|
||||
export function getReminderValueFromTimes(
|
||||
reminderTimes: string[] | undefined,
|
||||
eventDateStr: string,
|
||||
hasTime: boolean
|
||||
): string {
|
||||
if (!reminderTimes || reminderTimes.length === 0) {
|
||||
return getDefaultReminderValue(hasTime);
|
||||
}
|
||||
|
||||
// 取第一个提醒时间点
|
||||
const reminderTime = new Date(reminderTimes[0]);
|
||||
const eventDate = new Date(eventDateStr);
|
||||
|
||||
if (!hasTime) {
|
||||
// 无时间:使用 UTC 日期部分计算,避免时区问题
|
||||
const reminderUTCDate = new Date(Date.UTC(
|
||||
reminderTime.getUTCFullYear(),
|
||||
reminderTime.getUTCMonth(),
|
||||
reminderTime.getUTCDate()
|
||||
));
|
||||
const eventUTCDate = new Date(Date.UTC(
|
||||
eventDate.getUTCFullYear(),
|
||||
eventDate.getUTCMonth(),
|
||||
eventDate.getUTCDate()
|
||||
));
|
||||
|
||||
const reminderHours = reminderTime.getUTCHours();
|
||||
const diffDays = Math.round((eventUTCDate.getTime() - reminderUTCDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// 查找匹配的选项
|
||||
for (const option of REMINDER_OPTIONS_NO_TIME) {
|
||||
if (option.hours === reminderHours && option.days === diffDays) {
|
||||
return option.value;
|
||||
}
|
||||
}
|
||||
// 默认返回 "0"
|
||||
return '0';
|
||||
} else {
|
||||
// 有时间:计算总提前分钟数,包括跨天
|
||||
// 使用 UTC 时间戳计算差值,确保兼容本地时间和 UTC 时间
|
||||
// eventDateStr 可能是不同时区格式,统一使用 UTC 组件计算
|
||||
const eventTime = Date.UTC(
|
||||
eventDate.getUTCFullYear(),
|
||||
eventDate.getUTCMonth(),
|
||||
eventDate.getUTCDate(),
|
||||
eventDate.getUTCHours(),
|
||||
eventDate.getUTCMinutes(),
|
||||
eventDate.getUTCSeconds()
|
||||
);
|
||||
const reminderTimeVal = reminderTime.getTime();
|
||||
const diffMs = eventTime - reminderTimeVal;
|
||||
const diffMinutes = Math.round(diffMs / (1000 * 60));
|
||||
|
||||
// 查找匹配的选项
|
||||
for (const option of REMINDER_OPTIONS_WITH_TIME) {
|
||||
if (option.minutes === diffMinutes) {
|
||||
return option.value;
|
||||
}
|
||||
}
|
||||
// 默认返回 "0"
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化提醒时间显示
|
||||
* @param reminderTimes 提醒时间点数组
|
||||
* @returns 格式化的时间字符串,如 "2月9日 09:00"
|
||||
*/
|
||||
export function formatReminderTimeDisplay(reminderTimes: string[] | undefined): string {
|
||||
if (!reminderTimes || reminderTimes.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const reminderTime = new Date(reminderTimes[0]);
|
||||
return reminderTime.toLocaleString('zh-CN', {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 统一的重复事件创建函数
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* 创建重复事件的数据(用于标记完成时自动创建下一周期)
|
||||
* 包含所有必要字段的完整处理
|
||||
*/
|
||||
export interface CreateNextEventData {
|
||||
type: EventType;
|
||||
title: string;
|
||||
date: string; // 下一次提醒日期
|
||||
is_lunar: boolean;
|
||||
repeat_type: RepeatType;
|
||||
repeat_interval: number | null;
|
||||
next_reminder_date: string | null;
|
||||
is_holiday?: boolean;
|
||||
content?: string;
|
||||
priority?: PriorityType;
|
||||
reminder_times?: string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建下一周期事件的完整数据
|
||||
* 自动计算 next_reminder_date 并继承所有字段
|
||||
* 注意:根据新日期重新计算 reminder_times
|
||||
*/
|
||||
export function createNextRecurringEventData(
|
||||
currentEvent: Event,
|
||||
nextValidDate: string
|
||||
): CreateNextEventData {
|
||||
// 计算新的 next_reminder_date
|
||||
const newNextReminderDate = calculateNextReminderDate(
|
||||
nextValidDate,
|
||||
currentEvent.repeat_type as RepeatType,
|
||||
currentEvent.repeat_interval
|
||||
);
|
||||
|
||||
// 根据原事件的 reminder_times 判断是有时间还是无时间
|
||||
// 并重新计算基于新日期的 reminder_times
|
||||
let newReminderTimes: string[] | null = null;
|
||||
|
||||
if (currentEvent.reminder_times && currentEvent.reminder_times.length > 0) {
|
||||
// 判断原始事件是否有时间(日期中有 T 且包含有效时间)
|
||||
const hasTime = currentEvent.date.includes('T') &&
|
||||
!currentEvent.date.endsWith('T00:00:00.000Z');
|
||||
|
||||
// 从 reminder_times 反推提醒选项值
|
||||
const reminderValue = getReminderValueFromTimes(
|
||||
currentEvent.reminder_times,
|
||||
currentEvent.date,
|
||||
hasTime
|
||||
);
|
||||
|
||||
// 根据新日期重新计算 reminder_times
|
||||
newReminderTimes = calculateReminderTimes(
|
||||
nextValidDate,
|
||||
hasTime,
|
||||
reminderValue
|
||||
);
|
||||
}
|
||||
|
||||
// 构建完整的事件数据
|
||||
const eventData: CreateNextEventData = {
|
||||
type: currentEvent.type,
|
||||
title: currentEvent.title,
|
||||
date: nextValidDate,
|
||||
is_lunar: currentEvent.is_lunar ?? false,
|
||||
repeat_type: currentEvent.repeat_type,
|
||||
repeat_interval: currentEvent.repeat_interval ?? null,
|
||||
next_reminder_date: newNextReminderDate,
|
||||
is_holiday: currentEvent.is_holiday ?? false,
|
||||
priority: currentEvent.priority,
|
||||
// 重新计算的 reminder_times
|
||||
reminder_times: newReminderTimes,
|
||||
};
|
||||
|
||||
// 只有当 content 有值时才包含
|
||||
if (currentEvent.content) {
|
||||
eventData.content = currentEvent.content;
|
||||
}
|
||||
|
||||
return eventData;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user