refactor(credit): 将信用客户模块重构为小程序客户模块
- 替换 CreditCompany 相关 API 为 CreditMpCustomer API - 更新页面路由路径和组件名称 - 修改数据模型字段映射关系 - 调整界面布局使用 Cell 组件展示客户信息 - 更新搜索框提示文本内容 - 修改分配员工逻辑和确认提示 - 调整筛选条件从行业改为状态 - 更新联系电话获取逻辑从备注中提取 - 修改详情页编辑跳转路径 - 移除原公司详情页面的复杂业务逻辑 - 优化员工加载逻辑并添加缓存机制 - 更新客户列表项点击事件处理方式
This commit is contained in:
@@ -28,6 +28,8 @@ export interface CreditMpCustomer {
|
||||
files?: string;
|
||||
// 是否有数据
|
||||
hasData?: string;
|
||||
// 步骤
|
||||
step?: number;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 是否推荐
|
||||
|
||||
@@ -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 = <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()
|
||||
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<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 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<string | null>(null)
|
||||
const [company, setCompany] = useState<CreditCompany | null>(null)
|
||||
const [row, setRow] = useState<CreditMpCustomer | null>(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 (
|
||||
<View className="bg-pink-50 min-h-screen">
|
||||
@@ -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() {
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : !company ? (
|
||||
) : !row ? (
|
||||
<View className="bg-white rounded-xl border border-pink-100 py-10">
|
||||
<Empty description="暂无客户信息" />
|
||||
</View>
|
||||
) : (
|
||||
<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="mt-3 space-y-3 text-sm">
|
||||
<View className="flex items-start justify-between gap-3">
|
||||
<Text className="text-gray-500">统一代码</Text>
|
||||
<Text className="text-gray-400 text-right break-all">{code || '—'}</Text>
|
||||
<View className="text-base font-semibold text-gray-900">{title}</View>
|
||||
{!!desc && (
|
||||
<View className="mt-2 text-xs text-gray-500">
|
||||
<Text>{desc}</Text>
|
||||
</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">
|
||||
<Text className="text-gray-500">地址</Text>
|
||||
<Text className="text-gray-700 text-right break-all">{address || '—'}</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex items-start justify-between gap-3">
|
||||
<Text className="text-gray-500">跟进人</Text>
|
||||
<View className="text-right">
|
||||
{followPeople.length ? (
|
||||
followPeople.map((p, idx) => (
|
||||
<Text
|
||||
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 className="mt-3">
|
||||
<CellGroup>
|
||||
<Cell title="订单号" description={String(row.id ?? '—')} />
|
||||
<Cell title="状态" description={String(row.statusTxt || '—')} />
|
||||
<Cell title="分配人ID" description={row.userId ? String(row.userId) : '未分配'} />
|
||||
<Cell title="创建时间" description={fmtTime(row.createTime) || '—'} />
|
||||
<Cell title="更新时间" description={fmtTime(row.updateTime) || '—'} />
|
||||
{!!row.url && <Cell title="链接" description={String(row.url)} />}
|
||||
{!!row.files && <Cell title="文件" description={String(row.files)} />}
|
||||
{!!row.comments && <Cell title="备注" description={String(row.comments)} />}
|
||||
</CellGroup>
|
||||
</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>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = <T,>(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<CreditCompany[]>([])
|
||||
const staffLoadingPromiseRef = useRef<Promise<User[]> | null>(null)
|
||||
const [list, setList] = useState<CreditMpCustomer[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -88,8 +84,8 @@ export default function CreditCompanyPage() {
|
||||
const [followVisible, setFollowVisible] = useState(false)
|
||||
const [followStatus, setFollowStatus] = useState<FollowStatus>('全部')
|
||||
|
||||
const [industryVisible, setIndustryVisible] = useState(false)
|
||||
const [industryText, setIndustryText] = useState<string>('全部')
|
||||
const [statusVisible, setStatusVisible] = useState(false)
|
||||
const [statusText, setStatusText] = useState<string>('全部')
|
||||
|
||||
const [selectMode, setSelectMode] = useState(false)
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||
@@ -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<string>()
|
||||
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<User[]> => {
|
||||
if (staffList.length) return staffList
|
||||
if (staffLoadingPromiseRef.current) return staffLoadingPromiseRef.current
|
||||
|
||||
setStaffLoading(true)
|
||||
const p = (async () => {
|
||||
try {
|
||||
const res = await listUsers({ isAdmin: true } as any)
|
||||
setStaffList((res || []) as User[])
|
||||
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() {
|
||||
<ConfigProvider>
|
||||
<View className="py-2">
|
||||
<SearchBar
|
||||
placeholder="公司名称 / 统一代码"
|
||||
placeholder="拖欠方 / 关键词"
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onSearch={() => reload(true)}
|
||||
@@ -422,8 +440,8 @@ export default function CreditCompanyPage() {
|
||||
<Button size="small" fill="outline" onClick={() => setFollowVisible(true)}>
|
||||
{followStatus === '全部' ? '跟进状态' : followStatus}
|
||||
</Button>
|
||||
<Button size="small" fill="outline" onClick={() => setIndustryVisible(true)}>
|
||||
{industryText === '全部' ? '行业' : industryText}
|
||||
<Button size="small" fill="outline" onClick={() => setStatusVisible(true)}>
|
||||
{statusText === '全部' ? '状态' : statusText}
|
||||
</Button>
|
||||
<View className="flex-1" />
|
||||
<Button size="small" fill="outline" icon={<Copy />} onClick={copyPhones}>
|
||||
@@ -470,17 +488,24 @@ export default function CreditCompanyPage() {
|
||||
(c as any)?.userRealName ||
|
||||
(c as any)?.followRealName ||
|
||||
(c.userId ? staffNameMap.get(Number(c.userId)) : undefined)
|
||||
const name = c.matchName || c.name || `企业${c.id || ''}`
|
||||
const industry = getCompanyIndustry(c)
|
||||
const phones = getCompanyPhones(c)
|
||||
const name = String(c.toUser || '').trim() || `客户${c.id || ''}`
|
||||
const status = getRowStatus(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]
|
||||
return (
|
||||
<CellGroup key={c.id || idx} className="mb-3">
|
||||
<Cell
|
||||
onClick={() => {
|
||||
if (selectMode) return
|
||||
if (selectMode) {
|
||||
toggleSelectId(id, !selected)
|
||||
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">
|
||||
@@ -504,9 +529,9 @@ export default function CreditCompanyPage() {
|
||||
<View className="text-base font-bold text-gray-900 truncate">
|
||||
{name}
|
||||
</View>
|
||||
{!!industry && (
|
||||
{!!desc && (
|
||||
<View className="text-xs text-gray-500 truncate mt-1">
|
||||
{industry}
|
||||
{desc}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -523,6 +548,11 @@ export default function CreditCompanyPage() {
|
||||
|
||||
<View className="mt-2 flex items-center justify-between gap-2">
|
||||
<View className="text-xs text-gray-600 truncate">
|
||||
{!!status && (
|
||||
<Text className="mr-2">
|
||||
{status}
|
||||
</Text>
|
||||
)}
|
||||
<Text className="mr-2">
|
||||
{primaryPhone ? primaryPhone : '暂无电话'}
|
||||
</Text>
|
||||
@@ -649,21 +679,22 @@ export default function CreditCompanyPage() {
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
<Popup
|
||||
visible={industryVisible}
|
||||
position="bottom"
|
||||
style={{ height: '45vh' }}
|
||||
onClose={() => setIndustryVisible(false)}
|
||||
>
|
||||
<Popup visible={statusVisible} position="bottom" style={{ height: '45vh' }} onClose={() => setStatusVisible(false)}>
|
||||
<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>
|
||||
{visibleIndustryOptions.map(s => (
|
||||
{visibleStatusOptions.map(s => (
|
||||
<Cell
|
||||
key={s}
|
||||
title={<Text className={s === industryText ? 'text-blue-600' : ''}>{s}</Text>}
|
||||
title={<Text className={s === statusText ? 'text-blue-600' : ''}>{s}</Text>}
|
||||
onClick={() => {
|
||||
setIndustryText(s)
|
||||
setIndustryVisible(false)
|
||||
setStatusText(s)
|
||||
setStatusVisible(false)
|
||||
reload(true).then()
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user