feat(dealer/customer): 实现客户列表的无限滚动和搜索功能- 在客户列表页面添加 InfiniteLoading 组件,实现无限滚动加载- 添加搜索功能,支持按关键词搜索客户

- 优化数据加载逻辑,解决重复请求问题
- 在 Header 组件中增加用户登录状态和信息的检查
This commit is contained in:
2025-09-06 10:29:35 +08:00
parent 42be544acd
commit 6f799e6775
3 changed files with 220 additions and 84 deletions

View File

@@ -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 = () => {
@@ -93,10 +122,10 @@ const CustomerIndex = () => {
try { try {
// 并行获取各状态的数量 // 并行获取各状态的数量
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([ const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
pageShopDealerApply({type:0}), // 全部 pageShopDealerApply({type: 0}), // 全部
pageShopDealerApply({applyStatus: 10,type:0}), // 跟进中 pageShopDealerApply({applyStatus: 10, type: 0}), // 跟进中
pageShopDealerApply({applyStatus: 20,type:0}), // 已签约 pageShopDealerApply({applyStatus: 20, type: 0}), // 已签约
pageShopDealerApply({applyStatus: 30,type:0}) // 已取消 pageShopDealerApply({applyStatus: 30, type: 0}) // 已取消
]); ]);
setStatusCounts({ setStatusCounts({
@@ -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>
); );
}; };

View File

@@ -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>

View File

@@ -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 {
@@ -96,7 +115,7 @@ const Header = (props: any) => {
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => { const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
const {code, encryptedData, iv} = detail const {code, encryptedData, iv} = detail
Taro.login({ Taro.login({
success: (loginRes)=> { success: (loginRes) => {
if (code) { if (code) {
Taro.request({ Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone', url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
@@ -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}