feat(admin): 添加管理员用户算力点管理和日志功能
- 在管理员API中新增用户算力点调整功能和信用日志相关接口 - 实现后端用户算力点调整逻辑和信用日志的增删改查操作 - 在管理员界面用户管理页面添加算力点调整功能 - 新增独立的算力点日志管理页面,支持日志查看和编辑 - 添加算力点日志相关的数据模型和服务函数 - 更新数据库文档说明第三方资料存储结构 - 添加dayjs依赖用于时间格式化显示
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"copy-to-clipboard": "^4.0.2",
|
||||
"dayjs": "^1.11.20",
|
||||
"localforage": "^1.10.0",
|
||||
"lucide-react": "^1.16.0",
|
||||
"motion": "^12.38.0",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"copy-to-clipboard": "^4.0.2",
|
||||
"dayjs": "^1.11.20",
|
||||
"localforage": "^1.10.0",
|
||||
"lucide-react": "^1.16.0",
|
||||
"motion": "^12.38.0",
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"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>;
|
||||
|
||||
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>{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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { App } from "antd";
|
||||
|
||||
import { deleteAdminCreditLog, fetchAdminCreditLogs, saveAdminCreditLog, type AdminCreditLog } from "@/services/api/admin";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
const defaultPageSize = 10;
|
||||
|
||||
export function useAdminCreditLogs() {
|
||||
const { message } = App.useApp();
|
||||
const queryClient = useQueryClient();
|
||||
const token = useUserStore((state) => state.token);
|
||||
const clearSession = useUserStore((state) => state.clearSession);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(defaultPageSize);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["admin", "credit-logs", token, keyword, page, pageSize],
|
||||
queryFn: () => fetchAdminCreditLogs(token, { keyword, page, pageSize }),
|
||||
enabled: Boolean(token),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (log: Partial<AdminCreditLog>) => saveAdminCreditLog(token, log),
|
||||
onSuccess: async (_, log) => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "credit-logs"] });
|
||||
message.success(log.id ? "日志已保存" : "日志已新增");
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : "保存失败"),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => deleteAdminCreditLog(token, id),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "credit-logs"] });
|
||||
message.success("日志已删除");
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : "删除失败"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isError) {
|
||||
const errorMessage = query.error instanceof Error ? query.error.message : "读取日志失败";
|
||||
message.error(errorMessage);
|
||||
if (errorMessage.includes("未登录") || errorMessage.includes("权限不足") || errorMessage.includes("登录状态无效")) clearSession();
|
||||
}
|
||||
}, [clearSession, message, query.error, query.isError]);
|
||||
|
||||
const updateFilters = (next: Partial<{ keyword: string; page: number; pageSize: number }>) => {
|
||||
const queryState = { keyword, page, pageSize, ...next };
|
||||
if (next.keyword !== undefined || next.pageSize !== undefined) queryState.page = 1;
|
||||
setKeyword(queryState.keyword);
|
||||
setPage(queryState.page);
|
||||
setPageSize(queryState.pageSize);
|
||||
};
|
||||
|
||||
const data = query.data;
|
||||
|
||||
return {
|
||||
logs: data?.items || [],
|
||||
keyword,
|
||||
page,
|
||||
pageSize,
|
||||
total: data?.total || 0,
|
||||
isLoading: query.isFetching || saveMutation.isPending || deleteMutation.isPending,
|
||||
searchLogs: (value = keyword) => updateFilters({ keyword: value }),
|
||||
changePage: (value: number) => updateFilters({ page: value }),
|
||||
changePageSize: (value: number) => updateFilters({ pageSize: value }),
|
||||
resetFilters: () => updateFilters({ keyword: "", page: 1, pageSize: defaultPageSize }),
|
||||
refreshLogs: () => query.refetch(),
|
||||
saveLog: (log: Partial<AdminCreditLog>) => saveMutation.mutateAsync(log),
|
||||
deleteLog: (id: string) => deleteMutation.mutateAsync(id),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FileTextOutlined, HomeOutlined, LogoutOutlined, PictureOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { FileTextOutlined, HomeOutlined, LogoutOutlined, PictureOutlined, SettingOutlined, TransactionOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import { Button, Flex, Layout, Menu, Typography, theme } from "antd";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
@@ -13,6 +13,7 @@ import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
const adminMenus = [
|
||||
{ key: "/admin/users", icon: <UserOutlined />, label: "用户管理" },
|
||||
{ key: "/admin/credit-logs", icon: <TransactionOutlined />, label: "算力点日志" },
|
||||
{ key: "/admin/prompts", icon: <FileTextOutlined />, label: "提示词管理" },
|
||||
{ key: "/admin/assets", icon: <PictureOutlined />, label: "素材库" },
|
||||
{ key: "/admin/settings", icon: <SettingOutlined />, label: "系统设置" },
|
||||
@@ -32,10 +33,12 @@ export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||
? "/admin/assets"
|
||||
: pathname.startsWith("/admin/prompts")
|
||||
? "/admin/prompts"
|
||||
: pathname.startsWith("/admin/users")
|
||||
? "/admin/users"
|
||||
: "";
|
||||
const pageTitle = pathname.startsWith("/admin/settings") ? "系统设置" : pathname.startsWith("/admin/assets") ? "素材库管理" : pathname.startsWith("/admin/prompts") ? "提示词管理" : "用户管理";
|
||||
: pathname.startsWith("/admin/credit-logs")
|
||||
? "/admin/credit-logs"
|
||||
: pathname.startsWith("/admin/users")
|
||||
? "/admin/users"
|
||||
: "";
|
||||
const pageTitle = pathname.startsWith("/admin/settings") ? "系统设置" : pathname.startsWith("/admin/assets") ? "素材库管理" : pathname.startsWith("/admin/prompts") ? "提示词管理" : pathname.startsWith("/admin/credit-logs") ? "算力点日志" : "用户管理";
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady) return;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { ProTable, type ProColumns } from "@ant-design/pro-components";
|
||||
import { Avatar, Button, Card, Col, Flex, Form, Input, InputNumber, Modal, Row, Select, Space, Tag, Tooltip, Typography } from "antd";
|
||||
import { Avatar, Button, Card, Col, Divider, Flex, Form, Input, InputNumber, Modal, Row, Select, Space, Tag, Tooltip, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { AdminUser } from "@/services/api/admin";
|
||||
@@ -21,7 +22,7 @@ const statusOptions = [
|
||||
];
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
const { users, keyword, page, pageSize, total, isLoading, searchUsers, changePage, changePageSize, resetFilters, refreshUsers, saveUser: saveAdminUser, deleteUser } = useAdminUsers();
|
||||
const { users, keyword, page, pageSize, total, isLoading, searchUsers, changePage, changePageSize, resetFilters, refreshUsers, saveUser: saveAdminUser, adjustCredits, deleteUser } = useAdminUsers();
|
||||
const [form] = Form.useForm<UserFormValues>();
|
||||
const [keywordText, setKeywordText] = useState(keyword);
|
||||
const [editingUser, setEditingUser] = useState<Partial<AdminUser> | null>(null);
|
||||
@@ -30,15 +31,22 @@ export default function AdminUsersPage() {
|
||||
useEffect(() => setKeywordText(keyword), [keyword]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingUser) form.setFieldsValue({ role: "user", status: "active", credits: 0, ...editingUser, password: "" });
|
||||
if (editingUser) form.setFieldsValue({ role: "user", status: "active", ...editingUser, password: "" });
|
||||
}, [editingUser, form]);
|
||||
|
||||
const saveUser = async () => {
|
||||
const value = await form.validateFields();
|
||||
await saveAdminUser({ ...editingUser, ...value, password: value.password || undefined });
|
||||
const userValue = { ...value };
|
||||
delete userValue.credits;
|
||||
await saveAdminUser({ ...editingUser, ...userValue, password: value.password || undefined });
|
||||
setEditingUser(null);
|
||||
};
|
||||
|
||||
const saveCredits = async () => {
|
||||
if (!editingUser?.id) return;
|
||||
await adjustCredits(editingUser.id, form.getFieldValue("credits") || 0);
|
||||
};
|
||||
|
||||
const columns: ProColumns<AdminUser>[] = [
|
||||
{
|
||||
title: "用户",
|
||||
@@ -46,7 +54,7 @@ export default function AdminUsersPage() {
|
||||
width: 260,
|
||||
render: (_, item) => (
|
||||
<Flex align="center" gap={10} style={{ minWidth: 0 }}>
|
||||
<Avatar src={item.avatarUrl}>{(item.displayName || item.username || "U").slice(0, 1).toUpperCase()}</Avatar>
|
||||
<Avatar src={item.avatarUrl || undefined}>{(item.displayName || item.username || "U").slice(0, 1).toUpperCase()}</Avatar>
|
||||
<Flex vertical style={{ minWidth: 0 }}>
|
||||
<Typography.Text strong ellipsis>
|
||||
{item.displayName || item.username}
|
||||
@@ -86,7 +94,7 @@ export default function AdminUsersPage() {
|
||||
title: "最近登录",
|
||||
dataIndex: "lastLoginAt",
|
||||
width: 180,
|
||||
render: (_, item) => <Typography.Text type="secondary">{item.lastLoginAt || "-"}</Typography.Text>,
|
||||
render: (_, item) => <Typography.Text type="secondary">{item.lastLoginAt ? dayjs(item.lastLoginAt).format("YYYY-MM-DD HH:mm:ss") : "-"}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
@@ -161,7 +169,7 @@ export default function AdminUsersPage() {
|
||||
}
|
||||
options={{ density: true, setting: true, reload: () => void refreshUsers() }}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setEditingUser({ role: "user", status: "active", credits: 0 })}>
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setEditingUser({ role: "user", status: "active" })}>
|
||||
新增
|
||||
</Button>,
|
||||
]}
|
||||
@@ -179,6 +187,7 @@ export default function AdminUsersPage() {
|
||||
|
||||
<Modal title={editingUser?.id ? "编辑用户" : "新增用户"} open={Boolean(editingUser)} width={680} onCancel={() => setEditingUser(null)} onOk={() => void saveUser()} okText="保存" cancelText="取消" destroyOnHidden>
|
||||
<Form form={form} layout="vertical" requiredMark={false}>
|
||||
<Typography.Text strong>基础信息</Typography.Text>
|
||||
<Row gutter={14}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="username" label="用户名" rules={[{ required: true, message: "请输入用户名" }]}>
|
||||
@@ -210,17 +219,25 @@ export default function AdminUsersPage() {
|
||||
<Select options={statusOptions} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="credits" label="算力点">
|
||||
<InputNumber min={0} precision={0} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="avatarUrl" label="头像 URL">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{editingUser?.id ? (
|
||||
<>
|
||||
<Divider style={{ margin: "4px 0 16px" }} />
|
||||
<Typography.Text strong>算力点调整</Typography.Text>
|
||||
<Row gutter={14}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="算力点">
|
||||
<Space.Compact style={{ width: "100%" }}>
|
||||
<Form.Item name="credits" noStyle>
|
||||
<InputNumber min={0} precision={0} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
<Button onClick={() => void saveCredits()}>调整</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
) : null}
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { App } from "antd";
|
||||
|
||||
import { deleteAdminUser, fetchAdminUsers, saveAdminUser, type AdminUser } from "@/services/api/admin";
|
||||
import { adjustAdminUserCredits, deleteAdminUser, fetchAdminUsers, saveAdminUser, type AdminUser } from "@/services/api/admin";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
const defaultPageSize = 10;
|
||||
@@ -43,6 +43,15 @@ export function useAdminUsers() {
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : "删除失败"),
|
||||
});
|
||||
|
||||
const creditMutation = useMutation({
|
||||
mutationFn: ({ id, credits }: { id: string; credits: number }) => adjustAdminUserCredits(token, id, credits),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
|
||||
message.success("算力点已调整");
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : "调整失败"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isError) {
|
||||
const errorMessage = query.error instanceof Error ? query.error.message : "读取用户失败";
|
||||
@@ -67,13 +76,14 @@ export function useAdminUsers() {
|
||||
page,
|
||||
pageSize,
|
||||
total: data?.total || 0,
|
||||
isLoading: query.isFetching || saveMutation.isPending || deleteMutation.isPending,
|
||||
isLoading: query.isFetching || saveMutation.isPending || deleteMutation.isPending || creditMutation.isPending,
|
||||
searchUsers: (value = keyword) => updateFilters({ keyword: value }),
|
||||
changePage: (value: number) => updateFilters({ page: value }),
|
||||
changePageSize: (value: number) => updateFilters({ pageSize: value }),
|
||||
resetFilters: () => updateFilters({ keyword: "", page: 1, pageSize: defaultPageSize }),
|
||||
refreshUsers: () => query.refetch(),
|
||||
saveUser: (user: Partial<AdminUser> & { password?: string }) => saveMutation.mutateAsync(user),
|
||||
adjustCredits: (id: string, credits: number) => creditMutation.mutateAsync({ id, credits }),
|
||||
deleteUser: (id: string) => deleteMutation.mutateAsync(id),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,23 @@ export type AdminUserListResponse = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type AdminCreditLog = {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
balance: number;
|
||||
relatedId: string;
|
||||
remark: string;
|
||||
extra: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type AdminCreditLogListResponse = {
|
||||
items: AdminCreditLog[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type AdminUserQuery = {
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
@@ -47,10 +64,26 @@ export async function saveAdminUser(token: string, user: Partial<AdminUser> & {
|
||||
return apiPost<AdminUser>("/api/admin/users", user, token);
|
||||
}
|
||||
|
||||
export async function adjustAdminUserCredits(token: string, id: string, credits: number) {
|
||||
return apiPost<AdminUser>(`/api/admin/users/${encodeURIComponent(id)}/credits`, { credits }, token);
|
||||
}
|
||||
|
||||
export async function deleteAdminUser(token: string, id: string) {
|
||||
return apiDelete<boolean>(`/api/admin/users/${encodeURIComponent(id)}`, token);
|
||||
}
|
||||
|
||||
export async function fetchAdminCreditLogs(token: string, query: AdminUserQuery = {}) {
|
||||
return apiGet<AdminCreditLogListResponse>("/api/admin/credit-logs", compactApiParams(query), token);
|
||||
}
|
||||
|
||||
export async function saveAdminCreditLog(token: string, log: Partial<AdminCreditLog>) {
|
||||
return apiPost<AdminCreditLog>("/api/admin/credit-logs", log, token);
|
||||
}
|
||||
|
||||
export async function deleteAdminCreditLog(token: string, id: string) {
|
||||
return apiDelete<boolean>(`/api/admin/credit-logs/${encodeURIComponent(id)}`, token);
|
||||
}
|
||||
|
||||
export async function fetchAdminPromptCategories(token: string) {
|
||||
return apiGet<AdminPromptCategory[]>("/api/admin/prompt-categories", undefined, token);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user