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:
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function EditPractice() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [form, setForm] = useState({ title: '', description: '', difficulty: 'BEGINNER', category: '', sortOrder: 0, isActive: true });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const res = await fetch(`${API_BASE}/admin/practices/${params.id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setForm({ title: data.title, description: data.description || '', difficulty: data.difficulty || 'BEGINNER', category: data.category || '', sortOrder: data.sortOrder || 0, isActive: data.isActive ?? true });
|
||||
}
|
||||
} catch {}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const res = await fetch(`${API_BASE}/admin/practices/${params.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success('更新成功');
|
||||
router.push('/admin/practices');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
toast.error(err.message || '更新失败');
|
||||
}
|
||||
} catch { toast.error('网络错误'); }
|
||||
}
|
||||
|
||||
if (loading) return <div className="p-6 space-y-4"><Skeleton className="h-8 w-48" /><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /></div>;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-6">编辑练习</h1>
|
||||
<form onSubmit={submit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">标题</label>
|
||||
<input className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm" required
|
||||
value={form.title} onChange={e => setForm({ ...form, title: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">描述</label>
|
||||
<textarea className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm" rows={4}
|
||||
value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">难度</label>
|
||||
<select className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||||
value={form.difficulty} onChange={e => setForm({ ...form, difficulty: e.target.value })}>
|
||||
<option value="BEGINNER">入门</option>
|
||||
<option value="INTERMEDIATE">中等</option>
|
||||
<option value="ADVANCED">高级</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">分类</label>
|
||||
<input className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||||
value={form.category} onChange={e => setForm({ ...form, category: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1">排序</label>
|
||||
<input type="number" className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm"
|
||||
value={form.sortOrder} onChange={e => setForm({ ...form, sortOrder: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="isActive" checked={form.isActive}
|
||||
onChange={e => setForm({ ...form, isActive: e.target.checked })} />
|
||||
<label htmlFor="isActive" className="text-sm text-foreground">启用</label>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button type="submit" className="rounded-lg bg-primary text-primary-foreground px-6 py-2 text-sm font-medium hover:bg-primary/90">保存</button>
|
||||
<button type="button" onClick={() => router.back()} className="rounded-lg border border-border px-6 py-2 text-sm font-medium hover:bg-accent">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user