sửa lỗi hiển thị polyline, polygon ở map, thêm component ScanQrCode

This commit is contained in:
Tran Anh Tuan
2025-11-05 16:23:47 +07:00
parent 300271fce7
commit 62b18e5bc0
11 changed files with 351 additions and 40 deletions

View File

@@ -39,6 +39,12 @@
"backgroundColor": "#000000"
}
}
],
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
}
]
],
"experiments": {

View File

@@ -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>
);
}

View File

@@ -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
View 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 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",
},
});

View File

@@ -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"
/>

View File

@@ -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,

View File

@@ -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}

View File

@@ -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("");

View File

@@ -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
View File

@@ -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",

View File

@@ -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",