This commit is contained in:
oneao committed 2026-04-19 21:30:51 +08:00
1 parent b6415c0d65
commit 89aaef1d6b
29 files changed
+1118 -277

No files matched your search

@@ -0,0 +1,25 @@
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>;
+47 -36
View File
@@ -1,43 +1,54 @@
import { Button, Toast, useToast, useThemeColor } from "heroui-native";
import { View } from "react-native";
import { BottomSheet, Button, TextField, Input, useBottomSheetAwareHandlers } from "heroui-native";
import { useState, useRef, useEffect } from "react";
import { Keyboard } from "react-native";
export default function ToastExample() {
const { toast } = useToast();
const themeColorForeground = useThemeColor("foreground");
const BottomSheetTextInput = ({ isOpen }: { isOpen: boolean }) => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
const inputRef = useRef<any>(null);
useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
} else {
inputRef.current?.blur();
}
}, [isOpen]);
return (
<View className="gap-4 p-4">
<Button
onPress={() =>
toast.show({
variant: "success",
label: "You have upgraded your plan",
description: "You can continue using HeroUI Chat",
actionLabel: "Close",
onActionPress: ({ hide }) => hide(),
})
}
>
Show Success Toast
</Button>
<TextField>
<Input ref={inputRef} placeholder="Type here..." onFocus={onFocus} onBlur={onBlur} />
</TextField>
);
};
<Button
onPress={() =>
toast.show({
duration: 0,
component: (props) => (
<Toast variant="accent" {...props}>
<Toast.Title>Custom Toast</Toast.Title>
<Toast.Description>This uses a custom component</Toast.Description>
<Toast.Action onPress={() => props.hide()}>Undo</Toast.Action>
<Toast.Close className="absolute top-0 right-0" />
</Toast>
),
})
export default function BottomSheetExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<BottomSheet
isOpen={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
Keyboard.dismiss(); // ✅ 关闭时收键盘
}
>
Show Custom Toast
</Button>
</View>
}}
animation="disable-all"
>
<BottomSheet.Trigger asChild>
<Button variant="secondary" className="mt-30">
Open Bottom Sheet
</Button>
</BottomSheet.Trigger>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content enableBlurKeyboardOnGesture keyboardBlurBehavior="restore">
<BottomSheetTextInput isOpen={isOpen} />
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
);
}
@@ -0,0 +1,39 @@
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>
);
}
@@ -1,6 +1,8 @@
import React from "react";
import React, { useEffect } from "react";
import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus";
import { s3ListBucketsApi } from "@/request/api";
export default function Home() {
// const [dailyData, setDailyData] = useState<any>(null);
@@ -15,6 +17,12 @@ export default function Home() {
// .catch(() => {});
// }, []);
useEffect(() => {
s3ListBucketsApi().then((res) => {
console.log(res);
});
}, []);
return (
<LayoutView>
<LayoutView.ScrollView>
+5 -3
View File
@@ -30,12 +30,14 @@ export default function My() {
<LayoutView.ScrollView>
<View className="flex-row items-center">
<Avatar alt="">
<Avatar.Image source={{ uri: "https://example.com/avatar.jpg" }} />
<Avatar.Fallback className="text-sm">JD</Avatar.Fallback>
<Avatar.Image source={{ uri: userStore.userInfo.avatar }} />
<Avatar.Fallback className="text-sm">{userStore.userInfo.nickname}</Avatar.Fallback>
</Avatar>
<View className="ml-3 mr-1">
<Text className="text-xl font-semibold text-foreground">JD</Text>
<Text className="text-xl font-semibold text-foreground">
{userStore.userInfo.nickname}
</Text>
</View>
<ChevronRightIcon size={18} color="#c7c7cc" />
+75 -35
View File
@@ -26,6 +26,48 @@ import { UserRoundIcon, UsersRoundIcon } from "lucide-react-native";
import { toast } from "sonner-native";
import { FadeIn } from "react-native-reanimated";
import { generateCode } from "@/utils/common";
import { Keyboard } from "react-native";
import { forwardRef } from "react";
const BottomSheetTextInput = forwardRef<any, React.ComponentProps<typeof Input>>((props, ref) => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
return (
<Input
ref={ref}
{...props}
onFocus={(e) => {
onFocus(e);
props.onFocus?.(e);
}}
onBlur={(e) => {
onBlur(e);
props.onBlur?.(e);
}}
/>
);
});
const BottomSheetTextInputOTP = forwardRef<any, React.ComponentProps<typeof InputOTP>>(
(props, ref) => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
return (
<InputOTP
ref={ref}
{...props}
onFocus={(e) => {
onFocus(e);
props.onFocus?.(e);
}}
onBlur={(e) => {
onBlur(e);
props.onBlur?.(e);
}}
/>
);
},
);
export default function Space() {
const router = useRouter();
@@ -54,28 +96,38 @@ export default function Space() {
const [createLoading, setCreateLoading] = useState(false);
const createInputRef = useRef<any>(null);
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
useEffect(() => {
listUserSpaces();
}, []);
useEffect(() => {
if (showCreate) {
setTimeout(() => {
requestAnimationFrame(() => {
createInputRef.current?.focus();
}, 0);
});
} else {
setCreateForm({
name: "",
});
setCreateLoading(false);
Keyboard.dismiss();
}
}, [showCreate]);
useEffect(() => {
if (showJoin) {
setTimeout(() => {
requestAnimationFrame(() => {
joinInputRef.current?.focus();
}, 0);
});
} else {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
Keyboard.dismiss();
}
}, [showJoin]);
useEffect(() => {
listUserSpaces();
}, []);
function listUserSpaces(loading = true) {
if (loading) {
setLoading(true);
@@ -396,7 +448,7 @@ export default function Space() {
className="flex-1"
onPress={() => {
setShowCreate(true);
createInputRef.current?.focus();
// createInputRef.current?.focus();
}}
>
<PlusIcon color="#fff" size={20} />
@@ -463,32 +515,25 @@ export default function Space() {
/>
)}
</LayoutView.ScrollView>
<BottomSheet
isOpen={showCreate}
onOpenChange={(val) => {
if (!val) {
setCreateForm({
name: "",
});
setCreateLoading(false);
}
setShowCreate(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<BottomSheet.Content
animation="disabled"
enableBlurKeyboardOnGesture
keyboardBlurBehavior="restore"
>
<View>
<TextField>
<Label>空间名称</Label>
<Input
<BottomSheetTextInput
ref={createInputRef}
placeholder="请输入空间名称"
onFocus={onFocus}
onBlur={onBlur}
value={createForm.name}
onChangeText={(val) =>
setCreateForm({
@@ -510,28 +555,23 @@ export default function Space() {
<BottomSheet
isOpen={showJoin}
onOpenChange={(val) => {
if (!val) {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
}
setShowJoin(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<BottomSheet.Content
animation={"disabled"}
enableBlurKeyboardOnGesture
keyboardBlurBehavior="restore"
>
<View>
<TextField>
<Label>邀请码</Label>
<InputOTP
<BottomSheetTextInputOTP
ref={joinInputRef}
maxLength={6}
onFocus={onFocus}
onBlur={onBlur}
value={joinForm.invite_code}
onChange={(val) => {
setJoinForm({
@@ -550,7 +590,7 @@ export default function Space() {
<InputOTP.Slot index={4} />
<InputOTP.Slot index={5} />
</InputOTP.Group>
</InputOTP>
</BottomSheetTextInputOTP>
</TextField>
<Button className="mt-3" onPress={handleJoin}>
+12 -11
View File
@@ -11,9 +11,16 @@ import { Toaster } from "sonner-native";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { useUserStore } from "@/store";
import { useRouter } from "expo-router";
import { HeroUINativeConfig } from "heroui-native";
SplashScreen.preventAutoHideAsync();
const config: HeroUINativeConfig = {
devInfo: {
stylingPrinciples: false,
},
};
export default function RootLayout() {
const router = useRouter();
const userStore = useUserStore();
@@ -46,15 +53,9 @@ export default function RootLayout() {
return (
<GestureHandlerRootView className="flex-1">
<KeyboardProvider>
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<HeroUINativeProvider
config={{
devInfo: {
stylingPrinciples: false,
},
}}
>
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<KeyboardProvider>
<HeroUINativeProvider config={config}>
<Stack
screenOptions={{
animation: "slide_from_right",
@@ -66,8 +67,8 @@ export default function RootLayout() {
/>
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
</HeroUINativeProvider>
</ThemeProvider>
</KeyboardProvider>
</KeyboardProvider>
</ThemeProvider>
<Toaster duration={1000} />
</GestureHandlerRootView>
);
+179 -14
View File
@@ -1,4 +1,4 @@
import React from "react";
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";
@@ -7,19 +7,55 @@ import { useState } from "react";
import { FadeIn } from "react-native-reanimated";
import { useUserStore } 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 onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
const authResult = useEvent(ExpoWeChat, "onAuthResult");
const [loading, setLoading] = useState(false);
const [isAgreed, setIsAgreed] = useState(true);
const [form, setForm] = useState({
type: 0,
openid: "",
code: "",
});
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 登录
@@ -30,23 +66,152 @@ export default function Login() {
setLoading(true);
setTimeout(() => {
requestAnimationFrame(() => {
userStore.setUserInfo({
id: 779895419121733,
token: "123",
nickname: "Zhang",
avatar: "",
setForm((val) => ({
...val,
type: 1,
}));
await ExpoQQ.login(["get_user_info"]);
}
/**
* {"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"}
*/
// function handleTest() {
// if (loading) {
// return;
// }
// setLoading(true);
// setTimeout(() => {
// requestAnimationFrame(() => {
// userStore.setUserInfo({
// id: 779895419121733,
// token: "ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY",
// nickname: "Zhang",
// avatar: "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
// });
// userStore.setAuthExpired(false);
// router.replace("/(tabs)/home");
// });
// setLoading(false);
// }, 2000);
// }
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,
});
userStore.setAuthExpired(false);
console.log(loginRes);
router.replace("/(tabs)/home");
});
if (loginRes.isSuccess) {
requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
setLoading(false);
}, 2000);
}
userStore.setAuthExpired(false);
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);
router.replace("/(tabs)/home");
});
}
} catch (err) {
console.error("登录接口出错", err);
} finally {
setLoading(false);
}
} else {
console.log("授权失败:", authResult.errorMessage);
setLoading(false);
}
};
handleLogin();
}, [authResult]);
return (
<>
+18
View File
@@ -47,3 +47,21 @@ interface SaveDataParams {
export function saveDataApi(params: SaveDataParams[]) {
return alovaInstance.Post<ResponseData<null>>(`/data/saveData`, params);
}
export function s3ListBucketsApi() {
return alovaInstance.Post<ResponseData<string[]>>(`/s3/listBuckets`);
}
export function s3UploadFileApi(files: { uri: string; name: string; type: string }[]) {
const form = new FormData();
files.forEach((file) => {
form.append("files", file as any);
});
return alovaInstance.Post<ResponseData<string[]>>("/s3/upload", form, {
headers: {
"Content-Type": "multipart/form-data",
},
});
}