From 0a72306d6a083a683a61ea76ccf220a7675146e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 14 Apr 2026 10:06:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(referral):=20=E6=B7=BB=E5=8A=A0=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E5=AE=A2=E6=88=B7=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增小程序端推荐客户页面,实现客户信息报备功能 - 添加推荐客户统计与推荐记录展示,支持分页加载更多 - 实现手机号格式校验及报备表单提交逻辑 - 新增拨打客户电话功能 - 在分销商首页添加“推荐客户”入口菜单项 - 新增推荐客户相关API接口,包括报备、查询列表、统计及状态更新 - 完善推荐客户页面样式,提升用户体验 --- .workbuddy/expert-history.json | 13 +- .workbuddy/memory/2026-04-14.md | 42 ++++ src/api/shop/referral.ts | 135 ++++++++++++ src/dealer/index.tsx | 11 +- src/dealer/referral/index.config.ts | 3 + src/dealer/referral/index.scss | 132 ++++++++++++ src/dealer/referral/index.tsx | 323 ++++++++++++++++++++++++++++ 7 files changed, 657 insertions(+), 2 deletions(-) create mode 100644 .workbuddy/memory/2026-04-14.md create mode 100644 src/api/shop/referral.ts create mode 100644 src/dealer/referral/index.config.ts create mode 100644 src/dealer/referral/index.scss create mode 100644 src/dealer/referral/index.tsx diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json index 09aee83..f15b4ca 100644 --- a/.workbuddy/expert-history.json +++ b/.workbuddy/expert-history.json @@ -11,7 +11,18 @@ "usedAt": 1775709039214, "industryId": "all" } + ], + "2d8018ea3c7f4b92a608c23c2ee6211a": [ + { + "expertId": "SeniorDeveloper", + "name": "Will", + "profession": "高级开发工程师", + "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png", + "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md", + "usedAt": 1776102350082, + "industryId": "all" + } ] }, - "lastUpdated": 1775713542885 + "lastUpdated": 1776103090047 } \ No newline at end of file diff --git a/.workbuddy/memory/2026-04-14.md b/.workbuddy/memory/2026-04-14.md new file mode 100644 index 0000000..c2c5bed --- /dev/null +++ b/.workbuddy/memory/2026-04-14.md @@ -0,0 +1,42 @@ +# 2026-04-14 日志 + +## 项目:客资管理系统(Customer Lead Management System) + +### 需求背景 +用户委托开发一个客资管理系统,具备以下功能: +1. **客资派单** - 管理员直接派单给业务员 +2. **全民推荐** - 任何人可推荐客户赚取推荐费 +3. **推荐人报备** - 注册用户可报备客户 +4. **实时跟进** - 实时查看跟进情况和成交状态 +5. **多管理员** - 支持多管理员设置 +6. **数据统计导出** - 统计报表功能 + +### 涉及项目 +- Java后端: `/Users/gxwebsoft/JAVA/mp-java` +- Vue后台管理: `/Users/gxwebsoft/VUE/mp-vue` +- 小程序端: `/Users/gxwebsoft/VUE/template-10582` + +### 状态 +- [x] 探索代码结构 +- [x] 设计数据库schema +- [x] 设计API接口 +- [x] 实施开发 + +### 实施成果 + +#### Java后端 (`/Users/gxwebsoft/JAVA/mp-java`) +- SQL脚本: `docs/sql/customer_lead_system.sql` +- Entity: CustomerLeadEntity, LeadDispatch, LeadFollowLog, LeadReferral, LeadStatistics, UserRoleExtend +- Mapper: CustomerLeadMapper, LeadDispatchMapper, LeadFollowLogMapper, LeadReferralMapper +- Service: CustomerLeadService, LeadReferralService +- Controller: CustomerLeadController, LeadReferralController +- 总结文档: `docs/ai/customer-lead-system-summary.md` + +#### Vue后台 (`/Users/gxwebsoft/VUE/mp-vue`) +- API: `api/cms/customerLead/index.ts`, `model.ts` +- 页面: `views/cms/customerLead/index.vue` + +#### 小程序端 (`/Users/gxwebsoft/VUE/template-10582`) +- API: `api/shop/referral.ts` +- 页面: `dealer/referral/index.tsx` +- 首页入口: 在分销商首页添加「推荐客户」功能 diff --git a/src/api/shop/referral.ts b/src/api/shop/referral.ts new file mode 100644 index 0000000..7a3a1d8 --- /dev/null +++ b/src/api/shop/referral.ts @@ -0,0 +1,135 @@ +/** + * 客资推荐人 API + * 小程序端调用 + */ +import request from '@/utils/request' + +/** + * 推荐人报备参数 + */ +export interface ReferralParam { + customerName: string + customerPhone: string + customerCompany?: string + requirement?: string + appointmentTime?: string + remarks?: string +} + +/** + * 推荐人报备结果 + */ +export interface ReferralResult { + referralId: number + referralCode: string + customerName: string + customerPhone: string + referralFee?: number + referralStatus: number + referralStatusText: string + createTime: string +} + +/** + * 推荐人统计 + */ +export interface ReferralStats { + totalCount: number + pendingCount: number + validCount: number + settledCount: number + pendingAmount: number +} + +/** + * 推荐人记录 + */ +export interface ReferralRecord { + referralId: number + referredLeadId: number + customerName: string + customerPhone: string + referralFee: number + referralStatus: number + referralStatusText: string + leadStatus?: number + leadStatusText?: string + dealAmount?: number + createTime: string + settlementTime?: string +} + +/** + * 注册用户报备客户 + */ +export function addReferral(data: ReferralParam) { + return request.post<{ code: number; message: string; data: ReferralResult }>( + '/lead/referral/user', + data + ) +} + +/** + * 获取推荐人的推荐记录 + */ +export function getReferralList(params: { pageNum?: number; pageSize?: number }) { + return request.get<{ code: number; message: string; data: { list: ReferralRecord[]; total: number } }>( + '/lead/referral/page', + { params } + ) +} + +/** + * 获取推荐人统计 + */ +export function getReferralStats(userId: number) { + return request.get<{ code: number; message: string; data: ReferralStats }>( + `/lead/referral/stats/${userId}` + ) +} + +/** + * 生成推荐码 + */ +export function generateReferralCode() { + return request.get<{ code: number; message: string; data: string }>( + '/lead/referral/generateCode' + ) +} + +/** + * 获取当前用户的推荐码 + */ +export function getMyReferralCode() { + return request.get<{ code: number; message: string; data: { referralCode: string } }>( + '/lead/referral/my/code' + ) +} + +/** + * 确认推荐有效(管理员) + */ +export function confirmReferral(referralId: number) { + return request.put<{ code: number; message: string }>( + `/lead/referral/confirm/${referralId}` + ) +} + +/** + * 作废推荐(管理员) + */ +export function invalidateReferral(referralId: number, reason?: string) { + return request.put<{ code: number; message: string }>( + `/lead/referral/invalidate/${referralId}`, + { reason } + ) +} + +/** + * 结算推荐费(管理员) + */ +export function settleReferral(referralId: number) { + return request.put<{ code: number; message: string }>( + `/lead/referral/settle/${referralId}` + ) +} diff --git a/src/dealer/index.tsx b/src/dealer/index.tsx index 97f7a95..c4e4c3b 100644 --- a/src/dealer/index.tsx +++ b/src/dealer/index.tsx @@ -7,7 +7,8 @@ import { QrCode, ArrowRight, Purse, - People + People, + Service } from '@nutui/icons-react-taro' import {useDealerUser} from '@/hooks/useDealerUser' import {useThemeStyles} from '@/hooks/useTheme' @@ -250,6 +251,14 @@ const DealerIndex: React.FC = () => { + navigateToPage('/dealer/referral/index')}> + + + + + + + navigateToPage('/dealer/qrcode/index')}> diff --git a/src/dealer/referral/index.config.ts b/src/dealer/referral/index.config.ts new file mode 100644 index 0000000..df28c7e --- /dev/null +++ b/src/dealer/referral/index.config.ts @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "推荐客户赚佣金" +} diff --git a/src/dealer/referral/index.scss b/src/dealer/referral/index.scss new file mode 100644 index 0000000..076818b --- /dev/null +++ b/src/dealer/referral/index.scss @@ -0,0 +1,132 @@ +.referral-page { + min-height: 100vh; + background-color: #f5f5f7; +} + +.stats-section { + padding: 24px 16px; + color: #fff; + + .stats-title { + margin-bottom: 16px; + text-align: center; + } + + .stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + + .stat-item { + text-align: center; + + .stat-value { + font-size: 20px; + font-weight: bold; + margin-bottom: 4px; + } + + .stat-label { + font-size: 12px; + } + } + } +} + +.form-section { + padding: 0 16px 16px; + + .section-title { + margin-bottom: 12px; + } + + .form-card { + background: #fff; + border-radius: 12px; + padding: 8px 0; + + .nut-input-text { + flex: 1; + text-align: right; + } + + .submit-btn { + padding: 16px; + + button { + width: 100%; + border-radius: 8px; + height: 44px; + line-height: 44px; + } + } + + .tips { + padding: 0 16px 16px; + text-align: center; + } + } +} + +.records-section { + padding: 0 16px 16px; + + .section-title { + margin-bottom: 12px; + } + + .empty-state { + background: #fff; + border-radius: 12px; + padding: 40px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + } + + .record-card { + background: #fff; + border-radius: 12px; + padding: 16px; + margin-bottom: 12px; + + .record-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + + .customer-info { + display: flex; + align-items: center; + } + + .status-tag { + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + } + } + + .record-body { + .record-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + + &:not(:last-child) { + border-bottom: 1px solid #f0f0f0; + } + } + } + } + + .no-more { + text-align: center; + padding: 16px; + } +} diff --git a/src/dealer/referral/index.tsx b/src/dealer/referral/index.tsx new file mode 100644 index 0000000..bb8c57e --- /dev/null +++ b/src/dealer/referral/index.tsx @@ -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 = { + 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([]) + 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 ( + + {/* 头部统计 */} + + + 我的推荐奖励 + + + + {stats.totalCount} + 总推荐 + + + {stats.pendingCount} + 待确认 + + + {stats.validCount} + 有效 + + + ¥{stats.pendingAmount.toFixed(2)} + 待结算 + + + + + {/* 报备表单 */} + + + 推荐新客户 + + + + + handleInput('customerName', e.detail.value)} + /> + + + handleInput('customerPhone', e.detail.value)} + /> + + + handleInput('customerCompany', e.detail.value)} + /> + + + handleInput('requirement', e.detail.value)} + style={{height: '80px', textAlign: 'left'}} + /> + + + + + + + + + + 报备成功后,业务员会尽快联系您的客户。成交后您将获得相应佣金奖励。 + + + + + + {/* 推荐记录 */} + + + 我的推荐记录 + + + {records.length === 0 ? ( + + 暂无推荐记录 + 快去推荐客户吧 + + ) : ( + + {records.map((item) => { + const statusInfo = STATUS_MAP[item.referralStatus] || STATUS_MAP[0] + return ( + + + + {item.customerName} + handleCall(item.customerPhone)} + > + {item.customerPhone} + + + + {statusInfo.text} + + + + + + 推荐时间 + {item.createTime} + + {item.referralFee > 0 && ( + + 奖励金额 + + ¥{item.referralFee.toFixed(2)} + + + )} + {item.leadStatusText && ( + + 客户状态 + {item.leadStatusText} + + )} + + + ) + })} + + {!hasMore && records.length > 0 && ( + + 没有更多了 + + )} + + )} + + + {/* 底部安全区 */} + + + ) +} + +export default ReferralPage