sửa lỗi hiển thị polyline, polygon ở map, thêm component ScanQrCode
This commit is contained in:
6
app.json
6
app.json
@@ -39,6 +39,12 @@
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-camera",
|
||||
{
|
||||
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
|
||||
@@ -40,8 +40,8 @@ import MapView, { Circle, Marker } from "react-native-maps";
|
||||
|
||||
|
||||
export default function HomeScreen() {
|
||||
const [gpsData, setGpsData] = useState<Model.GPSResonse | undefined>(
|
||||
undefined
|
||||
const [gpsData, setGpsData] = useState<Model.GPSResonse | null>(
|
||||
null
|
||||
);
|
||||
const [alarmData, setAlarmData] = useState<Model.AlarmResponse | null>(null);
|
||||
const [entityData, setEntityData] = useState<
|
||||
@@ -81,7 +81,7 @@ export default function HomeScreen() {
|
||||
// console.log("GPS Data: ", gpsData);
|
||||
setGpsData(gpsData);
|
||||
} else {
|
||||
setGpsData(undefined);
|
||||
setGpsData(null);
|
||||
setPolygonCoordinates([]);
|
||||
setPolylineCoordinates(null);
|
||||
}
|
||||
@@ -133,23 +133,6 @@ export default function HomeScreen() {
|
||||
// console.log("Unsubscribed EVENT_TRACK_POINTS_DATA");
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (polylineCoordinates !== null) {
|
||||
console.log("Polyline Khac null");
|
||||
} else {
|
||||
console.log("Polyline null");
|
||||
}
|
||||
}, [polylineCoordinates]);
|
||||
|
||||
useEffect(() => {
|
||||
if (polygonCoordinates.length > 0) {
|
||||
console.log("Polygon Khac null");
|
||||
} else {
|
||||
console.log("Polygon null");
|
||||
}
|
||||
}, [polygonCoordinates]);
|
||||
|
||||
useEffect(() => {
|
||||
setPolylineCoordinates(null);
|
||||
setPolygonCoordinates([]);
|
||||
@@ -336,7 +319,7 @@ export default function HomeScreen() {
|
||||
latitude: point.lat,
|
||||
longitude: point.lon,
|
||||
}}
|
||||
zIndex={50}
|
||||
// zIndex={50}
|
||||
// radius={platform === IOS_PLATFORM ? 200 : 50}
|
||||
radius={circleRadius}
|
||||
strokeColor="rgba(16, 85, 201, 0.7)"
|
||||
@@ -347,13 +330,14 @@ export default function HomeScreen() {
|
||||
})}
|
||||
{polylineCoordinates && (
|
||||
<PolylineWithLabel
|
||||
key={`polyline-${gpsData?.lat || 0}-${gpsData?.lon || 0}`}
|
||||
coordinates={polylineCoordinates.coordinates}
|
||||
label={polylineCoordinates.label}
|
||||
content={polylineCoordinates.content}
|
||||
strokeColor="#FF5733"
|
||||
strokeWidth={4}
|
||||
showDistance={false}
|
||||
zIndex={50}
|
||||
// zIndex={50}
|
||||
/>
|
||||
)}
|
||||
{polygonCoordinates.length > 0 && (
|
||||
@@ -367,22 +351,23 @@ export default function HomeScreen() {
|
||||
|
||||
return (
|
||||
<PolygonWithLabel
|
||||
key={polygonKey}
|
||||
key={`polygon-${index}-${gpsData?.lat || 0}-${gpsData?.lon || 0}`}
|
||||
coordinates={polygon.coordinates}
|
||||
label={polygon.label}
|
||||
content={polygon.content}
|
||||
fillColor="rgba(16, 85, 201, 0.6)"
|
||||
strokeColor="rgba(16, 85, 201, 0.8)"
|
||||
strokeWidth={2}
|
||||
zIndex={50}
|
||||
// zIndex={50}
|
||||
zoomLevel={zoomLevel}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{gpsData !== undefined && (
|
||||
{gpsData !== null && (
|
||||
<Marker
|
||||
key={platform === IOS_PLATFORM ? `${gpsData.lat}-${gpsData.lon}` : "gps-data"}
|
||||
coordinate={{
|
||||
latitude: gpsData.lat,
|
||||
longitude: gpsData.lon,
|
||||
@@ -392,12 +377,13 @@ export default function HomeScreen() {
|
||||
? "Tàu của mình - iOS"
|
||||
: "Tàu của mình - Android"
|
||||
}
|
||||
zIndex={200}
|
||||
zIndex={20}
|
||||
anchor={
|
||||
platform === IOS_PLATFORM
|
||||
? { x: 0.5, y: 0.5 }
|
||||
: { x: 0.6, y: 0.4 }
|
||||
}
|
||||
tracksViewChanges={platform === IOS_PLATFORM ? true : undefined}
|
||||
>
|
||||
<View className="w-8 h-8 items-center justify-center">
|
||||
<View style={styles.pingContainer}>
|
||||
@@ -418,6 +404,7 @@ export default function HomeScreen() {
|
||||
alarmData?.level || 0,
|
||||
gpsData.fishing
|
||||
);
|
||||
// console.log("Ship icon:", icon, "for level:", alarmData?.level, "fishing:", gpsData.fishing);
|
||||
return typeof icon === "string" ? { uri: icon } : icon;
|
||||
})()}
|
||||
style={{
|
||||
@@ -442,7 +429,7 @@ export default function HomeScreen() {
|
||||
<View className="absolute top-14 right-2 shadow-md">
|
||||
<SosButton />
|
||||
</View>
|
||||
<GPSInfoPanel gpsData={gpsData} />
|
||||
<GPSInfoPanel gpsData={gpsData!} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import ScanQRCode from "@/components/ScanQRCode";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
export default function Sensor() {
|
||||
const [scanModalVisible, setScanModalVisible] = useState(false);
|
||||
const [scannedData, setScannedData] = useState<string | null>(null);
|
||||
const handleQRCodeScanned = (data: string) => {
|
||||
setScannedData(data);
|
||||
// Alert.alert("QR Code Scanned", `Result: ${data}`);
|
||||
};
|
||||
|
||||
const handleScanPress = () => {
|
||||
setScanModalVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.titleText}>Cảm biến trên tàu</Text>
|
||||
|
||||
<Pressable style={styles.scanButton} onPress={handleScanPress}>
|
||||
<Text style={styles.scanButtonText}>📱 Scan QR Code</Text>
|
||||
</Pressable>
|
||||
|
||||
{scannedData && (
|
||||
<View style={styles.resultContainer}>
|
||||
<Text style={styles.resultLabel}>Last Scanned:</Text>
|
||||
<Text style={styles.resultText}>{scannedData}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<ScanQRCode
|
||||
visible={scanModalVisible}
|
||||
onClose={() => setScanModalVisible(false)}
|
||||
onScanned={handleQRCodeScanned}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -25,11 +62,48 @@ const styles = StyleSheet.create({
|
||||
fontSize: 32,
|
||||
fontWeight: "700",
|
||||
lineHeight: 40,
|
||||
marginBottom: 10,
|
||||
marginBottom: 30,
|
||||
fontFamily: Platform.select({
|
||||
ios: "System",
|
||||
android: "Roboto",
|
||||
default: "System",
|
||||
}),
|
||||
},
|
||||
scanButton: {
|
||||
backgroundColor: "#007AFF",
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 30,
|
||||
borderRadius: 10,
|
||||
marginVertical: 15,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
scanButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
},
|
||||
resultContainer: {
|
||||
marginTop: 30,
|
||||
padding: 15,
|
||||
backgroundColor: "#f0f0f0",
|
||||
borderRadius: 10,
|
||||
minWidth: "80%",
|
||||
},
|
||||
resultLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#666",
|
||||
marginBottom: 8,
|
||||
},
|
||||
resultText: {
|
||||
fontSize: 14,
|
||||
color: "#333",
|
||||
fontFamily: "Menlo",
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
|
||||
228
components/ScanQRCode.tsx
Normal file
228
components/ScanQRCode.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { CameraView, useCameraPermissions } from "expo-camera";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Modal,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
interface ScanQRCodeProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onScanned: (data: string) => void;
|
||||
}
|
||||
|
||||
export default function ScanQRCode({
|
||||
visible,
|
||||
onClose,
|
||||
onScanned,
|
||||
}: ScanQRCodeProps) {
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
const [scanned, setScanned] = useState(false);
|
||||
const cameraRef = useRef(null);
|
||||
|
||||
// Request camera permission when component mounts or when visible changes to true
|
||||
useEffect(() => {
|
||||
if (visible && !permission?.granted) {
|
||||
requestPermission();
|
||||
}
|
||||
}, [visible, permission, requestPermission]);
|
||||
|
||||
// Reset scanned state when modal opens
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setScanned(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const handleBarCodeScanned = ({
|
||||
type,
|
||||
data,
|
||||
}: {
|
||||
type: string;
|
||||
data: string;
|
||||
}) => {
|
||||
if (!scanned) {
|
||||
setScanned(true);
|
||||
onScanned(data);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!permission) {
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="slide">
|
||||
<View style={styles.container}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
<Text style={styles.loadingText}>
|
||||
Requesting camera permission...
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
if (!permission.granted) {
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="slide">
|
||||
<View style={styles.container}>
|
||||
<View style={styles.permissionContainer}>
|
||||
<Text style={styles.permissionTitle}>
|
||||
Camera Permission Required
|
||||
</Text>
|
||||
<Text style={styles.permissionText}>
|
||||
This app needs camera access to scan QR codes. Please allow camera
|
||||
access in your settings.
|
||||
</Text>
|
||||
<Pressable
|
||||
style={styles.button}
|
||||
onPress={() => requestPermission()}
|
||||
>
|
||||
<Text style={styles.buttonText}>Request Permission</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.button, styles.cancelButton]}
|
||||
onPress={onClose}
|
||||
>
|
||||
<Text style={styles.buttonText}>Cancel</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="slide">
|
||||
<CameraView
|
||||
ref={cameraRef}
|
||||
style={styles.camera}
|
||||
onBarcodeScanned={handleBarCodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ["qr"],
|
||||
}}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.unfocusedContainer} />
|
||||
<View style={styles.focusedRow}>
|
||||
<View style={styles.focusedContainer} />
|
||||
</View>
|
||||
<View style={styles.unfocusedContainer} />
|
||||
|
||||
<View style={styles.bottomContainer}>
|
||||
<Text style={styles.scanningText}>
|
||||
{/* Align QR code within the frame */}
|
||||
Đặt mã QR vào khung hình
|
||||
</Text>
|
||||
<Pressable style={styles.closeButton} onPress={onClose}>
|
||||
<Text style={styles.closeButtonText}>✕ Đóng</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</CameraView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
loadingText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
},
|
||||
permissionContainer: {
|
||||
backgroundColor: "#fff",
|
||||
marginHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
alignItems: "center",
|
||||
gap: 15,
|
||||
},
|
||||
permissionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#333",
|
||||
},
|
||||
permissionText: {
|
||||
fontSize: 14,
|
||||
color: "#666",
|
||||
textAlign: "center",
|
||||
lineHeight: 20,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#007AFF",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 30,
|
||||
borderRadius: 8,
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: "#666",
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
camera: {
|
||||
flex: 1,
|
||||
},
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
unfocusedContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
focusedRow: {
|
||||
height: "80%",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
focusedContainer: {
|
||||
aspectRatio: 1,
|
||||
width: "70%",
|
||||
borderColor: "#00ff00",
|
||||
borderWidth: 3,
|
||||
borderRadius: 10,
|
||||
},
|
||||
bottomContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
paddingBottom: 40,
|
||||
gap: 20,
|
||||
},
|
||||
scanningText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
closeButton: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
closeButtonText: {
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
@@ -61,10 +61,10 @@ const GPSInfoPanel = ({ gpsData }: GPSInfoPanelProps) => {
|
||||
{/* Nút toggle ở top-right */}
|
||||
<TouchableOpacity
|
||||
onPress={togglePanel}
|
||||
className="absolute top-2 right-2 z-10 bg-white rounded-full p-1 shadow-sm"
|
||||
className="absolute top-2 right-2 z-10 bg-white rounded-full p-1"
|
||||
>
|
||||
<MaterialIcons
|
||||
name={isExpanded ? "expand-more" : "expand-less"}
|
||||
name={isExpanded ? "close" : "close"}
|
||||
size={20}
|
||||
color="#666"
|
||||
/>
|
||||
|
||||
@@ -55,7 +55,6 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
|
||||
|
||||
const paddingScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.2), 0.5);
|
||||
const minWidthScale = Math.max(Math.pow(2, (zoomLevel - 10) * 0.25), 0.9);
|
||||
markerRef.current?.showCallout();
|
||||
return (
|
||||
<>
|
||||
<Polygon
|
||||
@@ -70,7 +69,7 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
|
||||
ref={markerRef}
|
||||
coordinate={centerPoint}
|
||||
zIndex={50}
|
||||
tracksViewChanges={false}
|
||||
tracksViewChanges={platform === ANDROID_PLATFORM ? false : true}
|
||||
anchor={{ x: 0.5, y: 0.5 }}
|
||||
title={platform === ANDROID_PLATFORM ? label : undefined}
|
||||
description={platform === ANDROID_PLATFORM ? content : undefined}
|
||||
@@ -78,7 +77,6 @@ export const PolygonWithLabel: React.FC<PolygonWithLabelProps> = ({
|
||||
<View style={styles.markerContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.labelContainer,
|
||||
{
|
||||
paddingHorizontal: 5 * paddingScale,
|
||||
paddingVertical: 5 * paddingScale,
|
||||
|
||||
@@ -47,7 +47,6 @@ export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
|
||||
? ` (${distance.toFixed(2)}km)`
|
||||
: `${distance.toFixed(2)}km`;
|
||||
}
|
||||
markerRef.current?.showCallout();
|
||||
return (
|
||||
<>
|
||||
<Polyline
|
||||
@@ -61,7 +60,7 @@ export const PolylineWithLabel: React.FC<PolylineWithLabelProps> = ({
|
||||
ref={markerRef}
|
||||
coordinate={middlePoint}
|
||||
zIndex={zIndex + 10}
|
||||
tracksViewChanges={false}
|
||||
tracksViewChanges={platform === ANDROID_PLATFORM ? false : true}
|
||||
anchor={{ x: 0.5, y: 0.5 }}
|
||||
title={platform === ANDROID_PLATFORM ? label : undefined}
|
||||
description={platform === ANDROID_PLATFORM ? content : undefined}
|
||||
|
||||
@@ -90,13 +90,11 @@ const SosButton = () => {
|
||||
|
||||
const handleClickButton = async (isActive: boolean) => {
|
||||
if (isActive) {
|
||||
console.log("Active");
|
||||
const resp = await queryDeleteSos();
|
||||
if (resp.status === 200) {
|
||||
await getSosData();
|
||||
}
|
||||
} else {
|
||||
console.log("No Active");
|
||||
setSelectedSosMessage(11); // Mặc định chọn lý do ma: 11
|
||||
setShowConfirmSosDialog(true);
|
||||
}
|
||||
@@ -255,7 +253,6 @@ const SosButton = () => {
|
||||
onPress={() => {
|
||||
setSelectedSosMessage(item.ma);
|
||||
setShowDropdown(false);
|
||||
|
||||
// Clear custom message nếu chọn khác lý do
|
||||
if (item.ma !== 999) {
|
||||
setCustomMessage("");
|
||||
|
||||
@@ -17,7 +17,7 @@ export const setRouterInstance = (router: Router) => {
|
||||
export const handle401 = () => {
|
||||
if (routerInstance) {
|
||||
removeStorageItem(TOKEN);
|
||||
(routerInstance as any).replace("/login");
|
||||
(routerInstance as any).replace("/auth/login");
|
||||
} else {
|
||||
console.warn("Router instance not set, cannot redirect to login");
|
||||
}
|
||||
|
||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~54.0.20",
|
||||
"expo-camera": "~17.0.9",
|
||||
"expo-constants": "~18.0.10",
|
||||
"expo-font": "~14.0.9",
|
||||
"expo-haptics": "~15.0.7",
|
||||
@@ -8463,6 +8464,26 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-camera": {
|
||||
"version": "17.0.9",
|
||||
"resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-17.0.9.tgz",
|
||||
"integrity": "sha512-KgticPGurqEsaPBIwbG0T6mzAVnqZasDdM/6OoJt5zPh6tWB09+th6cBF1WafIBMPy8AWbfyUQSqQXqOrNJClg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-web": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-web": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-constants": {
|
||||
"version": "18.0.10",
|
||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "~54.0.20",
|
||||
"expo-camera": "~17.0.9",
|
||||
"expo-constants": "~18.0.10",
|
||||
"expo-font": "~14.0.9",
|
||||
"expo-haptics": "~15.0.7",
|
||||
|
||||
Reference in New Issue
Block a user