diff --git a/src/app/(main)/comparison-records/page.tsx b/src/app/(main)/comparison-records/page.tsx new file mode 100644 index 0000000..04fb24e --- /dev/null +++ b/src/app/(main)/comparison-records/page.tsx @@ -0,0 +1,303 @@ +'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 }} + /> +