电商功能实现
本指南将详细介绍如何在 Shopify 主题中实现高级电商功能,提升用户购买体验和转化率。
高级购物车功能
1. 智能购物车抽屉
<!-- snippets/smart-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 class="cart-drawer__close" data-cart-close>
{% render 'icon-close' %}
</button>
</div>
<div class="cart-drawer__body">
{% if cart.item_count > 0 %}
<div class="cart-items" data-cart-items>
{% for item in cart.items %}
{% render 'cart-item', item: item %}
{% endfor %}
</div>
<!-- 免费配送进度条 -->
{% if settings.free_shipping_threshold > 0 %}
{% render 'shipping-progress-bar' %}
{% endif %}
<!-- 购物车总计 -->
<div class="cart-summary">
<div class="cart-subtotal">
<span>小计:</span>
<span data-cart-subtotal>{{ cart.total_price | money }}</span>
</div>
{% if cart.total_discounts > 0 %}
<div class="cart-discounts">
<span>折扣:</span>
<span>-{{ cart.total_discounts | money }}</span>
</div>
{% endif %}
</div>
<!-- 结账按钮 -->
<div class="cart-actions">
<button class="btn btn--primary btn--full" data-cart-checkout>
立即结账 - {{ cart.total_price | money }}
</button>
<a href="/cart" class="btn btn--secondary btn--full">
查看完整购物车
</a>
</div>
<!-- 相关推荐 -->
{% render 'cart-recommendations' %}
{% else %}
<div class="cart-empty">
{% render 'cart-empty-state' %}
</div>
{% endif %}
</div>
</div>
</div>
2. 动态购物车更新
// assets/cart-manager.js
class CartManager {
constructor() {
this.cart = null
this.listeners = []
this.init()
}
init() {
this.fetchCart()
this.bindEvents()
}
async fetchCart() {
try {
const response = await fetch('/cart.js')
this.cart = await response.json()
this.notifyListeners('cart:updated', this.cart)
return this.cart
} catch (error) {
console.error('获取购物车失败:', error)
}
}
async addItem(variantId, quantity = 1, properties = {}) {
try {
this.notifyListeners('cart:adding', { variantId, quantity })
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: variantId,
quantity: quantity,
properties: properties
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || '添加到购物车失败')
}
await this.fetchCart()
this.notifyListeners('cart:added', { variantId, quantity })
} catch (error) {
this.notifyListeners('cart:error', error.message)
throw error
}
}
async updateItem(key, quantity) {
try {
this.notifyListeners('cart:updating', { key, quantity })
const response = await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: key, quantity: quantity })
})
this.cart = await response.json()
this.notifyListeners('cart:updated', this.cart)
} catch (error) {
this.notifyListeners('cart:error', '更新购物车失败')
throw error
}
}
async removeItem(key) {
return this.updateItem(key, 0)
}
on(event, callback) {
this.listeners.push({ event, callback })
}
notifyListeners(event, data) {
this.listeners
.filter(listener => listener.event === event)
.forEach(listener => listener.callback(data))
}
getShippingProgress() {
const threshold = window.theme.freeShippingThreshold || 0
const progress = Math.min((this.cart.total_price / threshold) * 100, 100)
const remaining = Math.max(threshold - this.cart.total_price, 0)
return { progress, remaining, qualified: remaining === 0 }
}
}
// 全局购物车管理器
window.CartManager = new CartManager()
产品变体选择器
1. 智能变体选择器
<!-- snippets/variant-selector-smart.liquid -->
<div class="variant-selector" data-variant-selector data-product-id="{{ product.id }}">
{% for option in product.options_with_values %}
<div class="option-group" data-option-index="{{ forloop.index0 }}">
<label class="option-label">{{ option.name }}</label>
{% assign option_name = option.name | downcase %}
{% if option_name contains 'color' or option_name contains '颜色' %}
<!-- 颜色选择器 -->
<div class="color-options">
{% for value in option.values %}
{% assign color_class = value | handle %}
<label class="color-option">
<input type="radio"
name="option-{{ forloop.parentloop.index0 }}"
value="{{ value }}"
data-option-value
{% if forloop.first %}checked{% endif %}>
<span class="color-swatch color-swatch--{{ color_class }}"
style="background-color: {{ value | handle }};"
title="{{ value }}">
<span class="sr-only">{{ value }}</span>
</span>
</label>
{% endfor %}
</div>
{% elsif option_name contains 'size' or option_name contains '尺寸' %}
<!-- 尺寸选择器 -->
<div class="size-options">
{% for value in option.values %}
<label class="size-option">
<input type="radio"
name="option-{{ forloop.parentloop.index0 }}"
value="{{ value }}"
data-option-value
{% if forloop.first %}checked{% endif %}>
<span class="size-label">{{ value }}</span>
</label>
{% endfor %}
</div>
{% else %}
<!-- 下拉选择器 -->
<select class="option-select" data-option-value>
{% for value in option.values %}
<option value="{{ value }}" {% if forloop.first %}selected{% endif %}>
{{ value }}
</option>
{% endfor %}
</select>
{% endif %}
</div>
{% endfor %}
<!-- 变体信息显示 -->
<div class="variant-info" data-variant-info>
<div class="variant-price" data-variant-price>
{{ product.price | money }}
</div>
<div class="variant-availability" data-variant-availability>
{% if product.available %}
<span class="in-stock">有库存</span>
{% else %}
<span class="out-of-stock">缺货</span>
{% endif %}
</div>
<div class="variant-sku" data-variant-sku style="display: none;">
SKU: <span></span>
</div>
</div>
<input type="hidden" name="id" data-variant-id value="{{ product.selected_or_first_available_variant.id }}">
</div>
<script>
// 变体选择器
class VariantSelector {
constructor(element) {
this.container = element
this.productId = element.dataset.productId
this.options = element.querySelectorAll('[data-option-value]')
this.variantIdInput = element.querySelector('[data-variant-id]')
this.priceElement = element.querySelector('[data-variant-price]')
this.availabilityElement = element.querySelector('[data-variant-availability]')
this.skuElement = element.querySelector('[data-variant-sku]')
this.variants = window.productVariants || []
this.currentVariant = this.variants[0]
this.init()
}
init() {
this.options.forEach(option => {
option.addEventListener('change', () => this.onOptionChange())
})
this.updateVariantInfo()
}
onOptionChange() {
const selectedOptions = this.getSelectedOptions()
const variant = this.findVariant(selectedOptions)
if (variant) {
this.currentVariant = variant
this.updateVariantInfo()
this.updateAvailableOptions()
}
this.dispatchVariantChange()
}
getSelectedOptions() {
return Array.from(this.options).map(option => {
if (option.type === 'radio') {
const checked = this.container.querySelector(`input[name="${option.name}"]:checked`)
return checked ? checked.value : null
}
return option.value
})
}
findVariant(selectedOptions) {
return this.variants.find(variant => {
return variant.options.every((option, index) => {
return option === selectedOptions[index]
})
})
}
updateVariantInfo() {
if (!this.currentVariant) return
// 更新价格
if (this.priceElement) {
let priceHtml = this.formatMoney(this.currentVariant.price)
if (this.currentVariant.compare_at_price > this.currentVariant.price) {
priceHtml = `
<span class="sale-price">${this.formatMoney(this.currentVariant.price)}</span>
<span class="compare-price">${this.formatMoney(this.currentVariant.compare_at_price)}</span>
`
}
this.priceElement.innerHTML = priceHtml
}
// 更新库存状态
if (this.availabilityElement) {
const available = this.currentVariant.available
this.availabilityElement.innerHTML = available
? '<span class="in-stock">有库存</span>'
: '<span class="out-of-stock">缺货</span>'
}
// 更新SKU
if (this.skuElement && this.currentVariant.sku) {
this.skuElement.querySelector('span').textContent = this.currentVariant.sku
this.skuElement.style.display = 'block'
}
// 更新隐藏的变体ID
if (this.variantIdInput) {
this.variantIdInput.value = this.currentVariant.id
}
}
updateAvailableOptions() {
// 禁用不可用的选项组合
this.options.forEach((option, optionIndex) => {
const optionValues = option.type === 'radio'
? this.container.querySelectorAll(`input[name="${option.name}"]`)
: [option]
optionValues.forEach(optionValue => {
const testOptions = this.getSelectedOptions()
testOptions[optionIndex] = optionValue.value
const hasAvailableVariant = this.variants.some(variant => {
return variant.available && variant.options.every((opt, idx) => {
return idx === optionIndex || opt === testOptions[idx]
})
})
optionValue.disabled = !hasAvailableVariant
if (option.type === 'radio') {
optionValue.closest('label').classList.toggle('disabled', !hasAvailableVariant)
}
})
})
}
formatMoney(cents) {
return (cents / 100).toLocaleString('zh-CN', {
style: 'currency',
currency: 'CNY'
})
}
dispatchVariantChange() {
const event = new CustomEvent('variant:changed', {
detail: { variant: this.currentVariant }
})
this.container.dispatchEvent(event)
}
}
// 初始化变体选择器
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-variant-selector]').forEach(selector => {
new VariantSelector(selector)
})
})
</script>
高级搜索功能
1. 实时搜索系统
<!-- snippets/instant-search.liquid -->
<div class="instant-search" data-instant-search>
<form class="search-form" data-search-form>
<div class="search-input-wrapper">
<input type="search"
class="search-input"
placeholder="搜索商品..."
data-search-input
autocomplete="off">
<button type="submit" class="search-submit">
{% render 'icon-search' %}
</button>
</div>
</form>
<div class="search-results" data-search-results style="display: none;">
<div class="search-loading" data-search-loading>
<div class="loading-spinner"></div>
<span>搜索中...</span>
</div>
<div class="search-content" data-search-content></div>
<div class="search-footer" data-search-footer style="display: none;">
<a href="#" class="search-view-all" data-search-view-all>
查看所有结果
</a>
</div>
</div>
</div>
<script>
class InstantSearch {
constructor(element) {
this.container = element
this.form = element.querySelector('[data-search-form]')
this.input = element.querySelector('[data-search-input]')
this.results = element.querySelector('[data-search-results]')
this.loading = element.querySelector('[data-search-loading]')
this.content = element.querySelector('[data-search-content]')
this.footer = element.querySelector('[data-search-footer]')
this.viewAllLink = element.querySelector('[data-search-view-all]')
this.debounceTimer = null
this.currentQuery = ''
this.cache = new Map()
this.init()
}
init() {
this.input.addEventListener('input', (e) => {
this.handleInput(e.target.value)
})
this.input.addEventListener('focus', () => {
if (this.currentQuery) {
this.showResults()
}
})
this.input.addEventListener('blur', () => {
setTimeout(() => this.hideResults(), 200)
})
this.form.addEventListener('submit', (e) => {
e.preventDefault()
this.redirectToSearch()
})
// 键盘导航
this.input.addEventListener('keydown', (e) => {
this.handleKeydown(e)
})
}
handleInput(query) {
clearTimeout(this.debounceTimer)
this.currentQuery = query.trim()
if (this.currentQuery.length < 2) {
this.hideResults()
return
}
this.debounceTimer = setTimeout(() => {
this.performSearch(this.currentQuery)
}, 300)
}
async performSearch(query) {
if (this.cache.has(query)) {
this.displayResults(this.cache.get(query), query)
return
}
this.showLoading()
try {
const response = await fetch(
`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=8`
)
const data = await response.json()
const results = data.resources.results
this.cache.set(query, results)
this.displayResults(results, query)
} catch (error) {
console.error('搜索失败:', error)
this.hideResults()
}
}
displayResults(results, query) {
this.hideLoading()
if (!results.products || results.products.length === 0) {
this.content.innerHTML = `
<div class="search-empty">
<p>没有找到相关商品</p>
</div>
`
} else {
const productsHtml = results.products.map(product => `
<a href="${product.url}" class="search-result-item">
<img src="${product.featured_image ? product.featured_image.url + '&width=60' : ''}"
alt="${product.title}"
class="search-result-image"
loading="lazy">
<div class="search-result-info">
<h4 class="search-result-title">${this.highlightQuery(product.title, query)}</h4>
<p class="search-result-price">${this.formatMoney(product.price)}</p>
</div>
</a>
`).join('')
this.content.innerHTML = `
<div class="search-results-grid">
${productsHtml}
</div>
`
}
// 更新查看全部链接
this.viewAllLink.href = `/search?q=${encodeURIComponent(query)}`
this.footer.style.display = 'block'
this.showResults()
}
highlightQuery(text, query) {
const regex = new RegExp(`(${query})`, 'gi')
return text.replace(regex, '<mark>$1</mark>')
}
formatMoney(cents) {
return (cents / 100).toLocaleString('zh-CN', {
style: 'currency',
currency: 'CNY'
})
}
showLoading() {
this.loading.style.display = 'flex'
this.content.style.display = 'none'
this.footer.style.display = 'none'
this.showResults()
}
hideLoading() {
this.loading.style.display = 'none'
this.content.style.display = 'block'
}
showResults() {
this.results.style.display = 'block'
}
hideResults() {
this.results.style.display = 'none'
}
redirectToSearch() {
if (this.currentQuery) {
window.location.href = `/search?q=${encodeURIComponent(this.currentQuery)}`
}
}
handleKeydown(e) {
if (e.key === 'Escape') {
this.hideResults()
} else if (e.key === 'Enter') {
e.preventDefault()
this.redirectToSearch()
}
}
}
// 初始化即时搜索
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-instant-search]').forEach(search => {
new InstantSearch(search)
})
})
</script>
愿望清单功能
1. 本地存储愿望清单
// assets/wishlist.js
class Wishlist {
constructor() {
this.items = this.load()
this.listeners = []
this.init()
}
init() {
this.bindEvents()
this.updateUI()
}
bindEvents() {
document.addEventListener('click', (e) => {
if (e.target.matches('[data-wishlist-add]')) {
e.preventDefault()
const productId = e.target.dataset.wishlistAdd
this.add(productId)
}
if (e.target.matches('[data-wishlist-remove]')) {
e.preventDefault()
const productId = e.target.dataset.wishlistRemove
this.remove(productId)
}
if (e.target.matches('[data-wishlist-toggle]')) {
e.preventDefault()
const productId = e.target.dataset.wishlistToggle
this.toggle(productId)
}
})
}
add(productId) {
if (!this.items.includes(productId)) {
this.items.push(productId)
this.save()
this.updateUI()
this.notify('added', productId)
this.showNotification('已添加到愿望清单')
}
}
remove(productId) {
const index = this.items.indexOf(productId)
if (index > -1) {
this.items.splice(index, 1)
this.save()
this.updateUI()
this.notify('removed', productId)
this.showNotification('已从愿望清单移除')
}
}
toggle(productId) {
if (this.has(productId)) {
this.remove(productId)
} else {
this.add(productId)
}
}
has(productId) {
return this.items.includes(productId)
}
count() {
return this.items.length
}
clear() {
this.items = []
this.save()
this.updateUI()
this.notify('cleared')
}
load() {
try {
const stored = localStorage.getItem('shopify_wishlist')
return stored ? JSON.parse(stored) : []
} catch {
return []
}
}
save() {
localStorage.setItem('shopify_wishlist', JSON.stringify(this.items))
}
updateUI() {
// 更新愿望清单计数
document.querySelectorAll('[data-wishlist-count]').forEach(el => {
el.textContent = this.count()
})
// 更新按钮状态
document.querySelectorAll('[data-wishlist-toggle]').forEach(btn => {
const productId = btn.dataset.wishlistToggle
const isInWishlist = this.has(productId)
btn.classList.toggle('active', isInWishlist)
btn.setAttribute('aria-pressed', isInWishlist)
const text = btn.querySelector('[data-wishlist-text]')
if (text) {
text.textContent = isInWishlist ? '已收藏' : '收藏'
}
})
// 更新愿望清单页面
this.updateWishlistPage()
}
async updateWishlistPage() {
const container = document.querySelector('[data-wishlist-products]')
if (!container) return
if (this.items.length === 0) {
container.innerHTML = `
<div class="wishlist-empty">
<h3>愿望清单是空的</h3>
<p>添加一些商品到愿望清单吧</p>
<a href="/collections" class="btn btn--primary">继续购物</a>
</div>
`
return
}
try {
const productPromises = this.items.map(id =>
fetch(`/products/${id}.js`).then(r => r.json())
)
const products = await Promise.all(productPromises)
container.innerHTML = `
<div class="wishlist-grid">
${products.map(product => this.renderWishlistItem(product)).join('')}
</div>
`
} catch (error) {
console.error('加载愿望清单商品失败:', error)
}
}
renderWishlistItem(product) {
return `
<div class="wishlist-item">
<a href="/products/${product.handle}" class="wishlist-item__image">
<img src="${product.featured_image}" alt="${product.title}" loading="lazy">
</a>
<div class="wishlist-item__info">
<h4 class="wishlist-item__title">
<a href="/products/${product.handle}">${product.title}</a>
</h4>
<p class="wishlist-item__price">${this.formatMoney(product.price)}</p>
<div class="wishlist-item__actions">
<button class="btn btn--primary" onclick="CartManager.addItem(${product.variants[0].id})">
加入购物车
</button>
<button class="btn btn--outline" data-wishlist-remove="${product.id}">
移除
</button>
</div>
</div>
</div>
`
}
formatMoney(cents) {
return (cents / 100).toLocaleString('zh-CN', {
style: 'currency',
currency: 'CNY'
})
}
notify(action, productId) {
this.listeners.forEach(listener => {
listener({ action, productId, items: this.items })
})
}
on(callback) {
this.listeners.push(callback)
}
showNotification(message) {
const notification = document.createElement('div')
notification.className = 'notification notification--success'
notification.textContent = message
document.body.appendChild(notification)
setTimeout(() => {
notification.classList.add('notification--show')
}, 100)
setTimeout(() => {
notification.classList.remove('notification--show')
setTimeout(() => notification.remove(), 300)
}, 3000)
}
}
// 全局愿望清单
window.Wishlist = new Wishlist()
客户评价系统
1. 评价显示组件
<!-- snippets/product-reviews.liquid -->
<div class="product-reviews" data-product-reviews data-product-id="{{ product.id }}">
<div class="reviews-summary">
<div class="reviews-rating">
<div class="rating-stars">
{% assign rating = product.metafields.reviews.rating | default: 0 %}
{% render 'star-rating', rating: rating %}
</div>
<span class="rating-average">{{ rating | round: 1 }}</span>
<span class="rating-count">({{ product.metafields.reviews.count | default: 0 }} 条评价)</span>
</div>
<button class="btn btn--outline" data-write-review>
写评价
</button>
</div>
<div class="reviews-list" data-reviews-list>
<!-- 评价列表将通过JavaScript加载 -->
</div>
<div class="reviews-pagination" data-reviews-pagination style="display: none;">
<!-- 分页控件 -->
</div>
</div>
<!-- 评价表单模态框 -->
<div class="review-modal" data-review-modal style="display: none;">
<div class="review-modal__overlay"></div>
<div class="review-modal__content">
<div class="review-modal__header">
<h3>写评价</h3>
<button class="review-modal__close" data-review-modal-close>
{% render 'icon-close' %}
</button>
</div>
<form class="review-form" data-review-form>
<div class="form-group">
<label>评分</label>
<div class="rating-input" data-rating-input>
{% for i in (1..5) %}
<button type="button" class="rating-star" data-rating="{{ i }}">
★
</button>
{% endfor %}
</div>
<input type="hidden" name="rating" data-rating-value required>
</div>
<div class="form-group">
<label for="review-title">评价标题</label>
<input type="text" id="review-title" name="title" required>
</div>
<div class="form-group">
<label for="review-content">评价内容</label>
<textarea id="review-content" name="content" rows="4" required></textarea>
</div>
<div class="form-group">
<label for="reviewer-name">您的姓名</label>
<input type="text" id="reviewer-name" name="name" required>
</div>
<div class="form-group">
<label for="reviewer-email">邮箱</label>
<input type="email" id="reviewer-email" name="email" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn--primary">
提交评价
</button>
<button type="button" class="btn btn--secondary" data-review-modal-close>
取消
</button>
</div>
</form>
</div>
</div>
<script>
class ProductReviews {
constructor(element) {
this.container = element
this.productId = element.dataset.productId
this.reviewsList = element.querySelector('[data-reviews-list]')
this.writeReviewBtn = element.querySelector('[data-write-review]')
this.modal = document.querySelector('[data-review-modal]')
this.form = document.querySelector('[data-review-form]')
this.currentPage = 1
this.reviews = []
this.init()
}
init() {
this.loadReviews()
this.bindEvents()
}
bindEvents() {
this.writeReviewBtn?.addEventListener('click', () => {
this.openReviewModal()
})
this.modal?.querySelector('[data-review-modal-close]').addEventListener('click', () => {
this.closeReviewModal()
})
this.modal?.querySelector('.review-modal__overlay').addEventListener('click', () => {
this.closeReviewModal()
})
// 评分输入
this.modal?.querySelectorAll('[data-rating]').forEach(star => {
star.addEventListener('click', (e) => {
this.setRating(parseInt(e.target.dataset.rating))
})
})
this.form?.addEventListener('submit', (e) => {
e.preventDefault()
this.submitReview()
})
}
async loadReviews() {
try {
// 这里需要集成您选择的评价系统API
// 例如:Judge.me, Yotpo, 或自定义评价系统
const response = await fetch(`/apps/reviews/api/products/${this.productId}/reviews`)
const data = await response.json()
this.reviews = data.reviews
this.renderReviews()
} catch (error) {
console.error('加载评价失败:', error)
}
}
renderReviews() {
if (this.reviews.length === 0) {
this.reviewsList.innerHTML = `
<div class="reviews-empty">
<p>暂无评价,成为第一个评价的用户吧!</p>
</div>
`
return
}
const reviewsHtml = this.reviews.map(review => `
<div class="review-item">
<div class="review-header">
<div class="review-rating">
${this.renderStars(review.rating)}
</div>
<div class="review-meta">
<span class="review-author">${review.author}</span>
<span class="review-date">${this.formatDate(review.created_at)}</span>
</div>
</div>
<div class="review-content">
<h4 class="review-title">${review.title}</h4>
<p class="review-text">${review.content}</p>
</div>
<div class="review-actions">
<button class="review-helpful" data-review-helpful="${review.id}">
有帮助 (${review.helpful_count || 0})
</button>
</div>
</div>
`).join('')
this.reviewsList.innerHTML = reviewsHtml
}
renderStars(rating) {
let stars = ''
for (let i = 1; i <= 5; i++) {
stars += `<span class="star ${i <= rating ? 'filled' : ''}">★</span>`
}
return stars
}
openReviewModal() {
this.modal.style.display = 'block'
document.body.style.overflow = 'hidden'
}
closeReviewModal() {
this.modal.style.display = 'none'
document.body.style.overflow = ''
this.form.reset()
this.resetRating()
}
setRating(rating) {
const stars = this.modal.querySelectorAll('[data-rating]')
const ratingInput = this.modal.querySelector('[data-rating-value]')
stars.forEach((star, index) => {
star.classList.toggle('active', index < rating)
})
ratingInput.value = rating
}
resetRating() {
const stars = this.modal.querySelectorAll('[data-rating]')
stars.forEach(star => star.classList.remove('active'))
this.modal.querySelector('[data-rating-value]').value = ''
}
async submitReview() {
const formData = new FormData(this.form)
try {
const response = await fetch('/apps/reviews/api/reviews', {
method: 'POST',
body: formData
})
if (response.ok) {
this.closeReviewModal()
this.showNotification('评价提交成功,审核后将显示')
// 重新加载评价
this.loadReviews()
} else {
throw new Error('提交失败')
}
} catch (error) {
this.showNotification('提交失败,请重试', 'error')
}
}
formatDate(dateString) {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN')
}
showNotification(message, type = 'success') {
// 显示通知的实现
console.log(`${type}: ${message}`)
}
}
// 初始化产品评价
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-product-reviews]').forEach(reviews => {
new ProductReviews(reviews)
})
})
</script>
下一步学习
掌握电商功能实现后,建议继续学习:
高级电商功能是提升用户体验和转化率的关键!
最后更新时间: