feat: 完善提醒卡片右键菜单功能

- 优化checkbox样式:缩小尺寸(14px)、移除阴影、添加白色填充
- 调整布局:标题和内容左对齐
- 重构右键菜单为垂直分类布局:调整时间/颜色/操作
- 添加菜单边缘保护:自动计算位置避免超出浏览器
- 添加点击外部和ESC键关闭菜单
- 编辑弹窗优先级改为颜色圆点选择器
- 添加priority类型定义

🤖 Generated with Claude Code
This commit is contained in:
ddshi 2026-02-05 17:51:59 +08:00
parent 1559e603b0
commit f0cbd0e33c
5 changed files with 722 additions and 224 deletions

View File

@ -1,8 +1,47 @@
import { useMemo, useState } from 'react'; import { useMemo, useState, useCallback, useEffect, useRef } from 'react';
import { Paper, Text, Checkbox, Group, Stack, ActionIcon, Box } from '@mantine/core'; import {
import { IconDots, IconArrowForward, IconRepeat, IconRepeatOff } from '@tabler/icons-react'; Paper,
import type { Event, RepeatType } from '../../types'; 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 { getRepeatTypeLabel } from '../../utils/repeatCalculator';
import { useContextMenuStore } from '../../stores/contextMenu';
// 重复图标颜色常量
const REPEAT_ICON_COLOR = '#999';
// 优先级颜色映射
const PRIORITY_COLORS: Record<PriorityType, string> = {
none: 'rgba(0, 0, 0, 0.15)',
red: '#dc2626',
green: '#16a34a',
yellow: '#ca8a04',
};
// 优先级名称映射
const PRIORITY_NAMES: Record<PriorityType, string> = {
none: '默认',
red: '红色',
green: '绿色',
yellow: '黄色',
};
interface ReminderCardProps { interface ReminderCardProps {
event: Event; event: Event;
@ -12,12 +51,99 @@ interface ReminderCardProps {
onPostpone?: () => void; onPostpone?: () => void;
isMissed?: boolean; isMissed?: boolean;
onMissedToggle?: () => void; 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 }: ReminderCardProps) { export function ReminderCard({
event,
onToggle,
onClick,
onDelete,
onPostpone,
isMissed = false,
onMissedToggle,
onDateChange,
onPriorityChange,
}: 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 cardRef = useRef<HTMLDivElement>(null);
// 使用全局状态管理右键菜单
const openEventId = useContextMenuStore((state) => state.openEventId);
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);
// 打开菜单
openMenu(event.id);
}, [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 = () => {
if (!cardRef.current) return { left: 0, top: 0 };
const cardRect = cardRef.current.getBoundingClientRect();
const menuWidth = 180; // 菜单宽度
const menuHeight = 150; // 估算菜单高度
const padding = 8; // 边缘padding
// 计算左侧位置
let left = cardRect.left;
if (left + menuWidth > window.innerWidth - padding) {
left = window.innerWidth - menuWidth - padding;
}
if (left < padding) left = padding;
// 计算顶部位置(优先显示在下方)
let top = cardRect.bottom + 4;
if (top + menuHeight > window.innerHeight - padding) {
// 如果下方空间不足,显示在上方
top = cardRect.top - menuHeight - 4;
}
if (top < padding) top = padding;
return { left, top };
};
// 计算时间信息 // 计算时间信息
const timeInfo = useMemo(() => { const timeInfo = useMemo(() => {
@ -33,7 +159,6 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
const eventDate = new Date(dateStr); const eventDate = new Date(dateStr);
// 检查是否包含有效时间小时和分钟不全为0 // 检查是否包含有效时间小时和分钟不全为0
// 对于本地格式(如 "2025-02-05T00:00:00")和 ISO 格式都适用
const hours = eventDate.getHours(); const hours = eventDate.getHours();
const minutes = eventDate.getMinutes(); const minutes = eventDate.getMinutes();
const hasExplicitTime = hours !== 0 || minutes !== 0; const hasExplicitTime = hours !== 0 || minutes !== 0;
@ -45,7 +170,6 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
// 格式化显示 // 格式化显示
let timeStr: string; let timeStr: string;
if (hasExplicitTime) { if (hasExplicitTime) {
// 有设置时间,显示日期+时间
timeStr = eventDate.toLocaleString('zh-CN', { timeStr = eventDate.toLocaleString('zh-CN', {
month: 'numeric', month: 'numeric',
day: 'numeric', day: 'numeric',
@ -54,7 +178,6 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
hour12: false, hour12: false,
}); });
} else { } else {
// 没有设置时间00:00只显示日期
timeStr = eventDate.toLocaleString('zh-CN', { timeStr = eventDate.toLocaleString('zh-CN', {
month: 'numeric', month: 'numeric',
day: 'numeric', day: 'numeric',
@ -92,24 +215,46 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
return 'rgba(0, 0, 0, 0.06)'; return 'rgba(0, 0, 0, 0.06)';
}; };
// 获取循环图标颜色 // 获取优先级的checkbox边框样式
const getRepeatIconColor = (type: RepeatType) => { const getCheckboxBorderStyle = () => {
const colors: Record<RepeatType, string> = { const priority = event.priority || 'none';
daily: '#3b82f6', // 蓝色 const priorityColor = getPriorityColor(priority);
weekly: '#22c55e', // 绿色 const isHighPriority = priority !== 'none';
monthly: '#a855f7', // 紫色 return {
yearly: '#f59e0b', // 橙色 borderColor: priorityColor,
none: '#999', borderWidth: isHighPriority ? '2px' : '1px',
borderStyle: 'solid' as const,
}; };
return colors[type] || '#999';
}; };
// 处理日期调整
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 ( return (
<>
<Paper <Paper
ref={cardRef}
p="sm" p="sm"
radius={2} radius={2}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
onContextMenu={handleContextMenu}
data-reminder-card={event.id}
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1, opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1,
@ -131,6 +276,15 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
0% { opacity: 1; transform: translateX(0); } 0% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(20px); } 100% { opacity: 0; transform: translateX(20px); }
} }
/* 自定义checkbox样式去掉默认边框 */
.reminder-checkbox input[type="checkbox"] {
border-radius: 50% !important;
border: none !important;
background: transparent !important;
}
.reminder-checkbox .mantine-Checkbox-icon {
color: inherit !important;
}
`}</style> `}</style>
{/* 正常提醒卡片:左右分栏布局 */} {/* 正常提醒卡片:左右分栏布局 */}
{!isMissed ? ( {!isMissed ? (
@ -139,39 +293,56 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
<Box <Box
style={{ flex: 1, minWidth: 0, cursor: 'pointer' }} style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
onClick={(e) => { onClick={(e) => {
// 阻止事件冒泡到 Card避免重复触发
e.stopPropagation(); e.stopPropagation();
onClick(); onClick();
}} }}
> >
<Group gap="sm" wrap="nowrap" align="flex-start"> <Box style={{ display: 'flex', alignItems: 'flex-start' }}>
{/* Checkbox - 单独处理,不触发卡片点击 */} {/* Checkbox */}
<div <div
className="reminder-checkbox"
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'center',
paddingTop: 2, justifyContent: 'center',
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none', animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
width: 14,
height: 14,
borderRadius: '50%',
border: `1px solid ${getPriorityColor(event.priority || 'none')}`,
background: isCompleted ? getPriorityColor(event.priority || 'none') : '#ffffff',
cursor: 'pointer',
transition: 'all 0.2s ease',
flexShrink: 0,
position: 'relative',
marginTop: 2,
}} }}
onClick={(e) => { onClick={(e) => {
// 阻止事件冒泡,避免触发卡片点击
e.stopPropagation();
}}
>
<Checkbox
checked={isCompleted}
onChange={(e) => {
e.stopPropagation(); e.stopPropagation();
onToggle(); onToggle();
}} }}
size="xs" >
color="#1a1a1a" {/* 已完成时显示的对勾 */}
{isCompleted && (
<span
style={{ style={{
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none', position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
color: '#fff',
fontSize: '8px',
fontWeight: 'bold',
lineHeight: 1,
}} }}
/> >
</span>
)}
</div> </div>
{/* 标题和内容垂直排列 */}
<Box style={{ flex: 1, marginLeft: 8 }}>
{/* 标题 */} {/* 标题 */}
<Text <Text
fw={400} fw={400}
@ -186,7 +357,6 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
> >
{event.title} {event.title}
</Text> </Text>
</Group>
{/* 内容在标题下方 */} {/* 内容在标题下方 */}
{event.content && ( {event.content && (
@ -195,7 +365,7 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
c={isCompleted ? '#bbb' : '#999'} c={isCompleted ? '#bbb' : '#999'}
lineClamp={1} lineClamp={1}
style={{ style={{
marginLeft: 28, marginTop: 2,
opacity: isCompleted ? 0.6 : 1, opacity: isCompleted ? 0.6 : 1,
transition: 'opacity 0.2s ease', transition: 'opacity 0.2s ease',
}} }}
@ -204,6 +374,8 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
</Text> </Text>
)} )}
</Box> </Box>
</Box>
</Box>
{/* 右侧:循环图标 + 日期时间 */} {/* 右侧:循环图标 + 日期时间 */}
<Box style={{ flex: '0 0 auto', minWidth: 0, paddingLeft: 12, display: 'flex', alignItems: 'center', gap: 6 }}> <Box style={{ flex: '0 0 auto', minWidth: 0, paddingLeft: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
@ -212,7 +384,7 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
color: getRepeatIconColor(event.repeat_type), color: REPEAT_ICON_COLOR,
}} }}
title={getRepeatTypeLabel(event.repeat_type)} title={getRepeatTypeLabel(event.repeat_type)}
> >
@ -229,19 +401,27 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
</Box> </Box>
</Group> </Group>
) : ( ) : (
/* 逾期提醒卡片:保持原有结构 */ /* 逾期提醒卡片 */
<Group justify="space-between" wrap="nowrap"> <Group justify="space-between" wrap="nowrap">
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}> <Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
<div <div
className="reminder-checkbox"
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none', animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
width: 14,
height: 14,
borderRadius: '50%',
border: `1px solid ${getPriorityColor(event.priority || 'none')}`,
background: isCompleted ? getPriorityColor(event.priority || 'none') : '#ffffff',
cursor: 'pointer',
transition: 'all 0.2s ease',
flexShrink: 0,
position: 'relative',
}} }}
> onClick={(e) => {
<Checkbox
checked={isCompleted}
onChange={(e) => {
e.stopPropagation(); e.stopPropagation();
if (isMissed && !isCompleted) { if (isMissed && !isCompleted) {
setIsAnimating(true); setIsAnimating(true);
@ -254,9 +434,24 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
onToggle(); onToggle();
} }
}} }}
size="xs" >
color="#1a1a1a" {/* 已完成时显示的对勾 */}
/> {isCompleted && (
<span
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
color: '#fff',
fontSize: '10px',
fontWeight: 'bold',
lineHeight: 1,
}}
>
</span>
)}
</div> </div>
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}> <Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
@ -280,7 +475,7 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
flexShrink: 0, flexShrink: 0,
color: getRepeatIconColor(event.repeat_type), color: REPEAT_ICON_COLOR,
}} }}
title={getRepeatTypeLabel(event.repeat_type)} title={getRepeatTypeLabel(event.repeat_type)}
> >
@ -352,5 +547,144 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
</Group> </Group>
)} )}
</Paper> </Paper>
{/* 右键菜单 - 使用Portal渲染到body */}
{isMenuOpen && cardRef.current && (
<Portal>
<Box
data-context-menu
style={{
position: 'absolute',
left: getMenuPosition().left,
top: getMenuPosition().top,
zIndex: 1000,
background: '#fff',
border: '1px solid rgba(0, 0, 0, 0.08)',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
padding: '6px 8px',
minWidth: 180,
}}
onMouseDown={(e) => e.stopPropagation()}
>
{/* 调整时间 */}
<Box style={{ marginBottom: 6 }}>
<Text size="xs" c="#999" fw={400} style={{ marginBottom: 4, paddingLeft: 4, letterSpacing: '0.05em' }}>
</Text>
<Group gap={4}>
<ActionIcon
variant="subtle"
size="xs"
style={{
flex: 1,
height: 24,
color: '#666',
background: 'transparent',
}}
onClick={() => handleDateAdjust(0)}
>
<Text size="xs"></Text>
</ActionIcon>
<ActionIcon
variant="subtle"
size="xs"
style={{
flex: 1,
height: 24,
color: '#666',
background: 'transparent',
}}
onClick={() => handleDateAdjust(1)}
>
<Text size="xs"></Text>
</ActionIcon>
<ActionIcon
variant="subtle"
size="xs"
style={{
flex: 1,
height: 24,
color: '#666',
background: 'transparent',
}}
onClick={() => handleDateAdjust(7)}
>
<Text size="xs"></Text>
</ActionIcon>
</Group>
</Box>
<Divider my={4} style={{ borderColor: 'rgba(0, 0, 0, 0.06)' }} />
{/* 颜色 */}
<Box style={{ marginBottom: 6 }}>
<Text size="xs" c="#999" fw={400} style={{ marginBottom: 4, paddingLeft: 4, letterSpacing: '0.05em' }}>
</Text>
<Group gap={4}>
{(Object.keys(PRIORITY_COLORS) as PriorityType[]).map((priority) => (
<ActionIcon
key={priority}
variant={event.priority === priority ? 'filled' : 'subtle'}
size="xs"
style={{
flex: 1,
height: 24,
background: event.priority === priority ? PRIORITY_COLORS[priority] : 'transparent',
color: event.priority === priority ? '#fff' : '#666',
border: `1px solid ${event.priority === priority ? 'transparent' : 'rgba(0, 0, 0, 0.1)'}`,
}}
onClick={() => handlePriorityChange(priority)}
>
<Text size="xs" fw={400}>
{PRIORITY_NAMES[priority]}
</Text>
</ActionIcon>
))}
</Group>
</Box>
<Divider my={4} style={{ borderColor: 'rgba(0, 0, 0, 0.06)' }} />
{/* 操作 */}
<Group gap={4}>
<ActionIcon
variant="subtle"
size="xs"
style={{
flex: 1,
height: 24,
color: '#666',
background: 'transparent',
}}
onClick={() => {
onClick();
closeMenu();
}}
>
<Text size="xs" fw={400}></Text>
</ActionIcon>
<ActionIcon
variant="subtle"
size="xs"
style={{
flex: 1,
height: 24,
color: '#dc2626',
background: 'transparent',
}}
onClick={() => {
onDelete?.();
closeMenu();
}}
>
<Text size="xs" fw={400}></Text>
</ActionIcon>
</Group>
</Box>
</Portal>
)}
</>
); );
} }

View File

@ -1,4 +1,4 @@
import { useMemo, useRef, useState, useEffect } from 'react'; import { useMemo, useRef, useState } from 'react';
import { import {
Stack, Stack,
Text, Text,
@ -18,7 +18,7 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ReminderCard } from './ReminderCard'; import { ReminderCard } from './ReminderCard';
import type { Event } from '../../types'; import type { Event, PriorityType } from '../../types';
interface ReminderListProps { interface ReminderListProps {
events: Event[]; events: Event[];
@ -27,6 +27,8 @@ interface ReminderListProps {
onAddClick: () => void; onAddClick: () => void;
onDelete?: (event: Event) => void; onDelete?: (event: Event) => void;
onPostpone?: (event: Event) => void; onPostpone?: (event: Event) => void;
onDateChange?: (event: Event, date: string, repeatType: string, repeatInterval: number | null) => void;
onPriorityChange?: (event: Event, priority: PriorityType) => void;
} }
export function ReminderList({ export function ReminderList({
@ -36,6 +38,8 @@ export function ReminderList({
onAddClick, onAddClick,
onDelete, onDelete,
onPostpone, onPostpone,
onDateChange,
onPriorityChange,
}: ReminderListProps) { }: ReminderListProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
@ -299,6 +303,8 @@ export function ReminderList({
onDelete={onDelete ? () => onDelete(event) : undefined} onDelete={onDelete ? () => onDelete(event) : undefined}
onPostpone={onPostpone ? () => onPostpone(event) : undefined} onPostpone={onPostpone ? () => onPostpone(event) : undefined}
onMissedToggle={triggerArchiveShake} onMissedToggle={triggerArchiveShake}
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
isMissed={true} isMissed={true}
/> />
))} ))}
@ -329,6 +335,8 @@ export function ReminderList({
onDelete={onDelete ? () => onDelete(event) : undefined} onDelete={onDelete ? () => onDelete(event) : undefined}
onPostpone={onPostpone ? () => onPostpone(event) : undefined} onPostpone={onPostpone ? () => onPostpone(event) : undefined}
onMissedToggle={triggerArchiveShake} onMissedToggle={triggerArchiveShake}
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
isMissed={true} isMissed={true}
/> />
))} ))}
@ -376,6 +384,8 @@ 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}
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
/> />
))} ))}
</> </>
@ -405,6 +415,8 @@ 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}
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
/> />
))} ))}
</> </>
@ -434,6 +446,8 @@ 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}
onDateChange={onDateChange ? (date, repeatType, repeatInterval) => onDateChange(event, date, repeatType, repeatInterval) : undefined}
onPriorityChange={onPriorityChange ? (priority) => onPriorityChange(event, priority) : undefined}
/> />
))} ))}
</> </>

View File

@ -11,6 +11,7 @@ import {
Switch, Switch,
Select, Select,
Stack, Stack,
Box,
} from '@mantine/core'; } from '@mantine/core';
import { DatePickerInput, TimeInput } from '@mantine/dates'; import { DatePickerInput, TimeInput } from '@mantine/dates';
import { IconLogout, IconSettings } from '@tabler/icons-react'; import { IconLogout, IconSettings } from '@tabler/icons-react';
@ -21,7 +22,7 @@ import { AnniversaryList } from '../components/anniversary/AnniversaryList';
import { ReminderList } from '../components/reminder/ReminderList'; 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 } from '../types'; import type { Event, EventType, RepeatType, PriorityType } from '../types';
import { calculateNextReminderDate } from '../utils/repeatCalculator'; import { calculateNextReminderDate } from '../utils/repeatCalculator';
export function HomePage() { export function HomePage() {
@ -48,6 +49,7 @@ export function HomePage() {
const [formIsLunar, setFormIsLunar] = useState(false); const [formIsLunar, setFormIsLunar] = useState(false);
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');
// Initialize auth and data on mount // Initialize auth and data on mount
useEffect(() => { useEffect(() => {
@ -70,6 +72,7 @@ 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);
setFormPriority(event.priority || 'none');
// 提取时间(如果日期中包含时间信息) // 提取时间(如果日期中包含时间信息)
const eventDate = new Date(event.date); const eventDate = new Date(event.date);
const hours = eventDate.getHours(); const hours = eventDate.getHours();
@ -89,6 +92,7 @@ export function HomePage() {
setFormIsLunar(false); setFormIsLunar(false);
setFormRepeatType('none'); setFormRepeatType('none');
setFormIsHoliday(type === 'anniversary'); setFormIsHoliday(type === 'anniversary');
setFormPriority('none');
open(); open();
}; };
@ -123,6 +127,7 @@ export function HomePage() {
// 计算下一次提醒日期 // 计算下一次提醒日期
next_reminder_date: calculateNextReminderDate(dateStr, formRepeatType, undefined), next_reminder_date: calculateNextReminderDate(dateStr, formRepeatType, undefined),
is_holiday: formIsHoliday ?? false, is_holiday: formIsHoliday ?? false,
priority: formPriority,
}; };
// 只有当 content 有值时才包含 // 只有当 content 有值时才包含
@ -180,14 +185,80 @@ export function HomePage() {
// 构建新的本地时间字符串 // 构建新的本地时间字符串
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 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, { // 构建更新数据
const updateData: Record<string, any> = {
date: newDateStr, date: newDateStr,
}); priority: event.priority || 'none',
};
// 如果是重复提醒,同步更新 next_reminder_date
if (event.repeat_type && event.repeat_type !== 'none') {
const nextReminderDate = calculateNextReminderDate(
newDateStr,
event.repeat_type as RepeatType,
event.repeat_interval || undefined
);
updateData.next_reminder_date = nextReminderDate;
}
const result = await updateEventById(event.id, updateData);
if (result.error) { if (result.error) {
console.error('顺延失败:', result.error); console.error('顺延失败:', result.error);
} }
}; };
const handleDateChange = async (
event: Event,
date: string,
repeatType: string,
repeatInterval: number | null
) => {
if (event.type !== 'reminder') return;
// 保留原事件的时间信息
const originalDate = new Date(event.date);
const hours = originalDate.getHours();
const minutes = originalDate.getMinutes();
// 构建新的本地时间字符串
const newDateStr = `${date}T${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
// 构建更新数据
const updateData: Record<string, any> = {
date: newDateStr,
priority: event.priority || 'none',
};
// 如果是重复提醒,同步更新 next_reminder_date
if (repeatType && repeatType !== 'none') {
const nextReminderDate = calculateNextReminderDate(
newDateStr,
repeatType as RepeatType,
repeatInterval || undefined
);
updateData.next_reminder_date = nextReminderDate;
}
// 合并更新
const result = await updateEventById(event.id, updateData);
if (result.error) {
console.error('日期调整失败:', result.error);
}
fetchEvents(); // 刷新列表以更新UI
};
const handlePriorityChange = async (event: Event, priority: PriorityType) => {
if (event.type !== 'reminder') return;
// 合并更新 priority 和 date确保 updated_at 被更新
const result = await updateEventById(event.id, {
priority,
date: event.date,
});
if (result.error) {
console.error('优先级设置失败:', result.error);
}
fetchEvents(); // 刷新列表以更新UI
};
const resetForm = () => { const resetForm = () => {
setFormType('anniversary'); setFormType('anniversary');
setFormTitle(''); setFormTitle('');
@ -197,6 +268,7 @@ export function HomePage() {
setFormIsLunar(false); setFormIsLunar(false);
setFormRepeatType('none'); setFormRepeatType('none');
setFormIsHoliday(false); setFormIsHoliday(false);
setFormPriority('none');
setSelectedEvent(null); setSelectedEvent(null);
setIsEdit(false); setIsEdit(false);
}; };
@ -286,6 +358,8 @@ export function HomePage() {
onAddClick={() => handleAddClick('reminder')} onAddClick={() => handleAddClick('reminder')}
onDelete={handleDelete} onDelete={handleDelete}
onPostpone={handlePostpone} onPostpone={handlePostpone}
onDateChange={handleDateChange}
onPriorityChange={handlePriorityChange}
/> />
</div> </div>
@ -460,6 +534,38 @@ export function HomePage() {
}} }}
/> />
{/* Priority (only for reminders) */}
{formType === 'reminder' && (
<Box>
<Text size="xs" c="#666" style={{ letterSpacing: '0.05em', marginBottom: 8 }}>
</Text>
<Group gap={8}>
{([
{ value: 'none' as const, color: 'rgba(0, 0, 0, 0.15)' },
{ value: 'red' as const, color: '#dc2626' },
{ value: 'green' as const, color: '#16a34a' },
{ value: 'yellow' as const, color: '#ca8a04' },
].map((item) => (
<Box
key={item.value}
onClick={() => setFormPriority(item.value)}
style={{
width: 24,
height: 24,
borderRadius: '50%',
background: item.color,
border: formPriority === item.value ? '2px solid #1a1a1a' : '1px solid rgba(0, 0, 0, 0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease',
transform: formPriority === item.value ? 'scale(1.1)' : 'scale(1)',
}}
/>
)))}
</Group>
</Box>
)}
{/* Holiday switch (only for anniversaries) */} {/* Holiday switch (only for anniversaries) */}
{formType === 'anniversary' && ( {formType === 'anniversary' && (
<Switch <Switch

40
src/stores/contextMenu.ts Normal file
View File

@ -0,0 +1,40 @@
import { create } from 'zustand';
// 右键菜单状态管理
interface ContextMenuState {
// 当前打开菜单的事件IDnull表示没有菜单打开
openEventId: string | null;
// 打开菜单
openMenu: (eventId: string) => void;
// 关闭菜单
closeMenu: () => void;
// 切换菜单
toggleMenu: (eventId: string) => void;
// 点击其他位置关闭菜单
closeOnClickOutside: (eventId: string) => void;
}
export const useContextMenuStore = create<ContextMenuState>((set) => ({
openEventId: null,
openMenu: (eventId: string) => {
set({ openEventId: eventId });
},
closeMenu: () => {
set({ openEventId: null });
},
toggleMenu: (eventId: string) => {
set((state) => ({
openEventId: state.openEventId === eventId ? null : eventId,
}));
},
// 点击其他位置时如果点击的是不同的事件ID则关闭菜单
closeOnClickOutside: (clickedEventId: string) => {
set((state) => ({
openEventId: state.openEventId === clickedEventId ? state.openEventId : null,
}));
},
}));

View File

@ -13,6 +13,9 @@ export type EventType = 'anniversary' | 'reminder';
// Repeat types: 'daily'每天, 'weekly'每周, 'monthly'每月, 'yearly'每年, 'none'不重复 // Repeat types: 'daily'每天, 'weekly'每周, 'monthly'每月, 'yearly'每年, 'none'不重复
export type RepeatType = 'daily' | 'weekly' | 'monthly' | 'yearly' | 'none'; export type RepeatType = 'daily' | 'weekly' | 'monthly' | 'yearly' | 'none';
// Priority types: 'none'无色, 'red'红色, 'green'绿色, 'yellow'黄色
export type PriorityType = 'none' | 'red' | 'green' | 'yellow';
// Unified event type (matches backend API) // Unified event type (matches backend API)
export interface Event { export interface Event {
id: string; id: string;
@ -27,6 +30,7 @@ export interface Event {
next_reminder_date?: string | null; // 下一次提醒日期(计算用) next_reminder_date?: string | null; // 下一次提醒日期(计算用)
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
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }