WordPress区块设置面板,对接外部API动态筛选,咋整?

3,564字
15–23 分钟
in

在WordPress里搞自定义区块,想从外部API拉数据展示,比如足球排名,还得让内容编辑能自由切换国家、联赛和赛季,这需求听着就头大。别慌,WordPress自带的区块编辑器控制面板(就是右侧那个能调样式、改颜色的侧边栏)完全能安排得明明白白。这次就来盘一盘,怎么给自定义区块加上一套筛选控件,让API数据听话地按需刷新。

目录

加载国家数据

动手之前得先弄到可选的国家名单。外部API(比如RapidAPI的足球数据接口)有专门返回国家列表的端点。思路很简单:当区块被插入到文章或页面里时,自动去抓一遍国家数据,存到组件的状态里。这活儿可以用React的useState配合useEffect来干。

// edit.js 核心片段
import { useState, useEffect } from '@wordpress/element';

const [countriesData, setCountriesData] = useState(null);

useEffect(() => {
  const fetchCountries = {
    method: "GET",
    headers: {
      "X-RapidAPI-Key": "你自己的密钥",
      "X-RapidAPI-Host": "api-football-v1.p.rapidapi.com",
    },
  };
  fetch("https://api-football-v1.p.rapidapi.com/v3/countries", fetchCountries)
    .then(res => res.json())
    .then(data => {
      let rawList = { ...data };
      console.log("国家清单", rawList.response);
      setCountriesData(rawList.response);
    })
    .catch(err => console.error(err));
}, []);

这里有个小坑:API密钥千万别写死在代码里提交到仓库,得用WordPress的wp_add_inline_script或者环境变量注入。另外,useEffect的依赖数组是空的,意味着只在区块首次加载时执行一次。如果觉得每次编辑页面都请求太频繁,可以考虑把结果缓存到浏览器的localStorage里,设置个过期时间。

搭建设置面板

国家数据到位后,就要把控件塞到右侧的设置面板里。WordPress的@wordpress/block-editor包提供了InspectorControls组件,它就像一块画布,专门承接各种设置控件。在edit函数中这样用:

import { InspectorControls } from "@wordpress/block-editor";

// 在Edit组件内部
<InspectorControls>
  { countriesData && (
    <CustomLeagueControls
      props={props}
      countryList={countriesData}
      setExternalData={setExternalData}
    />
  )}
</InspectorControls>

InspectorControls本质上是一个插槽(slot),它的子组件会被渲染到右侧设置栏。用条件渲染{countriesData && ...}确保只有数据加载完成后才显示控件,避免界面闪白。这里把复杂的控件逻辑抽成了一个独立组件CustomLeagueControls,这样edit.js就不会变成几百行的庞然大物。

组合框选择

国家列表少说也有上百项,普通的下拉框翻起来费劲。ComboboxControl组件是更优解——它带搜索输入框,可以一边打字一边筛选项。先得把API返回的国家数组转成它认的格式:

let formattedCountryOptions = countriesList.map(country => ({
  label: country.name,
  value: country.name,
}));

然后在组件里渲染:

import { ComboboxControl, PanelBody } from "@wordpress/block-editor";

<PanelBody title="数据筛选" initialOpen={false}>
  <ComboboxControl
    label="选择国家"
    value={selectedCountry}
    options={filteredCountryOps}
    onChange={val => handleCountryChange(val)}
    onInputChange={inputVal => {
      setFilteredCountryOps(
        formattedCountryOptions.filter(opt =>
          opt.label.toLowerCase().startsWith(inputVal.toLowerCase())
        )
      );
    }}
  />
</PanelBody>

PanelBody相当于一个可折叠的面板区域,initialOpen={false}让它默认收起,保持界面整洁。onInputChange每敲一个键就重新过滤一次选项列表,实现实时搜索的效果。注意startsWith是前缀匹配,如果想做模糊匹配可以换成includes,但性能稍差。

联动筛选

选了国家之后,联赛列表得跟着变——这叫做联动。handleCountryChange函数里要干三件事:更新国家状态、请求该国家下的联赛、把返回的联赛数据转成下拉选项。

function handleCountryChange(countryName) {
  setSelectedCountry(countryName);

  const leagueRequest = {
    method: "GET",
    headers: { "X-RapidAPI-Key": "你的key", "X-RapidAPI-Host": "api-football-v1.p.rapidapi.com" }
  };

  fetch(`https://api-football-v1.p.rapidapi.com/v3/leagues?country=${countryName}`, leagueRequest)
    .then(res => res.json())
    .then(data => {
      const leaguesRaw = data.response;
      setLeaguesList(leaguesRaw);
      let leagueOpts = leaguesRaw.map(item => ({
        label: item.league.name,
        value: item.league.name,
      }));
      setFilteredLeagueOpts(leagueOpts);
    })
    .catch(err => console.error(err));
}

这里有个细节:联赛数据里还嵌套了赛季数组(seasons),所以在handleLeagueChange里需要额外提取出当前选中联赛的ID和可用赛季列表。代码逻辑类似,就不再重复贴了。需要留意的是,有些联赛(比如某些杯赛)可能没有标准的积分榜数据,或者积分榜是分组的(像世界杯小组赛)。真实项目里要针对这些边缘情况做兜底处理——比如弹个提示“该联赛暂无排名数据”。

提交数据按钮

当国家、联赛、赛季三个选项都选好之后,才显示“拉取数据”按钮。这个按钮的展示条件可以写成:

{ selectedSeason && (
  <button className="fetch-rankings" onClick={fetchRankingsData}>
    获取排名
  </button>
)}

点击按钮时调用fetchRankingsData,它把当前选中的联赛ID和赛季参数拼接到API请求里,拿到排名数据后通过setExternalData传回父组件,最终渲染成表格。整个过程就像搭积木:右侧面板负责收集筛选条件,点击按钮后触发请求,表格区域重新渲染。

下面是最终在编辑器里呈现的效果——右侧面板的三个组合框丝滑联动,选完赛季点按钮,左边区块的排名表瞬间刷新。

组件作用常见坑点
ComboboxControl带搜索的下拉框选项数据格式必须是{label,value}
PanelBody可折叠面板容器记得设置initialOpen避免一直展开
InspectorControls挂载到右侧设置栏只能用在Edit函数内

这波操作下来,一个能动态筛选外部API数据的WordPress区块就基本成型了。不过还有个遗留问题:刷新页面或者保存文章后,刚才选的那些国家、联赛、赛季全都归零了。这是因为筛选设置还没有存到数据库里。下回就专门来搞定数据持久化,让选好的配置怎么刷新都不丢。