add backend

This commit is contained in:
yoyuzh
2026-03-14 11:03:07 +08:00
parent d993d3f943
commit 8db2fa2aab
130 changed files with 15152 additions and 11861 deletions

9
front/.env.example Normal file
View File

@@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"

8
front/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example

View File

@@ -0,0 +1,8 @@
{
"hash": "1eac4ae6",
"configHash": "19e214db",
"lockfileHash": "126cd023",
"browserHash": "c5ddb224",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

20
front/README.md Normal file
View File

@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/7dcdc5c7-28c0-4121-959b-77273973e0ef
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

13
front/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5
front/metadata.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "Personal Portal",
"description": "A unified personal portal for managing files, school schedules, grades, and games with a glassmorphism design.",
"requestFramePermissions": []
}

5281
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
front/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"better-sqlite3": "^12.4.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.13.1",
"tailwind-merge": "^3.5.0",
"vite": "^6.2.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

25
front/src/App.tsx Normal file
View File

@@ -0,0 +1,25 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Layout } from './components/layout/Layout';
import Login from './pages/Login';
import Overview from './pages/Overview';
import Files from './pages/Files';
import School from './pages/School';
import Games from './pages/Games';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}>
<Route index element={<Navigate to="/overview" replace />} />
<Route path="overview" element={<Overview />} />
<Route path="files" element={<Files />} />
<Route path="school" element={<School />} />
<Route path="games" element={<Games />} />
</Route>
</Routes>
</BrowserRouter>
);
}

View File

@@ -0,0 +1,91 @@
import React from 'react';
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { cn } from '@/src/lib/utils';
import { LayoutDashboard, FolderOpen, GraduationCap, Gamepad2, LogOut } from 'lucide-react';
const NAV_ITEMS = [
{ name: '总览', path: '/overview', icon: LayoutDashboard },
{ name: '网盘', path: '/files', icon: FolderOpen },
{ name: '教务', path: '/school', icon: GraduationCap },
{ name: '游戏', path: '/games', icon: Gamepad2 },
];
export function Layout() {
const navigate = useNavigate();
const handleLogout = () => {
navigate('/login');
};
return (
<div className="min-h-screen flex flex-col bg-[#07101D] text-white relative overflow-hidden">
{/* Animated Gradient Background */}
<div className="fixed inset-0 z-0 pointer-events-none">
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] rounded-full bg-[#336EFF] opacity-20 mix-blend-screen filter blur-[120px] animate-blob" />
<div className="absolute top-[20%] right-[-10%] w-[50%] h-[50%] rounded-full bg-purple-600 opacity-20 mix-blend-screen filter blur-[120px] animate-blob animation-delay-2000" />
<div className="absolute bottom-[-20%] left-[20%] w-[60%] h-[60%] rounded-full bg-indigo-600 opacity-20 mix-blend-screen filter blur-[120px] animate-blob animation-delay-4000" />
</div>
{/* Top Navigation */}
<header className="sticky top-0 z-50 w-full glass-panel border-b border-white/10 bg-[#07101D]/60 backdrop-blur-xl">
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
{/* Brand */}
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#336EFF] to-blue-400 flex items-center justify-center shadow-lg shadow-[#336EFF]/20">
<span className="text-white font-bold text-lg leading-none">Y</span>
</div>
<div className="flex flex-col">
<span className="text-white font-bold text-sm tracking-wider">YOYUZH.XYZ</span>
<span className="text-slate-400 text-[10px] uppercase tracking-widest">Personal Portal</span>
</div>
</div>
{/* Nav Links */}
<nav className="hidden md:flex items-center gap-2">
{NAV_ITEMS.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 relative overflow-hidden group',
isActive
? 'text-white shadow-md shadow-[#336EFF]/20'
: 'text-slate-400 hover:text-white hover:bg-white/5'
)
}
>
{({ isActive }) => (
<>
{isActive && (
<div className="absolute inset-0 bg-[#336EFF] opacity-100 z-0" />
)}
<item.icon className="w-4 h-4 relative z-10" />
<span className="relative z-10">{item.name}</span>
</>
)}
</NavLink>
))}
</nav>
{/* User / Actions */}
<div className="flex items-center gap-4">
<button
onClick={handleLogout}
className="text-slate-400 hover:text-white transition-colors p-2 rounded-xl hover:bg-white/5 relative z-10"
aria-label="Logout"
>
<LogOut className="w-5 h-5" />
</button>
</div>
</div>
</header>
{/* Main Content */}
<main className="flex-1 container mx-auto px-4 py-8 relative z-10">
<Outlet />
</main>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cn } from "@/src/lib/utils"
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "outline" | "ghost" | "glass"
size?: "default" | "sm" | "lg" | "icon"
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", ...props }, ref) => {
return (
<button
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
"bg-[#336EFF] text-white hover:bg-[#2958cc] shadow-md shadow-[#336EFF]/20": variant === "default",
"border border-white/20 bg-transparent hover:bg-white/10 text-white": variant === "outline",
"hover:bg-white/10 text-white": variant === "ghost",
"glass-panel hover:bg-white/10 text-white": variant === "glass",
"h-10 px-4 py-2": size === "default",
"h-9 rounded-lg px-3": size === "sm",
"h-11 rounded-xl px-8": size === "lg",
"h-10 w-10": size === "icon",
},
className
)}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button }

View File

@@ -0,0 +1,78 @@
import * as React from "react"
import { cn } from "@/src/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"glass-panel rounded-2xl text-white shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-slate-400", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/src/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-11 w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#336EFF] disabled:cursor-not-allowed disabled:opacity-50 transition-colors",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

88
front/src/index.css Normal file
View File

@@ -0,0 +1,88 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
--color-bg-base: #07101D;
--color-primary: #336EFF;
--color-primary-hover: #2958cc;
--color-text-primary: #FFFFFF;
--color-text-secondary: #94A3B8; /* slate-400 */
--color-text-tertiary: rgba(255, 255, 255, 0.3);
--color-glass-bg: rgba(255, 255, 255, 0.03);
--color-glass-border: rgba(255, 255, 255, 0.08);
--color-glass-hover: rgba(255, 255, 255, 0.06);
--color-glass-active: rgba(255, 255, 255, 0.1);
}
body {
background-color: var(--color-bg-base);
color: var(--color-text-primary);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Glassmorphism utilities */
.glass-panel {
background: var(--color-glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--color-glass-border);
}
.glass-panel-hover:hover {
background: var(--color-glass-hover);
}
.glass-panel-active {
background: var(--color-glass-active);
}
/* Animations */
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(30px, -50px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
.animate-blob {
animation: blob 10s infinite alternate ease-in-out;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}

6
front/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

10
front/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

275
front/src/pages/Files.tsx Normal file
View File

@@ -0,0 +1,275 @@
import React, { useState } from 'react';
import { motion } from 'motion/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/src/components/ui/card';
import { Button } from '@/src/components/ui/button';
import {
Folder, FileText, Image as ImageIcon, Download, Monitor,
Star, ChevronRight, Upload, Plus, LayoutGrid, List, File,
MoreVertical
} from 'lucide-react';
import { cn } from '@/src/lib/utils';
const QUICK_ACCESS = [
{ name: '桌面', icon: Monitor },
{ name: '下载', icon: Download },
{ name: '文档', icon: FileText },
{ name: '图片', icon: ImageIcon },
];
const DIRECTORIES = [
{ name: '我的文件', icon: Folder },
{ name: '课程资料', icon: Folder },
{ name: '项目归档', icon: Folder },
{ name: '收藏夹', icon: Star },
];
const MOCK_FILES_DB: Record<string, any[]> = {
'我的文件': [
{ id: 1, name: '软件工程期末复习资料.pdf', type: 'pdf', size: '2.4 MB', modified: '2025-01-15 14:30' },
{ id: 2, name: '2025春季学期课表.xlsx', type: 'excel', size: '156 KB', modified: '2025-02-28 09:15' },
{ id: 3, name: '项目架构设计图.png', type: 'image', size: '4.1 MB', modified: '2025-03-01 16:45' },
{ id: 4, name: '实验报告模板.docx', type: 'word', size: '45 KB', modified: '2025-03-05 10:20' },
{ id: 5, name: '前端学习笔记', type: 'folder', size: '—', modified: '2025-03-10 11:00' },
],
'课程资料': [
{ id: 6, name: '高等数学', type: 'folder', size: '—', modified: '2025-02-20 10:00' },
{ id: 7, name: '大学物理', type: 'folder', size: '—', modified: '2025-02-21 11:00' },
{ id: 8, name: '软件工程', type: 'folder', size: '—', modified: '2025-02-22 14:00' },
],
'项目归档': [
{ id: 9, name: '2024秋季学期项目', type: 'folder', size: '—', modified: '2024-12-20 15:30' },
{ id: 10, name: '个人博客源码.zip', type: 'archive', size: '15.2 MB', modified: '2025-01-05 09:45' },
],
'收藏夹': [
{ id: 11, name: '常用工具网站.txt', type: 'document', size: '2 KB', modified: '2025-03-01 10:00' },
],
'我的文件/前端学习笔记': [
{ id: 12, name: 'React Hooks 详解.md', type: 'document', size: '12 KB', modified: '2025-03-08 09:00' },
{ id: 13, name: 'Tailwind 技巧.md', type: 'document', size: '8 KB', modified: '2025-03-09 14:20' },
{ id: 14, name: '示例代码', type: 'folder', size: '—', modified: '2025-03-10 10:00' },
],
'课程资料/软件工程': [
{ id: 15, name: '需求规格说明书.pdf', type: 'pdf', size: '1.2 MB', modified: '2025-03-05 16:00' },
{ id: 16, name: '系统设计文档.docx', type: 'word', size: '850 KB', modified: '2025-03-06 11:30' },
]
};
export default function Files() {
const [currentPath, setCurrentPath] = useState<string[]>(['我的文件']);
const [selectedFile, setSelectedFile] = useState<any | null>(null);
const activeDir = currentPath[currentPath.length - 1];
const pathKey = currentPath.join('/');
const currentFiles = MOCK_FILES_DB[pathKey] || [];
const handleSidebarClick = (name: string) => {
setCurrentPath([name]);
setSelectedFile(null);
};
const handleFolderDoubleClick = (file: any) => {
if (file.type === 'folder') {
setCurrentPath([...currentPath, file.name]);
setSelectedFile(null);
}
};
const handleBreadcrumbClick = (index: number) => {
setCurrentPath(currentPath.slice(0, index + 1));
setSelectedFile(null);
};
return (
<div className="flex flex-col lg:flex-row gap-6 h-[calc(100vh-8rem)]">
{/* Left Sidebar */}
<Card className="w-full lg:w-64 shrink-0 flex flex-col h-full overflow-y-auto">
<CardContent className="p-4 space-y-6">
<div className="space-y-1">
<p className="px-3 text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2">访</p>
{QUICK_ACCESS.map((item) => (
<button
key={item.name}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-slate-300 hover:text-white hover:bg-white/5 transition-colors"
>
<item.icon className="w-4 h-4 text-slate-400" />
{item.name}
</button>
))}
</div>
<div className="space-y-1">
<p className="px-3 text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2"></p>
{DIRECTORIES.map((item) => (
<button
key={item.name}
onClick={() => handleSidebarClick(item.name)}
className={cn(
"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
currentPath.length === 1 && currentPath[0] === item.name
? "bg-[#336EFF]/20 text-[#336EFF]"
: "text-slate-300 hover:text-white hover:bg-white/5"
)}
>
<item.icon className={cn("w-4 h-4", currentPath.length === 1 && currentPath[0] === item.name ? "text-[#336EFF]" : "text-slate-400")} />
{item.name}
</button>
))}
</div>
</CardContent>
</Card>
{/* Middle Content */}
<Card className="flex-1 flex flex-col h-full overflow-hidden">
{/* Header / Breadcrumbs */}
<div className="p-4 border-b border-white/10 flex items-center justify-between shrink-0">
<div className="flex items-center text-sm text-slate-400">
<button className="hover:text-white transition-colors"></button>
{currentPath.map((pathItem, index) => (
<React.Fragment key={index}>
<ChevronRight className="w-4 h-4 mx-1" />
<button
onClick={() => handleBreadcrumbClick(index)}
className={cn("transition-colors", index === currentPath.length - 1 ? "text-white font-medium" : "hover:text-white")}
>
{pathItem}
</button>
</React.Fragment>
))}
</div>
<div className="flex items-center gap-2 bg-black/20 p-1 rounded-lg">
<button className="p-1.5 rounded-md bg-white/10 text-white"><List className="w-4 h-4" /></button>
<button className="p-1.5 rounded-md text-slate-400 hover:text-white"><LayoutGrid className="w-4 h-4" /></button>
</div>
</div>
{/* File List */}
<div className="flex-1 overflow-y-auto p-4">
<table className="w-full text-left border-collapse">
<thead>
<tr className="text-xs font-semibold text-slate-500 uppercase tracking-wider border-b border-white/5">
<th className="pb-3 pl-4 font-medium"></th>
<th className="pb-3 font-medium hidden md:table-cell"></th>
<th className="pb-3 font-medium hidden lg:table-cell"></th>
<th className="pb-3 font-medium"></th>
<th className="pb-3"></th>
</tr>
</thead>
<tbody>
{currentFiles.length > 0 ? (
currentFiles.map((file) => (
<tr
key={file.id}
onClick={() => setSelectedFile(file)}
onDoubleClick={() => handleFolderDoubleClick(file)}
className={cn(
"group cursor-pointer transition-colors border-b border-white/5 last:border-0",
selectedFile?.id === file.id ? "bg-[#336EFF]/10" : "hover:bg-white/[0.02]"
)}
>
<td className="py-3 pl-4">
<div className="flex items-center gap-3">
{file.type === 'folder' ? (
<Folder className="w-5 h-5 text-[#336EFF]" />
) : file.type === 'image' ? (
<ImageIcon className="w-5 h-5 text-purple-400" />
) : (
<FileText className="w-5 h-5 text-blue-400" />
)}
<span className={cn("text-sm font-medium", selectedFile?.id === file.id ? "text-[#336EFF]" : "text-slate-200")}>
{file.name}
</span>
</div>
</td>
<td className="py-3 text-sm text-slate-400 hidden md:table-cell">{file.modified}</td>
<td className="py-3 text-sm text-slate-400 hidden lg:table-cell uppercase">{file.type}</td>
<td className="py-3 text-sm text-slate-400 font-mono">{file.size}</td>
<td className="py-3 pr-4 text-right">
<button className="p-1.5 rounded-md text-slate-500 opacity-0 group-hover:opacity-100 hover:bg-white/10 hover:text-white transition-all">
<MoreVertical className="w-4 h-4" />
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={5} className="py-12 text-center text-slate-500">
<div className="flex flex-col items-center justify-center space-y-3">
<Folder className="w-12 h-12 opacity-20" />
<p className="text-sm"></p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Bottom Actions */}
<div className="p-4 border-t border-white/10 flex items-center gap-3 shrink-0 bg-white/[0.01]">
<Button variant="default" className="gap-2">
<Upload className="w-4 h-4" />
</Button>
<Button variant="outline" className="gap-2">
<Plus className="w-4 h-4" />
</Button>
</div>
</Card>
{/* Right Sidebar (Details) */}
{selectedFile && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="w-full lg:w-72 shrink-0"
>
<Card className="h-full">
<CardHeader className="pb-4 border-b border-white/10">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="p-6 space-y-6">
<div className="flex flex-col items-center text-center space-y-3">
<div className="w-16 h-16 rounded-2xl bg-[#336EFF]/10 flex items-center justify-center">
{selectedFile.type === 'folder' ? (
<Folder className="w-8 h-8 text-[#336EFF]" />
) : selectedFile.type === 'image' ? (
<ImageIcon className="w-8 h-8 text-purple-400" />
) : (
<FileText className="w-8 h-8 text-blue-400" />
)}
</div>
<h3 className="text-sm font-medium text-white break-all">{selectedFile.name}</h3>
</div>
<div className="space-y-4">
<DetailItem label="位置" value={`网盘 > ${currentPath.join(' > ')}`} />
<DetailItem label="大小" value={selectedFile.size} />
<DetailItem label="修改时间" value={selectedFile.modified} />
<DetailItem label="类型" value={selectedFile.type.toUpperCase()} />
</div>
{selectedFile.type !== 'folder' && (
<Button variant="outline" className="w-full gap-2 mt-4">
<Download className="w-4 h-4" />
</Button>
)}
{selectedFile.type === 'folder' && (
<Button variant="default" className="w-full gap-2 mt-4" onClick={() => handleFolderDoubleClick(selectedFile)}>
</Button>
)}
</CardContent>
</Card>
</motion.div>
)}
</div>
);
}
function DetailItem({ label, value }: { label: string, value: string }) {
return (
<div>
<p className="text-xs font-medium text-slate-500 mb-1">{label}</p>
<p className="text-sm text-slate-300">{value}</p>
</div>
);
}

109
front/src/pages/Games.tsx Normal file
View File

@@ -0,0 +1,109 @@
import React, { useState } from 'react';
import { motion } from 'motion/react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card';
import { Button } from '@/src/components/ui/button';
import { Gamepad2, Rocket, Cat, Car, Play } from 'lucide-react';
import { cn } from '@/src/lib/utils';
const GAMES = [
{
id: 'cat',
name: 'CAT',
description: '简单的小猫升级游戏,通过点击获取经验,解锁不同形态的猫咪。',
icon: Cat,
color: 'from-orange-400 to-red-500',
category: 'featured'
},
{
id: 'race',
name: 'RACE',
description: '赛车休闲小游戏,躲避障碍物,挑战最高分记录。',
icon: Car,
color: 'from-blue-400 to-indigo-500',
category: 'featured'
}
];
export default function Games() {
const [activeTab, setActiveTab] = useState<'featured' | 'all'>('featured');
return (
<div className="space-y-8">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="glass-panel rounded-3xl p-8 relative overflow-hidden"
>
<div className="absolute top-0 right-0 w-64 h-64 bg-purple-500 rounded-full mix-blend-screen filter blur-[100px] opacity-20" />
<div className="relative z-10 space-y-4 max-w-2xl">
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-white/5 border border-white/10 w-fit">
<Gamepad2 className="w-4 h-4 text-purple-400" />
<span className="text-xs text-slate-300 font-medium tracking-wide uppercase">Entertainment</span>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-white tracking-tight"></h1>
<p className="text-sm text-slate-400 leading-relaxed">
</p>
</div>
</motion.div>
{/* Category Tabs */}
<div className="flex bg-black/20 p-1 rounded-xl w-fit">
<button
onClick={() => setActiveTab('featured')}
className={cn(
"px-6 py-2 text-sm font-medium rounded-lg transition-all",
activeTab === 'featured' ? "bg-white/10 text-white shadow-md" : "text-slate-400 hover:text-white"
)}
>
</button>
<button
onClick={() => setActiveTab('all')}
className={cn(
"px-6 py-2 text-sm font-medium rounded-lg transition-all",
activeTab === 'all' ? "bg-white/10 text-white shadow-md" : "text-slate-400 hover:text-white"
)}
>
</button>
</div>
{/* Game Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{GAMES.map((game, index) => (
<motion.div
key={game.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: index * 0.1 }}
>
<Card className="h-full flex flex-col hover:bg-white/[0.04] transition-colors group overflow-hidden relative">
<div className={cn("absolute top-0 left-0 w-full h-1 bg-gradient-to-r", game.color)} />
<CardHeader className="pb-4">
<div className="flex items-start justify-between">
<div className={cn("w-12 h-12 rounded-2xl flex items-center justify-center bg-gradient-to-br shadow-lg", game.color)}>
<game.icon className="w-6 h-6 text-white" />
</div>
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-500 bg-white/5 px-2 py-1 rounded-md">
{game.category}
</span>
</div>
<CardTitle className="text-xl mt-4">{game.name}</CardTitle>
<CardDescription className="line-clamp-2 mt-2">
{game.description}
</CardDescription>
</CardHeader>
<CardContent className="mt-auto pt-4">
<Button className="w-full gap-2 group-hover:bg-white group-hover:text-black transition-all">
<Play className="w-4 h-4" fill="currentColor" /> Launch
</Button>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
);
}

130
front/src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,130 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'motion/react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card';
import { Button } from '@/src/components/ui/button';
import { Input } from '@/src/components/ui/input';
import { LogIn, User, Lock } from 'lucide-react';
export default function Login() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
// Simulate login
setTimeout(() => {
setLoading(false);
navigate('/overview');
}, 1000);
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#07101D] relative overflow-hidden">
{/* Background Glow */}
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-[#336EFF] rounded-full mix-blend-screen filter blur-[128px] opacity-20 animate-pulse" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-600 rounded-full mix-blend-screen filter blur-[128px] opacity-20" />
<div className="container mx-auto px-4 grid lg:grid-cols-2 gap-12 items-center relative z-10">
{/* Left Side: Brand Info */}
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="flex flex-col space-y-6 max-w-lg"
>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass-panel border-white/10 w-fit">
<span className="w-2 h-2 rounded-full bg-[#336EFF] animate-pulse" />
<span className="text-sm text-slate-300 font-medium tracking-wide uppercase">Access Portal</span>
</div>
<div className="space-y-2">
<h2 className="text-xl text-[#336EFF] font-bold tracking-widest uppercase">YOYUZH.XYZ</h2>
<h1 className="text-5xl md:text-6xl font-bold text-white leading-tight">
<br />
</h1>
</div>
<p className="text-lg text-slate-400 leading-relaxed">
YOYUZH
</p>
</motion.div>
{/* Right Side: Login Form */}
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2, ease: 'easeOut' }}
className="w-full max-w-md mx-auto lg:mx-0 lg:ml-auto"
>
<Card className="border-white/10 backdrop-blur-2xl bg-white/5 shadow-2xl">
<CardHeader className="space-y-1 pb-8">
<CardTitle className="text-2xl font-bold text-white flex items-center gap-2">
<LogIn className="w-6 h-6 text-[#336EFF]" />
</CardTitle>
<CardDescription className="text-slate-400">
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin} className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300 ml-1"></label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<Input
type="text"
placeholder="账号 / 用户名 / 学号"
className="pl-10 bg-black/20 border-white/10 focus-visible:ring-[#336EFF]"
required
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300 ml-1"></label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-500" />
<Input
type="password"
placeholder="••••••••"
className="pl-10 bg-black/20 border-white/10 focus-visible:ring-[#336EFF]"
required
/>
</div>
</div>
</div>
{error && (
<div className="p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
{error}
</div>
)}
<Button
type="submit"
className="w-full h-12 text-base font-semibold"
disabled={loading}
>
{loading ? (
<span className="flex items-center gap-2">
<span className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
...
</span>
) : (
'进入系统'
)}
</Button>
</form>
</CardContent>
</Card>
</motion.div>
</div>
</div>
);
}

View File

@@ -0,0 +1,208 @@
import React from 'react';
import { motion } from 'motion/react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card';
import { Button } from '@/src/components/ui/button';
import {
FileText, Upload, FolderPlus, Database,
GraduationCap, BookOpen, Clock, HardDrive,
User, Mail, ChevronRight
} from 'lucide-react';
export default function Overview() {
const navigate = useNavigate();
const currentHour = new Date().getHours();
let greeting = '晚上好';
if (currentHour < 6) greeting = '凌晨好';
else if (currentHour < 12) greeting = '早上好';
else if (currentHour < 18) greeting = '下午好';
const currentTime = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
return (
<div className="space-y-6">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="glass-panel rounded-3xl p-8 relative overflow-hidden"
>
<div className="absolute top-0 right-0 w-64 h-64 bg-[#336EFF] rounded-full mix-blend-screen filter blur-[100px] opacity-20" />
<div className="relative z-10 space-y-2">
<h1 className="text-3xl md:text-4xl font-bold text-white tracking-tight">tester5595</h1>
<p className="text-[#336EFF] font-medium"> {currentTime} · {greeting}</p>
<p className="text-sm text-slate-400 mt-4 max-w-xl leading-relaxed">
</p>
</div>
</motion.div>
{/* Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard title="网盘文件总数" value="128" desc="包含 4 个分类" icon={FileText} delay={0.1} />
<MetricCard title="最近 7 天上传" value="6" desc="最新更新于 2 小时前" icon={Upload} delay={0.2} />
<MetricCard title="本周课程" value="18" desc="今日还有 2 节课" icon={BookOpen} delay={0.3} />
<MetricCard title="已录入成绩" value="42" desc="最近学期2025 秋" icon={GraduationCap} delay={0.4} />
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column */}
<div className="lg:col-span-2 space-y-6">
{/* Recent Files */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle></CardTitle>
<Button variant="ghost" size="sm" className="text-xs text-slate-400" onClick={() => navigate('/files')}>
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</CardHeader>
<CardContent>
<div className="space-y-2">
{[
{ name: '软件工程期末复习资料.pdf', size: '2.4 MB', time: '2小时前' },
{ name: '2025春季学期课表.xlsx', size: '156 KB', time: '昨天 14:30' },
{ name: '项目架构设计图.png', size: '4.1 MB', time: '3天前' },
].map((file, i) => (
<div key={i} className="flex items-center justify-between p-3 rounded-xl hover:bg-white/5 transition-colors cursor-pointer group" onClick={() => navigate('/files')}>
<div className="flex items-center gap-4 overflow-hidden">
<div className="w-10 h-10 rounded-xl bg-[#336EFF]/10 flex items-center justify-center shrink-0 group-hover:bg-[#336EFF]/20 transition-colors">
<FileText className="w-5 h-5 text-[#336EFF]" />
</div>
<div className="truncate">
<p className="text-sm font-medium text-white truncate">{file.name}</p>
<p className="text-xs text-slate-400 mt-0.5">{file.time}</p>
</div>
</div>
<span className="text-xs text-slate-500 font-mono shrink-0 ml-4">{file.size}</span>
</div>
))}
</div>
</CardContent>
</Card>
{/* Schedule */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle> / </CardTitle>
<div className="flex bg-black/20 rounded-lg p-1">
<button className="px-3 py-1 text-xs font-medium rounded-md bg-[#336EFF] text-white shadow-sm transition-colors"></button>
<button className="px-3 py-1 text-xs font-medium rounded-md text-slate-400 hover:text-white transition-colors"></button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[
{ time: '08:00 - 09:35', name: '高等数学 (下)', room: '教1-204' },
{ time: '10:00 - 11:35', name: '大学物理', room: '教2-101' },
{ time: '14:00 - 15:35', name: '软件工程', room: '计科楼 302' },
].map((course, i) => (
<div key={i} className="flex items-center gap-4 p-4 rounded-xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.04] transition-colors">
<div className="w-28 shrink-0 text-sm font-mono text-[#336EFF] bg-[#336EFF]/10 px-2 py-1 rounded-md text-center">{course.time}</div>
<div className="flex-1 truncate">
<p className="text-sm font-medium text-white truncate">{course.name}</p>
<p className="text-xs text-slate-400 flex items-center gap-1.5 mt-1">
<Clock className="w-3.5 h-3.5" /> {course.room}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Right Column */}
<div className="space-y-6">
{/* Quick Actions */}
<Card>
<CardHeader className="pb-4">
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3">
<QuickAction icon={Upload} label="上传文件" onClick={() => navigate('/files')} />
<QuickAction icon={FolderPlus} label="新建文件夹" onClick={() => navigate('/files')} />
<QuickAction icon={Database} label="进入网盘" onClick={() => navigate('/files')} />
<QuickAction icon={GraduationCap} label="查询成绩" onClick={() => navigate('/school')} />
</div>
</CardContent>
</Card>
{/* Storage */}
<Card>
<CardHeader className="pb-4">
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-5">
<div className="flex justify-between items-end">
<div className="space-y-1">
<p className="text-3xl font-bold text-white tracking-tight">12.6 <span className="text-sm text-slate-400 font-normal">GB</span></p>
<p className="text-xs text-slate-500 uppercase tracking-wider">使 / 50 GB</p>
</div>
<span className="text-xl font-mono text-[#336EFF] font-medium">25%</span>
</div>
<div className="h-2.5 w-full bg-black/40 rounded-full overflow-hidden shadow-inner">
<div className="h-full bg-gradient-to-r from-[#336EFF] to-blue-400 rounded-full" style={{ width: '25%' }} />
</div>
</CardContent>
</Card>
{/* Account Info */}
<Card>
<CardHeader className="pb-4">
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4 p-4 rounded-xl bg-white/[0.02] border border-white/5">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-bold text-xl shadow-lg">
T
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-white truncate">tester5595</p>
<p className="text-xs text-slate-400 truncate mt-0.5">tester5595@example.com</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
function MetricCard({ title, value, desc, icon: Icon, delay }: any) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay }}
>
<Card className="h-full hover:bg-white/[0.04] transition-colors">
<CardContent className="p-6 flex flex-col gap-4">
<div className="flex justify-between items-start">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-[#336EFF]/20 to-blue-500/10 flex items-center justify-center border border-[#336EFF]/20">
<Icon className="w-6 h-6 text-[#336EFF]" />
</div>
<span className="text-3xl font-bold text-white tracking-tight">{value}</span>
</div>
<div className="mt-2">
<p className="text-sm font-medium text-slate-300">{title}</p>
<p className="text-xs text-slate-500 mt-1">{desc}</p>
</div>
</CardContent>
</Card>
</motion.div>
);
}
function QuickAction({ icon: Icon, label, onClick }: any) {
return (
<button
onClick={onClick}
className="flex flex-col items-center justify-center gap-3 p-4 rounded-xl border border-white/5 bg-white/[0.02] hover:bg-white/[0.06] hover:border-white/10 transition-all group"
>
<Icon className="w-6 h-6 text-slate-400 group-hover:text-[#336EFF] transition-colors" />
<span className="text-xs font-medium text-slate-300 group-hover:text-white transition-colors">{label}</span>
</button>
);
}

291
front/src/pages/School.tsx Normal file
View File

@@ -0,0 +1,291 @@
import React, { useState } from 'react';
import { motion } from 'motion/react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/src/components/ui/card';
import { Button } from '@/src/components/ui/button';
import { Input } from '@/src/components/ui/input';
import { GraduationCap, Calendar, User, Lock, Search, BookOpen, ChevronRight, Award } from 'lucide-react';
import { cn } from '@/src/lib/utils';
export default function School() {
const [activeTab, setActiveTab] = useState<'schedule' | 'grades'>('schedule');
const [loading, setLoading] = useState(false);
const [queried, setQueried] = useState(false);
const handleQuery = (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setTimeout(() => {
setLoading(false);
setQueried(true);
}, 1500);
};
return (
<div className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Query Form */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Search className="w-5 h-5 text-[#336EFF]" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleQuery} className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 ml-1"></label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input defaultValue="2023123456" className="pl-9 bg-black/20" required />
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 ml-1"></label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input type="password" defaultValue="password123" className="pl-9 bg-black/20" required />
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 ml-1"></label>
<select className="flex h-11 w-full rounded-xl border border-white/10 bg-black/20 px-3 py-2 text-sm text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#336EFF]">
<option value="2025-spring">2025 </option>
<option value="2024-fall">2024 </option>
<option value="2024-spring">2024 </option>
</select>
</div>
<div className="grid grid-cols-2 gap-3 pt-2">
<Button type="submit" disabled={loading} className="w-full">
{loading ? '查询中...' : '查询课表'}
</Button>
<Button type="submit" variant="outline" disabled={loading} className="w-full" onClick={() => setActiveTab('grades')}>
{loading ? '查询中...' : '查询成绩'}
</Button>
</div>
</form>
</CardContent>
</Card>
{/* Data Summary */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DatabaseIcon className="w-5 h-5 text-[#336EFF]" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{queried ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<SummaryItem label="当前缓存账号" value="2023123456" icon={User} />
<SummaryItem label="已保存课表学期" value="2025 春" icon={Calendar} />
<SummaryItem label="已保存成绩" value="3 个学期" icon={Award} />
</div>
) : (
<div className="h-40 flex flex-col items-center justify-center text-slate-500 space-y-3 border border-dashed border-white/10 rounded-xl bg-white/[0.01]">
<Search className="w-8 h-8 opacity-50" />
<p className="text-sm"></p>
</div>
)}
</CardContent>
</Card>
</div>
{/* View Toggle */}
<div className="flex bg-black/20 p-1 rounded-xl w-fit">
<button
onClick={() => setActiveTab('schedule')}
className={cn(
"px-6 py-2 text-sm font-medium rounded-lg transition-all",
activeTab === 'schedule' ? "bg-[#336EFF] text-white shadow-md" : "text-slate-400 hover:text-white"
)}
>
</button>
<button
onClick={() => setActiveTab('grades')}
className={cn(
"px-6 py-2 text-sm font-medium rounded-lg transition-all",
activeTab === 'grades' ? "bg-[#336EFF] text-white shadow-md" : "text-slate-400 hover:text-white"
)}
>
</button>
</div>
{/* Content Area */}
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{activeTab === 'schedule' ? <ScheduleView queried={queried} /> : <GradesView queried={queried} />}
</motion.div>
</div>
);
}
function DatabaseIcon(props: any) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M3 5V19A9 3 0 0 0 21 19V5" />
<path d="M3 12A9 3 0 0 0 21 12" />
</svg>
);
}
function SummaryItem({ label, value, icon: Icon }: any) {
return (
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/5 flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-[#336EFF]/10 flex items-center justify-center shrink-0">
<Icon className="w-5 h-5 text-[#336EFF]" />
</div>
<div>
<p className="text-xs text-slate-400 mb-0.5">{label}</p>
<p className="text-sm font-medium text-white">{value}</p>
</div>
</div>
);
}
function ScheduleView({ queried }: { queried: boolean }) {
if (!queried) {
return (
<Card>
<CardContent className="h-64 flex flex-col items-center justify-center text-slate-500">
<BookOpen className="w-12 h-12 mb-4 opacity-20" />
<p></p>
</CardContent>
</Card>
);
}
const days = ['周一', '周二', '周三', '周四', '周五'];
const mockSchedule = [
{ day: 0, time: '08:00 - 09:35', name: '高等数学 (下)', room: '教1-204' },
{ day: 0, time: '10:00 - 11:35', name: '大学物理', room: '教2-101' },
{ day: 1, time: '14:00 - 15:35', name: '软件工程', room: '计科楼 302' },
{ day: 2, time: '08:00 - 09:35', name: '数据结构', room: '教1-105' },
{ day: 3, time: '16:00 - 17:35', name: '计算机网络', room: '计科楼 401' },
{ day: 4, time: '10:00 - 11:35', name: '操作系统', room: '教3-202' },
];
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{days.map((day, index) => (
<div key={day} className="space-y-3">
<div className="text-center py-2 bg-white/5 rounded-lg text-sm font-medium text-slate-300">
{day}
</div>
<div className="space-y-2">
{mockSchedule.filter(s => s.day === index).map((course, i) => (
<div key={i} className="p-3 rounded-xl bg-[#336EFF]/10 border border-[#336EFF]/20 hover:bg-[#336EFF]/20 transition-colors">
<p className="text-xs font-mono text-[#336EFF] mb-1">{course.time}</p>
<p className="text-sm font-medium text-white leading-tight mb-2">{course.name}</p>
<p className="text-xs text-slate-400 flex items-center gap-1">
<ChevronRight className="w-3 h-3" /> {course.room}
</p>
</div>
))}
{mockSchedule.filter(s => s.day === index).length === 0 && (
<div className="h-24 rounded-xl border border-dashed border-white/10 flex items-center justify-center text-xs text-slate-500">
</div>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
function GradesView({ queried }: { queried: boolean }) {
if (!queried) {
return (
<Card>
<CardContent className="h-64 flex flex-col items-center justify-center text-slate-500">
<Award className="w-12 h-12 mb-4 opacity-20" />
<p></p>
</CardContent>
</Card>
);
}
const terms = [
{
name: '2024 秋',
grades: [75, 78, 80, 83, 85, 88, 89, 96]
},
{
name: '2025 春',
grades: [70, 78, 82, 84, 85, 85, 86, 88, 93]
},
{
name: '2025 秋',
grades: [68, 70, 76, 80, 85, 86, 90, 94, 97]
}
];
const getScoreStyle = (score: number) => {
if (score >= 95) return 'bg-[#336EFF]/50 text-white';
if (score >= 90) return 'bg-[#336EFF]/40 text-white/90';
if (score >= 85) return 'bg-[#336EFF]/30 text-white/80';
if (score >= 80) return 'bg-slate-700/60 text-white/70';
if (score >= 75) return 'bg-slate-700/40 text-white/60';
return 'bg-slate-800/60 text-white/50';
};
return (
<Card className="bg-[#0f172a]/80 backdrop-blur-sm border-slate-800/50">
<CardHeader className="pb-2">
<CardTitle className="text-lg font-medium text-white"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{terms.map((term, i) => (
<div key={i} className="flex flex-col">
<h3 className="text-sm font-bold text-white border-b border-white/5 pb-3 mb-4">{term.name}</h3>
<div className="flex flex-col gap-2">
{term.grades.map((score, j) => (
<div
key={j}
className={cn(
"w-full py-1.5 rounded-full text-xs font-mono font-medium text-center transition-colors",
getScoreStyle(score)
)}
>
{score}
</div>
))}
</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}

26
front/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

24
front/vite.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
export default defineConfig(({mode}) => {
const env = loadEnv(mode, '.', '');
return {
plugins: [react(), tailwindcss()],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
},
};
});