Files
infinite-canvas/web/src/app/(admin)/admin/credit-logs/page.tsx
T
HouYunFei 2934b1d0bc refactor(ai): 重构AI接口算力点扣费逻辑并添加失败返还机制
- 移除预检查算力点步骤,改为请求前预扣算力点
- 添加请求失败时自动返还算力点的功能
- 修改copyAIResponse函数参数以支持失败回调处理
- 在响应状态异常时也执行算力点返还操作
- 从auth服务中移除EnsureUserCredits方法
- 新增RefundUserCredits方法用于返还用户算力点
- 添加AI_REFUND算力点日志类型常量
- 更新算力点日志类型说明及展示标签
2026-05-25 15:23:51 +08:00

222 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
import { ProTable, type ProColumns } from "@ant-design/pro-components";
import { Button, Card, Col, Form, Input, InputNumber, Modal, Row, Space, Tag, Tooltip, Typography } from "antd";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import type { AdminCreditLog } from "@/services/api/admin";
import { useAdminCreditLogs } from "./use-admin-credit-logs";
type CreditLogFormValues = Partial<AdminCreditLog>;
const creditLogTypeLabels: Record<string, string> = {
admin_adjust: "后台调整",
ai_consume: "模型消费",
ai_refund: "失败返还",
};
export default function AdminCreditLogsPage() {
const { logs, keyword, page, pageSize, total, isLoading, searchLogs, changePage, changePageSize, resetFilters, refreshLogs, saveLog: saveAdminLog, deleteLog } = useAdminCreditLogs();
const [form] = Form.useForm<CreditLogFormValues>();
const [keywordText, setKeywordText] = useState(keyword);
const [editingLog, setEditingLog] = useState<Partial<AdminCreditLog> | null>(null);
const [deletingLog, setDeletingLog] = useState<AdminCreditLog | null>(null);
useEffect(() => setKeywordText(keyword), [keyword]);
useEffect(() => {
if (editingLog) form.setFieldsValue({ type: "admin_adjust", amount: 0, balance: 0, ...editingLog });
}, [editingLog, form]);
const saveLog = async () => {
const value = await form.validateFields();
await saveAdminLog({ ...editingLog, ...value });
setEditingLog(null);
};
const columns: ProColumns<AdminCreditLog>[] = [
{
title: "用户 ID",
dataIndex: "userId",
width: 220,
render: (_, item) => <Typography.Text copyable>{item.userId}</Typography.Text>,
},
{
title: "类型",
dataIndex: "type",
width: 140,
render: (_, item) => <Tag>{creditLogTypeLabels[item.type] || item.type || "-"}</Tag>,
},
{
title: "变动",
dataIndex: "amount",
width: 100,
render: (_, item) => <Typography.Text type={item.amount >= 0 ? "success" : "danger"}>{item.amount}</Typography.Text>,
},
{
title: "余额",
dataIndex: "balance",
width: 100,
},
{
title: "备注",
dataIndex: "remark",
ellipsis: true,
render: (_, item) => <Typography.Text type="secondary">{item.remark || "-"}</Typography.Text>,
},
{
title: "创建时间",
dataIndex: "createdAt",
width: 180,
render: (_, item) => <Typography.Text type="secondary">{item.createdAt ? dayjs(item.createdAt).format("YYYY-MM-DD HH:mm:ss") : "-"}</Typography.Text>,
},
{
title: "操作",
key: "actions",
width: 96,
align: "right",
render: (_, item) => (
<Space size={4}>
<Tooltip title="编辑">
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => setEditingLog(item)} />
</Tooltip>
<Tooltip title="删除">
<Button danger type="text" size="small" icon={<DeleteOutlined />} onClick={() => setDeletingLog(item)} />
</Tooltip>
</Space>
),
},
];
return (
<main style={{ padding: 24 }}>
<Space direction="vertical" size={16} style={{ width: "100%" }}>
<Card variant="borderless">
<Form layout="vertical">
<Row gutter={16} align="bottom">
<Col flex="360px">
<Form.Item label="关键词">
<Input.Search value={keywordText} placeholder="搜索用户 ID、类型、备注或关联 ID" allowClear enterButton={<SearchOutlined />} onSearch={() => searchLogs(keywordText)} onChange={(event) => setKeywordText(event.target.value)} />
</Form.Item>
</Col>
<Col flex="none">
<Form.Item>
<Space>
<Button
onClick={() => {
setKeywordText("");
resetFilters();
}}
>
</Button>
<Button type="primary" icon={<ReloadOutlined />} onClick={() => searchLogs(keywordText)}>
</Button>
</Space>
</Form.Item>
</Col>
</Row>
</Form>
</Card>
<ProTable<AdminCreditLog>
rowKey="id"
columns={columns}
dataSource={logs}
loading={isLoading}
search={false}
defaultSize="middle"
tableLayout="fixed"
cardProps={{ variant: "borderless" }}
headerTitle={
<Space>
<Typography.Text strong></Typography.Text>
<Tag>{total} </Tag>
</Space>
}
options={{ density: true, setting: true, reload: () => void refreshLogs() }}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setEditingLog({ type: "admin_adjust", amount: 0, balance: 0 })}>
</Button>,
]}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (value) => `${value}`,
onChange: (nextPage, nextPageSize) => (nextPageSize !== pageSize ? changePageSize(nextPageSize) : changePage(nextPage)),
}}
/>
</Space>
<Modal title={editingLog?.id ? "编辑日志" : "新增日志"} open={Boolean(editingLog)} width={680} onCancel={() => setEditingLog(null)} onOk={() => void saveLog()} okText="保存" cancelText="取消" destroyOnHidden>
<Form form={form} layout="vertical" requiredMark={false}>
<Row gutter={14}>
<Col span={12}>
<Form.Item name="userId" label="用户 ID" rules={[{ required: true, message: "请输入用户 ID" }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="type" label="类型" rules={[{ required: true, message: "请输入类型" }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="amount" label="变动数量" rules={[{ required: true, message: "请输入变动数量" }]}>
<InputNumber precision={0} style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="balance" label="变动后余额" rules={[{ required: true, message: "请输入变动后余额" }]}>
<InputNumber min={0} precision={0} style={{ width: "100%" }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="relatedId" label="关联 ID">
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="createdAt" label="创建时间">
<Input placeholder="不填则新增时自动生成" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={3} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item name="extra" label="扩展信息">
<Input.TextArea rows={3} />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal
title="删除日志"
open={Boolean(deletingLog)}
onCancel={() => setDeletingLog(null)}
onOk={async () => {
if (!deletingLog) return;
await deleteLog(deletingLog.id);
setDeletingLog(null);
}}
okText="删除"
okButtonProps={{ danger: true }}
cancelText="取消"
>
</Modal>
</main>
);
}