|
|
"use client";
|
|
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
import { AdminLayout } from "@/components/layout/admin-layout";
|
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
import { ErrorBanner } from "@/components/ui/error-banner";
|
|
|
import { SecondaryButton } from "@/components/ui/primary-button";
|
|
|
import { SelectField } from "@/components/ui/select-field";
|
|
|
import { Card } from "@/components/ui/card";
|
|
|
import { formatDateTime } from "@/lib/frontend/format";
|
|
|
import { adminGetQueryLogs } from "@/lib/frontend/api-client";
|
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
|
import type {
|
|
|
DataPageStatus,
|
|
|
QuoteQueryLogRecord,
|
|
|
QuoteQueryOutcome,
|
|
|
} from "@/lib/frontend/types";
|
|
|
|
|
|
const PAGE_SIZE = 20;
|
|
|
|
|
|
const OUTCOME_OPTIONS = [
|
|
|
{ value: "", label: "全部结果" },
|
|
|
{ value: "success", label: "成功" },
|
|
|
{ value: "failed", label: "失败" },
|
|
|
{ value: "stale", label: "降级缓存" },
|
|
|
{ value: "processing", label: "进行中" },
|
|
|
];
|
|
|
|
|
|
const OUTCOME_LABEL: Record<QuoteQueryOutcome, string> = {
|
|
|
success: "成功",
|
|
|
failed: "失败",
|
|
|
stale: "降级缓存",
|
|
|
processing: "进行中",
|
|
|
};
|
|
|
|
|
|
function outcomeClass(outcome: QuoteQueryOutcome): string {
|
|
|
if (outcome === "success") return "text-success";
|
|
|
if (outcome === "failed") return "text-error";
|
|
|
if (outcome === "stale") return "text-warning";
|
|
|
return "text-text-secondary";
|
|
|
}
|
|
|
|
|
|
export default function QueryLogsPage() {
|
|
|
const { token } = useAuth();
|
|
|
const [status, setStatus] = useState<DataPageStatus>("loading");
|
|
|
const [rows, setRows] = useState<QuoteQueryLogRecord[]>([]);
|
|
|
const [total, setTotal] = useState(0);
|
|
|
const [page, setPage] = useState(1);
|
|
|
const [outcomeFilter, setOutcomeFilter] = useState("");
|
|
|
|
|
|
const load = useCallback(
|
|
|
async (p: number, outcome: string) => {
|
|
|
if (!token) return;
|
|
|
setStatus("loading");
|
|
|
const res = await adminGetQueryLogs(
|
|
|
"",
|
|
|
token,
|
|
|
p,
|
|
|
PAGE_SIZE,
|
|
|
undefined,
|
|
|
outcome || undefined,
|
|
|
);
|
|
|
if (res.code !== 0) {
|
|
|
setStatus("error");
|
|
|
return;
|
|
|
}
|
|
|
setRows(res.data.list);
|
|
|
setTotal(res.data.total);
|
|
|
setStatus(res.data.total === 0 ? "empty" : "success");
|
|
|
},
|
|
|
[token],
|
|
|
);
|
|
|
|
|
|
useEffect(() => {
|
|
|
void load(page, outcomeFilter);
|
|
|
}, [load, page, outcomeFilter]);
|
|
|
|
|
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
|
|
|
|
return (
|
|
|
<AdminLayout>
|
|
|
<PageHeader
|
|
|
title="查价记录"
|
|
|
subtitle="最近 1000 条询价流水:时间、客户、地址、货物与报价结果"
|
|
|
action={
|
|
|
<div className="w-36">
|
|
|
<SelectField
|
|
|
label=""
|
|
|
aria-label="查询结果"
|
|
|
name="outcome"
|
|
|
value={outcomeFilter}
|
|
|
options={OUTCOME_OPTIONS}
|
|
|
onChange={(e) => {
|
|
|
setPage(1);
|
|
|
setOutcomeFilter(e.target.value);
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
}
|
|
|
/>
|
|
|
|
|
|
{status === "loading" && (
|
|
|
<div className="space-y-2">
|
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
|
<Skeleton key={i} className="h-14 w-full" />
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
{status === "error" && (
|
|
|
<ErrorBanner
|
|
|
action={<SecondaryButton onClick={() => void load(page, outcomeFilter)}>重试</SecondaryButton>}
|
|
|
>
|
|
|
加载查价记录失败
|
|
|
</ErrorBanner>
|
|
|
)}
|
|
|
{status === "empty" && (
|
|
|
<Card>
|
|
|
<p className="text-sm text-text-secondary">暂无查价记录</p>
|
|
|
</Card>
|
|
|
)}
|
|
|
|
|
|
{status === "success" && (
|
|
|
<div className="space-y-4">
|
|
|
<div className="overflow-x-auto rounded-lg border border-border bg-surface">
|
|
|
<table className="min-w-full text-sm">
|
|
|
<thead className="border-b border-border bg-bg text-left text-text-secondary">
|
|
|
<tr>
|
|
|
<th className="px-3 py-3 font-medium">查询时间</th>
|
|
|
<th className="px-3 py-3 font-medium">客户</th>
|
|
|
<th className="px-3 py-3 font-medium">提货(联想)</th>
|
|
|
<th className="px-3 py-3 font-medium">派送(联想)</th>
|
|
|
<th className="px-3 py-3 font-medium">货物</th>
|
|
|
<th className="px-3 py-3 font-medium">结果</th>
|
|
|
<th className="px-3 py-3 font-medium">完成时间</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
{rows.map((row) => (
|
|
|
<tr key={row.quote_id} className="border-b border-border align-top">
|
|
|
<td className="px-3 py-3 whitespace-nowrap text-text-secondary">
|
|
|
{formatDateTime(row.created_at)}
|
|
|
</td>
|
|
|
<td className="px-3 py-3 font-mono text-xs">{row.customer_id}</td>
|
|
|
<td className="max-w-[180px] px-3 py-3 text-xs leading-relaxed">
|
|
|
{row.pickup_selected}
|
|
|
</td>
|
|
|
<td className="max-w-[180px] px-3 py-3 text-xs leading-relaxed">
|
|
|
{row.delivery_selected}
|
|
|
</td>
|
|
|
<td className="px-3 py-3 text-xs">{row.cargo_summary}</td>
|
|
|
<td className="px-3 py-3">
|
|
|
<span className={`text-xs font-medium ${outcomeClass(row.outcome)}`}>
|
|
|
{OUTCOME_LABEL[row.outcome]}
|
|
|
</span>
|
|
|
{row.failure_reason && row.outcome !== "success" && (
|
|
|
<p className="mt-1 max-w-[200px] text-xs text-text-secondary leading-relaxed">
|
|
|
{row.failure_reason}
|
|
|
</p>
|
|
|
)}
|
|
|
</td>
|
|
|
<td className="px-3 py-3 whitespace-nowrap text-text-secondary">
|
|
|
{row.completed_at ? formatDateTime(row.completed_at) : "—"}
|
|
|
</td>
|
|
|
</tr>
|
|
|
))}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
<span className="text-text-secondary">
|
|
|
共 {total} 条(系统最多保留 1000 条)
|
|
|
</span>
|
|
|
<div className="flex gap-2">
|
|
|
<SecondaryButton
|
|
|
disabled={page <= 1}
|
|
|
onClick={() => setPage((p) => p - 1)}
|
|
|
>
|
|
|
上一页
|
|
|
</SecondaryButton>
|
|
|
<span className="px-2 py-2 text-text-secondary">
|
|
|
{page} / {totalPages}
|
|
|
</span>
|
|
|
<SecondaryButton
|
|
|
disabled={page >= totalPages}
|
|
|
onClick={() => setPage((p) => p + 1)}
|
|
|
>
|
|
|
下一页
|
|
|
</SecondaryButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
</AdminLayout>
|
|
|
);
|
|
|
}
|