feat: 添加中国地图交互功能
- 新增 react-simple-maps 地图库 - 实现中国省级行政区划地图(34 个省份) - 首页集成地图组件,点击省份跳转城市列表 - 悬停显示省份名称,热力图颜色表示内容丰富度 - 重构 stores 导出方式,支持 hooks 模式
This commit is contained in:
191
frontend/src/components/common/ChinaMap.js
Normal file
191
frontend/src/components/common/ChinaMap.js
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user