| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- 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>
- );
- }
|