fix(layout): 优化首页布局并添加可调整面板 fixes #123

- 使用 react-resizable-panels 实现可调整大小的面板
- 重新布局首页结构,分为左、中、右三个可调整区域
- 更新 NoteForm 和 NoteHistory 组件以适应新布局
- 调整 History 组件样式,优化滚动体验
- 更新项目依赖,添加 react-resizable-panels
This commit is contained in:
思诺特
2025-04-27 21:55:38 +08:00
parent 508a0efd92
commit 246e8a1406
7 changed files with 355 additions and 298 deletions

View File

@@ -39,6 +39,7 @@
"react-hook-form": "^7.55.0", "react-hook-form": "^7.55.0",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.8",
"react-router-dom": "^7.5.1", "react-router-dom": "^7.5.1",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"remark-gfm": "1.0.0", "remark-gfm": "1.0.0",

View File

@@ -0,0 +1,54 @@
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@@ -2,10 +2,10 @@
@import 'tw-animate-css'; @import 'tw-animate-css';
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
html,body{ html, body, #root {
height: 100%; height: 100%;
width: 100%;
} }
/* 修改滚动条轨道颜色 */ /* 修改滚动条轨道颜色 */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; /* 控制滚动条的宽度 */ width: 8px; /* 控制滚动条的宽度 */

View File

@@ -9,6 +9,7 @@ import {
import { useState } from 'react' import { useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '@/components/ui/resizable'
interface IProps { interface IProps {
NoteForm: React.ReactNode NoteForm: React.ReactNode
@@ -19,59 +20,53 @@ const HomeLayout: FC<IProps> = ({ NoteForm, Preview, History }) => {
const [, setShowSettings] = useState(false) const [, setShowSettings] = useState(false)
return ( return (
<div className="flex h-screen flex-col overflow-hidden bg-white"> <div className="flex h-screen flex-col overflow-hidden">
<div className="flex flex-1"> <ResizablePanelGroup direction="horizontal" className="h-full w-full">
{/* 左侧部分Header + 表单 */} {/* 左表单 */}
<aside className="flex w-[340px] flex-col border-r border-neutral-200 bg-white"> <ResizablePanel defaultSize={18} minSize={10} maxSize={35}>
{/* Header */} <aside className="flex h-full flex-col overflow-hidden border-r border-neutral-200 bg-white">
<header className="flex h-16 items-center justify-between px-6"> <header className="flex h-16 items-center justify-between px-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-2xl"> <div className="flex h-10 w-10 items-center justify-center overflow-hidden rounded-2xl">
<img src="/icon.svg" alt="logo" className="h-full w-full object-contain" /> <img src="/icon.svg" alt="logo" className="h-full w-full object-contain" />
</div>
<div className="text-2xl font-bold text-gray-800">BiliNote</div>
</div> </div>
<div className="text-2xl font-bold text-gray-800">BiliNote</div> <div>
</div> <TooltipProvider>
<div> <Tooltip>
<TooltipProvider> <TooltipTrigger onClick={() => setShowSettings(true)}>
<Tooltip> <Link to={'/settings'}>
<TooltipTrigger onClick={() => setShowSettings(true)}> <SlidersHorizontal className="text-muted-foreground hover:text-primary cursor-pointer" />
<Link to={'/settings'}> </Link>
<SlidersHorizontal className="text-muted-foreground hover:text-primary cursor-pointer" /> </TooltipTrigger>
</Link> <TooltipContent>
</TooltipTrigger> <span></span>
<TooltipContent> </TooltipContent>
<span></span> </Tooltip>
</TooltipContent> </TooltipProvider>
</Tooltip> </div>
</TooltipProvider> </header>
</div> <div className="flex-1 overflow-auto p-4">{NoteForm}</div>
</header> </aside>
</ResizablePanel>
{/* 表单内容 */} <ResizableHandle />
<div className="flex-1 overflow-auto p-4">
{/*<NoteForm />*/}
{NoteForm}
</div>
</aside>
<aside className="flex h-full w-[300px] flex-col border-r border-neutral-200 bg-white">
{/* Header */}
{/* 表单内容 */} {/* 中间历史 */}
{/*<NoteForm />*/} <ResizablePanel defaultSize={16} minSize={10} maxSize={30}>
{History} <aside className="flex h-full flex-col overflow-hidden border-r border-neutral-200 bg-white">
</aside> <div className="flex-1 overflow-auto p-4">{History}</div>
</aside>
</ResizablePanel>
{/* 右侧预览区域 */} <ResizableHandle />
<main className="h-screen flex-1 overflow-hidden bg-white p-6">
{/*<Outlet />*/}
{Preview}
</main>
</div>
{/* 页脚 */} {/* 右边预览 */}
{/*<footer className="h-12 bg-white shadow-inner flex items-center justify-center text-sm text-neutral-600">*/} <ResizablePanel defaultSize={55} minSize={30}>
{/* © 2025 BiliNote. All rights reserved.*/} <main className="flex h-full flex-col overflow-hidden bg-white p-6">{Preview}</main>
{/*</footer>*/} </ResizablePanel>
</ResizablePanelGroup>
</div> </div>
) )
} }

View File

@@ -13,7 +13,7 @@ const History = () => {
<Clock className="h-4 w-4 text-neutral-500" /> <Clock className="h-4 w-4 text-neutral-500" />
<h2 className="text-base font-medium text-neutral-900"></h2> <h2 className="text-base font-medium text-neutral-900"></h2>
</div> </div>
<ScrollArea className="h-[800px] w-full"> <ScrollArea className="w-full sm:h-[480px] md:h-[720px] lg:h-[92%]">
{/*<div className="w-full flex-1 overflow-y-auto">*/} {/*<div className="w-full flex-1 overflow-y-auto">*/}
<NoteHistory onSelect={setCurrentTask} selectedId={currentTaskId} /> <NoteHistory onSelect={setCurrentTask} selectedId={currentTaskId} />
{/*</div>*/} {/*</div>*/}

View File

@@ -34,6 +34,7 @@ import NoteHistory from '@/pages/HomePage/components/NoteHistory.tsx'
import { useModelStore } from '@/store/modelStore' import { useModelStore } from '@/store/modelStore'
import { Alert } from 'antd' import { Alert } from 'antd'
import { Textarea } from '@/components/ui/textarea.tsx' import { Textarea } from '@/components/ui/textarea.tsx'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
// ✅ 定义表单 schema // ✅ 定义表单 schema
const formSchema = z.object({ const formSchema = z.object({
video_url: z.string().url('请输入正确的视频链接'), video_url: z.string().url('请输入正确的视频链接'),
@@ -150,263 +151,269 @@ const NoteForm = () => {
}, []) }, [])
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full w-full flex-col overflow-hidden p-4">
<Form {...form}> <div className="flex w-full items-center gap-2 py-1.5">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <Button type="submit" className="bg-primary w-full sm:w-full" disabled={isGenerating()}>
<div className="space-y-2"> {isGenerating() && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<div className="my-3 flex items-center justify-between"> {isGenerating() ? '正在生成…' : '生成笔记'}
<h2 className="block"></h2> </Button>
<TooltipProvider> </div>
<Tooltip> <ScrollArea className="sm:h-[400px] md:h-[800px]">
<TooltipTrigger asChild> <Form {...form}>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" /> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
</TooltipTrigger> <div className="space-y-2">
<TooltipContent> <div className="my-3 flex items-center justify-between">
<p className="text-xs">YouTube等平台</p> <h2 className="block"></h2>
</TooltipContent> <TooltipProvider>
</Tooltip> <Tooltip>
</TooltipProvider> <TooltipTrigger asChild>
</div> <Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">YouTube等平台</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex gap-2"> <div className="flex gap-2">
{/* 平台选择 */} {/* 平台选择 */}
<FormField
control={form.control}
name="platform"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-32">
<SelectValue placeholder="选择平台" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="bilibili"></SelectItem>
<SelectItem value="youtube">Youtube</SelectItem>
{/*<SelectItem value="local">本地视频</SelectItem>*/}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* 视频地址 */}
<FormField
control={form.control}
name="video_url"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input placeholder="视频链接" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/*<p className="text-xs text-neutral-500">*/}
{/* 支持哔哩哔哩视频链接,例如:*/}
{/* https://www.bilibili.com/video/BV1vc25YQE9X/*/}
{/*</p>*/}
<FormField
control={form.control}
name="quality"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs"></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择质量" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="fast"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="slow"></SelectItem>
</SelectContent>
</Select>
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="model_name"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs"></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择配置好的模型" />
</SelectTrigger>
</FormControl>
<SelectContent>
{modelList.map(item => {
return <SelectItem value={item.model_name}>{item.model_name}</SelectItem>
})}
</SelectContent>
</Select>
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="style"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs"></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择笔记风格" />
</SelectTrigger>
</FormControl>
<SelectContent>
{noteStyles.map(item => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="format"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs"></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<div className="flex flex-wrap space-x-1.5">
{noteFormats.map(item => (
<label key={item.value} className="flex items-center space-x-2">
<Checkbox
checked={field.value?.includes(item.value)}
onCheckedChange={checked => {
const currentValue = field.value ?? [] // ✨ 保底是数组
if (checked) {
field.onChange([...currentValue, item.value])
} else {
field.onChange(currentValue.filter(v => v !== item.value))
}
}}
/>
<span>{item.label}</span>
</label>
))}
</div>
</FormControl>
<FormField <FormField
control={form.control} control={form.control}
name="extras" name="platform"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<div className="my-3 flex items-center justify-between"> <Select onValueChange={field.onChange} defaultValue={field.value}>
<h2 className="block"></h2> <FormControl>
<TooltipProvider> <SelectTrigger className="w-32">
<Tooltip> <SelectValue placeholder="选择平台" />
<TooltipTrigger asChild> </SelectTrigger>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" /> </FormControl>
</TooltipTrigger> <SelectContent>
<TooltipContent> <SelectItem value="bilibili"></SelectItem>
<p className="text-xs">Prompt最后 </p> <SelectItem value="youtube">Youtube</SelectItem>
</TooltipContent> {/*<SelectItem value="local">本地视频</SelectItem>*/}
</Tooltip> </SelectContent>
</TooltipProvider> </Select>
</div>
<Textarea placeholder={'笔记需要罗列出 xxx 关键点'} {...field} />
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormMessage /> {/* 视频地址 */}
</FormItem> <FormField
)} control={form.control}
/> name="video_url"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input placeholder="视频链接" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/*<p className="text-xs text-neutral-500">*/}
{/* 支持哔哩哔哩视频链接,例如:*/}
{/* https://www.bilibili.com/video/BV1vc25YQE9X/*/}
{/*</p>*/}
<FormField
control={form.control}
name="quality"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs">
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择质量" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="fast"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="slow"></SelectItem>
</SelectContent>
</Select>
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
<div className={'flex w-full items-center gap-2 py-1.5'}> <FormField
{/* 提交按钮 */} control={form.control}
<Button type="submit" className="bg-primary w-full" disabled={isGenerating()}> name="model_name"
{isGenerating() && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} render={({ field }) => (
{isGenerating() ? '正在生成…' : '生成笔记'} <FormItem>
</Button> <div className="my-3 flex items-center justify-between">
</div> <h2 className="block"></h2>
</form> <TooltipProvider>
</Form> <Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs">
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择配置好的模型" />
</SelectTrigger>
</FormControl>
<SelectContent>
{modelList.map(item => {
return <SelectItem value={item.model_name}>{item.model_name}</SelectItem>
})}
</SelectContent>
</Select>
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="style"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs"></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择笔记风格" />
</SelectTrigger>
</FormControl>
<SelectContent>
{noteStyles.map(item => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="format"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<div className="flex flex-wrap space-x-1.5">
{noteFormats.map(item => (
<label key={item.value} className="flex items-center space-x-2">
<Checkbox
checked={field.value?.includes(item.value)}
onCheckedChange={checked => {
const currentValue = field.value ?? [] // ✨ 保底是数组
if (checked) {
field.onChange([...currentValue, item.value])
} else {
field.onChange(currentValue.filter(v => v !== item.value))
}
}}
/>
<span>{item.label}</span>
</label>
))}
</div>
</FormControl>
<FormField
control={form.control}
name="extras"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">Prompt最后 </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Textarea placeholder={'笔记需要罗列出 xxx 关键点'} {...field} />
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
{/* 添加一些额外的说明或功能介绍 */} {/* 添加一些额外的说明或功能介绍 */}

View File

@@ -31,7 +31,7 @@ const NoteHistory: FC<NoteHistoryProps> = ({ onSelect, selectedId }) => {
return ( return (
<> <>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 overflow-hidden">
{tasks.map(task => ( {tasks.map(task => (
<div <div
className={cn( className={cn(