refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态
This commit is contained in:
@@ -113,7 +113,7 @@ export default function ChannelsPage() {
|
||||
const sequence = ++requestSequence.current;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await listAdminChannels({ keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, limit: pageSize });
|
||||
const result = await listAdminChannels({ keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, pageSize });
|
||||
if (sequence !== requestSequence.current) return;
|
||||
setChannels(result.channels);
|
||||
setTotal(result.total);
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function AdminAnnouncementsPanel({
|
||||
keyword: queryKeyword || undefined,
|
||||
status: queryStatus === "all" ? undefined : queryStatus,
|
||||
page: targetPage,
|
||||
limit: targetPageSize,
|
||||
pageSize: targetPageSize,
|
||||
}),
|
||||
targetPage,
|
||||
targetPageSize,
|
||||
@@ -1024,13 +1024,13 @@ function assertAnnouncementMutationResult(
|
||||
}
|
||||
}
|
||||
|
||||
function assertAnnouncementListResult(result: unknown, expectedPage: number, expectedLimit: number) {
|
||||
if (!isRecord(result) || !Array.isArray(result.announcements) || !Number.isInteger(result.total) || (result.total as number) < 0 || result.page !== expectedPage || result.limit !== expectedLimit) {
|
||||
function assertAnnouncementListResult(result: unknown, expectedPage: number, expectedPageSize: number) {
|
||||
if (!isRecord(result) || !Array.isArray(result.announcements) || !Number.isInteger(result.total) || (result.total as number) < 0 || result.page !== expectedPage || result.pageSize !== expectedPageSize) {
|
||||
throw new Error("公告列表返回格式不完整");
|
||||
}
|
||||
const announcements = result.announcements.map((value) => normalizeAnnouncementListItem(value));
|
||||
if ((result.total as number) < announcements.length) throw new Error("公告列表总数与当前页数据不一致");
|
||||
return { announcements, total: result.total as number, page: expectedPage, limit: expectedLimit };
|
||||
return { announcements, total: result.total as number, page: expectedPage, pageSize: expectedPageSize };
|
||||
}
|
||||
|
||||
function normalizeAnnouncementListItem(value: unknown): SystemAnnouncement {
|
||||
@@ -1076,9 +1076,9 @@ async function inspectPendingReview(review: AnnouncementPendingReview) {
|
||||
for (const title of queryTitles) {
|
||||
let targetPage = 1;
|
||||
while (targetPage <= 50) {
|
||||
const data = assertAnnouncementListResult(await listAdminAnnouncements({ keyword: title, page: targetPage, limit: 100 }), targetPage, 100);
|
||||
const data = assertAnnouncementListResult(await listAdminAnnouncements({ keyword: title, page: targetPage, pageSize: 100 }), targetPage, 100);
|
||||
data.announcements.forEach((announcement) => candidates.set(announcement.id, announcement));
|
||||
if (targetPage >= Math.max(1, Math.ceil(data.total / data.limit))) break;
|
||||
if (targetPage >= Math.max(1, Math.ceil(data.total / data.pageSize))) break;
|
||||
if (targetPage === 50) throw new Error("同名匹配记录过多,无法安全定位目标;请稍后重试。");
|
||||
targetPage += 1;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { App, Button, Descriptions, Drawer, Progress, Skeleton, Tabs } from "antd";
|
||||
import { App, Button, Descriptions, Progress, Skeleton, Tabs } from "antd";
|
||||
import { AppDrawer } from "@/components/ui/product/app-drawer";
|
||||
import { IconButton } from "@/components/ui/base/buttons";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
@@ -46,7 +47,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
let active = true;
|
||||
void listAdminUserLedger(userId, { page: ledgerPage, limit: 20 })
|
||||
void listAdminUserLedger(userId, { page: ledgerPage, pageSize: 20 })
|
||||
.then((result) => {
|
||||
if (active) {
|
||||
setLedger(result.entries);
|
||||
@@ -61,7 +62,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
let active = true;
|
||||
void listAdminUserTasks(userId, { page: taskPage, limit: 20 })
|
||||
void listAdminUserTasks(userId, { page: taskPage, pageSize: 20 })
|
||||
.then((result) => {
|
||||
if (active) {
|
||||
setTasks(result.tasks);
|
||||
@@ -76,7 +77,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
let active = true;
|
||||
void listAdminUserAuditEvents(userId, { page: auditPage, limit: 20 })
|
||||
void listAdminUserAuditEvents(userId, { page: auditPage, pageSize: 20 })
|
||||
.then((result) => {
|
||||
if (active) {
|
||||
setEvents(result.events);
|
||||
@@ -90,12 +91,11 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
|
||||
}, [auditPage, message, userId]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
<AppDrawer
|
||||
title={detail ? `${detail.user.displayName || detail.user.username} · 用户详情` : "用户详情"}
|
||||
open={Boolean(userId)}
|
||||
onClose={onClose}
|
||||
size="min(920px, 100vw)"
|
||||
destroyOnHidden
|
||||
rootClassName="admin-drawer"
|
||||
extra={onNavigate ? (
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -229,7 +229,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
|
||||
) : (
|
||||
<EmptyState size="compact" title="没有用户详情" />
|
||||
)}
|
||||
</Drawer>
|
||||
</AppDrawer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function AnalyticsPanel({ users, channels }: Props) {
|
||||
const searchUsers = async (keyword: string) => {
|
||||
setSearchingUsers(true);
|
||||
try {
|
||||
const result = await listAdminUsers({ keyword: keyword.trim() || undefined, page: 1, limit: 50 });
|
||||
const result = await listAdminUsers({ keyword: keyword.trim() || undefined, page: 1, pageSize: 50 });
|
||||
setUserOptions(result.users);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "搜索用户失败");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Alert, App, Button, Form, Input, Modal, Segmented, Switch, Tabs } from "antd";
|
||||
import { Alert, App, Button, Form, Input, Segmented, Switch, Tabs } from "antd";
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import { FlaskConical, Plus } from "lucide-react";
|
||||
import { ModelIconPicker } from "@/components/model-logo";
|
||||
import { ModelIcon } from "@/components/model-picker";
|
||||
@@ -166,7 +167,7 @@ export function ChannelModelEditor({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<AppModal
|
||||
open
|
||||
title={
|
||||
<div className="admin-model-editor-title">
|
||||
@@ -183,7 +184,7 @@ export function ChannelModelEditor({
|
||||
keyboard={!busy}
|
||||
closable={!busy}
|
||||
onCancel={requestClose}
|
||||
styles={{ container: { padding: 0, overflow: "hidden" }, body: { padding: 0, minHeight: 0, flex: 1 }, header: { margin: 0 }, footer: { margin: 0 } }}
|
||||
flush styles={{ body: { minHeight: 0, flex: 1 }, header: { margin: 0 }, footer: { margin: 0 } }}
|
||||
footer={
|
||||
<div className="admin-model-editor-footer-actions">
|
||||
<div className="admin-model-editor-footer-status">
|
||||
@@ -396,7 +397,7 @@ export function ChannelModelEditor({
|
||||
]}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
</AppModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { App, Button, Empty, Modal, Spin } from "antd";
|
||||
import { App, Button, Modal, Spin } from "antd";
|
||||
import { ArrowDown, ArrowUp, GripVertical, ListOrdered } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { getChannelOrder, saveChannelOrder, type ChannelOrderItem } from "@/services/api/channel-order";
|
||||
|
||||
export function moveOrderItem<T extends { id: string }>(items: T[], id: string, target: number): T[] {
|
||||
@@ -86,7 +87,7 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
|
||||
<Spin spinning={loading}>
|
||||
<div className="max-h-[55vh] overflow-y-auto space-y-2" role="list" aria-label="展示顺序">
|
||||
{!loading && !items.length ? (
|
||||
<Empty description="暂无可排序条目" />
|
||||
<EmptyState size="compact" title="暂无可排序条目" />
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<div
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function CreditOperationsPanel({ users, activeOperation, onOperat
|
||||
keyword: debouncedKeyword || undefined,
|
||||
status: orderStatus,
|
||||
page: targetPage,
|
||||
limit: targetPageSize,
|
||||
pageSize: targetPageSize,
|
||||
});
|
||||
if (requestId !== ordersRequestRef.current) return;
|
||||
if (targetPage > 1 && result.total > 0 && result.orders.length === 0) {
|
||||
@@ -134,7 +134,7 @@ export default function CreditOperationsPanel({ users, activeOperation, onOperat
|
||||
if (activeOperation !== "adjustment") return;
|
||||
const requestId = ++userSearchRequestRef.current;
|
||||
setSearchingUsers(true);
|
||||
void listAdminUsers({ keyword: debouncedAdjustmentSearch.trim() || undefined, page: 1, limit: 50 })
|
||||
void listAdminUsers({ keyword: debouncedAdjustmentSearch.trim() || undefined, page: 1, pageSize: 50 })
|
||||
.then((result) => {
|
||||
if (requestId !== userSearchRequestRef.current) return;
|
||||
const selectedId = adjustmentForm.getFieldValue("userId");
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function RedemptionCodesPanel({ createOpen, onCreateOpenChange, o
|
||||
keyword: queryKeyword || undefined,
|
||||
validity: queryValidity === "all" ? undefined : queryValidity,
|
||||
page: targetPage,
|
||||
limit: targetPageSize,
|
||||
pageSize: targetPageSize,
|
||||
});
|
||||
if (requestId !== listRequestRef.current) return false;
|
||||
const lastPage = Math.max(1, Math.ceil(result.total / targetPageSize));
|
||||
@@ -641,7 +641,7 @@ function RedeemBatchCodesModal({ batch, onClose, onBatchChanged }: { batch: Rede
|
||||
setCodes([]);
|
||||
setTotal(0);
|
||||
try {
|
||||
const result = await listAdminRedeemBatchCodes(batch.id, { status: targetStatus === "all" ? undefined : targetStatus, page: targetPage, limit: targetPageSize });
|
||||
const result = await listAdminRedeemBatchCodes(batch.id, { status: targetStatus === "all" ? undefined : targetStatus, page: targetPage, pageSize: targetPageSize });
|
||||
if (requestId !== requestRef.current) return false;
|
||||
const lastPage = Math.max(1, Math.ceil(result.total / targetPageSize));
|
||||
if (targetPage > lastPage) {
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function StorageResourcesPanel() {
|
||||
provider: provider === "all" ? undefined : provider,
|
||||
userId: debouncedUserId || undefined,
|
||||
page,
|
||||
limit: pageSize,
|
||||
pageSize: pageSize,
|
||||
},
|
||||
controller.signal,
|
||||
)
|
||||
|
||||
@@ -93,8 +93,8 @@ export default function LogicalModelsPage() {
|
||||
const reload = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [modelResult, firstChannelPage] = await Promise.all([listAdminLogicalModels(), listAdminChannels({ page: 1, limit: 100 })]);
|
||||
const remainingChannelPages = await Promise.all(Array.from({ length: Math.max(0, Math.ceil(firstChannelPage.total / firstChannelPage.limit) - 1) }, (_, index) => listAdminChannels({ page: index + 2, limit: firstChannelPage.limit })));
|
||||
const [modelResult, firstChannelPage] = await Promise.all([listAdminLogicalModels(), listAdminChannels({ page: 1, pageSize: 100 })]);
|
||||
const remainingChannelPages = await Promise.all(Array.from({ length: Math.max(0, Math.ceil(firstChannelPage.total / firstChannelPage.pageSize) - 1) }, (_, index) => listAdminChannels({ page: index + 2, pageSize: firstChannelPage.pageSize })));
|
||||
const channels = [firstChannelPage, ...remainingChannelPages].flatMap((result) => result.channels);
|
||||
const channelModelResults = await Promise.all(channels.map((channel) => listAdminChannelModels(channel.id)));
|
||||
setModels(modelResult.models);
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function LogsPage() {
|
||||
useEffect(() => {
|
||||
const sequence = ++requestSequence.current;
|
||||
setLoading(true);
|
||||
void listAdminApiLogs({ recordType, keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, limit: pageSize })
|
||||
void listAdminApiLogs({ recordType, keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, pageSize })
|
||||
.then((result) => {
|
||||
if (sequence !== requestSequence.current) return;
|
||||
setLogs(result.logs);
|
||||
|
||||
@@ -121,14 +121,14 @@ export default function AdminPaymentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadOrders = async (page = orderPage, limit = orderPageSize) => {
|
||||
const loadOrders = async (page = orderPage, pageSize = orderPageSize) => {
|
||||
setOrdersLoading(true);
|
||||
try {
|
||||
const result = await listAdminPaymentOrders({ status: orderStatusFilter === "all" ? undefined : orderStatusFilter, keyword: orderKeyword.trim() || undefined, page, limit });
|
||||
const result = await listAdminPaymentOrders({ status: orderStatusFilter === "all" ? undefined : orderStatusFilter, keyword: orderKeyword.trim() || undefined, page, pageSize });
|
||||
setOrders(result.orders);
|
||||
setOrderTotal(result.total);
|
||||
setOrderPage(result.page);
|
||||
setOrderPageSize(result.limit);
|
||||
setOrderPageSize(result.pageSize);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "读取支付订单失败");
|
||||
} finally {
|
||||
@@ -136,14 +136,14 @@ export default function AdminPaymentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadRuns = async (page = runPage, limit = runPageSize) => {
|
||||
const loadRuns = async (page = runPage, pageSize = runPageSize) => {
|
||||
setRunsLoading(true);
|
||||
try {
|
||||
const result = await listAdminPaymentReconciliations({ providerId: runProviderFilter === "all" ? undefined : runProviderFilter, page, limit });
|
||||
const result = await listAdminPaymentReconciliations({ providerId: runProviderFilter === "all" ? undefined : runProviderFilter, page, pageSize });
|
||||
setRuns(result.runs);
|
||||
setRunTotal(result.total);
|
||||
setRunPage(result.page);
|
||||
setRunPageSize(result.limit);
|
||||
setRunPageSize(result.pageSize);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "读取对账记录失败");
|
||||
} finally {
|
||||
@@ -287,16 +287,16 @@ export default function AdminPaymentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openRunDetails = async (run: PaymentReconciliationRun, page = 1, limit = detailPageSize, result = detailResult) => {
|
||||
const openRunDetails = async (run: PaymentReconciliationRun, page = 1, pageSize = detailPageSize, result = detailResult) => {
|
||||
setDetailRun(run);
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const response = await listAdminPaymentReconciliationItems(run.id, { result: result === "all" ? undefined : result, page, limit });
|
||||
const response = await listAdminPaymentReconciliationItems(run.id, { result: result === "all" ? undefined : result, page, pageSize });
|
||||
setDetailRun(response.run);
|
||||
setDetailItems(response.items);
|
||||
setDetailTotal(response.total);
|
||||
setDetailPage(response.page);
|
||||
setDetailPageSize(response.limit);
|
||||
setDetailPageSize(response.pageSize);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "读取对账明细失败");
|
||||
} finally {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, Button, Drawer, Form, Input, Popconfirm, Select, Tabs } from "antd";
|
||||
import { App, Button, Form, Input, Popconfirm, Select, Tabs } from "antd";
|
||||
import { StatusBadge } from "@/components/ui/base/badges";
|
||||
import { Callout } from "@/components/ui/product/callout";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
@@ -7,6 +7,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import { PaginationBar } from "@/components/layout/workspace-page";
|
||||
import { AppDrawer } from "@/components/ui/product/app-drawer/app-drawer";
|
||||
import { PromptCodeEditor, type PromptCodeEditorHandle } from "@/components/prompt/prompt-code-editor";
|
||||
import {
|
||||
createAdminPromptTemplate,
|
||||
@@ -187,7 +188,8 @@ export default function StoryboardPromptsPage() {
|
||||
footer={<PaginationBar alwaysShow current={page} pageSize={pageSize} total={filtered.length} onChange={(nextPage, nextPageSize) => { setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} />}
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
<AppDrawer
|
||||
flush
|
||||
title={baseTemplate ? `基于 v${baseTemplate.version} 新建版本` : "新建提示词版本"}
|
||||
open={drawerOpen}
|
||||
size="min(1180px, 100vw)"
|
||||
@@ -196,8 +198,6 @@ export default function StoryboardPromptsPage() {
|
||||
closable={false}
|
||||
mask={{ closable: false }}
|
||||
keyboard={false}
|
||||
destroyOnHidden
|
||||
styles={{ body: { padding: 0 } }}
|
||||
extra={<div className="flex gap-2"><Popconfirm disabled={!dirty} title="放弃模板修改?" description="尚未保存的新版本内容将丢失。" okText="放弃修改" cancelText="继续编辑" okButtonProps={{ danger: true }} onConfirm={closeDrawer}><Button disabled={saving} onClick={() => { if (!dirty) closeDrawer(); }}>关闭</Button></Popconfirm><Button type="primary" loading={saving} disabled={!draftOperation || !draftName.trim() || !editorContent.trim()} onClick={() => void save()}>保存版本</Button></div>}
|
||||
>
|
||||
<Form form={form} layout="vertical" requiredMark={false} className="flex min-h-full flex-col">
|
||||
@@ -253,7 +253,7 @@ export default function StoryboardPromptsPage() {
|
||||
</aside>
|
||||
</div>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</AppDrawer>
|
||||
</AdminPageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
|
||||
role: state.role === "all" ? undefined : state.role,
|
||||
status: state.status === "all" ? undefined : state.status,
|
||||
page: state.page,
|
||||
limit: state.pageSize,
|
||||
pageSize: state.pageSize,
|
||||
})
|
||||
.then((result) => {
|
||||
if (sequence !== requestSequence.current) return;
|
||||
|
||||
Reference in New Issue
Block a user