u
This commit is contained in:
1 parent
cc88205cba
commit
bddbb49e19
8 files changed
+238
-180
No files matched your search
@@ -1,13 +1,52 @@
|
||||
import React from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { Text } from "react-native";
|
||||
import { useState } from "react";
|
||||
import { Alert, Button, Image, View, StyleSheet } from "react-native";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
|
||||
export default function ImagePickerExample() {
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
|
||||
const pickImage = async () => {
|
||||
// No permissions request is necessary for launching the image library.
|
||||
// Manually request permissions for videos on iOS when `allowsEditing` is set to `false`
|
||||
// and `videoExportPreset` is `'Passthrough'` (the default), ideally before launching the picker
|
||||
// so the app users aren't surprised by a system dialog after picking a video.
|
||||
// See "Invoke permissions for videos" sub section for more details.
|
||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
|
||||
if (!permissionResult.granted) {
|
||||
Alert.alert("Permission required", "Permission to access the media library is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
let result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ["images", "videos"],
|
||||
allowsEditing: true,
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
|
||||
if (!result.canceled) {
|
||||
setImage(result.assets[0].uri);
|
||||
}
|
||||
};
|
||||
|
||||
export default function Chat() {
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<Text className="text-base text-black">Hello</Text>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
<View style={styles.container}>
|
||||
<Button title="Pick an image from camera roll" onPress={pickImage} />
|
||||
{image && <Image source={{ uri: image }} style={styles.image} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
image: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
},
|
||||
});
|
||||
@@ -65,7 +65,9 @@ export default function RootLayout() {
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
|
||||
{/* {!isDev && <StatusBar style={isDark ? "light" : "dark"} />} */}
|
||||
|
||||
<StatusBar style={"auto"} />
|
||||
</HeroUINativeProvider>
|
||||
</KeyboardProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { DateRangePicker as YzDateRangePicker } from "@yz1311/react-native-wheel-picker";
|
||||
import { BottomSheet, Button } from "heroui-native";
|
||||
import { View, Text } from "react-native";
|
||||
import dayjs from "dayjs";
|
||||
import { toDate } from "@/utils/time";
|
||||
|
||||
interface DateRangePickerProps {
|
||||
startDate: Date | string;
|
||||
endDate: Date | string;
|
||||
show?: boolean;
|
||||
onClose?: () => void;
|
||||
onConfirm?: (start: string, end: string) => void;
|
||||
}
|
||||
|
||||
export default function DateRangePicker({
|
||||
startDate,
|
||||
endDate,
|
||||
show = false,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: DateRangePickerProps) {
|
||||
const [tempStart, setTempStart] = useState<Date | undefined>(() => toDate(startDate));
|
||||
const [tempEnd, setTempEnd] = useState<Date | undefined>(() => toDate(endDate));
|
||||
|
||||
useEffect(() => {
|
||||
setTempStart(toDate(startDate));
|
||||
setTempEnd(toDate(endDate));
|
||||
}, [startDate, endDate]);
|
||||
|
||||
const isOpen = show;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setTempStart(toDate(startDate));
|
||||
setTempEnd(toDate(endDate));
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
const start = dayjs(tempStart).format("YYYY-MM-DD");
|
||||
const end = dayjs(tempEnd).format("YYYY-MM-DD");
|
||||
|
||||
onConfirm?.(start, end);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const key = useMemo(
|
||||
() => `${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}`,
|
||||
[startDate, endDate],
|
||||
);
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(v) => {
|
||||
if (!v) handleClose();
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content>
|
||||
<View style={{ width: "100%" }}>
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center justify-between w-full">
|
||||
<Button size="sm" variant="tertiary" onPress={handleCancel}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
<Text className="text-gray-800 text-base font-medium">选择日期区间</Text>
|
||||
|
||||
<Button size="sm" onPress={handleConfirm}>
|
||||
确认
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* Range Picker */}
|
||||
<YzDateRangePicker
|
||||
key={`${dayjs(startDate).valueOf()}_${dayjs(endDate).valueOf()}`}
|
||||
showHeader={false}
|
||||
startDate={tempStart}
|
||||
endDate={tempEnd}
|
||||
onValueChange={(start: Date, end: Date) => {
|
||||
setTempStart(start);
|
||||
setTempEnd(end);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,11 @@ import dayjs from "dayjs";
|
||||
import { BottomSheet, Button } from "heroui-native";
|
||||
import { useState, useEffect } from "react";
|
||||
import { toDate } from "@/utils/time";
|
||||
import { View, Text } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { CheckIcon } from "lucide-react-native";
|
||||
|
||||
export type DatePickerMode = "year" | "month" | "date" | "time" | "datetime";
|
||||
|
||||
export type DateFormatType = "YYYY" | "YYYY-MM" | "YYYY-MM-DD" | "HH:mm:ss" | "YYYY-MM-DD HH:mm";
|
||||
|
||||
interface DatePickerProps {
|
||||
@@ -25,7 +27,7 @@ const defaultFormatMap: Record<DatePickerMode, DateFormatType> = {
|
||||
datetime: "YYYY-MM-DD HH:mm",
|
||||
};
|
||||
|
||||
export default function DatePicker({
|
||||
export default function DateTime({
|
||||
value,
|
||||
show = false,
|
||||
onClose,
|
||||
@@ -33,62 +35,46 @@ export default function DatePicker({
|
||||
mode = "date",
|
||||
format,
|
||||
}: DatePickerProps) {
|
||||
const [tempDate, setTempDate] = useState<Date>(toDate(value));
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const finalFormat = format ?? defaultFormatMap[mode] ?? "YYYY-MM-DD";
|
||||
|
||||
const finalFormat = format || defaultFormatMap[mode];
|
||||
const [tempDate, setTempDate] = useState<Date>(toDate(value) ?? new Date());
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTempDate(toDate(value));
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
setTempDate(toDate(value) ?? new Date());
|
||||
}
|
||||
}, [show, value]);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setTempDate(toDate(value));
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
const formatted = dayjs(tempDate).format(finalFormat);
|
||||
onConfirm?.(formatted);
|
||||
if (!tempDate) return;
|
||||
|
||||
onConfirm?.(dayjs(tempDate).format(finalFormat));
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet isOpen={isOpen} onOpenChange={(v) => !v && handleClose()}>
|
||||
<BottomSheet isOpen={show} onOpenChange={(v) => !v && handleClose()}>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content>
|
||||
<View style={{ width: "100%", alignItems: "center" }}>
|
||||
<View className="flex-row items-center justify-between w-full">
|
||||
<Button size="sm" variant="tertiary" onPress={handleCancel}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
<Text className="text-gray-800 text-base font-medium">选择时间</Text>
|
||||
|
||||
<Button size="sm" onPress={handleConfirm}>
|
||||
确认
|
||||
</Button>
|
||||
</View>
|
||||
<YzDatePicker
|
||||
showHeader={false}
|
||||
date={tempDate}
|
||||
mode={mode}
|
||||
isModal={false}
|
||||
onPickerCancel={handleCancel}
|
||||
onPickerConfirm={handleConfirm}
|
||||
onDateChange={(d: Date) => setTempDate(d)}
|
||||
/>
|
||||
|
||||
<Button className="w-full" onPress={handleConfirm}>
|
||||
<CheckIcon color="#fff" />
|
||||
<Button.Label>确认</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { DatePicker as YzDatePicker } from "@yz1311/react-native-wheel-picker";
|
||||
import { BottomSheet, Button } from "heroui-native";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import dayjs from "dayjs";
|
||||
import { toDate } from "@/utils/time";
|
||||
import { CheckIcon } from "lucide-react-native";
|
||||
|
||||
interface DateRangePickerProps {
|
||||
startDate?: Date | string;
|
||||
endDate?: Date | string;
|
||||
show?: boolean;
|
||||
onClose?: () => void;
|
||||
onConfirm?: (start: string, end: string) => void;
|
||||
}
|
||||
|
||||
export default function DateTimeRange({
|
||||
startDate,
|
||||
endDate,
|
||||
show = false,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: DateRangePickerProps) {
|
||||
const [tempStart, setTempStart] = useState<Date | undefined>(undefined);
|
||||
const [tempEnd, setTempEnd] = useState<Date | undefined>(undefined);
|
||||
|
||||
const [active, setActive] = useState<"start" | "end">("start");
|
||||
|
||||
// ✅ 每次打开重置
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTempStart(toDate(startDate) ?? new Date());
|
||||
setTempEnd(toDate(endDate) ?? undefined);
|
||||
setActive("start");
|
||||
}
|
||||
}, [show, startDate, endDate]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!tempStart) return;
|
||||
|
||||
onConfirm?.(
|
||||
dayjs(tempStart).format("YYYY-MM-DD"),
|
||||
tempEnd ? dayjs(tempEnd).format("YYYY-MM-DD") : "",
|
||||
);
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// ❗ start 修改 → 清空 end
|
||||
const handleStartChange = (date: Date) => {
|
||||
setTempStart(date);
|
||||
setTempEnd(undefined);
|
||||
};
|
||||
|
||||
// ❗ end 修改
|
||||
const handleEndChange = (date: Date) => {
|
||||
if (!tempStart) return;
|
||||
setTempEnd(date);
|
||||
};
|
||||
|
||||
const formatDate = (date?: Date) => (date ? dayjs(date).format("YYYY-MM-DD") : "请选择");
|
||||
|
||||
return (
|
||||
<BottomSheet isOpen={show} onOpenChange={(v) => !v && handleClose()}>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content>
|
||||
<View className="w-full items-center">
|
||||
{/* 顶部选择区 */}
|
||||
<View className="flex-row items-center justify-between w-full">
|
||||
{/* start */}
|
||||
<TouchableOpacity className="flex-1 items-center" onPress={() => setActive("start")}>
|
||||
<Text>开始时间</Text>
|
||||
<Text
|
||||
className={`text-base ${
|
||||
active === "start" ? "text-accent font-bold" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{formatDate(tempStart)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View className="w-px h-10 bg-gray-300 mx-2" />
|
||||
|
||||
{/* end */}
|
||||
<TouchableOpacity
|
||||
className="flex-1 items-center"
|
||||
style={{ opacity: tempStart ? 1 : 0.4 }}
|
||||
disabled={!tempStart}
|
||||
onPress={() => {
|
||||
if (!tempStart) return;
|
||||
|
||||
// ✅ 核心优化:未选 end → 自动补 start
|
||||
if (!tempEnd) {
|
||||
setTempEnd(tempStart);
|
||||
}
|
||||
|
||||
setActive("end");
|
||||
}}
|
||||
>
|
||||
<Text>结束时间</Text>
|
||||
<Text
|
||||
className={`text-base ${
|
||||
active === "end" ? "text-accent font-bold" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{formatDate(tempEnd)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Picker */}
|
||||
{active === "start" ? (
|
||||
<YzDatePicker
|
||||
showHeader={false}
|
||||
mode="date"
|
||||
date={tempStart ?? new Date()}
|
||||
onDateChange={handleStartChange}
|
||||
/>
|
||||
) : (
|
||||
<YzDatePicker
|
||||
showHeader={false}
|
||||
mode="date"
|
||||
date={tempEnd ?? tempStart ?? new Date()}
|
||||
minDate={tempStart}
|
||||
onDateChange={handleEndChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* confirm */}
|
||||
<Button className="w-full" onPress={handleConfirm}>
|
||||
<CheckIcon color="#fff" />
|
||||
<Button.Label>确认</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
@@ -33,14 +33,14 @@ export function getCurrentYear(): string {
|
||||
}
|
||||
|
||||
/** 字符串 / 数字 / Date → Date(无效返回 null) */
|
||||
export function toDate(input: string | number | Date | undefined | null): Date {
|
||||
if (!input) return new Date();
|
||||
export function toDate(input: string | number | Date | undefined | null): Date | undefined {
|
||||
if (!input) return undefined;
|
||||
|
||||
if (input instanceof Date) return input;
|
||||
|
||||
const d = dayjs(input);
|
||||
return d.isValid() ? d.toDate() : new Date();
|
||||
return d.isValid() ? d.toDate() : undefined;
|
||||
}
|
||||
|
||||
/** Date → YYYY-MM-DD */
|
||||
export function toDateString(date: string | number | Date): string {
|
||||
const d = dayjs(toDate(date));
|
||||
|
||||
Reference in new issue
Block a user