- 新增客户跟进记录模型定义 - 实现客户跟进记录的增删改查接口 - 在客户详情页添加跟进记录提交功能 -优化文章列表组件的UI展示效果 - 调整分享功能回调参数处理方式
591 lines
19 KiB
TypeScript
591 lines
19 KiB
TypeScript
import {useState, useEffect, useCallback} from 'react'
|
||
import {View, Text} from '@tarojs/components'
|
||
import Taro, {useDidShow} from '@tarojs/taro'
|
||
import {Loading, InfiniteLoading, Empty, Space, Tabs, TabPane, Tag, Button, SearchBar} from '@nutui/nutui-react-taro'
|
||
import {Phone, AngleDoubleLeft} from '@nutui/icons-react-taro'
|
||
import type {ShopDealerApply, ShopDealerApply as UserType} from "@/api/shop/shopDealerApply/model";
|
||
import {
|
||
CustomerStatus,
|
||
getStatusText,
|
||
getStatusTagType,
|
||
getStatusOptions,
|
||
mapApplyStatusToCustomerStatus,
|
||
mapCustomerStatusToApplyStatus
|
||
} from '@/utils/customerStatus';
|
||
import FixedButton from "@/components/FixedButton";
|
||
import navTo from "@/utils/common";
|
||
import {pageShopDealerApply, removeShopDealerApply, updateShopDealerApply} from "@/api/shop/shopDealerApply";
|
||
import {addShopDealerRecord} from "@/api/shop/shopDealerRecord";
|
||
|
||
// 扩展User类型,添加客户状态和保护天数
|
||
interface CustomerUser extends UserType {
|
||
customerStatus?: CustomerStatus;
|
||
protectDays?: number; // 剩余保护天数
|
||
}
|
||
|
||
const CustomerIndex = () => {
|
||
const [list, setList] = useState<CustomerUser[]>([])
|
||
const [loading, setLoading] = useState<boolean>(false)
|
||
const [activeTab, setActiveTab] = useState<CustomerStatus>('all')
|
||
const [searchValue, setSearchValue] = useState<string>('')
|
||
const [displaySearchValue, setDisplaySearchValue] = useState<string>('')
|
||
const [page, setPage] = useState(1)
|
||
const [hasMore, setHasMore] = useState(true)
|
||
|
||
// Tab配置
|
||
const tabList = getStatusOptions();
|
||
|
||
// 复制手机号
|
||
const copyPhone = (phone: string) => {
|
||
Taro.setClipboardData({
|
||
data: phone,
|
||
success: () => {
|
||
Taro.showToast({
|
||
title: '手机号已复制',
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
// 一键拨打
|
||
const makePhoneCall = (phone: string) => {
|
||
Taro.makePhoneCall({
|
||
phoneNumber: phone,
|
||
fail: () => {
|
||
Taro.showToast({
|
||
title: '拨打取消',
|
||
icon: 'error'
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
// 编辑跟进情况
|
||
const editComments = (customer: CustomerUser) => {
|
||
Taro.showModal({
|
||
title: '编辑跟进情况',
|
||
// @ts-ignore
|
||
editable: true,
|
||
placeholderText: '请输入跟进情况',
|
||
content: customer.comments || '',
|
||
success: async (res) => {
|
||
// @ts-ignore
|
||
if (res.confirm && res.content !== undefined) {
|
||
try {
|
||
// 添加跟进记录
|
||
await addShopDealerRecord({
|
||
dealerId: customer.userId,
|
||
// @ts-ignore
|
||
content: res.content.trim()
|
||
})
|
||
// 更新跟进情况
|
||
await updateShopDealerApply({
|
||
...customer,
|
||
// @ts-ignore
|
||
comments: res.content.trim()
|
||
});
|
||
|
||
Taro.showToast({
|
||
title: '更新成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
// 刷新列表
|
||
setList([]);
|
||
setPage(1);
|
||
setHasMore(true);
|
||
fetchCustomerData(activeTab, true);
|
||
} catch (error) {
|
||
console.error('更新跟进情况失败:', error);
|
||
Taro.showToast({
|
||
title: '更新失败,请重试',
|
||
icon: 'error'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 计算剩余保护天数(基于过期时间)
|
||
const calculateProtectDays = (expirationTime?: string, applyTime?: string): number => {
|
||
try {
|
||
// 优先使用过期时间字段
|
||
if (expirationTime) {
|
||
const expDate = new Date(expirationTime.replace(' ', 'T'));
|
||
const now = new Date();
|
||
|
||
// 计算剩余毫秒数
|
||
const remainingMs = expDate.getTime() - now.getTime();
|
||
|
||
// 转换为天数,向上取整
|
||
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24));
|
||
|
||
console.log('=== 基于过期时间计算 ===');
|
||
console.log('过期时间:', expirationTime);
|
||
console.log('当前时间:', now.toLocaleString());
|
||
console.log('剩余天数:', remainingDays);
|
||
console.log('======================');
|
||
|
||
return Math.max(0, remainingDays);
|
||
}
|
||
|
||
// 如果没有过期时间,回退到基于申请时间计算
|
||
if (!applyTime) return 0;
|
||
|
||
const protectionPeriod = 7; // 保护期7天
|
||
|
||
// 解析申请时间
|
||
let applyDate: Date;
|
||
if (applyTime.includes('T')) {
|
||
applyDate = new Date(applyTime);
|
||
} else {
|
||
applyDate = new Date(applyTime.replace(' ', 'T'));
|
||
}
|
||
|
||
// 获取当前时间
|
||
const now = new Date();
|
||
|
||
// 只比较日期部分,忽略时间
|
||
const applyDateOnly = new Date(applyDate.getFullYear(), applyDate.getMonth(), applyDate.getDate());
|
||
const currentDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||
|
||
// 计算已经过去的天数
|
||
const timeDiff = currentDateOnly.getTime() - applyDateOnly.getTime();
|
||
const daysPassed = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
|
||
|
||
// 计算剩余保护天数
|
||
const remainingDays = protectionPeriod - daysPassed;
|
||
|
||
console.log('=== 基于申请时间计算 ===');
|
||
console.log('申请时间:', applyTime);
|
||
console.log('已过去天数:', daysPassed);
|
||
console.log('剩余保护天数:', remainingDays);
|
||
console.log('======================');
|
||
|
||
return Math.max(0, remainingDays);
|
||
} catch (error) {
|
||
console.error('日期计算错误:', error);
|
||
return 0;
|
||
}
|
||
};
|
||
|
||
|
||
// 获取客户数据
|
||
const fetchCustomerData = useCallback(async (statusFilter?: CustomerStatus, resetPage = false, targetPage?: number) => {
|
||
setLoading(true);
|
||
try {
|
||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||
|
||
// 构建API参数,根据状态筛选
|
||
const params: any = {
|
||
type: 4,
|
||
page: currentPage
|
||
};
|
||
const applyStatus = mapCustomerStatusToApplyStatus(statusFilter || activeTab);
|
||
if (applyStatus !== undefined) {
|
||
params.applyStatus = applyStatus;
|
||
}
|
||
|
||
const res = await pageShopDealerApply(params);
|
||
|
||
if (res?.list && res.list.length > 0) {
|
||
// 正确映射状态并计算保护天数
|
||
const mappedList = res.list.map(customer => ({
|
||
...customer,
|
||
customerStatus: mapApplyStatusToCustomerStatus(customer.applyStatus || 10),
|
||
protectDays: calculateProtectDays(customer.expirationTime, customer.applyTime || customer.createTime || '')
|
||
}));
|
||
|
||
// 如果是重置页面或第一页,直接设置新数据;否则追加数据
|
||
if (resetPage || currentPage === 1) {
|
||
setList(mappedList);
|
||
} else {
|
||
setList(prevList => prevList.concat(mappedList));
|
||
}
|
||
|
||
// 正确判断是否还有更多数据
|
||
const hasMoreData = res.list.length >= 10; // 假设每页10条数据
|
||
setHasMore(hasMoreData);
|
||
} else {
|
||
if (resetPage || currentPage === 1) {
|
||
setList([]);
|
||
}
|
||
setHasMore(false);
|
||
}
|
||
|
||
setPage(currentPage);
|
||
} catch (error) {
|
||
console.error('获取客户数据失败:', error);
|
||
Taro.showToast({
|
||
title: '加载失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [activeTab, page]);
|
||
|
||
const reloadMore = async () => {
|
||
if (loading || !hasMore) return; // 防止重复加载
|
||
const nextPage = page + 1;
|
||
await fetchCustomerData(activeTab, false, nextPage);
|
||
}
|
||
|
||
|
||
// 防抖搜索功能
|
||
useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
setDisplaySearchValue(searchValue);
|
||
}, 300); // 300ms 防抖
|
||
|
||
return () => clearTimeout(timer);
|
||
}, [searchValue]);
|
||
|
||
// 根据搜索条件筛选数据(状态筛选已在API层面处理)
|
||
const getFilteredList = () => {
|
||
let filteredList = list;
|
||
|
||
// 按搜索关键词筛选
|
||
if (displaySearchValue.trim()) {
|
||
const keyword = displaySearchValue.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 [statusCounts, setStatusCounts] = useState({
|
||
all: 0,
|
||
pending: 0,
|
||
signed: 0,
|
||
cancelled: 0
|
||
});
|
||
|
||
// 获取所有状态的统计数量
|
||
const fetchStatusCounts = useCallback(async () => {
|
||
try {
|
||
// 并行获取各状态的数量
|
||
const [allRes, pendingRes, signedRes, cancelledRes] = await Promise.all([
|
||
pageShopDealerApply({type: 4}), // 全部
|
||
pageShopDealerApply({applyStatus: 10, type: 4}), // 跟进中
|
||
pageShopDealerApply({applyStatus: 20, type: 4}), // 已签约
|
||
pageShopDealerApply({applyStatus: 30, type: 4}) // 已取消
|
||
]);
|
||
|
||
setStatusCounts({
|
||
all: allRes?.count || 0,
|
||
pending: pendingRes?.count || 0,
|
||
signed: signedRes?.count || 0,
|
||
cancelled: cancelledRes?.count || 0
|
||
});
|
||
} catch (error) {
|
||
console.error('获取状态统计失败:', error);
|
||
}
|
||
}, []);
|
||
|
||
const getStatusCounts = () => statusCounts;
|
||
|
||
// 取消操作
|
||
const handleCancel = (customer: ShopDealerApply) => {
|
||
updateShopDealerApply({
|
||
...customer,
|
||
applyStatus: 30
|
||
}).then(() => {
|
||
Taro.showToast({
|
||
title: '取消成功',
|
||
icon: 'success'
|
||
});
|
||
// 重新加载当前tab的数据
|
||
setList([]);
|
||
setPage(1);
|
||
setHasMore(true);
|
||
fetchCustomerData(activeTab, true).then();
|
||
fetchStatusCounts().then();
|
||
})
|
||
};
|
||
|
||
// 删除
|
||
const handleDelete = (customer: ShopDealerApply) => {
|
||
removeShopDealerApply(customer.applyId).then(() => {
|
||
Taro.showToast({
|
||
title: '删除成功',
|
||
icon: 'success'
|
||
});
|
||
// 刷新当前tab的数据
|
||
setList([]);
|
||
setPage(1);
|
||
setHasMore(true);
|
||
fetchCustomerData(activeTab, true).then();
|
||
fetchStatusCounts().then();
|
||
})
|
||
}
|
||
|
||
// 初始化数据
|
||
useEffect(() => {
|
||
fetchCustomerData(activeTab, true).then();
|
||
fetchStatusCounts().then();
|
||
}, []);
|
||
|
||
// 当activeTab变化时重新获取数据
|
||
useEffect(() => {
|
||
setList([]); // 清空列表
|
||
setPage(1); // 重置页码
|
||
setHasMore(true); // 重置加载状态
|
||
fetchCustomerData(activeTab, true);
|
||
}, [activeTab]);
|
||
|
||
// 监听页面显示,当从其他页面返回时刷新数据
|
||
useDidShow(() => {
|
||
// 刷新当前tab的数据和统计信息
|
||
setList([]);
|
||
setPage(1);
|
||
setHasMore(true);
|
||
fetchCustomerData(activeTab, true);
|
||
fetchStatusCounts();
|
||
});
|
||
|
||
// 渲染客户项
|
||
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">
|
||
<View className="flex-1">
|
||
<View className="flex items-center justify-between mb-1">
|
||
<Text className="font-semibold text-gray-800 mr-2">
|
||
{customer.dealerName}
|
||
</Text>
|
||
{customer.customerStatus && (
|
||
<Tag type={getStatusTagType(customer.customerStatus)}>
|
||
{getStatusText(customer.customerStatus)}
|
||
</Tag>
|
||
)}
|
||
</View>
|
||
<View className="flex items-center mb-1">
|
||
<Space direction="vertical">
|
||
<Text className="text-xs text-gray-500">联系人:{customer.realName}</Text>
|
||
<View className="flex items-center">
|
||
<Text className="text-xs text-gray-500" onClick={(e) => {
|
||
e.stopPropagation();
|
||
makePhoneCall(customer.mobile || '');
|
||
}}>联系电话:{customer.mobile}</Text>
|
||
<View className="flex items-center ml-2">
|
||
<Phone
|
||
size={12}
|
||
className="text-green-500 mr-2"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
makePhoneCall(customer.mobile || '');
|
||
}}
|
||
/>
|
||
<Text
|
||
className="text-xs text-blue-500 cursor-pointer"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
copyPhone(customer.mobile || '');
|
||
}}
|
||
>
|
||
复制
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
<Text className="text-xs text-gray-500">
|
||
添加时间:{customer.createTime}
|
||
</Text>
|
||
</Space>
|
||
</View>
|
||
|
||
{/* 保护天数显示 */}
|
||
{customer.applyStatus === 10 && (
|
||
<View className="flex items-center my-1">
|
||
<Text className="text-xs text-gray-500 mr-2">保护期:</Text>
|
||
{customer.protectDays && customer.protectDays > 0 ? (
|
||
<Text className={`text-xs px-2 py-1 rounded ${
|
||
customer.protectDays <= 2
|
||
? 'bg-red-100 text-red-600'
|
||
: customer.protectDays <= 4
|
||
? 'bg-orange-100 text-orange-600'
|
||
: 'bg-green-100 text-green-600'
|
||
}`}>
|
||
剩余{customer.protectDays}天
|
||
</Text>
|
||
) : (
|
||
<Text className="text-xs px-2 py-1 rounded bg-gray-100 text-gray-500">
|
||
已过期
|
||
</Text>
|
||
)}
|
||
</View>
|
||
)}
|
||
|
||
<View className={'flex items-center gap-2'}>
|
||
<Text className="text-xs text-gray-500">报备人:{customer?.nickName}</Text>
|
||
<AngleDoubleLeft size={12} className={'text-blue-500'} />
|
||
<Text className={'text-xs text-gray-500'}>{customer?.refereeName}</Text>
|
||
</View>
|
||
|
||
{/* 显示 comments 字段 */}
|
||
<Space className="flex items-center">
|
||
<Text className="text-xs text-gray-500">跟进情况:{customer.comments || '暂无'}</Text>
|
||
<Text
|
||
className="text-xs text-blue-500 cursor-pointer"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
editComments(customer);
|
||
}}
|
||
>
|
||
编辑
|
||
</Text>
|
||
</Space>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 跟进中状态显示操作按钮 */}
|
||
{(customer.applyStatus === 10 && customer.userId == Taro.getStorageSync('UserId')) && (
|
||
<Space className="flex justify-end">
|
||
<Button
|
||
size="small"
|
||
onClick={() => navTo(`/dealer/customer/add?id=${customer.applyId}`, true)}
|
||
style={{marginRight: '8px', backgroundColor: '#52c41a', color: 'white'}}
|
||
>
|
||
签约
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
onClick={() => handleCancel(customer)}
|
||
style={{backgroundColor: '#ff4d4f', color: 'white'}}
|
||
>
|
||
取消
|
||
</Button>
|
||
</Space>
|
||
)}
|
||
{(customer.applyStatus === 30 && customer.userId == Taro.getStorageSync('UserId')) && (
|
||
<Space className="flex justify-end">
|
||
<Button
|
||
size="small"
|
||
onClick={() => handleDelete(customer)}
|
||
style={{backgroundColor: '#ff4d4f', color: 'white'}}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Space>
|
||
)}
|
||
</View>
|
||
);
|
||
|
||
// 渲染客户列表
|
||
const renderCustomerList = () => {
|
||
const filteredList = getFilteredList();
|
||
const isSearching = displaySearchValue.trim().length > 0;
|
||
|
||
return (
|
||
<View className="flex-1">
|
||
{/* 搜索结果统计 */}
|
||
{isSearching && (
|
||
<View className="bg-white px-4 py-2 border-b border-gray-100">
|
||
<Text className="text-sm text-gray-600">
|
||
搜索 "{displaySearchValue}" 的结果,共找到 {filteredList.length} 条记录
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
<View className="p-4" style={{
|
||
height: isSearching ? 'calc(90vh - 40px)' : '90vh',
|
||
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-3 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>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<View className="min-h-screen bg-gray-50">
|
||
{/* 搜索栏 */}
|
||
<View className="bg-white py-2 border-b border-gray-100">
|
||
<SearchBar
|
||
value={searchValue}
|
||
placeholder="搜索客户名称、手机号"
|
||
onChange={(value) => setSearchValue(value)}
|
||
onClear={() => {
|
||
setSearchValue('');
|
||
setDisplaySearchValue('');
|
||
}}
|
||
clearable
|
||
/>
|
||
</View>
|
||
|
||
{/* 顶部Tabs */}
|
||
<View className="bg-white">
|
||
<Tabs
|
||
value={activeTab}
|
||
onChange={(value) => setActiveTab(value as CustomerStatus)}
|
||
>
|
||
{tabList.map(tab => {
|
||
const counts = getStatusCounts();
|
||
const count = counts[tab.value as keyof typeof counts] || 0;
|
||
return (
|
||
<TabPane
|
||
key={tab.value}
|
||
title={`${tab.label}${count > 0 ? `(${count})` : ''}`}
|
||
value={tab.value}
|
||
/>
|
||
);
|
||
})}
|
||
</Tabs>
|
||
</View>
|
||
|
||
{/* 客户列表 */}
|
||
{renderCustomerList()}
|
||
|
||
<FixedButton text={'客户报备'} onClick={() => Taro.navigateTo({url: '/dealer/customer/add'})}/>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
export default CustomerIndex;
|