🎉 init: 小龙的工作空间
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { useAssignments } from "@/hooks/useAssignments";
|
||||
|
||||
export default function PagePage() {
|
||||
const { data, loading, error, refetch } = useAssignments();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">作业批改</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">作业批改</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="text-center py-8">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
<Button onClick={refetch} variant="secondary" size="sm" className="mt-2">重试</Button>
|
||||
</Card>
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="📭"
|
||||
title="暂无数据"
|
||||
description="还没有任何记录,点击上方按钮开始"
|
||||
action={<Button onClick={() => setOpen(true)} variant="primary" size="sm">创建第一条</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{data.map((item: any) => (
|
||||
<Card key={item.id}>
|
||||
<h3 className="font-medium text-gray-800">{item.title || item.name || `Item ${item.id.slice(0, 8)}`}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{item.createdAt || ""}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { useChapters } from "@/hooks/useChapters";
|
||||
|
||||
export default function PagePage() {
|
||||
const { data, loading, error, refetch } = useChapters();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">章节管理</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">章节管理</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="text-center py-8">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
<Button onClick={refetch} variant="secondary" size="sm" className="mt-2">重试</Button>
|
||||
</Card>
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="📭"
|
||||
title="暂无数据"
|
||||
description="还没有任何记录,点击上方按钮开始"
|
||||
action={<Button onClick={() => setOpen(true)} variant="primary" size="sm">创建第一条</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{data.map((item: any) => (
|
||||
<Card key={item.id}>
|
||||
<h3 className="font-medium text-gray-800">{item.title || item.name || `Item ${item.id.slice(0, 8)}`}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{item.createdAt || ""}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
|
||||
const courses = [
|
||||
{ id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" },
|
||||
{ id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" },
|
||||
{ id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" },
|
||||
];
|
||||
|
||||
export default function CoursePage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-bold text-gray-800">课程中心</h1>
|
||||
{courses.map(c => (
|
||||
<Card key={c.id} className="flex gap-3">
|
||||
<div className="w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center text-3xl flex-shrink-0">{c.cover}</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">{c.title}</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">{c.instructor}</p>
|
||||
<p className="text-sm font-bold text-red-500 mt-2">¥{c.price}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
|
||||
const courses = [
|
||||
{ id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" },
|
||||
{ id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" },
|
||||
{ id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" },
|
||||
];
|
||||
|
||||
export default function CoursePage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-bold text-gray-800">课程中心</h1>
|
||||
{courses.map(c => (
|
||||
<Card key={c.id} className="flex gap-3">
|
||||
<div className="w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center text-3xl flex-shrink-0">{c.cover}</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">{c.title}</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">{c.instructor}</p>
|
||||
<p className="text-sm font-bold text-red-500 mt-2">¥{c.price}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
|
||||
export default function ExercisePage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-bold text-gray-800">题库练习</h1>
|
||||
<p className="text-sm text-gray-500">章节练习和模拟考试功能</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Sidebar } from "@/components/layout/Sidebar";
|
||||
import { BottomNav } from "@/components/layout/BottomNav";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "EduPlatform",
|
||||
description: "一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className="bg-gray-50 min-h-screen">
|
||||
<div className="flex">
|
||||
{/* Desktop Sidebar */}
|
||||
<Sidebar items={[{"name":"首页","href":"/home","icon":"🏠"},{"name":"课程发布","href":"/courses","icon":"📋"},{"name":"章节管理","href":"/chapters","icon":"📅"},{"name":"学员进度","href":"/students","icon":"✏️"},{"name":"作业批改","href":"/assignments","icon":"📸"}] } projectName="EduPlatform" />
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 lg:pl-64 pb-20 lg:pb-0">
|
||||
<div className="max-w-2xl mx-auto px-4 py-6">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{/* Mobile Bottom Nav */}
|
||||
<BottomNav items={[{"name":"首页","href":"/home","icon":"🏠"},{"name":"课程发布","href":"/courses","icon":"📋"},{"name":"章节管理","href":"/chapters","icon":"📅"},{"name":"学员进度","href":"/students","icon":"✏️"}] } />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
|
||||
const courses = [
|
||||
{ id: "c1", title: "React 19 完全指南", instructor: "张老师", price: 199, cover: "📘" },
|
||||
{ id: "c2", title: "TypeScript 高级编程", instructor: "李老师", price: 149, cover: "📙" },
|
||||
{ id: "c3", title: "Node.js 后端实战", instructor: "王老师", price: 249, cover: "📗" },
|
||||
];
|
||||
|
||||
export default function CoursePage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-bold text-gray-800">课程中心</h1>
|
||||
{courses.map(c => (
|
||||
<Card key={c.id} className="flex gap-3">
|
||||
<div className="w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center text-3xl flex-shrink-0">{c.cover}</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">{c.title}</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">{c.instructor}</p>
|
||||
<p className="text-sm font-bold text-red-500 mt-2">¥{c.price}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
|
||||
export default function LivePage() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-xl font-bold text-gray-800">直播课堂</h1>
|
||||
<p className="text-sm text-gray-500">课程直播功能</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Hero Section */}
|
||||
<section className="bg-gradient-to-br from-blue-500 to-blue-700 rounded-2xl p-6 text-white">
|
||||
<h1 className="text-2xl font-bold">EduPlatform</h1>
|
||||
<p className="mt-2 text-blue-100 text-sm">应用首页</p>
|
||||
</section>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">快捷入口</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link key="0" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📋</span>
|
||||
<span className="text-sm text-gray-600">课程发布</span>
|
||||
</Link>
|
||||
<Link key="1" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📅</span>
|
||||
<span className="text-sm text-gray-600">章节管理</span>
|
||||
</Link>
|
||||
<Link key="2" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📝</span>
|
||||
<span className="text-sm text-gray-600">学员进度</span>
|
||||
</Link>
|
||||
<Link key="3" href="/home" className="bg-white rounded-xl border border-gray-100 p-4 text-center hover:shadow-md transition-shadow">
|
||||
<span className="text-2xl block mb-1">📸</span>
|
||||
<span className="text-sm text-gray-600">作业批改</span>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Recent Activity / Stats */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3">今日概览</h2>
|
||||
<div className="bg-white rounded-xl border border-gray-100 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">欢迎使用 EduPlatform</p>
|
||||
<p className="text-xs text-gray-400 mt-1">开始探索功能吧</p>
|
||||
</div>
|
||||
<span className="text-3xl">👋</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
|
||||
export default function PagePage() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">我的</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">学习报告、已购课程、设置</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{<Card>
|
||||
<p className="text-gray-500 text-sm">我的 页面内容</p>
|
||||
<p className="text-xs text-gray-400 mt-1">路由: /profile</p>
|
||||
</Card>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { useStudents } from "@/hooks/useStudents";
|
||||
|
||||
export default function PagePage() {
|
||||
const { data, loading, error, refetch } = useStudents();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">学员进度</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">学员进度</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="text-center py-8">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
<Button onClick={refetch} variant="secondary" size="sm" className="mt-2">重试</Button>
|
||||
</Card>
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="📭"
|
||||
title="暂无数据"
|
||||
description="还没有任何记录,点击上方按钮开始"
|
||||
action={<Button onClick={() => setOpen(true)} variant="primary" size="sm">创建第一条</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{data.map((item: any) => (
|
||||
<Card key={item.id}>
|
||||
<h3 className="font-medium text-gray-800">{item.title || item.name || `Item ${item.id.slice(0, 8)}`}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{item.createdAt || ""}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { use作业批改 } from "@/hooks/use作业批改";
|
||||
|
||||
export default function PagePage() {
|
||||
const { data, loading, error, refetch } = use作业批改();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">作业批改</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">作业批改</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="text-center py-8">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
<Button onClick={refetch} variant="secondary" size="sm" className="mt-2">重试</Button>
|
||||
</Card>
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="📭"
|
||||
title="暂无数据"
|
||||
description="还没有任何记录,点击上方按钮开始"
|
||||
action={<Button onClick={() => setOpen(true)} variant="primary" size="sm">创建第一条</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{data.map((item: any) => (
|
||||
<Card key={item.id}>
|
||||
<h3 className="font-medium text-gray-800">{item.title || item.name || `Item ${item.id.slice(0, 8)}`}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{item.createdAt || ""}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { use学员进度 } from "@/hooks/use学员进度";
|
||||
|
||||
export default function PagePage() {
|
||||
const { data, loading, error, refetch } = use学员进度();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">学员进度</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">学员进度</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="text-center py-8">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
<Button onClick={refetch} variant="secondary" size="sm" className="mt-2">重试</Button>
|
||||
</Card>
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="📭"
|
||||
title="暂无数据"
|
||||
description="还没有任何记录,点击上方按钮开始"
|
||||
action={<Button onClick={() => setOpen(true)} variant="primary" size="sm">创建第一条</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{data.map((item: any) => (
|
||||
<Card key={item.id}>
|
||||
<h3 className="font-medium text-gray-800">{item.title || item.name || `Item ${item.id.slice(0, 8)}`}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{item.createdAt || ""}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { use章节管理 } from "@/hooks/use章节管理";
|
||||
|
||||
export default function PagePage() {
|
||||
const { data, loading, error, refetch } = use章节管理();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">章节管理</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">章节管理</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="text-center py-8">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
<Button onClick={refetch} variant="secondary" size="sm" className="mt-2">重试</Button>
|
||||
</Card>
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="📭"
|
||||
title="暂无数据"
|
||||
description="还没有任何记录,点击上方按钮开始"
|
||||
action={<Button onClick={() => setOpen(true)} variant="primary" size="sm">创建第一条</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{data.map((item: any) => (
|
||||
<Card key={item.id}>
|
||||
<h3 className="font-medium text-gray-800">{item.title || item.name || `Item ${item.id.slice(0, 8)}`}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{item.createdAt || ""}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import { use课程发布 } from "@/hooks/use课程发布";
|
||||
|
||||
export default function PagePage() {
|
||||
const { data, loading, error, refetch } = use课程发布();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">课程发布</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">课程发布</p>
|
||||
</div>
|
||||
<Button onClick={() => setOpen(true)} variant="primary" size="sm">新增</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<Card className="text-center py-8">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
<Button onClick={refetch} variant="secondary" size="sm" className="mt-2">重试</Button>
|
||||
</Card>
|
||||
) : data.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="📭"
|
||||
title="暂无数据"
|
||||
description="还没有任何记录,点击上方按钮开始"
|
||||
action={<Button onClick={() => setOpen(true)} variant="primary" size="sm">创建第一条</Button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{data.map((item: any) => (
|
||||
<Card key={item.id}>
|
||||
<h3 className="font-medium text-gray-800">{item.title || item.name || `Item ${item.id.slice(0, 8)}`}</h3>
|
||||
<p className="text-xs text-gray-400 mt-1">{item.createdAt || ""}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export function BottomNav({ items }: { items: NavItem[] }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<nav className="lg:hidden fixed bottom-0 inset-x-0 bg-white border-t border-gray-100 z-40">
|
||||
<div className="flex items-center justify-around h-16">
|
||||
{items.map((item) => {
|
||||
const active = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex flex-col items-center gap-0.5 px-3 py-1 ${
|
||||
active ? "text-blue-600" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<span className="text-xl">{item.icon}</span>
|
||||
<span className="text-[10px]">{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface SidebarProps {
|
||||
items: NavItem[];
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export function Sidebar({ items, projectName }: SidebarProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside className="hidden lg:flex lg:flex-col lg:w-64 lg:fixed lg:inset-y-0 bg-white border-r border-gray-100">
|
||||
<div className="px-6 py-5 border-b border-gray-50">
|
||||
<h1 className="text-lg font-bold text-gray-800">{projectName}</h1>
|
||||
</div>
|
||||
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
|
||||
{items.map((item) => {
|
||||
const active = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors ${
|
||||
active ? "bg-blue-50 text-blue-700 font-medium" : "text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { type ButtonHTMLAttributes } from "react";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "danger" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function Button({ variant = "primary", size = "md", loading, children, className = "", disabled, ...props }: ButtonProps) {
|
||||
const base = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
||||
const variants = {
|
||||
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
||||
secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-400",
|
||||
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
|
||||
ghost: "text-gray-600 hover:bg-gray-100 focus:ring-gray-400",
|
||||
};
|
||||
const sizes = {
|
||||
sm: "px-3 py-1.5 text-sm",
|
||||
md: "px-4 py-2 text-sm",
|
||||
lg: "px-6 py-3 text-base",
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={`${base} ${variants[variant]} ${sizes[size]} ${className}`} disabled={disabled || loading} {...props}>
|
||||
{loading && <svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Card({ children, className = "", onClick }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`bg-white rounded-xl shadow-sm border border-gray-100 p-4 ${onClick ? "cursor-pointer hover:shadow-md transition-shadow" : ""} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
export function EmptyState({ icon = "📭", title, description, action }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<span className="text-5xl mb-4">{icon}</span>
|
||||
<h3 className="text-lg font-medium text-gray-700 mb-1">{title}</h3>
|
||||
{description && <p className="text-sm text-gray-500 mb-4">{description}</p>}
|
||||
{action}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { type InputHTMLAttributes } from "react";
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function Input({ label, error, className = "", id, ...props }: InputProps) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && <label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">{label}</label>}
|
||||
<input
|
||||
id={id}
|
||||
className={`w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${error ? "border-red-300" : "border-gray-300"} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="mt-1 text-xs text-red-500">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Modal({ open, onClose, title, children }: ModalProps) {
|
||||
useEffect(() => {
|
||||
if (open) document.body.style.overflow = "hidden";
|
||||
else document.body.style.overflow = "";
|
||||
return () => { document.body.style.overflow = ""; };
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={onClose} />
|
||||
<div className="relative bg-white rounded-2xl shadow-xl max-w-lg w-full mx-4 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">×</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for assignments
|
||||
type Assignment = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage assignments data.
|
||||
*/
|
||||
export function useAssignments() {
|
||||
const [data, setData] = useState<Assignment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/assignments`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for auth
|
||||
type Auth = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage auth data.
|
||||
*/
|
||||
export function useAuth() {
|
||||
const [data, setData] = useState<Auth[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/auth`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for chapters
|
||||
type Chapter = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage chapters data.
|
||||
*/
|
||||
export function useChapters() {
|
||||
const [data, setData] = useState<Chapter[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/chapters`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for courses
|
||||
type Cour = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage courses data.
|
||||
*/
|
||||
export function useCourses() {
|
||||
const [data, setData] = useState<Cour[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/courses`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number = 300): T {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(timer);
|
||||
}, [value, delay]);
|
||||
|
||||
return debounced;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for exercises
|
||||
type Exerci = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage exercises data.
|
||||
*/
|
||||
export function useExercises() {
|
||||
const [data, setData] = useState<Exerci[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/exercises`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
interface UseFormOptions<T> {
|
||||
initialValues: T;
|
||||
onSubmit: (values: T) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useForm<T extends Record<string, unknown>>({ initialValues, onSubmit }: UseFormOptions<T>) {
|
||||
const [values, setValues] = useState<T>(initialValues);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const setField = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
|
||||
setValues(prev => ({ ...prev, [field]: value }));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
await onSubmit(values);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Submit failed");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [values, onSubmit]);
|
||||
|
||||
return { values, setField, submitting, error, handleSubmit };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for 章节管理
|
||||
type Item = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage 章节管理 data.
|
||||
*/
|
||||
export function useItem() {
|
||||
const [data, setData] = useState<Item[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/章节管理`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for lessons
|
||||
type Lesson = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage lessons data.
|
||||
*/
|
||||
export function useLessons() {
|
||||
const [data, setData] = useState<Lesson[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/lessons`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for progress
|
||||
type Progres = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage progress data.
|
||||
*/
|
||||
export function useProgress() {
|
||||
const [data, setData] = useState<Progres[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/progress`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
// Generic data type for students
|
||||
type Student = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Fetch and manage students data.
|
||||
*/
|
||||
export function useStudents() {
|
||||
const [data, setData] = useState<Student[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`/api/students`);
|
||||
const json = await res.json();
|
||||
setData(Array.isArray(json) ? json : json?.data || []);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to load data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { data, loading, error, refetch: fetchData };
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "eduplatform",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"autoprefixer": "^10.4.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// Auto-generated API client for EduPlatform
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
params?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
async function request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { params, ...init } = options;
|
||||
let url = `${API_BASE}${endpoint}`;
|
||||
|
||||
if (params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([k, v]) => searchParams.set(k, String(v)));
|
||||
url += `?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...init.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ message: res.statusText }));
|
||||
throw new Error(err.message || `API Error: ${res.status}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(url: string, params?: Record<string, string | number>) =>
|
||||
request<T>(url, { method: "GET", params }),
|
||||
|
||||
post: <T>(url: string, data?: unknown) =>
|
||||
request<T>(url, { method: "POST", body: JSON.stringify(data) }),
|
||||
|
||||
put: <T>(url: string, data?: unknown) =>
|
||||
request<T>(url, { method: "PUT", body: JSON.stringify(data) }),
|
||||
|
||||
delete: <T>(url: string) =>
|
||||
request<T>(url, { method: "DELETE" }),
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated assignments service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/assignments");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/assignments", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated auth service
|
||||
import { api } from "./api";
|
||||
|
||||
export function postlogin(data: Record<string, unknown>) {
|
||||
return api.post("/api/auth", data);
|
||||
}
|
||||
|
||||
export function getme() {
|
||||
return api.get("/api/auth");
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated chapters service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/chapters");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/chapters", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated courses service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/courses");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/courses", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated exercises service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/exercises");
|
||||
}
|
||||
|
||||
export function postsubmit(data: Record<string, unknown>) {
|
||||
return api.post("/api/exercises", data);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Auto-generated lessons service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get_id(id: string) {
|
||||
return api.get(`/api/lessons/${id}`);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Auto-generated progress service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/progress");
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated students service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/students");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/students", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated 作业批改 service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/作业批改");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/作业批改", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated 学员进度 service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/学员进度");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/学员进度", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated 章节管理 service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/章节管理");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/章节管理", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Auto-generated 课程发布 service
|
||||
import { api } from "./api";
|
||||
|
||||
export function get() {
|
||||
return api.get("/api/课程发布");
|
||||
}
|
||||
|
||||
export function post(data: Record<string, unknown>) {
|
||||
return api.post("/api/课程发布", data);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./hooks/**/*.{js,ts,jsx,tsx,mdx}", "./services/**/*.{js,ts,jsx,tsx,mdx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Auto-generated types for EduPlatform
|
||||
|
||||
// ─── Base ───────────────────────────────────────────
|
||||
|
||||
|
||||
// ─── Base ───────────────────────────────────────────
|
||||
export interface User {
|
||||
id: string;
|
||||
phone: string;
|
||||
nickname: string;
|
||||
avatarUrl: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface Courses {
|
||||
id: string;
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
instructor?: string;
|
||||
price?: number;
|
||||
coverUrl?: string;
|
||||
category?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Chapters {
|
||||
id: string;
|
||||
userId?: string;
|
||||
courseId?: string;
|
||||
title: string;
|
||||
sortOrder?: number;
|
||||
durationMin?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Students {
|
||||
id: string;
|
||||
userId?: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
enrolledAt?: string;
|
||||
progress?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Assignments {
|
||||
id: string;
|
||||
userId?: string;
|
||||
courseId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
dueDate?: string;
|
||||
maxScore?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
|
||||
// ─── API Responses ──────────────────────────────────
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, string[]>;
|
||||
}
|
||||
Reference in New Issue
Block a user