This commit is contained in:
oneao committed 2026-04-21 22:12:34 +08:00
1 parent b1348343c9
commit b7f2efc7c3
13 files changed
+662 -456

No files matched your search

@@ -1,25 +0,0 @@
import { memo } from "react";
import {
KeyboardAwareScrollView,
KeyboardAwareScrollViewProps,
} from "react-native-keyboard-controller";
import {
SCROLLABLE_TYPE,
createBottomSheetScrollableComponent,
type BottomSheetScrollViewMethods,
} from "@gorhom/bottom-sheet";
import type { BottomSheetScrollViewProps } from "@gorhom/bottom-sheet/src/components/bottomSheetScrollable/types";
import Reanimated from "react-native-reanimated";
const AnimatedScrollView = Reanimated.createAnimatedComponent(KeyboardAwareScrollView);
const BottomSheetScrollViewComponent = createBottomSheetScrollableComponent<
BottomSheetScrollViewMethods,
BottomSheetScrollViewProps
>(SCROLLABLE_TYPE.SCROLLVIEW, AnimatedScrollView);
const BottomSheetKeyboardAwareScrollView = memo(BottomSheetScrollViewComponent);
BottomSheetKeyboardAwareScrollView.displayName = "BottomSheetKeyboardAwareScrollView";
export default BottomSheetKeyboardAwareScrollView as (
props: BottomSheetScrollViewProps & KeyboardAwareScrollViewProps,
) => ReturnType<typeof BottomSheetKeyboardAwareScrollView>;
+67 -42
View File
@@ -1,54 +1,79 @@
import { BottomSheet, Button, TextField, Input, useBottomSheetAwareHandlers } from "heroui-native";
import { useState, useRef, useEffect } from "react";
import { Keyboard } from "react-native";
import React, { useState } from "react";
import { View, Text } from "react-native";
import { Button } from "heroui-native";
import DateRangePicker from "@/components/DateRangePicker";
import dayjs from "dayjs";
const BottomSheetTextInput = ({ isOpen }: { isOpen: boolean }) => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
const inputRef = useRef<any>(null);
export default function RangeDemo() {
const [start, setStart] = useState("");
const [end, setEnd] = useState("");
const [show, setShow] = useState(false);
useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
} else {
inputRef.current?.blur();
}
}, [isOpen]);
// 快捷选择
const setToday = () => {
const today = dayjs().format("YYYY-MM-DD");
setStart(today);
setEnd(today);
};
const setLast7Days = () => {
const end = dayjs();
const start = end.subtract(6, "day");
setStart(start.format("YYYY-MM-DD"));
setEnd(end.format("YYYY-MM-DD"));
};
const setThisMonth = () => {
const start = dayjs().startOf("month");
const end = dayjs().endOf("month");
setStart(start.format("YYYY-MM-DD"));
setEnd(end.format("YYYY-MM-DD"));
};
return (
<TextField>
<Input ref={inputRef} placeholder="Type here..." onFocus={onFocus} onBlur={onBlur} />
</TextField>
);
};
<View className="flex-1 p-4 gap-4">
<Text className="text-lg font-semibold">日期区间选择 Demo</Text>
export default function BottomSheetExample() {
const [isOpen, setIsOpen] = useState(false);
{/* 当前选择展示 */}
<Text className="text-gray-700">开始:{start || "--"}</Text>
<Text className="text-gray-700">结束:{end || "--"}</Text>
return (
<BottomSheet
isOpen={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
{/* 打开选择器 */}
<Button onPress={() => setShow(true)}>
{start && end ? `${start} ~ ${end}` : "选择日期区间"}
</Button>
if (!open) {
Keyboard.dismiss(); // ✅ 关闭时收键盘
}
}}
animation="disable-all"
>
<BottomSheet.Trigger asChild>
<Button variant="secondary" className="mt-30">
Open Bottom Sheet
{/* 快捷选择 */}
<View className="flex-row gap-2 flex-wrap">
<Button size="sm" variant="tertiary" onPress={setToday}>
今天
</Button>
</BottomSheet.Trigger>
<Button size="sm" variant="tertiary" onPress={setLast7Days}>
最近7天
</Button>
<Button size="sm" variant="tertiary" onPress={setThisMonth}>
本月
</Button>
</View>
<BottomSheet.Portal>
<BottomSheet.Overlay />
{/* RangePicker */}
<DateRangePicker
startDate={start}
endDate={end}
show={show}
onClose={() => setShow(false)}
onConfirm={(s, e) => {
// 简单校验(开始不能大于结束)
if (dayjs(s).isAfter(dayjs(e))) {
console.warn("开始时间不能大于结束时间");
return;
}
<BottomSheet.Content enableBlurKeyboardOnGesture keyboardBlurBehavior="restore">
<BottomSheetTextInput isOpen={isOpen} />
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
setStart(s);
setEnd(e);
setShow(false);
}}
/>
</View>
);
}
@@ -1,39 +0,0 @@
import { BottomSheet, Button, TextField, Input, useBottomSheetAwareHandlers } from "heroui-native";
import { useState } from "react";
const BottomSheetTextInput = () => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
return (
<TextField>
<Input placeholder="Type here..." onFocus={onFocus} onBlur={onBlur} />
</TextField>
);
};
export default function BottomSheetExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<BottomSheet
isOpen={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
}}
>
<BottomSheet.Trigger asChild>
<Button variant="secondary" className="mt-30">
Open Bottom Sheet
</Button>
</BottomSheet.Trigger>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content keyboardBlurBehavior="restore">
<BottomSheetTextInput />
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
);
}
+6 -12
View File
@@ -3,11 +3,7 @@ import LayoutView from "@/components/layout/view";
import { loadDataApi } from "@/request/api";
import { View, Text } from "react-native";
import { useFamilyStore } from "@/store";
import {
HouseHeartIcon,
LayoutGridIcon,
LayersIcon,
} from "lucide-react-native";
import { LayoutGridIcon, LayersIcon } from "lucide-react-native";
import { IconAppFinance, IconAppClothes } from "@/components/iconfont";
import { PressableFeedback } from "heroui-native";
@@ -71,15 +67,13 @@ export default function Home() {
<LayoutView>
<LayoutView.ScrollView>
<View className="items-center justify-center">
<Text className="text-4xl font-bold tracking-tight text-gray-900">{family.name}</Text>
<View className="flex-row items-center justify-center gap-2">
<Text className="text-4xl font-bold tracking-tight text-gray-900">
{family.name || "-"}
</Text>
</View>
<View className="items-center mt-3">
<View className="flex-row items-center gap-2 mb-1">
<HouseHeartIcon size={18} color="#6a7282" />
<Text className="text-sm text-gray-500">家庭已陪伴你</Text>
</View>
<View className="flex-row items-center gap-2">
<Text className="text-3xl font-bold text-accent">{duration.days}</Text>
<Text className="text-gray-400">天</Text>
@@ -1,10 +1,24 @@
import React from "react";
import { View, Text } from "react-native";
import DatePicker from "@/components/DatePicker";
import { useState } from "react";
import { Button } from "heroui-native";
export default function Star() {
const [date, setDate] = useState("");
const [showDate, setShowDate] = useState(false);
return (
<View className="flex-1">
<Text className="text-base text-black">Star</Text>
<Button
onPress={() => {
setShowDate(!showDate);
}}
>
测试
</Button>
<DatePicker value={date} show={showDate} />
</View>
);
}
+182 -40
View File
@@ -1,11 +1,17 @@
import { useEffect } from "react";
import { Stack } from "expo-router";
import { Image, Text, View } from "react-native";
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
import { IconQq } from "@/components/iconfont";
import { IconQq, IconWeixin } from "@/components/iconfont";
import { useState } from "react";
import { FadeIn } from "react-native-reanimated";
import { useUserStore, useFamilyStore } from "@/store";
import { useRouter } from "expo-router";
import ExpoQQ from "expo-qq";
import ExpoWeChat from "expo-wechat";
import { useEvent } from "expo";
import { toast } from "sonner-native";
import { loginQqApi, loginWxApi } from "@/request/api";
export default function Login() {
const router = useRouter();
@@ -14,49 +20,190 @@ export default function Login() {
const [loading, setLoading] = useState(false);
const [isAgreed, setIsAgreed] = useState(true);
const [form, setForm] = useState({
type: 0,
openid: "",
code: "",
});
/**
* {"code": 1000, "data": {"avatar": "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
* "nickname": "Zhang", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo3OTY0NTY3MTI3ODU5ODl9LCJpYXQiOjE3NzY1ODU2MDR9.ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY", "userId": 796456712785989}, "isSuccess": true, "message": "success", "track_id": "djIRo23SZlA160zBTH2DX-kxeuLApKTNyZdxqqauSMk"}
*/
// 登录
const onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
const authResult = useEvent(ExpoWeChat, "onAuthResult");
function handleTest() {
useEffect(() => {
ExpoQQ.init("102826474", "");
ExpoWeChat.registerApp("wxdab3e21a1f7e392f", "");
}, []);
// 微信登录
async function handleLoginWechat() {
if (loading) {
return;
}
setLoading(true);
// 检查微信是否安装
const isInstalled = await ExpoWeChat.isWXAppInstalled();
if (!isInstalled) {
toast.warning(onLoginFinished?.errorMessage || "请先安装微信");
setLoading(false);
return;
}
setForm((val) => ({
...val,
type: 0,
}));
ExpoWeChat.sendAuthRequest("snsapi_userinfo", "state123");
}
// QQ 登录
async function handleLoginQQ() {
if (loading) {
return;
}
setLoading(true);
setTimeout(() => {
requestAnimationFrame(() => {
userStore.setUserInfo({
id: 796541901389893,
token:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo3OTY0NTY3MTI3ODU5ODl9LCJpYXQiOjE3NzY1ODU2MDR9.ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY",
nickname: "Zhang",
avatar:
"http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
});
userStore.setAuthExpired(false);
console.log(familyStore.inFamily());
// if (familyStore.inFamily()) {
// router.replace("/(tabs)/home");
// } else {
// router.replace("/family/entry");
// return;
// }
familyStore.setCurrentFamily({
family_id: 797139042881605,
});
router.replace("/(tabs)/home");
});
setLoading(false);
}, 500);
setForm((val) => ({
...val,
type: 1,
}));
await ExpoQQ.login(["get_user_info"]);
}
useEffect(() => {
if (!onLoginFinished) {
return;
}
if (!onLoginFinished.success) {
toast.warning(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
setLoading(false);
return;
}
const fetchUserInfo = async () => {
try {
const tokenInfo = await ExpoQQ.getLoginTokenInfo();
if (!tokenInfo.openId) {
toast.warning(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
setLoading(false);
return;
}
setForm((val) => ({
...val,
openid: String(tokenInfo.openId),
}));
await ExpoQQ.sendGetUserInfoReq();
} catch (e) {
console.log(e);
setLoading(false);
}
};
fetchUserInfo();
}, [onLoginFinished]);
// QQ登录
useEffect(() => {
const handleLogin = async () => {
if (!onGetUserInfo) {
setLoading(false);
return;
}
try {
const loginRes = await loginQqApi({
openid: form.openid,
nickname: onGetUserInfo.nickname || "",
avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
});
if (loginRes.isSuccess) {
requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
userStore.setAuthExpired(false);
if (loginRes.data.family_id === 0) {
router.replace("/family/entry");
} else {
familyStore.setCurrentFamily({
family_id: loginRes.data.family_id,
});
router.replace("/(tabs)/home");
}
});
}
} catch (err) {
console.error("登录接口出错", err);
} finally {
setLoading(false);
}
};
handleLogin();
}, [onGetUserInfo]);
// 微信登录
useEffect(() => {
if (!authResult) return;
const handleLogin = async () => {
if (authResult.errorCode === 0) {
try {
const loginRes = await loginWxApi({
code: authResult.code,
});
if (loginRes.isSuccess) {
requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
userStore.setAuthExpired(false);
if (loginRes.data.family_id === 0) {
router.replace("/family/entry");
} else {
familyStore.setCurrentFamily({
family_id: loginRes.data.family_id,
});
router.replace("/(tabs)/home");
}
});
}
} catch (err) {
console.error("登录接口出错", err);
} finally {
setLoading(false);
}
} else {
console.log("授权失败:", authResult.errorMessage);
setLoading(false);
}
};
handleLogin();
}, [authResult]);
return (
<>
<Stack.Screen
@@ -75,7 +222,7 @@ export default function Login() {
</View>
<View className="flex flex-col gap-4">
{/* <Button onPress={handleLoginWechat}>
<Button onPress={handleLoginWechat}>
{loading ? (
<Spinner entering={FadeIn.delay(50)} color="white" />
) : (
@@ -87,11 +234,6 @@ export default function Login() {
<Button variant="outline" onPress={handleLoginQQ}>
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
<Button.Label>QQ</Button.Label>
</Button> */}
<Button variant="outline" onPress={handleTest}>
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
<Button.Label>Test</Button.Label>
</Button>
</View>
+119
View File
@@ -0,0 +1,119 @@
import { Stack } from "expo-router";
import { Image, Text, View } from "react-native";
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
import { IconQq } from "@/components/iconfont";
import { useState } from "react";
import { FadeIn } from "react-native-reanimated";
import { useUserStore, useFamilyStore } from "@/store";
import { useRouter } from "expo-router";
export default function Login() {
const router = useRouter();
const userStore = useUserStore();
const familyStore = useFamilyStore();
const [loading, setLoading] = useState(false);
const [isAgreed, setIsAgreed] = useState(true);
/**
* {"code": 1000, "data": {"avatar": "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100", "family_id": 797225979793477, "nickname": "Zhang", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJfaWQiOjc5NjU0MTkwMTM4OTg5M30sImlhdCI6MTc3Njc3MTcyOX0.JldxjA-0u2hza3qNx20xgUyVBVVV6hQuVuTW1GcJyr8", "userId": 796541901389893}, "isSuccess": true, "message": "success", "track_id": "hI5gEsPHu1LGwkQaJ6lf1dDQby0rzxGVJj0KcNC3Nxw"}
*/
function handleTest() {
if (loading) {
return;
}
setLoading(true);
setTimeout(() => {
requestAnimationFrame(() => {
userStore.setUserInfo({
id: 796541901389893,
token:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJfaWQiOjc5NjU0MTkwMTM4OTg5M30sImlhdCI6MTc3Njc3MTcyOX0.JldxjA-0u2hza3qNx20xgUyVBVVV6hQuVuTW1GcJyr8",
nickname: "Zhang",
avatar:
"http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
});
userStore.setAuthExpired(false);
console.log(familyStore.inFamily());
// if (familyStore.inFamily()) {
// router.replace("/(tabs)/home");
// } else {
// router.replace("/family/entry");
// return;
// }
familyStore.setCurrentFamily({
family_id: 797225979793477,
});
router.replace("/(tabs)/home");
});
setLoading(false);
}, 500);
}
return (
<>
<Stack.Screen
options={{
animation: "fade",
}}
/>
<View className="flex-1 bg-white px-6 justify-center">
<View className="items-center mb-12">
<Image
source={require("@/static/images/logo.png")}
className="w-20 h-20 mb-4"
resizeMode="contain"
/>
<Text className="text-xl font-semibold text-gray-900">Welcome to Allapp</Text>
</View>
<View className="flex flex-col gap-4">
{/* <Button onPress={handleLoginWechat}>
{loading ? (
<Spinner entering={FadeIn.delay(50)} color="white" />
) : (
<IconWeixin color="white" />
)}
<Button.Label>WeChat</Button.Label>
</Button>
<Button variant="outline" onPress={handleLoginQQ}>
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
<Button.Label>QQ</Button.Label>
</Button> */}
<Button variant="outline" onPress={handleTest}>
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
<Button.Label>Test</Button.Label>
</Button>
</View>
<ControlField
isSelected={isAgreed}
onSelectedChange={setIsAgreed}
className="items-start mt-10"
>
<ControlField.Indicator>
<Checkbox className="w-5 h-5 rounded-sm" />
</ControlField.Indicator>
<View className="flex-row flex-wrap flex-1 gap-1">
<Text className="text-sm text-muted">登录即表示你同意我们的</Text>
<LinkButton size="sm">
<LinkButton.Label className="text-accent">用户协议</LinkButton.Label>
</LinkButton>
<Text className="text-sm text-muted">和</Text>
<LinkButton size="sm">
<LinkButton.Label className="text-accent">隐私政策</LinkButton.Label>
</LinkButton>
</View>
</ControlField>
</View>
</>
);
}
-262
View File
@@ -1,262 +0,0 @@
import { useEffect } from "react";
import { Stack } from "expo-router";
import { Image, Text, View } from "react-native";
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
import { IconQq, IconWeixin } from "@/components/iconfont";
import { useState } from "react";
import { FadeIn } from "react-native-reanimated";
import { useUserStore, useFamilyStore } from "@/store";
import { useRouter } from "expo-router";
import ExpoQQ from "expo-qq";
import ExpoWeChat from "expo-wechat";
import { useEvent } from "expo";
import { toast } from "sonner-native";
import { loginQqApi, loginWxApi } from "@/request/api";
export default function Login() {
const router = useRouter();
const userStore = useUserStore();
const familyStore = useFamilyStore();
const [loading, setLoading] = useState(false);
const [isAgreed, setIsAgreed] = useState(true);
const [form, setForm] = useState({
type: 0,
openid: "",
code: "",
});
// 登录
const onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
const authResult = useEvent(ExpoWeChat, "onAuthResult");
useEffect(() => {
ExpoQQ.init("102826474", "");
ExpoWeChat.registerApp("wxdab3e21a1f7e392f", "");
}, []);
// 微信登录
async function handleLoginWechat() {
if (loading) {
return;
}
setLoading(true);
// 检查微信是否安装
const isInstalled = await ExpoWeChat.isWXAppInstalled();
if (!isInstalled) {
toast.warning(onLoginFinished?.errorMessage || "请先安装微信");
setLoading(false);
return;
}
setForm((val) => ({
...val,
type: 0,
}));
ExpoWeChat.sendAuthRequest("snsapi_userinfo", "state123");
}
// QQ 登录
async function handleLoginQQ() {
if (loading) {
return;
}
setLoading(true);
setForm((val) => ({
...val,
type: 1,
}));
await ExpoQQ.login(["get_user_info"]);
}
useEffect(() => {
if (!onLoginFinished) {
return;
}
if (!onLoginFinished.success) {
toast.warning(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
setLoading(false);
return;
}
const fetchUserInfo = async () => {
try {
const tokenInfo = await ExpoQQ.getLoginTokenInfo();
if (!tokenInfo.openId) {
toast.warning(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
setLoading(false);
return;
}
setForm((val) => ({
...val,
openid: String(tokenInfo.openId),
}));
await ExpoQQ.sendGetUserInfoReq();
} catch (e) {
console.log(e);
setLoading(false);
}
};
fetchUserInfo();
}, [onLoginFinished]);
// QQ登录
useEffect(() => {
const handleLogin = async () => {
if (!onGetUserInfo) {
setLoading(false);
return;
}
try {
const loginRes = await loginQqApi({
openid: form.openid,
nickname: onGetUserInfo.nickname || "",
avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
});
if (loginRes.isSuccess) {
requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
userStore.setAuthExpired(false);
if (loginRes.data.family_id === 0) {
router.replace("/family/entry");
} else {
familyStore.setCurrentFamily({
family_id: loginRes.data.family_id,
});
router.replace("/(tabs)/home");
}
});
}
} catch (err) {
console.error("登录接口出错", err);
} finally {
setLoading(false);
}
};
handleLogin();
}, [onGetUserInfo]);
// 微信登录
useEffect(() => {
if (!authResult) return;
const handleLogin = async () => {
if (authResult.errorCode === 0) {
try {
const loginRes = await loginWxApi({
code: authResult.code,
});
if (loginRes.isSuccess) {
requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
userStore.setAuthExpired(false);
if (loginRes.data.family_id === 0) {
router.replace("/family/entry");
} else {
familyStore.setCurrentFamily({
family_id: loginRes.data.family_id,
});
router.replace("/(tabs)/home");
}
});
}
} catch (err) {
console.error("登录接口出错", err);
} finally {
setLoading(false);
}
} else {
console.log("授权失败:", authResult.errorMessage);
setLoading(false);
}
};
handleLogin();
}, [authResult]);
return (
<>
<Stack.Screen
options={{
animation: "fade",
}}
/>
<View className="flex-1 bg-white px-6 justify-center">
<View className="items-center mb-12">
<Image
source={require("@/static/images/logo.png")}
className="w-20 h-20 mb-4"
resizeMode="contain"
/>
<Text className="text-xl font-semibold text-gray-900">Welcome to Allapp</Text>
</View>
<View className="flex flex-col gap-4">
<Button onPress={handleLoginWechat}>
{loading ? (
<Spinner entering={FadeIn.delay(50)} color="white" />
) : (
<IconWeixin color="white" />
)}
<Button.Label>WeChat</Button.Label>
</Button>
<Button variant="outline" onPress={handleLoginQQ}>
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
<Button.Label>QQ</Button.Label>
</Button>
</View>
<ControlField
isSelected={isAgreed}
onSelectedChange={setIsAgreed}
className="items-start mt-10"
>
<ControlField.Indicator>
<Checkbox className="w-5 h-5 rounded-sm" />
</ControlField.Indicator>
<View className="flex-row flex-wrap flex-1 gap-1">
<Text className="text-sm text-muted">登录即表示你同意我们的</Text>
<LinkButton size="sm">
<LinkButton.Label className="text-accent">用户协议</LinkButton.Label>
</LinkButton>
<Text className="text-sm text-muted">和</Text>
<LinkButton size="sm">
<LinkButton.Label className="text-accent">隐私政策</LinkButton.Label>
</LinkButton>
</View>
</ControlField>
</View>
</>
);
}
@@ -0,0 +1,97 @@
import { DatePicker as YzDatePicker } from "@yz1311/react-native-wheel-picker";
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";
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 {
value: Date | string;
show?: boolean;
onClose?: () => void;
onConfirm?: (value: string) => void;
mode?: DatePickerMode;
format?: DateFormatType;
}
const defaultFormatMap: Record<DatePickerMode, DateFormatType> = {
year: "YYYY",
month: "YYYY-MM",
date: "YYYY-MM-DD",
time: "HH:mm:ss",
datetime: "YYYY-MM-DD HH:mm",
};
export default function DatePicker({
value,
show = false,
onClose,
onConfirm,
mode = "date",
format,
}: DatePickerProps) {
const [tempDate, setTempDate] = useState<Date>(toDate(value));
const [isOpen, setIsOpen] = useState(false);
const finalFormat = format || defaultFormatMap[mode];
useEffect(() => {
if (show) {
setTempDate(toDate(value));
setIsOpen(true);
} else {
setIsOpen(false);
}
}, [show, value]);
const handleClose = () => {
setIsOpen(false);
onClose?.();
};
const handleCancel = () => {
setTempDate(toDate(value));
handleClose();
};
const handleConfirm = () => {
const formatted = dayjs(tempDate).format(finalFormat);
onConfirm?.(formatted);
handleClose();
};
return (
<BottomSheet isOpen={isOpen} 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)}
/>
</View>
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
);
}
@@ -0,0 +1,97 @@
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>
);
}