Files
JKVideo/components/LoginModal.tsx
Developer 1e0931b5d1 fix: 登录后头像不更新 — 将 getUserInfo 合并进 login()
登录流程之前分两步:login() 只设 isLoggedIn,getUserInfo/setProfile
由 LoginModal 调用。网络抖动或 B站 session 传播延迟时 getUserInfo
抛错被静默吞掉,face 不更新,直到下次启动 restore() 才修复。

修复:login() 内部完成 getUserInfo + 持久化,LoginModal 只需
await login() 后关闭弹窗,不再重复获取头像。
2026-03-26 01:16:20 +08:00

237 lines
6.9 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 React, { useEffect, useState, useRef } from "react";
import {
Modal,
View,
Text,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
Animated,
Alert,
Linking,
} from "react-native";
import * as FileSystem from "expo-file-system/legacy";
import * as MediaLibrary from "expo-media-library";
import QRCode from "react-native-qrcode-svg";
import { Ionicons } from "@expo/vector-icons";
import { generateQRCode, pollQRCode } from "../services/bilibili";
import { useAuthStore } from "../store/authStore";
import { useTheme } from "../utils/theme";
interface Props {
visible: boolean;
onClose: () => void;
}
export function LoginModal({ visible, onClose }: Props) {
const [qrData, setQrData] = useState<string | null>(null);
const [qrKey, setQrKey] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const qrRef = useRef<any>(null);
const [status, setStatus] = useState<
"loading" | "waiting" | "scanned" | "done" | "error"
>("loading");
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const login = useAuthStore((s) => s.login);
const theme = useTheme();
// sheet 滑入动画
const slideY = useRef(new Animated.Value(300)).current;
useEffect(() => {
if (visible) {
Animated.spring(slideY, {
toValue: 0,
useNativeDriver: true,
bounciness: 4,
}).start();
} else {
slideY.setValue(300);
}
}, [visible]);
useEffect(() => {
if (!visible) return;
setStatus("loading");
setQrData(null);
setQrKey(null);
generateQRCode()
.then((data) => {
setQrData(data.url);
setQrKey(data.qrcode_key);
setStatus("waiting");
})
.catch(() => setStatus("error"));
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, [visible]);
useEffect(() => {
if (!qrKey || status !== "waiting") return;
let cancelled = false;
pollRef.current = setInterval(async () => {
if (cancelled) return;
try {
const result = await pollQRCode(qrKey);
if (cancelled) return;
if (result.code === 86038) {
setStatus("error");
clearInterval(pollRef.current!);
}
if (result.code === 86090) setStatus("scanned");
if (result.code === 0 && result.cookie) {
clearInterval(pollRef.current!);
try {
await login(result.cookie, "", "");
} catch {
if (!cancelled) setStatus("error");
return;
}
if (!cancelled) onClose();
}
} catch {
// Network error during poll — ignore, will retry next interval
}
}, 2000);
return () => {
cancelled = true;
if (pollRef.current) clearInterval(pollRef.current);
};
}, [qrKey, status]);
async function handleSaveQR() {
if (!qrRef.current) return;
setSaving(true);
try {
const { status: perm } = await MediaLibrary.requestPermissionsAsync();
if (perm !== "granted") {
Alert.alert("提示", "需要相册权限才能保存图片");
return;
}
qrRef.current.toDataURL(async (base64: string) => {
try {
const dest = `${FileSystem.cacheDirectory}bilibili_qr.png`;
await FileSystem.writeAsStringAsync(dest, base64, {
encoding: FileSystem.EncodingType.Base64,
});
await MediaLibrary.saveToLibraryAsync(dest);
Alert.alert("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [
{ text: "关闭", style: "cancel" },
{
text: "打开哔哩哔哩",
onPress: () => Linking.openURL("bilibili://"),
},
]);
} catch {
Alert.alert("失败", "保存失败,请重试");
} finally {
setSaving(false);
}
});
} catch {
Alert.alert("失败", "保存失败,请重试");
setSaving(false);
}
}
return (
<Modal
visible={visible}
transparent
animationType="none"
onRequestClose={onClose}
>
{/* 遮罩固定不动 */}
<View style={styles.overlay} pointerEvents="box-none" />
{/* sheet 独立滑入 */}
<Animated.View
style={[styles.sheetWrapper, { transform: [{ translateY: slideY }] }]}
>
<View style={[styles.sheet, { backgroundColor: theme.sheetBg }]}>
<Text style={[styles.title, { color: theme.modalText }]}></Text>
{status === "loading" && (
<ActivityIndicator
size="large"
color="#00AEEC"
style={styles.loader}
/>
)}
{(status === "waiting" || status === "scanned") && qrData && (
<>
<View style={styles.qrWrapper}>
<QRCode
value={qrData}
size={200}
getRef={(ref) => { qrRef.current = ref; }}
/>
<TouchableOpacity
style={styles.saveBtn}
onPress={handleSaveQR}
disabled={saving}
>
{saving ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Ionicons name="download-outline" size={16} color="#fff" />
)}
</TouchableOpacity>
</View>
<Text style={[styles.hint, { color: theme.modalTextSub }]}>
{status === "scanned"
? "扫描成功,请在手机确认"
: "使用 B站 APP 扫一扫"}
</Text>
</>
)}
{status === "error" && (
<Text style={[styles.hint, { color: theme.modalTextSub }]}></Text>
)}
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
<Text style={styles.closeTxt}></Text>
</TouchableOpacity>
</View>
</Animated.View>
</Modal>
);
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0,0,0,0.5)",
},
sheetWrapper: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
},
sheet: {
backgroundColor: "#fff",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
padding: 24,
alignItems: "center",
},
title: { fontSize: 18, fontWeight: "600", marginBottom: 20 },
loader: { marginVertical: 40 },
qrWrapper: { width: 200, height: 200, marginBottom: 12 },
saveBtn: {
position: "absolute",
bottom: 6,
right: 6,
backgroundColor: "rgba(0,0,0,0.45)",
borderRadius: 14,
width: 28,
height: 28,
alignItems: "center",
justifyContent: "center",
},
hint: { fontSize: 13, color: "#666", marginBottom: 20 },
closeBtn: { padding: 12 },
closeTxt: { fontSize: 14, color: "#00AEEC" },
});