Horizon 吸底加购与可售状态:库存文案、取货与加购反馈
本文基于 Horizon 4.2.0,源码固定到 5acd1b6(核验于 2026-10-07)。结论限于此提交,不表示其他版本行为相同;本篇为静态源码分析,未完成店铺运行验证。Dawn 上游已发布 v16.0.0(2026-08-10,GitHub Releases 核对),本库 Dawn 正文统一以 v15.3.0 为基线,升级前需重新核对;本库 Horizon 正文统一以 4.2.0 为基线(固定提交中 config/settings_schema.json 的 theme_version 为 4.2.0)。
对应哪些通用模式
映射购买区与库存状态与到货通知两个模块,展现形式为 Sticky Bar 与 Toast。此版本有吸底加购栏、库存文案与门店取货;在固定提交的全仓库搜索中未找到到货通知与 Toast(见文末搜索口径)。数量、加购提交与变体队列已在购买流程说明,变体切换见变体选择,吸底栏的存在与页面组合见商品页,本篇只补它们没写的显示条件、状态词与反馈链。
入口与文件
| 文件 | 职责 |
|---|---|
| sections/product-information.liquid | 输出吸底栏标记与样式,schema 设置 enable_sticky_add_to_cart(默认 true) |
| assets/sticky-add-to-cart.js | 显示/隐藏、变体同步、代点击主按钮 |
| blocks/buy-buttons.liquid | 可售判断与按钮文案、错误位、liveRegion、门店取货 |
| blocks/product-inventory.liquid、product-inventory.js | 库存状态文案(low / in_stock / out_of_stock) |
| assets/product-form.js、add-to-cart-button | 加购请求、错误与朗读、按钮动画 |
| assets/local-pickup.js | 取货可用性随变体刷新 |
| assets/header-actions.js、cart-drawer.js | 购物车数量朗读、可选的抽屉自动打开 |
数据从哪来
- 按钮可售(
buy-buttons.liquid):取selected_or_first_available_variant;variant.available为真显示products.product.add_to_cart;否则在「inventory_management == 'shopify'、库存 ≤ 0、策略deny」或quantity_rule.min大于库存时显示sold_out(该 Liquid 条件混用and/or,实际求值顺序未核验),其余为unavailable(含无变体)。不可售时按钮disabled,加速结账容器hidden。 - 库存块:
product-inventory只在inventory_management == 'shopify'时读inventory_quantity。库存 > 0 且 ≤inventory_threshold(默认 10)为低库存,show_inventory_quantity(默认true)决定显示「N left」还是「Low stock」;库存 ≤ 0 且策略continue显示In stock;未被追踪的变体只要存在即In stock。默认templates/product.json不含该块,需商家添加。 - 取货:
variant.store_availabilities | where: 'pick_up_enabled', true;摘要只读取列表第一项,抽屉(theme-drawer内的dialog)列出全部地点。切换变体后local-pickup.js请求商品地址?variant=§ion_id=并 morph。设置show_pickup_availability默认true。 - 加购请求:
product-form.js向routes.cart_add_url + '.js'提交FormData,附带页面上cart-items-component的分区 ID;响应含status即视为失败。
交互怎样运行
- 吸底栏:
sticky-add-to-cart.js用IntersectionObserver观察.buy-buttons-block:主购买区滚出视口上方才显示;页脚与视口相交时隐藏(观察器rootMargin为200px 0px 0px 0px);shopify-chat已注册且页面有该元素时不显示;找不到.buy-buttons-block或footer时不初始化观察器。data-stuck切换transform/opacity。价格、标题、变体名来自服务端渲染,变体切换后从返回的 HTML morph 整条栏并同步data-variant-available,数量变化时按钮文字追加「(n)」。 - 吸底栏点击:给主表单的加购按钮设
data-puppet='true'后调用.click(),走同一条handleSubmit;随后立即给自身设data-added并创建fly-to-cart元素,不等请求结果。 - 反馈链:点击时
handleClick即启动按钮的对勾动画(data-added,约 800 ms 后复位;add_to_cart_animation开启且非快速购买弹窗时还有飞入购物车动画),此时请求尚未返回。成功后,隐藏的role="status"区域写入Added(5 秒后清空),头部另有区域朗读Total items in cart: N;auto_open_cart_drawer开启(默认false,schema 中仅在cart_type == 'drawer'时可见)且不在 /cart 页时,抽屉自动打开。失败后(含超上限的预检与 422 响应)把消息(预检为quantity_error_max,422 为response.message)写入购买按钮组上方的.product-form-text__error与同一朗读区,10 秒后隐藏。网络异常只console.error并派发code为SERVICE_UNAVAILABLE的CartErrorEvent;已读到的监听者中,礼品卡表单只处理INVALID,吸底栏只复位data-puppet,均不向用户显示。
与通用契约的一致与差距
一致
- 吸底栏与主购买区同源:价格、变体名、可售状态都来自同一次商品渲染与变体切换结果;栏本身有
role="region"与名称(Quick add to cart bar);过渡放在prefers-reduced-motion: no-preference内,底边内距用--safe-area-inset-bottom;页脚附近与聊天入口出现时收起,与 Sticky Bar 的避让要求对应。无 JS 时该栏保持屏外,页内原位按钮仍在。 - 不可售选项在选择器里带
aria-disabled="true"与「- Sold out」,下拉项带「- Unavailable」,不只靠颜色。 - 库存追踪关闭时不显示数量;库存数字来自渲染时的
inventory_quantity,变体切换后随服务端结果替换。 - 数量超限在提交前拦截并显示
quantity_error_max;422 的平台message原样显示,没有改成成功。
差距
- 不可售时吸底栏整条隐藏(
display: none),而不是显示同样的售罄状态,与 Sticky Bar「固定栏显示同样状态」不同;标记里的Sold out文案因此看不到。 - 收起的栏仍可聚焦:栏收起只靠
opacity: 0与屏外位移,未见visibility、inert或aria-hidden,按静态阅读,按钮仍在 Tab 顺序与可访问性树中。窄屏(≤749px)按钮文字被display: none,图标 SVG 为aria-hidden,按钮没有aria-label,可访问名称按代码为空。 - 未见
scroll-padding:固定栏与固定页眉都没有为焦点滚动预留偏移,2.4.11 需实测。 - 成功反馈先于结果:对勾与飞入动画在点击时启动,422 失败时已播放;吸底栏还会无条件播放;
fly-to-cart.js与其样式中未见prefers-reduced-motion判断。 - 错误不常驻:错误 10 秒后自动隐藏,与 Toast「错误就近常驻」不同;只显示
message,不显示description;吸底栏触发的失败出现在主购买区,而栏只在主区滚出视口后显示,用户可能看不到,只有朗读区会播报。 - 朗读细节:成功文案只有
Added,不含商品与变体;朗读区同时写role="status"与aria-live="assertive",读屏实际优先级未核验;product-inventory的role="status"同时带aria-label,是否覆盖内容未核验;无「加载中」状态,变体请求返回前继续显示上一变体的库存文字。 - 状态词不全:库存 ≤ 0 但继续销售时显示
In stock,没有「缺货预订」;未使用incoming、next_incoming_date;没有预售文案;也没有对买家所在地区不可配送的处理(未见对应错误码分支)。 - 销售计划:
selling_plan只出现在quick-add.liquid(有销售计划的商品一律转商品页)与购物车行显示,商品页购买区没有计划选择器与selling_plan输入,需应用块提供,未核验。 - 取货摘要只读第一个地点,可能与抽屉内其他地点的可用性不一致。
- 到货通知与 Toast:在固定提交的全仓库搜索中未找到到货通知与 Toast(搜索词见文末);
cart_type只有page与drawer,没有弹出通知。email-signup是通用form 'customer'表单,与变体无关。
定制入口与风险
编辑器可改(schema 设置名):enable_sticky_add_to_cart(商品信息 section);inventory_threshold、show_inventory_quantity(库存块);show_pickup_availability、stacking、gift_card_form(购买按钮块);主题设置 add_to_cart_animation、cart_type、auto_open_cart_drawer。文案在 products.product.* 与 content.inventory_* 等 locale 键。耦合点:吸底栏依赖 .buy-buttons-block、product-form-component[data-product-id]、[ref="addToCartButton"] 与页脚元素;data-puppet 由 product-form 与吸底栏共同读写;修改按钮标记后须同时核对 morph 与状态复位。featured-product-information 中未见吸底栏设置。
建议的验证范围
均未执行:在有货、低库存、售罄、库存 ≤ 0 继续销售、未追踪四种变体上检查库存块、按钮与吸底栏;不存在的选项组合下吸底栏是否隐藏;超上限与 422 失败时对勾动画、错误位与朗读的先后;从吸底栏触发失败后错误是否可见;窄屏用读屏检查吸底按钮名称;Tab 是否进入屏外收起的栏;禁用 JS 与开启减少动态偏好;多取货地点下摘要与抽屉是否一致。
搜索口径:对固定提交整个检出(含 sections、snippets、blocks、assets、templates、locales、config,含隐藏文件)用 rg -uui 搜索 notify、back in stock、back_in_stock、restock、waitlist、coming soon、backorder、toast、snackbar,均无命中;notification 仅命中 header-actions.js 的注释;selling_plan 命中见上。