|
|
@@ -6,7 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
|
|
|
import dayjs, { type Dayjs } from 'dayjs';
|
|
|
import { history, useParams } from '@umijs/max';
|
|
|
import { ArrowLeft, FileUp, Plus, RefreshCw } from 'lucide-react';
|
|
|
-import { useEffect, useState } from 'react';
|
|
|
+import { useEffect, useRef, useState } from 'react';
|
|
|
|
|
|
import { PageHeader, Surface } from '@/components/Page';
|
|
|
import { useBotAccess } from '@/contexts/BotAccess';
|
|
|
@@ -183,6 +183,11 @@ function ChannelProfile({ chatId, overview, onChanged }: { chatId: string; overv
|
|
|
|
|
|
function ChannelAdmins({ chatId }: { chatId: string }) {
|
|
|
const [admins, setAdmins] = useState<ChatMember[]>([]);
|
|
|
+ const [candidates, setCandidates] = useState<ChatMember[]>([]);
|
|
|
+ const [candidateQuery, setCandidateQuery] = useState('');
|
|
|
+ const [candidatesLoading, setCandidatesLoading] = useState(false);
|
|
|
+ const [selectedCandidate, setSelectedCandidate] = useState<ChatMember | null>(null);
|
|
|
+ const candidateRequest = useRef(0);
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
const [open, setOpen] = useState(false);
|
|
|
const [form] = Form.useForm();
|
|
|
@@ -193,11 +198,42 @@ function ChannelAdmins({ chatId }: { chatId: string }) {
|
|
|
finally { setLoading(false); }
|
|
|
};
|
|
|
useEffect(() => { void load(); }, [chatId]);
|
|
|
+ const searchCandidates = async (query = '') => {
|
|
|
+ const request = ++candidateRequest.current;
|
|
|
+ setCandidatesLoading(true);
|
|
|
+ try {
|
|
|
+ const params = new URLSearchParams({ limit: '30' });
|
|
|
+ if (query.trim()) params.set('query', query.trim());
|
|
|
+ const result = await apiRequest<{ items: ChatMember[] }>(`/channels/${chatId}/admins/candidates?${params}`);
|
|
|
+ if (request === candidateRequest.current) setCandidates(result.items);
|
|
|
+ } catch (error) {
|
|
|
+ if (request === candidateRequest.current) {
|
|
|
+ setCandidates([]);
|
|
|
+ message.error(errorText(error));
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ if (request === candidateRequest.current) setCandidatesLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const openEditor = (admin?: ChatMember) => {
|
|
|
+ form.resetFields();
|
|
|
+ form.setFieldsValue({ user_id: admin?.user.id, privileges: admin?.privileges || [] });
|
|
|
+ setSelectedCandidate(null);
|
|
|
+ setCandidateQuery('');
|
|
|
+ setCandidates([]);
|
|
|
+ setOpen(true);
|
|
|
+ void searchCandidates();
|
|
|
+ };
|
|
|
+ const closeEditor = () => {
|
|
|
+ candidateRequest.current += 1;
|
|
|
+ setCandidatesLoading(false);
|
|
|
+ setOpen(false);
|
|
|
+ };
|
|
|
const submit = async (values: { user_id: string; privileges: string[] }) => {
|
|
|
try {
|
|
|
await apiRequest(`/channels/${chatId}/admins/${values.user_id}`, jsonOptions('PUT', { privileges: values.privileges, confirm: true }));
|
|
|
message.success('管理员已更新');
|
|
|
- setOpen(false);
|
|
|
+ closeEditor();
|
|
|
form.resetFields();
|
|
|
await load();
|
|
|
} catch (error) { message.error(errorText(error)); }
|
|
|
@@ -208,16 +244,40 @@ function ChannelAdmins({ chatId }: { chatId: string }) {
|
|
|
{ title: '身份', render: (_, item) => item.status === 'owner' ? '所有者' : '管理员' },
|
|
|
{ title: '权限', render: (_, item) => item.privileges.join('、') || '-' },
|
|
|
{ title: '操作', render: (_, item) => item.status === 'owner' ? null : <Space>
|
|
|
- <Button type="link" onClick={() => { form.setFieldsValue({ user_id: item.user.id, privileges: item.privileges }); setOpen(true); }}>调整</Button>
|
|
|
+ <Button type="link" onClick={() => openEditor(item)}>调整</Button>
|
|
|
<Popconfirm title="移除该频道管理员?" onConfirm={async () => { try { await apiRequest(`/channels/${chatId}/admins/${item.user.id}`, jsonOptions('DELETE', { confirm: true })); message.success('已移除管理员'); await load(); } catch (error) { message.error(errorText(error)); } }}><Button danger type="link">移除</Button></Popconfirm>
|
|
|
</Space> },
|
|
|
];
|
|
|
- return <Surface title="频道管理员" actions={<Space><Button icon={<RefreshCw size={16} />} loading={loading} onClick={() => void load()}>刷新</Button><Button type="primary" icon={<Plus size={16} />} onClick={() => { form.resetFields(); setOpen(true); }}>添加管理员</Button></Space>}>
|
|
|
- <Alert type="info" showIcon message="只能通过用户 ID 操作已订阅频道的用户,无法浏览全部订阅者。" style={{ marginBottom: 16 }} />
|
|
|
+ return <Surface title="频道管理员" actions={<Space><Button icon={<RefreshCw size={16} />} loading={loading} onClick={() => void load()}>刷新</Button><Button type="primary" icon={<Plus size={16} />} onClick={() => openEditor()}>添加管理员</Button></Space>}>
|
|
|
+ <Alert type="info" showIcon message="可搜索 Telegram 当前可见的订阅者,也可输入用户数字 ID;查询结果不保证是完整名册。" style={{ marginBottom: 16 }} />
|
|
|
<Table rowKey={(item) => item.user.id} columns={columns} dataSource={admins} loading={loading} pagination={false} scroll={{ x: 800 }} />
|
|
|
- <Modal title="添加或调整管理员" open={open} okText="确认调整" onOk={() => form.submit()} onCancel={() => setOpen(false)}>
|
|
|
+ <Modal title="添加或调整管理员" open={open} width={680} okText="确认调整" onOk={() => form.submit()} onCancel={closeEditor}>
|
|
|
+ <Input.Search
|
|
|
+ value={candidateQuery}
|
|
|
+ onChange={(event) => setCandidateQuery(event.target.value)}
|
|
|
+ onSearch={(query) => void searchCandidates(query)}
|
|
|
+ placeholder="按昵称或 @用户名搜索订阅者"
|
|
|
+ enterButton="搜索"
|
|
|
+ maxLength={64}
|
|
|
+ style={{ marginBottom: 12 }}
|
|
|
+ />
|
|
|
+ <Table
|
|
|
+ size="small"
|
|
|
+ rowKey={(item) => item.user.id}
|
|
|
+ loading={candidatesLoading}
|
|
|
+ dataSource={candidates}
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ y: 220 }}
|
|
|
+ locale={{ emptyText: '没有匹配的可见订阅者,可使用下方用户 ID' }}
|
|
|
+ columns={[
|
|
|
+ { title: candidateQuery ? '搜索结果' : '最近可见的订阅者', render: (_, item: ChatMember) => <Space direction="vertical" size={0}><Typography.Text>{userDisplayName(item.user)}</Typography.Text><Typography.Text type="secondary">{item.user.username ? `@${item.user.username} · ` : ''}{item.user.id}</Typography.Text></Space> },
|
|
|
+ { title: '操作', width: 80, render: (_, item: ChatMember) => <Button type="link" onClick={() => { form.setFieldValue('user_id', item.user.id); setSelectedCandidate(item); }}>选择</Button> },
|
|
|
+ ]}
|
|
|
+ style={{ marginBottom: 16 }}
|
|
|
+ />
|
|
|
<Form form={form} layout="vertical" onFinish={(values) => Modal.confirm({ title: '确认修改频道管理员权限?', okText: '确认', onOk: () => submit(values) })}>
|
|
|
- <Form.Item name="user_id" label="用户 ID" rules={[{ required: true, pattern: /^\d+$/, message: '请输入用户数字 ID' }]}><Input /></Form.Item>
|
|
|
+ <Form.Item name="user_id" label="用户 ID(也可手动输入)" rules={[{ required: true, pattern: /^\d+$/, message: '请输入用户数字 ID' }]}><Input onChange={() => setSelectedCandidate(null)} /></Form.Item>
|
|
|
+ {selectedCandidate && <Typography.Text type="secondary">已选择:{userDisplayName(selectedCandidate.user)}</Typography.Text>}
|
|
|
<Form.Item name="privileges" label="管理员权限" rules={[{ required: true, message: '至少选择一项权限' }]}><Checkbox.Group options={privilegeOptions} /></Form.Item>
|
|
|
</Form>
|
|
|
</Modal>
|