子仓库提交

This commit is contained in:
2025-11-10 09:19:56 +08:00
parent 62f92213f7
commit 5feb24e4e2
733 changed files with 141413 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
import React from 'react'
import {Navbar1} from '@/components/layouts/Navbar.tsx'
import Page from './SideBar/SideBar'
import './index.css'
function Main() {
return (
<div className = "parent-flex">
<Navbar1></Navbar1>
<div>
<Page ></Page>
</div>
</div>
)
}
export default Main

View File

@@ -0,0 +1,352 @@
'use client';
import { Book, Menu, Sunset, Trees, Zap } from "lucide-react";
import { Sprout, Map, Clipboard, Package, Brain, Droplets, Settings } from 'lucide-react';
import { MessageBell } from './components/MessageBell';
import { UserProfile } from './components/UserProfile';
import { ThemeToggle } from './ThemeToggle';
import { useElementHeight } from '@/hooks/useElementHeight';
import { useViewHeight } from '@/hooks/useViewHeight';
import { usePathname, useRouter } from 'next/navigation';
import { useRef, useEffect, useState } from 'react';
// 注释掉 Accordion 相关导入,因为不再需要二级菜单
// import {
// Accordion,
// AccordionContent,
// AccordionItem,
// AccordionTrigger,
// } from "@/components/ui/accordion";
import { useLayoutStore } from '@/stores/useLayoutStore';
import { Button } from "@/components/ui/button";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
interface MenuItem {
title: string;
url: string;
description?: string;
icon?: React.ReactNode;
items?: MenuItem[];
}
interface Navbar1Props {
navbarData: {
logo: {
url: string;
src: string;
alt: string;
title: string;
};
menu: MenuItem[];
auth: {
login: {
title: string;
url: string;
};
signup: {
title: string;
url: string;
};
};
};
}
const Navbar1 = ({ navbarData }: Navbar1Props) => {
const logo = navbarData.logo
const menu = navbarData.menu
const auth = navbarData.auth
const pathname = usePathname()
const router = useRouter()
const containerStyle = {
maxWidth:"100%",marginLeft:"0px",marginRight:"0px",paddingLeft:"1rem",paddingRight:"0rem"
}
// 检查当前路径是否匹配菜单项
const isMenuActive = (url: string) => {
// 精确匹配
if (pathname === url) return true;
// 检查是否是该菜单下的子路径
if (pathname.startsWith(url + '/')) return true;
return false;
}
// 使用自定义 Hook 计算高度
const { elementRef, updateHeight } = useElementHeight({
immediate: true, // 立即计算高度
onUpdate: (height: number) => {
// 更新 Zustand store 中的状态
const { setNavigatorHeight } = useLayoutStore.getState();
setNavigatorHeight(height);
}
});
// 监听页面高度变化
useViewHeight();
const handleMessageClick = () => {
// 处理消息点击事件,可以跳转到消息中心页面
console.log('Navigate to message center');
};
const handleProfileClick = () => {
// 处理个人中心点击事件
console.log('Navigate to profile page');
};
return (
<section className="py-4" ref={elementRef}>
<div className="container" style = {containerStyle}>
{/* Desktop Menu */}
<nav className="hidden justify-between lg:flex">
<div className="flex items-center gap-6">
{/* Logo */}
<span className="flex items-center gap-2">
<div className="flex items-center gap-3 flex-shrink-0">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center transition-colors">
<Sprout className="w-6 h-6 text-primary-foreground" />
</div>
<div>
<h1 className="text-primary transition-colors" style={{ color: 'var(--primary)' }}></h1>
<p className="text-xs text-muted-foreground">Smart Agriculture Management System</p>
</div>
</div>
</span>
<div className="flex items-center gap-1 overflow-x-auto flex-1 min-w-0" style={{ maxWidth: '70vw' }}>
<style jsx>{`
div::-webkit-scrollbar {
height: 6px;
}
div::-webkit-scrollbar-track {
background: transparent;
}
div::-webkit-scrollbar-thumb {
background-color: #d1d5db;
border-radius: 3px;
}
div::-webkit-scrollbar-thumb:hover {
background-color: #9ca3af;
}
div {
scrollbar-width: thin;
scrollbar-color: #d1d5db transparent;
}
.navigation-menu-container {
max-width: 70vw;
}
`}</style>
<NavigationMenu>
<NavigationMenuList className="flex gap-1 min-w-max">
{menu.map((item) => renderMenuItem(item, isMenuActive, router))}
</NavigationMenuList>
</NavigationMenu>
</div>
</div>
<div className="flex gap-2" style = {{alignItems:"center"}}>
<ThemeToggle />
<MessageBell onMessageClick={handleMessageClick} />
<UserProfile onProfileClick={handleProfileClick} />
</div>
</nav>
{/* Mobile Menu */}
<div className="block lg:hidden">
<div className="flex items-center justify-between">
{/* Logo */}
<a href={logo.url} className="flex items-center gap-2">
<img
src={logo.src}
className="max-h-8 dark:invert"
alt={logo.alt}
/>
</a>
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="icon">
<Menu className="size-4" />
</Button>
</SheetTrigger>
<SheetContent className="overflow-y-auto">
<SheetHeader>
<SheetTitle>
<a href={logo.url} className="flex items-center gap-2">
<img
src={logo.src}
className="max-h-8 dark:invert"
alt={logo.alt}
/>
</a>
</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-6 p-4">
{/* 简化移动端菜单,不再使用 Accordion */}
<div className="flex w-full flex-col gap-4">
{menu.map((item) => renderMobileMenuItem(item, isMenuActive, router))}
</div>
<div className="flex flex-col gap-3">
<div className="flex justify-center">
<ThemeToggle />
</div>
<div className="flex justify-center">
<MessageBell onMessageClick={handleMessageClick} />
</div>
<div className="flex justify-center">
<UserProfile onProfileClick={handleProfileClick} />
</div>
</div>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</div>
</section>
);
};
const renderMenuItem = (item: MenuItem, isMenuActive: (url: string) => boolean, router: any) => {
// 注释掉二级菜单相关代码,项目不需要二级菜单
// if (item.items) {
// return (
// <NavigationMenuItem key={item.title}>
// <NavigationMenuTrigger className="whitespace-nowrap">{item.title}</NavigationMenuTrigger>
// <NavigationMenuContent className="bg-popover text-popover-foreground">
// {item.items.map((subItem) => (
//
// <NavigationMenuLink asChild key={subItem.title} className="w-80">
// <SubMenuLink item={subItem} />
// </NavigationMenuLink>
// ))}
// </NavigationMenuContent>
// </NavigationMenuItem>
// );
// }
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
router.push(item.url);
};
return (
<NavigationMenuItem key={item.title}>
<NavigationMenuLink
href={item.url}
onClick={handleClick}
data-menu-item="true"
data-menu-url={item.url}
className={`
inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium gap-2 whitespace-nowrap relative cursor-pointer
${isMenuActive(item.url)
? 'bg-primary/10'
: 'bg-background hover:bg-muted hover:text-accent-foreground'
}
[&:not([data-active])]:text-foreground
`}
>
{item.icon && (
<span className={`
shrink-0
${isMenuActive(item.url)
? 'text-primary'
: 'text-muted-foreground'
}
hover:text-primary
[&.group-data-[state=open]]:text-muted-foreground
`}>
{item.icon}
</span>
)}
<span className={isMenuActive(item.url) ? 'text-primary' : ''}>
{item.title}
</span>
{isMenuActive(item.url) &&<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-green-600 dark:bg-green-400 rounded-full"></div>}
</NavigationMenuLink>
</NavigationMenuItem>
);
};
const renderMobileMenuItem = (item: MenuItem, isMenuActive: (url: string) => boolean, router: any) => {
// 注释掉移动端二级菜单相关代码
// if (item.items) {
// return (
// <AccordionItem key={item.title} value={item.title} className="border-b-0">
// <AccordionTrigger className="text-md py-0 font-semibold hover:no-underline gap-2">
// {item.icon && <span className="shrink-0">{item.icon}</span>}
// {item.title}
// </AccordionTrigger>
// <AccordionContent className="mt-2">
// {item.items.map((subItem) => (
// <SubMenuLink key={subItem.title} item={subItem} />
// ))}
// </AccordionContent>
// </AccordionItem>
// );
// }
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
router.push(item.url);
};
return (
<div
key={item.title}
onClick={handleClick}
className={`
text-md font-semibold flex items-center gap-2 p-2 rounded-md transition-colors cursor-pointer
${isMenuActive(item.url)
? 'bg-primary/10 text-primary'
: 'hover:bg-muted hover:text-accent-foreground'
}
`}
>
{item.icon && (
<span className={`
shrink-0
${isMenuActive(item.url) ? 'text-primary' : 'text-muted-foreground'}
`}>
{item.icon}
</span>
)}
<span className={isMenuActive(item.url) ? 'text-primary' : ''}>
{item.title}
</span>
</div>
);
};
// 注释掉 SubMenuLink 组件,因为不再需要二级菜单
// const SubMenuLink = ({ item }: { item: MenuItem }) => {
// return (
// <a
// className="hover:bg-muted hover:text-accent-foreground flex min-w-80 select-none flex-row gap-4 rounded-md p-3 leading-none no-underline outline-none transition-colors"
// href={item.url}
// >
// <div className="text-foreground">{item.icon}</div>
// <div>
// <div className="text-sm font-semibold">{item.title}</div>
// {item.description && (
// <p className="text-muted-foreground text-sm leading-snug">
// {item.description}
// </p>
// )}
// </div>
// </a>
// );
// };
export { Navbar1 };

View File

@@ -0,0 +1,113 @@
"use client"
import { ReactNode, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { AppSidebar, AppSidebarProps } from "@/components/app-sidebar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import React from 'react'
export interface SideBarProps {
children: ReactNode
data?: AppSidebarProps['data']
}
export default function SideBar({ children, data }: SideBarProps) {
const pathname = usePathname()
const [breadcrumbItems, setBreadcrumbItems] = useState<Array<{ title: string, url?: string, isPage?: boolean }>>([])
useEffect(() => {
if (!data || !data.navMain) return
const generateBreadcrumb = () => {
const items = [{ title: "首页", url: "/" }]
// 解析当前路径
const pathSegments = pathname.split('/').filter(Boolean)
if (pathSegments.length === 0) {
// 首页
items.push({ title: "首页", isPage: true })
} else if (pathSegments[0] === 'central-config') {
// 中心配置模块
items.push({ title: "中心配置", url: "/central-config" })
if (pathSegments.length === 1) {
// 中心配置主页
items.push({ title: "中心配置", isPage: true })
} else if (pathSegments.length >= 2) {
// 查找匹配的一级菜单
const mainModule = data.navMain.find(item =>
item.url === `/central-config/${pathSegments[1]}`
)
if (mainModule) {
items.push({ title: mainModule.title, url: mainModule.url })
if (pathSegments.length === 2) {
// 一级菜单页面
items.push({ title: mainModule.title, isPage: true })
} else if (pathSegments.length >= 3) {
// 查找匹配的二级菜单
const subModule = mainModule.items.find(item =>
item.url === pathname
)
if (subModule) {
items.push({ title: subModule.title, isPage: true })
}
}
}
}
}
setBreadcrumbItems(items)
}
generateBreadcrumb()
}, [pathname, data])
return (
<SidebarProvider>
<AppSidebar data={data} />
<SidebarInset>
<header className="flex sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
{breadcrumbItems.map((item, index) => (
<React.Fragment key={index}>
{index > 0 && <BreadcrumbSeparator className="hidden md:block" />}
<BreadcrumbItem className="hidden md:block">
{item.isPage ? (
<BreadcrumbPage>{item.title}</BreadcrumbPage>
) : item.url ? (
<BreadcrumbLink href={item.url}>{item.title}</BreadcrumbLink>
) : (
<span>{item.title}</span>
)}
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
</header>
<div className="flex flex-1 flex-col gap-4 p-4">
{children}
</div>
</SidebarInset>
</SidebarProvider>
)
}

View File

@@ -0,0 +1,267 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { LeftSidebar } from './components/LeftSidebar';
import { MainContent } from './components/MainContent';
import { cn } from '@/lib/utils';
// 菜单项数据结构定义
interface NavItem {
title: string;
url: string;
icon: string;
items?: {
title: string;
url: string;
isActive?: boolean;
}[];
}
interface SideBarData {
navMain: NavItem[];
}
interface SideBarOldProps {
children: React.ReactNode;
activePath?: string;
onNavigate?: (path: string) => void;
data?: SideBarData;
}
const defaultSideBarData: SideBarData = {
navMain: [
{
title: "租户管理",
url: "/central-config/tenant",
icon: "🏢",
items: [
{
title: "企业审核",
url: "/central-config/tenant/enterprise-audit",
isActive: false
},
{
title: "审核历史",
url: "/central-config/tenant/audit-history",
isActive: false
},
{
title: "企业信息",
url: "/central-config/tenant/enterprise-info",
isActive: false
},
{
title: "用户管理",
url: "/central-config/tenant/user-management",
isActive: false
}
]
},
{
title: "用户管理",
url: "/central-config/user",
icon: "👥",
items: [
{
title: "员工管理",
url: "/central-config/user/employee",
isActive: false
},
{
title: "角色管理",
url: "/central-config/user/role",
isActive: false
},
{
title: "菜单管理",
url: "/central-config/user/menu",
isActive: false
},
{
title: "权限配置管理",
url: "/central-config/user/permission",
isActive: false
}
]
},
{
title: "系统参数",
url: "/central-config/system",
icon: "🔧",
items: [
{
title: "系统设置",
url: "/central-config/system/settings",
isActive: false
},
{
title: "分类字典",
url: "/central-config/system/category",
isActive: false
},
{
title: "数据字典",
url: "/central-config/system/dictionary",
isActive: false
}
]
},
{
title: "系统监控",
url: "/central-config/monitor",
icon: "📈",
items: [
{
title: "登录日志",
url: "/central-config/monitor/login-log",
isActive: false
},
{
title: "操作日志",
url: "/central-config/monitor/operation-log",
isActive: false
},
{
title: "性能监控",
url: "/central-config/monitor/performance",
isActive: false
},
{
title: "网络日志",
url: "/central-config/monitor/network-log",
isActive: false
}
]
},
{
title: "消息中心",
url: "/central-config/message",
icon: "📨",
items: [
{
title: "消息发送",
url: "/central-config/message/send",
isActive: false
},
{
title: "消息模版",
url: "/central-config/message/template",
isActive: false
},
{
title: "消息日志",
url: "/central-config/message/log",
isActive: false
}
]
}
]
};
export function SideBarOld({
children,
activePath,
onNavigate,
data
}: SideBarOldProps) {
const router = useRouter();
const pathname = usePathname();
const [isMobile, setIsMobile] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
const [currentPath, setCurrentPath] = useState(pathname || activePath || '/machinery/list');
// 使用传入的数据或默认数据
const sidebarData = data || defaultSideBarData;
// 转换为 MenuItem 格式以兼容 LeftSidebar 组件
const menus = sidebarData.navMain.map(item => ({
id: item.url.replace(/\/[^\/]+/g, '').replace('/', '') || item.title.replace(/\s+/g, '-').toLowerCase(),
label: item.title,
icon: item.icon,
children: item.items?.map(child => ({
id: child.url.split('/').pop() || child.title.replace(/\s+/g, '-').toLowerCase(),
label: child.title,
path: child.url,
})),
}));
// 检测是否为移动设备
useEffect(() => {
const checkIsMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
// 监听路由变化,同步当前路径
useEffect(() => {
if (pathname) {
setCurrentPath(pathname);
}
}, [pathname]);
// 移动端时自动展开侧边栏
useEffect(() => {
if (isMobile) {
setIsCollapsed(false);
}
}, [isMobile]);
const handleNavigate = (path: string) => {
setCurrentPath(path);
onNavigate?.(path);
// 使用 Next.js 标准路由跳转
router.push(path);
};
return (
<div className={cn(
"flex h-full bg-background",
"min-h-0" // 确保父容器有正确的高度约束
)}>
{/* 左侧导航栏 - 独立滚动 */}
{!isMobile && (
<div className="sidebarScroll">
<LeftSidebar
menus={menus}
activePath={currentPath}
onNavigate={handleNavigate}
isMobile={isMobile}
isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
/>
</div>
)}
{/* 右侧主内容 - 独立滚动 */}
<div className="flex-1 contentScroll">
<MainContent
isMobile={isMobile}
sidebarOpen={!isCollapsed}
onToggleSidebar={() => setIsCollapsed(!isCollapsed)}
>
{children}
</MainContent>
</div>
{/* 移动端侧边栏 */}
{isMobile && (
<div className="fixed inset-y-0 left-0 z-50 w-64 sidebarScroll">
<LeftSidebar
menus={menus}
activePath={currentPath}
onNavigate={handleNavigate}
isMobile={isMobile}
isCollapsed={false}
onToggleCollapse={() => {}}
/>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,232 @@
'use client';
import { useState, useEffect } from 'react';
import { ChevronDown, ChevronLeft, ChevronRight, Menu, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
interface MenuItem {
id: string;
label: string;
icon?: React.ReactNode;
children?: {
id: string;
label: string;
path?: string;
}[];
}
interface LeftSidebarProps {
menus: MenuItem[];
activePath: string;
onNavigate: (path: string) => void;
isMobile?: boolean;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
}
export function LeftSidebar({
menus,
activePath,
onNavigate,
isMobile = false,
isCollapsed = false,
onToggleCollapse
}: LeftSidebarProps) {
// 初始状态下所有菜单都折叠
const getInitialExpandedMenus = () => {
return new Set<string>();
};
// 检查菜单是否有子菜单被选中或是否应该高亮
const isMenuActive = (menu: MenuItem) => {
// 检查是否有子菜单被选中
if (menu.children?.some(child => child.path === activePath)) {
return true;
}
// 如果当前路径匹配菜单的URL无子菜单的情况
if (activePath && activePath.includes(menu.id)) {
return true;
}
return false;
};
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(getInitialExpandedMenus());
// 不自动展开菜单,由用户手动控制
// 当侧边栏状态改变时,折叠所有菜单
useEffect(() => {
setExpandedMenus(new Set());
}, [isCollapsed]);
return (
<div
className={cn(
"bg-background border-r transition-all duration-300 flex flex-col h-full",
isMobile ? "fixed inset-y-0 left-0 z-50" : "relative",
isCollapsed ? "w-16" : "w-64"
)}
>
{/* 头部 - 缩小高度 */}
<div className="px-2 py-1 border-b">
<div className="flex items-center justify-between">
<h2 className={cn(
"font-medium text-sm transition-all duration-300",
isCollapsed ? "hidden" : "block"
)}>
</h2>
{isMobile ? (
<Button variant="ghost" size="icon" className="h-8 w-8">
<X className="w-4 h-4" />
</Button>
) : (
/* 根据侧边栏状态显示不同按钮 */
<Button
variant="ghost"
size="icon"
onClick={onToggleCollapse}
className="h-8 w-8"
title={isCollapsed ? "展开菜单" : "收起菜单"}
>
{isCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronLeft className="w-4 h-4" />
)}
</Button>
)}
</div>
</div>
{/* 导航菜单 */}
<div className={cn(
"flex-1 overflow-y-auto p-4",
isCollapsed ? "p-2" : "p-4"
)}>
<nav className="space-y-2">
{menus.map((menu) => (
<div key={menu.id}>
{/* 一级菜单 */}
{menu.children ? (
<Collapsible
open={expandedMenus.has(menu.id)}
onOpenChange={(open) => {
if (open) {
setExpandedMenus(prev => new Set(prev).add(menu.id));
} else {
setExpandedMenus(prev => {
const newSet = new Set(prev);
newSet.delete(menu.id);
return newSet;
});
}
}}
>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className={cn(
"w-full justify-between text-sm font-normal group",
isCollapsed ? "justify-center px-2 py-3" : "px-3 py-2",
isMenuActive(menu) && "text-primary"
)}
title={isCollapsed ? menu.label : undefined}
>
<div className="flex items-center gap-2">
{menu.icon && (
<span className={cn(
"flex-shrink-0",
isMenuActive(menu) ? "text-primary" : "text-muted-foreground group-hover:text-primary"
)}>
{menu.icon}
</span>
)}
{!isCollapsed && (
<span>{menu.label}</span>
)}
</div>
{menu.children && (
isCollapsed ? null : (
<ChevronRight
className={cn(
"h-4 w-4 shrink-0 transition-transform duration-200",
expandedMenus.has(menu.id) && "rotate-90"
)}
/>
)
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
{/* 二级菜单 */}
{!isCollapsed && (
<div className="ml-4 mt-1 space-y-1">
{menu.children.map((child) => (
<Button
key={child.id}
variant={activePath === child.path ? "secondary" : "ghost"}
className={cn(
"w-full justify-start text-xs font-normal h-8",
activePath === child.path
? "bg-primary/10 text-primary font-medium hover:bg-primary/10"
: "hover:bg-accent hover:text-foreground"
)}
onClick={() => child.path && onNavigate(child.path)}
>
{child.label}
</Button>
))}
</div>
)}
</CollapsibleContent>
</Collapsible>
) : (
<Button
variant="ghost"
className={cn(
"w-full justify-start text-sm font-normal group",
isCollapsed ? "justify-center px-2 py-3" : "px-3 py-2",
isMenuActive(menu) && "text-primary"
)}
title={isCollapsed ? menu.label : undefined}
>
<div className="flex items-center gap-2">
{menu.icon && (
<span className={cn(
"flex-shrink-0",
isMenuActive(menu) ? "text-primary" : "text-muted-foreground group-hover:text-primary"
)}>
{menu.icon}
</span>
)}
{!isCollapsed && (
<span>{menu.label}</span>
)}
</div>
</Button>
)}
</div>
))}
</nav>
</div>
{/* 底部 */}
<div className="p-4 border-t">
<div className={cn(
"text-xs text-muted-foreground",
isCollapsed ? "text-center" : "text-left"
)}>
{isCollapsed ? "管理" : "管理系统"}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,71 @@
'use client';
import { useState } from 'react';
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
interface MainContentProps {
children: React.ReactNode;
isMobile?: boolean;
sidebarOpen?: boolean;
onToggleSidebar?: () => void;
}
export function MainContent({
children,
isMobile = false,
sidebarOpen = false,
onToggleSidebar,
}: MainContentProps) {
const [showMobileSidebar, setShowMobileSidebar] = useState(false);
const handleToggleSidebar = () => {
if (isMobile) {
setShowMobileSidebar(!showMobileSidebar);
} else {
onToggleSidebar?.();
}
};
if (isMobile) {
return (
<>
{/* 移动端菜单按钮 */}
<div className="flex items-center justify-between p-4 border-b bg-background">
<Button
variant="ghost"
size="icon"
onClick={() => setShowMobileSidebar(true)}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{/* 移动端侧边栏 */}
<Sheet open={showMobileSidebar} onOpenChange={setShowMobileSidebar}>
<SheetContent side="left" className="p-0 w-64">
{/* 这里应该渲染LeftSidebar内容但需要通过props传递 */}
<div className="p-4">
<p className="text-muted-foreground"></p>
</div>
</SheetContent>
</Sheet>
{/* 主内容区域 */}
<div className="flex-1 p-6 bg-background">
{children}
</div>
</>
);
}
return (
<div className="flex-1 flex flex-col bg-background h-full">
<div className="flex-1 p-6 min-h-0 content-container relative">
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,50 @@
'use client';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '../ui/button';
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// 避免服务端渲染和客户端渲染不匹配
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
// 在组件挂载前不渲染,避免闪烁
if (!mounted) {
return (
<Button
variant="ghost"
size="icon"
disabled
className="transition-colors h-10 w-10"
>
<Sun className="size-5" />
</Button>
);
}
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
title={theme === 'light' ? '切换到深色模式' : '切换到浅色模式'}
className="transition-colors h-10 w-10"
>
{theme === 'light' ? (
<Moon className="size-5" />
) : (
<Sun className="size-5" />
)}
</Button>
);
}

View File

@@ -0,0 +1,282 @@
'use client';
import { useState } from 'react';
import { Bell, CheckCircle, X } from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog';
import { ScrollArea } from './ui/scroll-area';
interface Message {
id: string;
title: string;
content: string;
fullContent?: string;
time: string;
read: boolean;
type?: 'info' | 'warning' | 'success' | 'error';
}
interface MessageBellProps {
onMessageClick?: () => void;
}
export function MessageBell({ onMessageClick }: MessageBellProps) {
const [showMessages, setShowMessages] = useState(false);
const [showMessageDetail, setShowMessageDetail] = useState(false);
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
const [messages, setMessages] = useState<Message[]>([
{
id: '1',
title: '系统维护通知',
content: '系统将于今晚22:00进行维护升级',
fullContent: '尊敬的用户:\n\n为了提升系统性能和用户体验我们将于今晚22:00-23:00进行系统维护升级。\n\n维护期间系统将暂时无法访问。维护完成后系统将自动恢复正常。\n\n维护内容\n1. 数据库性能优化\n2. 新功能上线\n3. 安全补丁更新\n\n给您带来的不便敬请谅解。\n\n智慧农业管理系统',
time: '10分钟前',
read: false,
type: 'info',
},
{
id: '2',
title: '作业任务提醒',
content: '小麦播种作业任务已分配',
fullContent: '您好!\n\n新的作业任务已分配给您\n\n任务名称小麦播种作业\n作业地块1号地块东区\n作业面积50亩\n计划时间2024年10月15日 08:00\n负责驾驶员张三\n使用设备约翰迪尔拖拉机 JD-001\n\n请您及时查看任务详情做好作业准备工作。如有问题请及时联系调度中心。\n\n祝工作顺利',
time: '1小时前',
read: false,
type: 'success',
},
{
id: '3',
title: '设备预警',
content: '拖拉机JD-001需要保养维护',
fullContent: '设备预警通知\n\n设备名称约翰迪尔拖拉机\n设备编号JD-001\n当前工作时长498小时\n\n该设备即将达到保养周期500小时建议尽快安排保养维护。\n\n保养项目\n- 更换机油和机滤\n- 检查空气滤清器\n- 检查轮胎气压\n- 检查制动系统\n- 润滑各运动部件\n\n请及时联系维修部门预约保养时间。定期保养可以延长设备使用寿命确保作业安全。\n\n设备管理中心',
time: '2小时前',
read: false,
type: 'warning',
},
{
id: '4',
title: '消息日志通知',
content: '新的消息日志记录已生成',
fullContent: '消息日志通知\n\n系统已记录以下消息日志\n\n1. 短信发送记录\n - 接收人:张三\n - 内容:任务分配通知\n - 状态:发送成功\n - 时间2024-10-14 09:30:00\n\n2. 邮件发送记录\n - 接收人wangwu@example.com\n - 内容:设备保养提醒\n - 状态:发送成功\n - 时间2024-10-14 14:00:00\n\n3. 站内信记录\n - 接收人:李四\n - 内容:系统通知\n - 状态:已读\n - 时间2024-10-14 15:30:00\n\n请查看消息日志页面了解详细信息。',
time: '30分钟前',
read: true,
type: 'info',
},
{
id: '5',
title: '推送消息失败',
content: '部分推送消息发送失败',
fullContent: '推送消息失败通知\n\n以下推送消息发送失败\n\n失败原因设备离线\n- 接收人:赵六\n- 内容:天气预警通知\n- 失败时间2024-10-14 16:00:00\n- 重试次数3次\n\n处理建议\n1. 检查设备网络连接\n2. 确认推送服务状态\n3. 联系用户确认设备状态\n\n系统将在24小时后自动重试发送。\n\n技术支持团队',
time: '45分钟前',
read: false,
type: 'error',
},
]);
const unreadCount = messages.filter(m => !m.read).length;
const handleMessageItemClick = (message: Message) => {
// 标记消息为已读
if (!message.read) {
setMessages(messages.map(m =>
m.id === message.id ? { ...m, read: true } : m
));
}
// 显示消息详情
setSelectedMessage(message);
setShowMessageDetail(true);
setShowMessages(false);
};
const handleMarkAllRead = () => {
setMessages(messages.map(m => ({ ...m, read: true })));
};
const handleViewAllMessages = () => {
setShowMessages(false);
if (onMessageClick) {
onMessageClick();
}
};
const getMessageTypeColor = (type?: string) => {
switch (type) {
case 'warning':
return 'text-orange-600';
case 'error':
return 'text-red-600';
case 'success':
return 'text-green-600';
default:
return 'text-blue-600';
}
};
const getMessageTypeBg = (type?: string) => {
switch (type) {
case 'warning':
return 'bg-orange-50';
case 'error':
return 'bg-red-50';
case 'success':
return 'bg-green-50';
default:
return 'bg-blue-50';
}
};
return (
<>
<Popover open={showMessages} onOpenChange={setShowMessages}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Bell
className="w-5 h-5" />
{unreadCount > 0 && (
<Badge
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 bg-red-500 text-white text-xs"
>
{unreadCount}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-96 p-0" align="end">
<div className="p-4 border-b">
<div className="flex items-center justify-between">
<h4></h4>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleMarkAllRead}
className="text-xs h-7"
>
<CheckCircle className="w-3 h-3 mr-1" />
</Button>
)}
<Badge variant="outline">{unreadCount} </Badge>
</div>
</div>
</div>
<ScrollArea className="max-h-96">
{messages.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
<Bell className="w-12 h-12 mx-auto mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : (
messages.map((msg) => (
<div
key={msg.id}
className="p-4 border-b hover:bg-gray-50 cursor-pointer transition-colors"
onClick={() => handleMessageItemClick(msg)}
>
<div className="flex items-start gap-3">
<div className={`w-2 h-2 rounded-full mt-2 flex-shrink-0 ${msg.read ? 'bg-gray-300' : 'bg-blue-500'}`} />
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<h5 className={`text-sm ${!msg.read ? '' : 'text-muted-foreground'}`}>
{msg.title}
</h5>
{msg.type && (
<Badge
variant="outline"
className={`text-xs ${getMessageTypeColor(msg.type)} border-current`}
>
{msg.type === 'warning' ? '预警' :
msg.type === 'error' ? '错误' :
msg.type === 'success' ? '成功' : '通知'}
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">{msg.content}</p>
<p className="text-xs text-muted-foreground mt-2">{msg.time}</p>
</div>
</div>
</div>
))
)}
</ScrollArea>
<div className="p-2 border-t">
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={handleViewAllMessages}
>
</Button>
</div>
</PopoverContent>
</Popover>
{/* 消息详情对话框 */}
<Dialog open={showMessageDetail} onOpenChange={setShowMessageDetail}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Bell className={`w-5 h-5 ${selectedMessage ? getMessageTypeColor(selectedMessage.type) : ''}`} />
<span>{selectedMessage?.title}</span>
</div>
{selectedMessage?.type && (
<Badge
variant="outline"
className={`${getMessageTypeColor(selectedMessage.type)} border-current`}
>
{selectedMessage.type === 'warning' ? '⚠️ 预警' :
selectedMessage.type === 'error' ? '❌ 错误' :
selectedMessage.type === 'success' ? '✅ 成功' : ' 通知'}
</Badge>
)}
</div>
</DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
{selectedMessage && (
<div className="space-y-4">
<div className={`p-4 rounded-lg ${getMessageTypeBg(selectedMessage.type)}`}>
<div className="flex items-center justify-between mb-2">
<span className={`text-sm ${getMessageTypeColor(selectedMessage.type)}`}>
{selectedMessage.time}
</span>
</div>
</div>
<ScrollArea className="max-h-96">
<div className="space-y-3">
<div className="whitespace-pre-wrap text-sm leading-relaxed">
{selectedMessage.fullContent || selectedMessage.content}
</div>
</div>
</ScrollArea>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setShowMessageDetail(false)}
>
</Button>
<Button
onClick={() => {
setShowMessageDetail(false);
handleViewAllMessages();
}}
>
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,109 @@
'use client';
import { useState } from 'react';
import { User, UserCircle, LogOut } from 'lucide-react';
import { useAuth } from '@/components/auth/AuthContext';
import { toast } from 'sonner';
import { Button } from './ui/button';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
interface UserProfileProps {
onProfileClick?: () => void;
}
export function UserProfile({ onProfileClick }: UserProfileProps) {
const { user, logout } = useAuth();
const [showUserMenu, setShowUserMenu] = useState(false);
const handleProfileClick = () => {
if (onProfileClick) {
onProfileClick();
}
};
const handleLogout = () => {
setShowUserMenu(false);
logout();
toast.success('已安全退出登录');
};
return (
<Popover open={showUserMenu} onOpenChange={setShowUserMenu}>
<PopoverTrigger asChild>
<Button variant="ghost" className="gap-2">
<User className="w-5 h-5" />
<span className="text-sm hidden md:inline">{user?.realName || '用户'}</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-72 p-0" align="end">
<div className="p-4 border-b bg-gradient-to-r from-green-50 to-blue-50">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-green-600 rounded-full flex items-center justify-center text-white">
<UserCircle className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="mb-1">{user?.realName}</h4>
<p className="text-xs text-muted-foreground">{user?.is_superuser ? '系统管理员' : '普通用户'}</p>
</div>
</div>
</div>
<div className="p-2">
<div className="p-3 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{user?.username}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{user?.phone}</span>
</div>
{user?.enterpriseName && (
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="truncate max-w-[140px]" title={user?.enterpriseName}>
{user?.enterpriseName}
</span>
</div>
)}
{user?.department && (
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{user?.department}</span>
</div>
)}
{user?.lastLoginTime && (
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">:</span>
<span className="text-muted-foreground">{user?.lastLoginTime}</span>
</div>
)}
</div>
<div className="border-t pt-2 mt-2">
<Button
variant="ghost"
className="w-full justify-start text-sm"
onClick={() => {
setShowUserMenu(false);
handleProfileClick();
}}
>
<User className="w-4 h-4 mr-2" />
</Button>
<Button
variant="ghost"
className="w-full justify-start text-sm text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => {
setShowUserMenu(false);
handleLogout();
}}
>
<LogOut className="w-4 h-4 mr-2" />
退
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,185 @@
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { toast } from 'sonner';
interface User {
id: string;
username: string;
realName: string;
email?: string;
phone?: string;
role: 'admin' | 'user';
enterpriseName?: string;
department?: string;
lastLoginTime?: string;
}
interface AuthState {
isAuthenticated: boolean;
user: User | null;
token: string | null;
}
interface AuthContextType {
authState: AuthState;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
updateUser: (userData: Partial<User>) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
user: null,
token: null,
});
// 在组件挂载时检查本地存储的认证信息
useEffect(() => {
const token = localStorage.getItem('auth_token');
const userStr = localStorage.getItem('user_info');
if (token && userStr) {
try {
const user = JSON.parse(userStr);
setAuthState({
isAuthenticated: true,
user,
token,
});
} catch (error) {
console.error('Failed to parse user info:', error);
// 清除无效数据
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
// 如果数据无效,自动登录默认用户
autoLoginDefaultUser();
}
} else {
// 如果没有本地存储数据,自动登录默认用户
autoLoginDefaultUser();
}
}, []);
// 自动登录默认用户
const autoLoginDefaultUser = () => {
const defaultUser: User = {
id: '1',
username: 'admin',
realName: '系统管理员',
email: 'admin@smartagriculture.com',
phone: '13800138000',
role: 'admin',
enterpriseName: '智慧农业科技有限公司',
department: '技术部',
lastLoginTime: new Date().toLocaleString('zh-CN'),
};
const token = 'mock-jwt-token-default';
// 保存到本地存储
localStorage.setItem('auth_token', token);
localStorage.setItem('user_info', JSON.stringify(defaultUser));
setAuthState({
isAuthenticated: true,
user: defaultUser,
token,
});
};
const login = async (username: string, password: string): Promise<boolean> => {
try {
// 模拟登录请求
if (username === 'admin' && password === 'admin123') {
const user: User = {
id: '1',
username: 'admin',
realName: '系统管理员',
email: 'admin@smartagriculture.com',
phone: '13800138000',
role: 'admin',
enterpriseName: '智慧农业科技有限公司',
department: '技术部',
lastLoginTime: new Date().toLocaleString('zh-CN'),
};
const token = 'mock-jwt-token-' + Date.now();
// 保存到本地存储
localStorage.setItem('auth_token', token);
localStorage.setItem('user_info', JSON.stringify(user));
setAuthState({
isAuthenticated: true,
user,
token,
});
toast.success('登录成功');
return true;
} else {
toast.error('用户名或密码错误');
return false;
}
} catch (error) {
console.error('Login error:', error);
toast.error('登录失败,请重试');
return false;
}
};
const logout = () => {
// 清除本地存储
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
setAuthState({
isAuthenticated: false,
user: null,
token: null,
});
toast.success('已安全退出');
};
const updateUser = (userData: Partial<User>) => {
if (authState.user) {
const updatedUser = { ...authState.user, ...userData };
// 更新本地存储
localStorage.setItem('user_info', JSON.stringify(updatedUser));
setAuthState(prev => ({
...prev,
user: updatedUser,
}));
}
};
return (
<AuthContext.Provider value={{
authState,
login,
logout,
updateUser,
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

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

View File

@@ -0,0 +1,36 @@
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",
},
},
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,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

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%] sm: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,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,6 @@
.parent-flex {
display: flex;
flex-direction: column;
gap: 1rem; /* 控制子元素间距 */
width: 100%; /* 默认宽度 */
}