feat: 完善提醒卡片右键菜单功能
- 优化checkbox样式:缩小尺寸(14px)、移除阴影、添加白色填充
- 调整布局:标题和内容左对齐
- 重构右键菜单为垂直分类布局:调整时间/颜色/操作
- 添加菜单边缘保护:自动计算位置避免超出浏览器
- 添加点击外部和ESC键关闭菜单
- 编辑弹窗优先级改为颜色圆点选择器
- 添加priority类型定义
🤖 Generated with Claude Code
This commit is contained in:
parent
1559e603b0
commit
f0cbd0e33c
@ -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,156 +215,213 @@ 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
|
<>
|
||||||
p="sm"
|
<Paper
|
||||||
radius={2}
|
ref={cardRef}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
p="sm"
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
radius={2}
|
||||||
style={{
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
cursor: 'pointer',
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1,
|
onContextMenu={handleContextMenu}
|
||||||
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)',
|
data-reminder-card={event.id}
|
||||||
background: getBackground(),
|
style={{
|
||||||
border: `1px solid ${getBorderColor()}`,
|
cursor: 'pointer',
|
||||||
borderLeft: isMissed && !isCompleted ? '3px solid #c41c1c' : undefined,
|
opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1,
|
||||||
transition: 'all 0.3s ease',
|
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)',
|
||||||
animation: isAnimating ? 'reminder-card-fadeOut 0.3s ease-out forwards' : 'none',
|
background: getBackground(),
|
||||||
}}
|
border: `1px solid ${getBorderColor()}`,
|
||||||
>
|
borderLeft: isMissed && !isCompleted ? '3px solid #c41c1c' : undefined,
|
||||||
<style>{`
|
transition: 'all 0.3s ease',
|
||||||
@keyframes reminder-card-pulse {
|
animation: isAnimating ? 'reminder-card-fadeOut 0.3s ease-out forwards' : 'none',
|
||||||
0% { transform: scale(1); }
|
}}
|
||||||
50% { transform: scale(1.15); }
|
>
|
||||||
100% { transform: scale(1); }
|
<style>{`
|
||||||
}
|
@keyframes reminder-card-pulse {
|
||||||
@keyframes reminder-card-fadeOut {
|
0% { transform: scale(1); }
|
||||||
0% { opacity: 1; transform: translateX(0); }
|
50% { transform: scale(1.15); }
|
||||||
100% { opacity: 0; transform: translateX(20px); }
|
100% { transform: scale(1); }
|
||||||
}
|
}
|
||||||
`}</style>
|
@keyframes reminder-card-fadeOut {
|
||||||
{/* 正常提醒卡片:左右分栏布局 */}
|
0% { opacity: 1; transform: translateX(0); }
|
||||||
{!isMissed ? (
|
100% { opacity: 0; transform: translateX(20px); }
|
||||||
<Group justify="space-between" wrap="nowrap" align="flex-start">
|
}
|
||||||
{/* 左侧:Checkbox + 标题 + 内容 */}
|
/* 自定义checkbox样式,去掉默认边框 */
|
||||||
<Box
|
.reminder-checkbox input[type="checkbox"] {
|
||||||
style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
|
border-radius: 50% !important;
|
||||||
onClick={(e) => {
|
border: none !important;
|
||||||
// 阻止事件冒泡到 Card,避免重复触发
|
background: transparent !important;
|
||||||
e.stopPropagation();
|
}
|
||||||
onClick();
|
.reminder-checkbox .mantine-Checkbox-icon {
|
||||||
}}
|
color: inherit !important;
|
||||||
>
|
}
|
||||||
<Group gap="sm" wrap="nowrap" align="flex-start">
|
`}</style>
|
||||||
{/* Checkbox - 单独处理,不触发卡片点击 */}
|
{/* 正常提醒卡片:左右分栏布局 */}
|
||||||
<div
|
{!isMissed ? (
|
||||||
style={{
|
<Group justify="space-between" wrap="nowrap" align="flex-start">
|
||||||
display: 'flex',
|
{/* 左侧:Checkbox + 标题 + 内容 */}
|
||||||
alignItems: 'flex-start',
|
<Box
|
||||||
paddingTop: 2,
|
style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
|
||||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
onClick={(e) => {
|
||||||
}}
|
e.stopPropagation();
|
||||||
onClick={(e) => {
|
onClick();
|
||||||
// 阻止事件冒泡,避免触发卡片点击
|
}}
|
||||||
e.stopPropagation();
|
>
|
||||||
}}
|
<Box style={{ display: 'flex', alignItems: 'flex-start' }}>
|
||||||
>
|
{/* Checkbox */}
|
||||||
<Checkbox
|
<div
|
||||||
checked={isCompleted}
|
className="reminder-checkbox"
|
||||||
onChange={(e) => {
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
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) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggle();
|
onToggle();
|
||||||
}}
|
}}
|
||||||
size="xs"
|
>
|
||||||
color="#1a1a1a"
|
{/* 已完成时显示的对勾 */}
|
||||||
|
{isCompleted && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '8px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题和内容垂直排列 */}
|
||||||
|
<Box style={{ flex: 1, marginLeft: 8 }}>
|
||||||
|
{/* 标题 */}
|
||||||
|
<Text
|
||||||
|
fw={400}
|
||||||
|
size="xs"
|
||||||
|
lineClamp={1}
|
||||||
|
style={{
|
||||||
|
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||||
|
color: getTextColor(),
|
||||||
|
letterSpacing: '0.03em',
|
||||||
|
transition: 'color 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 内容在标题下方 */}
|
||||||
|
{event.content && (
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c={isCompleted ? '#bbb' : '#999'}
|
||||||
|
lineClamp={1}
|
||||||
|
style={{
|
||||||
|
marginTop: 2,
|
||||||
|
opacity: isCompleted ? 0.6 : 1,
|
||||||
|
transition: 'opacity 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{event.content}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 右侧:循环图标 + 日期时间 */}
|
||||||
|
<Box style={{ flex: '0 0 auto', minWidth: 0, paddingLeft: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
{event.repeat_type !== 'none' && (
|
||||||
|
<Box
|
||||||
style={{
|
style={{
|
||||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: REPEAT_ICON_COLOR,
|
||||||
}}
|
}}
|
||||||
/>
|
title={getRepeatTypeLabel(event.repeat_type)}
|
||||||
</div>
|
>
|
||||||
|
<IconRepeat size={12} />
|
||||||
{/* 标题 */}
|
</Box>
|
||||||
<Text
|
)}
|
||||||
fw={400}
|
|
||||||
size="xs"
|
|
||||||
lineClamp={1}
|
|
||||||
style={{
|
|
||||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
|
||||||
color: getTextColor(),
|
|
||||||
letterSpacing: '0.03em',
|
|
||||||
transition: 'color 0.2s ease',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{event.title}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
{/* 内容在标题下方 */}
|
|
||||||
{event.content && (
|
|
||||||
<Text
|
<Text
|
||||||
size="xs"
|
size="xs"
|
||||||
c={isCompleted ? '#bbb' : '#999'}
|
c={getTimeColor()}
|
||||||
lineClamp={1}
|
style={{ letterSpacing: '0.05em', whiteSpace: 'nowrap' }}
|
||||||
style={{
|
|
||||||
marginLeft: 28,
|
|
||||||
opacity: isCompleted ? 0.6 : 1,
|
|
||||||
transition: 'opacity 0.2s ease',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{event.content}
|
{timeInfo.timeStr}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
</Group>
|
||||||
|
) : (
|
||||||
{/* 右侧:循环图标 + 日期时间 */}
|
/* 逾期提醒卡片 */
|
||||||
<Box style={{ flex: '0 0 auto', minWidth: 0, paddingLeft: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
|
<Group justify="space-between" wrap="nowrap">
|
||||||
{event.repeat_type !== 'none' && (
|
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Box
|
<div
|
||||||
|
className="reminder-checkbox"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
color: getRepeatIconColor(event.repeat_type),
|
justifyContent: 'center',
|
||||||
|
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',
|
||||||
}}
|
}}
|
||||||
title={getRepeatTypeLabel(event.repeat_type)}
|
onClick={(e) => {
|
||||||
>
|
|
||||||
<IconRepeat size={12} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={getTimeColor()}
|
|
||||||
style={{ letterSpacing: '0.05em', whiteSpace: 'nowrap' }}
|
|
||||||
>
|
|
||||||
{timeInfo.timeStr}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
) : (
|
|
||||||
/* 逾期提醒卡片:保持原有结构 */
|
|
||||||
<Group justify="space-between" wrap="nowrap">
|
|
||||||
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={isCompleted}
|
|
||||||
onChange={(e) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isMissed && !isCompleted) {
|
if (isMissed && !isCompleted) {
|
||||||
setIsAnimating(true);
|
setIsAnimating(true);
|
||||||
@ -254,103 +434,257 @@ export function ReminderCard({ event, onToggle, onClick, onDelete, onPostpone, i
|
|||||||
onToggle();
|
onToggle();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
size="xs"
|
|
||||||
color="#1a1a1a"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
|
|
||||||
<Group gap={4} wrap="nowrap">
|
|
||||||
<Text
|
|
||||||
fw={400}
|
|
||||||
size="xs"
|
|
||||||
lineClamp={1}
|
|
||||||
style={{
|
|
||||||
textDecoration: isCompleted ? 'line-through' : 'none',
|
|
||||||
color: getTextColor(),
|
|
||||||
letterSpacing: '0.03em',
|
|
||||||
transition: 'color 0.2s ease',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{event.title}
|
|
||||||
</Text>
|
|
||||||
{event.repeat_type !== 'none' && (
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
color: getRepeatIconColor(event.repeat_type),
|
|
||||||
}}
|
|
||||||
title={getRepeatTypeLabel(event.repeat_type)}
|
|
||||||
>
|
|
||||||
<IconRepeat size={10} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c={getTimeColor()}
|
|
||||||
style={{ letterSpacing: '0.05em' }}
|
|
||||||
>
|
>
|
||||||
{timeInfo.timeStr}
|
{/* 已完成时显示的对勾 */}
|
||||||
</Text>
|
{isCompleted && (
|
||||||
{event.content && (
|
<span
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<Text
|
||||||
|
fw={400}
|
||||||
|
size="xs"
|
||||||
|
lineClamp={1}
|
||||||
|
style={{
|
||||||
|
textDecoration: isCompleted ? 'line-through' : 'none',
|
||||||
|
color: getTextColor(),
|
||||||
|
letterSpacing: '0.03em',
|
||||||
|
transition: 'color 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
{event.repeat_type !== 'none' && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
color: REPEAT_ICON_COLOR,
|
||||||
|
}}
|
||||||
|
title={getRepeatTypeLabel(event.repeat_type)}
|
||||||
|
>
|
||||||
|
<IconRepeat size={10} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
<Text
|
<Text
|
||||||
size="xs"
|
size="xs"
|
||||||
c={isCompleted ? '#bbb' : '#999'}
|
c={getTimeColor()}
|
||||||
lineClamp={1}
|
style={{ letterSpacing: '0.05em' }}
|
||||||
style={{
|
|
||||||
opacity: isCompleted ? 0.6 : 1,
|
|
||||||
transition: 'opacity 0.2s ease',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{event.content}
|
{timeInfo.timeStr}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
{event.content && (
|
||||||
</Stack>
|
<Text
|
||||||
</Group>
|
size="xs"
|
||||||
|
c={isCompleted ? '#bbb' : '#999'}
|
||||||
|
lineClamp={1}
|
||||||
|
style={{
|
||||||
|
opacity: isCompleted ? 0.6 : 1,
|
||||||
|
transition: 'opacity 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{event.content}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
{onPostpone && (
|
{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"
|
||||||
color="orange"
|
color="gray"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onPostpone();
|
onClick();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
color: '#e67e22',
|
color: '#999',
|
||||||
opacity: isHovered ? 1 : 0,
|
opacity: isHovered ? 1 : 0,
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
}}
|
}}
|
||||||
title="顺延到今天"
|
title="编辑"
|
||||||
>
|
>
|
||||||
<IconArrowForward size={12} />
|
<IconDots size={12} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
</Group>
|
||||||
<ActionIcon
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
color="gray"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onClick();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
color: '#999',
|
|
||||||
opacity: isHovered ? 1 : 0,
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
}}
|
|
||||||
title="编辑"
|
|
||||||
>
|
|
||||||
<IconDots size={12} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
)}
|
||||||
|
</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>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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
40
src/stores/contextMenu.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
// 右键菜单状态管理
|
||||||
|
interface ContextMenuState {
|
||||||
|
// 当前打开菜单的事件ID,null表示没有菜单打开
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}));
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user