Vue 3 统一右侧编辑面板:布局、滚动与底部操作区设计

从图片处理工作台的真实代码出发,拆解一个可维护的编辑页布局协议

在图片编辑类产品里,右侧面板经常同时承载文件信息、参数选择、提示说明、费用信息和提交按钮。功能少时,把这些内容简单地堆在一个 aside 里似乎也能工作;一旦选项变多,问题就会集中出现:面板高度不够时滚动条到底应该放在哪里?底部"开始处理"是否始终可见?移动端切成上下布局后,是否会出现页面滚动和面板滚动互相抢焦点?

我在开发图片猫(PicCat,项目演示入口:www.piccat.cn)的多个图片处理页面时,逐步把这些问题收敛成一套公共约定:编辑器外层负责左右布局,右侧面板负责自己的纵向滚动,底部操作使用独立的粘性操作条;在移动端则取消嵌套滚动,让页面承担主要纵向滚动。本文只讨论这套布局方法,以及它在当前代码中的实现边界。

文中代码均来自项目中的相关片段或在其基础上做了教学化裁剪。为方便阅读,省略了上传、任务提交和结果恢复等与布局无关的业务逻辑;没有把简化片段描述成可以直接复制运行的完整组件。

图 1:项目中的图片编辑工作台案例素材,说明右侧设置区通常会与预览区并存。该图是项目展示素材,不是本文代码运行截图。

一、先把问题拆成三个独立约束

1. 横向约束:预览区不能被设置区挤到不可用

桌面端通常采用"左侧预览 + 右侧设置"的 flex 布局。预览区需要 `flex: 1` 和 `min-width: 0`,右侧面板则使用固定或受限宽度。这里的 `min-width: 0` 很容易被忽略:flex 子项默认可能按内容最小宽度撑开,长文件名、长提示或选项文本会把预览区挤窄,最终出现横向溢出。

2. 纵向约束:只保留一个主要滚动容器

面板内容变长时,最稳妥的做法是让 `editor-controls-panel` 自己滚动,而不是在面板内部再包一层表单滚动容器。项目的公共样式明确使用 `overflow-y: auto` 和 `scrollbar-gutter: stable`,这样滚动条出现或消失时,内容宽度不会突然跳动。

移动端是另一个情况。项目在 768px 以下通过 `mobile-editor-workspace` 协议把预览区和设置区上下堆叠,并让编辑器主体承担页面纵向滚动;右侧面板本身改为 `overflow: visible`。这样做的目的不是让面板永远不滚动,而是避免两个相邻滚动层同时响应手势。

3. 操作约束:提交按钮不能被内容长度决定位置

如果"开始处理"只是表单最后一个普通按钮,用户必须滚到面板底部才能提交;当提示卡或选项发生变化时,按钮位置也会不断移动。当前项目用 `EditorStickyActionBar.vue` 把主按钮、费用摘要和结果下载动作抽成独立区域,并通过 `position: sticky; bottom: 0` 固定在面板底部。

二、公共样式先解决外壳,不替业务组件做决定

当前项目的 `webClientVue/src/assets/editor-panel.css` 是右侧面板的公共入口。它只负责外壳层面的事情:宽度、伸缩、内边距、滚动、边框、背景和响应式断点;选项按钮、提示文案和业务状态仍然由各自的 Vue 组件决定。

项目代码摘录:editor-panel.css 的右侧面板核心规则(保留布局相关部分)

.editor-controls-panel {
flex: 0 0 var(--editor-controls-width) !important;
width: var(--editor-controls-width) !important;
min-width: var(--editor-controls-min-width) !important;
max-width: var(--editor-controls-width) !important;
min-height: 0 !important;
box-sizing: border-box !important;
display: flex !important;
flex-direction: column !important;
gap: var(--editor-controls-gap) !important;
overflow-x: hidden !important;
overflow-y: auto !important;
scrollbar-gutter: stable !important;
}

这里有三个细节值得保留。第一,`flex: 0 0` 表达的是"面板宽度由公共变量决定,不参与左右空间争抢";第二,`min-height: 0` 允许 flex 子项在父容器中真正收缩,否则内容可能把面板撑出可视高度;第三,滚动只放在面板外壳,内部卡片默认不再各自创建纵向滚动。

项目中还保留了一些历史兼容规则,例如对特定组件覆盖宽度、间距或滚动方式。这说明统一样式正在逐步收敛,而不是所有页面已经完全同构。新增页面应优先接入公共类名,只有确认存在业务例外时才增加局部覆盖。

三、一个完整实例:3D 立体服装编辑页

`EcommerceClothing3D.vue` 是当前项目里结构比较清晰的实例。上传完成后,模板直接把预览区和右侧控制区放进同一个 `editor-container`;右侧 aside 同时带有 `editor-controls-panel` 和 `mobile-editor-workspace__controls`,前者接入公共桌面端协议,后者接入移动端上下堆叠协议。

项目代码摘录:EcommerceClothing3D.vue 的编辑态结构(业务内容已压缩)

<div v-else class="editor-container mobile-editor-workspace">
<section class="canvas-wrapper editor-checkerboard-bg
mobile-editor-workspace__preview">
<!-- 预览图、原图/新图切换、ZoomControls -->
</section>

<aside class="controls-panel editor-controls-panel
mobile-editor-workspace__controls">
<div class="file-info-collapse">图片信息与重新选择</div>
<div class="control-group">视图角度、分辨率、输出比例</div>
<UsageTips title="使用说明" />
<SugarPointDocs ... />
<EditorStickyActionBar
feature-id="ecommerce-clothing-3d"
operation="smart"
:cost="smartCost"
label="👕 开始生成"
:loading="isProcessing"
:disabled="!canGenerate"
hide-summary
@submit="handleGenerate"
/>
</aside>
</div>

这个结构的重点不是标签数量,而是层级关系:预览区和设置区是同级;设置区内先放信息与参数,再放说明和费用,最后放操作条。因为操作条是设置区的最后一个子节点,它可以在面板滚动上下文中稳定地贴近底部,而不会脱离当前工具的语义范围。

|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| 项目案例素材:处理前 | 项目案例素材:处理后 |

图 2:项目中的服装案例素材,适合说明"预览区需要稳定展示结果,设置区负责参数与操作"。图片是展示案例,不代表本文现场运行结果。

四、底部操作区为什么要独立成组件

`EditorStickyActionBar.vue` 的价值不只是复用一个按钮样式。它把处理页常见的交互状态集中起来:免费次数或费用摘要、加载状态、禁用状态、主提交事件,以及结果出现后的额外下载操作。业务页面只需要把当前功能 ID、操作类型、成本和提交回调传进去。

项目代码摘录:EditorStickyActionBar.vue 的结构化操作区(费用计算逻辑省略)

<div class="editor-sticky-action-bar" :class="{ 'is-loading': loading }">
<div v-if="!hideSummary" class="editor-sticky-action-bar__summary">
<slot name="summary" />
</div>
<slot name="hint" />
<button
class="editor-sticky-action-bar__primary"
:disabled="disabled || loading"
@click="$emit('submit')"
>
<span v-if="loading">{{ loadingText }}</span>
<template v-else><!-- 费用文案由组件内部计算,这里省略 --></template>
</button>
<slot name="extra" />
</div>

项目代码摘录:操作条的粘性定位与安全区处理

.editor-sticky-action-bar {
position: sticky;
bottom: 0;
z-index: 5;
display: flex;
flex-direction: column;
gap: 8px;
flex-shrink: 0;
margin-top: 12px;
padding: 10px 0 calc(10px + env(safe-area-inset-bottom));
background: #141522;
box-shadow: 0 -12px 18px rgba(8, 10, 24, 0.32);
}

这里的 `flex-shrink: 0` 很关键:操作条不能因为面板内容过多而被压缩到不可点击。`safe-area-inset-bottom` 则是给全面屏手机底部手势区域留出空间。需要注意,`position: sticky` 依赖滚动上下文;如果某个父级被设置成不合适的 `overflow: hidden`,或者面板没有明确高度,操作条可能看起来像普通流式元素。

五、当前实现与尚未统一的部分

|--------|----------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| 状态 | 源码证据 | 对文章读者的启示 |
| 已经实现 | `editor-panel.css` 统一 `.editor-controls-panel` 的宽度、滚动、间距和响应式规则;`ToolHeader`、`UsageTips`、`SugarPointDocs`、`ZoomControls` 等公共组件已被多个页面复用。 | 先建立布局协议,再让业务组件填充内容。 |
| 已经实现 | `EcommerceClothing3D.vue` 使用 `EditorStickyActionBar`,并在移动端使用 `mobile-editor-workspace` 避免嵌套纵向滚动。 | 底部操作区应该有清晰的滚动归属。 |
| 尚未完全统一 | `ImageEnhancer.vue` 仍有直接写在模板里的 `.action-buttons`,结果操作和提交操作分别维护;部分历史组件还保留自己的 `.controls-panel` 覆盖。 | 公共组件接入需要逐页迁移和回归,不宜一次性改掉所有局部样式。 |
| 建议方案 | 新页面只声明结构类名和业务状态,不再重复定义右侧面板宽度、滚动条和底部主按钮尺寸。 | 把"外壳规范"和"业务控件"分开,减少级联覆盖。 |

这张表是源码状态的区分,不是对整个项目完成度的泛化结论。比如公共 CSS 已经提供了统一协议,并不表示每一个旧页面都已经使用同一套底部操作组件;文章中把两者分开,是为了避免"有公共样式"被误读为"所有页面已经迁移完成"。

六、我会按这份清单验收一个新的编辑页

下面是建议验收清单,不等同于本文已经替项目完成了浏览器端验收。真正接入新页面时,我会把它作为桌面端、窄屏和异常状态的共同检查表。

  • 布局: 桌面端预览区不会被长文本挤压;右侧面板宽度、边框和内边距来自公共规则。
  • 滚动: 面板内容很长时只有预期的滚动层在滚动;滚动条出现前后,内容不会发生明显横向跳动。
  • 操作: 主操作在面板底部可达;加载时按钮禁用且文案变化;结果出现后下载和再次处理动作不覆盖主操作。
  • 移动端: 320、360、390、430px 宽度下预览区仍可见;页面滚动不会被内部面板截断;底部安全区有足够间距。
  • 内容边界: 长文件名、长提示、费用错误、余额查询失败和按钮禁用状态都不会撑破面板。
  • 键盘与语义: 按钮使用真实 button 元素,焦点可见,操作条不会遮住最后一个表单控件。

七、结语:统一的重点是滚动归属,而不是"右侧栏长得一样"

右侧编辑面板的难点通常不在某一个 CSS 属性,而在于先约定谁负责布局、谁负责滚动、谁负责提交。当前项目的实践可以概括为:公共 `editor-controls-panel` 管外壳,业务组件管内容,`EditorStickyActionBar` 管提交和结果动作,移动端通过 `mobile-editor-workspace` 减少嵌套滚动。

这套方案仍需要持续迁移旧页面,也需要在真实设备上验证不同浏览器的 sticky 行为。它的技术价值在于给后续页面提供了可复用的边界:新增功能不必重新发明一套右侧栏,只需要明确内容分组、状态和操作事件,再把它们放进同一套布局协议。

参考资料

Vue 单文件组件与样式作用域:Vue Single-File Components

Vue SFC 样式特性:SFC Syntax Specification

MDN:position sticky:CSS position

相关推荐
change_fate1 小时前
Vue 3 Composable 函数指南
前端·javascript·vue.js
tryCbest1 小时前
Vue3——第 9 篇:组件通信全攻略(下)——Provide / Inject 与事件总线
前端·javascript·vue.js
溪语流沙2 小时前
【Web全栈进阶】Nginx反向代理 + HTTPS:正式上线
前端·vue.js·python·nginx·https·fastapi
漫步人生走在路上2 小时前
外贸SEO中的品牌词与产品词策略:大卖家和小卖家的不同打法
大数据·javascript·网络·数据库·人工智能·搜索引擎
用户298698530142 小时前
React 前端 Word 转图片:页面与元素级导出的工程实践
前端·javascript·react.js
重生六道3 小时前
B站视频时长统计:一段JS脚本搞定分P/合集总时长(附倍速计算)
开发语言·javascript·音视频
迷途小羔羊3 小时前
环境搭建
前端·javascript·后端
夕阳_醉了3 小时前
让 AI 回答“打字般”浮现:用 Vue 3 + DeepSeek 实现流式聊天
前端·vue.js·人工智能
酷炫码神4 小时前
Day 9|Vue Router:把单页后台拆成能跳转的多页面
前端·javascript·vue.js