thêm FormSearch trong map

This commit is contained in:
Tran Anh Tuan
2025-12-04 15:02:53 +07:00
parent 42028eafc3
commit 4d60ce279e
6 changed files with 275 additions and 96 deletions

View File

@@ -1,14 +1,19 @@
import DraggablePanel from "@/components/DraggablePanel"; import DraggablePanel from "@/components/DraggablePanel";
import IconButton from "@/components/IconButton";
import type { PolygonWithLabelProps } from "@/components/map/PolygonWithLabel"; import type { PolygonWithLabelProps } from "@/components/map/PolygonWithLabel";
import type { PolylineWithLabelProps } from "@/components/map/PolylineWithLabel"; import type { PolylineWithLabelProps } from "@/components/map/PolylineWithLabel";
import ShipInfo from "@/components/map/ShipInfo"; import ShipInfo from "@/components/map/ShipInfo";
import { TagState, TagStateCallbackPayload } from "@/components/map/TagState"; import { TagState, TagStateCallbackPayload } from "@/components/map/TagState";
import ShipSearchForm, {
SearchShipResponse,
} from "@/components/ShipSearchForm";
import { EVENT_SEARCH_THINGS, IOS_PLATFORM, LIGHT_THEME } from "@/constants"; import { EVENT_SEARCH_THINGS, IOS_PLATFORM, LIGHT_THEME } from "@/constants";
import { usePlatform } from "@/hooks/use-platform"; import { usePlatform } from "@/hooks/use-platform";
import { useThemeContext } from "@/hooks/use-theme-context"; import { useThemeContext } from "@/hooks/use-theme-context";
import { searchThingEventBus } from "@/services/device_events"; import { searchThingEventBus } from "@/services/device_events";
import { getShipIcon } from "@/services/map_service"; import { getShipIcon } from "@/services/map_service";
import eventBus from "@/utils/eventBus"; import eventBus from "@/utils/eventBus";
import { AntDesign } from "@expo/vector-icons";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { import {
Animated, Animated,
@@ -36,15 +41,34 @@ export default function HomeScreen() {
const [polygonCoordinates, setPolygonCoordinates] = useState< const [polygonCoordinates, setPolygonCoordinates] = useState<
PolygonWithLabelProps[] PolygonWithLabelProps[]
>([]); >([]);
const [shipSearchFormOpen, setShipSearchFormOpen] = useState(false);
const [isPanelExpanded, setIsPanelExpanded] = useState(false);
const [things, setThings] = useState<Model.ThingsResponse | null>(null); const [things, setThings] = useState<Model.ThingsResponse | null>(null);
const platform = usePlatform(); const platform = usePlatform();
const theme = useThemeContext().colorScheme; const theme = useThemeContext().colorScheme;
const scale = useRef(new Animated.Value(0)).current; const scale = useRef(new Animated.Value(0)).current;
const opacity = useRef(new Animated.Value(1)).current; const opacity = useRef(new Animated.Value(1)).current;
const [shipSearchFormData, setShipSearchFormData] = useState<
SearchShipResponse | undefined
>(undefined);
const [tagStatePayload, setTagStatePayload] =
useState<TagStateCallbackPayload | null>(null);
// Thêm state để quản lý tracksViewChanges // Thêm state để quản lý tracksViewChanges
const [tracksViewChanges, setTracksViewChanges] = useState(true); const [tracksViewChanges, setTracksViewChanges] = useState(true);
useEffect(() => {
if (tagStatePayload) {
searchThings();
}
}, [tagStatePayload]);
useEffect(() => {
if (shipSearchFormData) {
searchThings();
}
}, [shipSearchFormData]);
const bodySearchThings: Model.SearchThingBody = { const bodySearchThings: Model.SearchThingBody = {
offset: 0, offset: 0,
limit: 50, limit: 50,
@@ -176,6 +200,7 @@ export default function HomeScreen() {
// }, [banzoneData, entityData]); // }, [banzoneData, entityData]);
// Hàm tính radius cố định khi zoom change // Hàm tính radius cố định khi zoom change
const calculateRadiusFromZoom = (zoom: number) => { const calculateRadiusFromZoom = (zoom: number) => {
const baseZoom = 10; const baseZoom = 10;
const baseRadius = 100; const baseRadius = 100;
@@ -231,37 +256,39 @@ export default function HomeScreen() {
}; };
useEffect(() => { useEffect(() => {
if (alarmData?.level === 3) { for (var thing of things?.things || []) {
const loop = Animated.loop( if (thing.metadata?.state_level === 3) {
Animated.sequence([ const loop = Animated.loop(
Animated.parallel([ Animated.sequence([
Animated.timing(scale, { Animated.parallel([
toValue: 3, // nở to 3 lần Animated.timing(scale, {
duration: 1500, toValue: 3, // nở to 3 lần
useNativeDriver: true, duration: 1500,
}), useNativeDriver: true,
Animated.timing(opacity, { }),
toValue: 0, // mờ dần Animated.timing(opacity, {
duration: 1500, toValue: 0, // mờ dần
useNativeDriver: true, duration: 1500,
}), useNativeDriver: true,
]), }),
Animated.parallel([ ]),
Animated.timing(scale, { Animated.parallel([
toValue: 0, Animated.timing(scale, {
duration: 0, toValue: 0,
useNativeDriver: true, duration: 0,
}), useNativeDriver: true,
Animated.timing(opacity, { }),
toValue: 1, Animated.timing(opacity, {
duration: 0, toValue: 1,
useNativeDriver: true, duration: 0,
}), useNativeDriver: true,
]), }),
]) ]),
); ])
loop.start(); );
return () => loop.stop(); loop.start();
return () => loop.stop();
}
} }
}, [things, scale, opacity]); }, [things, scale, opacity]);
@@ -276,26 +303,78 @@ export default function HomeScreen() {
} }
}, [isFirstLoad]); }, [isFirstLoad]);
const handleOnPressState = (state: TagStateCallbackPayload) => { const searchThings = async () => {
console.log("FormSearch Playload in Search Thing: ", shipSearchFormData);
// Xây dựng query state dựa trên logic bạn cung cấp // Xây dựng query state dựa trên logic bạn cung cấp
const stateNormalQuery = state.isNormal ? "normal" : ""; const stateNormalQuery = tagStatePayload?.isNormal ? "normal" : "";
const stateSosQuery = state.isSos ? "sos" : ""; const stateSosQuery = tagStatePayload?.isSos ? "sos" : "";
const stateWarningQuery = state.isWarning const stateWarningQuery = tagStatePayload?.isWarning
? stateNormalQuery + ",warning" ? stateNormalQuery + ",warning"
: stateNormalQuery; : stateNormalQuery;
const stateCriticalQuery = state.isDangerous const stateCriticalQuery = tagStatePayload?.isDangerous
? stateWarningQuery + ",critical" ? stateWarningQuery + ",critical"
: stateWarningQuery; : stateWarningQuery;
// Nếu bật tất cả filter thì không cần truyền stateQuery // Nếu bật tất cả filter thì không cần truyền stateQuery
const stateQuery = const stateQuery =
state.isNormal && state.isWarning && state.isDangerous && state.isSos tagStatePayload?.isNormal &&
tagStatePayload?.isWarning &&
tagStatePayload?.isDangerous &&
tagStatePayload?.isSos
? "" ? ""
: [stateCriticalQuery, stateSosQuery].filter(Boolean).join(","); : [stateCriticalQuery, stateSosQuery].filter(Boolean).join(",");
let metaFormQuery = {}; let metaFormQuery = {};
if (state.isDisconected) if (tagStatePayload?.isDisconected)
metaFormQuery = { ...metaFormQuery, connected: false }; metaFormQuery = { ...metaFormQuery, connected: false };
if (shipSearchFormData?.ship_name) {
metaFormQuery = {
...metaFormQuery,
ship_name: shipSearchFormData?.ship_name,
};
}
if (shipSearchFormData?.reg_number) {
metaFormQuery = {
...metaFormQuery,
reg_number: shipSearchFormData?.reg_number,
};
}
if (
shipSearchFormData?.ship_length[0] !== 0 ||
shipSearchFormData?.ship_length[1] !== 100
) {
metaFormQuery = {
...metaFormQuery,
ship_length: shipSearchFormData?.ship_length,
};
}
if (
shipSearchFormData?.ship_power[0] !== 0 ||
shipSearchFormData?.ship_power[1] !== 100000
) {
metaFormQuery = {
...metaFormQuery,
ship_power: shipSearchFormData?.ship_power,
};
}
if (shipSearchFormData?.alarm_list) {
metaFormQuery = {
...metaFormQuery,
alarm_list: shipSearchFormData?.alarm_list,
};
}
if (shipSearchFormData?.ship_type) {
metaFormQuery = {
...metaFormQuery,
ship_type: shipSearchFormData?.ship_type,
};
}
if (shipSearchFormData?.ship_group_id) {
metaFormQuery = {
...metaFormQuery,
ship_group_id: shipSearchFormData?.ship_group_id,
};
}
// Tạo metadata query // Tạo metadata query
const metaStateQuery = const metaStateQuery =
stateQuery !== "" ? { state_level: stateQuery } : null; stateQuery !== "" ? { state_level: stateQuery } : null;
@@ -312,10 +391,28 @@ export default function HomeScreen() {
not_empty: "ship_id", not_empty: "ship_id",
}, },
}; };
console.log("Search Params: ", searchParams);
// Gọi API tìm kiếm // Gọi API tìm kiếm
searchThingEventBus(searchParams); searchThingEventBus(searchParams);
}; };
const handleOnSubmitSearchForm = async (data: SearchShipResponse) => {
setShipSearchFormData(data);
setShipSearchFormOpen(false);
};
const hasActiveFilters = shipSearchFormData
? shipSearchFormData.ship_name !== "" ||
shipSearchFormData.reg_number !== "" ||
shipSearchFormData.ship_length[0] !== 0 ||
shipSearchFormData.ship_length[1] !== 100 ||
shipSearchFormData.ship_power[0] !== 0 ||
shipSearchFormData.ship_power[1] !== 100000 ||
shipSearchFormData.ship_type !== "" ||
shipSearchFormData.alarm_list !== "" ||
shipSearchFormData.ship_group_id !== "" ||
shipSearchFormData.group_id !== ""
: false;
return ( return (
<GestureHandlerRootView style={styles.container}> <GestureHandlerRootView style={styles.container}>
@@ -411,7 +508,20 @@ export default function HomeScreen() {
</> </>
)} )}
</MapView> </MapView>
<View className="absolute top-20 left-5">
{!isPanelExpanded && (
<IconButton
icon={<AntDesign name="filter" size={16} />}
type="primary"
size="large"
style={{
borderRadius: 10,
backgroundColor: hasActiveFilters ? "#B8D576" : "#fff",
}}
onPress={() => setShipSearchFormOpen(true)}
></IconButton>
)}
</View>
{/* <View className="absolute top-14 right-2 shadow-md"> {/* <View className="absolute top-14 right-2 shadow-md">
<SosButton /> <SosButton />
</View> </View>
@@ -420,10 +530,11 @@ export default function HomeScreen() {
{/* Draggable Panel */} {/* Draggable Panel */}
<DraggablePanel <DraggablePanel
minHeightPct={0.1} minHeightPct={0.1}
maxHeightPct={0.5} maxHeightPct={0.6}
initialState="min" initialState="min"
onExpandedChange={(expanded) => { onExpandedChange={(expanded) => {
console.log("Panel expanded:", expanded); console.log("Panel expanded:", expanded);
setIsPanelExpanded(expanded);
}} }}
> >
<> <>
@@ -437,22 +548,44 @@ export default function HomeScreen() {
(things?.metadata?.total_thing ?? 0) - (things?.metadata?.total_thing ?? 0) -
(things?.metadata?.total_connected ?? 0) || 0 (things?.metadata?.total_connected ?? 0) || 0
} }
onTagPress={handleOnPressState} onTagPress={(tagState) => {
setTagStatePayload(tagState);
}}
/> />
</View> </View>
<View style={{ width: "100%", paddingVertical: 8, flex: 1 }}> <View style={{ width: "100%", paddingVertical: 8, flex: 1 }}>
<Text <View className="flex flex-row items-center">
style={{ fontSize: 20, textAlign: "center", fontWeight: "600" }} <View className="flex-1 justify-center">
> <Text
Danh sách tàu thuyền style={{
</Text> fontSize: 20,
textAlign: "center",
fontWeight: "600",
}}
>
Danh sách tàu thuyền
</Text>
</View>
{isPanelExpanded && (
<IconButton
icon={<AntDesign name="filter" size={16} />}
type="primary"
shape="circle"
size="middle"
style={{
// borderRadius: 10,
backgroundColor: hasActiveFilters ? "#B8D576" : "#fff",
}}
onPress={() => setShipSearchFormOpen(true)}
></IconButton>
)}
</View>
<ScrollView <ScrollView
style={{ width: "100%", marginTop: 8, flex: 1 }} style={{ width: "100%", marginTop: 8, flex: 1 }}
contentContainerStyle={{ contentContainerStyle={{
alignItems: "center", alignItems: "center",
paddingBottom: 24, paddingBottom: 24,
// backgroundColor: "green",
}} }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
@@ -485,6 +618,12 @@ export default function HomeScreen() {
</View> </View>
</> </>
</DraggablePanel> </DraggablePanel>
<ShipSearchForm
initialValues={shipSearchFormData}
isOpen={shipSearchFormOpen}
onClose={() => setShipSearchFormOpen(false)}
onSubmit={handleOnSubmitSearchForm}
/>
</View> </View>
</GestureHandlerRootView> </GestureHandlerRootView>
); );

View File

@@ -1,4 +1,5 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
Pressable, Pressable,
@@ -34,14 +35,10 @@ export default function DraggablePanel({
children, children,
}: DraggablePanelProps) { }: DraggablePanelProps) {
const { height: screenHeight } = useWindowDimensions(); const { height: screenHeight } = useWindowDimensions();
const bottomOffset = useBottomTabBarHeight();
// Thêm chiều cao của bottom tab bar vào tính toán
const bottomOffset = 80; // 50 là chiều cao mặc định của tab bar
const minHeight = screenHeight * minHeightPct; const minHeight = screenHeight * minHeightPct;
const maxHeight = screenHeight * maxHeightPct; const maxHeight = screenHeight * maxHeightPct;
// State để quản lý icon
const [iconName, setIconName] = useState<"chevron-down" | "chevron-up">( const [iconName, setIconName] = useState<"chevron-down" | "chevron-up">(
initialState === "max" ? "chevron-down" : "chevron-up" initialState === "max" ? "chevron-down" : "chevron-up"
); );
@@ -115,17 +112,13 @@ export default function DraggablePanel({
? screenHeight - maxHeight - bottomOffset + 40 ? screenHeight - maxHeight - bottomOffset + 40
: screenHeight - minHeight - bottomOffset; : screenHeight - minHeight - bottomOffset;
translateY.value = withSpring( isExpanded.value = snapToMax;
targetY, runOnJS(notifyExpandedChange)(snapToMax);
{
damping: 20, translateY.value = withSpring(targetY, {
stiffness: 200, damping: 20,
}, stiffness: 50,
() => { });
"worklet";
isExpanded.value = snapToMax;
}
);
}); });
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => {
@@ -138,7 +131,7 @@ export default function DraggablePanel({
useAnimatedReaction( useAnimatedReaction(
() => { () => {
const currentHeight = screenHeight - translateY.value - bottomOffset; const currentHeight = screenHeight - translateY.value - bottomOffset;
return currentHeight > minHeight + 10; return currentHeight > minHeight;
}, },
(isCurrentlyExpanded) => { (isCurrentlyExpanded) => {
const newIcon = isCurrentlyExpanded ? "chevron-down" : "chevron-up"; const newIcon = isCurrentlyExpanded ? "chevron-down" : "chevron-up";
@@ -210,7 +203,7 @@ const styles = StyleSheet.create({
elevation: 10, elevation: 10,
}, },
header: { header: {
paddingTop: 12, paddingTop: 8,
paddingBottom: 8, paddingBottom: 8,
paddingHorizontal: 16, paddingHorizontal: 16,
alignItems: "center", alignItems: "center",

View File

@@ -20,16 +20,16 @@ export interface SelectOption {
} }
export interface SelectProps { export interface SelectProps {
value?: string | number; value?: string | number | (string | number)[];
defaultValue?: string | number; defaultValue?: string | number | (string | number)[];
options: SelectOption[]; options: SelectOption[];
onChange?: (value: string | number | undefined) => void; onChange?: (value: string | number | (string | number)[] | undefined) => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
loading?: boolean; loading?: boolean;
allowClear?: boolean; allowClear?: boolean;
showSearch?: boolean; showSearch?: boolean;
mode?: "single" | "multiple"; // multiple not implemented yet mode?: "single" | "multiple";
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
size?: "small" | "middle" | "large"; size?: "small" | "middle" | "large";
listStyle?: StyleProp<ViewStyle>; listStyle?: StyleProp<ViewStyle>;
@@ -38,7 +38,7 @@ export interface SelectProps {
/** /**
* Select * Select
* A Select component inspired by Ant Design, adapted for React Native. * A Select component inspired by Ant Design, adapted for React Native.
* Supports single selection, search, clear, loading, disabled states. * Supports single and multiple selection, search, clear, loading, disabled states.
*/ */
const Select: React.FC<SelectProps> = ({ const Select: React.FC<SelectProps> = ({
value, value,
@@ -55,16 +55,23 @@ const Select: React.FC<SelectProps> = ({
listStyle, listStyle,
size = "middle", size = "middle",
}) => { }) => {
const [selectedValue, setSelectedValue] = useState< const initialValue = value ?? defaultValue;
string | number | undefined const [selectedValues, setSelectedValues] = useState<(string | number)[]>(
>(value ?? defaultValue); Array.isArray(initialValue)
? initialValue
: initialValue
? [initialValue]
: []
);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [containerHeight, setContainerHeight] = useState(0); const [containerHeight, setContainerHeight] = useState(0);
const [textHeight, setTextHeight] = useState(0);
useEffect(() => { useEffect(() => {
setSelectedValue(value); const newVal = value ?? defaultValue;
}, [value]); setSelectedValues(Array.isArray(newVal) ? newVal : newVal ? [newVal] : []);
}, [value, defaultValue]);
const filteredOptions = showSearch const filteredOptions = showSearch
? options.filter((opt) => ? options.filter((opt) =>
@@ -72,17 +79,31 @@ const Select: React.FC<SelectProps> = ({
) )
: options; : options;
const selectedOption = options.find((opt) => opt.value === selectedValue);
const handleSelect = (val: string | number) => { const handleSelect = (val: string | number) => {
setSelectedValue(val); let newSelected: (string | number)[];
onChange?.(val); if (mode === "single") {
setIsOpen(false); newSelected = [val];
setSearchText(""); } else {
newSelected = selectedValues.includes(val)
? selectedValues.filter((v) => v !== val)
: [...selectedValues, val];
}
setSelectedValues(newSelected);
onChange?.(
mode === "single"
? newSelected.length > 0
? newSelected[0]
: undefined
: newSelected
);
if (mode === "single") {
setIsOpen(false);
setSearchText("");
}
}; };
const handleClear = () => { const handleClear = () => {
setSelectedValue(undefined); setSelectedValues([]);
onChange?.(undefined); onChange?.(undefined);
}; };
@@ -100,13 +121,26 @@ const Select: React.FC<SelectProps> = ({
? colors.backgroundSecondary ? colors.backgroundSecondary
: colors.surface; : colors.surface;
let displayText = placeholder;
if (selectedValues.length > 0) {
if (mode === "single") {
const opt = options.find((o) => o.value === selectedValues[0]);
displayText = opt?.label || placeholder;
} else {
const labels = selectedValues
.map((v) => options.find((o) => o.value === v)?.label)
.filter(Boolean);
displayText = labels.join(", ");
}
}
return ( return (
<View style={styles.wrapper}> <View style={styles.wrapper}>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.container, styles.container,
{ {
height: sz.height, height: Math.max(sz.height, textHeight + 16), // Add padding
paddingHorizontal: sz.paddingHorizontal, paddingHorizontal: sz.paddingHorizontal,
backgroundColor: selectBackgroundColor, backgroundColor: selectBackgroundColor,
borderColor: disabled ? colors.border : colors.primary, borderColor: disabled ? colors.border : colors.primary,
@@ -129,19 +163,19 @@ const Select: React.FC<SelectProps> = ({
fontSize: sz.fontSize, fontSize: sz.fontSize,
color: disabled color: disabled
? colors.textSecondary ? colors.textSecondary
: selectedValue : selectedValues.length > 0
? colors.text ? colors.text
: colors.textSecondary, : colors.textSecondary,
}, },
]} ]}
numberOfLines={1} onLayout={(e) => setTextHeight(e.nativeEvent.layout.height)}
> >
{selectedOption?.label || placeholder} {displayText}
</Text> </Text>
)} )}
</View> </View>
<View style={styles.suffix}> <View style={styles.suffix}>
{allowClear && selectedValue && !loading ? ( {allowClear && selectedValues.length > 0 && !loading ? (
<TouchableOpacity onPress={handleClear} style={styles.icon}> <TouchableOpacity onPress={handleClear} style={styles.icon}>
<AntDesign name="close" size={16} color={colors.textSecondary} /> <AntDesign name="close" size={16} color={colors.textSecondary} />
</TouchableOpacity> </TouchableOpacity>
@@ -193,7 +227,7 @@ const Select: React.FC<SelectProps> = ({
borderBottomColor: colors.separator, borderBottomColor: colors.separator,
}, },
item.disabled && styles.optionDisabled, item.disabled && styles.optionDisabled,
selectedValue === item.value && { selectedValues.includes(item.value) && {
backgroundColor: colors.primary + "20", // Add transparency to primary color backgroundColor: colors.primary + "20", // Add transparency to primary color
}, },
]} ]}
@@ -209,7 +243,7 @@ const Select: React.FC<SelectProps> = ({
item.disabled && { item.disabled && {
color: colors.textSecondary, color: colors.textSecondary,
}, },
selectedValue === item.value && { selectedValues.includes(item.value) && {
color: colors.primary, color: colors.primary,
fontWeight: "600", fontWeight: "600",
}, },
@@ -217,7 +251,7 @@ const Select: React.FC<SelectProps> = ({
> >
{item.label} {item.label}
</Text> </Text>
{selectedValue === item.value && ( {selectedValues.includes(item.value) && (
<AntDesign name="check" size={16} color={colors.primary} /> <AntDesign name="check" size={16} color={colors.primary} />
)} )}
</TouchableOpacity> </TouchableOpacity>

View File

@@ -91,8 +91,14 @@ const ShipSearchForm = (props: ShipSearchFormProps) => {
reset({ reset({
ship_name: props.initialValues.ship_name || "", ship_name: props.initialValues.ship_name || "",
reg_number: props.initialValues.reg_number || "", reg_number: props.initialValues.reg_number || "",
ship_length: [0, 100], ship_length: [
ship_power: [0, 100000], props.initialValues.ship_length?.[0] || 0,
props.initialValues.ship_length?.[1] || 100,
],
ship_power: [
props.initialValues.ship_power?.[0] || 0,
props.initialValues.ship_power?.[1] || 100000,
],
ship_type: props.initialValues.ship_type || "", ship_type: props.initialValues.ship_type || "",
alarm_list: props.initialValues.alarm_list || "", alarm_list: props.initialValues.alarm_list || "",
ship_group_id: props.initialValues.ship_group_id || "", ship_group_id: props.initialValues.ship_group_id || "",
@@ -129,7 +135,9 @@ const ShipSearchForm = (props: ShipSearchFormProps) => {
const onSubmitForm = (data: SearchShipResponse) => { const onSubmitForm = (data: SearchShipResponse) => {
props.onSubmit?.(data); props.onSubmit?.(data);
props.onClose(); console.log("Data: ", data);
// props.onClose();
}; };
const onReset = () => { const onReset = () => {
@@ -317,6 +325,7 @@ const ShipSearchForm = (props: ShipSearchFormProps) => {
value: type.id || 0, value: type.id || 0,
}))} }))}
placeholder="Chọn loại tàu" placeholder="Chọn loại tàu"
mode="multiple"
value={value} value={value}
onChange={onChange} onChange={onChange}
/> />
@@ -339,6 +348,7 @@ const ShipSearchForm = (props: ShipSearchFormProps) => {
value: type.value || "", value: type.value || "",
}))} }))}
placeholder="Chọn loại cảnh báo" placeholder="Chọn loại cảnh báo"
mode="multiple"
value={value} value={value}
onChange={onChange} onChange={onChange}
/> />
@@ -361,12 +371,14 @@ const ShipSearchForm = (props: ShipSearchFormProps) => {
value: group.id || "", value: group.id || "",
}))} }))}
placeholder="Chọn đội tàu" placeholder="Chọn đội tàu"
mode="multiple"
value={value} value={value}
onChange={onChange} onChange={onChange}
/> />
)} )}
/> />
</View> </View>
<View className="h-12"></View>
</View> </View>
</ScrollView> </ScrollView>

View File

@@ -290,12 +290,13 @@ export default function Slider({
{Object.entries(marks).map(([key, label]) => { {Object.entries(marks).map(([key, label]) => {
const val = Number(key); const val = Number(key);
const pos = getPositionFromValue(val); const pos = getPositionFromValue(val);
const leftPos = Math.max(0, Math.min(pos - 10, width - 40));
return ( return (
<Text <Text
key={key} key={key}
style={{ style={{
position: "absolute", position: "absolute",
left: pos - 10, left: leftPos,
fontSize: 12, fontSize: 12,
color: "#666", color: "#666",
textAlign: "center", textAlign: "center",

View File

@@ -11,7 +11,7 @@ export type TagStateCallbackPayload = {
isDisconected: boolean; // giữ đúng theo yêu cầu (1 'n') isDisconected: boolean; // giữ đúng theo yêu cầu (1 'n')
}; };
export type TagStateProps = { type TagStateProps = {
normalCount?: number; normalCount?: number;
warningCount?: number; warningCount?: number;
dangerousCount?: number; dangerousCount?: number;