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

View File

@ -83,8 +83,8 @@ export function ReminderList({
result.today.sort(sortByDate); result.today.sort(sortByDate);
result.tomorrow.sort(sortByDate); result.tomorrow.sort(sortByDate);
result.later.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; return result;
}, [events]); }, [events]);
@ -176,6 +176,7 @@ export function ReminderList({
onClick={() => onEventClick(event)} onClick={() => onEventClick(event)}
onToggle={() => onToggleComplete(event)} onToggle={() => onToggleComplete(event)}
onDelete={onDelete ? () => onDelete(event) : undefined} onDelete={onDelete ? () => onDelete(event) : undefined}
isMissed={true}
/> />
))} ))}
{grouped.missed.length > 3 && ( {grouped.missed.length > 3 && (

View File

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

View File

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

View File

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