diff --git a/app.json b/app.json
index 0b3bf6b..e8efb01 100644
--- a/app.json
+++ b/app.json
@@ -13,7 +13,8 @@
"infoPlist": {
"NSPhotoLibraryUsageDescription": "Allow $(PRODUCT_NAME) to access your photos.",
"NSLocationWhenInUseUsageDescription": "Allow $(PRODUCT_NAME) to access your location to get photo location data.",
- "ITSAppUsesNonExemptEncryption": false
+ "ITSAppUsesNonExemptEncryption": false,
+ "UIBackgroundModes": ["fetch", "location", "audio"]
},
"bundleIdentifier": "com.memowake.app"
},
@@ -30,7 +31,9 @@
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
- "android.permission.ACCESS_MEDIA_LOCATION"
+ "android.permission.ACCESS_MEDIA_LOCATION",
+ "FOREGROUND_SERVICE",
+ "WAKE_LOCK"
],
"edgeToEdgeEnabled": true,
"package": "com.memowake.app"
@@ -42,7 +45,18 @@
},
"plugins": [
"expo-router",
- "expo-secure-store",
+ "expo-secure-store", [
+ "expo-background-fetch",
+ {
+ "minimumInterval": 1
+ }
+ ],
+ [
+ "expo-task-manager",
+ {
+ "transparency": "opaque"
+ }
+ ],
[
"expo-location",
{
diff --git a/components/file-upload/autoUploadScreen.tsx b/components/file-upload/autoUploadScreen.tsx
new file mode 100644
index 0000000..87b1a6e
--- /dev/null
+++ b/components/file-upload/autoUploadScreen.tsx
@@ -0,0 +1,227 @@
+import React, { useEffect, useState } from 'react';
+import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import { registerBackgroundUploadTask, triggerManualUpload } from './backgroundUploader';
+
+export default function AutoUploadScreen() {
+ const [timeRange, setTimeRange] = useState('day');
+ const [isLoading, setIsLoading] = useState(false);
+ const [isRegistered, setIsRegistered] = useState(false);
+
+ // 注册后台任务
+ useEffect(() => {
+ const registerTask = async () => {
+ const registered = await registerBackgroundUploadTask();
+ setIsRegistered(registered);
+ };
+
+ registerTask();
+ }, []);
+
+ // 处理手动上传
+ const handleManualUpload = async () => {
+ try {
+ setIsLoading(true);
+ await triggerManualUpload(getDateRange(timeRange)[0], getDateRange(timeRange)[1]);
+ } catch (error) {
+ console.error('Upload error:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 获取时间范围文本
+ const getDateRangeText = (timeRange: string) => {
+ switch (timeRange) {
+ case 'day':
+ return '最近一天';
+ case 'week':
+ return '最近一周';
+ case 'month':
+ return '最近一个月';
+ case 'all':
+ return '全部';
+ default:
+ return '';
+ }
+ };
+
+ // 获取时间范围
+ const getDateRange = (timeRange: string) => {
+ const date = new Date();
+ switch (timeRange) {
+ case 'day':
+ date.setDate(date.getDate() - 1);
+ break;
+ case 'week':
+ date.setDate(date.getDate() - 7);
+ break;
+ case 'month':
+ date.setMonth(date.getMonth() - 1);
+ break;
+ case 'all':
+ date.setFullYear(date.getFullYear() - 1);
+ break;
+ default:
+ break;
+ }
+ return [date, new Date()];
+ };
+
+ return (
+
+ 自动上传设置
+
+
+ 选择时间范围:
+
+ setTimeRange('day')}
+ >
+ 一天
+
+ setTimeRange('week')}
+ >
+ 一周
+
+ setTimeRange('month')}
+ >
+ 一个月
+
+ setTimeRange('all')}
+ >
+ 全部
+
+
+
+ {getDateRangeText(timeRange)}
+
+
+
+
+
+
+ {isLoading ? '上传中...' : '开始上传'}
+
+
+
+
+
+
+ 后台自动上传状态: {isRegistered ? '已启用' : '未启用'}
+
+
+ 系统会自动在后台上传过去24小时内的新照片和视频
+
+
+
+ {isLoading && (
+
+
+ 正在上传,请稍候...
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 20,
+ backgroundColor: '#fff',
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ marginBottom: 20,
+ textAlign: 'center',
+ },
+ buttonGroup: {
+ marginBottom: 20,
+ },
+ sectionTitle: {
+ fontSize: 16,
+ marginBottom: 10,
+ color: '#333',
+ },
+ buttonRow: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ marginBottom: 10,
+ gap: 10,
+ },
+ timeButton: {
+ paddingVertical: 8,
+ paddingHorizontal: 15,
+ borderRadius: 20,
+ backgroundColor: '#f0f0f0',
+ borderWidth: 1,
+ borderColor: '#ddd',
+ },
+ activeButton: {
+ backgroundColor: '#007AFF',
+ borderColor: '#007AFF',
+ },
+ buttonText: {
+ color: '#333',
+ textAlign: 'center',
+ },
+ dateRangeText: {
+ fontSize: 14,
+ color: '#666',
+ marginTop: 8,
+ },
+ uploadButtonContainer: {
+ marginTop: 20,
+ },
+ uploadButton: {
+ backgroundColor: '#007AFF',
+ padding: 15,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ uploadButtonDisabled: {
+ backgroundColor: '#84c1ff',
+ },
+ uploadButtonText: {
+ color: '#fff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ statusContainer: {
+ marginTop: 30,
+ padding: 15,
+ backgroundColor: '#f8f8f8',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: '#eee',
+ },
+ statusText: {
+ fontSize: 15,
+ marginBottom: 5,
+ color: '#333',
+ },
+ hintText: {
+ fontSize: 13,
+ color: '#666',
+ },
+ loadingContainer: {
+ marginTop: 20,
+ alignItems: 'center',
+ },
+ loadingText: {
+ marginTop: 10,
+ color: '#666',
+ },
+});
\ No newline at end of file
diff --git a/components/file-upload/backgroundUploader.ts b/components/file-upload/backgroundUploader.ts
new file mode 100644
index 0000000..368596b
--- /dev/null
+++ b/components/file-upload/backgroundUploader.ts
@@ -0,0 +1,383 @@
+import { fetchApi } from '@/lib/server-api-util';
+import * as BackgroundFetch from 'expo-background-fetch';
+import * as ImageManipulator from 'expo-image-manipulator';
+import * as MediaLibrary from 'expo-media-library';
+import * as TaskManager from 'expo-task-manager';
+import { Alert } from 'react-native';
+
+const BACKGROUND_UPLOAD_TASK = 'background-upload-task';
+
+// 获取指定时间范围内的媒体文件
+export const getMediaByDateRange = async (startDate: Date, endDate: Date) => {
+ try {
+ const { status } = await MediaLibrary.requestPermissionsAsync();
+ if (status !== 'granted') {
+ console.log('Media library permission not granted');
+ return [];
+ }
+
+ // 获取媒体资源
+ const media = await MediaLibrary.getAssetsAsync({
+ mediaType: ['photo', 'video'],
+ first: 100, // 每次最多获取100个
+ sortBy: [MediaLibrary.SortBy.creationTime],
+ createdAfter: startDate.getTime(),
+ createdBefore: endDate.getTime(),
+ });
+
+ return media.assets;
+ } catch (error) {
+ console.error('Error getting media by date range:', error);
+ return [];
+ }
+};
+
+// 压缩图片
+const compressImage = async (uri: string): Promise<{ uri: string; file: File }> => {
+ try {
+ const manipResult = await ImageManipulator.manipulateAsync(
+ uri,
+ [
+ {
+ resize: {
+ width: 1200,
+ height: 1200,
+ },
+ },
+ ],
+ {
+ compress: 0.7,
+ format: ImageManipulator.SaveFormat.JPEG,
+ base64: false,
+ }
+ );
+
+ const response = await fetch(manipResult.uri);
+ const blob = await response.blob();
+ const filename = uri.split('/').pop() || `image_${Date.now()}.jpg`;
+ const file = new File([blob], filename, { type: 'image/jpeg' });
+
+ return { uri: manipResult.uri, file };
+ } catch (error) {
+ console.error('Error compressing image:', error);
+ throw error;
+ }
+};
+
+// 获取上传URL
+const getUploadUrl = async (file: File, metadata: Record = {}): Promise<{ upload_url: string; file_id: string }> => {
+ const body = {
+ filename: file.name,
+ content_type: file.type,
+ file_size: file.size,
+ metadata: {
+ ...metadata,
+ originalName: file.name,
+ fileType: file.type.startsWith('video/') ? 'video' : 'image',
+ isCompressed: 'true',
+ },
+ };
+
+ return await fetchApi<{ upload_url: string; file_id: string }>("/file/generate-upload-url", {
+ method: 'POST',
+ body: JSON.stringify(body)
+ });
+};
+
+// 确认上传
+const confirmUpload = async (file_id: string) => {
+ return await fetchApi('/file/confirm-upload', {
+ method: 'POST',
+ body: JSON.stringify({ file_id })
+ });
+};
+
+// 新增素材
+const addMaterial = async (file: string, compressFile: string) => {
+ await fetchApi('/material', {
+ method: 'POST',
+ body: JSON.stringify([{
+ "file_id": file,
+ "preview_file_id": compressFile
+ }])
+ }).catch((error) => {
+ // console.log(error);
+ })
+}
+
+// 上传文件到URL
+const uploadFile = async (file: File, uploadUrl: string): Promise => {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('PUT', uploadUrl);
+ xhr.setRequestHeader('Content-Type', file.type);
+
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resolve();
+ } else {
+ reject(new Error(`Upload failed with status ${xhr.status}`));
+ }
+ };
+
+ xhr.onerror = () => {
+ reject(new Error('Network error during upload'));
+ };
+
+ xhr.send(file);
+ });
+};
+
+// 检查并请求媒体库权限
+const checkMediaLibraryPermission = async (): Promise<{ hasPermission: boolean, status?: string }> => {
+ try {
+ const { status, accessPrivileges } = await MediaLibrary.getPermissionsAsync();
+
+ // 如果已经授权,直接返回
+ if (status === 'granted' && accessPrivileges === 'all') {
+ return { hasPermission: true, status };
+ }
+
+ // 如果没有授权,请求权限
+ const { status: newStatus, accessPrivileges: newPrivileges } = await MediaLibrary.requestPermissionsAsync();
+ const isGranted = newStatus === 'granted' && newPrivileges === 'all';
+
+ if (!isGranted) {
+ console.log('Media library permission not granted or limited access');
+ }
+
+ return {
+ hasPermission: isGranted,
+ status: newStatus
+ };
+ } catch (error) {
+ console.error('Error checking media library permission:', error);
+ return { hasPermission: false };
+ }
+};
+
+// 处理单个媒体文件上传
+const processMediaUpload = async (asset: MediaLibrary.Asset) => {
+ try {
+ // 检查权限
+ const { hasPermission } = await checkMediaLibraryPermission();
+ if (!hasPermission) {
+ throw new Error('No media library permission');
+ }
+
+ const isVideo = asset.mediaType === 'video';
+
+ // 上传原始文件
+ const uploadOriginalFile = async () => {
+ const response = await fetch(asset.uri);
+ const blob = await response.blob();
+ const filename = asset.filename ||
+ `${isVideo ? 'video' : 'image'}_${Date.now()}_original.${isVideo ? 'mp4' : 'jpg'}`;
+
+ const mimeType = isVideo ? 'video/mp4' : 'image/jpeg';
+ const file = new File([blob], filename, { type: mimeType });
+
+ console.log("Original file prepared for upload:", {
+ name: file.name,
+ size: file.size,
+ type: file.type
+ });
+
+ // 获取上传URL并上传原始文件
+ const { upload_url, file_id } = await getUploadUrl(file, {
+ originalUri: asset.uri,
+ creationTime: asset.creationTime,
+ mediaType: isVideo ? 'video' : 'image',
+ isCompressed: false
+ });
+
+ await uploadFile(file, upload_url);
+ await confirmUpload(file_id);
+ console.log(`Successfully uploaded original: ${filename}`);
+ return { success: true, file_id };
+ };
+
+ // 上传压缩文件(仅图片)
+ const uploadCompressedFile = async () => {
+ if (isVideo) return { success: true, file_id: null }; // 视频不压缩
+
+ try {
+ const { file: compressedFile } = await compressImage(asset.uri);
+ const filename = asset.filename ?
+ `compressed_${asset.filename}` :
+ `image_${Date.now()}_compressed.jpg`;
+
+ console.log("Compressed file prepared for upload:", {
+ name: filename,
+ size: compressedFile.size,
+ type: compressedFile.type
+ });
+
+ // 获取上传URL并上传压缩文件
+ const { upload_url, file_id } = await getUploadUrl(compressedFile, {
+ originalUri: asset.uri,
+ creationTime: asset.creationTime,
+ mediaType: 'image',
+ isCompressed: true
+ });
+
+ await uploadFile(compressedFile, upload_url);
+ await confirmUpload(file_id);
+ console.log(`Successfully uploaded compressed: ${filename}`);
+ return { success: true, file_id };
+ } catch (error) {
+ console.error('Error uploading compressed file:', error);
+ return { success: false, error };
+ }
+ };
+
+ // 同时上传原始文件和压缩文件
+ console.log("Asset info:", asset);
+ const [originalResult, compressedResult] = await Promise.all([
+ uploadOriginalFile(),
+ uploadCompressedFile()
+ ]);
+ console.log("originalResult", originalResult);
+ console.log("compressedResult", compressedResult);
+ addMaterial(originalResult.file_id, compressedResult.file_id)
+ return {
+ originalSuccess: originalResult.success,
+ compressedSuccess: compressedResult.success,
+ fileIds: {
+ original: originalResult.file_id,
+ compressed: compressedResult.file_id
+ }
+ };
+ } catch (error) {
+ console.error('Error in processMediaUpload:', error);
+ if (error.message === 'No media library permission') {
+ throw error;
+ }
+ return {
+ originalSuccess: false,
+ compressedSuccess: false,
+ error
+ };
+ }
+};
+
+// 注册后台任务
+export const registerBackgroundUploadTask = async () => {
+ try {
+ // 检查是否已注册
+ const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_UPLOAD_TASK);
+ if (isRegistered) {
+ await BackgroundFetch.unregisterTaskAsync(BACKGROUND_UPLOAD_TASK);
+ }
+
+ // 注册后台任务
+ await BackgroundFetch.registerTaskAsync(BACKGROUND_UPLOAD_TASK, {
+ minimumInterval: 15 * 60, // 15分钟
+ stopOnTerminate: false, // 应用关闭后继续运行
+ startOnBoot: true, // 系统启动后自动运行
+ });
+
+ console.log('Background upload task registered');
+ return true;
+ } catch (error) {
+ console.error('Error registering background task:', error);
+ return false;
+ }
+};
+
+// 定义后台任务
+TaskManager.defineTask(BACKGROUND_UPLOAD_TASK, async () => {
+ try {
+ console.log('Background upload task started');
+
+ // 检查并请求媒体库权限
+ const { hasPermission } = await checkMediaLibraryPermission();
+ if (!hasPermission) {
+ console.log('Media library permission not granted');
+ // 返回 NoData 而不是 Failed,这样系统可能会在稍后重试
+ return BackgroundFetch.BackgroundFetchResult.NoData;
+ }
+
+ // 获取过去24小时内的媒体
+ const now = new Date();
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+
+ const media = await getMediaByDateRange(yesterday, now);
+ console.log("media", media);
+
+ if (media.length === 0) {
+ console.log('No media found in the specified time range');
+ return BackgroundFetch.BackgroundFetchResult.NoData;
+ }
+
+ console.log(`Found ${media.length} media items to process`);
+
+ // 处理每个媒体文件
+ for (const item of media) {
+ try {
+ await processMediaUpload(item);
+ } catch (error) {
+ console.error(`Error processing media ${item.id}:`, error);
+ // 如果是权限错误,直接返回失败
+ if (error.message === 'No media library permission') {
+ return BackgroundFetch.BackgroundFetchResult.Failed;
+ }
+ // 其他错误继续处理下一个文件
+ }
+ }
+
+ console.log('Background upload task completed');
+ return BackgroundFetch.BackgroundFetchResult.NewData;
+ } catch (error) {
+ console.error('Error in background upload task:', error);
+ return BackgroundFetch.BackgroundFetchResult.Failed;
+ }
+});
+
+// 手动触发上传
+export const triggerManualUpload = async (startDate: Date, endDate: Date) => {
+ try {
+ console.log('Starting manual upload...');
+ console.log("startDate", startDate);
+ console.log("endDate", endDate);
+
+ const media = await getMediaByDateRange(startDate, endDate);
+ console.log("media", media);
+ if (media.length === 0) {
+ Alert.alert('提示', '在指定时间范围内未找到媒体文件');
+ return [];
+ }
+
+ const results = [];
+
+ for (const item of media) {
+ try {
+ const result = await processMediaUpload(item);
+ results.push({
+ id: item.id,
+ originalSuccess: result.originalSuccess,
+ compressedSuccess: result.compressedSuccess,
+ fileIds: result.fileIds,
+ });
+ } catch (error) {
+ console.error(`Error uploading ${item.filename}:`, error);
+ results.push({
+ id: item.id,
+ originalSuccess: false,
+ compressedSuccess: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ });
+ }
+ }
+
+ const originalSuccessCount = results.filter(r => r.originalSuccess).length;
+ const compressedSuccessCount = results.filter(r => r.compressedSuccess).length;
+ Alert.alert('上传完成', `成功上传 ${originalSuccessCount}/${media.length} 个原始文件,${compressedSuccessCount}/${media.length} 个压缩文件`);
+
+ return results;
+ } catch (error) {
+ console.error('Error in manual upload:', error);
+ Alert.alert('错误', '上传过程中出现错误');
+ throw error;
+ }
+};
diff --git a/components/file-upload/mediaLibraryUtils.ts b/components/file-upload/mediaLibraryUtils.ts
new file mode 100644
index 0000000..3c4e53a
--- /dev/null
+++ b/components/file-upload/mediaLibraryUtils.ts
@@ -0,0 +1,62 @@
+import * as MediaLibrary from 'expo-media-library';
+
+export type MediaItem = {
+ id: string;
+ uri: string;
+ creationTime: number;
+ filename: string;
+};
+
+// 获取指定时间范围内的图片
+export const getFilteredMedia = async (range: 'today' | 'week' | 'month' | 'all'): Promise => {
+ const { status } = await MediaLibrary.requestPermissionsAsync();
+ if (status !== 'granted') throw new Error('Permission not granted');
+ console.log("statusq111111111111111111", status);
+
+ let cutoffDate: Date;
+
+ switch (range) {
+ case 'today':
+ cutoffDate = new Date();
+ cutoffDate.setHours(0, 0, 0, 0);
+ break;
+ case 'week':
+ cutoffDate = new Date();
+ cutoffDate.setDate(cutoffDate.getDate() - 7);
+ break;
+ case 'month':
+ cutoffDate = new Date();
+ cutoffDate.setMonth(cutoffDate.getMonth() - 1);
+ break;
+ default:
+ cutoffDate = new Date(0); // 所有图片
+ }
+ console.log("cutoffDateq111111111111111111", cutoffDate);
+
+ const albums = await MediaLibrary.getAlbumsAsync({ includeSmartAlbums: true });
+ console.log("albumsq111111111111111111", albums);
+ let allAssets: MediaItem[] = [];
+
+ for (const album of albums) {
+ const result = await MediaLibrary.getAssetsAsync({
+ album: album.id,
+ mediaType: ['photo'],
+ first: 1000,
+ });
+ console.log("result111111111111", result);
+
+ const filtered = result.assets
+ .filter(asset => asset.creationTime > cutoffDate.getTime())
+ .map(asset => ({
+ id: asset.id,
+ uri: asset.uri,
+ creationTime: asset.creationTime,
+ filename: asset.filename,
+ }));
+ console.log("filtered111111111111", filtered);
+
+ }
+
+ console.log("allAssetsq111111111111111111", allAssets);
+ return allAssets;
+};
\ No newline at end of file
diff --git a/components/file-upload/uploadQueueManager.ts b/components/file-upload/uploadQueueManager.ts
new file mode 100644
index 0000000..38e722d
--- /dev/null
+++ b/components/file-upload/uploadQueueManager.ts
@@ -0,0 +1,93 @@
+import * as SecureStore from 'expo-secure-store';
+
+const QUEUE_KEY = 'uploadQueue';
+
+export type UploadItem = {
+ uri: string;
+ filename: string;
+};
+
+// 存储队列
+export const saveUploadQueue = async (queue: UploadItem[]) => {
+ await SecureStore.setItemAsync(QUEUE_KEY, JSON.stringify(queue));
+};
+
+// 获取队列
+export const getUploadQueue = async (): Promise => {
+ const data = await SecureStore.getItemAsync(QUEUE_KEY);
+ return data ? JSON.parse(data) : [];
+};
+
+// 移除已上传项
+export const removeFromQueue = async (uri: string) => {
+ const queue = await getUploadQueue();
+ const newQueue = queue.filter(item => item.uri !== uri);
+ await saveUploadQueue(newQueue);
+};
+
+export const uploadMediaFile = async (asset: any) => {
+ const uri = asset.uri;
+ const filename = uri.split('/').pop() || 'file.jpg';
+ const type =
+ asset.mediaType === 'photo'
+ ? `image/${filename.split('.').pop()}`
+ : `video/${filename.split('.').pop()}`;
+
+ const formData = new FormData();
+ formData.append('file', { uri, name: filename, type } as any);
+
+ await getUploadUrl({
+ ...formData,
+ name: filename,
+ type,
+ size: asset.fileSize
+ }, {}).then((res) => {
+ confirmUpload(res.file_id).then((confirmRes) => {
+ addMaterial(res.file_id, confirmRes.file_id)
+ }).catch((error) => {
+ console.log(error);
+ })
+ }).catch((error) => {
+ console.log(error);
+ })
+};
+
+// 获取上传URL
+const getUploadUrl = async (file: File, metadata: Record = {}): Promise => {
+ const body = {
+ filename: file.name,
+ content_type: file.type,
+ file_size: file.size,
+ metadata: {
+ ...metadata,
+ originalName: file.name,
+ fileType: 'image',
+ isCompressed: metadata.isCompressed || 'false',
+ },
+ };
+ return await fetchApi("/file/generate-upload-url", {
+ method: 'POST',
+ body: JSON.stringify(body)
+ });
+};
+
+// 向服务端confirm上传
+const confirmUpload = async (file_id: string): Promise => await fetchApi('/file/confirm-upload', {
+ method: 'POST',
+ body: JSON.stringify({
+ file_id
+ })
+});
+
+// 新增素材
+const addMaterial = async (file: string, compressFile: string) => {
+ await fetchApi('/material', {
+ method: 'POST',
+ body: JSON.stringify([{
+ "file_id": file,
+ "preview_file_id": compressFile
+ }])
+ }).catch((error) => {
+ // console.log(error);
+ })
+}
\ No newline at end of file
diff --git a/components/user-message.tsx/look.tsx b/components/user-message.tsx/look.tsx
index d593d92..ffb4c46 100644
--- a/components/user-message.tsx/look.tsx
+++ b/components/user-message.tsx/look.tsx
@@ -5,6 +5,7 @@ import { ThemedText } from '@/components/ThemedText';
import { FileUploadItem } from '@/types/upload';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, Image, TouchableOpacity, View } from 'react-native';
+import AutoUploadScreen from '../file-upload/autoUploadScreen';
import FilesUploader from '../file-upload/files-uploader';
interface Props {
@@ -60,7 +61,9 @@ export default function Look(props: Props) {
{t('auth.userMessage.choosePhoto', { ns: 'login' })}
- } />
+ }
+ />
+
diff --git a/contexts/auth-context.tsx b/contexts/auth-context.tsx
index 60ca9bb..6b9d9eb 100644
--- a/contexts/auth-context.tsx
+++ b/contexts/auth-context.tsx
@@ -92,7 +92,13 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const logout = () => {
// 清除 Redux store 中的认证信息
dispatch(clearCredentials());
-
+ if (Platform.OS === 'web') {
+ localStorage.setItem('user', "");
+ localStorage.setItem('token', "");
+ } else {
+ SecureStore.setItemAsync('user', "");
+ SecureStore.setItemAsync('token', "");
+ }
// 触发事件通知
eventEmitter.emit(EVENT_TYPES.USER_INFO_UPDATED, null);
diff --git a/package-lock.json b/package-lock.json
index 32c4a54..068ae76 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@types/react-redux": "^7.1.34",
"expo": "~53.0.12",
"expo-audio": "~0.4.7",
+ "expo-background-fetch": "^13.1.6",
"expo-blur": "~14.1.5",
"expo-constants": "~17.1.6",
"expo-dev-client": "~5.2.1",
@@ -37,6 +38,7 @@
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.9",
+ "expo-task-manager": "^13.1.6",
"expo-video": "~2.2.2",
"expo-video-thumbnails": "~9.1.3",
"expo-web-browser": "~14.2.0",
@@ -48,7 +50,7 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-i18next": "^15.5.3",
- "react-native": "0.79.4",
+ "react-native": "^0.79.4",
"react-native-gesture-handler": "~2.24.0",
"react-native-modal": "^14.0.0-rc.1",
"react-native-picker-select": "^9.3.1",
@@ -3358,6 +3360,18 @@
}
}
},
+ "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",
@@ -7798,6 +7812,18 @@
"react-native": "*"
}
},
+ "node_modules/expo-background-fetch": {
+ "version": "13.1.6",
+ "resolved": "https://registry.npmjs.org/expo-background-fetch/-/expo-background-fetch-13.1.6.tgz",
+ "integrity": "sha512-hl4kR32DaxoHFYqNsILLZG2mWssCkUb4wnEAHtDGmpxUP4SCnJILcAn99J6AGDFUw5lF6FXNZZCXNfcrFioO4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-task-manager": "~13.1.6"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-blur": {
"version": "14.1.5",
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.1.5.tgz",
@@ -8261,6 +8287,19 @@
}
}
},
+ "node_modules/expo-task-manager": {
+ "version": "13.1.6",
+ "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-13.1.6.tgz",
+ "integrity": "sha512-sYNAftpIeZ+j6ur17Jo0OpSTk9ks/MDvTbrNCimXMyjIt69XXYL/kAPYf76bWuxOuN8bcJ8Ef8YvihkwFG9hDA==",
+ "license": "MIT",
+ "dependencies": {
+ "unimodules-app-loader": "~5.1.3"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-updates-interface": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz",
@@ -9704,6 +9743,15 @@
"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",
@@ -10771,6 +10819,18 @@
"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",
@@ -15139,6 +15199,12 @@
"node": ">=4"
}
},
+ "node_modules/unimodules-app-loader": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-5.1.3.tgz",
+ "integrity": "sha512-nPUkwfkpJWvdOQrVvyQSUol93/UdmsCVd9Hkx9RgAevmKSVYdZI+S87W73NGKl6QbwK9L1BDSY5OrQuo8Oq15g==",
+ "license": "MIT"
+ },
"node_modules/unique-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
@@ -15830,4 +15896,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 6236ffb..bc50a88 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@types/react-redux": "^7.1.34",
"expo": "~53.0.12",
"expo-audio": "~0.4.7",
+ "expo-background-fetch": "^13.1.6",
"expo-blur": "~14.1.5",
"expo-constants": "~17.1.6",
"expo-dev-client": "~5.2.1",
@@ -42,6 +43,7 @@
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.9",
+ "expo-task-manager": "^13.1.6",
"expo-video": "~2.2.2",
"expo-video-thumbnails": "~9.1.3",
"expo-web-browser": "~14.2.0",
@@ -53,7 +55,7 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-i18next": "^15.5.3",
- "react-native": "0.79.4",
+ "react-native": "^0.79.4",
"react-native-gesture-handler": "~2.24.0",
"react-native-modal": "^14.0.0-rc.1",
"react-native-picker-select": "^9.3.1",
@@ -90,4 +92,4 @@
}
}
}
-}
+}
\ No newline at end of file