- 调整正常提醒卡片布局:左侧 checkbox+标题+内容,右侧日期时间 - 移除正常卡片悬停时的编辑按钮 - 纪念日/提醒/便签列表支持独立滚动 - 页面整体禁用滚动,支持 Ctrl+滚轮缩放 - 滚动条样式优化:默认隐藏,悬停时显示淡雅样式 Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
247 lines
7.9 KiB
TypeScript
247 lines
7.9 KiB
TypeScript
import { useMemo, useRef } from 'react';
|
||
import { Stack, Text, Paper, Group, Button, Box } from '@mantine/core';
|
||
import { IconPlus } from '@tabler/icons-react';
|
||
import { AnniversaryCard } from './AnniversaryCard';
|
||
import { getHolidaysForYear } from '../../constants/holidays';
|
||
import { calculateCountdown, formatCountdown } from '../../utils/countdown';
|
||
import { useAppStore } from '../../stores';
|
||
import type { Event } from '../../types';
|
||
|
||
interface AnniversaryListProps {
|
||
events: Event[];
|
||
onEventClick: (event: Event) => void;
|
||
onAddClick: () => void;
|
||
}
|
||
|
||
// 内置节假日事件类型
|
||
interface BuiltInHolidayEvent {
|
||
id: string;
|
||
title: string;
|
||
date: string;
|
||
is_holiday: boolean;
|
||
is_lunar: boolean;
|
||
repeat_type: 'yearly';
|
||
type: 'anniversary';
|
||
is_builtin: true;
|
||
}
|
||
|
||
export function AnniversaryList({ events, onEventClick, onAddClick }: AnniversaryListProps) {
|
||
const anniversaries = events.filter((e) => e.type === 'anniversary');
|
||
const showHolidays = useAppStore((state) => state.settings?.showHolidays ?? true);
|
||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 滚动条样式 - 仅在悬停时显示
|
||
const scrollbarStyle = `
|
||
.anniversary-scroll::-webkit-scrollbar {
|
||
width: 4px;
|
||
height: 4px;
|
||
}
|
||
.anniversary-scroll::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
.anniversary-scroll::-webkit-scrollbar-thumb {
|
||
background: transparent;
|
||
border-radius: 2px;
|
||
}
|
||
.anniversary-scroll:hover::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
}
|
||
`;
|
||
|
||
// 处理滚轮事件,实现列表独立滚动
|
||
const handleWheel = (e: React.WheelEvent) => {
|
||
const container = scrollContainerRef.current;
|
||
if (!container) return;
|
||
|
||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||
// 使用 1px 缓冲避免浮点数精度问题
|
||
const isAtTop = scrollTop <= 0;
|
||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1;
|
||
|
||
// 如果已经滚动到顶部且向下滚动,或者已经滚动到底部且向上滚动,则阻止事件冒泡
|
||
if ((isAtTop && e.deltaY > 0) || (isAtBottom && e.deltaY < 0)) {
|
||
e.stopPropagation();
|
||
}
|
||
// 如果在滚动范围内,允许事件继续传递以实现正常滚动
|
||
};
|
||
|
||
// 获取内置节假日
|
||
const builtInHolidays = useMemo(() => {
|
||
if (!showHolidays) return [];
|
||
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const holidays = getHolidaysForYear(year);
|
||
const nextYear = getHolidaysForYear(year + 1);
|
||
|
||
// 合并今年和明年的节假日,按日期排序
|
||
const allHolidays = [...holidays, ...nextYear].sort(
|
||
(a, b) => a.date.getTime() - b.date.getTime()
|
||
);
|
||
|
||
// 只取未来90天内的节假日,显示最近3个
|
||
const cutoffDate = new Date(now);
|
||
cutoffDate.setDate(cutoffDate.getDate() + 90);
|
||
|
||
return allHolidays
|
||
.filter((h) => h.date >= now && h.date <= cutoffDate)
|
||
.slice(0, 3)
|
||
.map((h): BuiltInHolidayEvent => ({
|
||
id: `builtin-${h.id}`,
|
||
title: h.name,
|
||
date: h.date.toISOString(),
|
||
is_holiday: true,
|
||
is_lunar: h.isLunar,
|
||
repeat_type: 'yearly',
|
||
type: 'anniversary',
|
||
is_builtin: true,
|
||
}));
|
||
}, [showHolidays]);
|
||
|
||
// 合并用户纪念日和内置节假日
|
||
const allAnniversaries = useMemo(() => {
|
||
// 用户创建的节假日按倒计时排序
|
||
const sortedUser = [...anniversaries].sort((a, b) => {
|
||
const countdownA = calculateCountdown(a.date, a.repeat_type, a.is_lunar);
|
||
const countdownB = calculateCountdown(b.date, b.repeat_type, b.is_lunar);
|
||
return countdownA.nextDate.getTime() - countdownB.nextDate.getTime();
|
||
});
|
||
|
||
// 内置节假日按日期排序
|
||
const sortedBuiltIn = [...builtInHolidays].sort((a, b) => {
|
||
const countdownA = calculateCountdown(a.date, a.repeat_type, a.is_lunar);
|
||
const countdownB = calculateCountdown(b.date, b.repeat_type, b.is_lunar);
|
||
return countdownA.nextDate.getTime() - countdownB.nextDate.getTime();
|
||
});
|
||
|
||
return {
|
||
user: sortedUser,
|
||
builtIn: sortedBuiltIn,
|
||
total: sortedUser.length + sortedBuiltIn.length,
|
||
};
|
||
}, [anniversaries, builtInHolidays]);
|
||
|
||
// 空状态
|
||
if (allAnniversaries.total === 0) {
|
||
return (
|
||
<Paper p="md" withBorder radius={4} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||
<Stack align="center" justify="center" style={{ flex: 1 }}>
|
||
<style>{scrollbarStyle}</style>
|
||
<Text c="#999" size="sm" style={{ letterSpacing: '0.05em' }}>
|
||
暂无纪念日
|
||
</Text>
|
||
<Button
|
||
variant="outline"
|
||
size="xs"
|
||
leftSection={<IconPlus size={12} />}
|
||
onClick={onAddClick}
|
||
style={{
|
||
borderColor: '#ccc',
|
||
color: '#1a1a1a',
|
||
borderRadius: 2,
|
||
}}
|
||
>
|
||
添加纪念日
|
||
</Button>
|
||
</Stack>
|
||
</Paper>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<style>{scrollbarStyle}</style>
|
||
<Paper p="md" withBorder radius={4} data-scroll-container="anniversary" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||
<Group justify="space-between" mb="sm" style={{ flexShrink: 0 }}>
|
||
<Group gap={8}>
|
||
<Text fw={500} size="sm" style={{ letterSpacing: '0.1em', color: '#1a1a1a' }}>
|
||
纪念日
|
||
</Text>
|
||
<Text size="xs" c="#999">
|
||
{anniversaries.length}
|
||
</Text>
|
||
</Group>
|
||
<Button
|
||
variant="subtle"
|
||
size="xs"
|
||
leftSection={<IconPlus size={12} />}
|
||
onClick={onAddClick}
|
||
style={{
|
||
color: '#666',
|
||
borderRadius: 2,
|
||
}}
|
||
>
|
||
添加
|
||
</Button>
|
||
</Group>
|
||
|
||
<Box
|
||
ref={scrollContainerRef}
|
||
onWheel={handleWheel}
|
||
className="anniversary-scroll"
|
||
style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}
|
||
>
|
||
<Stack gap="xs">
|
||
{/* 内置节假日 */}
|
||
{allAnniversaries.builtIn.length > 0 && (
|
||
<>
|
||
<Text size="xs" c="#888" fw={400} style={{ letterSpacing: '0.05em' }}>
|
||
即将到来
|
||
</Text>
|
||
{allAnniversaries.builtIn.map((holiday) => {
|
||
const countdown = calculateCountdown(holiday.date, holiday.repeat_type, holiday.is_lunar);
|
||
return (
|
||
<Paper
|
||
key={holiday.id}
|
||
p="sm"
|
||
radius={2}
|
||
style={{
|
||
cursor: 'pointer',
|
||
opacity: 0.7,
|
||
background: 'rgba(0, 0, 0, 0.02)',
|
||
border: '1px solid rgba(0, 0, 0, 0.04)',
|
||
}}
|
||
onClick={() => onEventClick(holiday as unknown as Event)}
|
||
>
|
||
<Group justify="space-between" wrap="nowrap">
|
||
<Group gap={6}>
|
||
<Text size="xs" c="#666">{holiday.title}</Text>
|
||
</Group>
|
||
<Text
|
||
size="xs"
|
||
c={countdown.isToday ? '#c41c1c' : '#888'}
|
||
style={{ letterSpacing: '0.05em' }}
|
||
>
|
||
{formatCountdown(countdown)}
|
||
</Text>
|
||
</Group>
|
||
</Paper>
|
||
);
|
||
})}
|
||
</>
|
||
)}
|
||
|
||
{/* 用户纪念日 */}
|
||
{allAnniversaries.user.length > 0 && (
|
||
<>
|
||
{allAnniversaries.builtIn.length > 0 && (
|
||
<Text size="xs" c="#666" fw={500} style={{ letterSpacing: '0.05em' }} mt="xs">
|
||
我的纪念日
|
||
</Text>
|
||
)}
|
||
{allAnniversaries.user.map((event) => (
|
||
<AnniversaryCard
|
||
key={event.id}
|
||
event={event}
|
||
onClick={() => onEventClick(event)}
|
||
/>
|
||
))}
|
||
</>
|
||
)}
|
||
</Stack>
|
||
</Box>
|
||
</Paper>
|
||
</>
|
||
);
|
||
}
|