'use client'; import { useState } from 'react'; import type { CSSProperties } from 'react'; import { App, Button, Card, Collapse, Descriptions, Drawer, Input, InputNumber, Select, Space, Spin, Table, Tag, Typography, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { api, errMsg } from '@/lib/api'; import { formatUtcTime, yuan } from '@/lib/format'; import { usePagedList } from '@/lib/usePagedList'; import type { ComparisonRecordDetail, ComparisonRecordListItem } from '@/lib/types'; const STATUS_COLOR: Record = { success: 'green', failed: 'red' }; // 「卡在哪一步」: platform_results[*].status 翻成人话(找店/加菜/起送/读价) const STUCK_LABEL: Record = { success: '成功', store_not_found: '没找到店', items_not_found: '菜没匹配上', below_minimum: '未达起送', unsupported: '平台不支持', failed: '失败', }; const fmtMs = (ms: number | null) => (ms == null ? '-' : `${(ms / 1000).toFixed(1)}s`); const cents = (c: number | null) => (c == null ? '-' : yuan(c)); // LLM message content 常是 json.dumps 的卡片列表;能 parse 成 JSON 就缩进展开,否则原样。 function pretty(s: string | null): string { if (!s) return ''; try { return JSON.stringify(JSON.parse(s), null, 2); } catch { return s; } } const preStyle: CSSProperties = { background: '#f6f8fa', padding: 8, borderRadius: 6, fontSize: 12, maxHeight: 340, overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, }; export default function ComparisonRecordsPage() { const { message } = App.useApp(); const [userId, setUserId] = useState(null); const [phone, setPhone] = useState(''); const [status, setStatus] = useState(); const [applied, setApplied] = useState>({}); const { items, total, page, pageSize, loading, onChange } = usePagedList('/admin/api/comparison-records', applied); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const search = () => setApplied({ user_id: userId ?? undefined, phone: phone || undefined, status }); const reset = () => { setUserId(null); setPhone(''); setStatus(undefined); setApplied({}); }; const openDetail = async (id: number) => { setDetailLoading(true); setDetail(null); try { const { data } = await api.get(`/admin/api/comparison-records/${id}`); setDetail(data); } catch (e) { message.error(errMsg(e)); } finally { setDetailLoading(false); } }; const closeDetail = () => { setDetail(null); setDetailLoading(false); }; const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', width: 64 }, { title: '用户', key: 'user', width: 140, render: (_, r) => (
{r.phone || `#${r.user_id}`}
{r.nickname &&
{r.nickname}
}
), }, { title: '状态', dataIndex: 'status', width: 72, render: (s: string) => {s} }, { title: '源平台', dataIndex: 'source_platform_name', width: 90, render: (v) => v || '-' }, { title: '店/商品', dataIndex: 'store_name', width: 150, ellipsis: true, render: (v) => v || '-' }, { title: '最优', dataIndex: 'best_platform_name', width: 80, render: (v) => v || '-' }, { title: '省', key: 'saved', width: 84, render: (_, r) => r.saved_amount_cents == null ? '-' : ( 0 ? '#3f8600' : '#999' }}>{yuan(r.saved_amount_cents)} ), }, { title: '耗时', dataIndex: 'total_ms', width: 64, render: fmtMs }, { title: '步', dataIndex: 'step_count', width: 48, render: (v) => v ?? '-' }, { title: 'LLM', dataIndex: 'llm_call_count', width: 56, render: (v) => v ?? '-' }, { title: '重试', dataIndex: 'retry_count', width: 50, render: (v: number | null) => (v ? {v} : v ?? '-'), }, { title: '机型/ROM', key: 'device', width: 150, ellipsis: true, render: (_, r) => [r.device_model, r.rom_name].filter(Boolean).join(' / ') || '-', }, { title: '时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) }, { title: '操作', key: 'op', fixed: 'right', width: 80, render: (_, r) => r.trace_url ? ( e.stopPropagation()}>trace ) : ( - ), }, ]; const platformResults = (detail?.raw_payload?.platform_results as Record[] | undefined) || []; return (

比价记录(debug)

setPhone(e.target.value)} onPressEnter={search} allowClear style={{ width: 150 }} />