import { useMemo, useState, useCallback, useEffect, useRef } from 'react'; import { Paper, Text, Group, Stack, ActionIcon, Box, Menu, Divider, Portal, } from '@mantine/core'; import { IconDots, IconArrowForward, IconRepeat, IconCalendar, IconEdit, IconTrash, IconCircleFilled, IconCheck, } from '@tabler/icons-react'; import type { Event, PriorityType } from '../../types'; import { getRepeatTypeLabel } from '../../utils/repeatCalculator'; import { useContextMenuStore } from '../../stores/contextMenu'; // 重复图标颜色常量 const REPEAT_ICON_COLOR = '#999'; // 优先级颜色映射 const PRIORITY_COLORS: Record = { none: 'rgba(0, 0, 0, 0.15)', red: '#dc2626', green: '#16a34a', yellow: '#ca8a04', }; // 优先级名称映射 const PRIORITY_NAMES: Record = { none: '默认', red: '红色', green: '绿色', yellow: '黄色', }; interface ReminderCardProps { event: Event; onToggle: () => void; onClick: () => void; onDelete?: () => void; onPostpone?: () => void; isMissed?: boolean; onMissedToggle?: () => void; onDateChange?: (date: string, repeatType: string, repeatInterval: number | null) => void; onPriorityChange?: (priority: PriorityType) => void; } export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, isMissed = false, onMissedToggle, onDateChange, onPriorityChange, }: ReminderCardProps) { const isCompleted = event.is_completed ?? false; const [isHovered, setIsHovered] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const cardRef = useRef(null); // 使用全局状态管理右键菜单 const openEventId = useContextMenuStore((state) => state.openEventId); const menuPosition = useContextMenuStore((state) => state.menuPosition); const { openMenu, closeMenu } = useContextMenuStore(); const isMenuOpen = openEventId === event.id; // 获取优先级颜色 const getPriorityColor = (priority?: PriorityType): string => { return PRIORITY_COLORS[priority || 'none']; }; // 右键点击处理 const handleContextMenu = useCallback((e: React.MouseEvent) => { e.preventDefault(); console.log('[ContextMenu] 右键点击事件, eventId:', event.id, 'x:', e.clientX, 'y:', e.clientY); // 打开菜单,传入鼠标位置 openMenu(event.id, { x: e.clientX, y: e.clientY }); }, [event.id, openMenu]); // 点击外部关闭菜单 useEffect(() => { if (!isMenuOpen) return; const handleMouseDown = (e: MouseEvent) => { const target = e.target as HTMLElement; // 如果点击不在当前卡片和菜单内,关闭菜单 if (!target.closest(`[data-reminder-card="${event.id}"]`) && !target.closest('[data-context-menu]')) { closeMenu(); } }; // ESC键关闭菜单 const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { closeMenu(); } }; document.addEventListener('mousedown', handleMouseDown); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handleMouseDown); document.removeEventListener('keydown', handleKeyDown); }; }, [isMenuOpen, event.id, closeMenu]); // 计算菜单位置(带边缘保护) const getMenuPosition = () => { const menuWidth = 180; // 菜单宽度 const menuHeight = 150; // 估算菜单高度 const padding = 8; // 边缘padding // 如果有鼠标位置,使用鼠标位置 let left = padding; let top = padding; if (menuPosition) { // 水平位置跟随鼠标,添加偏移避免遮挡 left = menuPosition.x + 4; // 垂直位置优先显示在鼠标下方 top = menuPosition.y + 4; } else if (cardRef.current) { // 回退到卡片位置 const cardRect = cardRef.current.getBoundingClientRect(); left = cardRect.left; top = cardRect.bottom + 4; } // 边缘保护:确保不超出视口 if (left + menuWidth > window.innerWidth - padding) { left = window.innerWidth - menuWidth - padding; } if (left < padding) left = padding; if (top + menuHeight > window.innerHeight - padding) { // 如果下方空间不足,显示在上方 top = menuPosition ? menuPosition.y - menuHeight - 4 : top; } if (top < padding) top = padding; return { left, top }; }; // 计算时间信息 const timeInfo = useMemo(() => { // 处理空日期情况 if (!event.date) { return { isPast: false, isToday: true, timeStr: '未设置时间', diff: 0, hasTime: false }; } const now = new Date(); const dateStr = event.date as string; // 统一使用 Date 对象处理 const eventDate = new Date(dateStr); // 检查是否包含有效时间(小时和分钟不全为0) const hours = eventDate.getHours(); const minutes = eventDate.getMinutes(); const hasExplicitTime = hours !== 0 || minutes !== 0; const diff = eventDate.getTime() - now.getTime(); const isPast = diff < 0; const isToday = eventDate.toDateString() === now.toDateString(); // 格式化显示 let timeStr: string; if (hasExplicitTime) { 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, hasTime: hasExplicitTime }; }, [event.date]); // 获取文字颜色 const getTextColor = () => { if (isCompleted) return '#999'; if (timeInfo.isPast) return '#666'; return '#1a1a1a'; }; // 获取时间颜色 const getTimeColor = () => { if (isCompleted) return '#bbb'; if (timeInfo.isPast) return '#c41c1c'; return '#666'; }; // 获取背景色 const getBackground = () => { if (isCompleted) return 'rgba(0, 0, 0, 0.02)'; if (isMissed) return 'rgba(196, 28, 28, 0.03)'; return 'rgba(0, 0, 0, 0.02)'; }; // 获取边框颜色 const getBorderColor = () => { if (isCompleted) return 'rgba(0, 0, 0, 0.06)'; if (isMissed) return 'rgba(196, 28, 28, 0.15)'; return 'rgba(0, 0, 0, 0.06)'; }; // 获取优先级的checkbox边框样式 const getCheckboxBorderStyle = () => { const priority = event.priority || 'none'; const priorityColor = getPriorityColor(priority); const isHighPriority = priority !== 'none'; return { borderColor: priorityColor, borderWidth: isHighPriority ? '2px' : '1px', borderStyle: 'solid' as const, }; }; // 处理日期调整 const handleDateAdjust = useCallback((days: number) => { const now = new Date(); const targetDate = new Date(now); targetDate.setDate(targetDate.getDate() + days); // 格式化为 YYYY-MM-DD const formattedDate = targetDate.toISOString().split('T')[0]; onDateChange?.(formattedDate, event.repeat_type, event.repeat_interval ?? null); closeMenu(); }, [onDateChange, event.repeat_type, event.repeat_interval, closeMenu]); // 处理优先级设置 const handlePriorityChange = useCallback((priority: PriorityType) => { onPriorityChange?.(priority); closeMenu(); }, [onPriorityChange, closeMenu]); return ( <> setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onContextMenu={handleContextMenu} data-reminder-card={event.id} style={{ cursor: 'pointer', opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1, transform: isHovered ? 'translateY(-1px)' : 'translateY(0)', background: getBackground(), border: `1px solid ${getBorderColor()}`, borderLeft: isMissed && !isCompleted ? '3px solid #c41c1c' : undefined, transition: 'all 0.3s ease', animation: isAnimating ? 'reminder-card-fadeOut 0.3s ease-out forwards' : 'none', }} > {/* 正常提醒卡片:左右分栏布局 */} {!isMissed ? ( {/* 左侧:Checkbox + 标题 + 内容 */} { e.stopPropagation(); onClick(); }} > {/* Checkbox */}
{ e.stopPropagation(); onToggle(); }} > {/* 已完成时显示的对勾 */} {isCompleted && ( )}
{/* 标题和内容垂直排列 */} {/* 标题 */} {event.title} {/* 内容在标题下方 */} {event.content && ( {event.content} )}
{/* 右侧:循环图标 + 日期时间 */} {event.repeat_type !== 'none' && ( )} {timeInfo.timeStr}
) : ( /* 逾期提醒卡片 */
{ e.stopPropagation(); if (isMissed && !isCompleted) { setIsAnimating(true); setTimeout(() => { setIsAnimating(false); onToggle(); onMissedToggle?.(); }, 300); } else { onToggle(); } }} > {/* 已完成时显示的对勾 */} {isCompleted && ( )}
{event.title} {event.repeat_type !== 'none' && ( )} {timeInfo.timeStr} {event.content && ( {event.content} )}
{onPostpone && ( { e.stopPropagation(); onPostpone(); }} style={{ color: '#e67e22', opacity: isHovered ? 1 : 0, transition: 'all 0.2s ease', }} title="顺延到今天" > )} { e.stopPropagation(); onClick(); }} style={{ color: '#999', opacity: isHovered ? 1 : 0, transition: 'all 0.2s ease', }} title="编辑" >
)}
{/* 右键菜单 - 使用Portal渲染到body */} {isMenuOpen && cardRef.current && ( e.stopPropagation()} > {/* 调整时间 */} 调整时间 handleDateAdjust(0)} > 今天 handleDateAdjust(1)} > 明天 handleDateAdjust(7)} > 一周后 {/* 颜色 */} 颜色 {(Object.keys(PRIORITY_COLORS) as PriorityType[]).map((priority) => ( handlePriorityChange(priority)} > {PRIORITY_NAMES[priority]} ))} {/* 操作 */} { onClick(); closeMenu(); }} > 编辑 { onDelete?.(); closeMenu(); }} > 删除 )} ); }