u
This commit is contained in:
1 parent
e893ef2e48
commit
b6415c0d65
25 files changed
+1495
-112
No files matched your search
@@ -20,9 +20,9 @@ const tabs: TabbarItem[] = [
|
||||
icon: HomeIcon,
|
||||
},
|
||||
{
|
||||
id: "/chat",
|
||||
title: "聊天",
|
||||
icon: MessageCircleIcon,
|
||||
id: "/space",
|
||||
title: "空间",
|
||||
icon: EarthIcon,
|
||||
},
|
||||
{
|
||||
id: "/tools",
|
||||
@@ -31,9 +31,9 @@ const tabs: TabbarItem[] = [
|
||||
isCenter: true,
|
||||
},
|
||||
{
|
||||
id: "/space",
|
||||
title: "空间",
|
||||
icon: EarthIcon,
|
||||
id: "/chat",
|
||||
title: "聊天",
|
||||
icon: MessageCircleIcon,
|
||||
},
|
||||
{
|
||||
id: "/my",
|
||||
|
||||
@@ -1,36 +1,43 @@
|
||||
import { Button, Dialog } from "heroui-native";
|
||||
import { Button, Toast, useToast, useThemeColor } from "heroui-native";
|
||||
import { View } from "react-native";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function DialogExample() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
export default function ToastExample() {
|
||||
const { toast } = useToast();
|
||||
const themeColorForeground = useThemeColor("foreground");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="primary" onPress={() => {
|
||||
setIsOpen(true)
|
||||
}}>Open Dialog</Button>
|
||||
<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>
|
||||
|
||||
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<Dialog.Content>
|
||||
<Dialog.Close variant="ghost" />
|
||||
<View className="mb-5 gap-1.5">
|
||||
<Dialog.Title>Confirm Action</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Are you sure you want to proceed with this action? This cannot be undone.
|
||||
</Dialog.Description>
|
||||
</View>
|
||||
<View className="flex-row justify-end gap-3">
|
||||
<Button variant="ghost" size="sm" onPress={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm">Confirm</Button>
|
||||
</View>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
</>
|
||||
<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>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Custom Toast
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,566 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { Text } from "react-native";
|
||||
import { View, Text } from "react-native";
|
||||
import {
|
||||
Button,
|
||||
Separator,
|
||||
Spinner,
|
||||
Chip,
|
||||
BottomSheet,
|
||||
Label,
|
||||
TextField,
|
||||
Input,
|
||||
InputOTP,
|
||||
type InputOTPRef,
|
||||
PressableFeedback,
|
||||
useBottomSheetAwareHandlers,
|
||||
} from "heroui-native";
|
||||
import { PlusIcon, HousePlusIcon, GripIcon } from "lucide-react-native";
|
||||
import { useUserStore } from "@/store";
|
||||
import { getUniqueIdApi, loadDataApi, loadDataBySqlApi, saveDataApi } from "@/request/api";
|
||||
import EmptyState from "@/components/EmptyStatus";
|
||||
import Sortable from "react-native-sortables";
|
||||
import { useSpaceStore } from "@/store";
|
||||
import { useRouter } from "expo-router";
|
||||
import { UserRoundIcon, UsersRoundIcon } from "lucide-react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { generateCode } from "@/utils/common";
|
||||
|
||||
export default function Space() {
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const spaceStore = useSpaceStore();
|
||||
|
||||
const ITEM_HEIGHT = 64;
|
||||
|
||||
const [spaces, setSpaces] = useState<any[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 加入
|
||||
const [showJoin, setShowJoin] = useState(false);
|
||||
const [joinLoading, setJoinLoading] = useState(false);
|
||||
const [joinForm, setJoinForm] = useState({
|
||||
invite_code: "",
|
||||
});
|
||||
const joinInputRef = useRef<InputOTPRef>(null);
|
||||
|
||||
// 新建
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: "",
|
||||
});
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const createInputRef = useRef<any>(null);
|
||||
|
||||
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
|
||||
|
||||
useEffect(() => {
|
||||
if (showCreate) {
|
||||
setTimeout(() => {
|
||||
createInputRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [showCreate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showJoin) {
|
||||
setTimeout(() => {
|
||||
joinInputRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [showJoin]);
|
||||
|
||||
useEffect(() => {
|
||||
listUserSpaces();
|
||||
}, []);
|
||||
|
||||
function listUserSpaces(loading = true) {
|
||||
if (loading) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
loadDataApi({
|
||||
view_name: "v_user_spaces",
|
||||
search_condition: `v_user_id = ?`,
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [userStore.userInfo.id],
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
setSpaces(res.data);
|
||||
if (loading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (createLoading) return;
|
||||
|
||||
if (createForm.name === "") {
|
||||
toast.warning("空间名称不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
setCreateLoading(true);
|
||||
|
||||
try {
|
||||
let inviteCode = "";
|
||||
let isExist = true;
|
||||
|
||||
// 循环生成直到不存在
|
||||
while (isExist) {
|
||||
inviteCode = generateCode(6);
|
||||
|
||||
const res = await loadDataBySqlApi({
|
||||
sql: `SELECT 1 FROM space WHERE invite_code = ? LIMIT 1`,
|
||||
args: [inviteCode],
|
||||
});
|
||||
|
||||
if (!res.isSuccess) {
|
||||
throw new Error("校验邀请码失败");
|
||||
}
|
||||
|
||||
isExist = res.data.length > 0;
|
||||
}
|
||||
|
||||
// 设置是否是默认空间
|
||||
let isDefault = false;
|
||||
|
||||
const defaultRes = await loadDataApi({
|
||||
view_name: "v_user_spaces",
|
||||
search_condition: `v_user_id = ?`,
|
||||
search_columns: ["v_is_default"],
|
||||
order_by: "",
|
||||
args: [userStore.userInfo.id],
|
||||
});
|
||||
|
||||
if (!defaultRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultRes.data.length == 0) {
|
||||
isDefault = true;
|
||||
}
|
||||
|
||||
// 创建
|
||||
const autoRes = await getUniqueIdApi(2);
|
||||
if (!autoRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spaceId = autoRes.data[0];
|
||||
const spaceMemberId = autoRes.data[1];
|
||||
|
||||
// 插入空间
|
||||
const requestData = [];
|
||||
|
||||
requestData.push({
|
||||
table_name: "space",
|
||||
key_field: "id",
|
||||
inserts: [
|
||||
{
|
||||
id: spaceId,
|
||||
name: createForm.name,
|
||||
invite_code: inviteCode,
|
||||
owner_id: userStore.userInfo.id,
|
||||
status: 0,
|
||||
},
|
||||
],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
// 插入空间用户
|
||||
requestData.push({
|
||||
table_name: "space_member",
|
||||
key_field: "id",
|
||||
inserts: [
|
||||
{
|
||||
id: spaceMemberId,
|
||||
space_id: spaceId,
|
||||
user_id: userStore.userInfo.id,
|
||||
role: 0,
|
||||
is_default: isDefault ? 1 : 0,
|
||||
sort_number: defaultRes.data.length,
|
||||
member_name: userStore.userInfo.nickname,
|
||||
},
|
||||
],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
toast.success("创建成功");
|
||||
setShowCreate(false);
|
||||
|
||||
setCreateForm({
|
||||
name: "",
|
||||
});
|
||||
|
||||
listUserSpaces();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setCreateLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleJoin() {
|
||||
if (joinLoading) return;
|
||||
|
||||
const code = joinForm.invite_code.trim();
|
||||
|
||||
if (!code) {
|
||||
toast.warning("邀请码不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[A-Za-z0-9]{6}$/.test(code)) {
|
||||
toast.warning("邀请码必须为6位字母或数字");
|
||||
return;
|
||||
}
|
||||
|
||||
setJoinLoading(true);
|
||||
|
||||
try {
|
||||
const res = await loadDataBySqlApi({
|
||||
sql: ` SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM space_member sm
|
||||
WHERE sm.space_id = s.id
|
||||
AND sm.user_id = ?
|
||||
) AS is_joined
|
||||
FROM space s
|
||||
WHERE s.invite_code = ?
|
||||
LIMIT 1;
|
||||
`,
|
||||
args: [userStore.userInfo.id, code],
|
||||
});
|
||||
|
||||
if (!res.isSuccess) return;
|
||||
|
||||
const row = res.data?.[0];
|
||||
|
||||
if (!row) {
|
||||
toast.warning("邀请码无效");
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.is_joined) {
|
||||
toast.warning("你已经在该空间了");
|
||||
return;
|
||||
}
|
||||
|
||||
const autoRes = await getUniqueIdApi(1);
|
||||
if (!autoRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置是否是默认空间
|
||||
let isDefault = false;
|
||||
|
||||
const defaultRes = await loadDataApi({
|
||||
view_name: "v_user_spaces",
|
||||
search_condition: `v_user_id = ?`,
|
||||
search_columns: ["v_is_default"],
|
||||
order_by: "",
|
||||
args: [userStore.userInfo.id],
|
||||
});
|
||||
|
||||
if (!defaultRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultRes.data.length == 0) {
|
||||
isDefault = true;
|
||||
}
|
||||
|
||||
// 加入
|
||||
const requestData = [];
|
||||
|
||||
requestData.push({
|
||||
table_name: "space_member",
|
||||
key_field: "id",
|
||||
inserts: [
|
||||
{
|
||||
id: autoRes.data[0],
|
||||
space_id: row.id,
|
||||
user_id: userStore.userInfo.id,
|
||||
role: 1,
|
||||
is_default: isDefault ? 1 : 0,
|
||||
sort_number: defaultRes.data.length,
|
||||
member_name: userStore.userInfo.nickname,
|
||||
},
|
||||
],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
toast.success("加入成功");
|
||||
setShowJoin(false);
|
||||
|
||||
setJoinForm({
|
||||
invite_code: "",
|
||||
});
|
||||
|
||||
listUserSpaces();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("加入失败,请稍后重试");
|
||||
} finally {
|
||||
setJoinLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||
<PressableFeedback
|
||||
key={item.space_id}
|
||||
onPress={() => {
|
||||
router.navigate({
|
||||
pathname: "/tab-space" as any,
|
||||
params: {
|
||||
spaceId: item.space_id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
height: ITEM_HEIGHT,
|
||||
}}
|
||||
className={`rounded-xl flex-row items-center px-4 py-3 bg-white ${
|
||||
index === spaces.length - 1 ? "mb-4" : ""
|
||||
}`}
|
||||
>
|
||||
<View className="w-full flex-row items-center justify-between">
|
||||
<View>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Text className="font-semibold text-gray-900">{item.name}</Text>
|
||||
|
||||
{item.owner_id === userStore.userInfo.id && (
|
||||
<Chip size="sm" variant="soft" color="success">
|
||||
创建者
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{item.v_is_default === 1 && (
|
||||
<Chip size="sm" variant="secondary" color="default">
|
||||
默认
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{item.space_id === spaceStore?.currentSpace?.space_id && (
|
||||
<Chip size="sm" variant="primary" color="accent">
|
||||
当前空间
|
||||
</Chip>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center gap-3 mt-3">
|
||||
<View className="flex-row gap-1 items-center">
|
||||
<UserRoundIcon size={12} />
|
||||
<Text className="text-xs">{item.v_member_name}</Text>
|
||||
</View>
|
||||
<View className="flex-row gap-1 items-center">
|
||||
<UsersRoundIcon size={12} />
|
||||
<Text className={`text-xs `}>{item.v_member_count}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Sortable.Handle>
|
||||
<GripIcon size={20} />
|
||||
</Sortable.Handle>
|
||||
</View>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
);
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<Text className="text-base text-black">我的空间</Text>
|
||||
<View className="flex-row gap-4">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onPress={() => {
|
||||
setShowCreate(true);
|
||||
createInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<PlusIcon color="#fff" size={20} />
|
||||
<Button.Label>创建新空间</Button.Label>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
setShowJoin(true);
|
||||
}}
|
||||
>
|
||||
<HousePlusIcon size={20} />
|
||||
<Button.Label>加入其它空间</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="flex-row my-3 items-center gap-3">
|
||||
<Separator className="flex-1" />
|
||||
<Text className="text-xs text-gray-400">我的空间</Text>
|
||||
<Separator className="flex-1" />
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
</View>
|
||||
) : spaces.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<Sortable.Grid
|
||||
keyExtractor={(item: any) => {
|
||||
return String(item.space_id);
|
||||
}}
|
||||
activeItemScale={1}
|
||||
columns={1}
|
||||
data={spaces}
|
||||
overDrag="vertical"
|
||||
renderItem={renderItem}
|
||||
rowGap={10}
|
||||
highlight-next-line
|
||||
customHandle
|
||||
autoScrollActivationOffset={200}
|
||||
onDragEnd={(dragData: any) => {
|
||||
const list = dragData.data;
|
||||
|
||||
const updates = list.map((item: any, index: number) => ({
|
||||
id: item.v_space_member_id,
|
||||
sort_number: index,
|
||||
}));
|
||||
|
||||
saveDataApi([
|
||||
{
|
||||
table_name: "space_member",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates,
|
||||
deletes: [],
|
||||
},
|
||||
]).then((_res) => {
|
||||
// console.log(res);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</LayoutView.ScrollView>
|
||||
|
||||
<BottomSheet
|
||||
isOpen={showCreate}
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
setCreateForm({
|
||||
name: "",
|
||||
});
|
||||
setCreateLoading(false);
|
||||
}
|
||||
|
||||
setShowCreate(val);
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content>
|
||||
<View>
|
||||
<TextField>
|
||||
<Label>空间名称</Label>
|
||||
<Input
|
||||
ref={createInputRef}
|
||||
placeholder="请输入空间名称"
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
value={createForm.name}
|
||||
onChangeText={(val) =>
|
||||
setCreateForm({
|
||||
name: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TextField>
|
||||
|
||||
<Button className="mt-3" onPress={handleCreate}>
|
||||
{createLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
<Button.Label>创建</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
|
||||
<BottomSheet
|
||||
isOpen={showJoin}
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
setJoinForm({
|
||||
invite_code: "",
|
||||
});
|
||||
setJoinLoading(false);
|
||||
}
|
||||
|
||||
setShowJoin(val);
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content>
|
||||
<View>
|
||||
<TextField>
|
||||
<Label>邀请码</Label>
|
||||
<InputOTP
|
||||
ref={joinInputRef}
|
||||
maxLength={6}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
value={joinForm.invite_code}
|
||||
onChange={(val) => {
|
||||
setJoinForm({
|
||||
invite_code: val,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={0} />
|
||||
<InputOTP.Slot index={1} />
|
||||
<InputOTP.Slot index={2} />
|
||||
</InputOTP.Group>
|
||||
<InputOTP.Separator />
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={3} />
|
||||
<InputOTP.Slot index={4} />
|
||||
<InputOTP.Slot index={5} />
|
||||
</InputOTP.Group>
|
||||
</InputOTP>
|
||||
</TextField>
|
||||
|
||||
<Button className="mt-3" onPress={handleJoin}>
|
||||
{joinLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
<Button.Label>加入</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
@@ -18,11 +18,11 @@ export default function NotFound() {
|
||||
className="mt-4"
|
||||
onPress={() => {
|
||||
requestAnimationFrame(() => {
|
||||
router.replace("/home");
|
||||
router.back();
|
||||
});
|
||||
}}
|
||||
>
|
||||
返回首页
|
||||
返回
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -8,10 +8,15 @@ import "@/global.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import { Toaster } from "sonner-native";
|
||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||
import { useUserStore } from "@/store";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function RootLayout() {
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const isDark = useIsDark();
|
||||
const isDev = __DEV__;
|
||||
|
||||
@@ -29,31 +34,40 @@ export default function RootLayout() {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userStore.authExpired) {
|
||||
requestAnimationFrame(() => {
|
||||
router.replace("/auth/login");
|
||||
});
|
||||
}
|
||||
}, [userStore.authExpired]);
|
||||
|
||||
if (!isReady) return null;
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView className="flex-1">
|
||||
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
|
||||
<HeroUINativeProvider
|
||||
config={{
|
||||
devInfo: {
|
||||
stylingPrinciples: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
animation: "slide_from_right",
|
||||
contentStyle: {
|
||||
backgroundColor: "#fff",
|
||||
<KeyboardProvider>
|
||||
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
|
||||
<HeroUINativeProvider
|
||||
config={{
|
||||
devInfo: {
|
||||
stylingPrinciples: false,
|
||||
},
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
|
||||
</HeroUINativeProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
animation: "slide_from_right",
|
||||
contentStyle: {
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
|
||||
</HeroUINativeProvider>
|
||||
</ThemeProvider>
|
||||
</KeyboardProvider>
|
||||
<Toaster duration={1000} />
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
|
||||
@@ -33,12 +33,14 @@ export default function Login() {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
id: 1,
|
||||
id: 779895419121733,
|
||||
token: "123",
|
||||
nickname: "Zhang",
|
||||
avatar: "",
|
||||
});
|
||||
|
||||
userStore.setAuthExpired(false);
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { View, Text } from "react-native";
|
||||
import Navbar from "@/components/layout/navbar";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { EarthIcon } from "lucide-react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export default function Space() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1, zIndex: 0 }}>
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<Text className="text-base text-black">Space</Text>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingTop: insets.top }}>
|
||||
<Navbar
|
||||
title={"空间"}
|
||||
titleIcon={EarthIcon}
|
||||
onBack={() => {
|
||||
router.back();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,12 @@ export function loginWxApi(params: LoginWxParams) {
|
||||
return alovaInstance.Post<ResponseData<LoginResult>>(`/auth/login/wechat`, params);
|
||||
}
|
||||
|
||||
export function getUniqueIdApi(count = 1) {
|
||||
return alovaInstance.Post<ResponseData<number[]>>(`/data/getUniqueId`, {
|
||||
count: count,
|
||||
});
|
||||
}
|
||||
|
||||
interface LoadDataBySqlParams {
|
||||
sql: string;
|
||||
args: any[];
|
||||
@@ -23,10 +29,21 @@ interface LoadDataParams {
|
||||
search_condition: string;
|
||||
order_by: string;
|
||||
search_columns: string[];
|
||||
args: string[];
|
||||
args: any[];
|
||||
}
|
||||
|
||||
export function loadDataApi(params: LoadDataParams) {
|
||||
return alovaInstance.Post<ResponseData<any[]>>(`/data/loadData`, params);
|
||||
}
|
||||
|
||||
interface SaveDataParams {
|
||||
table_name: string;
|
||||
key_field: string;
|
||||
inserts: any[];
|
||||
updates: any[];
|
||||
deletes: any[];
|
||||
}
|
||||
|
||||
export function saveDataApi(params: SaveDataParams[]) {
|
||||
return alovaInstance.Post<ResponseData<null>>(`/data/saveData`, params);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./app";
|
||||
export * from "./user";
|
||||
export * from "./space";
|
||||
@@ -0,0 +1,32 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
|
||||
interface Space {
|
||||
space_id: number;
|
||||
}
|
||||
|
||||
interface SpaceState {
|
||||
currentSpace: Space | null;
|
||||
setCurrentSpace: (space: Space | null) => void;
|
||||
clear: () => void;
|
||||
inSpace: () => boolean;
|
||||
}
|
||||
|
||||
export const useSpaceStore = create<SpaceState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
currentSpace: null,
|
||||
setCurrentSpace: (space) => set({ currentSpace: space }),
|
||||
clear: () => set({ currentSpace: null }),
|
||||
inSpace: () => {
|
||||
const currentSpace = get().currentSpace;
|
||||
return currentSpace !== null && currentSpace.space_id !== 0;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "space-storage",
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
export function generateCode(
|
||||
length: number = 6,
|
||||
chars: string = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789",
|
||||
): string {
|
||||
let result = "";
|
||||
const max = chars.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * max));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user