🆕 Bricks 2.4 已发布:查询循环性能提升 40%,迁移教程同步更新 →

首页 / 中文教程 / 教程

用 ACF 从 CPT 动态填充地图标记(Bricks 地图系列 PART 4:按分类术语过滤)

教程:用 ACF 从 CPT 动态填充地图标记(Bricks 地图系列 PART 4:按分类术语过滤)——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。

Ray ChanRay Chan·2026-08-12·约 5 分钟

“目录”

  • text: “前置要求”

  • text: “Bricks 结构”

  • text: “PHP”

  • text: “JavaScript”

  • text: “结论”

— 本教程提供 PHP 与 JS 代码,让你在 Bricks 构建器中实现:根据某个分类法(taxonomy)的术语(term)来过滤地图上的标记。 ## 目录 - 前置要求

  • Bricks 结构

  • PHP

  • JavaScript

  • 结论 ## 前置要求 - 本地图系列 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 &copy; <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: '&copy; <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/)
Ray Chan

站长

Ray Chan

WordPress Developer & Bricks Specialist

WordPress developer with 10+ years of client builds. Switched to Bricks in 2023 — now builds fast WordPress sites and migrates legacy Elementor/Divi projects.

延伸阅读