no message

This commit is contained in:
DESKTOP-PB0N82B\admin 2026-01-30 18:02:55 +08:00
parent 271c11b453
commit 93410c55d7
26 changed files with 2601 additions and 463 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://172.16.1.12" /> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self' http://172.16.1.12" />
<title>AssetPro</title> <title>AssetPro</title>
</head> </head>
<body> <body>

1169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,14 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"axios": "^1.13.2", "axios": "^1.13.2",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",

View File

@ -10,6 +10,7 @@ import FileLibrary from '@/pages/files/FileLibrary'
import { ToastProvider } from '@/components/ui/toast-provider' import { ToastProvider } from '@/components/ui/toast-provider'
import { DownloadManagerProvider } from '@/components/DownloadManager' import { DownloadManagerProvider } from '@/components/DownloadManager'
import { UserProvider } from '@/contexts/UserContext'
// 其他页面的占位符,以避免演示期间出现 404 // 其他页面的占位符,以避免演示期间出现 404
const PlaceholderPage = ({ title }: { title: string }) => ( const PlaceholderPage = ({ title }: { title: string }) => (
@ -24,36 +25,38 @@ const PlaceholderPage = ({ title }: { title: string }) => (
function App() { function App() {
return ( return (
<ToastProvider> <ToastProvider>
<DownloadManagerProvider> <UserProvider>
<HashRouter> <DownloadManagerProvider>
<Routes> <HashRouter>
<Route path="/" element={<Login />} /> <Routes>
{/* Full width pages with Header but no Sidebar */} <Route path="/" element={<Login />} />
<Route element={<MainLayout showSidebar={false} />}> {/* Full width pages with Header but no Sidebar */}
<Route path="/upload" element={<UploadResource />} /> <Route element={<MainLayout showSidebar={false} />}>
<Route path="/profile" element={<ProfilePage />} /> <Route path="/upload" element={<UploadResource />} />
<Route path="/app/projects/new" element={<NewProject />} /> <Route path="/profile" element={<ProfilePage />} />
</Route> <Route path="/app/projects/new" element={<NewProject />} />
</Route>
{/* Main Application Routes */} {/* Main Application Routes */}
<Route path="/app" element={<MainLayout />}> <Route path="/app" element={<MainLayout />}>
<Route index element={<Navigate to="/app/plugins" replace />} /> <Route index element={<Navigate to="/app/plugins" replace />} />
<Route path="plugins" element={<AssetLibrary />} /> <Route path="plugins" element={<AssetLibrary />} />
<Route path="components" element={<PlaceholderPage title="组件库" />} /> <Route path="components" element={<PlaceholderPage title="组件库" />} />
<Route path="tools" element={<PlaceholderPage title="工具库" />} /> <Route path="tools" element={<PlaceholderPage title="工具库" />} />
<Route path="prototypes" element={<PlaceholderPage title="原型库" />} /> <Route path="prototypes" element={<PlaceholderPage title="原型库" />} />
<Route path="design" element={<PlaceholderPage title="设计库" />} /> <Route path="design" element={<PlaceholderPage title="设计库" />} />
<Route path="projects" element={<ProjectLibrary />} /> <Route path="projects" element={<ProjectLibrary />} />
<Route path="files" element={<FileLibrary />} /> <Route path="files" element={<FileLibrary />} />
<Route path="cases" element={<PlaceholderPage title="案例库" />} /> <Route path="cases" element={<PlaceholderPage title="案例库" />} />
<Route path="pending" element={<PlaceholderPage title="待审核" />} /> <Route path="pending" element={<PlaceholderPage title="待审核" />} />
</Route> </Route>
{/* Legacy/Redirects */} {/* Legacy/Redirects */}
<Route path="/dashboard" element={<Navigate to="/app" replace />} /> <Route path="/dashboard" element={<Navigate to="/app" replace />} />
</Routes> </Routes>
</HashRouter> </HashRouter>
</DownloadManagerProvider> </DownloadManagerProvider>
</UserProvider>
</ToastProvider> </ToastProvider>
) )
} }

View File

@ -13,6 +13,29 @@ export interface LoginResponse {
token: string; token: string;
} }
export interface UserInfo {
userId: number;
userName: string;
nickName: string;
email: string;
phonenumber: string;
sex: string;
avatar: string;
status: string;
}
export interface GetInfoResponse {
code: number;
msg: string;
user: UserInfo;
roles: string[];
permissions: string[];
}
export const login = (data: LoginParams) => { export const login = (data: LoginParams) => {
return simClient.post<any, LoginResponse>('/login', data); return simClient.post<any, LoginResponse>('/login', data);
}; };
export const getUserInfo = () => {
return simClient.get<any, GetInfoResponse>('/getInfo');
};

View File

@ -5,7 +5,7 @@ import { AddSimulationResourceRequest, AddSimulationResourceResponse, GetSimulat
// 开发环境使用 Vite 代理,生产环境使用实际地址 // 开发环境使用 Vite 代理,生产环境使用实际地址
export const SIM_API_BASE_URL = import.meta.env.DEV export const SIM_API_BASE_URL = import.meta.env.DEV
? '/zichan-api' ? '/zichan-api'
: 'http://172.16.1.144:8081'; : 'http://172.16.1.144:8082';
const simClient = axios.create({ const simClient = axios.create({
baseURL: SIM_API_BASE_URL, baseURL: SIM_API_BASE_URL,

View File

@ -42,6 +42,7 @@ export interface SimulationResource {
env: string; env: string;
createdTime: string; createdTime: string;
createdBy: string; createdBy: string;
remark?: string;
} }
export interface GetSimulationListRequest { export interface GetSimulationListRequest {

View File

@ -1,12 +1,22 @@
import { Search, Plus, User, Minus, Square, X } from 'lucide-react' import { Search, Plus, User, Minus, Square, X, LogOut } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useToast } from '@/components/ui/toast-provider' import { useToast } from '@/components/ui/toast-provider'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Separator } from '@/components/ui/separator'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useUser } from '@/contexts/UserContext'
export function Header() { export function Header() {
const navigate = useNavigate() const navigate = useNavigate()
const { showToast } = useToast() const { showToast } = useToast()
const { user, logout } = useUser()
const handleMinimize = () => { const handleMinimize = () => {
window.electronAPI?.window.minimize() window.electronAPI?.window.minimize()
@ -21,108 +31,117 @@ export function Header() {
} }
return ( return (
<div className="h-16 border-b bg-white flex items-center justify-between px-6 shadow-sm z-10 relative drag-region"> <TooltipProvider>
{/* Left: Logo */} <div className="h-16 border-b bg-white flex items-center justify-between px-6 shadow-sm z-10 relative drag-region">
<div className="flex items-center gap-2"> {/* Left: Logo */}
<div className="w-8 h-8 bg-[#0e7490] rounded-md flex items-center justify-center"> <div className="flex items-center gap-2">
{/* Simple Logo Icon Placeholder */} <div className="w-8 h-8 bg-[#0e7490] rounded-md flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> {/* Simple Logo Icon Placeholder */}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> <svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</svg> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</div> </svg>
<span className="text-xl font-bold text-slate-800"></span>
</div>
{/* Center: Search (aligned slightly left or center) */}
<div className="flex-1 max-w-xl mx-12 flex justify-center">
<div className="relative w-full no-drag">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
placeholder="搜索资源名称/标签/ID..."
className="pl-10 bg-slate-50 border-slate-200 focus-visible:ring-[#0e7490]"
/>
</div>
</div>
{/* Right: Actions */}
<div className="flex items-center gap-4 no-drag">
<Button
onClick={() => navigate('/upload')}
className="bg-[#e0f2fe] text-[#0e7490] hover:bg-[#bae6fd] border-none shadow-none gap-2 font-medium"
>
<Plus className="w-4 h-4" />
</Button>
<div className="h-6 w-px bg-slate-200 mx-2"></div>
<div className="relative group h-full flex items-center">
<div
className="flex items-center gap-3 cursor-pointer hover:bg-slate-50 p-1 rounded-md transition-colors h-full"
onClick={() => navigate('/profile')}
>
<div className="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center overflow-hidden border border-slate-200">
<User className="w-4 h-4 text-slate-400" />
</div>
{/* Removed text as requested */}
</div> </div>
<span className="text-xl font-bold text-slate-800"></span>
</div>
{/* Dropdown Menu - Adjusted position and content */} {/* Center: Search (aligned slightly left or center) */}
<div className="absolute right-0 top-full mt-1 w-32 bg-white border border-slate-200 rounded-md shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-right z-50"> <div className="flex-1 max-w-xl mx-12 flex justify-center">
<div className="py-1"> <div className="relative w-full no-drag">
<button <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
onClick={(e) => { <Input
e.stopPropagation(); placeholder="搜索资源名称/标签/ID..."
localStorage.removeItem('token'); className="pl-10 bg-slate-50 border-slate-200 focus-visible:ring-[#0e7490]"
/>
</div>
</div>
{/* Right: Actions */}
<div className="flex items-center gap-4 no-drag">
<Button
onClick={() => navigate('/upload')}
className="bg-[#e0f2fe] text-[#0e7490] hover:bg-[#bae6fd] border-none shadow-none gap-2 font-medium"
>
<Plus className="w-4 h-4" />
</Button>
<Separator orientation="vertical" className="h-6 mx-2" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex items-center gap-3 cursor-pointer hover:bg-slate-50 p-1 rounded-md transition-colors no-drag">
<div className="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center overflow-hidden border border-slate-200">
{user?.avatar ? <img src={user.avatar} className="w-full h-full object-cover" /> : <User className="w-4 h-4 text-slate-400" />}
</div>
<span className="text-sm font-medium text-slate-700">{user?.userName || '加载中...'}</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={() => navigate('/profile')}>
<User className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-600 focus:text-red-600"
onClick={() => {
logout();
showToast('已安全退出', 'success'); showToast('已安全退出', 'success');
navigate('/'); // Go to login page navigate('/');
}} }}
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <LogOut className="mr-2 h-4 w-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> <span>退</span>
</svg> </DropdownMenuItem>
退 </DropdownMenuContent>
</button> </DropdownMenu>
</div>
<Separator orientation="vertical" className="h-6 mx-2" />
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-slate-400 hover:text-slate-900 hover:bg-slate-100 w-8 h-8"
onClick={handleMinimize}
>
<Minus className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-slate-400 hover:text-slate-900 hover:bg-slate-100 w-8 h-8"
onClick={handleMaximize}
>
<Square className="w-3.5 h-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-slate-400 hover:text-red-600 hover:bg-red-50 w-8 h-8"
onClick={handleClose}
>
<X className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
{/* Window Controls */}
<div className="h-6 w-px bg-slate-200 mx-2"></div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="text-slate-400 hover:text-slate-900 hover:bg-slate-100 w-8 h-8"
onClick={handleMinimize}
title="最小化"
>
<Minus className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-slate-400 hover:text-slate-900 hover:bg-slate-100 w-8 h-8"
onClick={handleMaximize}
title="最大化/还原"
>
<Square className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-slate-400 hover:text-red-600 hover:bg-red-50 w-8 h-8"
onClick={handleClose}
title="关闭"
>
<X className="w-4 h-4" />
</Button>
</div>
</div> </div>
</div> </TooltipProvider>
) )
} }

View File

@ -0,0 +1,38 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
cyan: "border-transparent bg-cyan-100 text-cyan-700 hover:bg-cyan-200/80",
slate: "border-transparent bg-slate-100 text-slate-600 hover:bg-slate-200/80",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> { }
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,198 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

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

View File

@ -0,0 +1,158 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-slate-200 bg-slate-50/50 px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-[#0e7490]/20 focus:border-[#0e7490] disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

117
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -6,14 +6,14 @@ import * as React from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export interface TextareaProps export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
className={cn( className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', 'flex min-h-[80px] w-full rounded-md border border-slate-200 bg-slate-50/50 px-3 py-2 text-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0e7490]/20 focus-visible:border-[#0e7490] disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
ref={ref} ref={ref}

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,65 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { getUserInfo, UserInfo } from '@/api/auth';
interface UserContextType {
user: UserInfo | null;
loading: boolean;
refreshUser: () => Promise<void>;
logout: () => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<UserInfo | null>(null);
const [loading, setLoading] = useState(true);
const refreshUser = useCallback(async () => {
const token = localStorage.getItem('token');
if (!token) {
setUser(null);
setLoading(false);
return;
}
try {
setLoading(true);
const response = await getUserInfo();
if (response.code === 200) {
setUser(response.user);
} else {
// Token invalid or other error
localStorage.removeItem('token');
setUser(null);
}
} catch (error) {
console.error('Failed to fetch user info:', error);
setUser(null);
} finally {
setLoading(false);
}
}, []);
const logout = useCallback(() => {
localStorage.removeItem('token');
setUser(null);
}, []);
useEffect(() => {
refreshUser();
}, [refreshUser]);
return (
<UserContext.Provider value={{ user, loading, refreshUser, logout }}>
{children}
</UserContext.Provider>
);
};
export const useUser = () => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
};

View File

@ -8,10 +8,12 @@ import bgImage from '@/assets/images/登录背景.png'
import { login } from '@/api/auth' import { login } from '@/api/auth'
import { useToast } from '@/components/ui/toast-provider' import { useToast } from '@/components/ui/toast-provider'
import { useUser } from '@/contexts/UserContext'
export default function Login() { export default function Login() {
const navigate = useNavigate() const navigate = useNavigate()
const { showToast } = useToast() const { showToast } = useToast()
const { refreshUser } = useUser()
const [username, setUsername] = useState('admin') // Default to admin as per request const [username, setUsername] = useState('admin') // Default to admin as per request
const [password, setPassword] = useState('123456') // Default password const [password, setPassword] = useState('123456') // Default password
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -29,6 +31,7 @@ export default function Login() {
// res structure: { msg: "...", code: 200, data: ..., token: "..." } // res structure: { msg: "...", code: 200, data: ..., token: "..." }
if (res.code === 200) { if (res.code === 200) {
localStorage.setItem('token', res.token) localStorage.setItem('token', res.token)
await refreshUser()
showToast('登录成功', 'success') showToast('登录成功', 'success')
navigate('/app') navigate('/app')
} else { } else {

View File

@ -9,6 +9,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useUser } from '@/contexts/UserContext'
// Mock Data // Mock Data
const MOCK_RESOURCES = [ const MOCK_RESOURCES = [
@ -55,6 +56,8 @@ export default function ProfilePage() {
const [activeTab, setActiveTab] = useState('resources') // info, resources, favorites const [activeTab, setActiveTab] = useState('resources') // info, resources, favorites
const [resourceFilter, setResourceFilter] = useState('all') // all, published, draft, auditing const [resourceFilter, setResourceFilter] = useState('all') // all, published, draft, auditing
const { user } = useUser()
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
switch (status) { switch (status) {
case 'published': case 'published':
@ -89,17 +92,17 @@ export default function ProfilePage() {
{/* User Profile Card */} {/* User Profile Card */}
<div className="bg-white rounded-xl p-6 border border-slate-200 flex flex-col items-center"> <div className="bg-white rounded-xl p-6 border border-slate-200 flex flex-col items-center">
<div className="w-20 h-20 bg-slate-100 rounded-full flex items-center justify-center mb-4 relative group cursor-pointer overflow-hidden"> <div className="w-20 h-20 bg-slate-100 rounded-full flex items-center justify-center mb-4 relative group cursor-pointer overflow-hidden">
{/* Placeholder Avatar */} {/* Avatar */}
<User className="w-8 h-8 text-slate-400" /> {user?.avatar ? <img src={user.avatar} className="w-full h-full object-cover" /> : <User className="w-8 h-8 text-slate-400" />}
<div className="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center transition-all"> <div className="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center transition-all">
<Edit className="w-4 h-4 text-white" /> <Edit className="w-4 h-4 text-white" />
</div> </div>
</div> </div>
<div className="text-center mb-1"> <div className="text-center mb-1">
<h2 className="font-bold text-lg text-slate-800 flex items-center gap-2 justify-center"> <h2 className="font-bold text-lg text-slate-800 flex items-center gap-2 justify-center">
{user?.nickName || user?.userName || '加载中...'}
<span className="text-[10px] bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded border border-amber-200"> <span className="text-[10px] bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded border border-amber-200">
</span> </span>
</h2> </h2>
</div> </div>

View File

@ -1,18 +1,19 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Filter, Download, Eye, Heart } from 'lucide-react' import { Filter, Download, Eye, Heart, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardFooter } from '@/components/ui/card' import { Card, CardContent, CardFooter } from '@/components/ui/card'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { getSimulationUploadList } from '@/api/simulation'
import { SimulationResource } from '@/api/simulationTypes'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
// 模拟数据 // 模拟数据
const tabs = [ const tabs = [
"全部", "地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊" "全部", "地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"
] ]
import { getSimulationUploadList } from '@/api/simulation'
import { SimulationResource } from '@/api/simulationTypes'
import { Loader2 } from 'lucide-react'
export default function AssetLibrary() { export default function AssetLibrary() {
const [activeTab, setActiveTab] = useState("全部") const [activeTab, setActiveTab] = useState("全部")
const [assets, setAssets] = useState<SimulationResource[]>([]) const [assets, setAssets] = useState<SimulationResource[]>([])
@ -27,7 +28,6 @@ export default function AssetLibrary() {
const response = await getSimulationUploadList({ const response = await getSimulationUploadList({
pageNum: 1, pageNum: 1,
pageSize: 20, // Fetch more initially pageSize: 20, // Fetch more initially
// If "全部" is selected, don't filter by type (or handle specific types if needed)
types: activeTab === "全部" ? undefined : activeTab types: activeTab === "全部" ? undefined : activeTab
}) })
@ -46,98 +46,117 @@ export default function AssetLibrary() {
}, [activeTab]) }, [activeTab])
return ( return (
<div className="bg-white rounded-xl shadow-sm min-h-full flex flex-col"> <TooltipProvider>
{/* Top Tabs & Filter Bar */} <div className="bg-white rounded-xl shadow-sm min-h-full flex flex-col">
<div className="flex items-center justify-between px-6 border-b border-slate-100"> {/* Top Tabs & Filter Bar */}
<div className="flex items-center gap-14 overflow-x-auto no-scrollbar pl-14 pr-14"> <div className="flex items-center justify-between px-6 border-b border-slate-100">
{tabs.map((tab) => ( <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 overflow-x-auto no-scrollbar py-2">
<button <TabsList className="bg-transparent h-auto gap-0 p-0">
key={tab} {tabs.map((tab) => (
onClick={() => setActiveTab(tab)} <TabsTrigger
className={cn( key={tab}
"py-4 text-sm font-medium transition-all relative", value={tab}
activeTab === tab className={cn(
? "text-[#0e7490] font-bold" "py-4 px-8 text-sm font-semibold transition-all relative rounded-none border-b-0 data-[state=active]:text-[#0e7490] data-[state=active]:bg-transparent data-[state=active]:shadow-none text-slate-500 hover:text-[#0e7490]",
: "text-slate-500 hover:text-slate-800" "after:absolute after:bottom-0 after:left-8 after:right-8 after:h-[3.5px] after:bg-[#0e7490] after:scale-x-0 data-[state=active]:after:scale-x-100 after:transition-transform after:duration-200 after:rounded-t-full"
)} )}
> >
{tab} {tab}
{activeTab === tab && ( </TabsTrigger>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[300%] h-0.5 bg-[#0e7490]" /> ))}
)} </TabsList>
</button> </Tabs>
))}
<div className="flex items-center gap-4">
<span className="text-xs text-slate-400 whitespace-nowrap"> {total} </span>
<Button variant="ghost" size="sm" className="gap-2 text-slate-600 hover:text-[#0e7490]">
<Filter className="w-4 h-4" />
</Button>
</div>
</div> </div>
<Button variant="ghost" size="sm" className="gap-2 text-slate-600 hover:text-[#0e7490]"> {/* Asset Grid */}
<Filter className="w-4 h-4" /> <div className="p-6">
{loading ? (
</Button> <div className="flex h-64 items-center justify-center text-slate-400">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : assets.length === 0 ? (
<div className="flex h-64 items-center justify-center text-slate-400 text-sm">
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{assets.map((asset) => (
<Card key={asset.id} className="overflow-hidden hover:shadow-xl transition-all duration-300 border-slate-100 group bg-white">
{/* Image Area */}
<div className="aspect-video bg-slate-100 relative overflow-hidden">
{/* Placeholder Gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-slate-200 to-slate-300 group-hover:scale-105 transition-transform duration-500" />
{/* Hover Actions Overlay */}
<div className="absolute top-3 right-3 flex gap-2 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-y-[-10px] group-hover:translate-y-0 z-10">
<Tooltip>
<TooltipTrigger asChild>
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Eye className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top"></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Heart className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top"></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Download className="w-4 h-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top"></TooltipContent>
</Tooltip>
</div>
</div>
<CardContent className="p-4 space-y-3">
<h3 className="font-bold text-slate-800 line-clamp-2 text-sm leading-relaxed min-h-[40px]" title={asset.name}>
{asset.name}
</h3>
<div className="flex flex-wrap gap-2">
{asset.tag && asset.tag.split(/[,]/).map(tag => (
<Badge key={tag} variant="slate" className="font-normal border-slate-100">
{tag.trim()}
</Badge>
))}
</div>
</CardContent>
<CardFooter className="p-4 pt-0 flex items-center justify-between text-xs text-slate-400 border-t border-slate-50 mt-2 pt-3">
<div className="flex items-center gap-2">
<span>{asset.remark || asset.createdBy || 'Unknown'}</span>
<span className="text-slate-300">|</span>
<span>{asset.createdTime ? asset.createdTime.split(' ')[0] : '-'}</span>
</div>
<div className="flex items-center gap-1">
<Download className="w-3 h-3" />
<span>0</span>
</div>
</CardFooter>
</Card>
))}
</div>
)}
</div>
</div> </div>
</TooltipProvider>
{/* Asset Grid */}
<div className="p-6">
{loading ? (
<div className="flex h-64 items-center justify-center text-slate-400">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : assets.length === 0 ? (
<div className="flex h-64 items-center justify-center text-slate-400 text-sm">
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{assets.map((asset) => (
<Card key={asset.id} className="overflow-hidden hover:shadow-xl transition-all duration-300 border-slate-100 group bg-white">
{/* Image Area */}
<div className="aspect-video bg-slate-100 relative overflow-hidden">
{/* Placeholder Gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-slate-200 to-slate-300 group-hover:scale-105 transition-transform duration-500" />
{/* Hover Actions Overlay */}
<div className="absolute top-3 right-3 flex gap-2 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-y-[-10px] group-hover:translate-y-0 z-10">
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Eye className="w-4 h-4" />
</button>
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Heart className="w-4 h-4" />
</button>
<button className="w-8 h-8 rounded-full bg-black/20 hover:bg-black/40 text-white flex items-center justify-center backdrop-blur-sm transition-colors">
<Download className="w-4 h-4" />
</button>
</div>
</div>
<CardContent className="p-4 space-y-3">
<h3 className="font-bold text-slate-800 line-clamp-2 text-sm leading-relaxed min-h-[40px]" title={asset.name}>
{asset.name}
</h3>
<div className="flex flex-wrap gap-2">
{asset.tag && asset.tag.split(/[,]/).map(tag => (
<span key={tag} className="px-2 py-0.5 bg-slate-50 text-slate-400 text-xs rounded-sm border border-slate-100">
{tag.trim()}
</span>
))}
</div>
</CardContent>
<CardFooter className="p-4 pt-0 flex items-center justify-between text-xs text-slate-400 border-t border-slate-50 mt-2 pt-3">
<div className="flex items-center gap-2">
<span>{asset.createdBy || 'Unknown'}</span>
<span className="text-slate-300">|</span>
<span>{asset.createdTime ? asset.createdTime.split(' ')[0] : '-'}</span>
</div>
<div className="flex items-center gap-1">
<Download className="w-3 h-3" />
<span>0</span>
</div>
</CardFooter>
</Card>
))}
</div>
)}
</div>
</div>
) )
} }

View File

@ -1,13 +1,23 @@
import { useState, type ChangeEvent, useContext } from 'react' import { useState, useEffect, type ChangeEvent, useContext } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ArrowLeft, Upload, X, CheckCircle2, Loader2 } from 'lucide-react' import { ArrowLeft, Upload, X, CheckCircle2, Loader2 } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { addSimulationResource } from '@/api/simulation' import { addSimulationResource } from '@/api/simulation'
import { AddSimulationResourceRequest, ProUploadItem } from '@/api/simulationTypes' import { AddSimulationResourceRequest, ProUploadItem } from '@/api/simulationTypes'
import { ToastContext } from '@/components/ui/toast-provider' import { ToastContext } from '@/components/ui/toast-provider'
import { useUser } from '@/contexts/UserContext'
const RESOURCE_LIBRARIES: Record<string, string[]> = { const RESOURCE_LIBRARIES: Record<string, string[]> = {
'插件库': ["地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"], '插件库': ["地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"],
@ -37,10 +47,21 @@ export default function UploadResource() {
urls: 'http://example.com', urls: 'http://example.com',
versions: 'v1.0.0', versions: 'v1.0.0',
env: 'Windows 10/Linux', env: 'Windows 10/Linux',
createdBy: 'Admin', createdBy: '',
files: [] as File[], files: [] as File[],
thumbnail: null as File | null,
thumbnailUrl: '' as string,
}) })
const { user } = useUser()
// 当用户信息加载后,自动设置上传人
useEffect(() => {
if (user?.nickName) {
setFormData(prev => ({ ...prev, createdBy: user.nickName }))
}
}, [user])
// Mock file upload progress // Mock file upload progress
const [uploadProgress, setUploadProgress] = useState(0) const [uploadProgress, setUploadProgress] = useState(0)
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
@ -78,6 +99,14 @@ export default function UploadResource() {
} }
} }
const handleThumbnailChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const url = URL.createObjectURL(file);
setFormData((prev) => ({ ...prev, thumbnail: file, thumbnailUrl: url }))
}
}
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
setIsUploading(true) setIsUploading(true)
@ -200,35 +229,42 @@ export default function UploadResource() {
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label> <label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label>
<select <Select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={formData.stores} value={formData.stores}
onChange={e => { onValueChange={value => {
const newStore = e.target.value;
setFormData({ setFormData({
...formData, ...formData,
stores: newStore, stores: value,
type: RESOURCE_LIBRARIES[newStore]?.[0] || '' type: RESOURCE_LIBRARIES[value]?.[0] || ''
}) })
}} }}
> >
{Object.keys(RESOURCE_LIBRARIES).map(store => ( <SelectTrigger className="w-full">
<option key={store} value={store}>{store}</option> <SelectValue placeholder="选择资源库" />
))} </SelectTrigger>
</select> <SelectContent>
{Object.keys(RESOURCE_LIBRARIES).map(store => (
<SelectItem key={store} value={store}>{store}</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label> <label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label>
<select <Select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={formData.type} value={formData.type}
onChange={e => setFormData({ ...formData, type: e.target.value })} onValueChange={value => setFormData({ ...formData, type: value })}
> >
{RESOURCE_LIBRARIES[formData.stores]?.map(type => ( <SelectTrigger className="w-full">
<option key={type} value={type}>{type}</option> <SelectValue placeholder="选择资源类型" />
))} </SelectTrigger>
</select> <SelectContent>
{RESOURCE_LIBRARIES[formData.stores]?.map(type => (
<SelectItem key={type} value={type}>{type}</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4"> <div className="grid grid-cols-[100px_1fr] items-center gap-4">
@ -239,20 +275,42 @@ export default function UploadResource() {
/> />
</div> </div>
<div className="grid grid-cols-[100px_1fr] items-center gap-4">
<label className="text-right text-sm text-slate-500"><span className="text-red-500 mr-1">*</span></label>
<Input
value={formData.createdBy}
readOnly
className="bg-slate-50 cursor-not-allowed"
/>
</div>
<div className="grid grid-cols-[100px_1fr] items-start gap-4"> <div className="grid grid-cols-[100px_1fr] items-start gap-4">
<label className="text-right text-sm text-slate-500 pt-2"><span className="text-red-500 mr-1">*</span></label> <label className="text-right text-sm text-slate-500 pt-2"><span className="text-red-500 mr-1">*</span></label>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{formData.tags.map(tag => ( {formData.tags.map(tag => (
<span key={tag} className="bg-[#0e7490] text-white px-3 py-1 rounded-full text-xs flex items-center gap-1"> <Badge key={tag} className="gap-1 bg-[#0e7490] hover:bg-[#0e7490]">
{tag} {tag}
<button className="hover:text-cyan-200"><X className="w-3 h-3" /></button> <button className="hover:text-cyan-200" onClick={() => {
</span> setFormData({ ...formData, tags: formData.tags.filter(t => t !== tag) })
}}>
<X className="w-3 h-3" />
</button>
</Badge>
))} ))}
{['交互插件', '渲染效果', '数据采集', '虚拟实验室', '教学模板'].map((tag: string) => ( {['交互插件', '渲染效果', '数据采集', '虚拟实验室', '教学模板'].map((tag: string) => (
<button key={tag} className="bg-slate-100 text-slate-600 hover:bg-slate-200 px-3 py-1 rounded-full text-xs transition-colors"> <Badge
key={tag}
variant="secondary"
className="cursor-pointer font-normal border-slate-200"
onClick={() => {
if (!formData.tags.includes(tag)) {
setFormData({ ...formData, tags: [...formData.tags, tag] })
}
}}
>
{tag} {tag}
</button> </Badge>
))} ))}
</div> </div>
<Input placeholder="自定义标签" className="max-w-[200px]" /> <Input placeholder="自定义标签" className="max-w-[200px]" />
@ -313,14 +371,32 @@ export default function UploadResource() {
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-800 text-xs text-slate-500"></h3> <h3 className="text-base font-bold text-slate-800 text-xs text-slate-500"></h3>
<div className="border-2 border-dashed border-slate-200 rounded-lg p-8 flex flex-col items-center justify-center text-center hover:bg-slate-50 transition-colors cursor-pointer h-40"> <div className="border-2 border-dashed border-slate-200 rounded-lg p-8 flex flex-col items-center justify-center text-center hover:bg-slate-50 transition-colors cursor-pointer h-40 relative overflow-hidden group">
<div className="w-10 h-10 bg-slate-100 text-slate-400 rounded-lg flex items-center justify-center mb-2"> <input
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> type="file"
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> accept="image/png, image/jpeg"
</svg> className="absolute inset-0 opacity-0 cursor-pointer z-10"
</div> onChange={handleThumbnailChange}
<p className="text-xs font-medium text-slate-600 mb-1"></p> />
<p className="text-[10px] text-slate-400">JPG/PNG | 800x600px | 5MB</p>
{formData.thumbnailUrl ? (
<div className="absolute inset-0 w-full h-full">
<img src={formData.thumbnailUrl} alt="Thumbnail preview" className="w-full h-full object-contain" />
<div className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<p className="text-white text-sm font-medium"></p>
</div>
</div>
) : (
<>
<div className="w-10 h-10 bg-slate-100 text-slate-400 rounded-lg flex items-center justify-center mb-2">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<p className="text-xs font-medium text-slate-600 mb-1"></p>
<p className="text-[10px] text-slate-400">JPG/PNG | 800x600px | 5MB</p>
</>
)}
</div> </div>
</div> </div>
@ -437,7 +513,11 @@ export default function UploadResource() {
</div> </div>
<div className="grid grid-cols-[80px_1fr] gap-4"> <div className="grid grid-cols-[80px_1fr] gap-4">
<span className="text-slate-500"></span> <span className="text-slate-500"></span>
<span className="text-slate-900">1</span> <span className="text-slate-900 line-clamp-1">{formData.description}</span>
</div>
<div className="grid grid-cols-[80px_1fr] gap-4">
<span className="text-slate-500"></span>
<span className="text-slate-900">{formData.createdBy}</span>
</div> </div>
</div> </div>
@ -454,8 +534,11 @@ export default function UploadResource() {
</ul> </ul>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-start gap-2">
<input type="checkbox" id="agreement" className="rounded border-slate-300 text-[#0e7490] focus:ring-[#0e7490]" /> <Checkbox
id="agreement"
className="mt-1 border-slate-300 data-[state=checked]:bg-[#0e7490] data-[state=checked]:border-[#0e7490]"
/>
<label htmlFor="agreement" className="text-sm text-slate-600"> <label htmlFor="agreement" className="text-sm text-slate-600">
<a href="#" className="text-[#0e7490] underline">仿</a> <a href="#" className="text-[#0e7490] underline"></a> <a href="#" className="text-[#0e7490] underline">仿</a> <a href="#" className="text-[#0e7490] underline"></a>
</label> </label>

View File

@ -17,6 +17,21 @@ import {
} from 'lucide-react' } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip'
// Mock Data for Directory Tree // Mock Data for Directory Tree
const directoryData = [ const directoryData = [
@ -104,14 +119,18 @@ export default function FileLibrary() {
<span className="text-sm truncate font-medium">{node.name}</span> <span className="text-sm truncate font-medium">{node.name}</span>
</div> </div>
{node.tag && ( {node.tag && (
<span className={`text-[10px] px-1.5 py-0.5 rounded shrink-0 ml-2 ${node.tag === '文档库' ? 'bg-blue-100 text-blue-600' : <Badge
node.tag === '公共' ? 'bg-green-100 text-green-600' : variant={
node.tag === '文档' ? 'bg-sky-100 text-sky-600' : node.tag === '文档库' ? 'secondary' :
node.tag === '私有' ? 'bg-orange-100 text-orange-600' : node.tag === '公共' ? 'cyan' :
'bg-purple-100 text-purple-600' node.tag === '文档' ? 'slate' :
}`}> node.tag === '私有' ? 'destructive' :
'default'
}
className="text-[10px] px-1.5 py-0 shrink-0 ml-2 h-4 leading-none font-normal"
>
{node.tag} {node.tag}
</span> </Badge>
)} )}
</div> </div>
{node.children && expandedNodes.includes(node.id) && ( {node.children && expandedNodes.includes(node.id) && (
@ -124,206 +143,184 @@ export default function FileLibrary() {
} }
return ( return (
<div className="flex h-full gap-6 overflow-hidden"> <TooltipProvider>
{/* Left Sidebar: Directory Tree */} <div className="flex h-full gap-6 overflow-hidden">
<div className="w-80 flex flex-col bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm"> {/* Left Sidebar: Directory Tree */}
<div className="p-4 border-b border-slate-100 flex items-center justify-between"> <div className="w-80 flex flex-col bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
<h2 className="font-bold text-slate-800 flex items-center gap-2"> <div className="p-4 border-b border-slate-100 flex items-center justify-between">
<h2 className="text-base font-bold text-slate-800 flex items-center gap-2">
</h2> <Folder className="w-4 h-4 text-[#0e7490]" />
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400">
<Filter className="w-4 h-4" /> </h2>
<span className="sr-only"></span> <Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-slate-600">
</Button> <RefreshCw className="w-4 h-4" />
</div> </Button>
<div className="flex-1 overflow-auto p-2">
{/* Quick Use Templates Section */}
<div className="bg-slate-50/50 rounded-lg p-3 mb-4">
<div className="flex items-center gap-2 text-xs font-semibold text-slate-500 mb-3 ml-1">
<LayoutTemplate className="w-3.5 h-3.5" />
使
</div>
<div className="grid grid-cols-2 gap-2">
{['项目计划书', '需求规格说明书', '用户操作手册', '技术文档', '测试报告', '课程教案'].map((name) => (
<button
key={name}
className="bg-white border border-slate-200 rounded-md py-1.5 px-2 text-[11px] text-slate-600 text-left hover:border-cyan-500 hover:text-cyan-600 transition-colors shadow-sm flex items-center gap-1.5"
>
<FileEdit className="w-3 h-3 text-slate-300" />
<span className="truncate">{name}</span>
</button>
))}
</div>
</div> </div>
<div className="flex-1 overflow-y-auto p-2">
{/* Recursive Tree */}
<div className="-ml-2">
{renderTree(directoryData)} {renderTree(directoryData)}
</div> </div>
</div> <div className="p-3 bg-slate-50 border-t border-slate-100">
</div> <Button className="w-full justify-start gap-2 bg-white text-slate-700 hover:bg-white hover:text-cyan-600 border border-slate-200 shadow-sm" variant="outline">
<Plus className="w-4 h-4" />
{/* Main Content Area */}
<div className="flex-1 flex flex-col gap-6 overflow-hidden">
{/* Top Header/Breadcrumbs and Actions */}
<div className="flex items-center justify-between bg-white rounded-xl border border-slate-200 px-6 py-3 shadow-sm">
<div className="flex items-center gap-1 text-sm font-medium text-slate-600">
<span>仿</span>
<ChevronRight className="w-4 h-4 text-slate-300" />
<span className="text-slate-900">仿</span>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" size="sm" className="gap-2 text-xs h-9">
<Download className="w-3.5 h-3.5 text-slate-500" />
</Button>
<Button variant="outline" size="sm" className="gap-2 text-xs h-9">
<LayoutTemplate className="w-3.5 h-3.5 text-slate-500" />
使
</Button>
<Button variant="ghost" size="sm" className="gap-2 text-xs h-9 text-slate-500">
<RefreshCw className="w-3.5 h-3.5" />
</Button> </Button>
</div> </div>
</div> </div>
{/* Document Preview Card */} {/* Right Content Area */}
<Card className="flex-1 overflow-auto border-slate-200 shadow-sm flex flex-col"> <div className="flex-1 flex flex-col gap-6 overflow-y-auto pr-2 no-scrollbar">
<CardHeader className="py-4 border-b border-slate-100 flex-row items-center justify-between space-y-0"> {/* Upper Header Card */}
<div className="flex items-center gap-3"> <Card className="border-slate-200 shadow-sm overflow-hidden">
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center"> <div className="bg-gradient-to-r from-cyan-600 to-[#0e7490] px-6 py-4 flex items-center justify-between text-white">
<FileText className="w-6 h-6" /> <div className="flex items-center gap-4">
</div> <div className="p-2 bg-white/20 rounded-lg backdrop-blur-sm">
<div> <FileEdit className="w-6 h-6" />
<CardTitle className="text-lg font-bold text-slate-800">仿.md</CardTitle> </div>
</div> <div>
</div> <h1 className="text-xl font-bold">仿</h1>
<div className="flex items-center gap-4 text-xs"> <p className="text-cyan-50 text-xs opacity-80 mt-1">
<button className="text-slate-500 hover:text-cyan-600 flex items-center gap-1.5 transition-colors"> 最后更新: 2024-07-20 14:30 | 编辑人: 张建国
<ExternalLink className="w-3.5 h-3.5" />
</button>
<button className="text-slate-500 hover:text-cyan-600 flex items-center gap-1.5 transition-colors">
<History className="w-3.5 h-3.5" />
</button>
<button className="text-slate-500 hover:text-cyan-600 flex items-center gap-1.5 transition-colors">
<FileDown className="w-3.5 h-3.5" />
PDF
</button>
<button className="text-cyan-600 hover:text-cyan-700 font-bold flex items-center gap-1.5 transition-colors pl-4 border-l border-slate-200">
<FileEdit className="w-3.5 h-3.5" />
</button>
</div>
</CardHeader>
<CardContent className="p-8 prose prose-slate max-w-none prose-headings:text-slate-800 prose-strong:text-slate-900 prose-p:text-slate-600 text-slate-700">
<h1 className="text-3xl font-bold mb-4">仿</h1>
<div className="flex gap-6 text-sm text-slate-500 mb-8 pb-8 border-b border-slate-100">
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-700">:</span> V1.0
</div>
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-700">:</span> 2024-07-20
</div>
<div className="flex items-center gap-1.5">
<span className="font-semibold text-slate-700">:</span>
</div>
</div>
<section className="space-y-6">
<div>
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">1.</h2>
<p>仿Unity引擎开发的虚拟仿真教学平台线</p>
</div>
<div>
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">2.</h2>
<ul className="list-disc pl-5 space-y-2">
<li><strong>:</strong> CPU i5及以上 8GB及以上 GTX1050及以上</li>
<li><strong>:</strong> Windows 10/11 64Unity Runtime 2021.3</li>
<li><strong>:</strong> 1920x1080 </li>
</ul>
</div>
<div>
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">3.</h2>
<div className="space-y-3">
<p>3.1 启动系统:双击桌面快捷方式仿.exe</p>
<p>3.2 选择实验:在主界面左侧实验列表中选择需要进行的光学实验类型</p>
<p>3.3 :
<span className="block pl-4 mt-2 mb-2"> 使</span>
<span className="block pl-4 mb-2"> </span>
<span className="block pl-4 mb-2"> 仿</span>
<span className="block pl-4 mb-2"> </span>
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2">
<div> <Button className="bg-white/10 hover:bg-white/20 text-white border-white/20 border gap-2">
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">4.</h2> <History className="w-4 h-4" />
<p></p>
</Button>
<Button className="bg-white text-[#0e7490] hover:bg-white/90 gap-2 font-bold shadow-md">
<ExternalLink className="w-4 h-4" />
</Button>
</div> </div>
</section>
</CardContent>
</Card>
{/* Bottom Associated Files Table */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
<div className="px-5 py-3 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-bold text-slate-800">
<Folder className="w-4 h-4 text-amber-500 fill-amber-500" />
</div> </div>
<Button variant="ghost" size="sm" className="h-8 text-cyan-600 font-medium hover:bg-cyan-50">
<Plus className="w-4 h-4 mr-1" /> <CardContent className="p-8">
<section className="prose prose-slate max-w-none text-slate-600 space-y-8">
</Button> <div>
</div> <h2 className="text-xl font-bold text-slate-800 flex items-center gap-3 mb-4">
<div className="overflow-x-auto"> <div className="w-1.5 h-6 bg-[#0e7490] rounded-full"></div>
<table className="w-full text-left text-sm"> 1.
<thead> </h2>
<tr className="border-b border-slate-100 bg-white/50"> <p className="leading-relaxed">仿3D光路模拟</p>
<th className="px-6 py-4 font-semibold text-slate-500"></th> </div>
<th className="px-6 py-4 font-semibold text-slate-500"></th>
<th className="px-6 py-4 font-semibold text-slate-500"></th> <div>
<th className="px-6 py-4 font-semibold text-slate-500"></th> <h2 className="text-xl font-bold text-slate-800 flex items-center gap-3 mb-4">
<th className="px-6 py-4 font-semibold text-slate-500 text-center"></th> <div className="w-1.5 h-6 bg-[#0e7490] rounded-full"></div>
</tr> 2.仿
</thead> </h2>
<tbody className="divide-y divide-slate-100"> <div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-50 rounded-lg border border-slate-100">
<h4 className="font-bold text-slate-700 mb-2 flex items-center gap-2">
<LayoutTemplate className="w-4 h-4 text-cyan-600" />
线
</h4>
<p className="text-sm">Ray Tracing算法</p>
</div>
<div className="p-4 bg-slate-50 rounded-lg border border-slate-100">
<h4 className="font-bold text-slate-700 mb-2 flex items-center gap-2">
<RefreshCw className="w-4 h-4 text-cyan-600" />
</h4>
<p className="text-sm"></p>
</div>
</div>
</div>
<div>
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-3 mb-4">
<div className="w-1.5 h-6 bg-[#0e7490] rounded-full"></div>
3.
</h2>
<div className="space-y-3">
<p>3.1 启动系统:双击桌面快捷方式仿.exe</p>
<p>3.2 选择实验:在主界面左侧实验列表中选择需要进行的光学实验类型</p>
<p>3.3 :
<span className="block pl-4 mt-2 mb-2"> 使</span>
<span className="block pl-4 mb-2"> </span>
<span className="block pl-4 mb-2"> 仿</span>
<span className="block pl-4 mb-2"> </span>
</p>
</div>
</div>
<div>
<h2 className="text-xl font-bold text-slate-800 flex items-center gap-3 mb-4">
<div className="w-1.5 h-6 bg-[#0e7490] rounded-full"></div>
4.
</h2>
<p></p>
</div>
</section>
</CardContent>
</Card>
{/* Bottom Associated Files Table */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm mb-6">
<div className="px-5 py-3 bg-slate-100/50 border-b border-slate-200 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-bold text-slate-800">
<Folder className="w-4 h-4 text-amber-500 fill-amber-500" />
</div>
<Button variant="ghost" size="sm" className="h-8 text-[#0e7490] font-medium hover:bg-cyan-50">
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent bg-slate-50/30">
<TableHead className="w-[45%] font-bold text-slate-600"></TableHead>
<TableHead className="font-bold text-slate-600"></TableHead>
<TableHead className="font-bold text-slate-600"></TableHead>
<TableHead className="font-bold text-slate-600"></TableHead>
<TableHead className="text-center font-bold text-slate-600"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{associatedFiles.map((file, idx) => ( {associatedFiles.map((file, idx) => (
<tr key={idx} className="hover:bg-slate-50/80 transition-colors group"> <TableRow key={idx} className="group cursor-default">
<td className="px-6 py-4"> <TableCell>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-slate-100 flex items-center justify-center text-slate-400 group-hover:bg-cyan-100 group-hover:text-cyan-600 transition-colors"> <div className="w-8 h-8 rounded bg-slate-50 flex items-center justify-center text-slate-400 group-hover:bg-[#e0f2fe] group-hover:text-[#0e7490] transition-colors">
<FileText className="w-4 h-4" /> <FileText className="w-4 h-4" />
</div> </div>
<span className="font-medium text-slate-700">{file.name}</span> <span className="font-medium text-slate-700">{file.name}</span>
</div> </div>
</td> </TableCell>
<td className="px-6 py-4 text-slate-500">{file.type}</td> <TableCell>
<td className="px-6 py-4 text-slate-500">{file.size}</td> <Badge variant="slate" className="font-normal">{file.type}</Badge>
<td className="px-6 py-4 text-slate-500">{file.date}</td> </TableCell>
<td className="px-6 py-4"> <TableCell className="text-slate-500">{file.size}</TableCell>
<div className="flex items-center justify-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> <TableCell className="text-slate-500">{file.date}</TableCell>
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-[#0e7490] hover:bg-cyan-50"> <TableCell>
<Download className="w-4 h-4" /> <div className="flex items-center justify-center gap-1 opacity-0 group-hover:opacity-100 transition-all">
</Button> <Tooltip>
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-slate-600"> <TooltipTrigger asChild>
<MoreHorizontal className="w-4 h-4" /> <Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-[#0e7490] hover:bg-cyan-50">
</Button> <Download className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top"></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-slate-600">
<MoreHorizontal className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top"></TooltipContent>
</Tooltip>
</div> </div>
</td> </TableCell>
</tr> </TableRow>
))} ))}
</tbody> </TableBody>
</table> </Table>
</div> </div>
</div> </div>
</div> </div>
</div> </TooltipProvider>
) )
} }

View File

@ -62,7 +62,7 @@ export default defineConfig({
}, },
// 代理仿真资源 API // 代理仿真资源 API
'/zichan-api': { '/zichan-api': {
target: 'http://172.16.1.144:8081', target: 'http://172.16.1.144:8082',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
rewrite: (path) => path.replace(/^\/zichan-api/, ''), rewrite: (path) => path.replace(/^\/zichan-api/, ''),