Skip to content

Commit 35ac62e

Browse files
committed
feat(dashboard): 首页统计与知识预览卡片支持点击跳转
核心统计卡片与知识预览卡片新增目标路径,点击或键盘回车即可跳转至对应模块,提升首页到各功能模块的可达性。
1 parent 945ca57 commit 35ac62e

2 files changed

Lines changed: 44 additions & 8 deletions

File tree

‎client/src/features/dashboard/components/KnowledgePreviewCard.tsx‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
* @ai-context: 通用组件:KnowledgePreviewCard。
88
*/
99
import { useRef } from 'react';
10+
import { useNavigate } from 'react-router-dom';
1011
import { motion, useMotionValue, useTransform, useSpring } from 'framer-motion';
11-
import { FileText, Layers, Timer, Lightbulb } from 'lucide-react';
12+
import { FileText, Layers, Timer, Lightbulb, ArrowUpRight } from 'lucide-react';
1213
import { cn } from '@/lib/utils';
1314
import { SPRING } from '@/lib/animation/springConfig';
1415

@@ -20,6 +21,8 @@ interface KnowledgeCard {
2021
title: string;
2122
excerpt?: string;
2223
time: string;
24+
/** 点击跳转目标路径 */
25+
targetPath?: string;
2326
}
2427

2528
interface KnowledgePreviewCardProps {
@@ -70,9 +73,19 @@ const asymmetricRadius = [
7073

7174
export default function KnowledgePreviewCard({ card, index }: KnowledgePreviewCardProps) {
7275
const ref = useRef<HTMLDivElement>(null);
76+
const navigate = useNavigate();
7377
const mouseX = useMotionValue(0);
7478
const mouseY = useMotionValue(0);
7579

80+
const clickable = !!card.targetPath;
81+
const handleClick = () => { if (card.targetPath) navigate(card.targetPath); };
82+
const handleKeyDown = (e: React.KeyboardEvent) => {
83+
if ((e.key === 'Enter' || e.key === ' ') && card.targetPath) {
84+
e.preventDefault();
85+
navigate(card.targetPath);
86+
}
87+
};
88+
7689
const rotateX = useSpring(
7790
useTransform(mouseY, [-0.5, 0.5], [8, -8]),
7891
{ stiffness: SPRING.default.stiffness, damping: SPRING.default.damping }
@@ -101,8 +114,12 @@ export default function KnowledgePreviewCard({ card, index }: KnowledgePreviewCa
101114
return (
102115
<motion.div
103116
ref={ref}
117+
role={clickable ? 'link' : undefined}
118+
tabIndex={clickable ? 0 : undefined}
119+
aria-label={clickable ? `打开${config.label}:${card.title}` : undefined}
104120
className={cn(
105-
'relative overflow-hidden cursor-default group',
121+
'relative overflow-hidden group focus-visible:outline-2 focus-visible:outline-brand-500',
122+
clickable ? 'cursor-pointer' : 'cursor-default',
106123
'border border-border/20 backdrop-blur-sm',
107124
'bg-bg-elevated/40 hover:bg-bg-elevated/60',
108125
'transition-colors duration-beat-x3',
@@ -116,7 +133,10 @@ export default function KnowledgePreviewCard({ card, index }: KnowledgePreviewCa
116133
}}
117134
onMouseMove={handleMouseMove}
118135
onMouseLeave={handleMouseLeave}
136+
onClick={handleClick}
137+
onKeyDown={handleKeyDown}
119138
whileHover={{ y: -4, scale: 1.02 }}
139+
whileTap={clickable ? { scale: 0.97 } : undefined}
120140
transition={SPRING.default}
121141
>
122142
{/* 交融渐变遮罩 */}
@@ -142,7 +162,12 @@ export default function KnowledgePreviewCard({ card, index }: KnowledgePreviewCa
142162
<div className="flex items-center gap-1.5">
143163
<Icon className={cn('w-3.5 h-3.5', config.accentColor)} strokeWidth={1.5} />
144164
<span className={cn('text-c2 font-medium', config.accentColor)}>{config.label}</span>
145-
<span className="text-c2 text-text-tertiary ml-auto">{card.time}</span>
165+
<span className="text-c2 text-text-tertiary ml-auto flex items-center gap-1">
166+
{card.time}
167+
{clickable && (
168+
<ArrowUpRight className="w-3 h-3 opacity-0 group-hover:opacity-70 transition-opacity duration-beat-x2" />
169+
)}
170+
</span>
146171
</div>
147172

148173
{/* 标题 */}

‎client/src/features/dashboard/pages/DashboardPage.tsx‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,10 @@ export default function DashboardPage() {
309309
}, [showRitual, lastSession]);
310310

311311
const heroStats = [
312-
{ label: '今日专注', value: todayPomodoroCount, unit: '次', accent: 'pomodoro', icon: Timer },
313-
{ label: '笔记总数', value: noteTotal, unit: '篇', accent: 'note', icon: FileText },
314-
{ label: '待复习', value: dueFlashcardCount, unit: '张', accent: 'flashcard', icon: Layers },
315-
{ label: '费曼进行中', value: feynmanInProgressCount, unit: '个', accent: 'feynman', icon: Lightbulb },
312+
{ label: '今日专注', value: todayPomodoroCount, unit: '次', accent: 'pomodoro', icon: Timer, path: '/pomodoro' },
313+
{ label: '笔记总数', value: noteTotal, unit: '篇', accent: 'note', icon: FileText, path: '/notes' },
314+
{ label: '待复习', value: dueFlashcardCount, unit: '张', accent: 'flashcard', icon: Layers, path: '/flashcards' },
315+
{ label: '费曼进行中', value: feynmanInProgressCount, unit: '个', accent: 'feynman', icon: Lightbulb, path: '/feynman' },
316316
];
317317

318318
/* ── 学习分析 ── */
@@ -332,6 +332,7 @@ export default function DashboardPage() {
332332
type: 'note',
333333
title: n.title || '无标题笔记',
334334
time: formatRelativeTime(new Date(n.updatedAt)),
335+
targetPath: `/notes/${n.id}`,
335336
});
336337
});
337338

@@ -347,6 +348,7 @@ export default function DashboardPage() {
347348
type: 'flashcard',
348349
title: card ? card.front.replace(/<[^>]*>/g, '').slice(0, 40) : '复习闪卡',
349350
time: formatRelativeTime(new Date(latestReview.reviewedAt)),
351+
targetPath: '/flashcards',
350352
});
351353
}
352354

@@ -361,6 +363,7 @@ export default function DashboardPage() {
361363
type: 'pomodoro',
362364
title: `${Math.round(latest.actualDuration / 60)} 分钟专注`,
363365
time: formatRelativeTime(new Date(latest.completedAt)),
366+
targetPath: '/pomodoro',
364367
});
365368
}
366369

@@ -374,6 +377,7 @@ export default function DashboardPage() {
374377
type: 'feynman',
375378
title: sortedFeynman[0].concept,
376379
time: formatRelativeTime(new Date(sortedFeynman[0].updatedAt)),
380+
targetPath: `/feynman/${sortedFeynman[0].id}`,
377381
});
378382
}
379383

@@ -420,14 +424,21 @@ export default function DashboardPage() {
420424
return (
421425
<motion.div
422426
key={stat.label}
427+
role="link"
428+
tabIndex={0}
429+
aria-label={`进入${stat.label}模块`}
430+
onClick={() => navigate(stat.path)}
431+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(stat.path); } }}
423432
className={cn(
424-
'relative p-5 rounded-kb-xl',
433+
'relative p-5 rounded-kb-xl cursor-pointer',
425434
'border border-border/15 backdrop-blur-sm',
426435
'bg-bg-elevated/30 hover:bg-bg-elevated/50',
427436
'transition-all duration-beat-x3 group',
437+
'focus-visible:outline-2 focus-visible:outline-brand-500',
428438
)}
429439
variants={heroStatVariant}
430440
whileHover={{ y: -2, scale: 1.02 }}
441+
whileTap={{ scale: 0.97 }}
431442
transition={SPRING.default}
432443
>
433444
<div className="flex items-center gap-2 mb-3">

0 commit comments

Comments
 (0)