首页 / 中文教程 / 教程

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

本教程提供 PHP 与 JS 代码,让你在 Brickshttps://brickslabs.com/go/bricks 构建器中实现:根据某个分类法(taxonomy)的术语(term)来过滤地图上的标记。

Ray Chan·2026-08-12·约 4 分钟

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

目录

前置要求

  • 本地图系列 PART 1PART 2PART 3 的全部代码已经跑通。
  • 建一个自定义分类法(本示例用 store_categories),并给每篇自定义文章分配一个术语(本示例用 mobileinternet)。

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:

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

先贴出需要整体替换的完整代码,然后再讲重点改动。最终代码如下:

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 初始化增加第二种过滤方式:

    // 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(),这样无论是搜索框触发还是按钮触发的过滤,都调用它:

        // 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:每次点击按钮,脚本取出按钮对应的术语,对列表和地图同时应用过滤:

// 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 的切换——点击某个过滤按钮时,把它加到该按钮上(仅用于样式):

    // 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 建站?我接客户项目。

从快速营销站到完整的 Bricks 建站,再到从 Elementor 迁移——我都做过。本站每篇教程都来自真实项目经验。告诉我你的需求,一个工作日内回复。

  • Bricks 建站与改版
  • Elementor / Divi → Bricks 迁移
  • Bricks → Astro / headless 性能升级
  • 速度优化,PageSpeed 95+ 目标