a393b5c0e3
- 系统配置页支持 bool 类型(Switch 控件),供「提现自动对账」「比价期广告」等运营开关使用 - message / Modal.confirm 静态调用迁移到 App.useApp() 的 message / modal,消除 React 19 "Static function can not consume context" 告警;providers 用 <App> 容器包裹全站 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: OuYingJun1024 <1034284404@qq.com> Reviewed-on: #12 Co-authored-by: ouzhou <ouzhou@wonderable.ai> Co-committed-by: ouzhou <ouzhou@wonderable.ai>
144 lines
4.2 KiB
TypeScript
144 lines
4.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
import { App, Button, Form, Input, Modal, Select, Table, Tag } from 'antd';
|
|
import { api, errMsg } from '@/lib/api';
|
|
import { formatUtcTime } from '@/lib/format';
|
|
import type { AdminInfo } from '@/lib/types';
|
|
|
|
const ROLES = [
|
|
{ value: 'super_admin', label: 'super_admin' },
|
|
{ value: 'finance', label: 'finance' },
|
|
{ value: 'operator', label: 'operator' },
|
|
];
|
|
// last_login_at 为 UTC 口径(datetime.now(utc)),按北京显示(勿用 new Date().toLocaleString)
|
|
const dt = (v: string | null) => (v ? formatUtcTime(v, 'YYYY-MM-DD HH:mm:ss') : '从未');
|
|
|
|
export default function AdminsPage() {
|
|
const { message, modal } = App.useApp();
|
|
const [admins, setAdmins] = useState<AdminInfo[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
const [form] = Form.useForm();
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const { data } = await api.get<AdminInfo[]>('/admin/api/admins');
|
|
setAdmins(data);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
load();
|
|
}, []);
|
|
|
|
const create = async () => {
|
|
const v = await form.validateFields();
|
|
try {
|
|
await api.post('/admin/api/admins', v);
|
|
message.success('已创建');
|
|
setCreateOpen(false);
|
|
form.resetFields();
|
|
load();
|
|
} catch (e) {
|
|
message.error(errMsg(e));
|
|
}
|
|
};
|
|
|
|
const changeRole = (a: AdminInfo, role: string) => {
|
|
if (role === a.role) return;
|
|
modal.confirm({
|
|
title: `把 ${a.username} 的角色改为 ${role}?`,
|
|
onOk: async () => {
|
|
try {
|
|
await api.patch(`/admin/api/admins/${a.id}`, { role });
|
|
message.success('已更新');
|
|
load();
|
|
} catch (e) {
|
|
message.error(errMsg(e));
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const toggle = (a: AdminInfo) => {
|
|
const next = a.status === 'active' ? 'disabled' : 'active';
|
|
modal.confirm({
|
|
title: `${next === 'disabled' ? '禁用' : '启用'}管理员 ${a.username}?`,
|
|
onOk: async () => {
|
|
try {
|
|
await api.patch(`/admin/api/admins/${a.id}`, { status: next });
|
|
message.success('已更新');
|
|
load();
|
|
} catch (e) {
|
|
message.error(errMsg(e));
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const columns: ColumnsType<AdminInfo> = [
|
|
{ title: 'ID', dataIndex: 'id', width: 70 },
|
|
{ title: '用户名', dataIndex: 'username' },
|
|
{
|
|
title: '角色',
|
|
dataIndex: 'role',
|
|
render: (r: string, a: AdminInfo) => (
|
|
<Select
|
|
size="small"
|
|
value={r}
|
|
style={{ width: 140 }}
|
|
options={ROLES}
|
|
onChange={(v) => changeRole(a, v)}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
render: (s: string) => <Tag color={s === 'active' ? 'green' : 'red'}>{s}</Tag>,
|
|
},
|
|
{ title: '最后登录', dataIndex: 'last_login_at', render: dt },
|
|
{
|
|
title: '操作',
|
|
key: 'op',
|
|
render: (_: unknown, a: AdminInfo) => (
|
|
<a onClick={() => toggle(a)}>{a.status === 'active' ? '禁用' : '启用'}</a>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<h2>管理员账号</h2>
|
|
<Button type="primary" style={{ marginBottom: 16 }} onClick={() => setCreateOpen(true)}>
|
|
新建管理员
|
|
</Button>
|
|
<Table rowKey="id" columns={columns} dataSource={admins} loading={loading} pagination={false} />
|
|
|
|
<Modal
|
|
title="新建管理员"
|
|
open={createOpen}
|
|
onOk={create}
|
|
onCancel={() => setCreateOpen(false)}
|
|
destroyOnClose
|
|
>
|
|
<Form form={form} layout="vertical" initialValues={{ role: 'operator' }}>
|
|
<Form.Item name="username" label="用户名" rules={[{ required: true, min: 3 }]}>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="password" label="密码(≥8 位)" rules={[{ required: true, min: 8 }]}>
|
|
<Input.Password />
|
|
</Form.Item>
|
|
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
|
|
<Select options={ROLES} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|