Compare commits
7 Commits
c022e7f92f
...
521a4d0a51
| Author | SHA1 | Date | |
|---|---|---|---|
| 521a4d0a51 | |||
| 006db2af07 | |||
| 6c270302f5 | |||
| 153838aec0 | |||
| 125da0e660 | |||
| b8d00ef850 | |||
| 1c02968071 |
@ -1,14 +1,163 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { HapticTab } from '@/components/HapticTab';
|
||||
import TabBarBackground from '@/components/ui/TabBarBackground';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { fetchApi } from '@/lib/server-api-util';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { Tabs } from 'expo-router';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
interface PollingData {
|
||||
title: string;
|
||||
id: string;
|
||||
content: string;
|
||||
extra: any;
|
||||
}
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const [pollingData, setPollingData] = useState<PollingData[]>([]);
|
||||
const pollingInterval = useRef<NodeJS.Timeout | number>(null);
|
||||
const tokenInterval = useRef<NodeJS.Timeout | number>(null);
|
||||
const isMounted = useRef(true);
|
||||
const [token, setToken] = useState('');
|
||||
const sendNotification = async (item: PollingData) => {
|
||||
// 请求通知权限
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
alert('请先允许通知权限');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调度本地通知
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: item.title,
|
||||
body: item.content,
|
||||
data: { screen: 'ask', extra: item.extra, id: item.id },
|
||||
priority: 'high', // 关键:设置 high 或 max
|
||||
},
|
||||
trigger: {
|
||||
seconds: 2, // 延迟2秒显示
|
||||
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL // 添加 type 字段
|
||||
}, // 延迟2秒显示
|
||||
});
|
||||
};
|
||||
|
||||
// 监听通知点击事件
|
||||
useEffect(() => {
|
||||
const notificationListener = Notifications.addNotificationResponseReceivedListener(response => {
|
||||
const data = response.notification.request.content.data;
|
||||
console.log('通知被点击,数据:', data);
|
||||
pollingData?.filter((item) => item.id !== data.id);
|
||||
});
|
||||
|
||||
// 清理监听器
|
||||
return () => {
|
||||
Notifications.removeNotificationSubscription(notificationListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 轮询获取推送消息
|
||||
const startPolling = useCallback(async (interval: number = 5000) => {
|
||||
|
||||
// 设置轮询
|
||||
pollingInterval.current = setInterval(async () => {
|
||||
if (isMounted.current) {
|
||||
await getMessageData();
|
||||
}
|
||||
}, interval);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (pollingInterval.current) {
|
||||
clearInterval(pollingInterval.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 获取推送消息
|
||||
const getMessageData = async () => {
|
||||
try {
|
||||
const response = await fetchApi<PollingData[]>("/notice/push/message", {
|
||||
method: "POST"
|
||||
});
|
||||
setPollingData((prev) => ([...prev, ...response]));
|
||||
} catch (error) {
|
||||
console.error('获取轮询数据时出错:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取认证token
|
||||
const getAuthToken = async (): Promise<string> => {
|
||||
let tokenValue = '';
|
||||
if (Platform.OS === 'web') {
|
||||
tokenValue = localStorage.getItem('token') || '';
|
||||
} else {
|
||||
tokenValue = (await SecureStore.getItemAsync('token')) || '';
|
||||
}
|
||||
setToken(tokenValue); // 只在获取到新token时更新状态
|
||||
return tokenValue;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
if (token) {
|
||||
// 启动轮询
|
||||
startPolling(5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推送消息出错:', error);
|
||||
}
|
||||
};
|
||||
checkAuthStatus();
|
||||
|
||||
return () => {
|
||||
// 清理函数
|
||||
if (pollingInterval.current) {
|
||||
clearInterval(pollingInterval.current);
|
||||
}
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
// 本地推送
|
||||
useEffect(() => {
|
||||
pollingData?.map((item) => {
|
||||
sendNotification(item)
|
||||
})
|
||||
}, [pollingData])
|
||||
|
||||
// 轮询获取token
|
||||
useEffect(() => {
|
||||
// 如果已经有token,直接返回
|
||||
if (token) {
|
||||
if (tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!tokenInterval.current) return;
|
||||
// 设置轮询
|
||||
tokenInterval.current = setInterval(async () => {
|
||||
if (isMounted.current) {
|
||||
const currentToken = await getAuthToken();
|
||||
// 如果获取到token,清除定时器
|
||||
if (currentToken && tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
};
|
||||
}, [token]); // 添加token作为依赖
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
@ -120,6 +269,16 @@ export default function TabLayout() {
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 对话详情页 */}
|
||||
<Tabs.Screen
|
||||
name="chat-details"
|
||||
options={{
|
||||
title: 'chat-details',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,107 +6,152 @@ import { ThemedText } from "@/components/ThemedText";
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { Message } from "@/types/ask";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
export default function AskScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
// 在组件内部添加 ref
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
// 用于控制是否显示问候页面
|
||||
const [isHello, setIsHello] = useState(true);
|
||||
|
||||
// 获取对话id
|
||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||
|
||||
// 用户对话信息收集
|
||||
const [userMessages, setUserMessages] = useState<Message[]>([]);
|
||||
// 选择图片
|
||||
const [selectedImages, setSelectedImages] = useState<string[]>([]);
|
||||
// 动画值
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
const fadeAnimChat = useRef(new Animated.Value(0)).current;
|
||||
|
||||
const createNewConversation = useCallback(async () => {
|
||||
// TODO 用户未输入时,显示提示信息
|
||||
setUserMessages([{
|
||||
content: {
|
||||
text: "请输入您的问题,寻找,请稍等..."
|
||||
},
|
||||
role: 'Assistant',
|
||||
timestamp: new Date().toISOString()
|
||||
}]);
|
||||
const data = await fetchApi<string>("/chat/new", {
|
||||
method: "POST",
|
||||
});
|
||||
setConversationId(data);
|
||||
}, []);
|
||||
|
||||
// 获取路由参数
|
||||
const { sessionId, newSession } = useLocalSearchParams<{
|
||||
sessionId: string;
|
||||
newSession: string;
|
||||
}>();
|
||||
// 添加自动滚动到底部的效果
|
||||
|
||||
// 处理滚动到底部
|
||||
useEffect(() => {
|
||||
if (scrollViewRef.current && !isHello) {
|
||||
scrollViewRef.current.scrollToEnd({ animated: true });
|
||||
}
|
||||
}, [userMessages, isHello]);
|
||||
|
||||
// 处理路由参数
|
||||
useEffect(() => {
|
||||
if (sessionId) {
|
||||
setConversationId(sessionId)
|
||||
setIsHello(false)
|
||||
setConversationId(sessionId);
|
||||
setIsHello(false);
|
||||
fetchApi<Message[]>(`/chats/${sessionId}/message-history`).then((res) => {
|
||||
setUserMessages(res)
|
||||
})
|
||||
setUserMessages(res);
|
||||
});
|
||||
}
|
||||
if (newSession) {
|
||||
setIsHello(false)
|
||||
createNewConversation()
|
||||
// if (newSession) {
|
||||
// setIsHello(false);
|
||||
// createNewConversation();
|
||||
// }
|
||||
}, [sessionId]);
|
||||
|
||||
// 动画效果
|
||||
useEffect(() => {
|
||||
if (isHello) {
|
||||
// 显示欢迎页,隐藏聊天页
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(fadeAnimChat, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
]).start();
|
||||
} else {
|
||||
// 显示聊天页,隐藏欢迎页
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(fadeAnimChat, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
]).start();
|
||||
}
|
||||
}, [sessionId, newSession])
|
||||
}, [isHello, fadeAnim, fadeAnimChat]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: 'white', paddingTop: insets.top }}>
|
||||
{/* 导航栏 - 保持在顶部 */}
|
||||
<View style={isHello ? "" : styles.navbar} className="relative w-full flex flex-row items-center justify-between pb-3 pt-[2rem]">
|
||||
{/* 点击去memo list 页面 */}
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
{/* 导航栏 */}
|
||||
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => {
|
||||
router.replace('/memo-list');
|
||||
}}
|
||||
onPress={() => router.push('/memo-list')}
|
||||
>
|
||||
<ReturnArrow />
|
||||
</TouchableOpacity>
|
||||
<ThemedText className={`!text-textSecondary font-semibold text-3xl w-full text-center flex-1 ${isHello ? "opacity-0" : ""}`}>MemoWake</ThemedText>
|
||||
<View />
|
||||
<ThemedText style={styles.title}>MemoWake</ThemedText>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
style={styles.keyboardAvoidingView}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 0}
|
||||
enabled={!isHello}
|
||||
>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={false}
|
||||
onContentSizeChange={() => {
|
||||
if (scrollViewRef.current && !isHello) {
|
||||
scrollViewRef.current.scrollToEnd({ animated: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 内容区域 */}
|
||||
<View className="flex-1">
|
||||
{isHello ? <AskHello /> : <Chat userMessages={userMessages} sessionId={sessionId} />}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View style={styles.contentContainer}>
|
||||
{/* 欢迎页面 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.absoluteView,
|
||||
{
|
||||
opacity: fadeAnim,
|
||||
// 使用 pointerEvents 控制交互
|
||||
pointerEvents: isHello ? 'auto' : 'none',
|
||||
zIndex: 1
|
||||
}
|
||||
]}
|
||||
>
|
||||
<AskHello />
|
||||
</Animated.View>
|
||||
|
||||
{/* 功能区 - 放在 KeyboardAvoidingView 内但在 ScrollView 外 */}
|
||||
<View className="w-full px-[1.5rem] mb-[2rem]">
|
||||
<SendMessage setUserMessages={setUserMessages} setConversationId={setConversationId} setIsHello={setIsHello} conversationId={conversationId} />
|
||||
{/* 聊天页面 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.absoluteView,
|
||||
{
|
||||
opacity: fadeAnimChat,
|
||||
// 使用 pointerEvents 控制交互
|
||||
pointerEvents: isHello ? 'none' : 'auto',
|
||||
zIndex: 0
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Chat userMessages={userMessages} sessionId={sessionId} setSelectedImages={setSelectedImages} selectedImages={selectedImages} />
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* 输入框 */}
|
||||
<View style={styles.inputContainer}>
|
||||
<SendMessage
|
||||
setIsHello={setIsHello}
|
||||
setUserMessages={setUserMessages}
|
||||
setConversationId={setConversationId}
|
||||
conversationId={conversationId}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
@ -114,83 +159,66 @@ export default function AskScreen() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
navbar: {
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: 'white',
|
||||
zIndex: 10,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'white',
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
paddingTop: 60
|
||||
},
|
||||
navbar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: 'white',
|
||||
// 使用 border 替代阴影
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(0,0,0,0.1)',
|
||||
// 如果需要更柔和的边缘,可以添加一个微妙的阴影
|
||||
elevation: 1, // Android
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 1,
|
||||
},
|
||||
hiddenNavbar: {
|
||||
shadowOpacity: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
backButton: {
|
||||
marginLeft: 16,
|
||||
padding: 12
|
||||
padding: 8,
|
||||
marginRight: 8,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
description: {
|
||||
fontSize: 16,
|
||||
color: '#666',
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
marginBottom: 40,
|
||||
paddingHorizontal: 20,
|
||||
lineHeight: 24,
|
||||
flex: 1,
|
||||
},
|
||||
chipsContainer: {
|
||||
width: "100%",
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
placeholder: {
|
||||
width: 40,
|
||||
},
|
||||
// 更新 keyboardAvoidingView 和 contentContainer 样式
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
marginBottom: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
overflow: "scroll",
|
||||
paddingBottom: 20,
|
||||
},
|
||||
chip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFF5E6',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 20,
|
||||
margin: 5,
|
||||
},
|
||||
chipText: {
|
||||
marginLeft: 6,
|
||||
color: '#FF9500',
|
||||
fontSize: 14,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
padding: 16,
|
||||
paddingBottom: 30,
|
||||
absoluteView: {
|
||||
position: 'absolute', // 保持绝对定位
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
borderColor: '#FF9500',
|
||||
borderWidth: 1,
|
||||
borderRadius: 25,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
width: '100%', // 确保输入框宽度撑满
|
||||
},
|
||||
voiceButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#FF9500',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 8, // 添加一点右边距
|
||||
inputContainer: {
|
||||
padding: 16,
|
||||
paddingBottom: 24,
|
||||
backgroundColor: 'white',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#f0f0f0',
|
||||
},
|
||||
});
|
||||
@ -3,7 +3,7 @@ import { registerBackgroundUploadTask, triggerManualUpload } from '@/components/
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
import { useRouter } from 'expo-router';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@ -15,17 +15,24 @@ export default function HomeScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [token, setToken] = useState('');
|
||||
const tokenInterval = useRef<NodeJS.Timeout | number>(null);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const getAuthToken = async (): Promise<string> => {
|
||||
let tokenValue = '';
|
||||
if (Platform.OS === 'web') {
|
||||
tokenValue = localStorage.getItem('token') || '';
|
||||
} else {
|
||||
tokenValue = (await SecureStore.getItemAsync('token')) || '';
|
||||
}
|
||||
setToken(tokenValue); // 只在获取到新token时更新状态
|
||||
return tokenValue;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
let token;
|
||||
if (Platform.OS === 'web') {
|
||||
token = localStorage.getItem('token') || '';
|
||||
} else {
|
||||
token = await SecureStore.getItemAsync('token') || '';
|
||||
}
|
||||
|
||||
const loggedIn = !!token;
|
||||
setIsLoggedIn(loggedIn);
|
||||
|
||||
@ -40,16 +47,46 @@ export default function HomeScreen() {
|
||||
}
|
||||
router.replace('/ask');
|
||||
}
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error('检查登录状态出错:', error);
|
||||
setIsLoading(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
// 轮询获取token
|
||||
useEffect(() => {
|
||||
// 如果已经有token,直接返回
|
||||
if (token) {
|
||||
if (tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!tokenInterval.current) return;
|
||||
// 设置轮询
|
||||
tokenInterval.current = setInterval(async () => {
|
||||
if (isMounted.current) {
|
||||
const currentToken = await getAuthToken();
|
||||
// 如果获取到token,清除定时器
|
||||
if (currentToken && tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
};
|
||||
}, [token]); // 添加token作为依赖
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className="flex-1 bg-bgPrimary justify-center items-center">
|
||||
@ -88,7 +125,7 @@ export default function HomeScreen() {
|
||||
{"\n"}
|
||||
{t('auth.welcomeAwaken.back', { ns: 'login' })}
|
||||
</Text>
|
||||
{/* <MessagePush /> */}
|
||||
|
||||
{/* 唤醒按钮 */}
|
||||
<TouchableOpacity
|
||||
className="bg-white rounded-full px-10 py-4 shadow-[0_2px_4px_rgba(0,0,0,0.1)] w-full items-center"
|
||||
|
||||
@ -16,7 +16,7 @@ import { fetchApi } from '@/lib/server-api-util';
|
||||
import { CountData, UserInfoDetails } from '@/types/user';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { FlatList, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
export default function OwnerPage() {
|
||||
const insets = useSafeAreaInsets();
|
||||
@ -59,54 +59,57 @@ export default function OwnerPage() {
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
<ScrollView
|
||||
<FlatList
|
||||
data={[]} // 空数据,因为我们只需要渲染一次
|
||||
renderItem={null} // 不需要渲染项目
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ flexGrow: 1, gap: 16, marginHorizontal: 16 }}
|
||||
>
|
||||
{/* 用户信息 */}
|
||||
<UserInfo userInfo={userInfoDetails} />
|
||||
ListHeaderComponent={
|
||||
<View style={{ gap: 16 }}>
|
||||
{/* 用户信息 */}
|
||||
<UserInfo userInfo={userInfoDetails} />
|
||||
|
||||
{/* 设置栏 */}
|
||||
<AlbumComponent setModalVisible={setModalVisible} />
|
||||
{/* 设置栏 */}
|
||||
<AlbumComponent setModalVisible={setModalVisible} />
|
||||
|
||||
{/* 资源数据 */}
|
||||
<View style={styles.resourceContainer}>
|
||||
<ResourceComponent title={t("generalSetting.usedStorage", { ns: "personal" })} subtitle={`${countData?.counter?.total_count?.video_count || 0}videos/${countData?.counter?.total_count?.photo_count || 0}photos`} data={{ all: userInfoDetails.total_bytes, used: countData.used_bytes }} icon={<UsedStorageSvg />} style={{ flex: 1 }} isFormatBytes={true} />
|
||||
<ResourceComponent title={t("generalSetting.remainingPoints", { ns: "personal" })} data={{ all: userInfoDetails.total_points, used: userInfoDetails.remain_points }} icon={<PointsSvg />} style={{ flex: 1 }} />
|
||||
</View>
|
||||
{/* 数据统计 */}
|
||||
<CountComponent
|
||||
data={[{ title: t("generalSetting.totalVideo", { ns: "personal" }), number: countData?.counter?.total_count?.video_count || 0 }, { title: t("generalSetting.totalPhoto", { ns: "personal" }), number: countData?.counter?.total_count?.photo_count || 0 }, { title: t("generalSetting.live", { ns: "personal" }), number: countData?.counter?.total_count?.live_count || 0 }, { title: t("generalSetting.videoLength", { ns: "personal" }), number: formatDuration(countData?.counter?.total_count?.video_length || 0) }]}
|
||||
/>
|
||||
{/* 资源数据 */}
|
||||
<View style={styles.resourceContainer}>
|
||||
<ResourceComponent title={t("generalSetting.usedStorage", { ns: "personal" })} subtitle={`${countData?.counter?.total_count?.video_count || 0}videos/${countData?.counter?.total_count?.photo_count || 0}photos`} data={{ all: userInfoDetails.total_bytes, used: countData.used_bytes }} icon={<UsedStorageSvg />} style={{ flex: 1 }} isFormatBytes={true} />
|
||||
<ResourceComponent title={t("generalSetting.remainingPoints", { ns: "personal" })} data={{ all: userInfoDetails.total_points, used: userInfoDetails.remain_points }} icon={<PointsSvg />} style={{ flex: 1 }} />
|
||||
</View>
|
||||
{/* 数据统计 */}
|
||||
<CountComponent
|
||||
data={[{ title: t("generalSetting.totalVideo", { ns: "personal" }), number: countData?.counter?.total_count?.video_count || 0 }, { title: t("generalSetting.totalPhoto", { ns: "personal" }), number: countData?.counter?.total_count?.photo_count || 0 }, { title: t("generalSetting.live", { ns: "personal" }), number: countData?.counter?.total_count?.live_count || 0 }, { title: t("generalSetting.videoLength", { ns: "personal" }), number: formatDuration(countData?.counter?.total_count?.video_length || 0) }]}
|
||||
/>
|
||||
|
||||
{/* 分类 */}
|
||||
<View style={{ height: 145 }}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 16 }} >
|
||||
{countData?.counter?.category_count && Object.entries(countData?.counter?.category_count).map(([key, value], index) => {
|
||||
return (
|
||||
<CategoryComponent
|
||||
key={index}
|
||||
title={key}
|
||||
data={[{ title: 'Video', number: value.video_count }, { title: 'Photo', number: value.photo_count }, { title: 'Length', number: formatDuration(value.video_length || 0) }]}
|
||||
bgSvg={value.cover_url}
|
||||
style={{ aspectRatio: 1, flex: 1 }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
{/* 分类 */}
|
||||
<View style={{ height: 145 }}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: 16 }} >
|
||||
{countData?.counter?.category_count && Object.entries(countData?.counter?.category_count).map(([key, value], index) => {
|
||||
return (
|
||||
<CategoryComponent
|
||||
key={index}
|
||||
title={key}
|
||||
data={[{ title: 'Video', number: value.video_count }, { title: 'Photo', number: value.photo_count }, { title: 'Length', number: formatDuration(value.video_length || 0) }]}
|
||||
bgSvg={value.cover_url}
|
||||
style={{ aspectRatio: 1, flex: 1 }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* 作品数据 */}
|
||||
<View className='flex flex-row justify-between gap-[1rem]'>
|
||||
<CreateCountComponent title={t("generalSetting.storiesCreated", { ns: "personal" })} icon={<StoriesSvg />} number={userInfoDetails.stories_count} />
|
||||
<CreateCountComponent title={t("generalSetting.conversationsWithMemo", { ns: "personal" })} icon={<ConversationsSvg />} number={userInfoDetails.conversations_count} />
|
||||
</View>
|
||||
|
||||
{/* 排行榜 */}
|
||||
<Ranking data={userInfoDetails.title_rankings} />
|
||||
|
||||
</ScrollView>
|
||||
{/* 作品数据 */}
|
||||
<View className='flex flex-row justify-between gap-[1rem]'>
|
||||
<CreateCountComponent title={t("generalSetting.storiesCreated", { ns: "personal" })} icon={<StoriesSvg />} number={userInfoDetails.stories_count} />
|
||||
<CreateCountComponent title={t("generalSetting.conversationsWithMemo", { ns: "personal" })} icon={<ConversationsSvg />} number={userInfoDetails.conversations_count} />
|
||||
</View>
|
||||
|
||||
{/* 排行榜 */}
|
||||
<Ranking data={userInfoDetails.title_rankings} />
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
{/* 设置弹窗 */}
|
||||
<SettingModal modalVisible={modalVisible} setModalVisible={setModalVisible} userInfo={userInfoDetails.user_info} />
|
||||
|
||||
@ -122,6 +125,9 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'white',
|
||||
paddingBottom: 86,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
resourceContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 16
|
||||
|
||||
@ -5,11 +5,12 @@ import UserName from '@/components/user-message.tsx/userName';
|
||||
import { fetchApi } from '@/lib/server-api-util';
|
||||
import { FileUploadItem } from '@/types/upload';
|
||||
import { User } from '@/types/user';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, StatusBar, View } from 'react-native';
|
||||
export type Steps = "userName" | "look" | "choice" | "done";
|
||||
export default function UserMessage() {
|
||||
// 步骤
|
||||
// 步骤
|
||||
const [steps, setSteps] = useState<Steps>("userName")
|
||||
const [username, setUsername] = useState('')
|
||||
const [avatar, setAvatar] = useState('')
|
||||
@ -18,6 +19,10 @@ export default function UserMessage() {
|
||||
const [userInfo, setUserInfo] = useState<User | null>(null);
|
||||
const statusBarHeight = StatusBar?.currentHeight ?? 0;
|
||||
|
||||
// 获取路由参数
|
||||
const params = useLocalSearchParams();
|
||||
const { username: usernameParam } = params;
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = async () => {
|
||||
const res = await fetchApi<User>("/iam/user-info");
|
||||
@ -44,7 +49,7 @@ export default function UserMessage() {
|
||||
useEffect(() => {
|
||||
getUserInfo();
|
||||
setSteps("userName")
|
||||
}, []);
|
||||
}, [usernameParam]);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
|
||||
4
assets/icons/svg/chatIn.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
||||
<path d="M4.913 2.658c2.075-.27 4.19-.408 6.337-.408 2.147 0 4.262.139 6.337.408 1.922.25 3.291 1.861 3.405 3.727a4.403 4.403 0 0 0-1.032-.211 50.89 50.89 0 0 0-8.42 0c-2.358.196-4.04 2.19-4.04 4.434v4.286a4.47 4.47 0 0 0 2.433 3.984L7.28 21.53A.75.75 0 0 1 6 21v-4.03a48.527 48.527 0 0 1-1.087-.128C2.905 16.58 1.5 14.833 1.5 12.862V6.638c0-1.97 1.405-3.718 3.413-3.979Z" />
|
||||
<path d="M15.75 7.5c-1.376 0-2.739.057-4.086.169C10.124 7.797 9 9.103 9 10.609v4.285c0 1.507 1.128 2.814 2.67 2.94 1.243.102 2.5.157 3.768.165l2.782 2.781a.75.75 0 0 0 1.28-.53v-2.39l.33-.026c1.542-.125 2.67-1.433 2.67-2.94v-4.286c0-1.505-1.125-2.811-2.664-2.94A49.392 49.392 0 0 0 15.75 7.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 780 B |
3
assets/icons/svg/chatNotIn.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 717 B |
3
assets/icons/svg/folder.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 3V13.8C1 14.9201 1 15.4798 1.21799 15.9076C1.40973 16.2839 1.71547 16.5905 2.0918 16.7822C2.5192 17 3.07899 17 4.19691 17H15.8031C16.921 17 17.48 17 17.9074 16.7822C18.2837 16.5905 18.5905 16.2841 18.7822 15.9078C19.0002 15.48 19.0002 14.9199 19.0002 13.7998L19.0002 6.19978C19.0002 5.07967 19.0002 4.51962 18.7822 4.0918C18.5905 3.71547 18.2839 3.40973 17.9076 3.21799C17.4798 3 16.9201 3 15.8 3H10M1 3H10M1 3C1 1.89543 1.89543 1 3 1H6.67452C7.1637 1 7.40886 1 7.63904 1.05526C7.84311 1.10425 8.03785 1.18526 8.2168 1.29492C8.41857 1.41857 8.59181 1.59182 8.9375 1.9375L10 3" stroke="#4C320C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
3
assets/icons/svg/personIn.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
|
||||
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 344 B |
3
assets/icons/svg/personNotIn.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
3
assets/icons/svg/yes.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 4.00001L4.33357 7L11 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
BIN
assets/images/png/owner/ask.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
@ -1,10 +1,15 @@
|
||||
import ChatSvg from "@/assets/icons/svg/chat.svg";
|
||||
import FolderSvg from "@/assets/icons/svg/folder.svg";
|
||||
import MoreSvg from "@/assets/icons/svg/more.svg";
|
||||
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
|
||||
import YesSvg from "@/assets/icons/svg/yes.svg";
|
||||
import { Message, Video } from "@/types/ask";
|
||||
import { MaterialItem } from "@/types/personal-info";
|
||||
import { useVideoPlayer, VideoView } from 'expo-video';
|
||||
import React from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Modal,
|
||||
Pressable,
|
||||
@ -20,18 +25,23 @@ import TypewriterText from "./typewriterText";
|
||||
import { mergeArrays } from "./utils";
|
||||
|
||||
interface RenderMessageProps {
|
||||
insets: { top: number };
|
||||
item: Message;
|
||||
sessionId: string;
|
||||
setModalVisible: React.Dispatch<React.SetStateAction<{ visible: boolean, data: Video | MaterialItem }>>;
|
||||
modalVisible: { visible: boolean, data: Video | MaterialItem };
|
||||
setModalDetailsVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
modalDetailsVisible: boolean;
|
||||
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
selectedImages: string[];
|
||||
}
|
||||
|
||||
const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: RenderMessageProps) => {
|
||||
const renderMessage = ({ insets, item, sessionId, setModalVisible, modalVisible, setModalDetailsVisible, modalDetailsVisible, setSelectedImages, selectedImages }: RenderMessageProps) => {
|
||||
const isUser = item.role === 'User';
|
||||
const isVideo = (data: Video | MaterialItem): data is Video => {
|
||||
return 'video' in data;
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
// 创建一个新的 VideoPlayer 组件
|
||||
const VideoPlayer = ({
|
||||
videoUrl,
|
||||
@ -90,25 +100,28 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
}
|
||||
</Text>
|
||||
|
||||
{(item.content.image_material_infos && item.content.image_material_infos.length > 0 || item.content.video_material_infos && item.content.video_material_infos.length > 0) && (
|
||||
{(mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.length || 0 > 0) && (
|
||||
<View className="relative">
|
||||
<View className="mt-2 flex flex-row gap-2 w-full">
|
||||
{mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.slice(0, 3)?.map((image, index, array) => (
|
||||
<View style={[styles.imageGridContainer, { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }]}>
|
||||
{mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.slice(0, 3)?.map((image) => (
|
||||
<Pressable
|
||||
key={`${image.role}-${image.timestamp}`}
|
||||
key={image?.id || image?.video?.id}
|
||||
onPress={() => {
|
||||
setModalVisible({ visible: true, data: image });
|
||||
}}
|
||||
style={({ pressed }) => [
|
||||
array.length === 1 ? styles.fullWidthImage : styles.gridImage,
|
||||
array.length === 2 && { width: '49%' },
|
||||
array.length >= 3 && { width: '32%' },
|
||||
{ opacity: pressed ? 0.8 : 1 } // 添加按下效果
|
||||
]}
|
||||
style={{
|
||||
width: '32%',
|
||||
aspectRatio: 1,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: image?.preview_file_info?.url || image.video?.preview_file_info?.url }}
|
||||
className="rounded-xl w-full h-full"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 12,
|
||||
}}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</Pressable>
|
||||
@ -116,12 +129,14 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
</View>
|
||||
{
|
||||
((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0)) > 3
|
||||
&& <View className="absolute top-1/2 -translate-y-1/2 -right-4 translate-x-1/2 bg-bgPrimary flex flex-row items-center gap-2 p-1 pl-2 rounded-full">
|
||||
&& <TouchableOpacity className="absolute top-1/2 -translate-y-1/2 -right-4 translate-x-1/2 bg-bgPrimary flex flex-row items-center gap-2 p-1 pl-2 rounded-full" onPress={() => {
|
||||
setModalDetailsVisible(true);
|
||||
}}>
|
||||
<ThemedText className="!text-white font-semibold">{((item.content.video_material_infos?.length || 0) + (item.content.image_material_infos?.length || 0))}</ThemedText>
|
||||
<View className="bg-white rounded-full p-2">
|
||||
<MoreSvg />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
)}
|
||||
@ -194,6 +209,96 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Modal>
|
||||
<Modal
|
||||
animationType="fade"
|
||||
visible={modalDetailsVisible}
|
||||
transparent={false}
|
||||
statusBarTranslucent={true}
|
||||
onRequestClose={() => {
|
||||
setModalDetailsVisible(false);
|
||||
}}
|
||||
>
|
||||
<View style={[detailsStyles.container, { paddingTop: insets?.top }]}>
|
||||
<View style={detailsStyles.header}>
|
||||
<TouchableOpacity onPress={() => setModalDetailsVisible(false)}>
|
||||
<ReturnArrow />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={detailsStyles.headerText}>{t('ask.selectPhoto', { ns: 'ask' })}</ThemedText>
|
||||
<FolderSvg />
|
||||
</View>
|
||||
<View style={{ overflow: 'scroll', height: "100%" }}>
|
||||
<FlatList
|
||||
data={mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])}
|
||||
numColumns={3}
|
||||
keyExtractor={(item) => item.id}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={detailsStyles.flatListContent}
|
||||
initialNumToRender={12}
|
||||
maxToRenderPerBatch={12}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={10}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={detailsStyles.gridItemContainer}
|
||||
key={item.id}
|
||||
>
|
||||
<View style={detailsStyles.gridItem}>
|
||||
<ThemedText style={detailsStyles.imageNumber}>
|
||||
{selectedImages?.map((image, index) => {
|
||||
if (image === item.id || image === item.video?.id) {
|
||||
return index + 1
|
||||
}
|
||||
})}
|
||||
</ThemedText>
|
||||
<Image
|
||||
source={{ uri: item?.preview_file_info?.url || item.video?.preview_file_info?.url }}
|
||||
style={detailsStyles.image}
|
||||
onError={(error) => console.log('Image load error:', error.nativeEvent.error)}
|
||||
onLoad={() => console.log('Image loaded successfully')}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[detailsStyles.circleMarker, selectedImages.includes(item?.id || item?.video?.id) ? detailsStyles.circleMarkerSelected : ""]}
|
||||
onPress={() => {
|
||||
setSelectedImages((prev) => {
|
||||
if (prev.includes(item?.id || item?.video?.id)) {
|
||||
return prev.filter((id) => id !== (item.id || item?.video?.id));
|
||||
} else {
|
||||
return [...prev, item.id || item.video?.id];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{selectedImages.includes(item?.id || item?.video?.id) ? <YesSvg width={16} height={16} /> : ""}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={detailsStyles.footer}>
|
||||
<TouchableOpacity
|
||||
style={detailsStyles.continueButton}
|
||||
onPress={async () => {
|
||||
// 如果用户没有选择 则为选择全部
|
||||
if (selectedImages?.length < 0) {
|
||||
setSelectedImages(mergeArrays(item.content.image_material_infos || [], item.content.video_material_infos || [])?.map((item) => {
|
||||
return item.id || item.video?.id
|
||||
}))
|
||||
}
|
||||
setModalDetailsVisible(false)
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={detailsStyles.continueButtonText}>
|
||||
{t('ask.continueAsking', { ns: 'ask' })}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@ -202,6 +307,12 @@ const renderMessage = ({ item, sessionId, setModalVisible, modalVisible }: Rende
|
||||
export default renderMessage;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imageGridContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
},
|
||||
video: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@ -276,4 +387,125 @@ const styles = StyleSheet.create({
|
||||
color: '#000',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
const detailsStyles = StyleSheet.create({
|
||||
gridItemContainer: {
|
||||
flex: 1, // 使用 flex 布局使项目平均分配空间
|
||||
maxWidth: '33.33%', // 每行最多4个项目
|
||||
aspectRatio: 1, // 保持1:1的宽高比
|
||||
},
|
||||
flatListContent: {
|
||||
paddingBottom: 100, // 为底部按钮留出更多空间
|
||||
paddingHorizontal: 8, // 添加水平内边距
|
||||
paddingTop: 8,
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: "#4C320C"
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
backgroundColor: '#fff',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
},
|
||||
imageNumber: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
zIndex: 10, // 确保数字显示在图片上方
|
||||
},
|
||||
imageNumberText: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
},
|
||||
numberText: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.9)', // 使用半透明蓝色背景
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
gridItem: {
|
||||
flex: 1, // 填充父容器
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderWidth: 1,
|
||||
borderColor: '#eee',
|
||||
height: '100%', // 确保高度填满容器
|
||||
position: 'relative',
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
circleMarker: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 3,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
circleMarkerSelected: {
|
||||
backgroundColor: '#FFB645',
|
||||
},
|
||||
markerText: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#000',
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 0,
|
||||
right: 0,
|
||||
paddingHorizontal: 16,
|
||||
zIndex: 10,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
continueButton: {
|
||||
backgroundColor: '#E2793F',
|
||||
borderRadius: 32,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
},
|
||||
continueButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
}
|
||||
});
|
||||
@ -1,20 +1,23 @@
|
||||
import { Message, Video } from '@/types/ask';
|
||||
import { MaterialItem } from '@/types/personal-info';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { Dispatch, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
FlatList,
|
||||
SafeAreaView
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import renderMessage from "./aiChat";
|
||||
|
||||
interface ChatProps {
|
||||
userMessages: Message[];
|
||||
sessionId: string;
|
||||
setSelectedImages: Dispatch<SetStateAction<string[]>>;
|
||||
selectedImages: string[];
|
||||
}
|
||||
|
||||
function ChatComponent({ userMessages, sessionId }: ChatProps) {
|
||||
function ChatComponent({ userMessages, sessionId, setSelectedImages, selectedImages }: ChatProps) {
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
const [modalVisible, setModalVisible] = React.useState({ visible: false, data: {} as Video | MaterialItem });
|
||||
|
||||
// 使用 useCallback 缓存 keyExtractor 函数
|
||||
@ -23,6 +26,9 @@ function ChatComponent({ userMessages, sessionId }: ChatProps) {
|
||||
// 使用 useMemo 缓存样式对象
|
||||
const contentContainerStyle = useMemo(() => ({ padding: 16 }), []);
|
||||
|
||||
// 详情弹窗
|
||||
const [modalDetailsVisible, setModalDetailsVisible] = useState<boolean>(false);
|
||||
|
||||
// 自动滚动到底部
|
||||
useEffect(() => {
|
||||
if (userMessages.length > 0) {
|
||||
@ -45,7 +51,7 @@ function ChatComponent({ userMessages, sessionId }: ChatProps) {
|
||||
updateCellsBatchingPeriod={50}
|
||||
initialNumToRender={10}
|
||||
windowSize={11}
|
||||
renderItem={({ item }) => renderMessage({ item, sessionId, modalVisible, setModalVisible })}
|
||||
renderItem={({ item }) => renderMessage({ setSelectedImages, selectedImages, insets, item, sessionId, modalVisible, setModalVisible, setModalDetailsVisible, modalDetailsVisible })}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@ -1,28 +1,39 @@
|
||||
import IP from "@/assets/icons/svg/ip.svg";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from 'react-native';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
|
||||
export default function AskHello() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white overflow-auto w-full">
|
||||
{/* 内容区域 IP与介绍文本*/}
|
||||
<View className="items-center flex-1">
|
||||
<ThemedText className="text-3xl font-bold text-center">
|
||||
{t('ask.hi', { ns: 'ask' })}
|
||||
{"\n"}
|
||||
{t('ask.iAmMemo', { ns: 'ask' })}
|
||||
</ThemedText>
|
||||
<View className="justify-center items-center"><IP /></View>
|
||||
|
||||
<ThemedText className="!text-textPrimary text-center -mt-[4rem]">
|
||||
{t('ask.ready', { ns: 'ask' })}
|
||||
{"\n"}
|
||||
{t('ask.justAsk', { ns: 'ask' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View className="flex-1 bg-white w-full">
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View className="items-center">
|
||||
<ThemedText className="text-3xl font-bold text-center">
|
||||
{t('ask.hi', { ns: 'ask' })}
|
||||
{"\n"}
|
||||
{t('ask.iAmMemo', { ns: 'ask' })}
|
||||
</ThemedText>
|
||||
<View className="justify-center items-center my-4">
|
||||
<IP />
|
||||
</View>
|
||||
<ThemedText className="!text-textPrimary text-center">
|
||||
{t('ask.ready', { ns: 'ask' })}
|
||||
{"\n"}
|
||||
{t('ask.justAsk', { ns: 'ask' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
import VoiceSvg from '@/assets/icons/svg/vioce.svg';
|
||||
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||
import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Keyboard,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
@ -12,13 +13,15 @@ import { fetchApi } from '@/lib/server-api-util';
|
||||
import { Message } from '@/types/ask';
|
||||
|
||||
interface Props {
|
||||
setIsHello: (isHello: boolean) => void,
|
||||
setIsHello: Dispatch<SetStateAction<boolean>>,
|
||||
conversationId: string | null,
|
||||
setUserMessages: Dispatch<SetStateAction<Message[]>>;
|
||||
setConversationId: (conversationId: string) => void,
|
||||
selectedImages: string[];
|
||||
setSelectedImages: Dispatch<SetStateAction<string[]>>;
|
||||
}
|
||||
export default function SendMessage(props: Props) {
|
||||
const { setIsHello, conversationId, setUserMessages, setConversationId } = props;
|
||||
const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props;
|
||||
|
||||
// 用户询问
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
@ -29,20 +32,23 @@ export default function SendMessage(props: Props) {
|
||||
method: "POST",
|
||||
});
|
||||
setConversationId(data);
|
||||
await getConversation({ session_id: data, user_text });
|
||||
await getConversation({ session_id: data, user_text, material_ids: [] });
|
||||
}, []);
|
||||
|
||||
// 获取对话信息
|
||||
const getConversation = useCallback(async ({ session_id, user_text }: { session_id: string, user_text: string }) => {
|
||||
const getConversation = useCallback(async ({ session_id, user_text, material_ids }: { session_id: string, user_text: string, material_ids: string[] }) => {
|
||||
// 获取对话信息必须要有对话id
|
||||
if (!session_id) return;
|
||||
|
||||
const response = await fetchApi<Message>(`/chat`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
session_id,
|
||||
user_text
|
||||
user_text,
|
||||
material_ids
|
||||
})
|
||||
});
|
||||
setSelectedImages([]);
|
||||
setUserMessages((prev: Message[]) => [...prev, response]?.filter((item: Message) => item.content.text !== '正在寻找,请稍等...'));
|
||||
}, []);
|
||||
|
||||
@ -70,17 +76,37 @@ export default function SendMessage(props: Props) {
|
||||
// 如果没有对话ID,创建新对话并获取消息,否则直接获取消息
|
||||
if (!conversationId) {
|
||||
createNewConversation(text);
|
||||
setIsHello(false);
|
||||
} else {
|
||||
getConversation({
|
||||
session_id: conversationId,
|
||||
user_text: text
|
||||
user_text: text,
|
||||
material_ids: selectedImages
|
||||
});
|
||||
}
|
||||
// 将输入框清空
|
||||
setInputValue('');
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const keyboardWillShowListener = Keyboard.addListener(
|
||||
'keyboardWillShow',
|
||||
() => {
|
||||
console.log('Keyboard will show');
|
||||
setIsHello(false);
|
||||
setUserMessages([{
|
||||
content: {
|
||||
text: "快来寻找你的记忆吧。。。"
|
||||
},
|
||||
role: 'Assistant',
|
||||
timestamp: new Date().toISOString()
|
||||
}])
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
keyboardWillShowListener.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
38
components/copy.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, TouchableOpacity } from 'react-native';
|
||||
|
||||
const CopyButton = ({ textToCopy }: { textToCopy: string }) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await Clipboard.setStringAsync(textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handleCopy} style={styles.button}>
|
||||
{isCopied ? (
|
||||
<Ionicons name="checkmark-circle" size={12} color="#FFB645" />
|
||||
) : (
|
||||
<Ionicons name="copy-outline" size={12} color="#333" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 4,
|
||||
},
|
||||
text: {
|
||||
marginLeft: 8,
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default CopyButton;
|
||||
@ -1,11 +1,18 @@
|
||||
import NavbarSvg from "@/assets/icons/svg/navbar.svg";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import ChatInSvg from "@/assets/icons/svg/chatIn.svg";
|
||||
import ChatNotInSvg from "@/assets/icons/svg/chatNotIn.svg";
|
||||
import PersonInSvg from "@/assets/icons/svg/personIn.svg";
|
||||
import PersonNotInSvg from "@/assets/icons/svg/personNotIn.svg";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import React from 'react';
|
||||
import { Platform, TouchableOpacity, View } from 'react-native';
|
||||
import { Dimensions, Image, Platform, TouchableOpacity, View } from 'react-native';
|
||||
import { Circle, Ellipse, G, Mask, Path, Rect, Svg } from 'react-native-svg';
|
||||
|
||||
const AskNavbar = () => {
|
||||
// 获取设备尺寸
|
||||
const { width } = Dimensions.get('window');
|
||||
// 获取路由
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<View className="absolute bottom-0 left-0 right-0 bg-white" style={{
|
||||
shadowColor: '#000',
|
||||
@ -14,10 +21,11 @@ const AskNavbar = () => {
|
||||
shadowRadius: 8,
|
||||
elevation: 10, // For Android
|
||||
}}>
|
||||
<NavbarSvg className="w-full h-full" />
|
||||
{/* <NavbarSvg className="w-[150%] h-full" /> */}
|
||||
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: width, height: 80, resizeMode: 'cover' }} />
|
||||
<View className="absolute bottom-0 top-0 left-0 right-0 flex flex-row justify-between items-center px-[2rem]">
|
||||
<TouchableOpacity onPress={() => router.push('/memo-list')} >
|
||||
<Ionicons name="chatbubbles-outline" size={24} color="#4C320C" />
|
||||
<TouchableOpacity onPress={() => router.push('/memo-list')} style={{ padding: 16 }}>
|
||||
{pathname === "/memo-list" ? <ChatInSvg width={24} height={24} /> : <ChatNotInSvg width={24} height={24} />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
@ -27,7 +35,7 @@ const AskNavbar = () => {
|
||||
params: { newSession: "true" }
|
||||
});
|
||||
}}
|
||||
className={`${Platform.OS === 'web' ? '-mt-[4rem]' : '-mt-[5rem] ml-[0.8rem]'}`}
|
||||
className={`${Platform.OS === 'web' ? '-mt-[4rem]' : width <= 375 ? '-mt-[5rem]' : '-mt-[5rem]'}`}
|
||||
>
|
||||
<View style={{
|
||||
shadowColor: '#FFB645',
|
||||
@ -67,9 +75,9 @@ const AskNavbar = () => {
|
||||
</Svg>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => router.push('/owner')}>
|
||||
<TouchableOpacity onPress={() => router.push('/owner')} style={{ padding: 16 }}>
|
||||
<View>
|
||||
<Ionicons name="person-outline" size={24} color="#4C320C" />
|
||||
{pathname === "/owner" ? <PersonInSvg width={24} height={24} /> : <PersonNotInSvg width={24} height={24} />}
|
||||
{/* <View className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full" /> */}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -45,9 +45,13 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
login({ ...res, email: res?.account }, res.access_token || '');
|
||||
router.replace('/user-message');
|
||||
const userInfo = await fetchApi<User>("/iam/user-info");
|
||||
if (userInfo?.nickname) {
|
||||
router.replace('/ask');
|
||||
} else {
|
||||
router.replace('/user-message');
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('Login failed', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { useAuth } from "../../contexts/auth-context";
|
||||
import { fetchApi } from "../../lib/server-api-util";
|
||||
import { User } from "../../types/user";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
import PrivacyModal from "../owner/qualification/privacy";
|
||||
|
||||
interface LoginProps {
|
||||
updateUrlParam: (status: string, value: string) => void;
|
||||
@ -25,7 +26,9 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword }: Log
|
||||
const [passwordsMatch, setPasswordsMatch] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai');
|
||||
// 协议弹窗
|
||||
const [privacyModalVisible, setPrivacyModalVisible] = useState(false);
|
||||
// 从 URL 参数中获取 task_id 和 steps
|
||||
const params = useLocalSearchParams<{ task_id?: string; steps?: string }>();
|
||||
const taskId = params.task_id;
|
||||
@ -263,10 +266,10 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword }: Log
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{t("auth.telLogin.agree", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => router.push({
|
||||
pathname: '/agreement',
|
||||
params: { type: 'service' }
|
||||
} as any)}>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('terms');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText className="text-sm !text-[#E2793F]">
|
||||
{t("auth.telLogin.terms", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
@ -274,10 +277,10 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword }: Log
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{t("auth.telLogin.and", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => router.push({
|
||||
pathname: '/agreement',
|
||||
params: { type: 'privacy' }
|
||||
} as any)}>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('privacy');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText className="text-sm !text-[#E2793F]">
|
||||
{t("auth.telLogin.privacyPolicy", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
@ -285,14 +288,25 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword }: Log
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{t("auth.telLogin.and", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => router.push({
|
||||
pathname: '/agreement',
|
||||
params: { type: 'user' }
|
||||
} as any)}>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('user');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText className="text-sm !text-[#E2793F]">
|
||||
{t("auth.telLogin.userAgreement", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{t("auth.telLogin.and", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('ai');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText className="text-sm !text-[#E2793F]">
|
||||
{t("auth.telLogin.aiAgreement", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{t("auth.telLogin.agreement", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
@ -319,6 +333,9 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword }: Log
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 协议弹窗 */}
|
||||
<PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} />
|
||||
</View>
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { fetchApi } from '@/lib/server-api-util';
|
||||
import { Policy } from '@/types/personal-info';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import RenderHtml from 'react-native-render-html';
|
||||
|
||||
const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, type: string }) => {
|
||||
@ -62,12 +62,8 @@ const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible:
|
||||
onRequestClose={() => {
|
||||
setModalVisible(!modalVisible);
|
||||
}}>
|
||||
<Pressable
|
||||
style={styles.centeredView}
|
||||
onPress={() => setModalVisible(false)}>
|
||||
<Pressable
|
||||
style={styles.modalView}
|
||||
onPress={(e) => e.stopPropagation()}>
|
||||
<View style={styles.centeredView}>
|
||||
<View style={styles.modalView}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={{ opacity: 0 }}>Settings</Text>
|
||||
<Text style={styles.modalTitle}>{type === 'ai' ? 'AI Policy' : type === 'terms' ? 'Terms of Service' : type === 'privacy' ? 'Privacy Policy' : 'User Agreement'}</Text>
|
||||
@ -85,9 +81,8 @@ const PrivacyModal = (props: { modalVisible: boolean, setModalVisible: (visible:
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@ -125,7 +125,13 @@ const UserInfo = (props: UserInfoProps) => {
|
||||
}
|
||||
<TouchableOpacity style={styles.edit} onPress={() => {
|
||||
setModalVisible(false);
|
||||
router.push('/user-message')
|
||||
// 携带参数跳转
|
||||
router.push({
|
||||
pathname: '/user-message',
|
||||
params: {
|
||||
username: "true"
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<EditSvg />
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -1,25 +1,26 @@
|
||||
import UserSvg from '@/assets/icons/svg/ataver.svg';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { UserInfoDetails } from '@/types/user';
|
||||
// import { Image } from 'expo-image';
|
||||
import { useState } from 'react';
|
||||
import { Image, ScrollView, View } from 'react-native';
|
||||
export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
|
||||
// 添加状态来跟踪图片加载状态
|
||||
const [imageError, setImageError] = useState(false);
|
||||
return (
|
||||
<View className='flex flex-row items-center mt-[1rem] gap-[1rem]'>
|
||||
<View className='flex flex-row justify-between items-center mt-[1rem] gap-[1rem] w-full'>
|
||||
{/* 用户名 */}
|
||||
<View className='flex flex-col gap-4 w-[68vw]'>
|
||||
<View className='flex flex-col gap-4 w-[75%]'>
|
||||
<View className='flex flex-row items-center justify-between w-full'>
|
||||
<View className='flex flex-row items-center gap-2'>
|
||||
<View className='flex flex-row items-center gap-2 w-full'>
|
||||
<ThemedText
|
||||
className='max-w-[36vw] !text-textSecondary !font-semibold !text-2xl'
|
||||
className='max-w-[80%] !text-textSecondary !font-semibold !text-2xl'
|
||||
numberOfLines={1} // 限制为1行
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{userInfo?.user_info?.nickname}
|
||||
</ThemedText>
|
||||
<ScrollView
|
||||
className='max-w-[26vw] '
|
||||
className='max-w-[20%]'
|
||||
horizontal // 水平滚动
|
||||
showsHorizontalScrollIndicator={false} // 隐藏滚动条
|
||||
contentContainerStyle={{
|
||||
@ -40,29 +41,39 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollView
|
||||
className='max-w-[68vw]'
|
||||
horizontal // 水平滚动
|
||||
showsHorizontalScrollIndicator={false} // 隐藏滚动条
|
||||
contentContainerStyle={{
|
||||
flexDirection: 'row',
|
||||
gap: 8 // 间距
|
||||
}}
|
||||
>
|
||||
<ThemedText style={{ color: '#AC7E35', fontSize: 12, fontWeight: '600' }}>User ID:{userInfo?.user_info?.user_id}</ThemedText>
|
||||
</ScrollView>
|
||||
<View>
|
||||
<ScrollView
|
||||
className='max-w-[85%]'
|
||||
horizontal // 水平滚动
|
||||
showsHorizontalScrollIndicator={false} // 隐藏滚动条
|
||||
contentContainerStyle={{
|
||||
flexDirection: 'row',
|
||||
gap: 8 // 间距
|
||||
}}
|
||||
>
|
||||
<ThemedText style={{ color: '#AC7E35', fontSize: 12, fontWeight: '600' }}>User ID: {userInfo?.user_info?.user_id}</ThemedText>
|
||||
</ScrollView>
|
||||
{/* <CopyButton textToCopy={userInfo?.user_info?.user_id || ""} /> */}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
{/* 头像 */}
|
||||
<View>
|
||||
{userInfo?.user_info?.avatar_file_url
|
||||
?
|
||||
<View className='w-auto'>
|
||||
{userInfo?.user_info?.avatar_file_url && !imageError ? (
|
||||
<Image
|
||||
source={{ uri: userInfo?.user_info?.avatar_file_url }}
|
||||
source={{ uri: userInfo.user_info.avatar_file_url }}
|
||||
style={{ width: 80, height: 80, borderRadius: 40 }}
|
||||
onError={() => {
|
||||
console.log('图片加载失败:', userInfo.user_info.avatar_file_url);
|
||||
setImageError(true);
|
||||
}}
|
||||
onLoad={() => {
|
||||
console.log('图片加载成功');
|
||||
}}
|
||||
/>
|
||||
:
|
||||
) : (
|
||||
<UserSvg width={80} height={80} />
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
</View >
|
||||
);
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
"hi": "Hi,",
|
||||
"iAmMemo": "I'm Memo!",
|
||||
"ready": "Ready to wake up your memories?",
|
||||
"justAsk": "Just ask MeMo, let me bring them back to life!"
|
||||
"justAsk": "Just ask MeMo, let me bring them back to life!",
|
||||
"selectPhoto": "Select Photo",
|
||||
"continueAsking": "Continue Asking"
|
||||
}
|
||||
}
|
||||
@ -48,7 +48,8 @@
|
||||
"codeVaild": "The code you entered is invalid",
|
||||
"sendAgain": "Did’nt receive a code?",
|
||||
"resend": "Resend",
|
||||
"goBack": "Go Back"
|
||||
"goBack": "Go Back",
|
||||
"aiAgreement": "AI Function Usage Norms"
|
||||
},
|
||||
"login": {
|
||||
"title": "Log in",
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
"hi": "Hi,",
|
||||
"iAmMemo": "I'm Memo!",
|
||||
"ready": "Ready to wake up your memories?",
|
||||
"justAsk": "Just ask MeMo, let me bring them back to life!"
|
||||
"justAsk": "Just ask MeMo, let me bring them back to life!",
|
||||
"selectPhoto": "Select Photo",
|
||||
"continueAsking": "Continue Asking"
|
||||
}
|
||||
}
|
||||
@ -48,7 +48,8 @@
|
||||
"codeValid": "您输入的验证码无效",
|
||||
"sendAgain": "没有收到验证码?",
|
||||
"resend": "重新发送",
|
||||
"goBack": "返回"
|
||||
"goBack": "返回",
|
||||
"aiAgreement": "《AI功能使用规范》"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录",
|
||||
|
||||
47
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"expo-audio": "~0.4.7",
|
||||
"expo-background-fetch": "^13.1.6",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-clipboard": "~7.1.5",
|
||||
"expo-constants": "~17.1.6",
|
||||
"expo-dev-client": "~5.2.1",
|
||||
"expo-device": "~7.1.4",
|
||||
@ -3360,18 +3361,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-async-storage/async-storage": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
|
||||
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"merge-options": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-picker/picker": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz",
|
||||
@ -7835,6 +7824,17 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-clipboard": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz",
|
||||
"integrity": "sha512-TCANUGOxouoJXxKBW5ASJl2WlmQLGpuZGemDCL2fO5ZMl57DGTypUmagb0CVUFxDl0yAtFIcESd78UsF9o64aw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-constants": {
|
||||
"version": "17.1.7",
|
||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.7.tgz",
|
||||
@ -9743,15 +9743,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@ -10819,18 +10810,6 @@
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-options": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-plain-obj": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@ -15896,4 +15875,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,6 @@
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "~2.3.2",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linking": "~7.1.5",
|
||||
@ -69,7 +68,8 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-redux": "^9.2.0"
|
||||
"react-redux": "^9.2.0",
|
||||
"expo-clipboard": "~7.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
|
||||