194 lines
6.7 KiB
TypeScript
194 lines
6.7 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
||
import { PERM, requirePermission } from '@/auth/permissions'
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { useState } from 'react'
|
||
import type { ColumnDef } from '@tanstack/react-table'
|
||
import type { ExcDemand } from '@/api/types'
|
||
import { fetchExcDemand, fetchExcDemands } from '@/api/modules/exc'
|
||
import { useListPage } from '@/lib/list-page'
|
||
import { DataTable } from '@/components/data-table/data-table'
|
||
import { PageHeader } from '@/components/page-header'
|
||
import { DetailSheet } from '@/features/studio/detail-sheet'
|
||
import {
|
||
DEMAND_STATUS_LABELS,
|
||
DISPATCH_MODE_LABELS,
|
||
DemandStatusBadge,
|
||
formatAmount,
|
||
formatTime,
|
||
} from '@/features/exc/labels'
|
||
import { Button } from '@/components/ui/button'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select'
|
||
|
||
export const Route = createFileRoute('/_authed/exc/demands')({
|
||
beforeLoad: () => requirePermission(PERM.EXC_READ),
|
||
component: ExcDemandsPage,
|
||
})
|
||
|
||
function ExcDemandsPage() {
|
||
const [status, setStatus] = useState<string>('all')
|
||
const [mode, setMode] = useState<string>('all')
|
||
const [detailId, setDetailId] = useState<number | null>(null)
|
||
|
||
const filters: Record<string, string> = {}
|
||
if (status !== 'all') filters.status = status
|
||
if (mode !== 'all') filters.dispatch_mode = mode
|
||
|
||
const list = useListPage('exc-demands', fetchExcDemands, filters)
|
||
|
||
const detail = useQuery({
|
||
queryKey: ['exc-demand', detailId],
|
||
queryFn: () => fetchExcDemand(detailId as number),
|
||
enabled: detailId !== null,
|
||
})
|
||
const demand = detail.data?.data
|
||
|
||
const columns: ColumnDef<ExcDemand>[] = [
|
||
{
|
||
header: '需求',
|
||
cell: ({ row }) => (
|
||
<Button
|
||
variant="link"
|
||
className="h-auto max-w-72 justify-start p-0 text-left"
|
||
onClick={() => setDetailId(row.original.id)}
|
||
>
|
||
<div className="min-w-0">
|
||
<div className="truncate font-medium">{row.original.title ?? '—'}</div>
|
||
<code className="font-mono text-xs text-muted-foreground">#{row.original.id}</code>
|
||
</div>
|
||
</Button>
|
||
),
|
||
},
|
||
{
|
||
header: '状态',
|
||
cell: ({ row }) => <DemandStatusBadge status={row.original.status} />,
|
||
},
|
||
{
|
||
header: '模式',
|
||
cell: ({ row }) =>
|
||
row.original.dispatch_mode ? DISPATCH_MODE_LABELS[row.original.dispatch_mode] : '—',
|
||
},
|
||
{
|
||
header: '买家',
|
||
cell: ({ row }) => row.original.buyer?.display_name ?? '—',
|
||
},
|
||
{
|
||
header: '参考价',
|
||
cell: ({ row }) => formatAmount(row.original.ref_amount, row.original.currency_code),
|
||
},
|
||
{
|
||
header: '报价/派单',
|
||
cell: ({ row }) => `${row.original.quotes_count} / ${row.original.dispatches_count}`,
|
||
},
|
||
{
|
||
header: '匹配服务商',
|
||
cell: ({ row }) =>
|
||
row.original.matched_provider_uid ? (
|
||
<code className="font-mono text-xs">{row.original.matched_provider_uid}</code>
|
||
) : (
|
||
'—'
|
||
),
|
||
},
|
||
{
|
||
header: '创建时间',
|
||
cell: ({ row }) => formatTime(row.original.created_at),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
title="需求监控"
|
||
description="全站服务需求及其报价与派单情况(只读)"
|
||
actions={
|
||
<div className="flex items-center gap-2">
|
||
<Select value={status} onValueChange={setStatus}>
|
||
<SelectTrigger size="sm" className="w-28">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部状态</SelectItem>
|
||
{Object.entries(DEMAND_STATUS_LABELS).map(([value, label]) => (
|
||
<SelectItem key={value} value={value}>
|
||
{label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<Select value={mode} onValueChange={setMode}>
|
||
<SelectTrigger size="sm" className="w-28">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">全部模式</SelectItem>
|
||
{Object.entries(DISPATCH_MODE_LABELS).map(([value, label]) => (
|
||
<SelectItem key={value} value={value}>
|
||
{label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
}
|
||
/>
|
||
<DataTable
|
||
columns={columns}
|
||
data={list.rows}
|
||
pagination={list.pagination}
|
||
loading={list.loading}
|
||
emptyText="没有需求记录"
|
||
onPageChange={list.setPage}
|
||
onPageSizeChange={list.setPageSize}
|
||
/>
|
||
<DetailSheet
|
||
open={detailId !== null}
|
||
onOpenChange={(open) => !open && setDetailId(null)}
|
||
title={demand?.title ?? `需求 #${detailId ?? ''}`}
|
||
description={demand ? `状态 ${DEMAND_STATUS_LABELS[demand.status] ?? demand.status}` : undefined}
|
||
fields={[
|
||
{ label: '摘要', value: demand?.summary ?? '—' },
|
||
{ label: '买家', value: demand?.buyer?.display_name ?? '—' },
|
||
{ label: '参考价', value: formatAmount(demand?.ref_amount, demand?.currency_code) },
|
||
{ label: '报价截止', value: formatTime(demand?.quote_deadline) },
|
||
{ label: '需求时间', value: formatTime(demand?.required_at) },
|
||
{ label: '匹配时间', value: formatTime(demand?.matched_at) },
|
||
{ label: '纬度', value: demand?.lat ?? '—' },
|
||
{ label: '经度', value: demand?.lng ?? '—' },
|
||
{ label: '时长(分钟)', value: demand?.duration ?? '—' },
|
||
]}
|
||
jsonBlocks={[
|
||
{ label: '关联服务', value: demand?.related_services?.length ? demand.related_services : null },
|
||
{
|
||
label: `报价(${demand?.quotes?.length ?? 0})`,
|
||
value: demand?.quotes?.length
|
||
? demand.quotes.map((q) => ({
|
||
user: q.user?.display_name ?? q.user_uid,
|
||
price: formatAmount(q.price, q.currency_code),
|
||
message: q.message,
|
||
}))
|
||
: null,
|
||
},
|
||
{
|
||
label: `派单(${demand?.dispatches?.length ?? 0})`,
|
||
value: demand?.dispatches?.length
|
||
? demand.dispatches.map((d) => ({
|
||
provider: d.provider?.display_name ?? '—',
|
||
round: d.round_no,
|
||
status: d.status,
|
||
score: d.score,
|
||
responded_at: d.responded_at,
|
||
}))
|
||
: null,
|
||
},
|
||
{ label: '地址', value: demand?.address ?? null },
|
||
]}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|