fix(admin): remove redundant confirmations and repair code access
Independent Staging Quality Gate / validate (push) Failing after 12m38s
Independent Staging Quality Gate / publish (push) Has been skipped

This commit is contained in:
Jesse_Chen
2026-08-16 17:54:58 +08:00
parent f12a43ad1d
commit 945d61fa1c
21 changed files with 314 additions and 413 deletions
@@ -7,7 +7,6 @@ import { App, Button, Card, Form, Input, Modal, Select, Space, Table, Tag, Typog
import { useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { ConfirmActionModal } from "./confirm-action-modal";
import { formatAdminDate } from "./resource-table";
const roleOptions = [
@@ -30,12 +29,11 @@ interface Administrator {
}
type AssignmentForm = { email: string; roleCode: RoleCode };
type PendingRoleAction = {
type RoleAction = {
action: "assign" | "revoke";
roleCode: RoleCode;
userId?: string;
email?: string;
label: string;
};
export default function AdministratorsResource() {
@@ -50,7 +48,6 @@ export default function AdministratorsResource() {
const [assignmentForm] = Form.useForm<AssignmentForm>();
const [assignmentOpen, setAssignmentOpen] = useState(false);
const [assignmentUser, setAssignmentUser] = useState<Administrator | null>(null);
const [pendingAction, setPendingAction] = useState<PendingRoleAction | null>(null);
const [saving, setSaving] = useState(false);
const canManage = Boolean(identity?.permissions.includes("admin.users.manage_roles"));
@@ -60,34 +57,32 @@ export default function AdministratorsResource() {
setAssignmentOpen(true);
}
function prepareAssignment(values: AssignmentForm) {
const role = roleOptions.find((item) => item.value === values.roleCode)!;
setPendingAction({
async function assignRole(values: AssignmentForm) {
await submitRoleAction({
action: "assign",
roleCode: values.roleCode,
...(assignmentUser ? { userId: assignmentUser.id } : { email: values.email.trim() }),
label: `${assignmentUser?.email ?? values.email.trim()} · ${role.label}`,
});
}
async function submitRoleAction() {
if (!pendingAction) return;
async function submitRoleAction(action: RoleAction) {
setSaving(true);
try {
await adminRequestJson("/api/admin/administrators", {
method: pendingAction.action === "assign" ? "POST" : "DELETE",
method: action.action === "assign" ? "POST" : "DELETE",
body: JSON.stringify({
userId: pendingAction.userId,
email: pendingAction.email,
roleCode: pendingAction.roleCode,
userId: action.userId,
email: action.email,
roleCode: action.roleCode,
}),
});
message.success(pendingAction.action === "assign" ? "管理员角色已分配" : "管理员角色已撤销");
setPendingAction(null);
message.success(action.action === "assign" ? "管理员角色已分配" : "管理员角色已撤销");
setAssignmentOpen(false);
setAssignmentUser(null);
assignmentForm.resetFields();
await tableQuery.refetch();
} catch (error) {
message.error(error instanceof Error ? error.message : "管理员角色操作失败");
} finally {
setSaving(false);
}
@@ -121,7 +116,7 @@ export default function AdministratorsResource() {
closable={canManage}
onClose={(event) => {
event.preventDefault();
setPendingAction({ action: "revoke", roleCode: role, userId: item.id, label: `${item.email} · ${role}` });
void submitRoleAction({ action: "revoke", roleCode: role, userId: item.id });
}}
>{role}</Tag>)}</Space>,
},
@@ -132,13 +127,14 @@ export default function AdministratorsResource() {
<Modal
title={assignmentUser ? `${assignmentUser.email} 分配角色` : "按邮箱分配管理员角色"}
open={assignmentOpen}
okText="继续"
okText="分配"
cancelText="取消"
confirmLoading={saving}
onOk={() => assignmentForm.submit()}
onCancel={() => setAssignmentOpen(false)}
destroyOnHidden
>
<Form<AssignmentForm> form={assignmentForm} layout="vertical" onFinish={prepareAssignment}>
<Form<AssignmentForm> form={assignmentForm} layout="vertical" onFinish={(values) => void assignRole(values)}>
<Form.Item name="email" label="用户邮箱" rules={[{ required: true }, { type: "email" }]}>
<Input disabled={Boolean(assignmentUser)} autoComplete="email" />
</Form.Item>
@@ -150,15 +146,6 @@ export default function AdministratorsResource() {
</Form.Item>
</Form>
</Modal>
<ConfirmActionModal
open={Boolean(pendingAction)}
title={pendingAction?.action === "revoke" ? `撤销角色:${pendingAction.label}` : `分配角色:${pendingAction?.label ?? ""}`}
okText={pendingAction?.action === "revoke" ? "确认撤销" : "确认分配"}
danger={pendingAction?.action === "revoke"}
confirmLoading={saving}
onCancel={() => setPendingAction(null)}
onConfirm={submitRoleAction}
/>
</Card>
);
}
@@ -18,7 +18,6 @@ import {
import { useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { ConfirmActionModal } from "./confirm-action-modal";
import { formatAdminDate, ResourceTable } from "./resource-table";
const { Text } = Typography;
@@ -48,8 +47,6 @@ export function SubscriptionsResource() {
const [selected, setSelected] = useState<Subscription | null>(null);
const [saving, setSaving] = useState(false);
const [revokingId, setRevokingId] = useState<string | null>(null);
const [revokeTarget, setRevokeTarget] = useState<Subscription | null>(null);
const [extendDays, setExtendDays] = useState<number | null>(null);
const canAdjust = Boolean(
identity?.permissions.includes("billing.adjustments.write"),
);
@@ -71,19 +68,16 @@ export function SubscriptionsResource() {
await table.tableQuery.refetch();
}
function prepareExtend(values: AdjustmentForm) {
setExtendDays(values.days);
}
async function extend() {
if (!selected || extendDays === null) return;
async function extend(values: AdjustmentForm) {
if (!selected) return;
setSaving(true);
try {
await adjust(selected, "extend", extendDays);
await adjust(selected, "extend", values.days);
message.success("订阅已延长");
setExtendDays(null);
setSelected(null);
form.resetFields();
} catch (error) {
message.error(error instanceof Error ? error.message : "延长订阅失败");
} finally {
setSaving(false);
}
@@ -94,7 +88,8 @@ export function SubscriptionsResource() {
try {
await adjust(item, "revoke", undefined);
message.success("订阅已撤销");
setRevokeTarget(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "撤销订阅失败");
} finally {
setRevokingId(null);
}
@@ -146,7 +141,6 @@ export function SubscriptionsResource() {
disabled={item.status !== "active"}
onClick={() => {
setSelected(item);
setExtendDays(null);
form.setFieldsValue({ days: 30 });
}}
>
@@ -159,7 +153,7 @@ export function SubscriptionsResource() {
danger
disabled={item.status !== "active"}
loading={revokingId === item.id}
onClick={() => setRevokeTarget(item)}
onClick={() => void revoke(item)}
>
</Button>
@@ -179,9 +173,10 @@ export function SubscriptionsResource() {
/>
<Modal
title="人工延长订阅"
open={Boolean(selected && extendDays === null)}
okText="继续"
open={Boolean(selected)}
okText="确认延长"
cancelText="取消"
confirmLoading={saving}
onOk={() => form.submit()}
onCancel={() => setSelected(null)}
destroyOnHidden
@@ -189,7 +184,7 @@ export function SubscriptionsResource() {
<Form<AdjustmentForm>
form={form}
layout="vertical"
onFinish={prepareExtend}
onFinish={extend}
>
<Form.Item name="days" label="延长天数" rules={[{ required: true }]}>
<InputNumber
@@ -201,23 +196,6 @@ export function SubscriptionsResource() {
</Form.Item>
</Form>
</Modal>
<ConfirmActionModal
open={Boolean(selected && extendDays !== null)}
title="延长订阅权益"
okText="确认延长"
confirmLoading={saving}
onCancel={() => setExtendDays(null)}
onConfirm={extend}
/>
<ConfirmActionModal
open={Boolean(revokeTarget)}
title="撤销订阅权益"
okText="确认撤销"
danger
confirmLoading={Boolean(revokingId)}
onCancel={() => setRevokeTarget(null)}
onConfirm={() => revoke(revokeTarget!)}
/>
</List>
);
}
@@ -248,17 +226,18 @@ export function OrdersResource() {
const { data: identity } = useGetIdentity<AdminIdentity>();
const invalidate = useInvalidate();
const [target, setTarget] = useState<{
order: Order;
orderId: string;
action: "retry_grant" | "compensate" | "record_refund";
} | null>(null);
const [saving, setSaving] = useState(false);
const canAdjust = Boolean(
identity?.permissions.includes("billing.adjustments.write"),
);
async function adjust() {
if (!target) return;
setSaving(true);
async function adjust(
order: Order,
action: "retry_grant" | "compensate" | "record_refund",
) {
setTarget({ orderId: order.id, action });
try {
const result = await adminRequestJson<{
data: { actionSuccess: boolean };
@@ -266,26 +245,25 @@ export function OrdersResource() {
method: "POST",
headers: { "x-request-id": crypto.randomUUID() },
body: JSON.stringify({
id: target.order.id,
action: target.action,
expectedVersion: target.order.adjustmentVersion,
id: order.id,
action,
expectedVersion: order.adjustmentVersion,
}),
});
await invalidate({ resource: "orders", invalidates: ["list"] });
if (target.action === "retry_grant" && !result.data.actionSuccess) {
if (action === "retry_grant" && !result.data.actionSuccess) {
message.warning("已执行重试,但权益发放仍失败,请查看最新错误");
} else {
message.success(
target.action === "record_refund"
action === "record_refund"
? "已记录账务全额退款状态"
: "订单权益操作已完成",
);
}
setTarget(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "订单操作失败");
} finally {
setSaving(false);
setTarget(null);
}
}
@@ -369,7 +347,8 @@ export function OrdersResource() {
<Button
size="small"
disabled={item.grantStatus !== "failed"}
onClick={() => setTarget({ order: item, action: "retry_grant" })}
loading={target?.orderId === item.id && target.action === "retry_grant"}
onClick={() => void adjust(item, "retry_grant")}
>
</Button>
@@ -378,7 +357,8 @@ export function OrdersResource() {
disabled={
item.grantStatus !== "failed" || item.grantType !== "credits"
}
onClick={() => setTarget({ order: item, action: "compensate" })}
loading={target?.orderId === item.id && target.action === "compensate"}
onClick={() => void adjust(item, "compensate")}
>
</Button>
@@ -390,7 +370,8 @@ export function OrdersResource() {
item.paidAt == null ||
item.status === "refunded"
}
onClick={() => setTarget({ order: item, action: "record_refund" })}
loading={target?.orderId === item.id && target.action === "record_refund"}
onClick={() => void adjust(item, "record_refund")}
>
退
</Button>
@@ -398,14 +379,11 @@ export function OrdersResource() {
) : null,
},
];
const modalTitle =
target?.action === "retry_grant"
? "重试失败的权益发放"
: target?.action === "compensate"
? "人工补偿失败的积分权益"
: "仅记录账务全额退款(不会调用支付网关)";
return (
<>
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
退
</Typography.Paragraph>
<ResourceTable<Order>
resource="orders"
title="支付订单与权益发放"
@@ -419,15 +397,6 @@ export function OrdersResource() {
"recorded",
].map((value) => ({ value, label: value }))}
/>
<ConfirmActionModal
open={Boolean(target)}
title={modalTitle}
okText="确认执行"
danger={target?.action === "record_refund"}
confirmLoading={saving}
onCancel={() => setTarget(null)}
onConfirm={adjust}
/>
</>
);
}
@@ -22,7 +22,6 @@ import {
import dayjs from "dayjs";
import { useState } from "react";
import { ConfirmActionModal } from "@/components/admin/confirm-action-modal";
import {
formatAdminDate,
ResourceTable,
@@ -72,52 +71,54 @@ export default function CodesPage() {
const { mutateAsync: updateCode, mutation: updateMutation } =
useUpdate<CodeRecord>();
const [createOpen, setCreateOpen] = useState(false);
const [pendingCreate, setPendingCreate] = useState<CreateValues | null>(null);
const [editRecord, setEditRecord] = useState<CodeRecord | null>(null);
const [pendingEdit, setPendingEdit] = useState<EditValues | null>(null);
const [generated, setGenerated] = useState<CodeRecord[]>([]);
const [revokeRecord, setRevokeRecord] = useState<CodeRecord | null>(null);
const [revoking, setRevoking] = useState(false);
const [revokingId, setRevokingId] = useState<string | null>(null);
const [createForm] = Form.useForm<CreateValues>();
const [editForm] = Form.useForm<EditValues>();
const writable = Boolean(
identity?.permissions.includes("billing.adjustments.write"),
);
const writable = Boolean(identity?.permissions.includes("admin.access"));
async function submitCreate() {
if (!pendingCreate) return;
const result = await createCodes({
resource: "codes",
values: {
credits: pendingCreate.credits,
count: pendingCreate.count,
expiresAt: pendingCreate.expiresAt?.toISOString() ?? null,
note: pendingCreate.note?.trim() || null,
},
successNotification: false,
});
setGenerated(result.data.generated);
setPendingCreate(null);
setCreateOpen(false);
createForm.resetFields();
async function submitCreate(values: CreateValues) {
try {
const result = await createCodes({
resource: "codes",
values: {
credits: values.credits,
count: values.count,
expiresAt: values.expiresAt?.toISOString() ?? null,
note: values.note?.trim() || null,
},
successNotification: false,
});
setGenerated(result.data.generated);
setCreateOpen(false);
createForm.resetFields();
} catch (error) {
message.error(error instanceof Error ? error.message : "生成兑换码失败");
}
}
async function submitEdit() {
if (!editRecord || !pendingEdit) return;
await updateCode({
resource: "codes",
id: editRecord.id,
values: {
note: pendingEdit.note?.trim() || null,
expiresAt: pendingEdit.expiresAt?.toISOString() ?? null,
},
});
setPendingEdit(null);
setEditRecord(null);
async function submitEdit(values: EditValues) {
if (!editRecord) return;
try {
await updateCode({
resource: "codes",
id: editRecord.id,
values: {
note: values.note?.trim() || null,
expiresAt: values.expiresAt?.toISOString() ?? null,
},
successNotification: false,
});
setEditRecord(null);
message.success("兑换码已保存");
} catch (error) {
message.error(error instanceof Error ? error.message : "保存兑换码失败");
}
}
async function revoke(record: CodeRecord) {
setRevoking(true);
setRevokingId(record.id);
try {
await adminRequestJson(`/api/admin/codes/${record.id}`, {
method: "DELETE",
@@ -125,11 +126,10 @@ export default function CodesPage() {
});
await invalidate({ resource: "codes", invalidates: ["list"] });
message.success("兑换码已撤销");
setRevokeRecord(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "撤销失败");
} finally {
setRevoking(false);
setRevokingId(null);
}
}
@@ -175,7 +175,6 @@ export default function CodesPage() {
<Button
size="small"
onClick={() => {
setPendingEdit(null);
setEditRecord(record);
editForm.setFieldsValue({
note: record.note ?? undefined,
@@ -188,8 +187,8 @@ export default function CodesPage() {
<Button
danger
size="small"
loading={revoking && revokeRecord?.id === record.id}
onClick={() => setRevokeRecord(record)}
loading={revokingId === record.id}
onClick={() => void revoke(record)}
>
</Button>
@@ -216,10 +215,7 @@ export default function CodesPage() {
writable ? (
<Button
type="primary"
onClick={() => {
setPendingCreate(null);
setCreateOpen(true);
}}
onClick={() => setCreateOpen(true)}
>
</Button>
@@ -230,10 +226,7 @@ export default function CodesPage() {
<Modal
title="批量生成兑换码"
open={createOpen}
onCancel={() => {
setPendingCreate(null);
setCreateOpen(false);
}}
onCancel={() => setCreateOpen(false)}
footer={null}
destroyOnHidden
>
@@ -241,7 +234,7 @@ export default function CodesPage() {
form={createForm}
layout="vertical"
initialValues={{ credits: 10, count: 1 }}
onFinish={setPendingCreate}
onFinish={(values) => void submitCreate(values)}
>
<Form.Item
name="credits"
@@ -289,14 +282,15 @@ export default function CodesPage() {
<Modal
title="编辑未兑换码"
open={Boolean(editRecord)}
onCancel={() => {
setPendingEdit(null);
setEditRecord(null);
}}
onCancel={() => setEditRecord(null)}
footer={null}
destroyOnHidden
>
<Form form={editForm} layout="vertical" onFinish={setPendingEdit}>
<Form
form={editForm}
layout="vertical"
onFinish={(values) => void submitEdit(values)}
>
<Form.Item name="expiresAt" label="到期时间">
<DatePicker showTime style={{ width: "100%" }} />
</Form.Item>
@@ -313,31 +307,6 @@ export default function CodesPage() {
</Button>
</Form>
</Modal>
<ConfirmActionModal
open={Boolean(pendingCreate)}
title="生成兑换码"
okText="确认生成"
confirmLoading={createMutation.isPending}
onCancel={() => setPendingCreate(null)}
onConfirm={submitCreate}
/>
<ConfirmActionModal
open={Boolean(pendingEdit)}
title="编辑未兑换码"
okText="确认保存"
confirmLoading={updateMutation.isPending}
onCancel={() => setPendingEdit(null)}
onConfirm={submitEdit}
/>
<ConfirmActionModal
open={Boolean(revokeRecord)}
title="撤销兑换码"
okText="确认撤销"
danger
confirmLoading={revoking}
onCancel={() => setRevokeRecord(null)}
onConfirm={() => revoke(revokeRecord!)}
/>
</>
);
}
@@ -1,59 +0,0 @@
"use client";
import { Alert, Modal, Typography } from "antd";
import { useState } from "react";
type ConfirmActionModalProps = {
open: boolean;
title: string;
okText: string;
danger?: boolean;
confirmLoading?: boolean;
onCancel: () => void;
onConfirm: () => Promise<void> | void;
};
export function ConfirmActionModal({
open,
title,
okText,
danger = false,
confirmLoading = false,
onCancel,
onConfirm,
}: ConfirmActionModalProps) {
const [actionLoading, setActionLoading] = useState(false);
const [actionError, setActionError] = useState<string>();
async function confirm() {
setActionError(undefined);
setActionLoading(true);
try {
await onConfirm();
} catch (error) {
setActionError(error instanceof Error ? error.message : "操作失败,请稍后再试");
} finally {
setActionLoading(false);
}
}
return (
<Modal
open={open}
title={title}
okText={okText}
cancelText="取消"
okButtonProps={{ danger }}
confirmLoading={confirmLoading || actionLoading}
onCancel={onCancel}
onOk={() => void confirm()}
afterOpenChange={() => setActionError(undefined)}
destroyOnHidden
>
<Typography.Paragraph type="secondary">
ID
</Typography.Paragraph>
{actionError ? <Alert type="error" showIcon message={actionError} /> : null}
</Modal>
);
}
@@ -8,7 +8,6 @@ import { App, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Ta
import { useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { ConfirmActionModal } from "./confirm-action-modal";
import { formatAdminDate } from "./resource-table";
const { Text } = Typography;
@@ -51,7 +50,6 @@ export default function FeatureFlagsManagement() {
const [open, setOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [publishingId, setPublishingId] = useState<string | null>(null);
const [publishTarget, setPublishTarget] = useState<FeatureFlag | null>(null);
const canWrite = Boolean(identity?.permissions.includes("ops.flags.write"));
function edit(item?: FeatureFlag) {
@@ -110,7 +108,8 @@ export default function FeatureFlagsManagement() {
});
message.success("功能开关已发布");
await table.tableQuery.refetch();
setPublishTarget(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "发布失败");
} finally {
setPublishingId(null);
}
@@ -125,7 +124,7 @@ export default function FeatureFlagsManagement() {
{
title: "操作",
fixed: "right",
render: (_, item) => <Space>{canWrite && <Button type="link" onClick={() => edit(item)}>稿</Button>}{canWrite && item.status !== "published" && <Button type="link" loading={publishingId === item.id} onClick={() => setPublishTarget(item)}></Button>}</Space>,
render: (_, item) => <Space>{canWrite && <Button type="link" onClick={() => edit(item)}>稿</Button>}{canWrite && item.status !== "published" && <Button type="link" loading={publishingId === item.id} onClick={() => void publish(item)}></Button>}</Space>,
},
];
@@ -152,13 +151,5 @@ export default function FeatureFlagsManagement() {
<Form.Item name="configJson" label="配置 JSON" rules={[{ required: true }]}><Input.TextArea rows={6} spellCheck={false} /></Form.Item>
</Form>
</Modal>
<ConfirmActionModal
open={Boolean(publishTarget)}
title={`发布功能开关${publishTarget ? `${publishTarget.flagKey}` : ""}`}
okText="确认发布"
confirmLoading={Boolean(publishingId)}
onCancel={() => setPublishTarget(null)}
onConfirm={() => publish(publishTarget!)}
/>
</List>;
}
@@ -78,7 +78,6 @@ type ModelsPayload = { data: ModelVersion[]; total: number; providers: Provider[
type DiscoveredModel = { id: string; label?: string };
type DiscoveredModelsPayload = { data: DiscoveredModel[] };
type ModelFilters = { q?: string; status?: string };
type VersionAction = { action: "publish" | "rollback"; model: ModelVersion };
const providerTypeLabels: Record<ProviderType, string> = {
openai: "OpenAI 官方",
@@ -114,7 +113,6 @@ export default function ModelManagement() {
const [discovering, setDiscovering] = useState(false);
const [discoveredModels, setDiscoveredModels] = useState<DiscoveredModel[]>([]);
const [actingId, setActingId] = useState<string | null>(null);
const [versionAction, setVersionAction] = useState<VersionAction | null>(null);
const [filters, setFilters] = useState<ModelFilters>({});
const canWrite = Boolean(identity?.permissions.includes("models.write"));
const canTest = Boolean(identity?.permissions.includes("models.test"));
@@ -271,15 +269,12 @@ export default function ModelManagement() {
}
}
async function submitVersionAction() {
if (!versionAction) return;
const { action, model } = versionAction;
async function runVersionAction(action: "publish" | "rollback", model: ModelVersion) {
try {
await act(action === "publish"
? { action, versionId: model.id }
: { action, configId: model.configId, targetVersion: model.version },
action === "publish" ? "模型已发布" : "模型已回滚", model.id);
setVersionAction(null);
} catch (error) {
message.error(error instanceof Error ? error.message : action === "publish" ? "发布失败" : "回滚失败");
}
@@ -318,8 +313,8 @@ export default function ModelManagement() {
render: (_, item) => <Space>
{canWrite && <Button type="link" onClick={() => openModel(item)}>稿</Button>}
{canTest && <Button type="link" loading={actingId === item.id} onClick={() => void testVersion(item)}></Button>}
{canPublish && item.status !== "published" && <Button type="link" disabled={item.isDefault && !item.enabled} title={item.isDefault && !item.enabled ? "默认模型必须先启用" : "发布前必须先通过此版本的短期连接测试"} loading={actingId === item.id} onClick={() => setVersionAction({ action: "publish", model: item })}></Button>}
{canRollback && item.status !== "draft" && <Button type="link" danger loading={actingId === item.id} onClick={() => setVersionAction({ action: "rollback", model: item })}></Button>}
{canPublish && item.status !== "published" && <Button type="link" disabled={item.isDefault && !item.enabled} title={item.isDefault && !item.enabled ? "默认模型必须先启用" : "发布前必须先通过此版本的短期连接测试"} loading={actingId === item.id} onClick={() => void runVersionAction("publish", item)}></Button>}
{canRollback && item.status !== "draft" && <Button type="link" danger loading={actingId === item.id} onClick={() => void runVersionAction("rollback", item)}></Button>}
</Space>,
},
];
@@ -412,21 +407,5 @@ export default function ModelManagement() {
</Row>
</Form>
</Modal>
<Modal
open={Boolean(versionAction)}
title={versionAction?.action === "rollback" ? `回滚到 v${versionAction.model.version}` : "发布模型版本"}
okText={versionAction?.action === "rollback" ? "确认回滚" : "确认发布"}
okButtonProps={{ danger: versionAction?.action === "rollback" }}
confirmLoading={Boolean(actingId)}
onCancel={() => setVersionAction(null)}
onOk={() => void submitVersionAction()}
destroyOnHidden
>
<Text>
{versionAction?.action === "rollback"
? "确认将此历史版本恢复为新的已发布版本?"
: "确认发布此模型版本?"}
</Text>
</Modal>
</List>;
}
@@ -2,7 +2,7 @@
import { PlusOutlined } from "@ant-design/icons";
import { List } from "@refinedev/antd";
import { Alert, App, Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Row, Col, Space, Switch, Table, Typography, type TableColumnsType } from "antd";
import { Alert, App, Button, Card, Form, Input, InputNumber, Modal, Row, Col, Space, Switch, Table, Typography, type TableColumnsType } from "antd";
import { useCallback, useEffect, useState } from "react";
const { Text } = Typography;
@@ -135,7 +135,7 @@ export function PackageManagement() {
{ title: "点数", dataIndex: "credits", align: "right" },
{ title: "排序", dataIndex: "sortOrder", align: "right" },
{ title: "状态", dataIndex: "enabled", render: (enabled) => <Text type={enabled ? undefined : "secondary"}>{enabled ? "启用" : "停用"}</Text> },
{ title: "操作", key: "actions", fixed: "right", render: (_, item) => <Space><Button type="link" onClick={() => openEditModal(item)}></Button>{item.enabled && <Popconfirm title="停用此套餐?" description="停用后用户将无法继续购买。" okText="停用" cancelText="取消" onConfirm={() => disablePackage(item.id)}><Button type="link" danger loading={disablingId === item.id}></Button></Popconfirm>}</Space> },
{ title: "操作", key: "actions", fixed: "right", render: (_, item) => <Space><Button type="link" onClick={() => openEditModal(item)}></Button>{item.enabled && <Button type="link" danger loading={disablingId === item.id} onClick={() => void disablePackage(item.id)}></Button>}</Space> },
];
return (
@@ -25,7 +25,6 @@ import {
import type { Dayjs } from "dayjs";
import { useCallback, useEffect, useState } from "react";
import { ConfirmActionModal } from "@/components/admin/confirm-action-modal";
import type { AdminIdentity } from "@/lib/admin/providers";
const { Text } = Typography;
@@ -109,7 +108,6 @@ export default function PaymentManagement() {
const [epaySaving, setEpaySaving] = useState(false);
const [epayTesting, setEpayTesting] = useState(false);
const [epayError, setEpayError] = useState("");
const [pendingEpaySettings, setPendingEpaySettings] = useState<EpaySettingsForm | null>(null);
const canAdjustBilling = Boolean(identity?.permissions.includes("billing.adjustments.write"));
const loadPayments = useCallback(async () => {
@@ -175,23 +173,19 @@ export default function PaymentManagement() {
}
}
function prepareEpaySettings(values: EpaySettingsForm) {
setPendingEpaySettings(values);
}
async function saveEpaySettings() {
if (!pendingEpaySettings) return;
async function saveEpaySettings(values: EpaySettingsForm) {
setEpaySaving(true);
try {
await responsePayload(await fetch("/api/admin/epay-settings", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...pendingEpaySettings, newKey: pendingEpaySettings.newKey || undefined }),
body: JSON.stringify({ ...values, newKey: values.newKey || undefined }),
}));
epayForm.setFieldValue("newKey", "");
message.success("Z-Pay(易支付)配置已保存");
await loadEpaySettings();
setPendingEpaySettings(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "保存易支付配置失败");
} finally {
setEpaySaving(false);
}
@@ -237,7 +231,7 @@ export default function PaymentManagement() {
{epaySettings && <Text type="secondary">{{ database: "数据库", environment: "环境变量", unconfigured: "未配置" }[epaySettings.source]} · {epaySettings.complete ? "配置完整" : "配置不完整"} · {epaySettings.keyConfigured ? "密钥已配置" : "密钥未配置"} · {epaySettings.chatEnabled ? "对话支付开放" : "对话支付关闭"}</Text>}
{epayError && <Alert type="error" showIcon message="易支付配置读取失败" description={epayError} action={<Button size="small" onClick={() => void loadEpaySettings()}></Button>} />}
<Alert type="info" showIcon message="商户密钥不会回显" description="密钥输入框始终为空;更新现有数据库配置时留空会保留原密钥。首次从环境变量迁移到数据库时必须重新输入密钥。" />
<Form<EpaySettingsForm> form={epayForm} layout="vertical" onFinish={prepareEpaySettings} requiredMark="optional">
<Form<EpaySettingsForm> form={epayForm} layout="vertical" onFinish={saveEpaySettings} requiredMark="optional">
<Row gutter={16}>
<Col xs={24} lg={12}><Form.Item name="gatewayUrl" label="网关地址" rules={[{ required: true, message: "请输入网关地址" }, { type: "url", message: "请输入有效 URL" }]}><Input placeholder="https://pay.example.com" /></Form.Item></Col>
<Col xs={24} lg={12}><Form.Item name="pid" label="商户 ID" rules={[{ required: true, message: "请输入商户 ID" }, { max: 200 }]}><Input /></Form.Item></Col>
@@ -277,14 +271,6 @@ export default function PaymentManagement() {
</Space>
</Card>
</Space>
<ConfirmActionModal
open={Boolean(pendingEpaySettings)}
title="保存易支付设置"
okText="确认保存"
confirmLoading={epaySaving}
onCancel={() => setPendingEpaySettings(null)}
onConfirm={saveEpaySettings}
/>
</List>
);
}
@@ -23,7 +23,6 @@ import {
import { useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { ConfirmActionModal } from "./confirm-action-modal";
import { formatAdminDate } from "./resource-table";
const { Text } = Typography;
@@ -91,7 +90,6 @@ type ProductForm = Omit<Product, "id" | "version" | "priceCents" | "status" | "e
entitlementsJson: string;
};
type PendingProductSave = Record<string, unknown>;
type ProductFilters = { q?: string; status?: string };
const defaultEntitlements: Entitlement[] = [{
@@ -121,8 +119,6 @@ export default function ProductManagement() {
const [open, setOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [publishingId, setPublishingId] = useState<string | null>(null);
const [publishTarget, setPublishTarget] = useState<Product | null>(null);
const [pendingSave, setPendingSave] = useState<PendingProductSave | null>(null);
const canWrite = Boolean(identity?.permissions.includes("billing.products.write"));
const canPublish = Boolean(identity?.permissions.includes("billing.products.publish"));
@@ -159,7 +155,7 @@ export default function ProductManagement() {
setOpen(true);
}
function prepareSave(values: ProductForm) {
async function save(values: ProductForm) {
let entitlements: unknown;
try {
entitlements = JSON.parse(values.entitlementsJson);
@@ -167,37 +163,33 @@ export default function ProductManagement() {
message.error("权益 JSON 格式不正确");
return;
}
setPendingSave({
action: "save",
id: editing?.id ?? null,
code: values.code.trim(),
name: values.name.trim(),
description: values.description?.trim() ?? "",
productType: values.productType,
billingPeriod: values.billingPeriod,
intervalCount: values.intervalCount,
priceCents: Math.round(values.priceYuan * 100),
currency: values.currency.toUpperCase(),
enabled: values.enabled,
sortOrder: values.sortOrder,
oneTimePerUser: values.oneTimePerUser,
entitlements,
});
}
async function save() {
if (!pendingSave) return;
setSaving(true);
try {
await adminRequestJson("/api/admin/products", {
method: "POST",
body: JSON.stringify(pendingSave),
body: JSON.stringify({
action: "save",
id: editing?.id ?? null,
code: values.code.trim(),
name: values.name.trim(),
description: values.description?.trim() ?? "",
productType: values.productType,
billingPeriod: values.billingPeriod,
intervalCount: values.intervalCount,
priceCents: Math.round(values.priceYuan * 100),
currency: values.currency.toUpperCase(),
enabled: values.enabled,
sortOrder: values.sortOrder,
oneTimePerUser: values.oneTimePerUser,
entitlements,
}),
});
message.success("商品草稿已保存");
setPendingSave(null);
setOpen(false);
form.resetFields();
await table.tableQuery.refetch();
} catch (error) {
message.error(error instanceof Error ? error.message : "保存商品草稿失败");
} finally {
setSaving(false);
}
@@ -212,7 +204,8 @@ export default function ProductManagement() {
});
message.success("商品已发布");
await table.tableQuery.refetch();
setPublishTarget(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "发布商品失败");
} finally {
setPublishingId(null);
}
@@ -235,7 +228,7 @@ export default function ProductManagement() {
fixed: "right",
render: (_, item) => <Space>
{canWrite && <Button type="link" onClick={() => openProduct(item)}>稿</Button>}
{canPublish && item.status !== "published" && <Button type="link" loading={publishingId === item.id} onClick={() => setPublishTarget(item)}></Button>}
{canPublish && item.status !== "published" && <Button type="link" loading={publishingId === item.id} onClick={() => void publish(item)}></Button>}
</Space>,
},
];
@@ -256,7 +249,7 @@ export default function ProductManagement() {
<Table {...table.tableProps} columns={columns} rowKey="id" scroll={{ x: "max-content" }} />
</Space>
<Modal title={editing ? `编辑 ${editing.name}` : "新建商品"} open={open} width={860} confirmLoading={saving} okText="保存草稿" cancelText="取消" onOk={() => form.submit()} onCancel={() => setOpen(false)} destroyOnHidden>
<Form<ProductForm> form={form} layout="vertical" onFinish={prepareSave} requiredMark="optional">
<Form<ProductForm> form={form} layout="vertical" onFinish={save} requiredMark="optional">
<Row gutter={16}>
<Col xs={24} md={8}><Form.Item name="code" label="商品代码" rules={[{ required: true }, { pattern: /^[a-z][a-z0-9_]{1,79}$/ }]}><Input disabled={Boolean(editing)} /></Form.Item></Col>
<Col xs={24} md={8}><Form.Item name="name" label="名称" rules={[{ required: true }, { max: 80 }]}><Input /></Form.Item></Col>
@@ -277,21 +270,5 @@ export default function ProductManagement() {
</Row>
</Form>
</Modal>
<ConfirmActionModal
open={Boolean(pendingSave)}
title="保存商品草稿"
okText="确认保存"
confirmLoading={saving}
onCancel={() => setPendingSave(null)}
onConfirm={save}
/>
<ConfirmActionModal
open={Boolean(publishTarget)}
title={`发布商品${publishTarget ? `${publishTarget.name}` : ""}`}
okText="确认发布"
confirmLoading={Boolean(publishingId)}
onCancel={() => setPublishTarget(null)}
onConfirm={() => publish(publishTarget!)}
/>
</List>;
}
@@ -4,8 +4,6 @@ import { useGetIdentity } from "@refinedev/core";
import { App, Button, Space, Typography, type TableColumnsType } from "antd";
import { useState } from "react";
import { ConfirmActionModal } from "./confirm-action-modal";
import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
@@ -38,7 +36,6 @@ export default function UsersPage() {
const { data: identity } = useGetIdentity<AdminIdentity>();
const [revealed, setRevealed] = useState<Record<string, RevealedBirthData>>({});
const [revealingId, setRevealingId] = useState<string | null>(null);
const [resetTarget, setResetTarget] = useState<UserRecord | null>(null);
const [resetting, setResetting] = useState(false);
const canReset = Boolean(identity?.permissions.includes("admin.users.manage_roles"));
const canReveal = Boolean(identity?.permissions.includes("admin.customers.birth_data.read"));
@@ -58,8 +55,7 @@ export default function UsersPage() {
}
}
async function resetAccount() {
if (!resetTarget) return;
async function resetAccount(user: UserRecord) {
setResetting(true);
try {
await adminRequestJson<{ data: { credits: number } }>(
@@ -67,21 +63,19 @@ export default function UsersPage() {
{
method: "POST",
body: JSON.stringify({
userId: resetTarget.id,
userId: user.id,
confirmation: "RESET",
}),
},
);
message.success(`已重置 ${resetTarget.email},登录身份、管理员角色和积分保持不变`);
message.success(`已重置 ${user.email},登录身份、管理员角色和积分保持不变`);
setRevealed((current) => {
const next = { ...current };
delete next[resetTarget.id];
delete next[user.id];
return next;
});
setResetTarget(null);
} catch (error) {
message.error(error instanceof Error ? error.message : "重置账号失败");
throw error;
} finally {
setResetting(false);
}
@@ -118,22 +112,11 @@ export default function UsersPage() {
{ title: "注册时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate },
...(canReset ? [{
title: "操作",
render: (_: unknown, item: UserRecord) => <Button danger size="small" onClick={() => setResetTarget(item)}>
render: (_: unknown, item: UserRecord) => <Button danger size="small" loading={resetting} onClick={() => void resetAccount(item)}>
</Button>,
}] : []),
];
return <>
<ResourceTable<UserRecord> resource="customers" title="用户资料(列表始终脱敏)" columns={columns} />
<ConfirmActionModal
open={Boolean(resetTarget)}
title={`确认重置 ${resetTarget?.email ?? "该账号"} 的资料与会话?登录身份、管理员角色、积分及账务审计记录会保留。`}
okText="确认重置"
danger
confirmLoading={resetting}
onCancel={() => setResetTarget(null)}
onConfirm={resetAccount}
/>
</>;
return <ResourceTable<UserRecord> resource="customers" title="用户资料(列表始终脱敏)" columns={columns} />;
}