qia-client/src/components/anniversary/AnniversaryList.tsx
ddshi 3fdee5cab4 feat: 优化提醒卡片样式和列表滚动功能
- 调整正常提醒卡片布局:左侧 checkbox+标题+内容,右侧日期时间
- 移除正常卡片悬停时的编辑按钮
- 纪念日/提醒/便签列表支持独立滚动
- 页面整体禁用滚动,支持 Ctrl+滚轮缩放
- 滚动条样式优化:默认隐藏,悬停时显示淡雅样式

Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
2026-02-03 16:29:25 +08:00

247 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
</>
);
}