702 lines
22 KiB
TypeScript
702 lines
22 KiB
TypeScript
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<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 {
|
||
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<HTMLDivElement>(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 (
|
||
<>
|
||
<Paper
|
||
ref={cardRef}
|
||
p="sm"
|
||
radius={2}
|
||
onMouseEnter={() => 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',
|
||
}}
|
||
>
|
||
<style>{`
|
||
@keyframes reminder-card-pulse {
|
||
0% { transform: scale(1); }
|
||
50% { transform: scale(1.15); }
|
||
100% { transform: scale(1); }
|
||
}
|
||
@keyframes reminder-card-fadeOut {
|
||
0% { opacity: 1; transform: translateX(0); }
|
||
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>
|
||
{/* 正常提醒卡片:左右分栏布局 */}
|
||
{!isMissed ? (
|
||
<Group justify="space-between" wrap="nowrap" align="flex-start">
|
||
{/* 左侧:Checkbox + 标题 + 内容 */}
|
||
<Box
|
||
style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onClick();
|
||
}}
|
||
>
|
||
<Box style={{ display: 'flex', alignItems: 'flex-start' }}>
|
||
{/* Checkbox */}
|
||
<div
|
||
className="reminder-checkbox"
|
||
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();
|
||
onToggle();
|
||
}}
|
||
>
|
||
{/* 已完成时显示的对勾 */}
|
||
{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={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
color: REPEAT_ICON_COLOR,
|
||
}}
|
||
title={getRepeatTypeLabel(event.repeat_type)}
|
||
>
|
||
<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
|
||
className="reminder-checkbox"
|
||
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',
|
||
}}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
if (isMissed && !isCompleted) {
|
||
setIsAnimating(true);
|
||
setTimeout(() => {
|
||
setIsAnimating(false);
|
||
onToggle();
|
||
onMissedToggle?.();
|
||
}, 300);
|
||
} else {
|
||
onToggle();
|
||
}
|
||
}}
|
||
>
|
||
{/* 已完成时显示的对勾 */}
|
||
{isCompleted && (
|
||
<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
|
||
size="xs"
|
||
c={getTimeColor()}
|
||
style={{ letterSpacing: '0.05em' }}
|
||
>
|
||
{timeInfo.timeStr}
|
||
</Text>
|
||
{event.content && (
|
||
<Text
|
||
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}>
|
||
{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
|
||
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>
|
||
)}
|
||
</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>
|
||
)}
|
||
</>
|
||
);
|
||
}
|