diff --git a/public/sw.js b/public/sw.js index 63d741e..8d4ea14 100644 --- a/public/sw.js +++ b/public/sw.js @@ -52,15 +52,15 @@ function checkReminders(events) { for (const reminderTime of event.reminder_times) { const rt = new Date(reminderTime); - // 检查是否在最近10分钟内(宽限期,处理后台运行不稳定的情况) + // 检查是否在最近3分钟内(宽限期,处理后台运行不稳定的情况) const diffMs = now.getTime() - rt.getTime(); const diffMinutes = diffMs / (1000 * 60); // 跳过还未到的提醒 if (diffMinutes < 0) continue; - // 如果在10分钟宽限期内,且还没发送过通知 - if (diffMinutes < 10) { + // 如果在3分钟宽限期内,且还没发送过通知 + if (diffMinutes < 3) { const tag = `reminder-${event.id}-${reminderTime}`; // 避免重复发送同一通知 diff --git a/src/components/common/PopoverTimePicker.tsx b/src/components/common/PopoverTimePicker.tsx new file mode 100644 index 0000000..9f57d37 --- /dev/null +++ b/src/components/common/PopoverTimePicker.tsx @@ -0,0 +1,222 @@ +import { useState, useRef, useEffect } from 'react'; +import { + Box, + Group, + Text, + Popover, + TextInput, + Stack, + Button, +} from '@mantine/core'; + +interface PopoverTimePickerProps { + value: string; // "HH:mm" format + onChange: (time: string) => void; + placeholder?: string; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; +} + +function padZero(num: number): string { + return String(num).padStart(2, '0'); +} + +// 验证小时格式 +const validateHour = (h: string): number => { + const num = parseInt(h, 10); + if (isNaN(num) || num < 0) return 0; + if (num > 23) return 23; + return num; +}; + +// 验证分钟格式(30分钟间隔) +const validateMinute = (m: string): number => { + const num = parseInt(m, 10); + if (isNaN(num) || num < 0) return 0; + if (num > 59) return 59; + return Math.round(num / 30) * 30; // 30分钟间隔 +}; + +export function PopoverTimePicker({ + value, + onChange, + placeholder = '选择时间', + size = 'sm', +}: PopoverTimePickerProps) { + const [opened, setOpened] = useState(false); + const [hourInput, setHourInput] = useState('00'); + const [minuteInput, setMinuteInput] = useState('00'); + + // 初始化输入值 + useEffect(() => { + if (value && value.includes(':')) { + const [h, m] = value.split(':'); + setHourInput(padZero(parseInt(h, 10) || 0)); + setMinuteInput(padZero(parseInt(m, 10) || 0)); + } + }, [value]); + + // 分钟选项(30分钟间隔) + const minuteOptions = [0, 30]; + + // 处理小时输入 + const handleHourChange = (h: string) => { + // 只允许输入数字,且最多2位 + const filtered = h.replace(/[^0-9]/g, '').slice(0, 2); + setHourInput(filtered); + }; + + // 处理分钟输入 + const handleMinuteChange = (m: string) => { + // 只允许输入数字,且最多2位 + const filtered = m.replace(/[^0-9]/g, '').slice(0, 2); + setMinuteInput(filtered); + }; + + // 应用时间 + const applyTime = () => { + const h = validateHour(hourInput); + const m = validateMinute(minuteInput); + const time = `${padZero(h)}:${padZero(m)}`; + onChange(time); + setHourInput(padZero(h)); + setMinuteInput(padZero(m)); + setOpened(false); + }; + + // 快速选择分钟 + const selectMinute = (m: number) => { + setMinuteInput(padZero(m)); + }; + + // 处理键盘事件 + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + applyTime(); + } else if (e.key === 'Escape') { + setOpened(false); + } + }; + + // 显示值 + const displayValue = value && value !== ':' ? value : ''; + + return ( + + + setOpened(true)} + styles={{ + input: { + cursor: 'pointer', + letterSpacing: '0.05em', + }, + }} + /> + + + + + {/* 数字输入区域 */} + + {/* 小时输入 */} + handleHourChange(e.currentTarget.value)} + onKeyDown={handleKeyDown} + placeholder="时" + size="sm" + style={{ width: 56 }} + maxLength={2} + styles={{ + input: { + textAlign: 'center', + fontSize: 16, + fontWeight: 500, + letterSpacing: '0.1em', + }, + }} + /> + + : + + {/* 分钟输入 */} + handleMinuteChange(e.currentTarget.value)} + onKeyDown={handleKeyDown} + placeholder="分" + size="sm" + style={{ width: 56 }} + maxLength={2} + styles={{ + input: { + textAlign: 'center', + fontSize: 16, + fontWeight: 500, + letterSpacing: '0.1em', + }, + }} + /> + + + {/* 快速选择分钟 */} + + {minuteOptions.map((m) => ( + + ))} + + + {/* 操作按钮 */} + + + + + + + + ); +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index a8e4222..29423e5 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -15,11 +15,10 @@ import { ActionIcon, } from '@mantine/core'; import { DatePickerInput } from '@mantine/dates'; -import { Popover } from '@mantine/core'; import { IconCalendar, IconClock, IconSettings, IconLogout, IconX } from '@tabler/icons-react'; import { useDisclosure } from '@mantine/hooks'; import { FixedCalendar } from '../components/common/FixedCalendar'; -import { WheelTimePicker } from '../components/common/WheelTimePicker'; +import { PopoverTimePicker } from '../components/common/PopoverTimePicker'; import { useNavigate } from 'react-router-dom'; import { useAppStore } from '../stores'; import { AnniversaryList } from '../components/anniversary/AnniversaryList'; @@ -551,58 +550,13 @@ export function HomePage() { - {/* Time selector (only for reminders) - Wheel picker */} + {/* Time selector (only for reminders) - Popover picker */} {formType === 'reminder' && ( - - - - } - leftSectionPointerEvents="none" - rightSection={ - formTime ? ( - setFormTime('')} - style={{ color: '#9ca3af' }} - > - - - ) : null - } - styles={{ - input: { - borderRadius: 2, - background: '#faf9f7', - borderColor: '#ddd', - color: '#666', - height: 32, - paddingLeft: 36, - paddingRight: formTime ? 28 : 8, - cursor: 'pointer', - }, - section: { - paddingLeft: 8, - }, - }} - /> - - - - setFormTime(time)} - /> - - + setFormTime(time)} + size="sm" + /> )}