调整帮助中心移动端文档布局
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -45,6 +45,7 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.25.76",
|
"zod": "^3.25.76",
|
||||||
"zustand": "^5.0.9",
|
"zustand": "^5.0.9",
|
||||||
},
|
},
|
||||||
@@ -1399,6 +1400,8 @@
|
|||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"vaul": ["vaul@1.1.2", "https://registry.npmmirror.com/vaul/-/vaul-1.1.2.tgz", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.25.76",
|
"zod": "^3.25.76",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import Wrap from '@/components/wrap'
|
|||||||
import {Children} from '@/lib/utils'
|
import {Children} from '@/lib/utils'
|
||||||
import Sidebar from './sidebar'
|
import Sidebar from './sidebar'
|
||||||
import HomePage from '@/components/home/page'
|
import HomePage from '@/components/home/page'
|
||||||
|
import SidebarDrawer from './sidebar-drawer'
|
||||||
|
|
||||||
export default function DocsLayout(props: Children) {
|
export default function DocsLayout(props: Children) {
|
||||||
return (
|
return (
|
||||||
<HomePage path={[
|
<HomePage path={[{label: '帮助中心', href: '/docs'}]}>
|
||||||
{label: '帮助中心', href: '/docs'},
|
<Wrap className="flex gap-3 flex-col md:flex-row">
|
||||||
]}>
|
<SidebarDrawer/>
|
||||||
<Wrap className="flex gap-3">
|
<Sidebar className="hidden md:block w-68"/>
|
||||||
<Sidebar/>
|
<div className="flex-1 bg-white rounded-lg p-4 md:p-6 min-h-[420px]">
|
||||||
<div className="flex-1 bg-white rounded-lg p-6 min-h-[420px]">
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export default function DocsIndexPage() {
|
export default function DocsIndexPage() {
|
||||||
return (
|
return (
|
||||||
<div></div>
|
<div className="text-center text-slate-500 py-12">
|
||||||
|
<p className="text-lg">欢迎来到帮助中心</p>
|
||||||
|
<p className="text-sm mt-2">请从左侧目录选择需要查看的文档</p>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/app/(home)/docs/sidebar-drawer.tsx
Normal file
37
src/app/(home)/docs/sidebar-drawer.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client'
|
||||||
|
import {useState} from 'react'
|
||||||
|
import {Menu} from 'lucide-react'
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from '@/components/ui/drawer'
|
||||||
|
import Sidebar from './sidebar'
|
||||||
|
|
||||||
|
export default function SidebarDrawer() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="md:hidden flex items-center justify-between bg-white rounded-lg p-3">
|
||||||
|
<span className="font-medium text-slate-900">帮助中心</span>
|
||||||
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<button className="flex items-center gap-2 text-slate-600 hover:text-slate-900 p-1">
|
||||||
|
<Menu size={20}/>
|
||||||
|
<span className="text-sm">目录</span>
|
||||||
|
</button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<div className="mx-auto w-full max-w-sm">
|
||||||
|
<div className="px-4 py-3 border-b">
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900">帮助中心</h3>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-2 max-h-[70vh] overflow-y-auto">
|
||||||
|
<Sidebar onClose={() => setOpen(false)}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,10 +3,7 @@ import {useState, useMemo, useCallback} from 'react'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {usePathname} from 'next/navigation'
|
import {usePathname} from 'next/navigation'
|
||||||
import {ChevronRight} from 'lucide-react'
|
import {ChevronRight} from 'lucide-react'
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
type Props = {
|
|
||||||
collapsed?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单配置
|
// 菜单配置
|
||||||
const MENU_ITEMS = [
|
const MENU_ITEMS = [
|
||||||
@@ -58,7 +55,12 @@ const MENU_ITEMS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Sidebar({collapsed = false}: Props) {
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar({className, onClose}: Props) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
// 获取当前文档 key
|
// 获取当前文档 key
|
||||||
@@ -100,9 +102,7 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={`bg-white rounded-lg p-3 transition-all duration-200 shrink-0 ${
|
className={merge(`bg-white rounded-lg p-3 transition-all duration-200 shrink-0`, className)}
|
||||||
collapsed ? 'w-20' : 'w-68'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<nav className="space-y-2">
|
<nav className="space-y-2">
|
||||||
{MENU_ITEMS.map(section => (
|
{MENU_ITEMS.map(section => (
|
||||||
@@ -110,9 +110,7 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
<div
|
<div
|
||||||
onClick={() => toggleGroup(section.group)}
|
onClick={() => toggleGroup(section.group)}
|
||||||
className={`flex items-center gap-2 cursor-pointer px-3 py-2 rounded-sm transition-colors ${
|
className={`flex items-center gap-2 cursor-pointer px-3 py-2 rounded-sm transition-colors ${
|
||||||
finalExpandedGroups[section.group] && !collapsed
|
finalExpandedGroups[section.group] && 'bg-blue-50'
|
||||||
? 'bg-blue-50'
|
|
||||||
: 'hover:bg-slate-50'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -123,15 +121,13 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
<ChevronRight size={16}/>
|
<ChevronRight size={16}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="text-lg font-semibold text-slate-900">
|
<div className="text-lg font-semibold text-slate-900">
|
||||||
{section.group}
|
{section.group}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{finalExpandedGroups[section.group] && (
|
{finalExpandedGroups[section.group] && (
|
||||||
<ul className={`mt-1 text-base ${collapsed ? 'hidden' : 'block'}`}>
|
<ul className="mt-1 text-base">
|
||||||
{section.items.map((item) => {
|
{section.items.map((item) => {
|
||||||
const isActive = currentKey === item.key
|
const isActive = currentKey === item.key
|
||||||
const href = getItemHref(item.key)
|
const href = getItemHref(item.key)
|
||||||
@@ -140,6 +136,7 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
<li key={item.key}>
|
<li key={item.key}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
|
onClick={() => onClose?.()}
|
||||||
className={`block pl-8 py-2 text-base cursor-pointer transition-colors ${
|
className={`block pl-8 py-2 text-base cursor-pointer transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-blue-50 font-semibold'
|
? 'bg-blue-50 font-semibold'
|
||||||
|
|||||||
135
src/components/ui/drawer.tsx
Normal file
135
src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import {Drawer as DrawerPrimitive} from 'vaul'
|
||||||
|
|
||||||
|
import {merge} from '@/lib/utils/index'
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={merge(
|
||||||
|
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay/>
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={merge(
|
||||||
|
'group/drawer-content fixed z-50 flex h-auto flex-col bg-background',
|
||||||
|
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
|
||||||
|
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
|
||||||
|
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
|
||||||
|
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block"/>
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({className, ...props}: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={merge(
|
||||||
|
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({className, ...props}: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={merge('mt-auto flex flex-col gap-2 p-4', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={merge('font-semibold text-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={merge('text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user