feat: 优化提醒卡片样式和归档功能
- 优化提醒卡片样式,统一黑白灰配色 - 添加checkbox勾选动画和过期提醒淡出效果 - 完善归档页功能(恢复/删除已过期完成提醒) - 修复过期检测逻辑(精确到时间点而非仅日期) Co-Authored-By: Claude (MiniMax-M2.1) <noreply@anthropic.com>
This commit is contained in:
parent
9e4b4022bd
commit
a8b4f17043
@ -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} />
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
/e/qia/client
|
|
||||||
@ -1 +0,0 @@
|
|||||||
/e/qia/client
|
|
||||||
Loading…
x
Reference in New Issue
Block a user