diff --git a/frontend/src/app/learning/analytics/page.tsx b/frontend/src/app/learning/analytics/page.tsx index 121e3f4..152db97 100644 --- a/frontend/src/app/learning/analytics/page.tsx +++ b/frontend/src/app/learning/analytics/page.tsx @@ -4,58 +4,33 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { apiFetch } from '@/lib/auth'; import { Skeleton } from '@/components/ui/skeleton'; +import { useT } from '@/i18n'; -interface Domain { - id: string; - name: string; - sessionCount: number; - mastery: number; - lastActive: string | null; - weak: boolean; -} - -interface Recommendation { - title: string; - url: string; -} - -interface Analytics { - domains: Domain[]; - totalSessions: number; - weakDomains: string[]; - recommendations: Recommendation[]; -} +interface Domain { id: string; name: string; sessionCount: number; mastery: number; lastActive: string | null; weak: boolean } +interface Recommendation { title: string; url: string } +interface Analytics { domains: Domain[]; totalSessions: number; weakDomains: string[]; recommendations: Recommendation[] } export default function LearningAnalyticsPage() { + const t = useT(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - useEffect(() => { - loadAnalytics(); - }, []); + useEffect(() => { loadAnalytics(); }, []); async function loadAnalytics() { try { const res = await apiFetch('/learning/analytics'); if (!res.ok) throw new Error('加载失败'); - const json = await res.json(); - setData(json); - } catch (e: any) { - setError(e.message); - } + setData(await res.json()); + } catch (e: any) { setError(e.message); } setLoading(false); } if (loading) return (
- - -
- - - -
+ +
{[1,2,3].map(i => )}
); @@ -63,65 +38,55 @@ export default function LearningAnalyticsPage() { if (error) return (

{error}

- +
); const coveredDomains = data?.domains.filter(d => d.sessionCount > 0) || []; const weakDomains = data?.domains.filter(d => d.weak) || []; - const avgMastery = data?.domains.length - ? Math.round(data.domains.reduce((sum, d) => sum + d.mastery, 0) / data.domains.length) - : 0; + const avgMastery = data?.domains.length ? Math.round(data.domains.reduce((sum, d) => sum + d.mastery, 0) / data.domains.length) : 0; return (
- - ← 返回我的 - -

学情分析

-

基于 AI 沙盒对话分析你的学习情况

+ ← {t.path.back} +

{t.learning.analytics}

+

{t.learning.analyticsDesc}

-
AI 对话次数
+
{t.learning.totalSessions}
{data?.totalSessions || 0}
-
涉及知识领域
+
{t.learning.domainsCovered}
{coveredDomains.length}/{data?.domains.length || 0}
-
平均掌握度
+
{t.learning.avgMastery}
{avgMastery}%
{data && data.domains.length > 0 && (
-

知识领域覆盖

+

{t.learning.knowledgeDomains}

{data.domains.map(domain => (
{domain.name} - {domain.weak && ( - 待加强 - )} + {domain.weak && {t.learning.toStrengthen}}
- {domain.sessionCount} 次对话 - = 60 ? 'text-green-600' : domain.mastery >= 30 ? 'text-amber-600' : 'text-red-500' - }`}>{domain.mastery}% + {t.learning.conversations.replace('{count}', String(domain.sessionCount))} + = 60 ? 'text-green-600' : domain.mastery >= 30 ? 'text-amber-600' : 'text-red-500'}`}>{domain.mastery}%
-
= 60 ? 'bg-green-500' : domain.mastery >= 30 ? 'bg-amber-500' : 'bg-red-500' - }`} style={{ width: `${domain.mastery}%` }} /> +
= 60 ? 'bg-green-500' : domain.mastery >= 30 ? 'bg-amber-500' : 'bg-red-500'}`} style={{ width: `${domain.mastery}%` }} />
))} @@ -131,15 +96,11 @@ export default function LearningAnalyticsPage() { {weakDomains.length > 0 && (
-

薄弱环节

-

- 以下领域你较少涉及,建议加强学习: -

+

{t.learning.weakAreas}

+

{t.learning.weakDesc}

{weakDomains.map(d => ( - - {d.name} - + {d.name} ))}
@@ -147,18 +108,15 @@ export default function LearningAnalyticsPage() { {data && data.recommendations.length > 0 && (
-

推荐学习

-

根据你的薄弱环节推荐以下内容

+

{t.learning.recommendations}

+

{t.learning.recDesc}

{data.recommendations.map((rec, i) => ( - -
- {rec.title[0]} -
+ +
{rec.title[0]}
{rec.title}
-
点击前往
+
{t.learning.clickToGo}
))} diff --git a/frontend/src/app/learning/path/page.tsx b/frontend/src/app/learning/path/page.tsx index 4b402a8..ef6b933 100644 --- a/frontend/src/app/learning/path/page.tsx +++ b/frontend/src/app/learning/path/page.tsx @@ -4,75 +4,47 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { apiFetch } from '@/lib/auth'; import { Skeleton } from '@/components/ui/skeleton'; +import { useT } from '@/i18n'; -interface Task { - label: string; - action: string; - keyword: string; -} - -interface StageLink { - title: string; - url: string; -} - -interface Stage { - id: string; - title: string; - icon: string; - description: string; - tasks: Task[]; - links: StageLink[]; - completedCount: number; - totalTasks: number; - progress: number; - unlocked: boolean; -} +interface Task { label: string; action: string; keyword: string } +interface StageLink { title: string; url: string } +interface Stage { id: string; title: string; icon: string; description: string; tasks: Task[]; links: StageLink[]; completedCount: number; totalTasks: number; progress: number; unlocked: boolean } export default function LearningPathPage() { + const t = useT(); const [stages, setStages] = useState([]); const [loading, setLoading] = useState(true); - useEffect(() => { - loadPath(); - }, []); + useEffect(() => { loadPath(); }, []); async function loadPath() { - try { - const res = await apiFetch('/learning/path'); - if (res.ok) setStages(await res.json()); - } catch {} + try { const res = await apiFetch('/learning/path'); if (res.ok) setStages(await res.json()); } catch {} setLoading(false); } if (loading) return (
- - + {[1,2,3,4].map(i => )}
); - const totalProgress = stages.length - ? Math.round(stages.reduce((s, st) => s + st.progress, 0) / stages.length) - : 0; + const totalProgress = stages.length ? Math.round(stages.reduce((s, st) => s + st.progress, 0) / stages.length) : 0; const totalCompleted = stages.reduce((s, st) => s + st.completedCount, 0); const totalTasks = stages.reduce((s, st) => s + st.totalTasks, 0); return (
- - ← 返回我的 - -

学习路径

-

从入门到精通,系统掌握 AI 技能

+ ← {t.path.back} +

{t.learning.path}

+

{t.learning.pathDesc}

- 总进度 - {totalCompleted}/{totalTasks} 任务 + {t.path.totalProgress} + {t.path.taskCount.replace('{completed}', String(totalCompleted)).replace('{total}', String(totalTasks))}
@@ -81,18 +53,14 @@ export default function LearningPathPage() {
-
{stages.map((stage, index) => (
-
+
{stage.progress === 100 ? '✅' : stage.icon}
-
@@ -102,25 +70,17 @@ export default function LearningPathPage() {

{stage.description}

- - {stage.completedCount}/{stage.totalTasks} - + {stage.completedCount}/{stage.totalTasks}
-
-
+
-
{stage.tasks.map((task, ti) => { const done = ti < stage.completedCount; return (
- + {done && } {task.label} @@ -129,13 +89,9 @@ export default function LearningPathPage() { ); })}
-
{stage.links.map((link, li) => ( - - {link.title} - + {link.title} ))}
diff --git a/frontend/src/app/my/favorites/page.tsx b/frontend/src/app/my/favorites/page.tsx index c882a0d..99df4eb 100644 --- a/frontend/src/app/my/favorites/page.tsx +++ b/frontend/src/app/my/favorites/page.tsx @@ -4,50 +4,30 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { apiFetch } from '../../../lib/auth'; import { Skeleton } from '@/components/ui/skeleton'; +import { useT } from '@/i18n'; -interface FavoritePrompt { - id: number; - promptId: number; - prompt: { - id: number; - title: string; - description: string; - likeCount: number; - }; -} +interface FavoritePrompt { id: number; promptId: number; prompt: { id: number; title: string; description: string; likeCount: number } } export default function FavoritesPage() { + const t = useT(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); - useEffect(() => { - loadData(); - }, []); + useEffect(() => { loadData(); }, []); async function loadData() { - try { - const res = await apiFetch('/prompts/favorites'); - const data = await res.json(); - setItems(data.items || []); - } catch (e) { console.error(e) } + try { const res = await apiFetch('/prompts/favorites'); const data = await res.json(); setItems(data.items || []); } catch (e) { console.error(e) } setLoading(false); } async function removeFavorite(promptId: number) { - try { - await apiFetch(`/prompts/${promptId}/favorite`, { method: 'POST' }); - setItems(items.filter(i => i.promptId !== promptId)); - } catch (e) { console.error(e) } + try { await apiFetch(`/prompts/${promptId}/favorite`, { method: 'POST' }); setItems(items.filter(i => i.promptId !== promptId)); } catch (e) { console.error(e) } } if (loading) return (
-
- - - -
+
{[1,2,3].map(i => )}
); @@ -55,34 +35,24 @@ export default function FavoritesPage() { return (
- - ← 返回我的 - -

我的收藏

-

收藏的提示词和课程内容

+ ← {t.favorites.back} +

{t.favorites.title}

+

{t.favorites.desc}

{items.length === 0 ? (
-
- ❤️ -
-

还没有收藏任何内容

- - 浏览提示词 - +
❤️
+

{t.favorites.empty}

+ {t.favorites.browsePrompts}
) : (
{items.map(item => (
- - {item.prompt.title} - - + {item.prompt.title} +

{item.prompt.description}

❤️ {item.prompt.likeCount}
diff --git a/frontend/src/app/my/settings/page.tsx b/frontend/src/app/my/settings/page.tsx index 5b54e44..c68a465 100644 --- a/frontend/src/app/my/settings/page.tsx +++ b/frontend/src/app/my/settings/page.tsx @@ -4,15 +4,12 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { apiFetch } from '../../../lib/auth'; import { Skeleton } from '@/components/ui/skeleton'; +import { useT } from '@/i18n'; -interface Profile { - nickname: string; - email: string; - phone: string; - avatar: string; -} +interface Profile { nickname: string; email: string; phone: string; avatar: string } export default function SettingsPage() { + const t = useT(); const router = useRouter(); const [profile, setProfile] = useState({ nickname: '', email: '', phone: '', avatar: '' }); const [loading, setLoading] = useState(true); @@ -20,17 +17,13 @@ export default function SettingsPage() { const [nickname, setNickname] = useState(''); const [email, setEmail] = useState(''); - useEffect(() => { - loadProfile(); - }, []); + useEffect(() => { loadProfile(); }, []); async function loadProfile() { try { const res = await apiFetch('/dashboard/profile'); const data = await res.json(); - setProfile(data); - setNickname(data.nickname || ''); - setEmail(data.email || ''); + setProfile(data); setNickname(data.nickname || ''); setEmail(data.email || ''); } catch (e) { console.error(e) } setLoading(false); } @@ -39,16 +32,13 @@ export default function SettingsPage() { e.preventDefault(); setSaving(true); try { - await apiFetch('/dashboard/profile', { - method: 'PUT', - body: JSON.stringify({ nickname, email }), - }); - alert('保存成功'); + await apiFetch('/dashboard/profile', { method: 'PUT', body: JSON.stringify({ nickname, email }) }); + alert(t.settings.saveSuccess); } catch (e) { console.error(e) } setSaving(false); } - async function handleLogout() { + function handleLogout() { localStorage.removeItem('token'); localStorage.removeItem('refreshToken'); router.push('/auth'); @@ -57,11 +47,7 @@ export default function SettingsPage() { if (loading) return (
-
- - - -
+
{[1,2,3].map(i => )}
); @@ -69,52 +55,35 @@ export default function SettingsPage() { return (
- -

设置

-

管理你的账号偏好

+ +

{t.settings.title}

+

{t.settings.desc}

-

个人信息

- +

{t.settings.personalInfo}

- - setNickname(e.target.value)} - className="w-full px-4 py-2.5 border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" - /> + + setNickname(e.target.value)} + className="w-full px-4 py-2.5 border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" />
-
- - setEmail(e.target.value)} - className="w-full px-4 py-2.5 border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" - /> + + setEmail(e.target.value)} + className="w-full px-4 py-2.5 border border-border rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-brand-500" />
-
-
-

账号安全

-
diff --git a/frontend/src/components/layout/footer.tsx b/frontend/src/components/layout/footer.tsx index 45a1d84..e293666 100644 --- a/frontend/src/components/layout/footer.tsx +++ b/frontend/src/components/layout/footer.tsx @@ -1,8 +1,20 @@ +'use client'; + +import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useT } from '@/i18n'; +import { API_BASE } from '@/lib/config'; export function Footer() { const t = useT(); + const [config, setConfig] = useState>({}); + + useEffect(() => { + fetch(`${API_BASE}/public/config`) + .then(r => r.json()).then(data => setConfig(data || {})) + .catch(() => {}); + }, []); + return (