qia-client/src/pages/SettingsPage.tsx
ddshi e7b6864b42 feat: 设置中添加节假日配置功能
- 扩展设置选项:显示数量选择(1/3/5/10个)
- 添加仅显示法定节假日开关
- 添加节假日筛选功能:可选择关注特定节假日
- 更新 AnniversaryList 使用新设置进行过滤

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:23:49 +08:00

474 lines
16 KiB
TypeScript

import { useEffect, useState, useMemo } from 'react';
import {
Container,
Title,
Text,
Switch,
Stack,
Paper,
Group,
Button,
Loader,
Select,
Divider,
Checkbox,
ScrollArea,
} from '@mantine/core';
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();
const settings = useAppStore((state) => state.settings);
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(() => {
const isAuthenticated = useAppStore.getState().isAuthenticated;
if (!isAuthenticated) {
navigate('/login', { replace: true });
}
}, [navigate]);
// 初始化权限状态
useEffect(() => {
if (isNotificationSupported()) {
setPermissionStatus(getNotificationPermission());
}
}, []);
// 处理浏览器通知开关
const handleBrowserNotificationToggle = async (enabled: boolean) => {
if (!isNotificationSupported()) {
notifications.show({
title: '不支持通知',
message: '您的浏览器不支持通知功能',
color: 'red',
});
return;
}
if (enabled) {
// 请求权限
setIsRequesting(true);
const permission = await requestNotificationPermission();
setIsRequesting(false);
setPermissionStatus(permission);
if (permission === 'granted') {
updateSettings({ browserNotifications: true });
// 同步所有提醒到 Service Worker
await syncRemindersToSW();
notifications.show({
title: '通知已开启',
message: '您将收到浏览器的提醒通知',
color: 'green',
});
} else {
// 权限被拒绝
notifications.show({
title: '无法开启通知',
message: '请在浏览器设置中允许通知权限',
color: 'red',
});
}
} else {
updateSettings({ browserNotifications: false });
}
};
// 同步提醒到 SW
const handleSyncReminders = async () => {
await syncRemindersToSW();
notifications.show({
title: '同步完成',
message: '提醒已同步到 Service Worker',
color: 'green',
});
};
// 手动触发 SW 检查
const handleTriggerCheck = async () => {
await triggerSWCheck();
notifications.show({
title: '检查已触发',
message: 'Service Worker 将立即检查提醒',
color: 'blue',
});
};
// 发送测试通知
const handleTestNotification = async () => {
if (Notification.permission !== 'granted') {
notifications.show({
title: '请先开启通知',
message: '需要先允许通知权限',
color: 'yellow',
});
return;
}
new Notification('测试通知', {
body: '这是一条测试通知,通知功能正常工作',
icon: '/favicon.png',
tag: 'test-notification',
});
notifications.show({
title: '测试通知已发送',
message: '请查看浏览器通知',
color: 'blue',
});
};
return (
<div
style={{
minHeight: '100vh',
background: '#faf9f7',
}}
>
<Container size="xl" py="md">
{/* Header */}
<Group mb="lg" style={{ flexShrink: 0 }}>
<Button
variant="subtle"
color="gray"
size="xs"
leftSection={<IconArrowLeft size={14} />}
onClick={() => navigate(-1)}
style={{
letterSpacing: '0.1em',
borderRadius: 2,
}}
>
</Button>
<Title
order={2}
style={{
fontWeight: 300,
fontSize: '1.25rem',
letterSpacing: '0.15em',
color: '#1a1a1a',
}}
>
</Title>
</Group>
<Paper p="lg" withBorder radius={4} style={{ maxWidth: 500 }}>
<Stack gap="lg">
{/* 节假日设置 */}
<Group justify="space-between">
<Group gap="sm">
<IconSettings size={18} color="#666" />
<Stack gap={2}>
<Text size="sm" fw={400} style={{ letterSpacing: '0.05em', color: '#1a1a1a' }}>
</Text>
<Text size="xs" c="#999" style={{ letterSpacing: '0.03em' }}>
</Text>
</Stack>
</Group>
<Switch
checked={settings.showHolidays}
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 }}>
<Group gap="sm">
<IconBell size={18} color="#666" />
<Stack gap={2}>
<Text size="sm" fw={400} style={{ letterSpacing: '0.05em', color: '#1a1a1a' }}>
</Text>
<Text size="xs" c="#999" style={{ letterSpacing: '0.03em' }}>
{permissionStatus === 'granted'
? '已开启 - 通过系统通知提醒您的重要事项'
: permissionStatus === 'denied'
? '已拒绝 - 请在浏览器设置中允许通知'
: '通过系统通知提醒您的重要事项'}
</Text>
</Stack>
</Group>
{isRequesting ? (
<Loader size="xs" />
) : (
<Switch
checked={settings.browserNotifications && permissionStatus === 'granted'}
onChange={(e) => handleBrowserNotificationToggle(e.currentTarget.checked)}
disabled={permissionStatus === 'denied'}
size="sm"
color="#1a1a1a"
/>
)}
</Group>
)}
{/* 通知功能测试按钮 */}
{settings.browserNotifications && permissionStatus === 'granted' && (
<Stack gap="sm" style={{ marginTop: 16 }}>
<Text size="sm" fw={400} style={{ letterSpacing: '0.05em', color: '#1a1a1a' }}>
</Text>
<Group>
<Button
variant="light"
size="xs"
leftSection={<IconRefresh size={14} />}
onClick={handleSyncReminders}
style={{ letterSpacing: '0.05em' }}
>
</Button>
<Button
variant="light"
size="xs"
leftSection={<IconBellCheck size={14} />}
onClick={handleTriggerCheck}
style={{ letterSpacing: '0.05em' }}
>
</Button>
<Button
variant="light"
size="xs"
leftSection={<IconBell size={14} />}
onClick={handleTestNotification}
style={{ letterSpacing: '0.05em' }}
>
</Button>
</Group>
</Stack>
)}
</Stack>
</Paper>
</Container>
</div>
);
}