162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
import { Button, Card, Form, Input, Modal, Space, Table, Typography, message } from 'antd';
|
||
import { useEffect, useMemo, useState } from 'react';
|
||
import { api } from '../lib/api';
|
||
|
||
type UserRow = {
|
||
id: string;
|
||
username: string;
|
||
nickname: string;
|
||
email?: string;
|
||
email_verified: boolean;
|
||
phone?: string;
|
||
role: number;
|
||
created_at: string;
|
||
};
|
||
|
||
type ResetValues = {
|
||
newPassword: string;
|
||
};
|
||
|
||
export function UsersPage() {
|
||
const [loading, setLoading] = useState(false);
|
||
const [rows, setRows] = useState<UserRow[]>([]);
|
||
const [resetOpen, setResetOpen] = useState(false);
|
||
const [resetUserId, setResetUserId] = useState<string | null>(null);
|
||
const [resetForm] = Form.useForm<ResetValues>();
|
||
|
||
const columns = useMemo(
|
||
() => [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 90 },
|
||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||
{ title: '昵称', dataIndex: 'nickname', key: 'nickname' },
|
||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||
{
|
||
title: '邮箱验证',
|
||
dataIndex: 'email_verified',
|
||
key: 'email_verified',
|
||
render: (v: boolean) => (v ? '已验证' : '未验证'),
|
||
width: 100,
|
||
},
|
||
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
|
||
{ title: '角色', dataIndex: 'role', key: 'role', width: 80 },
|
||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 160,
|
||
render: (_: any, row: UserRow) => (
|
||
<Space>
|
||
<Button
|
||
size="small"
|
||
onClick={() => {
|
||
setResetUserId(row.id);
|
||
resetForm.resetFields();
|
||
setResetOpen(true);
|
||
}}
|
||
>
|
||
重置密码
|
||
</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
],
|
||
[resetForm],
|
||
);
|
||
|
||
const load = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const res = await api.listUsers(200, 0);
|
||
const users = res?.data?.users || [];
|
||
setRows(
|
||
users.map((u: any) => ({
|
||
id: u.id,
|
||
username: u.username,
|
||
nickname: u.nickname,
|
||
email: u.email || undefined,
|
||
email_verified: Boolean(u.email_verified),
|
||
phone: u.phone || undefined,
|
||
role: u.role,
|
||
created_at: u.created_at,
|
||
})),
|
||
);
|
||
} catch (e: any) {
|
||
message.error(e?.message || '加载失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
void load();
|
||
}, []);
|
||
|
||
const onResetOk = async () => {
|
||
try {
|
||
const values = await resetForm.validateFields();
|
||
if (!resetUserId) return;
|
||
|
||
await api.resetUserPassword(resetUserId, values.newPassword);
|
||
message.success('密码已重置');
|
||
setResetOpen(false);
|
||
} catch (e: any) {
|
||
if (e?.errorFields) return;
|
||
message.error(e?.message || '重置失败');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Card>
|
||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||
用户管理
|
||
</Typography.Title>
|
||
<Button onClick={load} loading={loading}>
|
||
刷新
|
||
</Button>
|
||
</Space>
|
||
|
||
<Table
|
||
rowKey="id"
|
||
loading={loading}
|
||
columns={columns as any}
|
||
dataSource={rows}
|
||
pagination={false}
|
||
/>
|
||
</Space>
|
||
|
||
<Modal
|
||
title={`重置密码${resetUserId ? `(用户ID: ${resetUserId})` : ''}`}
|
||
open={resetOpen}
|
||
onOk={onResetOk}
|
||
onCancel={() => setResetOpen(false)}
|
||
okText="确认"
|
||
cancelText="取消"
|
||
>
|
||
<Form form={resetForm} layout="vertical">
|
||
<Form.Item
|
||
label="新密码"
|
||
name="newPassword"
|
||
rules={[
|
||
{ required: true, message: '请输入新密码' },
|
||
{ min: 8, message: '至少8位' },
|
||
{
|
||
validator: (_, v) => {
|
||
const hasLetter = /[a-zA-Z]/.test(v || '');
|
||
const hasNumber = /\d/.test(v || '');
|
||
if (!v) return Promise.resolve();
|
||
if (!hasLetter || !hasNumber) return Promise.reject(new Error('必须包含字母和数字'));
|
||
return Promise.resolve();
|
||
},
|
||
},
|
||
]}
|
||
>
|
||
<Input.Password placeholder="例如 NewPass1234" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</Card>
|
||
);
|
||
}
|