2025-07-23 21:09:40 +08:00

504 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import DeleteSvg from '@/assets/icons/svg/delete.svg';
import LogoutSvg from '@/assets/icons/svg/logout.svg';
import RightArrowSvg from '@/assets/icons/svg/rightArrow.svg';
import { useAuth } from '@/contexts/auth-context';
import { fetchApi } from '@/lib/server-api-util';
import { Address, User } from '@/types/user';
import * as Location from 'expo-location';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking, Modal, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { ThemedText } from '../ThemedText';
import DeleteModal from './delete';
import LcensesModal from './qualification/lcenses';
import PrivacyModal from './qualification/privacy';
import CustomSwitch from './switch';
import UserInfo from './userInfo';
import { checkNotificationPermission, getLocationPermission, getPermissions, requestLocationPermission, requestMediaLibraryPermission, requestNotificationPermission, reverseGeocode } from './utils';
const SettingModal = (props: { modalVisible: boolean, setModalVisible: (visible: boolean) => void, userInfo: User }) => {
const { modalVisible, setModalVisible, userInfo } = props;
const { t } = useTranslation();
const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai');
// 协议弹窗
const [privacyModalVisible, setPrivacyModalVisible] = useState(false);
// 许可证弹窗
const [lcensesModalVisible, setLcensesModalVisible] = useState(false);
// 删除弹窗
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const { logout } = useAuth();
const router = useRouter();
// 打开设置
const openAppSettings = () => {
Linking.openSettings();
};
// 通知消息权限开关
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const toggleNotifications = async () => {
if (notificationsEnabled) {
// 引导去设置关闭权限
openAppSettings()
} else {
requestNotificationPermission()
.then((granted) => {
setNotificationsEnabled(granted);
});
setModalVisible(false);
}
};
// 相册权限
const [albumEnabled, setAlbumEnabled] = useState(false);
const toggleAlbum = async () => {
if (albumEnabled) {
// 引导去设置关闭权限
openAppSettings()
} else {
requestMediaLibraryPermission()
.then((granted) => {
setAlbumEnabled(granted);
});
setModalVisible(false);
}
}
// 位置权限
const [locationEnabled, setLocationEnabled] = useState(false);
// 位置权限更改
const toggleLocation = async () => {
if (locationEnabled) {
// 如果权限已开启,点击则引导用户去设置关闭
openAppSettings();
} else {
requestLocationPermission()
.then((granted) => {
setLocationEnabled(granted);
});
setModalVisible(false);
}
};
// 正在获取位置信息
const [isLoading, setIsLoading] = useState(false);
// 动画开启
const [isRefreshing, setIsRefreshing] = useState(false);
// 当前位置状态
const [currentLocation, setCurrentLocation] = useState<Address>({} as Address);
// 获取当前位置
const getCurrentLocation = async () => {
setIsLoading(true);
setIsRefreshing(true);
try {
// 1. 首先检查当前权限状态 -- 获取当前的位置权限
let currentStatus = await getLocationPermission();
console.log('当前权限状态:', currentStatus);
// 2. 如果没有权限,则跳过获取位置
if (!currentStatus) {
console.log('没有权限,跳过获取位置')
return;
// const newStatus = await requestLocationPermission();
// setLocationEnabled(newStatus);
// currentStatus = newStatus;
// if (!currentStatus) {
// // alert('需要位置权限才能继续');
// return;
// }
}
// 3. 确保位置服务已启用
const isEnabled = await Location.hasServicesEnabledAsync();
if (!isEnabled) {
alert('请先启用位置服务');
return;
}
console.log('位置服务已启用');
// 4. 获取当前位置
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High, // 使用高精度
timeInterval: 10000, // 可选:最大等待时间(毫秒)
});
console.log('位置:', location);
// 地理位置逆编码
const address = await reverseGeocode(location.coords.latitude, location.coords.longitude);
// 5. 更新位置状态
if (address) {
setCurrentLocation(address);
}
return location;
} catch (error: any) {
if (error.code === 'PERMISSION_DENIED' || error.code === 'PERMISSION_DENIED_ERROR') {
alert('位置权限被拒绝,请在设置中启用位置服务');
} else if (error.code === 'TIMEOUT') {
alert('获取位置超时,请检查网络和位置服务');
} else {
alert(`无法获取您的位置: ${error.message || '未知错误'}`);
}
throw error; // 重新抛出错误以便上层处理
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};
// 退出登录
const handleLogout = () => {
fetchApi("/iam/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
.then(async (res) => {
await logout();
setModalVisible(false);
router.replace('/login');
})
.catch(() => {
console.error("jwt has expired.");
});
};
// 检查是否有权限
useEffect(() => {
if (modalVisible) {
// 位置权限
getLocationPermission().then((res) => {
console.log('位置权限:', res);
setLocationEnabled(res);
})
// 媒体库权限
getPermissions().then((res) => {
console.log('媒体库权限:', res);
setAlbumEnabled(res);
})
// 通知权限
checkNotificationPermission().then((res) => {
console.log('通知权限:', res);
setNotificationsEnabled(res);
})
}
}, [modalVisible])
return (
<>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(false);
}}>
<Pressable
style={styles.centeredView}
onPress={() => setModalVisible(false)}>
<Pressable
style={styles.modalView}
onPress={(e) => e.stopPropagation()}>
<View style={styles.modalHeader}>
<Text style={{ opacity: 0 }}>Settings</Text>
<Text style={styles.modalTitle}>{t('generalSetting.allTitle', { ns: 'personal' })}</Text>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Text style={styles.closeButton}>×</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
{/* 用户信息 */}
<UserInfo
userInfo={userInfo}
setModalVisible={setModalVisible}
modalVisible={modalVisible}
setCurrentLocation={setCurrentLocation}
getCurrentLocation={getCurrentLocation}
isLoading={isLoading}
isRefreshing={isRefreshing}
currentLocation={currentLocation}
/>
{/* 升级版本 */}
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.subscription', { ns: 'personal' })}</ThemedText>
<View style={styles.premium}>
<View>
<ThemedText style={styles.itemText}>{t('generalSetting.subscriptionTitle', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ color: '#AC7E35', fontSize: 12 }}>{t('generalSetting.subscriptionText', { ns: 'personal' })}</ThemedText>
</View>
<TouchableOpacity
style={styles.upgradeButton}
onPress={async () => {
}}
>
<Text style={styles.upgradeButtonText}>
{t('generalSetting.upgrade', { ns: 'personal' })}
</Text>
</TouchableOpacity>
</View>
</View> */}
{/* 消息通知 */}
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
<View style={styles.premium}>
<View>
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={notificationsEnabled}
toggleSwitch={toggleNotifications}
/>
</View>
</View> */}
{/* 权限信息 */}
<View style={{ marginTop: 16 }}>
<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.item}>
<ThemedText style={styles.itemText}>{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
<CustomSwitch
isEnabled={albumEnabled}
toggleSwitch={toggleAlbum}
/>
</View>
{/* 分割线 */}
<Divider />
{/* 位置权限 */}
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={locationEnabled}
toggleSwitch={toggleLocation}
/>
</View>
<Divider />
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={notificationsEnabled}
toggleSwitch={toggleNotifications}
/>
</View>
{/* 相册成片权限 */}
{/* <View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>Opus Permission</ThemedText>
</View>
<CustomSwitch
isEnabled={albumEnabled}
toggleSwitch={toggleAlbum}
/>
</View> */}
</View>
</View>
{/* 账号 */}
{/* <View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>Account</ThemedText>
<View style={styles.content}>
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>Notifications</ThemedText>
</View>
<CustomSwitch
isEnabled={notificationsEnabled}
toggleSwitch={toggleNotifications}
/>
</View>
<Divider />
<View style={styles.item}>
<ThemedText style={styles.itemText}>Delete Account</ThemedText>
<DeleteSvg />
</View>
</View>
</View> */}
{/* 协议 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('lcenses.title', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
<TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.qualification', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<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>
<RightArrowSvg />
</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>
{/* 退出 */}
<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>
<PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} />
<LcensesModal modalVisible={lcensesModalVisible} setModalVisible={setLcensesModalVisible} />
<DeleteModal modalVisible={deleteModalVisible} setModalVisible={setDeleteModalVisible} setSettingModalVisible={setModalVisible} />
</Modal>
</>
);
};
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalView: {
width: '100%',
height: '80%',
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 16,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#4C320C',
},
closeButton: {
fontSize: 28,
color: '#4C320C',
padding: 10,
},
modalContent: {
flex: 1,
},
modalText: {
fontSize: 16,
color: '#4C320C',
},
premium: {
backgroundColor: "#FAF9F6",
padding: 16,
borderRadius: 24,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
content: {
flex: 1,
flexDirection: 'column',
gap: 4,
backgroundColor: '#FAF9F6',
borderRadius: 24,
paddingVertical: 8
},
item: {
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
itemText: {
fontSize: 14,
fontWeight: '600',
color: '#4C320C',
},
upgradeButton: {
backgroundColor: '#E2793F',
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 8,
},
upgradeButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: "600"
},
switchContainer: {
width: 50,
height: 30,
borderRadius: 15,
justifyContent: 'center',
paddingHorizontal: 2,
},
switchOn: {
backgroundColor: '#E2793F',
alignItems: 'flex-end',
},
switchOff: {
backgroundColor: '#E5E5E5',
alignItems: 'flex-start',
},
switchCircle: {
width: 26,
height: 26,
borderRadius: 13,
},
switchCircleOn: {
backgroundColor: 'white',
},
switchCircleOff: {
backgroundColor: '#A5A5A5',
},
});
const Divider = () => {
return (
<View className='w-full h-[1px] bg-[#B5977F]'></View>
)
}
export default SettingModal;