feat(dealer/customer): 实现客户列表的无限滚动和搜索功能- 在客户列表页面添加 InfiniteLoading 组件,实现无限滚动加载- 添加搜索功能,支持按关键词搜索客户
- 优化数据加载逻辑,解决重复请求问题 - 在 Header 组件中增加用户登录状态和信息的检查
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import {useState, useEffect, useCallback} from 'react'
|
import {useState, useEffect, useCallback} from 'react'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {Loading, Space, Tabs, TabPane, Tag, Button} from '@nutui/nutui-react-taro'
|
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button} from '@nutui/nutui-react-taro'
|
||||||
import {Phone} from '@nutui/icons-react-taro'
|
import {Phone} from '@nutui/icons-react-taro'
|
||||||
import type {ShopDealerApply, ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
import type {ShopDealerApply, ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||||||
import {
|
import {
|
||||||
@@ -26,18 +26,23 @@ const CustomerIndex = () => {
|
|||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
|
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
|
||||||
const [searchValue, _] = useState<string>('')
|
const [searchValue, _] = useState<string>('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
|
||||||
// Tab配置
|
// Tab配置
|
||||||
const tabList = getStatusOptions();
|
const tabList = getStatusOptions();
|
||||||
|
|
||||||
|
|
||||||
// 获取客户数据
|
// 获取客户数据
|
||||||
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus) => {
|
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||||
|
|
||||||
// 构建API参数,根据状态筛选
|
// 构建API参数,根据状态筛选
|
||||||
const params: any = {
|
const params: any = {
|
||||||
type: 0
|
type: 0,
|
||||||
|
page: currentPage
|
||||||
};
|
};
|
||||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||||||
if (applyStatus !== undefined) {
|
if (applyStatus !== undefined) {
|
||||||
@@ -45,21 +50,45 @@ const CustomerIndex = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const res = await pageShopDealerApply(params);
|
const res = await pageShopDealerApply(params);
|
||||||
if (res?.list) {
|
let newList: CustomerUser[];
|
||||||
|
|
||||||
|
if (res?.list && res.list.length > 0) {
|
||||||
// 正确映射状态
|
// 正确映射状态
|
||||||
const mappedList = res.list.map(customer => ({
|
const mappedList = res.list.map(customer => ({
|
||||||
...customer,
|
...customer,
|
||||||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10)
|
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10)
|
||||||
}));
|
}));
|
||||||
setList(mappedList);
|
|
||||||
|
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||||||
|
newList = resetPage ? mappedList : list.concat(mappedList);
|
||||||
|
|
||||||
|
// 正确判断是否还有更多数据
|
||||||
|
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||||||
|
setHasMore(hasMoreData);
|
||||||
|
} else {
|
||||||
|
newList = resetPage ? [] : list;
|
||||||
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
setPage(currentPage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取客户数据失败:', error);
|
console.error('获取客户数据失败:', error);
|
||||||
|
Taro.showToast({
|
||||||
|
title: '加载失败,请重试',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
|
const reloadMore = async () => {
|
||||||
|
if (loading || !hasMore) return; // 防止重复加载
|
||||||
|
const nextPage = page + 1;
|
||||||
|
await fetchCustomerData(activeTab, false, nextPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
||||||
const getFilteredList = () => {
|
const getFilteredList = () => {
|
||||||
@@ -122,26 +151,34 @@ const CustomerIndex = () => {
|
|||||||
title: '取消成功',
|
title: '取消成功',
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
});
|
});
|
||||||
fetchCustomerData().then();
|
// 重新加载当前tab的数据
|
||||||
|
setList([]);
|
||||||
|
setPage(1);
|
||||||
|
setHasMore(true);
|
||||||
|
fetchCustomerData(activeTab, true).then();
|
||||||
fetchStatusCounts().then();
|
fetchStatusCounts().then();
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCustomerData().then();
|
fetchCustomerData(activeTab, true).then();
|
||||||
fetchStatusCounts().then();
|
fetchStatusCounts().then();
|
||||||
}, [fetchCustomerData, fetchStatusCounts]);
|
}, []);
|
||||||
|
|
||||||
// 当activeTab变化时重新获取数据
|
// 当activeTab变化时重新获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCustomerData(activeTab);
|
setList([]); // 清空列表
|
||||||
|
setPage(1); // 重置页码
|
||||||
|
setHasMore(true); // 重置加载状态
|
||||||
|
fetchCustomerData(activeTab, true);
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
// 渲染客户项
|
// 渲染客户项
|
||||||
const renderCustomerItem = (customer: CustomerUser) => (
|
const renderCustomerItem = (customer: CustomerUser) => (
|
||||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||||
<View className="flex items-center mb-3" onClick={() => navTo(`/dealer/customer/add?id=${customer.applyId}`, true)}>
|
<View className="flex items-center mb-3"
|
||||||
|
onClick={() => navTo(`/dealer/customer/add?id=${customer.applyId}`, true)}>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<View className="flex items-center justify-between mb-1">
|
<View className="flex items-center justify-between mb-1">
|
||||||
<Text className="font-semibold text-gray-800 mr-2">
|
<Text className="font-semibold text-gray-800 mr-2">
|
||||||
@@ -192,26 +229,49 @@ const CustomerIndex = () => {
|
|||||||
const renderCustomerList = () => {
|
const renderCustomerList = () => {
|
||||||
const filteredList = getFilteredList();
|
const filteredList = getFilteredList();
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<Space className="flex items-center justify-center py-8">
|
<View className="p-4" style={{
|
||||||
<Loading/>
|
height: '75vh',
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
overflowY: 'auto',
|
||||||
</Space>
|
overflowX: 'hidden'
|
||||||
);
|
}}>
|
||||||
|
<InfiniteLoading
|
||||||
|
target="scroll"
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={reloadMore}
|
||||||
|
onScroll={() => {
|
||||||
|
// 滚动事件处理
|
||||||
|
}}
|
||||||
|
onScrollToUpper={() => {
|
||||||
|
// 滚动到顶部事件处理
|
||||||
|
}}
|
||||||
|
loadingText={
|
||||||
|
<>
|
||||||
|
加载中...
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
loadMoreText={
|
||||||
if (filteredList.length === 0) {
|
filteredList.length === 0 ? (
|
||||||
return (
|
<Empty
|
||||||
<View className="flex items-center justify-center py-8">
|
style={{backgroundColor: 'transparent'}}
|
||||||
<Text className="text-gray-500">暂无客户数据</Text>
|
description={loading ? "加载中..." : "暂无客户数据"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View className={'h-12 flex items-center justify-center'}>
|
||||||
|
<Text className="text-gray-500 text-sm">没有更多了</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
return (
|
{loading && filteredList.length === 0 ? (
|
||||||
<View className="p-4">
|
<View className="flex items-center justify-center py-8">
|
||||||
{filteredList.map(renderCustomerItem)}
|
<Loading/>
|
||||||
|
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
filteredList.map(renderCustomerItem)
|
||||||
|
)}
|
||||||
|
</InfiniteLoading>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {useState, useEffect, useCallback} from 'react'
|
import {useState, useEffect, useCallback} from 'react'
|
||||||
import {View, Text} from '@tarojs/components'
|
import {View, Text} from '@tarojs/components'
|
||||||
import {Loading, Space, SearchBar} from '@nutui/nutui-react-taro'
|
import Taro from '@tarojs/taro'
|
||||||
|
import {Loading, InfiniteLoading, Empty, Space, SearchBar} from '@nutui/nutui-react-taro'
|
||||||
import type {ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
import type {ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||||||
import {
|
import {
|
||||||
CustomerStatus,
|
CustomerStatus,
|
||||||
@@ -17,62 +18,95 @@ const CustomerTrading = () => {
|
|||||||
const [list, setList] = useState<CustomerUser[]>([])
|
const [list, setList] = useState<CustomerUser[]>([])
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [searchValue, setSearchValue] = useState<string>('')
|
const [searchValue, setSearchValue] = useState<string>('')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
|
||||||
// 获取客户数据
|
// 获取客户数据
|
||||||
const fetchCustomerData = useCallback(async () => {
|
const fetchCustomerData = useCallback(async (resetPage = false, targetPage?: number, searchKeyword?: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||||
|
|
||||||
// 构建API参数,根据状态筛选
|
// 构建API参数,根据状态筛选
|
||||||
const params: any = {
|
const params: any = {
|
||||||
type: 3
|
type: 3,
|
||||||
|
page: currentPage
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加搜索关键词
|
||||||
|
if (searchKeyword && searchKeyword.trim()) {
|
||||||
|
params.keywords = searchKeyword.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const res = await pageShopDealerApply(params);
|
const res = await pageShopDealerApply(params);
|
||||||
if (res?.list) {
|
let newList: CustomerUser[];
|
||||||
|
|
||||||
|
if (res?.list && res.list.length > 0) {
|
||||||
// 正确映射状态
|
// 正确映射状态
|
||||||
const mappedList = res.list.map(customer => ({
|
const mappedList = res.list.map(customer => ({
|
||||||
...customer,
|
...customer,
|
||||||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10)
|
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10)
|
||||||
}));
|
}));
|
||||||
setList(mappedList);
|
|
||||||
|
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||||||
|
newList = resetPage ? mappedList : list.concat(mappedList);
|
||||||
|
|
||||||
|
// 正确判断是否还有更多数据
|
||||||
|
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||||||
|
setHasMore(hasMoreData);
|
||||||
|
} else {
|
||||||
|
newList = resetPage ? [] : list;
|
||||||
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setList(newList);
|
||||||
|
setPage(currentPage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取客户数据失败:', error);
|
console.error('获取客户数据失败:', error);
|
||||||
|
Taro.showToast({
|
||||||
|
title: '加载失败,请重试',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const reloadMore = async () => {
|
||||||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
if (loading || !hasMore) return; // 防止重复加载
|
||||||
const getFilteredList = () => {
|
const nextPage = page + 1;
|
||||||
let filteredList = list;
|
await fetchCustomerData(false, nextPage, searchValue);
|
||||||
|
|
||||||
// 按搜索关键词筛选
|
|
||||||
if (searchValue.trim()) {
|
|
||||||
const keyword = searchValue.trim().toLowerCase();
|
|
||||||
filteredList = filteredList.filter(customer =>
|
|
||||||
(customer.realName && customer.realName.toLowerCase().includes(keyword)) ||
|
|
||||||
(customer.dealerName && customer.dealerName.toLowerCase().includes(keyword)) ||
|
|
||||||
(customer.dealerCode && customer.dealerCode.toLowerCase().includes(keyword)) ||
|
|
||||||
(customer.mobile && customer.mobile.includes(keyword)) ||
|
|
||||||
(customer.userId && customer.userId.toString().includes(keyword))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredList;
|
|
||||||
|
// 获取列表数据(现在使用服务端搜索,不需要客户端过滤)
|
||||||
|
const getFilteredList = () => {
|
||||||
|
return list;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCustomerData().then();
|
fetchCustomerData(true).then();
|
||||||
}, [fetchCustomerData]);
|
|
||||||
|
|
||||||
// 当activeTab变化时重新获取数据
|
|
||||||
useEffect(() => {
|
|
||||||
fetchCustomerData().then();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 搜索处理函数
|
||||||
|
const handleSearch = (keyword: string) => {
|
||||||
|
setSearchValue(keyword);
|
||||||
|
setList([]);
|
||||||
|
setPage(1);
|
||||||
|
setHasMore(true);
|
||||||
|
fetchCustomerData(true, 1, keyword);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空搜索
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
setSearchValue('');
|
||||||
|
setList([]);
|
||||||
|
setPage(1);
|
||||||
|
setHasMore(true);
|
||||||
|
fetchCustomerData(true, 1, '');
|
||||||
|
};
|
||||||
|
|
||||||
// 渲染客户项
|
// 渲染客户项
|
||||||
const renderCustomerItem = (customer: CustomerUser) => (
|
const renderCustomerItem = (customer: CustomerUser) => (
|
||||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||||
@@ -91,7 +125,6 @@ const CustomerTrading = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -99,26 +132,49 @@ const CustomerTrading = () => {
|
|||||||
const renderCustomerList = () => {
|
const renderCustomerList = () => {
|
||||||
const filteredList = getFilteredList();
|
const filteredList = getFilteredList();
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<Space className="flex items-center justify-center py-8">
|
<View className="p-4" style={{
|
||||||
<Loading/>
|
height: '75vh',
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
overflowY: 'auto',
|
||||||
</Space>
|
overflowX: 'hidden'
|
||||||
);
|
}}>
|
||||||
|
<InfiniteLoading
|
||||||
|
target="scroll"
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={reloadMore}
|
||||||
|
onScroll={() => {
|
||||||
|
// 滚动事件处理
|
||||||
|
}}
|
||||||
|
onScrollToUpper={() => {
|
||||||
|
// 滚动到顶部事件处理
|
||||||
|
}}
|
||||||
|
loadingText={
|
||||||
|
<>
|
||||||
|
加载中...
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
loadMoreText={
|
||||||
if (filteredList.length === 0) {
|
filteredList.length === 0 ? (
|
||||||
return (
|
<Empty
|
||||||
<View className="flex items-center justify-center py-8">
|
style={{backgroundColor: 'transparent'}}
|
||||||
<Text className="text-gray-500">暂无客户数据</Text>
|
description={loading ? "加载中..." : "暂无客户数据"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View className={'h-12 flex items-center justify-center'}>
|
||||||
|
<Text className="text-gray-500 text-sm">没有更多了</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
return (
|
{loading && filteredList.length === 0 ? (
|
||||||
<View className="p-4">
|
<View className="flex items-center justify-center py-8">
|
||||||
{filteredList.map(renderCustomerItem)}
|
<Loading/>
|
||||||
|
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
filteredList.map(renderCustomerItem)
|
||||||
|
)}
|
||||||
|
</InfiniteLoading>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -131,8 +187,8 @@ const CustomerTrading = () => {
|
|||||||
placeholder="请输入搜索关键词"
|
placeholder="请输入搜索关键词"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(value) => setSearchValue(value)}
|
onChange={(value) => setSearchValue(value)}
|
||||||
onSearch={(value) => setSearchValue(value)}
|
onSearch={(value) => handleSearch(value)}
|
||||||
onClear={() => setSearchValue('')}
|
onClear={() => handleClearSearch()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,25 @@ const Header = (props: any) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 检查用户是否已登录并且有头像和昵称
|
||||||
|
if (isLoggedIn) {
|
||||||
|
const hasAvatar = user?.avatar || Taro.getStorageSync('Avatar');
|
||||||
|
const hasNickname = user?.nickname || Taro.getStorageSync('Nickname');
|
||||||
|
|
||||||
|
if (!hasAvatar || !hasNickname) {
|
||||||
|
Taro.showToast({
|
||||||
|
title: '您还没有上传头像和昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
Taro.navigateTo({
|
||||||
|
url: '/user/profile/profile'
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果已登录,获取最新用户信息
|
// 如果已登录,获取最新用户信息
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
try {
|
try {
|
||||||
@@ -184,7 +203,8 @@ const Header = (props: any) => {
|
|||||||
}}
|
}}
|
||||||
left={
|
left={
|
||||||
isLoggedIn ? (
|
isLoggedIn ? (
|
||||||
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}} onClick={() => navTo(`/user/profile/profile`,true)}>
|
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}}
|
||||||
|
onClick={() => navTo(`/user/profile/profile`, true)}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="22"
|
size="22"
|
||||||
src={user?.avatar}
|
src={user?.avatar}
|
||||||
|
|||||||
Reference in New Issue
Block a user