feat: 统一权限申请提示组件

This commit is contained in:
Junhui Chen 2025-07-23 14:58:41 +08:00
parent 4a052844e9
commit 9985e0517f
20 changed files with 550 additions and 288 deletions

View File

@ -36,6 +36,7 @@ export default function OwnerPage() {
// 设置弹窗 // 设置弹窗
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
// 数据统计 // 数据统计
const [countData, setCountData] = useState<CountData>({} as CountData); const [countData, setCountData] = useState<CountData>({} as CountData);
@ -107,8 +108,10 @@ export default function OwnerPage() {
</View> </View>
} }
/> />
{/* 设置弹窗 */} {/* 设置弹窗 - 使用条件渲染避免层级冲突 */}
<SettingModal modalVisible={modalVisible} setModalVisible={setModalVisible} userInfo={userInfoDetails.user_info} /> {modalVisible && (
<SettingModal modalVisible={modalVisible} setModalVisible={setModalVisible} userInfo={userInfoDetails.user_info} />
)}
{/* 导航栏 */} {/* 导航栏 */}
<AskNavbar /> <AskNavbar />

View File

@ -7,6 +7,7 @@ import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react'; import { useEffect } from 'react';
import 'react-native-reanimated'; import 'react-native-reanimated';
import '../global.css'; import '../global.css';
import { PermissionProvider } from '@/context/PermissionContext';
import { Provider } from "../provider"; import { Provider } from "../provider";
export default function RootLayout() { export default function RootLayout() {
@ -30,8 +31,9 @@ export default function RootLayout() {
return ( return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Provider> <PermissionProvider>
<Stack> <Provider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen <Stack.Screen
name="login" name="login"
@ -41,8 +43,9 @@ export default function RootLayout() {
}} }}
/> />
<Stack.Screen name="+not-found" /> <Stack.Screen name="+not-found" />
</Stack> </Stack>
</Provider> </Provider>
</PermissionProvider>
<StatusBar style="auto" /> <StatusBar style="auto" />
</ThemeProvider> </ThemeProvider>
); );

View File

@ -0,0 +1,111 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Pressable, StyleSheet, Text, View } from 'react-native';
interface PermissionAlertProps {
visible: boolean;
onConfirm: () => void;
onCancel: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
}
const PermissionAlert: React.FC<PermissionAlertProps> = ({ visible, onConfirm, onCancel, title, message, confirmText, cancelText }) => {
const { t } = useTranslation();
if (!visible) {
return null;
}
return (
<View style={styles.overlay}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalTitle}>{title}</Text>
<Text style={styles.modalMessage}>{message}</Text>
<View style={styles.buttonContainer}>
<Pressable style={[styles.button, styles.cancelButton]} onPress={onCancel}>
<Text style={styles.buttonText}>{cancelText || t('cancel', { ns: 'permission' })}</Text>
</Pressable>
<Pressable style={[styles.button, styles.confirmButton]} onPress={onConfirm}>
<Text style={styles.confirmButtonText}>{confirmText || t('goToSettings', { ns: 'permission' })}</Text>
</Pressable>
</View>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 99,
},
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
zIndex: 9999,
},
modalView: {
width: '80%',
backgroundColor: 'white',
borderRadius: 16,
padding: 24,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#4C320C',
marginBottom: 12,
},
modalMessage: {
fontSize: 16,
color: '#4C320C',
textAlign: 'center',
marginBottom: 24,
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
},
button: {
borderRadius: 20,
paddingVertical: 12,
flex: 1,
alignItems: 'center',
},
cancelButton: {
backgroundColor: '#F5F5F5',
marginRight: 8,
},
confirmButton: {
backgroundColor: '#E2793F',
marginLeft: 8,
},
buttonText: {
color: '#4C320C',
fontWeight: '600',
},
confirmButtonText: {
color: 'white',
fontWeight: '600',
},
});
export default PermissionAlert;

View File

@ -1,7 +1,9 @@
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { Alert, StyleSheet, TouchableOpacity, View } from 'react-native'; import { StyleSheet, TouchableOpacity, View } from 'react-native';
import i18n from '@/i18n';
import { PermissionService } from '@/lib/PermissionService';
import QRCode from 'react-native-qrcode-svg'; import QRCode from 'react-native-qrcode-svg';
import { captureRef } from 'react-native-view-shot'; import { captureRef } from 'react-native-view-shot';
@ -17,7 +19,7 @@ export default function QRDownloadScreen(prop: { url: string }) {
// 请求相册写入权限 // 请求相册写入权限
const { status } = await MediaLibrary.requestPermissionsAsync(); const { status } = await MediaLibrary.requestPermissionsAsync();
if (status !== 'granted') { if (status !== 'granted') {
Alert.alert('权限被拒绝', '需要保存图片到相册的权限'); PermissionService.show({ title: i18n.t('permission:title.permissionDenied'), message: i18n.t('permission:message.saveToAlbumPermissionRequired') });
return; return;
} }
@ -33,10 +35,10 @@ export default function QRDownloadScreen(prop: { url: string }) {
// 保存到相册 // 保存到相册
await MediaLibrary.saveToLibraryAsync(uri); await MediaLibrary.saveToLibraryAsync(uri);
Alert.alert('✅ 成功', '二维码已保存到相册!'); PermissionService.show({ title: i18n.t('permission:title.success'), message: i18n.t('permission:message.qrCodeSaved') });
} catch (error) { } catch (error) {
console.error('保存失败:', error); console.error('保存失败:', error);
Alert.alert('❌ 失败', '无法保存图片,请重试'); PermissionService.show({ title: i18n.t('permission:title.error'), message: i18n.t('permission:message.saveImageFailed') });
} }
}; };

View File

@ -4,7 +4,8 @@ import { uploadFileWithProgress } from '@/lib/background-uploader/uploader';
import { compressImage } from '@/lib/image-process/imageCompress'; import { compressImage } from '@/lib/image-process/imageCompress';
import { createVideoThumbnailFile } from '@/lib/video-process/videoThumbnail'; import { createVideoThumbnailFile } from '@/lib/video-process/videoThumbnail';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import * as Location from 'expo-location'; import { requestLocationPermission, requestMediaLibraryPermission } from '@/components/owner/utils';
import { PermissionService } from '@/lib/PermissionService';
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native';
@ -26,23 +27,6 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
const [files, setFiles] = useState<FileUploadItem[]>([]); const [files, setFiles] = useState<FileUploadItem[]>([]);
const [uploadQueue, setUploadQueue] = useState<FileUploadItem[]>([]); const [uploadQueue, setUploadQueue] = useState<FileUploadItem[]>([]);
// 请求权限
const requestPermissions = async () => {
if (Platform.OS !== 'web') {
const { status: mediaStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (mediaStatus !== 'granted') {
Alert.alert('需要媒体库权限', '请允许访问媒体库以选择图片');
return false;
}
const { status: locationStatus } = await Location.requestForegroundPermissionsAsync();;
if (locationStatus !== 'granted') {
Alert.alert('需要位置权限', '需要位置权限才能获取图片位置信息');
}
}
return true;
};
// 处理单个资源 // 处理单个资源
const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise<UploadResult | null> => { const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise<UploadResult | null> => {
console.log("asset111111", asset); console.log("asset111111", asset);
@ -158,7 +142,7 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
try { try {
// 统一通过 lib 的 uploadFileWithProgress 实现上传 // 统一通过 lib 的 uploadFileWithProgress 实现上传
const uploadUrlData = await getUploadUrl(task.file, { ...task.metadata, GPSVersionID: undefined }); const uploadUrlData = await getUploadUrl(task.file, { ...task.metadata, GPSVersionID: undefined });
const taskIndex = uploadTasks.indexOf(task); const taskIndex = uploadTasks.indexOf(task);
const totalTasks = uploadTasks.length; const totalTasks = uploadTasks.length;
const baseProgress = (taskIndex / totalTasks) * 100; const baseProgress = (taskIndex / totalTasks) * 100;
@ -261,9 +245,13 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
const pickImage = async () => { const pickImage = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const hasPermission = await requestPermissions(); const hasMediaPermission = await requestMediaLibraryPermission();
console.log("hasPermission", hasPermission); if (!hasMediaPermission) {
if (!hasPermission) return; setIsLoading(false);
return;
}
// 请求位置权限,但不强制要求
await requestLocationPermission();
const result = await ImagePicker.launchImageLibraryAsync({ const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: fileType, mediaTypes: fileType,
@ -290,13 +278,13 @@ export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
} }
})); }));
} catch (error) { } catch (error) {
Alert.alert('错误', '部分文件处理失败,请重试'); PermissionService.show({ title: '错误', message: '部分文件处理失败,请重试' });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
} catch (error) { } catch (error) {
Alert.alert('错误', '选择图片时出错,请重试'); PermissionService.show({ title: '错误', message: '选择图片时出错,请重试' });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@ -1,6 +1,8 @@
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import i18n from '@/i18n';
import { PermissionService } from '@/lib/PermissionService';
interface MediaStats { interface MediaStats {
total: number; total: number;
@ -46,7 +48,7 @@ const MediaStatsScreen = () => {
// 1. 请求媒体库权限 // 1. 请求媒体库权限
const { status } = await MediaLibrary.requestPermissionsAsync(); const { status } = await MediaLibrary.requestPermissionsAsync();
if (status !== 'granted') { if (status !== 'granted') {
Alert.alert('权限被拒绝', '需要访问媒体库权限来获取统计信息'); PermissionService.show({ title: i18n.t('permission:title.permissionDenied'), message: i18n.t('permission:message.getStatsPermissionRequired') });
return; return;
} }
@ -116,7 +118,7 @@ const MediaStatsScreen = () => {
setStats(stats); setStats(stats);
} catch (error) { } catch (error) {
console.error('获取媒体库统计信息失败:', error); console.error('获取媒体库统计信息失败:', error);
Alert.alert('错误', '获取媒体库统计信息失败'); PermissionService.show({ title: i18n.t('permission:title.error'), message: i18n.t('permission:message.getStatsFailed') });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@ -2,10 +2,11 @@ import { fetchApi } from '@/lib/server-api-util';
import { ConfirmUpload, defaultExifData, ExifData, FileStatus, ImagesPickerProps, UploadResult, UploadUrlResponse } from '@/types/upload'; import { ConfirmUpload, defaultExifData, ExifData, FileStatus, ImagesPickerProps, UploadResult, UploadUrlResponse } from '@/types/upload';
import * as ImageManipulator from 'expo-image-manipulator'; import * as ImageManipulator from 'expo-image-manipulator';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import * as Location from 'expo-location'; import { requestLocationPermission, requestMediaLibraryPermission } from '@/components/owner/utils';
import { PermissionService } from '@/lib/PermissionService';
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; import { Button, Platform, TouchableOpacity, View } from 'react-native';
import * as Progress from 'react-native-progress'; import * as Progress from 'react-native-progress';
export const ImagesPicker: React.FC<ImagesPickerProps> = ({ export const ImagesPicker: React.FC<ImagesPickerProps> = ({
@ -24,17 +25,13 @@ export const ImagesPicker: React.FC<ImagesPickerProps> = ({
// 请求权限 // 请求权限
const requestPermissions = async () => { const requestPermissions = async () => {
if (Platform.OS !== 'web') { if (Platform.OS !== 'web') {
const hasMediaPermission = await requestMediaLibraryPermission();
const { status: mediaStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!hasMediaPermission) {
if (mediaStatus !== 'granted') { setIsLoading(false);
Alert.alert('需要媒体库权限', '请允许访问媒体库以选择图片');
return false; return false;
} }
// 请求位置权限,但不强制要求
const { status: locationStatus } = await Location.requestForegroundPermissionsAsync();; await requestLocationPermission();
if (locationStatus !== 'granted') {
Alert.alert('需要位置权限', '需要位置权限才能获取图片位置信息');
}
} }
return true; return true;
}; };
@ -295,10 +292,10 @@ export const ImagesPicker: React.FC<ImagesPickerProps> = ({
throw error; // 重新抛出错误,让外层 catch 处理 throw error; // 重新抛出错误,让外层 catch 处理
} }
} catch (error) { } catch (error) {
Alert.alert('错误', '处理图片时出错'); PermissionService.show({ title: '错误', message: '处理图片时出错' });
} }
} catch (error) { } catch (error) {
Alert.alert('错误', '选择图片时出错,请重试'); PermissionService.show({ title: '错误', message: '选择图片时出错,请重试' });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@ -20,7 +20,13 @@ const AlbumComponent = ({ setModalVisible, style }: CategoryProps) => {
<TouchableOpacity style={{ flex: 3 }}> <TouchableOpacity style={{ flex: 3 }}>
<ThemedText style={styles.text}>{t('generalSetting.shareProfile', { ns: 'personal' })}</ThemedText> <ThemedText style={styles.text}>{t('generalSetting.shareProfile', { ns: 'personal' })}</ThemedText>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={() => setModalVisible(true)} style={[styles.text, { flex: 1, alignItems: "center", paddingVertical: 6 }]}> <TouchableOpacity
onPress={() => {
setModalVisible(true);
}}
activeOpacity={0.7}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={[styles.text, { flex: 1, alignItems: "center", paddingVertical: 6, zIndex: 999 }]}>
<SettingSvg /> <SettingSvg />
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

@ -19,6 +19,7 @@ import { checkNotificationPermission, getLocationPermission, getPermissions, req
const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, userInfo: User }) => { const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, userInfo: User }) => {
const { modalVisible, setModalVisible, userInfo } = props; const { modalVisible, setModalVisible, userInfo } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai'); const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai');
// 协议弹窗 // 协议弹窗
@ -36,28 +37,31 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
}; };
// 通知消息权限开关 // 通知消息权限开关
const [notificationsEnabled, setNotificationsEnabled] = useState(false); const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const toggleNotifications = () => { const toggleNotifications = async () => {
if (notificationsEnabled) { if (notificationsEnabled) {
// 引导去设置关闭权限 // 引导去设置关闭权限
openAppSettings() openAppSettings()
} else { } else {
console.log('请求通知权限'); requestNotificationPermission()
requestNotificationPermission().then((res) => { .then((granted) => {
setNotificationsEnabled(res as boolean); setNotificationsEnabled(granted);
}) });
setModalVisible(false);
} }
}; };
// 相册权限 // 相册权限
const [albumEnabled, setAlbumEnabled] = useState(false); const [albumEnabled, setAlbumEnabled] = useState(false);
const toggleAlbum = () => { const toggleAlbum = async () => {
if (albumEnabled) { if (albumEnabled) {
// 引导去设置关闭权限 // 引导去设置关闭权限
openAppSettings() openAppSettings()
} else { } else {
requestMediaLibraryPermission().then((res) => { requestMediaLibraryPermission()
setAlbumEnabled(res as boolean); .then((granted) => {
}) setAlbumEnabled(granted);
});
setModalVisible(false);
} }
} }
@ -66,12 +70,14 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
// 位置权限更改 // 位置权限更改
const toggleLocation = async () => { const toggleLocation = async () => {
if (locationEnabled) { if (locationEnabled) {
// 引导去设置关闭权限 // 如果权限已开启,点击则引导用户去设置关闭
openAppSettings() openAppSettings();
} else { } else {
requestLocationPermission().then((res) => { requestLocationPermission()
setLocationEnabled(res as boolean); .then((granted) => {
}) setLocationEnabled(granted);
});
setModalVisible(false);
} }
}; };
// 正在获取位置信息 // 正在获取位置信息
@ -92,16 +98,18 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
let currentStatus = await getLocationPermission(); let currentStatus = await getLocationPermission();
console.log('当前权限状态:', currentStatus); console.log('当前权限状态:', currentStatus);
// 2. 如果没有权限,则请求权限 // 2. 如果没有权限,则跳过获取位置
if (!currentStatus) { if (!currentStatus) {
const newStatus = await requestLocationPermission(); console.log('没有权限,跳过获取位置')
setLocationEnabled(newStatus); return;
currentStatus = newStatus; // const newStatus = await requestLocationPermission();
// setLocationEnabled(newStatus);
// currentStatus = newStatus;
if (!currentStatus) { // if (!currentStatus) {
alert('需要位置权限才能继续'); // // alert('需要位置权限才能继续');
return; // return;
} // }
} }
// 3. 确保位置服务已启用 // 3. 确保位置服务已启用
@ -161,10 +169,12 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
if (modalVisible) { if (modalVisible) {
// 位置权限 // 位置权限
getLocationPermission().then((res) => { getLocationPermission().then((res) => {
console.log('位置权限:', res);
setLocationEnabled(res); setLocationEnabled(res);
}) })
// 媒体库权限 // 媒体库权限
getPermissions().then((res) => { getPermissions().then((res) => {
console.log('媒体库权限:', res);
setAlbumEnabled(res); setAlbumEnabled(res);
}) })
// 通知权限 // 通知权限
@ -176,40 +186,41 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
}, [modalVisible]) }, [modalVisible])
return ( return (
<Modal <>
animationType="slide" <Modal
transparent={true} animationType="slide"
visible={modalVisible} transparent={true}
onRequestClose={() => { visible={modalVisible}
setModalVisible(!modalVisible); onRequestClose={() => {
}}> setModalVisible(false);
<Pressable }}>
style={styles.centeredView}
onPress={() => setModalVisible(false)}>
<Pressable <Pressable
style={styles.modalView} style={styles.centeredView}
onPress={(e) => e.stopPropagation()}> onPress={() => setModalVisible(false)}>
<View style={styles.modalHeader}> <Pressable
<Text style={{ opacity: 0 }}>Settings</Text> style={styles.modalView}
<Text style={styles.modalTitle}>{t('generalSetting.allTitle', { ns: 'personal' })}</Text> onPress={(e) => e.stopPropagation()}>
<TouchableOpacity onPress={() => setModalVisible(false)}> <View style={styles.modalHeader}>
<Text style={styles.closeButton}>×</Text> <Text style={{ opacity: 0 }}>Settings</Text>
</TouchableOpacity> <Text style={styles.modalTitle}>{t('generalSetting.allTitle', { ns: 'personal' })}</Text>
</View> <TouchableOpacity onPress={() => setModalVisible(false)}>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}> <Text style={styles.closeButton}>×</Text>
{/* 用户信息 */} </TouchableOpacity>
<UserInfo </View>
userInfo={userInfo} <ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
setModalVisible={setModalVisible} {/* 用户信息 */}
modalVisible={modalVisible} <UserInfo
setCurrentLocation={setCurrentLocation} userInfo={userInfo}
getCurrentLocation={getCurrentLocation} setModalVisible={setModalVisible}
isLoading={isLoading} modalVisible={modalVisible}
isRefreshing={isRefreshing} setCurrentLocation={setCurrentLocation}
currentLocation={currentLocation} getCurrentLocation={getCurrentLocation}
/> isLoading={isLoading}
{/* 升级版本 */} isRefreshing={isRefreshing}
{/* <View style={{ marginTop: 16 }}> currentLocation={currentLocation}
/>
{/* 升级版本 */}
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.subscription', { ns: 'personal' })}</ThemedText> <ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.subscription', { ns: 'personal' })}</ThemedText>
<View style={styles.premium}> <View style={styles.premium}>
<View> <View>
@ -228,8 +239,8 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> */} </View> */}
{/* 消息通知 */} {/* 消息通知 */}
{/* <View style={{ marginTop: 16 }}> {/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText> <ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
<View style={styles.premium}> <View style={styles.premium}>
<View> <View>
@ -241,42 +252,42 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
/> />
</View> </View>
</View> */} </View> */}
{/* 权限信息 */} {/* 权限信息 */}
<View style={{ marginTop: 16 }}> <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText> <ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText>
<View style={styles.content}> <View style={styles.content}>
{/* 相册权限 */} {/* 相册权限 */}
<View style={styles.item}> <View style={styles.item}>
<ThemedText style={styles.itemText}>{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText> <ThemedText style={styles.itemText}>{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
<CustomSwitch <CustomSwitch
isEnabled={albumEnabled} isEnabled={albumEnabled}
toggleSwitch={toggleAlbum} toggleSwitch={toggleAlbum}
/> />
</View>
{/* 分割线 */}
<Divider />
{/* 位置权限 */}
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
</View> </View>
<CustomSwitch {/* 分割线 */}
isEnabled={locationEnabled} <Divider />
toggleSwitch={toggleLocation} {/* 位置权限 */}
/> <View style={styles.item}>
</View> <View>
<Divider /> <ThemedText style={styles.itemText}>{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
<View style={styles.item}> </View>
<View> <CustomSwitch
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText> isEnabled={locationEnabled}
toggleSwitch={toggleLocation}
/>
</View> </View>
<CustomSwitch <Divider />
isEnabled={notificationsEnabled} <View style={styles.item}>
toggleSwitch={toggleNotifications} <View>
/> <ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
</View> </View>
{/* 相册成片权限 */} <CustomSwitch
{/* <View style={styles.item}> isEnabled={notificationsEnabled}
toggleSwitch={toggleNotifications}
/>
</View>
{/* 相册成片权限 */}
{/* <View style={styles.item}>
<View> <View>
<ThemedText style={styles.itemText}>Opus Permission</ThemedText> <ThemedText style={styles.itemText}>Opus Permission</ThemedText>
</View> </View>
@ -285,10 +296,10 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
toggleSwitch={toggleAlbum} toggleSwitch={toggleAlbum}
/> />
</View> */} </View> */}
</View>
</View> </View>
</View> {/* 账号 */}
{/* 账号 */} {/* <View style={{ marginTop: 16 }}>
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>Account</ThemedText> <ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>Account</ThemedText>
<View style={styles.content}> <View style={styles.content}>
<View style={styles.item}> <View style={styles.item}>
@ -307,78 +318,75 @@ const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible:
</View> </View>
</View> </View>
</View> */} </View> */}
{/* 协议 */} {/* 协议 */}
<View style={{ marginTop: 16 }}> <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('lcenses.title', { ns: 'personal' })}</ThemedText> <ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('lcenses.title', { ns: 'personal' })}</ThemedText>
<View style={styles.content}> <View style={styles.content}>
<TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} > <TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText> <ThemedText style={styles.itemText}>{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg /> <RightArrowSvg />
</TouchableOpacity> </TouchableOpacity>
<Divider /> <Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} > <TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText> <ThemedText style={styles.itemText}>{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
<RightArrowSvg /> <RightArrowSvg />
</TouchableOpacity> </TouchableOpacity>
<Divider /> <Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} > <TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText> <ThemedText style={styles.itemText}>{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
<RightArrowSvg /> <RightArrowSvg />
</TouchableOpacity> </TouchableOpacity>
<Divider /> <Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} > <TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText> <ThemedText style={styles.itemText}>{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg /> <RightArrowSvg />
</TouchableOpacity> </TouchableOpacity>
<Divider /> <Divider />
<TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} > <TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.qualification', { ns: 'personal' })}</ThemedText> <ThemedText style={styles.itemText}>{t('lcenses.qualification', { ns: 'personal' })}</ThemedText>
<RightArrowSvg /> <RightArrowSvg />
</TouchableOpacity> </TouchableOpacity>
<Divider /> <Divider />
<TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://beian.miit.gov.cn/")} > <TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://beian.miit.gov.cn/")} >
<ThemedText style={styles.itemText}>{t('lcenses.ICP', { ns: 'personal' })}ICP备2023032876号-4</ThemedText> <ThemedText style={styles.itemText}>{t('lcenses.ICP', { ns: 'personal' })}ICP备2023032876号-4</ThemedText>
<RightArrowSvg /> <RightArrowSvg />
</TouchableOpacity> </TouchableOpacity>
</View>
</View>
{/* 其他信息 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.otherInformation', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
<TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} >
<ThemedText style={styles.itemText}>{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
{/* <RightArrowSvg /> */}
</TouchableOpacity>
<Divider />
<View style={styles.item}>
<ThemedText style={styles.itemText}>{t('generalSetting.version', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText}>{"0.5.0"}</ThemedText>
</View> </View>
</View> </View>
</View> {/* 其他信息 */}
{/* 退出 */} <View style={{ marginTop: 16 }}>
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={handleLogout}> <ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.otherInformation', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.logout', { ns: 'personal' })}</ThemedText> <View style={styles.content}>
<LogoutSvg /> <TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} >
</TouchableOpacity> <ThemedText style={styles.itemText}>{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
{/* 注销账号 */} {/* <RightArrowSvg /> */}
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={() => setDeleteModalVisible(true)}> </TouchableOpacity>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.deleteAccount', { ns: 'personal' })}</ThemedText> <Divider />
<DeleteSvg /> <View style={styles.item}>
</TouchableOpacity> <ThemedText style={styles.itemText}>{t('generalSetting.version', { ns: 'personal' })}</ThemedText>
</ScrollView> <ThemedText style={styles.itemText}>{"0.5.0"}</ThemedText>
</View>
</View>
</View>
{/* 退出 */}
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={handleLogout}>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.logout', { ns: 'personal' })}</ThemedText>
<LogoutSvg />
</TouchableOpacity>
{/* 注销账号 */}
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={() => setDeleteModalVisible(true)}>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.deleteAccount', { ns: 'personal' })}</ThemedText>
<DeleteSvg />
</TouchableOpacity>
</ScrollView>
</Pressable>
</Pressable> </Pressable>
</Pressable>
{/* 协议弹窗 */} <PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} />
<PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} /> <LcensesModal modalVisible={lcensesModalVisible} setModalVisible={setLcensesModalVisible} />
{/* 许可证弹窗 */} <DeleteModal modalVisible={deleteModalVisible} setModalVisible={setDeleteModalVisible} setSettingModalVisible={setModalVisible} />
<LcensesModal modalVisible={lcensesModalVisible} setModalVisible={setLcensesModalVisible} /> </Modal>
{/* 通知 */} </>
{/* <AuthNotifications setNotificationsEnabled={setNotificationsEnabled} notificationsEnabled={notificationsEnabled} /> */}
{/* 退出登录 */}
<DeleteModal modalVisible={deleteModalVisible} setModalVisible={setDeleteModalVisible} setSettingModalVisible={setModalVisible} />
</Modal>
); );
}; };

View File

@ -1,10 +1,12 @@
// 地理位置逆编码 // 地理位置逆编码
import i18n from '@/i18n';
import { PermissionService } from '@/lib/PermissionService';
import { fetchApi } from '@/lib/server-api-util'; import { fetchApi } from '@/lib/server-api-util';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import * as Location from 'expo-location'; import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications'; import * as Notifications from 'expo-notifications';
import * as SecureStore from 'expo-secure-store'; import * as SecureStore from 'expo-secure-store';
import { Alert, Linking, Platform } from 'react-native'; import { Linking, Platform } from 'react-native';
interface Address { interface Address {
id: number; id: number;
@ -69,27 +71,13 @@ export const requestLocationPermission = async () => {
// 3. 如果用户之前选择了"拒绝且不再询问" // 3. 如果用户之前选择了"拒绝且不再询问"
if (status === 'denied' && !canAskAgain) { if (status === 'denied' && !canAskAgain) {
// 显示提示,引导用户去设置 // 显示提示,引导用户去设置
const openSettings = await new Promise(resolve => { const confirmed = await PermissionService.show({
Alert.alert( title: i18n.t('permission:title.locationPermissionRequired'),
'需要位置权限', message: i18n.t('permission:message.locationPreviouslyDenied'),
'您之前拒绝了位置权限。要使用此功能,请在设置中启用位置权限。',
[
{
text: '取消',
style: 'cancel',
onPress: () => resolve(false)
},
{
text: '去设置',
onPress: () => resolve(true)
}
]
);
}); });
if (openSettings) { if (confirmed) {
// 打开应用设置 openAppSettings();
await Linking.openSettings();
} }
return false; return false;
} }
@ -100,24 +88,25 @@ export const requestLocationPermission = async () => {
console.log('新权限状态:', newStatus); console.log('新权限状态:', newStatus);
if (newStatus !== 'granted') { if (newStatus !== 'granted') {
Alert.alert('需要位置权限', '请允许访问位置以使用此功能');
return false; return false;
} }
return true; return true;
} catch (error) { } catch (error) {
console.error('请求位置权限时出错:', error); console.error('请求位置权限时出错:', error);
Alert.alert('错误', '请求位置权限时出错');
return false; return false;
} }
}; };
export const openAppSettings = () => {
Linking.openSettings();
};
// 获取媒体库权限 // 获取媒体库权限
export const getPermissions = async () => { export const getPermissions = async () => {
if (Platform.OS !== 'web') { if (Platform.OS !== 'web') {
const { status: mediaStatus } = await ImagePicker.getMediaLibraryPermissionsAsync(); const { status: mediaStatus } = await ImagePicker.getMediaLibraryPermissionsAsync();
if (mediaStatus !== 'granted') { if (mediaStatus !== 'granted') {
// Alert.alert('需要媒体库权限', '请允许访问媒体库以继续');
return false; return false;
} }
return true; return true;
@ -130,7 +119,6 @@ export const requestPermissions = async () => {
if (Platform.OS !== 'web') { if (Platform.OS !== 'web') {
const mediaStatus = await ImagePicker.requestMediaLibraryPermissionsAsync(); const mediaStatus = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!mediaStatus.granted) { if (!mediaStatus.granted) {
// Alert.alert('需要媒体库权限', '请允许访问媒体库以继续');
return false; return false;
} }
return true; return true;
@ -182,20 +170,10 @@ export const requestMediaLibraryPermission = async (showAlert: boolean = true):
// 3. 如果之前被拒绝且不能再次询问 // 3. 如果之前被拒绝且不能再次询问
if (existingStatus === 'denied' && !canAskAgain) { if (existingStatus === 'denied' && !canAskAgain) {
if (showAlert) { if (showAlert) {
const openSettings = await new Promise<boolean>(resolve => { await PermissionService.show({
Alert.alert( title: i18n.t('permission:title.mediaLibraryPermissionRequired'),
'需要媒体库权限', message: i18n.t('permission:message.mediaLibraryPreviouslyDenied'),
'您之前拒绝了媒体库访问权限。要选择照片,请在设置中启用媒体库权限。',
[
{ text: '取消', style: 'cancel', onPress: () => resolve(false) },
{ text: '去设置', onPress: () => resolve(true) }
]
);
}); });
if (openSettings) {
await Linking.openSettings();
}
} }
return false; return false;
} }
@ -204,14 +182,20 @@ export const requestMediaLibraryPermission = async (showAlert: boolean = true):
const { status: newStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); const { status: newStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (newStatus !== 'granted' && showAlert) { if (newStatus !== 'granted' && showAlert) {
Alert.alert('需要媒体库权限', '请允许访问媒体库以方便后续操作'); await PermissionService.show({
title: i18n.t('permission:title.mediaLibraryPermissionRequired'),
message: i18n.t('permission:message.mediaLibraryPermissionRequired'),
});
} }
return newStatus === 'granted'; return newStatus === 'granted';
} catch (error) { } catch (error) {
console.error('请求媒体库权限时出错:', error); console.error('请求媒体库权限时出错:', error);
if (showAlert) { if (showAlert) {
Alert.alert('错误', '请求媒体库权限时出错'); await PermissionService.show({
title: i18n.t('permission:title.error'),
message: i18n.t('permission:message.requestPermissionError'),
});
} }
return false; return false;
} }
@ -240,28 +224,10 @@ export const requestNotificationPermission = async () => {
// 3. 如果用户之前选择了"拒绝且不再询问" // 3. 如果用户之前选择了"拒绝且不再询问"
if (status === 'denied' && !canAskAgain) { if (status === 'denied' && !canAskAgain) {
// 显示提示,引导用户去设置 // 显示提示,引导用户去设置
const openSettings = await new Promise(resolve => { await PermissionService.show({
Alert.alert( title: i18n.t('permission:title.notificationPermissionRequired'),
'需要通知权限', message: i18n.t('permission:message.notificationPreviouslyDenied'),
'您之前拒绝了通知权限。要使用此功能,请在设置中启用通知权限。',
[
{
text: '取消',
style: 'cancel',
onPress: () => resolve(false)
},
{
text: '去设置',
onPress: () => resolve(true)
}
]
);
}); });
if (openSettings) {
// 打开应用设置
await Linking.openSettings();
}
return false; return false;
} }
@ -271,14 +237,17 @@ export const requestNotificationPermission = async () => {
console.log('新通知权限状态:', newStatus); console.log('新通知权限状态:', newStatus);
if (newStatus !== 'granted') { if (newStatus !== 'granted') {
Alert.alert('需要通知权限', '请允许通知以使用此功能'); PermissionService.show({
title: '需要通知权限',
message: '请允许通知以使用此功能',
});
return false; return false;
} }
return true; return true;
} catch (error) { } catch (error) {
console.error('请求通知权限时出错:', error); console.error('请求通知权限时出错:', error);
Alert.alert('错误', '请求通知权限时出错'); PermissionService.show({ title: '错误', message: '请求通知权限时出错' });
return false; return false;
} }
}; };

View File

@ -0,0 +1,82 @@
import PermissionAlert from '@/components/common/PermissionAlert';
import i18n from '@/i18n';
import { PermissionService } from '@/lib/PermissionService';
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react';
import { Linking } from 'react-native';
interface PermissionAlertOptions {
title: string;
message: string;
confirmText?: string;
cancelText?: string;
}
interface PermissionContextType {
showPermissionAlert: (options: PermissionAlertOptions) => Promise<boolean>;
}
interface AlertData {
options: PermissionAlertOptions;
resolve: (value: boolean) => void;
}
const PermissionContext = createContext<PermissionContextType | undefined>(undefined);
export const PermissionProvider = ({ children }: { children: ReactNode }) => {
const [alertData, setAlertData] = useState<AlertData | null>(null);
const showPermissionAlert = useCallback((options: PermissionAlertOptions) => {
return new Promise<boolean>((resolve) => {
setAlertData({ options, resolve });
});
}, []);
useEffect(() => {
PermissionService.set(showPermissionAlert);
// Cleanup on unmount
return () => {
PermissionService.set(null as any); // or a no-op function
};
}, [showPermissionAlert]);
const handleConfirm = () => {
Linking.openSettings();
if (alertData?.resolve) {
alertData.resolve(true);
}
setAlertData(null);
};
const handleCancel = () => {
if (alertData?.resolve) {
alertData.resolve(false);
}
setAlertData(null);
};
return (
<PermissionContext.Provider value={{ showPermissionAlert }}>
{children}
{alertData && (
<PermissionAlert
visible={!!alertData}
title={alertData.options.title}
message={alertData.options.message}
confirmText={alertData.options.confirmText || i18n.t('permission:button.confirm')}
cancelText={alertData.options.cancelText || i18n.t('permission:button.cancel')}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
)}
</PermissionContext.Provider>
);
};
export const usePermission = (): PermissionContextType => {
const context = useContext(PermissionContext);
if (!context) {
throw new Error('usePermission must be used within a PermissionProvider');
}
return context;
};

View File

@ -7,6 +7,7 @@ import * as path from 'path';
function generateImports() { function generateImports() {
const localesPath = path.join(__dirname, 'locales'); const localesPath = path.join(__dirname, 'locales');
const namespaces = ['common', 'home', 'login', 'settings', 'upload', 'chat', 'me', 'permission'];
const languages = fs.readdirSync(localesPath); const languages = fs.readdirSync(localesPath);
let imports = ''; let imports = '';
let translationsMap = 'const translations = {\n'; let translationsMap = 'const translations = {\n';

View File

@ -32,7 +32,7 @@ i18n
resources: translations, resources: translations,
// 支持命名空间 // 支持命名空间
ns: ['common', 'example', 'download'], ns: ['common', 'example', 'download', 'permission'],
defaultNS: 'common', defaultNS: 'common',
// 设置默认语言为中文 // 设置默认语言为中文
@ -96,14 +96,16 @@ export const preloadCommonTranslations = async () => {
// 预加载 common 和 example 命名空间 // 预加载 common 和 example 命名空间
await Promise.all([ await Promise.all([
loadNamespaceForLanguage(currentLng, 'common'), loadNamespaceForLanguage(currentLng, 'common'),
loadNamespaceForLanguage(currentLng, 'example') loadNamespaceForLanguage(currentLng, 'example'),
loadNamespaceForLanguage(currentLng, 'permission')
]); ]);
// 如果当前语言不是英语,也预加载英语作为备用 // 如果当前语言不是英语,也预加载英语作为备用
if (currentLng !== 'en') { if (currentLng !== 'en') {
await Promise.all([ await Promise.all([
loadNamespaceForLanguage('en', 'common'), loadNamespaceForLanguage('en', 'common'),
loadNamespaceForLanguage('en', 'example') loadNamespaceForLanguage('en', 'example'),
loadNamespaceForLanguage('en', 'permission')
]); ]);
} }
}; };

View File

@ -17,7 +17,9 @@
"login": "Login", "login": "Login",
"trade": "沪ICP备2025133004号-2A", "trade": "沪ICP备2025133004号-2A",
"logout": "Logout", "logout": "Logout",
"self": "Personal Center" "self": "Personal Center",
"goToSettings": "Go to Settings",
"cancel": "Cancel"
}, },
"welcome": { "welcome": {
"welcome": "Welcome to MemoWake~", "welcome": "Welcome to MemoWake~",

View File

@ -0,0 +1,29 @@
{
"title": {
"permissionDenied": "Permission Denied",
"locationPermissionRequired": "Location Permission Required",
"mediaLibraryPermissionRequired": "Media Library Permission Required",
"notificationPermissionRequired": "Notification Permission Required",
"success": "✅ Success",
"error": "❌ Error",
"getMediaFailed": "Failed to Get Media"
},
"message": {
"locationPreviouslyDenied": "You have previously denied location permissions. To use this feature, please enable it in settings.",
"mediaLibraryPreviouslyDenied": "You have previously denied media library permissions. To use this feature, please enable it in settings.",
"notificationPreviouslyDenied": "You have previously denied notification permissions. To use this feature, please enable it in settings.",
"saveToAlbumPermissionRequired": "Permission is required to save images to the album.",
"qrCodeSaved": "QR code has been saved to the album!",
"saveImageFailed": "Failed to save the image, please try again.",
"getStatsPermissionRequired": "Permission to access the media library is required to get statistics.",
"getStatsFailed": "Failed to get media library statistics.",
"noMediaFound": "Could not retrieve any media. Please check permissions or your media library.",
"uploadError": "An error occurred during the upload process."
},
"button": {
"cancel": "Cancel",
"goToSettings": "Go to Settings",
"ok": "OK",
"confirm": "Go to Settings"
}
}

View File

@ -17,7 +17,9 @@
"login": "登录", "login": "登录",
"trade": "沪ICP备2025133004号-2A", "trade": "沪ICP备2025133004号-2A",
"logout": "退出登录", "logout": "退出登录",
"self": "个人中心" "self": "个人中心",
"goToSettings": "去设置",
"cancel": "取消"
}, },
"welcome": { "welcome": {
"welcome": "欢迎来到 MemoWake~", "welcome": "欢迎来到 MemoWake~",

View File

@ -0,0 +1,29 @@
{
"title": {
"permissionDenied": "权限被拒绝",
"locationPermissionRequired": "需要位置权限",
"mediaLibraryPermissionRequired": "需要媒体库权限",
"notificationPermissionRequired": "需要通知权限",
"success": "✅ 成功",
"error": "❌ 失败",
"getMediaFailed": "获取媒体资源失败"
},
"message": {
"locationPreviouslyDenied": "您之前拒绝了位置权限。要使用此功能,请在设置中启用位置权限。",
"mediaLibraryPreviouslyDenied": "您之前拒绝了媒体库权限。要使用此功能,请在设置中启用它。",
"notificationPreviouslyDenied": "您之前拒绝了通知权限。要使用此功能,请在设置中启用它。",
"saveToAlbumPermissionRequired": "需要保存图片到相册的权限",
"qrCodeSaved": "二维码已保存到相册!",
"saveImageFailed": "无法保存图片,请重试",
"getStatsPermissionRequired": "需要访问媒体库权限来获取统计信息",
"getStatsFailed": "获取媒体库统计信息失败",
"noMediaFound": "未能获取到任何媒体资源,请检查权限或媒体库。",
"uploadError": "上传过程中出现错误。"
},
"button": {
"cancel": "取消",
"goToSettings": "去设置",
"ok": "好的",
"confirm": "去设置"
}
}

View File

@ -4,23 +4,25 @@ import enAdmin from './locales/en/admin.json';
import enAsk from './locales/en/ask.json'; import enAsk from './locales/en/ask.json';
import enCommon from './locales/en/common.json'; import enCommon from './locales/en/common.json';
import enDownload from './locales/en/download.json'; import enDownload from './locales/en/download.json';
import enSupport from './locales/en/support.json';
import enExample from './locales/en/example.json'; import enExample from './locales/en/example.json';
import enFairclip from './locales/en/fairclip.json'; import enFairclip from './locales/en/fairclip.json';
import enLanding from './locales/en/landing.json'; import enLanding from './locales/en/landing.json';
import enLogin from './locales/en/login.json'; import enLogin from './locales/en/login.json';
import enPermission from './locales/en/permission.json';
import enPersonal from './locales/en/personal.json'; import enPersonal from './locales/en/personal.json';
import enSupport from './locales/en/support.json';
import enUpload from './locales/en/upload.json'; import enUpload from './locales/en/upload.json';
import zhAdmin from './locales/zh/admin.json'; import zhAdmin from './locales/zh/admin.json';
import zhAsk from './locales/zh/ask.json'; import zhAsk from './locales/zh/ask.json';
import zhCommon from './locales/zh/common.json'; import zhCommon from './locales/zh/common.json';
import zhDownload from './locales/zh/download.json'; import zhDownload from './locales/zh/download.json';
import zhSupport from './locales/zh/support.json';
import zhExample from './locales/zh/example.json'; import zhExample from './locales/zh/example.json';
import zhFairclip from './locales/zh/fairclip.json'; import zhFairclip from './locales/zh/fairclip.json';
import zhLanding from './locales/zh/landing.json'; import zhLanding from './locales/zh/landing.json';
import zhLogin from './locales/zh/login.json'; import zhLogin from './locales/zh/login.json';
import zhPermission from './locales/zh/permission.json';
import zhPersonal from './locales/zh/personal.json'; import zhPersonal from './locales/zh/personal.json';
import zhSupport from './locales/zh/support.json';
import zhUpload from './locales/zh/upload.json'; import zhUpload from './locales/zh/upload.json';
const translations = { const translations = {
@ -29,12 +31,13 @@ const translations = {
ask: enAsk, ask: enAsk,
common: enCommon, common: enCommon,
download: enDownload, download: enDownload,
support: enSupport,
example: enExample, example: enExample,
fairclip: enFairclip, fairclip: enFairclip,
landing: enLanding, landing: enLanding,
login: enLogin, login: enLogin,
permission: enPermission,
personal: enPersonal, personal: enPersonal,
support: enSupport,
upload: enUpload upload: enUpload
}, },
zh: { zh: {
@ -42,12 +45,13 @@ const translations = {
ask: zhAsk, ask: zhAsk,
common: zhCommon, common: zhCommon,
download: zhDownload, download: zhDownload,
support: zhSupport,
example: zhExample, example: zhExample,
fairclip: zhFairclip, fairclip: zhFairclip,
landing: zhLanding, landing: zhLanding,
login: zhLogin, login: zhLogin,
permission: zhPermission,
personal: zhPersonal, personal: zhPersonal,
support: zhSupport,
upload: zhUpload upload: zhUpload
}, },
}; };

21
lib/PermissionService.ts Normal file
View File

@ -0,0 +1,21 @@
interface PermissionAlertOptions {
title: string;
message: string;
}
type ShowPermissionAlertFunction = (options: PermissionAlertOptions) => Promise<boolean>;
let showPermissionAlertRef: ShowPermissionAlertFunction | null = null;
export const PermissionService = {
set: (fn: ShowPermissionAlertFunction) => {
showPermissionAlertRef = fn;
},
show: (options: PermissionAlertOptions): Promise<boolean> => {
if (!showPermissionAlertRef) {
console.error("PermissionAlert has not been set. Please ensure PermissionProvider is used at the root of your app.");
return Promise.resolve(false);
}
return showPermissionAlertRef(options);
},
};

View File

@ -1,5 +1,6 @@
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import { Alert } from 'react-native'; import { PermissionService } from '../PermissionService';
import i18n from '@/i18n';
import { getUploadTaskStatus, insertUploadTask } from '../db'; import { getUploadTaskStatus, insertUploadTask } from '../db';
import { getMediaByDateRange } from './media'; import { getMediaByDateRange } from './media';
import { ExtendedAsset } from './types'; import { ExtendedAsset } from './types';
@ -24,7 +25,7 @@ export const triggerManualUpload = async (
try { try {
const media = await getMediaByDateRange(startDate, endDate); const media = await getMediaByDateRange(startDate, endDate);
if (media.length === 0) { if (media.length === 0) {
Alert.alert('提示', '在指定时间范围内未找到媒体文件'); PermissionService.show({ title: i18n.t('permission:title.getMediaFailed'), message: i18n.t('permission:message.noMediaFound') });
return []; return [];
} }
@ -76,7 +77,7 @@ export const triggerManualUpload = async (
return finalResults; return finalResults;
} catch (error) { } catch (error) {
console.error('手动上传过程中出现错误:', error); console.error('手动上传过程中出现错误:', error);
Alert.alert('错误', '上传过程中出现错误'); PermissionService.show({ title: i18n.t('permission:title.error'), message: i18n.t('permission:message.uploadError') });
throw error; throw error;
} }
}; };