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

View File

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

View File

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

View File

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

View File

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