diff --git a/crop-x/next.config.js b/crop-x/next.config.js
new file mode 100644
index 0000000..7914547
--- /dev/null
+++ b/crop-x/next.config.js
@@ -0,0 +1,22 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ typescript: {
+ ignoreBuildErrors: false,
+ },
+ eslint: {
+ ignoreDuringBuilds: false,
+ },
+ experimental: {
+ turbo: {
+ rules: {
+ '*.svg': {
+ loaders: ['@svgr/webpack'],
+ as: '*.js',
+ },
+ },
+ },
+ },
+ transpilePackages: ['lucide-react'],
+}
+
+export default nextConfig
\ No newline at end of file
diff --git a/crop-x/package-lock.json b/crop-x/package-lock.json
index 7598c96..2def14b 100644
--- a/crop-x/package-lock.json
+++ b/crop-x/package-lock.json
@@ -56,7 +56,8 @@
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
- "zod": "^4.1.12"
+ "zod": "^4.1.12",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
@@ -3727,7 +3728,6 @@
"integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -3745,7 +3745,6 @@
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -3757,7 +3756,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -3798,7 +3796,6 @@
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
@@ -4018,7 +4015,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4214,7 +4210,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -4727,8 +4722,7 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@@ -4856,7 +4850,6 @@
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8680,7 +8673,6 @@
"dev": true,
"inBundle": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -9089,7 +9081,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -9202,7 +9193,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9233,7 +9223,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9246,7 +9235,6 @@
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.65.0.tgz",
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9827,8 +9815,7 @@
"version": "4.1.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -9915,7 +9902,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -9974,7 +9960,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10124,7 +10109,6 @@
"integrity": "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -10218,7 +10202,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -10475,6 +10458,35 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.8.tgz",
+ "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/crop-x/package.json b/crop-x/package.json
index 6c0c60f..5990372 100644
--- a/crop-x/package.json
+++ b/crop-x/package.json
@@ -66,7 +66,8 @@
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
- "zod": "^4.1.12"
+ "zod": "^4.1.12",
+ "zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
diff --git a/crop-x/src/app/(app)/central-config/layout.tsx b/crop-x/src/app/(app)/central-config/layout.tsx
index 91ffe44..e8d0b68 100644
--- a/crop-x/src/app/(app)/central-config/layout.tsx
+++ b/crop-x/src/app/(app)/central-config/layout.tsx
@@ -1,10 +1,9 @@
"use client"
import { ReactNode } from 'react'
-import SideBar from '@/components/layouts/SideBar/SideBar'
+import {SideBarOld} from '@/components/layouts/SideBar/SideBarOld'
// 中心配置路由数据
const centralConfigData = {
- versions: ["1.0.0", "2.0.0"],
navMain: [
{
title: "租户管理",
@@ -139,5 +138,5 @@ export default function CentralConfigLayout({
}: {
children: ReactNode
}) {
- return {children}
+ return {children}
}
\ No newline at end of file
diff --git a/crop-x/src/app/(app)/layout.tsx b/crop-x/src/app/(app)/layout.tsx
index 711d823..6b74acb 100644
--- a/crop-x/src/app/(app)/layout.tsx
+++ b/crop-x/src/app/(app)/layout.tsx
@@ -1,4 +1,4 @@
-import {Navbar} from "@/components/layouts/Navbar"
+import {Navbar1} from "@/components/layouts/NavBar"
import '@/styles/globals.css'
export default function DashboardLayout({
children,
@@ -7,7 +7,7 @@ export default function DashboardLayout({
}) {
return (
-
+
{/* 布局 UI */}
{/* 将 children 放在您希望渲染页面或嵌套布局的位置 */}
{children}
diff --git a/crop-x/src/components/app-sidebar.tsx b/crop-x/src/components/app-sidebar.tsx
index b5c2a77..9add399 100644
--- a/crop-x/src/components/app-sidebar.tsx
+++ b/crop-x/src/components/app-sidebar.tsx
@@ -21,7 +21,7 @@ import {
SidebarMenuItem,
SidebarRail,
} from "@/components/ui/sidebar"
-
+import { useLayoutStore } from '@/stores/useLayoutStore';
// Define the interface for menu data
interface MenuItem {
title: string
@@ -110,17 +110,10 @@ export interface AppSidebarProps extends React.ComponentProps
{
export function AppSidebar({ data, ...props }: AppSidebarProps) {
// Use external data if provided, otherwise use default data
const sidebarData = data || defaultData
-
+ const { navigatorHeight } = useLayoutStore();
return (
-
-
-
-
-
-
+
+
{/* We create a collapsible SidebarGroup for each parent. */}
{sidebarData.navMain.map((item) => (
{
+ const logo = navbarData.logo
+ const menu = navbarData.menu
+ const auth = navbarData.auth
+ const containerStyle = {
+ maxWidth:"100%",marginLeft:"0px",marginRight:"0px",paddingLeft:"1rem",paddingRight:"0rem"
+ }
-// 新的Navbar组件,支持外部传入navbar参数
-export function Navbar({ navbar }: NavbarProps) {
- // 使用外部传入的navbar数据,如果没有则使用默认数据
- const navbarData = navbar || defaultNavbar;
- const logo = navbarData.logo || defaultNavbar.logo;
- const menu = navbarData.menu || defaultNavbar.menu;
- const auth = navbarData.auth || defaultNavbar.auth;
+ // 使用自定义 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 (
-
-
+
+
+
{/* Desktop Menu */}
+
);
-}
-
+};
+
const renderMenuItem = (item: MenuItem) => {
if (item.items) {
return (
- {item.title}
+ {item.title}
{item.items.map((subItem) => (
+
@@ -243,9 +280,9 @@ const renderMenuItem = (item: MenuItem) => {
- {item.icon}
+ {item.icon && {item.icon}}
{item.title}
@@ -256,7 +293,8 @@ const renderMobileMenuItem = (item: MenuItem) => {
if (item.items) {
return (
-
+
+ {item.icon && {item.icon}}
{item.title}
@@ -269,7 +307,8 @@ const renderMobileMenuItem = (item: MenuItem) => {
}
return (
-
+
+ {item.icon && {item.icon}}
{item.title}
);
@@ -293,3 +332,5 @@ const SubMenuLink = ({ item }: { item: MenuItem }) => {
);
};
+
+export { Navbar1 };
diff --git a/crop-x/src/components/layouts/SideBar/SideBar.tsx b/crop-x/src/components/layouts/SideBar/SideBar.tsx
index 96da639..89a254f 100644
--- a/crop-x/src/components/layouts/SideBar/SideBar.tsx
+++ b/crop-x/src/components/layouts/SideBar/SideBar.tsx
@@ -1,7 +1,7 @@
"use client"
import { ReactNode, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
-import { AppSidebar, AppSidebarProps, SidebarData } from "@/components/app-sidebar"
+import { AppSidebar, AppSidebarProps } from "@/components/app-sidebar"
import {
Breadcrumb,
diff --git a/crop-x/src/components/layouts/SideBar/SideBarOld.tsx b/crop-x/src/components/layouts/SideBar/SideBarOld.tsx
new file mode 100644
index 0000000..7327f68
--- /dev/null
+++ b/crop-x/src/components/layouts/SideBar/SideBarOld.tsx
@@ -0,0 +1,276 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useRouter, usePathname } from 'next/navigation';
+import { LeftSidebar } from './components/LeftSidebar';
+import { MainContent } from './components/MainContent';
+
+// 菜单项数据结构定义
+interface NavItem {
+ title: string;
+ url: string;
+ icon: string;
+ items?: {
+ title: string;
+ url: string;
+ isActive?: boolean;
+ }[];
+}
+
+interface SideBarData {
+ navMain: NavItem[];
+}
+
+// 内部菜单项结构(用于LeftSidebar组件)
+interface MenuItem {
+ id: string;
+ label: string;
+ icon?: React.ReactNode;
+ children?: {
+ id: string;
+ label: string;
+ path?: string;
+ }[];
+}
+
+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
+ }
+ ]
+ }
+ ]
+};
+
+// 转换 SideBarData 为 MenuItem 格式的工具函数
+const convertSideBarDataToMenus = (sideBarData?: SideBarData): MenuItem[] => {
+ if (!sideBarData?.navMain) return [];
+
+ return 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,
+ })),
+ }));
+};
+
+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 menus = convertSideBarDataToMenus(data || defaultSideBarData);
+
+ // 检测是否为移动设备
+ 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);
+ };
+
+ // 获取当前页面的面包屑
+ const getCurrentBreadcrumb = () => {
+ const allItems: { label: string; path?: string }[] = [];
+
+ menus.forEach(menu => {
+ if (menu.children?.some(child => child.path === currentPath)) {
+ allItems.push({ label: menu.label });
+ const activeChild = menu.children.find(child => child.path === currentPath);
+ if (activeChild) {
+ allItems.push({ label: activeChild.label });
+ }
+ }
+ });
+
+ return allItems;
+ };
+
+ return (
+
+ {/* 左侧导航栏 */}
+ setIsCollapsed(!isCollapsed)}
+ />
+
+ {/* 右侧主内容 */}
+ setIsCollapsed(!isCollapsed)}
+ >
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/SideBar/components/LeftSidebar.tsx b/crop-x/src/components/layouts/SideBar/components/LeftSidebar.tsx
new file mode 100644
index 0000000..1ff44b5
--- /dev/null
+++ b/crop-x/src/components/layouts/SideBar/components/LeftSidebar.tsx
@@ -0,0 +1,179 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { ChevronDown, ChevronRight, Menu, X } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+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) {
+ // 根据activePath自动展开包含该路径的菜单
+ const getInitialExpandedMenus = () => {
+ const expanded = new Set();
+ menus.forEach(menu => {
+ if (menu.children?.some(child => child.path === activePath)) {
+ expanded.add(menu.id);
+ }
+ });
+ // 如果没有匹配的,默认展开第一个
+ if (expanded.size === 0 && menus.length > 0) {
+ expanded.add(menus[0].id);
+ }
+ return expanded;
+ };
+
+ const [expandedMenus, setExpandedMenus] = useState>(getInitialExpandedMenus());
+
+ // 当activePath或menus变化时,自动展开对应的菜单
+ useEffect(() => {
+ menus.forEach(menu => {
+ if (menu.children?.some(child => child.path === activePath)) {
+ setExpandedMenus(prev => {
+ const newSet = new Set(prev);
+ newSet.add(menu.id);
+ return newSet;
+ });
+ }
+ });
+ }, [activePath, menus]);
+
+ const toggleMenu = (menuId: string) => {
+ setExpandedMenus(prev => {
+ const newSet = new Set(prev);
+ if (newSet.has(menuId)) {
+ newSet.delete(menuId);
+ } else {
+ newSet.add(menuId);
+ }
+ return newSet;
+ });
+ };
+
+ return (
+
+ {/* 头部 */}
+
+
+
+ 导航菜单
+
+ {isMobile ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* 导航菜单 */}
+
+
+
+
+ {/* 底部 */}
+
+
+ {isCollapsed ? "管理" : "管理系统"}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/SideBar/components/MainContent.tsx b/crop-x/src/components/layouts/SideBar/components/MainContent.tsx
new file mode 100644
index 0000000..c7df2fa
--- /dev/null
+++ b/crop-x/src/components/layouts/SideBar/components/MainContent.tsx
@@ -0,0 +1,112 @@
+'use client';
+
+import { useState } from 'react';
+import { Menu, X, ChevronRight, Home, FileText, Settings } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface MainContentProps {
+ title?: string;
+ children: React.ReactNode;
+ isMobile?: boolean;
+ sidebarOpen?: boolean;
+ onToggleSidebar?: () => void;
+ breadcrumb?: {
+ label: string;
+ path?: string;
+ }[];
+}
+
+export function MainContent({
+ title = "当前页面",
+ children,
+ isMobile = false,
+ sidebarOpen = false,
+ onToggleSidebar,
+ breadcrumb = []
+}: MainContentProps) {
+ const [showMobileSidebar, setShowMobileSidebar] = useState(false);
+
+ const handleToggleSidebar = () => {
+ if (isMobile) {
+ setShowMobileSidebar(!showMobileSidebar);
+ } else {
+ onToggleSidebar?.();
+ }
+ };
+
+ return (
+ <>
+ {/* 移动端侧边栏遮罩 */}
+ {isMobile && showMobileSidebar && (
+ setShowMobileSidebar(false)}
+ />
+ )}
+
+ {/* 主内容区域 */}
+
+ {/* 顶部导航栏 */}
+
+
+
+ {/* 菜单按钮 */}
+
+
+ {/* 面包屑导航 */}
+
+
+ {breadcrumb.length > 0 ? (
+ breadcrumb.map((item, index) => (
+
+ ))
+ ) : (
+
{title}
+ )}
+
+
+
+
+
+
+ {/* 主内容区域 */}
+
+
+
+ {/* 页面内容 */}
+
+ {children}
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/SideBar/components/lib/utils.ts b/crop-x/src/components/layouts/SideBar/components/lib/utils.ts
new file mode 100644
index 0000000..1a860ee
--- /dev/null
+++ b/crop-x/src/components/layouts/SideBar/components/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/MessageBell.tsx b/crop-x/src/components/layouts/components/MessageBell.tsx
new file mode 100644
index 0000000..b3e2cdc
--- /dev/null
+++ b/crop-x/src/components/layouts/components/MessageBell.tsx
@@ -0,0 +1,283 @@
+'use client';
+
+import { useState } from 'react';
+import { Bell, CheckCircle, X } from 'lucide-react';
+import { useAuth } from './auth/AuthContext';
+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 { authState } = useAuth();
+ const [showMessages, setShowMessages] = useState(false);
+ const [showMessageDetail, setShowMessageDetail] = useState(false);
+ const [selectedMessage, setSelectedMessage] = useState
(null);
+ const [messages, setMessages] = useState([
+ {
+ 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 (
+ <>
+
+
+
+
+
+
+
+
消息通知
+
+ {unreadCount > 0 && (
+
+ )}
+ {unreadCount} 条未读
+
+
+
+
+ {messages.length === 0 ? (
+
+ ) : (
+ messages.map((msg) => (
+ handleMessageItemClick(msg)}
+ >
+
+
+
+
+
+ {msg.title}
+
+ {msg.type && (
+
+ {msg.type === 'warning' ? '预警' :
+ msg.type === 'error' ? '错误' :
+ msg.type === 'success' ? '成功' : '通知'}
+
+ )}
+
+
{msg.content}
+
{msg.time}
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+ {/* 消息详情对话框 */}
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/UserProfile.tsx b/crop-x/src/components/layouts/components/UserProfile.tsx
new file mode 100644
index 0000000..a844c3f
--- /dev/null
+++ b/crop-x/src/components/layouts/components/UserProfile.tsx
@@ -0,0 +1,109 @@
+'use client';
+
+import { useState } from 'react';
+import { User, UserCircle, LogOut } from 'lucide-react';
+import { useAuth } from './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 { authState, logout } = useAuth();
+ const [showUserMenu, setShowUserMenu] = useState(false);
+
+ const handleProfileClick = () => {
+ if (onProfileClick) {
+ onProfileClick();
+ }
+ };
+
+ const handleLogout = () => {
+ setShowUserMenu(false);
+ logout();
+ toast.success('已安全退出登录');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
{authState.user?.realName}
+
{authState.user?.role === 'admin' ? '系统管理员' : '普通用户'}
+
+
+
+
+
+
+ 用户名:
+ {authState.user?.username}
+
+
+ 手机号:
+ {authState.user?.phone}
+
+ {authState.user?.enterpriseName && (
+
+ 所属企业:
+
+ {authState.user?.enterpriseName}
+
+
+ )}
+ {authState.user?.department && (
+
+ 部门:
+ {authState.user?.department}
+
+ )}
+ {authState.user?.lastLoginTime && (
+
+ 上次登录:
+ {authState.user?.lastLoginTime}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/auth/AuthContext.tsx b/crop-x/src/components/layouts/components/auth/AuthContext.tsx
new file mode 100644
index 0000000..97efb45
--- /dev/null
+++ b/crop-x/src/components/layouts/components/auth/AuthContext.tsx
@@ -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;
+ logout: () => void;
+ updateUser: (userData: Partial) => void;
+}
+
+const AuthContext = createContext(undefined);
+
+interface AuthProviderProps {
+ children: ReactNode;
+}
+
+export function AuthProvider({ children }: AuthProviderProps) {
+ const [authState, setAuthState] = useState({
+ 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 => {
+ 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) => {
+ if (authState.user) {
+ const updatedUser = { ...authState.user, ...userData };
+
+ // 更新本地存储
+ localStorage.setItem('user_info', JSON.stringify(updatedUser));
+
+ setAuthState(prev => ({
+ ...prev,
+ user: updatedUser,
+ }));
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/lib/utils.ts b/crop-x/src/components/layouts/components/lib/utils.ts
new file mode 100644
index 0000000..1a860ee
--- /dev/null
+++ b/crop-x/src/components/layouts/components/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/ui/badge.tsx b/crop-x/src/components/layouts/components/ui/badge.tsx
new file mode 100644
index 0000000..12daad7
--- /dev/null
+++ b/crop-x/src/components/layouts/components/ui/badge.tsx
@@ -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,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/ui/button.tsx b/crop-x/src/components/layouts/components/ui/button.tsx
new file mode 100644
index 0000000..c9c71de
--- /dev/null
+++ b/crop-x/src/components/layouts/components/ui/button.tsx
@@ -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,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/ui/dialog.tsx b/crop-x/src/components/layouts/components/ui/dialog.tsx
new file mode 100644
index 0000000..dd48c69
--- /dev/null
+++ b/crop-x/src/components/layouts/components/ui/dialog.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/ui/popover.tsx b/crop-x/src/components/layouts/components/ui/popover.tsx
new file mode 100644
index 0000000..c5e7a64
--- /dev/null
+++ b/crop-x/src/components/layouts/components/ui/popover.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
\ No newline at end of file
diff --git a/crop-x/src/components/layouts/components/ui/scroll-area.tsx b/crop-x/src/components/layouts/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..6251548
--- /dev/null
+++ b/crop-x/src/components/layouts/components/ui/scroll-area.tsx
@@ -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,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
\ No newline at end of file
diff --git a/crop-x/src/components/ui/sidebar.tsx b/crop-x/src/components/ui/sidebar.tsx
index 7113452..1d32d0b 100644
--- a/crop-x/src/components/ui/sidebar.tsx
+++ b/crop-x/src/components/ui/sidebar.tsx
@@ -153,8 +153,8 @@ function SidebarProvider({
function Sidebar({
side = "left",
- variant = "sidebar",
- collapsible = "none",
+ variant = "inset",
+ collapsible = "offcanvas",
className,
children,
...props
diff --git a/crop-x/src/hooks/useElementHeight.ts b/crop-x/src/hooks/useElementHeight.ts
new file mode 100644
index 0000000..6e2276b
--- /dev/null
+++ b/crop-x/src/hooks/useElementHeight.ts
@@ -0,0 +1,101 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+import { useLayoutStore } from '@/stores/useLayoutStore';
+
+interface UseElementHeightOptions {
+ onUpdate?: (height: number) => void;
+ immediate?: boolean; // 是否立即计算
+}
+
+export const useElementHeight = (options: UseElementHeightOptions = {}) => {
+ const elementRef = useRef(null);
+ const [isClient, setIsClient] = useState(false);
+ const { setNavigatorHeight } = useLayoutStore();
+ const lastHeightRef = useRef(0);
+
+ // 确保在客户端执行
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ // 使用 useCallback 来避免无限循环
+ const calculateHeight = useCallback(() => {
+ if (!elementRef.current || !isClient) return 0;
+
+ const height = elementRef.current.offsetHeight;
+
+ // 只有当高度真正发生变化时才更新
+ if (Math.abs(height - lastHeightRef.current) > 1) { // 允许1px的误差避免微小变化
+ lastHeightRef.current = height;
+ setNavigatorHeight(height);
+
+ // 调用自定义回调
+ if (options.onUpdate) {
+ options.onUpdate(height);
+ }
+ }
+
+ return height;
+ }, [isClient, setNavigatorHeight, options.onUpdate]);
+
+ // 手动更新高度的函数
+ const updateHeight = useCallback(() => {
+ return calculateHeight();
+ }, [calculateHeight]);
+
+ useEffect(() => {
+ if (!isClient) return;
+
+ const element = elementRef.current;
+ if (!element) return;
+
+ // 立即计算一次(如果需要)
+ if (options.immediate) {
+ // 使用 setTimeout 来避免在渲染过程中立即调用
+ setTimeout(() => {
+ calculateHeight();
+ }, 0);
+ }
+
+ // 使用防抖来优化性能
+ let debounceTimer: NodeJS.Timeout;
+ const debouncedCalculateHeight = () => {
+ clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(calculateHeight, 100); // 100ms 防抖
+ };
+
+ // 创建 ResizeObserver 来监听元素大小变化
+ const resizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ const { height } = entry.contentRect;
+ if (Math.abs(height - lastHeightRef.current) > 1) {
+ debouncedCalculateHeight();
+ }
+ }
+ });
+
+ // 开始观察元素
+ resizeObserver.observe(element);
+
+ // 监听窗口大小变化
+ const handleResize = () => {
+ debouncedCalculateHeight();
+ };
+
+ window.addEventListener('resize', handleResize, { passive: true });
+ window.addEventListener('orientationchange', handleResize, { passive: true });
+
+ // 清理函数
+ return () => {
+ resizeObserver.disconnect();
+ window.removeEventListener('resize', handleResize);
+ window.removeEventListener('orientationchange', handleResize);
+ clearTimeout(debounceTimer);
+ };
+ }, [isClient, calculateHeight, options.immediate]);
+
+ return {
+ elementRef,
+ updateHeight,
+ isClient,
+ };
+};
\ No newline at end of file
diff --git a/crop-x/src/hooks/useViewHeight.ts b/crop-x/src/hooks/useViewHeight.ts
new file mode 100644
index 0000000..43535cc
--- /dev/null
+++ b/crop-x/src/hooks/useViewHeight.ts
@@ -0,0 +1,57 @@
+import { useEffect, useState } from 'react';
+import { useLayoutStore } from '@/stores/useLayoutStore';
+
+export const useViewHeight = () => {
+ const { setViewHeight, calculateMainBodyHeight } = useLayoutStore();
+ const [isClient, setIsClient] = useState(false);
+
+ // 确保在客户端执行
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ const getViewHeight = () => {
+ if (!isClient) return 0;
+
+ // 获取视口高度
+ const height = window.innerHeight || document.documentElement.clientHeight;
+ return height;
+ };
+
+ useEffect(() => {
+ if (!isClient) return;
+
+ // 立即计算一次
+ const initialHeight = getViewHeight();
+ setViewHeight(initialHeight);
+
+ // 监听窗口大小变化
+ const handleResize = () => {
+ const newHeight = getViewHeight();
+ setViewHeight(newHeight);
+ };
+
+ // 监听方向变化
+ const handleOrientationChange = () => {
+ // 方向变化时稍微延迟计算,确保浏览器已经完成调整
+ setTimeout(() => {
+ const newHeight = getViewHeight();
+ setViewHeight(newHeight);
+ }, 100);
+ };
+
+ window.addEventListener('resize', handleResize, { passive: true });
+ window.addEventListener('orientationchange', handleOrientationChange, { passive: true });
+
+ // 清理函数
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ window.removeEventListener('orientationchange', handleOrientationChange);
+ };
+ }, [isClient, setViewHeight]);
+
+ return {
+ getViewHeight,
+ isClient,
+ };
+};
\ No newline at end of file
diff --git a/crop-x/src/stores/useLayoutStore.ts b/crop-x/src/stores/useLayoutStore.ts
new file mode 100644
index 0000000..82ab4aa
--- /dev/null
+++ b/crop-x/src/stores/useLayoutStore.ts
@@ -0,0 +1,65 @@
+import { create } from 'zustand';
+
+interface LayoutState {
+ // 布局高度相关状态
+ navigatorHeight: number; // 导航栏高度(原 authProviderHeight)
+ viewHeight: number; // 页面总高度
+ mainBodyHeight: number; // 主体内容高度
+
+ // 更新导航栏高度
+ setNavigatorHeight: (height: number) => void;
+
+ // 更新页面总高度
+ setViewHeight: (height: number) => void;
+
+ // 计算并更新主体内容高度
+ calculateMainBodyHeight: () => void;
+
+ // 重置所有高度
+ resetHeights: () => void;
+}
+
+export const useLayoutStore = create((set, get) => ({
+ // 初始状态
+ navigatorHeight: 72, // 默认导航栏高度
+ viewHeight: 0, // 页面总高度
+ mainBodyHeight: 0, // 主体内容高度
+
+ // 更新导航栏高度
+ setNavigatorHeight: (height: number) => {
+ set({ navigatorHeight: height });
+ },
+
+ // 更新页面总高度
+ setViewHeight: (height: number) => {
+ const state = get();
+ set({ viewHeight: height });
+
+ // 自动计算主体内容高度
+ const newMainBodyHeight = height - state.navigatorHeight;
+ if (newMainBodyHeight >= 0) {
+ set({ mainBodyHeight: newMainBodyHeight });
+ }
+ },
+
+ // 计算并更新主体内容高度
+ calculateMainBodyHeight: () => {
+ const state = get();
+ const newMainBodyHeight = state.viewHeight - state.navigatorHeight;
+ if (newMainBodyHeight >= 0) {
+ set({ mainBodyHeight: newMainBodyHeight });
+ }
+ },
+
+ // 重置所有高度
+ resetHeights: () => {
+ set({
+ navigatorHeight: 72,
+ viewHeight: 0,
+ mainBodyHeight: 0
+ });
+ },
+}));
+
+// 获取当前布局状态的工具函数
+export const getLayoutState = () => useLayoutStore.getState();
\ No newline at end of file