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 { NoteEditor } from '../components/note/NoteEditor';
|
||||||
import { FloatingAIChat } from '../components/ai/FloatingAIChat';
|
import { FloatingAIChat } from '../components/ai/FloatingAIChat';
|
||||||
import type { Event, EventType, RepeatType, PriorityType } from '../types';
|
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() {
|
export function HomePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -54,7 +54,6 @@ export function HomePage() {
|
|||||||
const [formRepeatType, setFormRepeatType] = useState<RepeatType>('none');
|
const [formRepeatType, setFormRepeatType] = useState<RepeatType>('none');
|
||||||
const [formIsHoliday, setFormIsHoliday] = useState(false);
|
const [formIsHoliday, setFormIsHoliday] = useState(false);
|
||||||
const [formPriority, setFormPriority] = useState<PriorityType>('none');
|
const [formPriority, setFormPriority] = useState<PriorityType>('none');
|
||||||
const [formReminderValue, setFormReminderValue] = useState('0'); // 提醒选项值
|
|
||||||
|
|
||||||
// Initialize auth and data on mount
|
// Initialize auth and data on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -82,10 +81,7 @@ export function HomePage() {
|
|||||||
const eventDate = new Date(event.date);
|
const eventDate = new Date(event.date);
|
||||||
const hours = eventDate.getHours();
|
const hours = eventDate.getHours();
|
||||||
const minutes = eventDate.getMinutes();
|
const minutes = eventDate.getMinutes();
|
||||||
const hasTime = hours !== 0 || minutes !== 0;
|
setFormTime(hours === 0 && minutes === 0 ? '' : `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`);
|
||||||
setFormTime(hasTime ? `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}` : '');
|
|
||||||
// 从 reminder_times 反推用户选择的提醒选项值
|
|
||||||
setFormReminderValue(getReminderValueFromTimes(event.reminder_times, event.date, hasTime));
|
|
||||||
open();
|
open();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,7 +97,6 @@ export function HomePage() {
|
|||||||
setFormRepeatType('none');
|
setFormRepeatType('none');
|
||||||
setFormIsHoliday(type === 'anniversary');
|
setFormIsHoliday(type === 'anniversary');
|
||||||
setFormPriority('none');
|
setFormPriority('none');
|
||||||
setFormReminderValue('0');
|
|
||||||
open();
|
open();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -117,15 +112,13 @@ export function HomePage() {
|
|||||||
const day = String(dateObj.getDate()).padStart(2, '0');
|
const day = String(dateObj.getDate()).padStart(2, '0');
|
||||||
|
|
||||||
if (formTime) {
|
if (formTime) {
|
||||||
// 有时间:构建 UTC 时间格式(用户选择的是本地时间,需要转换为 UTC 存储)
|
// 有时间:构建本地时间格式
|
||||||
const hours = parseInt(formTime.split(':')[0]);
|
const hours = String(parseInt(formTime.split(':')[0])).padStart(2, '0');
|
||||||
const minutes = parseInt(formTime.split(':')[1]);
|
const minutes = String(parseInt(formTime.split(':')[1])).padStart(2, '0');
|
||||||
// 先用本地时间创建日期对象,然后获取 UTC 时间
|
dateStr = `${year}-${month}-${day}T${hours}:${minutes}:00`;
|
||||||
const localDate = new Date(year, dateObj.getMonth(), day, hours, minutes);
|
|
||||||
dateStr = localDate.toISOString();
|
|
||||||
} else {
|
} else {
|
||||||
// 无时间:使用 UTC 日期格式
|
// 无时间:只保存日期部分
|
||||||
dateStr = `${year}-${month}-${day}T00:00:00.000Z`;
|
dateStr = `${year}-${month}-${day}T00:00:00`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建事件数据,确保不包含 undefined 值
|
// 构建事件数据,确保不包含 undefined 值
|
||||||
@ -146,12 +139,6 @@ export function HomePage() {
|
|||||||
eventData.content = formContent;
|
eventData.content = formContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算提醒时间点(仅对提醒事项)
|
|
||||||
if (formType === 'reminder') {
|
|
||||||
const hasTime = !!formTime;
|
|
||||||
eventData.reminder_times = calculateReminderTimes(dateStr, hasTime, formReminderValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEdit && selectedEvent) {
|
if (isEdit && selectedEvent) {
|
||||||
await updateEventById(selectedEvent.id, eventData);
|
await updateEventById(selectedEvent.id, eventData);
|
||||||
} else {
|
} else {
|
||||||
@ -286,7 +273,6 @@ export function HomePage() {
|
|||||||
setFormRepeatType('none');
|
setFormRepeatType('none');
|
||||||
setFormIsHoliday(false);
|
setFormIsHoliday(false);
|
||||||
setFormPriority('none');
|
setFormPriority('none');
|
||||||
setFormReminderValue('0');
|
|
||||||
setSelectedEvent(null);
|
setSelectedEvent(null);
|
||||||
setIsEdit(false);
|
setIsEdit(false);
|
||||||
};
|
};
|
||||||
@ -429,13 +415,11 @@ export function HomePage() {
|
|||||||
]}
|
]}
|
||||||
value={formType}
|
value={formType}
|
||||||
onChange={(value) => value && setFormType(value as EventType)}
|
onChange={(value) => value && setFormType(value as EventType)}
|
||||||
readOnly={isEdit}
|
disabled={isEdit}
|
||||||
rightSectionPointerEvents={isEdit ? 'none' : 'auto'}
|
|
||||||
styles={{
|
styles={{
|
||||||
input: {
|
input: {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
background: '#faf9f7',
|
background: '#faf9f7',
|
||||||
pointerEvents: isEdit ? 'none' : 'auto',
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -443,13 +427,14 @@ export function HomePage() {
|
|||||||
{/* Title */}
|
{/* Title */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label={
|
label={
|
||||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em' }}>
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
||||||
标题<Box component="span" c="red">*</Box>
|
标题
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
placeholder="输入标题"
|
placeholder="输入标题"
|
||||||
value={formTitle}
|
value={formTitle}
|
||||||
onChange={(e) => setFormTitle(e.target.value)}
|
onChange={(e) => setFormTitle(e.target.value)}
|
||||||
|
required
|
||||||
styles={{
|
styles={{
|
||||||
input: {
|
input: {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@ -506,6 +491,11 @@ export function HomePage() {
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
withArrow
|
withArrow
|
||||||
shadow="md"
|
shadow="md"
|
||||||
|
onOpenChange={(opened) => {
|
||||||
|
if (!opened && formDate) {
|
||||||
|
// Keep the date
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<TextInput
|
<TextInput
|
||||||
@ -608,64 +598,29 @@ export function HomePage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Repeat and Reminder on same row */}
|
{/* Repeat type */}
|
||||||
<Group gap={12} grow>
|
<Select
|
||||||
{/* Repeat type */}
|
label={
|
||||||
<Select
|
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 4 }}>
|
||||||
label={
|
重复
|
||||||
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em' }}>
|
</Text>
|
||||||
重复
|
}
|
||||||
</Text>
|
data={[
|
||||||
}
|
{ value: 'none', label: '不重复' },
|
||||||
data={[
|
{ value: 'daily', label: '每天' },
|
||||||
{ value: 'none', label: '不重复' },
|
{ value: 'weekly', label: '每周' },
|
||||||
{ value: 'daily', label: '每天' },
|
{ value: 'monthly', label: '每月' },
|
||||||
{ value: 'weekly', label: '每周' },
|
{ value: 'yearly', label: '每年' },
|
||||||
{ value: 'monthly', label: '每月' },
|
]}
|
||||||
{ value: 'yearly', label: '每年' },
|
value={formRepeatType}
|
||||||
]}
|
onChange={(value) => value && setFormRepeatType(value as RepeatType)}
|
||||||
value={formRepeatType}
|
styles={{
|
||||||
onChange={(value) => value && setFormRepeatType(value as RepeatType)}
|
input: {
|
||||||
styles={{
|
borderRadius: 2,
|
||||||
input: {
|
background: '#faf9f7',
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Color (only for reminders) */}
|
{/* Color (only for reminders) */}
|
||||||
{formType === 'reminder' && (
|
{formType === 'reminder' && (
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { create } from 'zustand';
|
|||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
import type { User, Event, Note, AIConversation, EventType, RepeatType } from '../types';
|
import type { User, Event, Note, AIConversation, EventType, RepeatType } from '../types';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
import { calculateNextReminderDate, findNextValidReminderDate, isDuplicateReminder, createNextRecurringEventData } from '../utils/repeatCalculator';
|
import { calculateNextReminderDate, findNextValidReminderDate, isDuplicateReminder } from '../utils/repeatCalculator';
|
||||||
|
|
||||||
// 应用设置类型
|
// 应用设置类型
|
||||||
interface AppSettings {
|
interface AppSettings {
|
||||||
@ -171,11 +171,29 @@ export const useAppStore = create<AppState>()(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
// 使用统一的重复事件创建函数,自动继承所有字段(包括 priority 和 reminder_times)
|
// 计算新提醒的 next_reminder_date
|
||||||
const newEventData = createNextRecurringEventData(
|
const newNextReminderDate = calculateNextReminderDate(
|
||||||
currentEvent,
|
nextValidDate,
|
||||||
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);
|
const newEvent = await api.events.create(newEventData);
|
||||||
|
|
||||||
// 添加新事件到本地状态
|
// 添加新事件到本地状态
|
||||||
|
|||||||
@ -31,7 +31,6 @@ export interface Event {
|
|||||||
is_holiday?: boolean; // Only for anniversaries
|
is_holiday?: boolean; // Only for anniversaries
|
||||||
is_completed?: boolean; // Only for reminders
|
is_completed?: boolean; // Only for reminders
|
||||||
priority?: PriorityType; // Priority level for reminders
|
priority?: PriorityType; // Priority level for reminders
|
||||||
reminder_times?: string[]; // 提醒时间点列表(ISO 时间戳数组)
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_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 currentDate 当前日期(ISO 格式)
|
||||||
* @param repeatType 重复类型
|
* @param repeatType 重复类型
|
||||||
* @param interval 周数间隔(仅 weekly 类型使用,默认1)
|
* @param interval 周数间隔(仅 weekly 类型使用,默认1)
|
||||||
* @returns 下一次提醒日期(ISO 格式,UTC 时间)
|
* @returns 下一次提醒日期(ISO 格式,本地时间)
|
||||||
*/
|
*/
|
||||||
export function calculateNextDueDate(
|
export function calculateNextDueDate(
|
||||||
currentDate: string,
|
currentDate: string,
|
||||||
@ -13,36 +13,36 @@ export function calculateNextDueDate(
|
|||||||
interval: number = 1
|
interval: number = 1
|
||||||
): string {
|
): string {
|
||||||
const date = new Date(currentDate);
|
const date = new Date(currentDate);
|
||||||
const year = date.getUTCFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getUTCMonth();
|
const month = date.getMonth();
|
||||||
const day = date.getUTCDate();
|
const day = date.getDate();
|
||||||
const hours = date.getUTCHours();
|
const hours = date.getHours();
|
||||||
const minutes = date.getUTCMinutes();
|
const minutes = date.getMinutes();
|
||||||
|
|
||||||
switch (repeatType) {
|
switch (repeatType) {
|
||||||
case 'daily':
|
case 'daily':
|
||||||
// 每天:加1天
|
// 每天:加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':
|
case 'weekly':
|
||||||
// 每周:加7天 * interval
|
// 每周:加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':
|
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日)
|
// 处理月末日期(如3月31日 -> 4月30日)
|
||||||
if (nextMonth.getUTCDate() !== day) {
|
if (nextMonth.getDate() !== day) {
|
||||||
return new Date(Date.UTC(year, month + 1, 0, hours, minutes)).toISOString();
|
return new Date(year, month + 1, 0, hours, minutes).toISOString();
|
||||||
}
|
}
|
||||||
return nextMonth.toISOString();
|
return nextMonth.toISOString();
|
||||||
|
|
||||||
case 'yearly':
|
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日)
|
// 处理闰年(如2月29日 -> 2月28日)
|
||||||
if (nextYearDate.getUTCMonth() !== month) {
|
if (nextYearDate.getMonth() !== month) {
|
||||||
return new Date(Date.UTC(year + 1, month, 0, hours, minutes)).toISOString();
|
return new Date(year + 1, month, 0, hours, minutes).toISOString();
|
||||||
}
|
}
|
||||||
return nextYearDate.toISOString();
|
return nextYearDate.toISOString();
|
||||||
|
|
||||||
@ -231,286 +231,3 @@ export function formatDateDisplay(dateStr: string, showTime: boolean = true): st
|
|||||||
day: 'numeric',
|
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