2025-08-08 19:05:43 +08:00

511 lines
23 KiB
TypeScript
Raw Permalink 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 ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
import RightArrowSvg from '@/assets/icons/svg/rightArrow.svg';
import DeleteModal from '@/components/owner/delete';
import LcensesModal from '@/components/owner/qualification/lcenses';
import PrivacyModal from '@/components/owner/qualification/privacy';
import CustomSwitch from '@/components/owner/switch';
import UserInfo from '@/components/owner/userInfo';
import { checkNotificationPermission, getLocationPermission, getPermissions, requestLocationPermission, requestMediaLibraryPermission, requestNotificationPermission, reverseGeocode } from '@/components/owner/utils';
import { ThemedText } from '@/components/ThemedText';
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 { useFocusEffect, useRouter } from 'expo-router';
import * as SecureStore from 'expo-secure-store';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Linking, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context";
const Setting = () => {
const [userInfo, setUserInfo] = useState<User | null>(null);
const getUserInfo = async () => {
const res = await fetchApi<User>("/iam/user-info");
setUserInfo(res);
}
const insets = useSafeAreaInsets();
const { t } = useTranslation();
// 判断当前语言环境
let language = "";
const getLanguage = async () => {
if (Platform.OS === 'web') {
language = localStorage.getItem('i18nextLng') || "";
} else {
language = await SecureStore.getItemAsync('i18nextLng') || "";
}
}
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: boolean | ((prevState: boolean) => boolean)) => {
setNotificationsEnabled(granted);
});
}
};
// 相册权限
const [albumEnabled, setAlbumEnabled] = useState(false);
const toggleAlbum = async () => {
if (albumEnabled) {
// 引导去设置关闭权限
openAppSettings()
} else {
requestMediaLibraryPermission()
.then((granted: boolean | ((prevState: boolean) => boolean)) => {
setAlbumEnabled(granted);
});
}
}
// 位置权限
const [locationEnabled, setLocationEnabled] = useState(false);
// 位置权限更改
const toggleLocation = async () => {
if (locationEnabled) {
// 如果权限已开启,点击则引导用户去设置关闭
openAppSettings();
} else {
requestLocationPermission()
.then((granted: boolean | ((prevState: boolean) => boolean)) => {
setLocationEnabled(granted);
});
}
};
// 正在获取位置信息
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();
// 2. 如果没有权限,则跳过获取位置
if (!currentStatus) {
return;
// const newStatus = await requestLocationPermission();
// setLocationEnabled(newStatus);
// currentStatus = newStatus;
// if (!currentStatus) {
// // alert('需要位置权限才能继续');
// return;
// }
}
// 3. 确保位置服务已启用
const isEnabled = await Location.hasServicesEnabledAsync();
if (!isEnabled) {
alert(t('permission.locationPermissionRequired', { ns: 'common' }));
return;
}
// 4. 获取当前位置
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High, // 使用高精度
timeInterval: 10000, // 可选:最大等待时间(毫秒)
});
// 地理位置逆编码
const address = await reverseGeocode(location.coords.latitude, location.coords.longitude);
// 5. 更新位置状态
if (address) {
setCurrentLocation(address);
}
return location;
} catch (error: any) {
if (error.code === 'TIMEOUT') {
alert(t('permission.timeout', { ns: 'common' }));
} else {
alert(t('permission.notLocation', { ns: 'common' }) + error.message || t('permission.notError', { ns: 'common' }));
}
throw error; // 重新抛出错误以便上层处理
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};
// 退出登录
const handleLogout = () => {
fetchApi("/iam/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
.then(async (res) => {
await logout();
router.replace('/login');
})
.catch(() => {
console.error("jwt has expired.");
});
};
// 检查是否有权限
useFocusEffect(
useCallback(() => {
let isActive = true;
const checkPermissions = async () => {
// 位置权限
const locationRes = await getLocationPermission();
// 媒体库权限
const albumRes = await getPermissions();
// 通知权限
const notificationRes = await checkNotificationPermission();
if (isActive) {
setLocationEnabled(locationRes);
setAlbumEnabled(albumRes);
setNotificationsEnabled(notificationRes);
}
};
checkPermissions();
return () => {
isActive = false;
};
}, [])
);
// 获取语言环境
useEffect(() => {
getLanguage();
getUserInfo()
}, [])
return (
<View style={{ flex: 1, paddingTop: insets.top, paddingBottom: insets.bottom, backgroundColor: '#fff' }}>
<Pressable
style={styles.centeredView}
>
<Pressable
style={styles.modalView}
onPress={(e) => e.stopPropagation()}>
<View style={styles.modalHeader}>
<TouchableOpacity onPress={() => { router.push('/owner') }}>
<ReturnArrowSvg />
</TouchableOpacity>
<ThemedText style={styles.modalTitle} >{t('generalSetting.allTitle', { ns: 'personal' })}</ThemedText>
<Text style={{ opacity: 0 }}>×</Text>
</View>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
{/* 用户信息 */}
<UserInfo
userInfo={userInfo || {} as User}
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' }} type="sfPro">{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
{/* 相册权限 */}
<View style={styles.item}>
<ThemedText style={styles.itemText} type="sfPro">{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
<CustomSwitch
isEnabled={albumEnabled}
toggleSwitch={toggleAlbum}
/>
</View>
{/* 分割线 */}
<Divider />
{/* 位置权限 */}
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText} type="sfPro">{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={locationEnabled}
toggleSwitch={toggleLocation}
/>
</View>
<Divider />
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText} type="sfPro">{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' }} type="sfPro">{t('lcenses.title', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
<TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} >
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.qualification', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={[styles.item, { display: language == "en" ? 'none' : 'flex' }]} onPress={() => Linking.openURL("https://beian.miit.gov.cn/")} >
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.ICP', { ns: 'personal' })}ICP备2025133004号-2A</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
</View>
</View>
{/* 其他信息 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }} type="sfPro">{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} type="sfPro">{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
{/* <RightArrowSvg /> */}
</TouchableOpacity>
<Divider />
<View style={styles.item}>
<ThemedText style={styles.itemText} type="sfPro">{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' }} type="sfPro">{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' }} type="sfPro">{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} />
</View>
);
};
const styles = StyleSheet.create({
centeredView: {
flex: 1
},
modalView: {
width: '100%',
height: '100%',
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",
paddingHorizontal: 16,
paddingVertical: 20,
borderRadius: 24,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
content: {
flex: 1,
flexDirection: 'column',
gap: 4,
backgroundColor: '#FAF9F6',
borderRadius: 24,
padding: 8
},
item: {
paddingHorizontal: 16,
paddingVertical: 12,
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 Setting;