“目录”
-
text: “前置要求”
-
text: “Bricks 结构”
-
text: “PHP”
-
text: “JavaScript”
-
text: “结论”
— 本教程提供 PHP 与 JS 代码,让你在 Bricks 构建器中实现:根据某个分类法(taxonomy)的术语(term)来过滤地图上的标记。 ## 目录 - 前置要求
-
结论 ## 前置要求 - 本地图系列 PART 1、PART 2 和 PART 3 的全部代码已经跑通。
-
建一个自定义分类法(本示例用
store_categories),并给每篇自定义文章分配一个术语(本示例用mobile和internet)。 ## Bricks 结构 既然要按术语过滤标记,第一步是创建一组按钮,每个按钮对应一个术语名: 实现方式:在搜索输入框代码元素后面创建一个按钮容器,容器里放一个静态按钮——「ALL」按钮(用于清除过滤)——再加一个查询循环 div,遍历所有术语并为每个术语输出一个按钮。 按钮容器要加filterbtn-wrapper类: 「ALL」按钮上添加两个类:.filterbtn和.filterbtn--active:.filterbtn类被所有按钮共用(包括术语按钮)。.filterbtn--active有两个作用: - 让「ALL」按钮在页面加载时默认处于激活状态 -
让我们能在构建器里按自己的意愿样式化激活态 再把链接类型改为外部链接(External URL),值填
#: 并添加一个data-filter属性,值为*: 接下来进入 Terms 查询循环,填写如下设置: 再处理最后一个新元素:查询循环里的按钮。按钮文本由term_name函数动态生成,同样加上filterbtn类,外部链接设为#: 并添加一个data-filter属性,值用term_name函数: 过滤按钮全部就绪后,还要给每篇自定义文章加上过滤条件。回到 Stores 查询循环元素(isotope wrapper 下面那个): 给它也加一个data-filter属性,值用一个自定义函数(稍后在 functions.php 里创建),输出每篇自定义文章用到的所有术语: 一切就绪,开始写代码! ## PHP 上一期教程的代码不需要改动,只需要加上我们刚才用到的自定义函数——把一篇自定义文章的所有术语输出为字符串列表。把下面代码复制到 functions.php:php function bl_get_terms_from_post() { $term_obj_list = get_the_terms( $post->ID, 'store_categories' ); $terms_string = join( ', ', wp_list_pluck( $term_obj_list, 'name' ) ); return $terms_string; }注意store_categories是我们分类法的名称,记得换成你自己的分类法名称。 ## JavaScript 先贴出需要整体替换的完整代码,然后再讲重点改动。最终代码如下:javascript window.addEventListener('load', () => { const canvas = document.querySelector('#map'); if (!document.body.classList.contains('bricks-is-frontend') || canvas === null) { return; } var mbAttr = 'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>'; var mbUrl = 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw'; var streets = L.tileLayer(mbUrl, { id: 'mapbox/streets-v11', tileSize: 512, zoomOffset: -1, attribution: mbAttr }); var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }); var satellite = L.tileLayer(mbUrl, { id: 'mapbox/satellite-v9', tileSize: 512, zoomOffset: -1, attribution: mbAttr }); var map = L.map('map', { center: [canvas.dataset.leafletLat, canvas.dataset.leafletLong], zoom: canvas.dataset.leafletZoom, maxZoom: canvas.dataset.leafletMaxZoom, layers: [osm, cities] }); var baseLayers = { 'OpenStreetMap': osm, 'Streets': streets, 'Satellite': satellite }; var layerControl = L.control.layers(baseLayers).addTo(map); // Jump to map on click const storeHeadersLink = document.querySelectorAll('.store-header-link'); if (storeHeadersLink.length > 0) { storeHeadersLink.forEach((link) => { link.addEventListener('click', (e) => { e.preventDefault(); let lat = e.target.dataset.leafletLat; let long = e.target.dataset.leafletLong; map.flyTo([lat, long], 17); }); }); }; // Filter search input with IsotopeJS // quick search regex var qsRegex; var filterSelector = "*"; // init Isotope var iso = new Isotope('.isotope-wrapper', { itemSelector: '.store-wrapper', layoutMode: 'fitRows', filter: function(itemElem) { var search = qsRegex ? itemElem.textContent.match(qsRegex) : true; var filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true; return search && filterRes; }, }); var mapRendering = () => { boundsList = []; var filteredItems = iso.filteredItems; if (filteredItems < 1) { return; } filteredItems.forEach(elm => { elmt = elm.element; let header = elmt.querySelector('.store-header') let storeId = header.dataset.store; let lat = header.dataset.leafletLat; let long = header.dataset.leafletLong; stores[storeId].addTo(cities); boundsList.push(new L.LatLng(lat, long)); }); map.fitBounds(boundsList, { padding: [50, 50] }); } // use value of search field to filter var quicksearch = document.querySelector('.quicksearch'); quicksearch.addEventListener('keyup', debounce(function() { qsRegex = new RegExp(quicksearch.value, 'gi'); // delete the current markers cities.clearLayers(); //filter the store list iso.arrange(); //add the new filtered markers to the map mapRendering(); }, 200)); // debounce so filtering doesn't happen every millisecond function debounce(fn, threshold) { var timeout; threshold = threshold || 200; return function debounced() { clearTimeout(timeout); var args = arguments; var _this = this; function delayed() { fn.apply(_this, args); } timeout = setTimeout(delayed, threshold); }; }; // Filter terms // bind filter button click var filtersElem = document.querySelectorAll(".filterbtn-wrapper .filterbtn"); if (filtersElem.length > 0) { filtersElem.forEach(elem => elem.addEventListener("click", function(event) { event.preventDefault(); var filterValue = event.target.getAttribute("data-filter"); filterSelector = filterValue; // delete the current markers cities.clearLayers(); //filter the store list iso.arrange(); //add the new filtered markers to the map mapRendering(); })); }; // change filterbtn--active class on buttons for (var i = 0, len = filtersElem.length; i < len; i++) { var buttonGroup = filtersElem[i]; radioButtonGroup(buttonGroup); } function radioButtonGroup(buttonGroup) { buttonGroup.addEventListener("click", function(event) { filtersElem.forEach(btn => btn.classList.remove("filterbtn--active")); event.target.classList.add("filterbtn--active"); }); } });第一个改动:给 Isotope 初始化增加第二种过滤方式: ```javascript // init Isotope var iso = new Isotope(‘.isotope-wrapper’, { itemSelector: ‘.store-wrapper’, layoutMode: ‘fitRows’, filter: function(itemElem) { var search = qsRegex ? itemElem.textContent.match(qsRegex) : true; var filterRes = filterSelector != ‘*’ ? itemElem.dataset.filter.includes(filterSelector) : true; return search && filterRes; }, });
注意这里返回的是**搜索过滤和按钮过滤的「与」结果**(AND,不是 OR)。也就是说,两种过滤方式可以同时生效。 第二个重要改动:把标记过滤相关的代码抽成一个独立函数 `mapRendering()`,这样无论是搜索框触发还是按钮触发的过滤,都调用它:javascript // Populate the filtered markers var mapRendering = () => { boundsList = []; var filteredItems = iso.filteredItems; if (filteredItems < 1){ return; } filteredItems.forEach(elm => { elmt = elm.element; let header = elmt.querySelector(‘.store-header’) let storeId = header.dataset.store; let lat = header.dataset.leafletLat; let long = header.dataset.leafletLong; stores[storeId].addTo(cities); boundsList.push(new L.LatLng(lat, long) ); } ); map.fitBounds(boundsList, {padding: [50, 50]}); }
我们给过滤按钮加了 `eventListener`:每次点击按钮,脚本取出按钮对应的术语,对列表和地图同时应用过滤:javascript
// bind filter button click var filtersElem = document.querySelectorAll(“.filterbtn-wrapper .filterbtn”); if (filtersElem.length > 0) { filtersElem.forEach ( elem => elem.addEventListener(“click”, function(event) { event.preventDefault(); var filterValue = event.target.getAttribute(“data-filter”); filterSelector = filterValue; // delete the current markers cities.clearLayers(); //filter the store list iso.arrange(); //add the new filtered markers to the map mapRendering(); })); };
代码最后一部分负责激活类 `.filterbtn--active` 的切换——点击某个过滤按钮时,把它加到该按钮上(仅用于样式):javascript // change filterbtn–active class on buttons for (var i = 0, len = filtersElem.length; i < len; i++) { var buttonGroup = filtersElem[i]; radioButtonGroup(buttonGroup); } function radioButtonGroup(buttonGroup) { buttonGroup.addEventListener(“click”, function(event) { filtersElem.forEach(btn => btn.classList.remove(“filterbtn–active”)); event.target.classList.add(“filterbtn–active”); }); }
>
- [Bricks 内容切换器:纯 CSS 方案(radio + :has 实现标签切换)](/blog/a-css-approach-to-content-switcher-in-bricks/)
- [Bricks 用 ACF Checkbox 字段渲染自定义 SVG 图标列表](/blog/acf-checkbox-svg-bullets/)
- [Bricks 按 ACF Gallery 图片数量做动态条件输出](/blog/acf-gallery-images-count-condition/)
## 结论 希望一切就位,你现在可以在前端测试新的术语过滤了: 本期到此为止!希望你喜欢这个教程,下期见!
## 常见误区
几个坑:一是 Marker 数据从 ACF 取,但字段返回数组还是字符串没判,地图点不出来,先用 `is_array` 兜底;二是大量 Marker 一次性全渲染,地图卡顿,按数据量做聚类(cluster)或分批;三是坐标字段格式(lat/lng 顺序)搞反,点全飘到错误位置。地图类功能适合门店/服务点分布,Marker 多一定要做聚类和懒加载,别一次性全画。
## 延伸阅读
- 想要现成外贸独立站,从零搭出能收询盘的站?看 [铲子铺 chanzipu 的外贸建站教程](https://chanzipu.com/)(同用 Bricks)
## 常见问题(FAQ)
**Marker 点不出来?** 多半是 ACF 字段返回数组/字符串没判类型,用 `is_array` 兜底、按正确结构取坐标。
**Marker 多地图卡?** 做聚类(cluster)或分批渲染,别一次性全画,量大时明显卡顿。
**点飘到错误位置?** 检查 lat/lng 顺序有没有搞反,坐标格式对齐地图 API 要求。
**能按分类显示不同 Marker 吗?** 能,按 ACF 分类字段过滤 Marker 数据,再渲染对应点。
- 相关阅读:[Bricks Setup Guide](/blog/bricks-setup-guide/)
延伸阅读
Bricks 用 ACF Checkbox 字段渲染自定义 SVG 图标列表
教程:Bricks 用 ACF Checkbox 字段渲染自定义 SVG 图标列表——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
tutorialBricks 按 ACF Gallery 图片数量做动态条件输出
教程:Bricks 按 ACF Gallery 图片数量做动态条件输出——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
tutorialBricks 查询循环输出 ACF 图片字段的 Alt、Caption、标题数据
教程:Bricks 查询循环输出 ACF 图片字段的 Alt、Caption、标题数据——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
tutorialBricks 中输出 ACF Relationship 关联文章的数量
教程:Bricks 中输出 ACF Relationship 关联文章的数量——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
