预计阅读时间:12 分钟
1. 业务价值 (Business Value)
本组件旨在解决长页面产品详情导致的“购买中断”问题。通过在用户向下滑动时持续提供购买入口,并实时同步用户所选的规格信息,显著降低加购流失率并提升转化。
2. 核心逻辑与功能 (Core Functions)
2.1 智能滚动监听 (Scroll Intelligence)
- 触发机制:利用
Intersection Observer API监听。当指定的Target Selector(如产品主图或描述块)完全移出视口顶部时,横幅自动滑入。 - 自动收回:用户回滚至主购买区域时,横幅自动隐藏,避免视觉冗余。
2.2 深度变体同步 (Variant Synchronization)
- 实时监听:组件通过
MutationObserver实时监测浏览器 URL 变化(Shopify 切换变体时的标准行为)。 - 动态抓取:切换变体时,JS 会异步请求最新的产品数据,秒级同步横幅内的:
- 变体名称 (如:Red / XL)
- 实时售价 (Price)
-
划线价/促销价 (Compare at Price)
-
智能清洗:自动过滤并隐藏 Shopify 系统默认生成的
"Default Title"。若产品无变体,横幅将保持简洁,仅显示产品标题。
2.3 价格排列控制 (Price Layout Control)
- 顺序切换:支持在后台一键切换划线价与现价的左右顺序(Flex-direction 控制),适配不同国家用户的阅读习惯(如:先看原价对比,或先看现价)。
2.4 原生加购继承 (Native Add-to-Cart Proxy)
- 零冲突方案:点击横幅按钮不执行复杂的 AJAX 代码,而是通过 JS 触发页面原有的加购按钮点击事件。
- 兼容性:完美兼容主题自带的抽屉购物车 (Drawer Cart)、弹窗提醒 (Popup) 以及第三方购物车 App。
3. 技术参数 (Technical Specifications)
| 特性 | 技术栈 / 实现方式 |
|---|---|
| 框架依赖 | 无(Vanilla JavaScript / Native Liquid) |
| 价格处理 | 自动读取 shop.money_format 全局格式 |
| 移动端优化 | 隐藏长标题、按钮通栏化、增大点击热区 |
| 性能损耗 | < 10ms 执行延迟(异步请求与主流程解耦) |
| CSS 层级 | z-index: 2147483647 (确保在全站最顶层) |
4. 后台配置说明 (Schema Settings Guide)
管理员可在 Theme Customize 界面进行以下配置:
4.1 核心配置
- Target Selector: 监听的目标 CSS 选择器(默认
.product-page)。 - Price Order: 划线价位置。可选
row(左侧)或row-reverse(右侧)。
4.2 UI 样式
- Colors: 背景、文字、现价(高亮色)、原价(置灰)的颜色选择器。
- Button Styling: 按钮文字内容、背景色、圆角(0-50px)实时调整。
4.3 布局宽度
- Max Width: 横幅内容的显示上限宽度,确保在大屏显示器下与主题对齐。
5. 开发者维护指南 (Maintenance)
- 类名适配:若在不同主题部署,需通过浏览器 F12 确认产品主容器的 Class,并更新至
Target Selector配置项。 - 样式覆盖:如遇某些特定主题的 Header 遮挡,可适当降低
z-index或调整bottom偏移量。 - 多货币支持:代码已适配多货币切换,会自动抓取当前币种符号。
最终代码,代码位置位于sections/sticky-cart-bar.liquid,使用时添加名为“:
{% comment %}
Sticky Add to Cart Bar - 最终旗舰版
功能:
1. 变体实时同步:跟随页面选择器自动更新价格和名称
2. 智能显示:隐藏无变体产品的 "Default Title"
3. 划线价位置:支持后台自由切换(左/右)
4. 样式配置:背景、文字、价格颜色、按钮圆角均可自定义
5. 性能优化:原生JS,零依赖
{% endcomment %}
{%- if template.name == 'product' -%}
{%- assign target_class = section.settings.target_selector -%}
{%- assign cart_btn_selector = section.settings.add_to_cart_selector -%}
{%- assign current_variant = product.selected_or_first_available_variant -%}
<div id="sticky-cart-wrapper" class="sticky-cart-bar" style="display: none; background-color: {{ section.settings.bg_color }};">
<div class="sticky-cart-container">
<div class="sticky-cart-info">
<div class="sticky-cart-image">
<img id="sticky-variant-image" src="{{ current_variant.featured_image | default: product.featured_image | img_url: '100x100', crop: 'center' }}" alt="{{ product.title }}" width="50" height="50">
</div>
<div class="sticky-cart-details">
<span class="sticky-product-title" style="color: {{ section.settings.text_color }};">{{ product.title }}</span>
<span id="sticky-variant-title" class="sticky-variant-title" style="color: {{ section.settings.text_color }}; {% if current_variant.title contains 'Default Title' %}display:none;{% endif %}">
{{ current_variant.title }}
</span>
<div class="sticky-price-wrapper" style="flex-direction: {{ section.settings.price_order }};">
<span id="sticky-compare-price" class="sticky-product-compare-price" style="color: {{ section.settings.compare_price_color }}; {% unless current_variant.compare_at_price > current_variant.price %}display:none;{% endunless %}">
{{ current_variant.compare_at_price | money }}
</span>
<span id="sticky-current-price" class="sticky-product-price" style="color: {{ section.settings.price_color }};">
{{ current_variant.price | money }}
</span>
</div>
</div>
</div>
<div class="sticky-cart-actions">
<button type="button" class="sticky-submit-btn" onclick="handleStickyAdd()">
{{ section.settings.button_text }}
</button>
</div>
</div>
</div>
<style>
/* 基础样式优化 */
.sticky-cart-bar { position: fixed; bottom: 0; left: 0; width: 100%; box-shadow: 0 -4px 15px rgba(0,0,0,0.1); z-index: 2147483647; padding: 10px 15px; transform: translateY(100%); transition: transform 0.4s ease; box-sizing: border-box; }
.sticky-cart-bar.is-visible { display: block !important; transform: translateY(0); }
.sticky-cart-container { max-width: {{ section.settings.max_width }}px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
/* 左侧信息栏 */
.sticky-cart-info { display: flex; align-items: center; gap: 10px; min-width: 0; flex-shrink: 1; } /* min-width: 0 是防止flex子元素溢出的关键 */
.sticky-cart-image { flex-shrink: 0; }
.sticky-cart-image img { border-radius: 4px; object-fit: cover; display: block; }
.sticky-cart-details { display: flex; flex-direction: column; min-width: 0; }
.sticky-product-title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sticky-variant-title { font-size: 11px; opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 价格栏 */
.sticky-price-wrapper { display: flex; align-items: center; gap: 6px; margin-top: 2px; flex-wrap: wrap; }
.sticky-product-compare-price { text-decoration: line-through; font-size: 12px; opacity: 0.6; white-space: nowrap; }
.sticky-product-price { font-size: 14px; font-weight: 700; white-space: nowrap; }
/* 按钮 */
.sticky-cart-actions { flex-shrink: 0; }
.sticky-submit-btn { background: {{ section.settings.btn_bg_color }}; color: {{ section.settings.btn_text_color }}; border: none; padding: 10px 20px; font-size: 13px; font-weight: bold; cursor: pointer; border-radius: {{ section.settings.btn_radius }}px; text-transform: uppercase; white-space: nowrap; transition: opacity 0.2s; }
/* 移动端适配修复 */
@media (max-width: 767px) {
.sticky-cart-bar { padding: 8px 12px; }
.sticky-product-title { display: none; } /* 移动端通常隐藏标题以腾出空间 */
.sticky-cart-image img { width: 40px; height: 40px; }
.sticky-cart-info { gap: 8px; }
.sticky-cart-actions { flex-grow: 1; margin-left: 5px; }
.sticky-submit-btn { width: 100%; padding: 12px 10px; font-size: 12px; letter-spacing: 0; }
.sticky-price-wrapper { gap: 4px; }
.sticky-product-price { font-size: 13px; }
.sticky-product-compare-price { font-size: 11px; }
}
/* 针对超小屏幕(iPhone SE等)的极简处理 */
@media (max-width: 350px) {
.sticky-variant-title { display: none; }
.sticky-cart-image { display: none; }
}
</style>
<script>
// ... 原有脚本保持不变 ...
function formatMoney(cents) {
const format = "{{ shop.money_format }}";
if (!cents) return "";
let value = (cents / 100).toFixed(2);
return format.replace(/\{\{\s*amount\s*\}\}/, value).replace(/\{\{\s*amount_no_decimals\s*\}\}/, Math.round(value));
}
function handleStickyAdd() {
const customSelector = "{{ cart_btn_selector }}";
const selectors = [customSelector, '.cart-btn', 'form[action*="/cart/add"] button[type="submit"]', 'button[name="add"]'];
let mainBtn = null;
for (let selector of selectors) {
if (!selector) continue;
const found = document.querySelector(selector);
if (found && found.offsetParent !== null) {
mainBtn = found;
break;
}
}
if (mainBtn) { mainBtn.click(); } else {
const form = document.querySelector('form[action*="/cart/add"]');
if (form) form.submit();
}
}
document.addEventListener('DOMContentLoaded', function() {
const stickyBar = document.getElementById('sticky-cart-wrapper');
const target = document.querySelector("{{ target_class }}");
if (target && stickyBar) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const shouldShow = !entry.isIntersecting && entry.boundingClientRect.top < 0;
if (shouldShow) stickyBar.classList.add('is-visible');
else stickyBar.classList.remove('is-visible');
});
}, { threshold: 0 });
observer.observe(target);
}
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
fetch(window.location.pathname + '.js').then(res => res.json()).then(product => {
const urlParams = new URLSearchParams(window.location.search);
const variantId = urlParams.get('variant');
const variant = variantId ? product.variants.find(v => v.id == variantId) : product.variants[0];
if (variant) {
const vt = document.getElementById('sticky-variant-title');
if (variant.title.includes('Default Title')) { vt.style.display = 'none'; }
else { vt.style.display = 'block'; vt.textContent = variant.title; }
document.getElementById('sticky-current-price').innerHTML = formatMoney(variant.price);
const cp = document.getElementById('sticky-compare-price');
if (variant.compare_at_price > variant.price) {
cp.style.display = 'inline'; cp.innerHTML = formatMoney(variant.compare_at_price);
} else { cp.style.display = 'none'; }
}
});
}
}).observe(document, {subtree: true, childList: true});
});
</script>
{%- endif -%}
{% schema %}
{
"name": "Sticky Cart Bar",
"settings": [
{
"type": "header",
"content": "按钮绑定设置"
},
{
"type": "text",
"id": "add_to_cart_selector",
"label": "页面加购按钮类名 (Selector)",
"default": ".cart-btn"
},
{
"type": "text",
"id": "target_selector",
"label": "监听触发块 (Target Class)",
"default": ".product-page"
},
{
"type": "select",
"id": "price_order",
"label": "价格顺序",
"options": [
{ "value": "row", "label": "划线价在前" },
{ "value": "row-reverse", "label": "现价在前" }
],
"default": "row"
},
{ "type": "color", "id": "bg_color", "label": "背景色", "default": "#ffffff" },
{ "type": "color", "id": "text_color", "label": "文字色", "default": "#121212" },
{ "type": "color", "id": "price_color", "label": "现价色", "default": "#ff0000" },
{ "type": "color", "id": "compare_price_color", "label": "原价色", "default": "#9b9b9b" },
{ "type": "text", "id": "button_text", "label": "按钮文字", "default": "ADD TO CART" },
{ "type": "color", "id": "btn_bg_color", "label": "按钮背景", "default": "#000000" },
{ "type": "color", "id": "btn_text_color", "label": "按钮文字颜色", "default": "#ffffff" },
{ "type": "range", "id": "btn_radius", "label": "圆角", "min": 0, "max": 50, "step": 1, "default": 4 },
{ "type": "range", "id": "max_width", "label": "最大宽度", "min": 800, "max": 1600, "step": 10, "default": 1200 }
],
"presets": [{ "name": "Sticky Cart Bar" }]
}
{% endschema %}
本文由 EgbertZhang 原创,转载请注明出处。