Files
MuMuAINovel/frontend/src/components/UserMenu.tsx
T
2025-11-12 11:16:32 +08:00

502 lines
15 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.
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 type { User } from '../types';
import type { MenuProps } from 'antd';
const { Text } = Typography;
export default function UserMenu() {
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);
useEffect(() => {
loadCurrentUser();
}, []);
const loadCurrentUser = async () => {
try {
const user = await authApi.getCurrentUser();
setCurrentUser(user);
} catch (error) {
console.error('获取用户信息失败:', error);
}
};
const handleLogout = async () => {
try {
await authApi.logout();
message.success('已退出登录');
window.location.href = '/login';
} catch (error) {
console.error('退出登录失败:', error);
message.error('退出登录失败');
}
};
const handleShowUserManagement = async () => {
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 || '重置密码失败');
}
},
});
};
const handleChangePassword = async (values: { oldPassword: string; newPassword: string }) => {
try {
setChangingPassword(true);
await authApi.setPassword(values.newPassword);
message.success('密码修改成功');
setShowChangePassword(false);
changePasswordForm.resetFields();
} catch (error: any) {
console.error('修改密码失败:', error);
message.error(error.response?.data?.detail || '修改密码失败');
} finally {
setChangingPassword(false);
}
};
const menuItems: MenuProps['items'] = [
{
key: 'user-info',
label: (
<div style={{ padding: '8px 0' }}>
<Text strong>{currentUser?.display_name || currentUser?.username}</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>
Trust Level: {currentUser?.trust_level}
{currentUser?.is_admin && ' · 管理员'}
</Text>
</div>
),
disabled: true,
},
{
type: 'divider',
},
...(currentUser?.is_admin ? [{
key: 'user-management',
icon: <TeamOutlined />,
label: '用户管理',
onClick: handleShowUserManagement,
}, {
type: 'divider' as const,
}] : []),
{
key: 'change-password',
icon: <LockOutlined />,
label: '修改密码',
onClick: () => setShowChangePassword(true),
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
onClick: handleLogout,
},
];
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;
}
return (
<>
<Dropdown menu={{ items: menuItems }} placement="bottomRight">
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '8px 16px',
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
borderRadius: 24,
border: '1px solid rgba(102, 126, 234, 0.2)',
transition: 'all 0.3s ease',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 1)';
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.95)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.1)';
}}
>
<div style={{ position: 'relative' }}>
<Avatar
src={currentUser.avatar_url}
icon={<UserOutlined />}
size={40}
style={{
backgroundColor: '#1890ff',
border: '3px solid #fff',
boxShadow: '0 2px 8px rgba(102, 126, 234, 0.3)',
}}
/>
{currentUser.is_admin && (
<div style={{
position: 'absolute',
bottom: -2,
right: -2,
width: 18,
height: 18,
background: 'linear-gradient(135deg, #ffd700 0%, #ffaa00 100%)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '2px solid white',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
}}>
<CrownOutlined style={{ fontSize: 9, color: '#fff' }} />
</div>
)}
</div>
<Space direction="vertical" size={0} style={{ display: window.innerWidth <= 768 ? 'none' : 'flex' }}>
<Text strong style={{
color: '#262626',
fontSize: 14,
lineHeight: '20px',
}}>
{currentUser.display_name || currentUser.username}
</Text>
<Text style={{
color: '#8c8c8c',
fontSize: 12,
lineHeight: '18px',
}}>
{currentUser.is_admin ? '👑 管理员' : `🎖️ Trust Level ${currentUser.trust_level}`}
</Text>
</Space>
</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}
onCancel={() => {
setShowChangePassword(false);
changePasswordForm.resetFields();
}}
footer={null}
width={480}
centered
>
<Form
form={changePasswordForm}
layout="vertical"
onFinish={handleChangePassword}
autoComplete="off"
>
<Form.Item
label="新密码"
name="newPassword"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6个字符' },
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入新密码(至少6个字符)"
autoComplete="new-password"
/>
</Form.Item>
<Form.Item
label="确认密码"
name="confirmPassword"
dependencies={['newPassword']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请再次输入新密码"
autoComplete="new-password"
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => {
setShowChangePassword(false);
changePasswordForm.resetFields();
}}>
</Button>
<Button type="primary" htmlType="submit" loading={changingPassword}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</>
);
}