no message
This commit is contained in:
parent
271c11b453
commit
93410c55d7
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -20,6 +20,14 @@
|
|||
"author": "",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
"axios": "^1.13.2",
|
||||
"electron-store": "^8.1.0",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import FileLibrary from '@/pages/files/FileLibrary'
|
|||
import { ToastProvider } from '@/components/ui/toast-provider'
|
||||
|
||||
import { DownloadManagerProvider } from '@/components/DownloadManager'
|
||||
import { UserProvider } from '@/contexts/UserContext'
|
||||
|
||||
// 其他页面的占位符,以避免演示期间出现 404
|
||||
const PlaceholderPage = ({ title }: { title: string }) => (
|
||||
|
|
@ -24,6 +25,7 @@ const PlaceholderPage = ({ title }: { title: string }) => (
|
|||
function App() {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<UserProvider>
|
||||
<DownloadManagerProvider>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
|
|
@ -54,6 +56,7 @@ function App() {
|
|||
</Routes>
|
||||
</HashRouter>
|
||||
</DownloadManagerProvider>
|
||||
</UserProvider>
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,29 @@ export interface LoginResponse {
|
|||
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) => {
|
||||
return simClient.post<any, LoginResponse>('/login', data);
|
||||
};
|
||||
|
||||
export const getUserInfo = () => {
|
||||
return simClient.get<any, GetInfoResponse>('/getInfo');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { AddSimulationResourceRequest, AddSimulationResourceResponse, GetSimulat
|
|||
// 开发环境使用 Vite 代理,生产环境使用实际地址
|
||||
export const SIM_API_BASE_URL = import.meta.env.DEV
|
||||
? '/zichan-api'
|
||||
: 'http://172.16.1.144:8081';
|
||||
: 'http://172.16.1.144:8082';
|
||||
|
||||
const simClient = axios.create({
|
||||
baseURL: SIM_API_BASE_URL,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export interface SimulationResource {
|
|||
env: string;
|
||||
createdTime: string;
|
||||
createdBy: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface GetSimulationListRequest {
|
||||
|
|
|
|||
|
|
@ -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 { Input } from '@/components/ui/input'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
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() {
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
const { user, logout } = useUser()
|
||||
|
||||
const handleMinimize = () => {
|
||||
window.electronAPI?.window.minimize()
|
||||
|
|
@ -21,6 +31,7 @@ export function Header() {
|
|||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="h-16 border-b bg-white flex items-center justify-between px-6 shadow-sm z-10 relative drag-region">
|
||||
{/* Left: Logo */}
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -54,75 +65,83 @@ export function Header() {
|
|||
上传资源
|
||||
</Button>
|
||||
|
||||
<div className="h-6 w-px bg-slate-200 mx-2"></div>
|
||||
<Separator orientation="vertical" className="h-6 mx-2" />
|
||||
|
||||
<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')}
|
||||
>
|
||||
<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 className="w-4 h-4 text-slate-400" />
|
||||
{user?.avatar ? <img src={user.avatar} className="w-full h-full object-cover" /> : <User className="w-4 h-4 text-slate-400" />}
|
||||
</div>
|
||||
{/* Removed text as requested */}
|
||||
<span className="text-sm font-medium text-slate-700">{user?.userName || '加载中...'}</span>
|
||||
</div>
|
||||
|
||||
{/* Dropdown Menu - Adjusted position and content */}
|
||||
<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="py-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
localStorage.removeItem('token');
|
||||
</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');
|
||||
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">
|
||||
<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" />
|
||||
</svg>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>退出登录</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Window Controls */}
|
||||
<div className="h-6 w-px bg-slate-200 mx-2"></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}
|
||||
title="最小化"
|
||||
>
|
||||
<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}
|
||||
title="最大化/还原"
|
||||
>
|
||||
<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}
|
||||
title="关闭"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>关闭</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
<input
|
||||
type={type}
|
||||
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
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -13,7 +13,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||
return (
|
||||
<textarea
|
||||
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
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -8,10 +8,12 @@ import bgImage from '@/assets/images/登录背景.png'
|
|||
|
||||
import { login } from '@/api/auth'
|
||||
import { useToast } from '@/components/ui/toast-provider'
|
||||
import { useUser } from '@/contexts/UserContext'
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
const { refreshUser } = useUser()
|
||||
const [username, setUsername] = useState('admin') // Default to admin as per request
|
||||
const [password, setPassword] = useState('123456') // Default password
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
|
@ -29,6 +31,7 @@ export default function Login() {
|
|||
// res structure: { msg: "...", code: 200, data: ..., token: "..." }
|
||||
if (res.code === 200) {
|
||||
localStorage.setItem('token', res.token)
|
||||
await refreshUser()
|
||||
showToast('登录成功', 'success')
|
||||
navigate('/app')
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from 'lucide-react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useUser } from '@/contexts/UserContext'
|
||||
|
||||
// Mock Data
|
||||
const MOCK_RESOURCES = [
|
||||
|
|
@ -55,6 +56,8 @@ export default function ProfilePage() {
|
|||
const [activeTab, setActiveTab] = useState('resources') // info, resources, favorites
|
||||
const [resourceFilter, setResourceFilter] = useState('all') // all, published, draft, auditing
|
||||
|
||||
const { user } = useUser()
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'published':
|
||||
|
|
@ -89,17 +92,17 @@ export default function ProfilePage() {
|
|||
{/* User Profile Card */}
|
||||
<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">
|
||||
{/* Placeholder Avatar */}
|
||||
<User className="w-8 h-8 text-slate-400" />
|
||||
{/* Avatar */}
|
||||
{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">
|
||||
<Edit className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center mb-1">
|
||||
<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>
|
||||
</h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
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 { Card, CardContent, CardFooter } from '@/components/ui/card'
|
||||
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 = [
|
||||
"全部", "地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"
|
||||
]
|
||||
|
||||
import { getSimulationUploadList } from '@/api/simulation'
|
||||
import { SimulationResource } from '@/api/simulationTypes'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function AssetLibrary() {
|
||||
const [activeTab, setActiveTab] = useState("全部")
|
||||
const [assets, setAssets] = useState<SimulationResource[]>([])
|
||||
|
|
@ -27,7 +28,6 @@ export default function AssetLibrary() {
|
|||
const response = await getSimulationUploadList({
|
||||
pageNum: 1,
|
||||
pageSize: 20, // Fetch more initially
|
||||
// If "全部" is selected, don't filter by type (or handle specific types if needed)
|
||||
types: activeTab === "全部" ? undefined : activeTab
|
||||
})
|
||||
|
||||
|
|
@ -46,34 +46,35 @@ export default function AssetLibrary() {
|
|||
}, [activeTab])
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="bg-white rounded-xl shadow-sm min-h-full flex flex-col">
|
||||
{/* Top Tabs & Filter Bar */}
|
||||
<div className="flex items-center justify-between px-6 border-b border-slate-100">
|
||||
<div className="flex items-center gap-14 overflow-x-auto no-scrollbar pl-14 pr-14">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 overflow-x-auto no-scrollbar py-2">
|
||||
<TabsList className="bg-transparent h-auto gap-0 p-0">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
<TabsTrigger
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
value={tab}
|
||||
className={cn(
|
||||
"py-4 text-sm font-medium transition-all relative",
|
||||
activeTab === tab
|
||||
? "text-[#0e7490] font-bold"
|
||||
: "text-slate-500 hover:text-slate-800"
|
||||
"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]",
|
||||
"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}
|
||||
{activeTab === tab && (
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[300%] h-0.5 bg-[#0e7490]" />
|
||||
)}
|
||||
</button>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</div>
|
||||
</TabsList>
|
||||
</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>
|
||||
|
||||
{/* Asset Grid */}
|
||||
<div className="p-6">
|
||||
|
|
@ -96,15 +97,32 @@ export default function AssetLibrary() {
|
|||
|
||||
{/* 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>
|
||||
|
||||
|
|
@ -115,16 +133,16 @@ export default function AssetLibrary() {
|
|||
|
||||
<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">
|
||||
<Badge key={tag} variant="slate" className="font-normal border-slate-100">
|
||||
{tag.trim()}
|
||||
</span>
|
||||
</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.createdBy || 'Unknown'}</span>
|
||||
<span>{asset.remark || asset.createdBy || 'Unknown'}</span>
|
||||
<span className="text-slate-300">|</span>
|
||||
<span>{asset.createdTime ? asset.createdTime.split(' ')[0] : '-'}</span>
|
||||
</div>
|
||||
|
|
@ -139,5 +157,6 @@ export default function AssetLibrary() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { 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 { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { addSimulationResource } from '@/api/simulation'
|
||||
import { AddSimulationResourceRequest, ProUploadItem } from '@/api/simulationTypes'
|
||||
import { ToastContext } from '@/components/ui/toast-provider'
|
||||
import { useUser } from '@/contexts/UserContext'
|
||||
|
||||
const RESOURCE_LIBRARIES: Record<string, string[]> = {
|
||||
'插件库': ["地形", "天气", "特效", "高亮", "视频", "VR", "动画", "前端", "后端", "特殊"],
|
||||
|
|
@ -37,10 +47,21 @@ export default function UploadResource() {
|
|||
urls: 'http://example.com',
|
||||
versions: 'v1.0.0',
|
||||
env: 'Windows 10/Linux',
|
||||
createdBy: 'Admin',
|
||||
createdBy: '',
|
||||
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
|
||||
const [uploadProgress, setUploadProgress] = useState(0)
|
||||
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 () => {
|
||||
try {
|
||||
setIsUploading(true)
|
||||
|
|
@ -200,35 +229,42 @@ export default function UploadResource() {
|
|||
|
||||
<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>
|
||||
<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"
|
||||
<Select
|
||||
value={formData.stores}
|
||||
onChange={e => {
|
||||
const newStore = e.target.value;
|
||||
onValueChange={value => {
|
||||
setFormData({
|
||||
...formData,
|
||||
stores: newStore,
|
||||
type: RESOURCE_LIBRARIES[newStore]?.[0] || ''
|
||||
stores: value,
|
||||
type: RESOURCE_LIBRARIES[value]?.[0] || ''
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="选择资源库" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.keys(RESOURCE_LIBRARIES).map(store => (
|
||||
<option key={store} value={store}>{store}</option>
|
||||
<SelectItem key={store} value={store}>{store}</SelectItem>
|
||||
))}
|
||||
</select>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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>
|
||||
<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"
|
||||
<Select
|
||||
value={formData.type}
|
||||
onChange={e => setFormData({ ...formData, type: e.target.value })}
|
||||
onValueChange={value => setFormData({ ...formData, type: value })}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="选择资源类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{RESOURCE_LIBRARIES[formData.stores]?.map(type => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
<SelectItem key={type} value={type}>{type}</SelectItem>
|
||||
))}
|
||||
</select>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_1fr] items-center gap-4">
|
||||
|
|
@ -239,20 +275,42 @@ export default function UploadResource() {
|
|||
/>
|
||||
</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">
|
||||
<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="flex flex-wrap gap-2">
|
||||
{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}
|
||||
<button className="hover:text-cyan-200"><X className="w-3 h-3" /></button>
|
||||
</span>
|
||||
<button className="hover:text-cyan-200" onClick={() => {
|
||||
setFormData({ ...formData, tags: formData.tags.filter(t => t !== tag) })
|
||||
}}>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
{['交互插件', '渲染效果', '数据采集', '虚拟实验室', '教学模板'].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}
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<Input placeholder="自定义标签" className="max-w-[200px]" />
|
||||
|
|
@ -313,7 +371,23 @@ export default function UploadResource() {
|
|||
|
||||
<div className="space-y-2">
|
||||
<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">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png, image/jpeg"
|
||||
className="absolute inset-0 opacity-0 cursor-pointer z-10"
|
||||
onChange={handleThumbnailChange}
|
||||
/>
|
||||
|
||||
{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" />
|
||||
|
|
@ -321,6 +395,8 @@ export default function UploadResource() {
|
|||
</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>
|
||||
|
||||
|
|
@ -437,7 +513,11 @@ export default function UploadResource() {
|
|||
</div>
|
||||
<div className="grid grid-cols-[80px_1fr] gap-4">
|
||||
<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>
|
||||
|
||||
|
|
@ -454,8 +534,11 @@ export default function UploadResource() {
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="checkbox" id="agreement" className="rounded border-slate-300 text-[#0e7490] focus:ring-[#0e7490]" />
|
||||
<div className="flex items-start gap-2">
|
||||
<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">
|
||||
我已阅读并同意 <a href="#" className="text-[#0e7490] underline">《虚拟仿真课程资源管理库用户协议》</a> 和 <a href="#" className="text-[#0e7490] underline">《资源上传规范》</a>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,21 @@ import {
|
|||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
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
|
||||
const directoryData = [
|
||||
|
|
@ -104,14 +119,18 @@ export default function FileLibrary() {
|
|||
<span className="text-sm truncate font-medium">{node.name}</span>
|
||||
</div>
|
||||
{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' :
|
||||
node.tag === '公共' ? 'bg-green-100 text-green-600' :
|
||||
node.tag === '文档' ? 'bg-sky-100 text-sky-600' :
|
||||
node.tag === '私有' ? 'bg-orange-100 text-orange-600' :
|
||||
'bg-purple-100 text-purple-600'
|
||||
}`}>
|
||||
<Badge
|
||||
variant={
|
||||
node.tag === '文档库' ? 'secondary' :
|
||||
node.tag === '公共' ? 'cyan' :
|
||||
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}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{node.children && expandedNodes.includes(node.id) && (
|
||||
|
|
@ -124,132 +143,96 @@ export default function FileLibrary() {
|
|||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex h-full gap-6 overflow-hidden">
|
||||
{/* Left Sidebar: Directory Tree */}
|
||||
<div className="w-80 flex flex-col bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
|
||||
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||||
<h2 className="font-bold text-slate-800 flex items-center gap-2">
|
||||
文档目录
|
||||
<h2 className="text-base font-bold text-slate-800 flex items-center gap-2">
|
||||
<Folder className="w-4 h-4 text-[#0e7490]" />
|
||||
目录结构
|
||||
</h2>
|
||||
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400">
|
||||
<Filter className="w-4 h-4" />
|
||||
<span className="sr-only">筛选</span>
|
||||
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-slate-600">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{/* Recursive Tree */}
|
||||
<div className="-ml-2">
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{renderTree(directoryData)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
刷新
|
||||
<div className="p-3 bg-slate-50 border-t border-slate-100">
|
||||
<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" />
|
||||
新建分类
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document Preview Card */}
|
||||
<Card className="flex-1 overflow-auto border-slate-200 shadow-sm flex flex-col">
|
||||
<CardHeader className="py-4 border-b border-slate-100 flex-row items-center justify-between space-y-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-6 h-6" />
|
||||
{/* Right Content Area */}
|
||||
<div className="flex-1 flex flex-col gap-6 overflow-y-auto pr-2 no-scrollbar">
|
||||
{/* Upper Header Card */}
|
||||
<Card className="border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-cyan-600 to-[#0e7490] px-6 py-4 flex items-center justify-between text-white">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 bg-white/20 rounded-lg backdrop-blur-sm">
|
||||
<FileEdit className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg font-bold text-slate-800">光学实验仿真系统用户手册.md</CardTitle>
|
||||
<h1 className="text-xl font-bold">光学实验仿真文档</h1>
|
||||
<p className="text-cyan-50 text-xs opacity-80 mt-1">
|
||||
最后更新: 2024-07-20 14:30 | 编辑人: 张建国
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
<button className="text-slate-500 hover:text-cyan-600 flex items-center gap-1.5 transition-colors">
|
||||
<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 className="flex items-center gap-2">
|
||||
<Button className="bg-white/10 hover:bg-white/20 text-white border-white/20 border gap-2">
|
||||
<History className="w-4 h-4" />
|
||||
版本记录
|
||||
</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 className="space-y-6">
|
||||
<CardContent className="p-8">
|
||||
<section className="prose prose-slate max-w-none text-slate-600 space-y-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">1.系统概述</h2>
|
||||
<p>光学实验仿真系统是基于Unity引擎开发的虚拟仿真教学平台,旨在为高校物理实验教学提供沉浸式、交互式的光学实验体验。系统涵盖了几何光学、物理光学等多个实验模块,支持光线追踪、折射计算、干涉衍射模拟等核心功能。</p>
|
||||
<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>
|
||||
1.项目背景与目标
|
||||
</h2>
|
||||
<p className="leading-relaxed">本课程文档主要针对“光学实验虚拟仿真系统”的设计与实现进行详细说明。目标是通过高度真实的3D光路模拟,帮助学生在进入实验室前掌握基本的光学调试技巧,减少昂贵精密器材的误操作损坏风险。</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 64位操作系统,Unity Runtime 2021.3</li>
|
||||
<li><strong>屏幕分辨率:</strong> 建议 1920x1080 及以上</li>
|
||||
</ul>
|
||||
<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>
|
||||
2.仿真原理说明
|
||||
</h2>
|
||||
<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 flex items-center gap-3 mb-4">3.快速上手</h2>
|
||||
<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>
|
||||
|
|
@ -263,7 +246,10 @@ export default function FileLibrary() {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold flex items-center gap-3 mb-4">4.核心功能模块</h2>
|
||||
<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>
|
||||
|
|
@ -271,59 +257,70 @@ export default function FileLibrary() {
|
|||
</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="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-cyan-600 font-medium hover:bg-cyan-50">
|
||||
<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>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100 bg-white/50">
|
||||
<th className="px-6 py-4 font-semibold text-slate-500">文件名</th>
|
||||
<th className="px-6 py-4 font-semibold text-slate-500">文件类型</th>
|
||||
<th className="px-6 py-4 font-semibold text-slate-500">大小</th>
|
||||
<th className="px-6 py-4 font-semibold text-slate-500">最后修改</th>
|
||||
<th className="px-6 py-4 font-semibold text-slate-500 text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
<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) => (
|
||||
<tr key={idx} className="hover:bg-slate-50/80 transition-colors group">
|
||||
<td className="px-6 py-4">
|
||||
<TableRow key={idx} className="group cursor-default">
|
||||
<TableCell>
|
||||
<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" />
|
||||
</div>
|
||||
<span className="font-medium text-slate-700">{file.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-slate-500">{file.type}</td>
|
||||
<td className="px-6 py-4 text-slate-500">{file.size}</td>
|
||||
<td className="px-6 py-4 text-slate-500">{file.date}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center justify-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="slate" className="font-normal">{file.type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-500">{file.size}</TableCell>
|
||||
<TableCell className="text-slate-500">{file.date}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center justify-center gap-1 opacity-0 group-hover:opacity-100 transition-all">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="w-8 h-8 text-slate-400 hover:text-[#0e7490] hover:bg-cyan-50">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default defineConfig({
|
|||
},
|
||||
// 代理仿真资源 API
|
||||
'/zichan-api': {
|
||||
target: 'http://172.16.1.144:8081',
|
||||
target: 'http://172.16.1.144:8082',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/zichan-api/, ''),
|
||||
|
|
|
|||
Loading…
Reference in New Issue