feat: 设置中添加节假日配置功能

- 扩展设置选项:显示数量选择(1/3/5/10个)
- 添加仅显示法定节假日开关
- 添加节假日筛选功能:可选择关注特定节假日
- 更新 AnniversaryList 使用新设置进行过滤

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-02-28 10:23:49 +08:00
parent d73a87709c
commit e7b6864b42
3 changed files with 244 additions and 9 deletions

View File

@ -27,7 +27,10 @@ interface BuiltInHolidayEvent {
export function AnniversaryList({ events, onEventClick, onAddClick }: AnniversaryListProps) {
const anniversaries = events.filter((e) => e.type === 'anniversary');
const showHolidays = useAppStore((state) => state.settings?.showHolidays ?? true);
const settings = useAppStore((state) => state.settings);
const showHolidays = settings?.showHolidays ?? true;
const holidayDisplayCount = settings?.holidayDisplayCount ?? 3;
const showStatutoryOnly = settings?.showStatutoryOnly ?? false;
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [bottomPadding, setBottomPadding] = useState(0);
@ -108,17 +111,29 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
const nextYear = getHolidaysForYear(year + 1);
// 合并今年和明年的节假日,按日期排序
const allHolidays = [...holidays, ...nextYear].sort(
let allHolidays = [...holidays, ...nextYear].sort(
(a, b) => a.date.getTime() - b.date.getTime()
);
// 只取未来90天内的节假日显示最近3个
// 应用过滤规则
// 1. 如果 enabledHolidays 有内容,只显示选中的节假日
const enabledHolidays = settings?.enabledHolidays;
if (enabledHolidays && enabledHolidays.length > 0) {
allHolidays = allHolidays.filter((h) => enabledHolidays.includes(h.id));
}
// 2. 如果开启了仅显示法定节假日
if (showStatutoryOnly) {
allHolidays = allHolidays.filter((h) => h.isStatutory);
}
// 只取未来90天内的节假日
const cutoffDate = new Date(now);
cutoffDate.setDate(cutoffDate.getDate() + 90);
return allHolidays
.filter((h) => h.date >= now && h.date <= cutoffDate)
.slice(0, 3)
.slice(0, holidayDisplayCount)
.map((h): BuiltInHolidayEvent => ({
id: `builtin-${h.id}`,
title: h.name,
@ -129,7 +144,7 @@ export function AnniversaryList({ events, onEventClick, onAddClick }: Anniversar
type: 'anniversary',
is_builtin: true,
}));
}, [showHolidays]);
}, [showHolidays, holidayDisplayCount, showStatutoryOnly, settings]);
// 合并用户纪念日和内置节假日
const allAnniversaries = useMemo(() => {

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import {
Container,
Title,
@ -9,13 +9,18 @@ import {
Group,
Button,
Loader,
Select,
Divider,
Checkbox,
ScrollArea,
} from '@mantine/core';
import { IconArrowLeft, IconSettings, IconBell, IconRefresh, IconBellCheck } from '@tabler/icons-react';
import { IconArrowLeft, IconSettings, IconBell, IconRefresh, IconBellCheck, IconChevronDown } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import { useAppStore } from '../stores';
import { requestNotificationPermission, getNotificationPermission, isNotificationSupported } from '../services/notification';
import { syncRemindersToSW, triggerSWCheck } from '../services/swSync';
import { notifications } from '@mantine/notifications';
import { getBuiltInHolidays } from '../constants/holidays';
export function SettingsPage() {
const navigate = useNavigate();
@ -23,6 +28,88 @@ export function SettingsPage() {
const updateSettings = useAppStore((state) => state.updateSettings);
const [permissionStatus, setPermissionStatus] = useState<'granted' | 'denied' | 'default'>('default');
const [isRequesting, setIsRequesting] = useState(false);
const [holidayFilterExpanded, setHolidayFilterExpanded] = useState(false);
// 获取所有节假日列表
const allHolidays = useMemo(() => getBuiltInHolidays(), []);
// 法定节假日
const statutoryHolidays = useMemo(
() => allHolidays.filter((h) => h.isStatutory),
[allHolidays]
);
// 非法定节假日
const nonStatutoryHolidays = useMemo(
() => allHolidays.filter((h) => !h.isStatutory),
[allHolidays]
);
// 处理节假日开关
const handleHolidayToggle = (checked: boolean) => {
updateSettings({ showHolidays: checked });
if (!checked) {
// 关闭时也可以收起筛选面板
setHolidayFilterExpanded(false);
}
};
// 处理显示数量变化
const handleDisplayCountChange = (value: string | null) => {
updateSettings({ holidayDisplayCount: value ? parseInt(value, 10) : 3 });
};
// 处理仅显示法定节假日变化
const handleStatutoryOnlyChange = (checked: boolean) => {
updateSettings({ showStatutoryOnly: checked });
};
// 处理特定节假日选择
const handleHolidaySelection = (holidayId: string, checked: boolean) => {
const currentEnabled = settings.enabledHolidays || [];
let newEnabled: string[];
if (checked) {
newEnabled = [...currentEnabled, holidayId];
} else {
newEnabled = currentEnabled.filter((id) => id !== holidayId);
}
updateSettings({ enabledHolidays: newEnabled });
};
// 检查节假日是否被选中
const isHolidaySelected = (holidayId: string) => {
const enabled = settings.enabledHolidays || [];
return enabled.includes(holidayId);
};
// 全选/取消全选
const handleSelectAll = (checked: boolean) => {
if (checked) {
updateSettings({ enabledHolidays: allHolidays.map((h) => h.id) });
} else {
updateSettings({ enabledHolidays: [] });
}
};
// 选择所有法定节假日
const handleSelectStatutory = (checked: boolean) => {
if (checked) {
const statutoryIds = statutoryHolidays.map((h) => h.id);
const currentEnabled = settings.enabledHolidays || [];
// 合并现有选择和法定节假日
const newEnabled = [...new Set([...currentEnabled, ...statutoryIds])];
updateSettings({ enabledHolidays: newEnabled });
} else {
// 移除所有法定节假日
const statutoryIds = new Set(statutoryHolidays.map((h) => h.id));
const newEnabled = (settings.enabledHolidays || []).filter(
(id) => !statutoryIds.has(id)
);
updateSettings({ enabledHolidays: newEnabled });
}
};
// 页面加载时检查登录状态
useEffect(() => {
@ -170,18 +257,145 @@ export function SettingsPage() {
</Text>
<Text size="xs" c="#999" style={{ letterSpacing: '0.03em' }}>
3
</Text>
</Stack>
</Group>
<Switch
checked={settings.showHolidays}
onChange={(e) => updateSettings({ showHolidays: e.currentTarget.checked })}
onChange={(e) => handleHolidayToggle(e.currentTarget.checked)}
size="sm"
color="#1a1a1a"
/>
</Group>
{/* 节假日显示数量 */}
{settings.showHolidays && (
<Group justify="space-between">
<Text size="sm" c="#666" style={{ letterSpacing: '0.03em' }}>
</Text>
<Select
value={String(settings.holidayDisplayCount)}
onChange={handleDisplayCountChange}
data={[
{ value: '1', label: '1个' },
{ value: '3', label: '3个' },
{ value: '5', label: '5个' },
{ value: '10', label: '10个' },
]}
size="xs"
style={{ width: 100 }}
styles={{
input: {
background: 'transparent',
borderColor: '#e0e0e0',
},
}}
/>
</Group>
)}
{/* 仅显示法定节假日 */}
{settings.showHolidays && (
<Group justify="space-between">
<Text size="sm" c="#666" style={{ letterSpacing: '0.03em' }}>
</Text>
<Switch
checked={settings.showStatutoryOnly}
onChange={(e) => handleStatutoryOnlyChange(e.currentTarget.checked)}
size="sm"
color="#1a1a1a"
/>
</Group>
)}
{/* 节假日筛选 */}
{settings.showHolidays && (
<>
<Divider />
<Stack gap="sm">
<Group justify="space-between">
<Text size="sm" fw={400} style={{ letterSpacing: '0.05em', color: '#1a1a1a' }}>
</Text>
<Button
variant="subtle"
size="xs"
rightSection={<IconChevronDown size={14} />}
onClick={() => setHolidayFilterExpanded(!holidayFilterExpanded)}
style={{
color: '#666',
letterSpacing: '0.03em',
}}
>
{holidayFilterExpanded ? '收起' : '展开'}
</Button>
</Group>
{holidayFilterExpanded && (
<Stack gap="xs">
<Group gap="lg">
<Checkbox
label="全选"
checked={(settings.enabledHolidays || []).length === allHolidays.length}
onChange={(e) => handleSelectAll(e.currentTarget.checked)}
size="xs"
/>
<Checkbox
label="法定节假日"
checked={statutoryHolidays.every((h) => isHolidaySelected(h.id))}
indeterminate={
statutoryHolidays.some((h) => isHolidaySelected(h.id)) &&
!statutoryHolidays.every((h) => isHolidaySelected(h.id))
}
onChange={(e) => handleSelectStatutory(e.currentTarget.checked)}
size="xs"
/>
</Group>
<Divider my="xs" />
<Text size="xs" c="#888" fw={400}>
</Text>
<ScrollArea h={120}>
<Group gap="xs">
{statutoryHolidays.map((holiday) => (
<Checkbox
key={holiday.id}
label={holiday.name}
checked={isHolidaySelected(holiday.id)}
onChange={(e) => handleHolidaySelection(holiday.id, e.currentTarget.checked)}
size="xs"
/>
))}
</Group>
</ScrollArea>
<Text size="xs" c="#888" fw={400} mt="xs">
</Text>
<ScrollArea h={100}>
<Group gap="xs">
{nonStatutoryHolidays.map((holiday) => (
<Checkbox
key={holiday.id}
label={holiday.name}
checked={isHolidaySelected(holiday.id)}
onChange={(e) => handleHolidaySelection(holiday.id, e.currentTarget.checked)}
size="xs"
/>
))}
</Group>
</ScrollArea>
</Stack>
)}
</Stack>
</>
)}
{/* 浏览器通知设置 */}
{isNotificationSupported() && (
<Group justify="space-between" style={{ marginTop: 16 }}>

View File

@ -8,12 +8,18 @@ import { calculateNextReminderDate, findNextValidReminderDate, isDuplicateRemind
// 应用设置类型
interface AppSettings {
showHolidays: boolean; // 是否显示节假日
holidayDisplayCount: number; // 节假日显示数量
showStatutoryOnly: boolean; // 仅显示法定节假日
enabledHolidays: string[]; // 用户关注的节假日ID列表空表示全部
browserNotifications: boolean; // 是否启用浏览器通知
}
// 默认设置
const defaultSettings: AppSettings = {
showHolidays: true,
holidayDisplayCount: 3,
showStatutoryOnly: false,
enabledHolidays: [],
browserNotifications: false,
};