P8 平台轻量化改造 + SSG修复 + 编程导师 + 文档完善

- Prisma: Tool 模型加 affiliateLink;免费用户沙盒 10→5 次/日
- 后端: Tools API + /admin/tools CRUD 5 端点;Practices 完整模块
- 导航: 主菜单隐藏企业版/社区(URL 可访问)
- 首页: 重定位为 AI 工具指南;新增精选工具区块;Feature 重写
- 工具页: affiliateLink 绿色推荐 Badge
- SSG 修复: config.ts 构建时直连 localhost:4000,页面 108→127
- 沙盒: 新增编程导师场景(苏格拉底教学法)
- 练习系统: Practices 多场景练习(含结构化评分)
- 技能广场: 6 个付费 Skill(标题大师/回款助手等)
- 管理后台: Models/Posts/Practices CRUD 页面
- 文档: README + progress.md 全面更新;AGENTS.md 同步定位
- 清理: .env.example 移除;tsbuildinfo gitignore
This commit is contained in:
yuzhiran-dev
2026-06-18 18:14:07 +08:00
parent fb8152401b
commit 0f215d2aad
103 changed files with 5240 additions and 778 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { apiFetch } from '../../../lib/auth';
import { Skeleton } from '@/components/ui/skeleton';
import { toast } from 'sonner';
interface Stats {
totalCourses: number;
@@ -27,7 +28,7 @@ export default function DashboardPage() {
const res = await apiFetch('/dashboard/stats');
const data = await res.json();
setStats(data);
} catch (e) { console.error(e) }
} catch { /* ignore */ }
setLoading(false);
}
+3 -2
View File
@@ -5,6 +5,7 @@ import Link from 'next/link';
import { apiFetch } from '../../../lib/auth';
import { Skeleton } from '@/components/ui/skeleton';
import { useT } from '@/i18n';
import { toast } from 'sonner';
interface FavoritePrompt { id: number; promptId: number; prompt: { id: number; title: string; description: string; likeCount: number } }
@@ -16,12 +17,12 @@ export default function FavoritesPage() {
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 { /* ignore */ }
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 { toast.error("操作失败") }
}
if (loading) return (
+2 -1
View File
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import Link from 'next/link';
import { apiFetch } from '../../../lib/auth';
import { Skeleton } from '@/components/ui/skeleton';
import { toast } from 'sonner';
interface LearningItem {
courseId: number;
@@ -26,7 +27,7 @@ export default function LearningPage() {
const res = await apiFetch('/courses/my-learning');
const data = await res.json();
setItems(data.items || []);
} catch (e) { console.error(e) }
} catch { toast.error("加载失败") }
setLoading(false);
}
+10 -11
View File
@@ -6,6 +6,7 @@ import { apiFetch } from '../../../lib/auth';
import { Skeleton } from '@/components/ui/skeleton';
import PaymentModal from '@/components/ui/payment-modal';
import { useT } from '@/i18n';
import { toast } from 'sonner';
import { isWeChatBrowser } from '@/lib/wechat';
interface PayResult {
@@ -26,10 +27,10 @@ interface Order {
}
const PLANS = [
{ id: 'FREE', nameKey: 'planFree' as const, price: 0, period: '', popular: false, features: ['featureSandboxFree', 'featureModelsFree', 'featurePromptsFree', 'featureCoursesFree', 'featureAdsFree'] as const },
{ id: 'MONTHLY', nameKey: 'planMonthly' as const, price: 49.9, period: 'perMonth', popular: true, features: ['featureSandboxPro', 'featureModelsPro', 'featurePromptsPro', 'featureCoursesPro', 'featureAdsPro'] as const },
{ id: 'YEARLY', nameKey: 'planYearly' as const, price: 299, period: 'perYear', popular: false, features: ['featureSandboxUnlimited', 'featureModelsPremium', 'featurePromptsPremium', 'featureCoursesPremium', 'featureAdsPremium'] as const },
];
{ id: 'FREE', nameKey: 'planFree', price: 0, period: '', popular: false, features: ['featureSandboxFree', 'featureModelsFree', 'featurePromptsFree', 'featureCoursesFree', 'featureAdsFree'] },
{ id: 'MONTHLY', nameKey: 'planMonthly', price: 49.9, period: 'perMonth', popular: true, features: ['featureSandboxPro', 'featureModelsPro', 'featurePromptsPro', 'featureCoursesPro', 'featureAdsPro'] },
{ id: 'YEARLY', nameKey: 'planYearly', price: 299, period: 'perYear', popular: false, features: ['featureSandboxUnlimited', 'featureModelsPremium', 'featurePromptsPremium', 'featureCoursesPremium', 'featureAdsPremium'] },
] as const;
const FEATURE_LABELS = ['featureSandbox', 'featureModels', 'featurePrompts', 'featureCourses', 'featureAds'] as const;
@@ -54,12 +55,12 @@ export default function MemberPage() {
apiFetch('/orders'),
apiFetch('/sandbox/quota'),
]);
if (subRes.ok) setSubscription(await subRes.json());
if (subRes.ok) setSubscription(await (subRes as Response).json());
const ordersData = await ordersRes.json();
setOrders(ordersData.items || []);
const quotaData = await quotaRes.json();
if (quotaData.remaining !== undefined) setQuota(quotaData);
} catch (e) { console.error(e) }
} catch { toast.error("加载失败") }
setLoading(false);
}
@@ -82,9 +83,7 @@ export default function MemberPage() {
const payUrl = data.payResult.payUrl || data.payResult.redirectUrl;
const qrCode = data.payResult.qrcode || data.payResult.codeUrl;
if ((payUrl === 'mock://pay' || qrCode === 'mock://pay') || data.message === '模拟支付完成') {
loadData();
} else if (payUrl || qrCode) {
if (payUrl || qrCode) {
setPaymentModal({
open: true,
orderNo: data.order.orderNo,
@@ -93,7 +92,7 @@ export default function MemberPage() {
});
}
}
} catch (e) { console.error(e) }
} catch { toast.error("加载失败") }
setPayLoading(null);
}
@@ -146,7 +145,7 @@ export default function MemberPage() {
<h3 className="text-lg font-semibold text-foreground mb-1">{t.member[plan.nameKey]}</h3>
<div className="mb-4">
{plan.price > 0 ? (
<span className="text-3xl font-bold text-foreground">{plan.price === 49.9 ? t.member.priceMonthly : t.member.priceYearly}<span className="text-sm font-normal text-muted-foreground">{t.member[plan.period]}</span></span>
<span className="text-3xl font-bold text-foreground">{plan.price === 49.9 ? t.member.priceMonthly : t.member.priceYearly}<span className="text-sm font-normal text-muted-foreground">{plan.price > 0 ? t.member[plan.period as keyof typeof t.member] : ''}</span></span>
) : (
<span className="text-2xl font-bold text-foreground">¥0</span>
)}
+4 -3
View File
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import { apiFetch } from '../../../lib/auth';
import { Skeleton } from '@/components/ui/skeleton';
import { useT } from '@/i18n';
import { toast } from 'sonner';
interface Profile { nickname: string; email: string; phone: string; avatar: string }
@@ -24,7 +25,7 @@ export default function SettingsPage() {
const res = await apiFetch('/dashboard/profile');
const data = await res.json();
setProfile(data); setNickname(data.nickname || ''); setEmail(data.email || '');
} catch (e) { console.error(e) }
} catch { /* ignore */ }
setLoading(false);
}
@@ -33,8 +34,8 @@ export default function SettingsPage() {
setSaving(true);
try {
await apiFetch('/dashboard/profile', { method: 'PUT', body: JSON.stringify({ nickname, email }) });
alert(t.settings.saveSuccess);
} catch (e) { console.error(e) }
toast.success(t.settings.saveSuccess);
} catch { toast.error("保存失败") }
setSaving(false);
}