Files
whale-town-end/client/src/pages/UsersPage.tsx
2025-12-19 19:17:47 +08:00

162 lines
4.5 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 { 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>
);
}