EN
Shopify 知识库 · 指南

给 Dawn 增加倒计时分区:同源截止时间、服务端时钟与编辑器清理

基于 Dawn v16.0.0 固定源码,先以全仓库搜索确认主题未内置倒计时,再按 Dawn 自身写法新增一个 countdown-timer 分区:截止时间读 shop 元字段而不另填,剩余时间不在 Liquid 里算,自定义元素按分钟与秒两档更新、只在两个节点播报、提供隐藏按钮并在编辑器卸载时清理;代码通过 theme check,未在真实店铺运行。
历史资料
请结合文中的适用版本和来源阅读。

本文基于 Dawn v16.0.0,源码固定到 bc39a7d(核验于 2026-10-08:v16.0.0 标签指向该提交,config/settings_schema.json 的 theme_version 为 16.0.0,是 GitHub Releases 上的最新发布)。与本库其他 Dawn 文章不同,这一篇不是解读既有功能,而是新增一个主题里没有的功能。实现代码只做了静态检查,未在真实店铺运行;每一处未验证的判断都在文内标出。

倒计时该不该做、什么时候做,不在本文范围:真实性红线、合规与无障碍要求以 Countdown Timer 为准,本文只回答「在 Dawn 里怎样按它的要求实现」。

先确认 Dawn 没有倒计时

在固定提交全仓库搜索 countdown、count-down、timer(不区分大小写):命中只有 assets/constants.js 的 ON_CHANGE_DEBOUNCE_TIMER(购物车与批量加购的防抖常量)和几个语言文件里的无关词(如挪威语 Estimert totalsum)。搜索 setInterval 只命中 global.js 的轮播自动播放与 BulkAdd 请求队列;<time 元素在 sections、snippets、layout 中无命中。结论:Dawn v16.0.0 没有内置倒计时,也没有可以直接复用的计时组件,需要新增文件,不需要改动既有分区。

先定数据来源,再写代码

Countdown Timer 的第一条红线是「同一时间源」:倒计时读的截止时刻必须就是折扣实际结束的那份数据。这在 Dawn 里绕不开一个平台限制:

  • Liquid 读不到折扣的起止时间。 discount_application 对象只有 target_selection、target_type、title、total_allocated_amount、type、value、value_type(shopify.dev,核验于 2026-10-07),描述的是购物车里已经生效的折扣;shop 对象没有任何折扣或促销日程属性。折扣的 startsAt / endsAt 只在 Admin GraphQL(如 discountAutomaticAppCreate 的返回)里出现,店面主题拿不到。
  • 分区设置没有日期类型。 输入设置类型列表里只有 text、number、range、select 等基础类型和 product、metaobject 等资源类型,没有 date 或 datetime(shopify.dev 输入设置页,核验于 2026-10-07)。
  • Liquid 里的 now 会被缓存。 date 过滤器文档写明,'now' 输出的是 Liquid 最近一次渲染的时间,「可能不会在每次页面访问时更新」。所以剩余时间不能在 Liquid 里算。

因此本实现把截止时间放在 shop 级元字段 promo.ends_at(类型 date_time)里,分区只读不写:

方案做法与「同源」的距离
元字段(本文采用)应用或 Shopify Flow 在创建、修改折扣时把 endsAt 写入 shop.metafields.promo.ends_at;主题读它最近:主题与折扣之间只隔一个同步动作。同步本身不在主题内,本文没有实现,Flow 能否直接写该元字段未核验
分区文本设置(本文仅作回退)商家在编辑器里手填 ISO 8601 字符串最远:两处手填,必然出现不一致;只在没有元字段时使用,编辑器文案里明确要求与后台折扣一致
应用代理或 Storefront API 实时查询前端请求应用接口取 endsAt同源但引入外部依赖与额外请求;超出本文范围

date_time 元字段的值是「不预设时区的 ISO 8601 字符串,默认按 GMT」(shopify.dev 元字段类型页);Liquid 里 metafield.value 对 date_time 返回日期字符串,可以接 date 过滤器(shopify.dev metafield 对象页)。本实现据此把没有 Z 或 + 偏移的值视为 UTC,补上 Z 后再交给浏览器解析;date 过滤器解析这种字符串时按哪个时区取值,本文未验证,所以服务端只用它判断「是否已经结束」,不用它算剩余时长。

文件与职责

下表的每一条都对应 Dawn 既有写法,便于与主题其余部分保持一致:

文件职责参照的 Dawn 写法
sections/countdown-timer.liquid(新增)读截止时间、判断三种状态、输出静态截止文案与数字骨架、{% schema %}rich-text.liquid 的 section-{{ section.id }}-padding、color_scheme、page-width、t: 键;main-product.liquid 在分区内用 <script defer> 加载自己的脚本
assets/countdown-timer.js(新增)自定义元素:估计服务端时钟偏移、按两档间隔更新、到期收尾、隐藏按钮、状态播报global.js 的 customElements.define 与 matchMedia('(prefers-reduced-motion: reduce)');cart.js、product-info.js、cart-disclosure-modal.js 等在 disconnectedCallback 里清理
assets/section-countdown-timer.css(新增)分区样式;唯一的过渡放在 prefers-reduced-motion: no-preference 里base.css 的 --duration-short 与减少动态媒体查询
locales/en.default.json(追加)前台文案 sections.countdown_timer.*既有 sections.* 结构
locales/en.default.schema.json(追加)编辑器文案 sections.countdown-timer.*既有 sections.rich-text.* 结构
layout/theme.liquid(追加 4 行)把两条播报文案暴露为 window.countdownStrings同文件已有的 window.accessibilityStrings 等

分区 Liquid

{{ 'section-countdown-timer.css' | asset_url | stylesheet_tag }}
<script src="{{ 'countdown-timer.js' | asset_url }}" defer="defer"></script>

{%- liquid
  assign ends_at = shop.metafields.promo.ends_at.value
  if ends_at == blank
    assign ends_at = section.settings.ends_at_fallback
  endif
  assign ends_at = ends_at | strip
  assign last_char = ends_at | slice: -1
  assign has_offset = false
  if last_char == 'Z' or ends_at contains '+'
    assign has_offset = true
  endif
  if ends_at != blank and has_offset == false
    assign ends_at = ends_at | append: 'Z'
  endif
  assign ends_at_unix = ends_at | date: '%s' | plus: 0
  assign rendered_at_unix = 'now' | date: '%s' | plus: 0
  assign state = 'running'
  if ends_at == blank or ends_at_unix == 0
    assign state = 'missing'
  elsif rendered_at_unix >= ends_at_unix
    assign state = 'ended'
  endif
-%}

{%- style -%}
  .section-{{ section.id }}-padding {
    padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
    padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
  }

  @media screen and (min-width: 750px) {
    .section-{{ section.id }}-padding {
      padding-top: {{ section.settings.padding_top }}px;
      padding-bottom: {{ section.settings.padding_bottom }}px;
    }
  }
{%- endstyle -%}

{%- if state == 'missing' -%}
  {%- if request.design_mode -%}
    <div class="page-width section-{{ section.id }}-padding">
      <p class="caption-with-letter-spacing">{{ 'sections.countdown_timer.missing_end_time' | t }}</p>
    </div>
  {%- endif -%}
{%- else -%}
  <div class="color-{{ section.settings.color_scheme }} gradient section-{{ section.id }}-padding">
    <countdown-timer
      class="countdown-timer page-width"
      data-ends-at="{{ ends_at | escape }}"
      data-state="{{ state }}"
      data-section-id="{{ section.id }}"
    >
      {%- if section.settings.heading != blank -%}
        <h2 class="countdown-timer__heading inline-richtext {{ section.settings.heading_size }}">
          {{ section.settings.heading }}
        </h2>
      {%- endif -%}

      <p class="countdown-timer__ends-at">
        {{ 'sections.countdown_timer.ends_at_html' | t: ends_at: ends_at | date: '%Y-%m-%d %H:%M UTC' }}
        <time datetime="{{ ends_at | escape }}" data-ends-at-local hidden></time>
      </p>

      <div class="countdown-timer__clock" data-clock{% if state == 'ended' %} hidden{% endif %} aria-hidden="true">
        {%- assign units = 'days,hours,minutes,seconds' | split: ',' -%}
        {%- for unit in units -%}
          {%- assign unit_key = 'sections.countdown_timer.' | append: unit -%}
          <div class="countdown-timer__unit" data-unit-wrapper="{{ unit }}">
            <span class="countdown-timer__value h2" data-unit="{{ unit }}">--</span>
            <span class="countdown-timer__label caption-with-letter-spacing">{{ unit_key | t }}</span>
          </div>
        {%- endfor -%}
      </div>

      <p class="countdown-timer__ended" data-ended{% unless state == 'ended' %} hidden{% endunless %}>
        {{ 'sections.countdown_timer.ended' | t }}
      </p>

      {%- if section.settings.show_hide_button and state != 'ended' -%}
        <button type="button" class="link link--text countdown-timer__hide" data-hide hidden>
          {{ 'sections.countdown_timer.hide' | t }}
        </button>
      {%- endif -%}

      <p class="visually-hidden" role="status" data-status></p>
    </countdown-timer>
  </div>
{%- endif -%}

{% schema %}
{
  "name": "t:sections.countdown-timer.name",
  "tag": "section",
  "class": "section",
  "disabled_on": { "groups": ["footer"] },
  "settings": [
    { "type": "paragraph", "content": "t:sections.countdown-timer.settings.paragraph.content" },
    {
      "type": "text",
      "id": "ends_at_fallback",
      "label": "t:sections.countdown-timer.settings.ends_at_fallback.label",
      "info": "t:sections.countdown-timer.settings.ends_at_fallback.info"
    },
    {
      "type": "inline_richtext",
      "id": "heading",
      "default": "t:sections.countdown-timer.settings.heading.default",
      "label": "t:sections.countdown-timer.settings.heading.label"
    },
    {
      "type": "select",
      "id": "heading_size",
      "options": [
        { "value": "h2", "label": "t:sections.all.heading_size.options__1.label" },
        { "value": "h1", "label": "t:sections.all.heading_size.options__2.label" },
        { "value": "h0", "label": "t:sections.all.heading_size.options__3.label" }
      ],
      "default": "h1",
      "label": "t:sections.all.heading_size.label"
    },
    {
      "type": "checkbox",
      "id": "show_hide_button",
      "default": true,
      "label": "t:sections.countdown-timer.settings.show_hide_button.label",
      "info": "t:sections.countdown-timer.settings.show_hide_button.info"
    },
    { "type": "color_scheme", "id": "color_scheme", "label": "t:sections.all.colors.label", "default": "scheme-1" },
    { "type": "header", "content": "t:sections.all.padding.section_padding_heading" },
    { "type": "range", "id": "padding_top", "min": 0, "max": 100, "step": 4, "unit": "px", "label": "t:sections.all.padding.padding_top", "default": 36 },
    { "type": "range", "id": "padding_bottom", "min": 0, "max": 100, "step": 4, "unit": "px", "label": "t:sections.all.padding.padding_bottom", "default": 36 }
  ],
  "presets": [{ "name": "t:sections.countdown-timer.presets.name" }]
}
{% endschema %}

三种状态由服务端决定:

  • missing:没有截止时间,或字符串经 date: '%s' 得不到有效时间戳。前台不输出任何内容;只在 request.design_mode 下显示一行提示,告诉商家去哪里填。这对应通用契约里「数据缺失时隐藏倒计时」,同时沿用 Dawn 用 request.design_mode 区分编辑器的做法(主题编辑器适配)。
  • ended:渲染时刻已经晚于截止时刻。数字骨架带 hidden 输出,直接显示「已结束」。因为 'now' 可能是缓存时间,这个判断只会偏保守:缓存页面渲染得早,服务端判断为未结束时,由浏览器脚本补判;反过来不会发生,所以不会出现「已结束又重新开始」。
  • running:输出带时区的静态截止文案(UTC),<time datetime> 承载机器可读值,再输出四个值为 -- 的占位。没有 JavaScript 时访客看到的就是这一行绝对时间,没有假数字。

show_hide_button 产生的按钮带 hidden 输出,由脚本在确认能运行后再显示,避免无 JS 时出现没有作用的按钮。aria-hidden="true" 加在数字区上,让每分钟或每秒变化的数字不进入辅助技术的读取顺序;对读屏用户有意义的信息是上一行的绝对时间。

自定义元素

if (!customElements.get('countdown-timer')) {
  customElements.define(
    'countdown-timer',
    class CountdownTimer extends HTMLElement {
      static STORAGE_KEY = 'countdown-timer-hidden';

      connectedCallback() {
        this.endsAt = Date.parse(this.dataset.endsAt);
        this.clock = this.querySelector('[data-clock]');
        this.values = Object.fromEntries(
          Array.from(this.querySelectorAll('[data-unit]')).map((element) => [element.dataset.unit, element])
        );
        this.secondsWrapper = this.querySelector('[data-unit-wrapper="seconds"]');
        this.endedMessage = this.querySelector('[data-ended]');
        this.status = this.querySelector('[data-status]');
        this.hideButton = this.querySelector('[data-hide]');
        this.localTime = this.querySelector('[data-ends-at-local]');
        this.skew = 0;
        this.announcedHour = false;

        if (Number.isNaN(this.endsAt) || this.dataset.state === 'ended') {
          this.finish(false);
          return;
        }

        this.renderLocalTime();
        this.hideButton?.addEventListener('click', this.onHide.bind(this));
        if (this.hideButton && this.readHiddenPreference()) {
          this.hideClock();
        } else {
          this.hideButton?.removeAttribute('hidden');
        }

        this.estimateSkew().finally(() => {
          if (!this.isConnected) return;
          this.tick();
        });
      }

      disconnectedCallback() {
        clearTimeout(this.timer);
      }

      async estimateSkew() {
        try {
          const sentAt = Date.now();
          const response = await fetch(window.location.href, { method: 'HEAD', cache: 'no-store' });
          const serverDate = Date.parse(response.headers.get('date'));
          if (Number.isNaN(serverDate)) return;
          const roundTrip = Date.now() - sentAt;
          this.skew = serverDate + roundTrip / 2 - Date.now();
        } catch {
          this.skew = 0;
        }
      }

      remainingMs() {
        return this.endsAt - (Date.now() + this.skew);
      }

      tick() {
        const remaining = this.remainingMs();
        if (remaining <= 0) {
          this.finish(true);
          return;
        }

        const totalSeconds = Math.floor(remaining / 1000);
        const parts = {
          days: Math.floor(totalSeconds / 86400),
          hours: Math.floor((totalSeconds % 86400) / 3600),
          minutes: Math.floor((totalSeconds % 3600) / 60),
          seconds: totalSeconds % 60,
        };
        const underOneHour = remaining < 3600 * 1000;
        Object.entries(parts).forEach(([unit, value]) => {
          const element = this.values[unit];
          if (element) element.textContent = String(value).padStart(2, '0');
        });
        this.secondsWrapper?.toggleAttribute('hidden', !underOneHour);

        if (underOneHour && !this.announcedHour) {
          this.announcedHour = true;
          this.announce(window.countdownStrings.lessThanOneHour);
        }

        const interval = underOneHour ? 1000 : 60 * 1000;
        this.timer = setTimeout(this.tick.bind(this), interval - (remaining % interval || interval));
      }

      finish(announce) {
        clearTimeout(this.timer);
        this.dataset.state = 'ended';
        this.clock?.setAttribute('hidden', '');
        this.hideButton?.setAttribute('hidden', '');
        this.endedMessage?.removeAttribute('hidden');
        if (announce) this.announce(window.countdownStrings.ended);
      }

      announce(message) {
        if (!this.status || !message) return;
        this.status.textContent = '';
        requestAnimationFrame(() => {
          this.status.textContent = message;
        });
      }

      renderLocalTime() {
        if (!this.localTime || typeof Intl === 'undefined') return;
        try {
          const formatter = new Intl.DateTimeFormat(document.documentElement.lang || undefined, {
            dateStyle: 'medium',
            timeStyle: 'short',
            timeZoneName: 'short',
          });
          this.localTime.textContent = ` (${formatter.format(new Date(this.endsAt))})`;
          this.localTime.removeAttribute('hidden');
        } catch {
          // keep the server-rendered UTC text only
        }
      }

      onHide() {
        this.hideClock();
        try {
          sessionStorage.setItem(CountdownTimer.STORAGE_KEY, '1');
        } catch {
          // storage unavailable; hide for this page only
        }
      }

      hideClock() {
        this.clock?.setAttribute('hidden', '');
        this.hideButton?.setAttribute('hidden', '');
        clearTimeout(this.timer);
        this.timer = setTimeout(this.tick.bind(this), 60 * 1000);
      }

      readHiddenPreference() {
        try {
          return sessionStorage.getItem(CountdownTimer.STORAGE_KEY) === '1';
        } catch {
          return false;
        }
      }
    }
  );
}

逐项说明它为什么这样写:

  • 不以访客时钟为真值。 estimateSkew 对当前页发一个 HEAD 且 cache: 'no-store' 的请求,用响应的 Date 头减去往返时间的一半,估出本机时钟与服务端的偏移;之后所有剩余时长都加上这个偏移。这是 Countdown Timer 里的设计建议,未在 Shopify 店面上验证:HEAD 是否被店面与 CDN 正常响应、Date 头是否可读,都要在店铺里实测。请求失败时偏移取 0,退回本机时钟,这是本实现里明确接受的残余风险。
  • 不逐秒更新,也不逐秒朗读。 距离截止超过一小时时每分钟更新一次并隐藏秒位;最后一小时才每秒更新。setTimeout 的延时对齐到整分或整秒,避免累计漂移。role="status" 的区域只在两个节点写入文案:进入最后一小时,以及到期;其余时间保持空白。Dawn 自己对价格块、筛选结果数也是用 role="status" 做一次性播报(无障碍写法)。
  • 可以停止。 WCAG 2.2.2 要求自动更新的内容可以暂停、停止或隐藏。「隐藏倒计时」按钮把数字区藏起来,绝对截止时间仍然可见;选择记在 sessionStorage,同一会话内不再弹回。隐藏后脚本仍每分钟检查一次是否到期,到期时同样切换到「已结束」。
  • 到期即收尾,不重置。 finish 清掉定时器、隐藏数字与按钮、显示「已结束」,并把 data-state 改为 ended。没有任何路径会在到期后重新开始计时;刷新后服务端会按缓存时间或浏览器会按真实时间再次得到 ended。折扣本身是否失效由平台执行,倒计时不参与。
  • 编辑器里不泄漏定时器。 主题编辑器在分区重渲染时先触发 shopify:section:unload 再插入新 HTML,官方文档要求此时清理监听与变量。自定义元素被移出文档就会触发 disconnectedCallback,这里只需要 clearTimeout;Dawn 的 cart.js、product-info.js 等也是在这个回调里做清理,因此不需要再往 theme-editor.js 里加事件处理。estimateSkew 回来时先检查 isConnected,避免给已经卸载的元素起定时器。
  • 减少动态。 数字只做文本替换,没有翻页或缩放动画;CSS 里唯一的透明度过渡放在 prefers-reduced-motion: no-preference 下,与 base.css 处理动效的方式一致,所以脚本里不需要再读 matchMedia。
  • 本地时间只是补充。 renderLocalTime 用 Intl.DateTimeFormat 按页面 lang 把截止时刻格式化成访客时区并带时区缩写,填进服务端留下的 <time>;失败时保留 UTC 文案。页面 lang 来自 theme.liquid 的 request.locale.iso_code。

样式与文案

.countdown-timer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
  text-align: center;
}

.countdown-timer__heading,
.countdown-timer__ends-at,
.countdown-timer__ended {
  margin: 0;
}

.countdown-timer__clock {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2rem;
}

.countdown-timer__unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 6rem;
}

.countdown-timer__value {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: no-preference) {
  .countdown-timer__value {
    transition: opacity var(--duration-short) ease;
  }
}

.countdown-timer__hide {
  background: none;
  border: 0;
  cursor: pointer;
  font-size: 1.2rem;
}

tabular-nums 让数字等宽,更新时不会左右跳动。h2、caption-with-letter-spacing、link link--text、visually-hidden、page-width、color-* 与 gradient 都是 Dawn 既有类,不需要新写。

locales/en.default.json 在 sections 下追加:

"countdown_timer": {
  "days": "Days",
  "hours": "Hours",
  "minutes": "Minutes",
  "seconds": "Seconds",
  "ends_at_html": "Ends {{ ends_at }}",
  "ended": "This offer has ended.",
  "less_than_one_hour": "Less than one hour left.",
  "hide": "Hide countdown",
  "missing_end_time": "Countdown: set the shop metafield promo.ends_at or the fallback end time to show this section."
}

locales/en.default.schema.json 在 sections 下追加 countdown-timer 节点,含 name、settings.paragraph.content(写明值必须等于后台折扣的结束时间)、settings.ends_at_fallback.label / .info(只在元字段为空时使用,ISO 8601 UTC 示例)、settings.heading.label / .default、settings.show_hide_button.label / .info 与 presets.name。其他语言文件没有同步追加,Dawn 的 .theme-check.yml 关闭了 MatchingTranslations,缺失键会回退到默认语言;正式上线前应补齐店铺启用的语言。

layout/theme.liquid 在 window.accessibilityStrings 之前追加:

window.countdownStrings = {
  ended: `{{ 'sections.countdown_timer.ended' | t }}`,
  lessThanOneHour: `{{ 'sections.countdown_timer.less_than_one_hour' | t }}`,
};

对照通用契约

Countdown Timer 的要求本实现状态
同一时间源读 shop.metafields.promo.ends_at;文本设置只作回退部分:元字段与折扣之间的同步要由应用或 Flow 完成,本文未实现
不用客户端时钟作真值HEAD 请求的 Date 头估计偏移设计已落实,店面实测未做
不循环重置只有绝对截止时刻,到期进入 ended 且无重启路径已落实
绝对时间始终可读、机器可读UTC 文案 + <time datetime> + 访客时区文本已落实
只服务一个活动一个 shop 级元字段只能存一个时刻已落实;多活动场景不支持
不逐秒朗读数字区 aria-hidden,role="status" 仅两次写入已落实,读屏实测未做
WCAG 2.2.2 可暂停 / 停止 / 隐藏「隐藏倒计时」按钮,会话内记忆已落实;「必要」例外是否成立未判断
WCAG 2.2.1 时限本分区只用于营销截止,不限制访客操作是否适用需无障碍确认
减少动态无动画;唯一过渡在 no-preference 下已落实
已结束服务端与浏览器两处判定;撤下数字与按钮已落实;折扣失效与缓存页撤下由平台和商家流程负责
无 JavaScript服务端输出绝对截止时间,按钮默认隐藏已落实
时钟偏差过大或数据缺失缺失则不输出;偏移估计失败则退回本机时钟部分:无法检测的本机时钟错误仍会显示错误数字

安装与配置

  1. 把三个新文件放进 sections/ 与 assets/,追加两个语言文件与 theme.liquid 的改动。
  2. 为店铺创建 shop 级元字段定义 promo.ends_at,类型 date_time;由应用在折扣创建或修改时写入 endsAt,或在没有应用时手动填写(手动填写就失去了同源保证,属于退而求其次)。后台创建 shop 级定义的具体路径本文未逐步核验。
  3. 在主题编辑器里添加「Countdown timer」分区。没有元字段也没有回退值时,编辑器内只会看到一行提示,前台不输出。
  4. 后台折扣的结束时间必须与元字段一致;按 Shopify 折扣 与促销兑现核对到期后的购物车行为。

读源码时发现的问题

  • Dawn 的 theme-editor.js 只处理轮播、商品弹窗和图片缩放脚本,对分区级定时器没有通用清理机制;任何带 setInterval / setTimeout 的新分区都得自己在 disconnectedCallback 里收尾。
  • announcement-bar.liquid 的轮播是主题里唯一按时间自动更新的内容,它用 aria-live="polite" 并在自动播放时切到 off(global.js 的 play / pause);倒计时没有沿用这一套,因为每分钟变化的数字即使 polite 也会打扰读屏用户。

建议的验证范围

本篇只做了静态检查(theme check 与 node --check),以下均未执行:

  1. 在开发店铺把 date_time 元字段分别填为 2026-10-31T15:59:00 与 2026-10-31T15:59:00Z,核对服务端 ended 判断与前台静态文案的时区。
  2. 在店面对当前页发 HEAD 请求,确认有 Date 响应头且不被缓存层改写;故意调错本机时钟,核对数字按服务端时间显示。
  3. 在主题编辑器里反复修改分区设置、拖动顺序、删除分区,用浏览器性能面板确认没有残留定时器。
  4. 用屏幕阅读器确认数字区不被读出,进入最后一小时与到期时各只播报一次。
  5. 开启系统的减少动态设置,确认数字只做文本替换。
  6. 禁用 JavaScript,确认只显示绝对截止时间,没有占位数字与按钮。
  7. 把折扣结束时间与元字段故意设成不同值,确认流程上能发现并纠正;这是同源要求的最后一道人工检查。
  8. 同页放两个分区实例,确认各自独立计时、隐藏按钮互不影响。

在知识图谱中的位置

所处阶段
转化
经营领域
店面与体验
工具
Shopify
适合角色
开发

源码基线

读完还有没解决的问题?

把问题发邮件给我们,邮件里已带上本页地址。我们会据此补充或修订正文。