From 37d83c9ac89f566157da50c436eaf362e59daae8 Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 2 Jul 2026 15:26:10 +0800 Subject: [PATCH] feat: add mobile shadcn admin navigation --- frontend/src/app/layout/AppShell.tsx | 8 +- frontend/src/components/app/AppField.tsx | 18 +- frontend/src/components/app/AppSidebar.tsx | 108 +++++++---- frontend/src/components/app/AppTopbar.tsx | 17 +- frontend/src/components/ui/field.tsx | 216 +++++++++++++++++++++ 5 files changed, 324 insertions(+), 43 deletions(-) create mode 100644 frontend/src/components/ui/field.tsx diff --git a/frontend/src/app/layout/AppShell.tsx b/frontend/src/app/layout/AppShell.tsx index b06357e..50967bb 100644 --- a/frontend/src/app/layout/AppShell.tsx +++ b/frontend/src/app/layout/AppShell.tsx @@ -1,4 +1,4 @@ -import type * as React from 'react' +import * as React from 'react' import { AppSidebar } from '@/components/app/AppSidebar' import { AppTopbar } from '@/components/app/AppTopbar' @@ -8,11 +8,13 @@ type AppShellProps = { } export function AppShell({ children }: AppShellProps) { + const [mobileNavOpen, setMobileNavOpen] = React.useState(false) + return (
- +
- + setMobileNavOpen(true)} /> {children}
diff --git a/frontend/src/components/app/AppField.tsx b/frontend/src/components/app/AppField.tsx index 2a160cc..f1d324f 100644 --- a/frontend/src/components/app/AppField.tsx +++ b/frontend/src/components/app/AppField.tsx @@ -1,15 +1,25 @@ import type * as React from 'react' +import { Field, FieldDescription, FieldLabel } from '@/components/ui/field' + type AppFieldProps = { label: string + htmlFor?: string + description?: string + invalid?: boolean + required?: boolean children: React.ReactNode } -export function AppField({ label, children }: AppFieldProps) { +export function AppField({ label, htmlFor, description, invalid, required, children }: AppFieldProps) { return ( - + {description ? {description} : null} + ) } diff --git a/frontend/src/components/app/AppSidebar.tsx b/frontend/src/components/app/AppSidebar.tsx index 3304a94..b26fcc5 100644 --- a/frontend/src/components/app/AppSidebar.tsx +++ b/frontend/src/components/app/AppSidebar.tsx @@ -19,6 +19,7 @@ import { import { useAuth } from '@/features/auth/AuthProvider' import type { AuthMenu } from '@/features/auth/types' import { cn } from '@/lib/utils' +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' const iconMap = { audit: ScrollText, @@ -56,44 +57,83 @@ function visibleChildren(menu: AuthMenu) { return menu.children.filter((item) => item.is_visible && item.is_enabled) } -export function AppSidebar() { +type SidebarNavContentProps = { + groups: AuthMenu[] + currentHash: string + onNavigate?: () => void +} + +function SidebarBrand() { + return ( +
+

AI 工单平台

+

企业技术支持工作台

+
+ ) +} + +function SidebarNavContent({ groups, currentHash, onNavigate }: SidebarNavContentProps) { + return ( + + ) +} + +type AppSidebarProps = { + mobileOpen?: boolean + onMobileOpenChange?: (open: boolean) => void +} + +export function AppSidebar({ mobileOpen = false, onMobileOpenChange }: AppSidebarProps) { const { user } = useAuth() const currentHash = useCurrentHash() const groups = (user?.menus ?? []).filter((menu) => menu.is_visible && menu.is_enabled) return ( - + <> + + + + + 移动端导航 + + + onMobileOpenChange?.(false)} + /> + + + ) } diff --git a/frontend/src/components/app/AppTopbar.tsx b/frontend/src/components/app/AppTopbar.tsx index 24a4ccb..fdbd390 100644 --- a/frontend/src/components/app/AppTopbar.tsx +++ b/frontend/src/components/app/AppTopbar.tsx @@ -1,16 +1,29 @@ -import { Bell, ChevronRight, LogOut, UserRound } from 'lucide-react' +import { Bell, ChevronRight, LogOut, Menu, UserRound } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { useAuth } from '@/features/auth/AuthProvider' -export function AppTopbar() { +type AppTopbarProps = { + onMobileMenuClick?: () => void +} + +export function AppTopbar({ onMobileMenuClick }: AppTopbarProps) { const { logout, user } = useAuth() return (
+ AI 工单平台 当前工作区 diff --git a/frontend/src/components/ui/field.tsx b/frontend/src/components/ui/field.tsx new file mode 100644 index 0000000..62bf4cb --- /dev/null +++ b/frontend/src/components/ui/field.tsx @@ -0,0 +1,216 @@ +import * as React from "react" +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" +import { cn } from "@/lib/utils" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-2 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full md:flex-row md:items-center md:*:w-auto md:has-[>[data-slot=field-content]]:items-start md:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto md:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ className, ...props }: React.ComponentProps) { + return ( +