电商功能实现
本指南将详细介绍如何在 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>下一步学习
掌握电商功能实现后,建议继续学习:
高级电商功能是提升用户体验和转化率的关键!
最后更新时间: