feat: integrate invitation access, world NPCs, and deployment

This commit is contained in:
2026-09-08 22:22:38 +08:00
parent 2a3125075f
commit 513a3eba31
75 changed files with 9566 additions and 1070 deletions

View 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>;
}