Compare commits
2 Commits
9afc680833
...
04d61ae3b9
| Author | SHA1 | Date | |
|---|---|---|---|
| 04d61ae3b9 | |||
| 7a21043dd8 |
22
crop-x/next.config.js
Normal file
22
crop-x/next.config.js
Normal file
@@ -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
|
||||||
54
crop-x/package-lock.json
generated
54
crop-x/package-lock.json
generated
@@ -56,7 +56,8 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
@@ -3727,7 +3728,6 @@
|
|||||||
"integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==",
|
"integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -3745,7 +3745,6 @@
|
|||||||
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -3757,7 +3756,6 @@
|
|||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
@@ -3798,7 +3796,6 @@
|
|||||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.1",
|
"@typescript-eslint/scope-manager": "8.46.1",
|
||||||
"@typescript-eslint/types": "8.46.1",
|
"@typescript-eslint/types": "8.46.1",
|
||||||
@@ -4018,7 +4015,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4214,7 +4210,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.9",
|
"baseline-browser-mapping": "^2.8.9",
|
||||||
"caniuse-lite": "^1.0.30001746",
|
"caniuse-lite": "^1.0.30001746",
|
||||||
@@ -4727,8 +4722,7 @@
|
|||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
@@ -4856,7 +4850,6 @@
|
|||||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -8680,7 +8673,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -9089,7 +9081,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -9202,7 +9193,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -9233,7 +9223,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -9246,7 +9235,6 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.65.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.65.0.tgz",
|
||||||
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
|
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -9827,8 +9815,7 @@
|
|||||||
"version": "4.1.14",
|
"version": "4.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
|
||||||
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
|
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
@@ -9915,7 +9902,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -9974,7 +9960,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -10124,7 +10109,6 @@
|
|||||||
"integrity": "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw==",
|
"integrity": "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -10218,7 +10202,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -10475,6 +10458,35 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,8 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12",
|
||||||
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import SideBar from '@/components/layouts/SideBar/SideBar'
|
import {SideBarOld} from '@/components/layouts/SideBar/SideBarOld'
|
||||||
|
|
||||||
// 中心配置路由数据
|
// 中心配置路由数据
|
||||||
const centralConfigData = {
|
const centralConfigData = {
|
||||||
versions: ["1.0.0", "2.0.0"],
|
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "租户管理",
|
title: "租户管理",
|
||||||
@@ -139,5 +138,5 @@ export default function CentralConfigLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return <SideBar data={centralConfigData}>{children}</SideBar>
|
return <SideBarOld data={centralConfigData}>{children}</SideBarOld>
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Navbar} from "@/components/layouts/Navbar"
|
import {Navbar1} from "@/components/layouts/NavBar"
|
||||||
import '@/styles/globals.css'
|
import '@/styles/globals.css'
|
||||||
export default function DashboardLayout({
|
export default function DashboardLayout({
|
||||||
children,
|
children,
|
||||||
@@ -7,7 +7,7 @@ export default function DashboardLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Navbar></Navbar>
|
<Navbar1></Navbar1>
|
||||||
{/* 布局 UI */}
|
{/* 布局 UI */}
|
||||||
{/* 将 children 放在您希望渲染页面或嵌套布局的位置 */}
|
{/* 将 children 放在您希望渲染页面或嵌套布局的位置 */}
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
|
import { useLayoutStore } from '@/stores/useLayoutStore';
|
||||||
// Define the interface for menu data
|
// Define the interface for menu data
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
title: string
|
title: string
|
||||||
@@ -110,17 +110,10 @@ export interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|||||||
export function AppSidebar({ data, ...props }: AppSidebarProps) {
|
export function AppSidebar({ data, ...props }: AppSidebarProps) {
|
||||||
// Use external data if provided, otherwise use default data
|
// Use external data if provided, otherwise use default data
|
||||||
const sidebarData = data || defaultData
|
const sidebarData = data || defaultData
|
||||||
|
const { navigatorHeight } = useLayoutStore();
|
||||||
return (
|
return (
|
||||||
<Sidebar {...props}>
|
<Sidebar {...props} style = {{top: navigatorHeight + 'px'}}>
|
||||||
<SidebarHeader>
|
<SidebarContent className="gap-0" >
|
||||||
<VersionSwitcher
|
|
||||||
versions={sidebarData.versions || defaultData.versions}
|
|
||||||
defaultVersion={sidebarData.versions?.[0] || defaultData.versions[0]}
|
|
||||||
/>
|
|
||||||
<SearchForm />
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent className="gap-0">
|
|
||||||
{/* We create a collapsible SidebarGroup for each parent. */}
|
{/* We create a collapsible SidebarGroup for each parent. */}
|
||||||
{sidebarData.navMain.map((item) => (
|
{sidebarData.navMain.map((item) => (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { Menu } from "lucide-react";
|
'use client';
|
||||||
|
|
||||||
|
import { Book, Menu, Sunset, Trees, Zap } from "lucide-react";
|
||||||
import { Tractor, Map, Clipboard, Package, Brain, Droplets, Settings } from 'lucide-react';
|
import { Tractor, Map, Clipboard, Package, Brain, Droplets, Settings } from 'lucide-react';
|
||||||
|
import { MessageBell } from './components/MessageBell';
|
||||||
|
import { UserProfile } from './components/UserProfile';
|
||||||
|
import { AuthProvider } from './components/auth/AuthContext';
|
||||||
|
import { useElementHeight } from '@/hooks/useElementHeight';
|
||||||
|
import { useViewHeight } from '@/hooks/useViewHeight';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
|
import { useLayoutStore } from '@/stores/useLayoutStore';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
@@ -23,8 +31,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
|
|
||||||
// 菜单项接口定义
|
interface MenuItem {
|
||||||
export interface MenuItem {
|
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -32,45 +39,31 @@ export interface MenuItem {
|
|||||||
items?: MenuItem[];
|
items?: MenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo接口定义
|
interface Navbar1Props {
|
||||||
export interface LogoConfig {
|
logo?: {
|
||||||
url: string;
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 认证接口定义
|
|
||||||
export interface AuthConfig {
|
|
||||||
login: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
url: string;
|
||||||
};
|
src: string;
|
||||||
signup: {
|
alt: string;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Navbar数据接口定义
|
|
||||||
export interface NavbarData {
|
|
||||||
logo?: LogoConfig;
|
|
||||||
menu?: MenuItem[];
|
menu?: MenuItem[];
|
||||||
auth?: AuthConfig;
|
auth?: {
|
||||||
|
login: {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
signup: {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
const navbarData = {
|
||||||
// Navbar组件Props接口定义
|
|
||||||
export interface NavbarProps {
|
|
||||||
navbar?: NavbarData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认Navbar数据
|
|
||||||
const defaultNavbar: NavbarData = {
|
|
||||||
logo: {
|
logo: {
|
||||||
url: "/",
|
url: "/",
|
||||||
src: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/logos/shadcnblockscom-icon.svg",
|
src: "https://deifkwefumgah.cloudfront.net/shadcnblocks/block/logos/shadcnblockscom-icon.svg",
|
||||||
alt: "Crop-X Logo",
|
alt: "Crop-X Logo",
|
||||||
title: "Crop-X 智慧农业",
|
title: "智慧农业生产管理系统",
|
||||||
},
|
},
|
||||||
menu: [
|
menu: [
|
||||||
{
|
{
|
||||||
@@ -121,22 +114,47 @@ const defaultNavbar: NavbarData = {
|
|||||||
signup: { title: "注册", url: "/register" },
|
signup: { title: "注册", url: "/register" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const Navbar1 = () => {
|
||||||
|
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参数
|
// 使用自定义 Hook 计算高度
|
||||||
export function Navbar({ navbar }: NavbarProps) {
|
const { elementRef, updateHeight } = useElementHeight({
|
||||||
// 使用外部传入的navbar数据,如果没有则使用默认数据
|
immediate: true, // 立即计算高度
|
||||||
const navbarData = navbar || defaultNavbar;
|
onUpdate: (height: number) => {
|
||||||
const logo = navbarData.logo || defaultNavbar.logo;
|
// 更新 Zustand store 中的状态
|
||||||
const menu = navbarData.menu || defaultNavbar.menu;
|
const { setNavigatorHeight } = useLayoutStore.getState();
|
||||||
const auth = navbarData.auth || defaultNavbar.auth;
|
setNavigatorHeight(height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听页面高度变化
|
||||||
|
useViewHeight();
|
||||||
|
|
||||||
|
const handleMessageClick = () => {
|
||||||
|
// 处理消息点击事件,可以跳转到消息中心页面
|
||||||
|
console.log('Navigate to message center');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileClick = () => {
|
||||||
|
// 处理个人中心点击事件
|
||||||
|
console.log('Navigate to profile page');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-4">
|
<AuthProvider>
|
||||||
<div className="container">
|
<section className="py-4" ref={elementRef}>
|
||||||
|
<div className="container" style = {containerStyle}>
|
||||||
{/* Desktop Menu */}
|
{/* Desktop Menu */}
|
||||||
<nav className="hidden justify-between lg:flex">
|
<nav className="hidden justify-between lg:flex">
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
|
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-3 flex-shrink-0">
|
<div className="flex items-center gap-3 flex-shrink-0">
|
||||||
<div className="w-10 h-10 bg-green-600 rounded-lg flex items-center justify-center">
|
<div className="w-10 h-10 bg-green-600 rounded-lg flex items-center justify-center">
|
||||||
<Tractor className="w-6 h-6 text-white" />
|
<Tractor className="w-6 h-6 text-white" />
|
||||||
@@ -146,21 +164,41 @@ export function Navbar({ navbar }: NavbarProps) {
|
|||||||
<p className="text-xs text-muted-foreground">Smart Agriculture Management System</p>
|
<p className="text-xs text-muted-foreground">Smart Agriculture Management System</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
</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>
|
<NavigationMenu>
|
||||||
<NavigationMenuList>
|
<NavigationMenuList className="flex gap-1 min-w-max">
|
||||||
|
|
||||||
{menu.map((item) => renderMenuItem(item))}
|
{menu.map((item) => renderMenuItem(item))}
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2" style = {{alignItems:"center"}}>
|
||||||
<Button asChild variant="outline" size="sm">
|
<MessageBell onMessageClick={handleMessageClick} />
|
||||||
<a href={auth.login.url}>{auth.login.title}</a>
|
<UserProfile onProfileClick={handleProfileClick} />
|
||||||
</Button>
|
|
||||||
<Button asChild size="sm">
|
|
||||||
<a href={auth.signup.url}>{auth.signup.title}</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -203,15 +241,12 @@ export function Navbar({ navbar }: NavbarProps) {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Button asChild variant="outline">
|
<div className="flex justify-center">
|
||||||
<a href={auth.login.url}>{auth.login.title}</a>
|
<MessageBell onMessageClick={handleMessageClick} />
|
||||||
</Button>
|
</div>
|
||||||
<Button asChild>
|
<div className="flex justify-center">
|
||||||
<a href={auth.signup.url}>{auth.signup.title}</a>
|
<UserProfile onProfileClick={handleProfileClick} />
|
||||||
</Button>
|
</div>
|
||||||
<Button asChild>
|
|
||||||
系统管理员
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
@@ -220,16 +255,18 @@ export function Navbar({ navbar }: NavbarProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderMenuItem = (item: MenuItem) => {
|
const renderMenuItem = (item: MenuItem) => {
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
return (
|
return (
|
||||||
<NavigationMenuItem key={item.title}>
|
<NavigationMenuItem key={item.title}>
|
||||||
<NavigationMenuTrigger>{item.title}</NavigationMenuTrigger>
|
<NavigationMenuTrigger className="whitespace-nowrap">{item.title}</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
<NavigationMenuContent className="bg-popover text-popover-foreground">
|
||||||
{item.items.map((subItem) => (
|
{item.items.map((subItem) => (
|
||||||
|
|
||||||
<NavigationMenuLink asChild key={subItem.title} className="w-80">
|
<NavigationMenuLink asChild key={subItem.title} className="w-80">
|
||||||
<SubMenuLink item={subItem} />
|
<SubMenuLink item={subItem} />
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
@@ -243,9 +280,9 @@ const renderMenuItem = (item: MenuItem) => {
|
|||||||
<NavigationMenuItem key={item.title}>
|
<NavigationMenuItem key={item.title}>
|
||||||
<NavigationMenuLink
|
<NavigationMenuLink
|
||||||
href={item.url}
|
href={item.url}
|
||||||
className="bg-background hover:bg-muted hover:text-accent-foreground group inline-flex h-10 w-max items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors"
|
className="bg-background hover:bg-muted hover:text-accent-foreground group inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors gap-2 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon && <span className="shrink-0">{item.icon}</span>}
|
||||||
{item.title}
|
{item.title}
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
@@ -256,7 +293,8 @@ const renderMobileMenuItem = (item: MenuItem) => {
|
|||||||
if (item.items) {
|
if (item.items) {
|
||||||
return (
|
return (
|
||||||
<AccordionItem key={item.title} value={item.title} className="border-b-0">
|
<AccordionItem key={item.title} value={item.title} className="border-b-0">
|
||||||
<AccordionTrigger className="text-md py-0 font-semibold hover:no-underline">
|
<AccordionTrigger className="text-md py-0 font-semibold hover:no-underline gap-2">
|
||||||
|
{item.icon && <span className="shrink-0">{item.icon}</span>}
|
||||||
{item.title}
|
{item.title}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="mt-2">
|
<AccordionContent className="mt-2">
|
||||||
@@ -269,7 +307,8 @@ const renderMobileMenuItem = (item: MenuItem) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a key={item.title} href={item.url} className="text-md font-semibold">
|
<a key={item.title} href={item.url} className="text-md font-semibold flex items-center gap-2">
|
||||||
|
{item.icon && <span className="shrink-0">{item.icon}</span>}
|
||||||
{item.title}
|
{item.title}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@@ -293,3 +332,5 @@ const SubMenuLink = ({ item }: { item: MenuItem }) => {
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { Navbar1 };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { ReactNode, useEffect, useState } from 'react'
|
import { ReactNode, useEffect, useState } from 'react'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { AppSidebar, AppSidebarProps, SidebarData } from "@/components/app-sidebar"
|
import { AppSidebar, AppSidebarProps } from "@/components/app-sidebar"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
|
|||||||
273
crop-x/src/components/layouts/SideBar/SideBarOld.tsx
Normal file
273
crop-x/src/components/layouts/SideBar/SideBarOld.tsx
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
'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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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: <span className="text-lg">{item.icon}</span>,
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前页面的面包屑
|
||||||
|
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 (
|
||||||
|
<div className="flex h-screen bg-gray-100" style={{ height: '100vh' }}>
|
||||||
|
{/* 左侧导航栏 */}
|
||||||
|
<LeftSidebar
|
||||||
|
menus={menus}
|
||||||
|
activePath={currentPath}
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isCollapsed={!isMobile && isCollapsed}
|
||||||
|
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 右侧主内容 */}
|
||||||
|
<MainContent
|
||||||
|
breadcrumb={getCurrentBreadcrumb()}
|
||||||
|
isMobile={isMobile}
|
||||||
|
sidebarOpen={!isCollapsed}
|
||||||
|
onToggleSidebar={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MainContent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
179
crop-x/src/components/layouts/SideBar/components/LeftSidebar.tsx
Normal file
179
crop-x/src/components/layouts/SideBar/components/LeftSidebar.tsx
Normal file
@@ -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<string>();
|
||||||
|
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<Set<string>>(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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-white border-r border-gray-200 transition-all duration-300 flex flex-col",
|
||||||
|
isMobile ? "fixed inset-y-0 left-0 z-50" : "relative",
|
||||||
|
isCollapsed ? "w-16" : "w-64"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="p-4 border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className={cn(
|
||||||
|
"font-semibold text-gray-900 transition-all duration-300",
|
||||||
|
isCollapsed ? "hidden" : "block"
|
||||||
|
)}>
|
||||||
|
导航菜单
|
||||||
|
</h2>
|
||||||
|
{isMobile ? (
|
||||||
|
<X className="w-5 h-5 text-gray-600" />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={onToggleCollapse}
|
||||||
|
className="p-1 rounded-md hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5 text-gray-600" />
|
||||||
|
</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}>
|
||||||
|
{/* 一级菜单 */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleMenu(menu.id)}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-center justify-between px-3 py-2 rounded-md transition-colors text-sm",
|
||||||
|
"hover:bg-gray-100 hover:text-gray-900",
|
||||||
|
isCollapsed ? "justify-center px-2 py-3" : "px-3 py-2"
|
||||||
|
)}
|
||||||
|
title={isCollapsed ? menu.label : undefined}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{menu.icon && (
|
||||||
|
<span className="flex-shrink-0">
|
||||||
|
{menu.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span className="text-gray-700">{menu.label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isCollapsed && menu.children && (
|
||||||
|
expandedMenus.has(menu.id) ? (
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 二级菜单 */}
|
||||||
|
{!isCollapsed && menu.children && expandedMenus.has(menu.id) && (
|
||||||
|
<div className="ml-4 mt-1 space-y-1">
|
||||||
|
{menu.children.map((child) => (
|
||||||
|
<button
|
||||||
|
key={child.id}
|
||||||
|
onClick={() => child.path && onNavigate(child.path)}
|
||||||
|
className={cn(
|
||||||
|
"w-full text-left px-3 py-2 rounded-md transition-colors text-xs",
|
||||||
|
activePath === child.path
|
||||||
|
? "bg-green-50 text-green-700 font-medium border-l-2 border-green-600"
|
||||||
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{child.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部 */}
|
||||||
|
<div className="p-4 border-t border-gray-200">
|
||||||
|
<div className={cn(
|
||||||
|
"text-xs text-gray-500",
|
||||||
|
isCollapsed ? "text-center" : "text-left"
|
||||||
|
)}>
|
||||||
|
{isCollapsed ? "管理" : "管理系统"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
crop-x/src/components/layouts/SideBar/components/MainContent.tsx
Normal file
112
crop-x/src/components/layouts/SideBar/components/MainContent.tsx
Normal file
@@ -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 && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||||
|
onClick={() => setShowMobileSidebar(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主内容区域 */}
|
||||||
|
<div className="flex-1 flex flex-col min-h-screen bg-gray-50">
|
||||||
|
{/* 顶部导航栏 */}
|
||||||
|
<header className="bg-white border-b border-gray-200 px-4 py-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* 菜单按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={handleToggleSidebar}
|
||||||
|
className="p-2 rounded-md hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
showMobileSidebar ? (
|
||||||
|
<X className="w-5 h-5 text-gray-600" />
|
||||||
|
) : (
|
||||||
|
<Menu className="w-5 h-5 text-gray-600" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Menu className="w-5 h-5 text-gray-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 面包屑导航 */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Home className="w-4 h-4 text-gray-500" />
|
||||||
|
{breadcrumb.length > 0 ? (
|
||||||
|
breadcrumb.map((item, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||||
|
{item.path ? (
|
||||||
|
<a
|
||||||
|
href={item.path}
|
||||||
|
className="text-sm text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-900 font-medium">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-900 font-medium">{title}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* 主内容区域 */}
|
||||||
|
<main className="flex-1 overflow-auto">
|
||||||
|
<div className="p-6">
|
||||||
|
|
||||||
|
{/* 页面内容 */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 p-6 shadow-sm">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
283
crop-x/src/components/layouts/components/MessageBell.tsx
Normal file
283
crop-x/src/components/layouts/components/MessageBell.tsx
Normal file
@@ -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<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
crop-x/src/components/layouts/components/UserProfile.tsx
Normal file
109
crop-x/src/components/layouts/components/UserProfile.tsx
Normal file
@@ -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 (
|
||||||
|
<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">{authState.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">{authState.user?.realName}</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">{authState.user?.role === 'admin' ? '系统管理员' : '普通用户'}</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>{authState.user?.username}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">手机号:</span>
|
||||||
|
<span>{authState.user?.phone}</span>
|
||||||
|
</div>
|
||||||
|
{authState.user?.enterpriseName && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">所属企业:</span>
|
||||||
|
<span className="truncate max-w-[140px]" title={authState.user?.enterpriseName}>
|
||||||
|
{authState.user?.enterpriseName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{authState.user?.department && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">部门:</span>
|
||||||
|
<span>{authState.user?.department}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{authState.user?.lastLoginTime && (
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-muted-foreground">上次登录:</span>
|
||||||
|
<span className="text-muted-foreground">{authState.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
185
crop-x/src/components/layouts/components/auth/AuthContext.tsx
Normal file
185
crop-x/src/components/layouts/components/auth/AuthContext.tsx
Normal 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;
|
||||||
|
}
|
||||||
6
crop-x/src/components/layouts/components/lib/utils.ts
Normal file
6
crop-x/src/components/layouts/components/lib/utils.ts
Normal 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))
|
||||||
|
}
|
||||||
36
crop-x/src/components/layouts/components/ui/badge.tsx
Normal file
36
crop-x/src/components/layouts/components/ui/badge.tsx
Normal 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 }
|
||||||
56
crop-x/src/components/layouts/components/ui/button.tsx
Normal file
56
crop-x/src/components/layouts/components/ui/button.tsx
Normal 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 }
|
||||||
120
crop-x/src/components/layouts/components/ui/dialog.tsx
Normal file
120
crop-x/src/components/layouts/components/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
||||||
29
crop-x/src/components/layouts/components/ui/popover.tsx
Normal file
29
crop-x/src/components/layouts/components/ui/popover.tsx
Normal 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 }
|
||||||
46
crop-x/src/components/layouts/components/ui/scroll-area.tsx
Normal file
46
crop-x/src/components/layouts/components/ui/scroll-area.tsx
Normal 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 }
|
||||||
@@ -153,8 +153,8 @@ function SidebarProvider({
|
|||||||
|
|
||||||
function Sidebar({
|
function Sidebar({
|
||||||
side = "left",
|
side = "left",
|
||||||
variant = "sidebar",
|
variant = "inset",
|
||||||
collapsible = "none",
|
collapsible = "offcanvas",
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
|
|||||||
101
crop-x/src/hooks/useElementHeight.ts
Normal file
101
crop-x/src/hooks/useElementHeight.ts
Normal file
@@ -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<HTMLElement>(null);
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
const { setNavigatorHeight } = useLayoutStore();
|
||||||
|
const lastHeightRef = useRef<number>(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,
|
||||||
|
};
|
||||||
|
};
|
||||||
57
crop-x/src/hooks/useViewHeight.ts
Normal file
57
crop-x/src/hooks/useViewHeight.ts
Normal file
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
65
crop-x/src/stores/useLayoutStore.ts
Normal file
65
crop-x/src/stores/useLayoutStore.ts
Normal file
@@ -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<LayoutState>((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();
|
||||||
Reference in New Issue
Block a user