feat(地区管理): 新增地区地图功能并重构地区列表
- 添加地区地图页面,使用 echarts 展示世界地图和地区分布 - 重构地区列表页面,移除经纬度字段,改为使用国家代码 - 更新依赖版本并添加 i18n-iso-countries 用于国家代码转换 - 配置代理设置以支持 API 请求
This commit is contained in:
parent
07fca92ef3
commit
accb93bf16
16
.umirc.ts
16
.umirc.ts
|
|
@ -71,14 +71,19 @@ export default defineConfig({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '地点管理',
|
name: '地区管理',
|
||||||
path: '/area',
|
path: '/area',
|
||||||
access: 'canSeeArea',
|
access: 'canSeeArea',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
name: '地点列表',
|
name: '地区列表',
|
||||||
path: '/area/list',
|
path: '/area/list',
|
||||||
component: './Area/List',
|
component: './Area/List',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '地区地图',
|
||||||
|
path: '/area/map',
|
||||||
|
component: './Area/Map',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -265,5 +270,12 @@ export default defineConfig({
|
||||||
// component: './404',
|
// component: './404',
|
||||||
// },
|
// },
|
||||||
],
|
],
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: UMI_APP_API_URL,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: { '^/api': '' },
|
||||||
|
},
|
||||||
|
},
|
||||||
npmClient: 'pnpm',
|
npmClient: 'pnpm',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@
|
||||||
"@umijs/plugin-openapi": "^1.3.3",
|
"@umijs/plugin-openapi": "^1.3.3",
|
||||||
"antd": "^5.4.0",
|
"antd": "^5.4.0",
|
||||||
"dayjs": "^1.11.9",
|
"dayjs": "^1.11.9",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^6.0.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.5",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"react-phone-input-2": "^2.15.1",
|
"react-phone-input-2": "^2.15.1",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@ import {
|
||||||
DrawerForm,
|
DrawerForm,
|
||||||
ProColumns,
|
ProColumns,
|
||||||
ProFormInstance,
|
ProFormInstance,
|
||||||
ProFormText,
|
ProFormSelect,
|
||||||
ProTable,
|
ProTable
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
import { Button, message, Popconfirm, Space } from 'antd';
|
import { Button, message, Popconfirm, Space } from 'antd';
|
||||||
|
|
@ -14,8 +14,12 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||||
interface AreaItem {
|
interface AreaItem {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
latitude?: number;
|
code: string;
|
||||||
longitude?: number;
|
}
|
||||||
|
|
||||||
|
interface Country {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AreaList: React.FC = () => {
|
const AreaList: React.FC = () => {
|
||||||
|
|
@ -23,6 +27,7 @@ const AreaList: React.FC = () => {
|
||||||
const formRef = useRef<ProFormInstance>();
|
const formRef = useRef<ProFormInstance>();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<AreaItem | null>(null);
|
const [editing, setEditing] = useState<AreaItem | null>(null);
|
||||||
|
const [countries, setCountries] = useState<Country[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
@ -33,6 +38,20 @@ const AreaList: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [open, editing]);
|
}, [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>[] = [
|
const columns: ProColumns<AreaItem>[] = [
|
||||||
{
|
{
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
|
|
@ -42,18 +61,7 @@ const AreaList: React.FC = () => {
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{ title: '名称', dataIndex: 'name', width: 220 },
|
{ title: '名称', dataIndex: 'name', width: 220 },
|
||||||
{
|
{ title: '编码', dataIndex: 'code', width: 160 },
|
||||||
title: '纬度',
|
|
||||||
dataIndex: 'latitude',
|
|
||||||
width: 160,
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '经度',
|
|
||||||
dataIndex: 'longitude',
|
|
||||||
width: 160,
|
|
||||||
hideInSearch: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
dataIndex: 'actions',
|
dataIndex: 'actions',
|
||||||
|
|
@ -96,13 +104,13 @@ const AreaList: React.FC = () => {
|
||||||
|
|
||||||
const tableRequest = async (params: Record<string, any>) => {
|
const tableRequest = async (params: Record<string, any>) => {
|
||||||
try {
|
try {
|
||||||
const { current = 1, pageSize = 10, name } = params;
|
const { current = 1, pageSize = 10, keyword } = params;
|
||||||
const resp = await request('/area', {
|
const resp = await request('/area', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: {
|
params: {
|
||||||
currentPage: current,
|
currentPage: current,
|
||||||
pageSize,
|
pageSize,
|
||||||
name: name || undefined,
|
keyword: keyword || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { success, data, message: errMsg } = resp as any;
|
const { success, data, message: errMsg } = resp as any;
|
||||||
|
|
@ -170,21 +178,13 @@ const AreaList: React.FC = () => {
|
||||||
formRef={formRef}
|
formRef={formRef}
|
||||||
onFinish={handleSubmit}
|
onFinish={handleSubmit}
|
||||||
>
|
>
|
||||||
<ProFormText
|
<ProFormSelect
|
||||||
name="name"
|
name="code"
|
||||||
label="区域名称"
|
label="国家/地区"
|
||||||
placeholder="例如:Australia"
|
options={countries.map(c => ({ label: c.name, value: c.code }))}
|
||||||
rules={[{ required: true, message: '区域名称为必填项' }]}
|
placeholder="请选择国家/地区"
|
||||||
/>
|
rules={[{ required: true, message: '国家/地区为必填项' }]}
|
||||||
<ProFormText
|
showSearch
|
||||||
name="latitude"
|
|
||||||
label="纬度"
|
|
||||||
placeholder="例如:-33.8688"
|
|
||||||
/>
|
|
||||||
<ProFormText
|
|
||||||
name="longitude"
|
|
||||||
label="经度"
|
|
||||||
placeholder="例如:151.2093"
|
|
||||||
/>
|
/>
|
||||||
</DrawerForm>
|
</DrawerForm>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue