From 218cee579e92fcf9bfe435a349250cfe23304547 Mon Sep 17 00:00:00 2001 From: Jesse Date: Tue, 28 Jul 2026 10:04:36 +0800 Subject: [PATCH] fix(admin): preserve capability route contract --- frontend/src/app/admin/codes/page.tsx | 180 ++---------------- frontend/src/app/admin/page.tsx | 5 - frontend/src/components/admin/admin-app.tsx | 8 +- .../admin/audit-logs-resource.tsx} | 0 .../src/components/admin/codes-resource.tsx | 167 ++++++++++++++++ .../admin/consultations-resource.tsx} | 0 .../admin/credit-transactions-resource.tsx} | 0 .../admin/users-resource.tsx} | 0 8 files changed, 191 insertions(+), 169 deletions(-) delete mode 100644 frontend/src/app/admin/page.tsx rename frontend/src/{app/admin/audit-logs/page.tsx => components/admin/audit-logs-resource.tsx} (100%) create mode 100644 frontend/src/components/admin/codes-resource.tsx rename frontend/src/{app/admin/consultations/page.tsx => components/admin/consultations-resource.tsx} (100%) rename frontend/src/{app/admin/credit-transactions/page.tsx => components/admin/credit-transactions-resource.tsx} (100%) rename frontend/src/{app/admin/users/page.tsx => components/admin/users-resource.tsx} (100%) diff --git a/frontend/src/app/admin/codes/page.tsx b/frontend/src/app/admin/codes/page.tsx index da2ef5d8..d195b258 100644 --- a/frontend/src/app/admin/codes/page.tsx +++ b/frontend/src/app/admin/codes/page.tsx @@ -1,167 +1,27 @@ "use client"; -import { useCreate, useDelete, useGetIdentity, usePermissions, useUpdate } from "@refinedev/core"; -import { Button, DatePicker, Form, Input, InputNumber, Modal, Space, Tag, Typography, type TableColumnsType } from "antd"; -import dayjs from "dayjs"; -import { useState } from "react"; +import { useSearchParams } from "next/navigation"; -import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table"; -import type { AdminIdentity } from "@/lib/admin/providers"; +import AuditLogsResource from "@/components/admin/audit-logs-resource"; +import CodesResource from "@/components/admin/codes-resource"; +import ConsultationsResource from "@/components/admin/consultations-resource"; +import CreditTransactionsResource from "@/components/admin/credit-transactions-resource"; +import UsersResource from "@/components/admin/users-resource"; -type CodeRecord = { - id: string; - code?: string; - mask: string; - credits: number; - expiresAt: string | null; - note: string | null; - createdAt: string; - redeemedEmail: string | null; - redeemedAt: string | null; - revokedAt: string | null; - status: "available" | "expired" | "redeemed" | "revoked"; -}; +const resourceComponents = { + codes: CodesResource, + users: UsersResource, + "credit-transactions": CreditTransactionsResource, + consultations: ConsultationsResource, + "audit-logs": AuditLogsResource, +} as const; -type CreateValues = { - credits: number; - count: number; - expiresAt?: ReturnType; - note?: string; -}; -type EditValues = { note?: string; expiresAt?: ReturnType | null }; - -const statusColors: Record = { - available: "green", - expired: "orange", - redeemed: "blue", - revoked: "red", -}; - -export default function CodesPage() { - const { data: role } = usePermissions<"admin" | "viewer">({}); - const { data: identity } = useGetIdentity(); - const { mutate: createCodes, mutation: createMutation } = useCreate<{ id: string; generated: CodeRecord[] }>(); - const { mutate: updateCode, mutation: updateMutation } = useUpdate(); - const { mutate: revokeCode, mutation: revokeMutation } = useDelete(); - const [createOpen, setCreateOpen] = useState(false); - const [editRecord, setEditRecord] = useState(null); - const [generated, setGenerated] = useState([]); - const [createForm] = Form.useForm(); - const [editForm] = Form.useForm(); - const writable = role === "admin"; - - function submitCreate(values: CreateValues) { - createCodes({ - resource: "codes", - values: { - credits: values.credits, - count: values.count, - expiresAt: values.expiresAt?.toISOString() ?? null, - note: values.note?.trim() || null, - }, - successNotification: false, - }, { - onSuccess(result) { - setGenerated(result.data.generated); - setCreateOpen(false); - createForm.resetFields(); - }, - }); - } - - function submitEdit(values: EditValues) { - if (!editRecord) return; - updateCode({ - resource: "codes", - id: editRecord.id, - values: { - note: values.note?.trim() || null, - expiresAt: values.expiresAt?.toISOString() ?? null, - }, - }, { onSuccess: () => setEditRecord(null) }); - } - - function confirmRevoke(record: CodeRecord) { - Modal.confirm({ - title: "撤销此兑换码?", - content: `${record.mask} 撤销后不可兑换,且不能恢复。`, - okText: "确认撤销", - okButtonProps: { danger: true }, - cancelText: "取消", - onOk: () => new Promise((resolve, reject) => { - revokeCode({ resource: "codes", id: record.id }, { - onSuccess: () => resolve(), - onError: () => reject(new Error("撤销失败")), - }); - }), - }); - } - - const columns: TableColumnsType = [ - { title: "兑换码", dataIndex: "mask" }, - { title: "点数", dataIndex: "credits", sorter: true }, - { title: "状态", dataIndex: "status", sorter: true, render: (value) => {value} }, - { title: "到期时间", dataIndex: "expiresAt", sorter: true, render: formatAdminDate }, - { title: "备注", dataIndex: "note", render: (value) => value || "—" }, - { title: "兑换账户", dataIndex: "redeemedEmail", render: (value) => value || "—" }, - { title: "兑换时间", dataIndex: "redeemedAt", render: formatAdminDate }, - { title: "撤销时间", dataIndex: "revokedAt", render: formatAdminDate }, - { title: "创建时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate }, - { - title: "操作", - fixed: "right", - render: (_, record) => writable && record.status !== "redeemed" && record.status !== "revoked" ? ( - - - - - ) : "—", - }, +export default function AdminResourcesPage() { + const requested = useSearchParams().get("resource") ?? "codes"; + const Resource = resourceComponents[ + requested in resourceComponents + ? requested as keyof typeof resourceComponents + : "codes" ]; - - return ( - <> - - resource="codes" - title={`兑换码${identity ? ` · ${identity.email} (${identity.role})` : ""}`} - columns={columns} - statusOptions={[ - { label: "可用", value: "available" }, - { label: "已过期", value: "expired" }, - { label: "已兑换", value: "redeemed" }, - { label: "已撤销", value: "revoked" }, - ]} - extra={writable ? : viewer 只读} - /> - - setCreateOpen(false)} footer={null} destroyOnHidden> -
- - - - - -
-
- - 0} onCancel={() => setGenerated([])} footer={}> - 关闭后无法再次查看完整兑换码,请立即安全保存。 - {generated.map((record) => {record.code})} - - - setEditRecord(null)} footer={null} destroyOnHidden> -
- - - -
-
- - ); + return ; } diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx deleted file mode 100644 index 5b7410aa..00000000 --- a/frontend/src/app/admin/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation"; - -export default function AdminPage() { - redirect("/admin/codes"); -} diff --git a/frontend/src/components/admin/admin-app.tsx b/frontend/src/components/admin/admin-app.tsx index 74edd4e1..0f6620b1 100644 --- a/frontend/src/components/admin/admin-app.tsx +++ b/frontend/src/components/admin/admin-app.tsx @@ -32,10 +32,10 @@ export function AdminApp({ children }: { children: ReactNode }) { notificationProvider={notificationProvider} resources={[ { name: "codes", list: "/admin/codes", meta: { label: "兑换码", icon: } }, - { name: "users", list: "/admin/users", meta: { label: "用户资料", icon: } }, - { name: "credit-transactions", list: "/admin/credit-transactions", meta: { label: "积分流水", icon: } }, - { name: "consultations", list: "/admin/consultations", meta: { label: "咨询请求", icon: } }, - { name: "audit-logs", list: "/admin/audit-logs", meta: { label: "审计日志", icon: } }, + { name: "users", list: "/admin/codes?resource=users", meta: { label: "用户资料", icon: } }, + { name: "credit-transactions", list: "/admin/codes?resource=credit-transactions", meta: { label: "积分流水", icon: } }, + { name: "consultations", list: "/admin/codes?resource=consultations", meta: { label: "咨询请求", icon: } }, + { name: "audit-logs", list: "/admin/codes?resource=audit-logs", meta: { label: "审计日志", icon: } }, ]} options={{ syncWithLocation: true, diff --git a/frontend/src/app/admin/audit-logs/page.tsx b/frontend/src/components/admin/audit-logs-resource.tsx similarity index 100% rename from frontend/src/app/admin/audit-logs/page.tsx rename to frontend/src/components/admin/audit-logs-resource.tsx diff --git a/frontend/src/components/admin/codes-resource.tsx b/frontend/src/components/admin/codes-resource.tsx new file mode 100644 index 00000000..da2ef5d8 --- /dev/null +++ b/frontend/src/components/admin/codes-resource.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useCreate, useDelete, useGetIdentity, usePermissions, useUpdate } from "@refinedev/core"; +import { Button, DatePicker, Form, Input, InputNumber, Modal, Space, Tag, Typography, type TableColumnsType } from "antd"; +import dayjs from "dayjs"; +import { useState } from "react"; + +import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table"; +import type { AdminIdentity } from "@/lib/admin/providers"; + +type CodeRecord = { + id: string; + code?: string; + mask: string; + credits: number; + expiresAt: string | null; + note: string | null; + createdAt: string; + redeemedEmail: string | null; + redeemedAt: string | null; + revokedAt: string | null; + status: "available" | "expired" | "redeemed" | "revoked"; +}; + +type CreateValues = { + credits: number; + count: number; + expiresAt?: ReturnType; + note?: string; +}; +type EditValues = { note?: string; expiresAt?: ReturnType | null }; + +const statusColors: Record = { + available: "green", + expired: "orange", + redeemed: "blue", + revoked: "red", +}; + +export default function CodesPage() { + const { data: role } = usePermissions<"admin" | "viewer">({}); + const { data: identity } = useGetIdentity(); + const { mutate: createCodes, mutation: createMutation } = useCreate<{ id: string; generated: CodeRecord[] }>(); + const { mutate: updateCode, mutation: updateMutation } = useUpdate(); + const { mutate: revokeCode, mutation: revokeMutation } = useDelete(); + const [createOpen, setCreateOpen] = useState(false); + const [editRecord, setEditRecord] = useState(null); + const [generated, setGenerated] = useState([]); + const [createForm] = Form.useForm(); + const [editForm] = Form.useForm(); + const writable = role === "admin"; + + function submitCreate(values: CreateValues) { + createCodes({ + resource: "codes", + values: { + credits: values.credits, + count: values.count, + expiresAt: values.expiresAt?.toISOString() ?? null, + note: values.note?.trim() || null, + }, + successNotification: false, + }, { + onSuccess(result) { + setGenerated(result.data.generated); + setCreateOpen(false); + createForm.resetFields(); + }, + }); + } + + function submitEdit(values: EditValues) { + if (!editRecord) return; + updateCode({ + resource: "codes", + id: editRecord.id, + values: { + note: values.note?.trim() || null, + expiresAt: values.expiresAt?.toISOString() ?? null, + }, + }, { onSuccess: () => setEditRecord(null) }); + } + + function confirmRevoke(record: CodeRecord) { + Modal.confirm({ + title: "撤销此兑换码?", + content: `${record.mask} 撤销后不可兑换,且不能恢复。`, + okText: "确认撤销", + okButtonProps: { danger: true }, + cancelText: "取消", + onOk: () => new Promise((resolve, reject) => { + revokeCode({ resource: "codes", id: record.id }, { + onSuccess: () => resolve(), + onError: () => reject(new Error("撤销失败")), + }); + }), + }); + } + + const columns: TableColumnsType = [ + { title: "兑换码", dataIndex: "mask" }, + { title: "点数", dataIndex: "credits", sorter: true }, + { title: "状态", dataIndex: "status", sorter: true, render: (value) => {value} }, + { title: "到期时间", dataIndex: "expiresAt", sorter: true, render: formatAdminDate }, + { title: "备注", dataIndex: "note", render: (value) => value || "—" }, + { title: "兑换账户", dataIndex: "redeemedEmail", render: (value) => value || "—" }, + { title: "兑换时间", dataIndex: "redeemedAt", render: formatAdminDate }, + { title: "撤销时间", dataIndex: "revokedAt", render: formatAdminDate }, + { title: "创建时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate }, + { + title: "操作", + fixed: "right", + render: (_, record) => writable && record.status !== "redeemed" && record.status !== "revoked" ? ( + + + + + ) : "—", + }, + ]; + + return ( + <> + + resource="codes" + title={`兑换码${identity ? ` · ${identity.email} (${identity.role})` : ""}`} + columns={columns} + statusOptions={[ + { label: "可用", value: "available" }, + { label: "已过期", value: "expired" }, + { label: "已兑换", value: "redeemed" }, + { label: "已撤销", value: "revoked" }, + ]} + extra={writable ? : viewer 只读} + /> + + setCreateOpen(false)} footer={null} destroyOnHidden> +
+ + + + + +
+
+ + 0} onCancel={() => setGenerated([])} footer={}> + 关闭后无法再次查看完整兑换码,请立即安全保存。 + {generated.map((record) => {record.code})} + + + setEditRecord(null)} footer={null} destroyOnHidden> +
+ + + +
+
+ + ); +} diff --git a/frontend/src/app/admin/consultations/page.tsx b/frontend/src/components/admin/consultations-resource.tsx similarity index 100% rename from frontend/src/app/admin/consultations/page.tsx rename to frontend/src/components/admin/consultations-resource.tsx diff --git a/frontend/src/app/admin/credit-transactions/page.tsx b/frontend/src/components/admin/credit-transactions-resource.tsx similarity index 100% rename from frontend/src/app/admin/credit-transactions/page.tsx rename to frontend/src/components/admin/credit-transactions-resource.tsx diff --git a/frontend/src/app/admin/users/page.tsx b/frontend/src/components/admin/users-resource.tsx similarity index 100% rename from frontend/src/app/admin/users/page.tsx rename to frontend/src/components/admin/users-resource.tsx