news 2026/9/29 20:16:56

div上下拖拽(resize)实战:用 CSS cursor 与 flex 布局打造可拖拽面板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
div上下拖拽(resize)实战:用 CSS cursor 与 flex 布局打造可拖拽面板

1. 从后台面板的“拖不动”说起

div 上下拖拽 resize 这件事,看起来只是把鼠标放到一条线上往下拉,但真动手写,问题一个接一个:鼠标移到分割线上不变形、拖到一半内容被盖住、松手后高度回弹、flex 容器里子元素把父级撑爆。我最近在做一个后台配置面板,左侧是树形目录,右侧是详情区,底部还要留一块日志输出,用户希望能像 IDE 那样上下拖动中间区域的高度。搜了一圈,发现大多数方案要么依赖第三方库,要么用resize: vertical这种 CSS3 属性凑合,但resize只在元素右下角生成一个小三角,根本没法让整条分割线都可拖。

这篇就聚焦纯前端实现:用 flex 布局搭骨架,用cursor: ns-resize给出上下拖拽的视觉反馈,用mousedown / mousemove / mouseup三件套绑定事件,再配合min-height / max-height做高度约束。适合正在写后台面板、代码编辑器、日志查看器、聊天窗口这类需要动态分配上下空间的开发者。读完你能直接复制一套 HTML/CSS/JS 骨架,在浏览器里跑起来,并且知道每个参数为什么这么设。

核心检索词先摆出来:div 上下拖拽、resize、css cursor、flex 布局、鼠标事件绑定、高度约束。这几个词会贯穿全文,每一步都对应一个可验证的结果。

2. 动手前先把 TaoToken 的 Key 和文档准备好

如果你只是本地写个 demo,其实不需要任何外部服务。但如果你打算把拖拽面板接到真实的大模型接口上,比如做一个可调高度的对话窗口、代码补全面板,那就需要一个稳定的 API 入口。我平时用 TaoToken 来统一管理模型调用,它的 API 地址是 https://taotoken.net/api,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。注意 API 地址后面不加任何 UTM 参数,直接请求即可。

接入前你需要先拿到 API Key。打开 https://taotoken.net/api-keys ,登录后创建一个新的 Key,复制保存。这个 Key 就是你后续在fetch或 SDK 里填的凭证。如果你用的是 Claude Code 这类编码工具,可以参考 https://taotoken.net/doc 里的接入说明,把 base URL 指向https://taotoken.net/api,模型名按文档里列出的填。

提示:Key 只显示一次,建议存到环境变量里,不要硬编码进前端代码。前端 demo 可以用一个后端代理转发,避免 Key 暴露。

准备好 Key 之后,你就可以把拖拽面板的“内容区”换成真实的模型对话或代码输出。拖拽本身是纯前端行为,和 API 无关,但两者结合才是完整场景。下面先专注拖拽骨架,最后再把它接到请求上。

3. 可复制的 HTML/CSS/JS 骨架

3.1 HTML 结构:三层分离

结构上我分成三层:外层 flex 容器、上方可拖拽区、下方固定区。上方区里再分内容层和拖拽手柄层。内容层用position: absolute独立出来,手柄层用透明 div 趴在后面控制高度。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>div 上下拖拽 resize 实战</title> <link rel="stylesheet" href="./resize.css" /> </head> <body> <div class="drawer__wrap" id="drawerWrap"> <!-- 上方可拖拽区域 --> <div class="top__info" id="topInfo"> <!-- 拖拽手柄:透明,负责接收鼠标事件 --> <div class="resize__bar" id="resizeBar"></div> <!-- 视觉分割线 --> <div class="resize__line" id="resizeLine"></div> <!-- 内容层:绝对定位,不受手柄影响 --> <div class="resize__save"> <div class="tree__info">左侧目录</div> <div class="node__info"> <div class="detail__title">右侧标题</div> <div class="detail__info">右侧详情内容,可以放模型输出</div> </div> </div> </div> <!-- 下方固定区域 --> <div class="bottom__info">下方日志区</div> </div> <script src="./resize.js"></script> </body> </html>

这里的关键是.resize__bar和.resize__save的层级关系。.resize__bar在前,.resize__save在后,配合position: absolute和z-index,让内容层盖住手柄层,但手柄层仍然能通过::-webkit-scrollbar放大拖拽区域。

3.2 CSS:flex 布局 + cursor + 透明手柄

CSS 部分我拆成几块讲。外层.drawer__wrap用display: flex; flex-direction: column;让上下两块垂直排列。上方.top__info设position: relative; overflow: hidden;,这样内容层绝对定位时不会溢出。

* { box-sizing: border-box; margin: 0; padding: 0; } body { height: 100vh; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; } .drawer__wrap { display: flex; flex-direction: column; width: 100%; height: 100vh; overflow: hidden; } .top__info { width: 100%; position: relative; overflow: hidden; display: flex; justify-content: center; /* 初始高度,JS 会动态覆盖 */ height: 230px; min-height: 80px; max-height: 70vh; } .resize__save { position: absolute; top: 0; right: 0; bottom: 10px; /* 留出 10px 给拖拽线 */ left: 0; overflow-x: hidden; overflow-y: auto; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: center; z-index: 2; } .tree__info { flex: 0 0 35%; overflow: auto; padding: 8px; } .node__info { flex: 0 0 65%; padding: 8px; border-left: 1px solid #e5e7eb; overflow: auto; } .resize__bar { width: 300px; height: 230px; /* 与 top__info 初始高度一致 */ resize: vertical; cursor: ns-resize; opacity: 0; overflow: scroll; min-height: 80px; max-height: 70vh; position: relative; z-index: 1; } .resize__bar::-webkit-scrollbar { width: 300px; height: inherit; } .resize__line { position: absolute; right: 45%; left: 44%; bottom: 0; height: 3px; background: #d8d8d8; cursor: ns-resize; z-index: 3; transition: background 0.15s ease; } .resize__bar:hover ~ .resize__line, .resize__bar:active ~ .resize__line { background: #6b7280; } .bottom__info { flex: 1; width: 100%; padding: 16px; overflow: auto; background: #f9fafb; }

几个参数解释一下。.resize__bar的height必须和.top__info的初始高度一致,否则拖拽起点会跳。opacity: 0让它视觉隐藏,但resize: vertical和overflow: scroll仍然生效。::-webkit-scrollbar的width: 300px是为了把滚动条区域放大到和手柄同宽,这样鼠标在整条线上都能触发拖拽,而不是只有右下角那个小三角。

.resize__line用left: 44%; right: 45%;让它居中偏左一点,视觉上像一条短分割线。cursor: ns-resize是上下拖拽的标准光标,用户一看就知道这里能拖。

3.3 JS:鼠标事件绑定与高度约束

纯 CSS 的resize属性只能拖右下角,所以真正好用的方案还是 JS 事件。核心逻辑:mousedown记录起始 Y 和起始高度,mousemove计算差值并更新高度,mouseup解绑事件。

(function () { const wrap = document.getElementById('drawerWrap'); const topInfo = document.getElementById('topInfo'); const bar = document.getElementById('resizeBar'); const line = document.getElementById('resizeLine'); const MIN_HEIGHT = 80; const MAX_HEIGHT = () => window.innerHeight * 0.7; let startY = 0; let startHeight = 0; let dragging = false; function onMouseDown(e) { dragging = true; startY = e.clientY; startHeight = topInfo.getBoundingClientRect().height; document.body.style.cursor = 'ns-resize'; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); e.preventDefault(); } function onMouseMove(e) { if (!dragging) return; const delta = e.clientY - startY; let next = startHeight + delta; next = Math.max(MIN_HEIGHT, Math.min(next, MAX_HEIGHT())); topInfo.style.height = next + 'px'; bar.style.height = next + 'px'; } function onMouseUp() { dragging = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } bar.addEventListener('mousedown', onMouseDown); line.addEventListener('mousedown', onMouseDown); // 窗口尺寸变化时重新约束 window.addEventListener('resize', () => { const current = topInfo.getBoundingClientRect().height; if (current > MAX_HEIGHT()) { topInfo.style.height = MAX_HEIGHT() + 'px'; bar.style.height = MAX_HEIGHT() + 'px'; } }); })();

这里有几个细节值得说。第一,startHeight用getBoundingClientRect().height而不是offsetHeight,前者是浮点数,拖拽更平滑。第二,MAX_HEIGHT用函数而不是常量,因为窗口大小可能变。第三,mousemove和mouseup绑在document上而不是bar上,这样鼠标拖出分割线区域也不会丢失事件。第四,e.preventDefault()防止拖拽时选中文本。

注意:如果你在 Vue/React 里用,记得在组件卸载时移除document上的监听,否则会内存泄漏。React 里可以用useEffect的 cleanup 函数处理。

4. 验证请求与成功结果

4.1 浏览器里怎么验证拖拽生效

把上面三个文件保存到同一个目录,用浏览器打开index.html。你会看到上方是左右分栏的内容区,下方是灰色日志区,中间有一条短横线。把鼠标移到横线上,光标变成上下箭头,按住往下拖,上方区域变高,下方区域自动缩小;往上拖则相反。拖到最小 80px 或最大 70vh 时停住,不会继续。

打开 DevTools 的 Elements 面板,选中.top__info,拖动时能看到style="height: 320px"这样的内联样式实时变化。选中.resize__bar,它的height也同步变化。这说明 JS 在正确更新两个元素的高度。

4.2 把拖拽面板接到模型请求上

拖拽骨架跑通后,你可以把.node__info里的内容换成真实的模型输出。比如做一个可调高度的对话窗口,上方是对话历史,下方是输入框。请求时用fetch调 TaoToken 的 API:

async function askModel(prompt) { const res = await fetch('https://taotoken.net/api/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + process.env.TAOTOKEN_API_KEY }, body: JSON.stringify({ model: 'claude-3-5-sonnet', messages: [{ role: 'user', content: prompt }] }) }); const data = await res.json(); return data.choices[0].message.content; }

注意Authorization头里的 Key 不要写死在前端。生产环境建议用后端代理,前端只调自己的接口。如果你只是本地测试,可以把 Key 放在.env里,用构建工具注入。

请求成功后,把返回内容渲染到.node__info里,拖拽调整高度时内容区会自动滚动,不会溢出。这就是拖拽面板和模型接口结合后的完整效果。

5. 本篇常见错排查

5.1 鼠标移到分割线不变光标

最常见的原因是.resize__line被其他元素盖住了。检查.resize__save的z-index是不是比.resize__line高。如果.resize__save的bottom: 10px没留出来,它会盖住底部的线。另外确认.resize__line有cursor: ns-resize,并且pointer-events没有被设成none。

5.2 拖拽时内容被遮挡或溢出

.top__info必须设overflow: hidden,否则内容层绝对定位后会超出父级。.resize__save的bottom要留出和分割线等高的距离,比如bottom: 10px,这样内容不会盖住拖拽线。如果内容还是溢出,检查.resize__save有没有overflow-y: auto。

5.3 拖拽到边界后回弹或跳变

回弹通常是因为min-height和max-height只设在了 CSS 上,JS 更新时没有同步约束。我的做法是在onMouseMove里用Math.max和Math.min双重限制,并且同时更新topInfo和bar的高度。如果只更新一个,另一个会在下次拖拽时用旧值计算,导致跳变。

5.4 flex 容器里高度不生效

在 flex 子项上设height有时会被flex-grow或flex-shrink覆盖。解决办法是给.top__info设flex: 0 0 auto,让它不参与伸缩,高度完全由 JS 控制。.bottom__info设flex: 1,自动占满剩余空间。这样拖拽时只有上方高度变,下方自动补位。

5.5 移动端触摸拖拽无效

上面的代码只绑了鼠标事件。移动端需要加touchstart / touchmove / touchend,并且用e.touches[0].clientY取坐标。另外要加touch-action: none到.resize__line上,防止浏览器把拖拽识别成页面滚动。如果你主要做桌面端后台,可以先不管移动端,但知道这个坑在哪。

6. 把拖拽面板用到你的真实项目里

拖拽骨架本身不复杂,难的是和业务结合时不破坏布局。我的经验是:把.top__info的高度存到localStorage,刷新页面后恢复用户上次拖拽的位置,体验会好很多。代码就一行:

const saved = localStorage.getItem('panelHeight'); if (saved) { topInfo.style.height = saved + 'px'; bar.style.height = saved + 'px'; } // 在 onMouseUp 里保存 localStorage.setItem('panelHeight', topInfo.getBoundingClientRect().height);

如果你要把这个面板接到模型对话或代码补全上,建议先用 https://taotoken.net/api-keys 创建一个专用 Key,再对照 https://taotoken.net/doc 里的接入文档配置 base URL。需要长期跑编码任务或 Agent 的话,可以看看 https://taotoken.net/coding-plan 里的方案,比按次调用更划算。想先验证模型输出效果,直接打开 https://taotoken.net/chat 试几句,确认返回格式再写进你的面板。

拖拽这件事,CSS 的resize属性只能算玩具,真正可控的还是 JS 事件加 flex 布局。把cursor: ns-resize和min-height / max-height这两个细节做到位,用户拖起来就会觉得“跟 IDE 一样顺手”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 20:15:26

VSCode 同步设置及扩展插件:用 TaoToken 统一多设备配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 20:14:59

Tampermonkey油猴脚本安装与使用教程:从零开始玩转浏览器扩展

折腾浏览器这么多年&#xff0c;我一直觉得浏览器扩展是提升上网效率最直接的方式&#xff0c;而在所有扩展里&#xff0c;油猴&#xff08;Tampermonkey&#xff09;绝对算得上是一把“万能钥匙”。第一次接触它时&#xff0c;我还以为它只是某个网页小插件&#xff0c;后来才…

作者头像 李华
网站建设 2026/9/29 20:13:26

用 Skill 把死锁与 CAP 讲成寓言:TaoToken 配置骨架与验证动作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 20:12:56

用 LLM 当评审员:四条纪律,和一次「它把不存在的证据编得很专业」的事故

流水线里有一类活&#xff0c;确定性代码判不了&#xff1a;这段讲解讲得对不对、这个说法有没有依据、这份内容是不是偷偷降低了难度。 于是很自然想到让另一个模型来判。这一步是整条流水线里最诱人的——便宜、能规模化、还能给出看起来很专业的分数。 它也是最容易自欺的一…

作者头像 李华