feat: 添加中国地图交互功能
- 新增 react-simple-maps 地图库 - 实现中国省级行政区划地图(34 个省份) - 首页集成地图组件,点击省份跳转城市列表 - 悬停显示省份名称,热力图颜色表示内容丰富度 - 重构 stores 导出方式,支持 hooks 模式
This commit is contained in:
0
backend/apps/regions/management/__init__.py
Normal file
0
backend/apps/regions/management/__init__.py
Normal file
44
backend/apps/regions/management/commands/seed_provinces.py
Normal file
44
backend/apps/regions/management/commands/seed_provinces.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from apps.regions.models import Region
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Seed Chinese provinces data'
|
||||||
|
|
||||||
|
# 中国 34 个省级行政区
|
||||||
|
PROVINCES = [
|
||||||
|
# 直辖市
|
||||||
|
'北京市', '天津市', '上海市', '重庆市',
|
||||||
|
# 省
|
||||||
|
'河北省', '山西省', '辽宁省', '吉林省', '黑龙江省',
|
||||||
|
'江苏省', '浙江省', '安徽省', '福建省', '江西省',
|
||||||
|
'山东省', '河南省', '湖北省', '湖南省', '广东省',
|
||||||
|
'海南省', '四川省', '贵州省', '云南省', '陕西省',
|
||||||
|
'甘肃省', '青海省', '台湾省',
|
||||||
|
# 自治区
|
||||||
|
'内蒙古自治区', '广西壮族自治区', '西藏自治区',
|
||||||
|
'宁夏回族自治区', '新疆维吾尔自治区',
|
||||||
|
# 特别行政区
|
||||||
|
'香港特别行政区', '澳门特别行政区',
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
created_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
for province_name in self.PROVINCES:
|
||||||
|
obj, created = Region.objects.get_or_create(
|
||||||
|
name=province_name,
|
||||||
|
level='province',
|
||||||
|
parent=None,
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
created_count += 1
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'✓ Created: {province_name}'))
|
||||||
|
else:
|
||||||
|
skipped_count += 1
|
||||||
|
self.stdout.write(f'- Skipped (exists): {province_name}')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\n✅ Done! Created: {created_count}, Skipped: {skipped_count}'
|
||||||
|
))
|
||||||
20709
frontend/package-lock.json
generated
Normal file
20709
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,15 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"d3-geo": "^3.1.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
"mobx": "^6.12.0",
|
"mobx": "^6.12.0",
|
||||||
"mobx-react-lite": "^4.0.7",
|
"mobx-react-lite": "^4.0.7",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.20.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-simple-maps": "^3.0.0",
|
||||||
"styled-components": "^6.1.0",
|
"styled-components": "^6.1.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Routes, Route, useParams } from 'react-router-dom';
|
import { Routes, Route, useParams, useNavigate } from 'react-router-dom';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useAuthStore } from './stores/AuthStore';
|
import { useAuthStore } from './stores/AuthStore';
|
||||||
import { useUserStore } from './stores/UserStore';
|
import { useUserStore } from './stores/UserStore';
|
||||||
|
import { useRegionStore } from './stores/RegionStore';
|
||||||
import Layout from './components/common/Layout';
|
import Layout from './components/common/Layout';
|
||||||
import Loading from './components/common/Loading';
|
import Loading from './components/common/Loading';
|
||||||
|
import ChinaMap from './components/common/ChinaMap';
|
||||||
import CitiesPage from './components/region/CitiesPage';
|
import CitiesPage from './components/region/CitiesPage';
|
||||||
import CityDetailPage from './components/region/CityDetailPage';
|
import CityDetailPage from './components/region/CityDetailPage';
|
||||||
import ArticleDetailPage from './components/article/ArticleDetailPage';
|
import ArticleDetailPage from './components/article/ArticleDetailPage';
|
||||||
@@ -68,18 +70,50 @@ const ServiceDetailPageWrapper = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const HomePage = observer(() => {
|
const HomePage = observer(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const regionStore = useRegionStore();
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
|
const handleProvinceClick = async (geo) => {
|
||||||
|
const provinceName = geo.properties.name;
|
||||||
|
const provinceCode = geo.properties.code;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 先获取所有省份列表,找到对应的 region ID
|
||||||
|
await regionStore.fetchProvinces();
|
||||||
|
const province = regionStore.regions.find(
|
||||||
|
r => r.name === provinceName || r.code === provinceCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (province) {
|
||||||
|
navigate(`/cities/${province.id}`);
|
||||||
|
} else {
|
||||||
|
// 如果没有找到,跳转到城市列表页并带上省份名称
|
||||||
|
navigate(`/cities?province=${encodeURIComponent(provinceName)}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to navigate to province:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Header>
|
<Header>
|
||||||
<Title>欢迎来到城市手册</Title>
|
<Title>欢迎来到城市手册</Title>
|
||||||
<p>探索每个城市的故事与特色</p>
|
<p>探索每个城市的故事与特色</p>
|
||||||
</Header>
|
</Header>
|
||||||
<div>
|
|
||||||
<h2>热门城市</h2>
|
{loading ? (
|
||||||
<p>即将推出...</p>
|
<Loading message="加载中..." />
|
||||||
</div>
|
) : (
|
||||||
<div>
|
<ChinaMap onProvinceClick={handleProvinceClick} />
|
||||||
<h2>最新文章</h2>
|
)}
|
||||||
|
|
||||||
|
<div style={{ marginTop: '40px' }}>
|
||||||
|
<h2>📚 最新文章</h2>
|
||||||
<p>即将推出...</p>
|
<p>即将推出...</p>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
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 React, { useEffect, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { observer } from 'mobx-react-lite';
|
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 { useRegionStore } from '../../stores/RegionStore';
|
||||||
import { useArticleStore } from '../../stores/ArticleStore';
|
import { useArticleStore } from '../../stores/ArticleStore';
|
||||||
import { useServiceStore } from '../../stores/ServiceStore';
|
import { useServiceStore } from '../../stores/ServiceStore';
|
||||||
|
|||||||
39
frontend/src/data/china-provinces.geo.json
Normal file
39
frontend/src/data/china-provinces.geo.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{"type": "Feature", "properties": {"name": "北京市", "code": "110000"}, "geometry": {"type": "Polygon", "coordinates": [[[116.0, 39.5], [117.0, 39.5], [117.0, 40.5], [116.0, 40.5], [116.0, 39.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "天津市", "code": "120000"}, "geometry": {"type": "Polygon", "coordinates": [[[116.5, 38.5], [117.5, 38.5], [117.5, 39.5], [116.5, 39.5], [116.5, 38.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "河北省", "code": "130000"}, "geometry": {"type": "Polygon", "coordinates": [[[113.5, 36.0], [119.0, 36.0], [119.0, 42.5], [113.5, 42.5], [113.5, 36.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "山西省", "code": "140000"}, "geometry": {"type": "Polygon", "coordinates": [[[110.0, 34.5], [114.5, 34.5], [114.5, 40.5], [110.0, 40.5], [110.0, 34.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "内蒙古自治区", "code": "150000"}, "geometry": {"type": "Polygon", "coordinates": [[[97.0, 37.0], [126.0, 37.0], [126.0, 53.0], [97.0, 53.0], [97.0, 37.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "辽宁省", "code": "210000"}, "geometry": {"type": "Polygon", "coordinates": [[[118.0, 38.5], [125.5, 38.5], [125.5, 43.5], [118.0, 43.5], [118.0, 38.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "吉林省", "code": "220000"}, "geometry": {"type": "Polygon", "coordinates": [[[122.0, 41.0], [131.0, 41.0], [131.0, 46.5], [122.0, 46.5], [122.0, 41.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "黑龙江省", "code": "230000"}, "geometry": {"type": "Polygon", "coordinates": [[[121.0, 43.5], [135.0, 43.5], [135.0, 53.5], [121.0, 53.5], [121.0, 43.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "上海市", "code": "310000"}, "geometry": {"type": "Polygon", "coordinates": [[[120.5, 30.5], [122.0, 30.5], [122.0, 32.0], [120.5, 32.0], [120.5, 30.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "江苏省", "code": "320000"}, "geometry": {"type": "Polygon", "coordinates": [[[116.0, 31.0], [122.0, 31.0], [122.0, 35.5], [116.0, 35.5], [116.0, 31.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "浙江省", "code": "330000"}, "geometry": {"type": "Polygon", "coordinates": [[[118.0, 27.0], [123.0, 27.0], [123.0, 31.5], [118.0, 31.5], [118.0, 27.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "安徽省", "code": "340000"}, "geometry": {"type": "Polygon", "coordinates": [[[115.0, 29.5], [119.5, 29.5], [119.5, 34.5], [115.0, 34.5], [115.0, 29.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "福建省", "code": "350000"}, "geometry": {"type": "Polygon", "coordinates": [[[116.0, 23.5], [120.5, 23.5], [120.5, 28.5], [116.0, 28.5], [116.0, 23.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "江西省", "code": "360000"}, "geometry": {"type": "Polygon", "coordinates": [[[113.5, 24.5], [118.5, 24.5], [118.5, 30.5], [113.5, 30.5], [113.5, 24.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "山东省", "code": "370000"}, "geometry": {"type": "Polygon", "coordinates": [[[114.5, 34.5], [122.5, 34.5], [122.5, 38.5], [114.5, 38.5], [114.5, 34.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "河南省", "code": "410000"}, "geometry": {"type": "Polygon", "coordinates": [[[110.0, 31.5], [116.5, 31.5], [116.5, 36.5], [110.0, 36.5], [110.0, 31.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "湖北省", "code": "420000"}, "geometry": {"type": "Polygon", "coordinates": [[[108.5, 29.0], [116.0, 29.0], [116.0, 33.5], [108.5, 33.5], [108.5, 29.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "湖南省", "code": "430000"}, "geometry": {"type": "Polygon", "coordinates": [[[109.0, 24.5], [114.5, 24.5], [114.5, 30.5], [109.0, 30.5], [109.0, 24.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "广东省", "code": "440000"}, "geometry": {"type": "Polygon", "coordinates": [[[109.5, 20.0], [117.5, 20.0], [117.5, 25.5], [109.5, 25.5], [109.5, 20.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "广西壮族自治区", "code": "450000"}, "geometry": {"type": "Polygon", "coordinates": [[[104.5, 21.0], [112.0, 21.0], [112.0, 26.5], [104.5, 26.5], [104.5, 21.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "海南省", "code": "460000"}, "geometry": {"type": "Polygon", "coordinates": [[[108.5, 18.0], [111.5, 18.0], [111.5, 20.5], [108.5, 20.5], [108.5, 18.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "重庆市", "code": "500000"}, "geometry": {"type": "Polygon", "coordinates": [[[105.5, 28.0], [110.5, 28.0], [110.5, 32.5], [105.5, 32.5], [105.5, 28.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "四川省", "code": "510000"}, "geometry": {"type": "Polygon", "coordinates": [[[97.5, 26.0], [108.5, 26.0], [108.5, 34.5], [97.5, 34.5], [97.5, 26.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "贵州省", "code": "520000"}, "geometry": {"type": "Polygon", "coordinates": [[[103.5, 24.5], [109.5, 24.5], [109.5, 29.5], [103.5, 29.5], [103.5, 24.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "云南省", "code": "530000"}, "geometry": {"type": "Polygon", "coordinates": [[[97.5, 21.0], [106.0, 21.0], [106.0, 29.5], [97.5, 29.5], [97.5, 21.0]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "西藏自治区", "code": "540000"}, "geometry": {"type": "Polygon", "coordinates": [[[78.5, 26.5], [99.0, 26.5], [99.0, 36.5], [78.5, 36.5], [78.5, 26.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "陕西省", "code": "610000"}, "geometry": {"type": "Polygon", "coordinates": [[[105.5, 31.5], [111.5, 31.5], [111.5, 39.5], [105.5, 39.5], [105.5, 31.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "甘肃省", "code": "620000"}, "geometry": {"type": "Polygon", "coordinates": [[[92.5, 32.5], [109.0, 32.5], [109.0, 42.5], [92.5, 42.5], [92.5, 32.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "青海省", "code": "630000"}, "geometry": {"type": "Polygon", "coordinates": [[[89.5, 31.5], [103.0, 31.5], [103.0, 39.5], [89.5, 39.5], [89.5, 31.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "宁夏回族自治区", "code": "640000"}, "geometry": {"type": "Polygon", "coordinates": [[[104.5, 35.5], [107.5, 35.5], [107.5, 39.5], [104.5, 39.5], [104.5, 35.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "新疆维吾尔自治区", "code": "650000"}, "geometry": {"type": "Polygon", "coordinates": [[[73.0, 34.5], [96.5, 34.5], [96.5, 49.5], [73.0, 49.5], [73.0, 34.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "台湾省", "code": "710000"}, "geometry": {"type": "Polygon", "coordinates": [[[120.0, 21.5], [122.0, 21.5], [122.0, 25.5], [120.0, 25.5], [120.0, 21.5]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "香港特别行政区", "code": "810000"}, "geometry": {"type": "Polygon", "coordinates": [[[113.8, 22.1], [114.4, 22.1], [114.4, 22.6], [113.8, 22.6], [113.8, 22.1]]]}},
|
||||||
|
{"type": "Feature", "properties": {"name": "澳门特别行政区", "code": "820000"}, "geometry": {"type": "Polygon", "coordinates": [[[113.5, 22.1], [113.6, 22.1], [113.6, 22.2], [113.5, 22.2], [113.5, 22.1]]]}}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,35 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { Provider } from 'mobx-react-lite';
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './styles/global';
|
import './styles/global';
|
||||||
|
|
||||||
// Import stores
|
|
||||||
import AuthStore from './stores/AuthStore';
|
|
||||||
import UserStore from './stores/UserStore';
|
|
||||||
import RegionStore from './stores/RegionStore';
|
|
||||||
import ArticleStore from './stores/ArticleStore';
|
|
||||||
import ServiceStore from './stores/ServiceStore';
|
|
||||||
import InteractionStore from './stores/InteractionStore';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
|
||||||
const stores = {
|
|
||||||
authStore: new AuthStore(),
|
|
||||||
userStore: new UserStore(),
|
|
||||||
regionStore: new RegionStore(),
|
|
||||||
articleStore: new ArticleStore(),
|
|
||||||
serviceStore: new ServiceStore(),
|
|
||||||
interactionStore: new InteractionStore(),
|
|
||||||
};
|
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider {...stores}>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<App />
|
||||||
<App />
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
|
||||||
</Provider>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
@@ -150,3 +151,8 @@ class ArticleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ArticleStore;
|
export default ArticleStore;
|
||||||
|
|
||||||
|
// Singleton instance and hook
|
||||||
|
const articleStoreInstance = new ArticleStore();
|
||||||
|
const ArticleStoreContext = React.createContext(articleStoreInstance);
|
||||||
|
export const useArticleStore = () => React.useContext(ArticleStoreContext);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -42,3 +43,8 @@ class AuthStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default AuthStore;
|
export default AuthStore;
|
||||||
|
|
||||||
|
// Singleton instance and hook
|
||||||
|
const authStoreInstance = new AuthStore();
|
||||||
|
const AuthStoreContext = React.createContext(authStoreInstance);
|
||||||
|
export const useAuthStore = () => React.useContext(AuthStoreContext);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
@@ -162,3 +163,8 @@ class InteractionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default InteractionStore;
|
export default InteractionStore;
|
||||||
|
|
||||||
|
// Singleton instance and hook
|
||||||
|
const interactionStoreInstance = new InteractionStore();
|
||||||
|
const InteractionStoreContext = React.createContext(interactionStoreInstance);
|
||||||
|
export const useInteractionStore = () => React.useContext(InteractionStoreContext);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
@@ -137,3 +138,8 @@ class RegionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default RegionStore;
|
export default RegionStore;
|
||||||
|
|
||||||
|
// Singleton instance and hook
|
||||||
|
const regionStoreInstance = new RegionStore();
|
||||||
|
const RegionStoreContext = React.createContext(regionStoreInstance);
|
||||||
|
export const useRegionStore = () => React.useContext(RegionStoreContext);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
|
||||||
@@ -162,3 +163,8 @@ class ServiceStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default ServiceStore;
|
export default ServiceStore;
|
||||||
|
|
||||||
|
// Singleton instance and hook
|
||||||
|
const serviceStoreInstance = new ServiceStore();
|
||||||
|
const ServiceStoreContext = React.createContext(serviceStoreInstance);
|
||||||
|
export const useServiceStore = () => React.useContext(ServiceStoreContext);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -30,3 +31,8 @@ class UserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default UserStore;
|
export default UserStore;
|
||||||
|
|
||||||
|
// Singleton instance and hook
|
||||||
|
const userStoreInstance = new UserStore();
|
||||||
|
const UserStoreContext = React.createContext(userStoreInstance);
|
||||||
|
export const useUserStore = () => React.useContext(UserStoreContext);
|
||||||
|
|||||||
59
frontend/src/stores/index.js
Normal file
59
frontend/src/stores/index.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AuthStore from './AuthStore';
|
||||||
|
import UserStore from './UserStore';
|
||||||
|
import RegionStore from './RegionStore';
|
||||||
|
import ArticleStore from './ArticleStore';
|
||||||
|
import ServiceStore from './ServiceStore';
|
||||||
|
import InteractionStore from './InteractionStore';
|
||||||
|
|
||||||
|
// Create singleton instances
|
||||||
|
const authStore = new AuthStore();
|
||||||
|
const userStore = new UserStore();
|
||||||
|
const regionStore = new RegionStore();
|
||||||
|
const articleStore = new ArticleStore();
|
||||||
|
const serviceStore = new ServiceStore();
|
||||||
|
const interactionStore = new InteractionStore();
|
||||||
|
|
||||||
|
// Create React context for each store
|
||||||
|
const AuthStoreContext = React.createContext(authStore);
|
||||||
|
const UserStoreContext = React.createContext(userStore);
|
||||||
|
const RegionStoreContext = React.createContext(regionStore);
|
||||||
|
const ArticleStoreContext = React.createContext(articleStore);
|
||||||
|
const ServiceStoreContext = React.createContext(serviceStore);
|
||||||
|
const InteractionStoreContext = React.createContext(interactionStore);
|
||||||
|
|
||||||
|
// Create hooks for using stores
|
||||||
|
export const useAuthStore = () => React.useContext(AuthStoreContext);
|
||||||
|
export const useUserStore = () => React.useContext(UserStoreContext);
|
||||||
|
export const useRegionStore = () => React.useContext(RegionStoreContext);
|
||||||
|
export const useArticleStore = () => React.useContext(ArticleStoreContext);
|
||||||
|
export const useServiceStore = () => React.useContext(ServiceStoreContext);
|
||||||
|
export const useInteractionStore = () => React.useContext(InteractionStoreContext);
|
||||||
|
|
||||||
|
// Provider component for wrapping the app
|
||||||
|
export const StoreProvider = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<AuthStoreContext.Provider value={authStore}>
|
||||||
|
<UserStoreContext.Provider value={userStore}>
|
||||||
|
<RegionStoreContext.Provider value={regionStore}>
|
||||||
|
<ArticleStoreContext.Provider value={articleStore}>
|
||||||
|
<ServiceStoreContext.Provider value={serviceStore}>
|
||||||
|
<InteractionStoreContext.Provider value={interactionStore}>
|
||||||
|
{children}
|
||||||
|
</InteractionStoreContext.Provider>
|
||||||
|
</ServiceStoreContext.Provider>
|
||||||
|
</ArticleStoreContext.Provider>
|
||||||
|
</RegionStoreContext.Provider>
|
||||||
|
</UserStoreContext.Provider>
|
||||||
|
</AuthStoreContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
authStore,
|
||||||
|
userStore,
|
||||||
|
regionStore,
|
||||||
|
articleStore,
|
||||||
|
serviceStore,
|
||||||
|
interactionStore,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user