|
|
@@ -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>
|
|
|
+ );
|
|
|
+}
|