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

+2 -1
View File
@@ -20,6 +20,7 @@
"@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33",
"@yz1311/react-native-wheel-picker": "^0.4.5",
"alova": "^3.5.1",
"axios": "^1.15.0",
"dayjs": "^1.11.20",
@@ -50,6 +51,7 @@
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0",
"react-native-image-crop-picker": "^0.51.1",
"react-native-keyboard-controller": "1.20.7",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
@@ -69,7 +71,6 @@
},
"devDependencies": {
"@types/react": "~19.2.2",
"@yz1311/react-native-wheel-picker": "^0.4.5",
"babel-preset-expo": "^55.0.16",
"oxfmt": "^0.44.0",
"oxlint": "^1.59.0",
+18 -36
View File
@@ -29,6 +29,9 @@ importers:
'@react-navigation/native':
specifier: ^7.1.33
version: 7.2.2(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
'@yz1311/react-native-wheel-picker':
specifier: ^0.4.5
version: 0.4.5(deprecated-react-native-prop-types@5.0.0)(moment@2.30.1)(react-native-modal@14.0.0-rc.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
alova:
specifier: ^3.5.1
version: 3.5.1
@@ -119,6 +122,9 @@ importers:
react-native-gesture-handler:
specifier: ~2.30.0
version: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-image-crop-picker:
specifier: ^0.51.1
version: 0.51.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-keyboard-controller:
specifier: 1.20.7
version: 1.20.7(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -171,9 +177,6 @@ importers:
'@types/react':
specifier: ~19.2.2
version: 19.2.14
'@yz1311/react-native-wheel-picker':
specifier: ^0.4.5
version: 0.4.5(deprecated-react-native-prop-types@5.0.0)(moment@2.30.1)(react-native-modal@14.0.0-rc.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
babel-preset-expo:
specifier: ^55.0.16
version: 55.0.17(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@55.0.15)(react-refresh@0.14.2)
@@ -1028,56 +1031,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.44.0':
resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.44.0':
resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.44.0':
resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.44.0':
resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.44.0':
resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.44.0':
resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.44.0':
resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.44.0':
resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==}
@@ -1150,56 +1145,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.60.0':
resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.60.0':
resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.60.0':
resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.60.0':
resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.60.0':
resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.60.0':
resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.60.0':
resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.60.0':
resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==}
@@ -1621,28 +1608,24 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
@@ -2601,7 +2584,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
@@ -2937,84 +2920,72 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.31.1:
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
@@ -3583,6 +3554,12 @@ packages:
react-native: '*'
react-native-svg: '>=9.7.0'
react-native-image-crop-picker@0.51.1:
resolution: {integrity: sha512-GIFRyXJgv1dPceKd/hraK9q9V38v45rSg2ONR6RiSePcOJemkHpc/PMU86pq6lWPilDYHSxbZmea2pNMk85ayw==}
peerDependencies:
react: '*'
react-native: '*'
react-native-is-edge-to-edge@1.2.1:
resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==}
peerDependencies:
@@ -8092,6 +8069,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
react-native-image-crop-picker@0.51.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
react-native-is-edge-to-edge@1.2.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
+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));