diff --git a/config/Request.ts b/config/Request.ts index 9b540b0..548ca40 100644 --- a/config/Request.ts +++ b/config/Request.ts @@ -1,5 +1,4 @@ import { ROUTE_LOGIN } from '@/constants'; -import { apiConfig } from '@/services/ApiConfigService'; import { getToken, removeToken } from '@/utils/localStorageUtils'; import { history, RequestConfig } from '@umijs/max'; import { message } from 'antd'; @@ -85,18 +84,9 @@ export const handleRequestConfig: RequestConfig = { // Request interceptors requestInterceptors: [ (url: string, options: any) => { - console.log('URL Request:', url, options); - - // Nếu URL không phải absolute URL, thêm base URL - let finalUrl = url; - if (!url.startsWith('http')) { - const baseUrl = apiConfig.getApiBaseUrl(); - finalUrl = `${baseUrl}${url}`; - } - const token = getToken(); return { - url: finalUrl, + url, options: { ...options, headers: { diff --git a/config/proxy.ts b/config/proxy.ts index a99bbb4..ea18c12 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -1,59 +1,24 @@ -// Hàm lấy IP từ hostname hiện tại (chỉ hoạt động runtime) -const getCurrentIP = () => { - if (typeof window !== 'undefined') { - const hostname = window.location.hostname; - // Nếu là localhost hoặc IP local, trả về IP mặc định - if ( - hostname === 'localhost' || - hostname.startsWith('192.168.') || - hostname.startsWith('10.') - ) { - console.log('Host name: ', hostname); - - return hostname; - } - // Nếu là domain, có thể cần map sang IP tương ứng - return hostname; - } - return process.env.REACT_APP_API_HOST || '192.168.30.102'; // fallback từ env +const proxy: Record = { + dev: { + '/api': { + target: 'http://192.168.30.103:81', + changeOrigin: true, + }, + }, + test: { + '/test': { + target: 'https://test-sgw-device.gms.vn', + changeOrigin: true, + secure: false, + }, + }, + prod: { + '/test': { + target: 'https://prod-sgw-device.gms.vn', + changeOrigin: true, + secure: false, + }, + }, }; -// Hàm tạo proxy config động -const createDynamicProxy = () => { - const currentIP = getCurrentIP(); - const isLocalIP = - currentIP.startsWith('192.168.') || - currentIP.startsWith('10.') || - currentIP === 'localhost'; - - return { - dev: { - '/api': { - target: `http://${currentIP}:81`, - changeOrigin: true, - }, - }, - test: { - '/test': { - target: isLocalIP - ? `http://${currentIP}:81` - : 'https://test-sgw-device.gms.vn', - changeOrigin: true, - secure: !isLocalIP, - }, - }, - prod: { - '/test': { - target: isLocalIP - ? `http://${currentIP}:81` - : 'https://prod-sgw-device.gms.vn', - changeOrigin: true, - secure: !isLocalIP, - }, - }, - }; -}; - -const proxy: Record = createDynamicProxy(); - export default proxy; diff --git a/src/hooks/useRealtimeGps.ts b/src/hooks/useRealtimeGps.ts new file mode 100644 index 0000000..1e2e07e --- /dev/null +++ b/src/hooks/useRealtimeGps.ts @@ -0,0 +1,19 @@ +// src/hooks/useRealtimeGps.ts +import { useEffect, useState } from 'react'; +import { eventBus } from '@/utils/eventBus'; + +export default function useRealtimeGps() { + const [gpsData, setGpsData] = useState(null); + + useEffect(() => { + const handleGpsUpdate = (data: API.GPSResonse) => { + setGpsData(data); + }; + eventBus.on('gpsData:update', handleGpsUpdate); + + // cleanup khi unmount + return () => eventBus.off('gpsData:update', handleGpsUpdate); + }, []); + + return gpsData; +} diff --git a/src/models/getSos.ts b/src/models/getSos.ts index 209c74a..bf228e2 100644 --- a/src/models/getSos.ts +++ b/src/models/getSos.ts @@ -1,22 +1,43 @@ +// src/models/useGetGpsModel.ts import { getGPS } from '@/services/controller/DeviceController'; -import { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; +import { eventBus } from '@/utils/eventBus'; export default function useGetGpsModel() { const [gpsData, setGpsData] = useState(null); const [loading, setLoading] = useState(false); - const getGPSData = useCallback(async () => { + + const getGPSData = useCallback(async () => { setLoading(true); try { - const res = await getGPS(); // đổi URL cho phù hợp - console.log('GPS Data fetched:', res); + // Bypass cache để GPS luôn realtime, gọi trực tiếp API + const data = await getGPS(); + console.log('GPS fetched from API:', data); - setGpsData(res); + setGpsData(data); + // Luôn emit event GPS để đảm bảo realtime + try { + eventBus.emit('gpsData:update', data); + } catch (e) { + console.warn('Failed to emit gpsData:update event', e); + } } catch (err) { console.error('Fetch gps data failed', err); } finally { setLoading(false); } }, []); + + // ✅ Lắng nghe event cập nhật cache từ nơi khác (nếu có) + useEffect(() => { + const handleUpdate = (data: API.GPSResonse) => { + console.log('GPS cache updated via eventBus'); + setGpsData(data); + }; + eventBus.on('gpsData:update', handleUpdate); + return () => eventBus.off('gpsData:update', handleUpdate); + }, []); + return { gpsData, loading, diff --git a/src/pages/Home/components/BaseMap.tsx b/src/pages/Home/components/BaseMap.tsx index 1042a79..3a0e64a 100644 --- a/src/pages/Home/components/BaseMap.tsx +++ b/src/pages/Home/components/BaseMap.tsx @@ -59,11 +59,6 @@ interface FeatureData { text?: string; } -// Interface for layer object -interface Layer { - layer: VectorLayer; -} - class MapManager { mapRef: React.RefObject; map: Map | null; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 3d506ca..f2ac07a 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -23,12 +23,16 @@ import { InfoOutlined, } from '@ant-design/icons'; import { history, useModel } from '@umijs/max'; +import { eventBus } from '@/utils/eventBus'; import { FloatButton, Popover } from 'antd'; import { useCallback, useEffect, useRef, useState } from 'react'; import GpsInfo from './components/GpsInfo'; import ShipInfo from './components/ShipInfo'; import SosButton from './components/SosButton'; import VietNamMap, { MapManager } from './components/VietNamMap'; +import Point from 'ol/geom/Point'; +import LineString from 'ol/geom/LineString'; +import { fromLonLat } from 'ol/proj'; // // Define missing types locally for now // interface AlarmData { @@ -56,7 +60,11 @@ const HomePage: React.FC = () => { const mapRef = useRef(null); const [isShipInfoOpen, setIsShipInfoOpen] = useState(false); const { gpsData, getGPSData } = useModel('getSos'); - + const hasCenteredRef = useRef(false); + const banzonesCacheRef = useRef([]); + function isSameBanzones(newData: any[], oldData: any[]): boolean { + return JSON.stringify(newData) === JSON.stringify(oldData); + } const onFeatureClick = useCallback((feature: any) => { console.log('OnClick Feature: ', feature); console.log( @@ -90,46 +98,19 @@ const HomePage: React.FC = () => { const [isShowGPSData, setIsShowGPSData] = useState(true); - useEffect(() => { - getGPSData(); - const interval = setInterval(() => { - getGPSData(); - getEntitiesData(mapManagerRef.current, gpsData!); - fetchAndAddTrackPoints(mapManagerRef.current); - fetchAndProcessEntities(mapManagerRef.current); - }, 5000); - return () => { - clearInterval(interval); - console.log('MapManager destroyed in HomePage cleanup'); - }; - }, []); + // Hàm tổng hợp: gọi GPS trước, sau đó gọi các API còn lại + const refreshAllData = async () => { + try { + // Gọi GPS, khi xong sẽ emit event gpsData:update và các API khác sẽ tự động được gọi qua event handler + await getGPSData(); + } catch (e) { + console.error('refreshAllData error', e); + } + }; - useEffect(() => { - const timer = setTimeout(() => { - // getGPSData(); - getEntitiesData(mapManagerRef.current, gpsData!); - fetchAndAddTrackPoints(mapManagerRef.current); - fetchAndProcessEntities(mapManagerRef.current); - }, 1000); // delay 1s - return () => clearTimeout(timer); - }, [gpsData]); - - // Helper function to add features to the map - const addFeatureToMap = ( - mapManager: MapManager, - gpsData: GpsData, - alarm: API.AlarmResponse, - ) => { - console.log( - 'Adding feature to map with GPS data:', - gpsData, - 'and alarm:', - alarm, - ); - const isSos = alarm?.alarms.find( - (a) => a.id === ENTITY_TYPE_ENUM.SOS_WARNING, - ); - console.log('Is SOS Alarm Present:', isSos); + // Helper function to add features to the map (kept above pollers to avoid use-before-define) + function addFeatureToMap(mapManager: MapManager, gpsData: GpsData, alarm: API.AlarmResponse,) { + const isSos = alarm?.alarms.find((a) => a.id === ENTITY_TYPE_ENUM.SOS_WARNING); if (mapManager?.featureLayer && gpsData) { mapManager.featureLayer.getSource()?.clear(); @@ -143,26 +124,158 @@ const HomePage: React.FC = () => { }, ); } - }; + } - const fetchAndProcessEntities = async (mapManager: MapManager) => { + useEffect(() => { + // Chỉ poll GPS, các API còn lại sẽ được gọi khi có GPS mới + getGPSData(); + const gpsInterval = setInterval(() => { + getGPSData(); + }, 5000); // cập nhật mỗi 5s + + // Vẽ marker tàu và polyline ngay khi vào trang nếu đã có dữ liệu + if (gpsData) { + eventBus.emit('ship:update', gpsData); + } + // Nếu có trackpoints, emit luôn (nếu bạn lưu trackpoints ở state, có thể emit ở đây) + // eventBus.emit('trackpoints:update', trackpoints); + + return () => { + clearInterval(gpsInterval); + console.log('MapManager destroyed in HomePage cleanup'); + }; + }, []); + + // Lắng nghe eventBus cho tất cả các loại dữ liệu + useEffect(() => { + // GPS cập nhật => emit ship update ngay lập tức, gọi API khác không đồng bộ + const onGpsUpdate = async (data: API.GPSResonse) => { + try { + if (data) { + // Emit ship update ngay lập tức để marker tàu cập nhật realtime + eventBus.emit('ship:update', data); + + // Gọi các API khác trong background, không block ship update + setTimeout(async () => { + try { + const alarm = await queryAlarms(); + eventBus.emit('alarm:update', alarm); + const trackpoints = await queryShipTrackPoints(); + eventBus.emit('trackpoints:update', trackpoints); + const entities = await queryEntities(); + eventBus.emit('entities:update', entities); + } catch (e) { + console.error('Background API calls error', e); + } + }, 0); + } + } catch (e) { + console.error('onGpsUpdate handler error', e); + } + }; + + + // Cập nhật marker tàu khi có GPS mới + const onShipUpdate = (data: API.GPSResonse) => { + const mapManager = mapManagerRef.current; + if (!mapManager || !data) return; + + // Cache feature trong mapManager + if (!mapManager.shipFeature) { + mapManager.shipFeature = mapManager.addPoint( + [data.lon, data.lat], + { bearing: data.h || 0 }, + { + icon: getShipIcon(0, data?.fishing || false), + scale: 0.1, + animate: false, + }, + ); + } else { + const geom = mapManager.shipFeature.getGeometry(); + if (geom instanceof Point) { + geom.setCoordinates([data.lon, data.lat]); + } + } + + // Set map center to current GPS position + if (!hasCenteredRef.current && mapManager.map && mapManager.map.getView) { + mapManager.map.getView().setCenter(fromLonLat([data.lon, data.lat])); + hasCenteredRef.current = true; + } + + }; + + + // Cập nhật alarm khi có event + const onAlarmUpdate = (alarm: API.AlarmResponse) => { + if (gpsData) addFeatureToMap(mapManagerRef.current, gpsData, alarm); + }; + + // Cập nhật trackpoints khi có event + const onTrackpointsUpdate = (trackpoints: API.ShipTrackPoint[]) => { + const source = mapManagerRef.current?.featureLayer?.getSource(); + if (source && trackpoints.length > 0) { + const features = source.getFeatures(); + // Tìm polyline theo id + const polyline = features.find(f => f.get('id') === MAP_TRACKPOINTS_ID); + const coordinates = trackpoints.map((point) => [point.lon, point.lat]); + if (polyline && polyline.getGeometry() instanceof LineString) { + polyline.getGeometry().setCoordinates(coordinates); + } else { + mapManagerRef.current.addLineString( + coordinates, + { id: MAP_TRACKPOINTS_ID }, + { strokeColor: 'blue', strokeWidth: 3 }, + ); + } + } + }; + + // Cập nhật entities/banzones khi có event + const onEntitiesUpdate = async (entities: API.TransformedEntity[]) => { + const mapManager = mapManagerRef.current; + if (mapManager) { + await drawBanzones(mapManager); + } + }; + + eventBus.on('gpsData:update', onGpsUpdate); + eventBus.on('ship:update', onShipUpdate); + eventBus.on('alarm:update', onAlarmUpdate); + eventBus.on('trackpoints:update', onTrackpointsUpdate); + eventBus.on('entities:update', onEntitiesUpdate); + + return () => { + eventBus.off('gpsData:update', onGpsUpdate); + eventBus.off('ship:update', onShipUpdate); + eventBus.off('alarm:update', onAlarmUpdate); + eventBus.off('trackpoints:update', onTrackpointsUpdate); + eventBus.off('entities:update', onEntitiesUpdate); + }; + }, [gpsData]); + + // Vẽ lại banzones (polygon, polyline vùng cấm, vùng cá thử...) + async function drawBanzones(mapManager: MapManager) { try { - const entities: API.TransformedEntity[] = await queryEntities(); - console.log('Fetched entities:', entities.length); + const layer = mapManager.featureLayer?.getSource(); + layer?.getFeatures()?.forEach(f => { + if ([MAP_POLYGON_BAN, MAP_POLYLINE_BAN].includes(f.get('id'))) { + layer.removeFeature(f); + } + }); + const entities = await queryEntities(); for (const entity of entities) { if (entity.id === '50:2' && entity.valueString !== '[]') { const banzones = await queryBanzones(); const zones: any[] = JSON.parse(entity.valueString); - zones.forEach((zone: any) => { const geom = banzones.find((b) => b.id === zone.zone_id); if (geom) { const { geom_type, geom_lines, geom_poly } = geom.geom || {}; if (geom_type === 2) { - const coordinates = convertWKTLineStringToLatLngArray( - geom_lines || '', - ); + const coordinates = convertWKTLineStringToLatLngArray(geom_lines || ''); if (coordinates.length > 0) { mapManager.addLineString( coordinates, @@ -197,33 +310,17 @@ const HomePage: React.FC = () => { } } } catch (error) { - console.error('Error fetching entities:', error); + console.error('Error drawing banzones:', error); } - }; + } - const fetchAndAddTrackPoints = async (mapManager: MapManager) => { - try { - const trackpoints: API.ShipTrackPoint[] = await queryShipTrackPoints(); - if (trackpoints.length > 0) { - mapManager.addLineString( - trackpoints.map((point) => [point.lon, point.lat]), - { id: MAP_TRACKPOINTS_ID }, - { strokeColor: 'blue', strokeWidth: 3 }, - ); - } - } catch (error) { - console.error('Error fetching ship track points:', error); + useEffect(() => { + // Vẽ lại banzones khi vào trang + const mapManager = mapManagerRef.current; + if (mapManager) { + drawBanzones(mapManager); } - }; - - const getEntitiesData = async (mapManager: MapManager, gpsData: GpsData) => { - try { - const alarm: API.AlarmResponse = await queryAlarms(); - addFeatureToMap(mapManager, gpsData, alarm); - } catch (error) { - console.error('Error fetching entities:', error); - } - }; + }, []); return (
@@ -269,14 +366,9 @@ const HomePage: React.FC = () => {
{ + onRefresh={async (value) => { if (value) { - // Ensure null check for gpsData in all usages - if (gpsData) { - getEntitiesData(mapManagerRef.current, gpsData); - } else { - console.warn('GPS data is null, skipping getEntitiesData call'); - } + await refreshAllData(); } }} /> @@ -286,4 +378,4 @@ const HomePage: React.FC = () => { ); }; -export default HomePage; +export default HomePage; \ No newline at end of file diff --git a/src/services/service/MapService.ts b/src/services/service/MapService.ts index 82a221a..a165a90 100644 --- a/src/services/service/MapService.ts +++ b/src/services/service/MapService.ts @@ -31,7 +31,6 @@ export const osmLayer = new TileLayer({ }); // const layerVN = await getLayer('base-vn'); - export const tinhGeoLayer = createGeoJSONLayer({ data: '', style: createLabelAndFillStyle('#77BEF0', '#000000'), // vừa màu vừa label diff --git a/src/utils/cacheStore.ts b/src/utils/cacheStore.ts new file mode 100644 index 0000000..1baf116 --- /dev/null +++ b/src/utils/cacheStore.ts @@ -0,0 +1,30 @@ +// src/utils/cacheStore.ts +interface CacheItem { + data: T; + timestamp: number; +} + +const TTL = 1 * 1000; // dữ liệu có hiệu lực trong 1 giây để GPS realtime +const cache: Record> = {}; + +export function getCache(key: string): T | null { + const item = cache[key]; + if (!item) return null; + if (Date.now() - item.timestamp > TTL) { + delete cache[key]; + return null; + } + return item.data; +} + +export function setCache(key: string, data: T) { + cache[key] = { data, timestamp: Date.now() }; +} + +export function invalidate(key: string) { + delete cache[key]; +} + +export function getAllCacheKeys(): string[] { + return Object.keys(cache); +} \ No newline at end of file diff --git a/src/utils/eventBus.ts b/src/utils/eventBus.ts new file mode 100644 index 0000000..7a498c6 --- /dev/null +++ b/src/utils/eventBus.ts @@ -0,0 +1,24 @@ +// src/utils/eventBus.ts +type EventHandler = (data: T) => void; + +class EventBus { + private listeners: Record = {}; + + on(event: string, handler: EventHandler) { + if (!this.listeners[event]) this.listeners[event] = []; + this.listeners[event].push(handler); + } + + off(event: string, handler: EventHandler) { + this.listeners[event] = (this.listeners[event] || []).filter( + (h) => h !== handler, + ); + } + + emit(event: string, data?: any) { + (this.listeners[event] || []).forEach((h) => h(data)); + } +} + +// ✅ phải có dòng này +export const eventBus = new EventBus(); diff --git a/src/utils/queryWithCache.ts b/src/utils/queryWithCache.ts new file mode 100644 index 0000000..cbb4126 --- /dev/null +++ b/src/utils/queryWithCache.ts @@ -0,0 +1,27 @@ +// utils/queryWithCache.ts +import { getCache, setCache, invalidate, getAllCacheKeys } from './cacheStore'; +import { eventBus } from './eventBus'; + +export async function queryWithCache( + key: string, + fetcher: () => Promise, +): Promise { + const cached = getCache(key); + if (cached) return cached; + + const data = await fetcher(); + setCache(key, data); + + // Phát sự kiện để các component khác biết có data mới + eventBus.emit(`${key}:update`, data); + + return data; +} + +// Xóa cache theo key hoặc prefix +queryWithCache.clear = (prefix = '') => { + const keys = getAllCacheKeys(); + for (const key of keys) { + if (key.startsWith(prefix)) invalidate(key); + } +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 133cfd8..e0c7e17 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,15 @@ { - "extends": "./src/.umi/tsconfig.json" + "extends": "./src/.umi/tsconfig.json", + "compilerOptions": { + "target": "es2017", + "lib": ["dom", "es2017"], + "module": "esnext", + "moduleResolution": "node", + "jsx": "react-jsx", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] }