From 26253aa0d748dc5b591cb8c67cf7ed55b8e1d791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 20 Mar 2026 00:02:52 +0800 Subject: [PATCH] =?UTF-8?q?refactor(credit):=20=E5=B0=86=E4=BF=A1=E7=94=A8?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E6=A8=A1=E5=9D=97=E9=87=8D=E6=9E=84=E4=B8=BA?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=AE=A2=E6=88=B7=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换 CreditCompany 相关 API 为 CreditMpCustomer API - 更新页面路由路径和组件名称 - 修改数据模型字段映射关系 - 调整界面布局使用 Cell 组件展示客户信息 - 更新搜索框提示文本内容 - 修改分配员工逻辑和确认提示 - 调整筛选条件从行业改为状态 - 更新联系电话获取逻辑从备注中提取 - 修改详情页编辑跳转路径 - 移除原公司详情页面的复杂业务逻辑 - 优化员工加载逻辑并添加缓存机制 - 更新客户列表项点击事件处理方式 --- .../credit/creditMpCustomer/model/index.ts | 2 + src/credit/mp-customer/detail.tsx | 272 +++--------------- src/credit/mp-customer/index.tsx | 171 ++++++----- 3 files changed, 148 insertions(+), 297 deletions(-) diff --git a/src/api/credit/creditMpCustomer/model/index.ts b/src/api/credit/creditMpCustomer/model/index.ts index ff41d8c..7c2d181 100644 --- a/src/api/credit/creditMpCustomer/model/index.ts +++ b/src/api/credit/creditMpCustomer/model/index.ts @@ -28,6 +28,8 @@ export interface CreditMpCustomer { files?: string; // 是否有数据 hasData?: string; + // 步骤 + step?: number; // 备注 comments?: string; // 是否推荐 diff --git a/src/credit/mp-customer/detail.tsx b/src/credit/mp-customer/detail.tsx index b9e4ad0..b24499a 100644 --- a/src/credit/mp-customer/detail.tsx +++ b/src/credit/mp-customer/detail.tsx @@ -1,129 +1,36 @@ import { useCallback, useMemo, useState } from 'react' import Taro, { useDidShow, useRouter } from '@tarojs/taro' import { View, Text } from '@tarojs/components' -import { Button, ConfigProvider, Empty, Loading } from '@nutui/nutui-react-taro' +import { Button, Cell, CellGroup, ConfigProvider, Empty, Loading } 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' +import { getCreditMpCustomer } from '@/api/credit/creditMpCustomer' +import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model' -type CustomerStatus = '保护期内' | '已签约' | '已完成' | '保护期外' - -type FollowPerson = { - name: string - time?: string - isCurrent?: 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 getCompanyIndustry = (c?: CreditCompany) => { - if (!c) return '' - return String( - c.nationalStandardIndustryCategories6 || - c.nationalStandardIndustryCategories2 || - c.nationalStandardIndustryCategories || - c.institutionType || - '' - ).trim() -} - -const parseContactFromComments = (comments?: string) => { - const txt = String(comments || '').trim() +const fmtTime = (t?: string) => { + const txt = String(t || '').trim() if (!txt) return '' - const m = txt.match(/联系人:([^;;]+)/) - return String(m?.[1] || '').trim() + const d = dayjs(txt) + return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : txt } -const getStatusTextClass = (s: CustomerStatus) => { - if (s === '保护期内') return 'text-green-600' - if (s === '已签约') return 'text-orange-500' - if (s === '已完成') return 'text-blue-600' - return 'text-gray-500' +const buildLocation = (row?: CreditMpCustomer | null) => { + if (!row) return '' + return [row.province, row.city, row.region].filter(Boolean).join(' ') } -const normalizeFollowPeople = (company: CreditCompany): FollowPerson[] => { - // 兼容后端可能下发多种字段;未下发时给出静态示例,保证 UI/逻辑可演示。 - const anyCompany = company as any - - const incoming: FollowPerson[] = [] - - const arr1 = anyCompany?.followPeople as Array | undefined - if (Array.isArray(arr1) && arr1.length) { - for (const it of arr1) { - const name = String(it?.name || it?.realName || it?.userRealName || '').trim() - if (!name) continue - incoming.push({ name, time: it?.time || it?.date, isCurrent: !!it?.isCurrent }) - } - } - - const arr2 = anyCompany?.followHistory as Array | undefined - if (Array.isArray(arr2) && arr2.length) { - for (const it of arr2) { - const name = String(it?.name || it?.realName || it?.userRealName || it || '').trim() - if (!name) continue - incoming.push({ name, time: it?.time || it?.date, isCurrent: !!it?.isCurrent }) - } - } - - const currentName = String( - anyCompany?.followRealName || anyCompany?.userRealName || anyCompany?.realName || '' - ).trim() - if (currentName) incoming.push({ name: currentName, isCurrent: true }) - - const cleaned = incoming.filter(x => x?.name).map(x => ({ ...x, name: String(x.name).trim() })) - const deduped: FollowPerson[] = [] - const seen = new Set() - for (const p of cleaned) { - const k = p.name - if (seen.has(k)) continue - seen.add(k) - deduped.push(p) - } - - if (deduped.length) { - // 规则:往期跟进人(灰色)按时间顺序在前;当前跟进人(蓝色)放在最后。 - const current = deduped.filter(p => p.isCurrent) - const history = deduped.filter(p => !p.isCurrent) - history.sort((a, b) => { - const at = a.time ? dayjs(a.time).valueOf() : 0 - const bt = b.time ? dayjs(b.time).valueOf() : 0 - return at - bt - }) - // 多个 current 时取最后一个为“当前” - const currentOne = current.length ? current[current.length - 1] : undefined - return currentOne ? history.concat([{ ...currentOne, isCurrent: true }]) : history - } - - // 无真实跟进人信息:尽量展示一个可识别的占位,避免“凭空造历史数据”造成误导。 - const uid = Number(company?.userId) - if (Number.isFinite(uid) && uid > 0) return [{ name: `员工${uid}`, isCurrent: true }] - return [] +const buildDesc = (row?: CreditMpCustomer | null) => { + if (!row) return '' + const price = row.price ? `${row.price}元` : '' + const years = row.years ? `${row.years}年` : '' + const loc = buildLocation(row) + return [price, years, loc].filter(Boolean).join(' · ') } -const getAssignDateText = (company?: CreditCompany) => { - const anyCompany = company as any - const raw = anyCompany?.assignDate || anyCompany?.assignTime || anyCompany?.updateTime || anyCompany?.createTime - const t = String(raw || '').trim() - if (!t) return '' - const d = dayjs(t) - if (!d.isValid()) return t - return d.format('YYYY-MM-DD') -} - -export default function CreditCompanyDetailPage() { +export default function CreditMpCustomerDetailPage() { const router = useRouter() - const companyId = useMemo(() => { + const rowId = useMemo(() => { const id = Number((router?.params as any)?.id) return Number.isFinite(id) && id > 0 ? id : undefined }, [router?.params]) @@ -139,61 +46,31 @@ export default function CreditCompanyDetailPage() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - const [company, setCompany] = useState(null) + const [row, setRow] = useState(null) const reload = useCallback(async () => { setError(null) setLoading(true) try { - if (!companyId) throw new Error('缺少客户ID') - const res = await getCreditCompany(companyId) - setCompany(res as CreditCompany) + if (!rowId) throw new Error('缺少客户ID') + const res = await getCreditMpCustomer(rowId) + setRow((res || null) as CreditMpCustomer | null) } catch (e) { console.error('加载客户详情失败:', e) - setCompany(null) + setRow(null) setError(String((e as any)?.message || '加载失败')) } finally { setLoading(false) } - }, [companyId]) + }, [rowId]) useDidShow(() => { reload().then() }) - const name = useMemo(() => { - const c = company || ({} as CreditCompany) - return String(c.matchName || c.name || '').trim() - }, [company]) - - const code = useMemo(() => String(company?.code || '').trim(), [company?.code]) - const industry = useMemo(() => getCompanyIndustry(company || undefined), [company]) - const contact = useMemo(() => parseContactFromComments(company?.comments), [company?.comments]) - - const phones = useMemo(() => { - const arr = [...splitPhones(company?.tel), ...splitPhones(company?.moreTel)] - return uniq(arr) - }, [company?.moreTel, company?.tel]) - - const address = useMemo(() => { - const c = company || ({} as CreditCompany) - const region = [c.province, c.city, c.region].filter(Boolean).join('') - const addr = String(c.address || '').trim() - return (region + addr).trim() || region || addr - }, [company]) - - const followPeople = useMemo(() => (company ? normalizeFollowPeople(company) : []), [company]) - const assignDate = useMemo(() => getAssignDateText(company || undefined), [company]) - - const customerStatus = useMemo(() => { - const anyCompany = company as any - const raw = String(anyCompany?.customerStatus || anyCompany?.statusText || '').trim() - const allowed: CustomerStatus[] = ['保护期内', '已签约', '已完成', '保护期外'] - if (allowed.includes(raw as any)) return raw as CustomerStatus - return '保护期内' - }, [company]) - const headerOffset = statusBarHeight + 80 + const title = String(row?.toUser || '').trim() || '客户详情' + const desc = buildDesc(row) return ( @@ -219,7 +96,7 @@ export default function CreditCompanyDetailPage() { onClick={async () => { try { const res = await Taro.showActionSheet({ itemList: ['编辑客户', '刷新'] }) - if (res.tapIndex === 0 && companyId) Taro.navigateTo({ url: `/credit/my-customer/edit?id=${companyId}` }) + if (res.tapIndex === 0 && rowId) Taro.navigateTo({ url: `/credit/creditMpCustomer/add?id=${rowId}` }) if (res.tapIndex === 1) reload() } catch (e) { const msg = String((e as any)?.errMsg || (e as any)?.message || e || '') @@ -254,95 +131,36 @@ export default function CreditCompanyDetailPage() { - ) : !company ? ( + ) : !row ? ( ) : ( - {name || '—'} - - - - 统一代码 - {code || '—'} + {title} + {!!desc && ( + + {desc} + )} - - 所属行业 - {industry || '—'} - - - - 客户联系人 - {contact || '—'} - - - - 客户联系方式 - - {phones.length ? ( - {phones.join(',')} - ) : ( - - )} - - - - - 地址 - {address || '—'} - - - - 跟进人 - - {followPeople.length ? ( - followPeople.map((p, idx) => ( - - {idx === followPeople.length - 1 ? p.name : `${p.name},`} - - )) - ) : ( - - )} - - - - - 分配日期 - {assignDate || '—'} - - - - 客户状态 - {customerStatus} - + + + + + + + + {!!row.url && } + {!!row.files && } + {!!row.comments && } + )} - - - - ) } + diff --git a/src/credit/mp-customer/index.tsx b/src/credit/mp-customer/index.tsx index bb069cc..c73110d 100644 --- a/src/credit/mp-customer/index.tsx +++ b/src/credit/mp-customer/index.tsx @@ -19,9 +19,8 @@ import { import { Copy, Phone } from '@nutui/icons-react-taro' import RegionData from '@/api/json/regions-data.json' -import { updateCreditCompany } from '@/api/credit/creditCompany' -import type { CreditCompany } from '@/api/credit/creditCompany/model' -import { pageCreditMpCustomer } from '@/api/credit/creditMpCustomer' +import { getCreditMpCustomer, pageCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer' +import type { CreditMpCustomer, CreditMpCustomerParam } from '@/api/credit/creditMpCustomer/model' import { listUsers } from '@/api/system/user' import type { User } from '@/api/system/user/model' import { hasRole } from '@/utils/permission' @@ -45,7 +44,7 @@ const safeParseJSON = (v: any): T | null => { } } -const getCompanyIdKey = (c: CreditCompany) => String(c?.id || '') +const getRowIdKey = (c: CreditMpCustomer) => String(c?.id || '') const splitPhones = (raw?: string) => { const text = String(raw || '').trim() @@ -57,25 +56,22 @@ const splitPhones = (raw?: string) => { .filter(Boolean) } -const getCompanyPhones = (c: CreditCompany) => { - const arr = [...splitPhones(c.tel), ...splitPhones(c.moreTel)] +const getRowPhones = (c: CreditMpCustomer) => { + // CreditMpCustomer 标准字段无电话:尝试从备注中提取手机号(提取不到则为空) + const raw = String(c.comments || '') + const picked = raw.match(/1\d{10}/g) || [] + const arr = splitPhones(picked.join(',')) return Array.from(new Set(arr)) } -const getCompanyIndustry = (c: CreditCompany) => { - // 兼容:不同数据源字段可能不一致,优先取更具体的大类 - return String( - c.nationalStandardIndustryCategories6 || - c.nationalStandardIndustryCategories2 || - c.nationalStandardIndustryCategories || - c.institutionType || - '' - ).trim() +const getRowStatus = (c: CreditMpCustomer) => { + return String((c as any)?.statusTxt || (c as any)?.statusText || '').trim() } export default function CreditCompanyPage() { const serverPageRef = useRef(1) - const [list, setList] = useState([]) + const staffLoadingPromiseRef = useRef | null>(null) + const [list, setList] = useState([]) const [hasMore, setHasMore] = useState(true) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -88,8 +84,8 @@ export default function CreditCompanyPage() { const [followVisible, setFollowVisible] = useState(false) const [followStatus, setFollowStatus] = useState('全部') - const [industryVisible, setIndustryVisible] = useState(false) - const [industryText, setIndustryText] = useState('全部') + const [statusVisible, setStatusVisible] = useState(false) + const [statusText, setStatusText] = useState('全部') const [selectMode, setSelectMode] = useState(false) const [selectedIds, setSelectedIds] = useState([]) @@ -144,8 +140,8 @@ export default function CreditCompanyPage() { }, [staffList]) const getFollowStatus = useCallback( - (c: CreditCompany): FollowStatus => { - const k = getCompanyIdKey(c) + (c: CreditMpCustomer): FollowStatus => { + const k = getRowIdKey(c) const stored = k ? followMap[k] : undefined if (stored) return stored return '未联系' @@ -153,7 +149,7 @@ export default function CreditCompanyPage() { [followMap] ) - const setFollowStatusFor = async (c: CreditCompany) => { + const setFollowStatusFor = async (c: CreditMpCustomer) => { if (!c?.id) return try { const res = await Taro.showActionSheet({ @@ -163,7 +159,7 @@ export default function CreditCompanyPage() { if (!next) return setFollowMap(prev => { - const k = getCompanyIdKey(c) + const k = getRowIdKey(c) const merged = { ...prev, [k]: next } Taro.setStorageSync(FOLLOW_MAP_STORAGE_KEY, merged) return merged @@ -182,9 +178,9 @@ export default function CreditCompanyPage() { } const applyFilters = useCallback( - (incoming: CreditCompany[]) => { + (incoming: CreditMpCustomer[]) => { const city = cityText === '全部' ? '' : cityText - const industry = industryText === '全部' ? '' : industryText + const status = statusText === '全部' ? '' : statusText const follow = followStatus === '全部' ? '' : followStatus return incoming.filter(c => { @@ -195,9 +191,9 @@ export default function CreditCompanyPage() { if (!full.includes(city) && String(c.city || '') !== city) return false } - if (industry) { - const ind = getCompanyIndustry(c) - if (!ind.includes(industry)) return false + if (status) { + const txt = getRowStatus(c) + if (!txt.includes(status)) return false } if (follow) { @@ -207,7 +203,7 @@ export default function CreditCompanyPage() { return true }) }, - [cityText, followStatus, getFollowStatus, industryText] + [cityText, followStatus, getFollowStatus, statusText] ) const reload = useCallback( @@ -240,8 +236,8 @@ export default function CreditCompanyPage() { keywords: searchValue?.trim() || undefined } - const res = await pageCreditMpCustomer(params as any) - const incoming = (res?.list || []) as CreditCompany[] + const res = await pageCreditMpCustomer(params as CreditMpCustomerParam) + const incoming = (res?.list || []) as CreditMpCustomer[] const filtered = applyFilters(incoming) if (resetPage) { @@ -292,11 +288,11 @@ export default function CreditCompanyPage() { ensureStaffLoaded().then() }) - const visibleIndustryOptions = useMemo(() => { + const visibleStatusOptions = useMemo(() => { const uniq = new Set() for (const m of list) { - const ind = getCompanyIndustry(m) - if (ind) uniq.add(ind) + const txt = getRowStatus(m) + if (txt) uniq.add(txt) } const arr = Array.from(uniq) arr.sort() @@ -313,7 +309,7 @@ export default function CreditCompanyPage() { const copyPhones = async () => { const pool = selectMode && selectedIds.length ? list.filter(c => selectedIds.includes(Number(c.id))) : list - const phones = pool.flatMap(c => getCompanyPhones(c)) + const phones = pool.flatMap(c => getRowPhones(c)) const unique = Array.from(new Set(phones)).filter(Boolean) if (!unique.length) { Taro.showToast({ title: '暂无可复制的电话', icon: 'none' }) @@ -338,23 +334,33 @@ export default function CreditCompanyPage() { } const openAddCustomer = () => { - Taro.navigateTo({ url: '/credit/my-customer/add' }) + Taro.navigateTo({ url: '/credit/creditMpCustomer/add' }) } - const ensureStaffLoaded = async () => { - if (staffLoading) return - if (staffList.length) return + const ensureStaffLoaded = useCallback(async (): Promise => { + if (staffList.length) return staffList + if (staffLoadingPromiseRef.current) return staffLoadingPromiseRef.current + setStaffLoading(true) - try { - const res = await listUsers({ isAdmin: true } as any) - setStaffList((res || []) as User[]) - } catch (e) { - console.error('加载员工列表失败:', e) - Taro.showToast({ title: '加载员工失败', icon: 'none' }) - } finally { - setStaffLoading(false) - } - } + const p = (async () => { + try { + const res = await listUsers({ isStaff: true } as any) + const arr = (res || []) as User[] + setStaffList(arr) + return arr + } catch (e) { + console.error('加载员工列表失败:', e) + Taro.showToast({ title: '加载员工失败', icon: 'none' }) + return [] + } finally { + setStaffLoading(false) + staffLoadingPromiseRef.current = null + } + })() + + staffLoadingPromiseRef.current = p + return p + }, [staffList]) const openAssign = async () => { if (!canAssign) { @@ -371,7 +377,11 @@ export default function CreditCompanyPage() { Taro.showToast({ title: '请先勾选客户', icon: 'none' }) return } - await ensureStaffLoaded() + const staff = await ensureStaffLoaded() + if (!staff.length) { + Taro.showToast({ title: '暂无可分配员工', icon: 'none' }) + return + } setStaffPopupVisible(true) } @@ -387,8 +397,16 @@ export default function CreditCompanyPage() { setAssigning(true) try { + const staffName = staffNameMap.get(Number(staffSelectedId)) || `员工${staffSelectedId}` + const confirmRes = await Taro.showModal({ + title: '确认分配', + content: `确定将 ${selectedIds.length} 个客户分配给「${staffName}」吗?` + }) + if (!confirmRes.confirm) return + for (const id of selectedIds) { - await updateCreditCompany({ id, userId: staffSelectedId } as any) + const detail = await getCreditMpCustomer(Number(id)) + await updateCreditMpCustomer({ ...(detail || {}), id, userId: staffSelectedId } as any) } Taro.showToast({ title: '分配成功', icon: 'success' }) setStaffPopupVisible(false) @@ -408,7 +426,7 @@ export default function CreditCompanyPage() { reload(true)} @@ -422,8 +440,8 @@ export default function CreditCompanyPage() { -