From 76d317d2eaf71223af3ba42553be298b7836aac5 Mon Sep 17 00:00:00 2001 From: linkeyu Date: Wed, 22 Jul 2026 10:42:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=8A=A0=E9=80=9F=E6=AF=94?= =?UTF-8?q?=E4=BB=B7=E8=AE=B0=E5=BD=95=E9=A1=B5=E5=B9=B6=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E8=80=97=E6=97=B6=E5=8F=A3=E5=BE=84=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 变更内容 - 列表从最多串行拉取 200 页改为仅请求当前页 - 概览改用后端聚合接口,与列表并行加载 - 主耗时五项只展示成功比价数据 - 中途退出五项在桌面端同一行展示,小屏自动换行 - 翻页不再重复请求概览 ## 依赖 - 配套 Server MR #152 ## 验证 - npx tsc --noEmit - npm run build --------- Co-authored-by: linkeyu <798648091@qq.com> Reviewed-on: https://gitea.shaguabijia.com/WonderableAI/shaguabijia-admin-web/pulls/55 Co-authored-by: linkeyu Co-committed-by: linkeyu --- src/app/(main)/comparison-records/page.tsx | 162 +++++++++------------ src/lib/types.ts | 19 +++ 2 files changed, 86 insertions(+), 95 deletions(-) diff --git a/src/app/(main)/comparison-records/page.tsx b/src/app/(main)/comparison-records/page.tsx index 5a06e7c..ae9cf14 100644 --- a/src/app/(main)/comparison-records/page.tsx +++ b/src/app/(main)/comparison-records/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import type { CSSProperties, ReactNode } from 'react'; import { App, Button, Card, Col, Collapse, DatePicker, Descriptions, Divider, Drawer, Input, InputNumber, @@ -9,8 +9,13 @@ import { import type { ColumnsType } from 'antd/es/table'; import dayjs, { type Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; -import { formatWallTime, percentile, yuan } from '@/lib/format'; -import type { ComparisonRecordDetail, ComparisonRecordListItem, CursorPage } from '@/lib/types'; +import { formatWallTime, yuan } from '@/lib/format'; +import type { + ComparisonRecordDetail, + ComparisonRecordListItem, + ComparisonRecordsSummary, + CursorPage, +} from '@/lib/types'; const { RangePicker } = DatePicker; @@ -105,8 +110,11 @@ export default function ComparisonRecordsPage() { date_from: dayjs().subtract(1, 'day').format('YYYY-MM-DD'), date_to: dayjs().subtract(1, 'day').format('YYYY-MM-DD'), }); - const [allItems, setAllItems] = useState([]); - const [loading, setLoading] = useState(false); + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [overview, setOverview] = useState(null); + const [listLoading, setListLoading] = useState(false); + const [summaryLoading, setSummaryLoading] = useState(false); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); // LLM 单价(元/百万 token),本地持久化;仅用于前端估算成本,不入库、不影响其它页面 @@ -125,7 +133,8 @@ export default function ComparisonRecordsPage() { const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); - const search = () => + const search = () => { + setPage(1); setApplied({ date_from: range[0].format('YYYY-MM-DD'), date_to: range[1].format('YYYY-MM-DD'), @@ -135,6 +144,7 @@ export default function ComparisonRecordsPage() { store: store.trim() || undefined, product: product.trim() || undefined, }); + }; const reset = () => { setUserId(null); @@ -143,6 +153,7 @@ export default function ComparisonRecordsPage() { setStore(''); setProduct(''); const yesterday = dayjs().subtract(1, 'day'); + setPage(1); setRange([yesterday, yesterday]); setApplied({ date_from: yesterday.format('YYYY-MM-DD'), @@ -152,49 +163,50 @@ export default function ComparisonRecordsPage() { useEffect(() => { let alive = true; - const loadAll = async () => { - setLoading(true); + const loadPage = async () => { + setListLoading(true); try { - const serverFilters = { ...applied }; - delete serverFilters.date_from; - delete serverFilters.date_to; - const rows: ComparisonRecordListItem[] = []; - let cursor = 0; - for (let pageIndex = 0; pageIndex < 200; pageIndex += 1) { - const { data } = await api.get>( - '/admin/api/comparison-records', - { params: { ...serverFilters, limit: 100, cursor } }, - ); - rows.push(...data.items); - if (data.next_cursor == null || data.items.length === 0) break; - cursor = data.next_cursor; - } + const { data } = await api.get>( + '/admin/api/comparison-records', + { params: { ...applied, limit: pageSize, cursor: (page - 1) * pageSize } }, + ); if (alive) { - setAllItems(rows); - setPage(1); + setItems(data.items); + setTotal(data.total ?? 0); } } catch (e) { if (alive) message.error(errMsg(e)); } finally { - if (alive) setLoading(false); + if (alive) setListLoading(false); } }; - loadAll(); + loadPage(); + return () => { + alive = false; + }; + }, [applied, message, page, pageSize]); + + useEffect(() => { + let alive = true; + const loadSummary = async () => { + setSummaryLoading(true); + try { + const { data } = await api.get( + '/admin/api/comparison-records/summary', + { params: applied }, + ); + if (alive) setOverview(data); + } catch (e) { + if (alive) message.error(errMsg(e)); + } finally { + if (alive) setSummaryLoading(false); + } + }; + loadSummary(); return () => { alive = false; }; }, [applied, message]); - - const filteredItems = useMemo(() => { - const from = String(applied.date_from ?? ''); - const to = String(applied.date_to ?? ''); - return allItems.filter((item) => { - const date = dayjs(item.created_at).format('YYYY-MM-DD'); - return (!from || date >= from) && (!to || date <= to); - }); - }, [allItems, applied]); - const total = filteredItems.length; - const items = filteredItems.slice((page - 1) * pageSize, page * pageSize); const onChange = (nextPage: number, nextPageSize: number) => { setPageSize(nextPageSize); setPage(nextPageSize === pageSize ? nextPage : 1); @@ -218,46 +230,6 @@ export default function ComparisonRecordsPage() { setDetailLoading(false); }; - const overview = useMemo(() => { - const completed = filteredItems.filter( - (item) => item.status === 'success' || item.status === 'failed', - ); - const cancelled = filteredItems.filter((item) => item.status === 'cancelled'); - const success = filteredItems.filter((item) => item.status === 'success'); - const successRateDenominator = filteredItems.length - cancelled.length; - const completedDurations = completed - .map((item) => item.total_ms) - .filter((value): value is number => value != null); - const cancelledDurations = cancelled - .map((item) => item.total_ms) - .filter((value): value is number => value != null); - const costs = filteredItems - .map((item) => item.llm_cost_yuan) - .filter((value): value is number => value != null); - const avg = (values: number[]) => - values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null; - return { - started: filteredItems.length, - completed: completed.length, - success: success.length, - successRate: successRateDenominator ? success.length / successRateDenominator : null, - avgTokenCost: avg(costs), - lowerPriceRate: success.length - ? success.filter((item) => (item.saved_amount_cents ?? 0) > 0).length / success.length - : null, - avgMs: avg(completedDurations), - p5Ms: percentile(completedDurations, 0.05), - p50Ms: percentile(completedDurations, 0.5), - p95Ms: percentile(completedDurations, 0.95), - p99Ms: percentile(completedDurations, 0.99), - cancelled: cancelled.length, - cancelledRate: filteredItems.length ? cancelled.length / filteredItems.length : null, - cancelledP5Ms: percentile(cancelledDurations, 0.05), - cancelledP50Ms: percentile(cancelledDurations, 0.5), - cancelledP95Ms: percentile(cancelledDurations, 0.95), - }; - }, [filteredItems]); - const columns: ColumnsType = [ { title: '时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatWallTime(v) }, { @@ -424,28 +396,28 @@ export default function ComparisonRecordsPage() { - + 数据概览 - - - - - - - - - - - + + + + + + + + + + + 中途退出 - - - - - + + + + + @@ -453,7 +425,7 @@ export default function ComparisonRecordsPage() { rowKey="id" columns={columns} dataSource={items} - loading={loading} + loading={listLoading} pagination={{ current: page, pageSize, diff --git a/src/lib/types.ts b/src/lib/types.ts index 936b3ab..caec6d6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -364,6 +364,25 @@ export interface ComparisonRecordListItem { created_at: string; } +export interface ComparisonRecordsSummary { + started: number; + completed: number; + success: number; + success_rate: number | null; + avg_token_cost: number | null; + lower_price_rate: number | null; + avg_duration_ms: number | null; + p5_duration_ms: number | null; + p50_duration_ms: number | null; + p95_duration_ms: number | null; + p99_duration_ms: number | null; + cancelled: number; + cancelled_rate: number | null; + cancelled_p5_ms: number | null; + cancelled_p50_ms: number | null; + cancelled_p95_ms: number | null; +} + // 单次 LLM 调用明细(pricebot chat() 收口落盘,server 按 trace 拉来) export interface LlmCall { ts?: number;