主题开发实战
本指南将通过实际项目案例,深入介绍 Shopify 主题开发的最佳实践、架构设计和具体实现技巧。
项目架构设计
1. 主题文件结构规划
theme/
├── assets/ # 静态资源
│ ├── styles/
│ │ ├── base/ # 基础样式
│ │ │ ├── reset.css
│ │ │ ├── variables.css
│ │ │ └── typography.css
│ │ ├── components/ # 组件样式
│ │ │ ├── buttons.css
│ │ │ ├── cards.css
│ │ │ └── forms.css
│ │ ├── layout/ # 布局样式
│ │ │ ├── header.css
│ │ │ ├── footer.css
│ │ │ └── grid.css
│ │ └── pages/ # 页面特定样式
│ │ ├── product.css
│ │ ├── collection.css
│ │ └── cart.css
│ ├── scripts/
│ │ ├── modules/ # 功能模块
│ │ │ ├── cart.js
│ │ │ ├── product.js
│ │ │ └── search.js
│ │ ├── utilities/ # 工具函数
│ │ │ ├── dom.js
│ │ │ ├── api.js
│ │ │ └── helpers.js
│ │ └── main.js # 主入口文件
│ └── images/ # 图片资源
├── config/ # 配置文件
├── layout/ # 布局模板
├── sections/ # 分区文件
│ ├── header.liquid
│ ├── footer.liquid
│ ├── product-hero.liquid
│ ├── product-recommendations.liquid
│ ├── collection-filters.liquid
│ └── newsletter-signup.liquid
├── snippets/ # 代码片段
│ ├── components/ # UI组件
│ │ ├── product-card.liquid
│ │ ├── price-display.liquid
│ │ ├── quantity-selector.liquid
│ │ └── variant-selector.liquid
│ ├── utilities/ # 工具片段
│ │ ├── image-responsive.liquid
│ │ ├── social-sharing.liquid
│ │ └── breadcrumbs.liquid
│ └── icons/ # 图标片段
│ ├── icon-cart.liquid
│ ├── icon-search.liquid
│ └── icon-arrow.liquid
└── templates/ # 模板文件
2. 组件化开发策略
<!-- snippets/components/product-card.liquid -->
{% comment %}
产品卡片组件
参数:
- product: 产品对象 (必需)
- image_size: 图片尺寸 (可选, 默认: '300x300')
- show_vendor: 是否显示品牌 (可选, 默认: false)
- show_price: 是否显示价格 (可选, 默认: true)
- css_class: 自定义CSS类 (可选)
- lazy_load: 是否懒加载图片 (可选, 默认: true)
{% endcomment %}
{% assign image_size = image_size | default: '300x300' %}
{% assign show_vendor = show_vendor | default: false %}
{% assign show_price = show_price | default: true %}
{% assign lazy_load = lazy_load | default: true %}
<article class="product-card {{ css_class }}" data-product-id="{{ product.id }}">
<!-- 产品图片 -->
<div class="product-card__image">
<a href="{{ product.url }}" aria-label="{{ product.title }}">
{% if product.featured_image %}
{% if lazy_load %}
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-src="{{ product.featured_image | img_url: image_size }}"
alt="{{ product.featured_image.alt | default: product.title }}"
class="product-card__img lazyload">
{% else %}
<img src="{{ product.featured_image | img_url: image_size }}"
alt="{{ product.featured_image.alt | default: product.title }}"
class="product-card__img">
{% endif %}
<!-- 鼠标悬停显示第二张图片 -->
{% if product.images[1] %}
<img src="{{ product.images[1] | img_url: image_size }}"
alt="{{ product.images[1].alt | default: product.title }}"
class="product-card__img product-card__img--hover">
{% endif %}
{% else %}
<div class="product-card__placeholder">
{% render 'icon-image-placeholder' %}
</div>
{% endif %}
<!-- 产品标签 -->
{% if product.tags contains 'new' %}
<span class="product-card__badge product-card__badge--new">新品</span>
{% elsif product.compare_at_price > product.price %}
{% assign discount_percent = product.compare_at_price | minus: product.price | times: 100 | divided_by: product.compare_at_price | round %}
<span class="product-card__badge product-card__badge--sale">-{{ discount_percent }}%</span>
{% endif %}
<!-- 缺货标识 -->
{% unless product.available %}
<span class="product-card__badge product-card__badge--soldout">缺货</span>
{% endunless %}
</a>
<!-- 快速添加到购物车 -->
{% if product.available and product.variants.size == 1 %}
<button type="button"
class="product-card__quick-add"
data-variant-id="{{ product.first_available_variant.id }}"
aria-label="快速添加 {{ product.title }} 到购物车">
{% render 'icon-cart' %}
</button>
{% endif %}
</div>
<!-- 产品信息 -->
<div class="product-card__content">
{% if show_vendor and product.vendor %}
<p class="product-card__vendor">{{ product.vendor }}</p>
{% endif %}
<h3 class="product-card__title">
<a href="{{ product.url }}">{{ product.title }}</a>
</h3>
{% if show_price %}
<div class="product-card__price">
{% render 'price-display', product: product %}
</div>
{% endif %}
<!-- 产品评分 -->
{% if product.metafields.reviews.rating %}
<div class="product-card__rating">
{% render 'star-rating', rating: product.metafields.reviews.rating %}
</div>
{% endif %}
<!-- 颜色选项 -->
{% assign color_options = product.options_with_values | where: 'name', 'Color' | first %}
{% if color_options.values.size > 1 %}
<div class="product-card__colors">
{% for color in color_options.values limit: 5 %}
<span class="product-card__color"
style="background-color: {{ color | handleize }}"
title="{{ color }}"></span>
{% endfor %}
{% if color_options.values.size > 5 %}
<span class="product-card__color-more">+{{ color_options.values.size | minus: 5 }}</span>
{% endif %}
</div>
{% endif %}
</div>
</article>
3. 响应式布局系统
<!-- snippets/utilities/responsive-grid.liquid -->
{% comment %}
响应式网格组件
参数:
- items: 项目数组
- columns_mobile: 移动端列数 (默认: 1)
- columns_tablet: 平板端列数 (默认: 2)
- columns_desktop: 桌面端列数 (默认: 4)
- gap: 间距 (默认: 'medium')
- item_snippet: 项目渲染片段 (默认: 'grid-item')
{% endcomment %}
{% assign columns_mobile = columns_mobile | default: 1 %}
{% assign columns_tablet = columns_tablet | default: 2 %}
{% assign columns_desktop = columns_desktop | default: 4 %}
{% assign gap = gap | default: 'medium' %}
{% assign item_snippet = item_snippet | default: 'grid-item' %}
<div class="responsive-grid responsive-grid--{{ gap }}"
style="--columns-mobile: {{ columns_mobile }};
--columns-tablet: {{ columns_tablet }};
--columns-desktop: {{ columns_desktop }};">
{% for item in items %}
<div class="responsive-grid__item">
{% render item_snippet, item: item, index: forloop.index %}
</div>
{% endfor %}
</div>
<!-- 对应的CSS -->
<style>
.responsive-grid {
display: grid;
grid-template-columns: repeat(var(--columns-mobile), 1fr);
gap: var(--gap-small);
}
.responsive-grid--small { --gap-small: 0.5rem; --gap-medium: 1rem; --gap-large: 1.5rem; }
.responsive-grid--medium { --gap-small: 1rem; --gap-medium: 1.5rem; --gap-large: 2rem; }
.responsive-grid--large { --gap-small: 1.5rem; --gap-medium: 2rem; --gap-large: 2.5rem; }
@media (min-width: 768px) {
.responsive-grid {
grid-template-columns: repeat(var(--columns-tablet), 1fr);
gap: var(--gap-medium);
}
}
@media (min-width: 1024px) {
.responsive-grid {
grid-template-columns: repeat(var(--columns-desktop), 1fr);
gap: var(--gap-large);
}
}
</style>
高级功能实现
1. 智能搜索和过滤
<!-- sections/collection-filters.liquid -->
<div class="collection-filters" data-collection="{{ collection.handle }}">
<!-- 搜索框 -->
<div class="filter-group">
<label for="search-input" class="filter-label">搜索</label>
<div class="search-input-wrapper">
<input type="text"
id="search-input"
class="search-input"
placeholder="搜索商品..."
data-search-input>
{% render 'icon-search' %}
</div>
</div>
<!-- 价格范围 -->
<div class="filter-group">
<label class="filter-label">价格范围</label>
<div class="price-range">
<input type="range"
id="price-min"
class="price-slider"
min="0"
max="{{ collection.products | map: 'price' | sort | last }}"
data-price-min>
<input type="range"
id="price-max"
class="price-slider"
min="0"
max="{{ collection.products | map: 'price' | sort | last }}"
data-price-max>
<div class="price-display">
<span data-price-min-display>¥0</span> -
<span data-price-max-display>¥{{ collection.products | map: 'price' | sort | last | money_without_currency }}</span>
</div>
</div>
</div>
<!-- 品牌过滤 -->
{% assign vendors = collection.products | map: 'vendor' | uniq | sort %}
{% if vendors.size > 1 %}
<div class="filter-group">
<label class="filter-label">品牌</label>
<div class="filter-options" data-filter="vendor">
{% for vendor in vendors %}
<label class="filter-option">
<input type="checkbox" value="{{ vendor | handleize }}" data-filter-checkbox>
<span class="filter-option-text">{{ vendor }}</span>
<span class="filter-option-count">({{ collection.products | where: 'vendor', vendor | size }})</span>
</label>
{% endfor %}
</div>
</div>
{% endif %}
<!-- 产品类型过滤 -->
{% assign product_types = collection.products | map: 'type' | uniq | sort %}
{% if product_types.size > 1 %}
<div class="filter-group">
<label class="filter-label">类型</label>
<div class="filter-options" data-filter="type">
{% for type in product_types %}
<label class="filter-option">
<input type="checkbox" value="{{ type | handleize }}" data-filter-checkbox>
<span class="filter-option-text">{{ type }}</span>
<span class="filter-option-count">({{ collection.products | where: 'type', type | size }})</span>
</label>
{% endfor %}
</div>
</div>
{% endif %}
<!-- 标签过滤 -->
{% assign all_tags = "" %}
{% for product in collection.products %}
{% for tag in product.tags %}
{% unless all_tags contains tag %}
{% if all_tags == "" %}
{% assign all_tags = tag %}
{% else %}
{% assign all_tags = all_tags | append: "," | append: tag %}
{% endif %}
{% endunless %}
{% endfor %}
{% endfor %}
{% assign unique_tags = all_tags | split: "," | sort %}
{% if unique_tags.size > 0 %}
<div class="filter-group">
<label class="filter-label">标签</label>
<div class="filter-options" data-filter="tags">
{% for tag in unique_tags %}
<label class="filter-option">
<input type="checkbox" value="{{ tag | handleize }}" data-filter-checkbox>
<span class="filter-option-text">{{ tag }}</span>
</label>
{% endfor %}
</div>
</div>
{% endif %}
<!-- 库存状态 -->
<div class="filter-group">
<label class="filter-label">库存状态</label>
<div class="filter-options" data-filter="availability">
<label class="filter-option">
<input type="checkbox" value="available" data-filter-checkbox>
<span class="filter-option-text">有库存</span>
</label>
</div>
</div>
<!-- 清除所有过滤器 -->
<button type="button" class="filter-clear" data-clear-filters>
清除所有过滤器
</button>
</div>
<!-- 产品网格 -->
<div class="products-grid" data-products-container>
{% for product in collection.products %}
<div class="product-item"
data-product-item
data-vendor="{{ product.vendor | handleize }}"
data-type="{{ product.type | handleize }}"
data-price="{{ product.price }}"
data-available="{{ product.available }}"
data-tags="{{ product.tags | join: ',' | handleize }}">
{% render 'product-card', product: product %}
</div>
{% endfor %}
</div>
<script>
// 过滤器JavaScript实现
class CollectionFilters {
constructor() {
this.container = document.querySelector('[data-products-container]')
this.items = document.querySelectorAll('[data-product-item]')
this.searchInput = document.querySelector('[data-search-input]')
this.priceMin = document.querySelector('[data-price-min]')
this.priceMax = document.querySelector('[data-price-max]')
this.checkboxes = document.querySelectorAll('[data-filter-checkbox]')
this.clearBtn = document.querySelector('[data-clear-filters]')
this.init()
}
init() {
this.searchInput?.addEventListener('input', this.handleSearch.bind(this))
this.priceMin?.addEventListener('input', this.handlePriceFilter.bind(this))
this.priceMax?.addEventListener('input', this.handlePriceFilter.bind(this))
this.checkboxes.forEach(cb => {
cb.addEventListener('change', this.handleFilter.bind(this))
})
this.clearBtn?.addEventListener('click', this.clearAllFilters.bind(this))
}
handleSearch(e) {
const query = e.target.value.toLowerCase()
this.filterProducts()
}
handlePriceFilter() {
this.filterProducts()
}
handleFilter() {
this.filterProducts()
}
filterProducts() {
const searchQuery = this.searchInput?.value.toLowerCase() || ''
const minPrice = parseInt(this.priceMin?.value) || 0
const maxPrice = parseInt(this.priceMax?.value) || Infinity
const activeFilters = {
vendor: [],
type: [],
tags: [],
availability: []
}
// 收集活跃的过滤器
this.checkboxes.forEach(cb => {
if (cb.checked) {
const filterType = cb.closest('[data-filter]').dataset.filter
activeFilters[filterType].push(cb.value)
}
})
// 过滤产品
this.items.forEach(item => {
let show = true
// 搜索过滤
if (searchQuery) {
const productText = item.textContent.toLowerCase()
if (!productText.includes(searchQuery)) {
show = false
}
}
// 价格过滤
const price = parseInt(item.dataset.price)
if (price < minPrice || price > maxPrice) {
show = false
}
// 品牌过滤
if (activeFilters.vendor.length > 0) {
if (!activeFilters.vendor.includes(item.dataset.vendor)) {
show = false
}
}
// 类型过滤
if (activeFilters.type.length > 0) {
if (!activeFilters.type.includes(item.dataset.type)) {
show = false
}
}
// 标签过滤
if (activeFilters.tags.length > 0) {
const productTags = item.dataset.tags.split(',')
const hasMatchingTag = activeFilters.tags.some(tag =>
productTags.includes(tag)
)
if (!hasMatchingTag) {
show = false
}
}
// 库存过滤
if (activeFilters.availability.includes('available')) {
if (item.dataset.available !== 'true') {
show = false
}
}
// 显示/隐藏产品
item.style.display = show ? 'block' : 'none'
})
this.updateResultsCount()
}
updateResultsCount() {
const visibleItems = Array.from(this.items).filter(item =>
item.style.display !== 'none'
)
// 更新结果计数显示
const countElement = document.querySelector('[data-results-count]')
if (countElement) {
countElement.textContent = `显示 ${visibleItems.length} 个商品`
}
}
clearAllFilters() {
this.searchInput.value = ''
this.priceMin.value = this.priceMin.min
this.priceMax.value = this.priceMax.max
this.checkboxes.forEach(cb => cb.checked = false)
this.items.forEach(item => {
item.style.display = 'block'
})
this.updateResultsCount()
}
}
// 初始化过滤器
document.addEventListener('DOMContentLoaded', () => {
new CollectionFilters()
})
</script>
2. 动态购物车系统
<!-- snippets/cart-drawer.liquid -->
<div class="cart-drawer" data-cart-drawer>
<div class="cart-drawer__overlay" data-cart-overlay></div>
<div class="cart-drawer__content">
<!-- 购物车头部 -->
<div class="cart-drawer__header">
<h2 class="cart-drawer__title">购物车 (<span data-cart-count>{{ cart.item_count }}</span>)</h2>
<button type="button" class="cart-drawer__close" data-cart-close>
{% render 'icon-close' %}
</button>
</div>
<!-- 购物车内容 -->
<div class="cart-drawer__body" data-cart-items>
{% if cart.item_count > 0 %}
{% for item in cart.items %}
<div class="cart-item" data-cart-item="{{ item.key }}">
<div class="cart-item__image">
<img src="{{ item.image | img_url: '80x80' }}"
alt="{{ item.title }}"
loading="lazy">
</div>
<div class="cart-item__details">
<h4 class="cart-item__title">{{ item.product.title }}</h4>
{% if item.variant.title != 'Default Title' %}
<p class="cart-item__variant">{{ item.variant.title }}</p>
{% endif %}
<!-- 商品属性 -->
{% unless item.properties == empty %}
<div class="cart-item__properties">
{% for property in item.properties %}
{% unless property.last == blank %}
<p class="cart-item__property">
<strong>{{ property.first }}:</strong> {{ property.last }}
</p>
{% endunless %}
{% endfor %}
</div>
{% endunless %}
<!-- 价格和数量 -->
<div class="cart-item__price-quantity">
<div class="cart-item__quantity">
<button type="button"
class="quantity-btn quantity-btn--minus"
data-quantity-change="{{ item.key }}"
data-quantity="decrease">
{% render 'icon-minus' %}
</button>
<input type="number"
class="quantity-input"
value="{{ item.quantity }}"
min="0"
data-quantity-input="{{ item.key }}">
<button type="button"
class="quantity-btn quantity-btn--plus"
data-quantity-change="{{ item.key }}"
data-quantity="increase">
{% render 'icon-plus' %}
</button>
</div>
<div class="cart-item__price">
<span class="cart-item__unit-price">{{ item.price | money }}</span>
{% if item.quantity > 1 %}
<span class="cart-item__total-price">总计: {{ item.line_price | money }}</span>
{% endif %}
</div>
</div>
<!-- 折扣信息 -->
{% if item.line_level_discount_allocations.size > 0 %}
<div class="cart-item__discounts">
{% for discount in item.line_level_discount_allocations %}
<p class="cart-item__discount">
{% render 'icon-discount' %}
{{ discount.discount_application.title }} (-{{ discount.amount | money }})
</p>
{% endfor %}
</div>
{% endif %}
</div>
<!-- 删除按钮 -->
<button type="button"
class="cart-item__remove"
data-cart-remove="{{ item.key }}"
aria-label="移除 {{ item.title }}">
{% render 'icon-trash' %}
</button>
</div>
{% endfor %}
{% else %}
<div class="cart-empty" data-cart-empty>
<div class="cart-empty__icon">
{% render 'icon-cart-empty' %}
</div>
<h3 class="cart-empty__title">购物车是空的</h3>
<p class="cart-empty__text">添加一些商品开始购物吧</p>
<a href="/collections" class="btn btn--primary">继续购物</a>
</div>
{% endif %}
</div>
<!-- 购物车底部 -->
{% if cart.item_count > 0 %}
<div class="cart-drawer__footer">
<!-- 小计 -->
<div class="cart-summary">
<div class="cart-summary__line">
<span>小计:</span>
<span data-cart-subtotal>{{ cart.total_price | money }}</span>
</div>
<!-- 配送信息 -->
<div class="cart-summary__shipping">
{% if cart.total_price >= settings.free_shipping_threshold %}
<p class="shipping-message shipping-message--free">
{% render 'icon-truck' %}
恭喜!您已获得免费配送
</p>
{% else %}
{% assign remaining = settings.free_shipping_threshold | minus: cart.total_price %}
<p class="shipping-message">
{% render 'icon-truck' %}
再购买 {{ remaining | money }} 即可获得免费配送
</p>
<div class="shipping-progress">
{% assign progress = cart.total_price | times: 100 | divided_by: settings.free_shipping_threshold %}
<div class="shipping-progress__bar" style="width: {{ progress }}%"></div>
</div>
{% endif %}
</div>
<!-- 购物车折扣 -->
{% if cart.cart_level_discount_applications.size > 0 %}
<div class="cart-discounts">
{% for discount in cart.cart_level_discount_applications %}
<div class="cart-discount">
{% render 'icon-discount' %}
<span>{{ discount.title }}</span>
<span>-{{ discount.total_allocated_amount | money }}</span>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<!-- 操作按钮 -->
<div class="cart-actions">
<button type="button" class="btn btn--secondary btn--full" data-cart-close>
继续购物
</button>
<a href="/cart" class="btn btn--outline btn--full">
查看购物车
</a>
<button type="submit"
class="btn btn--primary btn--full"
name="add"
data-cart-checkout>
立即结账
</button>
</div>
<!-- 推荐商品 -->
{% if settings.show_cart_recommendations %}
<div class="cart-recommendations">
<h4 class="cart-recommendations__title">您可能还喜欢</h4>
<div class="cart-recommendations__grid">
{% assign recommended_products = collections.recommendations.products | limit: 4 %}
{% for product in recommended_products %}
{% render 'product-card-mini', product: product %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
<script>
// 购物车抽屉功能
class CartDrawer {
constructor() {
this.drawer = document.querySelector('[data-cart-drawer]')
this.overlay = document.querySelector('[data-cart-overlay]')
this.closeBtn = document.querySelector('[data-cart-close]')
this.cartTriggers = document.querySelectorAll('[data-cart-open]')
this.init()
}
init() {
// 绑定事件
this.cartTriggers.forEach(trigger => {
trigger.addEventListener('click', () => this.open())
})
this.closeBtn?.addEventListener('click', () => this.close())
this.overlay?.addEventListener('click', () => this.close())
// 数量变更
document.addEventListener('change', this.handleQuantityChange.bind(this))
document.addEventListener('click', this.handleCartActions.bind(this))
// 键盘事件
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.drawer.classList.contains('is-open')) {
this.close()
}
})
}
open() {
this.drawer.classList.add('is-open')
document.body.classList.add('cart-drawer-open')
// 焦点管理
const firstFocusable = this.drawer.querySelector('button, input, a')
firstFocusable?.focus()
}
close() {
this.drawer.classList.remove('is-open')
document.body.classList.remove('cart-drawer-open')
}
async handleQuantityChange(e) {
if (!e.target.matches('[data-quantity-input]')) return
const key = e.target.dataset.quantityInput
const quantity = parseInt(e.target.value)
if (quantity === 0) {
await this.removeItem(key)
} else {
await this.updateQuantity(key, quantity)
}
}
async handleCartActions(e) {
// 数量增减按钮
if (e.target.matches('[data-quantity-change]')) {
e.preventDefault()
const key = e.target.dataset.quantityChange
const action = e.target.dataset.quantity
const input = document.querySelector(`[data-quantity-input="${key}"]`)
const currentQuantity = parseInt(input.value)
let newQuantity = currentQuantity
if (action === 'increase') {
newQuantity = currentQuantity + 1
} else if (action === 'decrease') {
newQuantity = Math.max(0, currentQuantity - 1)
}
input.value = newQuantity
if (newQuantity === 0) {
await this.removeItem(key)
} else {
await this.updateQuantity(key, newQuantity)
}
}
// 删除商品
if (e.target.matches('[data-cart-remove]')) {
e.preventDefault()
const key = e.target.dataset.cartRemove
await this.removeItem(key)
}
// 结账
if (e.target.matches('[data-cart-checkout]')) {
window.location.href = '/checkout'
}
}
async updateQuantity(key, quantity) {
try {
this.setLoading(true)
const response = await fetch('/cart/change.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: key,
quantity: quantity
})
})
const cart = await response.json()
this.updateCartUI(cart)
} catch (error) {
console.error('更新购物车失败:', error)
this.showError('更新失败,请重试')
} finally {
this.setLoading(false)
}
}
async removeItem(key) {
try {
this.setLoading(true)
const response = await fetch('/cart/change.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: key,
quantity: 0
})
})
const cart = await response.json()
this.updateCartUI(cart)
} catch (error) {
console.error('删除商品失败:', error)
this.showError('删除失败,请重试')
} finally {
this.setLoading(false)
}
}
updateCartUI(cart) {
// 更新购物车计数
document.querySelectorAll('[data-cart-count]').forEach(el => {
el.textContent = cart.item_count
})
// 更新小计
document.querySelectorAll('[data-cart-subtotal]').forEach(el => {
el.textContent = this.formatMoney(cart.total_price)
})
// 如果购物车为空,刷新页面显示空状态
if (cart.item_count === 0) {
location.reload()
}
}
setLoading(loading) {
this.drawer.classList.toggle('is-loading', loading)
}
showError(message) {
// 显示错误提示
const errorEl = document.createElement('div')
errorEl.className = 'cart-error'
errorEl.textContent = message
this.drawer.appendChild(errorEl)
setTimeout(() => {
errorEl.remove()
}, 3000)
}
formatMoney(cents) {
return (cents / 100).toLocaleString('zh-CN', {
style: 'currency',
currency: 'CNY'
})
}
}
// 初始化购物车抽屉
document.addEventListener('DOMContentLoaded', () => {
new CartDrawer()
})
</script>
3. 高级产品功能
<!-- templates/product.liquid -->
<div class="product-page" data-product-id="{{ product.id }}">
<!-- 产品面包屑 -->
<nav class="breadcrumbs">
{% render 'breadcrumbs' %}
</nav>
<!-- 产品主要内容 -->
<div class="product-main">
<!-- 产品图片画廊 -->
<div class="product-gallery">
{% render 'product-gallery', product: product %}
</div>
<!-- 产品信息 -->
<div class="product-info">
<!-- 产品基本信息 -->
<div class="product-header">
{% if product.vendor %}
<p class="product-vendor">{{ product.vendor }}</p>
{% endif %}
<h1 class="product-title">{{ product.title }}</h1>
<!-- 产品评分 -->
{% if product.metafields.reviews.rating %}
<div class="product-rating">
{% render 'star-rating', rating: product.metafields.reviews.rating %}
<span class="rating-count">({{ product.metafields.reviews.count }} 条评价)</span>
</div>
{% endif %}
</div>
<!-- 产品价格 -->
<div class="product-price">
{% render 'price-display', product: product, size: 'large' %}
</div>
<!-- 产品表单 -->
{% form 'product', product, class: 'product-form', data-product-form: '' %}
<!-- 变体选择器 -->
{% if product.variants.size > 1 %}
<div class="product-variants">
{% for option in product.options_with_values %}
<div class="variant-option" data-option-position="{{ option.position }}">
<label class="variant-label">{{ option.name }}</label>
{% case option.name %}
{% when 'Color' or '颜色' %}
{% render 'variant-color-selector', option: option, product: product %}
{% when 'Size' or '尺寸' %}
{% render 'variant-size-selector', option: option, product: product %}
{% else %}
{% render 'variant-dropdown-selector', option: option, product: product %}
{% endcase %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- 数量选择器 -->
<div class="quantity-selector">
<label for="quantity" class="quantity-label">数量</label>
<div class="quantity-input-wrapper">
<button type="button" class="quantity-btn" data-quantity="decrease">
{% render 'icon-minus' %}
</button>
<input type="number"
id="quantity"
name="quantity"
value="1"
min="1"
class="quantity-input"
data-quantity-input>
<button type="button" class="quantity-btn" data-quantity="increase">
{% render 'icon-plus' %}
</button>
</div>
</div>
<!-- 购买按钮 -->
<div class="product-actions">
<button type="submit"
class="btn btn--primary btn--large btn--full product-form__cart-submit"
data-add-to-cart>
<span data-add-to-cart-text>
{% if product.available %}
加入购物车
{% else %}
缺货
{% endif %}
</span>
<div class="loading-spinner" data-loading-spinner></div>
</button>
<!-- 立即购买 -->
{% if product.available %}
<button type="button"
class="btn btn--secondary btn--large btn--full"
data-buy-now>
立即购买
</button>
{% endif %}
<!-- 愿望清单 -->
<button type="button"
class="btn btn--outline wishlist-btn"
data-wishlist-toggle="{{ product.id }}">
{% render 'icon-heart' %}
<span>收藏</span>
</button>
</div>
<!-- 库存状态 -->
<div class="stock-status" data-stock-status>
{% assign selected_variant = product.selected_or_first_available_variant %}
{% if selected_variant.available %}
{% if selected_variant.inventory_quantity <= 5 and selected_variant.inventory_policy == 'deny' %}
<span class="stock-low">仅剩 {{ selected_variant.inventory_quantity }} 件</span>
{% else %}
<span class="stock-available">有库存</span>
{% endif %}
{% else %}
<span class="stock-unavailable">缺货</span>
{% endif %}
</div>
<!-- 产品描述 -->
{% if product.description %}
<div class="product-description">
<h3>产品描述</h3>
<div class="description-content">
{{ product.description }}
</div>
</div>
{% endif %}
<!-- 产品详情标签页 -->
<div class="product-tabs">
<div class="tab-nav">
<button class="tab-btn active" data-tab="description">详细信息</button>
{% if product.metafields.specifications %}
<button class="tab-btn" data-tab="specifications">产品规格</button>
{% endif %}
<button class="tab-btn" data-tab="shipping">配送信息</button>
<button class="tab-btn" data-tab="reviews">用户评价</button>
</div>
<div class="tab-content">
<div class="tab-panel active" data-panel="description">
{{ product.description }}
</div>
{% if product.metafields.specifications %}
<div class="tab-panel" data-panel="specifications">
{{ product.metafields.specifications | metafield_tag }}
</div>
{% endif %}
<div class="tab-panel" data-panel="shipping">
{% render 'shipping-information' %}
</div>
<div class="tab-panel" data-panel="reviews">
{% render 'product-reviews', product: product %}
</div>
</div>
</div>
<!-- 隐藏的变体ID输入 -->
<input type="hidden" name="id" data-variant-id value="{{ product.selected_or_first_available_variant.id }}">
{% endform %}
</div>
</div>
<!-- 推荐商品 -->
<div class="product-recommendations">
{% render 'product-recommendations', product: product %}
</div>
<!-- 最近浏览的商品 -->
<div class="recently-viewed">
{% render 'recently-viewed-products' %}
</div>
</div>
<!-- 产品数据 -->
<script>
window.productData = {{ product | json }};
window.variantData = {{ product.variants | json }};
</script>
下一步学习
掌握了主题开发实战后,建议继续学习:
- 自定义分区开发 - 创建灵活的页面分区
- 自定义代码片段 - 开发可复用组件
- 主题设置配置 - 主题自定义选项
- 响应式设计实现 - 移动优先设计
- 电商功能实现 - 高级电商功能
- 第三方集成 - 外部服务集成
通过实战项目的锻炼,您将能够开发出专业级别的 Shopify 主题!
最后更新时间: