重构布局组件,优化导航栏和头部结构;调整样式以改善响应式设计
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
81
src/app/admin/_client/layout.tsx
Normal file
81
src/app/admin/_client/layout.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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/>
|
||||||
退出登录
|
退出登录
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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">
|
||||||
共
|
共
|
||||||
{' '}
|
{' '}
|
||||||
|
|||||||
@@ -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}
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user