feat: add leaflet visitor map if user not wanna register any token

This commit is contained in:
moonrailgun 2024-01-30 22:14:27 +08:00
parent 4ed941b013
commit 3baa1ab55b
8 changed files with 257 additions and 96 deletions

View File

@ -110,6 +110,9 @@ importers:
filesize:
specifier: ^10.0.12
version: 10.0.12
leaflet:
specifier: ^1.9.4
version: 1.9.4
lodash-es:
specifier: ^4.17.21
version: 4.17.21
@ -134,6 +137,9 @@ importers:
react-icons:
specifier: ^4.12.0
version: 4.12.0(react@18.2.0)
react-leaflet:
specifier: ^4.2.1
version: 4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0)
react-resizable:
specifier: ^3.0.5
version: 3.0.5(react-dom@18.2.0)(react@18.2.0)
@ -159,6 +165,9 @@ importers:
specifier: ^4.4.1
version: 4.4.1(@types/react@18.2.21)(react@18.2.0)
devDependencies:
'@types/leaflet':
specifier: ^1.9.8
version: 1.9.8
'@types/loadable__component':
specifier: ^5.13.8
version: 5.13.8
@ -6498,6 +6507,18 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==}
peerDependencies:
leaflet: ^1.9.0
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
leaflet: 1.9.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@reduxjs/toolkit@1.9.7(react-redux@7.2.9)(react@17.0.2):
resolution: {integrity: sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==}
peerDependencies:
@ -8108,7 +8129,6 @@ packages:
/@types/geojson@7946.0.13:
resolution: {integrity: sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==}
dev: false
/@types/geojson@7946.0.8:
resolution: {integrity: sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==}
@ -8189,6 +8209,12 @@ packages:
'@types/node': 18.17.12
dev: false
/@types/leaflet@1.9.8:
resolution: {integrity: sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==}
dependencies:
'@types/geojson': 7946.0.13
dev: true
/@types/loadable__component@5.13.8:
resolution: {integrity: sha512-0FF/WihuPkR5IFOHiBzC95bSACvgQNUQCuNy1WF8F/lCBBHgS2SxarIk4CTjWM10A72ovpmXZDRcuAXZNS+/kQ==}
dependencies:
@ -14335,6 +14361,10 @@ packages:
readable-stream: 2.3.8
dev: true
/leaflet@1.9.4:
resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==}
dev: false
/less@4.2.0:
resolution: {integrity: sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==}
engines: {node: '>=6'}
@ -18895,6 +18925,19 @@ packages:
- encoding
dev: false
/react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==}
peerDependencies:
leaflet: ^1.9.0
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
'@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.2.0)(react@18.2.0)
leaflet: 1.9.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false

View File

@ -1,95 +0,0 @@
import type { LarkMapProps, PointLayerProps } from '@antv/larkmap';
import { FullscreenControl, LarkMap, PointLayer } from '@antv/larkmap';
import React from 'react';
import { useSettingsStore } from '../../store/settings';
import { useGlobalConfig } from '../../hooks/useConfig';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
const layerOptions: Omit<PointLayerProps, 'source'> = {
autoFit: true,
shape: 'circle',
size: 5,
blend: 'additive',
color: {
field: 'count',
value: [
'rgb(102,37,6)',
'rgb(153,52,4)',
'rgb(204,76,2)',
'rgb(236,112,20)',
'rgb(254,153,41)',
'rgb(254,196,79)',
'rgb(254,227,145)',
],
},
};
function useMapConfig(mapType: 'Mapbox' | 'Gaode' = 'Mapbox'): LarkMapProps {
const { amapToken, mapboxToken } = useGlobalConfig();
const colorScheme = useSettingsStore((state) => state.colorScheme);
const baseOption: LarkMapProps['mapOptions'] = {
center: [120.210792, 30.246026],
zoom: 0,
};
if (mapType === 'Gaode') {
return {
mapType: 'Gaode',
mapOptions: {
...baseOption,
style:
colorScheme === 'light'
? 'amap://styles/light'
: 'amap://styles/dark',
token: amapToken,
},
logoVisible: false,
};
} else {
return {
mapType: 'Mapbox',
mapOptions: {
...baseOption,
style: colorScheme === 'light' ? 'light' : 'dark',
token: mapboxToken,
},
logoVisible: false,
};
}
}
interface WebsiteVisitorMapProps {
websiteId: string;
}
export const WebsiteVisitorMap: React.FC<WebsiteVisitorMapProps> = React.memo(
(props) => {
const config = useMapConfig();
const workspaceId = useCurrentWorkspaceId();
const { startDate, endDate } = useGlobalRangeDate();
const startAt = startDate.valueOf();
const endAt = endDate.valueOf();
const { data } = trpc.website.geoStats.useQuery({
workspaceId,
websiteId: props.websiteId,
startAt,
endAt,
});
const source: PointLayerProps['source'] = {
data: data ?? [],
parser: { type: 'json', x: 'longitude', y: 'latitude' },
};
return (
<LarkMap {...config} style={{ height: '60vh' }}>
<FullscreenControl />
<PointLayer {...layerOptions} source={source} />
</LarkMap>
);
}
);
WebsiteVisitorMap.displayName = 'WebsiteVisitorMap';

View File

@ -0,0 +1,86 @@
import {
FullscreenControl,
LarkMap,
LarkMapProps,
PointLayer,
PointLayerProps,
} from '@antv/larkmap';
import React from 'react';
import { AppRouterOutput } from '../../../api/trpc';
import { useGlobalConfig } from '../../../hooks/useConfig';
import { useSettingsStore } from '../../../store/settings';
import { mapCenter } from './utils';
const layerOptions: Omit<PointLayerProps, 'source'> = {
autoFit: true,
shape: 'circle',
size: 5,
blend: 'additive',
color: {
field: 'count',
value: [
'rgb(102,37,6)',
'rgb(153,52,4)',
'rgb(204,76,2)',
'rgb(236,112,20)',
'rgb(254,153,41)',
'rgb(254,196,79)',
'rgb(254,227,145)',
],
},
};
function useMapConfig(mapType: 'Mapbox' | 'Gaode' = 'Mapbox'): LarkMapProps {
const { amapToken, mapboxToken } = useGlobalConfig();
const colorScheme = useSettingsStore((state) => state.colorScheme);
const baseOption: LarkMapProps['mapOptions'] = {
center: [mapCenter.lng, mapCenter.lat],
zoom: 0,
};
if (mapType === 'Gaode') {
return {
mapType: 'Gaode',
mapOptions: {
...baseOption,
style:
colorScheme === 'light'
? 'amap://styles/light'
: 'amap://styles/dark',
token: amapToken,
},
logoVisible: false,
};
} else {
return {
mapType: 'Mapbox',
mapOptions: {
...baseOption,
style: colorScheme === 'light' ? 'light' : 'dark',
token: mapboxToken,
},
logoVisible: false,
};
}
}
export const VisitorLarkMap: React.FC<{
data: AppRouterOutput['website']['geoStats'];
mapType: 'Mapbox' | 'Gaode';
}> = React.memo((props) => {
const config = useMapConfig(props.mapType);
const source: PointLayerProps['source'] = {
data: props.data ?? [],
parser: { type: 'json', x: 'longitude', y: 'latitude' },
};
return (
<LarkMap {...config} style={{ height: '60vh' }}>
<FullscreenControl />
<PointLayer {...layerOptions} source={source} />
</LarkMap>
);
});
VisitorLarkMap.displayName = 'VisitorLarkMap';

View File

@ -0,0 +1,3 @@
.leaflet-control-attribution.leaflet-control {
display: none;
}

View File

@ -0,0 +1,59 @@
import React from 'react';
import { AppRouterOutput } from '../../../api/trpc';
import {
MapContainer,
CircleMarker,
Popup,
TileLayer,
useMap,
} from 'react-leaflet';
import { mapCenter } from './utils';
import 'leaflet/dist/leaflet.css';
import './VisitorLeafletMap.css';
export const UserDataPoint: React.FC<{
longitude: number;
latitude: number;
count: number;
}> = React.memo((props) => {
const map = useMap();
return (
<CircleMarker
center={{
lat: props.latitude,
lng: props.longitude,
}}
radius={5}
stroke={false}
fill={true}
fillColor="rgb(236,112,20)"
fillOpacity={0.8}
>
<Popup>{props.count} users</Popup>
</CircleMarker>
);
});
UserDataPoint.displayName = 'UserDataPoint';
export const VisitorLeafletMap: React.FC<{
data: AppRouterOutput['website']['geoStats'];
}> = React.memo((props) => {
return (
<MapContainer
className="w-full h-[60vh]"
center={mapCenter}
zoom={2}
minZoom={2}
maxZoom={10}
scrollWheelZoom={true}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{props.data.map((item) => (
<UserDataPoint key={`${item.longitude},${item.latitude}`} {...item} />
))}
</MapContainer>
);
});
VisitorLeafletMap.displayName = 'VisitorLeafletMap';

View File

@ -0,0 +1,58 @@
import React from 'react';
import { trpc } from '../../../api/trpc';
import { useCurrentWorkspaceId } from '../../../store/user';
import { useGlobalRangeDate } from '../../../hooks/useGlobalRangeDate';
import loadable from '@loadable/component';
import { Loading } from '../../Loading';
import { useGlobalConfig } from '../../../hooks/useConfig';
const VisitorLeafletMap = loadable(() =>
import('./VisitorLeafletMap').then((m) => m.VisitorLeafletMap)
);
const VisitorLarkMap = loadable(() =>
import('./VisitorLarkMap').then((m) => m.VisitorLarkMap)
);
function useMapType() {
const { amapToken, mapboxToken } = useGlobalConfig();
if (mapboxToken) {
return 'Mapbox';
} else if (amapToken) {
return 'Gaode';
} else {
return 'Leaflet';
}
}
interface WebsiteVisitorMapProps {
websiteId: string;
}
export const WebsiteVisitorMap: React.FC<WebsiteVisitorMapProps> = React.memo(
(props) => {
const workspaceId = useCurrentWorkspaceId();
const { startDate, endDate } = useGlobalRangeDate();
const startAt = startDate.valueOf();
const endAt = endDate.valueOf();
const { data } = trpc.website.geoStats.useQuery({
workspaceId,
websiteId: props.websiteId,
startAt,
endAt,
});
const mapType = useMapType();
if (!data) {
return <Loading />;
}
if (mapType === 'Leaflet') {
return <VisitorLeafletMap data={data} />;
}
return <VisitorLarkMap mapType={mapType} data={data} />;
}
);
WebsiteVisitorMap.displayName = 'WebsiteVisitorMap';

View File

@ -0,0 +1,4 @@
export const mapCenter = {
lat: 30.246026,
lng: 120.210792,
};

View File

@ -32,6 +32,7 @@
"dayjs": "^1.11.9",
"eventemitter-strict": "^1.0.1",
"filesize": "^10.0.12",
"leaflet": "^1.9.4",
"lodash-es": "^4.17.21",
"millify": "^6.1.0",
"pretty-ms": "^9.0.0",
@ -40,6 +41,7 @@
"react-easy-sort": "^1.5.3",
"react-grid-layout": "1.4.2",
"react-icons": "^4.12.0",
"react-leaflet": "^4.2.1",
"react-resizable": "^3.0.5",
"react-router": "^6.15.0",
"react-router-dom": "^6.15.0",
@ -50,6 +52,7 @@
"zustand": "^4.4.1"
},
"devDependencies": {
"@types/leaflet": "^1.9.8",
"@types/loadable__component": "^5.13.8",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.21",