jinyaqiu 828e84710f
All checks were successful
Dev Deploy / Explore-Gitea-Actions (push) Successful in 24s
feat: 个人中心
2025-07-14 10:42:59 +08:00

469 lines
21 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 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, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { ThemedText } from '../ThemedText';
import LcensesModal from './qualification/lcenses';
import PrivacyModal from './qualification/privacy';
import CustomSwitch from './switch';
import UserInfo from './userInfo';
import { getLocationPermission, getPermissions, requestLocationPermission, requestMediaLibraryPermission, 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 { logout } = useAuth();
const router = useRouter();
// 打开设置
const openAppSettings = () => {
Linking.openSettings();
};
// 通知消息权限开关
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const toggleNotifications = () => setNotificationsEnabled(previous => !previous);
// 相册权限
const [albumEnabled, setAlbumEnabled] = useState(false);
const toggleAlbum = () => {
if (albumEnabled) {
// 引导去设置关闭权限
openAppSettings()
} else {
requestMediaLibraryPermission().then((res) => {
setAlbumEnabled(res as boolean);
})
}
}
// 位置权限
const [locationEnabled, setLocationEnabled] = useState(false);
// 位置权限更改
const toggleLocation = async () => {
if (locationEnabled) {
// 引导去设置关闭权限
openAppSettings()
} else {
requestLocationPermission().then((res) => {
setLocationEnabled(res as boolean);
})
}
};
// 正在获取位置信息
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) {
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. 更新位置状态
setCurrentLocation(address as 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) => {
setLocationEnabled(res);
})
// 媒体库权限
getPermissions().then((res) => {
setAlbumEnabled(res);
})
}
}, [modalVisible])
return (
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
setModalVisible(!modalVisible);
}}>
<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}>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={() => { 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>
<Divider />
<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('ai'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.aiPolicy', { 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>
</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 />
{Platform.OS !== 'ios' && (
<View>
<TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} >
<ThemedText style={styles.itemText}>{t('generalSetting.cleanCache', { ns: 'personal' })}</ThemedText>
{/* <RightArrowSvg /> */}
</TouchableOpacity>
<Divider />
</View>
)}
<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>
</ScrollView>
</Pressable>
</Pressable>
{/* 协议弹窗 */}
<PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} />
{/* 许可证弹窗 */}
<LcensesModal modalVisible={lcensesModalVisible} setModalVisible={setLcensesModalVisible} />
{/* 通知 */}
{/* <AuthNotifications setNotificationsEnabled={setNotificationsEnabled} notificationsEnabled={notificationsEnabled} /> */}
</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;