mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 08:54:23 +08:00
fff7074218
- 在管理员API中新增用户算力点调整功能和信用日志相关接口 - 实现后端用户算力点调整逻辑和信用日志的增删改查操作 - 在管理员界面用户管理页面添加算力点调整功能 - 新增独立的算力点日志管理页面,支持日志查看和编辑 - 添加算力点日志相关的数据模型和服务函数 - 更新数据库文档说明第三方资料存储结构 - 添加dayjs依赖用于时间格式化显示
262 lines
12 KiB
TypeScript
262 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
||
import { ProTable, type ProColumns } from "@ant-design/pro-components";
|
||
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";
|
||
import { useAdminUsers } from "./use-admin-users";
|
||
|
||
type UserFormValues = Partial<AdminUser> & { password?: string };
|
||
|
||
const roleOptions = [
|
||
{ label: "普通用户", value: "user" },
|
||
{ label: "管理员", value: "admin" },
|
||
];
|
||
|
||
const statusOptions = [
|
||
{ label: "正常", value: "active" },
|
||
{ label: "禁用", value: "ban" },
|
||
];
|
||
|
||
export default function AdminUsersPage() {
|
||
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);
|
||
const [deletingUser, setDeletingUser] = useState<AdminUser | null>(null);
|
||
|
||
useEffect(() => setKeywordText(keyword), [keyword]);
|
||
|
||
useEffect(() => {
|
||
if (editingUser) form.setFieldsValue({ role: "user", status: "active", ...editingUser, password: "" });
|
||
}, [editingUser, form]);
|
||
|
||
const saveUser = async () => {
|
||
const value = await form.validateFields();
|
||
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: "用户",
|
||
dataIndex: "username",
|
||
width: 260,
|
||
render: (_, item) => (
|
||
<Flex align="center" gap={10} style={{ minWidth: 0 }}>
|
||
<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}
|
||
</Typography.Text>
|
||
<Typography.Text type="secondary" ellipsis>
|
||
{item.username}
|
||
</Typography.Text>
|
||
</Flex>
|
||
</Flex>
|
||
),
|
||
},
|
||
{
|
||
title: "角色",
|
||
dataIndex: "role",
|
||
width: 100,
|
||
render: (_, item) => <Tag color={item.role === "admin" ? "gold" : "default"}>{item.role === "admin" ? "管理员" : "用户"}</Tag>,
|
||
},
|
||
{
|
||
title: "状态",
|
||
dataIndex: "status",
|
||
width: 90,
|
||
render: (_, item) => <Tag color={item.status === "ban" ? "red" : "green"}>{item.status === "ban" ? "禁用" : "正常"}</Tag>,
|
||
},
|
||
{
|
||
title: "算力点",
|
||
dataIndex: "credits",
|
||
width: 100,
|
||
render: (_, item) => <Typography.Text>{item.credits}</Typography.Text>,
|
||
},
|
||
{
|
||
title: "Linux.do",
|
||
dataIndex: "linuxDoId",
|
||
width: 140,
|
||
render: (_, item) => <Typography.Text type="secondary">{item.linuxDoId || "-"}</Typography.Text>,
|
||
},
|
||
{
|
||
title: "最近登录",
|
||
dataIndex: "lastLoginAt",
|
||
width: 180,
|
||
render: (_, item) => <Typography.Text type="secondary">{item.lastLoginAt ? dayjs(item.lastLoginAt).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={() => setEditingUser(item)} />
|
||
</Tooltip>
|
||
<Tooltip title="删除">
|
||
<Button danger type="text" size="small" icon={<DeleteOutlined />} onClick={() => setDeletingUser(item)} />
|
||
</Tooltip>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<main style={{ padding: 24 }}>
|
||
<Flex vertical gap={16}>
|
||
<Card variant="borderless">
|
||
<Form layout="vertical">
|
||
<Row gutter={16} align="bottom">
|
||
<Col flex="360px">
|
||
<Form.Item label="关键词">
|
||
<Input.Search
|
||
value={keywordText}
|
||
placeholder="搜索用户名、昵称、邮箱或 Linux.do ID"
|
||
allowClear
|
||
enterButton={<SearchOutlined />}
|
||
onSearch={() => searchUsers(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={() => searchUsers(keywordText)}>
|
||
查询
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
</Form>
|
||
</Card>
|
||
<ProTable<AdminUser>
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={users}
|
||
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 refreshUsers() }}
|
||
toolBarRender={() => [
|
||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setEditingUser({ role: "user", status: "active" })}>
|
||
新增
|
||
</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)),
|
||
}}
|
||
/>
|
||
</Flex>
|
||
|
||
<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: "请输入用户名" }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="password" label={editingUser?.id ? "新密码" : "密码"} rules={editingUser?.id ? [] : [{ required: true, message: "请输入密码" }]}>
|
||
<Input.Password autoComplete="new-password" />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="displayName" label="昵称">
|
||
<Input />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="email" label="邮箱">
|
||
<Input />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="role" label="角色" rules={[{ required: true, message: "请选择角色" }]}>
|
||
<Select options={roleOptions} />
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item name="status" label="状态" rules={[{ required: true, message: "请选择状态" }]}>
|
||
<Select options={statusOptions} />
|
||
</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>
|
||
|
||
<Modal
|
||
title="删除用户"
|
||
open={Boolean(deletingUser)}
|
||
onCancel={() => setDeletingUser(null)}
|
||
onOk={async () => {
|
||
if (!deletingUser) return;
|
||
await deleteUser(deletingUser.id);
|
||
setDeletingUser(null);
|
||
}}
|
||
okText="删除"
|
||
okButtonProps={{ danger: true }}
|
||
cancelText="取消"
|
||
>
|
||
确定删除「{deletingUser?.displayName || deletingUser?.username}」吗?删除后该账号将无法继续登录。
|
||
</Modal>
|
||
</main>
|
||
);
|
||
}
|