1- import React , { useEffect , useState , useRef } from "react" ;
1+ import React , { useRef } from "react" ;
22import {
33 Modal ,
44 View ,
55 Text ,
66 StyleSheet ,
77 TouchableOpacity ,
8- ActivityIndicator ,
98 Animated ,
10- Alert ,
11- Linking ,
129} from "react-native" ;
13- import * as FileSystem from "expo-file-system/legacy" ;
14- import * as MediaLibrary from "expo-media-library" ;
15- import QRCode from "react-native-qrcode-svg" ;
1610import { Ionicons } from "@expo/vector-icons" ;
17- import { generateQRCode , pollQRCode } from "../services/api" ;
11+ import { pollQRCode } from "../services/api" ;
1812import { useAuthStore } from "../store/authStore" ;
1913import { useTheme } from "../utils/theme" ;
2014
@@ -24,21 +18,12 @@ interface Props {
2418}
2519
2620export function LoginModal ( { visible, onClose } : Props ) {
27- const [ qrData , setQrData ] = useState < string | null > ( null ) ;
28- const [ qrKey , setQrKey ] = useState < string | null > ( null ) ;
29- const [ saving , setSaving ] = useState ( false ) ;
30- const qrRef = useRef < any > ( null ) ;
31- const [ status , setStatus ] = useState <
32- "loading" | "waiting" | "scanned" | "done" | "error"
33- > ( "loading" ) ;
34- const pollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
3521 const login = useAuthStore ( ( s ) => s . login ) ;
3622 const theme = useTheme ( ) ;
3723
38- // sheet 滑入动画
3924 const slideY = useRef ( new Animated . Value ( 300 ) ) . current ;
4025
41- useEffect ( ( ) => {
26+ React . useEffect ( ( ) => {
4227 if ( visible ) {
4328 Animated . spring ( slideY , {
4429 toValue : 0 ,
@@ -50,85 +35,11 @@ export function LoginModal({ visible, onClose }: Props) {
5035 }
5136 } , [ visible ] ) ;
5237
53- useEffect ( ( ) => {
54- if ( ! visible ) return ;
55- setStatus ( "loading" ) ;
56- setQrData ( null ) ;
57- setQrKey ( null ) ;
58- generateQRCode ( )
59- . then ( ( data ) => {
60- setQrData ( data . url ) ;
61- setQrKey ( data . qrcode_key ) ;
62- setStatus ( "waiting" ) ;
63- } )
64- . catch ( ( ) => setStatus ( "error" ) ) ;
65-
66- return ( ) => {
67- if ( pollRef . current ) clearInterval ( pollRef . current ) ;
68- } ;
69- } , [ visible ] ) ;
70-
71- useEffect ( ( ) => {
72- if ( ! qrKey || status !== "waiting" ) return ;
73- let cancelled = false ;
74- pollRef . current = setInterval ( async ( ) => {
75- if ( cancelled ) return ;
76- try {
77- const result = await pollQRCode ( qrKey ) ;
78- if ( cancelled ) return ;
79- if ( result . code === 86038 ) {
80- setStatus ( "error" ) ;
81- clearInterval ( pollRef . current ! ) ;
82- }
83- if ( result . code === 86090 ) setStatus ( "scanned" ) ;
84- if ( result . code === 0 && result . cookie ) {
85- clearInterval ( pollRef . current ! ) ;
86- try {
87- await login ( result . cookie , "" , "" ) ;
88- } catch {
89- if ( ! cancelled ) setStatus ( "error" ) ;
90- return ;
91- }
92- if ( ! cancelled ) onClose ( ) ;
93- }
94- } catch {
95- // Network error during poll — ignore, will retry next interval
96- }
97- } , 2000 ) ;
98- return ( ) => {
99- cancelled = true ;
100- if ( pollRef . current ) clearInterval ( pollRef . current ) ;
101- } ;
102- } , [ qrKey , status ] ) ;
103-
104- async function handleSaveQR ( ) {
105- if ( ! qrRef . current ) return ;
106- setSaving ( true ) ;
107- try {
108- const { status : perm } = await MediaLibrary . requestPermissionsAsync ( ) ;
109- if ( perm !== "granted" ) {
110- Alert . alert ( "提示" , "需要相册权限才能保存图片" ) ;
111- return ;
112- }
113- qrRef . current . toDataURL ( async ( base64 : string ) => {
114- try {
115- const dest = `${ FileSystem . cacheDirectory } qrcode_temp.png` ;
116- await FileSystem . writeAsStringAsync ( dest , base64 , {
117- encoding : FileSystem . EncodingType . Base64 ,
118- } ) ;
119- await MediaLibrary . saveToLibraryAsync ( dest ) ;
120- Alert . alert ( "已保存" , "二维码已存入相册,请扫码登录" , [
121- { text : "关闭" , style : "cancel" } ,
122- ] ) ;
123- } catch {
124- Alert . alert ( "失败" , "保存失败,请重试" ) ;
125- } finally {
126- setSaving ( false ) ;
127- }
128- } ) ;
129- } catch {
130- Alert . alert ( "失败" , "保存失败,请重试" ) ;
131- setSaving ( false ) ;
38+ async function handleLogin ( ) {
39+ const result = await pollQRCode ( 'mock-key' ) ;
40+ if ( result . code === 0 && result . cookie ) {
41+ await login ( result . cookie , '' , '' ) ;
42+ onClose ( ) ;
13243 }
13344 }
13445
@@ -139,54 +50,32 @@ export function LoginModal({ visible, onClose }: Props) {
13950 animationType = "none"
14051 onRequestClose = { onClose }
14152 >
142- { /* 遮罩固定不动 */ }
14353 < View style = { styles . overlay } pointerEvents = "box-none" />
14454
145- { /* sheet 独立滑入 */ }
14655 < Animated . View
14756 style = { [ styles . sheetWrapper , { transform : [ { translateY : slideY } ] } ] }
14857 >
14958 < View style = { [ styles . sheet , { backgroundColor : theme . sheetBg } ] } >
150- < Text style = { [ styles . title , { color : theme . modalText } ] } > 扫码登录</ Text >
151- { status === "loading" && (
152- < ActivityIndicator
153- size = "large"
154- color = "#00AEEC"
155- style = { styles . loader }
156- />
157- ) }
158- { ( status === "waiting" || status === "scanned" ) && qrData && (
159- < >
160- < View style = { styles . qrWrapper } >
161- < QRCode
162- value = { qrData }
163- size = { 200 }
164- getRef = { ( ref ) => { qrRef . current = ref ; } }
165- />
166- < TouchableOpacity
167- style = { styles . saveBtn }
168- onPress = { handleSaveQR }
169- disabled = { saving }
170- >
171- { saving ? (
172- < ActivityIndicator size = "small" color = "#fff" />
173- ) : (
174- < Ionicons name = "download-outline" size = { 16 } color = "#fff" />
175- ) }
176- </ TouchableOpacity >
177- </ View >
178- < Text style = { [ styles . hint , { color : theme . modalTextSub } ] } >
179- { status === "scanned"
180- ? "扫描成功,请在手机确认"
181- : "使用手机 APP 扫一扫" }
182- </ Text >
183- </ >
184- ) }
185- { status === "error" && (
186- < Text style = { [ styles . hint , { color : theme . modalTextSub } ] } > 二维码已过期,请关闭重试</ Text >
187- ) }
59+ < Text style = { [ styles . title , { color : theme . modalText } ] } > 登录</ Text >
60+
61+ < View style = { styles . iconWrap } >
62+ < Ionicons name = "person-circle-outline" size = { 72 } color = "#00AEEC" />
63+ </ View >
64+
65+ < Text style = { [ styles . hint , { color : theme . modalTextSub } ] } >
66+ 演示模式:点击下方按钮一键登录
67+ </ Text >
68+
69+ < TouchableOpacity
70+ style = { styles . loginBtn }
71+ onPress = { handleLogin }
72+ activeOpacity = { 0.85 }
73+ >
74+ < Text style = { styles . loginBtnText } > 一键登录</ Text >
75+ </ TouchableOpacity >
76+
18877 < TouchableOpacity style = { styles . closeBtn } onPress = { onClose } >
189- < Text style = { styles . closeTxt } > 关闭</ Text >
78+ < Text style = { [ styles . closeTxt , { color : "#00AEEC" } ] } > 关闭</ Text >
19079 </ TouchableOpacity >
19180 </ View >
19281 </ Animated . View >
@@ -206,27 +95,22 @@ const styles = StyleSheet.create({
20695 right : 0 ,
20796 } ,
20897 sheet : {
209- backgroundColor : "#fff" ,
21098 borderTopLeftRadius : 16 ,
21199 borderTopRightRadius : 16 ,
212100 padding : 24 ,
213101 alignItems : "center" ,
214102 } ,
215103 title : { fontSize : 18 , fontWeight : "600" , marginBottom : 20 } ,
216- loader : { marginVertical : 40 } ,
217- qrWrapper : { width : 200 , height : 200 , marginBottom : 12 } ,
218- saveBtn : {
219- position : "absolute" ,
220- bottom : 6 ,
221- right : 6 ,
222- backgroundColor : "rgba(0,0,0,0.45)" ,
223- borderRadius : 14 ,
224- width : 28 ,
225- height : 28 ,
226- alignItems : "center" ,
227- justifyContent : "center" ,
104+ iconWrap : { marginBottom : 16 } ,
105+ hint : { fontSize : 13 , marginBottom : 24 , textAlign : "center" } ,
106+ loginBtn : {
107+ backgroundColor : "#00AEEC" ,
108+ borderRadius : 24 ,
109+ paddingVertical : 12 ,
110+ paddingHorizontal : 48 ,
111+ marginBottom : 8 ,
228112 } ,
229- hint : { fontSize : 13 , color : "#666 " , marginBottom : 20 } ,
113+ loginBtnText : { color : "#fff " , fontSize : 16 , fontWeight : "600" } ,
230114 closeBtn : { padding : 12 } ,
231- closeTxt : { fontSize : 14 , color : "#00AEEC" } ,
115+ closeTxt : { fontSize : 14 } ,
232116} ) ;
0 commit comments