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"
+ />
)}