chore: initial import to Nexus_Edu
This commit is contained in:
85
src/app/(dashboard)/assignments/page.tsx
Normal file
85
src/app/(dashboard)/assignments/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { TeacherAssignmentList } from '@/features/assignment/components/TeacherAssignmentList';
|
||||
import { StudentAssignmentList } from '@/features/assignment/components/StudentAssignmentList';
|
||||
import { CreateAssignmentModal } from '@/features/assignment/components/CreateAssignmentModal';
|
||||
import { AssignmentStats } from '@/features/assignment/components/AssignmentStats';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function AssignmentsPage() {
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [analyzingId, setAnalyzingId] = useState<string | null>(null);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const isStudent = user.role === 'Student';
|
||||
|
||||
const handleNavigateToGrading = (id: string) => {
|
||||
// Teachers go to grading tool, Students view results (though usually handled by onViewResult)
|
||||
router.push(`/grading/${id}`);
|
||||
};
|
||||
|
||||
const handleNavigateToPreview = (id: string) => {
|
||||
router.push(`/student-exam/${id}`);
|
||||
};
|
||||
|
||||
const handleViewResult = (id: string) => {
|
||||
router.push(`/student-result/${id}`);
|
||||
};
|
||||
|
||||
if (analyzingId && !isStudent) {
|
||||
return (
|
||||
// Fix: cast props to any to avoid framer-motion type errors
|
||||
<motion.div
|
||||
{...({
|
||||
initial: { opacity: 0, x: 20 },
|
||||
animate: { opacity: 1, x: 0 },
|
||||
exit: { opacity: 0, x: -20 }
|
||||
} as any)}
|
||||
className="h-[calc(100vh-100px)]"
|
||||
>
|
||||
<AssignmentStats
|
||||
assignmentId={analyzingId}
|
||||
onBack={() => setAnalyzingId(null)}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
{isStudent ? (
|
||||
<StudentAssignmentList
|
||||
onStartExam={handleNavigateToPreview}
|
||||
onViewResult={handleViewResult}
|
||||
/>
|
||||
) : (
|
||||
<TeacherAssignmentList
|
||||
onNavigateToGrading={handleNavigateToGrading}
|
||||
onNavigateToPreview={handleNavigateToPreview}
|
||||
onAnalyze={setAnalyzingId}
|
||||
setIsCreating={setIsCreating}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isCreating && (
|
||||
<CreateAssignmentModal
|
||||
onClose={() => setIsCreating(false)}
|
||||
onSuccess={() => {
|
||||
setIsCreating(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
src/app/(dashboard)/classes/page.tsx
Normal file
13
src/app/(dashboard)/classes/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { ClassManagement } from '@/views/ClassManagement';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
export default function ClassesPage() {
|
||||
const { user } = useAuth();
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return <ClassManagement currentUser={user} />;
|
||||
}
|
||||
9
src/app/(dashboard)/consoleconfig/page.tsx
Normal file
9
src/app/(dashboard)/consoleconfig/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { ConsoleConfig } from '@/views/ConsoleConfig';
|
||||
|
||||
export default function ConsoleConfigPage() {
|
||||
return <ConsoleConfig />;
|
||||
}
|
||||
8
src/app/(dashboard)/curriculum/page.tsx
Normal file
8
src/app/(dashboard)/curriculum/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Curriculum } from '@/views/Curriculum';
|
||||
|
||||
export default function CurriculumPage() {
|
||||
return <Curriculum />;
|
||||
}
|
||||
76
src/app/(dashboard)/dashboard/page.tsx
Normal file
76
src/app/(dashboard)/dashboard/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { CalendarClock, ListTodo, Plus } from 'lucide-react';
|
||||
import { TeacherDashboard } from '@/features/dashboard/components/TeacherDashboard';
|
||||
import { StudentDashboard } from '@/features/dashboard/components/StudentDashboard';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
// Helper for adapting onNavigate to useRouter
|
||||
const useNavigationAdapter = () => {
|
||||
const router = useRouter();
|
||||
return (view: string) => {
|
||||
// Map old string views to new routes
|
||||
const routeMap: Record<string, string> = {
|
||||
'dashboard': '/dashboard',
|
||||
'assignments': '/assignments',
|
||||
'classes': '/classes',
|
||||
'exams': '/exams',
|
||||
'curriculum': '/curriculum',
|
||||
'settings': '/settings',
|
||||
'messages': '/messages',
|
||||
'schedule': '/schedule',
|
||||
'student-exam': '/student-exam', // might need ID handling
|
||||
'student-result': '/student-result'
|
||||
};
|
||||
router.push(routeMap[view] || '/dashboard');
|
||||
};
|
||||
};
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigationAdapter();
|
||||
const router = useRouter();
|
||||
const today = new Date().toLocaleDateString('zh-CN', { weekday: 'long', month: 'long', day: 'numeric' });
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-8 pb-8">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">早安, {user.realName} {user.role === 'Student' ? '同学' : '老师'} 👋</h2>
|
||||
<p className="text-gray-500 mt-1 font-medium flex items-center gap-2">
|
||||
<CalendarClock size={16} className="text-blue-500" />
|
||||
{today}
|
||||
</p>
|
||||
</div>
|
||||
{user.role === 'Teacher' && (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => router.push('/assignments')}
|
||||
className="flex items-center gap-2 bg-white text-gray-700 px-4 py-2.5 rounded-xl text-sm font-bold shadow-sm hover:bg-gray-50 transition-all border border-gray-100"
|
||||
>
|
||||
<ListTodo size={18} />
|
||||
待办事项 (3)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/exams')}
|
||||
className="flex items-center gap-2 bg-gray-900 text-white px-5 py-2.5 rounded-xl text-sm font-bold shadow-lg hover:bg-black hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
<Plus size={18} />
|
||||
快速创建
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{user.role === 'Student' ? (
|
||||
<StudentDashboard user={user} onNavigate={navigate} />
|
||||
) : (
|
||||
<TeacherDashboard user={user} onNavigate={navigate} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
src/app/(dashboard)/exams/page.tsx
Normal file
8
src/app/(dashboard)/exams/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { ExamEngine } from '@/views/ExamEngine';
|
||||
|
||||
export default function ExamsPage() {
|
||||
return <ExamEngine />;
|
||||
}
|
||||
87
src/app/(dashboard)/layout.tsx
Normal file
87
src/app/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Sidebar } from '@/components/Sidebar';
|
||||
import { Bell, Search } from 'lucide-react';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary'; // Import ErrorBoundary
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const { user, loading } = useAuth();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
if (loading) {
|
||||
return <div className="h-screen flex items-center justify-center"><Loader2 className="animate-spin" /></div>;
|
||||
}
|
||||
|
||||
if (!user) return null; // Middleware or AuthProvider will redirect
|
||||
|
||||
const getHeaderTitle = (path: string) => {
|
||||
if (path.includes('/dashboard')) return '教学概览';
|
||||
if (path.includes('/curriculum')) return '课程标准';
|
||||
if (path.includes('/exams')) return '考试引擎';
|
||||
if (path.includes('/assignments')) return '作业发布';
|
||||
if (path.includes('/questions')) return '题库资源';
|
||||
if (path.includes('/classes')) return '班级管理';
|
||||
if (path.includes('/settings')) return '系统设置';
|
||||
if (path.includes('/messages')) return '消息中心';
|
||||
if (path.includes('/schedule')) return '课程表';
|
||||
return 'EduNexus';
|
||||
};
|
||||
|
||||
const roleName = user.role === 'Teacher' ? '教师' : (user.role === 'Admin' ? '管理员' : '学生');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-[#F2F2F7] font-sans text-gray-900">
|
||||
<Sidebar />
|
||||
|
||||
<main className="flex-1 lg:ml-72 p-4 md:p-8 overflow-y-auto h-screen scroll-smooth">
|
||||
{/* Top Bar */}
|
||||
<header className="flex justify-between items-center mb-8 sticky top-0 z-40 py-4 -mx-4 px-4 md:-mx-8 md:px-8 bg-[#F2F2F7]/80 backdrop-blur-xl transition-all">
|
||||
<div className="flex flex-col ml-12 lg:ml-0">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 tracking-tight line-clamp-1">
|
||||
{getHeaderTitle(pathname || '')}
|
||||
</h1>
|
||||
<p className="text-xs md:text-sm text-gray-500 font-medium mt-0.5">2024-2025 学年 第一学期</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="relative hidden md:block group">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-blue-500 transition-colors" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="全局搜索..."
|
||||
className="pl-10 pr-4 py-2.5 bg-white rounded-full border-none shadow-sm text-sm focus:ring-2 focus:ring-blue-500/20 outline-none w-48 lg:w-64 transition-all placeholder:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/messages')}
|
||||
className="w-10 h-10 md:w-11 md:h-11 bg-white rounded-full flex items-center justify-center shadow-sm text-gray-500 hover:text-blue-600 hover:shadow-md transition-all relative"
|
||||
>
|
||||
<Bell size={20} />
|
||||
<span className="absolute top-3 right-3.5 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span>
|
||||
</button>
|
||||
<div className="flex items-center gap-3 bg-white pr-4 pl-1.5 py-1.5 rounded-full shadow-sm hover:shadow-md transition-all cursor-pointer">
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt="Profile"
|
||||
className="w-8 h-8 md:w-9 md:h-9 rounded-full border border-gray-100 object-cover"
|
||||
/>
|
||||
<div className="flex flex-col hidden md:flex">
|
||||
<span className="text-sm font-semibold text-gray-900 leading-none">{user.realName}</span>
|
||||
<span className="text-[11px] text-gray-500 font-medium mt-0.5 uppercase tracking-wide">{roleName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/app/(dashboard)/messages/page.tsx
Normal file
11
src/app/(dashboard)/messages/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Messages } from '@/views/Messages';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
export default function MessagesPage() {
|
||||
const { user } = useAuth();
|
||||
if (!user) return null;
|
||||
return <Messages currentUser={user} />;
|
||||
}
|
||||
8
src/app/(dashboard)/questions/page.tsx
Normal file
8
src/app/(dashboard)/questions/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { QuestionBank } from '@/views/QuestionBank';
|
||||
|
||||
export default function QuestionsPage() {
|
||||
return <QuestionBank />;
|
||||
}
|
||||
10
src/app/(dashboard)/schedule/page.tsx
Normal file
10
src/app/(dashboard)/schedule/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Schedule } from '@/views/Schedule';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
export default function SchedulePage() {
|
||||
const { user } = useAuth();
|
||||
return <Schedule currentUser={user || undefined} />;
|
||||
}
|
||||
8
src/app/(dashboard)/settings/page.tsx
Normal file
8
src/app/(dashboard)/settings/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Settings } from '@/views/Settings';
|
||||
|
||||
export default function SettingsPage() {
|
||||
return <Settings />;
|
||||
}
|
||||
16
src/app/(dashboard)/student-result/[id]/page.tsx
Normal file
16
src/app/(dashboard)/student-result/[id]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { StudentResult } from '@/views/StudentResult';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function StudentResultPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<StudentResult
|
||||
assignmentId={params.id}
|
||||
onBack={() => router.back()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
src/app/(fullscreen)/grading/[id]/page.tsx
Normal file
16
src/app/(fullscreen)/grading/[id]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Grading } from '@/views/Grading';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function GradingPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Grading
|
||||
assignmentId={params.id}
|
||||
onBack={() => router.back()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
src/app/(fullscreen)/layout.tsx
Normal file
19
src/app/(fullscreen)/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
export default function FullscreenLayout({ children }: { children: React.ReactNode }) {
|
||||
const { loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return <div className="h-screen flex items-center justify-center"><Loader2 className="animate-spin" /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F2F2F7] font-sans text-gray-900">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/app/(fullscreen)/student-exam/[id]/page.tsx
Normal file
16
src/app/(fullscreen)/student-exam/[id]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { StudentExamRunner } from '@/views/StudentExamRunner';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function ExamRunnerPage({ params }: { params: { id: string } }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<StudentExamRunner
|
||||
assignmentId={params.id}
|
||||
onExit={() => router.push('/assignments')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
48
src/app/api/auth/login/route.ts
Normal file
48
src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
import { NextRequest } from 'next/server';
|
||||
import { successResponse, errorResponse, dbDelay } from '@/lib/server-utils';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
await dbDelay();
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { username, password } = body;
|
||||
|
||||
// Simple mock validation
|
||||
if (!username) {
|
||||
return errorResponse('Username is required');
|
||||
}
|
||||
|
||||
let role = 'Teacher';
|
||||
let name = '李明';
|
||||
let id = 'u-tea-1';
|
||||
|
||||
if (username === 'student' || username.startsWith('s')) {
|
||||
role = 'Student';
|
||||
name = '王小明';
|
||||
id = 'u-stu-1';
|
||||
} else if (username === 'admin') {
|
||||
role = 'Admin';
|
||||
name = '系统管理员';
|
||||
id = 'u-adm-1';
|
||||
}
|
||||
|
||||
const token = `mock-jwt-token-${id}-${Date.now()}`;
|
||||
|
||||
return successResponse({
|
||||
token,
|
||||
user: {
|
||||
id,
|
||||
realName: name,
|
||||
studentId: username,
|
||||
avatarUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${name}`,
|
||||
gender: 'Male',
|
||||
schoolId: 's-1',
|
||||
role
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return errorResponse('Invalid request body');
|
||||
}
|
||||
}
|
||||
28
src/app/api/auth/me/route.ts
Normal file
28
src/app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
import { NextRequest } from 'next/server';
|
||||
import { successResponse, errorResponse, extractToken, dbDelay } from '@/lib/server-utils';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
await dbDelay();
|
||||
|
||||
const token = extractToken(request);
|
||||
if (!token) {
|
||||
return errorResponse('Unauthorized', 401);
|
||||
}
|
||||
|
||||
// In a real app, verify JWT here.
|
||||
// For mock, we return a default user or parse the mock token if it contained info.
|
||||
|
||||
return successResponse({
|
||||
id: "u-1",
|
||||
realName: "李明 (Real API)",
|
||||
studentId: "T2024001",
|
||||
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alex",
|
||||
gender: "Male",
|
||||
schoolId: "s-1",
|
||||
role: "Teacher",
|
||||
email: 'liming@school.edu',
|
||||
phone: '13800138000',
|
||||
bio: '来自真实 API 的数据'
|
||||
});
|
||||
}
|
||||
27
src/app/api/config/db/route.ts
Normal file
27
src/app/api/config/db/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import { NextRequest } from 'next/server';
|
||||
import { successResponse, errorResponse } from '@/lib/server-utils';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { host, port, user, password, database } = body;
|
||||
|
||||
if (!host || !user) {
|
||||
return errorResponse('Missing required fields');
|
||||
}
|
||||
|
||||
await db.testConnection({
|
||||
host,
|
||||
port: Number(port),
|
||||
user,
|
||||
password,
|
||||
database
|
||||
});
|
||||
|
||||
return successResponse({ message: 'Connection successful' });
|
||||
} catch (e: any) {
|
||||
return errorResponse(e.message || 'Connection failed', 500);
|
||||
}
|
||||
}
|
||||
23
src/app/api/org/classes/route.ts
Normal file
23
src/app/api/org/classes/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
import { NextRequest } from 'next/server';
|
||||
import { successResponse, dbDelay } from '@/lib/server-utils';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
await dbDelay();
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const role = searchParams.get('role');
|
||||
|
||||
let classes = [
|
||||
{ id: 'c-1', name: '高一 (10) 班', gradeName: '高一年级', teacherName: '李明', studentCount: 32, inviteCode: 'X7K9P' },
|
||||
{ id: 'c-2', name: '高一 (12) 班', gradeName: '高一年级', teacherName: '张伟', studentCount: 28, inviteCode: 'M2L4Q' },
|
||||
{ id: 'c-3', name: 'AP 微积分先修班', gradeName: '高三年级', teacherName: '李明', studentCount: 15, inviteCode: 'Z9J1W' },
|
||||
{ id: 'c-4', name: '物理奥赛集训队', gradeName: '高二年级', teacherName: '王博士', studentCount: 20, inviteCode: 'H4R8T' },
|
||||
];
|
||||
|
||||
if (role === 'Student') {
|
||||
classes = classes.slice(0, 1);
|
||||
}
|
||||
|
||||
return successResponse(classes);
|
||||
}
|
||||
52
src/app/globals.css
Normal file
52
src/app/globals.css
Normal file
@@ -0,0 +1,52 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 242, 242, 247;
|
||||
--background-end-rgb: 242, 242, 247;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: #F2F2F7;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #C1C1C1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #A8A8A8;
|
||||
}
|
||||
|
||||
/* Utility to hide scrollbar but keep functionality */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Recharts Tooltip Fix */
|
||||
.recharts-tooltip-wrapper {
|
||||
z-index: 100;
|
||||
outline: none;
|
||||
}
|
||||
30
src/app/layout.tsx
Normal file
30
src/app/layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { AuthProvider } from "@/lib/auth-context";
|
||||
import { ToastProvider } from "@/components/ui/Toast";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "EduNexus Pro",
|
||||
description: "Enterprise Education Management System",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className={inter.className}>
|
||||
<ToastProvider>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ToastProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
12
src/app/loading.tsx
Normal file
12
src/app/loading.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="h-screen w-full flex items-center justify-center bg-[#F2F2F7]/50 backdrop-blur-sm z-50">
|
||||
<div className="bg-white/80 p-8 rounded-3xl shadow-xl flex flex-col items-center backdrop-blur-xl border border-white/50">
|
||||
<Loader2 className="animate-spin text-blue-600 mb-4" size={32} />
|
||||
<p className="text-sm font-bold text-gray-500">EduNexus Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
src/app/login/page.tsx
Normal file
82
src/app/login/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { LoginForm } from '@/features/auth/components/LoginForm';
|
||||
import { RegisterForm } from '@/features/auth/components/RegisterForm';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||
const { login, register } = useAuth();
|
||||
|
||||
// Adapt the legacy onLoginSuccess prop to use the Context method
|
||||
const handleLoginSuccess = (user: any) => {
|
||||
// 登录成功后,token 已存储在 localStorage
|
||||
// 使用 window.location.href 强制跳转,触发 AuthContext 重新初始化并验证 token
|
||||
window.location.href = '/dashboard';
|
||||
};
|
||||
|
||||
// We need to wrap the context calls to match the signature expected by existing components
|
||||
// Ideally, modify components to use useAuth() directly, but this is a migration adapter.
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[#F5F5F7]">
|
||||
{/* Fix: cast props to any to avoid framer-motion type errors */}
|
||||
<motion.div
|
||||
{...({
|
||||
animate: { scale: [1, 1.2, 1], rotate: [0, 90, 0] },
|
||||
transition: { duration: 20, repeat: Infinity, ease: "linear" }
|
||||
} as any)}
|
||||
className="absolute -top-[30%] -left-[10%] w-[800px] h-[800px] rounded-full bg-blue-400/20 blur-[100px]"
|
||||
/>
|
||||
{/* Fix: cast props to any to avoid framer-motion type errors */}
|
||||
<motion.div
|
||||
{...({
|
||||
animate: { scale: [1, 1.5, 1], rotate: [0, -60, 0] },
|
||||
transition: { duration: 15, repeat: Infinity, ease: "linear" }
|
||||
} as any)}
|
||||
className="absolute top-[20%] -right-[20%] w-[600px] h-[600px] rounded-full bg-purple-400/20 blur-[100px]"
|
||||
/>
|
||||
|
||||
<div className="w-full max-w-md z-10 mx-4 perspective-1000">
|
||||
<AnimatePresence mode="wait">
|
||||
{mode === 'login' ? (
|
||||
// Fix: cast props to any to avoid framer-motion type errors
|
||||
<motion.div
|
||||
key="login"
|
||||
{...({
|
||||
initial: { opacity: 0, rotateY: -90 },
|
||||
animate: { opacity: 1, rotateY: 0 },
|
||||
exit: { opacity: 0, rotateY: 90 },
|
||||
transition: { duration: 0.4 }
|
||||
} as any)}
|
||||
>
|
||||
<LoginForm
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
onSwitch={() => setMode('register')}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
// Fix: cast props to any to avoid framer-motion type errors
|
||||
<motion.div
|
||||
key="register"
|
||||
{...({
|
||||
initial: { opacity: 0, rotateY: 90 },
|
||||
animate: { opacity: 1, rotateY: 0 },
|
||||
exit: { opacity: 0, rotateY: -90 },
|
||||
transition: { duration: 0.4 }
|
||||
} as any)}
|
||||
>
|
||||
<RegisterForm
|
||||
onLoginSuccess={handleLoginSuccess}
|
||||
onSwitch={() => setMode('login')}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/app/not-found.tsx
Normal file
23
src/app/not-found.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import Link from 'next/link';
|
||||
import { AlertTriangle, Home } from 'lucide-react';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-[#F2F2F7] text-center px-4">
|
||||
<div className="w-20 h-20 bg-gray-100 rounded-3xl flex items-center justify-center mb-6 shadow-sm">
|
||||
<AlertTriangle size={40} className="text-gray-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">页面未找到</h2>
|
||||
<p className="text-gray-500 mb-8 max-w-md">
|
||||
您访问的页面可能已被移除、更名或暂时不可用。
|
||||
</p>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-xl font-bold shadow-lg shadow-blue-500/30 hover:bg-blue-700 transition-all"
|
||||
>
|
||||
<Home size={18} />
|
||||
返回首页
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/app/page.tsx
Normal file
27
src/app/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export default function RootPage() {
|
||||
const router = useRouter();
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (user) {
|
||||
router.replace('/dashboard');
|
||||
} else {
|
||||
router.replace('/login');
|
||||
}
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-[#F2F2F7]">
|
||||
<Loader2 className="animate-spin text-blue-600" size={40} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user