From 1559e603b0ac6fdae8e82ec163248c33923e1965 Mon Sep 17 00:00:00 2001 From: ddshi <8811906+ddshi@user.noreply.gitee.com> Date: Wed, 4 Feb 2026 13:51:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E6=8F=90=E9=86=92=E5=AE=8C=E6=88=90=E7=A7=BB=E9=99=A4=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E3=80=81=E9=80=BE=E6=9C=9F=E5=88=97=E8=A1=A8=E5=B1=95?= =?UTF-8?q?=E5=BC=80=E6=94=B6=E8=B5=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重复提醒完成流程优化: - 勾选完成重复提醒后,自动移除repeat_type、repeat_interval、next_reminder_date - 自动创建下一周期的新提醒记录 - 合并API调用,确保状态更新原子性 - 逾期列表展开/收起功能: - 默认收起,最多显示3条逾期提醒 - 超过3条时显示"还有 X 个逾期提醒..."链接 - 展开后底部显示"收起"按钮 - 时间显示优化: - 无时间提醒(00:00)只显示日期,不显示时间 - 归档列表同样适用此规则 - 其他优化: - 归档抖动动画反馈 - 分类折叠功能 Co-Authored-By: Claude Opus 4.5 --- .../reminder/ArchiveReminderModal.tsx | 32 ++- src/components/reminder/ReminderCard.tsx | 107 +++++--- src/components/reminder/ReminderList.tsx | 193 ++++++++++++--- src/pages/ArchivePage.tsx | 32 ++- src/pages/HomePage.tsx | 35 ++- src/stores/index.ts | 107 +++++++- src/types/index.ts | 13 +- src/utils/repeatCalculator.ts | 233 ++++++++++++++++++ 8 files changed, 637 insertions(+), 115 deletions(-) create mode 100644 src/utils/repeatCalculator.ts diff --git a/src/components/reminder/ArchiveReminderModal.tsx b/src/components/reminder/ArchiveReminderModal.tsx index 4c01767..e3179aa 100644 --- a/src/components/reminder/ArchiveReminderModal.tsx +++ b/src/components/reminder/ArchiveReminderModal.tsx @@ -19,6 +19,31 @@ interface ArchiveReminderModalProps { onDelete: (event: Event) => void; } +/** + * 格式化日期显示,根据是否有时间选择显示格式 + */ +function formatArchiveDate(dateStr: string): string { + const date = new Date(dateStr); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const hasExplicitTime = hours !== 0 || minutes !== 0; + + if (hasExplicitTime) { + return date.toLocaleString('zh-CN', { + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); + } else { + return date.toLocaleString('zh-CN', { + month: 'numeric', + day: 'numeric', + }); + } +} + export function ArchiveReminderModal({ opened, onClose, @@ -91,12 +116,7 @@ export function ArchiveReminderModal({ {event.title} - {new Date(event.date).toLocaleString('zh-CN', { - month: 'numeric', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - })} + {formatArchiveDate(event.date)} diff --git a/src/components/reminder/ReminderCard.tsx b/src/components/reminder/ReminderCard.tsx index 819cbbc..5350d5a 100644 --- a/src/components/reminder/ReminderCard.tsx +++ b/src/components/reminder/ReminderCard.tsx @@ -1,7 +1,8 @@ import { useMemo, useState } from 'react'; import { Paper, Text, Checkbox, Group, Stack, ActionIcon, Box } from '@mantine/core'; -import { IconDots, IconArrowForward } from '@tabler/icons-react'; -import type { Event } from '../../types'; +import { IconDots, IconArrowForward, IconRepeat, IconRepeatOff } from '@tabler/icons-react'; +import type { Event, RepeatType } from '../../types'; +import { getRepeatTypeLabel } from '../../utils/repeatCalculator'; interface ReminderCardProps { event: Event; @@ -10,9 +11,10 @@ interface ReminderCardProps { onDelete?: () => void; onPostpone?: () => void; isMissed?: boolean; + onMissedToggle?: () => void; } -export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, isMissed = false }: ReminderCardProps) { +export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, isMissed = false, onMissedToggle }: ReminderCardProps) { const isCompleted = event.is_completed ?? false; const [isHovered, setIsHovered] = useState(false); const [isAnimating, setIsAnimating] = useState(false); @@ -25,26 +27,16 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i } const now = new 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格式,检查小时和分钟 + // 统一使用 Date 对象处理 + const eventDate = new Date(dateStr); + + // 检查是否包含有效时间(小时和分钟不全为0) + // 对于本地格式(如 "2025-02-05T00:00:00")和 ISO 格式都适用 const hours = eventDate.getHours(); const minutes = eventDate.getMinutes(); - const isoHasTime = hours !== 0 || minutes !== 0; + const hasExplicitTime = hours !== 0 || minutes !== 0; const diff = eventDate.getTime() - now.getTime(); const isPast = diff < 0; @@ -52,8 +44,7 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i // 格式化显示 let timeStr: string; - // 优先使用 hasExplicitTime(本地格式),如果没有显式时间才检查 ISO 格式 - if (hasExplicitTime || isoHasTime) { + if (hasExplicitTime) { // 有设置时间,显示日期+时间 timeStr = eventDate.toLocaleString('zh-CN', { month: 'numeric', @@ -63,14 +54,14 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i hour12: false, }); } else { - // 没有设置时间,只显示日期 + // 没有设置时间(00:00),只显示日期 timeStr = eventDate.toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', }); } - return { isPast, isToday, timeStr, diff, hasTime: hasExplicitTime || isoHasTime }; + return { isPast, isToday, timeStr, diff, hasTime: hasExplicitTime }; }, [event.date]); // 获取文字颜色 @@ -101,6 +92,18 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i return 'rgba(0, 0, 0, 0.06)'; }; + // 获取循环图标颜色 + const getRepeatIconColor = (type: RepeatType) => { + const colors: Record = { + daily: '#3b82f6', // 蓝色 + weekly: '#22c55e', // 绿色 + monthly: '#a855f7', // 紫色 + yearly: '#f59e0b', // 橙色 + none: '#999', + }; + return colors[type] || '#999'; + }; + return ( - {/* 右侧:日期时间 */} - + {/* 右侧:循环图标 + 日期时间 */} + + {event.repeat_type !== 'none' && ( + + + + )} { setIsAnimating(false); onToggle(); + onMissedToggle?.(); }, 300); } else { onToggle(); @@ -244,19 +260,34 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i - - {event.title} - + + + {event.title} + + {event.repeat_type !== 'none' && ( + + + + )} + (null); + // 分类折叠状态 + const [collapsed, setCollapsed] = useState({ + today: false, + tomorrow: false, + later: false, + }); + + // 逾期列表展开状态(默认收起) + const [missedExpanded, setMissedExpanded] = useState(false); + + // 归档图标抖动动画状态 + const [archiveShake, setArchiveShake] = useState(false); + + // 归档图标抖动动画样式 + const shakeAnimationStyle = ` + @keyframes archiveShake { + 0%, 100% { transform: scale(1); } + 25% { transform: scale(1.3); } + 50% { transform: scale(1); } + 75% { transform: scale(1.3); } + } + `; + + // 触发归档图标抖动动画 + const triggerArchiveShake = () => { + setArchiveShake(true); + setTimeout(() => { + setArchiveShake(false); + }, 400); + }; + + // 切换分类折叠状态 + const toggleCollapse = (category: keyof typeof collapsed) => { + setCollapsed((prev) => ({ + ...prev, + [category]: !prev[category], + })); + }; + // 滚动条样式 - 仅在悬停时显示 const scrollbarStyle = ` .reminder-scroll::-webkit-scrollbar { @@ -171,12 +216,10 @@ export function ReminderList({ ); } - // 已过提醒数量提示 - const missedCount = grouped.missed.length; - return ( <> + {/* Header */} @@ -184,11 +227,29 @@ export function ReminderList({ 提醒 - {missedCount > 0 && ( - - {missedCount}个逾期 - - )} + + + ) : ( + <> + {/* 收起状态:只显示前3个 */} + {grouped.missed.slice(0, 3).map((event) => ( + onEventClick(event)} + onToggle={() => onToggleComplete(event)} + onDelete={onDelete ? () => onDelete(event) : undefined} + onPostpone={onPostpone ? () => onPostpone(event) : undefined} + onMissedToggle={triggerArchiveShake} + isMissed={true} + /> + ))} + {grouped.missed.length > 3 && ( + setMissedExpanded(true)} + > + 还有 {grouped.missed.length - 3} 个逾期提醒... + + )} + )} @@ -249,12 +355,21 @@ export function ReminderList({ {/* 今天 */} {grouped.today.length > 0 && ( <> - + toggleCollapse('today')} + > 今天 + {collapsed.today ? ( + + ) : ( + + )} - {grouped.today.map((event) => ( + {!collapsed.today && grouped.today.map((event) => ( 0 && ( <> - + toggleCollapse('tomorrow')} + > 明天 + {collapsed.tomorrow ? ( + + ) : ( + + )} - {grouped.tomorrow.map((event) => ( + {!collapsed.tomorrow && grouped.tomorrow.map((event) => ( 0 && ( <> - + toggleCollapse('later')} + > 更久之后 + {collapsed.later ? ( + + ) : ( + + )} - {grouped.later.map((event) => ( + {!collapsed.later && grouped.later.map((event) => ( state.events); @@ -132,12 +157,7 @@ export function ArchivePage() { {event.title} - {new Date(event.date).toLocaleString('zh-CN', { - month: 'numeric', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - })} + {formatArchiveDate(event.date)} {event.content && ( diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index ea8409c..c49929e 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -13,7 +13,7 @@ import { Stack, } from '@mantine/core'; import { DatePickerInput, TimeInput } from '@mantine/dates'; -import { IconLogout, IconSettings, IconArchive } from '@tabler/icons-react'; +import { IconLogout, IconSettings } from '@tabler/icons-react'; import { useDisclosure } from '@mantine/hooks'; import { useNavigate } from 'react-router-dom'; import { useAppStore } from '../stores'; @@ -22,6 +22,7 @@ import { ReminderList } from '../components/reminder/ReminderList'; import { NoteEditor } from '../components/note/NoteEditor'; import { FloatingAIChat } from '../components/ai/FloatingAIChat'; import type { Event, EventType, RepeatType } from '../types'; +import { calculateNextReminderDate } from '../utils/repeatCalculator'; export function HomePage() { const navigate = useNavigate(); @@ -56,6 +57,7 @@ export function HomePage() { const handleLogout = async () => { await logout(); + navigate('/landing'); }; const handleEventClick = (event: Event) => { @@ -111,16 +113,23 @@ export function HomePage() { dateStr = `${year}-${month}-${day}T00:00:00`; } - const eventData = { + // 构建事件数据,确保不包含 undefined 值 + const eventData: Record = { type: formType, title: formTitle, - content: formContent || undefined, date: dateStr, is_lunar: formIsLunar, repeat_type: formRepeatType, - is_holiday: formIsHoliday || undefined, + // 计算下一次提醒日期 + next_reminder_date: calculateNextReminderDate(dateStr, formRepeatType, undefined), + is_holiday: formIsHoliday ?? false, }; + // 只有当 content 有值时才包含 + if (formContent.trim()) { + eventData.content = formContent; + } + if (isEdit && selectedEvent) { await updateEventById(selectedEvent.id, eventData); } else { @@ -220,20 +229,6 @@ export function HomePage() { 掐日子 - {/* 归档入口 - 一直显示 */} - {/* 设置入口 */}