在使用 Strapi 无头 CMS 过程中,因其本身未提供多级列举选择(下拉菜单联动)无法在 Content Manager 中的实时交互功能,由 @Chris Ebert 国家选择插件启发,便试着在其基础上实现省、市、区、县…多级列举选择功能~

一、搭建 Strapi 环境(可跳过)

  1. 确保服务器内已部署 node.js 环境(建议v20.xx.x版本),再执行 `npx create-strapi-app@latest strapi-name --quickstart` 安装 Strapi 项目
    <hao-tag-link link="https://ezdoc.cn/docs/strapi/dev-docs/installation/cli" logo="https://strapi.io/assets/favicon-32x32.png" title="Strapi 开发者文档 | EzDoc" described="ezdoc.cn"></hao-tag-link>

  2. 下载报错则需修改为 npm 阿里云镜像源

npm config set registry https://registry.npmmirror.com
  1. 如遇个别组件(如 sharp)下载失败,则需在 strapi-name 文件夹内新建 .npmrc 文件,内容为:

registry=https://registry.npmmirror.com
sharp_binary_host=https://npmmirror.com/mirrors/sharp
sharp_libvips_binary_host=https://npmmirror.com/mirrors/sharp-libvips
  1. 再接着进入 strapi-name 项目目录内运行终端

npm install
npm run develop
  1. 弹出以下内容即为成功

 Project information                                                          
┌────────────────────┬──────────────────────────────────────────────────┐
│ Time               │ Wed Jan 15 2025 15:10:13 GMT+0800 (China Standa… │
│ Launched in        │ 14431 ms                                         │
│ Environment        │ development                                      │
│ Process PID        │ 31396                                            │
│ Version            │ 5.7.0 (node v20.18.1)                            │
│ Edition            │ Enterprise                                       │
│ Database           │ mysql                                            │
│ Database name      │ strapi                                           │
└────────────────────┴──────────────────────────────────────────────────┘
 Actions available                                                            

One more thing...
Create your first administrator 💻 by going to the administration panel at:

┌─────────────────────────────┐
│ http://localhost:1337/admin │
└─────────────────────────────┘

[2024-12-10 08:49:37.549] info: Strapi started successfully
  1. (可选)解锁高级版指令:`npx @yek-plus/strapi-crack`

二、复制并创建 country-select 插件

(1) 安装原版插件

  • 在项目根目录启动终端并执行

npm install strapi-plugin-country-select 
  • (报错可加上 --legacy-peer-deps 再次尝试)

(2) 将插件复制到插件目录

  1. 创建插件目录

mkdir -p src/plugins/region-select
  1. 复制插件代码

cp -R node_modules/strapi-plugin-country-select/* src/plugins/region-select/
  1. 删除 node_modules 中的原始插件版本(防止冲突):

npm uninstall strapi-plugin-country-select
  1. 打开根目录的 package.json 注册本地插件:

"dependencies": {
  "strapi-plugin-country-select": "file:./src/plugins/region-select"
}
  1. 重新安装依赖

npm install

三、修改插件代码

  1. 将原有的国家数据替换为省市数据。创建一个新的 regions.json 文件,保存到 src/plugins/region-select/dist/admin/src/components/data 中:

{
  "江苏省": {
    "苏州市": {
      "姑苏区": ["平江街道", "沧浪街道", "金阊街道"],
      "虎丘区": ["狮山街道", "枫桥街道", "横塘街道"],
      "吴中区": ["长桥街道", "越溪街道", "木渎镇"],
      "相城区": ["元和街道", "黄桥街道", "望亭镇"],
      "吴江区": ["松陵街道", "同里镇", "震泽镇"],
      "苏州工业园区": ["湖西社区", "湖东社区", "斜塘街道"],
      "常熟市": ["虞山镇", "海虞镇", "古里镇"],
      "张家港市": ["杨舍镇", "塘桥镇", "金港镇"],
      "昆山市": ["玉山镇", "花桥镇", "周市镇"],
      "太仓市": ["城厢镇", "沙溪镇", "浮桥镇"]
    }
  },
  "上海市": {
    "黄浦区": ["南京东路街道", "外滩街道", "半淞园路街道"],
    "徐汇区": ["天平路街道", "湖南路街道", "斜土路街道"],
    "长宁区": ["华阳路街道", "江苏路街道", "新华路街道"],
    "静安区": ["静安寺街道", "南京西路街道", "曹家渡街道"],
    "普陀区": ["长寿路街道", "甘泉路街道", "石泉路街道"],
    "虹口区": ["欧阳路街道", "曲阳路街道", "广中路街道"],
    "杨浦区": ["四平路街道", "控江路街道", "长白新村街道"],
    "闵行区": ["江川路街道", "古美路街道", "莘庄镇"],
    "宝山区": ["友谊路街道", "吴淞街道", "张庙街道"],
    "嘉定区": ["新成路街道", "真新街道", "嘉定镇街道"],
    "浦东新区": ["陆家嘴街道", "周家渡街道", "花木街道"],
    "金山区": ["石化街道", "朱泾镇", "枫泾镇"],
    "松江区": ["岳阳街道", "永丰街道", "泗泾镇"],
    "青浦区": ["夏阳街道", "盈浦街道", "朱家角镇"],
    "奉贤区": ["南桥镇", "奉城镇", "四团镇"],
    "崇明区": ["城桥镇", "堡镇", "新河镇"]
  },
  "浙江省": {
    "杭州市": {
      "上城区": ["清波街道", "湖滨街道", "小营街道"],
      "拱墅区": ["米市巷街道", "湖墅街道", "大关街道"],
      "西湖区": ["西溪街道", "文新街道", "翠苑街道"],
      "滨江区": ["西兴街道", "长河街道", "浦沿街道"],
      "萧山区": ["城厢街道", "北干街道", "蜀山街道"],
      "余杭区": ["南苑街道", "临平街道", "星桥街道"],
      "富阳区": ["富春街道", "东洲街道", "春江街道"],
      "临安区": ["锦城街道", "锦北街道", "玲珑街道"],
      "桐庐县": ["桐君街道", "城南街道", "分水镇"],
      "淳安县": ["千岛湖镇", "汾口镇", "威坪镇"],
      "建德市": ["新安江街道", "洋溪街道", "梅城镇"]
    },
    "宁波市": {
      "海曙区": ["南门街道", "西门街道", "鼓楼街道"],
      "江北区": ["白沙街道", "甬江街道", "文教街道"],
      "北仑区": ["新碶街道", "小港街道", "大碶街道"],
      "镇海区": ["招宝山街道", "蛟川街道", "骆驼街道"],
      "鄞州区": ["钟公庙街道", "首南街道", "中河街道"],
      "奉化区": ["锦屏街道", "岳林街道", "西坞街道"],
      "象山县": ["丹东街道", "丹西街道", "石浦镇"],
      "宁海县": ["桃源街道", "跃龙街道", "梅林街道"],
      "余姚市": ["阳明街道", "梨洲街道", "兰江街道"],
      "慈溪市": ["浒山街道", "宗汉街道", "坎墩街道"]
    },
    "温州市": {
      "鹿城区": ["五马街道", "南门街道", "松台街道"],
      "龙湾区": ["永中街道", "永兴街道", "海滨街道"],
      "瓯海区": ["景山街道", "梧田街道", "新桥街道"],
      "洞头区": ["北岙街道", "东屏街道", "元觉街道"],
      "永嘉县": ["瓯北街道", "桥头镇", "桥下镇"],
      "平阳县": ["昆阳镇", "鳌江镇", "水头镇"],
      "苍南县": ["灵溪镇", "龙港市", "宜山镇"],
      "文成县": ["大峃镇", "玉壶镇", "珊溪镇"],
      "泰顺县": ["罗阳镇", "司前畲族镇", "雅阳镇"],
      "瑞安市": ["安阳街道", "玉海街道", "锦湖街道"],
      "乐清市": ["乐成街道", "柳市镇", "虹桥镇"]
    }
  }
}
  1. 找到文件 src/plugins/country-select/dist/admin/src/components/CountrySelect/index.d.js 并修改代码为:

import React, { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { Select, Option } from '@strapi/design-system/Select';
import { Field, FieldLabel, FieldInput, FieldHint, FieldError } from '@strapi/design-system/Field';
import { Stack } from '@strapi/design-system/Stack';
import data from './data/regions.json'; // 请确保此路径正确

const ProvinceCityAreaCountySelect = ({ name, value = {}, onChange, error, description, required }) => {
  const { formatMessage } = useIntl();
  const [province, setProvince] = useState(value.province || '');
  const [city, setCity] = useState(value.city || '');
  const [area, setArea] = useState(value.area || '');
  const [county, setCounty] = useState(value.county || '');

  useEffect(() => {
    onChange({ target: { name, value: { province, city, area, county } } });
  }, [province, city, area, county]);

  return (
    <Field name={name} error={error} hint={description} required={required}>
      <Stack spacing={1}>
        <FieldLabel>{formatMessage({ id: 'components.ProvinceCityAreaCountySelect.label', defaultMessage: 'Location' })}</FieldLabel>
        <FieldInput>
          <Select
            placeholder={formatMessage({ id: 'components.ProvinceCityAreaCountySelect.selectProvince', defaultMessage: 'Select Province' })}
            value={province}
            onChange={setProvince}
          >
            {Object.keys(data).map((prov) => (
              <Option key={prov} value={prov}>
                {prov}
              </Option>
            ))}
          </Select>
        </FieldInput>
        {province && (
          <FieldInput>
            <Select
              placeholder={formatMessage({ id: 'components.ProvinceCityAreaCountySelect.selectCity', defaultMessage: 'Select City' })}
              value={city}
              onChange={setCity}
            >
              {Object.keys(data[province] || {}).map((ct) => (
                <Option key={ct} value={ct}>
                  {ct}
                </Option>
              ))}
            </Select>
          </FieldInput>
        )}
        {city && (
          <FieldInput>
            <Select
              placeholder={formatMessage({ id: 'components.ProvinceCityAreaCountySelect.selectArea', defaultMessage: 'Select Area' })}
              value={area}
              onChange={setArea}
            >
              {Object.keys(data[province][city] || {}).map((ar) => (
                <Option key={ar} value={ar}>
                  {ar}
                </Option>
              ))}
            </Select>
          </FieldInput>
        )}
        {area && (
          <FieldInput>
            <Select
              placeholder={formatMessage({ id: 'components.ProvinceCityAreaCountySelect.selectCounty', defaultMessage: 'Select County' })}
              value={county}
              onChange={setCounty}
            >
              {(data[province][city][area] || []).map((cnty) => (
                <Option key={cnty} value={cnty}>
                  {cnty}
                </Option>
              ))}
            </Select>
          </FieldInput>
        )}
        <FieldHint />
        <FieldError />
      </Stack>
    </Field>
  );
};

export default ProvinceCityAreaCountySelect;
  1. 最后在项目的 config/plugins.ts 文件中启用插件:

export default ({ env }) => ({
  'region-select': {
    enabled: true,
  },
});
  1. 保存并测试

npm run build && npm run develop

四、使用后如何移除插件

  1. 删除 src/plugins/region-select 文件夹

  2. 去除 config/plugins.ts 文件中代码

export default () => ({});