fix(admin): remove redundant confirmations and repair code access
This commit is contained in:
@@ -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} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user