add backend
This commit is contained in:
9
front/.env.example
Normal file
9
front/.env.example
Normal 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
8
front/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
8
front/.vite/deps/_metadata.json
Normal file
8
front/.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"hash": "1eac4ae6",
|
||||
"configHash": "19e214db",
|
||||
"lockfileHash": "126cd023",
|
||||
"browserHash": "c5ddb224",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
3
front/.vite/deps/package.json
Normal file
3
front/.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
20
front/README.md
Normal file
20
front/README.md
Normal 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
13
front/index.html
Normal 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
5
front/metadata.json
Normal 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
5281
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
front/package.json
Normal file
39
front/package.json
Normal 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
25
front/src/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
front/src/components/layout/Layout.tsx
Normal file
91
front/src/components/layout/Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
36
front/src/components/ui/button.tsx
Normal file
36
front/src/components/ui/button.tsx
Normal 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 }
|
||||
78
front/src/components/ui/card.tsx
Normal file
78
front/src/components/ui/card.tsx
Normal 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 }
|
||||
24
front/src/components/ui/input.tsx
Normal file
24
front/src/components/ui/input.tsx
Normal 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
88
front/src/index.css
Normal 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
6
front/src/lib/utils.ts
Normal 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
10
front/src/main.tsx
Normal 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
275
front/src/pages/Files.tsx
Normal 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
109
front/src/pages/Games.tsx
Normal 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
130
front/src/pages/Login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
208
front/src/pages/Overview.tsx
Normal file
208
front/src/pages/Overview.tsx
Normal 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
291
front/src/pages/School.tsx
Normal 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
26
front/tsconfig.json
Normal 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
24
front/vite.config.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user