lv 1 ماه پیش
والد
کامیت
026c246c5e

+ 1 - 0
android/app/proguard-rules.pro

@@ -12,3 +12,4 @@
 -keep class com.facebook.react.turbomodule.** { *; }
 
 # Add any project specific keep options here:
+

+ 7 - 0
app.json

@@ -56,6 +56,13 @@
           }
         }
       ],
+      [
+        "expo-file-system",
+        {
+          "supportsOpeningDocumentsInPlace": true,
+          "enableFileSharing": true
+        }
+      ],
       [
         "expo-build-properties",
         {

+ 4 - 0
ios/LoanAssistant/Info.plist

@@ -38,6 +38,8 @@
     <string>12.0</string>
     <key>LSRequiresIPhoneOS</key>
     <true/>
+    <key>LSSupportsOpeningDocumentsInPlace</key>
+    <true/>
     <key>NSAppTransportSecurity</key>
     <dict>
       <key>NSAllowsArbitraryLoads</key>
@@ -55,6 +57,8 @@
     </array>
     <key>RCTNewArchEnabled</key>
     <true/>
+    <key>UIFileSharingEnabled</key>
+    <true/>
     <key>UILaunchStoryboardName</key>
     <string>SplashScreen</string>
     <key>UIRequiredDeviceCapabilities</key>

+ 0 - 6
ios/Podfile.lock

@@ -52,8 +52,6 @@ PODS:
     - SDWebImageWebPCoder (~> 0.14.6)
   - ExpoKeepAwake (55.0.6):
     - ExpoModulesCore
-  - ExpoLinearGradient (55.0.13):
-    - ExpoModulesCore
   - ExpoLinking (55.0.11):
     - ExpoModulesCore
   - ExpoLogBox (55.0.10):
@@ -2332,7 +2330,6 @@ DEPENDENCIES:
   - ExpoGlassEffect (from `../node_modules/expo-glass-effect/ios`)
   - ExpoImage (from `../node_modules/expo-image/ios`)
   - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
-  - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
   - ExpoLinking (from `../node_modules/expo-linking/ios`)
   - "ExpoLogBox (from `../node_modules/@expo/log-box`)"
   - ExpoModulesCore (from `../node_modules/expo-modules-core`)
@@ -2468,8 +2465,6 @@ EXTERNAL SOURCES:
     :path: "../node_modules/expo-image/ios"
   ExpoKeepAwake:
     :path: "../node_modules/expo-keep-awake/ios"
-  ExpoLinearGradient:
-    :path: "../node_modules/expo-linear-gradient/ios"
   ExpoLinking:
     :path: "../node_modules/expo-linking/ios"
   ExpoLogBox:
@@ -2674,7 +2669,6 @@ SPEC CHECKSUMS:
   ExpoGlassEffect: 72bcb9dc634262c59897ff7c53c16e2ff03990d0
   ExpoImage: ef931bba1fd3e907c2262216d17eb21095c9ac2b
   ExpoKeepAwake: a1baf9810a2dee1905aa9bdd336852598f7220e9
-  ExpoLinearGradient: c654e92d726a6d64c588a0988bb22bea331d5e79
   ExpoLinking: b42fd76bdda8662ddf797b17cb4cc793adabfe98
   ExpoLogBox: a3de999775d423ac9cb85d24bd47628e5392761f
   ExpoModulesCore: 964f01116faf01b68467a6c8ea06e14d65fe7b3c

+ 1 - 1
package.json

@@ -27,6 +27,7 @@
     "expo-build-properties": "~55.0.13",
     "expo-constants": "~55.0.12",
     "expo-device": "~55.0.13",
+    "expo-file-system": "~55.0.16",
     "expo-font": "~55.0.6",
     "expo-glass-effect": "~55.0.10",
     "expo-image": "~55.0.8",
@@ -42,7 +43,6 @@
     "react": "19.2.0",
     "react-dom": "19.2.0",
     "react-native": "0.83.4",
-    "react-native-fs-turbo": "^0.5.1",
     "react-native-gesture-handler": "~2.30.1",
     "react-native-mmkv": "^4.3.1",
     "react-native-nitro-modules": "^0.35.3",

+ 7 - 18
pnpm-lock.yaml

@@ -43,6 +43,9 @@ importers:
       expo-device:
         specifier: ~55.0.13
         version: 55.0.13(expo@55.0.12)
+      expo-file-system:
+        specifier: ~55.0.16
+        version: 55.0.16(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
       expo-font:
         specifier: ~55.0.6
         version: 55.0.6(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -88,9 +91,6 @@ importers:
       react-native:
         specifier: 0.83.4
         version: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
-      react-native-fs-turbo:
-        specifier: ^0.5.1
-        version: 0.5.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
       react-native-gesture-handler:
         specifier: ~2.30.1
         version: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -2674,8 +2674,8 @@ packages:
   expo-eas-client@55.0.5:
     resolution: {integrity: sha512-wRagCeSbSnSGVXgP7V+qiGfXzZ9hTVKWvKIOP7lwrX3MIEenNmNlO4D3RVC3aNU2GhmO3ZCZIIEre80KZoUUHA==}
 
-  expo-file-system@55.0.15:
-    resolution: {integrity: sha512-GEo0CzfmRfR7nOjp5p4Tb9XWtgPxDIYRiQws79DpBQsX15UsCdDw7/se3aFO6NyZuGFx/85KsdD7SPGphbE/jw==}
+  expo-file-system@55.0.16:
+    resolution: {integrity: sha512-EetQ/zVFK07Vmz4Yke0fvoES4xVwScTdd0PMoLekuMX7puE4op75pNnEdh1M0AeWzkqLrBoZIaU2ynSrKN5VZg==}
     peerDependencies:
       expo: '*'
       react-native: '*'
@@ -4304,12 +4304,6 @@ packages:
       react-native-svg:
         optional: true
 
-  react-native-fs-turbo@0.5.1:
-    resolution: {integrity: sha512-r5r8S05E/lapqG+BEnCN9hhHiiSns4SH/zZO7HhZze5LF4Z6uH1tLi/nK/zgq2A9hXrEdBBwlrEg4ltbY4hbAQ==}
-    peerDependencies:
-      react: '*'
-      react-native: '*'
-
   react-native-gesture-handler@2.30.1:
     resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==}
     peerDependencies:
@@ -8476,7 +8470,7 @@ snapshots:
 
   expo-eas-client@55.0.5: {}
 
-  expo-file-system@55.0.15(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)):
+  expo-file-system@55.0.16(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)):
     dependencies:
       expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
       react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
@@ -8681,7 +8675,7 @@ snapshots:
       babel-preset-expo: 55.0.16(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@55.0.12)(react-refresh@0.14.2)
       expo-asset: 55.0.13(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
       expo-constants: 55.0.12(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(typescript@5.9.3)
-      expo-file-system: 55.0.15(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
+      expo-file-system: 55.0.16(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
       expo-font: 55.0.6(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
       expo-keep-awake: 55.0.6(expo@55.0.12)(react@19.2.0)
       expo-modules-autolinking: 55.0.15(typescript@5.9.3)
@@ -10279,11 +10273,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  react-native-fs-turbo@0.5.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
-    dependencies:
-      react: 19.2.0
-      react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
-
   react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
     dependencies:
       '@egjs/hammerjs': 2.0.17

+ 1 - 0
src/app/(tabs)/index.tsx

@@ -92,6 +92,7 @@ function getGreeting() {
 }
 
 export default function HomeScreen() {
+  
   return (
     <SafeAreaView className="flex-1 bg-surface" edges={['top']}>
       <View className="h-14 flex-row items-center justify-between border-b border-outline-variant/20 bg-surface-container-lowest px-5">

+ 15 - 3
src/app/(tabs)/profile.tsx

@@ -1,7 +1,10 @@
 import { MenuRow } from '@/components/ui/menu-row';
+import api from '@/utils/api';
 import { signOut, useAuthContext } from '@/utils/auth';
-import { Modal, Toast } from '@ant-design/react-native';
+import { useSWC } from '@/utils/cache';
+import { ActivityIndicator, Modal, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
+import { Image } from 'expo-image';
 import { Pressable, ScrollView, Text, View } from 'react-native';
 import { SafeAreaView } from 'react-native-safe-area-context';
 export default function ProfileScreen() {
@@ -27,6 +30,15 @@ export default function ProfileScreen() {
     ]);
   };
 
+  const {data: userInfo, loading, error}: any = useSWC("me", async ()=> {
+    return await api.post('user/profile')
+  }, {
+    cacheOnly: true,
+    cacheTimeout: 15*60
+  });
+  if (loading) {
+    return <ActivityIndicator />;
+  }
   return (
     <SafeAreaView className="flex-1 bg-surface" edges={['top']}>
       <View className="h-14 flex-row items-center justify-between border-b border-outline-variant/20 bg-surface-container-lowest px-5">
@@ -52,7 +64,7 @@ export default function ProfileScreen() {
           <View className="relative mb-4">
             <View className="h-28 w-28 rounded-full bg-primary-container p-1.5">
               <View className="h-full w-full items-center justify-center rounded-full border-4 border-surface-container-lowest bg-slate-900">
-                <Ionicons name="person" size={48} color="#ffffff" />
+                {userInfo?.avatar&&<Image source={{uri: userInfo.avatar}} />}
               </View>
             </View>
             <View className="absolute bottom-1 right-1 h-8 w-8 items-center justify-center rounded-full border-4 border-surface-container-lowest bg-primary">
@@ -60,7 +72,7 @@ export default function ProfileScreen() {
             </View>
           </View>
           <Text className="mb-2 text-3xl font-extrabold tracking-tight text-on-surface">
-            张建华
+            {userInfo?.nickname}
           </Text>
           <Text className="text-base font-medium text-on-surface-variant">
             高级信贷经理 · 智融金融

+ 80 - 10
src/app/sign-in.tsx

@@ -16,7 +16,10 @@ import {
 } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
+import CaptchaBox, { CaptchaRes } from '@/components/captcha-box';
 import { site } from '@/config.json';
+import { useInterval } from '@/hooks/hooks';
+import api, { ApiError } from '@/utils/api';
 
 function FieldLabel({ children }: { children: React.ReactNode }) {
   return (
@@ -38,14 +41,77 @@ export default function SignInScreen() {
   const { redirectTo } = useLocalSearchParams<{ redirectTo?: string }>();
   const { setToken } = useAuth();
   const scrollView = useRef<ScrollView>(null);
-  const handleSendCode = () => {
+
+  const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
+    const [smsTtl, setSmsTtl] = useState(0);
+  
+    
+    const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
+
+
+    const needsCaptchaRef = useRef(false);
+    const handleCaptcha = (res: CaptchaRes) => {
+      let fun = waitCaptcha.current;
+      waitCaptcha.current = null;
+  
+      setCaptchaVisible(false);
+      fun?.(res);
+    }
+    
+  const handleSendCode = async () => {
     if (mobile.trim().length !== 11) {
       Toast.fail('请先输入 11 位手机号');
       return;
     }
-    Toast.success('验证码已发送');
+    let captcha: CaptchaRes = null!;
+    while (true) {
+      try {
+        if (needsCaptchaRef.current) {
+          setCaptchaVisible(true);
+          captcha = await new Promise<CaptchaRes>((resolve) => {
+            waitCaptcha.current = resolve;
+          });
+          if (!captcha || !captcha.ok) {
+            return;
+          }
+        }
+        const res = await api.post<{
+          needsCaptcha?: boolean;
+          timerout: number;
+        }>("sms/send?__session_id=" + captcha?.sid || '', {
+          mobile: mobile,
+          event: 'login',
+          captcha_code: captcha?.code,
+        });
+
+        if (res.needsCaptcha) {
+          needsCaptchaRef.current = true;
+          setCaptchaVisible(true);
+          captcha = await new Promise<CaptchaRes>((resolve) => {
+            waitCaptcha.current = resolve;
+          });
+          if (!captcha || !captcha.ok) {
+            return;
+          }
+          continue;
+        }
+        setSmsTtl(res.timerout);
+        break;
+
+      } catch (e) {
+        if (ApiError.isApiError(e)) {
+          if (e.code === -99) {
+            Toast.fail("图形验证码无效");
+            return;
+          }
+        }
+        Toast.fail("发送验证码失败");
+        return;
+      }
+    }
   };
 
+
   const handleLogin = async () => {
 
     setFlushAgree(false);
@@ -85,7 +151,7 @@ export default function SignInScreen() {
       if (redirectTo) {
         router.replace(redirectTo as never);
       } else {
-        router.dismiss();
+        router.back();
       }
     } catch (error) {
       console.error('登录失败:', error);
@@ -95,6 +161,12 @@ export default function SignInScreen() {
       Toast.remove(toastKey);
     }
   };
+
+   useInterval(() => {
+      setSmsTtl((pre) => pre - 1)
+    }, smsTtl > 0 ? 1000 : null);
+  
+    
   const insets = useSafeAreaInsets();
   return (
     <View className="flex-1 bg-surface">
@@ -202,15 +274,13 @@ export default function SignInScreen() {
                     />
                     {authMode === 'sms' ? (
                       <Pressable
-                        disabled={loading}
+                        disabled={loading || smsTtl > 0}
                         hitSlop={8}
                         onPress={handleSendCode}
-                        style={({ pressed }) => ({
-                          opacity: loading ? 0.5 : pressed ? 0.72 : 1,
-                        })}
+  
                       >
-                        <Text className="text-lg font-bold leading-6 text-primary">
-                          获取验证码
+                        <Text className={`text-lg font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed' : 'text-primary'}`}>
+                          {smsTtl > 0 ? `${smsTtl}s` : '发送验证码'}
                         </Text>
                       </Pressable>
                     ) : (
@@ -303,7 +373,7 @@ export default function SignInScreen() {
           </View>
         </ScrollView>
       </KeyboardAvoidingView>
-      {/* <CaptchaBox visible onClose={alert} /> */}
+      <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
     </View>
   );
 }

+ 98 - 16
src/app/sign-up.tsx

@@ -1,9 +1,11 @@
-import CaptchaBox from '@/components/captcha-box';
+import CaptchaBox, { CaptchaRes } from '@/components/captcha-box';
 import { site } from '@/config.json';
+import { useInterval } from '@/hooks/hooks';
+import api, { ApiError } from '@/utils/api';
 import { signUp, useAuth } from '@/utils/auth';
 import { Button, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
-import { router, useLocalSearchParams } from 'expo-router';
+import { Link, router, useLocalSearchParams } from 'expo-router';
 import { openBrowserAsync } from 'expo-web-browser';
 import React, { useRef, useState } from 'react';
 import {
@@ -38,18 +40,98 @@ export default function SignUpScreen() {
   const [flushAgree, setFlushAgree] = useState(false);
   const [loading, setLoading] = useState(false);
   const scrollView = useRef<ScrollView>(null);
+  const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
+  const [smsTtl, setSmsTtl] = useState(0);
   const { setToken } = useAuth();
 
   const { signIn, } = useLocalSearchParams<{ signIn: string; redirectTo?: string }>();
-  const handleSendCode = () => {
+  const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
+
+  const needsCaptchaRef = useRef(false);
+  const handleCaptcha = (res: CaptchaRes) => {
+
+    let fun = waitCaptcha.current;
+    waitCaptcha.current = null;
+alert(1)
+    setCaptchaVisible(false);
+    fun?.(res);
+  }
+  const handleSendCode = async () => {
     if (mobile.trim().length !== 11) {
       Toast.fail('请先输入 11 位手机号');
       return;
     }
+    let captcha: CaptchaRes = null!;
+    while (true) {
+      if (needsCaptchaRef.current) {
+        setCaptchaVisible(true);
+        captcha = await new Promise<CaptchaRes>((resolve) => {
+          waitCaptcha.current = resolve;
+        });
+        if (!captcha || !captcha.ok) {
+          return;
+        }
+      }
+      try {
+        const res = await api.post<{
+          needsCaptcha?: boolean;
+          timerout: number;
+        }>("sms/send?__session_id=" + captcha?.sid || '', {
+          mobile: mobile,
+          event: 'register',
+          captcha_code: captcha?.code,
+        });
+
+        if (res.needsCaptcha) {
+          needsCaptchaRef.current = true;
+          setCaptchaVisible(true);
+          captcha = await new Promise<CaptchaRes>((resolve) => {
+            waitCaptcha.current = resolve;
+          });
+
+        alert(JSON.stringify(captcha));
+          if (!captcha || !captcha.ok) {
+            return;
+          }
+          continue;
+        }
+        setSmsTtl(res.timerout);
+        break;
 
-    Toast.success('验证码已发送');
+      } catch (e) {
+        if (ApiError.isApiError(e)) {
+          if (e.code === -1) {
+            Toast.fail("该手机已被注册");
+            return;
+          }
+          if (e.code === -99) {
+            Toast.fail("图形验证码无效");
+            return;
+          }
+        }
+        Toast.fail("发送验证码失败");
+        return;
+      }
+    }
   };
 
+  // useEffect(() => {
+  //   let isMount = true;
+  //   setTimeout(()=> {
+  //     if (!isMount) {
+  //       return;
+  //     }
+  //     setSmsTtl((pre)=> pre --);
+  //   })
+  //   return ()=> {
+  //     isMount = false;
+  //   }
+  // }, [])
+
+  useInterval(() => {
+    setSmsTtl((pre) => pre - 1)
+  }, smsTtl > 0 ? 1000 : null);
+
   const handleRegister = async () => {
     setFlushAgree(false);
 
@@ -188,15 +270,13 @@ export default function SignUpScreen() {
                       className="flex-1 p-0 text-xl font-medium text-on-surface"
                     />
                     <Pressable
-                      disabled={loading}
+                      disabled={loading || smsTtl > 0}
                       hitSlop={8}
                       onPress={handleSendCode}
-                      style={({ pressed }) => ({
-                        opacity: loading ? 0.5 : pressed ? 0.72 : 1,
-                      })}
+
                     >
-                      <Text className="text-lg font-bold leading-6 text-primary">
-                        获取验证码
+                      <Text className={`text-lg font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed' : 'text-primary'}`}>
+                        {smsTtl > 0 ? `${smsTtl}s` : '发送验证码'}
                       </Text>
                     </Pressable>
                   </View>
@@ -294,10 +374,7 @@ export default function SignUpScreen() {
             </View>
 
             <View className="pt-4">
-              <View
-                className={`flex-row items-start gap-3 rounded-md border-2 border-transparent px-2 transition delay-300 ${flushAgree ? 'border-primary/50' : ''
-                  }`}
-              >
+              <View className={`flex-row items-start gap-3 px-2 rounded-md border-2 transition-colors duration-700 ${flushAgree ? ' border-primary/50' : 'border-transparent'}`}>
                 <Pressable
                   onPress={() => setAgreed((value) => !value)}
                   hitSlop={8}
@@ -316,7 +393,12 @@ export default function SignUpScreen() {
                 </Pressable>
                 <Text className="flex-1 text-sm leading-6 text-on-surface-variant">
                   注册即代表您已阅读并同意
-                  <Text className="font-semibold text-primary cursor-pointer" onPress={() => openBrowserAsync(`${site}`)}>《用户服务协议》</Text>
+                  <Link href={{
+                    pathname: '/web',
+                    params: { uri: site }
+                  }}>
+                    <Text className="font-semibold text-primary">《用户服务协议》</Text>
+                  </Link>
                   <Text className="font-semibold text-primary cursor-pointer" onPress={() => openBrowserAsync(`${site}`)}>《隐私政策》</Text>
                   ,以及授权该应用获取您的公开信息。
@@ -326,7 +408,7 @@ export default function SignUpScreen() {
           </View>
         </ScrollView>
       </KeyboardAvoidingView>
-      <CaptchaBox visible onClose={alert} />
+      <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
     </View>
   );
 }

+ 64 - 22
src/components/captcha-box.tsx

@@ -1,51 +1,94 @@
-import { site } from '@/config.json';
+import { api as apiCfg } from '@/config.json';
+import { Colors } from '@/constants/theme';
 import api from '@/utils/api';
-import { Button, Icon, Modal } from "@ant-design/react-native";
+import { Icon, Modal } from "@ant-design/react-native";
 import { Image } from 'expo-image';
 import { useCallback, useEffect, useRef, useState } from "react";
 import { Pressable, TextInput, View } from 'react-native';
 
-function bin2uri(arrayBuffer: ArrayBuffer) {
+function bin2uri(data: Blob) {
     return new Promise<string>((resolve, reject) => {
-        const blob = new Blob([arrayBuffer], { type: 'image/png' });
+      
         const reader = new FileReader();
         reader.onloadend = () => {
             resolve(reader.result as string);
         };
         reader.onerror = reject;
-        reader.readAsDataURL(blob);
+        reader.readAsDataURL(data);
     });
 }
+export 
+interface CaptchaRes {
+    ok: boolean;
+    code?: string;
+    sid?: string;
+}
 
-
-export default function CaptchaBox({ onClose, visible }: { onClose: (code?: string) => void; visible: boolean }) {
+export default function CaptchaBox({ onClose, visible }: { onClose: (res: CaptchaRes) => void; visible: boolean }) {
+    const [v, setVisible] = useState<boolean>(false);
     const [uri, setUri] = useState<string>(null!);
     const [value, setValue] = useState("");
-    const idRef = useRef<string>(null);
-    const refresh = useCallback(async () => {
+    const idRef = useRef<string|undefined>(null);
+    const resRef = useRef<CaptchaRes>({ok: false});
+
+      const refresh = useCallback(async () => {
         try {
-            const res = await api.rawRequest<ArrayBuffer>({
-                url: `${site}index.php?s=captcha&?t=${Date.now()}`,
+            const res = await api.rawRequest<Blob>({
+                url: `${apiCfg.url}common/captcha&?t=${Date.now()}`,
                 timeout: 15000,
-                responseType: 'arraybuffer'
+                responseType: 'blob'
             });
-            idRef.current = res.headers['x-captcha-id'];
+            idRef.current = res.headers['x-session_id'] as string;
             setUri(await bin2uri(res.data));
 
         } catch (e) {
             console.error(e);
         }
     }, []);
+    // alert(v)
+    useEffect(()=> {
+        setVisible(visible);
+        setUri(null!);
+        if (visible) {
+            refresh();
+            setValue('');
+        }
+    }, [refresh, visible]);
+  
 
-    useEffect(() => { refresh() }, [])
 
-    return <Modal visible={visible} transparent>
-        <View className='w-full p-4 items-center justify-center  h-32'>
-            <View className="w-full h-24">
-                {uri ? <Image className="w-full h-12" source={{ uri }} /> : <View className="flex-1 h-12 bg-gray-200" />}
-                <Pressable className='w-8 h-8 m-2 justify-center items-center' onPress={refresh}><Icon name="reload" /></Pressable>
+    return <Modal visible={v} transparent style={{width: 208}} onClose={()=>onClose(resRef.current)}
+    
+    footer={[
+        {
+            text: '取消',
+            style: {color: Colors.textSecondary},
+            onPress: ()=> {
+                resRef.current!.ok = false;
+                setVisible(false);
+            }
+        },
+        {
+            text: '确认',
+            onPress: ()=> {
+                // if (value.length !== 6) {
+                //     Toast.fail("请输入 6 位字符");
+                //     return;
+                // }
+                resRef.current!.ok = true;
+                resRef.current.code = value;
+                resRef.current.sid = idRef.current!;
+                setVisible(false);   
+            }
+        }
+    ]}
+    >
+        <View className='w-48 items-center justify-center'>
+            <View className="w-full">
+                {uri ? <Image style={{width: 175, height: 44}} className="w-full h-12 bg-gray-500" source={{ uri }} /> : <View className="w-full h-12 bg-gray-200" />}
+                <Pressable className='w-8 h-8 p-1 justify-center items-center' onPress={refresh}><Icon name="reload" /></Pressable>
             </View>
-            <View className="h-10 flex-row items-center rounded-2xl bg-surface-container-low p-0">
+            <View className="h-8 flex-row items-center rounded-2xl bg-surface-container-low p-0">
 
                 <TextInput
                     value={value}
@@ -54,10 +97,9 @@ export default function CaptchaBox({ onClose, visible }: { onClose: (code?: stri
                     maxLength={6}
                     placeholder="请输入图中字符"
                     placeholderTextColor="#9ca3af"
-                    className="flex-1 text-center  p-0 text-xl font-medium text-on-surface"
+                    className="flex-1 h-8 text-center p-0 text-sm font-medium text-on-surface"
                 />
             </View>
         </View>
-        <Button>确定</Button>
     </Modal>
 }

+ 3 - 0
src/utils/cache.ts

@@ -46,6 +46,9 @@ export function useSWC<T>(key: string, action: () => Promise<T>, options?: UseSW
     useEffect(() => {
         let isMounted = true;
         if (dataRef.current && optionsRef.current?.cacheOnly) {
+
+            setLoading(false);
+            setError(null);
             return;
         }
         setLoading(true);

+ 4 - 3
src/utils/storage.ts

@@ -1,5 +1,6 @@
 // import RNDeviceInfo from 'react-native-device-info';
-import RNFS from 'react-native-fs-turbo';
+import * as fs from 'expo-file-system/legacy';
+
 import { createMMKV, MMKV } from 'react-native-mmkv';
 
 let globalStorage: MMKV | null = null;
@@ -17,7 +18,7 @@ export function getCaches() {
     if (!caches) {
         caches = createMMKV({
             id: `caches`,
-            path: `${RNFS.CachesDirectoryPath}`,
+            path: fs.cacheDirectory!,
         });
     }
     return caches;
@@ -28,7 +29,7 @@ export function getApiCache() {
     if (!apiCache) {
         apiCache = createMMKV({
             id: `api_cache`,
-            path: `${RNFS.CachesDirectoryPath}`,
+            path: fs.cacheDirectory!,
         });
     }
     return apiCache;