feat(referral): 添加推荐客户功能及相关接口

- 新增小程序端推荐客户页面,实现客户信息报备功能
- 添加推荐客户统计与推荐记录展示,支持分页加载更多
- 实现手机号格式校验及报备表单提交逻辑
- 新增拨打客户电话功能
- 在分销商首页添加“推荐客户”入口菜单项
- 新增推荐客户相关API接口,包括报备、查询列表、统计及状态更新
- 完善推荐客户页面样式,提升用户体验
This commit is contained in:
2026-04-14 10:06:17 +08:00
parent 128563bfeb
commit 0a72306d6a
7 changed files with 657 additions and 2 deletions

View File

@@ -0,0 +1,323 @@
import React, {useState, useEffect} from 'react'
import {View, Text, ScrollView, Input, Button} from '@tarojs/components'
import {ConfigProvider, Field, Cell, CellGroup, Toast} from '@nutui/nutui-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import Taro from '@tarojs/taro'
import {addReferral, getReferralList, getReferralStats} from '@/api/shop/referral'
import './index.scss'
// 状态映射
const STATUS_MAP: Record<number, { text: string; color: string }> = {
0: {text: '待确认', color: '#ff9800'},
1: {text: '有效', color: '#4caf50'},
2: {text: '无效', color: '#9e9e9e'},
3: {text: '已结算', color: '#2196f3'}
}
const ReferralPage: React.FC = () => {
const {dealerUser} = useDealerUser()
const [loading, setLoading] = useState(false)
const [submitting, setSubmitting] = useState(false)
// 表单数据
const [formData, setFormData] = useState({
customerName: '',
customerPhone: '',
customerCompany: '',
requirement: '',
remarks: ''
})
// 统计
const [stats, setStats] = useState({
totalCount: 0,
pendingCount: 0,
validCount: 0,
settledCount: 0,
pendingAmount: 0
})
// 记录列表
const [records, setRecords] = useState<any[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
// 加载数据
const loadData = async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
// 获取统计
const statsRes = await getReferralStats(dealerUser.userId)
if (statsRes.data.code === 0) {
setStats(statsRes.data.data)
}
// 获取列表
const listRes = await getReferralList({pageNum: 1, pageSize: 10})
if (listRes.data.code === 0) {
setRecords(listRes.data.data.list || [])
setHasMore(listRes.data.data.list?.length === 10)
}
} catch (error) {
console.error('加载失败', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
loadData()
}, [dealerUser])
// 输入处理
const handleInput = (field: string, value: string) => {
setFormData(prev => ({...prev, [field]: value}))
}
// 表单验证
const validateForm = () => {
if (!formData.customerName.trim()) {
Toast.text('请输入客户姓名')
return false
}
if (!formData.customerPhone.trim()) {
Toast.text('请输入客户电话')
return false
}
if (!/^1[3-9]\d{9}$/.test(formData.customerPhone)) {
Toast.text('请输入正确的手机号')
return false
}
return true
}
// 提交报备
const handleSubmit = async () => {
if (!validateForm()) return
try {
setSubmitting(true)
const res = await addReferral(formData)
if (res.data.code === 0) {
Toast.text('报备成功!')
// 清空表单
setFormData({
customerName: '',
customerPhone: '',
customerCompany: '',
requirement: '',
remarks: ''
})
// 刷新数据
loadData()
} else {
Toast.text(res.data.message || '报备失败')
}
} catch (error: any) {
Toast.text(error.message || '报备失败')
} finally {
setSubmitting(false)
}
}
// 拨打电话
const handleCall = (phone: string) => {
if (phone) {
Taro.makePhoneCall({phoneNumber: phone})
}
}
// 加载更多
const loadMore = async () => {
if (!hasMore || loading) return
try {
const nextPage = page + 1
const res = await getReferralList({pageNum: nextPage, pageSize: 10})
if (res.data.code === 0 && res.data.data.list) {
setRecords(prev => [...prev, ...res.data.data.list])
setPage(nextPage)
setHasMore(res.data.data.list.length === 10)
}
} catch (error) {
console.error('加载更多失败', error)
}
}
return (
<View className="referral-page">
{/* 头部统计 */}
<View className="stats-section" style={{background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'}}>
<View className="stats-title">
<Text className="text-white text-lg font-bold"></Text>
</View>
<View className="stats-grid">
<View className="stat-item">
<Text className="stat-value text-white">{stats.totalCount}</Text>
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}></Text>
</View>
<View className="stat-item">
<Text className="stat-value text-white">{stats.pendingCount}</Text>
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}></Text>
</View>
<View className="stat-item">
<Text className="stat-value text-white">{stats.validCount}</Text>
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}></Text>
</View>
<View className="stat-item">
<Text className="stat-value text-white">¥{stats.pendingAmount.toFixed(2)}</Text>
<Text className="stat-label" style={{color: 'rgba(255,255,255,0.8)'}}></Text>
</View>
</View>
</View>
{/* 报备表单 */}
<View className="form-section">
<View className="section-title">
<Text className="font-bold text-gray-800"></Text>
</View>
<View className="form-card">
<CellGroup>
<Cell title="客户姓名">
<Input
className="nut-input-text"
placeholder="请输入客户姓名"
value={formData.customerName}
onInput={(e) => handleInput('customerName', e.detail.value)}
/>
</Cell>
<Cell title="联系电话">
<Input
className="nut-input-text"
type="number"
maxlength={11}
placeholder="请输入客户电话"
value={formData.customerPhone}
onInput={(e) => handleInput('customerPhone', e.detail.value)}
/>
</Cell>
<Cell title="公司名称">
<Input
className="nut-input-text"
placeholder="请输入公司名称(选填)"
value={formData.customerCompany}
onInput={(e) => handleInput('customerCompany', e.detail.value)}
/>
</Cell>
<Cell title="需求描述">
<Input
className="nut-input-text"
type="textarea"
placeholder="请描述客户需求(选填)"
value={formData.requirement}
onInput={(e) => handleInput('requirement', e.detail.value)}
style={{height: '80px', textAlign: 'left'}}
/>
</Cell>
</CellGroup>
<View className="submit-btn">
<Button
type="primary"
loading={submitting}
onClick={handleSubmit}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none'
}}
>
</Button>
</View>
<View className="tips">
<Text className="text-gray-500 text-sm">
</Text>
</View>
</View>
</View>
{/* 推荐记录 */}
<View className="records-section">
<View className="section-title">
<Text className="font-bold text-gray-800"></Text>
</View>
{records.length === 0 ? (
<View className="empty-state">
<Text className="text-gray-400"></Text>
<Text className="text-gray-400 text-sm"></Text>
</View>
) : (
<ScrollView
scrollY
onScrollToLower={loadMore}
style={{height: '300px'}}
>
{records.map((item) => {
const statusInfo = STATUS_MAP[item.referralStatus] || STATUS_MAP[0]
return (
<View key={item.referralId} className="record-card">
<View className="record-header">
<View className="customer-info">
<Text className="font-bold text-gray-800">{item.customerName}</Text>
<Text
className="text-blue-500 text-sm ml-2"
onClick={() => handleCall(item.customerPhone)}
>
{item.customerPhone}
</Text>
</View>
<View
className="status-tag"
style={{backgroundColor: statusInfo.color + '20', color: statusInfo.color}}
>
{statusInfo.text}
</View>
</View>
<View className="record-body">
<View className="record-row">
<Text className="text-gray-500 text-sm"></Text>
<Text className="text-gray-700 text-sm">{item.createTime}</Text>
</View>
{item.referralFee > 0 && (
<View className="record-row">
<Text className="text-gray-500 text-sm"></Text>
<Text className="text-red-500 font-bold text-sm">
¥{item.referralFee.toFixed(2)}
</Text>
</View>
)}
{item.leadStatusText && (
<View className="record-row">
<Text className="text-gray-500 text-sm"></Text>
<Text className="text-gray-700 text-sm">{item.leadStatusText}</Text>
</View>
)}
</View>
</View>
)
})}
{!hasMore && records.length > 0 && (
<View className="no-more">
<Text className="text-gray-400 text-sm"></Text>
</View>
)}
</ScrollView>
)}
</View>
{/* 底部安全区 */}
<View className="h-20"></View>
</View>
)
}
export default ReferralPage