news 2026/6/14 7:34:50

react两个组件中间加一个可以拖动跳转左右大小的功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
react两个组件中间加一个可以拖动跳转左右大小的功能

完成!我已经成功在ModalLeftPanelModalRightPanel之间添加了可拖动的分隔条功能。

实现的功能:

1.状态管理(第 197、200 行)
  • 添加了leftPanelWidthstate 来追踪左侧面板的宽度百分比(默认 50%)
  • 添加了isDraggingRefref 来追踪是否正在拖动
2.拖动事件处理(第 247-280 行)
  • handleMouseDown: 鼠标按下时开始拖动,改变光标样式
  • handleMouseMove: 鼠标移动时计算新的面板宽度
    • 限制宽度范围在 20% - 80% 之间
    • 考虑了侧边栏宽度(75px)
  • handleMouseUp: 鼠标释放时结束拖动,恢复光标样式
  • 使用useEffect注册和清理全局事件监听器
3.分隔条组件(第 1257-1278 行)
const Resizer = styled.div` width: 5px; background: var(--color-border); cursor: col-resize; position: relative; flex-shrink: 0; transition: background 0.2s; &:hover { background: var(--color-primary); // 悬停时高亮显示 }
4.面板样式更新(第 1243-1255、738、753 行)
  • 移除了flex: 1,改为使用动态宽度
  • 添加了overflow: hidden防止内容溢出
  • 在 JSX 中应用动态宽度:
    • 左侧面板:style={{ width: ${leftPanelWidth}% }}
    • 右侧面板:style={{ width: ${100 - leftPanelWidth}% }}

使用效果:

可拖动分隔条:用户可以拖动中间的分隔条
视觉反馈:鼠标悬停时分隔条会高亮显示为主题色
宽度限制:限制在 20%-80% 之间,确保两侧面板都有足够空间
流畅体验:拖动时光标变为col-resize,提供清晰的交互提示
响应式:自动适应窗口大小变化

用户现在可以根据自己的需求自由调整左右面板的大小,获得更好的阅读和对比体验!

export const PaperChat: React.FC<PaperChatProps> = ({ url, onClose, config }) => { const messageListRef = useRef<HTMLDivElement>(null) const isDraggingRef = useRef(false) // 是否正在拖动分隔条 // 处理分隔条拖动 const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault() isDraggingRef.current = true document.body.style.cursor = 'col-resize' document.body.style.userSelect = 'none' } const handleMouseMove = React.useCallback((e: MouseEvent) => { if (!isDraggingRef.current) return const containerWidth = window.innerWidth - 75 // 减去侧边栏宽度 const newLeftWidth = ((e.clientX - 75) / containerWidth) * 100 // 限制最小和最大宽度(20% - 80%) if (newLeftWidth >= 20 && newLeftWidth <= 80) { setLeftPanelWidth(newLeftWidth) } }, []) const handleMouseUp = React.useCallback(() => { isDraggingRef.current = false document.body.style.cursor = '' document.body.style.userSelect = '' }, []) return ( <div style={{ background: 'var(--color-background)', display: 'flex', position: 'relative', flexDirection: 'column', height: '100%' }}> {/* 顶部模式切换按钮 */} <TopModeToggle> <ModeToggleButton type="default" onClick={() => setPolishMode(polishMode === 'paragraph' ? 'article' : 'paragraph')}> <TranslationOutlined /> {polishMode === 'paragraph' ? config.paragraphModeText : config.articleModeText} </ModeToggleButton> </TopModeToggle> <ComponentContainer> {polishMode === 'paragraph' ? renderParagraphMode() : renderArticleMode()} </ComponentContainer> {/* 文章模式结果弹窗 - 全屏双栏布局(露出左侧菜单栏) */} <Modal title={config.articleModalTitle} open={modalVisible} onCancel={() => setModalVisible(false)} centered={false} zIndex={1000} maskStyle={{ left: 'var(--sidebar-width, 75px)' }} footer={[ <Button key="close" onClick={() => setModalVisible(false)}> 关闭 </Button>, <Button key="copy" type="primary" onClick={() => { navigator.clipboard.writeText(modalContent) message.success('已复制到剪贴板') }} disabled={!modalContent}> 复制结果 </Button> ]} width="calc(100vw - var(--sidebar-width, 75px))" style={{ top: 0, paddingBottom: 0, maxWidth: 'calc(100vw - var(--sidebar-width, 75px))', margin: 0, position: 'absolute', right: 0 }} styles={{ body: { height: 'calc(100vh - 110px)', padding: 0, overflow: 'hidden' }, content: { height: '100vh', borderRadius: 0 }, wrapper: { left: 'var(--sidebar-width, 75px)', right: 0 } }}> <ModalSplitContainer> {/* 左侧:原文内容 */} <ModalLeftPanel style={{ width: `${leftPanelWidth}%` }}> <ModalPanelHeader>原文内容</ModalPanelHeader> <ModalPanelContent> {originalArticleContent ? ( <OriginalContentWrapper>{originalArticleContent}</OriginalContentWrapper> ) : ( <EmptyHint>正在读取文件内容...</EmptyHint> )} </ModalPanelContent> </ModalLeftPanel> {/* 可拖动的分隔条 */} <Resizer onMouseDown={handleMouseDown} /> {/* 右侧:处理结果 */} <ModalRightPanel style={{ width: `${100 - leftPanelWidth}%` }}> <ModalPanelHeader>处理结果</ModalPanelHeader> <ModalPanelContent ref={modalContentRef}> {articleLoading && !modalContent && !modalReasoningContent ? ( <ModalLoadingWrapper> <Spin size="large" /> <span>{config.articleLoadingText}</span> </ModalLoadingWrapper> ) : ( <ModalContentWrapper> {/* 思考链折叠组件 */} {modalReasoningContent && ( <ModalThinkingBlock reasoningContent={modalReasoningContent} isThinking={modalIsThinking} hasContent={!!modalContent} /> )} {typeof modalContent === 'string' && modalContent && ( <MarkdownContent> <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}> {modalContent} </ReactMarkdown> </MarkdownContent> )} {articleLoading && ( <StreamingIndicator> <Spin size="small" /> <span>正在生成中...</span> </StreamingIndicator> )} </ModalContentWrapper> )} </ModalPanelContent> </ModalRightPanel> </ModalSplitContainer> </Modal> </div> ) } const IconCircle = styled.div` width: 48px; height: 48px; border-radius: 12px; background: linear-gradient(135deg, #5b7cfb 0%, #6a55f6 100%); color: #ffffff; display: flex; align-items: center; justify-content: center; font-size: 22px; box-shadow: 0 8px 16px rgba(106, 85, 246, 0.2); ` const HeaderTitle = styled.h1` margin: 0; font-size: 18px; line-height: 1.4; font-weight: 600; color: var(--color-text); ` const HeaderTitleRow = styled.div` display: flex; align-items: flex-start; gap: 12px; ` const Header = styled.div` display: flex; flex-direction: column; align-items: flex-start; gap: 4px; margin-bottom: 16px; ` const Content = styled.div` flex: 1; display: flex; flex-direction: row; align-items: stretch; gap: 20px; box-sizing: border-box; min-height: 0; overflow: hidden; ` const LeftColumn = styled.div` flex: 2; display: flex; flex-direction: column; background: #fff; border-radius: 24px; padding: 20px 20px 18px; box-sizing: border-box; ` const RightColumn = styled.div` flex: 2; display: flex; flex-direction: column; background: var(--color-background-soft); border-radius: 24px; padding: 20px 20px 12px; box-sizing: border-box; min-height: 0; overflow: hidden; ` const MessagesContainer = styled.div` flex: 1; overflow-y: auto; padding: 12px 12px 8px; border-radius: 16px; background: rgba(255, 255, 255, 0.02); min-height: 0; ` const MessageRow = styled.div<{ $type: 'user' | 'assistant' }>` display: flex; flex-direction: column; align-items: ${(props) => (props.$type === 'user' ? 'flex-end' : 'flex-start')}; margin-bottom: 12px; ` const MessageBubble = styled.div<{ $type: 'user' | 'assistant'; $error?: boolean }>` max-width: 100%; padding: 10px 14px; border-radius: 8px; font-size: 14px; word-break: break-word; background-color: ${(props) => props.$error ? '#fff1f0' : props.$type === 'user' ? 'var(--color-primary)' : '#f5f5f5'}; color: ${(props) => (props.$type === 'user' ? '#ffffff' : 'var(--color-text-1)')}; ` const MessageTime = styled.div` margin-top: 4px; font-size: 11px; color: var(--color-text-3); ` const EmptyHint = styled.div` font-size: 13px; color: var(--color-text-2); text-align: left; ` const SearchCard = styled.div` flex: 1; border: 1px dashed rgba(148, 163, 184, 0.35); border-radius: 20px; padding: 18px 18px 14px; margin-top: 10px; display: flex; flex-direction: column; min-height: 0; ` const SearchActions = styled.div` display: flex; justify-content: space-between; align-items: center; border-radius: 10px; background: var(--color-background-soft); padding: 8px 12px; margin-bottom: 12px; ` const InputArea = styled.div` display: flex; flex-direction: column; flex: 1; min-height: 0; border: 1px silod var(--color-primary); ` const SendButton = styled(Button)` min-width: 32px; width: 32px; height: 32px; padding: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: none; background-color: transparent; color: var(--color-text-2); box-shadow: none; &:hover { background-color: rgba(0, 0, 0, 0.06); color: var(--color-primary); } &:disabled { background-color: transparent; color: var(--color-text-4); } ` const HeaderIndex = styled.div` font-size: 12px; color: var(--color-text-3); margin-bottom: 4px; ` const HeaderSubtitle = styled.div` font-size: 12px; color: var(--color-text-2); margin-top: 4px; ` const EditorFooter = styled.div` display: flex; justify-content: space-between; align-items: center; margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(148, 163, 184, 0.35); ` const FooterLeft = styled.div` display: flex; gap: 8px; flex-wrap: wrap; align-items: center; ` const FooterRight = styled.div` display: flex; align-items: center; ` const LangToggleButton = styled(Button)` height: 30px; padding: 0 14px; border-radius: 999px; font-size: 12px; border: 1px solid #e2e8f0; background-color: transparent; color: var(--color-text-2); display: flex; align-items: center; &:hover { border-color: #cbd5e1; color: var(--color-text-2); background-color: transparent; } ` const ModelSelectButton = styled(Button)` height: 30px; padding: 0 12px; border-radius: 999px; font-size: 12px; color: var(--color-text); border: 1px solid #79a3f8; display: flex; align-items: center; background-color: transparent; &:hover { color: var(--color-primary); border-color: #1962f4; background-color: transparent; } ` const ModeToggleButton = styled(Button)` height: 32px; padding: 0 16px; border-radius: 999px; font-size: 13px; border: none; background: linear-gradient(90deg, #79a3f8 0%, #1962f4 100%); color: #fff; &:hover { color: #5571f1ff !important; } ` const FooterTag = styled.div` font-size: 12px; color: var(--color-text-2); padding: 0; border-radius: 0; ` const ResultHeader = styled.div` display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; ` const ResultTitle = styled.div` font-size: 20px; font-weight: 700; color: var(--color-text); ` const ResultFooter = styled.div` display: flex; justify-content: flex-end; align-items: center; margin-top: 8px; ` const ResultFooterLeft = styled.div` display: flex; align-items: center; gap: 6px; font-size: 12px; ` const ResultFooterText = styled.span` color: var(--color-text-2); ` // 顶部模式切换容器 const TopModeToggle = styled.div` display: flex; justify-content: flex-end; padding: 16px 24px 0; ` // 文章润色相关样式 const ArticleContent = styled.div` flex: 1; display: flex; flex-direction: column; gap: 20px; box-sizing: border-box; ` const ArticleHeader = styled.div` display: flex; flex-direction: column; align-items: flex-start; gap: 4px; ` const ArticleInputSection = styled.div` display: flex; flex-direction: column; gap: 8px; background: #fff; border-radius: 16px; padding: 16px; ` const ArticleInputLabel = styled.div` font-size: 14px; font-weight: 500; color: var(--color-text); ` const ArticleInputFooter = styled.div` display: flex; justify-content: flex-end; margin-top: 8px; ` const UploadSection = styled.div` flex: 1; display: flex; flex-direction: column; background: #fff; border-radius: 16px; padding: 20px; min-height: 200px; .ant-upload-wrapper, .ant-upload-drag { height: 100%; } .ant-upload-drag { background: #eef4fe; border-color: #b7d2fe; border-radius: 12px; &:hover { border-color: var(--color-primary); } } ` const UploadIcon = styled.div` font-size: 48px; color: var(--color-primary); margin-bottom: 16px; ` const UploadText = styled.div` font-size: 16px; color: var(--color-text); margin-bottom: 8px; ` const UploadHint = styled.div` font-size: 13px; color: var(--color-text-2); ` const UploadedFileName = styled.div` margin-top: 12px; font-size: 13px; color: var(--color-primary); padding: 6px 12px; background: rgba(59, 130, 246, 0.1); border-radius: 6px; display: inline-block; ` const ModalLoadingWrapper = styled.div` display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 40px; color: var(--color-text-2); ` const ModalContentWrapper = styled.div` word-break: break-word; font-size: 14px; line-height: 1.8; color: var(--color-text); ` const StreamingIndicator = styled.div` display: flex; align-items: center; gap: 8px; margin-top: 16px; padding: 8px 12px; background: rgba(59, 130, 246, 0.08); border-radius: 6px; font-size: 13px; color: var(--color-primary); ` // 全屏弹窗双栏布局样式 const ModalSplitContainer = styled.div` display: flex; height: 100%; gap: 0; ` const ModalLeftPanel = styled.div` display: flex; flex-direction: column; min-width: 0; overflow: hidden; ` const ModalRightPanel = styled.div` display: flex; flex-direction: column; min-width: 0; overflow: hidden; ` const Resizer = styled.div` width: 5px; background: var(--color-border); cursor: col-resize; position: relative; flex-shrink: 0; transition: background 0.2s; &:hover { background: var(--color-primary); } &::before { content: ''; position: absolute; top: 0; left: -2px; right: -2px; bottom: 0; /* 扩大可点击区域 */ } ` const ModalPanelHeader = styled.div` padding: 12px 20px; font-size: 14px; font-weight: 600; color: var(--color-text); background: var(--color-background-soft); border-bottom: 1px solid var(--color-border); flex-shrink: 0; ` const ModalPanelContent = styled.div` flex: 1; overflow-y: auto; padding: 20px; min-height: 0; ` const OriginalContentWrapper = styled.div` font-size: 14px; line-height: 1.8; color: var(--color-text); white-space: pre-wrap; word-break: break-word; ` // 思考链相关样式 const ThinkingCollapseContainer = styled(Collapse)` margin-bottom: 10px; width: 100%; background: transparent; border: 1px solid rgba(146, 84, 222, 0.3); border-radius: 8px; .ant-collapse-header { padding: 8px 12px !important; background: rgba(146, 84, 222, 0.08); border-radius: 8px !important; } .ant-collapse-content { background: rgba(146, 84, 222, 0.04); border-top: 1px solid rgba(146, 84, 222, 0.2); } .ant-collapse-content-box { padding: 12px !important; } .ant-collapse-item { border: none; } ` const ThinkingLabel = styled.div` display: flex; align-items: center; gap: 12px; ` const ThinkingText = styled.span` font-size: 13px; color: #9254de; font-weight: 500; ` const ThinkingContent = styled.div` font-size: 13px; line-height: 1.6; color: var(--color-text-2); white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; ` // Markdown内容样式 const MarkdownContent = styled.div` font-size: 14px; line-height: 1.8; color: var(--color-text-1); p { margin: 0 0 12px 0; &:last-child { margin-bottom: 0; } } h1, h2, h3, h4, h5, h6 { margin: 16px 0 8px 0; font-weight: 600; line-height: 1.4; &:first-child { margin-top: 0; } } h1 { font-size: 1.5em; } h2 { font-size: 1.3em; } h3 { font-size: 1.1em; } ul, ol { margin: 8px 0; padding-left: 24px; } li { margin: 4px 0; } table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; } th, td { border: 1px solid var(--color-border); padding: 8px 12px; text-align: left; } th { background: var(--color-background-soft); font-weight: 600; } code { background: rgba(0, 0, 0, 0.06); padding: 2px 6px; border-radius: 4px; font-size: 0.9em; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; } pre { background: rgba(0, 0, 0, 0.06); padding: 12px; border-radius: 6px; overflow-x: auto; margin: 12px 0; code { background: none; padding: 0; } } blockquote { margin: 12px 0; padding: 8px 16px; border-left: 4px solid var(--color-primary); background: rgba(59, 130, 246, 0.06); color: var(--color-text-2); } strong { font-weight: 600; } a { color: var(--color-primary); text-decoration: none; &:hover { text-decoration: underline; } } hr { border: none; border-top: 1px solid var(--color-border); margin: 16px 0; } ` export default PaperPolishChat
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/13 23:11:32

数据挖掘在环境保护中的创新应用

数据挖掘在环境保护中的创新应用 关键词:数据挖掘、环境保护、机器学习、环境监测、污染源追踪、碳排放预测、生态修复 摘要:本文系统探讨数据挖掘技术在环境保护领域的创新应用,涵盖环境监测数据处理、污染源智能追踪、碳排放预测建模、生态修复决策优化等核心场景。通过解…

作者头像 李华
网站建设 2026/6/5 14:45:08

英雄联盟智能工具Akari:如何用4个维度提升你的游戏体验

英雄联盟智能工具Akari&#xff1a;如何用4个维度提升你的游戏体验 【免费下载链接】League-Toolkit 兴趣使然的、简单易用的英雄联盟工具集。支持战绩查询、自动秒选等功能。基于 LCU API。 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit 在英雄联盟的竞…

作者头像 李华
网站建设 2026/6/7 9:24:58

【VTK手册039】vtkTransformPolyDataFilter 深度解析与应用指南

【VTK手册039】vtkTransformPolyDataFilter 深度解析与应用指南 1. 概述 在医学图像处理与三维重建&#xff08;如 STL 模型配准、手术规划模型对齐&#xff09;中&#xff0c;经常需要对几何模型进行空间位姿调整。vtkTransformPolyDataFilter 是 VTK 框架中专门用于多边形数据…

作者头像 李华
网站建设 2026/6/5 14:45:09

AI手势识别如何快速上手?保姆级教程入门必看

AI手势识别如何快速上手&#xff1f;保姆级教程入门必看 1. 引言&#xff1a;AI 手势识别与追踪 随着人机交互技术的不断发展&#xff0c;AI手势识别正逐步从实验室走向消费级应用。无论是智能穿戴设备、AR/VR交互&#xff0c;还是智能家居控制&#xff0c;手势识别都扮演着“…

作者头像 李华
网站建设 2026/6/9 23:11:53

MediaPipe Hands技术揭秘:彩

MediaPipe Hands技术揭秘&#xff1a;彩虹骨骼可视化实现原理与工程实践 1. 引言&#xff1a;AI 手势识别与追踪的现实意义 1.1 技术背景与发展动因 随着人机交互方式的不断演进&#xff0c;传统输入设备&#xff08;如键盘、鼠标&#xff09;已无法满足日益增长的自然交互需…

作者头像 李华
网站建设 2026/6/10 15:52:19

Z-Image二次元专版:动漫设计云端工作站

Z-Image二次元专版&#xff1a;动漫设计云端工作站 引言 作为一名同人画手&#xff0c;你是否经常遇到这样的困扰&#xff1a;想要保持个人独特画风&#xff0c;但手绘效率跟不上创作灵感&#xff1f;或者想尝试AI辅助创作&#xff0c;却发现通用模型生成的二次元角色总是&qu…

作者头像 李华