Skip to Content
@anker-in/headless-ui 2.0 is released 🎉

Specs (对比表单)

支持动态产品切换、响应式列数、富文本规格值、多货币价格展示的产品对比组件【✅ 已发布】

加载中...
当前视口: 1920px × 600px场景: 两款产品规格对比
打开链接

功能特性

  • 产品对比表格: 左侧规格列表,右侧产品对比,清晰展示多产品差异
  • 动态产品切换: 下拉菜单选择不同产品进行对比,支持实时切换
  • 响应式列数: 移动端最多 2 列,桌面端最多 3 列,自动适配屏幕
  • SKU 信息展示: 产品图片、价格、优惠券信息完整展示
  • 富文本支持: 规格值支持 HTML 和图片展示,支持复杂内容
  • 价格格式化: 支持多货币、多地区价格展示,优惠券价格自动显示
  • 智能图片识别: 自动识别规格值中的图片 URL,支持 8 种图片格式
  • 点击外部关闭: 下拉菜单点击外部自动关闭,交互友好

Props 参数

主要 Props

Prop类型默认值必需说明
dataSpecsData-规格对比数据配置
buildDataBuildData-Shopify 产品数据
onChange(product, index) => void--产品选择回调
onSecondaryChange(product, index) => void--次要操作回调
classNamestring''-自定义类名

类型定义

SpecsData

/** * 规格对比数据配置 */ interface SpecsData { data: { /** * 左侧规格菜单 */ LeftMenu: { data: Array<{ /** * 规格分类标题 */ title: string /** * 规格项列表 */ subTitle: string[] /** * 是否包含产品信息(图片、价格、按钮) * 设置为 false 可仅显示规格对比表格 */ isProduct?: boolean }> } /** * 右侧产品数据 */ RightMenu: { menus: Array<{ /** * 产品 handle,用于匹配 buildData */ handle: string /** * SKU 编号 */ sku: string /** * 规格值对象 * 键为规格名称,值为规格内容 * 支持三种格式: * 1. 纯文本字符串 * 2. 图片 URL 字符串(自动识别) * 3. 图文对象 { text: string; imgUrl: string } */ subTitle: { [specName: string]: string | { text: string; imgUrl: string } } }> } /** * 默认选中配置 */ DefaultSelectMenu: { /** * 逗号分隔的默认 SKU * 示例: "A1340011,A2524014,A3456789" * 移动端自动限制为前 2 个 */ sku: string /** * 按钮文本 * 可以是统一字符串,或按索引自定义 */ buttonText: string | { [index: string]: string } } } }

BuildData

/** * Shopify 产品数据 */ interface BuildData { products: Array<{ /** * 产品 handle,对应 RightMenu.menus[].handle */ handle: string /** * 产品名称 */ name: string /** * 产品标题 */ title: string /** * 产品价格 */ price: { value: number currencyCode: string } /** * 产品变体列表 */ variants: Array<{ /** * SKU 编号 */ sku: string /** * 变体价格 */ price: number /** * 变体图片 */ image: { url: string altText: string } /** * 是否可售 */ availableForSale: boolean /** * 优惠券列表 */ coupons?: Array<{ /** * 优惠后价格 */ variant_price4wscode: number }> }> }> }

SpecValue 类型

/** * 规格值支持三种格式 */ type SpecValue = | string // 纯文本: "27,650mAh" | string // 图片 URL: "https://example.com/image.png" | { // 图文对象 text: string // 文本内容,支持 HTML imgUrl: string // 图片 URL }

使用示例

示例 1: 基础产品对比

import { Specs } from '@anker-in/headless-ui/biz' export default function BasicSpecsExample() { const data = { data: { LeftMenu: { data: [ { title: 'Specifications', isProduct: true, subTitle: [ 'Battery Capacity', 'Charging Power', 'Ports', 'Weight', ], }, ], }, RightMenu: { menus: [ { handle: 'power-bank-250w', sku: 'A1340011', subTitle: { 'Battery Capacity': '27,650mAh', 'Charging Power': '250W', Ports: '2× USB-C, 1× USB-A', Weight: '548g', }, }, { handle: 'power-bank-140w', sku: 'A2524014', subTitle: { 'Battery Capacity': '20,000mAh', 'Charging Power': '140W', Ports: '2× USB-C', Weight: '368g', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011,A2524014', buttonText: 'Buy Now', }, }, } const buildData = { products: [ { handle: 'power-bank-250w', name: 'Anker Prime 250W', title: 'Anker Prime 250W Power Bank', price: { value: 179.99, currencyCode: 'USD' }, variants: [ { sku: 'A1340011', price: 179.99, image: { url: 'https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5?w=400&q=80', altText: 'Power Bank', }, availableForSale: true, }, ], }, { handle: 'power-bank-140w', name: 'Anker 737 140W', title: 'Anker 737 Power Bank', price: { value: 149.99, currencyCode: 'USD' }, variants: [ { sku: 'A2524014', price: 149.99, image: { url: 'https://images.unsplash.com/photo-1585338107529-13afc5f02586?w=400&q=80', altText: 'Power Bank', }, availableForSale: true, }, ], }, ], } return <Specs data={data} buildData={buildData} /> }

示例 2: 带图片规格值

import { Specs } from '@anker-in/headless-ui/biz' export default function ImageSpecsExample() { const data = { data: { LeftMenu: { data: [ { title: 'Design', isProduct: true, subTitle: ['Color', 'Size Chart', 'Material'], }, ], }, RightMenu: { menus: [ { handle: 'product-a', sku: 'SKU001', subTitle: { // 纯图片 URL,自动识别并显示 Color: 'https://example.com/color-swatch.png', // 图文混合对象 'Size Chart': { text: 'View Full Chart', imgUrl: 'https://example.com/size-chart.png', }, // 纯文本 Material: 'Premium Aluminum Alloy', }, }, ], }, DefaultSelectMenu: { sku: 'SKU001', buttonText: 'Add to Cart', }, }, } const buildData = { products: [ { handle: 'product-a', name: 'Product A', title: 'Product A', price: { value: 99.99, currencyCode: 'USD' }, variants: [ { sku: 'SKU001', price: 99.99, image: { url: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400&q=80', altText: 'Product', }, availableForSale: true, }, ], }, ], } return <Specs data={data} buildData={buildData} /> }

示例 3: 三产品对比(移动端限制为 2 列)

import { Specs } from '@anker-in/headless-ui/biz' export default function ThreeProductsExample() { const data = { data: { LeftMenu: { data: [ { title: 'Specifications', isProduct: true, subTitle: ['Capacity', 'Power', 'Price Range'], }, ], }, RightMenu: { menus: [ { handle: 'pro-model', sku: 'A1340011', subTitle: { Capacity: '27,650mAh', Power: '250W', 'Price Range': 'Premium', }, }, { handle: 'standard-model', sku: 'A2524014', subTitle: { Capacity: '20,000mAh', Power: '140W', 'Price Range': 'Mid-range', }, }, { handle: 'lite-model', sku: 'A1229011', subTitle: { Capacity: '10,000mAh', Power: '20W', 'Price Range': 'Budget', }, }, ], }, DefaultSelectMenu: { // 注意:移动端自动限制为前 2 个 SKU sku: 'A1340011,A2524014,A1229011', buttonText: 'Buy Now', }, }, } const buildData = { products: [ /* 3 个产品数据 */ ], } return <Specs data={data} buildData={buildData} /> }

示例 4: 自定义按钮文本(多产品)

import { Specs } from '@anker-in/headless-ui/biz' export default function CustomButtonTextExample() { const data = { data: { LeftMenu: { data: [ { title: 'Specifications', isProduct: true, subTitle: ['Capacity', 'Power'], }, ], }, RightMenu: { menus: [ { handle: 'pro-model', sku: 'A1340011', subTitle: { Capacity: '27,650mAh', Power: '250W', }, }, { handle: 'standard-model', sku: 'A2524014', subTitle: { Capacity: '20,000mAh', Power: '140W', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011,A2524014', // 为不同产品设置不同按钮文本 buttonText: { '0': 'Buy Pro Model', '1': 'Buy Standard Model', }, }, }, } const buildData = { products: [ /* 产品数据 */ ], } return <Specs data={data} buildData={buildData} /> }

示例 5: 仅规格对比(无产品信息)

import { Specs } from '@anker-in/headless-ui/biz' export default function SpecsOnlyExample() { const data = { data: { LeftMenu: { data: [ { title: 'Technical Specs', // 不显示产品卡片 isProduct: false, subTitle: ['Input', 'Output', 'Dimensions', 'Warranty'], }, ], }, RightMenu: { menus: [ { handle: 'product-a', sku: 'A1340011', subTitle: { Input: '100-240V~50/60Hz', Output: '5V⎓3A / 9V⎓3A', Dimensions: '29 × 29 × 29mm', Warranty: '24 Months', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011', // 不显示按钮 buttonText: '', }, }, } const buildData = { products: [ /* 产品数据 */ ], } return <Specs data={data} buildData={buildData} /> }

示例 6: 带优惠券价格

import { Specs } from '@anker-in/headless-ui/biz' export default function CouponPriceExample() { const data = { data: { LeftMenu: { data: [ { title: 'Specifications', isProduct: true, subTitle: ['Capacity', 'Power'], }, ], }, RightMenu: { menus: [ { handle: 'power-bank', sku: 'A1340011', subTitle: { Capacity: '27,650mAh', Power: '250W', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011', buttonText: 'Buy Now', }, }, } const buildData = { products: [ { handle: 'power-bank', name: 'Anker Prime', title: 'Anker Prime Power Bank', price: { value: 179.99, currencyCode: 'USD' }, variants: [ { sku: 'A1340011', price: 179.99, image: { url: 'https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5?w=400&q=80', altText: 'Power Bank', }, availableForSale: true, // 优惠券数据 coupons: [ { // 优惠后价格 variant_price4wscode: 159.99, }, ], }, ], }, ], } // 组件会自动显示优惠后价格和划线的原价 return <Specs data={data} buildData={buildData} /> }

示例 7: HTML 富文本规格值

import { Specs } from '@anker-in/headless-ui/biz' export default function RichTextExample() { const data = { data: { LeftMenu: { data: [ { title: 'Features', isProduct: true, subTitle: ['Fast Charging', 'Safety', 'Compatibility'], }, ], }, RightMenu: { menus: [ { handle: 'power-bank', sku: 'A1340011', subTitle: { // 支持 HTML 标签 'Fast Charging': '<p><b>USB-C PD 3.1</b><br/>Up to 250W</p>', Safety: '<p>MultiProtect™ System<br/><small>Surge, temperature, and short-circuit protection</small></p>', Compatibility: 'Compatible with all USB-C devices', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011', buttonText: 'Buy Now', }, }, } const buildData = { products: [ /* 产品数据 */ ], } return <Specs data={data} buildData={buildData} /> }

示例 8: 带产品选择回调

import { Specs } from '@anker-in/headless-ui/biz' import { useState } from 'react' export default function WithCallbackExample() { const [selectedProduct, setSelectedProduct] = useState<any>(null) const handleChange = (product: any, index: number) => { console.log('Product selected:', product.name, 'at index', index) setSelectedProduct(product) // 执行其他操作,如更新 URL、发送事件等 } const data = { data: { LeftMenu: { data: [ { title: 'Specifications', isProduct: true, subTitle: ['Capacity', 'Power'], }, ], }, RightMenu: { menus: [ { handle: 'power-bank-a', sku: 'A1340011', subTitle: { Capacity: '27,650mAh', Power: '250W', }, }, { handle: 'power-bank-b', sku: 'A2524014', subTitle: { Capacity: '20,000mAh', Power: '140W', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011,A2524014', buttonText: 'Buy Now', }, }, } const buildData = { products: [ /* 产品数据 */ ], } return ( <div> <Specs data={data} buildData={buildData} onChange={handleChange} /> {selectedProduct &amp;&amp; ( <p>当前选中产品: {selectedProduct.name}</p> )} </div> ) }

示例 9: 自定义样式

import { Specs } from '@anker-in/headless-ui/biz' export default function CustomStyleExample() { const data = { data: { LeftMenu: { data: [ { title: 'Specifications', isProduct: true, subTitle: ['Capacity', 'Power'], }, ], }, RightMenu: { menus: [ { handle: 'power-bank', sku: 'A1340011', subTitle: { Capacity: '27,650mAh', Power: '250W', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011', buttonText: 'Buy Now', }, // 自定义 CSS style: ` .specs-wrapper { background: #fff !important; border-radius: 8px; } .specs-sku-node-text { border-radius: 4px !important; } `, }, } const buildData = { products: [ /* 产品数据 */ ], } return <Specs data={data} buildData={buildData} className="my-custom-specs" /> }

示例 10: 多分类规格对比

import { Specs } from '@anker-in/headless-ui/biz' import scenarios from './scenarios.json' export default function MultiCategoryExample() { const data = { data: { LeftMenu: { data: [ // 第一个分类 { title: 'Power Specifications', isProduct: true, subTitle: ['Battery Capacity', 'Charging Power', 'Fast Charging'], }, // 第二个分类 { title: 'Physical Specifications', isProduct: false, subTitle: ['Weight', 'Dimensions', 'Material'], }, // 第三个分类 { title: 'Other Information', isProduct: false, subTitle: ['Warranty', 'Package Contents', 'Certifications'], }, ], }, RightMenu: { menus: [ { handle: 'power-bank', sku: 'A1340011', subTitle: { // 第一个分类的规格值 'Battery Capacity': '27,650mAh', 'Charging Power': '250W', 'Fast Charging': 'USB-C PD 3.1', // 第二个分类的规格值 Weight: '548g', Dimensions: '159 × 54 × 49.5mm', Material: 'Aluminum Alloy', // 第三个分类的规格值 Warranty: '24 Months', 'Package Contents': 'Power Bank, USB-C Cable, Manual', Certifications: 'CE, FCC, RoHS', }, }, ], }, DefaultSelectMenu: { sku: 'A1340011', buttonText: 'Buy Now', }, }, } const buildData = { products: [ /* 产品数据 */ ], } return <Specs data={data} buildData={buildData} /> }

响应式行为

列数限制

屏幕尺寸最大列数说明
移动端 (< 768px)2 列自动限制最多显示 2 个产品
平板/桌面 (≥ 768px)3 列根据 DefaultSelectMenu.sku 决定列数

重要: 移动端会自动截取前 2 个 SKU,即使 DefaultSelectMenu.sku 配置了 3 个产品。

实现原理:

// 移动端检测 const isMobile = typeof window !== 'undefined' &amp;&amp; window.innerWidth < 768 // SKU 列表处理 const skuList = data.data.DefaultSelectMenu.sku.split(',') const displaySkus = isMobile ? skuList.slice(0, 2) : skuList // 列数计算 const isShowMax = displaySkus.length === 3 &amp;&amp; !isMobile

断点样式

// 规格表格容器 <div className=" grid grid-cols-3 // 默认 3 列布局 l-tablet:flex // 平板及以上改为 flex 布局 l-tablet:flex-col " /> // SKU 卡片网格 <div className={cn( 'grid', `grid-cols-${isShowMax ? 3 : 2}`, // 动态列数 'l-tablet:w-full', 'l-tablet:gap-4', 'laptop:gap-8', 'desktop:gap-12', 'lg-desktop:gap-16' )} /> // 下拉菜单 <div className="relative w-full l-tablet:w-auto" />

规格表格响应式

断点间距布局
移动端gap-4垂直堆叠
平板gap-4水平对比
笔记本gap-8水平对比
桌面gap-12水平对比
超大屏gap-16水平对比

核心算法解析

1. SKU 选择与匹配算法

组件通过 SKU 匹配产品和变体数据,支持自动限制移动端列数。

/** * SKU 选择算法 * 1. 解析逗号分隔的 SKU 字符串 * 2. 移动端限制为前 2 个 * 3. 匹配 buildData 中的产品和变体 */ const skuList = data.data.DefaultSelectMenu.sku.split(',') const isMobile = typeof window !== 'undefined' &amp;&amp; window.innerWidth < 768 const displaySkus = isMobile ? skuList.slice(0, 2) : skuList // 匹配产品和变体 const selectedProducts = displaySkus.map((sku) => { for (const product of buildData.products) { const variant = product.variants.find((v) => v.sku === sku) if (variant) { return { product, variant, sku, } } } return null }) // 初始化 active 状态 const [activeProducts, setActiveProducts] = useState( selectedProducts.map((item, index) => ({ product: item?.product, variant: item?.variant, index, })) )

算法复杂度: O(n × m),n 为 SKU 数量,m 为产品数量

2. 智能图片识别算法

自动识别规格值中的图片 URL,支持 8 种图片格式。

/** * 图片 URL 识别算法 * 检测字符串是否为图片 URL */ const IMAGE_EXTENSIONS = [ '.jpeg', '.jpg', '.gif', '.png', '.webp', '.bmp', '.svg', '.tiff', ] function isImageUrl(value: string): boolean { if (typeof value !== 'string') return false // 转为小写进行比较 const lowerValue = value.toLowerCase() // 检查是否包含任何图片扩展名 return IMAGE_EXTENSIONS.some((ext) => lowerValue.includes(ext)) } // 使用示例 const specValue = 'https://example.com/color-red.png' if (isImageUrl(specValue)) { // 渲染图片 return <img src={specValue} alt="Spec Image" /> } else if (typeof specValue === 'object' &amp;&amp; specValue.imgUrl) { // 渲染图文混合 return ( <div> <div dangerouslySetInnerHTML={{ __html: specValue.text }} /> <img src={specValue.imgUrl} alt="Spec Image" /> </div> ) } else { // 渲染纯文本 return <div>{specValue}</div> }

支持的图片格式: JPEG, JPG, GIF, PNG, WebP, BMP, SVG, TIFF

3. 价格格式化算法

支持多货币、多地区价格展示,自动处理优惠券价格。

/** * 价格格式化算法 * 支持优惠券价格显示 */ function formatPrice(variant: Variant): { displayPrice: string originalPrice?: string hasCoupon: boolean } { const { price, coupons, currencyCode } = variant // 检查是否有优惠券 const couponPrice = coupons?.[0]?.variant_price4wscode const hasCoupon = couponPrice != null &amp;&amp; couponPrice < price // 格式化价格 const formatCurrency = (amount: number) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currencyCode || 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(amount) } if (hasCoupon) { return { displayPrice: formatCurrency(couponPrice!), originalPrice: formatCurrency(price), hasCoupon: true, } } return { displayPrice: formatCurrency(price), hasCoupon: false, } } // 渲染价格 const { displayPrice, originalPrice, hasCoupon } = formatPrice(variant) return ( <div> <span className="text-lg font-bold">{displayPrice}</span> {hasCoupon &amp;&amp; ( <span className="ml-2 text-sm line-through text-gray-500"> {originalPrice} </span> )} </div> )

支持的货币: USD, EUR, GBP, JPY, CNY 等所有 ISO 4217 货币代码

4. 下拉菜单自动关闭算法

点击下拉菜单外部自动关闭,使用 useRef 优化性能。

/** * 点击外部关闭算法 * 使用 useRef 和 useEffect 监听点击事件 */ function useClickOutside( ref: React.RefObject<HTMLElement>, handler: () => void ) { useEffect(() => { const listener = (event: MouseEvent | TouchEvent) => { // 如果点击的是 ref 元素内部,不执行 handler if (!ref.current || ref.current.contains(event.target as Node)) { return } // 点击外部,执行 handler handler() } document.addEventListener('mousedown', listener) document.addEventListener('touchstart', listener) return () => { document.removeEventListener('mousedown', listener) document.removeEventListener('touchstart', listener) } }, [ref, handler]) } // 使用示例 const dropdownRef = useRef<HTMLDivElement>(null) const [isOpen, setIsOpen] = useState(false) useClickOutside(dropdownRef, () => { setIsOpen(false) }) return ( <div ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)}>Toggle</button> {isOpen &amp;&amp; <div>Dropdown Content</div>} </div> )

5. 动态按钮文本算法

支持统一按钮文本或按索引自定义,提供灵活配置。

/** * 按钮文本获取算法 * 支持字符串或对象格式 */ function getButtonText( buttonText: string | { [index: string]: string }, index: number ): string { // 统一文本 if (typeof buttonText === 'string') { return buttonText } // 按索引自定义 if (typeof buttonText === 'object' &amp;&amp; buttonText[index] != null) { return buttonText[index] } // 默认文本 return 'Buy Now' } // 使用示例 const buttonText = { '0': 'Buy Pro Model', '1': 'Buy Standard Model', } // 渲染按钮 activeProducts.map((item, index) => ( <button key={index}>{getButtonText(buttonText, index)}</button> ))

6. 规格值渲染算法

统一处理三种规格值格式(文本、图片、图文混合)。

/** * 规格值渲染算法 * 自动识别并渲染不同格式 */ function renderSpecValue(value: SpecValue): React.ReactNode { // 1. 图文混合对象 if (typeof value === 'object' &amp;&amp; value.imgUrl) { return ( <div className="flex flex-col gap-2"> {value.text &amp;&amp; ( <div className="text-sm" dangerouslySetInnerHTML={{ __html: value.text }} /> )} <img src={value.imgUrl} alt="Spec" className="max-w-full h-auto rounded" /> </div> ) } // 2. 纯图片 URL if (typeof value === 'string' &amp;&amp; isImageUrl(value)) { return ( <img src={value} alt="Spec" className="max-w-full h-auto rounded" /> ) } // 3. 纯文本(可能包含 HTML) if (typeof value === 'string') { // 检查是否包含 HTML 标签 if (value.includes('<') &amp;&amp; value.includes('>')) { return ( <div className="text-sm" dangerouslySetInnerHTML={{ __html: value }} /> ) } return <div className="text-sm">{value}</div> } return null } // 使用示例 <div>{renderSpecValue(subTitle['Battery Capacity'])}</div>

7. 产品切换回调算法

处理下拉菜单产品选择,更新状态并触发回调。

/** * 产品切换算法 * 更新 active 状态并触发回调 */ function handleProductChange( newProduct: Product, newVariant: Variant, columnIndex: number ) { // 1. 更新 active 状态 const newActiveProducts = [...activeProducts] newActiveProducts[columnIndex] = { product: newProduct, variant: newVariant, index: columnIndex, } setActiveProducts(newActiveProducts) // 2. 触发 onChange 回调 if (onChange) { onChange(newProduct, columnIndex) } // 3. 关闭下拉菜单 setOpenDropdown(null) // 4. 可选:发送分析事件 if (typeof window !== 'undefined' &amp;&amp; window.gtag) { window.gtag('event', 'product_compare_switch', { product_name: newProduct.name, product_sku: newVariant.sku, column_index: columnIndex, }) } } // 渲染下拉选项 buildData.products.map((product) => product.variants.map((variant) => ( <button key={variant.sku} onClick={() => handleProductChange(product, variant, columnIndex)} > {product.name} </button> )) )

设计规范

对比产品数量

  • 最少: 1 个产品(不显示下拉菜单)
  • 推荐: 2-3 个产品
  • 最多: 3 个产品(移动端自动限制为 2 个)

规格项命名

  • 简短明了,建议不超过 20 个字符
  • 使用名词或名词短语
  • 统一使用英文或中文

良好示例:

  • ✅ Battery Capacity, Charging Power, Ports
  • ✅ 电池容量、充电功率、接口数量

避免使用:

  • ❌ How much battery does it have?
  • ❌ What is the charging speed?

规格值格式

  • 文本: 优先使用单位符号(如 27,650mAh 而非 27650 mAh)
  • 图片: 确保图片尺寸一致,推荐 360×360px
  • HTML: 使用 &lt;b&gt; 标签强调关键信息,避免复杂样式

产品卡片设计

元素桌面端移动端
产品图片200×200px120×120px
产品名称16px, 2 行截断14px, 2 行截断
价格24px, 粗体20px, 粗体
按钮高度 48px高度 44px

无障碍性

  • ✅ 使用语义化 HTML 标签(&lt;h3&gt; 作为规格标题)
  • ✅ 下拉菜单支持键盘导航(可增强)
  • ✅ 图片必须提供 alt 属性
  • ⚠️ 建议添加 aria-label 到下拉触发器
  • ⚠️ 建议添加 role="grid" 到对比表格

可增强的无障碍性功能:

// 下拉触发器 <button aria-label={`Select product for column ${index + 1}`} aria-expanded={isOpen} aria-haspopup="listbox" > {product.name} </button> // 对比表格 <div role="grid" aria-label="Product comparison table"> <div role="row"> <div role="columnheader">Specifications</div> <div role="columnheader">{product1.name}</div> <div role="columnheader">{product2.name}</div> </div> <div role="row"> <div role="rowheader">Battery Capacity</div> <div role="gridcell">27,650mAh</div> <div role="gridcell">20,000mAh</div> </div> </div>

性能优化

  • ✅ 使用 React.forwardRef() 支持 ref 转发
  • ✅ 使用 useMemo() 缓存菜单列表计算
  • ✅ 使用 useMediaQuery 优化响应式检测
  • ✅ 下拉菜单使用 useRef 优化点击外部检测
  • ✅ 条件渲染减少 DOM 节点
  • ✅ 图片懒加载(可增强)

性能优化示例:

// 1. 缓存菜单列表计算 const displaySkus = useMemo(() => { const skuList = data.data.DefaultSelectMenu.sku.split(',') return isMobile ? skuList.slice(0, 2) : skuList }, [data.data.DefaultSelectMenu.sku, isMobile]) // 2. 缓存产品匹配结果 const selectedProducts = useMemo(() => { return displaySkus.map((sku) => { for (const product of buildData.products) { const variant = product.variants.find((v) => v.sku === sku) if (variant) { return { product, variant, sku } } } return null }) }, [displaySkus, buildData.products]) // 3. 条件渲染减少 DOM 节点 { isProduct &amp;&amp; ( <div className="product-card"> {/* 产品卡片内容 */} </div> ) } // 4. 图片懒加载 <img src={imageUrl} alt={altText} loading="lazy" />

常见问题 FAQ

1. 如何隐藏产品卡片,只显示规格表格?

设置 isProduct: false 并清空 buttonText:

LeftMenu: { data: [{ title: 'Specs', isProduct: false, // 不显示产品卡片 subTitle: [...] }] }, DefaultSelectMenu: { sku: '...', buttonText: '' // 不显示按钮 }

2. 为什么移动端只显示 2 个产品?

这是性能和用户体验优化。移动端屏幕窄,3 列会导致内容过于拥挤。组件会自动检测 isMobile 并限制最多显示前 2 个 SKU。

3. 如何为不同产品设置不同的按钮文本?

使用对象格式的 buttonText:

DefaultSelectMenu: { sku: 'A1340011,A2524014', buttonText: { '0': 'Buy Pro Model', // 索引对应 sku 的顺序 '1': 'Buy Basic Model' } }

4. 规格值支持哪些图片格式?

支持常见图片格式: .jpeg, .jpg, .gif, .png, .webp, .bmp, .svg, .tiff。组件会自动识别 URL 路径中的扩展名。

5. 如何显示优惠券价格?

确保 buildData.products[].variants[].coupons 包含优惠券数据:

variants: [{ sku: 'A1340011', price: 179.99, coupons: [{ variant_price4wscode: 159.99 // 优惠后价格 }] }]

组件会自动显示优惠后的价格和划线的原价。

6. 下拉菜单如何关闭?

  • 点击下拉菜单外的任何区域
  • 选择一个产品后自动关闭
  • 不支持 ESC 键关闭(可增强)

7. 如何自定义规格表格样式?

通过 data.style 字段注入自定义 CSS:

data: { style: ` .specs-wrapper { background: #fff !important; } .specs-sku-node-text { border-radius: 0 !important; } ` }

8. 规格值可以包含 HTML 吗?

可以。规格值支持 HTML 标签,组件会使用 dangerouslySetInnerHTML 渲染。但请确保 HTML 内容是安全的,避免 XSS 攻击。

subTitle: { 'Fast Charging': '<p><b>USB-C PD 3.1</b><br/>Up to 250W</p>' }

9. 如何处理产品选择事件?

使用 onChange 回调:

const handleChange = (product: any, index: number) => { console.log('Product selected:', product.name, 'at index', index) // 执行其他操作 } <Specs data={data} buildData={buildData} onChange={handleChange} />

10. 如何处理 SKU 不存在的情况?

组件会自动跳过不存在的 SKU。如果所有 SKU 都不存在,组件不会渲染任何产品卡片。建议在传入数据前验证 SKU 的有效性:

// 验证 SKU 是否存在 const validateSku = (sku: string, buildData: BuildData): boolean => { return buildData.products.some((product) => product.variants.some((variant) => variant.sku === sku) ) } const skuList = data.data.DefaultSelectMenu.sku.split(',') const validSkus = skuList.filter((sku) => validateSku(sku, buildData)) if (validSkus.length === 0) { console.error('No valid SKUs found') }

相关资源

Last updated on