update:1.更新用户管理-支持管理员新增用户

This commit is contained in:
xiamuceer
2025-11-13 11:43:45 +08:00
parent cb57c21569
commit 4516a2bcf7
10 changed files with 1266 additions and 261 deletions
+2
View File
@@ -15,6 +15,7 @@ import ChapterAnalysis from './pages/ChapterAnalysis';
import WritingStyles from './pages/WritingStyles';
import Settings from './pages/Settings';
import MCPPlugins from './pages/MCPPlugins';
import UserManagement from './pages/UserManagement';
// import Polish from './pages/Polish';
import Login from './pages/Login';
import AuthCallback from './pages/AuthCallback';
@@ -38,6 +39,7 @@ function App() {
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
<Route path="/mcp-plugins" element={<ProtectedRoute><MCPPlugins /></ProtectedRoute>} />
<Route path="/user-management" element={<ProtectedRoute><UserManagement /></ProtectedRoute>} />
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
<Route path="/project/:projectId" element={<ProtectedRoute><ProjectDetail /></ProtectedRoute>}>
<Route index element={<Navigate to="world-setting" replace />} />
+18 -255
View File
@@ -1,20 +1,17 @@
import { useState, useEffect } from 'react';
import { Dropdown, Avatar, Space, Typography, message, Modal, Table, Button, Tag, Popconfirm, Pagination, Form, Input } from 'antd';
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined, KeyOutlined } from '@ant-design/icons';
import { authApi, userApi } from '../services/api';
import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button } from 'antd';
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
import { authApi } from '../services/api';
import type { User } from '../types';
import type { MenuProps } from 'antd';
import { useNavigate } from 'react-router-dom';
const { Text } = Typography;
export default function UserMenu() {
const navigate = useNavigate();
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [showUserManagement, setShowUserManagement] = useState(false);
const [showChangePassword, setShowChangePassword] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [changePasswordForm] = Form.useForm();
const [changingPassword, setChangingPassword] = useState(false);
@@ -42,98 +39,12 @@ export default function UserMenu() {
}
};
const handleShowUserManagement = async () => {
const handleShowUserManagement = () => {
if (!currentUser?.is_admin) {
message.warning('只有管理员可以访问用户管理');
return;
}
setShowUserManagement(true);
loadUsers();
};
const loadUsers = async () => {
try {
setLoading(true);
const userList = await userApi.listUsers();
setUsers(userList);
} catch (error) {
console.error('获取用户列表失败:', error);
message.error('获取用户列表失败');
} finally {
setLoading(false);
}
};
const handleSetAdmin = async (userId: string, isAdmin: boolean) => {
try {
await userApi.setAdmin(userId, isAdmin);
message.success(isAdmin ? '已设置为管理员' : '已取消管理员权限');
loadUsers();
} catch (error) {
console.error('设置管理员失败:', error);
message.error('设置管理员失败');
}
};
const handleDeleteUser = async (userId: string) => {
try {
await userApi.deleteUser(userId);
message.success('用户已删除');
loadUsers();
} catch (error) {
console.error('删除用户失败:', error);
message.error('删除用户失败');
}
};
const handleResetPassword = async (userId: string, username: string) => {
Modal.confirm({
title: '重置用户密码',
content: (
<div>
<p> <strong>{username}</strong> </p>
<p style={{ color: '#8c8c8c', fontSize: 12 }}><strong>{username}@666</strong></p>
</div>
),
okText: '确定重置',
cancelText: '取消',
onOk: async () => {
try {
const result = await userApi.resetPassword(userId);
if (result.default_password) {
Modal.success({
title: '密码重置成功',
content: (
<div>
<p> <strong>{result.username}</strong> </p>
<p style={{
padding: '8px 12px',
background: '#f5f5f5',
borderRadius: 4,
fontSize: 16,
fontFamily: 'monospace',
color: '#1890ff',
fontWeight: 'bold'
}}>
{result.default_password}
</p>
<p style={{ color: '#ff4d4f', fontSize: 12, marginTop: 8 }}>
</p>
</div>
),
});
} else {
message.success('密码重置成功');
}
loadUsers();
} catch (error: any) {
console.error('重置密码失败:', error);
message.error(error.response?.data?.detail || '重置密码失败');
}
},
});
navigate('/user-management');
};
const handleChangePassword = async (values: { oldPassword: string; newPassword: string }) => {
@@ -169,14 +80,17 @@ export default function UserMenu() {
{
type: 'divider',
},
...(currentUser?.is_admin ? [{
key: 'user-management',
icon: <TeamOutlined />,
label: '用户管理',
onClick: handleShowUserManagement,
}, {
type: 'divider' as const,
}] : []),
...(currentUser?.is_admin ? [
{
key: 'user-management',
icon: <TeamOutlined />,
label: '用户管理',
onClick: handleShowUserManagement,
},
{
type: 'divider' as const,
}
] : []),
{
key: 'change-password',
icon: <LockOutlined />,
@@ -194,95 +108,6 @@ export default function UserMenu() {
},
];
const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
render: (text: string, record: User) => (
<Space>
<Avatar src={record.avatar_url} icon={<UserOutlined />} size="small" />
<div>
<div>{record.display_name || text}</div>
<Text type="secondary" style={{ fontSize: 12 }}>{text}</Text>
</div>
</Space>
),
},
{
title: 'Trust Level',
dataIndex: 'trust_level',
key: 'trust_level',
width: 120,
render: (level: number) => <Tag color="blue">{level}</Tag>,
},
{
title: '角色',
dataIndex: 'is_admin',
key: 'is_admin',
width: 100,
render: (isAdmin: boolean) => (
isAdmin ? <Tag color="gold" icon={<CrownOutlined />}></Tag> : <Tag></Tag>
),
},
{
title: '最后登录',
dataIndex: 'last_login',
key: 'last_login',
width: 180,
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'actions',
width: 280,
render: (_: unknown, record: User) => {
const isSelf = record.user_id === currentUser?.user_id;
return (
<Space size="small">
{record.is_admin ? (
<Popconfirm
title="确定要取消管理员权限吗?"
onConfirm={() => handleSetAdmin(record.user_id, false)}
disabled={isSelf}
>
<Button size="small" disabled={isSelf}>
</Button>
</Popconfirm>
) : (
<Button
size="small"
type="primary"
onClick={() => handleSetAdmin(record.user_id, true)}
>
</Button>
)}
<Button
size="small"
icon={<KeyOutlined />}
onClick={() => handleResetPassword(record.user_id, record.username)}
disabled={isSelf}
title={isSelf ? '不能重置自己的密码' : '重置为默认密码'}
>
</Button>
<Popconfirm
title="确定要删除该用户吗?此操作不可恢复!"
onConfirm={() => handleDeleteUser(record.user_id)}
disabled={isSelf}
>
<Button size="small" danger disabled={isSelf}>
</Button>
</Popconfirm>
</Space>
);
},
},
];
if (!currentUser) {
return null;
}
@@ -365,68 +190,6 @@ export default function UserMenu() {
</div>
</Dropdown>
<Modal
title="用户管理"
open={showUserManagement}
onCancel={() => setShowUserManagement(false)}
footer={null}
width={900}
centered
styles={{
body: {
padding: 0,
display: 'flex',
flexDirection: 'column',
height: 'calc(100vh - 380px)',
}
}}
>
<div style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}>
<div style={{
flex: 1,
overflow: 'hidden',
padding: '0 12px',
display: 'flex',
flexDirection: 'column',
}}>
<Table
columns={columns}
dataSource={users.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
rowKey="user_id"
loading={loading}
pagination={false}
scroll={{ x: 800, y: 'calc(100vh - 520px)' }}
sticky
/>
</div>
<div style={{
padding: '16px 24px',
borderTop: '1px solid #f0f0f0',
background: '#fff',
display: 'flex',
justifyContent: 'center',
flexShrink: 0,
}}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={users.length}
showSizeChanger
showTotal={(total) => `${total} 个用户`}
pageSizeOptions={['10', '20', '50', '100']}
onChange={(page, newPageSize) => {
setCurrentPage(page);
setPageSize(newPageSize);
}}
/>
</div>
</div>
</Modal>
<Modal
title="修改密码"
open={showChangePassword}
+6
View File
@@ -554,6 +554,12 @@ export default function MCPPluginsPage() {
checked={plugin.enabled}
onChange={(checked) => handleToggle(plugin, checked)}
size={isMobile ? 'small' : 'default'}
style={{
flexShrink: 0,
height: isMobile ? 16 : 22,
minHeight: isMobile ? 16 : 22,
lineHeight: isMobile ? '16px' : '22px'
}}
/>
</Tooltip>
<Tooltip title="测试连接">
+767
View File
@@ -0,0 +1,767 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Table,
Button,
Modal,
Form,
Input,
Switch,
Space,
Tag,
Popconfirm,
message,
Card,
Typography,
Badge,
InputNumber,
Tooltip,
Row,
Col,
Pagination,
Dropdown,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
KeyOutlined,
StopOutlined,
CheckCircleOutlined,
ArrowLeftOutlined,
TeamOutlined,
UserOutlined,
SearchOutlined,
MoreOutlined,
} from '@ant-design/icons';
import { adminApi } from '../services/api';
import type { User } from '../types';
import UserMenu from '../components/UserMenu';
const { Title, Text } = Typography;
interface UserWithStatus extends User {
is_active?: boolean;
}
export default function UserManagement() {
const navigate = useNavigate();
const [users, setUsers] = useState<UserWithStatus[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false);
const [currentUser, setCurrentUser] = useState<UserWithStatus | null>(null);
const [newPassword, setNewPassword] = useState('');
const [pageSize, setPageSize] = useState(20);
const [currentPage, setCurrentPage] = useState(1);
const [searchText, setSearchText] = useState('');
const [form] = Form.useForm();
const [editForm] = Form.useForm();
// 过滤用户列表
const filteredUsers = users.filter(user => {
if (!searchText) return true;
const searchLower = searchText.toLowerCase();
return (
user.username?.toLowerCase().includes(searchLower) ||
user.display_name?.toLowerCase().includes(searchLower) ||
user.user_id?.toLowerCase().includes(searchLower)
);
});
// 加载用户列表
const loadUsers = async () => {
setLoading(true);
try {
const res = await adminApi.getUsers();
setUsers(res.users);
} catch (error) {
console.error('加载用户列表失败:', error);
message.error('加载用户列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadUsers();
}, []);
// 添加用户
const handleCreate = async (values: any) => {
try {
const res = await adminApi.createUser(values);
message.success('用户创建成功');
// 如果有默认密码,显示给管理员
if (res.default_password) {
Modal.info({
title: '用户创建成功',
content: (
<div>
<p><Text strong>{values.username}</Text></p>
<p><Text strong copyable>{res.default_password}</Text></p>
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
</p>
</div>
),
width: 500,
});
}
setModalVisible(false);
form.resetFields();
loadUsers();
} catch (error) {
console.error('创建用户失败:', error);
message.error('创建用户失败');
}
};
// 编辑用户
const handleEdit = (user: UserWithStatus) => {
setCurrentUser(user);
editForm.setFieldsValue({
display_name: user.display_name,
avatar_url: user.avatar_url,
trust_level: user.trust_level,
is_admin: user.is_admin,
});
setEditModalVisible(true);
};
const handleUpdate = async (values: any) => {
if (!currentUser) return;
try {
await adminApi.updateUser(currentUser.user_id, values);
message.success('用户信息更新成功');
setEditModalVisible(false);
editForm.resetFields();
loadUsers();
} catch (error) {
console.error('更新用户失败:', error);
message.error('更新用户失败');
}
};
// 切换用户状态
const handleToggleStatus = async (user: UserWithStatus) => {
const isActive = user.is_active !== false;
const action = isActive ? '禁用' : '启用';
try {
await adminApi.toggleUserStatus(user.user_id, !isActive);
message.success(`用户已${action}`);
loadUsers();
} catch (error) {
console.error(`${action}用户失败:`, error);
message.error(`${action}用户失败`);
}
};
// 重置密码
const handleResetPassword = (user: UserWithStatus) => {
setCurrentUser(user);
setNewPassword('');
setResetPasswordModalVisible(true);
};
const handleResetPasswordConfirm = async () => {
if (!currentUser) return;
try {
const res = await adminApi.resetPassword(
currentUser.user_id,
newPassword || undefined
);
Modal.info({
title: '密码重置成功',
content: (
<div>
<p><Text strong>{currentUser.username}</Text></p>
<p><Text strong copyable>{res.new_password}</Text></p>
<p style={{ color: '#ff4d4f', marginTop: 16 }}>
</p>
</div>
),
width: 500,
});
setResetPasswordModalVisible(false);
setNewPassword('');
} catch (error) {
console.error('重置密码失败:', error);
message.error('重置密码失败');
}
};
// 删除用户
const handleDelete = async (user: UserWithStatus) => {
try {
await adminApi.deleteUser(user.user_id);
message.success('用户已删除');
loadUsers();
} catch (error) {
console.error('删除用户失败:', error);
message.error('删除用户失败');
}
};
const isMobile = window.innerWidth <= 768;
// 表格列定义
const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 150,
render: (text: string) => (
<Space>
<UserOutlined style={{ color: '#1890ff' }} />
<Text strong>{text}</Text>
</Space>
),
},
{
title: '显示名称',
dataIndex: 'display_name',
key: 'display_name',
width: 150,
},
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
width: 100,
render: (isActive: boolean) => (
<Badge
status={isActive !== false ? 'success' : 'error'}
text={isActive !== false ? '正常' : '已禁用'}
/>
),
},
{
title: '角色',
dataIndex: 'is_admin',
key: 'is_admin',
width: 100,
render: (isAdmin: boolean) => (
<Tag color={isAdmin ? 'gold' : 'blue'}>
{isAdmin ? '👑 管理员' : '普通用户'}
</Tag>
),
},
{
title: '信任等级',
dataIndex: 'trust_level',
key: 'trust_level',
width: 100,
render: (level: number) => (
<Tag color={level === -1 ? 'default' : level >= 5 ? 'green' : 'blue'}>
{level === -1 ? '已禁用' : `Level ${level}`}
</Tag>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
render: (date: string) => date ? new Date(date).toLocaleString('zh-CN') : '-',
},
{
title: '最后登录',
dataIndex: 'last_login',
key: 'last_login',
width: 180,
render: (date: string) => date ? new Date(date).toLocaleString('zh-CN') : '从未登录',
},
{
title: '操作',
key: 'action',
width: isMobile ? 80 : 300,
fixed: 'right' as const,
render: (_: any, record: UserWithStatus) => {
const isActive = record.is_active !== false;
// 移动端:使用下拉菜单
if (isMobile) {
const menuItems = [
{
key: 'edit',
label: '编辑用户',
icon: <EditOutlined />,
onClick: () => handleEdit(record),
},
{
key: 'reset',
label: '重置密码',
icon: <KeyOutlined />,
onClick: () => handleResetPassword(record),
},
{
key: 'toggle',
label: isActive ? '禁用用户' : '启用用户',
icon: isActive ? <StopOutlined /> : <CheckCircleOutlined />,
danger: isActive,
onClick: () => {
Modal.confirm({
title: `确定${isActive ? '禁用' : '启用'}该用户吗?`,
onOk: () => handleToggleStatus(record),
okText: '确定',
cancelText: '取消',
});
},
},
...(!record.is_admin ? [{
key: 'delete',
label: '删除用户',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
Modal.confirm({
title: '确定删除该用户吗?此操作不可恢复!',
onOk: () => handleDelete(record),
okText: '确定',
cancelText: '取消',
okButtonProps: { danger: true },
});
},
}] : []),
];
return (
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
<Button type="text" icon={<MoreOutlined />} />
</Dropdown>
);
}
// 桌面端:保持原有按钮样式
return (
<Space size="small">
<Tooltip title="编辑用户">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
</Tooltip>
<Tooltip title="重置密码">
<Button
type="link"
size="small"
icon={<KeyOutlined />}
onClick={() => handleResetPassword(record)}
>
</Button>
</Tooltip>
<Popconfirm
title={`确定${isActive ? '禁用' : '启用'}该用户吗?`}
onConfirm={() => handleToggleStatus(record)}
okText="确定"
cancelText="取消"
>
<Tooltip title={isActive ? '禁用用户' : '启用用户'}>
<Button
type="link"
size="small"
danger={isActive}
icon={isActive ? <StopOutlined /> : <CheckCircleOutlined />}
>
{isActive ? '禁用' : '启用'}
</Button>
</Tooltip>
</Popconfirm>
{!record.is_admin && (
<Popconfirm
title="确定删除该用户吗?此操作不可恢复!"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Tooltip title="删除用户">
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
>
</Button>
</Tooltip>
</Popconfirm>
)}
</Space>
);
},
},
];
return (
<div style={{
height: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: isMobile ? '20px 16px' : '40px 24px',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}>
<div style={{
maxWidth: 1400,
margin: '0 auto',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}>
{/* 顶部导航卡片 */}
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
marginBottom: isMobile ? 20 : 24,
}}
>
<Row align="middle" justify="space-between" gutter={[16, 16]}>
<Col xs={24} sm={12}>
<Space direction="vertical" size={4}>
<Title level={isMobile ? 3 : 2} style={{ margin: 0 }}>
<TeamOutlined style={{ color: '#fa8c16', marginRight: 8 }} />
</Title>
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 14 }}>
</Text>
</Space>
</Col>
<Col xs={24} sm={12}>
<Space size={12} style={{ display: 'flex', justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
style={{
borderRadius: 8,
borderColor: '#d9d9d9',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#667eea';
e.currentTarget.style.color = '#667eea';
e.currentTarget.style.boxShadow = '0 2px 12px rgba(102, 126, 234, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#d9d9d9';
e.currentTarget.style.color = 'rgba(0, 0, 0, 0.88)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
}}
>
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setModalVisible(true)}
style={{
borderRadius: 8,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.4)'
}}
>
</Button>
<UserMenu />
</Space>
</Col>
</Row>
</Card>
{/* 主内容卡片 */}
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
bodyStyle={{
padding: 0,
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* 搜索栏 */}
<div style={{
padding: '16px 24px 0 24px',
borderBottom: '1px solid #f0f0f0',
}}>
<Input
placeholder="搜索用户名、显示名称或用户ID"
prefix={<SearchOutlined style={{ color: '#999' }} />}
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1); // 搜索时重置到第一页
}}
allowClear
style={{
borderRadius: 8,
}}
/>
</div>
{/* 表格区域 */}
<div style={{
flex: 1,
overflow: 'auto',
padding: '16px 24px 0 24px',
}}>
<Table
columns={columns}
dataSource={filteredUsers.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
rowKey="user_id"
loading={loading}
scroll={{
x: 1400,
y: 'calc(100vh - 410px)'
}}
pagination={false}
/>
</div>
{/* 固定分页控件 */}
<div style={{
padding: '16px 24px 24px 24px',
borderTop: '1px solid #f0f0f0',
background: 'rgba(255, 255, 255, 0.95)',
display: 'flex',
justifyContent: 'center',
}}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={filteredUsers.length}
showSizeChanger
showTotal={(total) => `${total} 个用户${searchText ? ' (已过滤)' : ''}`}
pageSizeOptions={[20, 50, 100]}
onChange={(page, size) => {
setCurrentPage(page);
setPageSize(size);
}}
onShowSizeChange={(_current, size) => {
setCurrentPage(1);
setPageSize(size);
}}
/>
</div>
</Card>
</div>
{/* 添加用户对话框 */}
<Modal
title={<span><PlusOutlined style={{ marginRight: 8 }} /></span>}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
form.resetFields();
}}
onOk={() => form.submit()}
width={isMobile ? '90%' : 600}
centered
okText="创建"
cancelText="取消"
>
<Form
form={form}
layout="vertical"
onFinish={handleCreate}
>
<Form.Item
label="用户名"
name="username"
rules={[
{ required: true, message: '请输入用户名' },
{ min: 3, max: 20, message: '用户名长度3-20位' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: '只能包含字母、数字和下划线' },
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
label="显示名称"
name="display_name"
rules={[
{ required: true, message: '请输入显示名称' },
{ min: 2, max: 50, message: '显示名称长度2-50位' },
]}
>
<Input placeholder="请输入显示名称" />
</Form.Item>
<Form.Item
label="初始密码"
name="password"
extra="留空则自动生成 username@666"
rules={[
{ min: 6, message: '密码长度至少6位' },
]}
>
<Input.Password placeholder="留空则自动生成" />
</Form.Item>
<Form.Item
label="头像URL"
name="avatar_url"
>
<Input placeholder="请输入头像URL(可选)" />
</Form.Item>
<Form.Item
label="信任等级"
name="trust_level"
initialValue={0}
>
<InputNumber min={0} max={9} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label="设为管理员"
name="is_admin"
valuePropName="checked"
initialValue={false}
>
<Switch
size={isMobile ? 'small' : 'default'}
style={{
flexShrink: 0,
height: isMobile ? 16 : 22,
minHeight: isMobile ? 16 : 22,
lineHeight: isMobile ? '16px' : '22px'
}}
/>
</Form.Item>
</Form>
</Modal>
{/* 编辑用户对话框 */}
<Modal
title={<span><EditOutlined style={{ marginRight: 8 }} /></span>}
open={editModalVisible}
onCancel={() => {
setEditModalVisible(false);
editForm.resetFields();
}}
onOk={() => editForm.submit()}
width={isMobile ? '90%' : 600}
centered
okText="保存"
cancelText="取消"
>
<Form
form={editForm}
layout="vertical"
onFinish={handleUpdate}
>
<Form.Item
label="显示名称"
name="display_name"
rules={[
{ required: true, message: '请输入显示名称' },
{ min: 2, max: 50, message: '显示名称长度2-50位' },
]}
>
<Input placeholder="请输入显示名称" />
</Form.Item>
<Form.Item
label="头像URL"
name="avatar_url"
>
<Input placeholder="请输入头像URL(可选)" />
</Form.Item>
<Form.Item
label="信任等级"
name="trust_level"
>
<InputNumber min={0} max={9} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label="设为管理员"
name="is_admin"
valuePropName="checked"
>
<Switch
size={isMobile ? 'small' : 'default'}
style={{
flexShrink: 0,
height: isMobile ? 16 : 22,
minHeight: isMobile ? 16 : 22,
lineHeight: isMobile ? '16px' : '22px'
}}
/>
</Form.Item>
</Form>
</Modal>
{/* 重置密码对话框 */}
<Modal
title={<span><KeyOutlined style={{ marginRight: 8 }} /></span>}
open={resetPasswordModalVisible}
onCancel={() => {
setResetPasswordModalVisible(false);
setNewPassword('');
}}
onOk={handleResetPasswordConfirm}
width={isMobile ? '90%' : 500}
centered
okText="确认重置"
cancelText="取消"
>
<div style={{ marginBottom: 16 }}>
<Text><Text strong>{currentUser?.username}</Text></Text>
</div>
<Form layout="vertical">
<Form.Item
label="新密码"
extra="留空则重置为默认密码 username@666"
>
<Input.Password
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="留空则使用默认密码"
/>
</Form.Item>
</Form>
</Modal>
</div>
);
}
+58
View File
@@ -520,4 +520,62 @@ export const mcpPluginApi = {
// 调用工具
callTool: (data: MCPToolCallRequest) =>
api.post<unknown, MCPToolCallResponse>('/mcp/call', data),
};
// 管理员API
export const adminApi = {
// 获取用户列表
getUsers: () =>
api.get<unknown, { total: number; users: User[] }>('/admin/users'),
// 添加用户
createUser: (data: {
username: string;
display_name: string;
password?: string;
avatar_url?: string;
trust_level?: number;
is_admin?: boolean;
}) =>
api.post<unknown, {
success: boolean;
message: string;
user: User;
default_password?: string;
}>('/admin/users', data),
// 编辑用户
updateUser: (userId: string, data: {
display_name?: string;
avatar_url?: string;
trust_level?: number;
}) =>
api.put<unknown, {
success: boolean;
message: string;
user: User;
}>(`/admin/users/${userId}`, data),
// 切换用户状态(启用/禁用)
toggleUserStatus: (userId: string, isActive: boolean) =>
api.post<unknown, {
success: boolean;
message: string;
is_active: boolean;
}>(`/admin/users/${userId}/toggle-status`, { is_active: isActive }),
// 重置密码
resetPassword: (userId: string, newPassword?: string) =>
api.post<unknown, {
success: boolean;
message: string;
new_password: string;
}>(`/admin/users/${userId}/reset-password`, { new_password: newPassword }),
// 删除用户
deleteUser: (userId: string) =>
api.delete<unknown, {
success: boolean;
message: string;
}>(`/admin/users/${userId}`),
};