3. 回放页面骨架搭建

盘口回放页面的骨架,决定了后续所有行情渲染、逐笔回放、播放控制逻辑能否被清晰、稳定地挂载。本节不追求视觉美化,而是先把“页面由哪些语义区块组成、每个区块承担什么职责、它们之间如何用 CSS 布局划分空间”这三件事定下来。骨架搭得对,后面写 JavaScript 时只需按 id 或 data 属性找到容器,把数据往里填即可;骨架搭得乱,回放逻辑就会和 DOM 结构纠缠在一起,越写越难维护。

一、先定语义结构:四个职责明确的区域

一个盘口回放页面在功能上可以拆成四块:行情区负责展示标的的价格走势或分时图;盘口区展示买卖五档(或十档)报价与挂单量;成交区展示逐笔成交明细;控制区承载播放、暂停、步进、倍速、时间轴拖动等操作。这四块在语义上并不是平级的“div 堆叠”,而应使用 HTML5 语义标签表达其角色:

  • 整个回放器用一个 <section> 或 <main> 包裹,标注 aria-label="盘口回放器";
  • 行情区、盘口区、成交区各自用 <section>,并配 <h3> 标题,便于屏幕阅读器跳转;
  • 控制区用 <form> 或带 role="toolbar" 的容器,因为播放/暂停/倍速本质是一组操作控件;
  • 逐笔成交这类“多行同构数据”用 <table> 或 <ul>,而不是一堆无意义的 <div>。

语义化的直接收益是:后续用 document.querySelector('[data-region="orderbook"]') 这类选择器定位容器时,结构含义一目了然;同时键盘用户可以通过标题快速在区域间跳转。下面这张结构关系图展示了四个区域在 DOM 中的层级与并列关系,以及每个区域内部应挂载的关键子容器。

盘口回放器 DOM 语义结构 section[aria-label="盘口回放器"] 根容器 · 承载整体布局 行情区 data-region="chart" 盘口区 data-region="orderbook" 成交区 data-region="trades" 控制区 role="toolbar" #price-canvas #time-axis #last-price 价格走势与时间刻度 #ask-levels #bid-levels #spread 卖档 / 买档 / 价差 #trade-list #trade-summary #volume-bar 逐笔明细与量能 #btn-play #btn-step #speed-select 播放 / 步进 / 倍速 #timeline(时间轴)· 横跨底部,与四个区域共享同一回放时钟 拖动或点击时间轴 → 触发 seek 事件 → 各区域按同一时间戳重绘 约定:所有区域只读取统一回放状态对象 { currentTime, speed, playing },不各自维护时间 这是骨架阶段就要定下的数据契约,避免后期出现“行情走了、盘口没动”的错位

二、用 Grid 划分主布局,用 Flex 处理区内排列

四个区域的空间关系适合用 CSS Grid 表达,因为它是二维的:行情区通常占据左侧较大面积,盘口区与成交区在右侧上下或左右排列,控制区与时间轴横跨底部。一个可直接使用的布局方案如下(示例参数,可按实际屏幕调整):

.replay-root {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
  grid-template-rows: minmax(0, 1fr) auto auto;
  grid-template-areas:
    "chart   orderbook"
    "chart   trades"
    "control control"
    "timeline timeline";
  gap: 10px;
  height: 100vh;
  padding: 10px;
  box-sizing: border-box;
}
[data-region="chart"]     { grid-area: chart; }
[data-region="orderbook"] { grid-area: orderbook; }
[data-region="trades"]    { grid-area: trades; }
[data-region="control"]   { grid-area: control; }
#timeline                 { grid-area: timeline; }

这里的关键点有三个。第一,minmax(0, 2fr) 中的 0 不能省:如果写成 2fr,当行情区内部有长文本或 canvas 时,网格项可能被内容撑破,导致右侧盘口区被挤压。第二,grid-template-areas 让“哪个区域在哪”变成可读的字符画,调整布局时只改这几行,不用动 DOM。第三,height: 100vh 配合 minmax(0, 1fr) 行,使行情区能随窗口伸缩,而控制区和时间轴保持内容高度。

区域内部则更适合 Flex。例如盘口区需要“卖五档在上、买五档在下、中间显示价差”,用 display: flex; flex-direction: column; 加 justify-content: space-between 即可;成交区是一个可滚动的列表,用 flex: 1; overflow-y: auto; 让列表占满剩余高度并独立滚动。Grid 管“大块怎么分”,Flex 管“块内怎么排”,两者分工明确,不要用 Grid 去硬排每一行报价,也不要拿 Flex 去模拟二维网格。

三、UI 框架还是原生组件:按回放页面的真实需求选

回放页面的控件其实很有限:几个按钮、一个下拉框、一个滑块、若干表格行。这类需求下,引入完整 UI 框架(如带主题、表单校验、弹窗体系的重型库)往往得不偿失——它会带来额外体积和样式覆盖成本,而回放页面的核心复杂度在数据渲染与时钟同步,不在表单。更务实的策略是原生组件优先,按需引入轻量库:

  • 按钮、下拉、滑块直接用原生 <button>、<select>、<input type="range">,配合少量自定义 CSS 统一外观;
  • 逐笔成交列表若行数可达数千,用原生 <table> 加固定表头即可,必要时再引入虚拟滚动;
  • 只有当需要复杂交互组件(如可拖拽缩放的 K 线图、日期范围选择器)时,才引入对应的单一功能库,而不是整套框架。

判断标准可以落到一个具体问题上:这个库是否解决了回放页面的主要矛盾?如果主要矛盾是“每秒要重绘几十行盘口和成交数据”,那么性能优化(如 requestAnimationFrame 批量更新、DOM 复用)比 UI 框架重要得多。骨架阶段就把这个判断做掉,可以避免后期为了换框架而重构布局。

骨架阶段建议给每个区域容器加上 data-region 属性,并约定“JavaScript 只通过 data-region 和内部固定 id 访问 DOM”。这样即使以后把某个区域从 <section> 换成自定义组件,选择器也不用改,回放逻辑与结构解耦。

四、骨架搭完后的自检

布局写完后,不要急着写回放逻辑,先用静态占位内容验证骨架是否成立:给行情区放一个固定尺寸的 <canvas>,给盘口区填五行假报价,给成交区填二十行假成交,然后做三件事——把浏览器窗口从 1920px 缩到 1024px,观察盘口区是否被挤压变形;把成交区内容增加到 500 行,观察是否只在成交区内部滚动而不撑破整页;用 Tab 键从控制区按钮依次切换,观察焦点顺序是否与视觉顺序一致。这三项通过,说明 Grid 区域划分、Flex 内部滚动和语义焦点顺序都已就位,后续把假数据换成真实回放数据时,页面不会因为结构问题返工。

至此,回放页面的“容器”已经准备好:四个语义区域各司其职,Grid 负责二维分区,Flex 负责区内排列,控件策略确定为原生优先。下一步要做的,就是让这些容器真正“动起来”——建立统一回放时钟,并让各区域按同一时间戳读取数据、重绘界面。