20260625215406

This commit is contained in:
oneao committed 2026-06-25 21:54:06 +08:00
1 parent 2e928710df
commit 5e6f9252d5
9 files changed
+270 -210

No files matched your search

+6 -2
View File
@@ -77,9 +77,13 @@ func AuthWS() fiber.Handler {
}
// 验证用户属于该家庭,同时获取 nickname / avatar / role
// JOIN b_user 确保能拿到 avatar(v_family_member 视图可能不含此字段)
dbClient := db.New()
rows, err := dbClient.LoadData(c.Context(), "b_family_member",
"user_id = ? AND family_id = ?", "", nil,
rows, err := dbClient.LoadDataBySQL(c.Context(),
`SELECT m.role, m.nickname, u.avatar
FROM b_family_member m
JOIN b_user u ON m.user_id = u.id
WHERE m.user_id = ? AND m.family_id = ?`,
[]any{userID, familyID})
if err != nil || len(rows) == 0 {
return httpx.Unauthorized(c, "不属于该家庭")
@@ -1,38 +1,68 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Image, Text, TextInput, View, Keyboard } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { useRouter } from "expo-router";
import { Menu, PressableFeedback, Spinner } from "heroui-native";
import { SendHorizonalIcon, SearchIcon } from "lucide-react-native";
import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus";
import { loadDataApi, loadDataPageApi } from "@/request/api";
import { useUserStore, useFamilyStore } from "@/store";
import { useChatWebSocket, ChatMessage, OnlineUser } from "@/hooks/useChatWebSocket";
import { cn } from "@/utils/cn";
import { buildFileUrl } from "@/utils/file";
import { useCallback, useEffect, useRef, useState } from "react";
import { FlatList, Image, Text, TextInput, View, Keyboard } from "react-native";
import { useRouter } from "expo-router";
import {
Button,
Spinner,
BottomSheet,
PressableFeedback,
} from "heroui-native";
import { SendHorizonalIcon, SearchIcon } from "lucide-react-native";
import { cn } from "@/utils/cn";
import { ChatMsgType } from "@/types";
function formatTime(ts: string) {
if (!ts) return "";
const d = new Date(ts.replace(" ", "T") + "Z");
const h = d.getHours().toString().padStart(2, "0");
const m = d.getMinutes().toString().padStart(2, "0");
return `${h}:${m}`;
const d = new Date(ts);
if (isNaN(d.getTime())) return "";
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
}
// ====== 消息气泡 ======
// ===================== 在线栏 =====================
function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: () => void }) {
const router = useRouter();
return (
<View className="flex-row items-center justify-between px-4 h-12 bg-white rounded-2xl shadow-md">
<View className="flex-row items-center gap-3 flex-1">
{users.slice(0, 6).map((u) => (
<PressableFeedback
key={u.user_id}
onPress={() => router.navigate(`/family/member/${u.user_id}` as any)}
>
<View>
{u.avatar ? (
<Image
source={{ uri: buildFileUrl(u.avatar) }}
className="w-7 h-7 rounded-full bg-gray-200"
/>
) : (
<View className="w-7 h-7 rounded-full bg-gray-300 items-center justify-center">
<Text className="text-xs text-white">{u.nickname?.charAt(0) || "?"}</Text>
</View>
)}
<View className="absolute -right-0.5 -bottom-0.5 w-3 h-3 rounded-full bg-green-500 border-2 border-white" />
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
))}
</View>
<PressableFeedback onPress={onSearch}>
<View className="w-8 h-8 items-center justify-center">
<SearchIcon size={18} color="#9CA3AF" />
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
</View>
);
}
// ===================== 消息气泡 =====================
function ChatMessageItem({
item,
isSelf,
members,
isAdmin,
onRecall,
onDelete,
item, isSelf, members, isAdmin, onRecall, onDelete,
}: {
item: ChatMessage;
isSelf: boolean;
@@ -45,35 +75,29 @@ function ChatMessageItem({
const member = members[item.user_id];
const nickname = item.nickname || member?.nickname || "未知";
const [showActions, setShowActions] = useState(false);
const msgTime = new Date(item.create_time.replace(" ", "T") + "Z").getTime();
const within2Min = (Date.now() - msgTime) < 2 * 60 * 1000;
const msgTime = new Date(item.create_time).getTime();
const within2Min = !isNaN(msgTime) && Date.now() - msgTime < 2 * 60 * 1000;
const canRecall = isSelf && within2Min;
const canDelete = isAdmin;
// 撤回
if (item.recalled === 1) {
return (
<View className="items-center py-2">
<Text className="text-xs text-gray-400">{nickname} 撤回了一条消息</Text>
<View className="items-center py-3 px-4">
<Text className="text-sm text-gray-400">{nickname} 撤回了一条消息</Text>
</View>
);
}
// 系统消息
if (item.msg_type === ChatMsgType.System) {
return (
<View className="items-center py-2">
<Text className="text-xs text-gray-400">{item.content}</Text>
<View className="items-center py-3 px-4">
<Text className="text-sm text-gray-400">{item.content}</Text>
</View>
);
}
return (
<>
<PressableFeedback
onLongPress={() => (canRecall || canDelete) && setShowActions(true)}
>
<PressableFeedback onLongPress={() => (canRecall || isAdmin) && setShowActions(true)}>
<View className={cn("px-4 py-1", isSelf ? "items-end" : "items-start")}>
{!isSelf && (
<PressableFeedback onPress={() => router.navigate(`/family/member/${item.user_id}` as any)}>
@@ -82,22 +106,15 @@ function ChatMessageItem({
</PressableFeedback>
)}
{/* 文本 */}
{item.msg_type === ChatMsgType.Text && (
<View
className={cn(
"max-w-[75%] rounded-2xl px-3 py-2",
isSelf ? "bg-green-500" : "bg-white"
)}
>
<View className={cn("max-w-[75%] rounded-2xl px-3 py-2", isSelf ? "bg-green-500" : "bg-white")}>
<Text className={cn("text-base leading-5", isSelf ? "text-white" : "text-gray-900")}>
{item.content}
</Text>
</View>
)}
{/* 图片 */}
{item.msg_type === ChatMsgType.Image && item.attachments && item.attachments.length > 0 && (
{item.msg_type === ChatMsgType.Image && item.attachments?.length && (
<View className="flex-row flex-wrap gap-1 max-w-[75%] justify-end">
{item.attachments.map((att, i) => (
<Image
@@ -110,8 +127,7 @@ function ChatMessageItem({
</View>
)}
{/* 视频 */}
{item.msg_type === ChatMsgType.Video && item.attachments && item.attachments.length > 0 && (
{item.msg_type === ChatMsgType.Video && item.attachments?.length && (
<View className="w-48 h-32 rounded-xl bg-gray-800 items-center justify-center">
<Text className="text-white text-2xl">▶</Text>
{item.attachments[0].duration ? (
@@ -122,17 +138,19 @@ function ChatMessageItem({
</View>
)}
{/* 文件 */}
{item.msg_type === ChatMsgType.File && item.attachments && item.attachments.length > 0 && (
{item.msg_type === ChatMsgType.File && item.attachments?.length && (
<View
className={cn(
"max-w-[75%] rounded-xl px-3 py-2 flex-row items-center gap-2",
isSelf ? "bg-green-500" : "bg-white"
isSelf ? "bg-green-500" : "bg-white",
)}
>
<Text className={cn(isSelf ? "text-white" : "text-gray-900")}>📎</Text>
<View className="flex-1">
<Text className={cn("text-sm", isSelf ? "text-white" : "text-gray-900")} numberOfLines={1}>
<Text
className={cn("text-sm", isSelf ? "text-white" : "text-gray-900")}
numberOfLines={1}
>
{item.attachments[0].file_name}
</Text>
<Text className={cn("text-xs", isSelf ? "text-white/70" : "text-gray-400")}>
@@ -144,7 +162,6 @@ function ChatMessageItem({
</View>
)}
{/* 转发 */}
{item.msg_type === ChatMsgType.Forward && item.source_data && (
<View className="max-w-[75%] rounded-xl overflow-hidden bg-white border border-gray-200">
<View className="px-3 py-2">
@@ -154,7 +171,8 @@ function ChatMessageItem({
try {
const d = JSON.parse(item.source_data);
return Object.entries(d)
.filter(([k]) => !["id", "create_time", "update_time", "create_by", "update_by", "family_id"].includes(k))
.filter(([k]) =>
!["id", "create_time", "update_time", "create_by", "update_by", "family_id"].includes(k))
.map(([k, v]) => `${k}: ${v}`)
.join(" ");
} catch {
@@ -171,108 +189,77 @@ function ChatMessageItem({
</View>
)}
<Text className="text-[10px] text-gray-300 mt-0.5 mr-1">{formatTime(item.create_time)}</Text>
<Text className="text-[10px] text-gray-300 mt-0.5 mr-1">
{formatTime(item.create_time)}
</Text>
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
{/* 操作面板 */}
<BottomSheet
isOpen={showActions}
onClose={() => setShowActions(false)}
snapPoints={["auto"]}
>
<View className="px-4 py-4 gap-2">
{showActions && (
<Menu isOpen={showActions} onOpenChange={setShowActions}>
<Menu.Portal>
<Menu.Overlay />
<Menu.Content presentation="bottom-sheet">
{canRecall && (
<Button
<Menu.Item
onPress={() => {
onRecall(item.id);
setShowActions(false);
}}
className="w-full"
>
撤回
</Button>
<Menu.ItemTitle>撤回</Menu.ItemTitle>
</Menu.Item>
)}
{canDelete && (
<Button
{isAdmin && (
<Menu.Item
onPress={() => {
onDelete(item.id);
setShowActions(false);
}}
className="w-full"
color="danger"
>
删除
</Button>
<Menu.ItemTitle>删除</Menu.ItemTitle>
</Menu.Item>
)}
</Menu.Content>
</Menu.Portal>
</Menu>
)}
<Button onPress={() => setShowActions(false)} className="w-full" variant="light">
取消
</Button>
</View>
</BottomSheet>
</>
);
}
// ====== 在线头像栏 ======
function ChatOnlineBar({ users, onSearch }: { users: OnlineUser[]; onSearch: () => void }) {
const router = useRouter();
return (
<View className="h-12 px-4 flex-row items-center justify-between border-b border-gray-100 bg-white">
<View className="flex-row items-center gap-3">
<Text className="text-xs text-gray-400">{users.length} 人在线</Text>
{users.slice(0, 6).map((u) => (
<PressableFeedback
key={u.user_id}
onPress={() => router.navigate(`/family/member/${u.user_id}` as any)}
>
<View className="items-center">
{u.avatar ? (
<Image source={{ uri: u.avatar }} className="w-7 h-7 rounded-full bg-gray-200" />
) : (
<View className="w-7 h-7 rounded-full bg-gray-300 items-center justify-center">
<Text className="text-xs text-white">{u.nickname?.charAt(0) || "?"}</Text>
</View>
)}
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
))}
</View>
<PressableFeedback onPress={onSearch}>
<View className="w-8 h-8 items-center justify-center">
<SearchIcon size={18} color="#9ca3af" />
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
</View>
);
}
// ===================== 页面 =====================
// ====== 页面 ======
export default function ChatPage() {
const router = useRouter();
const userId = useUserStore((s) => s.userInfo.id);
const familyId = useFamilyStore((s) => s.currentFamily.family_id);
const ws = useChatWebSocket();
const [history, setHistory] = useState<ChatMessage[]>([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const [members, setMembers] = useState<Record<number, { nickname: string; avatar: string; role: number }>>({});
const [inputText, setInputText] = useState("");
const [adminRole, setAdminRole] = useState(0);
const listRef = useRef<any>(null);
const flatListRef = useRef<FlatList>(null);
const ws = useChatWebSocket();
// 键盘弹起时自动滚到底部
useEffect(() => {
const show = Keyboard.addListener("keyboardWillShow", () => {
setTimeout(() => listRef.current?.scrollToEnd?.({ animated: true }), 100);
});
return () => show.remove();
}, []);
// 成员映射
useEffect(() => {
if (!familyId) return;
loadDataApi({
view_name: "b_family_member",
view_name: "v_family_member",
search_condition: "family_id = ?",
search_columns: ["user_id", "nickname", "avatar", "role"],
args: [familyId],
@@ -280,7 +267,7 @@ export default function ChatPage() {
}).then((res) => {
if (res.isSuccess && res.data) {
const map: Record<number, any> = {};
for (const m of res.data) map[m.user_id] = m;
for (const r of res.data) map[r.user_id] = r;
setMembers(map);
const me = map[userId];
if (me) setAdminRole(me.role ?? 99);
@@ -302,12 +289,17 @@ export default function ChatPage() {
search_columns: [],
});
if (res.isSuccess && res.data) {
const rows = res.data.rows as ChatMessage[];
const mediaIds = rows.filter((r) => r.msg_type >= ChatMsgType.Image && r.msg_type <= ChatMsgType.File).map((r) => r.id);
if (mediaIds.length > 0) {
const ordered = (res.data.rows as any[])
.map((r) => ({ ...r, msg_type: r.type ?? r.msg_type ?? 0 } as ChatMessage))
.reverse();
const mids = ordered
.filter((r) => r.msg_type >= ChatMsgType.Image && r.msg_type <= ChatMsgType.File)
.map((r) => r.id);
if (mids.length) {
const fileRes = await loadDataApi({
view_name: "b_file",
search_condition: `target_id IN (${mediaIds.join(",")})`,
search_condition: `target_id IN (${mids.join(",")})`,
order_by: "sort_order ASC",
args: [],
search_columns: [],
@@ -318,33 +310,35 @@ export default function ChatPage() {
if (!fileMap[f.target_id]) fileMap[f.target_id] = [];
fileMap[f.target_id].push(f);
}
for (const r of rows) {
for (const r of ordered) {
if (fileMap[r.id]) (r as any).attachments = fileMap[r.id];
}
}
}
if (p === 1) setHistory(rows.reverse());
else setHistory((prev) => [...rows.reverse(), ...prev]);
setHasMore(rows.length >= 50);
if (p === 1) setHistory(ordered);
else setHistory((prev) => [...ordered, ...prev]);
setHasMore(ordered.length >= 50);
}
},
[familyId]
[familyId],
);
useEffect(() => {
loadHistory(1).finally(() => setLoading(false));
}, [loadHistory]);
// 加载更多
const handleLoadMore = useCallback(async () => {
if (loadingMore || !hasMore) return;
setLoadingMore(true);
const nextPage = page + 1;
await loadHistory(nextPage);
setPage(nextPage);
const np = page + 1;
await loadHistory(np);
setPage(np);
setLoadingMore(false);
}, [loadingMore, hasMore, page, loadHistory]);
// 合并实时消息
// 合并消息 (旧→新)
const allMessages = (() => {
const ids = new Set(history.map((m) => m.id));
const merged = [...history];
@@ -366,9 +360,6 @@ export default function ChatPage() {
ws.send({ type: "send", content: text });
}, [inputText, ws]);
const handleRecall = useCallback((id: number) => ws.recall(id), [ws]);
const handleDelete = useCallback((id: number) => ws.deleteMessage(id), [ws]);
if (loading) {
return (
<LayoutView>
@@ -378,48 +369,22 @@ export default function ChatPage() {
}
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior="padding"
keyboardVerticalOffset={-60}
>
<LayoutView>
<View className="flex-1 bg-gray-50">
{/* 连接状态 */}
<View className="h-7 px-4 flex-row items-center gap-1.5">
<View className={cn("w-2 h-2 rounded-full", ws.isConnected ? "bg-green-500" : "bg-red-400")} />
<Text className="text-xs text-gray-400">{ws.isConnected ? "已连接" : "连接中..."}</Text>
</View>
{/* 在线成员 + 搜索入口 */}
<ChatOnlineBar users={ws.onlineUsers} onSearch={() => router.navigate("/chat/history")} />
{/* 消息列表 */}
<FlatList
ref={flatListRef}
data={allMessages}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<ChatMessageItem
item={item}
isSelf={item.user_id === userId}
members={members}
isAdmin={adminRole === 0}
onRecall={handleRecall}
onDelete={handleDelete}
<LayoutView.FlatList
ref={listRef}
header={
<ChatOnlineBar
users={ws.onlineUsers}
onSearch={() => router.navigate("/chat/history")}
/>
)}
inverted
onEndReached={handleLoadMore}
onEndReachedThreshold={0.3}
className="flex-1"
contentContainerStyle={{ paddingVertical: 8 }}
ListFooterComponent={
loadingMore ? (
<View className="py-4 items-center">
<Spinner size="sm" />
</View>
) : null
}
/>
{/* 输入栏 */}
<View className="flex-row items-center gap-2 px-3 py-2 bg-white border-t border-gray-200">
footer={
<View className="flex-row items-center gap-2 px-3 py-2 bg-white rounded-2xl shadow-md">
<TextInput
className="flex-1 h-10 bg-gray-100 rounded-full px-4 text-base"
placeholder="输入消息..."
@@ -428,11 +393,11 @@ export default function ChatPage() {
onSubmitEditing={handleSend}
returnKeyType="send"
/>
<PressableFeedback onPress={handleSend} disabled={!inputText.trim()}>
<PressableFeedback onPress={handleSend} isDisabled={!inputText.trim()}>
<View
className={cn(
"w-10 h-10 rounded-full items-center justify-center",
inputText.trim() ? "bg-green-500" : "bg-gray-300"
inputText.trim() ? "bg-green-500" : "bg-gray-300",
)}
>
<SendHorizonalIcon size={18} color="white" />
@@ -440,7 +405,43 @@ export default function ChatPage() {
<PressableFeedback.Ripple />
</PressableFeedback>
</View>
}
data={allMessages}
keyExtractor={(item) => String(item.id)}
onScroll={({ nativeEvent }) => {
if (nativeEvent.contentOffset.y < 30 && hasMore && !loadingMore) {
handleLoadMore();
}
}}
scrollEventThrottle={200}
onContentSizeChange={() => {
if (loadingMore) return;
listRef.current?.scrollToEnd({ animated: false });
}}
ListHeaderComponent={
loadingMore ? (
<View className="py-2 items-center">
<Spinner size="sm" />
</View>
) : null
}
ListEmptyComponent={
<View className="items-center py-20">
<Text className="text-gray-400 text-sm">暂无消息,开始聊天吧</Text>
</View>
}
renderItem={({ item }) => (
<ChatMessageItem
item={item}
isSelf={item.user_id === userId}
members={members}
isAdmin={adminRole === 0}
onRecall={ws.recall}
onDelete={ws.deleteMessage}
/>
)}
/>
</LayoutView>
</KeyboardAvoidingView>
);
}
+88 -31
View File
@@ -1,4 +1,4 @@
import React, { forwardRef } from "react";
import React, { forwardRef, type ReactNode } from "react";
import {
SectionList,
FlatList,
@@ -37,7 +37,6 @@ function Shadow({ children }: { children: React.ReactElement }) {
function useContentPadding(config?: LayoutConfig) {
const insets = useSafeAreaInsets();
return {
paddingTop: insets.top + (config?.topExtraPadding ?? 50),
paddingBottom: insets.bottom + (config?.bottomExtraPadding ?? 80),
@@ -68,11 +67,7 @@ LayoutView.ScrollView = forwardRef<
any,
React.ComponentProps<typeof KeyboardAwareScrollView> & LayoutConfig
>(({ topExtraPadding, bottomExtraPadding, contentContainerStyle, ...props }, ref) => {
const padding = useContentPadding({
topExtraPadding,
bottomExtraPadding,
});
const padding = useContentPadding({ topExtraPadding, bottomExtraPadding });
return (
<Wrapper>
<Shadow>
@@ -91,24 +86,15 @@ LayoutView.ScrollView = forwardRef<
LayoutView.SectionView = forwardRef(
<T,>(
{
topExtraPadding,
bottomExtraPadding,
contentContainerStyle,
...props
}: React.ComponentProps<typeof SectionList<T>> & LayoutConfig,
props: React.ComponentProps<typeof SectionList<T>> & LayoutConfig,
ref: React.Ref<SectionList<T>>,
) => {
const { topExtraPadding, bottomExtraPadding, contentContainerStyle, ...rest } = props;
const padding = useContentPadding({ topExtraPadding, bottomExtraPadding });
return (
<Wrapper>
<Shadow>
<SectionList
ref={ref}
{...props}
contentContainerStyle={[padding, contentContainerStyle]}
/>
<SectionList ref={ref} {...rest} contentContainerStyle={[padding, contentContainerStyle]} />
</Shadow>
</Wrapper>
);
@@ -118,20 +104,59 @@ LayoutView.SectionView = forwardRef(
LayoutConfig & { ref?: React.Ref<SectionList<T>> },
) => React.ReactElement;
/* ========================= FlatList ========================= */
/* ========================= FlatList / LegendList ========================= */
type PinnedListConfig = {
header?: ReactNode;
footer?: ReactNode;
};
LayoutView.FlatList = forwardRef(
<T,>(
{
header,
footer,
topExtraPadding,
bottomExtraPadding,
contentContainerStyle,
...props
}: React.ComponentProps<typeof FlatList<T>> & LayoutConfig,
}: React.ComponentProps<typeof FlatList<T>> & LayoutConfig & PinnedListConfig,
ref: React.Ref<FlatList<T>>,
) => {
const padding = useContentPadding({ topExtraPadding, bottomExtraPadding });
if (header || footer) {
return (
<>
{header ? (
<View
style={{
paddingTop: padding.paddingTop,
paddingHorizontal: padding.paddingHorizontal,
}}
>
{header}
</View>
) : null}
<Wrapper>
<Shadow>
<FlatList ref={ref} {...props} contentContainerStyle={contentContainerStyle} />
</Shadow>
</Wrapper>
{footer ? (
<View
style={{
paddingBottom: padding.paddingBottom,
paddingHorizontal: padding.paddingHorizontal,
}}
>
{footer}
</View>
) : null}
</>
);
}
return (
<Wrapper>
<Shadow>
@@ -141,37 +166,69 @@ LayoutView.FlatList = forwardRef(
);
},
) as <T>(
props: React.ComponentProps<typeof FlatList<T>> & LayoutConfig & { ref?: React.Ref<FlatList<T>> },
props: React.ComponentProps<typeof FlatList<T>> &
LayoutConfig &
PinnedListConfig & { ref?: React.Ref<FlatList<T>> },
) => React.ReactElement;
/* ========================= LegendList ========================= */
LayoutView.LegendList = forwardRef(
<T,>(
{
header,
footer,
topExtraPadding,
bottomExtraPadding,
contentContainerStyle,
...props
}: React.ComponentProps<typeof LegendList<T>> & LayoutConfig,
}: React.ComponentProps<typeof LegendList<T>> & LayoutConfig & PinnedListConfig,
ref: React.Ref<any>,
) => {
const padding = useContentPadding({ topExtraPadding, bottomExtraPadding });
if (header || footer) {
return (
<>
{header ? (
<View
style={{
paddingTop: padding.paddingTop,
paddingHorizontal: padding.paddingHorizontal,
}}
>
{header}
</View>
) : null}
<Wrapper>
<Shadow>
<LegendList ref={ref} {...props} contentContainerStyle={contentContainerStyle} />
</Shadow>
</Wrapper>
{footer ? (
<View
style={{
paddingBottom: padding.paddingBottom,
paddingHorizontal: padding.paddingHorizontal,
}}
>
{footer}
</View>
) : null}
</>
);
}
return (
<Wrapper>
<Shadow>
<LegendList
ref={ref}
{...props}
contentContainerStyle={[padding, contentContainerStyle]}
/>
<LegendList ref={ref} {...props} contentContainerStyle={[padding, contentContainerStyle]} />
</Shadow>
</Wrapper>
);
},
) as <T>(
props: React.ComponentProps<typeof LegendList<T>> & LayoutConfig & { ref?: React.Ref<any> },
props: React.ComponentProps<typeof LegendList<T>> &
LayoutConfig &
PinnedListConfig & { ref?: React.Ref<any> },
) => React.ReactElement;
/* ========================= */
@@ -67,7 +67,7 @@ export function useChatWebSocket() {
const apiUrl = process.env.EXPO_PUBLIC_API_URL || "";
const wsProtocol = apiUrl.startsWith("https") ? "wss" : "ws";
const host = apiUrl.replace(/^https?:\/\//, "");
return `${wsProtocol}://${host}/app/chat/ws?token=${token}&family_id=${familyId}`;
return `${wsProtocol}://${host}/chat/ws?token=${token}&family_id=${familyId}`;
}, [token, familyId]);
const connect = useCallback(() => {
+1 -1
View File
@@ -59,7 +59,7 @@ export default function AppLayout({ tabs }: { tabs: TabbarItem[] }) {
</View>
</View>
<View style={{ position: "absolute", left: 0, right: 0, bottom: 0 }}>
<View style={{ paddingBottom: insets.bottom }}>
<View style={{ paddingBottom: insets.bottom + 16 }}>
<Tabbar
tabs={tabs}
value={activeTabItem?.id}
+1 -1
View File
@@ -65,7 +65,7 @@ export default function ModuleLayout({ tabs }: { tabs: TabbarItem[] }) {
zIndex: 10,
}}
>
<View style={{ paddingBottom: insets.bottom }}>
<View style={{ paddingBottom: insets.bottom + 16 }}>
<Tabbar tabs={tabs} value={activeTabItem?.id} onChange={(id) => router.push(id as any)} />
</View>
</View>
-2
View File
@@ -62,8 +62,6 @@ const alovaInstance = createAlova({
},
beforeRequest(method) {
console.log(process.env.EXPO_PUBLIC_API_URL)
// 白名单接口直接放行
if (isWhiteListed(method.url)) return;