feat(admin): 后台列表分页 + 积分显示格式化
9 张 admin 列表表格接服务端分页(共享 Pager 组件,10条/页,alwaysShow 单页也显示分页条); 积分数值统一 pts() 格式化(去除 DecimalField 序列化的多余小数位,整数不再显示 .0000)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { adminApi } from "../../api";
|
||||
import { Pager } from "../../components/pager";
|
||||
import { IconKitSvg } from "../../components/IconKitSvg";
|
||||
import type { AdminIntegrity, AdminProjectRow } from "../../types";
|
||||
|
||||
type Notify = (type: "success" | "error" | "info", text: string) => void;
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ key: "", label: "全部" },
|
||||
{ key: "scripting", label: "脚本" },
|
||||
@@ -33,6 +36,7 @@ export function AdminGovernancePage({ notify }: { notify: Notify }) {
|
||||
const [projects, setProjects] = useState<AdminProjectRow[]>([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [tab, setTab] = useState("");
|
||||
const [integrity, setIntegrity] = useState<AdminIntegrity | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
@@ -40,7 +44,8 @@ export function AdminGovernancePage({ notify }: { notify: Notify }) {
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await adminApi.adminProjects({ status: tab || undefined, page_size: 60 });
|
||||
const res = await adminApi.adminProjects({ status: tab || undefined, page, page_size: PAGE_SIZE });
|
||||
if (res.results.length === 0 && res.count > 0 && page > 1) { setPage((p) => Math.max(1, p - 1)); return; }
|
||||
setProjects(res.results);
|
||||
setCount(res.count);
|
||||
} catch {
|
||||
@@ -49,7 +54,7 @@ export function AdminGovernancePage({ notify }: { notify: Notify }) {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tab]);
|
||||
}, [tab, page]);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
@@ -95,7 +100,7 @@ export function AdminGovernancePage({ notify }: { notify: Notify }) {
|
||||
<div className="admin-toolbar">
|
||||
<div className="tabs-sub">
|
||||
{STATUS_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>
|
||||
<span className="mono gov-total">{count} 个项目</span>
|
||||
@@ -122,6 +127,7 @@ export function AdminGovernancePage({ notify }: { notify: Notify }) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pager page={page} total={count} pageSize={PAGE_SIZE} onChange={setPage} alwaysShow />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Ticket, X } from "lucide-react";
|
||||
import { adminApi } from "../../api";
|
||||
import { Pager } from "../../components/pager";
|
||||
import { IconKitSvg } from "../../components/IconKitSvg";
|
||||
import type { Invitation } from "../../types";
|
||||
|
||||
type Notify = (type: "success" | "error" | "info", text: string) => void;
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
type KindFilter = "" | "create_team" | "join_team";
|
||||
|
||||
// 色调用 restraint pill 颜色类(.pill.info / .ok / .err / .neutral),默认尺寸即 L2。
|
||||
@@ -26,6 +29,7 @@ export function AdminInvitesPage({ notify }: { notify: Notify }) {
|
||||
const [invites, setInvites] = useState<Invitation[]>([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [kind, setKind] = useState<KindFilter>("");
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalEmail, setModalEmail] = useState("");
|
||||
@@ -35,7 +39,8 @@ export function AdminInvitesPage({ notify }: { notify: Notify }) {
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await adminApi.invitations({ kind: kind || undefined, page_size: 100 });
|
||||
const res = await adminApi.invitations({ kind: kind || undefined, page, page_size: PAGE_SIZE });
|
||||
if (res.results.length === 0 && res.count > 0 && page > 1) { setPage((p) => Math.max(1, p - 1)); return; }
|
||||
setInvites(res.results);
|
||||
setCount(res.count);
|
||||
} catch {
|
||||
@@ -44,7 +49,7 @@ export function AdminInvitesPage({ notify }: { notify: Notify }) {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kind]);
|
||||
}, [kind, page]);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
@@ -115,7 +120,7 @@ export function AdminInvitesPage({ notify }: { notify: Notify }) {
|
||||
key={t.key || "all"}
|
||||
type="button"
|
||||
className={`tab-sub${kind === t.key ? " active" : ""}`}
|
||||
onClick={() => setKind(t.key)}
|
||||
onClick={() => { setKind(t.key); setPage(1); }}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
@@ -175,6 +180,7 @@ export function AdminInvitesPage({ notify }: { notify: Notify }) {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pager page={page} total={count} pageSize={PAGE_SIZE} onChange={setPage} alwaysShow />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Server, X } from "lucide-react";
|
||||
import { adminApi } from "../../api";
|
||||
import { Pager } from "../../components/pager";
|
||||
import { IconKitSvg } from "../../components/IconKitSvg";
|
||||
import type { AdminModel, AdminProvider } from "../../types";
|
||||
import { pts } from "../stage-config";
|
||||
|
||||
type Notify = (type: "success" | "error" | "info", text: string) => void;
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const CAPABILITIES = ["text", "image", "video", "vision", "audio", "export"];
|
||||
|
||||
function statusPill(active: boolean) {
|
||||
@@ -20,6 +24,7 @@ const EMPTY_MODEL = { id: "", provider: "", name: "", display_name: "", capabili
|
||||
export function AdminModelsPage({ notify }: { notify: Notify }) {
|
||||
const [providers, setProviders] = useState<AdminProvider[]>([]);
|
||||
const [models, setModels] = useState<AdminModel[]>([]);
|
||||
const [modelPage, setModelPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [provModal, setProvModal] = useState<typeof EMPTY_PROVIDER | null>(null);
|
||||
const [modelModal, setModelModal] = useState<typeof EMPTY_MODEL | null>(null);
|
||||
@@ -152,14 +157,14 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
|
||||
<div className="admin-detail-subhead" style={{ marginTop: 28 }}>模型</div>
|
||||
<div className="admin-table-wrap">
|
||||
<table className="t admin-table">
|
||||
<thead><tr><th>供应商</th><th>模型</th><th>能力</th><th>单价</th><th>状态</th><th>默认</th><th className="col-actions">操作</th></tr></thead>
|
||||
<thead><tr><th>供应商</th><th>模型</th><th>能力</th><th>单价(积分/次)</th><th>状态</th><th>默认</th><th className="col-actions">操作</th></tr></thead>
|
||||
<tbody>
|
||||
{models.map((m) => (
|
||||
{models.slice((modelPage - 1) * PAGE_SIZE, modelPage * PAGE_SIZE).map((m) => (
|
||||
<tr key={m.id}>
|
||||
<td className="mono">{m.provider_name}</td>
|
||||
<td>{m.display_name || m.name}</td>
|
||||
<td><span className="pill neutral"><span className="dot" />{m.capability}</span></td>
|
||||
<td className="num mono">¥{m.unit_price}</td>
|
||||
<td className="num mono">{pts(m.unit_price)} 积分</td>
|
||||
<td>{statusPill(m.status === "active")}</td>
|
||||
<td>{m.is_default ? <span className="pill info"><span className="dot" />默认</span> : <span className="muted">—</span>}</td>
|
||||
<td className="col-actions">
|
||||
@@ -172,6 +177,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pager page={modelPage} total={models.length} pageSize={PAGE_SIZE} onChange={setModelPage} alwaysShow />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -246,7 +252,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<div className="field">
|
||||
<label className="field-label">单价</label>
|
||||
<label className="field-label">单价(积分/次)</label>
|
||||
<input className="input" type="text" placeholder="0" value={modelModal.unit_price} onChange={(e) => setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
|
||||
</div>
|
||||
<div className="field">
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { adminApi } from "../../api";
|
||||
import { Pager } from "../../components/pager";
|
||||
import { IconKitSvg } from "../../components/IconKitSvg";
|
||||
import type { AdminReviewAsset } from "../../types";
|
||||
|
||||
type Notify = (type: "success" | "error" | "info", text: string) => void;
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const TABS = [
|
||||
{ key: "", label: "全部" },
|
||||
{ key: "none", label: "未送审" },
|
||||
@@ -25,6 +28,7 @@ export function AdminReviewsPage({ notify }: { notify: Notify }) {
|
||||
const [assets, setAssets] = useState<AdminReviewAsset[]>([]);
|
||||
const [count, setCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [tab, setTab] = useState("");
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -33,7 +37,8 @@ export function AdminReviewsPage({ notify }: { notify: Notify }) {
|
||||
setLoading(true);
|
||||
setSelected(new Set());
|
||||
try {
|
||||
const res = await adminApi.assetReviews({ review_status: tab || undefined, page_size: 60 });
|
||||
const res = await adminApi.assetReviews({ review_status: tab || undefined, page, page_size: PAGE_SIZE });
|
||||
if (res.results.length === 0 && res.count > 0 && page > 1) { setPage((p) => Math.max(1, p - 1)); return; }
|
||||
setAssets(res.results);
|
||||
setCount(res.count);
|
||||
} catch {
|
||||
@@ -42,7 +47,7 @@ export function AdminReviewsPage({ notify }: { notify: Notify }) {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tab]);
|
||||
}, [tab, page]);
|
||||
|
||||
useEffect(() => { void load(); }, [load]);
|
||||
|
||||
@@ -102,7 +107,7 @@ export function AdminReviewsPage({ 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>
|
||||
</div>
|
||||
@@ -150,6 +155,7 @@ export function AdminReviewsPage({ notify }: { notify: Notify }) {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Pager page={page} total={count} pageSize={PAGE_SIZE} onChange={setPage} alwaysShow />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user