From 02e6f2a0ab4dd0f55ca4c62a4b202b56b65040b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 5 Mar 2026 13:25:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(company):=20=E6=B7=BB=E5=8A=A0=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E8=B7=9F=E8=BF=9B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在路由配置中注册新的跟进页面路径 - 修改客户详情页面的跟进按钮逻辑,跳转到新页面并验证参数 - 新增客户跟进步骤一页面,包含完整的表单功能 - 实现联系人信息解析、电话号码提取等业务逻辑 - 添加录音上传、截图上传等功能模块 - 实现意向选择、沟通情况填写等交互组件 - 添加本地存储草稿功能和提交验证逻辑 - 实现业务规则:多次沟通后的审核流程判断 --- src/app.config.ts | 1 + src/credit/company/detail.tsx | 7 +- src/credit/company/follow-step1.config.ts | 7 + src/credit/company/follow-step1.tsx | 427 ++++++++++++++++++++++ 4 files changed, 440 insertions(+), 2 deletions(-) create mode 100644 src/credit/company/follow-step1.config.ts create mode 100644 src/credit/company/follow-step1.tsx diff --git a/src/app.config.ts b/src/app.config.ts index 94adef1..5ba6c05 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -125,6 +125,7 @@ export default { "company/index", "company/add", "company/detail", + "company/follow-step1", "company/edit" ] } diff --git a/src/credit/company/detail.tsx b/src/credit/company/detail.tsx index e1277ef..7df249a 100644 --- a/src/credit/company/detail.tsx +++ b/src/credit/company/detail.tsx @@ -332,8 +332,11 @@ export default function CreditCompanyDetailPage() { block style={{ background: '#ef4444', borderColor: '#ef4444' }} onClick={() => { - console.log('follow company:', companyId) - Taro.showToast({ title: '开始跟进该客户', icon: 'none' }) + if (!companyId) { + Taro.showToast({ title: '缺少客户ID', icon: 'none' }) + return + } + Taro.navigateTo({ url: `/credit/company/follow-step1?id=${companyId}` }) }} > 跟进 diff --git a/src/credit/company/follow-step1.config.ts b/src/credit/company/follow-step1.config.ts new file mode 100644 index 0000000..d4db67b --- /dev/null +++ b/src/credit/company/follow-step1.config.ts @@ -0,0 +1,7 @@ +export default definePageConfig({ + navigationBarTitleText: '客户跟进', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff', + navigationStyle: 'custom' +}) + diff --git a/src/credit/company/follow-step1.tsx b/src/credit/company/follow-step1.tsx new file mode 100644 index 0000000..f68ff76 --- /dev/null +++ b/src/credit/company/follow-step1.tsx @@ -0,0 +1,427 @@ +import { useCallback, useMemo, useState } from 'react' +import Taro, { useDidShow, useRouter } from '@tarojs/taro' +import { View, Text } from '@tarojs/components' +import { Button, ConfigProvider, Empty, Loading, TextArea } from '@nutui/nutui-react-taro' +import { Setting } from '@nutui/icons-react-taro' +import dayjs from 'dayjs' + +import { getCreditCompany } from '@/api/credit/creditCompany' +import type { CreditCompany } from '@/api/credit/creditCompany/model' + +type Intention = '无意向' | '有意向' + +type FollowStep1Draft = { + submitted: boolean + submittedAt: string + companyId: number + contact: string + phone: string + audioSelected: boolean + smsShots: number + callShots: number + remark: string + intention: Intention + // 业务规则模拟: + // 多次沟通 + 有意向 => 无需审核(可直接进入下一步,下一步不在本页实现) + // 多次沟通 + 无意向 => 需管理员审核 + communicationCount: number + needApproval: boolean + isApproved: boolean +} + +const splitPhones = (raw?: string) => { + const text = String(raw || '').trim() + if (!text) return [] + return text + .split(/[\s,,;;、\n\r]+/g) + .map(s => s.trim()) + .filter(Boolean) +} + +const uniq = (arr: T[]) => Array.from(new Set(arr)) + +const parseContactFromComments = (comments?: string) => { + const txt = String(comments || '').trim() + if (!txt) return '' + const m = txt.match(/联系人:([^;;]+)/) + return String(m?.[1] || '').trim() +} + +const makeThumb = () => ({ + id: `${Date.now()}-${Math.random().toString(16).slice(2)}` +}) + +const getDraftKey = (companyId: number) => `credit_company_follow_step1:${companyId}` +const getCountKey = (companyId: number) => `credit_company_follow_comm_count:${companyId}` + +const safeParseJSON = (v: any): T | null => { + try { + if (!v) return null + if (typeof v === 'object') return v as T + if (typeof v === 'string') return JSON.parse(v) as T + return null + } catch (_e) { + return null + } +} + +export default function CreditCompanyFollowStep1Page() { + const router = useRouter() + const companyId = useMemo(() => { + const id = Number((router?.params as any)?.id) + return Number.isFinite(id) && id > 0 ? id : undefined + }, [router?.params]) + + const statusBarHeight = useMemo(() => { + try { + const info = Taro.getSystemInfoSync() + return Number(info?.statusBarHeight || 0) + } catch (_e) { + return 0 + } + }, []) + + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [company, setCompany] = useState(null) + + const [submitted, setSubmitted] = useState(false) + const [audioSelected, setAudioSelected] = useState(false) + const [smsShots, setSmsShots] = useState>([]) + const [callShots, setCallShots] = useState>([]) + const [remark, setRemark] = useState('') + const [intention, setIntention] = useState(undefined) + + const loadDraft = useCallback(() => { + if (!companyId) return + const raw = Taro.getStorageSync(getDraftKey(companyId)) + const saved = safeParseJSON(raw) + if (!saved?.submitted) return + setSubmitted(true) + setAudioSelected(!!saved.audioSelected) + setSmsShots(Array.from({ length: Math.max(0, Math.min(6, Number(saved.smsShots || 0))) }, makeThumb)) + setCallShots(Array.from({ length: Math.max(0, Math.min(6, Number(saved.callShots || 0))) }, makeThumb)) + setRemark(String(saved.remark || '')) + setIntention(saved.intention) + }, [companyId]) + + const reloadCompany = useCallback(async () => { + setError(null) + setLoading(true) + try { + if (!companyId) throw new Error('缺少客户ID') + const res = await getCreditCompany(companyId) + setCompany(res as CreditCompany) + } catch (e) { + console.error('加载客户信息失败:', e) + setCompany(null) + setError(String((e as any)?.message || '加载失败')) + } finally { + setLoading(false) + } + }, [companyId]) + + useDidShow(() => { + reloadCompany().then() + loadDraft() + }) + + const contact = useMemo(() => parseContactFromComments(company?.comments), [company?.comments]) + + const phone = useMemo(() => { + const arr = uniq([...splitPhones(company?.tel), ...splitPhones(company?.moreTel)]) + return String(arr[0] || '').trim() + }, [company?.moreTel, company?.tel]) + + const canEdit = !submitted + + const addSmsShot = () => { + if (!canEdit) return + if (smsShots.length >= 6) { + Taro.showToast({ title: '短信截图已达上限(6张)', icon: 'none' }) + return + } + setSmsShots(prev => prev.concat(makeThumb())) + } + + const addCallShot = () => { + if (!canEdit) return + if (callShots.length >= 6) { + Taro.showToast({ title: '电话沟通截图已达上限(6张)', icon: 'none' }) + return + } + setCallShots(prev => prev.concat(makeThumb())) + } + + const chooseAudio = async () => { + if (!canEdit) return + // 本步骤仅做“选择录音文件”的交互模拟 + setAudioSelected(true) + Taro.showToast({ title: '已选择录音文件(模拟)', icon: 'none' }) + } + + const submit = async () => { + if (!companyId) { + Taro.showToast({ title: '缺少客户ID', icon: 'none' }) + return + } + if (!intention) { + Taro.showToast({ title: '请选择意向(无意向/有意向)', icon: 'none' }) + return + } + if (!remark.trim()) { + Taro.showToast({ title: '请填写沟通情况', icon: 'none' }) + return + } + + if (!smsShots.length && !callShots.length) { + Taro.showToast({ title: '建议至少上传1张截图(非必填)', icon: 'none' }) + } + + const prevCountRaw = Taro.getStorageSync(getCountKey(companyId)) + const prevCount = Number(prevCountRaw || 0) + const communicationCount = (Number.isFinite(prevCount) ? prevCount : 0) + 1 + Taro.setStorageSync(getCountKey(companyId), communicationCount) + + const needApproval = communicationCount > 1 && intention === '无意向' + const isApproved = false // 模拟:默认未审核;后续步骤可检查该标志 + + const payload: FollowStep1Draft = { + submitted: true, + submittedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'), + companyId, + contact, + phone, + audioSelected, + smsShots: smsShots.length, + callShots: callShots.length, + remark: remark.trim(), + intention, + communicationCount, + needApproval, + isApproved + } + + Taro.setStorageSync(getDraftKey(companyId), payload) + + setSubmitted(true) + + await Taro.showModal({ + title: '提示', + content: '跟进信息已提交\n请等待管理员审核', + showCancel: false + }) + } + + const headerOffset = statusBarHeight + 80 + + return ( + + + + + 12:00 + + 信号 + Wi-Fi + 电池 + + + + + Taro.navigateBack()}> + 返回 + + 客户跟进 + + { + try { + const res = await Taro.showActionSheet({ itemList: ['刷新'] }) + if (res.tapIndex === 0) reloadCompany() + } catch (e) { + const msg = String((e as any)?.errMsg || (e as any)?.message || e || '') + if (msg.includes('cancel')) return + } + }} + > + ... + + + + + + + + + + {loading ? ( + + + 加载中... + + ) : error ? ( + + {error} + + + + + ) : !company ? ( + + + + ) : ( + + 加微信前沟通 + + + + + 联系人 + + {contact || '—'} + + + + + 联系电话 + + {phone || '—'} + + + + + 电话录音 + + + + + + + + + 短信截图 + {smsShots.length}/6 + + + {smsShots.map((x, idx) => ( + + 图{idx + 1} + + ))} + = 6 ? 'border-gray-200 text-gray-200' : 'border-gray-300 text-gray-400' + }`} + onClick={addSmsShot} + > + + + + + + + + + 电话沟通截图 + {callShots.length}/6 + + + {callShots.map((x, idx) => ( + + 图{idx + 1} + + ))} + = 6 ? 'border-gray-200 text-gray-200' : 'border-gray-300 text-gray-400' + }`} + onClick={addCallShot} + > + + + + + + + + + 沟通情况* + + +