u
This commit is contained in:
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>;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user