feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { apiFetch } from '../../../lib/auth';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface Stats {
|
||||
totalCourses: number;
|
||||
completedCourses: number;
|
||||
totalSandboxSessions: number;
|
||||
totalPrompts: number;
|
||||
totalLikes: number;
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const router = useRouter();
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await apiFetch('/dashboard/stats');
|
||||
const data = await res.json();
|
||||
setStats(data);
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
</div>
|
||||
<Skeleton className="h-64 w-full rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<button onClick={() => router.back()} className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">
|
||||
← 返回
|
||||
</button>
|
||||
<h1 className="text-3xl font-bold text-foreground">数据看板</h1>
|
||||
<p className="mt-2 text-muted-foreground">查看你的学习数据</p>
|
||||
</div>
|
||||
|
||||
{stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">学习课程</div>
|
||||
<div className="text-2xl font-bold text-brand-600">{stats.completedCourses}/{stats.totalCourses}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">已完成/总数</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">沙箱使用</div>
|
||||
<div className="text-2xl font-bold text-purple-600">{stats.totalSandboxSessions}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">总对话次数</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">提示词</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.totalPrompts}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">收藏/创建</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card rounded-xl border border-border p-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">获得点赞</div>
|
||||
<div className="text-2xl font-bold text-red-600">{stats.totalLikes}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">总点赞数</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-card rounded-2xl border border-border p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">学习进度</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="text-center p-4 bg-muted/50 rounded-xl">
|
||||
<div className="text-3xl font-bold text-foreground">{stats?.totalCourses || 0}</div>
|
||||
<div className="text-sm text-muted-foreground">总课程数</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-muted/50 rounded-xl">
|
||||
<div className="text-3xl font-bold text-foreground">{stats?.completedCourses || 0}</div>
|
||||
<div className="text-sm text-muted-foreground">已完成</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-muted/50 rounded-xl">
|
||||
<div className="text-3xl font-bold text-foreground">
|
||||
{stats?.totalCourses ? Math.round((stats.completedCourses / stats.totalCourses) * 100) : 0}%
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">完成率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '../../../lib/auth';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface FavoritePrompt {
|
||||
id: number;
|
||||
promptId: number;
|
||||
prompt: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
likeCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function FavoritesPage() {
|
||||
const [items, setItems] = useState<FavoritePrompt[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
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) }
|
||||
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) }
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
</div>
|
||||
<Skeleton className="h-64 w-full rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<Link href="/my" className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">
|
||||
← 返回我的
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-foreground">我的收藏</h1>
|
||||
<p className="mt-2 text-muted-foreground">收藏的提示词和课程内容</p>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
❤️
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-4">还没有收藏任何内容</p>
|
||||
<Link href="/prompts" className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">
|
||||
浏览提示词
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{items.map(item => (
|
||||
<div key={item.id} className="bg-card rounded-xl border border-border p-5 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<Link href={`/prompts/${item.prompt.id}`} className="font-semibold text-foreground hover:text-brand-600">
|
||||
{item.prompt.title}
|
||||
</Link>
|
||||
<button onClick={() => removeFavorite(item.promptId)} className="text-muted-foreground hover:text-red-500 text-sm">
|
||||
❤️
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-2 line-clamp-2">{item.prompt.description}</p>
|
||||
<div className="text-xs text-muted-foreground">❤️ {item.prompt.likeCount}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '../../../lib/auth';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface LearningItem {
|
||||
courseId: number;
|
||||
courseTitle: string;
|
||||
progress: number;
|
||||
completedLessons: number;
|
||||
totalLessons: number;
|
||||
}
|
||||
|
||||
export default function LearningPage() {
|
||||
const [items, setItems] = useState<LearningItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const res = await apiFetch('/courses/my-learning');
|
||||
const data = await res.json();
|
||||
setItems(data.items || []);
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
</div>
|
||||
<Skeleton className="h-64 w-full rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<Link href="/my" className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">
|
||||
← 返回我的
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-foreground">学习进度</h1>
|
||||
<p className="mt-2 text-muted-foreground">跟踪你的课程学习进度</p>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<div className="w-16 h-16 bg-muted rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
📚
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-4">还没有学习任何课程</p>
|
||||
<Link href="/courses" className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">
|
||||
去选课
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{items.map(item => (
|
||||
<Link key={item.courseId} href={`/courses/${item.courseId}`}
|
||||
className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold text-foreground">{item.courseTitle}</h3>
|
||||
<span className="text-sm text-muted-foreground">{item.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2 mb-2">
|
||||
<div className="bg-brand-600 h-2 rounded-full transition-all" style={{ width: `${item.progress}%` }} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
已完成 {item.completedLessons}/{item.totalLessons} 课时
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '../../../lib/auth';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface Subscription {
|
||||
id: number;
|
||||
plan: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface Order {
|
||||
id: number;
|
||||
orderNo: string;
|
||||
amount: number;
|
||||
planType: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function MemberPage() {
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [payLoading, setPayLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const [subRes, ordersRes] = await Promise.all([
|
||||
apiFetch('/subscriptions/current').catch(() => ({ ok: false })),
|
||||
apiFetch('/orders'),
|
||||
]);
|
||||
|
||||
if (subRes.ok) {
|
||||
const subData = await subRes.json();
|
||||
setSubscription(subData);
|
||||
}
|
||||
|
||||
const ordersData = await ordersRes.json();
|
||||
setOrders(ordersData.items || []);
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function handleSubscribe(planType: string) {
|
||||
setPayLoading(true);
|
||||
try {
|
||||
const res = await apiFetch('/orders/create', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
amount: planType === 'MONTHLY' ? 29.9 : 199,
|
||||
planType,
|
||||
payChannel: 'wxpay',
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.order && data.payResult) {
|
||||
alert('订单创建成功,请扫码支付(模拟模式)');
|
||||
loadData();
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
setPayLoading(false);
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
</div>
|
||||
<Skeleton className="h-64 w-full rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<Link href="/my" className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">
|
||||
← 返回我的
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-foreground">会员中心</h1>
|
||||
<p className="mt-2 text-muted-foreground">管理你的会员订阅</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-card rounded-2xl border border-border p-6 mb-8">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">当前会员</h2>
|
||||
{subscription ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||||
subscription.plan === 'YEARLY' ? 'bg-purple-100 text-purple-700' : 'bg-blue-100 text-blue-700'
|
||||
}`}>
|
||||
{subscription.plan === 'YEARLY' ? '年卡会员' : '月卡会员'}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
到期时间:{new Date(subscription.endDate).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
会员权益:全部课程 + 不限次沙箱 + 专属提示词库 + 去广告
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-4">你当前是免费用户</p>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => handleSubscribe('MONTHLY')}
|
||||
disabled={payLoading}
|
||||
className="px-6 py-3 bg-brand-600 text-white rounded-xl hover:bg-brand-700 disabled:opacity-50"
|
||||
>
|
||||
{payLoading ? '处理中...' : '开通月卡 ¥29.9/月'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubscribe('YEARLY')}
|
||||
disabled={payLoading}
|
||||
className="px-6 py-3 border border-brand-600 text-brand-600 rounded-xl hover:bg-brand-50 disabled:opacity-50"
|
||||
>
|
||||
{payLoading ? '处理中...' : '开通年卡 ¥199/年'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">订单记录</h2>
|
||||
{orders.length === 0 ? (
|
||||
<p className="text-muted-foreground text-center py-8">暂无订单记录</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{orders.map(order => (
|
||||
<div key={order.id} className="bg-card rounded-xl border border-border p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{order.planType === 'MONTHLY' ? '月卡会员' : '年卡会员'}</div>
|
||||
<div className="text-sm text-muted-foreground">{new Date(order.createdAt).toLocaleDateString()}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-foreground">¥{order.amount}</div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
order.status === 'PAID' ? 'bg-green-100 text-green-700' :
|
||||
order.status === 'PENDING' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-muted text-muted-foreground'
|
||||
}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '../../lib/auth';
|
||||
|
||||
interface LearningItem {
|
||||
courseId: number;
|
||||
courseTitle: string;
|
||||
progress: number;
|
||||
completedLessons: number;
|
||||
totalLessons: number;
|
||||
}
|
||||
|
||||
export default function MyPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">我的</h1>
|
||||
<p className="mt-2 text-muted-foreground">管理你的个人信息和收藏</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Link href="/my/learning" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-brand-100 rounded-xl flex items-center justify-center text-brand-600 text-xl mb-4">📚</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">学习进度</h3>
|
||||
<p className="text-sm text-muted-foreground">查看你的课程学习进度</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/my/favorites" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center text-red-600 text-xl mb-4">❤️</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">我的收藏</h3>
|
||||
<p className="text-sm text-muted-foreground">提示词、课程等收藏内容</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/my/member" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-amber-600 text-xl mb-4">👑</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">会员中心</h3>
|
||||
<p className="text-sm text-muted-foreground">管理会员订阅和权益</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/my/settings" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-muted rounded-xl flex items-center justify-center text-muted-foreground text-xl mb-4">⚙️</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">设置</h3>
|
||||
<p className="text-sm text-muted-foreground">账号设置和安全偏好</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/learning/analytics" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-brand-100 rounded-xl flex items-center justify-center text-brand-600 text-xl mb-4">📊</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">学情分析</h3>
|
||||
<p className="text-sm text-muted-foreground">基于对话的知识掌握度分析</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/learning/path" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center text-green-600 text-xl mb-4">🗺️</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">学习路径</h3>
|
||||
<p className="text-sm text-muted-foreground">分阶段系统掌握 AI 技能</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { apiFetch } from '../../../lib/auth';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
interface Profile {
|
||||
nickname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
const [profile, setProfile] = useState<Profile>({ nickname: '', email: '', phone: '', avatar: '' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [nickname, setNickname] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
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 || '');
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function handleSave(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
try {
|
||||
await apiFetch('/dashboard/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ nickname, email }),
|
||||
});
|
||||
alert('保存成功');
|
||||
} catch (e) { console.error(e) }
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
router.push('/auth');
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
<Skeleton className="h-24 rounded-xl" />
|
||||
</div>
|
||||
<Skeleton className="h-64 w-full rounded-xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<button onClick={() => router.back()} className="text-sm text-muted-foreground hover:text-foreground mb-4 block">← 返回</button>
|
||||
<h1 className="text-3xl font-bold text-foreground">设置</h1>
|
||||
<p className="mt-2 text-muted-foreground">管理你的账号偏好</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="bg-card rounded-2xl border border-border p-6 space-y-6">
|
||||
<h2 className="text-lg font-semibold text-foreground">个人信息</h2>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">昵称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={nickname}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">邮箱</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="px-6 py-2.5 bg-brand-600 text-white rounded-xl text-sm font-medium hover:bg-brand-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? '保存中...' : '保存修改'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="bg-card rounded-2xl border border-border p-6 mt-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">账号安全</h2>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-6 py-2.5 border border-red-200 text-red-600 rounded-xl text-sm font-medium hover:bg-red-50"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user