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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user