lv 1 lună în urmă
părinte
comite
197ac50881

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


BIN
assets/images/android-icon-foreground.png


+ 5 - 5
src/app/(tabs)/analytics.tsx

@@ -1,3 +1,7 @@
+import { SectionHeader } from '@/components/ui/section-header';
+import type { ListResponse } from '@/utils/api';
+import api from '@/utils/api';
+import { useSWC } from '@/utils/cache';
 import { ActivityIndicator } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import { BlurView } from 'expo-blur';
@@ -6,11 +10,7 @@ import React, { useCallback } from 'react';
 import { Pressable, Text, View } from 'react-native';
 import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { SectionHeader } from '@/components/ui/section-header';
-import api from '@/utils/api';
-import { useSWC } from '@/utils/cache';
 import { UploadComponent } from '../../components/upload';
-import type { ListResponse } from '@/utils/api';
 
 
 type AnalysisRecord = {
@@ -52,7 +52,7 @@ export default function AnalyticsScreen() {
       className="flex-1"
       contentInset={{ top: insets.top, bottom: insets.bottom }}
       automaticallyAdjustContentInsets
-      contentContainerClassName="px-5 pt-3 pb-24"
+      contentContainerStyle={{paddingTop: insets.top + 12, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
       onScroll={scrollHandler}
       scrollEventThrottle={16}
     >

+ 9 - 8
src/app/(tabs)/customer.tsx

@@ -1,3 +1,7 @@
+import { StatusBadge } from '@/components/ui/status-badge';
+import type { ListResponse } from '@/utils/api';
+import api from '@/utils/api';
+import { getApiCache } from '@/utils/storage';
 import { ActivityIndicator, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import { BlurView } from 'expo-blur';
@@ -6,10 +10,6 @@ import React, { useCallback, useRef, useState } from 'react';
 import { Pressable, Text, TextInput, View } from 'react-native';
 import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { StatusBadge } from '@/components/ui/status-badge';
-import api from '@/utils/api';
-import { getApiCache } from '@/utils/storage';
-import type { ListResponse } from '@/utils/api';
 
 type CustomerLoanStatus = 'matched' | 'unmatch' | 'pending' | 'completed' | undefined;
 const CustomerLoanStatusText: Record<NonNullable<CustomerLoanStatus>, string> = {
@@ -229,9 +229,10 @@ export default function CustomerScreens() {
     <>
       <Animated.FlatList
         className="flex-1"
-        contentInset={{ top: insets.top, bottom: insets.bottom }}
-        automaticallyAdjustContentInsets
-        contentContainerClassName="px-5 pt-3 pb-24 gap-3"
+       contentInset={{ top: insets.top, bottom: insets.bottom }}
+      automaticallyAdjustContentInsets
+      contentContainerStyle={{paddingTop: insets.top + 12, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
+      
         data={list}
         keyExtractor={keyExtractor}
         renderItem={renderCustomer}
@@ -251,7 +252,7 @@ export default function CustomerScreens() {
         header: () => (
           <Animated.View style={headerStyle} className="border-b border-outline-variant/60 android:bg-surface-container-lowest/90">
             <BlurView tint="light" className="bg-background">
-              <Text style={{ marginTop: insets.top }} className="pl-10 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+              <Text style={{ marginTop: insets.top }} className="pl-5 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
                 客户
               </Text>
             </BlurView>

+ 7 - 6
src/app/(tabs)/reports.tsx

@@ -1,3 +1,4 @@
+import { StatusBadge } from '@/components/ui/status-badge';
 import { Ionicons } from '@expo/vector-icons';
 import { BlurView } from 'expo-blur';
 import { Stack } from 'expo-router';
@@ -5,7 +6,6 @@ import React, { useState } from 'react';
 import { Pressable, Text, View } from 'react-native';
 import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { StatusBadge } from '@/components/ui/status-badge';
 
 type ReportTab = '全部' | '征信报告' | '匹配结果';
 
@@ -114,9 +114,10 @@ export default function ReportsScreen() {
 
     <Animated.ScrollView
       className="flex-1"
-      contentInset={{ top: insets.top, bottom: insets.bottom }}
+    contentInset={{ top: insets.top, bottom: insets.bottom }}
       automaticallyAdjustContentInsets
-      contentContainerClassName="px-5 pt-3 pb-24"
+      contentContainerStyle={{paddingTop: insets.top, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
+
       showsVerticalScrollIndicator={false}
       onScroll={scrollHandler}
       scrollEventThrottle={16}
@@ -126,15 +127,15 @@ export default function ReportsScreen() {
         headerTransparent: true,
         header: () => (
           <Animated.View style={headerStyle} className="border-b border-outline-variant/60 android:bg-surface-container-lowest/90">
-            <BlurView tint="light" className="bg-background">
-              <Text style={{ marginTop: insets.top }} className="pl-10 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+            <BlurView tint="light" className="bg-background justify-center">
+              <Text style={{ marginTop: insets.top + 4 }} className="pl-5 align-middle h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
                 报表
               </Text>
             </BlurView>
           </Animated.View>
         ),
       }} />
-      <Text className="h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+      <Text className="bg-primary h-[44px] pt-1 text-3xl font-extrabold tracking-tight text-on-surface">
         报表
       </Text>
       <Text className="mb-5 text-base leading-7 text-on-surface-variant">

+ 53 - 53
src/app/_layout.tsx

@@ -1,5 +1,12 @@
+import { AnimatedSplashOverlay } from '@/components/animated-icon';
+import { antdTheme } from '@/constants/antd-theme';
+import { Colors } from '@/constants/theme';
 import '@/global.css';
+import api from '@/utils/api';
+import { AuthProvider } from '@/utils/auth';
+import { getGlobalStorage } from '@/utils/storage';
 import { Modal, Provider, Toast } from '@ant-design/react-native';
+import type { Theme } from '@react-navigation/native';
 import {
   DefaultTheme as ReactNavigationDefaultTheme,
   ThemeProvider,
@@ -12,13 +19,6 @@ import * as SplashScreen from 'expo-splash-screen';
 import * as updates from 'expo-updates';
 import { useEffect, useState } from 'react';
 import { Linking, Platform, View } from 'react-native';
-import { AnimatedSplashOverlay } from '@/components/animated-icon';
-import { antdTheme } from '@/constants/antd-theme';
-import { Colors } from '@/constants/theme';
-import api from '@/utils/api';
-import { AuthProvider } from '@/utils/auth';
-import { getGlobalStorage } from '@/utils/storage';
-import type { Theme } from '@react-navigation/native';
 
 
 SplashScreen.preventAutoHideAsync();
@@ -39,7 +39,7 @@ export const DefaultTheme: Theme = {
 
 
 export default function RootLayout() {
-  const [initializing, setIniting] = useState(true);
+  const [initializing, setInitlizing] = useState(true);
 
 
   // const colorScheme = useColorScheme();
@@ -54,8 +54,7 @@ export default function RootLayout() {
       try {
         const lastTime = parseInt(getGlobalStorage().getString('last_update_app') || '0');
         if (now - lastTime < 15 * 60000) {
-          SplashScreen.hide();
-          return true;
+          return;
         }
         const {
           version,
@@ -67,10 +66,8 @@ export default function RootLayout() {
           version: application.nativeApplicationVersion,
         }, { timeout: 5000 });
         getGlobalStorage().set('last_update_app', now);
-
-        SplashScreen.hide();
         if (!version || version === application.nativeApplicationVersion) {
-          return true;
+          return;
         }
         const va = version.split('.');
         let vb = (application.nativeApplicationVersion || '').split('.');
@@ -82,8 +79,9 @@ export default function RootLayout() {
           }
         }
         if (!update) {
-          return true;
+          return;
         }
+        SplashScreen.hide();
         let ok = true;
         do {
           ok = await new Promise<boolean>((resolve, reject) => {
@@ -95,15 +93,16 @@ export default function RootLayout() {
                     if (pkg) {
                       if (await Linking.canOpenURL(pkg)) {
                         Linking.openURL(pkg);
-                        resolve(false);
+                        resolve(true);
                         return;
                       }
                     }
                     Modal.alert('无法自动更新', "请前往应用商店更新");
-                    resolve(true);
+                    resolve(false);
                     return;
                   }
-                  resolve(true);
+                  // todo other
+                  resolve(false);
                 }
               }
             ];
@@ -119,55 +118,56 @@ export default function RootLayout() {
             Modal.alert(`${version}可更新`, force ? '请更新后继续使用' : '是否立即更新?', actions);
           });
         } while (!ok);
-        return true;
       } catch (e) {
-
-        SplashScreen.hide();
         console.warn(e);
-        return true;
       }
     }
 
     (async () => {
       if (__DEV__) {
-        setIniting(false);
+        setInitlizing(false);
         SplashScreen.hide();
         return;
       }
-      if (!await checkApp()) {
-        setIniting(false);
-        return;
-      }
-
-      try {
-
-        const res = await updates.checkForUpdateAsync();
-        // alert(JSON.stringify(res))
-        if (res.isAvailable) {
-
-          Modal.alert("发现热更新", "需要立即下载", [
-            {
-              text: '确认',
-              onPress: async () => {
-                const l = Toast.loading("正在下载更新 ...");
-                try {
-                  await updates.fetchUpdateAsync();
-                  await updates.reloadAsync();
-                  // eslint-disable-next-line @typescript-eslint/no-unused-vars
-                } catch (e) {
-                  Modal.alert("提示", "更新遇到问题");
+      await checkApp();
+      SplashScreen.hide();
+
+        while (true) {
+          let res = await new Promise<string|void>(async (resolve, reject) => {
+            const res = await updates.checkForUpdateAsync();
+            if (res.isAvailable) {
+
+              Modal.alert("发现热更新", "需要立即下载", [
+                {
+                  text: '确认',
+                  onPress: async () => {
+                    const l = Toast.loading("正在下载更新 ...");
+                    try {
+                      await updates.fetchUpdateAsync();
+                      Toast.remove(l);
+                      await updates.reloadAsync();
+                      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+                    } catch (e) {
+                      Toast.remove(l);
+                      Modal.alert("提示", "更新遇到问题", [{
+                          text: '确认',
+                          onPress: resolve,
+                      }], ()=>{resolve();return true});
+                    }
+                  }
                 }
-                Toast.remove(l);
-              }
+              ], () => false);
+            } else {
+              resolve('updateok');
             }
-          ], () => false);
+          });
+          if (res === 'updateok') {
+            break;
+          }
         }
-
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
-      } catch (e) {
-        // alert(e?.message || e+"");
-      }
-      setIniting(false);
+      
+      setInitlizing(false);
 
     })();
 
@@ -196,7 +196,7 @@ export default function RootLayout() {
             </Stack>
           </AuthProvider>}
       </Provider>
-      {initializing && <View className='absolute left-0 top-0 right-0 bottom-0 bg-[#f6f6f6] z-50'>
+      {(initializing || !fontsLoaded)&& <View className='absolute left-0 top-0 right-0 bottom-0 bg-[#f6f6f6] z-50'>
         <Image contentFit='contain' style={{ flex: 1 }} source={require('@/assets/images/uploading.jpg')} />
       </View>}
     </ThemeProvider>

+ 47 - 19
src/app/sign-in.tsx

@@ -1,25 +1,32 @@
+import type { CaptchaRes } from '@/components/captcha-box';
+import CaptchaBox from '@/components/captcha-box';
+import { site } from '@/config.json';
+import { useInterval } from '@/hooks/hooks';
+import api, { ApiError } from '@/utils/api';
+import { signIn, smsSignIn, useAuth } from '@/utils/auth';
 import { Button, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import { Link, router, useLocalSearchParams } from 'expo-router';
 import { openBrowserAsync } from 'expo-web-browser';
-import React, { useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import {
   KeyboardAvoidingView,
   Platform,
   Pressable,
+  ScrollView,
   StyleSheet,
   Text,
   TextInput,
   View
 } from 'react-native';
-import Animated, { scrollTo, useAnimatedRef, useDerivedValue, useSharedValue } from 'react-native-reanimated';
+import Animated, {
+  interpolateColor,
+  useAnimatedStyle,
+  useSharedValue,
+  withSequence,
+  withTiming,
+} from 'react-native-reanimated';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import CaptchaBox from '@/components/captcha-box';
-import { site } from '@/config.json';
-import { useInterval } from '@/hooks/hooks';
-import api, { ApiError } from '@/utils/api';
-import { signIn, smsSignIn, useAuth } from '@/utils/auth';
-import type { CaptchaRes } from '@/components/captcha-box';
 
 function FieldLabel({ children }: { children: React.ReactNode }) {
   return (
@@ -40,12 +47,30 @@ export default function SignInScreen() {
 
   const { redirectTo } = useLocalSearchParams<{ redirectTo?: string }>();
   const { setToken } = useAuth();
-  const scrollViewRef = useAnimatedRef();
-  const scrollY = useSharedValue(0);
+  const scrollViewRef = useRef<ScrollView>(null);
+
+  const agreeHighlight = useSharedValue(0);
+  useEffect(() => {
+    if (agreementHighlight) {
+      agreeHighlight.value = withSequence(
+        withTiming(1, { duration: 140 }),
+        withTiming(0, { duration: 140 }),
+        withTiming(1, { duration: 140 }),
+        withTiming(0, { duration: 140 }),
+        withTiming(1, { duration: 200 }),
+      );
+    } else {
+      agreeHighlight.value = withTiming(0, { duration: 200 });
+    }
+  }, [agreementHighlight, agreeHighlight]);
 
-  useDerivedValue(() => {
-    scrollTo(scrollViewRef, 0, scrollY.value, true);
-  });
+  const agreeBoxStyle = useAnimatedStyle(() => ({
+    borderColor: interpolateColor(
+      agreeHighlight.value,
+      [0, 1],
+      ['rgba(37, 99, 235, 0)', 'rgba(37, 99, 235, 0.5)'],
+    ),
+  }));
 
   const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
   const [smsTtl, setSmsTtl] = useState(0);
@@ -118,7 +143,7 @@ export default function SignInScreen() {
 
   const handleLogin = async () => {
     setFlushAgree(false);
-    scrollY.value = 0;
+    scrollViewRef.current?.scrollTo({ y: 0, animated: true });
     if (mobile.trim().length !== 11) {
       Toast.fail('请输入正确的手机号');
       return;
@@ -136,7 +161,7 @@ export default function SignInScreen() {
     }
 
     if (!agreed) {
-      scrollY.value = 99999;
+      scrollViewRef.current?.scrollToEnd({ animated: true });
       setFlushAgree(true);
       Toast.fail('请先阅读并同意协议');
       return;
@@ -170,7 +195,7 @@ export default function SignInScreen() {
         className="flex-1"
         behavior={Platform.OS === 'ios' ? 'padding' : undefined}
       >
-        <Animated.ScrollView
+        <ScrollView
           ref={scrollViewRef}
           className="flex-1"
           contentContainerClassName="px-8"
@@ -335,7 +360,10 @@ export default function SignInScreen() {
                 ))}
               </View>
 
-              <View className={`flex-row items-start gap-3 px-2 rounded-md border-2 transition-colors duration-700 ${agreementHighlight ? ' border-primary/50' : 'border-transparent'}`}>
+              <Animated.View
+                style={agreeBoxStyle}
+                className="flex-row items-start gap-3 px-2 rounded-lg border-2"
+              >
                 <Pressable
                   onPress={() => setAgreed((value) => !value)}
                   hitSlop={8}
@@ -364,10 +392,10 @@ export default function SignInScreen() {
                   <Text className="font-semibold text-primary cursor-pointer" onPress={() => openBrowserAsync(`${site}`)}>《隐私政策》</Text>
                   ,以及授权该应用获取您的公开信息。
                 </Text>
-              </View>
+              </Animated.View>
             </View>
           </View>
-        </Animated.ScrollView>
+        </ScrollView>
       </KeyboardAvoidingView>
       <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
       {loading && <View className="absolute left-0 right-0 top-0 bottom-0 z-1 bg-white/5" />}

+ 53 - 21
src/app/sign-up.tsx

@@ -1,25 +1,31 @@
+import type { CaptchaRes } from '@/components/captcha-box';
+import CaptchaBox 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 { Link, router, useLocalSearchParams } from 'expo-router';
-import { openBrowserAsync } from 'expo-web-browser';
-import React, { useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
 import {
   KeyboardAvoidingView,
   Platform,
   Pressable,
+  ScrollView,
   StyleSheet,
   Text,
   TextInput,
   View
 } from 'react-native';
-import Animated, { scrollTo, useAnimatedRef, useDerivedValue, useSharedValue } from 'react-native-reanimated';
+import Animated, {
+  interpolateColor,
+  useAnimatedStyle,
+  useSharedValue,
+  withSequence,
+  withTiming,
+} from 'react-native-reanimated';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import CaptchaBox 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 type { CaptchaRes } from '@/components/captcha-box';
 
 function FieldLabel({ children }: { children: React.ReactNode }) {
   return (
@@ -41,12 +47,30 @@ export default function SignUpScreen() {
   const [agreementHighlight, setFlushAgree] = useState(false);
   const [loading, setLoading] = useState(false);
 
-  const scrollViewRef = useAnimatedRef();
-  const scrollY = useSharedValue(0);
+  const scrollViewRef = useRef<ScrollView>(null);
+
+  const agreeHighlight = useSharedValue(0);
+  useEffect(() => {
+    if (agreementHighlight) {
+      agreeHighlight.value = withSequence(
+        withTiming(1, { duration: 140 }),
+        withTiming(0, { duration: 140 }),
+        withTiming(1, { duration: 140 }),
+        withTiming(0, { duration: 140 }),
+        withTiming(1, { duration: 200 }),
+      );
+    } else {
+      agreeHighlight.value = withTiming(0, { duration: 200 });
+    }
+  }, [agreementHighlight, agreeHighlight]);
 
-  useDerivedValue(() => {
-    scrollTo(scrollViewRef, 0, scrollY.value, true);
-  });
+  const agreeBoxStyle = useAnimatedStyle(() => ({
+    borderColor: interpolateColor(
+      agreeHighlight.value,
+      [0, 1],
+      ['rgba(37, 99, 235, 0)', 'rgba(37, 99, 235, 0.5)'],
+    ),
+  }));
 
   const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
   const [smsTtl, setSmsTtl] = useState(0);
@@ -120,7 +144,7 @@ export default function SignUpScreen() {
   const handleRegister = async () => {
 
     setFlushAgree(false);
-    scrollY.value = 0;
+    scrollViewRef.current?.scrollTo({ y: 0, animated: true });
     if (mobile.trim().length !== 11) {
       Toast.fail('请输入正确的手机号');
       return;
@@ -153,7 +177,7 @@ export default function SignUpScreen() {
 
 
     if (!agreed) {
-      scrollY.value = 99999;
+      scrollViewRef.current?.scrollToEnd({ animated: true });
       setFlushAgree(true);
 
       Toast.fail('请先阅读并同意协议');
@@ -191,7 +215,7 @@ export default function SignUpScreen() {
         className="flex-1"
         behavior={Platform.OS === 'ios' ? 'padding' : undefined}
       >
-        <Animated.ScrollView
+        <ScrollView
           ref={scrollViewRef}
           className="flex-1"
           contentContainerClassName="px-8"
@@ -360,7 +384,10 @@ export default function SignUpScreen() {
               </View>
             </View>
 
-            <View className={`flex-row items-start gap-3 px-2 rounded-md border-2 transition-colors duration-700 ${agreementHighlight ? ' border-primary/50' : 'border-transparent'}`}>
+            <Animated.View
+              style={agreeBoxStyle}
+              className="flex-row items-start gap-3 px-2 rounded-lg border-2"
+            >
               <Pressable
                 onPress={() => setAgreed((value) => !value)}
                 hitSlop={8}
@@ -386,12 +413,17 @@ export default function SignUpScreen() {
                   <Text className="font-semibold text-primary">《用户服务协议》</Text>
                 </Link>
-                <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 cursor-pointer">《隐私政策》</Text>
+                </Link>
                 ,以及授权该应用获取您的公开信息。
               </Text>
-            </View>
+            </Animated.View>
           </View>
-        </Animated.ScrollView>
+        </ScrollView>
       </KeyboardAvoidingView>
       <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
     </View>