refactor(credit): 将信用客户模块重构为小程序客户模块
- 替换 CreditCompany 相关 API 为 CreditMpCustomer API - 更新页面路由路径和组件名称 - 修改数据模型字段映射关系 - 调整界面布局使用 Cell 组件展示客户信息 - 更新搜索框提示文本内容 - 修改分配员工逻辑和确认提示 - 调整筛选条件从行业改为状态 - 更新联系电话获取逻辑从备注中提取 - 修改详情页编辑跳转路径 - 移除原公司详情页面的复杂业务逻辑 - 优化员工加载逻辑并添加缓存机制 - 更新客户列表项点击事件处理方式
This commit is contained in:
@@ -28,6 +28,8 @@ export interface CreditMpCustomer {
|
|||||||
files?: string;
|
files?: string;
|
||||||
// 是否有数据
|
// 是否有数据
|
||||||
hasData?: string;
|
hasData?: string;
|
||||||
|
// 步骤
|
||||||
|
step?: number;
|
||||||
// 备注
|
// 备注
|
||||||
comments?: string;
|
comments?: string;
|
||||||
// 是否推荐
|
// 是否推荐
|
||||||
|
|||||||
@@ -1,129 +1,36 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
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 { Setting } from '@nutui/icons-react-taro'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { getCreditCompany } from '@/api/credit/creditCompany'
|
import { getCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||||
import type { CreditCompany } from '@/api/credit/creditCompany/model'
|
import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model'
|
||||||
|
|
||||||
type CustomerStatus = '保护期内' | '已签约' | '已完成' | '保护期外'
|
const fmtTime = (t?: string) => {
|
||||||
|
const txt = String(t || '').trim()
|
||||||
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 = <T,>(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()
|
|
||||||
if (!txt) return ''
|
if (!txt) return ''
|
||||||
const m = txt.match(/联系人:([^;;]+)/)
|
const d = dayjs(txt)
|
||||||
return String(m?.[1] || '').trim()
|
return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : txt
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusTextClass = (s: CustomerStatus) => {
|
const buildLocation = (row?: CreditMpCustomer | null) => {
|
||||||
if (s === '保护期内') return 'text-green-600'
|
if (!row) return ''
|
||||||
if (s === '已签约') return 'text-orange-500'
|
return [row.province, row.city, row.region].filter(Boolean).join(' ')
|
||||||
if (s === '已完成') return 'text-blue-600'
|
|
||||||
return 'text-gray-500'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeFollowPeople = (company: CreditCompany): FollowPerson[] => {
|
const buildDesc = (row?: CreditMpCustomer | null) => {
|
||||||
// 兼容后端可能下发多种字段;未下发时给出静态示例,保证 UI/逻辑可演示。
|
if (!row) return ''
|
||||||
const anyCompany = company as any
|
const price = row.price ? `${row.price}元` : ''
|
||||||
|
const years = row.years ? `${row.years}年` : ''
|
||||||
const incoming: FollowPerson[] = []
|
const loc = buildLocation(row)
|
||||||
|
return [price, years, loc].filter(Boolean).join(' · ')
|
||||||
const arr1 = anyCompany?.followPeople as Array<any> | 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<any> | 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<string>()
|
|
||||||
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 getAssignDateText = (company?: CreditCompany) => {
|
export default function CreditMpCustomerDetailPage() {
|
||||||
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() {
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const companyId = useMemo(() => {
|
const rowId = useMemo(() => {
|
||||||
const id = Number((router?.params as any)?.id)
|
const id = Number((router?.params as any)?.id)
|
||||||
return Number.isFinite(id) && id > 0 ? id : undefined
|
return Number.isFinite(id) && id > 0 ? id : undefined
|
||||||
}, [router?.params])
|
}, [router?.params])
|
||||||
@@ -139,61 +46,31 @@ export default function CreditCompanyDetailPage() {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [company, setCompany] = useState<CreditCompany | null>(null)
|
const [row, setRow] = useState<CreditMpCustomer | null>(null)
|
||||||
|
|
||||||
const reload = useCallback(async () => {
|
const reload = useCallback(async () => {
|
||||||
setError(null)
|
setError(null)
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
if (!companyId) throw new Error('缺少客户ID')
|
if (!rowId) throw new Error('缺少客户ID')
|
||||||
const res = await getCreditCompany(companyId)
|
const res = await getCreditMpCustomer(rowId)
|
||||||
setCompany(res as CreditCompany)
|
setRow((res || null) as CreditMpCustomer | null)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载客户详情失败:', e)
|
console.error('加载客户详情失败:', e)
|
||||||
setCompany(null)
|
setRow(null)
|
||||||
setError(String((e as any)?.message || '加载失败'))
|
setError(String((e as any)?.message || '加载失败'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [companyId])
|
}, [rowId])
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
reload().then()
|
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 headerOffset = statusBarHeight + 80
|
||||||
|
const title = String(row?.toUser || '').trim() || '客户详情'
|
||||||
|
const desc = buildDesc(row)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-pink-50 min-h-screen">
|
<View className="bg-pink-50 min-h-screen">
|
||||||
@@ -219,7 +96,7 @@ export default function CreditCompanyDetailPage() {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const res = await Taro.showActionSheet({ itemList: ['编辑客户', '刷新'] })
|
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()
|
if (res.tapIndex === 1) reload()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
||||||
@@ -254,95 +131,36 @@ export default function CreditCompanyDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : !company ? (
|
) : !row ? (
|
||||||
<View className="bg-white rounded-xl border border-pink-100 py-10">
|
<View className="bg-white rounded-xl border border-pink-100 py-10">
|
||||||
<Empty description="暂无客户信息" />
|
<Empty description="暂无客户信息" />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View className="bg-white rounded-xl border border-pink-100 p-4">
|
<View className="bg-white rounded-xl border border-pink-100 p-4">
|
||||||
<View className="text-base font-semibold text-gray-900">{name || '—'}</View>
|
<View className="text-base font-semibold text-gray-900">{title}</View>
|
||||||
|
{!!desc && (
|
||||||
<View className="mt-3 space-y-3 text-sm">
|
<View className="mt-2 text-xs text-gray-500">
|
||||||
<View className="flex items-start justify-between gap-3">
|
<Text>{desc}</Text>
|
||||||
<Text className="text-gray-500">统一代码</Text>
|
|
||||||
<Text className="text-gray-400 text-right break-all">{code || '—'}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex items-start justify-between gap-3">
|
|
||||||
<Text className="text-gray-500">所属行业</Text>
|
|
||||||
<Text className="text-red-500 text-right break-all">{industry || '—'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-start justify-between gap-3">
|
|
||||||
<Text className="text-gray-500">客户联系人</Text>
|
|
||||||
<Text className="text-gray-400 text-right break-all">{contact || '—'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-start justify-between gap-3">
|
|
||||||
<Text className="text-gray-500">客户联系方式</Text>
|
|
||||||
<View className="text-right">
|
|
||||||
{phones.length ? (
|
|
||||||
<Text className="text-gray-400 break-all">{phones.join(',')}</Text>
|
|
||||||
) : (
|
|
||||||
<Text className="text-gray-400">—</Text>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-start justify-between gap-3">
|
<View className="mt-3">
|
||||||
<Text className="text-gray-500">地址</Text>
|
<CellGroup>
|
||||||
<Text className="text-gray-700 text-right break-all">{address || '—'}</Text>
|
<Cell title="订单号" description={String(row.id ?? '—')} />
|
||||||
</View>
|
<Cell title="状态" description={String(row.statusTxt || '—')} />
|
||||||
|
<Cell title="分配人ID" description={row.userId ? String(row.userId) : '未分配'} />
|
||||||
<View className="flex items-start justify-between gap-3">
|
<Cell title="创建时间" description={fmtTime(row.createTime) || '—'} />
|
||||||
<Text className="text-gray-500">跟进人</Text>
|
<Cell title="更新时间" description={fmtTime(row.updateTime) || '—'} />
|
||||||
<View className="text-right">
|
{!!row.url && <Cell title="链接" description={String(row.url)} />}
|
||||||
{followPeople.length ? (
|
{!!row.files && <Cell title="文件" description={String(row.files)} />}
|
||||||
followPeople.map((p, idx) => (
|
{!!row.comments && <Cell title="备注" description={String(row.comments)} />}
|
||||||
<Text
|
</CellGroup>
|
||||||
key={`${p.name}-${idx}`}
|
|
||||||
className={p.isCurrent ? 'text-blue-600 break-all' : 'text-gray-400 break-all'}
|
|
||||||
>
|
|
||||||
{idx === followPeople.length - 1 ? p.name : `${p.name},`}
|
|
||||||
</Text>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Text className="text-gray-400">—</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-start justify-between gap-3">
|
|
||||||
<Text className="text-gray-500">分配日期</Text>
|
|
||||||
<Text className="text-gray-400 text-right">{assignDate || '—'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-start justify-between gap-3">
|
|
||||||
<Text className="text-gray-500">客户状态</Text>
|
|
||||||
<Text className={`${getStatusTextClass(customerStatus)} text-right`}>{customerStatus}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="fixed z-50 bottom-0 left-0 right-0 bg-pink-50 px-4 py-4 safe-area-bottom">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
block
|
|
||||||
style={{ background: '#ef4444', borderColor: '#ef4444' }}
|
|
||||||
onClick={() => {
|
|
||||||
if (!companyId) {
|
|
||||||
Taro.showToast({ title: '缺少客户ID', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Taro.navigateTo({ url: `/credit/my-customer/follow-step1?id=${companyId}` })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
跟进
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ import {
|
|||||||
import { Copy, Phone } from '@nutui/icons-react-taro'
|
import { Copy, Phone } from '@nutui/icons-react-taro'
|
||||||
|
|
||||||
import RegionData from '@/api/json/regions-data.json'
|
import RegionData from '@/api/json/regions-data.json'
|
||||||
import { updateCreditCompany } from '@/api/credit/creditCompany'
|
import { getCreditMpCustomer, pageCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||||
import type { CreditCompany } from '@/api/credit/creditCompany/model'
|
import type { CreditMpCustomer, CreditMpCustomerParam } from '@/api/credit/creditMpCustomer/model'
|
||||||
import { pageCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
|
||||||
import { listUsers } from '@/api/system/user'
|
import { listUsers } from '@/api/system/user'
|
||||||
import type { User } from '@/api/system/user/model'
|
import type { User } from '@/api/system/user/model'
|
||||||
import { hasRole } from '@/utils/permission'
|
import { hasRole } from '@/utils/permission'
|
||||||
@@ -45,7 +44,7 @@ const safeParseJSON = <T,>(v: any): T | null => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCompanyIdKey = (c: CreditCompany) => String(c?.id || '')
|
const getRowIdKey = (c: CreditMpCustomer) => String(c?.id || '')
|
||||||
|
|
||||||
const splitPhones = (raw?: string) => {
|
const splitPhones = (raw?: string) => {
|
||||||
const text = String(raw || '').trim()
|
const text = String(raw || '').trim()
|
||||||
@@ -57,25 +56,22 @@ const splitPhones = (raw?: string) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCompanyPhones = (c: CreditCompany) => {
|
const getRowPhones = (c: CreditMpCustomer) => {
|
||||||
const arr = [...splitPhones(c.tel), ...splitPhones(c.moreTel)]
|
// 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))
|
return Array.from(new Set(arr))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCompanyIndustry = (c: CreditCompany) => {
|
const getRowStatus = (c: CreditMpCustomer) => {
|
||||||
// 兼容:不同数据源字段可能不一致,优先取更具体的大类
|
return String((c as any)?.statusTxt || (c as any)?.statusText || '').trim()
|
||||||
return String(
|
|
||||||
c.nationalStandardIndustryCategories6 ||
|
|
||||||
c.nationalStandardIndustryCategories2 ||
|
|
||||||
c.nationalStandardIndustryCategories ||
|
|
||||||
c.institutionType ||
|
|
||||||
''
|
|
||||||
).trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreditCompanyPage() {
|
export default function CreditCompanyPage() {
|
||||||
const serverPageRef = useRef(1)
|
const serverPageRef = useRef(1)
|
||||||
const [list, setList] = useState<CreditCompany[]>([])
|
const staffLoadingPromiseRef = useRef<Promise<User[]> | null>(null)
|
||||||
|
const [list, setList] = useState<CreditMpCustomer[]>([])
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -88,8 +84,8 @@ export default function CreditCompanyPage() {
|
|||||||
const [followVisible, setFollowVisible] = useState(false)
|
const [followVisible, setFollowVisible] = useState(false)
|
||||||
const [followStatus, setFollowStatus] = useState<FollowStatus>('全部')
|
const [followStatus, setFollowStatus] = useState<FollowStatus>('全部')
|
||||||
|
|
||||||
const [industryVisible, setIndustryVisible] = useState(false)
|
const [statusVisible, setStatusVisible] = useState(false)
|
||||||
const [industryText, setIndustryText] = useState<string>('全部')
|
const [statusText, setStatusText] = useState<string>('全部')
|
||||||
|
|
||||||
const [selectMode, setSelectMode] = useState(false)
|
const [selectMode, setSelectMode] = useState(false)
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||||
@@ -144,8 +140,8 @@ export default function CreditCompanyPage() {
|
|||||||
}, [staffList])
|
}, [staffList])
|
||||||
|
|
||||||
const getFollowStatus = useCallback(
|
const getFollowStatus = useCallback(
|
||||||
(c: CreditCompany): FollowStatus => {
|
(c: CreditMpCustomer): FollowStatus => {
|
||||||
const k = getCompanyIdKey(c)
|
const k = getRowIdKey(c)
|
||||||
const stored = k ? followMap[k] : undefined
|
const stored = k ? followMap[k] : undefined
|
||||||
if (stored) return stored
|
if (stored) return stored
|
||||||
return '未联系'
|
return '未联系'
|
||||||
@@ -153,7 +149,7 @@ export default function CreditCompanyPage() {
|
|||||||
[followMap]
|
[followMap]
|
||||||
)
|
)
|
||||||
|
|
||||||
const setFollowStatusFor = async (c: CreditCompany) => {
|
const setFollowStatusFor = async (c: CreditMpCustomer) => {
|
||||||
if (!c?.id) return
|
if (!c?.id) return
|
||||||
try {
|
try {
|
||||||
const res = await Taro.showActionSheet({
|
const res = await Taro.showActionSheet({
|
||||||
@@ -163,7 +159,7 @@ export default function CreditCompanyPage() {
|
|||||||
if (!next) return
|
if (!next) return
|
||||||
|
|
||||||
setFollowMap(prev => {
|
setFollowMap(prev => {
|
||||||
const k = getCompanyIdKey(c)
|
const k = getRowIdKey(c)
|
||||||
const merged = { ...prev, [k]: next }
|
const merged = { ...prev, [k]: next }
|
||||||
Taro.setStorageSync(FOLLOW_MAP_STORAGE_KEY, merged)
|
Taro.setStorageSync(FOLLOW_MAP_STORAGE_KEY, merged)
|
||||||
return merged
|
return merged
|
||||||
@@ -182,9 +178,9 @@ export default function CreditCompanyPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const applyFilters = useCallback(
|
const applyFilters = useCallback(
|
||||||
(incoming: CreditCompany[]) => {
|
(incoming: CreditMpCustomer[]) => {
|
||||||
const city = cityText === '全部' ? '' : cityText
|
const city = cityText === '全部' ? '' : cityText
|
||||||
const industry = industryText === '全部' ? '' : industryText
|
const status = statusText === '全部' ? '' : statusText
|
||||||
const follow = followStatus === '全部' ? '' : followStatus
|
const follow = followStatus === '全部' ? '' : followStatus
|
||||||
|
|
||||||
return incoming.filter(c => {
|
return incoming.filter(c => {
|
||||||
@@ -195,9 +191,9 @@ export default function CreditCompanyPage() {
|
|||||||
if (!full.includes(city) && String(c.city || '') !== city) return false
|
if (!full.includes(city) && String(c.city || '') !== city) return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (industry) {
|
if (status) {
|
||||||
const ind = getCompanyIndustry(c)
|
const txt = getRowStatus(c)
|
||||||
if (!ind.includes(industry)) return false
|
if (!txt.includes(status)) return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (follow) {
|
if (follow) {
|
||||||
@@ -207,7 +203,7 @@ export default function CreditCompanyPage() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[cityText, followStatus, getFollowStatus, industryText]
|
[cityText, followStatus, getFollowStatus, statusText]
|
||||||
)
|
)
|
||||||
|
|
||||||
const reload = useCallback(
|
const reload = useCallback(
|
||||||
@@ -240,8 +236,8 @@ export default function CreditCompanyPage() {
|
|||||||
keywords: searchValue?.trim() || undefined
|
keywords: searchValue?.trim() || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await pageCreditMpCustomer(params as any)
|
const res = await pageCreditMpCustomer(params as CreditMpCustomerParam)
|
||||||
const incoming = (res?.list || []) as CreditCompany[]
|
const incoming = (res?.list || []) as CreditMpCustomer[]
|
||||||
const filtered = applyFilters(incoming)
|
const filtered = applyFilters(incoming)
|
||||||
|
|
||||||
if (resetPage) {
|
if (resetPage) {
|
||||||
@@ -292,11 +288,11 @@ export default function CreditCompanyPage() {
|
|||||||
ensureStaffLoaded().then()
|
ensureStaffLoaded().then()
|
||||||
})
|
})
|
||||||
|
|
||||||
const visibleIndustryOptions = useMemo(() => {
|
const visibleStatusOptions = useMemo(() => {
|
||||||
const uniq = new Set<string>()
|
const uniq = new Set<string>()
|
||||||
for (const m of list) {
|
for (const m of list) {
|
||||||
const ind = getCompanyIndustry(m)
|
const txt = getRowStatus(m)
|
||||||
if (ind) uniq.add(ind)
|
if (txt) uniq.add(txt)
|
||||||
}
|
}
|
||||||
const arr = Array.from(uniq)
|
const arr = Array.from(uniq)
|
||||||
arr.sort()
|
arr.sort()
|
||||||
@@ -313,7 +309,7 @@ export default function CreditCompanyPage() {
|
|||||||
|
|
||||||
const copyPhones = async () => {
|
const copyPhones = async () => {
|
||||||
const pool = selectMode && selectedIds.length ? list.filter(c => selectedIds.includes(Number(c.id))) : list
|
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)
|
const unique = Array.from(new Set(phones)).filter(Boolean)
|
||||||
if (!unique.length) {
|
if (!unique.length) {
|
||||||
Taro.showToast({ title: '暂无可复制的电话', icon: 'none' })
|
Taro.showToast({ title: '暂无可复制的电话', icon: 'none' })
|
||||||
@@ -338,23 +334,33 @@ export default function CreditCompanyPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openAddCustomer = () => {
|
const openAddCustomer = () => {
|
||||||
Taro.navigateTo({ url: '/credit/my-customer/add' })
|
Taro.navigateTo({ url: '/credit/creditMpCustomer/add' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureStaffLoaded = async () => {
|
const ensureStaffLoaded = useCallback(async (): Promise<User[]> => {
|
||||||
if (staffLoading) return
|
if (staffList.length) return staffList
|
||||||
if (staffList.length) return
|
if (staffLoadingPromiseRef.current) return staffLoadingPromiseRef.current
|
||||||
|
|
||||||
setStaffLoading(true)
|
setStaffLoading(true)
|
||||||
|
const p = (async () => {
|
||||||
try {
|
try {
|
||||||
const res = await listUsers({ isAdmin: true } as any)
|
const res = await listUsers({ isStaff: true } as any)
|
||||||
setStaffList((res || []) as User[])
|
const arr = (res || []) as User[]
|
||||||
|
setStaffList(arr)
|
||||||
|
return arr
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载员工列表失败:', e)
|
console.error('加载员工列表失败:', e)
|
||||||
Taro.showToast({ title: '加载员工失败', icon: 'none' })
|
Taro.showToast({ title: '加载员工失败', icon: 'none' })
|
||||||
|
return []
|
||||||
} finally {
|
} finally {
|
||||||
setStaffLoading(false)
|
setStaffLoading(false)
|
||||||
|
staffLoadingPromiseRef.current = null
|
||||||
}
|
}
|
||||||
}
|
})()
|
||||||
|
|
||||||
|
staffLoadingPromiseRef.current = p
|
||||||
|
return p
|
||||||
|
}, [staffList])
|
||||||
|
|
||||||
const openAssign = async () => {
|
const openAssign = async () => {
|
||||||
if (!canAssign) {
|
if (!canAssign) {
|
||||||
@@ -371,7 +377,11 @@ export default function CreditCompanyPage() {
|
|||||||
Taro.showToast({ title: '请先勾选客户', icon: 'none' })
|
Taro.showToast({ title: '请先勾选客户', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await ensureStaffLoaded()
|
const staff = await ensureStaffLoaded()
|
||||||
|
if (!staff.length) {
|
||||||
|
Taro.showToast({ title: '暂无可分配员工', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
setStaffPopupVisible(true)
|
setStaffPopupVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,8 +397,16 @@ export default function CreditCompanyPage() {
|
|||||||
|
|
||||||
setAssigning(true)
|
setAssigning(true)
|
||||||
try {
|
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) {
|
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' })
|
Taro.showToast({ title: '分配成功', icon: 'success' })
|
||||||
setStaffPopupVisible(false)
|
setStaffPopupVisible(false)
|
||||||
@@ -408,7 +426,7 @@ export default function CreditCompanyPage() {
|
|||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<View className="py-2">
|
<View className="py-2">
|
||||||
<SearchBar
|
<SearchBar
|
||||||
placeholder="公司名称 / 统一代码"
|
placeholder="拖欠方 / 关键词"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={setSearchValue}
|
onChange={setSearchValue}
|
||||||
onSearch={() => reload(true)}
|
onSearch={() => reload(true)}
|
||||||
@@ -422,8 +440,8 @@ export default function CreditCompanyPage() {
|
|||||||
<Button size="small" fill="outline" onClick={() => setFollowVisible(true)}>
|
<Button size="small" fill="outline" onClick={() => setFollowVisible(true)}>
|
||||||
{followStatus === '全部' ? '跟进状态' : followStatus}
|
{followStatus === '全部' ? '跟进状态' : followStatus}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" fill="outline" onClick={() => setIndustryVisible(true)}>
|
<Button size="small" fill="outline" onClick={() => setStatusVisible(true)}>
|
||||||
{industryText === '全部' ? '行业' : industryText}
|
{statusText === '全部' ? '状态' : statusText}
|
||||||
</Button>
|
</Button>
|
||||||
<View className="flex-1" />
|
<View className="flex-1" />
|
||||||
<Button size="small" fill="outline" icon={<Copy />} onClick={copyPhones}>
|
<Button size="small" fill="outline" icon={<Copy />} onClick={copyPhones}>
|
||||||
@@ -470,17 +488,24 @@ export default function CreditCompanyPage() {
|
|||||||
(c as any)?.userRealName ||
|
(c as any)?.userRealName ||
|
||||||
(c as any)?.followRealName ||
|
(c as any)?.followRealName ||
|
||||||
(c.userId ? staffNameMap.get(Number(c.userId)) : undefined)
|
(c.userId ? staffNameMap.get(Number(c.userId)) : undefined)
|
||||||
const name = c.matchName || c.name || `企业${c.id || ''}`
|
const name = String(c.toUser || '').trim() || `客户${c.id || ''}`
|
||||||
const industry = getCompanyIndustry(c)
|
const status = getRowStatus(c)
|
||||||
const phones = getCompanyPhones(c)
|
const price = c.price ? `${c.price}元` : ''
|
||||||
|
const years = c.years ? `${c.years}年` : ''
|
||||||
|
const location = [c.province, c.city, c.region].filter(Boolean).join(' ')
|
||||||
|
const desc = [price, years, location].filter(Boolean).join(' · ')
|
||||||
|
const phones = getRowPhones(c)
|
||||||
const primaryPhone = phones[0]
|
const primaryPhone = phones[0]
|
||||||
return (
|
return (
|
||||||
<CellGroup key={c.id || idx} className="mb-3">
|
<CellGroup key={c.id || idx} className="mb-3">
|
||||||
<Cell
|
<Cell
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectMode) return
|
if (selectMode) {
|
||||||
|
toggleSelectId(id, !selected)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!c?.id) return
|
if (!c?.id) return
|
||||||
Taro.navigateTo({ url: `/credit/my-customer/detail?id=${c.id}` })
|
Taro.navigateTo({ url: `/credit/mp-customer/detail?id=${c.id}` })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className="flex gap-3 items-start w-full">
|
<View className="flex gap-3 items-start w-full">
|
||||||
@@ -504,9 +529,9 @@ export default function CreditCompanyPage() {
|
|||||||
<View className="text-base font-bold text-gray-900 truncate">
|
<View className="text-base font-bold text-gray-900 truncate">
|
||||||
{name}
|
{name}
|
||||||
</View>
|
</View>
|
||||||
{!!industry && (
|
{!!desc && (
|
||||||
<View className="text-xs text-gray-500 truncate mt-1">
|
<View className="text-xs text-gray-500 truncate mt-1">
|
||||||
{industry}
|
{desc}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -523,6 +548,11 @@ export default function CreditCompanyPage() {
|
|||||||
|
|
||||||
<View className="mt-2 flex items-center justify-between gap-2">
|
<View className="mt-2 flex items-center justify-between gap-2">
|
||||||
<View className="text-xs text-gray-600 truncate">
|
<View className="text-xs text-gray-600 truncate">
|
||||||
|
{!!status && (
|
||||||
|
<Text className="mr-2">
|
||||||
|
{status}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<Text className="mr-2">
|
<Text className="mr-2">
|
||||||
{primaryPhone ? primaryPhone : '暂无电话'}
|
{primaryPhone ? primaryPhone : '暂无电话'}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -649,21 +679,22 @@ export default function CreditCompanyPage() {
|
|||||||
</View>
|
</View>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
||||||
<Popup
|
<Popup visible={statusVisible} position="bottom" style={{ height: '45vh' }} onClose={() => setStatusVisible(false)}>
|
||||||
visible={industryVisible}
|
|
||||||
position="bottom"
|
|
||||||
style={{ height: '45vh' }}
|
|
||||||
onClose={() => setIndustryVisible(false)}
|
|
||||||
>
|
|
||||||
<View className="p-4">
|
<View className="p-4">
|
||||||
|
<View className="flex items-center justify-between mb-3">
|
||||||
|
<Text className="text-base font-medium">状态</Text>
|
||||||
|
<Text className="text-sm text-gray-500" onClick={() => setStatusVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
{visibleIndustryOptions.map(s => (
|
{visibleStatusOptions.map(s => (
|
||||||
<Cell
|
<Cell
|
||||||
key={s}
|
key={s}
|
||||||
title={<Text className={s === industryText ? 'text-blue-600' : ''}>{s}</Text>}
|
title={<Text className={s === statusText ? 'text-blue-600' : ''}>{s}</Text>}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIndustryText(s)
|
setStatusText(s)
|
||||||
setIndustryVisible(false)
|
setStatusVisible(false)
|
||||||
reload(true).then()
|
reload(true).then()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user