feat(地区管理): 新增地区地图功能并重构地区列表

- 添加地区地图页面,使用 echarts 展示世界地图和地区分布
- 重构地区列表页面,移除经纬度字段,改为使用国家代码
- 更新依赖版本并添加 i18n-iso-countries 用于国家代码转换
- 配置代理设置以支持 API 请求
This commit is contained in:
tikkhun 2025-12-01 23:55:26 +08:00
parent 07fca92ef3
commit accb93bf16
5 changed files with 178 additions and 37 deletions

View File

@ -71,14 +71,19 @@ export default defineConfig({
],
},
{
name: '地管理',
name: '地管理',
path: '/area',
access: 'canSeeArea',
routes: [
{
name: '地列表',
name: '地列表',
path: '/area/list',
component: './Area/List',
},
{
name: '地区地图',
path: '/area/map',
component: './Area/Map',
},
],
},
@ -265,5 +270,12 @@ export default defineConfig({
// component: './404',
// },
],
proxy: {
'/api': {
target: UMI_APP_API_URL,
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
npmClient: 'pnpm',
});

View File

@ -22,9 +22,10 @@
"@umijs/plugin-openapi": "^1.3.3",
"antd": "^5.4.0",
"dayjs": "^1.11.9",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"file-saver": "^2.0.5",
"i18n-iso-countries": "^7.14.0",
"print-js": "^1.6.0",
"react-phone-input-2": "^2.15.1",
"react-toastify": "^11.0.5",

1
public/world.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@ import {
DrawerForm,
ProColumns,
ProFormInstance,
ProFormText,
ProTable,
ProFormSelect,
ProTable
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import { Button, message, Popconfirm, Space } from 'antd';
@ -14,8 +14,12 @@ import React, { useEffect, useRef, useState } from 'react';
interface AreaItem {
id: number;
name: string;
latitude?: number;
longitude?: number;
code: string;
}
interface Country {
code: string;
name: string;
}
const AreaList: React.FC = () => {
@ -23,6 +27,7 @@ const AreaList: React.FC = () => {
const formRef = useRef<ProFormInstance>();
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<AreaItem | null>(null);
const [countries, setCountries] = useState<Country[]>([]);
useEffect(() => {
if (!open) return;
@ -33,6 +38,20 @@ const AreaList: React.FC = () => {
}
}, [open, editing]);
useEffect(() => {
const fetchCountries = async () => {
try {
const resp = await request('/area/countries', { method: 'GET' });
const { success, data, message: errMsg } = resp as any;
if (!success) throw new Error(errMsg || '获取国家列表失败');
setCountries(data || []);
} catch (e: any) {
message.error(e.message || '获取国家列表失败');
}
};
fetchCountries();
}, []);
const columns: ProColumns<AreaItem>[] = [
{
title: 'ID',
@ -42,18 +61,7 @@ const AreaList: React.FC = () => {
hideInSearch: true,
},
{ title: '名称', dataIndex: 'name', width: 220 },
{
title: '纬度',
dataIndex: 'latitude',
width: 160,
hideInSearch: true,
},
{
title: '经度',
dataIndex: 'longitude',
width: 160,
hideInSearch: true,
},
{ title: '编码', dataIndex: 'code', width: 160 },
{
title: '操作',
dataIndex: 'actions',
@ -96,13 +104,13 @@ const AreaList: React.FC = () => {
const tableRequest = async (params: Record<string, any>) => {
try {
const { current = 1, pageSize = 10, name } = params;
const { current = 1, pageSize = 10, keyword } = params;
const resp = await request('/area', {
method: 'GET',
params: {
currentPage: current,
pageSize,
name: name || undefined,
keyword: keyword || undefined,
},
});
const { success, data, message: errMsg } = resp as any;
@ -170,21 +178,13 @@ const AreaList: React.FC = () => {
formRef={formRef}
onFinish={handleSubmit}
>
<ProFormText
name="name"
label="区域名称"
placeholder="例如Australia"
rules={[{ required: true, message: '区域名称为必填项' }]}
/>
<ProFormText
name="latitude"
label="纬度"
placeholder="例如:-33.8688"
/>
<ProFormText
name="longitude"
label="经度"
placeholder="例如151.2093"
<ProFormSelect
name="code"
label="国家/地区"
options={countries.map(c => ({ label: c.name, value: c.code }))}
placeholder="请选择国家/地区"
rules={[{ required: true, message: '国家/地区为必填项' }]}
showSearch
/>
</DrawerForm>
</>

View File

@ -0,0 +1,127 @@
import React, { useEffect, useState } from 'react';
import ReactECharts from 'echarts-for-react';
import { request } from '@umijs/max';
import { Spin, message } from 'antd';
import * as echarts from 'echarts/core';
import { MapChart } from 'echarts/charts';
import { TooltipComponent, VisualMapComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import * as countries from 'i18n-iso-countries';
// 注册 ECharts 组件
echarts.use([TooltipComponent, VisualMapComponent, MapChart, CanvasRenderer]);
// 注册 i18n-iso-countries 语言包
countries.registerLocale(require('i18n-iso-countries/langs/en.json'));
interface AreaItem {
id: number;
name: string; // 中文名
code: string; // 国家代码
}
const AreaMap: React.FC = () => {
const [option, setOption] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAndSetMapData = async () => {
try {
// 1. 动态加载 world.json 地图数据
const worldMapResponse = await fetch('/world.json');
const worldMap = await worldMapResponse.json();
echarts.registerMap('world', worldMap);
// 2. 从后端获取已存储的区域列表
const areaResponse = await request('/area', {
method: 'GET',
params: {
currentPage: 1,
pageSize: 9999,
},
});
if (!areaResponse.success) {
throw new Error(areaResponse.message || '获取区域列表失败');
}
const savedAreas: AreaItem[] = areaResponse.data?.list || [];
// 3. 将后端数据转换为 ECharts 需要的格式
const mapData = savedAreas.map(area => {
let nameEn = countries.getName(area.code, 'en');
return {
name: nameEn || area.code,
value: 1,
chineseName: area.name,
};
});
// 4. 配置 ECharts 地图选项
const mapOption = {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
if (params.data && params.data.chineseName) {
return `${params.data.chineseName}`;
}
return `${params.name}`;
},
},
visualMap: {
left: 'left',
min: 0,
max: 1,
inRange: {
color: ['#f0f0f0', '#1890ff'],
},
calculable: false,
show: false,
},
series: [
{
name: 'World Map',
type: 'map',
map: 'world',
roam: true,
emphasis: {
label: {
show: false,
},
itemStyle: {
areaColor: '#ffc107',
},
},
data: mapData,
},
],
};
setOption(mapOption);
} catch (error: any) {
message.error(`加载地图数据失败: ${error.message}`);
} finally {
setLoading(false);
}
};
fetchAndSetMapData();
}, []);
if (loading) {
return <Spin size="large" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }} />;
}
return (
<ReactECharts
echarts={echarts}
option={option}
style={{ height: '80vh', width: '100%' }}
notMerge={true}
lazyUpdate={true}
/>
);
};
export default AreaMap;