Skip to content

总体思路

  • 把“涨/跌”当成数据状态trend: 'up'|'down'),文本颜色直接用 data-trend 控制。
  • 背景闪烁交给伪元素 ::after 只做 opacity 动画,避免影响布局与重绘范围。
  • A/B 两套动画名cell-flash-a / cell-flash-b),每次变价时在 -a/-b 间切换,使浏览器认为是不同的 animation-name必重播
  • flip 不依赖时间,由“价格是否变化”驱动(每次变化 flip ^= 1),规避“时间戳没变导致不触发”的问题。
  • animation-fill-mode: both:首帧立即生效、尾帧保持透明,观感更自然。
  • 秒级时间戳统一处理:后端若给 10 位秒级时间戳,前端显示时务必 *1000,但不要用时间戳去决定动画 flip。

注意点

关键点就在这里: 浏览器只有在 computed style 中与动画相关的属性发生变化 时才会重播动画;最稳妥的做法就是让 animation-name 改变(比如 cell-flash-acell-flash-b),因此我们用 A/B 两套 keyframes 来强制重播。

补充要点(伪元素同理适用):

  • 仅仅再次添加同一个类名、或只改背景/颜色这类与动画无关的属性,不会重播。

  • 可触发重播的还包括:

    1. 切换 animation-name(最稳);
    2. 暂时设 animation: none → 强制一次 reflow → 再恢复原动画;
    3. 改变 animation-duration / delay / iteration-count 等动画属性(多数浏览器会重启)。
  • ::after 是否重播不取决于它是伪元素,而取决于它的 动画相关 computed style 是否变化

  • 频繁 tick 的场景下,用 flip ^= 1 切换 -a/-b 类名,是最简单、性能也稳的方案。

具体步骤

  1. 状态建模:为每行维护 bid/asktrend 与一个布尔 flip(0/1)。每次价格变化,比较新旧值设置 trend,并执行 flip ^= 1
  2. 类名策略:根据 trendflip 组合出四种类之一: tick-flash-up-a / tick-flash-up-b / tick-flash-down-a / tick-flash-down-b
  3. 动画实现:提供两套 @keyframescell-flash-a / cell-flash-b),仅动画 opacity,时长、透明度、缓动通过 CSS 变量可调。
  4. 颜色与可访问性:文本颜色用 data-trend 控制;背景色在伪元素上设置;在 @media (prefers-reduced-motion) 内启用动画,尊重系统“减少动效”设置。
  5. 渲染细节:在切类名前可移除旧类名并强制一次 reflow(读 offsetWidth),确保快速连续更新仍然稳定重播。
  6. 时间展示:显示 HH:mm:ss 时,若后端是秒级 ts,格式化函数用 new Date(ts * 1000)不要再用秒数奇偶去决定 flip

最简可运行示例

纯原生 HTML/CSS/JS,完整复制到本地即可打开测试(点击按钮模拟“涨/跌”ticks)。

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>报价闪烁最简示例</title>
    <style>
      :root {
        --flash-ms: 900ms;
        --flash-alpha: 0.18;
        --flash-ease: cubic-bezier(0.22, 1, 0.36, 1);
        --profit-rgb: 70, 205, 124;
        --loss-rgb: 235, 72, 63;
      }
      .cell-quote {
        position: relative;
        display: inline-block;
        padding: 6px 10px;
        border-radius: 6px;
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        isolation: isolate; /* 独立合成层,减少影响范围 */
      }
      .cell-quote::after {
        content: "";
        position: absolute;
        inset: 0;
        opacity: 0;
        pointer-events: none;
      }
      /* 文本着色由数据态驱动 */
      .cell-quote[data-trend="up"] {
        color: rgb(var(--profit-rgb));
      }
      .cell-quote[data-trend="down"] {
        color: rgb(var(--loss-rgb));
      }

      /* 两套 keyframes:仅动画透明度,名字不同以强制重播 */
      @keyframes cell-flash-a {
        0%,
        60% {
          opacity: 1;
        }
        100% {
          opacity: 0;
        }
      }
      @keyframes cell-flash-b {
        0%,
        60% {
          opacity: 1;
        }
        100% {
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: no-preference) {
        .tick-flash-up-a::after {
          background: rgba(var(--profit-rgb), var(--flash-alpha));
          /* 复杂:both 让首帧立即可见,结束保持透明 */
          animation: cell-flash-a var(--flash-ms) var(--flash-ease) both;
        }
        .tick-flash-down-a::after {
          background: rgba(var(--loss-rgb), var(--flash-alpha));
          animation: cell-flash-a var(--flash-ms) var(--flash-ease) both;
        }
        .tick-flash-up-b::after {
          background: rgba(var(--profit-rgb), var(--flash-alpha));
          animation: cell-flash-b var(--flash-ms) var(--flash-ease) both;
        }
        .tick-flash-down-b::after {
          background: rgba(var(--loss-rgb), var(--flash-alpha));
          animation: cell-flash-b var(--flash-ms) var(--flash-ease) both;
        }
      }

      button {
        margin: 8px 6px;
        padding: 6px 10px;
      }
    </style>
  </head>
  <body>
    <div id="bid" class="cell-quote" data-trend="up">100.00</div>
    <div style="margin-top:12px">
      <button id="upBtn">模拟上涨 Tick</button>
      <button id="downBtn">模拟下跌 Tick</button>
    </div>

    <script>
      let flip = 0; // 复杂:A/B 轮换位
      let price = 100.0;

      // 复杂:应用一次“变价”并强制重播动画
      function tick(direction) {
        const el = document.getElementById("bid");
        price += direction === "up" ? +0.01 : -0.01;
        el.textContent = price.toFixed(2);
        el.dataset.trend = direction;

        // 复杂:根据方向与 flip 选择四种类名之一
        const cls =
          direction === "up"
            ? flip
              ? "tick-flash-up-b"
              : "tick-flash-up-a"
            : flip
            ? "tick-flash-down-b"
            : "tick-flash-down-a";

        // 复杂:先移除所有闪烁类,避免叠加;读 offsetWidth 触发一次 reflow,保证动画从 0% 开始
        el.classList.remove(
          "tick-flash-up-a",
          "tick-flash-up-b",
          "tick-flash-down-a",
          "tick-flash-down-b"
        );
        void el.offsetWidth; // 强制 reflow

        el.classList.add(cls);

        // 复杂:flip ^= 1 使 0/1 互切,下一次必换 animation-name
        flip ^= 1;
      }

      document.getElementById("upBtn").onclick = () => tick("up");
      document.getElementById("downBtn").onclick = () => tick("down");
    </script>
  </body>
</html>

若需要显示时间(HH:mm:ss),且后端返回秒级时间戳 ts(例如 1761807859),请这样格式化:

js
// 复杂:秒级时间戳转毫秒
function formatHHmmss(tsSec) {
  const d = new Date(tsSec * 1000);
  const p = (n) => String(n).padStart(2, "0");
  return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}

以上思路与示例即可平滑迁移到你的表格/组件里:把数据变更 → 设 trend → flip 翻转 → 切 A/B 类名这条链路打通,动画就会在每次价格变化时稳定触发,且可通过变量灵活调参。

本站总访问