feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径

This commit is contained in:
yuzhiran-dev
2026-05-18 09:48:51 +08:00
commit 11bb86854c
277 changed files with 37755 additions and 0 deletions
+165
View File
@@ -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">
&larr;
</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>
);
}