重构布局组件,优化导航栏和头部结构;调整样式以改善响应式设计

This commit is contained in:
2025-06-09 11:17:38 +08:00
parent 5ba7d45e97
commit 1383f2028a
13 changed files with 119 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
## TODO ## TODO
页头个人中心 全局修改断点命名
页头链接完善跳转地址 页头链接完善跳转地址

View File

@@ -13,21 +13,21 @@ export default function Header(props: HeaderProps) {
return ( return (
<header className={merge( <header className={merge(
`flex-none h-16`, `flex-none h-16 overflow-hidden`,
`flex items-stretch`, `flex items-stretch`,
)}> )}>
{/* left */} {/* left */}
<div className="flex-auto flex items-center gap-2"> <div className="flex-auto flex items-center gap-2">
<Button <Button
theme="ghost" theme="ghost"
className="w-9 h-9" className="w-9 h-9 ml-4 md:ml-0"
onClick={toggleNavbar}> onClick={toggleNavbar}>
{navbar {navbar
? <PanelLeftCloseIcon/> ? <PanelLeftCloseIcon/>
: <PanelLeftOpenIcon/> : <PanelLeftOpenIcon/>
} }
</Button> </Button>
<span> <span className="max-md:hidden">
</span> </span>
</div> </div>

View File

@@ -0,0 +1,81 @@
'use client'
import {ReactNode} from 'react'
import {useLayoutStore} from '@/components/providers/StoreProvider'
import {merge} from '@/lib/utils'
import {Slot} from '@radix-ui/react-slot'
type AdminLayoutProps = {
navbar: ReactNode
header: ReactNode
content: ReactNode
}
export default function Layout(props: AdminLayoutProps) {
const navbar = useLayoutStore(store => store.navbar)
const setNevBar = useLayoutStore(store => store.setNavbar)
return (
<div className="relative h-dvh overflow-hidden">
{/* 结构 */}
<div
data-expand={navbar}
className={merge(
`transition-[grid-template-columns] duration-300 ease-in-out`,
`w-full h-full grid`,
`grid-rows-[64px_1fr]`,
`data-[expand=true]:grid-cols-[200px_1fr]`,
`data-[expand=false]:grid-cols-[0px_1fr]`,
`md:data-[expand=false]:grid-cols-[64px_1fr]`,
)}
>
<div className="col-start-1 row-start-1 row-span-2 bg-card overflow-hidden relative z-20">
{props.navbar}
</div>
<div className="col-start-2 row-start-1 bg-card overflow-hidden relative z-20">
{props.header}
</div>
<svg className="col-start-2 row-start-2 w-full h-full z-20 pointer-events-none" preserveAspectRatio="none">
<defs>
<mask id="top-left-rounded-mask">
<rect width="100%" height="100%" fill="white"/>
<circle cx="16" cy="16" r="16" fill="black"/>
<rect x="16" y="0" width="100%" height="32" fill="black"/>
<rect x="0" y="16" width="32" height="100%" fill="black"/>
<rect x="16" y="16" width="100%" height="100%" fill="black"/>
</mask>
</defs>
<rect width="100%" height="100%" className="fill-card" mask="url(#top-left-rounded-mask)"/>
</svg>
<div
data-expand={navbar}
className={merge(
`md:hidden`,
`transition-opacity duration-300 ease-in-out`,
`col-start-1 row-start-1 col-span-2 row-span-2 bg-black/50 z-10`,
`max-md:data-[expand=true]:opacity-100 data-[expand=false]:opacity-0`,
`max-md:data-[expand=true]:pointer-events-auto data-[expand=false]:pointer-events-none`,
)}
onClick={() => setNevBar(false)}
>
</div>
</div>
{/* 内容 */}
<div
data-expand={navbar}
className={merge(
`transition-[margin] duration-300 ease-in-out`,
`absolute inset-0 overflow-hidden`,
`mt-[64px]`,
`md:data-[expand=true]:ml-[200px]`,
`md:data-[expand=false]:ml-[64px]`,
)}>
{props.content}
</div>
</div>
)
}

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import {ReactNode, useState} from 'react' import {ComponentProps, ReactNode, useState} from 'react'
import {merge} from '@/lib/utils' import {merge} from '@/lib/utils'
import {useLayoutStore} from '@/components/providers/StoreProvider' import {useLayoutStore} from '@/components/providers/StoreProvider'
import Link from 'next/link' import Link from 'next/link'
@@ -19,7 +19,7 @@ import {Eye} from 'lucide-react'
import {Archive} from 'lucide-react' import {Archive} from 'lucide-react'
import {ArchiveRestore} from 'lucide-react' import {ArchiveRestore} from 'lucide-react'
export type NavbarProps = {} export type NavbarProps = {} & ComponentProps<'nav'>
export default function Navbar(props: NavbarProps) { export default function Navbar(props: NavbarProps) {
const navbar = useLayoutStore(store => store.navbar) const navbar = useLayoutStore(store => store.navbar)
@@ -29,10 +29,8 @@ export default function Navbar(props: NavbarProps) {
data-expand={navbar} data-expand={navbar}
className={merge( className={merge(
`transition-[flex-basis] duration-300 ease-in-out`, `transition-[flex-basis] duration-300 ease-in-out`,
`flex-none`,
`flex flex-col overflow-hidden group`, `flex flex-col overflow-hidden group`,
`data-[expand=true]:basis-52 data-[expand=false]:basis-16`, `data-[expand=true]:basis-52 data-[expand=false]:basis-16`,
` `,
)}> )}>
{/* logo */} {/* logo */}
<Link <Link
@@ -56,7 +54,7 @@ export default function Navbar(props: NavbarProps) {
{/* routes */} {/* routes */}
<section className={merge( <section className={merge(
`transition-[padding] duration-300 ease-in-out`, `transition-[padding] duration-300 ease-in-out`,
`flex-auto overflow-auto`, `flex-auto overflow-x-hidden overflow-y-auto`,
`group-data-[expand=true]:px-4 group-data-[expand=false]:px-3`, `group-data-[expand=true]:px-4 group-data-[expand=false]:px-3`,
)}> )}>
<TooltipProvider> <TooltipProvider>
@@ -119,6 +117,13 @@ function NavItem(props: {
} }
} }
const setNavbar = useLayoutStore(store => store.setNavbar)
const closeNavBarIfMobile = () => {
if (window.innerWidth < 768) {
setNavbar(false)
}
}
return ( return (
<Tooltip open={open} onOpenChange={handleOpenChange}> <Tooltip open={open} onOpenChange={handleOpenChange}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -129,7 +134,9 @@ function NavItem(props: {
`hover:bg-gray-100`, `hover:bg-gray-100`,
`group-data-[expand=true]:px-4`, `group-data-[expand=true]:px-4`,
)} )}
href={props.href}> href={props.href}
onClick={closeNavBarIfMobile}
>
<span className="flex-none w-10 h-10 flex items-center justify-center">{props.icon}</span> <span className="flex-none w-10 h-10 flex items-center justify-center">{props.icon}</span>
<span className={merge( <span className={merge(
`flex-auto`, `flex-auto`,

View File

@@ -96,10 +96,9 @@ export default function BillsPage(props: BillsPageProps) {
{/* 操作区 */} {/* 操作区 */}
<section className="flex justify-between flex-wrap"> <section className="flex justify-between flex-wrap">
<div> <div>
<h1 className="text-xl font-bold"></h1>
</div> </div>
<Form form={form} onSubmit={onSubmit} className="flex items-end gap-4"> <Form form={form} onSubmit={onSubmit} className="flex items-end gap-4 flex-wrap">
<FormField name="type" label={<span className="text-sm"></span>}> <FormField name="type" label={<span className="text-sm"></span>}>
{({id, field}) => ( {({id, field}) => (
<Select value={field.value} onValueChange={field.onChange}> <Select value={field.value} onValueChange={field.onChange}>

View File

@@ -99,7 +99,7 @@ export default function ChannelsPage(props: ChannelsPageProps) {
<Page> <Page>
<section className="flex justify-between"> <section className="flex justify-between">
<div></div> <div></div>
<Form form={filterForm} handler={filterHandler} className="flex-none flex gap-4 items-end"> <Form form={filterForm} handler={filterHandler} className="flex-auto flex flex-wrap gap-4 items-end">
<FormField<FilterSchema, 'auth_type'> name="auth_type" label={<span className="text-sm"></span>}> <FormField<FilterSchema, 'auth_type'> name="auth_type" label={<span className="text-sm"></span>}>
{({field}) => ( {({field}) => (
<Select value={field.value} onValueChange={field.onChange}> <Select value={field.value} onValueChange={field.onChange}>

View File

@@ -1,7 +1,7 @@
import {ReactNode} from 'react' import {ReactNode} from 'react'
import {merge} from '@/lib/utils'
import Header from './_client/header' import Header from './_client/header'
import Navbar from '@/app/admin/_client/navbar' import Navbar from './_client/navbar'
import Layout from './_client/layout'
export type AdminLayoutProps = { export type AdminLayoutProps = {
children: ReactNode children: ReactNode
@@ -9,17 +9,10 @@ export type AdminLayoutProps = {
export default async function AdminLayout(props: AdminLayoutProps) { export default async function AdminLayout(props: AdminLayoutProps) {
return ( return (
<div className={merge( <Layout
`h-screen bg-card overflow-hidden min-w-7xl overflow-y-hidden`, navbar={<Navbar/>}
`flex items-stretch`, header={<Header/>}
)}> content={props.children}
/>
<Navbar/>
<div className="flex-auto flex flex-col items-stretch">
<Header/>
{props.children}
</div>
</div>
) )
} }

View File

@@ -108,19 +108,18 @@ export default function ShortResource(props: ShortResourceProps) {
}, },
}) })
const onSubmit = async (value: FilterSchema) => { const handler = form.handleSubmit(async (value: FilterSchema) => {
await refresh(1, data.size) await refresh(1, data.size)
} })
return ( return (
<> <>
{/* 操作区 */} {/* 操作区 */}
<section className="flex justify-between flex-wrap"> <section className="flex justify-between flex-wrap">
<div> <div>
</div> </div>
<Form form={form} onSubmit={onSubmit} className="flex items-end gap-4 flex-wrap"> <Form form={form} handler={handler} className="flex items-end gap-4 flex-wrap">
<FormField name="resource_no" label={<span className="text-sm"></span>}> <FormField name="resource_no" label={<span className="text-sm"></span>}>
{({id, field}) => ( {({id, field}) => (
<Input {...field} id={id} className="h-9"/> <Input {...field} id={id} className="h-9"/>

View File

@@ -52,10 +52,10 @@ export default function UserCenter() {
<span>{profile.name}</span> <span>{profile.name}</span>
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent className="w-40 p-1" align="end"> <HoverCardContent className="w-36 p-1" align="end">
<Button <Button
theme="ghost" theme="ghost"
className="w-full justify-start" className="w-full justify-start text-sm h-9 px-3"
onClick={() => router.push('/admin/profile')}> onClick={() => router.push('/admin/profile')}>
<UserPenIcon/> <UserPenIcon/>
@@ -63,7 +63,7 @@ export default function UserCenter() {
<Button <Button
theme="ghost" theme="ghost"
color="fail" color="fail"
className="w-full justify-start" className="w-full justify-start text-sm h-9 px-3"
onClick={doLogout}> onClick={doLogout}>
<LogOutIcon/> <LogOutIcon/>
退 退

View File

@@ -10,7 +10,8 @@ export default function Page(props: ComponentProps<'main'> & PageProps) {
<main <main
{...props} {...props}
className={merge( className={merge(
`flex-auto rounded-tl-xl overflow-hidden relative`, `relative size-full`,
props.className,
)}> )}>
{/* background */} {/* background */}

View File

@@ -28,7 +28,7 @@ export default function StoreProvider(props: ProfileProviderProps) {
const layout = useRef<StoreApi<LayoutStore>>(null) const layout = useRef<StoreApi<LayoutStore>>(null)
if (!layout.current) { if (!layout.current) {
console.log('📦 create layout store') console.log('📦 create layout store')
layout.current = createLayoutStore() layout.current = createLayoutStore(window.matchMedia(`(min-width: 1024px)`).matches)
} }
return ( return (

View File

@@ -107,7 +107,7 @@ function Pagination({
const paginationItems = generatePaginationItems() const paginationItems = generatePaginationItems()
return ( return (
<div className={`flex items-center justify-between gap-4 ${className || ''}`}> <div className={`flex flex-wrap items-center justify-end gap-4 ${className || ''}`}>
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
{' '} {' '}

View File

@@ -11,9 +11,9 @@ export type LayoutActions = {
setNavbar: (navbar: boolean) => void setNavbar: (navbar: boolean) => void
} }
export const createLayoutStore = () => { export const createLayoutStore = (open: boolean) => {
return createStore<LayoutStore>()(setState => ({ return createStore<LayoutStore>()(setState => ({
navbar: true, navbar: open,
toggleNavbar: () => setState((state) => { toggleNavbar: () => setState((state) => {
return {navbar: !state.navbar} return {navbar: !state.navbar}
}), }),