diff --git a/core/frontend/src/routes/admin/admin-governance.tsx b/core/frontend/src/routes/admin/admin-governance.tsx index bbc1e94..ea9040b 100644 --- a/core/frontend/src/routes/admin/admin-governance.tsx +++ b/core/frontend/src/routes/admin/admin-governance.tsx @@ -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([]); const [count, setCount] = useState(0); const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); const [tab, setTab] = useState(""); const [integrity, setIntegrity] = useState(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 }) {
{STATUS_TABS.map((t) => ( - + ))}
{count} 个项目 @@ -122,6 +127,7 @@ export function AdminGovernancePage({ notify }: { notify: Notify }) { ))} +
)} diff --git a/core/frontend/src/routes/admin/admin-invites.tsx b/core/frontend/src/routes/admin/admin-invites.tsx index 37cec86..2ca51e5 100644 --- a/core/frontend/src/routes/admin/admin-invites.tsx +++ b/core/frontend/src/routes/admin/admin-invites.tsx @@ -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([]); const [count, setCount] = useState(0); const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); const [kind, setKind] = useState(""); 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} @@ -175,6 +180,7 @@ export function AdminInvitesPage({ notify }: { notify: Notify }) { })} + )} diff --git a/core/frontend/src/routes/admin/admin-models.tsx b/core/frontend/src/routes/admin/admin-models.tsx index d573668..97374be 100644 --- a/core/frontend/src/routes/admin/admin-models.tsx +++ b/core/frontend/src/routes/admin/admin-models.tsx @@ -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([]); const [models, setModels] = useState([]); + const [modelPage, setModelPage] = useState(1); const [loading, setLoading] = useState(true); const [provModal, setProvModal] = useState(null); const [modelModal, setModelModal] = useState(null); @@ -152,14 +157,14 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
模型
- + - {models.map((m) => ( + {models.slice((modelPage - 1) * PAGE_SIZE, modelPage * PAGE_SIZE).map((m) => ( - +
供应商模型能力单价状态默认操作
供应商模型能力单价(积分/次)状态默认操作
{m.provider_name} {m.display_name || m.name} {m.capability}¥{m.unit_price}{pts(m.unit_price)} 积分 {statusPill(m.status === "active")} {m.is_default ? 默认 : } @@ -172,6 +177,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) { ))}
+
)} @@ -246,7 +252,7 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
- + setModelModal((m) => m && ({ ...m, unit_price: e.target.value }))} />
diff --git a/core/frontend/src/routes/admin/admin-reviews.tsx b/core/frontend/src/routes/admin/admin-reviews.tsx index 835eef0..6df46f8 100644 --- a/core/frontend/src/routes/admin/admin-reviews.tsx +++ b/core/frontend/src/routes/admin/admin-reviews.tsx @@ -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([]); const [count, setCount] = useState(0); const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); const [tab, setTab] = useState(""); const [selected, setSelected] = useState>(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 }) {
{TABS.map((t) => ( - + ))}
@@ -150,6 +155,7 @@ export function AdminReviewsPage({ notify }: { notify: Notify }) { ))} +
)} diff --git a/core/frontend/src/routes/admin/admin-tasks.tsx b/core/frontend/src/routes/admin/admin-tasks.tsx index 8d76a33..0ccd482 100644 --- a/core/frontend/src/routes/admin/admin-tasks.tsx +++ b/core/frontend/src/routes/admin/admin-tasks.tsx @@ -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([]); 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(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 }) {
{TABS.map((t) => ( - + ))}
-
@@ -116,7 +122,7 @@ export function AdminTasksPage({ notify }: { notify: Notify }) { {t.model_name || } {statusPill(t.status)} - ¥{t.estimated_cost} → ¥{t.actual_cost} + {pts(t.estimated_cost)} → {pts(t.actual_cost)} 积分{t.margin_yuan != null ? ` · 毛利 ¥${t.margin_yuan}` : ""} {t.cost_anomaly && 异常} {fmtDate(t.created_at)} @@ -130,6 +136,7 @@ export function AdminTasksPage({ notify }: { notify: Notify }) { ))} +
)} @@ -149,7 +156,8 @@ export function AdminTasksPage({ notify }: { notify: Notify }) {
状态{statusPill(detail.status)}
团队{detail.team_name || "—"}
模型{detail.model_name || "—"}
-
成本¥{detail.estimated_cost} → ¥{detail.actual_cost}{detail.cost_anomaly ? " ⚠" : ""}
+
计价{pts(detail.estimated_cost)} → {pts(detail.actual_cost)} 积分{detail.cost_anomaly ? " ⚠" : ""}
+
平台成本{Number(detail.base_cost || 0) > 0 ? `¥${detail.base_cost} · 毛利 ${detail.margin_yuan != null ? `¥${detail.margin_yuan}` : "—"}` : "未知"}
{detail.error_message && ( <>