feat: integrate invitation access, world NPCs, and deployment
This commit is contained in:
68
client/src/pages/InvitationCodesPage.tsx
Normal file
68
client/src/pages/InvitationCodesPage.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Button, DatePicker, Form, Input, InputNumber, Modal, Space, Table, Tag, Typography, message } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
type Row = { id: string; code: string; max_uses: number; used_count: number; effective_status: string; expires_at?: string; note?: string; created_at: string };
|
||||
|
||||
const statusText: Record<string, [string, string]> = {
|
||||
active: ['可使用', 'green'], exhausted: ['已用完', 'default'], expired: ['已过期', 'orange'], revoked: ['已作废', 'red'],
|
||||
};
|
||||
|
||||
export function InvitationCodesPage() {
|
||||
const [rows, setRows] = useState<Row[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [generated, setGenerated] = useState<string[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try { setRows((await api.listInvitationCodes())?.data?.items || []); }
|
||||
catch (e: any) { message.error(e?.message || '加载失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
useEffect(() => { void load(); }, []);
|
||||
|
||||
const generate = async () => {
|
||||
try {
|
||||
const value = await form.validateFields();
|
||||
const res = await api.generateInvitationCodes({ ...value, expires_at: value.expires_at?.toISOString() });
|
||||
setGenerated((res?.data?.codes || []).map((item: any) => item.code));
|
||||
setOpen(false); form.resetFields(); await load();
|
||||
} catch (e: any) { if (!e?.errorFields) message.error(e?.message || '生成失败'); }
|
||||
};
|
||||
|
||||
const copyGenerated = async () => {
|
||||
await navigator.clipboard.writeText(generated.join('\n'));
|
||||
message.success('已复制全部邀请码');
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '邀请码', dataIndex: 'code' },
|
||||
{ title: '状态', dataIndex: 'effective_status', render: (v: string) => { const item = statusText[v] || [v, 'default']; return <Tag color={item[1]}>{item[0]}</Tag>; } },
|
||||
{ title: '使用量', render: (_: unknown, row: Row) => `${row.used_count} / ${row.max_uses}` },
|
||||
{ title: '有效期至', dataIndex: 'expires_at', render: (v?: string) => v ? new Date(v).toLocaleString() : '永久' },
|
||||
{ title: '备注', dataIndex: 'note' },
|
||||
{ title: '操作', render: (_: unknown, row: Row) => <Button danger size="small" disabled={row.effective_status === 'revoked'} onClick={() => Modal.confirm({ title: '作废该邀请码?', onOk: async () => { await api.revokeInvitationCode(row.id); await load(); } })}>作废</Button> },
|
||||
];
|
||||
|
||||
return <Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>邀请码管理</Typography.Title>
|
||||
<Space><Button onClick={load} loading={loading}>刷新</Button><Button type="primary" onClick={() => setOpen(true)}>生成邀请码</Button></Space>
|
||||
</Space>
|
||||
<Table rowKey="id" columns={columns as any} dataSource={rows} loading={loading} pagination={{ pageSize: 20 }} />
|
||||
<Modal title="生成邀请码" open={open} onOk={generate} onCancel={() => setOpen(false)} okText="生成" cancelText="取消">
|
||||
<Form form={form} layout="vertical" initialValues={{ count: 10, max_uses: 1 }}>
|
||||
<Form.Item name="count" label="数量" rules={[{ required: true }]}><InputNumber min={1} max={200} style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="max_uses" label="每个邀请码可使用次数" rules={[{ required: true }]}><InputNumber min={1} max={10000} style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="expires_at" label="有效期至(可选)"><DatePicker showTime style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="note" label="备注"><Input maxLength={255} placeholder="例如:8 月内测用户" /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal title="邀请码已生成" open={generated.length > 0} onCancel={() => setGenerated([])} footer={<Button type="primary" onClick={copyGenerated}>复制全部</Button>}>
|
||||
<Typography.Paragraph type="warning">完整邀请码只显示这一次,请立即复制保存。</Typography.Paragraph>
|
||||
<Input.TextArea value={generated.join('\n')} autoSize={{ minRows: 5, maxRows: 14 }} readOnly />
|
||||
</Modal>
|
||||
</Space>;
|
||||
}
|
||||
Reference in New Issue
Block a user