feat(admin): 后台列表分页 + 积分显示格式化

9 张 admin 列表表格接服务端分页(共享 Pager 组件,10条/页,alwaysShow 单页也显示分页条);
积分数值统一 pts() 格式化(去除 DecimalField 序列化的多余小数位,整数不再显示 .0000)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-07-07 10:16:40 +08:00
co-authored by Claude Sonnet 5
parent bf20de956c
commit 299b2c9ecd
5 changed files with 51 additions and 19 deletions
+14 -6
View File
@@ -1,11 +1,15 @@
import { useCallback, useEffect, useState } from "react";
import { Activity, X } from "lucide-react";
import { adminApi } from "../../api";
import { Pager } from "../../components/pager";
import { IconKitSvg } from "../../components/IconKitSvg";
import type { AdminTask, AdminTaskDetail } from "../../types";
import { pts } from "../stage-config";
type Notify = (type: "success" | "error" | "info", text: string) => void;
const PAGE_SIZE = 10;
const TABS = [
{ key: "", label: "全部" },
{ key: "failed", label: "失败" },
@@ -30,6 +34,7 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
const [tasks, setTasks] = useState<AdminTask[]>([]);
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [tab, setTab] = useState("");
const [anomalyOnly, setAnomalyOnly] = useState(false);
const [detail, setDetail] = useState<AdminTaskDetail | null>(null);
@@ -39,7 +44,8 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
const load = useCallback(async () => {
setLoading(true);
try {
const res = await adminApi.tasks({ status: tab || undefined, anomaly: anomalyOnly ? "1" : undefined, page_size: 60 });
const res = await adminApi.tasks({ status: tab || undefined, anomaly: anomalyOnly ? "1" : undefined, page, page_size: PAGE_SIZE });
if (res.results.length === 0 && res.count > 0 && page > 1) { setPage((p) => Math.max(1, p - 1)); return; }
setTasks(res.results);
setCount(res.count);
} catch {
@@ -48,7 +54,7 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab, anomalyOnly]);
}, [tab, anomalyOnly, page]);
useEffect(() => { void load(); }, [load]);
@@ -90,10 +96,10 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
<div className="admin-toolbar">
<div className="tabs-sub">
{TABS.map((t) => (
<button key={t.key || "all"} type="button" className={`tab-sub${tab === t.key ? " active" : ""}`} onClick={() => setTab(t.key)}>{t.label}</button>
<button key={t.key || "all"} type="button" className={`tab-sub${tab === t.key ? " active" : ""}`} onClick={() => { setTab(t.key); setPage(1); }}>{t.label}</button>
))}
</div>
<button type="button" className={`chip admin-anomaly-chip${anomalyOnly ? " active" : ""}`} onClick={() => setAnomalyOnly((v) => !v)}>
<button type="button" className={`chip admin-anomaly-chip${anomalyOnly ? " active" : ""}`} onClick={() => { setAnomalyOnly((v) => !v); setPage(1); }}>
仅成本异常
</button>
</div>
@@ -116,7 +122,7 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
<td>{t.model_name || <span className="muted">—</span>}</td>
<td>{statusPill(t.status)}</td>
<td className="mono">
¥{t.estimated_cost} → ¥{t.actual_cost}
{pts(t.estimated_cost)} → {pts(t.actual_cost)} 积分{t.margin_yuan != null ? ` · 毛利 ¥${t.margin_yuan}` : ""}
{t.cost_anomaly && <span className="pill err admin-inline-pill">异常</span>}
</td>
<td className="mono col-time">{fmtDate(t.created_at)}</td>
@@ -130,6 +136,7 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
))}
</tbody>
</table>
<Pager page={page} total={count} pageSize={PAGE_SIZE} onChange={setPage} alwaysShow />
</div>
)}
@@ -149,7 +156,8 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
<div><span className="k">状态</span><span className="v">{statusPill(detail.status)}</span></div>
<div><span className="k">团队</span><span className="v">{detail.team_name || "—"}</span></div>
<div><span className="k">模型</span><span className="v">{detail.model_name || "—"}</span></div>
<div><span className="k">成本</span><span className="v mono">¥{detail.estimated_cost} → ¥{detail.actual_cost}{detail.cost_anomaly ? " ⚠" : ""}</span></div>
<div><span className="k">计价</span><span className="v mono">{pts(detail.estimated_cost)} → {pts(detail.actual_cost)} 积分{detail.cost_anomaly ? " ⚠" : ""}</span></div>
<div><span className="k">平台成本</span><span className="v mono">{Number(detail.base_cost || 0) > 0 ? `¥${detail.base_cost} · 毛利 ${detail.margin_yuan != null ? `¥${detail.margin_yuan}` : "—"}` : "未知"}</span></div>
</div>
{detail.error_message && (
<>