lv il y a 3 semaines
Parent
commit
d32fdd1112

+ 1 - 1
CLAUDE.md

@@ -29,7 +29,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 ### 路由([src/app/](src/app/))
 - `expo-router` + `experiments.typedRoutes`,新增页面文件即新增路由,不需要中央路由表
-- 底部 Tab 在 [src/app/(tabs)/](src/app/(tabs)/)(`index/customer/analytics/reports/profile`),改 Tab 结构同时改 `(tabs)/_layout.tsx`
+- 底部 Tab 在 [src/app/(tabs)/](src/app/(tabs)/)(`index/customer/credit/reports/profile`),改 Tab 结构同时改 `(tabs)/_layout.tsx`
 - 业务子流程目录:`credit/`、`customer/`;登录注册:`sign-in.tsx`、`sign-up.tsx`;Web 专属入口 `web.tsx` 和 `+html.tsx`
 - `app.json -> experiments.baseUrl: "/h5"`,Web 部署到 `/h5`,本地 dev server 也带这个前缀
 

+ 4 - 4
ios/LoanAssistant.xcodeproj/project.pbxproj

@@ -372,11 +372,11 @@
 				);
 				OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
 				PRODUCT_BUNDLE_IDENTIFIER = com.cdloan.assistant;
-				PRODUCT_NAME = "app";
+				PRODUCT_NAME = app;
 				SWIFT_OBJC_BRIDGING_HEADER = "LoanAssistant/LoanAssistant-Bridging-Header.h";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_VERSION = 5.0;
-				TARGETED_DEVICE_FAMILY = "1";
+				TARGETED_DEVICE_FAMILY = 1;
 				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Debug;
@@ -403,10 +403,10 @@
 				);
 				OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
 				PRODUCT_BUNDLE_IDENTIFIER = com.cdloan.assistant;
-				PRODUCT_NAME = "app";
+				PRODUCT_NAME = app;
 				SWIFT_OBJC_BRIDGING_HEADER = "LoanAssistant/LoanAssistant-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
-				TARGETED_DEVICE_FAMILY = "1";
+				TARGETED_DEVICE_FAMILY = 1;
 				VERSIONING_SYSTEM = "apple-generic";
 			};
 			name = Release;

+ 3 - 3
ios/Podfile.lock

@@ -2681,7 +2681,7 @@ SPEC CHECKSUMS:
   EXUpdates: 1457ed285cd7c5e9d4c388ace518af55fa4b21ec
   EXUpdatesInterface: 39d05400a9226c438565dbaaa8447b5f0672ba45
   FBLazyVector: 061f518bbd81677ed8a8317e2ae60b8779495808
-  hermes-engine: f4c4bf9a6979a9d777e0e4696e56be72362744d6
+  hermes-engine: e4ecc5957f671df3fa4828a33b435bd6955c25a5
   libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
   libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
   libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
@@ -2697,7 +2697,7 @@ SPEC CHECKSUMS:
   React: 3e14066ac707b3e369d09e2e923d8bee7f8c33ff
   React-callinvoker: bd7959a24564feaf5e4c8c11789e64884da13482
   React-Core: f060f4e14e9301685ce63ea65fbe903bf3397e45
-  React-Core-prebuilt: 0bd7bd67a89706f97a8d6150c409951862aecfc2
+  React-Core-prebuilt: e958d0ca3f8828f37f35c4cff2594b6e080fd991
   React-CoreModules: 89a1a544297be88ca4cdff317423f9e0d0c192a4
   React-cxxreact: 81b1bfa305b1b759cc0fe18327644816216e5993
   React-debug: 234fddb309822e13b9b442ef1bdca8d2b8c3cbf2
@@ -2760,7 +2760,7 @@ SPEC CHECKSUMS:
   ReactAppDependencyProvider: 2b19d66e5ddfe8dc7afb6338a4626156cbf2bab1
   ReactCodegen: 43f0948182edee9407c7b977fb059455dfeb9361
   ReactCommon: c6e81cc1ae185fa84863f3ea1d58caac4be741d7
-  ReactNativeDependencies: bcf3a9cabdc4c83653b9318342a4025f5a2effc1
+  ReactNativeDependencies: e3081feec6a8266d996ca8ce3d5707360258baf8
   RNGestureHandler: 0ea8153746a92b3744d4eaadade647debedf646e
   RNReanimated: 86e5991396f1aa514db90d6c79d4c3e37a37bb10
   RNScreens: 02e62f995ceb94ea465864b3bf4d4767b87f0362

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

@@ -1,10 +1,10 @@
+import { Colors } from '@/constants/theme';
+import { useAuth } from '@/utils/auth';
 import { BlurView } from 'expo-blur';
 import { Image } from 'expo-image';
 import { router, Tabs } from 'expo-router';
 import React, { useEffect } from 'react';
 import { Platform, View } from 'react-native';
-import { Colors } from '@/constants/theme';
-import { useAuth } from '@/utils/auth';
 const EXPLORE_ICON = require('@/assets/images/tabIcons/explore.png');
 const HOME_ICON = require('@/assets/images/tabIcons/home.png');
 const ICON_STYLE = { width: 24, height: 24 };
@@ -40,7 +40,7 @@ const SCREEN_OPTIONS = {
 const TABS = [
   { name: 'index', title: '首页', tabBarIcon: tinted(HOME_ICON) },
   { name: 'customer', title: '客户', tabBarIcon: tinted(EXPLORE_ICON) },
-  { name: 'analytics', title: '分析', tabBarIcon: tinted(EXPLORE_ICON) },
+  { name: 'credit', title: '分析', tabBarIcon: tinted(EXPLORE_ICON) },
   { name: 'reports', title: '报告', tabBarIcon: tinted(EXPLORE_ICON) },
   { name: 'profile', title: '我的', tabBarIcon: plain(EXPLORE_ICON) },
 ];

+ 0 - 216
src/app/(tabs)/analytics.tsx

@@ -1,216 +0,0 @@
-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, Modal } from '@ant-design/react-native';
-import { Ionicons } from '@expo/vector-icons';
-import { BlurView } from 'expo-blur';
-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;
-  status: 'pending' | 'completed' | 'failed' | 'canceled';
-  score?: string;
-  customer_name?: string;
-  name?: string;
-};
-
-export default function AnalyticsScreen() {
-  const { data: list, loading, load } = useSWC<ListResponse<AnalysisRecord>>("credit_index_list", async () => {
-    return api.post("credit/list", { size: 10 });
-  }, {
-    cacheOnly: true,
-    cacheTimeout: 120,
-    autoStart: false,
-  })
-
-
-  useFocusEffect(
-    useCallback(() => {
-      load();
-    }, [])
-  );
-  const insets = useSafeAreaInsets();
-
-  const scrollOffsetY = useSharedValue(0);
-  const scrollHandler = useAnimatedScrollHandler((e) => {
-    scrollOffsetY.value = e.contentOffset.y;
-  });
-  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 }}
-      automaticallyAdjustContentInsets
-      contentContainerStyle={{paddingTop: insets.top + 12, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20}}
-      onScroll={scrollHandler}
-      scrollEventThrottle={16}
-    >
-      <Stack.Screen options={{
-        headerShown: true,
-        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-5 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>
-
-      <View className="mb-3 rounded-2xl border border-outline-variant bg-surface-container-lowest p-4">
-        <Text className="mb-3 text-xs font-bold uppercase tracking-widest text-outline">
-          选择客户
-        </Text>
-        <Pressable
-          className="flex-row items-center rounded-2xl bg-surface-container-low px-4 py-3.5 active:opacity-90"
-          onPress={handleSelectCustomer}
-        >
-          <Ionicons name="person-outline" size={18} color="#94a3b8" />
-          <Text className="ml-3 flex-1 text-base font-medium text-on-surface">
-            {selectedCustomer ? `${selectedCustomer.name}(${selectedCustomer.mobile})` : '无'}
-          </Text>
-          {selectedCustomer&&<Pressable className='mx-2' hitSlop={8} onPress={()=>setSelectedCustommer(undefined)}>
-            <Ionicons name="close-circle" size={20} color="#94a3b8" />
-          </Pressable>}
-          <Ionicons name="chevron-down" size={18} color="#94a3b8" />
-        </Pressable>
-      </View>
-
-      <UploadComponent askCustomer customerId={selectedCustomer?.id} onComplete={onUploadComplete} />
-
-
-
-      <SectionHeader title="解析记录" />
-      <View className="gap-3">
-        {loading === true && <ActivityIndicator />}
-        {list?.list?.map((record) => (
-          <Pressable
-            key={record.id}
-            className="flex-row items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-3.5 active:opacity-90 active:scale-[0.99]"
-          >
-            <View
-              className={`mr-3 h-11 w-11 items-center justify-center rounded-full ${record.status === 'completed'
-                ? 'bg-green-50'
-                : record.status === 'pending'
-                  ? 'bg-blue-50'
-                  : 'bg-error-container'
-                }`}
-            >
-              <Ionicons
-                name={
-                  record.status === 'completed'
-                    ? 'checkmark-circle'
-                    : record.status === 'pending'
-                      ? 'hourglass-outline'
-                      : 'alert-circle'
-                }
-                size={20}
-                color={
-                  record.status === 'completed'
-                    ? Colors.success.DEFAULT
-                    : record.status === 'pending'
-                      ? Colors.tint
-                      : Colors.error.DEFAULT
-                }
-              />
-            </View>
-
-            <View className="flex-1">
-              <View className="mb-1 flex-row items-center justify-between gap-3">
-                <Text className="text-base font-bold text-on-surface">
-                  {record.customer_name || record.name || '客户'}
-                </Text>
-                <Text className="text-xs text-on-surface-variant">{record.status}</Text>
-              </View>
-              {(record.status === 'pending' || record.status === 'failed') && (
-                <View>
-                  <View className="mb-2 flex-row items-center justify-between">
-                    <Text className="text-sm text-on-surface-variant">...</Text>
-                    <Text className="text-sm font-bold text-primary">
-                      {record.score}
-                    </Text>
-                  </View>
-
-                </View>)}
-
-              {record.status === 'completed' && (
-                <View>
-                  <View className="mb-2 flex-row items-center justify-between">
-                    <Text className="text-sm text-on-surface-variant">...</Text>
-                    <Text className="text-sm font-bold text-primary">
-                      {record.score}
-                    </Text>
-                  </View>
-                  <View className="h-1.5 rounded-full bg-surface-container">
-                    <View
-                      className="h-full rounded-full bg-primary-container"
-                      style={{ width: '10%' }}
-                    />
-                  </View>
-                </View>)}
-              {record.status === 'failed' && (
-                <Text className="text-sm text-error">解析失败,请联系管理员</Text>
-              )}
-
-            </View>
-
-            <Ionicons name="chevron-forward" size={18} color="#94a3b8" />
-          </Pressable>
-        ))}
-      </View>
-    </Animated.ScrollView>
-  );
-}

+ 321 - 0
src/app/(tabs)/credit.tsx

@@ -0,0 +1,321 @@
+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, Icon, Modal, Toast } from '@ant-design/react-native';
+import { Ionicons } from '@expo/vector-icons';
+import clsx from 'clsx';
+import { BlurView } from 'expo-blur';
+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';
+
+export type CreditStatus = 'pending' | 'analyzing' | 'completed' | 'failed' | 'canceled' | 'all' | undefined;
+export const CreditStatusText: Record<NonNullable<CreditStatus>, string> = {
+  pending: '等待',
+  analyzing: '分析中',
+  completed: '完成',
+  failed: '失败',
+  canceled: '取消',
+  all: '所有'
+};
+
+export const LevelStatus = {
+  '差': {
+    bg: Colors.error.DEFAULT,
+    width: 20
+  },
+  '中': {
+    bg: Colors.warn,
+    width: 40,
+  },
+  '良': {
+    bg: '#99aa00',
+    width: 60
+  },
+  '优': {
+    bg: Colors.success.DEFAULT,
+    width: 100
+  },
+  '-': {
+    bg: '',
+    width: 0
+  },
+}
+export type Credit = {
+  id: string;
+  customer_id?: string;
+  name?: string;
+  updatetime?: number;
+  tags?: string[];
+  recommend?: string[];
+  suggestions?: string[];
+  level?: '差' | '中' | '良' | '优';
+  amount?: string;
+  status: CreditStatus;
+}
+
+export default function CreditScreen() {
+  const { data: list, loading, refresh, load } = useSWC<ListResponse<Credit>>("credit_index_list", async () => {
+    return api.post("credit/list", { size: 10 });
+  }, {
+    cacheOnly: true,
+    cacheTimeout: 120,
+    autoStart: false,
+  })
+
+
+  useFocusEffect(
+    useCallback(() => {
+      load();
+    }, [])
+  );
+  const insets = useSafeAreaInsets();
+
+  const scrollOffsetY = useSharedValue(0);
+  const scrollHandler = useAnimatedScrollHandler((e) => {
+    scrollOffsetY.value = e.contentOffset.y;
+  });
+  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!.credit_count < 1 ? '正在等待分析征信' : '已有分析报告'}, 是否要再提交一份?`, [{
+              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();
+      return;
+    }
+    refresh();
+  }, [handleSelectCustomer, refresh]);
+
+  const handleItemPress = useCallback((item: Credit) => {
+    if (item.customer_id) {
+      // @ts-ignore
+      navigation.navigate('credit/detail', {
+        id: item.id
+      });
+      return;
+    }
+    Modal.alert('提示', '分析报告还未匹配客户', [{
+      text: '添加新客户',
+      onPress: () => {
+
+        // @ts-ignore
+        navigation.navigate("customer/add", {
+          creditId: item.id,
+          name: item.name,
+          onCallback: async ({ customerId }: { customerId?: string }) => {
+
+          }
+        });
+      }
+    }, {
+      text: '绑定现有客户',
+      onPress: () => {
+        // @ts-ignore
+        navigation.navigate('customer/select', {
+          q: item.name,
+          onSelect: async (c: CustomerType | undefined) => {
+            if (!c) {
+              return;
+            }
+            return await (new Promise<boolean | void>((resolve) => {
+              Modal.alert(`征信信息:${item.name}`, c.credit_count > 0 ? `用户:${c!.name} 已有分析报告,确定要绑定?` : `确认绑定到客户 ${c!.name}?`, [{
+                text: '取消',
+                style: { color: Colors.secondary.DEFAULT },
+                onPress: () => resolve(false)
+              }, {
+                text: '确认绑定',
+                onPress: async () => {
+                  const l = Toast.loading("请稍候");
+                  try {
+                    await api.post("credit/bind", {
+                      id: item.id,
+                      customer_id: c.id
+                    });
+                    resolve();
+                    Toast.success("绑定成功!");
+                  }catch(e) {
+                    Toast.fail("绑定失败");
+                  }
+                  Toast.remove(l);
+                  
+                }
+              }], () => { resolve(false); return true });
+
+            }));
+            
+          }
+        });
+      }
+    }, {
+      text: '取消',
+      style: { color: Colors.secondary.DEFAULT }
+    }])
+  }, []);
+
+  return (
+    <Animated.ScrollView
+      className="flex-1"
+      automaticallyAdjustContentInsets
+      contentContainerStyle={{ paddingTop: insets.top + 12, paddingBottom: insets.bottom + 44 + 20, paddingHorizontal: 20 }}
+      onScroll={scrollHandler}
+      scrollEventThrottle={16}
+    >
+      <Stack.Screen options={{
+        headerShown: true,
+        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-5 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>
+
+      <View className="mb-3 rounded-2xl border border-outline-variant bg-surface-container-lowest p-4">
+        <Text className="mb-3 text-xs font-bold uppercase tracking-widest text-outline">
+          选择客户
+        </Text>
+        <Pressable
+          className="flex-row items-center rounded-2xl bg-surface-container-low px-4 py-3.5 active:opacity-90"
+          onPress={handleSelectCustomer}
+        >
+          <Ionicons name="person-outline" size={18} color="#94a3b8" />
+          <Text className="ml-3 flex-1 text-base font-medium text-on-surface">
+            {selectedCustomer ? `${selectedCustomer.name}(${selectedCustomer.mobile})` : '无'}
+          </Text>
+          {selectedCustomer && <Pressable className='mx-2' hitSlop={8} onPress={() => setSelectedCustommer(undefined)}>
+            <Ionicons name="close-circle" size={20} color="#94a3b8" />
+          </Pressable>}
+          <Ionicons name="chevron-down" size={18} color="#94a3b8" />
+        </Pressable>
+      </View>
+      <UploadComponent askCustomer customerId={selectedCustomer?.id} onComplete={onUploadComplete} />
+
+
+
+      <SectionHeader title="解析记录" />
+      <View className="gap-3">
+        {loading === true && <ActivityIndicator />}
+        {list?.list?.map((record) => (
+          <Pressable
+            onPress={() => handleItemPress(record)}
+            key={record.id}
+            className="flex-row items-center rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-3.5 active:opacity-90 active:scale-[0.99]"
+          >
+            <View
+              className={clsx('mr-3 h-11 w-11 items-center justify-center rounded-full', {
+                'bg-green-50': record.status === 'completed',
+                'bg-blue-50': record.status === 'analyzing',
+                'bg-warn-50': record.status === 'failed',
+                'bg-error-50': record.status === 'canceled'
+              })}>
+              <Ionicons
+                name={
+                  record.status === 'completed'
+                    ? 'checkmark-circle'
+                    : record.status === 'pending'
+                      ? 'hourglass-outline' :
+                      record.status === 'analyzing' ? 'cloud-circle'
+                        : 'alert-circle'
+                }
+                size={20}
+                color={
+                  record.status === 'completed'
+                    ? Colors.success.DEFAULT
+                    : record.status === 'pending'
+                      ? Colors.tint :
+                      record.status === 'canceled' ?
+                        Colors.error.DEFAULT
+                        : Colors.error.DEFAULT
+                }
+              />
+            </View>
+
+            <View className="flex-1">
+              <View className="mb-1 flex-row items-center">
+                <Text className="text-base font-bold text-on-surface">
+                  {record.name || '客户'}
+                </Text>
+                <View className='flex-1'>
+                  {!!record.customer_id && <Icon size={14} color={Colors.primary.container} name='user' />}
+                </View>
+                <Text className="text-xs text-on-surface-variant">{CreditStatusText[record.status!]}</Text>
+              </View>
+
+
+              {(record.status === 'completed' || record.status === 'pending') && (
+                <View>
+                  <View className="mb-2 flex-row items-center justify-between">
+                    <Text className="text-sm" style={{ color: LevelStatus[record.level || '-']?.bg }}>
+                      {record.status === 'completed' ? record.level : ''}
+                    </Text>
+                    {/* <Text className="text-sm font-bold text-primary">
+                      {record.level||''}
+                    </Text> */}
+                  </View>
+                  <View className="h-1.5 rounded-full bg-surface-container">
+                    <View
+                      className="h-full rounded-full" style={{
+                        backgroundColor: LevelStatus[record.level || '良']?.bg,
+                        width: record.status === 'completed' ? LevelStatus[record.level || '-']?.width + '%' || '50%' as any : 0
+                      }}
+                    />
+                  </View>
+                </View>)}
+              {record.status === 'failed' && (
+                <Text className="text-sm text-warn">解析失败,稍后重试</Text>
+              )}
+              {record.status === 'canceled' && (
+                <Text className="text-sm text-error">解析终止,请联系管理员</Text>
+              )}
+            </View>
+
+          </Pressable>
+        ))}
+      </View>
+    </Animated.ScrollView>
+  );
+}

+ 90 - 50
src/app/(tabs)/customer.tsx

@@ -3,17 +3,18 @@ import { Colors } from '@/constants/theme';
 import type { ListResponse } from '@/utils/api';
 import api from '@/utils/api';
 import { getApiCache } from '@/utils/storage';
-import { ActivityIndicator, Icon, Toast } from '@ant-design/react-native';
+import { ActivityIndicator, Modal, Toast } from '@ant-design/react-native';
 import { Ionicons } from '@expo/vector-icons';
 import clsx from 'clsx';
 import { BlurView } from 'expo-blur';
-import { Link, Stack } from 'expo-router';
+import { Link, Stack, useNavigation } 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';
+import type { Credit } from './credit';
 
-export type CustomerLoanStatus = 'idle' | 'unmatch' | 'completed' | 'all' | 'unknow' | undefined ;
+export type CustomerLoanStatus = 'idle' | 'unmatch' | 'completed' | 'all' | 'unknow' | undefined;
 export const CustomerLoanStatusText: Record<NonNullable<CustomerLoanStatus>, string> = {
   all: '所有',
   idle: '待匹配',
@@ -29,71 +30,110 @@ export type Customer = {
   mobile: string;
   loan_status: CustomerLoanStatus;
   note: string;
-  score?: string;
+  level?: Credit['level'];
   updatetime: string;
   credit_status?: string;
   credit_count: number;
+  credit_id?: string;
 };
 
 const PAGE_SIZE = 15;
 const CACHE_KEY = 'customer_first';
 
 function CustomerCard({ item }: { item: Customer }) {
+  const navigation = useNavigation();
+  const handleCredit = useCallback(async () => {
+    if (item.credit_count > 0) {
+      // @ts-ignore
+      navigation.navigate('credit/detail', {
+        // @ts-ignore
+        id: item.credit_id,
+        // customer_id: item.id,
+        name: item.name
+      });
+      return;
+    }
+    // @ts-ignore
+    navigation.navigate('credit/select', {
+      onSelect: async (cr: Credit) => {
+        if (!cr) {
+          return;
+        }
+        return await new Promise<boolean | void>((resolve) => {
+          Modal.alert(`征信信息:${cr.name}`, `确认绑定到客户 ${item.name}?`, [{
+            text: '取消',
+            style: { color: Colors.secondary.DEFAULT },
+            onPress: () => resolve(false)
+          }, {
+            text: '确认绑定',
+            onPress: async () => {
+              const l = Toast.loading("请稍候");
+              try {
+                await api.post("credit/bind", {
+                  id: cr.id,
+                  customer_id: item.id
+                });
+                resolve();
+                Toast.success("绑定成功!");
+              } catch (e) {
+                Toast.fail("绑定失败");
+              }
+              Toast.remove(l);
+
+            }
+          }], () => { resolve(false); return true });
+        });
+      }
+    });
+  }, [item]);
+
   return (
-    
-      <View
-        className="rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-4 mb-4"
-      >
-        <View className="mb-3 flex-row items-start justify-between gap-3">
-          <View className="flex-1 flex-row items-center gap-3">
-            <View className="h-11 w-11 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-lg font-bold text-on-surface">{item.name}</Text>
-              <Text className="mt-1 text-sm text-on-surface-variant">{item.mobile}</Text>
-            </View>
+
+    <View
+      className="rounded-2xl border border-outline-variant bg-surface-container-lowest px-4 py-4 mb-4"
+    >
+      <View className="mb-3 flex-row items-start justify-between gap-3">
+        <View className="flex-1 flex-row items-center gap-3">
+          <View className="h-11 w-11 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-lg font-bold text-on-surface">{item.name}</Text>
+            <Text className="mt-1 text-sm text-on-surface-variant">{item.mobile}</Text>
           </View>
-          <StatusBadge text={CustomerLoanStatusText[item.loan_status || 'unknow']} variant="secondary" />
         </View>
+        <StatusBadge text={CustomerLoanStatusText[item.loan_status || 'unknow']} variant="secondary" />
+      </View>
 
-        {/* <Text className="mb-2.5 text-sm leading-6 text-on-surface-variant">{item.note}</Text> */}
+      {/* <Text className="mb-2.5 text-sm leading-6 text-on-surface-variant">{item.note}</Text> */}
 
-        <View className="mb-1 flex-row flex-wrap items-center gap-3 pb-2">
+      <View className="mb-1 flex-row flex-wrap items-center gap-3 pb-2">
+        <View className="flex-row items-center gap-1">
+          <Ionicons name="cloud-upload" size={14} color={item.credit_id ? Colors.success.DEFAULT : item.credit_count > 0 ? Colors.primary['fixed-dim'] : ''} />
+          <Text className="text-xs text-on-surface-variant">{item.credit_count}</Text>
+        </View>
+        {item.level ? (
           <View className="flex-row items-center gap-1">
-            <Ionicons name="document-text-outline" size={16} color="#94a3b8" />
-            <Text className="text-xs text-on-surface-variant">{item.loan_status}</Text>
+            <Text className="text-xs text-on-surface-variant"> 评级:{item.level}</Text>
           </View>
-          {item.score ? (
-            <View className="flex-row items-center gap-1">
-              <Ionicons name="analytics-outline" size={16} color="#94a3b8" />
-              <Text className="text-xs text-on-surface-variant">评分 {item.score}</Text>
-            </View>
-          ) : null}
-          <Text className="ml-auto text-xs text-outline">{item.credit_count==0?<Icon name='cloud-upload' size={14} color={Colors.tint} /> : item.credit_status}</Text>
-        </View>
+        ) : null}
+      </View>
 
-        <View className="flex-row gap-3">
-          <Link href={{
-      pathname: '/credit/detail', params: {
-        // id: item.credit_id,
-        customer_id: item.id,
-        name: item.name
-      }
-    }} asChild>
-          <Pressable
-            className="flex-1 items-center rounded-xl bg-primary-container py-2.5 active:opacity-88"
-          >
-            <Text className="text-sm font-bold text-on-primary">征信管理</Text>
-          </Pressable>
-            </Link>
-          <Pressable
-            className="flex-1 items-center rounded-xl bg-surface-container-high py-2.5 active:opacity-88"
-          >
-            <Text className="text-sm font-semibold text-on-surface">编辑资料</Text>
-          </Pressable>
-        </View>
+      <View className="flex-row gap-3">
+
+        <Pressable
+          onPress={handleCredit}
+          className="flex-1 items-center rounded-xl bg-primary-container py-2.5 active:opacity-88"
+        >
+          <Text className="text-sm font-bold text-on-primary">{item.credit_count > 0 ? '征信管理' : '添加征信'}</Text>
+        </Pressable>
+        <Pressable
+          className="flex-1 items-center rounded-xl bg-surface-container-high py-2.5 active:opacity-88"
+        >
+          <Text className="text-sm font-semibold text-on-surface">编辑资料</Text>
+        </Pressable>
       </View>
+    </View>
   );
 }
 

+ 5 - 4
src/app/(tabs)/profile.tsx

@@ -1,12 +1,13 @@
+import { MenuRow } from '@/components/ui/menu-row';
+import { Colors } from '@/constants/theme';
+import api from '@/utils/api';
+import { signOut, useAuthContext } from '@/utils/auth';
+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';
-import { MenuRow } from '@/components/ui/menu-row';
-import api from '@/utils/api';
-import { signOut, useAuthContext } from '@/utils/auth';
-import { useSWC } from '@/utils/cache';
 export default function ProfileScreen() {
   const { setToken } = useAuthContext();
 

+ 37 - 34
src/app/_layout.tsx

@@ -12,6 +12,7 @@ import {
   ThemeProvider,
 } from '@react-navigation/native';
 import * as application from 'expo-application';
+import { BlurView } from 'expo-blur';
 import { useFonts } from 'expo-font';
 import { Image } from 'expo-image';
 import { Stack } from 'expo-router';
@@ -132,41 +133,41 @@ export default function RootLayout() {
       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});
-                    }
+      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 });
                   }
                 }
-              ], () => false);
-            } else {
-              resolve('updateok');
-            }
-          });
-          if (res === 'updateok') {
-            break;
+              }
+            ], () => false);
+          } else {
+            resolve('updateok');
           }
+        });
+        if (res === 'updateok') {
+          break;
         }
-         
-      
+      }
+
+
       setInitlizing(false);
 
     })();
@@ -184,9 +185,11 @@ export default function RootLayout() {
             <Stack screenOptions={{
               headerShown: true,
               headerTransparent: true,
-              headerBlurEffect: 'light',
               headerBackButtonDisplayMode: 'minimal',
-              headerBackground: Platform.OS === 'android' ? () => <View style={{ flex: 1, backgroundColor: 'rgba(255, 255, 255, 0.9)' }} /> : undefined,
+              headerBackground: () => (
+                <BlurView intensity={80} tint="light" style={{ flex: 1 }} />
+              ),
+              // headerBackground: Platform.OS === 'android' ? () => <View style={{ flex: 1, backgroundColor: 'rgba(255, 255, 255, 0.9)' }} /> : undefined,
               // headerLeft: ({ canGoBack }) => canGoBack && <Icon name="arrow-left" size={24} />,
               animation: 'ios_from_right'
             }}>
@@ -196,7 +199,7 @@ export default function RootLayout() {
             </Stack>
           </AuthProvider>}
       </Provider>
-      {(initializing || !fontsLoaded)&& <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>

+ 2 - 17
src/app/credit/detail.tsx

@@ -10,18 +10,7 @@ import { Link, Stack, useLocalSearchParams, useRouter } from "expo-router";
 import { useCallback, useEffect } from "react";
 import { ScrollView, Text, View } from "react-native";
 import { useSafeAreaInsets } from "react-native-safe-area-context";
-
-type Credit = {
-  id: string;
-  customer_id?: string;
-  name?: string;
-  updatetime?: number;
-  tags?: string[];
-  recommend?: string[];
-  suggestions?: string[];
-  level?: string;
-  amount?: string;
-}
+import { LevelStatus, type Credit } from "../(tabs)/credit";
 
 const tagsBg = ["primary", "tertiary", "error", "success"];
 
@@ -115,7 +104,7 @@ export default function CreditDetail() {
         <View className="mb-3 rounded-3xl border border-outline-variant bg-surface-container-lowest px-5 py-6">
           <View className="items-center">
             <View className="h-28 w-28 items-center justify-center rounded-full border-[10px] border-primary-fixed bg-primary-fixed/30">
-              <Text className="text-4xl font-extrabold text-primary">{detail?.level??"??"}</Text>
+              <Text className="text-4xl font-extrabold" style={{color: LevelStatus[detail?.level||'良'].bg}}>{detail?.level??"??"}</Text>
             </View>
             <Text className="mt-4 text-sm text-on-surface-variant">建议可申请额度</Text>
             <Text className="mt-1 text-2xl font-extrabold text-on-surface">...</Text>
@@ -196,10 +185,6 @@ export default function CreditDetail() {
         >
           智能匹配银行产品
         </UIButton>
-        <View className="h-3" />
-        <UIButton type="second" icon="reload" onPress={() => router.back()}>
-          重新上传征信
-        </UIButton>
       </ScrollView>
     </View>
   );

+ 234 - 4
src/app/credit/select.tsx

@@ -1,5 +1,235 @@
-import { View } from "react-native";
+import { StatusBadge } from '@/components/ui/status-badge';
+import { Colors } from '@/constants/theme';
+import type { ListResponse } from '@/utils/api';
+import api from '@/utils/api';
+import { ActivityIndicator, Icon, Toast } from '@ant-design/react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { useRoute } from '@react-navigation/native';
+import clsx from 'clsx';
+import { Link, 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 { Credit, CreditStatus } from '../(tabs)/credit';
 
-export default function SelectScreen() {
-    return <View />;
-}
+const PAGE_SIZE = 15;
+const CreditStatusText: Record<NonNullable<CreditStatus>, string> = {
+
+  completed: '完成',
+  pending: '等待',
+  analyzing: '分析中',
+  failed: '失败',
+  canceled: '取消',
+  all: '所有'
+};
+export default function CreditSelectScreen() {
+    const insets = useSafeAreaInsets();
+    const navigation = useNavigation();
+    const route = useRoute();
+
+
+    // @ts-ignore
+    const {current: currentId, q} = route.params;
+
+    const [searchKey, setSearchKey] = useState(q||'');
+    const [list, setList] = useState<Credit[]>([]);
+    const [loading, setLoading] = useState(true);
+    const [activeStatus, setActiveStatus] = useState<CreditStatus>('completed');
+
+    const startRef = useRef(0);
+    const statusRef = useRef<CreditStatus>('completed');
+    const hasMoreRef = useRef(false);
+    const loadingRef = useRef(false);
+    const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+    const load = useCallback(async (start: number, status: CreditStatus, q?: string) => {
+        if (loadingRef.current) return;
+        loadingRef.current = true;
+        statusRef.current = status;
+        startRef.current = start;
+        setLoading(true);
+        try {
+            const res = await api.post<ListResponse<Credit>>('credit/select', {
+                start,
+                size: PAGE_SIZE,
+                status,
+                q,
+            });
+            if (statusRef.current !== status) return;
+            const next = res?.list ?? [];
+           
+            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, 'all');
+    }, [load]);
+
+    const loadMore = useCallback(() => {
+        if (!hasMoreRef.current || loadingRef.current) return;
+        load(startRef.current, statusRef.current, searchKey);
+    }, [load, searchKey]);
+
+    const handleSelectStatus = useCallback(
+        (status: CreditStatus) => {
+            const next = activeStatus === status ? undefined : status;
+            setActiveStatus(next);
+            load(0, next, searchKey);
+        },
+        [activeStatus, load, searchKey]
+    );
+
+    const handlePick = useCallback( 
+        async (item: Credit) => {
+            // @ts-ignore
+            const res = await route.params?.onSelect?.(item);
+            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, statusRef.current, k);
+        }, 600);
+    }, [load, searchKey]);
+    const handleReset = useCallback(()=> {
+        handleSearch('');
+    }, [handleSearch]);
+
+
+    useEffect(() => {
+        return () => {
+            if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
+        };
+    }, []);
+    const renderItem = useCallback(
+        ({ item }: { item: Credit }) => (
+            <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 active:opacity-90 active:scale-[0.99]"
+            >
+                {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={clsx("text-base font-bold text-on-surface", {
+                        'text-secondary': !item.name
+                    })}>
+                        {item.name||'信息待分析'}
+                    </Text>
+                </View>
+                <StatusBadge text={CreditStatusText[item.status!]} variant={item.status == 'completed' ? 'success' : item.status == 'canceled' ? 'tertiary' : item.status == 'failed' ? 'error' : '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="#94a3b8" />
+                <TextInput
+                    value={searchKey}
+                    onChangeText={handleSearch}
+                    placeholder="搜索客户姓名 / 手机号"
+                    placeholderTextColor="#94a3b8"
+                    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="#94a3b8" />
+                    </Pressable>
+                ) : null}
+            </View>
+
+            <View className="mb-4 flex-row flex-wrap gap-2">
+                {Object.entries(CreditStatusText).map(([key, label]) => {
+                    const active = activeStatus === (key as CreditStatus);
+                    return (
+                        <Pressable
+                            key={key}
+                            onPress={() => handleSelectStatus(key as CreditStatus)}
+                            className={clsx("rounded-md px-1 border border-transparent active:opacity-84", {
+                                 'border-primary-container': active,
+                            })}
+                        >
+                            <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="#94a3b8" />
+                <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: '选择征信', headerRight: ()=><Link className="items-center" href="/(tabs)/credit" replace>
+                <Icon color={Colors.tint} size={17} name="cloud-upload" /></Link> }} />
+            <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>
+    );
+}

+ 20 - 13
src/app/customer/add.tsx

@@ -1,6 +1,10 @@
+import UIButton from "@/components/ui/ui-button";
+import { Colors } from "@/constants/theme";
+import api from "@/utils/api";
 import { DatePicker, Input, Modal, Radio, Toast } from "@ant-design/react-native";
 import { Ionicons } from "@expo/vector-icons";
 import { usePreventRemove, useRoute } from "@react-navigation/native";
+import type { NavigationAction } from "@react-navigation/routers";
 import { Link, Stack, useNavigation } from "expo-router";
 import React, { useCallback, useRef, useState } from "react";
 import {
@@ -14,11 +18,7 @@ import {
   View,
 } from "react-native";
 import { useSafeAreaInsets } from "react-native-safe-area-context";
-import UIButton from "@/components/ui/ui-button";
-import { Colors } from "@/constants/theme";
-import api from "@/utils/api";
 import { UploadScreen } from "../../components/upload";
-import type { NavigationAction } from "@react-navigation/routers";
 
 type CustomerForm = {
   name: string;
@@ -274,15 +274,17 @@ function PickerField({
 export default function AddCustomerScreen() {
   const insets = useSafeAreaInsets();
   const navigation = useNavigation();
-  const [form, setForm] = useState<CustomerForm>(EMPTY_CUSTOMER_FORM);
+
+  const route = useRoute();
+  // @ts-ignore
+  const [form, setForm] = useState<CustomerForm>({...EMPTY_CUSTOMER_FORM, name: route.params?.name||""});
   const [errors, setErrors] = useState<FieldErrors>({});
   const [birthDateSource, setBirthDateSource] = useState<BirthDateSource>("unset");
   const [saving, setSaving] = useState(false);
   const leaveConfirmVisibleRef = useRef(false);
   const savedLeaveRef = useRef(false);
-  const route = useRoute();
   const scrollViewRef = useRef<ScrollView>(null!);
-
+const [customerId, setCustomerId] = useState('');
   const hasDraft = Boolean(
     form.name.trim() ||
     form.mobile.trim() ||
@@ -418,14 +420,15 @@ export default function AddCustomerScreen() {
     const toastKey = Toast.loading("正在保存客户...");
 
     try {
-      const customerId = await api.post("/customer/add", buildCustomerPayload(form));
+      const customerId = await api.post<string>("customer/add", buildCustomerPayload(form));
       Toast.success("客户已保存");
       savedLeaveRef.current = true;
       // setForm(EMPTY_CUSTOMER_FORM);
       // setBirthDateSource("unset");
       // setErrors({});
-      // @ts-ignore
 
+      setCustomerId(customerId);
+      // @ts-ignore
       if (route.params?.onGoBack) {
         // @ts-ignore
         route.params.onGoBack({ customerId });
@@ -459,7 +462,7 @@ export default function AddCustomerScreen() {
 
   const [selectCredit, setSelectCredit] = useState(false);
 
-
+  const [credit, setCredit] = useState(route.params);
   return (
     <View className="flex-1 bg-surface">
       <Stack.Screen options={{ title: "添加客户" }} />
@@ -491,9 +494,13 @@ export default function AddCustomerScreen() {
               </View>
               <View className="flex-1">
                 <Text className="text-base font-bold text-on-surface">提示:</Text>
-                <Text className="mt-1 text-sm leading-6 text-on-surface-variant">
+                {/* @ts-ignore */}
+                {credit.creditId? <Text className="mt-1 text-sm leading-6 text-on-surface-variant">
+                  {/* @ts-ignore */}
+                  添加后,自动绑定 <Text className="text-primary font-bold">{credit.name}</Text> 的征信信息。您可以<Text onPress={()=>setCredit({})} className="text-primary font-bold">取消</Text>绑定。
+                </Text> : <Text className="mt-1 text-sm leading-6 text-on-surface-variant">
                   您可以先<Text className="text-primary font-bold" onPress={() => setSelectCredit(true)}>上传分析征信</Text>,然后从已分析征信中<Link href="/credit/select" asChild><Text className="text-primary font-bold">选择</Text></Link>一条信息以填充客户资料。
-                </Text>
+                </Text>}
               </View>
             </View>
           </View>
@@ -691,7 +698,7 @@ export default function AddCustomerScreen() {
           </UIButton>
         </ScrollView>
       </KeyboardAvoidingView>
-      <UploadScreen visible={selectCredit} onClose={onSelectCredit} />
+      <UploadScreen customerId={customerId} visible={selectCredit} onClose={onSelectCredit} />
     </View>
   );
 }

+ 6 - 9
src/app/customer/select.tsx

@@ -2,7 +2,6 @@ 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';
@@ -14,7 +13,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
 import type { Customer, CustomerLoanStatus } from '../(tabs)/customer';
 
 const PAGE_SIZE = 15;
-const CACHE_KEY = 'customer_first';
 export const CustomerLoanStatusText: Record<NonNullable<CustomerLoanStatus>, string> = {
   idle: '待匹配',
   completed: '已完成',
@@ -29,7 +27,11 @@ export default function CustomerSelectScreen() {
     const navigation = useNavigation();
     const route = useRoute();
 
-    const [searchKey, setSearchKey] = useState('');
+
+    // @ts-ignore
+    const {current: currentId, q} = route.params;
+
+    const [searchKey, setSearchKey] = useState(q||'');
     const [list, setList] = useState<Customer[]>([]);
     const [loading, setLoading] = useState(true);
     const [activeStatus, setActiveStatus] = useState<CustomerLoanStatus>('idle');
@@ -58,9 +60,7 @@ export default function CustomerSelectScreen() {
             });
             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;
@@ -98,7 +98,6 @@ export default function CustomerSelectScreen() {
         async (item: Customer) => {
             // @ts-ignore
             const res = await route.params?.onSelect?.(item);
-            // alert(res);
             if (false === res) {
                 return;
             }
@@ -125,8 +124,6 @@ export default function CustomerSelectScreen() {
             if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
         };
     }, []);
-    // @ts-ignore
-    const currentId = route.params?.current;
     const renderItem = useCallback(
         ({ item }: { item: Customer }) => (
             <Pressable

+ 28 - 26
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,14 +20,6 @@ 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[], customerId?: string) => void;
@@ -53,7 +53,7 @@ function alertPermissionDenied() {
     );
 }
 
-const takePhoto = async (upload: UploadHandler) => {
+const takePhoto = async (upload: UploadHandler, ...args: any[]) => {
     const permission = await requestCameraPermissionsAsync();
     if (!permission.granted) {
         alertPermissionDenied();
@@ -62,7 +62,7 @@ const takePhoto = async (upload: UploadHandler) => {
     const toastKey = Toast.loading('正在打开相机...');
     try {
         const result = await launchCameraAsync({ allowsEditing: false, quality: 0.9 });
-        if (!result.canceled) upload(result.assets);
+        if (!result.canceled) upload(result.assets, ...args);
     } catch (err) {
         console.warn(err);
         Toast.fail('打开相机失败,请重试');
@@ -71,7 +71,7 @@ const takePhoto = async (upload: UploadHandler) => {
     }
 };
 
-const pickImage = async (upload: UploadHandler) => {
+const pickImage = async (upload: UploadHandler, ...args: any[]) => {
     const permission = await requestMediaLibraryPermissionsAsync();
     if (!permission.granted) {
         alertPermissionDenied();
@@ -84,7 +84,7 @@ const pickImage = async (upload: UploadHandler) => {
             allowsEditing: false,
             quality: 0.9,
         });
-        if (!result.canceled) upload(result.assets);
+        if (!result.canceled) upload(result.assets, ...args);
     } catch (err) {
         console.warn(err);
         Toast.fail('打开相册失败,请重试');
@@ -93,14 +93,14 @@ const pickImage = async (upload: UploadHandler) => {
     }
 };
 
-const pickDocument = async (upload: UploadHandler) => {
+const pickDocument = async (upload: UploadHandler, ...args: any[]) => {
     const toastKey = Toast.loading('正在打开文件选择器...');
     try {
         const result = await getDocumentAsync({
             type: DOCUMENT_MIME_TYPES,
             copyToCacheDirectory: true,
         });
-        if (!result.canceled) upload(result.assets);
+        if (!result.canceled) upload(result.assets, ...args);
     } catch (err) {
         console.warn(err);
         Toast.fail('打开文件失败,请重试');
@@ -114,14 +114,16 @@ async function uploadAndCreateCredit(assets: Asset[], customerId?: string) {
     const formData = new FormData();
     formData.append('file', file);
     const attachment = await api.uploadFile<Attachment>('common/upload', { body: formData });
-    await api.post('/credit/create', { att: JSON.stringify(attachment) });
+    await api.post('credit/create', { customer_id: customerId, att: JSON.stringify(attachment) });
 }
 
 export function UploadScreen({
     visible,
     onClose,
+    customerId
 }: {
     visible: boolean;
+    customerId?: string;
     onClose: (isCancel: boolean) => void;
 }) {
     const [state, setState] = useState<UploadState>(0);
@@ -133,7 +135,7 @@ export function UploadScreen({
     const upload = useCallback<UploadHandler>(async (assets) => {
         setState(1);
         try {
-            await uploadAndCreateCredit(assets);
+            await uploadAndCreateCredit(assets, customerId);
             setState(2);
         } catch (err) {
             console.warn(err);
@@ -170,7 +172,7 @@ export function UploadScreen({
                         <Text className="mt-4 ml-4 text-2xl">AI 正在分析</Text>
                     </View>
                     <Text className="mt-4 ml-4 text-2xl">
-                        <Link asChild href="/(tabs)/analytics" onPress={() => onClose(false)}>
+                        <Link asChild href="/(tabs)/credit" onPress={() => onClose(false)}>
                             <Text className="text-primary">分析页</Text>
                         </Link>
                         查看状态和结果
@@ -207,21 +209,21 @@ export function UploadComponent({ askCustomer, onComplete, customerId }: { askCu
                 Toast.fail('上传失败,请重试');
             }
         },
-        [onComplete, customerId]
+        [onComplete]
     );
 
     const handleSelect = useCallback(
         (index: number) => {
             if (Platform.OS === 'ios') {
-                if (index === 0) pickDocument(upload);
+                if (index === 0) pickDocument(upload, customerId);
                 else if (index === 1) pickImage(upload);
                 else if (index === 2) takePhoto(upload);
                 return;
             }
-            if (index === 0) pickDocument(upload);
-            else if (index === 1) takePhoto(upload);
+            if (index === 0) pickDocument(upload, customerId);
+            else if (index === 1) takePhoto(upload, customerId);
         },
-        [upload]
+        [upload, customerId]
     );
 
     const selectFile = useCallback(async () => {
@@ -229,12 +231,12 @@ export function UploadComponent({ askCustomer, onComplete, customerId }: { askCu
             const ask = await new Promise<boolean>((resolve) => {
                 Modal.alert("注意", "您没有选择客户,是否在分析完征信信息后匹配或新增客户?", [{
                     text: "否,先选客户",
-                    onPress: ()=> resolve(false),
+                    onPress: () => resolve(false),
                 }, {
                     text: "是,先分析",
-                    style: {color: Colors.error.DEFAULT},
-                    onPress: ()=> resolve(true),
-                }], ()=> {
+                    style: { color: Colors.error.DEFAULT },
+                    onPress: () => resolve(true),
+                }], () => {
                     resolve(false);
                     return true
                 });