feat: 优化提醒卡片样式和归档功能

- 优化提醒卡片样式,统一黑白灰配色
- 添加checkbox勾选动画和过期提醒淡出效果
- 完善归档页功能(恢复/删除已过期完成提醒)
- 修复过期检测逻辑(精确到时间点而非仅日期)

Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-02-03 14:14:08 +08:00
parent 9e4b4022bd
commit a8b4f17043
5 changed files with 120 additions and 47 deletions

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { Paper, Text, Checkbox, Group, Stack, ActionIcon } from '@mantine/core';
import { IconDots } from '@tabler/icons-react';
import type { Event } from '../../types';
@ -8,13 +8,15 @@ interface ReminderCardProps {
onToggle: () => void;
onClick: () => void;
onDelete?: () => void;
isMissed?: boolean;
}
export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
export function ReminderCard({ event, onToggle, onClick, isMissed = false }: ReminderCardProps) {
const isCompleted = event.is_completed ?? false;
const [isHovered, setIsHovered] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
// 计算距离提醒时间的相关显示
// 计算时间信息
const timeInfo = useMemo(() => {
const now = new Date();
const eventDate = new Date(event.date);
@ -22,7 +24,6 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
const isPast = diff < 0;
const isToday = eventDate.toDateString() === now.toDateString();
// 格式化时间显示
const timeStr = eventDate.toLocaleString('zh-CN', {
month: 'numeric',
day: 'numeric',
@ -33,12 +34,32 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
return { isPast, isToday, timeStr, diff };
}, [event.date]);
// 颜色主题
const getThemeColor = () => {
// 获取文字颜色
const getTextColor = () => {
if (isCompleted) return '#999';
if (timeInfo.isPast) return '#666';
return '#1a1a1a';
};
// 获取时间颜色
const getTimeColor = () => {
if (isCompleted) return '#bbb';
if (timeInfo.isPast) return '#c41c1c';
if (timeInfo.isToday) return '#666';
return '#888';
return '#666';
};
// 获取背景色
const getBackground = () => {
if (isCompleted) return 'rgba(0, 0, 0, 0.02)';
if (isMissed) return 'rgba(196, 28, 28, 0.03)';
return 'rgba(0, 0, 0, 0.02)';
};
// 获取边框颜色
const getBorderColor = () => {
if (isCompleted) return 'rgba(0, 0, 0, 0.06)';
if (isMissed) return 'rgba(196, 28, 28, 0.15)';
return 'rgba(0, 0, 0, 0.06)';
};
return (
@ -49,26 +70,55 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
onMouseLeave={() => setIsHovered(false)}
style={{
cursor: 'pointer',
opacity: isCompleted ? 0.4 : 1,
transition: 'all 0.2s ease',
opacity: isAnimating ? 0 : isCompleted ? 0.4 : 1,
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)',
borderLeft: `2px solid ${getThemeColor()}`,
background: 'rgba(0, 0, 0, 0.02)',
border: '1px solid rgba(0, 0, 0, 0.04)',
background: getBackground(),
border: `1px solid ${getBorderColor()}`,
borderLeft: isMissed && !isCompleted ? '3px solid #c41c1c' : undefined,
transition: 'all 0.3s ease',
animation: isAnimating ? 'reminder-card-fadeOut 0.3s ease-out forwards' : 'none',
}}
>
<style>{`
@keyframes reminder-card-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
@keyframes reminder-card-fadeOut {
0% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(20px); }
}
`}</style>
<Group justify="space-between" wrap="nowrap">
<Group gap="sm" style={{ flex: 1, minWidth: 0 }}>
{/* Checkbox - 点击切换完成状态 */}
<Checkbox
checked={isCompleted}
onChange={(e) => {
e.stopPropagation();
onToggle();
{/* Checkbox */}
<div
style={{
display: 'flex',
alignItems: 'center',
animation: isAnimating ? 'reminder-card-pulse 0.3s ease-out' : 'none',
}}
size="xs"
color="#1a1a1a"
/>
>
<Checkbox
checked={isCompleted}
onChange={(e) => {
e.stopPropagation();
if (isMissed && !isCompleted) {
// 已过期提醒:先播放动画,动画结束后再触发 toggle
setIsAnimating(true);
setTimeout(() => {
setIsAnimating(false);
onToggle();
}, 300);
} else {
onToggle();
}
}}
size="xs"
color="#1a1a1a"
/>
</div>
<Stack gap={2} style={{ flex: 1, minWidth: 0 }} onClick={onClick}>
{/* Title */}
@ -78,23 +128,34 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
lineClamp={1}
style={{
textDecoration: isCompleted ? 'line-through' : 'none',
color: isCompleted ? '#bbb' : '#1a1a1a',
color: getTextColor(),
letterSpacing: '0.03em',
transition: 'color 0.2s ease',
}}
>
{event.title}
</Text>
{/* Time and content */}
<Group gap="xs">
<Text size="xs" c={getThemeColor()}>
{timeInfo.timeStr}
</Text>
</Group>
{/* Time */}
<Text
size="xs"
c={getTimeColor()}
style={{ letterSpacing: '0.05em' }}
>
{timeInfo.timeStr}
</Text>
{/* Content preview */}
{event.content && !isCompleted && (
<Text size="xs" c="#999" lineClamp={1}>
{/* Content preview - 始终显示 */}
{event.content && (
<Text
size="xs"
c={isCompleted ? '#bbb' : '#999'}
lineClamp={1}
style={{
opacity: isCompleted ? 0.6 : 1,
transition: 'opacity 0.2s ease',
}}
>
{event.content}
</Text>
)}
@ -111,7 +172,11 @@ export function ReminderCard({ event, onToggle, onClick }: ReminderCardProps) {
e.stopPropagation();
onClick();
}}
style={{ color: '#999' }}
style={{
color: '#999',
opacity: isHovered ? 1 : 0,
transition: 'all 0.2s ease',
}}
title="编辑"
>
<IconDots size={12} />

View File

@ -83,8 +83,8 @@ export function ReminderList({
result.today.sort(sortByDate);
result.tomorrow.sort(sortByDate);
result.later.sort(sortByDate);
// 已过期按时间倒序(最近的在上面
result.missed.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
// 已过期按时间正序(最早的在前
result.missed.sort(sortByDate);
return result;
}, [events]);
@ -176,6 +176,7 @@ export function ReminderList({
onClick={() => onEventClick(event)}
onToggle={() => onToggleComplete(event)}
onDelete={onDelete ? () => onDelete(event) : undefined}
isMissed={true}
/>
))}
{grouped.missed.length > 3 && (

View File

@ -8,6 +8,7 @@ import {
Group,
Button,
ActionIcon,
Box,
} from '@mantine/core';
import { IconArrowLeft, IconRotateClockwise, IconTrash, IconArchive } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
@ -17,21 +18,27 @@ import type { Event } from '../types';
export function ArchivePage() {
const navigate = useNavigate();
const events = useAppStore((state) => state.events);
const fetchEvents = useAppStore((state) => state.fetchEvents);
const updateEventById = useAppStore((state) => state.updateEventById);
const deleteEventById = useAppStore((state) => state.deleteEventById);
// 页面加载时检查登录状态
// 页面加载时获取数据
useEffect(() => {
const isAuthenticated = useAppStore.getState().isAuthenticated;
if (!isAuthenticated) {
navigate('/login', { replace: true });
} else {
fetchEvents();
}
}, [navigate]);
}, [navigate, fetchEvents]);
// 获取已归档的提醒(已过期且已勾选的)
const archivedReminders = events.filter(
(e) => e.type === 'reminder' && e.is_completed
).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const archivedReminders = events.filter((e) => {
if (e.type !== 'reminder' || !e.is_completed) return false;
const eventDate = new Date(e.date);
const now = new Date();
return eventDate < now; // 仅已过期的
}).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const handleRestore = async (event: Event) => {
await updateEventById(event.id, { is_completed: false });
@ -42,15 +49,17 @@ export function ArchivePage() {
};
return (
<div
<Box
style={{
minHeight: '100vh',
background: '#faf9f7',
paddingTop: 80,
paddingBottom: 40,
}}
>
<Container size="xl" py="md">
<Container size="xs">
{/* Header */}
<Group mb="lg" style={{ flexShrink: 0 }}>
<Group mb="lg">
<Button
variant="subtle"
color="gray"
@ -81,7 +90,7 @@ export function ArchivePage() {
</Group>
{/* Content */}
<Paper p="md" withBorder radius={4} style={{ maxWidth: 600 }}>
<Paper p="md" withBorder radius={4}>
{archivedReminders.length === 0 ? (
<Stack align="center" justify="center" py="xl">
<Text c="#999" size="sm" ta="center" style={{ letterSpacing: '0.05em' }}>
@ -159,6 +168,6 @@ export function ArchivePage() {
)}
</Paper>
</Container>
</div>
</Box>
);
}

View File

@ -1 +0,0 @@
/e/qia/client

View File

@ -1 +0,0 @@
/e/qia/client