feat: 优化提醒时间显示和顺延功能

- 修复时间显示问题:无时间仅显示日期,有时间显示日期+时间
- 修复时区转换问题:使用本地时间格式存储
- 逾期提醒增加【顺延】操作:日期调整为今日,时间不变
- 修复空日期处理和归档页过滤逻辑

Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-02-03 14:57:55 +08:00
parent a8b4f17043
commit 7b0afbb27b
4 changed files with 144 additions and 31 deletions

View File

@ -1,6 +1,6 @@
import { useMemo, useState, useEffect } from 'react'; import { useMemo, useState, useEffect } from 'react';
import { Paper, Text, Checkbox, Group, Stack, ActionIcon } from '@mantine/core'; import { Paper, Text, Checkbox, Group, Stack, ActionIcon } from '@mantine/core';
import { IconDots } from '@tabler/icons-react'; import { IconDots, IconArrowForward } from '@tabler/icons-react';
import type { Event } from '../../types'; import type { Event } from '../../types';
interface ReminderCardProps { interface ReminderCardProps {
@ -8,30 +8,69 @@ interface ReminderCardProps {
onToggle: () => void; onToggle: () => void;
onClick: () => void; onClick: () => void;
onDelete?: () => void; onDelete?: () => void;
onPostpone?: () => void;
isMissed?: boolean; isMissed?: boolean;
} }
export function ReminderCard({ event, onToggle, onClick, isMissed = false }: ReminderCardProps) { export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, isMissed = false }: ReminderCardProps) {
const isCompleted = event.is_completed ?? false; const isCompleted = event.is_completed ?? false;
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [isAnimating, setIsAnimating] = useState(false); const [isAnimating, setIsAnimating] = useState(false);
// 计算时间信息 // 计算时间信息
const timeInfo = useMemo(() => { const timeInfo = useMemo(() => {
// 处理空日期情况
if (!event.date) {
return { isPast: false, isToday: true, timeStr: '未设置时间', diff: 0, hasTime: false };
}
const now = new Date(); const now = new Date();
const eventDate = new Date(event.date); // 解析日期处理本地时间格式无时区和ISO格式
let eventDate: Date;
const dateStr = event.date as string;
// 如果是本地时间格式YYYY-MM-DDTHH:mm:ss添加时区信息避免UTC转换
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(dateStr)) {
// 本地时间格式,直接使用
eventDate = new Date(dateStr);
} else {
// ISO格式可能有时区
eventDate = new Date(dateStr);
}
// 判断是否是有意设置的时间不是默认的00:00
// 对于本地格式的日期检查日期字符串中是否包含非00:00的时间
const timeMatch = dateStr.match(/T(\d{2}):(\d{2})/);
const hasExplicitTime = timeMatch && (timeMatch[1] !== '00' || timeMatch[2] !== '00');
// 对于ISO格式检查小时和分钟
const hours = eventDate.getHours();
const minutes = eventDate.getMinutes();
const isoHasTime = hours !== 0 || minutes !== 0;
const diff = eventDate.getTime() - now.getTime(); const diff = eventDate.getTime() - now.getTime();
const isPast = diff < 0; const isPast = diff < 0;
const isToday = eventDate.toDateString() === now.toDateString(); const isToday = eventDate.toDateString() === now.toDateString();
const timeStr = eventDate.toLocaleString('zh-CN', { // 格式化显示
month: 'numeric', let timeStr: string;
day: 'numeric', // 优先使用 hasExplicitTime本地格式如果没有显式时间才检查 ISO 格式
hour: '2-digit', if (hasExplicitTime || isoHasTime) {
minute: '2-digit', // 有设置时间,显示日期+时间
}); timeStr = eventDate.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
} else {
// 没有设置时间,只显示日期
timeStr = eventDate.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
});
}
return { isPast, isToday, timeStr, diff }; return { isPast, isToday, timeStr, diff, hasTime: hasExplicitTime || isoHasTime };
}, [event.date]); }, [event.date]);
// 获取文字颜色 // 获取文字颜色
@ -164,6 +203,26 @@ export function ReminderCard({ event, onToggle, onClick, isMissed = false }: Rem
{/* Quick actions */} {/* Quick actions */}
<Group gap={4}> <Group gap={4}>
{/* 顺延按钮 - 仅在逾期状态显示 */}
{isMissed && onPostpone && (
<ActionIcon
size="sm"
variant="subtle"
color="orange"
onClick={(e) => {
e.stopPropagation();
onPostpone();
}}
style={{
color: '#e67e22',
opacity: isHovered ? 1 : 0,
transition: 'all 0.2s ease',
}}
title="顺延到今天"
>
<IconArrowForward size={12} />
</ActionIcon>
)}
<ActionIcon <ActionIcon
size="sm" size="sm"
variant="subtle" variant="subtle"

View File

@ -21,6 +21,7 @@ interface ReminderListProps {
onAddClick: () => void; onAddClick: () => void;
onDelete?: (event: Event) => void; onDelete?: (event: Event) => void;
onRestore?: (event: Event) => void; onRestore?: (event: Event) => void;
onPostpone?: (event: Event) => void;
} }
export function ReminderList({ export function ReminderList({
@ -30,18 +31,20 @@ export function ReminderList({
onAddClick, onAddClick,
onDelete, onDelete,
onRestore, onRestore,
onPostpone,
}: ReminderListProps) { }: ReminderListProps) {
const grouped = useMemo(() => { const grouped = useMemo(() => {
const now = new Date(); const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // 仅按日期部分判断,获取今天的开始时间
today.setHours(0, 0, 0, 0); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const tomorrow = new Date(today); todayStart.setHours(0, 0, 0, 0);
tomorrow.setDate(tomorrow.getDate() + 1); const tomorrowStart = new Date(todayStart);
tomorrow.setHours(0, 0, 0, 0); tomorrowStart.setDate(tomorrowStart.getDate() + 1);
const dayAfterTomorrow = new Date(tomorrow); tomorrowStart.setHours(0, 0, 0, 0);
dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 1); const dayAfterTomorrowStart = new Date(tomorrowStart);
dayAfterTomorrow.setHours(0, 0, 0, 0); dayAfterTomorrowStart.setDate(dayAfterTomorrowStart.getDate() + 1);
dayAfterTomorrowStart.setHours(0, 0, 0, 0);
const reminders = events.filter((e) => e.type === 'reminder'); const reminders = events.filter((e) => e.type === 'reminder');
@ -53,21 +56,29 @@ export function ReminderList({
}; };
reminders.forEach((event) => { reminders.forEach((event) => {
const eventDate = new Date(event.date); // 无日期的提醒不视为逾期,放入今天分组
if (!event.date) {
// 已过期且已完成的去归档页 result.today.push(event);
if (event.is_completed && eventDate < now) {
return; return;
} }
// 未过期或已完成未过期的,按时间分组 const eventDate = new Date(event.date);
if (eventDate < now) { // 仅取日期部分进行比较(忽略时间)
const eventDateOnly = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate());
// 已过期且已完成的去归档页
if (event.is_completed && eventDateOnly < todayStart) {
return;
}
// 按日期部分判断是否逾期
if (eventDateOnly < todayStart) {
// 已过期未完成 // 已过期未完成
result.missed.push(event); result.missed.push(event);
} else if (eventDate < tomorrow) { } else if (eventDateOnly < tomorrowStart) {
// 今天 // 今天
result.today.push(event); result.today.push(event);
} else if (eventDate < dayAfterTomorrow) { } else if (eventDateOnly < dayAfterTomorrowStart) {
// 明天 // 明天
result.tomorrow.push(event); result.tomorrow.push(event);
} else { } else {
@ -176,6 +187,7 @@ export function ReminderList({
onClick={() => onEventClick(event)} onClick={() => onEventClick(event)}
onToggle={() => onToggleComplete(event)} onToggle={() => onToggleComplete(event)}
onDelete={onDelete ? () => onDelete(event) : undefined} onDelete={onDelete ? () => onDelete(event) : undefined}
onPostpone={onPostpone ? () => onPostpone(event) : undefined}
isMissed={true} isMissed={true}
/> />
))} ))}

View File

@ -35,10 +35,14 @@ export function ArchivePage() {
// 获取已归档的提醒(已过期且已勾选的) // 获取已归档的提醒(已过期且已勾选的)
const archivedReminders = events.filter((e) => { const archivedReminders = events.filter((e) => {
if (e.type !== 'reminder' || !e.is_completed) return false; if (e.type !== 'reminder' || !e.is_completed) return false;
if (!e.date) return false; // 跳过无日期的
const eventDate = new Date(e.date); const eventDate = new Date(e.date);
const now = new Date(); const now = new Date();
return eventDate < now; // 仅已过期的 return eventDate < now; // 仅已过期的
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }).sort((a, b) => {
if (!a.date || !b.date) return 0;
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
const handleRestore = async (event: Event) => { const handleRestore = async (event: Event) => {
await updateEventById(event.id, { is_completed: false }); await updateEventById(event.id, { is_completed: false });

View File

@ -69,6 +69,11 @@ export function HomePage() {
setFormIsLunar(event.is_lunar); setFormIsLunar(event.is_lunar);
setFormRepeatType(event.repeat_type); setFormRepeatType(event.repeat_type);
setFormIsHoliday(event.is_holiday || false); setFormIsHoliday(event.is_holiday || false);
// 提取时间(如果日期中包含时间信息)
const eventDate = new Date(event.date);
const hours = eventDate.getHours();
const minutes = eventDate.getMinutes();
setFormTime(hours === 0 && minutes === 0 ? '' : `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`);
open(); open();
}; };
@ -92,15 +97,26 @@ export function HomePage() {
// 确保 date 是 Date 对象 // 确保 date 是 Date 对象
const dateObj = formDate instanceof Date ? formDate : new Date(formDate as unknown as string); const dateObj = formDate instanceof Date ? formDate : new Date(formDate as unknown as string);
const dateStr = formTime let dateStr: string;
? new Date(dateObj.setHours(parseInt(formTime.split(':')[0]), parseInt(formTime.split(':')[1]))) const year = dateObj.getFullYear();
: dateObj; const month = String(dateObj.getMonth() + 1).padStart(2, '0');
const day = String(dateObj.getDate()).padStart(2, '0');
if (formTime) {
// 有时间:构建本地时间格式
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 {
// 无时间:只保存日期部分
dateStr = `${year}-${month}-${day}T00:00:00`;
}
const eventData = { const eventData = {
type: formType, type: formType,
title: formTitle, title: formTitle,
content: formContent || undefined, content: formContent || undefined,
date: dateStr.toISOString(), date: dateStr,
is_lunar: formIsLunar, is_lunar: formIsLunar,
repeat_type: formRepeatType, repeat_type: formRepeatType,
is_holiday: formIsHoliday || undefined, is_holiday: formIsHoliday || undefined,
@ -151,6 +167,27 @@ export function HomePage() {
fetchEvents(); fetchEvents();
}; };
const handlePostpone = async (event: Event) => {
if (event.type !== 'reminder') return;
// 将日期顺延到今天,保留原事件的时间
const today = new Date();
const originalDate = new Date(event.date);
// 提取原时间的小时和分钟
const hours = originalDate.getHours();
const minutes = originalDate.getMinutes();
// 构建新的本地时间字符串
const newDateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
const result = await updateEventById(event.id, {
date: newDateStr,
});
if (result.error) {
console.error('顺延失败:', result.error);
}
};
const resetForm = () => { const resetForm = () => {
setFormType('anniversary'); setFormType('anniversary');
setFormTitle(''); setFormTitle('');
@ -264,6 +301,7 @@ export function HomePage() {
onAddClick={() => handleAddClick('reminder')} onAddClick={() => handleAddClick('reminder')}
onDelete={handleDelete} onDelete={handleDelete}
onRestore={handleRestore} onRestore={handleRestore}
onPostpone={handlePostpone}
/> />
</div> </div>
</Grid.Col> </Grid.Col>