- 新增 i18n 翻译资源,覆盖睡眠、饮水、体重、锻炼、用药 AI 识别、步数、健身圆环、基础代谢及设置等核心模块 - 重构相关页面及组件(如 SleepDetail, WaterDetail, WorkoutHistory 等)使用 `useI18n` 钩子替换硬编码文本 - 升级 `utils/date` 工具库与 `DateSelector` 组件,支持基于语言环境的日期格式化与显示 - 完善登录页、注销流程及权限申请弹窗的双语提示信息 - 优化部分页面的 UI 细节与字体样式以适配多语言显示
159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
import AsyncStorage from '@/utils/kvStore';
|
||
import { usePathname, useRouter } from 'expo-router';
|
||
import { useCallback } from 'react';
|
||
import { Alert } from 'react-native';
|
||
|
||
import { ROUTES } from '@/constants/Routes';
|
||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||
import { useI18n } from '@/hooks/useI18n';
|
||
import { STORAGE_KEYS, api } from '@/services/api';
|
||
import { logout as logoutAction } from '@/store/userSlice';
|
||
|
||
type RedirectParams = Record<string, string | number | boolean | undefined>;
|
||
|
||
type EnsureOptions = {
|
||
redirectTo?: string;
|
||
redirectParams?: RedirectParams;
|
||
shouldBack?: boolean; // 登录成功后是否返回上一页而不是重定向
|
||
};
|
||
|
||
export function useAuthGuard() {
|
||
const router = useRouter();
|
||
const dispatch = useAppDispatch();
|
||
const currentPath = usePathname();
|
||
const user = useAppSelector(state => state.user);
|
||
const { t } = useI18n();
|
||
|
||
// 判断登录状态:优先使用 token,因为 token 是登录的根本凭证
|
||
// profile.id 可能在初始化时还未加载,但 token 已经从 AsyncStorage 恢复
|
||
const isLoggedIn = !!user?.token;
|
||
|
||
const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise<boolean> => {
|
||
if (isLoggedIn) return true;
|
||
|
||
const redirectTo = options?.redirectTo ?? currentPath ?? ROUTES.TAB_STATISTICS;
|
||
const paramsJson = options?.redirectParams ? JSON.stringify(options.redirectParams) : undefined;
|
||
const shouldBack = options?.shouldBack;
|
||
|
||
router.push({
|
||
pathname: '/auth/login',
|
||
params: {
|
||
redirectTo,
|
||
...(paramsJson ? { redirectParams: paramsJson } : {}),
|
||
...(shouldBack ? { shouldBack: 'true' } : {}),
|
||
},
|
||
} as any);
|
||
return false;
|
||
}, [isLoggedIn, router, currentPath]);
|
||
|
||
const pushIfAuthedElseLogin = useCallback((pathname: string, params?: RedirectParams) => {
|
||
if (isLoggedIn) {
|
||
router.push({ pathname, params } as any);
|
||
return;
|
||
}
|
||
const paramsJson = params ? JSON.stringify(params) : undefined;
|
||
router.push({ pathname: '/auth/login', params: { redirectTo: pathname, ...(paramsJson ? { redirectParams: paramsJson } : {}) } } as any);
|
||
}, [isLoggedIn, router]);
|
||
|
||
const guardHandler = useCallback(
|
||
<T extends any[]>(fn: (...args: T) => any | Promise<any>, options?: EnsureOptions) => {
|
||
return async (...args: T) => {
|
||
const ok = await ensureLoggedIn(options);
|
||
if (!ok) return;
|
||
return fn(...args);
|
||
};
|
||
},
|
||
[ensureLoggedIn]
|
||
);
|
||
|
||
// 退出登录功能
|
||
const handleLogout = useCallback(async () => {
|
||
try {
|
||
// 调用 Redux action 清除本地状态和缓存
|
||
await dispatch(logoutAction()).unwrap();
|
||
|
||
// 跳转到登录页面
|
||
router.push('/auth/login');
|
||
} catch (error) {
|
||
console.error('退出登录失败:', error);
|
||
Alert.alert(t('authGuard.logout.error'), t('authGuard.logout.errorMessage'));
|
||
}
|
||
}, [dispatch, router, t]);
|
||
|
||
// 带确认对话框的退出登录
|
||
const confirmLogout = useCallback(() => {
|
||
Alert.alert(
|
||
t('authGuard.confirmLogout.title'),
|
||
t('authGuard.confirmLogout.message'),
|
||
[
|
||
{
|
||
text: t('authGuard.confirmLogout.cancelButton'),
|
||
style: 'cancel',
|
||
},
|
||
{
|
||
text: t('authGuard.confirmLogout.confirmButton'),
|
||
style: 'default',
|
||
onPress: handleLogout,
|
||
},
|
||
]
|
||
);
|
||
}, [handleLogout, t]);
|
||
|
||
// 注销账号功能
|
||
const handleDeleteAccount = useCallback(async () => {
|
||
try {
|
||
// 调用注销账号API
|
||
await api.delete('/api/users/delete-account');
|
||
|
||
// 清除额外的本地数据
|
||
await AsyncStorage.multiRemove(['@user_personal_info', STORAGE_KEYS.onboardingCompleted]);
|
||
|
||
// 执行退出登录逻辑
|
||
await dispatch(logoutAction()).unwrap();
|
||
|
||
Alert.alert(t('authGuard.deleteAccount.successTitle'), t('authGuard.deleteAccount.successMessage'), [
|
||
{
|
||
text: t('authGuard.deleteAccount.confirmButton'),
|
||
onPress: () => router.push('/auth/login'),
|
||
},
|
||
]);
|
||
} catch (error: any) {
|
||
console.error('注销账号失败:', error);
|
||
const message = error?.message || t('authGuard.deleteAccount.errorMessage');
|
||
Alert.alert(t('authGuard.deleteAccount.errorTitle'), message);
|
||
}
|
||
}, [dispatch, router, t]);
|
||
|
||
// 带确认对话框的注销账号
|
||
const confirmDeleteAccount = useCallback(() => {
|
||
Alert.alert(
|
||
t('authGuard.confirmDeleteAccount.title'),
|
||
t('authGuard.confirmDeleteAccount.message'),
|
||
[
|
||
{
|
||
text: t('authGuard.confirmDeleteAccount.cancelButton'),
|
||
style: 'cancel',
|
||
},
|
||
{
|
||
text: t('authGuard.confirmDeleteAccount.confirmButton'),
|
||
style: 'destructive',
|
||
onPress: handleDeleteAccount,
|
||
},
|
||
],
|
||
{ cancelable: true }
|
||
);
|
||
}, [handleDeleteAccount, t]);
|
||
|
||
return {
|
||
isLoggedIn,
|
||
ensureLoggedIn,
|
||
pushIfAuthedElseLogin,
|
||
guardHandler,
|
||
handleLogout,
|
||
confirmLogout,
|
||
handleDeleteAccount,
|
||
confirmDeleteAccount,
|
||
} as const;
|
||
}
|
||
|