Settings.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. import {
  2. Alert,
  3. Button,
  4. Checkbox,
  5. Col,
  6. Descriptions,
  7. Form,
  8. Input,
  9. InputNumber,
  10. Modal,
  11. Row,
  12. Select,
  13. Space,
  14. Switch,
  15. Table,
  16. Tag,
  17. Tooltip,
  18. Typography,
  19. message,
  20. } from 'antd';
  21. import {
  22. Bot,
  23. CirclePlus,
  24. KeyRound,
  25. Pencil,
  26. PlugZap,
  27. RefreshCw,
  28. Save,
  29. ShieldCheck,
  30. Trash2,
  31. } from 'lucide-react';
  32. import { useCallback, useEffect, useState } from 'react';
  33. import { PageHeader, Surface } from '@/components/Page';
  34. import { apiRequest, jsonOptions, setCsrfToken, setSelectedBotId } from '@/services/api';
  35. import type {
  36. AdminUser,
  37. BotPermissionDefinition,
  38. BotProfile,
  39. BotRole,
  40. TelegramSettings,
  41. } from '@/types';
  42. interface SystemSettings {
  43. web_address: string;
  44. bind_host: string;
  45. upload_limit_mb: number;
  46. supervisor_mode: boolean;
  47. bot: { connected: boolean; id: string; username: string; name: string };
  48. }
  49. interface BotFormValues {
  50. label: string;
  51. bot_token?: string;
  52. enabled: boolean;
  53. sudo_users_id?: string;
  54. log_group_id?: string;
  55. gban_log_group_id?: string;
  56. message_dump_chat?: string;
  57. role_ids: string[];
  58. }
  59. interface RoleFormValues {
  60. name: string;
  61. description?: string;
  62. permissions: string[];
  63. }
  64. const TELEGRAM_APP_URL = 'https://my.telegram.org/apps';
  65. const API_HASH_PATTERN = /^[A-Fa-f0-9]{32}$/;
  66. function runtimeTag(profile: BotProfile) {
  67. if (!profile.enabled) return <Tag>已停用</Tag>;
  68. if (!profile.ready_to_connect) return <Tag color="warning">配置未完成</Tag>;
  69. const state = profile.runtime?.state || 'stopped';
  70. const values: Record<string, { color: string; text: string }> = {
  71. running: { color: 'success', text: '运行中' },
  72. starting: { color: 'processing', text: '启动中' },
  73. stopping: { color: 'processing', text: '停止中' },
  74. failed: { color: 'error', text: '启动失败' },
  75. stopped: { color: 'default', text: '已停止' },
  76. };
  77. const current = values[state] || values.stopped;
  78. return (
  79. <Tooltip title={profile.runtime?.error || undefined}>
  80. <Tag color={current.color}>{current.text}</Tag>
  81. </Tooltip>
  82. );
  83. }
  84. export default function SettingsPage() {
  85. const [settings, setSettings] = useState<SystemSettings | null>(null);
  86. const [telegram, setTelegram] = useState<TelegramSettings | null>(null);
  87. const [editingBot, setEditingBot] = useState<BotProfile | null>(null);
  88. const [botModalOpen, setBotModalOpen] = useState(false);
  89. const [editingRole, setEditingRole] = useState<BotRole | null>(null);
  90. const [roleModalOpen, setRoleModalOpen] = useState(false);
  91. const [globalForm] = Form.useForm();
  92. const [botForm] = Form.useForm<BotFormValues>();
  93. const [roleForm] = Form.useForm<RoleFormValues>();
  94. const [passwordForm] = Form.useForm();
  95. const reloadTelegram = useCallback(async () => {
  96. const current = await apiRequest<TelegramSettings>('/settings/telegram');
  97. setTelegram(current);
  98. globalForm.setFieldsValue({ api_id: current.api_id || undefined, api_hash: '' });
  99. window.dispatchEvent(new Event('bot-profiles-changed'));
  100. }, [globalForm]);
  101. useEffect(() => {
  102. apiRequest<SystemSettings>('/settings').then(setSettings).catch((error) => message.error(error.message));
  103. reloadTelegram().catch((error) => message.error(error.message));
  104. }, [reloadTelegram]);
  105. const openBotModal = (profile?: BotProfile) => {
  106. const target = profile || null;
  107. setEditingBot(target);
  108. botForm.setFieldsValue(
  109. target
  110. ? {
  111. label: target.label,
  112. bot_token: '',
  113. enabled: target.enabled,
  114. sudo_users_id: target.sudo_users_id.join(' '),
  115. log_group_id: target.log_group_id,
  116. gban_log_group_id: target.gban_log_group_id,
  117. message_dump_chat: target.message_dump_chat,
  118. role_ids: target.role_ids || ['full_access'],
  119. }
  120. : {
  121. label: '',
  122. bot_token: '',
  123. enabled: true,
  124. sudo_users_id: '',
  125. log_group_id: '0',
  126. gban_log_group_id: '0',
  127. message_dump_chat: '0',
  128. role_ids: ['full_access'],
  129. },
  130. );
  131. setBotModalOpen(true);
  132. };
  133. const saveBot = (values: BotFormValues) => {
  134. Modal.confirm({
  135. title: editingBot ? '保存并重启该机器人?' : '创建并启动该机器人?',
  136. content: '启用且配置完整时,系统会立即连接 Telegram。',
  137. okText: '确认保存',
  138. cancelText: '取消',
  139. onOk: async () => {
  140. const path = editingBot ? `/bots/${editingBot.bot_id}` : '/bots';
  141. const method = editingBot ? 'PUT' : 'POST';
  142. const saved = await apiRequest<BotProfile>(
  143. path,
  144. jsonOptions(method, { ...values, confirm: true }),
  145. );
  146. setBotModalOpen(false);
  147. message.success(editingBot ? '机器人配置已更新' : '机器人已创建');
  148. await reloadTelegram();
  149. if (!editingBot) setSelectedBotId(saved.bot_id);
  150. },
  151. });
  152. };
  153. const openRoleModal = (role?: BotRole) => {
  154. const target = role || null;
  155. setEditingRole(target);
  156. roleForm.setFieldsValue(
  157. target
  158. ? {
  159. name: target.name,
  160. description: target.description,
  161. permissions: target.permissions,
  162. }
  163. : {
  164. name: '',
  165. description: '',
  166. permissions: [],
  167. },
  168. );
  169. setRoleModalOpen(true);
  170. };
  171. const saveRole = (values: RoleFormValues) => {
  172. Modal.confirm({
  173. title: editingRole ? '保存角色权限?' : '创建职责角色?',
  174. content: editingRole
  175. ? '使用该角色的机器人会自动重启并应用新权限。'
  176. : '创建后可将角色分配给一个或多个机器人。',
  177. okText: '确认保存',
  178. cancelText: '取消',
  179. onOk: async () => {
  180. const path = editingRole ? `/roles/${editingRole.role_id}` : '/roles';
  181. const method = editingRole ? 'PUT' : 'POST';
  182. await apiRequest<BotRole>(
  183. path,
  184. jsonOptions(method, { ...values, confirm: true }),
  185. );
  186. setRoleModalOpen(false);
  187. message.success(editingRole ? '角色权限已更新' : '职责角色已创建');
  188. await reloadTelegram();
  189. },
  190. });
  191. };
  192. const roleNames = new Map(
  193. (telegram?.roles || []).map((role) => [role.role_id, role.name]),
  194. );
  195. const permissionNames = new Map(
  196. (telegram?.permission_catalog || []).map((permission) => [
  197. permission.key,
  198. permission.name,
  199. ]),
  200. );
  201. const permissionGroups = (telegram?.permission_catalog || []).reduce<
  202. Record<string, BotPermissionDefinition[]>
  203. >((groups, permission) => {
  204. (groups[permission.group] ||= []).push(permission);
  205. return groups;
  206. }, {});
  207. const botColumns = [
  208. {
  209. title: '机器人',
  210. key: 'bot',
  211. render: (_: unknown, profile: BotProfile) => (
  212. <div>
  213. <strong>{profile.label}</strong>
  214. <div style={{ color: '#52606d', marginTop: 2 }}>
  215. {profile.identity?.username ? `@${profile.identity.username}` : '尚未验证身份'}
  216. </div>
  217. </div>
  218. ),
  219. },
  220. {
  221. title: '令牌',
  222. dataIndex: 'bot_token_configured',
  223. width: 96,
  224. render: (configured: boolean) => (
  225. <Tag color={configured ? 'success' : 'warning'}>{configured ? '已配置' : '未配置'}</Tag>
  226. ),
  227. },
  228. {
  229. title: '连接状态',
  230. key: 'runtime',
  231. width: 120,
  232. render: (_: unknown, profile: BotProfile) => runtimeTag(profile),
  233. },
  234. {
  235. title: '职责角色',
  236. dataIndex: 'role_ids',
  237. minWidth: 180,
  238. render: (roleIds: string[]) => (
  239. <Space size={[4, 4]} wrap>
  240. {(roleIds || []).map((roleId) => (
  241. <Tag key={roleId}>{roleNames.get(roleId) || roleId}</Tag>
  242. ))}
  243. {!roleIds?.length ? <Tag color="warning">未分配职责</Tag> : null}
  244. </Space>
  245. ),
  246. },
  247. {
  248. title: '媒体中转群',
  249. dataIndex: 'message_dump_chat',
  250. width: 150,
  251. render: (value: string) => (value && value !== '0' ? value : '未配置'),
  252. },
  253. {
  254. title: '操作',
  255. key: 'actions',
  256. width: 190,
  257. fixed: 'right' as const,
  258. render: (_: unknown, profile: BotProfile) => (
  259. <Space size={4}>
  260. <Tooltip title="验证机器人令牌">
  261. <Button
  262. aria-label="验证机器人令牌"
  263. icon={<PlugZap size={16} />}
  264. disabled={!profile.bot_token_configured}
  265. onClick={() =>
  266. Modal.confirm({
  267. title: `验证 ${profile.label} 的机器人令牌?`,
  268. okText: '开始验证',
  269. cancelText: '取消',
  270. onOk: async () => {
  271. const identity = await apiRequest<{ username: string }>(
  272. `/bots/${profile.bot_id}/test`,
  273. jsonOptions('POST', { confirm: true }),
  274. );
  275. message.success(`机器人令牌有效:@${identity.username}`);
  276. await reloadTelegram();
  277. },
  278. })
  279. }
  280. />
  281. </Tooltip>
  282. <Tooltip title="重启机器人">
  283. <Button
  284. aria-label="重启机器人"
  285. icon={<RefreshCw size={16} />}
  286. disabled={!profile.enabled || !profile.ready_to_connect}
  287. onClick={() =>
  288. Modal.confirm({
  289. title: `重启 ${profile.label}?`,
  290. okText: '确认重启',
  291. cancelText: '取消',
  292. onOk: async () => {
  293. await apiRequest(
  294. `/bots/${profile.bot_id}/restart`,
  295. jsonOptions('POST', { confirm: true }),
  296. );
  297. message.success('机器人正在重启');
  298. await reloadTelegram();
  299. },
  300. })
  301. }
  302. />
  303. </Tooltip>
  304. <Tooltip title="编辑机器人">
  305. <Button
  306. aria-label="编辑机器人"
  307. icon={<Pencil size={16} />}
  308. onClick={() => openBotModal(profile)}
  309. />
  310. </Tooltip>
  311. <Tooltip title="删除机器人">
  312. <Button
  313. danger
  314. aria-label="删除机器人"
  315. icon={<Trash2 size={16} />}
  316. onClick={() =>
  317. Modal.confirm({
  318. title: `删除 ${profile.label}?`,
  319. content: '工作进程会停止,群数据和操作日志不会被删除。',
  320. okText: '确认删除',
  321. okButtonProps: { danger: true },
  322. cancelText: '取消',
  323. onOk: async () => {
  324. await apiRequest(
  325. `/bots/${profile.bot_id}`,
  326. jsonOptions('DELETE', { confirm: true }),
  327. );
  328. message.success('机器人配置已删除');
  329. await reloadTelegram();
  330. },
  331. })
  332. }
  333. />
  334. </Tooltip>
  335. </Space>
  336. ),
  337. },
  338. ];
  339. const roleColumns = [
  340. {
  341. title: '角色',
  342. key: 'role',
  343. minWidth: 180,
  344. render: (_: unknown, role: BotRole) => (
  345. <div>
  346. <Space size={6}>
  347. <strong>{role.name}</strong>
  348. {role.builtin ? <Tag color="processing">内置</Tag> : <Tag>自定义</Tag>}
  349. </Space>
  350. <div style={{ color: '#52606d', marginTop: 4 }}>{role.description || '无说明'}</div>
  351. </div>
  352. ),
  353. },
  354. {
  355. title: '权限',
  356. dataIndex: 'permissions',
  357. minWidth: 360,
  358. render: (permissions: string[]) => (
  359. <Space size={[4, 4]} wrap>
  360. {permissions.map((permission) => (
  361. <Tag key={permission}>{permissionNames.get(permission) || permission}</Tag>
  362. ))}
  363. {!permissions.length ? <Tag color="warning">无权限</Tag> : null}
  364. </Space>
  365. ),
  366. },
  367. {
  368. title: '操作',
  369. key: 'actions',
  370. width: 110,
  371. fixed: 'right' as const,
  372. render: (_: unknown, role: BotRole) =>
  373. role.builtin ? null : (
  374. <Space size={4}>
  375. <Tooltip title="编辑角色">
  376. <Button
  377. aria-label={`编辑角色 ${role.name}`}
  378. icon={<Pencil size={16} />}
  379. onClick={() => openRoleModal(role)}
  380. />
  381. </Tooltip>
  382. <Tooltip title="删除角色">
  383. <Button
  384. danger
  385. aria-label={`删除角色 ${role.name}`}
  386. icon={<Trash2 size={16} />}
  387. onClick={() =>
  388. Modal.confirm({
  389. title: `删除角色 ${role.name}?`,
  390. content: '已分配给机器人的角色不能删除。',
  391. okText: '确认删除',
  392. okButtonProps: { danger: true },
  393. cancelText: '取消',
  394. onOk: async () => {
  395. await apiRequest(
  396. `/roles/${role.role_id}`,
  397. jsonOptions('DELETE', { confirm: true }),
  398. );
  399. message.success('职责角色已删除');
  400. await reloadTelegram();
  401. },
  402. })
  403. }
  404. />
  405. </Tooltip>
  406. </Space>
  407. ),
  408. },
  409. ];
  410. return (
  411. <>
  412. <PageHeader title="系统设置" />
  413. <Surface
  414. title="机器人配置中心"
  415. actions={
  416. <Button type="primary" icon={<CirclePlus size={16} />} onClick={() => openBotModal()}>
  417. 添加机器人
  418. </Button>
  419. }
  420. >
  421. <Form
  422. form={globalForm}
  423. layout="vertical"
  424. onFinish={(values) =>
  425. Modal.confirm({
  426. title: '更新 Telegram 应用接口凭据?',
  427. content: '所有启用的机器人会依次重启并应用新配置。',
  428. okText: '确认更新',
  429. cancelText: '取消',
  430. onOk: async () => {
  431. await apiRequest(
  432. '/settings/telegram',
  433. jsonOptions('PUT', { ...values, confirm: true }),
  434. );
  435. message.success('Telegram 应用接口配置已更新');
  436. await reloadTelegram();
  437. },
  438. })
  439. }
  440. >
  441. <Row gutter={16} align="bottom">
  442. <Col xs={24} md={8}>
  443. <Form.Item
  444. name="api_id"
  445. label="Telegram 应用编号(API ID)"
  446. extra={
  447. <span>
  448. 来自{' '}
  449. <a href={TELEGRAM_APP_URL} target="_blank" rel="noreferrer">
  450. my.telegram.org/apps
  451. </a>
  452. ,不是机器人编号。
  453. </span>
  454. }
  455. rules={[{ required: true, message: '请输入 Telegram 应用编号' }]}
  456. >
  457. <InputNumber min={1} precision={0} style={{ width: '100%' }} />
  458. </Form.Item>
  459. </Col>
  460. <Col xs={24} md={10}>
  461. <Form.Item
  462. name="api_hash"
  463. label="Telegram 应用密钥(API Hash)"
  464. extra="32 位十六进制字符串,不是机器人令牌。"
  465. rules={[
  466. {
  467. required: !telegram?.api_hash_configured,
  468. message: '请输入 Telegram 应用密钥',
  469. },
  470. {
  471. pattern: API_HASH_PATTERN,
  472. message: 'Telegram 应用密钥必须是 32 位十六进制字符串',
  473. },
  474. ]}
  475. >
  476. <Input.Password
  477. autoComplete="new-password"
  478. placeholder={
  479. telegram?.api_hash_configured ? '已配置,留空保持不变' : '请输入 Telegram 应用密钥'
  480. }
  481. />
  482. </Form.Item>
  483. </Col>
  484. <Col xs={24} md={6}>
  485. <Form.Item>
  486. <Button type="primary" htmlType="submit" icon={<Save size={16} />} block>
  487. 保存 API 配置
  488. </Button>
  489. </Form.Item>
  490. </Col>
  491. </Row>
  492. </Form>
  493. {!telegram?.api_ready ? (
  494. <Alert
  495. type="warning"
  496. showIcon
  497. message="Telegram 应用接口凭据尚未配置"
  498. description="填写 my.telegram.org/apps 提供的应用编号和应用密钥后,已启用的机器人才会启动。"
  499. />
  500. ) : null}
  501. <div className="table-wrap" style={{ marginTop: 16 }}>
  502. <Table<BotProfile>
  503. rowKey="bot_id"
  504. columns={botColumns}
  505. dataSource={telegram?.bots || []}
  506. pagination={false}
  507. scroll={{ x: 980 }}
  508. locale={{ emptyText: '尚未添加机器人' }}
  509. />
  510. </div>
  511. </Surface>
  512. <div style={{ height: 16 }} />
  513. <Surface
  514. title="职责角色"
  515. actions={
  516. <Button type="primary" icon={<CirclePlus size={16} />} onClick={() => openRoleModal()}>
  517. 新建角色
  518. </Button>
  519. }
  520. >
  521. <Alert
  522. type="info"
  523. showIcon
  524. message="机器人可同时分配多个角色,最终权限为所有角色权限的并集。"
  525. style={{ marginBottom: 16 }}
  526. />
  527. <div className="table-wrap">
  528. <Table<BotRole>
  529. rowKey="role_id"
  530. columns={roleColumns}
  531. dataSource={telegram?.roles || []}
  532. pagination={false}
  533. scroll={{ x: 760 }}
  534. />
  535. </div>
  536. </Surface>
  537. <div style={{ height: 16 }} />
  538. <div className="split-grid">
  539. <Surface title="运行信息">
  540. {settings ? (
  541. <Descriptions column={1} size="small" bordered>
  542. <Descriptions.Item label="本地地址">{settings.web_address}</Descriptions.Item>
  543. <Descriptions.Item label="监听地址">{settings.bind_host}</Descriptions.Item>
  544. <Descriptions.Item label="上传限制">{settings.upload_limit_mb} MB</Descriptions.Item>
  545. <Descriptions.Item label="运行模式">
  546. <Tag color={settings.supervisor_mode ? 'processing' : 'success'}>
  547. {settings.supervisor_mode ? '多机器人监管' : '机器人工作进程'}
  548. </Tag>
  549. </Descriptions.Item>
  550. </Descriptions>
  551. ) : null}
  552. </Surface>
  553. <Surface title="管理员密码" className="danger-zone">
  554. <Form
  555. form={passwordForm}
  556. layout="vertical"
  557. onFinish={async (values) => {
  558. const user = await apiRequest<AdminUser>(
  559. '/auth/password',
  560. jsonOptions('PUT', values),
  561. );
  562. setCsrfToken(user.csrf_token);
  563. passwordForm.resetFields();
  564. message.success('管理员密码已更新,其他会话已失效');
  565. }}
  566. >
  567. <Form.Item name="current_password" label="当前密码" rules={[{ required: true }]}>
  568. <Input.Password autoComplete="current-password" />
  569. </Form.Item>
  570. <Form.Item
  571. name="new_password"
  572. label="新密码"
  573. rules={[
  574. { required: true },
  575. { min: 10, message: '至少 10 个字符' },
  576. { pattern: /[A-Za-z]/, message: '至少包含一个字母' },
  577. { pattern: /\d/, message: '至少包含一个数字' },
  578. ]}
  579. >
  580. <Input.Password autoComplete="new-password" />
  581. </Form.Item>
  582. <Button type="primary" danger htmlType="submit" icon={<KeyRound size={16} />}>
  583. 修改密码
  584. </Button>
  585. </Form>
  586. </Surface>
  587. </div>
  588. <div style={{ height: 16 }} />
  589. <Surface title="安全策略">
  590. <Descriptions column={{ xs: 1, sm: 2, md: 3 }} size="small">
  591. <Descriptions.Item label="机器人凭据">
  592. <Bot size={16} /> 本地 0600 文件
  593. </Descriptions.Item>
  594. <Descriptions.Item label="密码摘要">
  595. <ShieldCheck size={16} /> scrypt + 独立盐
  596. </Descriptions.Item>
  597. <Descriptions.Item label="写操作">跨站请求防护校验</Descriptions.Item>
  598. </Descriptions>
  599. </Surface>
  600. <Modal
  601. title={editingBot ? `编辑 ${editingBot.label}` : '添加机器人'}
  602. open={botModalOpen}
  603. onCancel={() => setBotModalOpen(false)}
  604. onOk={() => botForm.submit()}
  605. okText="继续"
  606. cancelText="取消"
  607. destroyOnHidden
  608. >
  609. <Form form={botForm} layout="vertical" onFinish={saveBot} preserve={false}>
  610. <Form.Item name="label" label="机器人名称" rules={[{ required: true }, { max: 60 }]}>
  611. <Input maxLength={60} />
  612. </Form.Item>
  613. <Form.Item
  614. name="bot_token"
  615. label="机器人令牌"
  616. extra="填写 BotFather 提供的完整令牌,每个机器人单独配置。"
  617. rules={editingBot ? [] : [{ required: true }]}
  618. >
  619. <Input.Password
  620. autoComplete="new-password"
  621. placeholder={editingBot?.bot_token_configured ? '已配置,留空保持不变' : ''}
  622. />
  623. </Form.Item>
  624. <Form.Item name="enabled" label="启用" valuePropName="checked">
  625. <Switch />
  626. </Form.Item>
  627. <Form.Item
  628. name="role_ids"
  629. label="职责角色"
  630. extra="可多选;不选择角色时机器人只保持连接,不执行管理职责。"
  631. >
  632. <Select
  633. mode="multiple"
  634. allowClear
  635. placeholder="选择一个或多个职责角色"
  636. options={(telegram?.roles || []).map((role) => ({
  637. value: role.role_id,
  638. label: role.name,
  639. }))}
  640. />
  641. </Form.Item>
  642. <Form.Item name="sudo_users_id" label="超级管理员用户 ID">
  643. <Input placeholder="多个 ID 使用空格分隔" />
  644. </Form.Item>
  645. <Row gutter={12}>
  646. <Col xs={24} sm={12}>
  647. <Form.Item name="log_group_id" label="日志群 ID">
  648. <Input />
  649. </Form.Item>
  650. </Col>
  651. <Col xs={24} sm={12}>
  652. <Form.Item name="gban_log_group_id" label="封禁日志群 ID">
  653. <Input />
  654. </Form.Item>
  655. </Col>
  656. </Row>
  657. <Form.Item name="message_dump_chat" label="媒体中转群 ID">
  658. <Input />
  659. </Form.Item>
  660. </Form>
  661. </Modal>
  662. <Modal
  663. title={editingRole ? `编辑角色 ${editingRole.name}` : '新建职责角色'}
  664. open={roleModalOpen}
  665. onCancel={() => setRoleModalOpen(false)}
  666. onOk={() => roleForm.submit()}
  667. okText="继续"
  668. cancelText="取消"
  669. width={720}
  670. destroyOnHidden
  671. >
  672. <Form form={roleForm} layout="vertical" onFinish={saveRole} preserve={false}>
  673. <Form.Item
  674. name="name"
  675. label="角色名称"
  676. rules={[{ required: true, message: '请输入角色名称' }, { max: 60 }]}
  677. >
  678. <Input maxLength={60} />
  679. </Form.Item>
  680. <Form.Item name="description" label="角色说明" rules={[{ max: 200 }]}>
  681. <Input.TextArea maxLength={200} rows={2} />
  682. </Form.Item>
  683. <Form.Item name="permissions" label="职责权限">
  684. <Checkbox.Group style={{ width: '100%' }}>
  685. <Space direction="vertical" size={14} style={{ width: '100%' }}>
  686. {Object.entries(permissionGroups).map(([group, permissions]) => (
  687. <div key={group}>
  688. <Typography.Text strong>{group}</Typography.Text>
  689. <Row gutter={[12, 8]} style={{ marginTop: 8 }}>
  690. {permissions.map((permission) => (
  691. <Col xs={24} sm={12} key={permission.key}>
  692. <Checkbox value={permission.key}>
  693. <span>{permission.name}</span>
  694. <Typography.Text
  695. type="secondary"
  696. style={{ display: 'block', fontSize: 12 }}
  697. >
  698. {permission.description}
  699. </Typography.Text>
  700. </Checkbox>
  701. </Col>
  702. ))}
  703. </Row>
  704. </div>
  705. ))}
  706. </Space>
  707. </Checkbox.Group>
  708. </Form.Item>
  709. </Form>
  710. </Modal>
  711. </>
  712. );
  713. }