feat(credit): 重构客户管理模块并新增信用客户功能
- 将公司相关页面重命名为客户管理页面 - 新增信用客户详情、编辑、跟进页面 - 实现客户状态跟踪和跟进流程 - 更新应用配置中的页面路由映射 - 优化订单详情页面的时间轴显示逻辑
This commit is contained in:
@@ -42,15 +42,18 @@ export default {
|
|||||||
"customer/index",
|
"customer/index",
|
||||||
"order/index",
|
"order/index",
|
||||||
"order/add",
|
"order/add",
|
||||||
"company/index",
|
|
||||||
"company/add",
|
|
||||||
"company/detail",
|
|
||||||
"company/follow-step1",
|
|
||||||
"company/edit",
|
|
||||||
"my-order/index",
|
"my-order/index",
|
||||||
"my-order/detail",
|
"my-order/detail",
|
||||||
'creditMpCustomer/index',
|
'creditMpCustomer/index',
|
||||||
'creditMpCustomer/add',
|
'creditMpCustomer/add',
|
||||||
|
"creditMpCustomer/detail",
|
||||||
|
"creditMpCustomer/follow-step1",
|
||||||
|
"creditMpCustomer/edit",
|
||||||
|
"mp-customer/index",
|
||||||
|
"mp-customer/add",
|
||||||
|
"mp-customer/detail",
|
||||||
|
"mp-customer/follow-step1",
|
||||||
|
"mp-customer/edit"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -219,7 +219,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/company/edit?id=${companyId}` })
|
if (res.tapIndex === 0 && companyId) Taro.navigateTo({ url: `/credit/my-customer/edit?id=${companyId}` })
|
||||||
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 || '')
|
||||||
@@ -336,7 +336,7 @@ export default function CreditCompanyDetailPage() {
|
|||||||
Taro.showToast({ title: '缺少客户ID', icon: 'none' })
|
Taro.showToast({ title: '缺少客户ID', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Taro.navigateTo({ url: `/credit/company/follow-step1?id=${companyId}` })
|
Taro.navigateTo({ url: `/credit/my-customer/follow-step1?id=${companyId}` })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
跟进
|
跟进
|
||||||
@@ -1,28 +1,53 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import Taro, { useDidShow } from '@tarojs/taro'
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import { Button, Cell, CellGroup, ConfigProvider, Empty, Space } from '@nutui/nutui-react-taro'
|
import { Button, Cell, CellGroup, ConfigProvider, Empty, Space } from '@nutui/nutui-react-taro'
|
||||||
import { ArrowRight, CheckNormal, Checked } from '@nutui/icons-react-taro'
|
import { ArrowRight, CheckNormal, Checked } from '@nutui/icons-react-taro'
|
||||||
|
|
||||||
import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model'
|
import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model'
|
||||||
import { listCreditMpCustomer, removeCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
import { pageCreditMpCustomer, removeCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||||
|
|
||||||
export default function CreditMpCustomerListPage() {
|
export default function CreditMpCustomerListPage() {
|
||||||
const [list, setList] = useState<CreditMpCustomer[]>([])
|
const [list, setList] = useState<CreditMpCustomer[]>([])
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const limit = 20
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
|
||||||
const reload = useCallback(async () => {
|
const hasMore = useMemo(() => list.length < count, [count, list.length])
|
||||||
setLoading(true)
|
|
||||||
|
const fetchPage = useCallback(
|
||||||
|
async (opts: { nextPage: number; replace: boolean }) => {
|
||||||
try {
|
try {
|
||||||
const data = await listCreditMpCustomer()
|
if (opts.replace) setLoading(true)
|
||||||
setList(data || [])
|
else setLoadingMore(true)
|
||||||
|
|
||||||
|
const res = await pageCreditMpCustomer({ page: opts.nextPage, limit } as any)
|
||||||
|
const incoming = (res?.list || []) as CreditMpCustomer[]
|
||||||
|
const total = Number(res?.count || 0)
|
||||||
|
setCount(Number.isFinite(total) ? total : 0)
|
||||||
|
setPage(opts.nextPage)
|
||||||
|
setList(prev => (opts.replace ? incoming : prev.concat(incoming)))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取数据失败:', e)
|
console.error('获取数据失败:', e)
|
||||||
Taro.showToast({ title: (e as any)?.message || '获取数据失败', icon: 'none' })
|
Taro.showToast({ title: (e as any)?.message || '获取数据失败', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
setLoadingMore(false)
|
||||||
}
|
}
|
||||||
}, [])
|
},
|
||||||
|
[limit]
|
||||||
|
)
|
||||||
|
|
||||||
|
const reload = useCallback(async () => {
|
||||||
|
await fetchPage({ nextPage: 1, replace: true })
|
||||||
|
}, [fetchPage])
|
||||||
|
|
||||||
|
const loadMore = useCallback(async () => {
|
||||||
|
if (loading || loadingMore || !hasMore) return
|
||||||
|
await fetchPage({ nextPage: page + 1, replace: false })
|
||||||
|
}, [fetchPage, hasMore, loading, loadingMore, page])
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
reload()
|
reload()
|
||||||
@@ -83,6 +108,12 @@ export default function CreditMpCustomerListPage() {
|
|||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View className="px-3 pt-3 pb-6">
|
<View className="px-3 pt-3 pb-6">
|
||||||
|
<View className="mb-2 text-xs text-gray-500 flex items-center justify-between">
|
||||||
|
<Text>共{count}条</Text>
|
||||||
|
<Text>
|
||||||
|
已加载{list.length}条
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
<CellGroup>
|
<CellGroup>
|
||||||
{list.map(row => {
|
{list.map(row => {
|
||||||
const recommended = row.recommend === 1
|
const recommended = row.recommend === 1
|
||||||
@@ -118,6 +149,11 @@ export default function CreditMpCustomerListPage() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
|
<View className="mt-3 flex justify-center">
|
||||||
|
<Button fill="none" size="small" style={{ color: '#bdbdbd' }} disabled={!hasMore || loadingMore} onClick={loadMore}>
|
||||||
|
{hasMore ? (loadingMore ? '加载中...' : '加载更多') : '没有更多了'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
<View className="mt-2 text-xs text-gray-400">
|
<View className="mt-2 text-xs text-gray-400">
|
||||||
长按任意一条可删除
|
长按任意一条可删除
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
7
src/credit/mp-customer/detail.config.ts
Normal file
7
src/credit/mp-customer/detail.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '客户详情',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
})
|
||||||
|
|
||||||
348
src/credit/mp-customer/detail.tsx
Normal file
348
src/credit/mp-customer/detail.tsx
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
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 { 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 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()
|
||||||
|
if (!txt) return ''
|
||||||
|
const m = txt.match(/联系人:([^;;]+)/)
|
||||||
|
return String(m?.[1] || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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() {
|
||||||
|
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<string | null>(null)
|
||||||
|
const [company, setCompany] = useState<CreditCompany | 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)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载客户详情失败:', e)
|
||||||
|
setCompany(null)
|
||||||
|
setError(String((e as any)?.message || '加载失败'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [companyId])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-pink-50 min-h-screen">
|
||||||
|
<ConfigProvider>
|
||||||
|
<View className="fixed z-50 top-0 left-0 right-0 bg-pink-50" style={{ paddingTop: `${statusBarHeight}px` }}>
|
||||||
|
<View className="px-4 h-10 flex items-center justify-between text-sm text-gray-900">
|
||||||
|
<Text className="font-medium">12:00</Text>
|
||||||
|
<View className="flex items-center gap-2 text-xs text-gray-600">
|
||||||
|
<Text>信号</Text>
|
||||||
|
<Text>Wi-Fi</Text>
|
||||||
|
<Text>电池</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="px-4 pb-2 flex items-center justify-between">
|
||||||
|
<Text className="text-sm text-gray-700" onClick={() => Taro.navigateBack()}>
|
||||||
|
返回
|
||||||
|
</Text>
|
||||||
|
<Text className="text-base font-semibold text-gray-900">客户详情</Text>
|
||||||
|
<View className="flex items-center gap-3">
|
||||||
|
<View
|
||||||
|
className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700"
|
||||||
|
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 === 1) reload()
|
||||||
|
} catch (e) {
|
||||||
|
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
||||||
|
if (msg.includes('cancel')) return
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>...</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700"
|
||||||
|
onClick={() => Taro.showToast({ title: '设置(示意)', icon: 'none' })}
|
||||||
|
>
|
||||||
|
<Setting size={14} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{ paddingTop: `${headerOffset}px` }} className="max-w-md mx-auto px-4 pb-24">
|
||||||
|
{loading ? (
|
||||||
|
<View className="py-16 flex justify-center items-center text-gray-500">
|
||||||
|
<Loading />
|
||||||
|
<Text className="ml-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
|
<View className="bg-white rounded-xl border border-pink-100 p-6">
|
||||||
|
<View className="text-red-500 text-sm">{error}</View>
|
||||||
|
<View className="mt-4">
|
||||||
|
<Button type="primary" onClick={reload}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : !company ? (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/credit/mp-customer/edit.config.ts
Normal file
7
src/credit/mp-customer/edit.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '修改客户信息',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
})
|
||||||
|
|
||||||
364
src/credit/mp-customer/edit.tsx
Normal file
364
src/credit/mp-customer/edit.tsx
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Cell, CellGroup, ConfigProvider, Popup, Tag, TextArea } from '@nutui/nutui-react-taro'
|
||||||
|
|
||||||
|
import { getCreditCompany } from '@/api/credit/creditCompany'
|
||||||
|
import type { CreditCompany } from '@/api/credit/creditCompany/model'
|
||||||
|
import { listUsers } from '@/api/system/user'
|
||||||
|
import type { User } from '@/api/system/user/model'
|
||||||
|
import FixedButton from '@/components/FixedButton'
|
||||||
|
|
||||||
|
type CustomerStatus = '保护期内' | '已签约' | '已完成' | '保护期外'
|
||||||
|
|
||||||
|
const STATUS_OPTIONS: CustomerStatus[] = ['保护期内', '已签约', '已完成', '保护期外']
|
||||||
|
const PENDING_EDIT_STORAGE_KEY = 'credit_company_pending_edit_map'
|
||||||
|
|
||||||
|
const safeParseJSON = <T,>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getCompanyPhones = (c?: CreditCompany | null) => {
|
||||||
|
if (!c) return []
|
||||||
|
const arr = [...splitPhones(c.tel), ...splitPhones(c.moreTel)]
|
||||||
|
return Array.from(new Set(arr))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCompanyIndustry = (c?: CreditCompany | null) => {
|
||||||
|
if (!c) return ''
|
||||||
|
return String(
|
||||||
|
c.nationalStandardIndustryCategories6 ||
|
||||||
|
c.nationalStandardIndustryCategories2 ||
|
||||||
|
c.nationalStandardIndustryCategories ||
|
||||||
|
c.institutionType ||
|
||||||
|
''
|
||||||
|
).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
type PendingEdit = {
|
||||||
|
status: CustomerStatus
|
||||||
|
remark: string
|
||||||
|
submittedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPendingMap = (): Record<string, PendingEdit> => {
|
||||||
|
try {
|
||||||
|
const raw = Taro.getStorageSync(PENDING_EDIT_STORAGE_KEY)
|
||||||
|
return safeParseJSON<Record<string, PendingEdit>>(raw) || {}
|
||||||
|
} catch (_e) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePendingMap = (map: Record<string, PendingEdit>) => {
|
||||||
|
try {
|
||||||
|
Taro.setStorageSync(PENDING_EDIT_STORAGE_KEY, map)
|
||||||
|
} catch (_e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreditCompanyEditPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const companyId = Number(router?.params?.id)
|
||||||
|
|
||||||
|
const statusBarHeight = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const info = Taro.getSystemInfoSync()
|
||||||
|
return Number(info?.statusBarHeight || 0)
|
||||||
|
} catch (_e) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [company, setCompany] = useState<CreditCompany | null>(null)
|
||||||
|
const [staffList, setStaffList] = useState<User[]>([])
|
||||||
|
const [statusPickerVisible, setStatusPickerVisible] = useState(false)
|
||||||
|
|
||||||
|
const [customerStatus, setCustomerStatus] = useState<CustomerStatus>('保护期内')
|
||||||
|
const [remark, setRemark] = useState('')
|
||||||
|
const [pending, setPending] = useState(false)
|
||||||
|
const [pendingAt, setPendingAt] = useState<string | undefined>(undefined)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const ensureStaffLoaded = useCallback(async () => {
|
||||||
|
if (staffList.length) return
|
||||||
|
try {
|
||||||
|
const res = await listUsers({ isStaff: true } as any)
|
||||||
|
setStaffList((res || []) as User[])
|
||||||
|
} catch (_e) {
|
||||||
|
// ignore (只影响“跟进人”展示)
|
||||||
|
}
|
||||||
|
}, [staffList.length])
|
||||||
|
|
||||||
|
const staffNameMap = useMemo(() => {
|
||||||
|
const map = new Map<number, string>()
|
||||||
|
for (const u of staffList) {
|
||||||
|
if (!u?.userId) continue
|
||||||
|
map.set(u.userId, String(u.realName || u.nickname || u.username || `员工${u.userId}`))
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [staffList])
|
||||||
|
|
||||||
|
const followRealName = useMemo(() => {
|
||||||
|
if (!company?.userId) return ''
|
||||||
|
return staffNameMap.get(Number(company.userId)) || ''
|
||||||
|
}, [company?.userId, staffNameMap])
|
||||||
|
|
||||||
|
const assignDate = useMemo(() => {
|
||||||
|
// 兼容:接口未提供“分配日期”字段时,用 updateTime/createTime 兜底展示
|
||||||
|
return String(company?.updateTime || company?.createTime || '')
|
||||||
|
}, [company?.createTime, company?.updateTime])
|
||||||
|
|
||||||
|
const pendingKey = useMemo(() => (Number.isFinite(companyId) && companyId > 0 ? String(companyId) : ''), [companyId])
|
||||||
|
|
||||||
|
const loadCompany = useCallback(async () => {
|
||||||
|
if (!Number.isFinite(companyId) || companyId <= 0) {
|
||||||
|
Taro.showToast({ title: '参数错误', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const data = await getCreditCompany(companyId)
|
||||||
|
setCompany(data)
|
||||||
|
|
||||||
|
// 从本地 pending map 恢复“待审核”状态与内容(纯前端模拟)
|
||||||
|
const map = loadPendingMap()
|
||||||
|
const pendingEdit = pendingKey ? map[pendingKey] : undefined
|
||||||
|
if (pendingEdit) {
|
||||||
|
setCustomerStatus(pendingEdit.status)
|
||||||
|
setRemark(pendingEdit.remark)
|
||||||
|
setPending(true)
|
||||||
|
setPendingAt(pendingEdit.submittedAt)
|
||||||
|
} else {
|
||||||
|
setCustomerStatus('保护期内')
|
||||||
|
setRemark('')
|
||||||
|
setPending(false)
|
||||||
|
setPendingAt(undefined)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载客户信息失败:', e)
|
||||||
|
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [companyId, pendingKey])
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
loadCompany().then()
|
||||||
|
ensureStaffLoaded().then()
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitOrWithdraw = async () => {
|
||||||
|
if (submitting) return
|
||||||
|
if (!pendingKey) return
|
||||||
|
|
||||||
|
// 撤回修改
|
||||||
|
if (pending) {
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
const map = loadPendingMap()
|
||||||
|
delete map[pendingKey]
|
||||||
|
savePendingMap(map)
|
||||||
|
setPending(false)
|
||||||
|
setPendingAt(undefined)
|
||||||
|
Taro.showToast({ title: '已撤回,可重新修改', icon: 'success' })
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交修改(仅备注 + 状态),进入待审核(纯前端模拟)
|
||||||
|
const r = remark.trim()
|
||||||
|
if (!r) {
|
||||||
|
Taro.showToast({ title: '请填写备注信息', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
const map = loadPendingMap()
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
map[pendingKey] = { status: customerStatus, remark: r, submittedAt: now }
|
||||||
|
savePendingMap(map)
|
||||||
|
|
||||||
|
setPending(true)
|
||||||
|
setPendingAt(now)
|
||||||
|
Taro.showToast({ title: '提交成功,等待审核', icon: 'success' })
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const phones = useMemo(() => getCompanyPhones(company), [company])
|
||||||
|
const industry = useMemo(() => getCompanyIndustry(company), [company])
|
||||||
|
const companyName = String(company?.matchName || company?.name || '')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-pink-50 min-h-screen">
|
||||||
|
<ConfigProvider>
|
||||||
|
<View className="fixed z-50 top-0 left-0 right-0 bg-pink-50" style={{ paddingTop: `${statusBarHeight}px` }}>
|
||||||
|
<View className="px-4 h-10 flex items-center justify-between text-sm text-gray-900">
|
||||||
|
<Text className="font-medium">12:00</Text>
|
||||||
|
<View className="flex items-center gap-2 text-xs text-gray-600">
|
||||||
|
<Text>信号</Text>
|
||||||
|
<Text>Wi-Fi</Text>
|
||||||
|
<Text>电池</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="px-4 pb-2 flex items-center justify-between">
|
||||||
|
<Text className="text-sm text-gray-700" onClick={() => Taro.navigateBack()}>
|
||||||
|
返回
|
||||||
|
</Text>
|
||||||
|
<Text className="text-base font-semibold text-gray-900">修改客户信息</Text>
|
||||||
|
<View className="flex items-center gap-3">
|
||||||
|
<View className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700">
|
||||||
|
<Text>...</Text>
|
||||||
|
</View>
|
||||||
|
<View className="w-7 h-7 rounded-full border border-gray-300" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{ paddingTop: `${statusBarHeight + 80}px` }} className="max-w-md mx-auto px-3">
|
||||||
|
{loading ? (
|
||||||
|
<View className="py-12 text-center text-gray-500">
|
||||||
|
<Text>加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CellGroup>
|
||||||
|
<Cell title="客户名称" description={companyName || '-'} />
|
||||||
|
<Cell title="统一代码" description={String(company?.code || '-')} />
|
||||||
|
<Cell
|
||||||
|
title="所属行业"
|
||||||
|
description={
|
||||||
|
<Text className="text-red-500">
|
||||||
|
{industry || '-'}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Cell title="客户联系人" description={String((company as any)?.contactName || '-')} />
|
||||||
|
<Cell
|
||||||
|
title="客户联系方式"
|
||||||
|
description={
|
||||||
|
phones.length ? (
|
||||||
|
<View className="flex flex-col">
|
||||||
|
{phones.map(p => (
|
||||||
|
<Text key={p} className="text-gray-600">
|
||||||
|
{p}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Cell title="地址" description={String(company?.address || '-')} />
|
||||||
|
<Cell
|
||||||
|
title="跟进人"
|
||||||
|
description={
|
||||||
|
<Text className="text-blue-600">
|
||||||
|
{followRealName || '未分配'}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Cell title="分配日期" description={assignDate || '-'} />
|
||||||
|
<Cell
|
||||||
|
title="客户状态"
|
||||||
|
description={
|
||||||
|
<View className="flex items-center gap-2">
|
||||||
|
<Text className={customerStatus === '保护期内' ? 'text-green-600' : 'text-gray-700'}>
|
||||||
|
{customerStatus}
|
||||||
|
</Text>
|
||||||
|
{pending && <Tag type="warning">待审核</Tag>}
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (pending) return
|
||||||
|
setStatusPickerVisible(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CellGroup>
|
||||||
|
|
||||||
|
<View className="mt-3 px-1">
|
||||||
|
<View className="text-sm text-gray-700 mb-2">修改内容备注信息</View>
|
||||||
|
<TextArea
|
||||||
|
placeholder="请输入客户最新信息情况,管理员审核通过后,客户信息才可变更!"
|
||||||
|
value={remark}
|
||||||
|
onChange={(v) => setRemark(v)}
|
||||||
|
rows={4}
|
||||||
|
maxLength={500}
|
||||||
|
showCount
|
||||||
|
autoHeight
|
||||||
|
disabled={pending}
|
||||||
|
/>
|
||||||
|
{pending && (
|
||||||
|
<View className="mt-2 text-xs text-gray-500">
|
||||||
|
已提交审核{pendingAt ? `:${pendingAt}` : ''}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
visible={statusPickerVisible}
|
||||||
|
position="bottom"
|
||||||
|
style={{ height: '45vh' }}
|
||||||
|
onClose={() => setStatusPickerVisible(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={() => setStatusPickerVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<CellGroup>
|
||||||
|
{STATUS_OPTIONS.map(s => (
|
||||||
|
<Cell
|
||||||
|
key={s}
|
||||||
|
title={<Text className={s === customerStatus ? 'text-blue-600' : ''}>{s}</Text>}
|
||||||
|
onClick={() => {
|
||||||
|
setCustomerStatus(s)
|
||||||
|
setStatusPickerVisible(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CellGroup>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
|
<FixedButton
|
||||||
|
text={submitting ? '处理中...' : pending ? '撤回修改' : '确定修改'}
|
||||||
|
background="#ef4444"
|
||||||
|
disabled={loading || submitting}
|
||||||
|
onClick={submitOrWithdraw}
|
||||||
|
/>
|
||||||
|
</ConfigProvider>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/credit/mp-customer/follow-step1.config.ts
Normal file
7
src/credit/mp-customer/follow-step1.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '客户跟进',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationBarBackgroundColor: '#ffffff',
|
||||||
|
navigationStyle: 'custom'
|
||||||
|
})
|
||||||
|
|
||||||
427
src/credit/mp-customer/follow-step1.tsx
Normal file
427
src/credit/mp-customer/follow-step1.tsx
Normal file
@@ -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 = <T,>(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 = <T,>(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<string | null>(null)
|
||||||
|
const [company, setCompany] = useState<CreditCompany | null>(null)
|
||||||
|
|
||||||
|
const [submitted, setSubmitted] = useState(false)
|
||||||
|
const [audioSelected, setAudioSelected] = useState(false)
|
||||||
|
const [smsShots, setSmsShots] = useState<Array<{ id: string }>>([])
|
||||||
|
const [callShots, setCallShots] = useState<Array<{ id: string }>>([])
|
||||||
|
const [remark, setRemark] = useState('')
|
||||||
|
const [intention, setIntention] = useState<Intention | undefined>(undefined)
|
||||||
|
|
||||||
|
const loadDraft = useCallback(() => {
|
||||||
|
if (!companyId) return
|
||||||
|
const raw = Taro.getStorageSync(getDraftKey(companyId))
|
||||||
|
const saved = safeParseJSON<FollowStep1Draft>(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 (
|
||||||
|
<View className="bg-pink-50 min-h-screen">
|
||||||
|
<ConfigProvider>
|
||||||
|
<View className="fixed z-50 top-0 left-0 right-0 bg-pink-50" style={{ paddingTop: `${statusBarHeight}px` }}>
|
||||||
|
<View className="px-4 h-10 flex items-center justify-between text-sm text-gray-900">
|
||||||
|
<Text className="font-medium">12:00</Text>
|
||||||
|
<View className="flex items-center gap-2 text-xs text-gray-600">
|
||||||
|
<Text>信号</Text>
|
||||||
|
<Text>Wi-Fi</Text>
|
||||||
|
<Text>电池</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="px-4 pb-2 flex items-center justify-between">
|
||||||
|
<Text className="text-sm text-gray-700" onClick={() => Taro.navigateBack()}>
|
||||||
|
返回
|
||||||
|
</Text>
|
||||||
|
<Text className="text-base font-semibold text-gray-900">客户跟进</Text>
|
||||||
|
<View className="flex items-center gap-3">
|
||||||
|
<View
|
||||||
|
className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700"
|
||||||
|
onClick={async () => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>...</Text>
|
||||||
|
</View>
|
||||||
|
<View className="w-7 h-7 rounded-full border border-gray-300 flex items-center justify-center text-gray-700">
|
||||||
|
<Setting size={14} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{ paddingTop: `${headerOffset}px` }} className="max-w-md mx-auto px-4 pb-28">
|
||||||
|
{loading ? (
|
||||||
|
<View className="py-16 flex justify-center items-center text-gray-500">
|
||||||
|
<Loading />
|
||||||
|
<Text className="ml-2">加载中...</Text>
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
|
<View className="bg-white rounded-xl border border-pink-100 p-6">
|
||||||
|
<View className="text-red-500 text-sm">{error}</View>
|
||||||
|
<View className="mt-4">
|
||||||
|
<Button type="primary" onClick={reloadCompany}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : !company ? (
|
||||||
|
<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-sm text-gray-900 font-semibold">加微信前沟通</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">{contact || '—'}</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">{phone || '—'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex items-center justify-between gap-3">
|
||||||
|
<Text className="text-gray-500">
|
||||||
|
电话录音
|
||||||
|
</Text>
|
||||||
|
<Button size="small" fill="outline" disabled={!canEdit} onClick={chooseAudio}>
|
||||||
|
{audioSelected ? '已选择' : '点击上传'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="pt-2 border-t border-pink-100" />
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-gray-700">短信截图</Text>
|
||||||
|
<Text className="text-xs text-gray-400">{smsShots.length}/6</Text>
|
||||||
|
</View>
|
||||||
|
<View className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{smsShots.map((x, idx) => (
|
||||||
|
<View
|
||||||
|
key={x.id}
|
||||||
|
className="w-16 h-16 rounded-lg bg-gray-200 flex items-center justify-center text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
图{idx + 1}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
<View
|
||||||
|
className={`w-16 h-16 rounded-lg border border-dashed flex items-center justify-center text-2xl ${
|
||||||
|
!canEdit || smsShots.length >= 6 ? 'border-gray-200 text-gray-200' : 'border-gray-300 text-gray-400'
|
||||||
|
}`}
|
||||||
|
onClick={addSmsShot}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-gray-700">电话沟通截图</Text>
|
||||||
|
<Text className="text-xs text-gray-400">{callShots.length}/6</Text>
|
||||||
|
</View>
|
||||||
|
<View className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{callShots.map((x, idx) => (
|
||||||
|
<View
|
||||||
|
key={x.id}
|
||||||
|
className="w-16 h-16 rounded-lg bg-gray-200 flex items-center justify-center text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
图{idx + 1}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
<View
|
||||||
|
className={`w-16 h-16 rounded-lg border border-dashed flex items-center justify-center text-2xl ${
|
||||||
|
!canEdit || callShots.length >= 6 ? 'border-gray-200 text-gray-200' : 'border-gray-300 text-gray-400'
|
||||||
|
}`}
|
||||||
|
onClick={addCallShot}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text className="text-gray-700">
|
||||||
|
沟通情况<Text className="text-red-500">*</Text>
|
||||||
|
</Text>
|
||||||
|
<View className="mt-2">
|
||||||
|
<TextArea
|
||||||
|
value={remark}
|
||||||
|
onChange={setRemark}
|
||||||
|
placeholder="请输入"
|
||||||
|
disabled={!canEdit}
|
||||||
|
maxLength={300}
|
||||||
|
style={{ background: '#fff' }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text className="text-gray-700">
|
||||||
|
意向选择<Text className="text-red-500">*</Text>
|
||||||
|
</Text>
|
||||||
|
<View className="mt-2 grid grid-cols-2 gap-3">
|
||||||
|
<View
|
||||||
|
className={`rounded-xl border px-3 py-2 text-center ${
|
||||||
|
intention === '无意向' ? 'border-red-400 bg-red-50 text-red-600' : 'border-gray-200 text-gray-700'
|
||||||
|
} ${!canEdit ? 'opacity-60' : ''}`}
|
||||||
|
onClick={() => canEdit && setIntention('无意向')}
|
||||||
|
>
|
||||||
|
无意向
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`rounded-xl border px-3 py-2 text-center ${
|
||||||
|
intention === '有意向' ? 'border-red-400 bg-red-50 text-red-600' : 'border-gray-200 text-gray-700'
|
||||||
|
} ${!canEdit ? 'opacity-60' : ''}`}
|
||||||
|
onClick={() => canEdit && setIntention('有意向')}
|
||||||
|
>
|
||||||
|
有意向
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="mt-2 text-xs text-gray-400">
|
||||||
|
规则:多次沟通后标记“有意向”可直接进入下一步;多次沟通后标记“无意向”需管理员审核。
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{submitted && (
|
||||||
|
<View className="pt-2 text-xs text-gray-400">
|
||||||
|
跟进信息已提交(表单已锁定)
|
||||||
|
</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
|
||||||
|
disabled={!canEdit}
|
||||||
|
style={{
|
||||||
|
background: submitted ? '#94a3b8' : '#ef4444',
|
||||||
|
borderColor: submitted ? '#94a3b8' : '#ef4444'
|
||||||
|
}}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</ConfigProvider>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,8 +19,9 @@ 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 { pageCreditCompany, updateCreditCompany } from '@/api/credit/creditCompany'
|
import { updateCreditCompany } from '@/api/credit/creditCompany'
|
||||||
import type { CreditCompany } from '@/api/credit/creditCompany/model'
|
import type { CreditCompany } from '@/api/credit/creditCompany/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'
|
||||||
@@ -239,7 +240,7 @@ export default function CreditCompanyPage() {
|
|||||||
keywords: searchValue?.trim() || undefined
|
keywords: searchValue?.trim() || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await pageCreditCompany(params)
|
const res = await pageCreditMpCustomer(params as any)
|
||||||
const incoming = (res?.list || []) as CreditCompany[]
|
const incoming = (res?.list || []) as CreditCompany[]
|
||||||
const filtered = applyFilters(incoming)
|
const filtered = applyFilters(incoming)
|
||||||
|
|
||||||
@@ -337,7 +338,7 @@ export default function CreditCompanyPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openAddCustomer = () => {
|
const openAddCustomer = () => {
|
||||||
Taro.navigateTo({ url: '/credit/company/add' })
|
Taro.navigateTo({ url: '/credit/my-customer/add' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensureStaffLoaded = async () => {
|
const ensureStaffLoaded = async () => {
|
||||||
@@ -479,7 +480,7 @@ export default function CreditCompanyPage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectMode) return
|
if (selectMode) return
|
||||||
if (!c?.id) return
|
if (!c?.id) return
|
||||||
Taro.navigateTo({ url: `/credit/company/detail?id=${c.id}` })
|
Taro.navigateTo({ url: `/credit/my-customer/detail?id=${c.id}` })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className="flex gap-3 items-start w-full">
|
<View className="flex gap-3 items-start w-full">
|
||||||
@@ -18,10 +18,45 @@ type TimelineNode = {
|
|||||||
actionText?: string
|
actionText?: string
|
||||||
actionType?: 'detail'
|
actionType?: 'detail'
|
||||||
variant?: 'normal' | 'returned'
|
variant?: 'normal' | 'returned'
|
||||||
|
state?: 'done' | 'current' | 'todo'
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeText = (v: any) => String(v ?? '').trim()
|
const safeText = (v: any) => String(v ?? '').trim()
|
||||||
|
|
||||||
|
const STEP_TITLES = ['未受理', '已受理', '材料提交', '合同签订', '执行回款', '完结'] as const
|
||||||
|
|
||||||
|
const clampStep = (n: number) => Math.max(0, Math.min(5, n))
|
||||||
|
|
||||||
|
const inferStepFromStatusText = (txt: string) => {
|
||||||
|
const t = safeText(txt)
|
||||||
|
if (!t) return undefined
|
||||||
|
if (t.includes('完结') || t.includes('已办结')) return 5
|
||||||
|
if (t.includes('执行回款')) return 4
|
||||||
|
if (t.includes('合同') || t.includes('签订')) return 3
|
||||||
|
if (t.includes('材料')) return 2
|
||||||
|
if (t.includes('已受理') || (t.includes('受理') && !t.includes('未受理'))) return 1
|
||||||
|
if (t.includes('未受理')) return 0
|
||||||
|
const n = Number(t)
|
||||||
|
if (Number.isInteger(n) && n >= 0 && n <= 5) return n
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDemandStep = (d?: CreditMpCustomer | null) => {
|
||||||
|
const anyDemand = (d || {}) as any
|
||||||
|
const raw =
|
||||||
|
anyDemand?.step ??
|
||||||
|
anyDemand?.stepStatus ??
|
||||||
|
anyDemand?.statusStep ??
|
||||||
|
anyDemand?.stepNum ??
|
||||||
|
anyDemand?.stepCode ??
|
||||||
|
anyDemand?.statusTxt
|
||||||
|
|
||||||
|
const txt = safeText(raw)
|
||||||
|
const n = Number(txt)
|
||||||
|
if (Number.isInteger(n) && n >= 0 && n <= 5) return n
|
||||||
|
return inferStepFromStatusText(txt)
|
||||||
|
}
|
||||||
|
|
||||||
const formatDate = (t?: string) => {
|
const formatDate = (t?: string) => {
|
||||||
const txt = safeText(t)
|
const txt = safeText(t)
|
||||||
if (!txt) return ''
|
if (!txt) return ''
|
||||||
@@ -116,44 +151,53 @@ export default function CreditMyOrderDetailPage() {
|
|||||||
const topStatus = useMemo(() => safeText((company as any)?.customerStatus || (company as any)?.statusText) || '保护期内', [company])
|
const topStatus = useMemo(() => safeText((company as any)?.customerStatus || (company as any)?.statusText) || '保护期内', [company])
|
||||||
|
|
||||||
const timeline = useMemo<TimelineNode[]>(() => {
|
const timeline = useMemo<TimelineNode[]>(() => {
|
||||||
const returned = safeText(demand?.statusTxt).includes('退回')
|
const statusTxt = safeText(demand?.statusTxt)
|
||||||
const base: TimelineNode[] = [
|
const returned = statusTxt.includes('退回')
|
||||||
{
|
const step = getDemandStep(demand)
|
||||||
title: '第一步:加微信前沟通',
|
const currentStep = typeof step === 'number' ? clampStep(step) : 0
|
||||||
status: '审核通过',
|
const isNumericStatusTxt = Number.isInteger(Number(statusTxt))
|
||||||
time: '2025-11-11',
|
const currentStatusText = !isNumericStatusTxt && statusTxt ? statusTxt : '进行中'
|
||||||
actionText: '查看详情',
|
|
||||||
actionType: 'detail'
|
const createTime = safeText(demand?.createTime)
|
||||||
},
|
const updateTime = safeText(demand?.updateTime)
|
||||||
{ title: '已受理', desc: `负责人:${topFollower || 'XXX'}`, time: '2025-01-01 11:30:30' },
|
const currentTime = updateTime || createTime
|
||||||
{ title: '材料提交与梳理(第二步节点完成)', desc: '审核评估', time: '2025-01-01 11:30:30', tag: '第二步完成' },
|
|
||||||
{ title: '合同已签订(第五步节点完成)', desc: '项目启动', time: '2025-01-01 11:30:30', tag: '第五步完成' },
|
const base: TimelineNode[] = STEP_TITLES.map((title, idx) => {
|
||||||
{
|
const isCurrent = !returned && idx === currentStep
|
||||||
title: '执行回款(第六步节点完成)',
|
const isDone = returned ? idx <= currentStep : idx < currentStep
|
||||||
desc: '部分回款:金额\n全部回款:金额',
|
const state: TimelineNode['state'] = isCurrent ? 'current' : isDone ? 'done' : 'todo'
|
||||||
time: '2025-03-12 11:30:30',
|
const node: TimelineNode = {
|
||||||
tag: '第六步完成'
|
title,
|
||||||
},
|
state,
|
||||||
{ title: '完结', desc: '已办结/已终止', time: '2025-01-01 11:30:30' }
|
status: isCurrent ? currentStatusText : isDone ? '已完成' : '待处理'
|
||||||
]
|
}
|
||||||
|
|
||||||
|
if (idx === 0 && createTime) node.time = createTime
|
||||||
|
if (isCurrent && currentTime) node.time = currentTime
|
||||||
|
if (isCurrent) node.tag = '当前'
|
||||||
|
if (idx === 1 && topFollower) node.desc = `负责人:${topFollower}`
|
||||||
|
return node
|
||||||
|
})
|
||||||
|
|
||||||
if (returned) {
|
if (returned) {
|
||||||
base.splice(2, 0, {
|
const anyDemand = (demand || {}) as any
|
||||||
title: '已退回',
|
const reason =
|
||||||
variant: 'returned',
|
safeText(anyDemand?.returnReason || anyDemand?.reason || anyDemand?.rejectReason || anyDemand?.rejectMsg || anyDemand?.auditRemark) ||
|
||||||
desc: '原因:需求不清晰,请重新提交',
|
'需求已退回,请根据提示重新提交'
|
||||||
time: '2025-01-01 11:30:30'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
base.push({
|
base.push({
|
||||||
title: '已退回',
|
title: '已退回',
|
||||||
variant: 'returned',
|
variant: 'returned',
|
||||||
desc: '原因:需求不清晰,请重新提交',
|
state: 'current',
|
||||||
time: '2025-01-01 11:30:30'
|
status: '已退回',
|
||||||
|
desc: `原因:${reason}`,
|
||||||
|
time: currentTime || undefined,
|
||||||
|
tag: '退回'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return base
|
return base
|
||||||
}, [demand?.statusTxt, topFollower])
|
}, [demand, topFollower])
|
||||||
|
|
||||||
const onNodeAction = async (node: TimelineNode) => {
|
const onNodeAction = async (node: TimelineNode) => {
|
||||||
if (node.actionType === 'detail') {
|
if (node.actionType === 'detail') {
|
||||||
@@ -222,8 +266,21 @@ export default function CreditMyOrderDetailPage() {
|
|||||||
{timeline.map((n, idx) => {
|
{timeline.map((n, idx) => {
|
||||||
const isLast = idx === timeline.length - 1
|
const isLast = idx === timeline.length - 1
|
||||||
const isReturned = n.variant === 'returned'
|
const isReturned = n.variant === 'returned'
|
||||||
const circleClass = isReturned ? 'bg-red-600' : 'bg-red-500'
|
const state = n.state || 'todo'
|
||||||
const lineClass = isReturned ? 'bg-red-200' : 'bg-red-200'
|
const circleClass = isReturned
|
||||||
|
? 'bg-red-600'
|
||||||
|
: state === 'done'
|
||||||
|
? 'bg-green-500'
|
||||||
|
: state === 'current'
|
||||||
|
? 'bg-orange-500'
|
||||||
|
: 'bg-gray-300'
|
||||||
|
const lineClass = isReturned
|
||||||
|
? 'bg-red-200'
|
||||||
|
: state === 'done'
|
||||||
|
? 'bg-green-200'
|
||||||
|
: state === 'current'
|
||||||
|
? 'bg-orange-200'
|
||||||
|
: 'bg-gray-200'
|
||||||
const tag = safeText(n.tag)
|
const tag = safeText(n.tag)
|
||||||
const status = safeText(n.status)
|
const status = safeText(n.status)
|
||||||
const time = safeText(n.time)
|
const time = safeText(n.time)
|
||||||
@@ -239,7 +296,11 @@ export default function CreditMyOrderDetailPage() {
|
|||||||
<View className="flex-1 pb-4">
|
<View className="flex-1 pb-4">
|
||||||
<View className="flex items-start justify-between gap-3">
|
<View className="flex items-start justify-between gap-3">
|
||||||
<View className="min-w-0">
|
<View className="min-w-0">
|
||||||
<Text className={`text-sm font-semibold ${isReturned ? 'text-red-600' : 'text-gray-900'}`}>
|
<Text
|
||||||
|
className={`text-sm font-semibold ${
|
||||||
|
isReturned ? 'text-red-600' : state === 'done' ? 'text-gray-900' : state === 'current' ? 'text-orange-600' : 'text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{n.title}
|
{n.title}
|
||||||
</Text>
|
</Text>
|
||||||
{(status || time) && (
|
{(status || time) && (
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ type ListQuery = {
|
|||||||
userId?: number
|
userId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STEP_STATUS_TEXT: Record<number, string> = {
|
||||||
|
0: '未受理',
|
||||||
|
1: '已受理',
|
||||||
|
2: '材料提交',
|
||||||
|
3: '合同签订',
|
||||||
|
4: '执行回款',
|
||||||
|
5: '完结'
|
||||||
|
}
|
||||||
|
|
||||||
const getCurrentUserId = (): number | undefined => {
|
const getCurrentUserId = (): number | undefined => {
|
||||||
try {
|
try {
|
||||||
const raw = Taro.getStorageSync('UserId')
|
const raw = Taro.getStorageSync('UserId')
|
||||||
@@ -31,11 +40,30 @@ const buildDesc = (row: CreditMpCustomer) => {
|
|||||||
return [price, years, location].filter(Boolean).join(' · ')
|
return [price, years, location].filter(Boolean).join(' · ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStepStatusText = (row: CreditMpCustomer) => {
|
||||||
|
const anyRow = row as any
|
||||||
|
const raw =
|
||||||
|
anyRow?.step ??
|
||||||
|
anyRow?.stepStatus ??
|
||||||
|
anyRow?.statusStep ??
|
||||||
|
anyRow?.stepNum ??
|
||||||
|
anyRow?.stepCode ??
|
||||||
|
row.statusTxt
|
||||||
|
|
||||||
|
const txt = String(raw ?? '').trim()
|
||||||
|
if (!txt) return '处理中'
|
||||||
|
|
||||||
|
const n = Number(txt)
|
||||||
|
if (Number.isInteger(n) && n in STEP_STATUS_TEXT) return STEP_STATUS_TEXT[n]
|
||||||
|
return txt
|
||||||
|
}
|
||||||
|
|
||||||
const getStatusBadgeClass = (s?: string) => {
|
const getStatusBadgeClass = (s?: string) => {
|
||||||
const txt = String(s || '').trim()
|
const txt = String(s ?? '').trim()
|
||||||
if (!txt) return 'bg-gray-400'
|
if (!txt) return 'bg-gray-400'
|
||||||
if (txt.includes('退回')) return 'bg-red-500'
|
if (txt.includes('退回')) return 'bg-red-500'
|
||||||
if (txt.includes('完结') || txt.includes('已办结')) return 'bg-green-600'
|
if (txt.includes('完结') || txt.includes('已办结')) return 'bg-green-600'
|
||||||
|
if (txt.includes('未受理')) return 'bg-gray-400'
|
||||||
if (txt.includes('受理') || txt.includes('通过') || txt.includes('签订')) return 'bg-orange-500'
|
if (txt.includes('受理') || txt.includes('通过') || txt.includes('签订')) return 'bg-orange-500'
|
||||||
return 'bg-blue-500'
|
return 'bg-blue-500'
|
||||||
}
|
}
|
||||||
@@ -155,7 +183,7 @@ export default function CreditMyOrderPage() {
|
|||||||
list.map(row => {
|
list.map(row => {
|
||||||
const title = String(row.toUser || '-').trim()
|
const title = String(row.toUser || '-').trim()
|
||||||
const desc = buildDesc(row)
|
const desc = buildDesc(row)
|
||||||
const statusTxt = String(row.statusTxt || '处理中').trim()
|
const statusTxt = getStepStatusText(row)
|
||||||
const time = String(row.createTime || '').slice(0, 19).replace('T', ' ')
|
const time = String(row.createTime || '').slice(0, 19).replace('T', ' ')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ function Home() {
|
|||||||
|
|
||||||
{admin && (
|
{admin && (
|
||||||
<View className='ctaWrap gap-3'>
|
<View className='ctaWrap gap-3'>
|
||||||
<View className='ctaBtn' onClick={() => navTo('/credit/company/index', true)}>
|
<View className='ctaBtn' onClick={() => navTo('/credit/creditMpCustomer/index', true)}>
|
||||||
<Text className='ctaBtnText'>客户管理</Text>
|
<Text className='ctaBtnText'>客户管理</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className='ctaBtn' onClick={() => navTo('/credit/order/index', true)}>
|
<View className='ctaBtn' onClick={() => navTo('/credit/order/index', true)}>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const UserCard = forwardRef<any, any>((_, ref) => {
|
|||||||
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
const [IsLogin, setIsLogin] = useState<boolean>(false)
|
||||||
const [userInfo, setUserInfo] = useState<User>()
|
const [userInfo, setUserInfo] = useState<User>()
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const themeStyles = useThemeStyles();
|
const themeStyles = useThemeStyles();
|
||||||
const canShowScanButton = (() => {
|
const canShowScanButton = (() => {
|
||||||
const v: any = (userInfo as any)?.isAdmin
|
const v: any = (userInfo as any)?.isAdmin
|
||||||
|
|||||||
Reference in New Issue
Block a user