亚马逊风格切片器

本 Demo 展示了如何使用 SpreadJS 的 GeneralSlicerData 类创建自定义样式的切片器。通过实现自定义的 AmazonSlicer 类,演示了构建亚马逊风格的商品筛选界面,支持按品牌、内存、网络和评价等维度筛选手机产品,并实时展示筛选结果。

概述 本 Demo 展示了如何基于 SpreadJS 的 GeneralSlicerData 类创建自定义切片器。通过自定义 AmazonSlicer 类,实现了亚马逊风格的商品筛选界面,包含品牌、内存、网络和用户评价四个筛选维度,筛选结果实时更新显示。 实现思路 使用 GeneralSlicerData 创建切片器数据源,传入商品数据和列名 定义 AmazonSlicer 自定义类,封装切片器的 UI 构建和交互逻辑 在 setData 方法中调用 attachListener 将切片器附加到数据源 根据 getExclusiveData 获取唯一值列表,构建筛选项 UI 为筛选项添加点击事件,调用 doFilter 执行筛选或 doUnfilter 清除筛选 实现 onFiltered 回调方法,根据筛选结果更新 UI 状态(高亮选中项、显示被筛选项) 使用 getFilteredRowIndexes 获取筛选后的行索引,刷新商品列表显示 代码解析 创建切片器数据源 使用 GeneralSlicerData 构造函数创建切片器数据源。datas 是二维数组格式的商品数据,dataNames 是列名数组。GeneralSlicerData 会自动处理数据并提供筛选相关的方法。 自定义切片器类 setData 方法是切片器的核心初始化方法: getData(columnName) 获取该列的所有数据 getExclusiveData(columnName) 获取该列的唯一值列表(去重) attachListener(this) 将当前切片器注册为监听器,数据筛选时会自动触发 onFiltered 回调 构建筛选 UI 遍历唯一值列表,为每个值创建一个筛选项。getRowIndexes(columnName, exclusiveRowIndex) 方法返回该唯一值对应的所有数据行索引,用于统计数量并显示在筛选项旁边。 执行筛选操作 点击筛选项时: 遍历所有筛选项,找出被选中的项 将选中项的文本值转换为唯一数据索引(exclusiveRowIndex) 调用 doFilter(columnName, {exclusiveRowIndexes: indexes}) 执行筛选 如果没有选中项,调用 doUnfilter(columnName) 清除筛选 更新筛选状态 当数据被筛选后,onFiltered 回调自动触发: getFilteredIndexes(columnName) 返回当前列被选中的筛选项索引 getFilteredOutIndexes(columnName, FilteredOutDataType.byCurrentColumn) 返回被当前列筛选掉的项索引 通过添加 CSS 类来高亮显示不同状态的筛选项 刷新商品列表 getFilteredRowIndexes() 返回所有筛选后的行索引。根据这些索引从原始数据中提取对应的商品信息,重新渲染商品列表。 运行效果 页面左侧显示四个筛选器:Brand(品牌)、Internal Memory(内存)、Network(网络)、Avg. Customer Review(平均评价) 每个筛选器显示该列的唯一值及对应数量,可以通过复选框多选筛选 评价筛选器使用星级图片显示评分,点击时筛选该评分及以上的所有商品 点击筛选器标题可以清除该列的所有筛选条件 筛选结果实时显示在右侧的商品列表区域,包含商品图片、描述、品牌和价格 多个筛选器之间联动,显示被其他列筛选掉的项(灰色显示) API 参考 GeneralSlicerData 构造函数 data:二维数组格式的数据 columnNames:字符串数组,指定每列的名称 GeneralSlicerData 核心方法 getData(columnName):获取指定列的所有数据 getExclusiveData(columnName):获取指定列的唯一值列表(去重) getRowIndexes(columnName, exclusiveRowIndex):根据唯一值索引获取对应的原始数据行索引 doFilter(columnName, conditional):执行筛选,conditional 参数为 {exclusiveRowIndexes: [索引数组]} doUnfilter(columnName):清除指定列的筛选条件 attachListener(listener):注册监听器,筛选时会触发监听器的 onFiltered 方法 getFilteredIndexes(columnName):获取当前列被选中的筛选项索引 getFilteredOutIndexes(columnName, filteredOutDataType):获取被筛选掉的项索引,filteredOutDataType 可以是 byCurrentColumn(被当前列筛掉)或 byOtherColumns(被其他列筛掉) getFilteredRowIndexes():获取所有筛选后的行索引
var dataNames = ["Image", "Description", "Brand", "Price", "Internal Memory", "Network", "Avg. Customer Review", "ShipToChina"]; //"size", var datas = [["NOKIA521.jpg", "Nokia Lumia 521 T-Mobile Cell Phone - White", "Nokia", "49.99", "16", "3G", "4", true], ["LG450.jpg", "LG 450 Black - No Contract (T-Mobile)", "LG", "29.99", "4", "3G", "4", true], ["LGC365.jpg", "LG Xpression C395 Unlocked GSM Slider Cell Phone with Touchscreen + Full QWERTY Keyboard - Red", "LG", "49.99", "32", "3G", "3.5", false], ["BLUPink.jpg", "BLU Dash JR W D141w Unlocked GSM Dual-SIM Android Cell Phone - Pink", "BLU", "18.99", "4", "3G", "4", true], ["BLUWhite.jpg", "BLU Dash JR W D141w Unlocked GSM Dual-SIM Android Cell Phone - White", "BLU", "129.99", "32", "4G", "3.5", false], ["LGA275.jpg", "LG A275 Black Unlocked GSM Dual SIM QuadBand Cell Phone - International Version - No Warranty", "LG", "79.89", "4", "3G", "3.5", false], ["BLUBlue.jpg", "BLU Dash JR W D141w Unlocked GSM Dual-SIM Android Cell Phone - Blue", "BLU", "59.99", "4", "GSM", "4", false], ["LGVN250.jpg", "LG Cosmos VN250 Verizon Phone (POST PAID)", "LG", "449.99", "64", "4G", "3.5", false], ["LGOptimus.jpg", "LG Optimus Dynamic Android Prepaid Phone with Triple Minutes (Tracfone)", "LG", "29.99", "16", "GSM", "4", true], ["LG305c.jpg", "Tracfone LG 305C with Triple Minutes for Life (Tracfone)", "LG", "99.99", "16", "3G", "4", false], ["SamsungC3520.jpg", "Samsung GT-C3520I Unlocked Quad-Band GSM Phone with 1.3 MP Camera, MP3 Player...", "Samsung", "64.99", "8", "3G", "3.5", false], ["LGVN251.jpg", "LG VN251 VN 251 COSMOS 2 Verizon Wireless Slider Keyboard Bluetooth Cell Phone", "LG", "58.69", "16", "4G", "4", false], ["ZTEUSAZ222.jpg", "AT&T Z222 Go Phone (AT&T)", "ZTE USA", "24.85", "4", "4G", "4", false], ["BLUQ170T.jpg", "BLU Q170T Samba TV Unlocked Dual SIM Quad-Band GSM Phone (Black/Red)", "BLU", "26.08", "32", "3G", "3.5", true], ["Apple4SWhite.jpg", "Apple iPhone 4S GSM Unlocked 16GB Smartphone - White", "Apple", "199.99", "16", "GSM", "4", false], ["LG840G.jpg", "LG 840G Prepaid Phone With Triple Minutes (Tracfone)", "LG", "48.95", "32", "4G", "3.5", false], ["SamsungA157V.jpg", "Samsung a157V (AT&T)", "Samsung", "14.99", "8", "3G", "3.5", false], ["LGVN150.jpg", "LG Revere VN150 Bluetooth CDMA Camera Flip Cell Phone Verizon or PagePlus", "LG", "72.50", "2", "GSM", "3.5", false], ["NOKIA106.jpg", "Nokia 106 Unlocked GSM Dual-Band Cell Phone w/ SMS and FM Radio - Black", "Nokia", "17.99", "2", "GSM", "4", false], ["ZTEZ222.jpg", "Unlocked ZTE Z222 Flip Phone With Camera For ATT, T-Mobile and Other Supported GSM Networks...", "ZTE", "27.79", "16", "4G", "3.5", false], ["Apple4SUnlocked.jpg", "Apple iPhone 4S 16GB 3G WiFi Black Smartphone Unlocked", "Apple", "182.99", "16", "3G", "4", false], ["BLUQ170TBlue.jpg", "BLU Q170T Samba TV Unlocked Dual SIM Quad-Band GSM Phone (Black/Blue)", "BLU", "22.99", "2", "GSM", "4", false], ["SamsungC3520Gray.jpg", "Samsung GT-C3520BLK Unlocked GSM Cell Phone,Charcoal Gray", "Samsung", "99.99", "8", "3G", "3.5", false], ["LG306G.jpg", "LG 306G 3G Cell Phone | TracFone", "TracFone", "4.99", "2", "GSM", "3.5", false]]; var path = (location.origin + location.pathname); var srcBase = "$DEMOROOT$/spread/source/"; // if (path.indexOf("#") === -1) { // // srcBase = location.origin + location.pathname; // if (path.indexOf("demos/") > -1) { // srcBase = path.substr(0, path.indexOf("demos/")); // } // } window.onload = function () { var dataSource = new GC.Spread.Slicers.GeneralSlicerData(datas, dataNames); var brandSlicer = new AmazonSlicer(document.getElementById('slicer_Brand')); brandSlicer.setData(dataSource, "Brand"); var memorySlicer = new AmazonSlicer(document.getElementById('slicer_Memory')); memorySlicer.setData(dataSource, "Internal Memory"); var netWorkSlicer = new AmazonSlicer(document.getElementById('slicer_Network')); netWorkSlicer.setData(dataSource, "Network"); var customerReviewSlicer = new AmazonSlicer(document.getElementById('slicer_CustomerReview')); customerReviewSlicer.setData(dataSource, "Avg. Customer Review"); }; function AmazonSlicer(container) { this.container = container; } AmazonSlicer.prototype.constructor = AmazonSlicer; AmazonSlicer.prototype.setData = function (slicerData, columnName) { this.slicerData = slicerData; this.columnName = columnName; this.data = slicerData.getData(columnName); this.exclusiveDatas = slicerData.getExclusiveData(columnName); this.slicerData.attachListener(this); this.onDataLoaded(); } AmazonSlicer.prototype.onDataLoaded = function () { let slicer = this; var header = '<div class="slicerHeader">' + '<div class="slicerHeaderBorder"></div>' + '<span style="font-size:medium">' + this.columnName + '</span>' + '<div class="slicerHeaderBorder"></div>' + '</div>'; if (slicer.container.className.indexOf('slicer') < 0) { slicer.container.className = slicer.container.className + ' slicer'; } var datas = slicer.exclusiveDatas; let slicerContent = header; if (slicer.columnName === "Avg. Customer Review") { for (var i = 0; i < datas.length; i++) { var count = 0; for (var j = 0; j < datas.length; j++) { if (parseFloat(datas[j]) >= parseFloat(datas[i])) { count += slicer.slicerData.getRowIndexes(slicer.columnName, slicer.slicerData.getExclusiveData(slicer.columnName).indexOf(datas[j])).length; } } var imageSrcPrefix = srcBase ? srcBase : ''; var image = '<img src="' + imageSrcPrefix + 'images\/' + datas[i] + '.png" alt="' + datas[i] + '"style="vertical-align:middle"></img><span>& Up</span>' slicerContent += '<div>' + image + '<span style="color:#A29999">(' + count + ')</span></div>'; } } else { for (var i = 0; i < datas.length; i++) { var count = slicer.slicerData.getRowIndexes(slicer.columnName, slicer.slicerData.getExclusiveData(slicer.columnName).indexOf(datas[i])).length; // todo label include checkbox slicerContent += '<label style="display: block">' + '<input type="checkbox"/><span>' + datas[i] + '</span><span style="color:#A29999">(' + count + ')</span>' + '</label>'; } } slicer.container.innerHTML = slicerContent; var headerEle = slicer.container.firstChild; headerEle.onmousedown = function (e) { slicer.slicerData.doUnfilter(slicer.columnName); if (slicer.columnName !== "Avg. Customer Review") { var childNodes = slicer.container.childNodes; for (var i = 1, length = childNodes.length; i < length; i++) { childNodes[i].childNodes[0].checked = false; } } } var items = []; var children = slicer.container.childNodes children.forEach((item, index) => { if (index === 0) { return; } items.push(item); item.className = item.className + ' slicerItem'; item.onmouseenter = function (e) { e.target.className += ' hover'; } item.onmouseleave = function (e) { e.target.className = e.target.className.replace('hover', '').trim(); } if (slicer.columnName === "Avg. Customer Review") { item.onclick = function (e) { var exclusiveData = slicer.slicerData.getExclusiveData(slicer.columnName), childNodes = this.parentNode.childNodes, indexes = []; var currentAlt = item.childNodes[0].alt; for (var i = 1, length = childNodes.length; i < length; i++) { var tempAlt = childNodes[i].childNodes[0].alt; if (parseFloat(tempAlt) >= parseFloat(currentAlt)) { indexes.push(exclusiveData.indexOf(tempAlt)); } } if (indexes.length === 0) { slicer.slicerData.doUnfilter(slicer.columnName); } else { slicer.slicerData.doFilter(slicer.columnName, {exclusiveRowIndexes: indexes}) } } } else { // label checkbox item.onclick = function (e) { var exclusiveData = slicer.slicerData.getExclusiveData(slicer.columnName), childNodes = item.parentNode.childNodes, indexes = []; for (var i = 1, length = childNodes.length; i < length; i++) { if (childNodes[i].childNodes[0].checked) { indexes.push(exclusiveData.indexOf(childNodes[i].childNodes[1].innerText)) } } if (indexes.length === 0) { slicer.slicerData.doUnfilter(slicer.columnName); } else { slicer.slicerData.doFilter(slicer.columnName, {exclusiveRowIndexes: indexes}) } } } }); slicer.items = items; slicer.onFiltered(null); }; AmazonSlicer.prototype.resetClass = function () { var items = this.items; var classes = ["filtered", "partial", "filteredOutBySelf", "filteredOutByOther"]; for (var i = 0; i < items.length; i++) { for (var k = 0; k < classes.length; k++) { items[i].className = items[i].className.replace(classes[k], '').trim(); } } }; AmazonSlicer.prototype.onFiltered = function () { this.resetClass(); var items = this.items; var filteredItems = this.slicerData.getFilteredIndexes(this.columnName); for (var i = 0; i < filteredItems.length; i++) { if (items[filteredItems[i]].className.indexOf('filtered') < 0) { items[filteredItems[i]].className = items[filteredItems[i]].className + ' filtered'; } } var filteredOutItems = this.slicerData.getFilteredOutIndexes(this.columnName, GC.Spread.Slicers.FilteredOutDataType.byCurrentColumn); for (var i = 0; i < filteredOutItems.length; i++) { if (items[filteredOutItems[i]].className.indexOf('filteredOutBySelf').indexOf < 0) { items[filteredOutItems[i]].className = items[filteredOutItems[i]].className + ' filteredOutBySelf' } } var filteredOutByOtherItems = this.slicerData.getFilteredOutIndexes(this.columnName, GC.Spread.Slicers.FilteredOutDataType.byOtherColumns); for (var i = 0; i < filteredOutByOtherItems.length; i++) { if (items[filteredOutByOtherItems[i]].className.indexOf('filteredOutByOther') < 0) { items[filteredOutByOtherItems[i]].className = items[filteredOutByOtherItems[i]].className + 'filteredOutByOther' } } this.refreshList(); }; AmazonSlicer.prototype.refreshList = function () { var list = document.getElementById('ss'); var indexes = this.slicerData.getFilteredRowIndexes(); let listItemsHtml = ''; for (var i = 0, rowCount = indexes.length; i < rowCount; i++) { var imageSrcPrefix = srcBase ? srcBase : ''; var image1 = '<div class="p_image"><img src="' + imageSrcPrefix + 'images\/' + datas[indexes[i]][0] + '" style="width:160px;height:160px"></img></div>'; var image2 = '<div class="p_vote"><img src="' + imageSrcPrefix + 'images\/' + datas[indexes[i]][6] + '.png"/></div>'; listItemsHtml += '<div style="width:200px;margin-left:20px;margin-top:10px;display:inline-block" >' + image1 + '<div class="p_description" style="height:86px;font-size:medium;color:#0066C0">' + datas[indexes[i]][1] + '</div>' + '<div class="p_company">by ' + datas[indexes[i]][2] + '</div>' + '<div class="p_price" style="font-weight:bold;color:#B52704">$' + datas[indexes[i]][3] + '</div>' + image2 + '</div>' } list.innerHTML = listItemsHtml; }
<!doctype html> <html style="height:100%;font-size:14px;"> <head> <meta name="spreadjs culture" content="zh-cn" /> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css"> <script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-resources-zh/dist/gc.spread.sheets.resources.zh.min.js" type="text/javascript"></script> <script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <div class="sample-tutorial"> <div id="ss" class="sample-spreadsheets"></div> <!--<div class="sample-options layout_slicer">--> <div class="options-container"> <p class="desc">选择不同选项以筛选商店中的结果。</p> <div id="slicer_Brand" class="slicer"></div> <div id="slicer_Memory" class="slicer"></div> <div id="slicer_Network" class="slicer"></div> <div id="slicer_CustomerReview" class="slicer"></div> </div> <!--</div>--> </div> </body> </html>
.sample-tutorial { position: relative; height: 100%; overflow: hidden; } .sample-spreadsheets { width: calc(100% - 280px); height: 100%; overflow: auto; float: left; } .options-container { float: right; width: 280px; padding: 12px; height: 100%; box-sizing: border-box; background: #fbfbfb; overflow: auto; } .option-row { font-size: 14px; padding: 5px; margin-top: 10px; } .slicer { margin-bottom: 10px; } .slicerHeader { position: relative; padding-top: 1px; font-weight: 600; } .hover { color: #E47911; cursor: default; } .desc { padding: 2px 10px; margin-top: 0; background-color: #F4F8EB; } body { position: absolute; top: 0; bottom: 0; left: 0; right: 0; }