Compare commits

...

3 Commits

4 changed files with 181 additions and 149 deletions

View File

@ -20,6 +20,8 @@ import {
TouchableOpacity, TouchableOpacity,
View View
} from 'react-native'; } from 'react-native';
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { runOnJS } from 'react-native-reanimated';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AskScreen() { export default function AskScreen() {
@ -46,6 +48,25 @@ export default function AskScreen() {
} }
}, []); }, []);
const updateState = (value: string) => {
console.log('Received in JS:', value);
// 更新 React 状态或路由
router.replace(value);
};
// 右滑
const gesture = Gesture.Pan()
.onEnd((event) => {
const { translationX } = event;
const threshold = 100; // 滑动阈值
if (translationX > threshold) {
// 从左向右滑动,跳转页面
runOnJS(router.replace)("memo-list");
}
})
.minPointers(1)
.activeOffsetX([-10, 10]); // 在 X 方向触发的范围
useEffect(() => { useEffect(() => {
if (!isHello && userMessages.length > 0) { if (!isHello && userMessages.length > 0) {
scrollToEnd(); scrollToEnd();
@ -236,86 +257,88 @@ export default function AskScreen() {
); );
return ( return (
<View style={[styles.container, { paddingTop: insets.top }]}> <GestureDetector gesture={gesture}>
{/* 导航栏 */} <View style={[styles.container, { paddingTop: insets.top }]}>
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}> {/* 导航栏 */}
<TouchableOpacity <View style={[styles.navbar, isHello && styles.hiddenNavbar]}>
style={styles.backButton} <TouchableOpacity
onPress={() => { style={styles.backButton}
try { onPress={() => {
if (TextInput.State?.currentlyFocusedInput) { try {
const input = TextInput.State.currentlyFocusedInput(); if (TextInput.State?.currentlyFocusedInput) {
if (input) input.blur(); const input = TextInput.State.currentlyFocusedInput();
if (input) input.blur();
}
} catch (error) {
console.log('失去焦点失败:', error);
} }
} catch (error) { Keyboard.dismiss();
console.log('失去焦点失败:', error); router.push('/memo-list');
} }}
Keyboard.dismiss(); >
router.push('/memo-list'); <ReturnArrow />
}} </TouchableOpacity>
> <ThemedText style={styles.title} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
<ReturnArrow /> <View style={styles.placeholder} />
</TouchableOpacity>
<ThemedText style={styles.title} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
<View style={styles.placeholder} />
</View>
<View style={styles.contentContainer}>
{/* 欢迎页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnim,
pointerEvents: isHello ? 'auto' : 'none',
zIndex: 1
}
]}
>
<AskHello setUserMessages={setUserMessages} setConversationId={setConversationId} setIsHello={setIsHello} />
</Animated.View>
{/* 聊天页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnimChat,
pointerEvents: isHello ? 'none' : 'auto',
zIndex: 0
}
]}
>
<Chat
ref={chatListRef}
userMessages={userMessages}
sessionId={sessionId}
setSelectedImages={setSelectedImages}
selectedImages={selectedImages}
style={styles.chatContainer}
contentContainerStyle={styles.chatContentContainer}
showsVerticalScrollIndicator={false}
onContentSizeChange={() => scrollToEnd()}
/>
</Animated.View>
</View>
{/* 输入框区域 */}
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={0} >
<View style={styles.inputContainer} key={conversationId}>
<SendMessage
setIsHello={setIsHello}
conversationId={conversationId}
setConversationId={setConversationId}
setUserMessages={setUserMessages}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
/>
</View> </View>
</KeyboardAvoidingView>
</View > <View style={styles.contentContainer}>
{/* 欢迎页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnim,
pointerEvents: isHello ? 'auto' : 'none',
zIndex: 1
}
]}
>
<AskHello setUserMessages={setUserMessages} setConversationId={setConversationId} setIsHello={setIsHello} />
</Animated.View>
{/* 聊天页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnimChat,
pointerEvents: isHello ? 'none' : 'auto',
zIndex: 0
}
]}
>
<Chat
ref={chatListRef}
userMessages={userMessages}
sessionId={sessionId}
setSelectedImages={setSelectedImages}
selectedImages={selectedImages}
style={styles.chatContainer}
contentContainerStyle={styles.chatContentContainer}
showsVerticalScrollIndicator={false}
onContentSizeChange={() => scrollToEnd()}
/>
</Animated.View>
</View>
{/* 输入框区域 */}
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={0} >
<View style={styles.inputContainer} key={conversationId}>
<SendMessage
setIsHello={setIsHello}
conversationId={conversationId}
setConversationId={setConversationId}
setUserMessages={setUserMessages}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
/>
</View>
</KeyboardAvoidingView>
</View >
</GestureDetector>
); );
} }

View File

@ -11,7 +11,7 @@ import { checkNotificationPermission, getLocationPermission, getPermissions, req
import { ThemedText } from '@/components/ThemedText'; import { ThemedText } from '@/components/ThemedText';
import { useAuth } from '@/contexts/auth-context'; import { useAuth } from '@/contexts/auth-context';
import { fetchApi } from '@/lib/server-api-util'; import { fetchApi } from '@/lib/server-api-util';
import { Address } from '@/types/user'; import { Address, User, UserInfoDetails } from '@/types/user';
import * as Location from 'expo-location'; import * as Location from 'expo-location';
import { useFocusEffect, useRouter } from 'expo-router'; import { useFocusEffect, useRouter } from 'expo-router';
import * as SecureStore from 'expo-secure-store'; import * as SecureStore from 'expo-secure-store';
@ -20,8 +20,14 @@ import { useTranslation } from 'react-i18next';
import { Linking, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { Linking, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
const Setting = (props: { userInfo: any }) => { const Setting = (props: { userInfo: UserInfoDetails }) => {
const { userInfo } = props; const [userInfo, setUserInfo] = useState<User | null>(null);
const getUserInfo = async () => {
const res = await fetchApi<User>("/iam/user-info");
setUserInfo(res);
}
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { t } = useTranslation(); const { t } = useTranslation();
// 判断当前语言环境 // 判断当前语言环境
@ -200,6 +206,7 @@ const Setting = (props: { userInfo: any }) => {
// 获取语言环境 // 获取语言环境
useEffect(() => { useEffect(() => {
getLanguage(); getLanguage();
getUserInfo()
}, []) }, [])
return ( return (
@ -220,7 +227,7 @@ const Setting = (props: { userInfo: any }) => {
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}> <ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
{/* 用户信息 */} {/* 用户信息 */}
<UserInfo <UserInfo
userInfo={userInfo} userInfo={userInfo || {} as User}
setCurrentLocation={setCurrentLocation} setCurrentLocation={setCurrentLocation}
getCurrentLocation={getCurrentLocation} getCurrentLocation={getCurrentLocation}
isLoading={isLoading} isLoading={isLoading}

View File

@ -20,6 +20,7 @@ export default function Look(props: Props) {
const { fileData, setFileData, isLoading, handleUser, avatar } = props; const { fileData, setFileData, isLoading, handleUser, avatar } = props;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<View className="flex-1 bg-textPrimary justify-between p-[2rem]"> <View className="flex-1 bg-textPrimary justify-between p-[2rem]">
<View className="flex-1 justify-center items-center"> <View className="flex-1 justify-center items-center">
@ -32,11 +33,11 @@ export default function Look(props: Props) {
{t('auth.userMessage.avatorText2', { ns: 'login' })} {t('auth.userMessage.avatorText2', { ns: 'login' })}
</ThemedText> </ThemedText>
{ {
fileData[0]?.previewUrl fileData[0]?.preview || fileData[0]?.previewUrl
? ?
<Image <Image
className='rounded-full w-[10rem] h-[10rem]' className='rounded-full w-[10rem] h-[10rem]'
source={{ uri: fileData[0].previewUrl }} source={{ uri: fileData[0].preview || fileData[0].previewUrl }}
/> />
: :
avatar avatar

View File

@ -1,8 +1,8 @@
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
export type ExtendedAsset = MediaLibrary.Asset & { export type ExtendedAsset = MediaLibrary.Asset & {
size?: number; size?: number;
exif?: Record<string, any> | null; exif?: Record<string, any> | null;
}; };
// 上传任务类型 // 上传任务类型
@ -18,92 +18,93 @@ export type UploadTask = {
// 文件元数据信息 // 文件元数据信息
interface FileSize { interface FileSize {
value: number; value: number;
unit: string; unit: string;
} }
interface FileMetadata { interface FileMetadata {
originalName: string; originalName: string;
type: string; type: string;
isCompressed: string; isCompressed: string;
fileType: string; fileType: string;
} }
// 后端返回的文件信息 // 后端返回的文件信息
interface FileInfo { interface FileInfo {
file_id: number; file_id: number;
name: string; name: string;
size: FileSize; size: FileSize;
content_type: string; // 这里与 ConfirmUpload 的 content_type 定义不同,需要注意 content_type: string; // 这里与 ConfirmUpload 的 content_type 定义不同,需要注意
upload_time: string; upload_time: string;
storage_medium: string; storage_medium: string;
file_path: string; // 这里与 ConfirmUpload 的 file_path 定义不同 file_path: string; // 这里与 ConfirmUpload 的 file_path 定义不同
uploader_id: number; uploader_id: number;
upload_status: string; upload_status: string;
deletion_status: string; deletion_status: string;
metadata: FileMetadata; metadata: FileMetadata;
} }
// 上传队列项 - 作为唯一的类型定义 // 上传队列项 - 作为唯一的类型定义
// 定义 EXIF 数据类型 // 定义 EXIF 数据类型
export type ExifData = { export type ExifData = {
GPSLatitude?: number | undefined; GPSLatitude?: number | undefined;
GPSLongitude?: number | undefined; GPSLongitude?: number | undefined;
GPSAltitude?: number | undefined; GPSAltitude?: number | undefined;
DateTimeOriginal?: string | undefined; DateTimeOriginal?: string | undefined;
Make?: string | undefined; Make?: string | undefined;
Model?: string | undefined; Model?: string | undefined;
ExposureTime?: number | undefined; ExposureTime?: number | undefined;
FNumber?: number | undefined; FNumber?: number | undefined;
ISOSpeedRatings?: number | undefined; ISOSpeedRatings?: number | undefined;
FocalLength?: number | undefined; FocalLength?: number | undefined;
[key: string]: any; [key: string]: any;
}; };
// 默认的 EXIF 数据结构 // 默认的 EXIF 数据结构
export const defaultExifData: ExifData = { export const defaultExifData: ExifData = {
GPSLatitude: undefined, GPSLatitude: undefined,
GPSLongitude: undefined, GPSLongitude: undefined,
GPSAltitude: undefined, GPSAltitude: undefined,
DateTimeOriginal: undefined, DateTimeOriginal: undefined,
Make: undefined, Make: undefined,
Model: undefined, Model: undefined,
ExposureTime: undefined, ExposureTime: undefined,
FNumber: undefined, FNumber: undefined,
ISOSpeedRatings: undefined, ISOSpeedRatings: undefined,
FocalLength: undefined, FocalLength: undefined,
}; };
// 压缩图片可配置参数 // 压缩图片可配置参数
export interface ImagesuploaderProps { export interface ImagesuploaderProps {
children?: React.ReactNode; children?: React.ReactNode;
style?: import('react-native').StyleProp<import('react-native').ViewStyle>; style?: import('react-native').StyleProp<import('react-native').ViewStyle>;
onPickImage?: (file: File, exifData: ExifData) => void; onPickImage?: (file: File, exifData: ExifData) => void;
compressQuality?: number; compressQuality?: number;
maxWidth?: number; maxWidth?: number;
maxHeight?: number; maxHeight?: number;
preserveExif?: boolean; preserveExif?: boolean;
uploadOriginal?: boolean; uploadOriginal?: boolean;
onUploadComplete?: (result: FileUploadItem[]) => void; onUploadComplete?: (result: FileUploadItem[]) => void;
onProgress?: (progress: any) => void; // TODO: Define a proper type for progress onProgress?: (progress: any) => void; // TODO: Define a proper type for progress
multipleChoice?: boolean; multipleChoice?: boolean;
fileType?: any[]; // TODO: Use MediaType from expo-image-picker fileType?: any[]; // TODO: Use MediaType from expo-image-picker
showPreview?: boolean; showPreview?: boolean;
} }
export interface FileUploadItem { export interface FileUploadItem {
id: string; id: string;
uri: string; // 用于本地展示的资源URI uri: string; // 用于本地展示的资源URI
name: string; name: string;
progress: number; progress: number;
status: 'pending' | 'uploading' | 'success' | 'error'; // 统一状态 status: 'pending' | 'uploading' | 'success' | 'error'; // 统一状态
error?: string | null; error?: string | null;
previewUrl: string; // 预览URL previewUrl: string; // 预览URL
file?: File; preview: string; // 预览URL
type: 'image' | 'video'; file?: File;
thumbnail?: string; // 缩略图URL type: 'image' | 'video';
thumbnailFile?: File; // 缩略图文件对象 thumbnail?: string; // 缩略图URL
originalFile?: FileInfo // 上传后返回的文件信息 thumbnailFile?: File; // 缩略图文件对象
originalFile?: FileInfo // 上传后返回的文件信息
} }
// 确认上传返回 // 确认上传返回