feat: 设置中添加节假日配置功能
- 扩展设置选项:显示数量选择(1/3/5/10个) - 添加仅显示法定节假日开关 - 添加节假日筛选功能:可选择关注特定节假日 - 更新 AnniversaryList 使用新设置进行过滤 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d73a87709c
commit
e7b6864b42
@ -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(() => {
|
||||
|
||||
@ -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 }}>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user