import { Checkbox, Col, Divider, Input, message, Modal, Row, Typography } from 'antd'; import Link from 'antd/lib/typography/Link'; import { isEmpty } from 'lodash'; import type { ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage, useIntl, useRequest } from 'umi'; import AutoEmpty from '../Empty'; type City = { cityId: string; cityName: string; }; type CityGroup = { name: string; child: City[]; }; type CountryData = { countryId: string; name: string; city: CityGroup[]; }; export interface CityModalProps { view?: boolean; cityIds?: string[]; defaultShowSelected?: boolean; linkText?: ReactNode; modalTitle?: ReactNode; onFinish?: (data: { cityIds: string[] }) => void; /** 返回true则拦截此次选中变更 */ cancelCityCreator?: (cancelIds: string[], checked: boolean) => Promise; } const CityModal = (props: CityModalProps) => { const { view = false, cityIds, defaultShowSelected = false, linkText, modalTitle, onFinish, cancelCityCreator, } = props; const [open, setOpen] = useState(false); const [selectedCityIds, setSelectedCityIds] = useState([]); const [searchText, setSearchText] = useState(''); const [activeTab, setActiveTab] = useState(); const { formatMessage } = useIntl(); const { data, run: runAll } = useRequest( () => ({ url: '/goapi/config/city/all', method: 'POST', }), { manual: true }, ); // 处理城市选择变化 const handleCityChange = useCallback( async (cityId: string, checked: boolean) => { const result = await cancelCityCreator?.([cityId], checked); if (result) { return; } const newSelected = checked ? [...selectedCityIds, cityId] : selectedCityIds.filter((id) => id !== cityId); setSelectedCityIds(newSelected); }, [cancelCityCreator, selectedCityIds], ); // 处理分组全选 const handleGroupToggle = useCallback( async (group: CityGroup, checked: boolean) => { const groupCityIds = group.child.map((city) => city.cityId); const result = await cancelCityCreator?.(groupCityIds, checked); if (result) { return; } const newSelected = checked ? [...new Set([...selectedCityIds, ...groupCityIds])] : selectedCityIds.filter((id) => !groupCityIds.includes(id)); setSelectedCityIds(newSelected); }, [cancelCityCreator, selectedCityIds], ); // 计算国家下已选城市数量 const getSelectedCount = useCallback( (country: CountryData) => { return ( country?.city?.flatMap((group) => group?.child?.filter((city) => selectedCityIds.includes(city.cityId)), )?.length || 0 ); }, [selectedCityIds], ); // 处理搜索过滤 const filteredData = useMemo(() => { const countrySort = (a: CountryData, b: CountryData) => { const aSelectedCount = getSelectedCount(a); const bSelectedCount = getSelectedCount(b); return bSelectedCount - aSelectedCount; }; const l = Array.isArray(data) ? [...data] : []; if (!searchText) return l.sort(countrySort); const res = l.filter((country: CountryData) => country?.name.toLowerCase().includes(searchText.toLowerCase()), ); return res.sort(countrySort); }, [data, getSelectedCount, searchText]); const onOk = useCallback(async () => { if (isEmpty(selectedCityIds)) { message.error( formatMessage({ id: 'component.ApplyDetail.detail.city.message', defaultMessage: 'Please select your city', }), ); return; } onFinish?.({ cityIds: selectedCityIds }); setOpen(false); }, [formatMessage, onFinish, selectedCityIds]); const [showSelected, setShowSelected] = useState(view || defaultShowSelected); // 渲染城市分组内容 const renderCityContent = useCallback( (country?: CountryData) => { const { city: countryCity } = country ?? {}; return ( <> setShowSelected(e.target.checked)}> {formatMessage({ id: 'component.CityModal.selectedCity.checked.tips' })}
{countryCity ?.filter((g) => { return showSelected ? g.child.some((city) => selectedCityIds.includes(city.cityId)) : true; }) .map((g) => { return { name: g.name, child: g.child.filter((city) => { return showSelected ? selectedCityIds.includes(city.cityId) : true; }), }; }) .map((g) => { const { name, child } = g; const everyChecked = child?.every((city) => selectedCityIds.includes(city.cityId), ); // 半选 const indeterminate = child?.some((city) => selectedCityIds.includes(city.cityId)) && !everyChecked; return (
handleGroupToggle(g, e.target.checked)} > {name} {child?.map((city) => ( handleCityChange(city.cityId, e.target.checked)} > {city.cityName} ))}
); })}
); }, [formatMessage, handleCityChange, handleGroupToggle, selectedCityIds, showSelected, view], ); // 渲染国家 const renderCountry = useCallback( (country: CountryData) => { const selectedCount = getSelectedCount(country); return (
setActiveTab(country.countryId)} style={{ padding: '12px 16px', cursor: 'pointer', backgroundColor: activeTab === country.countryId ? 'rgba(241, 102, 34, 0.06)' : 'transparent', borderRight: activeTab === country.countryId ? '3px solid #F16622' : 'none', }} >
{country.name}
{selectedCount > 0 && ( {formatMessage({ id: 'base.common.select.count' }, { count: selectedCount })} )}
); }, [activeTab, formatMessage, getSelectedCount], ); useEffect(() => { if (open) { runAll(); } }, [open, runAll]); const [activeCountry, setActiveCountry] = useState(); useEffect(() => { const find = filteredData.find((country) => country.countryId === activeTab); if (find) { setActiveCountry(find); } else { setActiveCountry(filteredData[0]); setActiveTab(filteredData[0]?.countryId); } }, [filteredData, activeTab]); useEffect(() => { setSelectedCityIds(cityIds || []); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> setOpen(true)}> {linkText ?? ( )} ) } open={open} width={900} onCancel={() => setOpen(false)} destroyOnClose={true} onOk={onOk} okButtonProps={view ? { style: { display: 'none' } } : undefined} > setSearchText(e.target.value)} style={{ marginBottom: 16 }} />
{filteredData.map(renderCountry)}
{renderCityContent(activeCountry)}
); }; export default CityModal;