This commit is contained in:
oneao committed 2026-04-22 22:05:07 +08:00
1 parent cc88205cba
commit bddbb49e19
8 files changed
+238 -180

No files matched your search

+49 -10
View File
@@ -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,
},
});
+3 -1
View File
@@ -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>
);
}
+17 -31
View File
@@ -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>
);
}
+4 -4
View File
@@ -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));