Compare commits
5 Commits
904028649c
...
device_sgw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28739ddcd9 | ||
|
|
53dfb861dd | ||
|
|
076d0460cb | ||
|
|
b44f1a9b29 | ||
|
|
ef353862e4 |
@@ -1,6 +1,5 @@
|
|||||||
import { defineConfig } from '@umijs/max';
|
import { defineConfig } from '@umijs/max';
|
||||||
import proxy from './config/proxy';
|
import proxy from './config/proxy';
|
||||||
|
|
||||||
const { REACT_APP_ENV = 'dev' } = process.env as {
|
const { REACT_APP_ENV = 'dev' } = process.env as {
|
||||||
REACT_APP_ENV: 'dev' | 'test' | 'prod';
|
REACT_APP_ENV: 'dev' | 'test' | 'prod';
|
||||||
};
|
};
|
||||||
@@ -14,6 +13,7 @@ export default defineConfig({
|
|||||||
locale: {
|
locale: {
|
||||||
default: 'vi-VN',
|
default: 'vi-VN',
|
||||||
},
|
},
|
||||||
|
favicons: ['/logo.png'],
|
||||||
layout: {
|
layout: {
|
||||||
title: '2025 Sản phẩm của Mobifone v1.0',
|
title: '2025 Sản phẩm của Mobifone v1.0',
|
||||||
},
|
},
|
||||||
|
|||||||
1
public/avatar.svg
Normal file
1
public/avatar.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679201365371" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1897" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M509.31257004 540.79389452h4.91415801c44.99525866-0.76783723 81.39074114-16.58528313 108.26504222-46.83806846 59.12346278-66.6482671 49.29514673-180.9024399 48.2201749-191.80572705-3.83918618-81.85144307-42.53817965-121.01113989-74.48020676-139.28566479C572.42878628 149.19693275 544.63307997 141.82569571 513.61245812 141.21142578H511.00181181c-17.04598575 0-50.52368662 2.76421361-82.61928101 21.03873851-32.24916171 18.27452489-71.56242513 57.43422101-75.40161058 139.89993473-1.07497185 10.90328787-10.90328787 125.15746067 48.22017491 191.80572704 26.72073376 30.2527846 63.11621625 46.07023049 108.11147491 46.83806846z m-115.32914464-234.80461006c0-0.46070263 0.15356731-0.92140454 0.15356731-1.22853914 5.06772532-110.10785129 83.23355022-121.93254442 116.7112518-121.93254443H512.69105358c41.4632078 0.92140454 111.95066108 17.81382227 116.71125108 121.93254443 0 0.46070263 0 0.92140454 0.15356731 1.22853914 0.15356731 1.07497185 10.90328787 105.50082861-37.93115624 160.47797059-19.34949674 21.80657575-45.14882595 32.55629632-79.08722946 32.86343166h-1.53567446c-33.78483618-0.30713461-59.73773271-11.05685517-78.93366214-32.86343165-48.68087753-54.67000739-38.23829157-159.55656606-38.08472427-160.4779706z" p-id="1898"></path><path d="M827.3507296 730.29611058v-0.46070263c0-1.22853915-0.15356731-2.457079-0.15356731-3.83918618-0.92140454-30.40635263-2.91778164-101.50807511-69.56604874-124.23605539-0.46070263-0.15356731-1.07497185-0.30713461-1.53567375-0.46070264-69.25891341-17.66025497-126.84670245-57.5877883-127.46097237-58.04849021-9.3676134-6.60339979-22.26727838-4.29988808-28.87067744 5.06772532-6.60339979 9.3676134-4.29988808 22.26727838 5.0677253 28.87067743 2.6106463 1.84280907 63.73048619 44.38098873 140.20706862 64.03762079 35.78121256 12.74609694 39.77396603 50.98438852 40.84893787 85.99776458 0 1.38210717 0 2.6106463 0.15356802 3.83918545 0.15356731 13.82106951-0.76783723 35.16694264-3.22491624 47.45233838-24.87792469 14.12820412-122.39324633 62.96264895-270.73938991 62.96264823-147.73187366 0-245.86146522-48.98801213-270.89295721-63.11621625-2.457079-12.28539504-3.53205084-33.63126816-3.22491624-47.45233767 0-1.22853915 0.15356731-2.457079 0.15356802-3.83918545 1.07497185-35.01337533 5.06772532-73.25166689 40.84893786-85.99776457 76.47658314-19.65663206 137.596423-62.34837901 140.20706862-64.03762079 9.3676134-6.60339979 11.67112511-19.50306404 5.06772531-28.87067744-6.60339979-9.3676134-19.50306404-11.67112511-28.87067745-5.06772605-0.61426993 0.46070263-57.89492363 40.38823598-127.46097236 58.04849094-0.61426993 0.15356731-1.07497185 0.30713461-1.53567375 0.46070264-66.6482671 22.88154832-68.64464421 93.9832708-69.56604874 124.2360554 0 1.38210717 0 2.6106463-0.15356731 3.83918617v0.46070191c-0.15356731 7.98550697-0.30713461 48.98801213 7.83193893 69.56604875 1.53567446 3.99275348 4.29988808 7.37123702 7.98550697 9.67474873 4.60702342 3.07134893 115.02200931 73.4052342 299.76363465 73.40523419s295.15661197-70.48745329 299.76363538-73.40523419c3.53205084-2.3035117 6.44983249-5.68199525 7.98550624-9.67474873 7.67837163-20.4244693 7.52480433-61.42697448 7.37123703-69.41248072z" p-id="1899"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
38
src/app.tsx
38
src/app.tsx
@@ -1,8 +1,9 @@
|
|||||||
|
import { LogoutOutlined } from '@ant-design/icons';
|
||||||
import { history, RunTimeLayoutConfig } from '@umijs/max';
|
import { history, RunTimeLayoutConfig } from '@umijs/max';
|
||||||
|
import { Dropdown } from 'antd';
|
||||||
import { handleRequestConfig } from '../config/Request';
|
import { handleRequestConfig } from '../config/Request';
|
||||||
import logo from '../public/logo.png';
|
import logo from '../public/logo.png';
|
||||||
import UnAccessPage from './components/403/403Page';
|
import UnAccessPage from './components/403/403Page';
|
||||||
import Footer from './components/Footer/Footer';
|
|
||||||
import { ROUTE_LOGIN } from './constants';
|
import { ROUTE_LOGIN } from './constants';
|
||||||
import { parseJwt } from './utils/jwtTokenUtils';
|
import { parseJwt } from './utils/jwtTokenUtils';
|
||||||
import { getToken, removeToken } from './utils/localStorageUtils';
|
import { getToken, removeToken } from './utils/localStorageUtils';
|
||||||
@@ -69,13 +70,40 @@ export const layout: RunTimeLayoutConfig = (initialState) => {
|
|||||||
},
|
},
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
paddingInline: 0,
|
||||||
|
},
|
||||||
|
avatarProps: {
|
||||||
|
size: 'small',
|
||||||
|
src: '/avatar.svg', // 👈 ở đây dùng icon thay vì src
|
||||||
|
render: (_, dom) => {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'logout',
|
||||||
|
icon: <LogoutOutlined />,
|
||||||
|
label: 'Đăng xuất',
|
||||||
|
onClick: () => {
|
||||||
|
removeToken();
|
||||||
|
history.push(ROUTE_LOGIN);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dom}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
layout: 'mix',
|
layout: 'mix',
|
||||||
logout: () => {
|
logout: () => {
|
||||||
removeToken();
|
removeToken();
|
||||||
history.push(ROUTE_LOGIN);
|
history.push(ROUTE_LOGIN);
|
||||||
},
|
},
|
||||||
footerRender: () => <Footer />,
|
// footerRender: () => <Footer />,
|
||||||
onPageChange: () => {
|
onPageChange: () => {
|
||||||
if (!initialState.initialState) {
|
if (!initialState.initialState) {
|
||||||
history.push(ROUTE_LOGIN);
|
history.push(ROUTE_LOGIN);
|
||||||
@@ -103,6 +131,12 @@ export const layout: RunTimeLayoutConfig = (initialState) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
unAccessible: <UnAccessPage />,
|
unAccessible: <UnAccessPage />,
|
||||||
|
token: {
|
||||||
|
pageContainer: {
|
||||||
|
paddingInlinePageContainerContent: 0,
|
||||||
|
paddingBlockPageContainerContent: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { DefaultFooter } from '@ant-design/pro-components';
|
import { DefaultFooter } from '@ant-design/pro-components';
|
||||||
|
import './style.less';
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
return <DefaultFooter copyright="2025 Sản phẩm của Mobifone v1.0" />;
|
return (
|
||||||
|
<DefaultFooter
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
copyright="2025 Sản phẩm của Mobifone v1.2"
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|||||||
3
src/components/Footer/style.less
Normal file
3
src/components/Footer/style.less
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.ant-pro-global-footer-copyright {
|
||||||
|
color: white !important; /* hoặc mã màu bạn muốn */
|
||||||
|
}
|
||||||
@@ -6,3 +6,7 @@ export enum STATUS {
|
|||||||
UPDATE_FISHING_LOG_SUCCESS = 'UPDATE_FISHING_LOG_SUCCESS',
|
UPDATE_FISHING_LOG_SUCCESS = 'UPDATE_FISHING_LOG_SUCCESS',
|
||||||
UPDATE_FISHING_LOG_FAIL = 'UPDATE_FISHING_LOG_FAIL',
|
UPDATE_FISHING_LOG_FAIL = 'UPDATE_FISHING_LOG_FAIL',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ENTITY_TYPE_ENUM {
|
||||||
|
SOS_WARNING = '50:15',
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
export const DEFAULT_NAME = 'Umi Max';
|
export const DEFAULT_NAME = 'Umi Max';
|
||||||
export const TOKEN = 'token';
|
export const TOKEN = 'token';
|
||||||
export const BASE_URL = 'https://sgw-device.gms.vn';
|
export const BASE_URL = 'https://sgw-device.gms.vn';
|
||||||
|
export const MAP_TRACKPOINTS_ID = 'ship-trackpoints';
|
||||||
|
export const MAP_POLYLINE_BAN = 'ban-polyline';
|
||||||
|
export const MAP_POLYGON_BAN = 'ban-polygon';
|
||||||
|
|
||||||
// Global Constants
|
// Global Constants
|
||||||
|
|
||||||
// Route Constants
|
// Route Constants
|
||||||
@@ -22,3 +26,5 @@ export const API_GET_GPS = '/api/sgw/gps';
|
|||||||
export const API_GET_FISH = '/api/sgw/fishspecies';
|
export const API_GET_FISH = '/api/sgw/fishspecies';
|
||||||
export const API_UPDATE_FISHING_LOGS = '/api/sgw/fishingLog';
|
export const API_UPDATE_FISHING_LOGS = '/api/sgw/fishingLog';
|
||||||
export const API_SOS = '/api/sgw/sos';
|
export const API_SOS = '/api/sgw/sos';
|
||||||
|
export const API_PATH_SHIP_TRACK_POINTS = '/api/sgw/trackpoints';
|
||||||
|
export const API_GET_ALL_BANZONES = '/api/sgw/banzones';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function useGetGpsModel() {
|
|||||||
const res = await getGPS(); // đổi URL cho phù hợp
|
const res = await getGPS(); // đổi URL cho phù hợp
|
||||||
console.log('GPS Data fetched:', res);
|
console.log('GPS Data fetched:', res);
|
||||||
|
|
||||||
setGpsData(res || []);
|
setGpsData(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fetch gps data failed', err);
|
console.error('Fetch gps data failed', err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Footer from '@/components/Footer/Footer';
|
||||||
import { ROUTE_HOME } from '@/constants';
|
import { ROUTE_HOME } from '@/constants';
|
||||||
import { login } from '@/services/controller/AuthController';
|
import { login } from '@/services/controller/AuthController';
|
||||||
import { parseJwt } from '@/utils/jwtTokenUtils';
|
import { parseJwt } from '@/utils/jwtTokenUtils';
|
||||||
@@ -127,6 +128,17 @@ const LoginPage = () => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</LoginFormPage>
|
</LoginFormPage>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 99,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,16 +7,21 @@ import {
|
|||||||
} from '@/utils/mapUtils';
|
} from '@/utils/mapUtils';
|
||||||
import { Feature, Map, View } from 'ol';
|
import { Feature, Map, View } from 'ol';
|
||||||
import { Coordinate } from 'ol/coordinate';
|
import { Coordinate } from 'ol/coordinate';
|
||||||
|
import { easeOut } from 'ol/easing';
|
||||||
|
import { EventsKey } from 'ol/events';
|
||||||
import { createEmpty, extend, Extent, isEmpty } from 'ol/extent';
|
import { createEmpty, extend, Extent, isEmpty } from 'ol/extent';
|
||||||
import { MultiPolygon, Point, Polygon } from 'ol/geom';
|
import { LineString, MultiPolygon, Point, Polygon } from 'ol/geom';
|
||||||
import BaseLayer from 'ol/layer/Base';
|
import BaseLayer from 'ol/layer/Base';
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
|
import { unByKey } from 'ol/Observable';
|
||||||
import { fromLonLat } from 'ol/proj';
|
import { fromLonLat } from 'ol/proj';
|
||||||
|
import { getVectorContext } from 'ol/render';
|
||||||
|
import RenderEvent from 'ol/render/Event';
|
||||||
import VectorSource from 'ol/source/Vector';
|
import VectorSource from 'ol/source/Vector';
|
||||||
|
import { Circle as CircleStyle, Style } from 'ol/style';
|
||||||
import Fill from 'ol/style/Fill';
|
import Fill from 'ol/style/Fill';
|
||||||
import Icon from 'ol/style/Icon';
|
import Icon from 'ol/style/Icon';
|
||||||
import Stroke from 'ol/style/Stroke';
|
import Stroke from 'ol/style/Stroke';
|
||||||
import Style from 'ol/style/Style';
|
|
||||||
import Text from 'ol/style/Text';
|
import Text from 'ol/style/Text';
|
||||||
|
|
||||||
interface MapManagerConfig {
|
interface MapManagerConfig {
|
||||||
@@ -25,6 +30,12 @@ interface MapManagerConfig {
|
|||||||
onFeaturesClick?: (features: any[]) => void;
|
onFeaturesClick?: (features: any[]) => void;
|
||||||
onError?: (error: string[]) => void;
|
onError?: (error: string[]) => void;
|
||||||
}
|
}
|
||||||
|
interface AnimationConfig {
|
||||||
|
duration?: number;
|
||||||
|
maxRadius?: number;
|
||||||
|
strokeColor?: string;
|
||||||
|
strokeWidthBase?: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface StyleConfig {
|
interface StyleConfig {
|
||||||
icon?: string; // URL của icon
|
icon?: string; // URL của icon
|
||||||
@@ -32,10 +43,18 @@ interface StyleConfig {
|
|||||||
textColor?: string; // màu chữ
|
textColor?: string; // màu chữ
|
||||||
textStrokeColor?: string; // màu viền chữ
|
textStrokeColor?: string; // màu viền chữ
|
||||||
textOffsetY?: number; // độ lệch theo trục Y
|
textOffsetY?: number; // độ lệch theo trục Y
|
||||||
|
strokeColor?: string; // màu đường kẻ cho LineString
|
||||||
|
strokeWidth?: number; // độ dày đường kẻ cho LineString
|
||||||
|
fillColor?: string; // màu fill cho Polygon
|
||||||
|
borderColor?: string; // màu viền cho Polygon
|
||||||
|
borderWidth?: number; // độ dày viền cho Polygon
|
||||||
|
minScale?: number; // tỷ lệ nhỏ nhất
|
||||||
|
maxScale?: number; // tỷ lệ lớn nhất
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface for feature data
|
// Interface for feature data
|
||||||
interface FeatureData {
|
interface FeatureData {
|
||||||
|
id?: string | number;
|
||||||
bearing?: number;
|
bearing?: number;
|
||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
@@ -58,6 +77,7 @@ class MapManager {
|
|||||||
private errors: string[];
|
private errors: string[];
|
||||||
private layers: BaseLayer[]; // Assuming layers is defined elsewhere
|
private layers: BaseLayer[]; // Assuming layers is defined elsewhere
|
||||||
private isInitialized: boolean; // Thêm thuộc tính để theo dõi trạng thái khởi tạo
|
private isInitialized: boolean; // Thêm thuộc tính để theo dõi trạng thái khởi tạo
|
||||||
|
private eventKey: EventsKey | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
mapRef: React.RefObject<HTMLDivElement>,
|
mapRef: React.RefObject<HTMLDivElement>,
|
||||||
@@ -75,11 +95,12 @@ class MapManager {
|
|||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.layers = []; // Initialize layers (adjust based on actual usage)
|
this.layers = []; // Initialize layers (adjust based on actual usage)
|
||||||
this.isInitialized = false; // Khởi tạo là false
|
this.isInitialized = false; // Khởi tạo là false
|
||||||
|
this.eventKey = undefined; // Khởi tạo eventKey
|
||||||
}
|
}
|
||||||
|
|
||||||
async getListLayers(): Promise<[]> {
|
async getListLayers(): Promise<[]> {
|
||||||
const resp: [] = await getAllLayer();
|
const resp: [] = await getAllLayer();
|
||||||
console.log('resp', resp);
|
// console.log('resp', resp);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +112,15 @@ class MapManager {
|
|||||||
for (const layerMeta of listLayers) {
|
for (const layerMeta of listLayers) {
|
||||||
try {
|
try {
|
||||||
const data = await getLayer(layerMeta); // lấy GeoJSON từ server
|
const data = await getLayer(layerMeta); // lấy GeoJSON từ server
|
||||||
|
let style = {};
|
||||||
|
if (layerMeta === 'base-countries') {
|
||||||
|
style = createLabelAndFillStyle('#F3F2EC', '#E5BEB5');
|
||||||
|
} else {
|
||||||
|
style = createLabelAndFillStyle('#77BEF0', '#000000');
|
||||||
|
}
|
||||||
const vectorLayer = createGeoJSONLayer({
|
const vectorLayer = createGeoJSONLayer({
|
||||||
data,
|
data,
|
||||||
style: createLabelAndFillStyle('#77BEF0', '#000000'),
|
style: style,
|
||||||
});
|
});
|
||||||
|
|
||||||
dynamicLayers.push(vectorLayer);
|
dynamicLayers.push(vectorLayer);
|
||||||
@@ -319,10 +345,11 @@ class MapManager {
|
|||||||
|
|
||||||
// Lưu feature
|
// Lưu feature
|
||||||
this.features.push(feature);
|
this.features.push(feature);
|
||||||
this.flyToFeature(feature);
|
|
||||||
this.featureLayer?.getSource()?.addFeature(feature);
|
this.featureLayer?.getSource()?.addFeature(feature);
|
||||||
|
if (styleConfig.animate) {
|
||||||
console.log('Point added successfully:', { coord, data, styleConfig });
|
this.animatedMarker(feature, styleConfig.animationConfig);
|
||||||
|
}
|
||||||
|
// console.log('Point added successfully:', { coord, data, styleConfig });
|
||||||
|
|
||||||
return feature;
|
return feature;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -333,6 +360,74 @@ class MapManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
animatedMarker = (
|
||||||
|
feature: Feature<Point>,
|
||||||
|
config: AnimationConfig = {},
|
||||||
|
): void => {
|
||||||
|
const {
|
||||||
|
duration = 3000,
|
||||||
|
maxRadius = 20,
|
||||||
|
strokeColor = 'rgba(255, 0, 0, 0.8)',
|
||||||
|
strokeWidthBase = 0.25,
|
||||||
|
} = config;
|
||||||
|
console.log('Starting animatedMarker with config:', config);
|
||||||
|
|
||||||
|
const flashGeom = feature.getGeometry()?.clone() as Point | undefined;
|
||||||
|
if (!flashGeom || !this.featureLayer) {
|
||||||
|
console.error('Invalid geometry or featureLayer for animation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tạo 2 "pha" sóng, mỗi sóng bắt đầu cách nhau duration/2
|
||||||
|
const waveCount = 1;
|
||||||
|
const waveOffsets = Array.from(
|
||||||
|
{ length: waveCount },
|
||||||
|
(_, i) => (duration / waveCount) * i,
|
||||||
|
);
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
const listenerKey: EventsKey = this.featureLayer.on(
|
||||||
|
'postrender',
|
||||||
|
(event: RenderEvent) => {
|
||||||
|
const frameState = event.frameState;
|
||||||
|
if (!frameState) return;
|
||||||
|
|
||||||
|
const elapsedTotal = frameState.time - start;
|
||||||
|
if (elapsedTotal >= duration * 3) {
|
||||||
|
// chạy 2 chu kỳ rồi dừng (bạn có thể bỏ điều kiện này để chạy mãi)
|
||||||
|
unByKey(listenerKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vectorContext = getVectorContext(event);
|
||||||
|
|
||||||
|
waveOffsets.forEach((offset) => {
|
||||||
|
const elapsed =
|
||||||
|
(frameState.time - start - offset + duration) % duration;
|
||||||
|
const elapsedRatio = elapsed / duration;
|
||||||
|
|
||||||
|
const radius = easeOut(elapsedRatio) * (maxRadius - 5) + 5;
|
||||||
|
const opacity = easeOut(1 - elapsedRatio);
|
||||||
|
|
||||||
|
const style = new Style({
|
||||||
|
image: new CircleStyle({
|
||||||
|
radius,
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: strokeColor.replace('0.8', opacity.toString()),
|
||||||
|
width: strokeWidthBase + opacity,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
vectorContext.setStyle(style);
|
||||||
|
vectorContext.drawGeometry(flashGeom);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map!.render();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.eventKey = listenerKey;
|
||||||
|
};
|
||||||
|
|
||||||
addPolygon(
|
addPolygon(
|
||||||
coords: Coordinate[][],
|
coords: Coordinate[][],
|
||||||
@@ -444,15 +539,99 @@ class MapManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hàm addLineString đã được sửa đổi
|
||||||
|
addLineString(
|
||||||
|
coords: Coordinate[],
|
||||||
|
data: FeatureData = {},
|
||||||
|
styleConfig: StyleConfig = {},
|
||||||
|
): Feature | null {
|
||||||
|
try {
|
||||||
|
// Kiểm tra coords là mảng hợp lệ và có ít nhất 2 tọa độ
|
||||||
|
if (
|
||||||
|
!Array.isArray(coords) ||
|
||||||
|
coords.length < 2 ||
|
||||||
|
!coords.every(
|
||||||
|
(coord) =>
|
||||||
|
Array.isArray(coord) &&
|
||||||
|
coord.length === 2 &&
|
||||||
|
typeof coord[0] === 'number' &&
|
||||||
|
typeof coord[1] === 'number',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid coordinates for LineString: ${JSON.stringify(coords)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chuyển đổi tọa độ từ [lon, lat] sang hệ tọa độ của OpenLayers
|
||||||
|
const transformedCoords = coords.map((coord) => fromLonLat(coord));
|
||||||
|
const geometry = new LineString(transformedCoords);
|
||||||
|
const feature = new Feature({
|
||||||
|
geometry,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tạo style cho LineString
|
||||||
|
const style = new Style({
|
||||||
|
text: new Text({
|
||||||
|
text: data.text || '',
|
||||||
|
font: styleConfig.font || '16px Arial', // Tăng kích thước font
|
||||||
|
fill: new Fill({ color: styleConfig.textColor || 'black' }), // Đổi màu thành đen để nổi bật
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: styleConfig.textStrokeColor || 'white',
|
||||||
|
width: 2, // Tăng độ rộng viền
|
||||||
|
}),
|
||||||
|
placement: 'line',
|
||||||
|
textAlign: 'center',
|
||||||
|
textBaseline: 'middle', // Đổi thành 'middle' để căn giữa theo chiều dọc
|
||||||
|
offsetY: styleConfig.textOffsetY || -10, // Dịch text lên trên một chút
|
||||||
|
}),
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: styleConfig.strokeColor || 'blue',
|
||||||
|
width: styleConfig.strokeWidth || 5,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
feature.setStyle(style);
|
||||||
|
|
||||||
|
// Thêm feature vào features và featureLayer
|
||||||
|
if (this.featureLayer && this.isInitialized) {
|
||||||
|
this.features.push(feature);
|
||||||
|
this.featureLayer.getSource()?.addFeature(feature);
|
||||||
|
// console.log(
|
||||||
|
// 'LineString added successfully at',
|
||||||
|
// new Date().toLocaleTimeString(),
|
||||||
|
// ':',
|
||||||
|
// { coords, data, styleConfig },
|
||||||
|
// );
|
||||||
|
// Đảm bảo bản đồ được render lại
|
||||||
|
this.map?.render();
|
||||||
|
return feature;
|
||||||
|
} else {
|
||||||
|
throw new Error('featureLayer or map is not initialized');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.errors.push(error.message);
|
||||||
|
this.onError(this.errors);
|
||||||
|
console.error(
|
||||||
|
'Error adding LineString at',
|
||||||
|
new Date().toLocaleTimeString(),
|
||||||
|
':',
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createPolygonStyle = (
|
createPolygonStyle = (
|
||||||
data: Record<string, any> = {},
|
data: Record<string, any> = {},
|
||||||
styleConfig: Record<string, any> = {},
|
styleConfig: Record<string, any> = {},
|
||||||
zoom: number,
|
zoom: number,
|
||||||
) => {
|
) => {
|
||||||
console.log(
|
// console.log(
|
||||||
`createPolygonStyle called with zoom: ${zoom}, styleConfig:`,
|
// `createPolygonStyle called with zoom: ${zoom}, styleConfig:`,
|
||||||
styleConfig,
|
// styleConfig,
|
||||||
); // Debug
|
// ); // Debug
|
||||||
const textContent = `Tên: ${data?.name ?? 'Chưa có'}\n\n Loại: ${
|
const textContent = `Tên: ${data?.name ?? 'Chưa có'}\n\n Loại: ${
|
||||||
data?.type ?? 0
|
data?.type ?? 0
|
||||||
}\n\nThông tin: ${data?.description ?? 'Chưa có'}`;
|
}\n\nThông tin: ${data?.description ?? 'Chưa có'}`;
|
||||||
@@ -490,7 +669,7 @@ class MapManager {
|
|||||||
text: textStyle,
|
text: textStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Polygon style created:`, style); // Debug
|
// console.log(`Polygon style created:`, style); // Debug
|
||||||
return [style];
|
return [style];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -635,7 +814,7 @@ class MapManager {
|
|||||||
|
|
||||||
view.fit(extent, {
|
view.fit(extent, {
|
||||||
padding: [300, 300, 300, 300],
|
padding: [300, 300, 300, 300],
|
||||||
maxZoom: features.length === 1 ? 10 : 12,
|
maxZoom: features.length === 1 ? 8 : 10,
|
||||||
duration,
|
duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
import { ROUTE_TRIP } from '@/constants';
|
import {
|
||||||
import { queryAlarms } from '@/services/controller/DeviceController';
|
MAP_POLYGON_BAN,
|
||||||
|
MAP_POLYLINE_BAN,
|
||||||
|
MAP_TRACKPOINTS_ID,
|
||||||
|
ROUTE_TRIP,
|
||||||
|
} from '@/constants';
|
||||||
|
import { ENTITY_TYPE_ENUM } from '@/constants/enums';
|
||||||
|
import {
|
||||||
|
queryAlarms,
|
||||||
|
queryEntities,
|
||||||
|
queryShipTrackPoints,
|
||||||
|
} from '@/services/controller/DeviceController';
|
||||||
|
import { queryBanzones } from '@/services/controller/MapController';
|
||||||
import { getShipIcon } from '@/services/service/MapService';
|
import { getShipIcon } from '@/services/service/MapService';
|
||||||
|
import {
|
||||||
|
convertWKTLineStringToLatLngArray,
|
||||||
|
convertWKTtoLatLngString,
|
||||||
|
getBanzoneNameByType,
|
||||||
|
} from '@/utils/geomUtils';
|
||||||
import {
|
import {
|
||||||
CommentOutlined,
|
CommentOutlined,
|
||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
@@ -14,6 +30,21 @@ import ShipInfo from './components/ShipInfo';
|
|||||||
import SosButton from './components/SosButton';
|
import SosButton from './components/SosButton';
|
||||||
import VietNamMap, { MapManager } from './components/VietNamMap';
|
import VietNamMap, { MapManager } from './components/VietNamMap';
|
||||||
|
|
||||||
|
// // Define missing types locally for now
|
||||||
|
// interface AlarmData {
|
||||||
|
// level: number;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface EntityData {
|
||||||
|
// id: string;
|
||||||
|
// valueString: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// interface TrackPoint {
|
||||||
|
// lat: number;
|
||||||
|
// lon: number;
|
||||||
|
// }
|
||||||
|
|
||||||
export interface GpsData {
|
export interface GpsData {
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lon: number;
|
||||||
@@ -61,42 +92,108 @@ const HomePage: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getGPSData();
|
getGPSData();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
getGPSData();
|
||||||
|
getEntitiesData(mapManagerRef.current, gpsData!);
|
||||||
|
fetchAndAddTrackPoints(mapManagerRef.current);
|
||||||
|
fetchAndProcessEntities(mapManagerRef.current);
|
||||||
|
}, 5000);
|
||||||
return () => {
|
return () => {
|
||||||
mapManagerRef.current?.destroy();
|
clearInterval(interval);
|
||||||
console.log('MapManager destroyed in HomePage cleanup');
|
console.log('MapManager destroyed in HomePage cleanup');
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
getEntitiesData();
|
// getGPSData();
|
||||||
}, 500); // delay 1s
|
getEntitiesData(mapManagerRef.current, gpsData!);
|
||||||
|
fetchAndAddTrackPoints(mapManagerRef.current);
|
||||||
|
fetchAndProcessEntities(mapManagerRef.current);
|
||||||
|
}, 1000); // delay 1s
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [gpsData]);
|
}, [gpsData]);
|
||||||
|
|
||||||
const getEntitiesData = async () => {
|
// Helper function to add features to the map
|
||||||
try {
|
const addFeatureToMap = (
|
||||||
const alarm = await queryAlarms();
|
mapManager: MapManager,
|
||||||
console.log('GPS Data:', gpsData);
|
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);
|
||||||
|
|
||||||
if (mapManagerRef.current?.featureLayer) {
|
if (mapManager?.featureLayer && gpsData) {
|
||||||
mapManagerRef.current.featureLayer.getSource()?.clear();
|
mapManager.featureLayer.getSource()?.clear();
|
||||||
try {
|
mapManager.addPoint(
|
||||||
mapManagerRef.current.addPoint(
|
[gpsData.lon, gpsData.lat],
|
||||||
[gpsData!.lon, gpsData!.lat],
|
{ bearing: gpsData.h || 0 },
|
||||||
{
|
{
|
||||||
bearing: gpsData!.h || 0,
|
icon: getShipIcon(alarm.level || 0, gpsData?.fishing || false),
|
||||||
},
|
scale: 0.1,
|
||||||
{
|
animate: isSos ? true : false,
|
||||||
icon: getShipIcon(alarm.level || 1, gpsData?.fishing || false),
|
},
|
||||||
scale: 0.1,
|
);
|
||||||
},
|
}
|
||||||
);
|
};
|
||||||
} catch (parseError) {
|
|
||||||
console.error(
|
const fetchAndProcessEntities = async (mapManager: MapManager) => {
|
||||||
`Error parsing valueString for entity: ${parseError}`,
|
try {
|
||||||
parseError,
|
const entities: API.TransformedEntity[] = await queryEntities();
|
||||||
);
|
console.log('Fetched entities:', entities.length);
|
||||||
|
|
||||||
|
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 || '',
|
||||||
|
);
|
||||||
|
if (coordinates.length > 0) {
|
||||||
|
mapManager.addLineString(
|
||||||
|
coordinates,
|
||||||
|
{
|
||||||
|
id: MAP_POLYLINE_BAN,
|
||||||
|
text: `${geom.name} - ${zone.message}`,
|
||||||
|
},
|
||||||
|
{ strokeColor: 'red', strokeWidth: 4 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (geom_type === 1) {
|
||||||
|
const coordinates = convertWKTtoLatLngString(geom_poly || '');
|
||||||
|
if (coordinates.length > 0) {
|
||||||
|
mapManager.addPolygon(
|
||||||
|
coordinates,
|
||||||
|
{
|
||||||
|
id: MAP_POLYGON_BAN,
|
||||||
|
name: geom.name,
|
||||||
|
type: getBanzoneNameByType(geom.type || 4),
|
||||||
|
description: zone.message,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
strokeColor: '#FCC61D',
|
||||||
|
strokeWidth: 2,
|
||||||
|
fillColor: '#FCC61D',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -104,6 +201,30 @@ const HomePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<VietNamMap
|
<VietNamMap
|
||||||
@@ -150,7 +271,12 @@ const HomePage: React.FC = () => {
|
|||||||
<SosButton
|
<SosButton
|
||||||
onRefresh={(value) => {
|
onRefresh={(value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
getEntitiesData();
|
// Ensure null check for gpsData in all usages
|
||||||
|
if (gpsData) {
|
||||||
|
getEntitiesData(mapManagerRef.current, gpsData);
|
||||||
|
} else {
|
||||||
|
console.warn('GPS data is null, skipping getEntitiesData call');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const AlarmTable: React.FC<AlarmTableProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProList<API.Alarm>
|
<ProList<API.Alarm>
|
||||||
// bordered
|
bordered
|
||||||
actionRef={actionRef}
|
actionRef={actionRef}
|
||||||
metas={columns}
|
metas={columns}
|
||||||
polling={DURATION_POLLING_PRESENTATIONS}
|
polling={DURATION_POLLING_PRESENTATIONS}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@/services/controller/TripController';
|
} from '@/services/controller/TripController';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { useModel } from '@umijs/max';
|
import { useModel } from '@umijs/max';
|
||||||
import { Button, message, theme } from 'antd';
|
import { Button, Grid, message, theme } from 'antd';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import CreateOrUpdateFishingLog from './CreateOrUpdateFishingLog';
|
import CreateOrUpdateFishingLog from './CreateOrUpdateFishingLog';
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ const CreateNewHaulOrTrip: React.FC<CreateNewHaulOrTripProps> = ({
|
|||||||
const checkHaulFinished = () => {
|
const checkHaulFinished = () => {
|
||||||
return trips?.fishing_logs?.some((h) => h.status === 0);
|
return trips?.fishing_logs?.some((h) => h.status === 0);
|
||||||
};
|
};
|
||||||
|
const { useBreakpoint } = Grid;
|
||||||
|
const screens = useBreakpoint();
|
||||||
|
|
||||||
const createNewHaul = async () => {
|
const createNewHaul = async () => {
|
||||||
if (trips?.fishing_logs?.some((f) => f.status === 0)) {
|
if (trips?.fishing_logs?.some((f) => f.status === 0)) {
|
||||||
@@ -76,7 +78,10 @@ const CreateNewHaulOrTrip: React.FC<CreateNewHaulOrTripProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{
|
||||||
|
padding: screens.sm ? '0px': '10px',
|
||||||
|
marginRight: screens.sm ? '24px': '0px',
|
||||||
|
}}>
|
||||||
{trips?.trip_status === 2 ? (
|
{trips?.trip_status === 2 ? (
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
@@ -126,7 +131,7 @@ const CreateNewHaulOrTrip: React.FC<CreateNewHaulOrTripProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ProCard } from '@ant-design/pro-components';
|
import { ProCard } from '@ant-design/pro-components';
|
||||||
import { useModel } from '@umijs/max';
|
import { useModel } from '@umijs/max';
|
||||||
import { Flex, Grid } from 'antd';
|
import { Flex, Grid } from 'antd';
|
||||||
import { useState } from 'react';
|
|
||||||
import HaulTable from './HaulTable';
|
import HaulTable from './HaulTable';
|
||||||
import TripCostTable from './TripCost';
|
import TripCostTable from './TripCost';
|
||||||
import TripCrews from './TripCrews';
|
import TripCrews from './TripCrews';
|
||||||
@@ -23,7 +22,6 @@ const MainTripBody: React.FC<MainTripBodyProps> = ({
|
|||||||
// console.log('tripInfo:', tripInfo);
|
// console.log('tripInfo:', tripInfo);
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
const screens = useBreakpoint();
|
const screens = useBreakpoint();
|
||||||
const [isResponsive, setIsResponsive] = useState(false);
|
|
||||||
const { data, getApi } = useModel('getTrip');
|
const { data, getApi } = useModel('getTrip');
|
||||||
const tripCosts = Array.isArray(tripInfo?.trip_cost)
|
const tripCosts = Array.isArray(tripInfo?.trip_cost)
|
||||||
? tripInfo.trip_cost
|
? tripInfo.trip_cost
|
||||||
@@ -77,11 +75,14 @@ const MainTripBody: React.FC<MainTripBodyProps> = ({
|
|||||||
padding: 0,
|
padding: 0,
|
||||||
paddingInline: 0,
|
paddingInline: 0,
|
||||||
gap: 10,
|
gap: 10,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ProCard
|
<ProCard
|
||||||
split={screens.lg ? 'vertical' : 'horizontal'}
|
gutter={16} // tạo khoảng cách thay vì split (không có border)
|
||||||
bodyStyle={{ padding: 0, gap: 5 }}
|
direction={screens.lg ? 'row' : 'column'} // responsive: ngang/dọc
|
||||||
|
bodyStyle={{ padding: 0, gap: 10 }}
|
||||||
|
style={{ color: 'transparent' }}
|
||||||
>
|
>
|
||||||
<ProCard
|
<ProCard
|
||||||
colSpan={{ xs: 24, sm: 24, lg: 12, xl: 12 }}
|
colSpan={{ xs: 24, sm: 24, lg: 12, xl: 12 }}
|
||||||
@@ -92,7 +93,7 @@ const MainTripBody: React.FC<MainTripBodyProps> = ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderBottom: '1px solid #f0f0f0',
|
// borderBottom: '1px solid #f0f0f0',
|
||||||
}}
|
}}
|
||||||
title="Chi phí chuyến đi"
|
title="Chi phí chuyến đi"
|
||||||
style={{ minHeight: 300 }}
|
style={{ minHeight: 300 }}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import MainTripBody from './components/MainTripBody';
|
|||||||
import TripCancleOrFinishedButton from './components/TripCancelOrFinishButton';
|
import TripCancleOrFinishedButton from './components/TripCancelOrFinishButton';
|
||||||
const DetailTrip = () => {
|
const DetailTrip = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [responsive, setResponsive] = useState(false);
|
|
||||||
const [showAlarmList, setShowAlarmList] = useState(true);
|
const [showAlarmList, setShowAlarmList] = useState(true);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [alarmList, setAlarmList] = useState<API.Alarm[]>([]);
|
const [alarmList, setAlarmList] = useState<API.Alarm[]>([]);
|
||||||
@@ -49,10 +48,16 @@ const DetailTrip = () => {
|
|||||||
(
|
(
|
||||||
<PageContainer
|
<PageContainer
|
||||||
header={{
|
header={{
|
||||||
title: data ? data.name : 'Chuyến đi',
|
title: (
|
||||||
|
<div style={{ marginLeft: screens.md ? '24px' : '10px' }}>
|
||||||
|
{data ? data.name : 'Chuyến đi'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
tags: <BadgeTripStatus status={data?.trip_status || 0} />,
|
tags: <BadgeTripStatus status={data?.trip_status || 0} />,
|
||||||
}}
|
}}
|
||||||
|
content={null}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
ghost
|
||||||
extra={[
|
extra={[
|
||||||
<CreateNewHaulOrTrip
|
<CreateNewHaulOrTrip
|
||||||
trips={data || undefined}
|
trips={data || undefined}
|
||||||
@@ -99,8 +104,9 @@ const DetailTrip = () => {
|
|||||||
defaultMessage: 'Cảnh báo',
|
defaultMessage: 'Cảnh báo',
|
||||||
})}
|
})}
|
||||||
colSpan={{ xs: 24, md: 24, lg: 5 }}
|
colSpan={{ xs: 24, md: 24, lg: 5 }}
|
||||||
bodyStyle={{ paddingInline: 0, paddingBlock: 8 }}
|
bodyStyle={{ paddingInline: 0, paddingBlock: 0 }}
|
||||||
bordered
|
bordered
|
||||||
|
// style={{ borderBlockEnd: 'none'}}
|
||||||
>
|
>
|
||||||
{data ? (
|
{data ? (
|
||||||
<AlarmTable alarmList={alarmList} isLoading={isLoading} />
|
<AlarmTable alarmList={alarmList} isLoading={isLoading} />
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import {
|
|||||||
API_GET_GPS,
|
API_GET_GPS,
|
||||||
API_PATH_ENTITIES,
|
API_PATH_ENTITIES,
|
||||||
API_PATH_SHIP_INFO,
|
API_PATH_SHIP_INFO,
|
||||||
|
API_PATH_SHIP_TRACK_POINTS,
|
||||||
API_SOS,
|
API_SOS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
function transformEntityResponse(
|
export function transformEntityResponse(
|
||||||
raw: API.EntityResponse,
|
raw: API.EntityResponse,
|
||||||
): API.TransformedEntity {
|
): API.TransformedEntity {
|
||||||
return {
|
return {
|
||||||
@@ -19,7 +20,7 @@ function transformEntityResponse(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEntities(): Promise<API.TransformedEntity[]> {
|
export async function queryEntities(): Promise<API.TransformedEntity[]> {
|
||||||
const rawList = await request<API.EntityResponse[]>(API_PATH_ENTITIES);
|
const rawList = await request<API.EntityResponse[]>(API_PATH_ENTITIES);
|
||||||
return rawList.map(transformEntityResponse);
|
return rawList.map(transformEntityResponse);
|
||||||
}
|
}
|
||||||
@@ -52,3 +53,7 @@ export async function sendSosMessage(message: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queryShipTrackPoints() {
|
||||||
|
return await request<API.ShipTrackPoint[]>(API_PATH_SHIP_TRACK_POINTS);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { API_GET_ALL_LAYER, API_GET_LAYER_INFO } from '@/constants';
|
import { API_GET_ALL_BANZONES, API_GET_ALL_LAYER, API_GET_LAYER_INFO } from '@/constants';
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
export async function getLayer(name: string) {
|
export async function getLayer(name: string) {
|
||||||
@@ -8,3 +8,7 @@ export async function getLayer(name: string) {
|
|||||||
export async function getAllLayer() {
|
export async function getAllLayer() {
|
||||||
return request(API_GET_ALL_LAYER);
|
return request(API_GET_ALL_LAYER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queryBanzones() {
|
||||||
|
return request<API.Zone[]>(API_GET_ALL_BANZONES);
|
||||||
|
}
|
||||||
40
src/services/controller/typings.d.ts
vendored
40
src/services/controller/typings.d.ts
vendored
@@ -124,6 +124,14 @@ declare namespace API {
|
|||||||
fishing: boolean;
|
fishing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShipTrackPoint {
|
||||||
|
time: number;
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
s: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Trips
|
// Trips
|
||||||
interface FishingGear {
|
interface FishingGear {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -328,4 +336,36 @@ declare namespace API {
|
|||||||
evtid?: string;
|
evtid?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Banzone
|
||||||
|
export interface Zone {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
type?: number;
|
||||||
|
conditions?: Condition[];
|
||||||
|
enabled?: boolean;
|
||||||
|
updated_at?: Date;
|
||||||
|
geom?: Geom;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Condition {
|
||||||
|
max?: number;
|
||||||
|
min?: number;
|
||||||
|
type?: Type;
|
||||||
|
to?: number;
|
||||||
|
from?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Type {
|
||||||
|
LengthLimit = 'length_limit',
|
||||||
|
MonthRange = 'month_range',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Geom {
|
||||||
|
geom_type?: number;
|
||||||
|
geom_poly?: string;
|
||||||
|
geom_lines?: string;
|
||||||
|
geom_point?: string;
|
||||||
|
geom_radius?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/utils/geomUtils.ts
Normal file
76
src/utils/geomUtils.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
export const convertWKTPointToLatLng = (wktString: string) => {
|
||||||
|
if (
|
||||||
|
!wktString ||
|
||||||
|
typeof wktString !== 'string' ||
|
||||||
|
!wktString.startsWith('POINT')
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = wktString.match(/POINT\s*\(([-\d.]+)\s+([-\d.]+)\)/);
|
||||||
|
if (!matched) return null;
|
||||||
|
|
||||||
|
const lng = parseFloat(matched[1]);
|
||||||
|
const lat = parseFloat(matched[2]);
|
||||||
|
|
||||||
|
return [lng, lat]; // [longitude, latitude]
|
||||||
|
};
|
||||||
|
export const convertWKTLineStringToLatLngArray = (wktString: string) => {
|
||||||
|
if (
|
||||||
|
!wktString ||
|
||||||
|
typeof wktString !== 'string' ||
|
||||||
|
!wktString.startsWith('LINESTRING')
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = wktString.match(/LINESTRING\s*\((.*)\)/);
|
||||||
|
if (!matched) return [];
|
||||||
|
|
||||||
|
const coordinates = matched[1].split(',').map((coordStr) => {
|
||||||
|
const [x, y] = coordStr.trim().split(' ').map(Number);
|
||||||
|
return [x, y]; // [lng, lat]
|
||||||
|
});
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertWKTtoLatLngString = (wktString: string) => {
|
||||||
|
if (
|
||||||
|
!wktString ||
|
||||||
|
typeof wktString !== 'string' ||
|
||||||
|
!wktString.startsWith('MULTIPOLYGON')
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = wktString.match(/MULTIPOLYGON\s*\(\(\((.*)\)\)\)/);
|
||||||
|
if (!matched) return [];
|
||||||
|
|
||||||
|
const polygons = matched[1]
|
||||||
|
.split(')),((') // chia các polygon
|
||||||
|
.map((polygonStr) =>
|
||||||
|
polygonStr
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.map((coordStr) => {
|
||||||
|
const [x, y] = coordStr.trim().split(' ').map(Number);
|
||||||
|
return [x, y];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return polygons;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBanzoneNameByType = (type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 1:
|
||||||
|
return 'Cấm đánh bắt';
|
||||||
|
case 2:
|
||||||
|
return 'Cấm di chuyển';
|
||||||
|
case 3:
|
||||||
|
return 'Vùng an toàn';
|
||||||
|
default:
|
||||||
|
return 'Chưa có';
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user