feat(dealer/customer): 实现客户列表的无限滚动和搜索功能- 在客户列表页面添加 InfiniteLoading 组件,实现无限滚动加载- 添加搜索功能,支持按关键词搜索客户
- 优化数据加载逻辑,解决重复请求问题 - 在 Header 组件中增加用户登录状态和信息的检查
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
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 type {ShopDealerApply, ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||||
import {
|
||||
@@ -26,18 +26,23 @@ const CustomerIndex = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
|
||||
const [searchValue, _] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
|
||||
// Tab配置
|
||||
const tabList = getStatusOptions();
|
||||
|
||||
|
||||
// 获取客户数据
|
||||
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus) => {
|
||||
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 0
|
||||
type: 0,
|
||||
page: currentPage
|
||||
};
|
||||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||||
if (applyStatus !== undefined) {
|
||||
@@ -45,21 +50,45 @@ const CustomerIndex = () => {
|
||||
}
|
||||
|
||||
const res = await pageShopDealerApply(params);
|
||||
if (res?.list) {
|
||||
let newList: CustomerUser[];
|
||||
|
||||
if (res?.list && res.list.length > 0) {
|
||||
// 正确映射状态
|
||||
const mappedList = res.list.map(customer => ({
|
||||
...customer,
|
||||
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) {
|
||||
console.error('获取客户数据失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTab]);
|
||||
|
||||
const reloadMore = async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
await fetchCustomerData(activeTab, false, nextPage);
|
||||
}
|
||||
|
||||
|
||||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
||||
const getFilteredList = () => {
|
||||
@@ -93,10 +122,10 @@ const CustomerIndex = () => {
|
||||
try {
|
||||
// 并行获取各状态的数量
|
||||
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
||||
pageShopDealerApply({type:0}), // 全部
|
||||
pageShopDealerApply({applyStatus: 10,type:0}), // 跟进中
|
||||
pageShopDealerApply({applyStatus: 20,type:0}), // 已签约
|
||||
pageShopDealerApply({applyStatus: 30,type:0}) // 已取消
|
||||
pageShopDealerApply({type: 0}), // 全部
|
||||
pageShopDealerApply({applyStatus: 10, type: 0}), // 跟进中
|
||||
pageShopDealerApply({applyStatus: 20, type: 0}), // 已签约
|
||||
pageShopDealerApply({applyStatus: 30, type: 0}) // 已取消
|
||||
]);
|
||||
|
||||
setStatusCounts({
|
||||
@@ -122,26 +151,34 @@ const CustomerIndex = () => {
|
||||
title: '取消成功',
|
||||
icon: 'success'
|
||||
});
|
||||
fetchCustomerData().then();
|
||||
// 重新加载当前tab的数据
|
||||
setList([]);
|
||||
setPage(1);
|
||||
setHasMore(true);
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
})
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
fetchCustomerData().then();
|
||||
fetchCustomerData(activeTab, true).then();
|
||||
fetchStatusCounts().then();
|
||||
}, [fetchCustomerData, fetchStatusCounts]);
|
||||
}, []);
|
||||
|
||||
// 当activeTab变化时重新获取数据
|
||||
useEffect(() => {
|
||||
fetchCustomerData(activeTab);
|
||||
setList([]); // 清空列表
|
||||
setPage(1); // 重置页码
|
||||
setHasMore(true); // 重置加载状态
|
||||
fetchCustomerData(activeTab, true);
|
||||
}, [activeTab]);
|
||||
|
||||
// 渲染客户项
|
||||
const renderCustomerItem = (customer: CustomerUser) => (
|
||||
<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 items-center justify-between mb-1">
|
||||
<Text className="font-semibold text-gray-800 mr-2">
|
||||
@@ -192,26 +229,49 @@ const CustomerIndex = () => {
|
||||
const renderCustomerList = () => {
|
||||
const filteredList = getFilteredList();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Space className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredList.length === 0) {
|
||||
return (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Text className="text-gray-500">暂无客户数据</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="p-4">
|
||||
{filteredList.map(renderCustomerItem)}
|
||||
<View className="p-4" style={{
|
||||
height: '75vh',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}}>
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
// 滚动事件处理
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
// 滚动到顶部事件处理
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中...
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
filteredList.length === 0 ? (
|
||||
<Empty
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
description={loading ? "加载中..." : "暂无客户数据"}
|
||||
/>
|
||||
) : (
|
||||
<View className={'h-12 flex items-center justify-center'}>
|
||||
<Text className="text-gray-500 text-sm">没有更多了</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{loading && filteredList.length === 0 ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
filteredList.map(renderCustomerItem)
|
||||
)}
|
||||
</InfiniteLoading>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {useState, useEffect, useCallback} from 'react'
|
||||
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 {
|
||||
CustomerStatus,
|
||||
@@ -17,62 +18,95 @@ const CustomerTrading = () => {
|
||||
const [list, setList] = useState<CustomerUser[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
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);
|
||||
try {
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
|
||||
// 构建API参数,根据状态筛选
|
||||
const params: any = {
|
||||
type: 3
|
||||
type: 3,
|
||||
page: currentPage
|
||||
};
|
||||
|
||||
// 添加搜索关键词
|
||||
if (searchKeyword && searchKeyword.trim()) {
|
||||
params.keywords = searchKeyword.trim();
|
||||
}
|
||||
|
||||
const res = await pageShopDealerApply(params);
|
||||
if (res?.list) {
|
||||
let newList: CustomerUser[];
|
||||
|
||||
if (res?.list && res.list.length > 0) {
|
||||
// 正确映射状态
|
||||
const mappedList = res.list.map(customer => ({
|
||||
...customer,
|
||||
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) {
|
||||
console.error('获取客户数据失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reloadMore = async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
await fetchCustomerData(false, nextPage, searchValue);
|
||||
}
|
||||
|
||||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
||||
|
||||
// 获取列表数据(现在使用服务端搜索,不需要客户端过滤)
|
||||
const getFilteredList = () => {
|
||||
let filteredList = list;
|
||||
|
||||
// 按搜索关键词筛选
|
||||
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;
|
||||
return list;
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
fetchCustomerData().then();
|
||||
}, [fetchCustomerData]);
|
||||
|
||||
// 当activeTab变化时重新获取数据
|
||||
useEffect(() => {
|
||||
fetchCustomerData().then();
|
||||
fetchCustomerData(true).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) => (
|
||||
<View key={customer.userId} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
||||
@@ -91,7 +125,6 @@ const CustomerTrading = () => {
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -99,26 +132,49 @@ const CustomerTrading = () => {
|
||||
const renderCustomerList = () => {
|
||||
const filteredList = getFilteredList();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Space className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredList.length === 0) {
|
||||
return (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Text className="text-gray-500">暂无客户数据</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="p-4">
|
||||
{filteredList.map(renderCustomerItem)}
|
||||
<View className="p-4" style={{
|
||||
height: '75vh',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}}>
|
||||
<InfiniteLoading
|
||||
target="scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
onScroll={() => {
|
||||
// 滚动事件处理
|
||||
}}
|
||||
onScrollToUpper={() => {
|
||||
// 滚动到顶部事件处理
|
||||
}}
|
||||
loadingText={
|
||||
<>
|
||||
加载中...
|
||||
</>
|
||||
}
|
||||
loadMoreText={
|
||||
filteredList.length === 0 ? (
|
||||
<Empty
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
description={loading ? "加载中..." : "暂无客户数据"}
|
||||
/>
|
||||
) : (
|
||||
<View className={'h-12 flex items-center justify-center'}>
|
||||
<Text className="text-gray-500 text-sm">没有更多了</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{loading && filteredList.length === 0 ? (
|
||||
<View className="flex items-center justify-center py-8">
|
||||
<Loading/>
|
||||
<Text className="text-gray-500 mt-2 ml-2">加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
filteredList.map(renderCustomerItem)
|
||||
)}
|
||||
</InfiniteLoading>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -131,8 +187,8 @@ const CustomerTrading = () => {
|
||||
placeholder="请输入搜索关键词"
|
||||
value={searchValue}
|
||||
onChange={(value) => setSearchValue(value)}
|
||||
onSearch={(value) => setSearchValue(value)}
|
||||
onClear={() => setSearchValue('')}
|
||||
onSearch={(value) => handleSearch(value)}
|
||||
onClear={() => handleClearSearch()}
|
||||
/>
|
||||
</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) {
|
||||
try {
|
||||
@@ -96,7 +115,7 @@ const Header = (props: any) => {
|
||||
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
|
||||
const {code, encryptedData, iv} = detail
|
||||
Taro.login({
|
||||
success: (loginRes)=> {
|
||||
success: (loginRes) => {
|
||||
if (code) {
|
||||
Taro.request({
|
||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
||||
@@ -184,7 +203,8 @@ const Header = (props: any) => {
|
||||
}}
|
||||
left={
|
||||
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
|
||||
size="22"
|
||||
src={user?.avatar}
|
||||
|
||||
Reference in New Issue
Block a user