hiển thị thuyền thông tin tàu

This commit is contained in:
Tran Anh Tuan
2025-12-03 16:22:25 +07:00
parent 47e9bac0f9
commit 22a3b591c6
22 changed files with 2135 additions and 260 deletions

View File

@@ -1,6 +1,6 @@
import { ThemedText } from "@/components/themed-text";
import { useAppTheme } from "@/hooks/use-app-theme";
import { View } from "react-native";
import { ScrollView, View } from "react-native";
interface DescriptionProps {
title?: string;
@@ -13,12 +13,14 @@ export const Description = ({
const { colors } = useAppTheme();
return (
<View className="flex-row gap-2 ">
<ThemedText
style={{ color: colors.textSecondary, fontSize: 16 }}
>
<ThemedText style={{ color: colors.textSecondary, fontSize: 16 }}>
{title}:
</ThemedText>
<ThemedText style={{ fontSize: 16 }}>{description}</ThemedText>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<ThemedText style={{ color: colors.textSecondary, fontSize: 16 }}>
{description || "-"}
</ThemedText>
</ScrollView>
</View>
);
};

141
components/map/ShipInfo.tsx Normal file
View File

@@ -0,0 +1,141 @@
import { STATUS_DANGEROUS, STATUS_NORMAL, STATUS_WARNING } from "@/constants";
import { useAppTheme } from "@/hooks/use-app-theme";
import { useI18n } from "@/hooks/use-i18n";
import { formatRelativeTime } from "@/services/time_service";
import { convertToDMS, kmhToKnot } from "@/utils/geom";
import { Ionicons } from "@expo/vector-icons";
import { ScrollView, Text, View } from "react-native";
import { ThemedText } from "../themed-text";
interface ShipInfoProps {
thingMetadata?: Model.ThingMetadata;
}
const ShipInfo = ({ thingMetadata }: ShipInfoProps) => {
const { t } = useI18n();
const { colors } = useAppTheme();
// Định nghĩa màu sắc theo trạng thái
const statusConfig = {
normal: {
dotColor: "bg-green-500",
badgeColor: "bg-green-100",
badgeTextColor: "text-green-700",
badgeText: "Bình thường",
},
warning: {
dotColor: "bg-yellow-500",
badgeColor: "bg-yellow-100",
badgeTextColor: "text-yellow-700",
badgeText: "Cảnh báo",
},
danger: {
dotColor: "bg-red-500",
badgeColor: "bg-red-100",
badgeTextColor: "text-red-700",
badgeText: "Nguy hiểm",
},
};
const getThingStatus = () => {
switch (thingMetadata?.state_level) {
case STATUS_NORMAL:
return "normal";
case STATUS_WARNING:
return "warning";
case STATUS_DANGEROUS:
return "danger";
default:
return "normal";
}
};
const gpsData: Model.GPSResponse = JSON.parse(thingMetadata?.gps || "{}");
const currentStatus = statusConfig[getThingStatus()];
// Format tọa độ
const formatCoordinate = (lat: number, lon: number) => {
const latDir = lat >= 0 ? "N" : "S";
const lonDir = lon >= 0 ? "E" : "W";
return `${Math.abs(lat).toFixed(4)}°${latDir}, ${Math.abs(lon).toFixed(
4
)}°${lonDir}`;
};
return (
<View className="px-4 py-3">
{/* Header: Tên tàu và trạng thái */}
<View className="flex-row items-center justify-between mb-3">
<View className="flex-row items-center gap-2">
{/* Status dot */}
<View className={`h-3 w-3 rounded-full ${currentStatus.dotColor}`} />
{/* Tên tàu */}
<Text className="text-lg font-semibold text-gray-900">
{thingMetadata?.ship_name}
</Text>
</View>
{/* Badge trạng thái */}
<View className={`px-3 py-1 rounded-full ${currentStatus.badgeColor}`}>
<Text
className={`text-md font-medium ${currentStatus.badgeTextColor}`}
>
{currentStatus.badgeText}
</Text>
</View>
</View>
{/* Mã tàu */}
{/* <Text className="text-base text-gray-600 mb-2">{shipCode}</Text> */}
<View className="flex-row items-center justify-between gap-2 mb-3">
<View className="flex-row items-center gap-2 w-2/3">
<Ionicons name="speedometer-outline" size={16} color="#6B7280" />
<Text className="text-md text-gray-600">
{kmhToKnot(gpsData.s || 0)} {t("home.speed_units")}
</Text>
</View>
<View className="flex-row items-start gap-2 w-1/3 ">
<Ionicons name="compass-outline" size={16} color="#6B7280" />
<Text className="text-md text-gray-600">{gpsData.h}°</Text>
</View>
</View>
{/* Tọa độ */}
<View className="flex-row items-center justify-between gap-2 mb-2">
<View className="flex-row items-center gap-2 w-2/3">
<Ionicons name="location-outline" size={16} color="#6B7280" />
<Text className="text-md text-gray-600">
{convertToDMS(gpsData.lat || 0, true)},
{convertToDMS(gpsData.lon || 0, false)}
</Text>
</View>
<View className=" flex-row items-start gap-2 w-1/3 ">
<Ionicons name="time-outline" size={16} color="#6B7280" />
<Text className="text-md text-gray-600">
{formatRelativeTime(gpsData.t)}
</Text>
</View>
</View>
{/* <View className="flex">
<Description title="Trạng thái" description={thingMetadata?.state} />
</View> */}
{thingMetadata?.state !== "" && (
<View className="flex-row items-center gap-2">
<ThemedText style={{ color: colors.textSecondary, fontSize: 15 }}>
Trạng thái:
</ThemedText>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ flex: 1 }}
>
<ThemedText style={{ color: colors.text, fontSize: 15 }}>
{thingMetadata?.state || "-"}
</ThemedText>
</ScrollView>
</View>
)}
</View>
);
};
export default ShipInfo;

165
components/map/TagState.tsx Normal file
View File

@@ -0,0 +1,165 @@
import { useState } from "react";
import { Pressable, ScrollView, StyleSheet, View } from "react-native";
import { ThemedText } from "../themed-text";
export type TagStateCallbackPayload = {
isNormal: boolean;
isWarning: boolean;
isDangerous: boolean;
isSos: boolean;
isDisconected: boolean; // giữ đúng theo yêu cầu (1 'n')
};
export type TagStateProps = {
normalCount?: number;
warningCount?: number;
dangerousCount?: number;
sosCount?: number;
disconnectedCount?: number;
onTagPress?: (selection: TagStateCallbackPayload) => void;
className?: string;
};
export function TagState({
normalCount = 0,
warningCount = 0,
dangerousCount = 0,
sosCount = 0,
disconnectedCount = 0,
onTagPress,
className = "",
}: TagStateProps) {
// Quản lý trạng thái các tag ở cấp cha để trả về tổng hợp
const [activeStates, setActiveStates] = useState({
normal: false,
warning: false,
dangerous: false,
sos: false,
disconnected: false,
});
const toggleState = (state: keyof typeof activeStates) => {
setActiveStates((prev) => {
const next = { ...prev, [state]: !prev[state] };
// Gọi callback với object tổng hợp sau khi cập nhật
onTagPress?.({
isNormal: next.normal,
isWarning: next.warning,
isDangerous: next.dangerous,
isSos: next.sos,
isDisconected: next.disconnected,
});
return next;
});
};
const renderTag = (
state: "normal" | "warning" | "dangerous" | "sos" | "disconnected",
count: number,
label: string,
colors: {
defaultBg: string;
defaultBorder: string;
defaultText: string;
activeBg: string;
activeBorder: string;
activeText: string;
}
) => {
if (count === 0) return null;
const pressed = activeStates[state];
return (
<Pressable
key={state}
onPress={() => toggleState(state)}
style={[
styles.tag,
{
backgroundColor: pressed ? colors.activeBg : colors.defaultBg,
borderColor: pressed ? colors.activeBorder : colors.defaultBorder,
},
]}
>
<ThemedText
style={[
styles.tagText,
{
color: pressed ? colors.activeText : colors.defaultText,
},
]}
type="defaultSemiBold"
>
{label}: {count}
</ThemedText>
</Pressable>
);
};
return (
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className={className} style={styles.container}>
{renderTag("normal", normalCount, "Bình thường", {
defaultBg: "#FFFFFF",
defaultBorder: "#22C55E",
defaultText: "#22C55E",
activeBg: "#22C55E",
activeBorder: "#22C55E",
activeText: "#FFFFFF",
})}
{renderTag("warning", warningCount, "Cảnh báo", {
defaultBg: "#FFFFFF",
defaultBorder: "#EAB308",
defaultText: "#EAB308",
activeBg: "#EAB308",
activeBorder: "#EAB308",
activeText: "#FFFFFF",
})}
{renderTag("dangerous", dangerousCount, "Nguy hiểm", {
defaultBg: "#FFFFFF",
defaultBorder: "#F97316",
defaultText: "#F97316",
activeBg: "#F97316",
activeBorder: "#F97316",
activeText: "#FFFFFF",
})}
{renderTag("sos", sosCount, "SOS", {
defaultBg: "#FFFFFF",
defaultBorder: "#EF4444",
defaultText: "#EF4444",
activeBg: "#EF4444",
activeBorder: "#EF4444",
activeText: "#FFFFFF",
})}
{renderTag("disconnected", disconnectedCount, "Mất kết nối", {
defaultBg: "#FFFFFF",
defaultBorder: "#6B7280",
defaultText: "#6B7280",
activeBg: "#6B7280",
activeBorder: "#6B7280",
activeText: "#FFFFFF",
})}
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
gap: 8,
padding: 5,
},
tag: {
borderWidth: 1,
borderRadius: 20,
paddingHorizontal: 10,
paddingVertical: 5,
minWidth: 100,
alignItems: "center",
},
tagText: {
fontSize: 14,
textAlign: "center",
},
});