- 扩展设置选项:显示数量选择(1/3/5/10个) - 添加仅显示法定节假日开关 - 添加节假日筛选功能:可选择关注特定节假日 - 更新 AnniversaryList 使用新设置进行过滤 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
474 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|