feat: control layout

This commit is contained in:
siyue
2024-10-31 23:40:17 +08:00
parent 6ae4b1bf9a
commit 6868e9619f
5 changed files with 328 additions and 83 deletions
@@ -78,10 +78,6 @@ interface MenuButtonProps {
tooltip?: string;
}
const handleToolbarClick = (e: React.MouseEvent) => {
e.stopPropagation();
};
const MenuButton = ({
onClick,
isActive = false,
@@ -93,9 +89,16 @@ const MenuButton = ({
const theme = useResumeStore((state) => state.theme);
const [showTooltip, setShowTooltip] = React.useState(false);
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onClick();
};
return (
<div className="relative">
<div className="relative" onClick={(e) => e.stopPropagation()}>
<Button
onMouseDown={(e) => e.preventDefault()}
variant={isActive ? "secondary" : "ghost"}
size="sm"
className={cn(
@@ -110,7 +113,7 @@ const MenuButton = ({
disabled ? "opacity-50" : "",
className
)}
onClick={onClick}
onClick={handleClick}
disabled={disabled}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
@@ -427,6 +430,7 @@ const RichTextEditor = ({
? "bg-neutral-900/30 border-neutral-800"
: "bg-card border-gray-100"
)}
onClick={(e) => e.stopPropagation()}
>
{/* Toolbar */}
<div
@@ -436,7 +440,6 @@ const RichTextEditor = ({
? "bg-neutral-900/50 border-neutral-800"
: "bg-background"
)}
onClick={handleToolbarClick}
>
<div className="flex items-center">
<HeadingSelect editor={editor} />
+274 -44
View File
@@ -1,8 +1,16 @@
"use client";
import { useRef, useState } from "react";
import { useRef, useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Eye, Edit2, Menu } from "lucide-react";
import {
Eye,
Edit2,
Menu,
PanelLeft,
PanelRight,
Maximize2,
Minimize2
} from "lucide-react";
import { useResumeStore } from "@/store/useResumeStore";
import { EditorHeader } from "@/components/editor/EditorHeader";
import { SidePanel } from "@/components/editor/SidePanel";
@@ -16,39 +24,228 @@ import {
ResizablePanelGroup
} from "@/components/ui/resizable";
import { cn } from "@/lib/utils";
import CustomHandle from "./compoents/CustomHandle";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/components/ui/tooltip";
const LAYOUT_CONFIG = {
DEFAULT: [20, 35, 45],
SIDE_COLLAPSED: [0, 50, 50],
EDIT_FOCUSED_WITH_SIDE: [0, 100, 0],
PREVIEW_FOCUSED_WITH_SIDE: [0, 0, 100],
EDIT_FOCUSED_NO_SIDE: [0, 100, 0],
PREVIEW_FOCUSED_NO_SIDE: [0, 0, 100]
};
// 创建自定义拖拽手柄组件
const DragHandle = ({ show = true }) => {
const theme = useResumeStore((state) => state.theme);
if (!show) return null;
return (
<ResizableHandle className="relative w-1.5 group">
<div
className={cn(
"absolute inset-y-0 left-1/2 w-1 -translate-x-1/2",
"group-hover:bg-primary/20 group-data-[dragging=true]:bg-primary",
theme === "dark" ? "bg-neutral-700/50" : "bg-gray-200"
)}
/>
<div
className={cn(
"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
"w-4 h-8 rounded-full opacity-0 group-hover:opacity-100",
"flex items-center justify-center",
theme === "dark" ? "bg-neutral-800" : "bg-gray-200"
)}
>
<div className="w-0.5 h-4 bg-gray-400 rounded-full" />
</div>
</ResizableHandle>
);
};
export default function Home() {
const theme = useResumeStore((state) => state.theme);
const previewRef = useRef<HTMLDivElement>(null);
const themeConfig = getThemeConfig(theme === "dark");
// 移动端状态管理
const [showSidebar, setShowSidebar] = useState(false);
const [isPreviewMode, setIsPreviewMode] = useState(false);
const [sidePanelCollapsed, setSidePanelCollapsed] = useState(false);
const [focusedPanel, setFocusedPanel] = useState<null | "edit" | "preview">(
null
);
const [showMobileSidebar, setShowMobileSidebar] = useState(false);
const [isMobilePreview, setIsMobilePreview] = useState(false);
const [panelSizes, setPanelSizes] = useState(LAYOUT_CONFIG.DEFAULT);
const [layoutKey, setLayoutKey] = useState(0);
useScrollbarTheme();
const updateLayout = (newSizes: number[]) => {
setPanelSizes(newSizes);
setLayoutKey((prev) => prev + 1);
};
const [defaultLayout] = useState([15, 40, 50]);
useEffect(() => {
let newSizes;
if (sidePanelCollapsed) {
if (focusedPanel === "edit") {
newSizes = [...LAYOUT_CONFIG.EDIT_FOCUSED_NO_SIDE];
} else if (focusedPanel === "preview") {
newSizes = [...LAYOUT_CONFIG.PREVIEW_FOCUSED_NO_SIDE];
} else {
newSizes = [...LAYOUT_CONFIG.SIDE_COLLAPSED];
}
} else {
if (focusedPanel === "edit") {
newSizes = [...LAYOUT_CONFIG.EDIT_FOCUSED_WITH_SIDE];
} else if (focusedPanel === "preview") {
newSizes = [...LAYOUT_CONFIG.PREVIEW_FOCUSED_WITH_SIDE];
} else {
newSizes = [...LAYOUT_CONFIG.DEFAULT];
}
}
updateLayout([...newSizes]);
}, [sidePanelCollapsed, focusedPanel]);
// 移动端切换按钮
const MobileNav = () => (
<div className={`fixed bottom-6 right-6 flex gap-2 md:hidden z-50`}>
const toggleSidePanel = () => {
setSidePanelCollapsed(!sidePanelCollapsed);
};
const togglePanelFocus = (panel: "edit" | "preview") => {
setFocusedPanel(focusedPanel === panel ? null : panel);
};
// 底部控制栏
const LayoutControls = () => (
<div
className={cn(
"absolute bottom-6 left-1/2 -translate-x-1/2",
"flex items-center gap-2 z-10 p-2 rounded-full",
theme === "dark"
? "bg-neutral-900/80 border border-neutral-800"
: "bg-white/80 border border-gray-200",
"backdrop-blur-sm shadow-lg"
)}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={sidePanelCollapsed ? "secondary" : "ghost"}
size="icon"
className="h-9 w-9 rounded-full"
onClick={toggleSidePanel}
>
<PanelLeft className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
{sidePanelCollapsed ? "展开侧边栏" : "收起侧边栏"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div
className={cn(
"h-5 w-px mx-1",
theme === "dark" ? "bg-neutral-800" : "bg-gray-200"
)}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={focusedPanel === "edit" ? "secondary" : "ghost"}
size="icon"
className="h-9 w-9 rounded-full"
onClick={() => togglePanelFocus("edit")}
>
{focusedPanel === "edit" ? (
<Minimize2 className="h-4 w-4" />
) : (
<Edit2 className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
{focusedPanel === "edit" ? "退出编辑模式" : "专注编辑"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={focusedPanel === "preview" ? "secondary" : "ghost"}
size="icon"
className="h-9 w-9 rounded-full"
onClick={() => togglePanelFocus("preview")}
>
{focusedPanel === "preview" ? (
<Minimize2 className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
{focusedPanel === "preview" ? "退出预览模式" : "专注预览"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
// 移动端控制栏
const MobileControls = () => (
<div
className={cn(
"fixed bottom-6 right-6 flex gap-2 md:hidden z-50",
"p-2 rounded-full",
theme === "dark"
? "bg-neutral-900/80 border border-neutral-800"
: "bg-white/80 border border-gray-200",
"backdrop-blur-sm shadow-lg"
)}
>
<motion.button
className={`p-3 rounded-full ${themeConfig.buttonPrimary} text-white shadow-lg`}
className={cn(
"p-3 rounded-full",
theme === "dark" ? "hover:bg-neutral-800" : "hover:bg-gray-100"
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setShowSidebar(true)}
onClick={() => setShowMobileSidebar(true)}
>
<Menu size={24} />
<Menu className="h-5 w-5" />
</motion.button>
<motion.button
className={`p-3 rounded-full ${themeConfig.buttonPrimary} text-white shadow-lg`}
className={cn(
"p-3 rounded-full",
theme === "dark" ? "hover:bg-neutral-800" : "hover:bg-gray-100"
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setIsPreviewMode(!isPreviewMode)}
onClick={() => setIsMobilePreview(!isMobilePreview)}
>
{isPreviewMode ? <Edit2 size={24} /> : <Eye size={24} />}
{isMobilePreview ? (
<Edit2 className="h-5 w-5" />
) : (
<Eye className="h-5 w-5" />
)}
</motion.button>
</div>
);
@@ -56,23 +253,26 @@ export default function Home() {
// 移动端侧边栏
const MobileSidebar = () => (
<AnimatePresence>
{showSidebar && (
{showMobileSidebar && (
<>
<motion.div
className="fixed inset-0 bg-black/50 z-50 md:hidden"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowSidebar(false)}
onClick={() => setShowMobileSidebar(false)}
/>
<motion.div
className="fixed top-0 left-0 h-full w-80 z-50 md:hidden"
className={cn(
"fixed top-0 left-0 h-full w-80 z-50 md:hidden",
theme === "dark" ? "bg-neutral-900" : "bg-white"
)}
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", damping: 20 }}
>
<SidePanel onClose={() => setShowSidebar(false)} />
<SidePanel onClose={() => setShowMobileSidebar(false)} />
</motion.div>
</>
)}
@@ -81,47 +281,77 @@ export default function Home() {
return (
<main
className={`w-full h-screen ${themeConfig.bg} ${themeConfig.text} overflow-hidden`}
className={cn(
"w-full h-screen overflow-hidden",
theme === "dark"
? "bg-neutral-900 text-neutral-200"
: "bg-white text-gray-900"
)}
>
<EditorHeader previewRef={previewRef} isMobile={true} />
<EditorHeader previewRef={previewRef} />
<div className="hidden md:block h-[calc(100vh-64px)] overflow-hidden">
{/* 桌面端布局 */}
{/* 桌面端布局 */}
<div className="hidden md:block h-[calc(100vh-64px)]">
<ResizablePanelGroup
key={layoutKey}
direction="horizontal"
className="h-full rounded-lg border"
className={cn(
"h-full rounded-lg",
theme === "dark"
? "border-neutral-800 bg-neutral-900/50"
: "border border-gray-200 bg-white"
)}
>
{/* 侧边栏面板 */}
{!sidePanelCollapsed && (
<>
<ResizablePanel
defaultSize={panelSizes[0]}
minSize={20}
className={cn(
theme === "dark"
? "bg-neutral-900 border-r border-neutral-800"
: ""
)}
>
<div className="h-full overflow-y-auto">
<SidePanel />
</div>
</ResizablePanel>
<DragHandle />
</>
)}
{/* 编辑面板 */}
<ResizablePanel
defaultSize={defaultLayout[0]}
minSize={15}
maxSize={30}
defaultSize={panelSizes[1]}
className={cn(
theme === "dark"
? "bg-neutral-900 border-r border-neutral-800"
: ""
)}
>
<div className="h-full overflow-y-auto">
<SidePanel />
</div>
</ResizablePanel>
<CustomHandle />
<ResizablePanel
minSize={20}
maxSize={30}
defaultSize={defaultLayout[1]}
>
<div className="h-full overflow-y-auto">
<div className="h-full">
<EditPanel />
</div>
</ResizablePanel>
<CustomHandle />
<ResizablePanel defaultSize={defaultLayout[2]} minSize={50}>
<DragHandle />
{/* 预览面板 */}
<ResizablePanel defaultSize={panelSizes[2]} className="bg-gray-100">
<div className="h-full overflow-y-auto">
<PreviewPanel />
</div>
</ResizablePanel>
</ResizablePanelGroup>
<LayoutControls />
</div>
{/* 移动端布局 */}
<div className="md:hidden h-[calc(100vh-64px)] overflow-hidden">
<div className="md:hidden h-[calc(100vh-64px)]">
<AnimatePresence mode="wait">
{isPreviewMode ? (
{isMobilePreview ? (
<motion.div
key="preview"
initial={{ opacity: 0, x: 300 }}
@@ -147,7 +377,7 @@ export default function Home() {
</AnimatePresence>
</div>
<MobileNav />
<MobileControls />
<MobileSidebar />
</main>
);
@@ -30,11 +30,11 @@ export function EditorHeader({ previewRef, isMobile }: EditorHeaderProps) {
<div className="flex items-center justify-between px-6 h-full">
<div className="flex items-center space-x-6 scrollbar-hide">
<motion.div
className="flex items-center space-x-2 shrink-0"
className="flex items-center space-x-2 shrink-0 cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<span className="text-lg font-semibold">Resume</span>
<span className="text-lg font-semibold">Magic Resume</span>
<div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
</motion.div>
@@ -76,15 +76,6 @@ export function EditorHeader({ previewRef, isMobile }: EditorHeaderProps) {
{/* 在移动端隐藏这些按钮 */}
<div className="hidden md:flex items-center space-x-3">
<motion.button
className={`px-4 py-2 ${themeConfig.button} rounded-lg text-sm font-medium flex items-center space-x-2`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Eye className="w-4 h-4" />
<span></span>
</motion.button>
<PdfExport />
</div>
</div>
@@ -467,7 +467,7 @@ export function PreviewPanel() {
return (
<motion.div
ref={previewRef}
className={cn("flex-1 overflow-y-auto", "bg-gray-100")}
className={cn("flex-1 overflow-y-auto")}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
@@ -296,6 +296,7 @@ export function SidePanel() {
</Select>
</div>
{/* 基础字号选择 */}
<div className="space-y-2">
<Label
className={cn(
@@ -304,30 +305,50 @@ export function SidePanel() {
>
</Label>
<div className="flex items-center gap-4">
<Slider
value={[globalSettings?.baseFontSize || 0]}
min={12}
max={100}
step={1}
onValueChange={([value]) =>
updateGlobalSettings?.({ baseFontSize: value })
}
<Select
value={globalSettings?.baseFontSize?.toString()}
onValueChange={(value) =>
updateGlobalSettings?.({ baseFontSize: parseInt(value) })
}
>
<motion.div
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
>
<SelectTrigger
className={cn(
"border transition-colors",
theme === "dark"
? "border-neutral-800 bg-neutral-900 text-neutral-200"
: "border-gray-200 bg-white text-gray-700"
)}
>
<SelectValue />
</SelectTrigger>
</motion.div>
<SelectContent
className={cn(
theme === "dark"
? "[&_[role=slider]]:bg-neutral-200"
: "[&_[role=slider]]:bg-gray-900"
)}
/>
<span
className={cn(
"min-w-[3ch] text-sm",
theme === "dark" ? "text-neutral-300" : "text-gray-600"
? "bg-neutral-900 border-neutral-800 text-[#fff] hover:text-white"
: "bg-white border-gray-200"
)}
>
{globalSettings?.baseFontSize}px
</span>
</div>
{[12, 13, 14, 15, 16, 18, 20, 24].map((size) => (
<SelectItem
key={size}
value={size.toString()}
className={cn(
"cursor-pointer",
theme === "dark"
? "focus:bg-neutral-800 hover:bg-neutral-800"
: "focus:bg-gray-100 hover:bg-gray-100"
)}
>
{size}px
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</SettingCard>