Files
app_03_pregnancy/frontend/src/app/pages/LoginPage.tsx

131 lines
5.3 KiB
TypeScript

import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { motion } from "motion/react";
import { Heart } from "lucide-react";
import { useUser } from "../context/UserContext";
import { devLogin, fetchApi, getToken, clearToken, decodeToken } from "../services/api";
import { debugLog, clearDebugLogs } from "../services/debug";
export function LoginPage() {
const navigate = useNavigate();
const { setTokenAndUser, fetchProfile } = useUser();
const [error, setError] = useState("");
const [debugInfo, setDebugInfo] = useState<string[]>([]);
const addDebug = (msg: string) => {
console.log(msg);
setDebugInfo(prev => [...prev.slice(-20), msg]);
};
useEffect(() => {
clearDebugLogs();
// 收集调试信息(直接在页面显示,排查缓存问题)
const infos: string[] = [];
const urlOpenid = new URLSearchParams(window.location.search).get('openid');
const cachedToken = getToken();
const ts = new Date().toISOString().slice(11, 19);
infos.push(`[${ts}] URL: ${window.location.href.substring(0, 100)}`);
infos.push(`[${ts}] urlOpenid=${urlOpenid || '(空)'}`);
let cacheOpenid = '';
if (cachedToken) {
try {
const p = decodeToken(cachedToken);
cacheOpenid = p.openid;
infos.push(`[${ts}] cache openid=${p.openid}, uid=${p.uid}, exp=${p.exp ? new Date(p.exp * 1000).toLocaleString() : '?'}`);
} catch (e) {
infos.push(`[${ts}] 缓存 token 解码失败: ${e}`);
}
} else {
infos.push(`[${ts}] 无缓存 token`);
}
addDebug(infos.join('\n'));
// 身份一致性检查
if (urlOpenid && cachedToken) {
try {
const payload = decodeToken(cachedToken);
if (payload.openid !== urlOpenid) {
addDebug(`[auth] openid 不一致: URL=${urlOpenid}, cache=${payload.openid},清除缓存`);
clearToken();
}
} catch (e) {
addDebug(`[auth] token 解码失败,清除: ${e}`);
clearToken();
}
}
const params = new URLSearchParams(window.location.search);
debugLog(`URL: ${window.location.href.substring(0, 100)}`);
debugLog(`search: ${window.location.search}`);
const openidParam = params.get('openid');
if (openidParam && openidParam.trim()) {
addDebug(`开始登录: openid=${openidParam}`);
fetchApi("/auth/openid-login", {
method: "POST",
body: JSON.stringify({ openid: openidParam }),
}).then((res: any) => {
addDebug(`openid-login OK: id=${res.user_id}, phone=${res.mobile}`);
setTokenAndUser(res.token, { id: res.user_id, openid: res.openid, phone: res.mobile });
fetchProfile().then((dto) => {
if (dto) addDebug(`profile: name=${dto.name}, phone=${dto.phone}, onboarded=${dto.is_onboarded}`);
navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true });
}).catch((e: any) => {
debugLog(`fetchProfile error: ${e?.message}`);
setError("登录已失效,请在小程序重新进入");
});
}).catch((e: any) => {
addDebug(`openid-login error: ${e?.message}`);
setError("登录失败: " + (e?.message || ""));
});
return;
}
addDebug(`no openid, fallback to dev-login`);
devLogin().then((res) => {
debugLog(`dev-login: openid=${res.openid}, phone=${res.mobile}`);
setTokenAndUser(res.token, { id: res.user_id, openid: res.openid, phone: res.mobile });
fetchProfile().then((dto) => {
if (dto) debugLog(`fetchProfile: name=${dto.name}, phone=${dto.phone}`);
navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true });
});
}).catch((err) => {
debugLog(`dev-login error: ${err?.message}`);
setError(err?.message || "登录失败,请刷新重试");
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="min-h-dvh bg-morandi-50 flex items-center justify-center p-4 relative">
{/* 调试浮层:点击可关闭 */}
{debugInfo.length > 0 && (
<div
className="absolute top-0 left-0 right-0 z-50 bg-black/80 text-green-400 text-xs p-3 overflow-auto max-h-[50%] cursor-pointer font-mono"
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', lineHeight: 1.5 }}
onClick={() => setDebugInfo([])}
>
<div style={{borderBottom:'1px solid #0f0',paddingBottom:4,marginBottom:4,fontWeight:'bold',fontSize:14}}>H5应用调试</div>
{debugInfo.map((msg, i) => (
<div key={i}>{msg}</div>
))}
</div>
)}
<div className="text-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 1.5, ease: "linear" }}
className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-morandi-300 to-morandi-400 rounded-full mb-4"
>
<Heart className="w-8 h-8 text-white fill-white" />
</motion.div>
{error ? (
<p className="text-sm text-red-500">{error}</p>
) : (
<p className="text-sm text-gray-500">加载中...</p>
)}
</div>
</div>
);
}