diff --git a/src/app.config.ts b/src/app.config.ts index ed9449e..6d7bb52 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -130,8 +130,10 @@ export default { "company/follow-step1", "company/edit", "my-order/index", -'creditMpCustomer/index', - 'creditMpCustomer/add',] + "my-order/detail", + 'creditMpCustomer/index', + 'creditMpCustomer/add', + ] } ], window: { diff --git a/src/credit/my-order/detail.config.ts b/src/credit/my-order/detail.config.ts new file mode 100644 index 0000000..e4a70e3 --- /dev/null +++ b/src/credit/my-order/detail.config.ts @@ -0,0 +1,6 @@ +export default definePageConfig({ + navigationBarTitleText: '客户详情-跟进', + navigationBarTextStyle: 'black', + navigationBarBackgroundColor: '#ffffff' +}) + diff --git a/src/credit/my-order/detail.tsx b/src/credit/my-order/detail.tsx new file mode 100644 index 0000000..2cb5bf3 --- /dev/null +++ b/src/credit/my-order/detail.tsx @@ -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(null) + const [tipVisible, setTipVisible] = useState(true) + + const [demand, setDemand] = useState(null) + const [company, setCompany] = useState(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(() => { + 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 ( + + + + + + + {displayName} + 统一代码:{unifiedCode} + + + + {topFollower} + {topDate} + + + {topStatus} + + + + + + + 所属行业 + {industry} + + + 客户联系方式 + {phone} + + + 地址 + {address} + + + + + {loading ? ( + + 加载中... + + ) : error ? ( + + + + + + + ) : ( + + + 我的需求进度 + 时间线 + + + + {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 ( + + + + {!isLast && } + + + + + + + {n.title} + + {(status || time) && ( + + {!!status && 状态:{status}} + {!!time && 时间:{time}} + + )} + + + {!!tag && ( + + {tag} + + )} + + + {!!desc && ( + + {desc} + + )} + + {!!n.actionText && ( + + + + )} + + + ) + })} + + + )} + + + setTipVisible(true)} + > + ? + + + setTipVisible(false)} + style={{ borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }} + > + + + 进度查询 + setTipVisible(false)}> + 关闭 + + + + 1. 查询进度:可查看自己发布的需求的处理状态、处理结果; + 2. 若可接单会安排给销售员,可跟踪项目每一步状态;若不可接,状态为退回。 + + + + + + ) +} + diff --git a/src/credit/my-order/index.tsx b/src/credit/my-order/index.tsx index f8eb5d0..a2383ae 100644 --- a/src/credit/my-order/index.tsx +++ b/src/credit/my-order/index.tsx @@ -1,347 +1,209 @@ -import { useMemo, useState } from 'react' -import Taro from '@tarojs/taro' +import { useCallback, useEffect, useMemo, useState } from 'react' +import Taro, { useDidShow } from '@tarojs/taro' import { View, Text } from '@tarojs/components' -import { Button, ConfigProvider, DatePicker, Empty, Input, Popup } from '@nutui/nutui-react-taro' -import { Search } from '@nutui/icons-react-taro' -import dayjs from 'dayjs' +import { Button, ConfigProvider, Empty, Input, Loading } from '@nutui/nutui-react-taro' +import { ArrowRight, Search } from '@nutui/icons-react-taro' -type PayStatus = '全部回款' | '部分回款' | '未回款' -type AmountSort = '默认' | '从高到低' | '从低到高' +import { pageCreditMpCustomer } from '@/api/credit/creditMpCustomer' +import type { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model' -type OrderItem = { - id: string - orderNo: string - companyName: string - unifiedCode: string - projectName: string - follower: string - payStatus: PayStatus - date: string // YYYY-MM-DD - principal: number - interest: number +type ListQuery = { + page: number + limit: number + keywords?: string + userId?: number } -const formatYmd = (d?: Date | null) => { - if (!d) return '' - return dayjs(d).format('YYYY-MM-DD') -} - -const getStatusStyle = (s: PayStatus) => { - if (s === '全部回款') return 'bg-green-500' - 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 - }) +const getCurrentUserId = (): number | undefined => { + try { + const raw = Taro.getStorageSync('UserId') + const n = Number(raw) + return Number.isFinite(n) && n > 0 ? n : undefined + } catch { + return undefined } - 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(() => makeMockOrders(1)) - const [mockPage, setMockPage] = useState(1) +const getStatusBadgeClass = (s?: string) => { + 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('') - const [amountSort, setAmountSort] = useState('默认') - const [payFilter, setPayFilter] = useState('全部回款') +export default function CreditMyOrderPage() { + const [list, setList] = useState([]) + const [count, setCount] = useState(0) + const [loading, setLoading] = useState(false) + const [loadingMore, setLoadingMore] = useState(false) - const [datePopupVisible, setDatePopupVisible] = useState(false) - const [datePickerVisible, setDatePickerVisible] = useState(false) - const [picking, setPicking] = useState<'start' | 'end'>('start') - const [startDate, setStartDate] = useState(null) - const [endDate, setEndDate] = useState(null) + const [keywords, setKeywords] = useState('') + const [page, setPage] = useState(1) + const limit = 10 - const filteredList = useMemo(() => { - let list = rawList.slice() + const userId = useMemo(() => getCurrentUserId(), []) - const q = searchValue.trim() - if (q) { - const qq = q.toLowerCase() - list = list.filter(o => { - return ( - String(o.companyName || '').toLowerCase().includes(qq) || - String(o.unifiedCode || '').toLowerCase().includes(qq) || - String(o.orderNo || '').toLowerCase().includes(qq) - ) - }) + const hasMore = useMemo(() => list.length < count, [count, list.length]) + + const fetchPage = useCallback( + async (opts: { nextPage: number; replace: boolean }) => { + if (!userId) { + setList([]) + setCount(0) + 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) { - list = list.filter(o => o.payStatus === payFilter) - } - - 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' }) + const goDetail = (id?: number) => { + if (!id) return + Taro.navigateTo({ url: `/credit/my-order/detail?id=${id}` }) } return ( - + + - - - 金额排序 - {amountSort === '从高到低' ? '↓' : amountSort === '从低到高' ? '↑' : '↕'} - - - {payFilter} - - setDatePopupVisible(true)} - > - {timeText} - - - - - 总订单量:{stats.total}个 - 订单本金:{stats.principal}元 - 利息金额:{stats.interest}元 + + 仅展示我发布的需求 + 共{count}条 - {!filteredList.length ? ( + {loading ? ( + + 加载中... + + ) : !list.length ? ( - + ) : ( - filteredList.map(o => ( - - - 订单号:{o.orderNo} - - {o.payStatus} + list.map(row => { + const title = String(row.toUser || '-').trim() + const desc = buildDesc(row) + const statusTxt = String(row.statusTxt || '处理中').trim() + const time = String(row.createTime || '').slice(0, 19).replace('T', ' ') + + return ( + goDetail(row.id)} + > + + {title} + + + {statusTxt} + + + + + + {!!desc && ( + + {desc} + + )} + + + 需求ID:{row.id ?? '-'} + {time || ''} - - {/* 项目名称是核心分组/统计维度(一个公司可有多个项目=多条订单),因此需要突出显示 */} - - {o.projectName} - - - - 公司:{o.companyName} - 跟进人:{o.follower} - 日期:{o.date} - - - - - 本金: - {o.principal}元 - - - 利息: - {o.interest}元 - - - - )) + ) + }) )} - - - + {!!list.length && ( + + + + )} - - setDatePopupVisible(false)} - style={{ borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }} - > - - 时间筛选 - - { setPicking('start'); setDatePickerVisible(true) }}> - 开始日期 - {formatYmd(startDate) || '未选择'} - - { setPicking('end'); setDatePickerVisible(true) }}> - 结束日期 - {formatYmd(endDate) || '未选择'} - - - - - - - - - - - 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) - }} - /> ) diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 0d3a60a..6ccf0f4 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -36,12 +36,12 @@ function Home() { }) const onAction = (type: 'progress' | 'guide' | 'kefu') => { - const textMap = { - progress: '查询进度', - guide: '业务指南', - kefu: '在线客服' - } as const + if (type === 'progress') { + navTo('/credit/my-order/index', true) + return + } + const textMap = { guide: '业务指南', kefu: '在线客服' } as const Taro.showToast({title: `${textMap[type]}(示例)`, icon: 'none'}) }