|
|
@@ -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 }) => ({
|