feat: 添加中国地图交互功能

- 新增 react-simple-maps 地图库
- 实现中国省级行政区划地图(34 个省份)
- 首页集成地图组件,点击省份跳转城市列表
- 悬停显示省份名称,热力图颜色表示内容丰富度
- 重构 stores 导出方式,支持 hooks 模式
This commit is contained in:
maoshen
2026-04-14 01:11:07 +00:00
parent fd43febada
commit 56da90b88a
17 changed files with 21133 additions and 38 deletions

View File

@@ -0,0 +1,191 @@
import React, { useState } from 'react';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import { scaleQuantile } from 'd3-scale';
import styled from 'styled-components';
import { useNavigate } from 'react-router-dom';
import chinaGeo from '../../data/china-provinces.geo.json';
const MapContainer = styled.div`
width: 100%;
max-width: 1000px;
margin: 0 auto;
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
`;
const MapTitle = styled.h2`
text-align: center;
margin-bottom: 20px;
color: #333;
font-size: 24px;
`;
const Tooltip = styled.div`
position: absolute;
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
pointer-events: none;
z-index: 1000;
white-space: nowrap;
transform: translate(-50%, -100%);
margin-top: -10px;
`;
const Legend = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-top: 16px;
gap: 20px;
font-size: 13px;
color: #666;
`;
const LegendItem = styled.div`
display: flex;
align-items: center;
gap: 6px;
`;
const LegendColor = styled.div`
width: 16px;
height: 16px;
border-radius: 3px;
background: ${(props) => props.color};
border: 1px solid #ddd;
`;
const MapWrapper = styled.div`
position: relative;
width: 100%;
height: 500px;
`;
const ChinaMap = ({ onProvinceClick }) => {
const navigate = useNavigate();
const [tooltipContent, setTooltipContent] = useState('');
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
const [showTooltip, setShowTooltip] = useState(false);
// 从 API 获取各省数据(文章数、服务数等)用于热力图
// 暂时用静态数据演示,后续可以连接 API
const provinceData = {
'北京市': { articles: 15, services: 8 },
'上海市': { articles: 12, services: 10 },
'广东省': { articles: 20, services: 15 },
'四川省': { articles: 18, services: 12 },
'浙江省': { articles: 14, services: 9 },
'江苏省': { articles: 16, services: 11 },
};
const colorScale = scaleQuantile()
.domain(chinaGeo.features.map((f) => provinceData[f.properties.name]?.articles || 0))
.range(['#e3f2fd', '#90caf9', '#42a5f5', '#1976d2', '#0d47a1']);
const handleProvinceClick = (geo) => {
const provinceName = geo.properties.name;
const provinceCode = geo.properties.code;
if (onProvinceClick) {
onProvinceClick(geo);
} else {
// 默认跳转到城市列表页
// 后续需要根据省份 code 查询对应的 region ID
console.log(`点击了:${provinceName}`, provinceCode);
// navigate(`/cities?province=${provinceCode}`);
}
};
return (
<MapContainer>
<MapTitle>选择省份</MapTitle>
<MapWrapper>
<ComposableMap
projection="geoMercator"
projectionConfig={{
scale: 1000,
center: [105, 38],
}}
style={{
width: '100%',
height: '100%',
}}
>
<ZoomableGroup zoom={1}>
<Geographies geography={chinaGeo}>
{({ geographies }) =>
geographies.map((geo) => {
const provinceName = geo.properties.name;
const articleCount = provinceData[provinceName]?.articles || 0;
const fillColor = colorScale(articleCount);
return (
<Geography
key={geo.rsmKey}
geography={geo}
fill={fillColor}
stroke="#fff"
strokeWidth={1}
style={{
default: {
outline: 'none',
cursor: 'pointer',
transition: 'all 0.3s',
},
hover: {
fill: '#ff6b6b',
outline: 'none',
transform: 'scale(1.02)',
},
pressed: {
fill: '#c92a2a',
outline: 'none',
},
}}
onMouseEnter={() => {
setShowTooltip(true);
setTooltipContent(provinceName);
}}
onMouseMove={(e) => {
const { clientX, clientY } = e;
setTooltipPosition({ x: clientX, y: clientY });
}}
onMouseLeave={() => {
setShowTooltip(false);
}}
onClick={() => handleProvinceClick(geo)}
/>
);
})
}
</Geographies>
</ZoomableGroup>
</ComposableMap>
{showTooltip && (
<Tooltip style={{ left: tooltipPosition.x, top: tooltipPosition.y }}>
{tooltipContent}
</Tooltip>
)}
</MapWrapper>
<Legend>
<LegendItem>
<LegendColor color="#e3f2fd" />
<span>较少内容</span>
</LegendItem>
<LegendItem>
<LegendColor color="#0d47a1" />
<span>丰富内容</span>
</LegendItem>
</Legend>
</MapContainer>
);
};
export default ChinaMap;

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { observer } from 'mobx-react-lite';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { useRegionStore } from '../../stores/RegionStore';
import { useArticleStore } from '../../stores/ArticleStore';
import { useServiceStore } from '../../stores/ServiceStore';