hiển thị thuyền thông tin tàu
This commit is contained in:
@@ -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
141
components/map/ShipInfo.tsx
Normal 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
165
components/map/TagState.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user