lv 1 tháng trước cách đây
mục cha
commit
e11912bd78

+ 50 - 9
src/app/(tabs)/analytics.tsx

@@ -1,18 +1,19 @@
+import type { Customer as CustomerType } from '@/app/(tabs)/customer';
 import { SectionHeader } from '@/components/ui/section-header';
+import { Colors } from '@/constants/theme';
 import type { ListResponse } from '@/utils/api';
 import api from '@/utils/api';
 import { useSWC } from '@/utils/cache';
-import { ActivityIndicator } from '@ant-design/react-native';
+import { ActivityIndicator, Modal } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import { BlurView } from 'expo-blur';
-import { Stack, useFocusEffect } from 'expo-router';
-import React, { useCallback } from 'react';
+import { Stack, useFocusEffect, useNavigation } from 'expo-router';
+import React, { useCallback, 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 { UploadComponent } from '../../components/upload';
 
-
 type AnalysisRecord = {
   id: string;
   createtime: string;
@@ -46,11 +47,47 @@ export default function AnalyticsScreen() {
   const headerStyle = useAnimatedStyle(() => ({
     opacity: interpolate(scrollOffsetY.value, [0, 24 + insets.top], [0, 1], Extrapolation.CLAMP),
   }));
+  const navigation = useNavigation();
+
+  const [selectedCustomer, setSelectedCustommer] = useState<CustomerType|undefined>();
+  const handleSelectCustomer = useCallback(()=> {
+   
+    // @ts-ignore
+    navigation.navigate('customer/select', {
+      current: selectedCustomer?.id,
+      onSelect: async (c: CustomerType| undefined) => {
+        if (c?.loan_status != 'idle' && c?.loan_status != 'unmatch') {
+          const res = await new Promise<boolean | void>((resolve)=>{
+            Modal.alert("注意", `用户${c!.name} ${c!.loan_status == 'pending' ? '正在等待分析征信': '已有分析报告'}, 是否要再提交一份?`, [{
+            text: '取消',
+            style: {color: Colors.secondary.DEFAULT},
+            onPress: ()=>resolve(false)
+            }, {
+              text: '是的',
+              onPress:()=>resolve(),
+            }], ()=> {resolve(false); return true});
+          });
+          if (res !== false) {
+            setSelectedCustommer(c);
+          }
+          return res;
+        }
+        setSelectedCustommer(c);
+      }
+    });
+
+  }, [selectedCustomer, setSelectedCustommer]);
+
+  const onUploadComplete = useCallback((s?: 'cancel' | 'break') => {
+    if (s == 'break') {
+      handleSelectCustomer();
+    }
+  }, [handleSelectCustomer]);
 
   return (
     <Animated.ScrollView
       className="flex-1"
-      contentInset={{ top: insets.top, bottom: insets.bottom }}
+      // contentInset={{ top: insets.top, bottom: insets.bottom }}
       automaticallyAdjustContentInsets
       contentContainerStyle={{paddingTop: insets.top + 12, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
       onScroll={scrollHandler}
@@ -62,14 +99,14 @@ export default function AnalyticsScreen() {
         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>
           </Animated.View>
         ),
       }} />
-      <Text className="pl-5 h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
+      <Text className=" h-[44px] text-3xl font-extrabold tracking-tight text-on-surface">
         征信分析
       </Text>
 
@@ -82,16 +119,20 @@ export default function AnalyticsScreen() {
           style={({ pressed }) => ({
             opacity: pressed ? 0.9 : 1,
           })}
+          onPress={handleSelectCustomer}
         >
           <Ionicons name="person-outline" size={18} color="#737686" />
           <Text className="ml-3 flex-1 text-base font-medium text-on-surface">
-            张德发(138****8888)
+            {selectedCustomer ? `${selectedCustomer.name}(${selectedCustomer.mobile})` : '无'}
           </Text>
+          {selectedCustomer&&<Pressable className='mx-2' hitSlop={8} onPress={()=>setSelectedCustommer(undefined)}>
+            <Ionicons name="close-circle" size={20} color="#c3c6d7" />
+          </Pressable>}
           <Ionicons name="chevron-down" size={18} color="#c3c6d7" />
         </Pressable>
       </View>
 
-      <UploadComponent onComplete={() => { refresh() }} />
+      <UploadComponent askCustomer customerId={selectedCustomer?.id} onComplete={onUploadComplete} />
 
 
 

+ 86 - 38
src/app/(tabs)/customer.tsx

@@ -4,22 +4,26 @@ 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 clsx from 'clsx';
 import { BlurView } from 'expo-blur';
-import { Stack, useFocusEffect } from 'expo-router';
-import React, { useCallback, useRef, useState } from 'react';
+import { Stack } from 'expo-router';
+import React, { useCallback, useEffect, 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';
 
-type CustomerLoanStatus = 'matched' | 'unmatch' | 'pending' | 'completed' | undefined;
-const CustomerLoanStatusText: Record<NonNullable<CustomerLoanStatus>, string> = {
-  matched: '已匹配',
-  unmatch: '未匹配',
+export type CustomerLoanStatus = 'idle' | 'matched' | 'unmatch' | 'pending' | 'completed' | 'all' | undefined;
+export const CustomerLoanStatusText: Record<NonNullable<CustomerLoanStatus>, string> = {
+  all: '所有',
+  idle: '待匹配',
   pending: '匹配中',
+  matched: '已匹配',
   completed: '已完成',
+  unmatch: '匹配失败',
 };
 
-type Customer = {
+
+export type Customer = {
   id: string;
   name: string;
   mobile: string;
@@ -94,11 +98,15 @@ const keyExtractor = (item: Customer) => item.id;
 export default function CustomerScreens() {
   const [searchKey, setSearchKey] = useState('');
   const [list, setList] = useState<Customer[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [activeStatus, setActiveStatus] = useState<CustomerLoanStatus>('idle');
+
   const startRef = useRef(0);
-  const loanStatusRef = useRef<CustomerLoanStatus>(undefined);
-  const hasMoreRef = useRef(true);
+  const loanStatusRef = useRef<CustomerLoanStatus>('idle');
+  const hasMoreRef = useRef(false);
   const loadingRef = useRef(false);
-  const [loading, setLoading] = useState(true);
+  const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
   const insets = useSafeAreaInsets();
 
   const scrollOffsetY = useSharedValue(0);
@@ -109,14 +117,14 @@ export default function CustomerScreens() {
     opacity: interpolate(scrollOffsetY.value, [0, 24 + insets.top], [0, 1], Extrapolation.CLAMP),
   }));
 
-  const load = useCallback(async (start: number, loanStatus?: CustomerLoanStatus) => {
+  const load = useCallback(async (start: number, loanStatus: CustomerLoanStatus, q?: string) => {
     if (loadingRef.current) return;
     loadingRef.current = true;
     loanStatusRef.current = loanStatus;
     startRef.current = start;
     setLoading(true);
 
-    if (start === 0 && !loanStatus) {
+    if (start === 0 && !loanStatus && !q) {
       const cache = getApiCache().getObject<ListResponse<Customer>>(CACHE_KEY);
       if (cache) {
         setList(cache.list);
@@ -133,10 +141,11 @@ export default function CustomerScreens() {
         start,
         size: PAGE_SIZE,
         loanStatus,
+        q,
       });
       if (loanStatusRef.current !== loanStatus) return;
       const next = res?.list ?? [];
-      if (start === 0 && !loanStatus && next.length) {
+      if (start === 0 && !loanStatus && !q && next.length) {
         getApiCache().setObject(CACHE_KEY, res, 60);
       }
       hasMoreRef.current = next.length >= PAGE_SIZE;
@@ -153,16 +162,44 @@ export default function CustomerScreens() {
     }
   }, []);
 
-  useFocusEffect(
-    useCallback(() => {
-      load(0);
-    }, [load])
-  );
+  useEffect(() => {
+    load(0, 'idle');
+  }, [load]);
 
   const loadMore = useCallback(() => {
     if (!hasMoreRef.current || loadingRef.current) return;
-    load(startRef.current, loanStatusRef.current);
-  }, [load]);
+    load(startRef.current, loanStatusRef.current, searchKey);
+  }, [load, searchKey]);
+
+  const handleSelectStatus = useCallback(
+    (status: CustomerLoanStatus) => {
+      const next = activeStatus === status ? undefined : status;
+      setActiveStatus(next);
+      load(0, next, searchKey);
+    },
+    [activeStatus, load, searchKey]
+  );
+
+  const handleSearch = useCallback(
+    (k: string) => {
+      setSearchKey(k);
+      if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
+      searchTimerRef.current = setTimeout(() => {
+        load(0, loanStatusRef.current, k);
+      }, 600);
+    },
+    [load]
+  );
+
+  const handleReset = useCallback(() => {
+    handleSearch('');
+  }, [handleSearch]);
+
+  useEffect(() => {
+    return () => {
+      if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
+    };
+  }, []);
 
   const ListHeader = (
     <>
@@ -175,29 +212,40 @@ export default function CustomerScreens() {
         <Ionicons name="search-outline" size={20} color="#737686" />
         <TextInput
           value={searchKey}
-          onChangeText={setSearchKey}
+          onChangeText={handleSearch}
           placeholder="搜索客户姓名 / 手机号"
           placeholderTextColor="#9ca3af"
           className="ml-3 flex-1 p-0 text-base text-on-surface"
         />
         {searchKey.length > 0 ? (
-          <Pressable hitSlop={8} onPress={() => setSearchKey('')}>
+          <Pressable hitSlop={8} onPress={handleReset}>
             <Ionicons name="close-circle" size={20} color="#c3c6d7" />
           </Pressable>
         ) : null}
       </View>
 
-      <View className="mb-5 flex-row gap-2">
-        {Object.entries(CustomerLoanStatusText).map(([key, item]) => (
-          <Pressable
-            key={key}
-            onPress={() => { }}
-            className="rounded-full bg-surface-container-lowest px-4 py-2"
-            style={({ pressed }) => ({ opacity: pressed ? 0.84 : 1 })}
-          >
-            <Text className="text-sm font-bold text-on-surface-variant">{item}</Text>
-          </Pressable>
-        ))}
+      <View className="mb-5 flex-row flex-wrap gap-2">
+        {Object.entries(CustomerLoanStatusText).map(([key, label]) => {
+          const active = activeStatus === (key as CustomerLoanStatus);
+          return (
+            <Pressable
+              key={key}
+              onPress={() => handleSelectStatus(key as CustomerLoanStatus)}
+              className={clsx('rounded-md px-1 border border-transparent', {
+                'border-primary-container': active,
+              })}
+              style={({ pressed }) => ({ opacity: pressed ? 0.84 : 1 })}
+            >
+              <Text
+                className={clsx('text-sm font-bold text-on-surface-variant', {
+                  'text-primary': active,
+                })}
+              >
+                {label}
+              </Text>
+            </Pressable>
+          );
+        })}
       </View>
     </>
   );
@@ -229,10 +277,10 @@ export default function CustomerScreens() {
     <>
       <Animated.FlatList
         className="flex-1"
-       contentInset={{ top: insets.top, bottom: insets.bottom }}
-      automaticallyAdjustContentInsets
-      contentContainerStyle={{paddingTop: insets.top + 12, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
-      
+        // 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}
@@ -259,7 +307,7 @@ export default function CustomerScreens() {
           </Animated.View>
         ),
       }} />
-      <View className="absolute bottom-7 right-5">
+      <View style={{bottom: insets.bottom + 44}} className="absolute pb-2 right-5">
         <Pressable
           className="h-14 w-14 items-center justify-center rounded-full bg-primary-container"
           style={({ pressed }) => ({

+ 3 - 3
src/app/(tabs)/reports.tsx

@@ -114,9 +114,9 @@ export default function ReportsScreen() {
 
     <Animated.ScrollView
       className="flex-1"
-    contentInset={{ top: insets.top, bottom: insets.bottom }}
+    // contentInset={{ top: insets.top, bottom: insets.bottom }}
       automaticallyAdjustContentInsets
-      contentContainerStyle={{paddingTop: insets.top, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
+      contentContainerStyle={{paddingTop: insets.top + 12, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
 
       showsVerticalScrollIndicator={false}
       onScroll={scrollHandler}
@@ -135,7 +135,7 @@ export default function ReportsScreen() {
           </Animated.View>
         ),
       }} />
-      <Text className="bg-primary h-[44px] pt-1 text-3xl font-extrabold tracking-tight text-on-surface">
+      <Text className="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">

+ 247 - 0
src/app/customer/select.tsx

@@ -0,0 +1,247 @@
+import { StatusBadge } from '@/components/ui/status-badge';
+import { Colors } from '@/constants/theme';
+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 { useRoute } from '@react-navigation/native';
+import clsx from 'clsx';
+import { Stack, useNavigation } from 'expo-router';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { FlatList, Pressable, Text, TextInput, View } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { type Customer, type CustomerLoanStatus } from '../(tabs)/customer';
+
+const PAGE_SIZE = 15;
+const CACHE_KEY = 'customer_first';
+export const CustomerLoanStatusText: Record<NonNullable<CustomerLoanStatus>, string> = {
+  idle: '待匹配',
+  pending: '匹配中',
+  matched: '已匹配',
+  completed: '已完成',
+  unmatch: '匹配失败',
+  all: '所有'
+};
+
+
+export default function CustomerSelectScreen() {
+    const insets = useSafeAreaInsets();
+    const navigation = useNavigation();
+    const route = useRoute();
+
+    const [searchKey, setSearchKey] = useState('');
+    const [list, setList] = useState<Customer[]>([]);
+    const [loading, setLoading] = useState(true);
+    const [activeStatus, setActiveStatus] = useState<CustomerLoanStatus>('idle');
+
+    const startRef = useRef(0);
+    const loanStatusRef = useRef<CustomerLoanStatus>('idle');
+    const hasMoreRef = useRef(false);
+    const loadingRef = useRef(false);
+    const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+    const load = useCallback(async (start: number, loanStatus: CustomerLoanStatus, q?: string) => {
+        if (loadingRef.current) return;
+        loadingRef.current = true;
+        loanStatusRef.current = loanStatus;
+        startRef.current = start;
+        setLoading(true);
+
+        
+
+        try {
+            const res = await api.post<ListResponse<Customer>>('customer/list', {
+                start,
+                size: PAGE_SIZE,
+                loanStatus,
+                q,
+            });
+            if (loanStatusRef.current !== loanStatus) return;
+            const next = res?.list ?? [];
+            if (start === 0 && !loanStatus && next.length) {
+                getApiCache().setObject(CACHE_KEY, res, 60);
+            }
+            hasMoreRef.current = next.length >= PAGE_SIZE;
+            setList((prev) => (start === 0 ? next : prev.concat(next)));
+            startRef.current = start + next.length;
+            if (!next.length && start > 0) {
+                Toast.offline('没有更多数据可加载');
+            }
+        } catch {
+            Toast.fail('加载列表失败!');
+        } finally {
+            setLoading(false);
+            loadingRef.current = false;
+        }
+    }, []);
+
+    // 首次加载
+    useEffect(() => {
+        load(0, 'idle');
+    }, [load]);
+
+    const loadMore = useCallback(() => {
+        if (!hasMoreRef.current || loadingRef.current) return;
+        load(startRef.current, loanStatusRef.current, searchKey);
+    }, [load, searchKey]);
+
+    const handleSelectStatus = useCallback(
+        (status: CustomerLoanStatus) => {
+            const next = activeStatus === status ? undefined : status;
+            setActiveStatus(next);
+            load(0, next, searchKey);
+        },
+        [activeStatus, load, searchKey]
+    );
+
+    const handlePick = useCallback( 
+        async (item: Customer) => {
+            // @ts-ignore
+            const res = await route.params?.onSelect?.(item);
+            // alert(res);
+            if (false === res) {
+                return;
+            }
+            navigation.goBack();
+        },
+        [navigation]
+    );
+
+    const handleSearch = useCallback((k: string) => {
+        setSearchKey(k);
+        if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
+
+        searchTimerRef.current = setTimeout(() => {
+            load(0, loanStatusRef.current, k);
+        }, 600);
+    }, [load, searchKey]);
+    const handleReset = useCallback(()=> {
+        handleSearch('');
+    }, [handleSearch]);
+
+
+    useEffect(() => {
+        return () => {
+            if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
+        };
+    }, []);
+    // @ts-ignore
+    const currentId = route.params?.current;
+    const renderItem = useCallback(
+        ({ item }: { item: Customer }) => (
+            <Pressable
+                onPress={() => handlePick(item)}
+                className="mb-2 flex-row items-center gap-3 rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-3"
+                style={({ pressed }) => ({
+                    opacity: pressed ? 0.85 : 1,
+                    transform: [{ scale: pressed ? 0.995 : 1 }],
+                })}
+            >
+                {currentId == item.id && <Ionicons name='checkmark-circle-sharp' size={20} color={Colors.tint} />}
+                <View className="h-10 w-10 items-center justify-center rounded-full bg-primary-fixed">
+                    <Text className="text-base font-bold text-primary">{item.name?.[0] ?? '?'}</Text>
+                </View>
+                <View className="flex-1">
+                    <Text className="text-base font-bold text-on-surface" numberOfLines={1}>
+                        {item.name}
+                    </Text>
+                    <Text className="mt-0.5 text-sm text-on-surface-variant" numberOfLines={1}>
+                        {item.mobile}
+                    </Text>
+                </View>
+                {item.loan_status && <StatusBadge text={CustomerLoanStatusText[item.loan_status]} variant="secondary" />}
+            </Pressable>
+        ),
+        [handlePick]
+    );
+
+    const ListHeader = (
+        <>
+            <View className="mb-3 flex-row items-center rounded-2xl bg-surface-container-low px-4 py-3">
+                <Ionicons name="search-outline" size={20} color="#737686" />
+                <TextInput
+                    value={searchKey}
+                    onChangeText={handleSearch}
+                    placeholder="搜索客户姓名 / 手机号"
+                    placeholderTextColor="#9ca3af"
+                    className="ml-3 flex-1 p-0 text-base text-on-surface"
+                />
+                {searchKey.length > 0 ? (
+                    <Pressable hitSlop={8} onPress={handleReset}>
+                        <Ionicons name="close-circle" size={20} color="#c3c6d7" />
+                    </Pressable>
+                ) : null}
+            </View>
+
+            <View className="mb-4 flex-row flex-wrap gap-2">
+                {Object.entries(CustomerLoanStatusText).map(([key, label]) => {
+                    const active = activeStatus === (key as CustomerLoanStatus);
+                    return (
+                        <Pressable
+                            key={key}
+                            onPress={() => handleSelectStatus(key as CustomerLoanStatus)}
+                            className={clsx("rounded-md px-1 border border-transparent", {
+                                 'border-primary-container': active,
+                            })}
+                            style={({ pressed }) => ({ opacity: pressed ? 0.84 : 1 })}
+                        >
+                            <Text
+                                className={clsx("text-sm font-bold text-on-surface-variant", {
+                                    'text-primary': active
+                                })}
+                            >
+                                {label}
+                            </Text>
+                        </Pressable>
+                    );
+                })}
+            </View>
+        </>
+    );
+
+    const ListEmpty =
+        !loading ? (
+            <View className="items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-6 py-14">
+                <Ionicons name="people-outline" size={44} color="#c3c6d7" />
+                <Text className="mt-4 text-base text-on-surface-variant">暂无匹配客户</Text>
+            </View>
+        ) : (
+            <View className="flex-row items-center justify-center pt-8">
+                <ActivityIndicator />
+                <Text className="ml-2">加载中</Text>
+            </View>
+        );
+
+    const ListFooter =
+        loading && list.length > 0 ? (
+            <View className="mt-2 flex-row items-center justify-center">
+                <ActivityIndicator />
+                <Text className="ml-2 pb-4">加载中</Text>
+            </View>
+        ) : null;
+
+    return (
+        <View className="flex-1 bg-surface">
+            <Stack.Screen options={{ title: '选择客户' }} />
+            <FlatList
+                className="flex-1"
+                contentContainerStyle={{
+                    paddingTop: insets.top + 60,
+                    paddingBottom: insets.bottom + 24,
+                    paddingHorizontal: 20,
+                }}
+                data={list}
+                keyExtractor={(item) => item.id}
+                renderItem={renderItem}
+                keyboardShouldPersistTaps="handled"
+                showsVerticalScrollIndicator={false}
+                ListHeaderComponent={ListHeader}
+                ListEmptyComponent={ListEmpty}
+                ListFooterComponent={ListFooter}
+                onEndReached={loadMore}
+                onEndReachedThreshold={0.5}
+            />
+        </View>
+    );
+}

+ 35 - 15
src/components/upload.tsx

@@ -1,8 +1,16 @@
+import UIButton from '@/components/ui/ui-button';
+import { Colors } from '@/constants/theme';
+import api from '@/utils/api';
+import { openSystemSettings } from '@/utils/os';
 import { ActionSheet, ActivityIndicator, Icon, Modal, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import clsx from 'clsx';
+import type { DocumentPickerAsset } from 'expo-document-picker';
 import { getDocumentAsync } from 'expo-document-picker';
 import { File } from 'expo-file-system';
+import type {
+    ImagePickerAsset
+} from 'expo-image-picker';
 import {
     launchCameraAsync,
     launchImageLibraryAsync,
@@ -12,16 +20,9 @@ import {
 import { Link } from 'expo-router';
 import { useCallback, useEffect, useState } from 'react';
 import { Platform, Pressable, Text, View } from 'react-native';
-import UIButton from '@/components/ui/ui-button';
-import { Colors } from '@/constants/theme';
-import api from '@/utils/api';
-import { openSystemSettings } from '@/utils/os';
-import type { DocumentPickerAsset} from 'expo-document-picker';
-import type {
-    ImagePickerAsset} from 'expo-image-picker';
 
 type Asset = ImagePickerAsset | DocumentPickerAsset;
-type UploadHandler = (assets: Asset[]) => void;
+type UploadHandler = (assets: Asset[], customerId?: string) => void;
 
 type UploadState = 0 | 1 | 2; // 0: 待选择 / 1: 上传中 / 2: 已提交
 
@@ -108,7 +109,7 @@ const pickDocument = async (upload: UploadHandler) => {
     }
 };
 
-async function uploadAndCreateCredit(assets: Asset[]) {
+async function uploadAndCreateCredit(assets: Asset[], customerId?: string) {
     const file = new File(assets[0].uri);
     const formData = new FormData();
     formData.append('file', file);
@@ -190,14 +191,14 @@ export function UploadScreen({
     );
 }
 
-export function UploadComponent({ onComplete }: { onComplete: () => void }) {
+export function UploadComponent({ askCustomer, onComplete, customerId }: { askCustomer?: boolean; customerId?: string; onComplete: (status?: 'cancel' | 'break') => void }) {
     const [state, setState] = useState<UploadState>(0);
 
     const upload = useCallback<UploadHandler>(
-        async (assets) => {
+        async (assets, customerId) => {
             setState(1);
             try {
-                await uploadAndCreateCredit(assets);
+                await uploadAndCreateCredit(assets, customerId);
                 setState(0);
                 onComplete?.();
             } catch (err) {
@@ -206,7 +207,7 @@ export function UploadComponent({ onComplete }: { onComplete: () => void }) {
                 Toast.fail('上传失败,请重试');
             }
         },
-        [onComplete]
+        [onComplete, customerId]
     );
 
     const handleSelect = useCallback(
@@ -223,7 +224,26 @@ export function UploadComponent({ onComplete }: { onComplete: () => void }) {
         [upload]
     );
 
-    const selectFile = useCallback(() => {
+    const selectFile = useCallback(async () => {
+        if (askCustomer && !customerId) {
+            const ask = await new Promise<boolean>((resolve) => {
+                Modal.alert("注意", "您没有选择客户,是否在分析完征信信息后匹配或新增客户?", [{
+                    text: "否,先选客户",
+                    onPress: ()=> resolve(false),
+                }, {
+                    text: "是,先分析",
+                    style: {color: Colors.error.DEFAULT},
+                    onPress: ()=> resolve(true),
+                }], ()=> {
+                    resolve(false);
+                    return true
+                });
+            });
+            if (!ask) {
+                onComplete('break')
+                return;
+            }
+        }
         ActionSheet.showActionSheetWithOptions(
             {
                 title: '请选择征信上传方式',
@@ -235,7 +255,7 @@ export function UploadComponent({ onComplete }: { onComplete: () => void }) {
             },
             handleSelect
         );
-    }, [handleSelect]);
+    }, [handleSelect, askCustomer, customerId]);
 
     const isUploading = state === 1;
     return (

+ 7 - 6
src/utils/api.ts

@@ -1,10 +1,10 @@
+import type { AxiosRequestConfig } from 'axios';
 import axios from 'axios';
-import { fetch } from 'expo/fetch';
 import Constants from 'expo-constants';
-import { Platform } from 'react-native';
-import type {AccessToken} from './auth';
-import type { AxiosRequestConfig } from 'axios';
 import type { FetchRequestInit } from 'expo/fetch';
+import { fetch } from 'expo/fetch';
+import { Platform } from 'react-native';
+import type { AccessToken } from './auth';
 
 const { api: apiConfig, jsVersion } = require('@/config.json') as AppConfig;
 let accessToken: AccessToken | undefined | null = null;
@@ -104,16 +104,17 @@ const apiClient = axios.create({
 
 if (__DEV__) {
     apiClient.interceptors.request.use((config) => {
-        console.log('📤', config.method?.toUpperCase(), config.url, config.data ?? config.params ?? '');
+        console.log('📤', config.method, config.url, config.data ?? config.params ?? '');
         return config;
     });
     apiClient.interceptors.response.use(
         (response) => {
-            console.log('📡', response.status, response.config.url, response.data);
+            console.log('📡', response.status, response.config.url);
             return response;
         },
         (error) => {
             console.log('❌', error.config?.url, error.response?.status, error.response?.data ?? error.message);
+            console.log(error.data);
             return Promise.reject(error);
         }
     );