feat(credit): 重构信用模块订单页面并添加详情功能
- 将信用模块我的订单页面从模拟数据改为真实API数据 - 添加订单详情页面及路由配置 - 集成信用客户管理API接口调用 - 实现订单列表的搜索、分页和加载更多功能 - 添加订单状态筛选和统计信息展示 - 新增订单详情的时间线展示功能 - 更新首页进度查询跳转逻辑 - 优化页面加载和错误处理机制
This commit is contained in:
@@ -130,8 +130,10 @@ export default {
|
|||||||
"company/follow-step1",
|
"company/follow-step1",
|
||||||
"company/edit",
|
"company/edit",
|
||||||
"my-order/index",
|
"my-order/index",
|
||||||
'creditMpCustomer/index',
|
"my-order/detail",
|
||||||
'creditMpCustomer/add',]
|
'creditMpCustomer/index',
|
||||||
|
'creditMpCustomer/add',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
window: {
|
window: {
|
||||||
|
|||||||
6
src/credit/my-order/detail.config.ts
Normal file
6
src/credit/my-order/detail.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '客户详情-跟进',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
navigationBarBackgroundColor: '#ffffff'
|
||||||
|
})
|
||||||
|
|
||||||
317
src/credit/my-order/detail.tsx
Normal file
317
src/credit/my-order/detail.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import Taro, { useDidShow, useRouter } from '@tarojs/taro'
|
||||||
|
import { View, Text } from '@tarojs/components'
|
||||||
|
import { Button, ConfigProvider, Empty, Loading, Popup } from '@nutui/nutui-react-taro'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
import { getCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||||
|
import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model'
|
||||||
|
import { getCreditCompany } from '@/api/credit/creditCompany'
|
||||||
|
import type { CreditCompany } from '@/api/credit/creditCompany/model'
|
||||||
|
|
||||||
|
type TimelineNode = {
|
||||||
|
title: string
|
||||||
|
status?: string
|
||||||
|
time?: string
|
||||||
|
desc?: string
|
||||||
|
tag?: string
|
||||||
|
actionText?: string
|
||||||
|
actionType?: 'detail'
|
||||||
|
variant?: 'normal' | 'returned'
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeText = (v: any) => String(v ?? '').trim()
|
||||||
|
|
||||||
|
const formatDate = (t?: string) => {
|
||||||
|
const txt = safeText(t)
|
||||||
|
if (!txt) return ''
|
||||||
|
const d = dayjs(txt)
|
||||||
|
if (!d.isValid()) return txt
|
||||||
|
return d.format('YY-MM-DD')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCompanyIndustry = (c?: CreditCompany | null) => {
|
||||||
|
const anyCompany = (c || {}) as any
|
||||||
|
return safeText(
|
||||||
|
anyCompany.nationalStandardIndustryCategories6 ||
|
||||||
|
anyCompany.nationalStandardIndustryCategories2 ||
|
||||||
|
anyCompany.nationalStandardIndustryCategories ||
|
||||||
|
anyCompany.institutionType ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildAddress = (c?: CreditCompany | null, fallback?: CreditMpCustomer | null) => {
|
||||||
|
const cc = c || ({} as CreditCompany)
|
||||||
|
const province = safeText((cc as any).province) || safeText(fallback?.province)
|
||||||
|
const city = safeText((cc as any).city) || safeText(fallback?.city)
|
||||||
|
const region = safeText((cc as any).region) || safeText(fallback?.region)
|
||||||
|
const addr = safeText((cc as any).address)
|
||||||
|
return safeText([province, city, region].filter(Boolean).join('') + addr) || safeText([province, city, region].filter(Boolean).join(' '))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreditMyOrderDetailPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const id = useMemo(() => {
|
||||||
|
const n = Number((router?.params as any)?.id)
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : undefined
|
||||||
|
}, [router?.params])
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [tipVisible, setTipVisible] = useState(true)
|
||||||
|
|
||||||
|
const [demand, setDemand] = useState<CreditMpCustomer | null>(null)
|
||||||
|
const [company, setCompany] = useState<CreditCompany | null>(null)
|
||||||
|
|
||||||
|
const reload = useCallback(async () => {
|
||||||
|
setError(null)
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
if (!id) throw new Error('缺少需求ID')
|
||||||
|
const d = await getCreditMpCustomer(id)
|
||||||
|
setDemand(d as CreditMpCustomer)
|
||||||
|
|
||||||
|
const companyId = Number((d as any)?.companyId)
|
||||||
|
if (Number.isFinite(companyId) && companyId > 0) {
|
||||||
|
try {
|
||||||
|
const c = await getCreditCompany(companyId)
|
||||||
|
setCompany(c as CreditCompany)
|
||||||
|
} catch (_e) {
|
||||||
|
setCompany(null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCompany(null)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载需求详情失败:', e)
|
||||||
|
setDemand(null)
|
||||||
|
setCompany(null)
|
||||||
|
setError(String((e as any)?.message || '加载失败'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
reload().then()
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayName = useMemo(() => {
|
||||||
|
const c = company as any
|
||||||
|
return safeText(c?.matchName || c?.name) || safeText(demand?.toUser) || '广西万宇工程建设有限公司'
|
||||||
|
}, [company, demand?.toUser])
|
||||||
|
|
||||||
|
const unifiedCode = useMemo(() => safeText((company as any)?.code) || '823339008887788', [company])
|
||||||
|
const industry = useMemo(() => getCompanyIndustry(company) || '建筑业', [company])
|
||||||
|
const phone = useMemo(() => safeText((company as any)?.tel) || '15878179339', [company])
|
||||||
|
const address = useMemo(() => buildAddress(company, demand) || '广西南宁市西乡塘区北大中心10栋11号', [company, demand])
|
||||||
|
|
||||||
|
const topFollower = useMemo(() => safeText((company as any)?.followRealName || (company as any)?.userRealName) || '罗天东', [company])
|
||||||
|
const topDate = useMemo(() => {
|
||||||
|
const t = safeText((company as any)?.assignDate || (company as any)?.assignTime || company?.updateTime || company?.createTime || demand?.createTime)
|
||||||
|
return formatDate(t) || '25-11-10'
|
||||||
|
}, [company, demand?.createTime])
|
||||||
|
|
||||||
|
const topStatus = useMemo(() => safeText((company as any)?.customerStatus || (company as any)?.statusText) || '保护期内', [company])
|
||||||
|
|
||||||
|
const timeline = useMemo<TimelineNode[]>(() => {
|
||||||
|
const returned = safeText(demand?.statusTxt).includes('退回')
|
||||||
|
const base: TimelineNode[] = [
|
||||||
|
{
|
||||||
|
title: '第一步:加微信前沟通',
|
||||||
|
status: '审核通过',
|
||||||
|
time: '2025-11-11',
|
||||||
|
actionText: '查看详情',
|
||||||
|
actionType: 'detail'
|
||||||
|
},
|
||||||
|
{ title: '已受理', desc: `负责人:${topFollower || 'XXX'}`, time: '2025-01-01 11:30:30' },
|
||||||
|
{ title: '材料提交与梳理(第二步节点完成)', desc: '审核评估', time: '2025-01-01 11:30:30', tag: '第二步完成' },
|
||||||
|
{ title: '合同已签订(第五步节点完成)', desc: '项目启动', time: '2025-01-01 11:30:30', tag: '第五步完成' },
|
||||||
|
{
|
||||||
|
title: '执行回款(第六步节点完成)',
|
||||||
|
desc: '部分回款:金额\n全部回款:金额',
|
||||||
|
time: '2025-03-12 11:30:30',
|
||||||
|
tag: '第六步完成'
|
||||||
|
},
|
||||||
|
{ title: '完结', desc: '已办结/已终止', time: '2025-01-01 11:30:30' }
|
||||||
|
]
|
||||||
|
|
||||||
|
if (returned) {
|
||||||
|
base.splice(2, 0, {
|
||||||
|
title: '已退回',
|
||||||
|
variant: 'returned',
|
||||||
|
desc: '原因:需求不清晰,请重新提交',
|
||||||
|
time: '2025-01-01 11:30:30'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
base.push({
|
||||||
|
title: '已退回',
|
||||||
|
variant: 'returned',
|
||||||
|
desc: '原因:需求不清晰,请重新提交',
|
||||||
|
time: '2025-01-01 11:30:30'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}, [demand?.statusTxt, topFollower])
|
||||||
|
|
||||||
|
const onNodeAction = async (node: TimelineNode) => {
|
||||||
|
if (node.actionType === 'detail') {
|
||||||
|
Taro.showToast({ title: '查看详情(示例)', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="bg-pink-50 min-h-screen">
|
||||||
|
<ConfigProvider>
|
||||||
|
<View className="max-w-md mx-auto px-4 pb-12 pt-3">
|
||||||
|
<View className="bg-white rounded-xl border border-pink-100 p-3">
|
||||||
|
<View className="flex items-start justify-between gap-3">
|
||||||
|
<View className="min-w-0">
|
||||||
|
<Text className="text-base font-semibold text-gray-900">{displayName}</Text>
|
||||||
|
<View className="mt-1 text-xs text-gray-500">统一代码:{unifiedCode}</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="shrink-0 text-right">
|
||||||
|
<View className="text-xs text-gray-700">{topFollower}</View>
|
||||||
|
<View className="text-xs text-gray-500 mt-1">{topDate}</View>
|
||||||
|
<View className="mt-1 inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-50 border border-green-200">
|
||||||
|
<View className="w-2 h-2 rounded-full bg-green-500" />
|
||||||
|
<Text className="text-xs text-green-700">{topStatus}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-3 grid grid-cols-1 gap-2 text-sm text-gray-700">
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-gray-500">所属行业</Text>
|
||||||
|
<Text className="font-medium">{industry}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-gray-500">客户联系方式</Text>
|
||||||
|
<Text className="font-medium">{phone}</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex items-start justify-between gap-3">
|
||||||
|
<Text className="text-gray-500 shrink-0">地址</Text>
|
||||||
|
<Text className="font-medium text-right">{address}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<View className="mt-3 bg-white rounded-xl border border-pink-100 py-10 flex justify-center">
|
||||||
|
<Loading>加载中...</Loading>
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
|
<View className="mt-3 bg-white rounded-xl border border-pink-100 py-10">
|
||||||
|
<Empty description={error} />
|
||||||
|
<View className="mt-2 flex justify-center">
|
||||||
|
<Button size="small" type="primary" onClick={reload}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className="mt-3 bg-white rounded-xl border border-pink-100 p-3">
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-sm font-semibold text-gray-900">我的需求进度</Text>
|
||||||
|
<Text className="text-xs text-gray-400">时间线</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="mt-3">
|
||||||
|
{timeline.map((n, idx) => {
|
||||||
|
const isLast = idx === timeline.length - 1
|
||||||
|
const isReturned = n.variant === 'returned'
|
||||||
|
const circleClass = isReturned ? 'bg-red-600' : 'bg-red-500'
|
||||||
|
const lineClass = isReturned ? 'bg-red-200' : 'bg-red-200'
|
||||||
|
const tag = safeText(n.tag)
|
||||||
|
const status = safeText(n.status)
|
||||||
|
const time = safeText(n.time)
|
||||||
|
const desc = safeText(n.desc)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={`${n.title}-${idx}`} className="flex items-stretch">
|
||||||
|
<View className="w-6 flex flex-col items-center">
|
||||||
|
<View className={`w-3 h-3 rounded-full mt-1 ${circleClass}`} />
|
||||||
|
{!isLast && <View className={`flex-1 w-[2px] ${lineClass}`} />}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex-1 pb-4">
|
||||||
|
<View className="flex items-start justify-between gap-3">
|
||||||
|
<View className="min-w-0">
|
||||||
|
<Text className={`text-sm font-semibold ${isReturned ? 'text-red-600' : 'text-gray-900'}`}>
|
||||||
|
{n.title}
|
||||||
|
</Text>
|
||||||
|
{(status || time) && (
|
||||||
|
<View className="mt-1 text-xs text-gray-500 flex flex-wrap gap-x-3 gap-y-1">
|
||||||
|
{!!status && <Text>状态:{status}</Text>}
|
||||||
|
{!!time && <Text>时间:{time}</Text>}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!!tag && (
|
||||||
|
<View className="shrink-0 px-2 py-1 rounded-full bg-yellow-100 border border-yellow-200">
|
||||||
|
<Text className="text-xs text-yellow-800">{tag}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!!desc && (
|
||||||
|
<View className="mt-2 text-xs text-gray-700 whitespace-pre-line">
|
||||||
|
<Text>{desc}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!!n.actionText && (
|
||||||
|
<View className="mt-2">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
style={{ background: '#22c55e', borderColor: '#22c55e' }}
|
||||||
|
onClick={() => onNodeAction(n)}
|
||||||
|
>
|
||||||
|
{n.actionText}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
className="fixed right-4 bottom-6 w-11 h-11 rounded-full bg-red-500 flex items-center justify-center shadow-lg"
|
||||||
|
onClick={() => setTipVisible(true)}
|
||||||
|
>
|
||||||
|
<Text className="text-white text-lg font-semibold">?</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Popup
|
||||||
|
visible={tipVisible}
|
||||||
|
position="bottom"
|
||||||
|
onClose={() => setTipVisible(false)}
|
||||||
|
style={{ borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }}
|
||||||
|
>
|
||||||
|
<View className="px-4 py-4 bg-white">
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-base font-semibold text-gray-900">进度查询</Text>
|
||||||
|
<Text className="text-sm text-gray-500" onClick={() => setTipVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="mt-3 text-sm text-gray-700 leading-6">
|
||||||
|
<View>1. 查询进度:可查看自己发布的需求的处理状态、处理结果;</View>
|
||||||
|
<View>2. 若可接单会安排给销售员,可跟踪项目每一步状态;若不可接,状态为退回。</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
</ConfigProvider>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,347 +1,209 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro, { useDidShow } from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import { Button, ConfigProvider, DatePicker, Empty, Input, Popup } from '@nutui/nutui-react-taro'
|
import { Button, ConfigProvider, Empty, Input, Loading } from '@nutui/nutui-react-taro'
|
||||||
import { Search } from '@nutui/icons-react-taro'
|
import { ArrowRight, Search } from '@nutui/icons-react-taro'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
type PayStatus = '全部回款' | '部分回款' | '未回款'
|
import { pageCreditMpCustomer } from '@/api/credit/creditMpCustomer'
|
||||||
type AmountSort = '默认' | '从高到低' | '从低到高'
|
import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model'
|
||||||
|
|
||||||
type OrderItem = {
|
type ListQuery = {
|
||||||
id: string
|
page: number
|
||||||
orderNo: string
|
limit: number
|
||||||
companyName: string
|
keywords?: string
|
||||||
unifiedCode: string
|
userId?: number
|
||||||
projectName: string
|
|
||||||
follower: string
|
|
||||||
payStatus: PayStatus
|
|
||||||
date: string // YYYY-MM-DD
|
|
||||||
principal: number
|
|
||||||
interest: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatYmd = (d?: Date | null) => {
|
const getCurrentUserId = (): number | undefined => {
|
||||||
if (!d) return ''
|
try {
|
||||||
return dayjs(d).format('YYYY-MM-DD')
|
const raw = Taro.getStorageSync('UserId')
|
||||||
}
|
const n = Number(raw)
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : undefined
|
||||||
const getStatusStyle = (s: PayStatus) => {
|
} catch {
|
||||||
if (s === '全部回款') return 'bg-green-500'
|
return undefined
|
||||||
if (s === '部分回款') return 'bg-orange-500'
|
|
||||||
return 'bg-red-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMockOrders = (page: number): OrderItem[] => {
|
|
||||||
const companies = [
|
|
||||||
{ name: '广西万宇工程建设有限公司', code: '91450100MA00000001' },
|
|
||||||
{ name: '广西远恒建筑有限公司', code: '91450100MA00000002' },
|
|
||||||
{ name: '南宁宏达工程有限公司', code: '91450100MA00000003' },
|
|
||||||
{ name: '桂林鑫盛建设有限公司', code: '91450100MA00000004' }
|
|
||||||
]
|
|
||||||
const followers = ['罗天东', '张三', '李四', '王五']
|
|
||||||
const suffix = ['一期', '二期', '改扩建', '配套', '市政', '装饰', '机电']
|
|
||||||
|
|
||||||
// page=1:给出 16 条示例,统计更直观(项目=订单维度,而非公司维度)
|
|
||||||
const size = page === 1 ? 16 : 6
|
|
||||||
const baseNo = 9099009999 + (page - 1) * 100
|
|
||||||
|
|
||||||
const list: OrderItem[] = []
|
|
||||||
for (let i = 0; i < size; i++) {
|
|
||||||
const c = companies[(page + i) % companies.length]
|
|
||||||
const follower = followers[(i + page) % followers.length]
|
|
||||||
const payStatus: PayStatus =
|
|
||||||
page === 1 ? '全部回款' : (['全部回款', '部分回款', '未回款'][(i + page) % 3] as PayStatus)
|
|
||||||
|
|
||||||
const orderNo = String(baseNo + i)
|
|
||||||
const date = dayjs('2025-10-10').subtract((page - 1) * 7 + (i % 6), 'day').format('YYYY-MM-DD')
|
|
||||||
|
|
||||||
// page=1:让本金/利息合计更规整(本金 2000,利息 200)
|
|
||||||
const principal = page === 1 ? (i < 8 ? 100 : 150) : 200 + ((i + page) % 5) * 50
|
|
||||||
const interest = page === 1 ? (i < 8 ? 10 : 15) : 20 + ((i + page) % 4) * 5
|
|
||||||
|
|
||||||
const projectName = `${c.name}${suffix[(i + page) % suffix.length]}项目`
|
|
||||||
|
|
||||||
list.push({
|
|
||||||
id: `${page}-${i}-${orderNo}`,
|
|
||||||
orderNo,
|
|
||||||
companyName: c.name,
|
|
||||||
unifiedCode: c.code,
|
|
||||||
projectName,
|
|
||||||
follower,
|
|
||||||
payStatus,
|
|
||||||
date,
|
|
||||||
principal,
|
|
||||||
interest
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreditOrderPage() {
|
const buildDesc = (row: CreditMpCustomer) => {
|
||||||
|
const price = row.price ? `${row.price}元` : ''
|
||||||
|
const years = row.years ? `${row.years}年` : ''
|
||||||
|
const location = [row.province, row.city, row.region].filter(Boolean).join(' ')
|
||||||
|
return [price, years, location].filter(Boolean).join(' · ')
|
||||||
|
}
|
||||||
|
|
||||||
const [rawList, setRawList] = useState<OrderItem[]>(() => makeMockOrders(1))
|
const getStatusBadgeClass = (s?: string) => {
|
||||||
const [mockPage, setMockPage] = useState(1)
|
const txt = String(s || '').trim()
|
||||||
|
if (!txt) return 'bg-gray-400'
|
||||||
|
if (txt.includes('退回')) return 'bg-red-500'
|
||||||
|
if (txt.includes('完结') || txt.includes('已办结')) return 'bg-green-600'
|
||||||
|
if (txt.includes('受理') || txt.includes('通过') || txt.includes('签订')) return 'bg-orange-500'
|
||||||
|
return 'bg-blue-500'
|
||||||
|
}
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('')
|
export default function CreditMyOrderPage() {
|
||||||
const [amountSort, setAmountSort] = useState<AmountSort>('默认')
|
const [list, setList] = useState<CreditMpCustomer[]>([])
|
||||||
const [payFilter, setPayFilter] = useState<PayStatus>('全部回款')
|
const [count, setCount] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
|
|
||||||
const [datePopupVisible, setDatePopupVisible] = useState(false)
|
const [keywords, setKeywords] = useState('')
|
||||||
const [datePickerVisible, setDatePickerVisible] = useState(false)
|
const [page, setPage] = useState(1)
|
||||||
const [picking, setPicking] = useState<'start' | 'end'>('start')
|
const limit = 10
|
||||||
const [startDate, setStartDate] = useState<Date | null>(null)
|
|
||||||
const [endDate, setEndDate] = useState<Date | null>(null)
|
|
||||||
|
|
||||||
const filteredList = useMemo(() => {
|
const userId = useMemo(() => getCurrentUserId(), [])
|
||||||
let list = rawList.slice()
|
|
||||||
|
|
||||||
const q = searchValue.trim()
|
const hasMore = useMemo(() => list.length < count, [count, list.length])
|
||||||
if (q) {
|
|
||||||
const qq = q.toLowerCase()
|
const fetchPage = useCallback(
|
||||||
list = list.filter(o => {
|
async (opts: { nextPage: number; replace: boolean }) => {
|
||||||
return (
|
if (!userId) {
|
||||||
String(o.companyName || '').toLowerCase().includes(qq) ||
|
setList([])
|
||||||
String(o.unifiedCode || '').toLowerCase().includes(qq) ||
|
setCount(0)
|
||||||
String(o.orderNo || '').toLowerCase().includes(qq)
|
return
|
||||||
)
|
}
|
||||||
})
|
|
||||||
|
const query: ListQuery = {
|
||||||
|
page: opts.nextPage,
|
||||||
|
limit,
|
||||||
|
keywords: keywords.trim() || undefined,
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (opts.replace) setLoading(true)
|
||||||
|
else setLoadingMore(true)
|
||||||
|
|
||||||
|
const res = await pageCreditMpCustomer(query 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) {
|
||||||
|
console.error('获取我的需求失败:', e)
|
||||||
|
Taro.showToast({ title: (e as any)?.message || '获取数据失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
setLoadingMore(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[keywords, limit, userId]
|
||||||
|
)
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
reload().then()
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => reload()
|
||||||
|
Taro.eventCenter.on('credit:order:created', handler)
|
||||||
|
return () => {
|
||||||
|
Taro.eventCenter.off('credit:order:created', handler)
|
||||||
}
|
}
|
||||||
|
}, [reload])
|
||||||
|
|
||||||
if (payFilter) {
|
const goDetail = (id?: number) => {
|
||||||
list = list.filter(o => o.payStatus === payFilter)
|
if (!id) return
|
||||||
}
|
Taro.navigateTo({ url: `/credit/my-order/detail?id=${id}` })
|
||||||
|
|
||||||
if (startDate || endDate) {
|
|
||||||
const start = startDate ? dayjs(startDate).startOf('day') : null
|
|
||||||
const end = endDate ? dayjs(endDate).endOf('day') : null
|
|
||||||
list = list.filter(o => {
|
|
||||||
const t = dayjs(o.date, 'YYYY-MM-DD')
|
|
||||||
if (start && t.isBefore(start)) return false
|
|
||||||
if (end && t.isAfter(end)) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amountSort !== '默认') {
|
|
||||||
list.sort((a, b) => {
|
|
||||||
const av = Number(a.principal || 0)
|
|
||||||
const bv = Number(b.principal || 0)
|
|
||||||
return amountSort === '从高到低' ? bv - av : av - bv
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return list
|
|
||||||
}, [amountSort, endDate, payFilter, rawList, searchValue, startDate])
|
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
|
||||||
// 业务说明:一个公司可能对应多个项目(多条订单),因此“订单数量”=项目/订单条数,而不是公司数。
|
|
||||||
const total = filteredList.length
|
|
||||||
const principal = filteredList.reduce((sum, o) => sum + Number(o.principal || 0), 0)
|
|
||||||
const interest = filteredList.reduce((sum, o) => sum + Number(o.interest || 0), 0)
|
|
||||||
return { total, principal, interest }
|
|
||||||
}, [filteredList])
|
|
||||||
|
|
||||||
const timeText = useMemo(() => {
|
|
||||||
const s = formatYmd(startDate)
|
|
||||||
const e = formatYmd(endDate)
|
|
||||||
if (!s && !e) return '时间筛选'
|
|
||||||
if (s && e) return `${s}~${e}`
|
|
||||||
return s ? `${s}~` : `~${e}`
|
|
||||||
}, [endDate, startDate])
|
|
||||||
|
|
||||||
const pickAmountSort = async () => {
|
|
||||||
try {
|
|
||||||
const options: AmountSort[] = ['默认', '从高到低', '从低到高']
|
|
||||||
const res = await Taro.showActionSheet({ itemList: options })
|
|
||||||
const next = options[res.tapIndex]
|
|
||||||
if (next) setAmountSort(next)
|
|
||||||
} catch (e) {
|
|
||||||
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
|
||||||
if (msg.includes('cancel')) return
|
|
||||||
console.error('选择金额排序失败:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pickPayFilter = async () => {
|
|
||||||
try {
|
|
||||||
const options: PayStatus[] = ['全部回款', '部分回款', '未回款']
|
|
||||||
const res = await Taro.showActionSheet({ itemList: options })
|
|
||||||
const next = options[res.tapIndex]
|
|
||||||
if (next) setPayFilter(next)
|
|
||||||
} catch (e) {
|
|
||||||
const msg = String((e as any)?.errMsg || (e as any)?.message || e || '')
|
|
||||||
if (msg.includes('cancel')) return
|
|
||||||
console.error('选择回款筛选失败:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMore = () => {
|
|
||||||
const nextPage = mockPage + 1
|
|
||||||
const incoming = makeMockOrders(nextPage)
|
|
||||||
setMockPage(nextPage)
|
|
||||||
setRawList(prev => prev.concat(incoming))
|
|
||||||
console.log('加载更多:', { nextPage, appended: incoming.length })
|
|
||||||
Taro.showToast({ title: '已加载更多', icon: 'none' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-pink-50 min-h-screen">
|
<View className="bg-pink-50 min-h-screen">
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<View className="max-w-md mx-auto">
|
<View className="max-w-md mx-auto">
|
||||||
<View className="px-4 pt-2">
|
<View className="px-4 pt-3">
|
||||||
<View className="bg-white rounded-full border border-pink-100 px-3 py-2 flex items-center gap-2">
|
<View className="bg-white rounded-full border border-pink-100 px-3 py-2 flex items-center gap-2">
|
||||||
<Search size={16} className="text-gray-400" />
|
<Search size={16} className="text-gray-400" />
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
value={searchValue}
|
value={keywords}
|
||||||
onChange={setSearchValue}
|
onChange={setKeywords}
|
||||||
placeholder="请输入公司名称、统一代码查询、订单号查询"
|
placeholder="请输入拖欠方名称查询"
|
||||||
|
onConfirm={reload}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<Button size="small" type="primary" onClick={reload}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="mt-3 grid grid-cols-3 gap-2">
|
<View className="mt-2 text-xs text-gray-500 flex items-center justify-between">
|
||||||
<View
|
<Text>仅展示我发布的需求</Text>
|
||||||
className="bg-white rounded-xl border border-pink-100 px-2 py-2 text-xs text-gray-700 flex items-center justify-center"
|
<Text>共{count}条</Text>
|
||||||
onClick={pickAmountSort}
|
|
||||||
>
|
|
||||||
<Text className="mr-1">金额排序</Text>
|
|
||||||
<Text className="text-gray-400">{amountSort === '从高到低' ? '↓' : amountSort === '从低到高' ? '↑' : '↕'}</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
className="bg-white rounded-xl border border-pink-100 px-2 py-2 text-xs text-gray-700 flex items-center justify-center"
|
|
||||||
onClick={pickPayFilter}
|
|
||||||
>
|
|
||||||
<Text>{payFilter}</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
className="bg-white rounded-xl border border-pink-100 px-2 py-2 text-xs text-gray-700 flex items-center justify-center"
|
|
||||||
onClick={() => setDatePopupVisible(true)}
|
|
||||||
>
|
|
||||||
<Text className="truncate">{timeText}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="mt-2 text-xs text-gray-400 flex items-center justify-between">
|
|
||||||
<Text>总订单量:{stats.total}个</Text>
|
|
||||||
<Text>订单本金:{stats.principal}元</Text>
|
|
||||||
<Text>利息金额:{stats.interest}元</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="px-4 mt-3 pb-6">
|
<View className="px-4 mt-3 pb-6">
|
||||||
{!filteredList.length ? (
|
{loading ? (
|
||||||
|
<View className="bg-white rounded-xl border border-pink-100 py-10 flex items-center justify-center">
|
||||||
|
<Loading>加载中...</Loading>
|
||||||
|
</View>
|
||||||
|
) : !list.length ? (
|
||||||
<View className="bg-white rounded-xl border border-pink-100 py-10">
|
<View className="bg-white rounded-xl border border-pink-100 py-10">
|
||||||
<Empty description="暂无订单" />
|
<Empty description={userId ? '暂无需求' : '未登录或缺少用户信息'} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
filteredList.map(o => (
|
list.map(row => {
|
||||||
<View key={o.id} className="bg-white rounded-xl border border-pink-100 p-3 mb-3">
|
const title = String(row.toUser || '-').trim()
|
||||||
<View className="flex items-center justify-between">
|
const desc = buildDesc(row)
|
||||||
<Text className="text-xs text-gray-500">订单号:{o.orderNo}</Text>
|
const statusTxt = String(row.statusTxt || '处理中').trim()
|
||||||
<View className={`px-2 py-1 rounded-full ${getStatusStyle(o.payStatus)}`}>
|
const time = String(row.createTime || '').slice(0, 19).replace('T', ' ')
|
||||||
<Text className="text-xs text-white">{o.payStatus}</Text>
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={String(row.id)}
|
||||||
|
className="bg-white rounded-xl border border-pink-100 p-3 mb-3"
|
||||||
|
onClick={() => goDetail(row.id)}
|
||||||
|
>
|
||||||
|
<View className="flex items-center justify-between">
|
||||||
|
<Text className="text-sm font-semibold text-gray-900">{title}</Text>
|
||||||
|
<View className="flex items-center gap-2">
|
||||||
|
<View className={`px-2 py-1 rounded-full ${getStatusBadgeClass(statusTxt)}`}>
|
||||||
|
<Text className="text-xs text-white">{statusTxt}</Text>
|
||||||
|
</View>
|
||||||
|
<ArrowRight color="#cccccc" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!!desc && (
|
||||||
|
<View className="mt-2 text-xs text-gray-600">
|
||||||
|
<Text>{desc}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View className="mt-2 text-xs text-gray-500 flex items-center justify-between">
|
||||||
|
<Text>需求ID:{row.id ?? '-'}</Text>
|
||||||
|
<Text>{time || ''}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
)
|
||||||
{/* 项目名称是核心分组/统计维度(一个公司可有多个项目=多条订单),因此需要突出显示 */}
|
})
|
||||||
<View className="mt-2">
|
|
||||||
<Text className="text-sm font-semibold text-rose-600">{o.projectName}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-600">
|
|
||||||
<Text>公司:{o.companyName}</Text>
|
|
||||||
<Text>跟进人:{o.follower}</Text>
|
|
||||||
<Text>日期:{o.date}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="mt-2 flex items-center gap-6 text-xs text-gray-700">
|
|
||||||
<Text>
|
|
||||||
<Text className="text-gray-400">本金:</Text>
|
|
||||||
{o.principal}元
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Text className="text-gray-400">利息:</Text>
|
|
||||||
{o.interest}元
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View className="mt-2 flex justify-center">
|
{!!list.length && (
|
||||||
<Button
|
<View className="mt-2 flex justify-center">
|
||||||
fill="none"
|
<Button
|
||||||
size="small"
|
fill="none"
|
||||||
style={{ color: '#bdbdbd' }}
|
size="small"
|
||||||
onClick={loadMore}
|
style={{ color: '#bdbdbd' }}
|
||||||
>
|
disabled={!hasMore || loadingMore}
|
||||||
加载更多
|
onClick={loadMore}
|
||||||
</Button>
|
>
|
||||||
</View>
|
{hasMore ? (loadingMore ? '加载中...' : '加载更多') : '没有更多了'}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Popup
|
|
||||||
visible={datePopupVisible}
|
|
||||||
position="bottom"
|
|
||||||
onClose={() => setDatePopupVisible(false)}
|
|
||||||
style={{ borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }}
|
|
||||||
>
|
|
||||||
<View className="px-4 py-4 bg-white">
|
|
||||||
<View className="text-base font-semibold text-gray-900">时间筛选</View>
|
|
||||||
<View className="mt-3 text-sm text-gray-700">
|
|
||||||
<View className="flex items-center justify-between py-2" onClick={() => { setPicking('start'); setDatePickerVisible(true) }}>
|
|
||||||
<Text>开始日期</Text>
|
|
||||||
<Text className="text-gray-500">{formatYmd(startDate) || '未选择'}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex items-center justify-between py-2" onClick={() => { setPicking('end'); setDatePickerVisible(true) }}>
|
|
||||||
<Text>结束日期</Text>
|
|
||||||
<Text className="text-gray-500">{formatYmd(endDate) || '未选择'}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="mt-4 flex items-center justify-between gap-3">
|
|
||||||
<Button
|
|
||||||
fill="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setStartDate(null)
|
|
||||||
setEndDate(null)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" onClick={() => setDatePopupVisible(false)}>
|
|
||||||
完成
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Popup>
|
|
||||||
|
|
||||||
<DatePicker
|
|
||||||
visible={datePickerVisible}
|
|
||||||
title={picking === 'start' ? '选择开始日期' : '选择结束日期'}
|
|
||||||
type="date"
|
|
||||||
value={(picking === 'start' ? startDate : endDate) || new Date()}
|
|
||||||
startDate={dayjs('2020-01-01').toDate()}
|
|
||||||
endDate={dayjs('2035-12-31').toDate()}
|
|
||||||
onClose={() => setDatePickerVisible(false)}
|
|
||||||
onCancel={() => setDatePickerVisible(false)}
|
|
||||||
onConfirm={(_options, selectedValue) => {
|
|
||||||
const [y, m, d] = (selectedValue || []).map(v => Number(v))
|
|
||||||
const next = new Date(y, (m || 1) - 1, d || 1, 0, 0, 0)
|
|
||||||
|
|
||||||
if (picking === 'start') {
|
|
||||||
setStartDate(next)
|
|
||||||
if (endDate && dayjs(endDate).isBefore(next, 'day')) setEndDate(null)
|
|
||||||
} else {
|
|
||||||
setEndDate(next)
|
|
||||||
if (startDate && dayjs(next).isBefore(startDate, 'day')) setStartDate(null)
|
|
||||||
}
|
|
||||||
setDatePickerVisible(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ function Home() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const onAction = (type: 'progress' | 'guide' | 'kefu') => {
|
const onAction = (type: 'progress' | 'guide' | 'kefu') => {
|
||||||
const textMap = {
|
if (type === 'progress') {
|
||||||
progress: '查询进度',
|
navTo('/credit/my-order/index', true)
|
||||||
guide: '业务指南',
|
return
|
||||||
kefu: '在线客服'
|
}
|
||||||
} as const
|
|
||||||
|
|
||||||
|
const textMap = { guide: '业务指南', kefu: '在线客服' } as const
|
||||||
Taro.showToast({title: `${textMap[type]}(示例)`, icon: 'none'})
|
Taro.showToast({title: `${textMap[type]}(示例)`, icon: 'none'})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user