From 40a803e7e74c24db02c3375ca6b429efc221dfc9 Mon Sep 17 00:00:00 2001 From: lpf Date: Tue, 10 Mar 2026 09:37:42 +0800 Subject: [PATCH] fix ui --- webui/src/pages/ChannelSettings.tsx | 507 ++++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 webui/src/pages/ChannelSettings.tsx diff --git a/webui/src/pages/ChannelSettings.tsx b/webui/src/pages/ChannelSettings.tsx new file mode 100644 index 0000000..08ab1b8 --- /dev/null +++ b/webui/src/pages/ChannelSettings.tsx @@ -0,0 +1,507 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Check, LogOut, QrCode, RefreshCw, ShieldCheck, Smartphone, Users, Wifi, WifiOff } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { useAppContext } from '../context/AppContext'; +import { useUI } from '../context/UIContext'; + +type ChannelKey = 'telegram' | 'whatsapp' | 'discord' | 'feishu' | 'qq' | 'dingtalk' | 'maixcam'; + +type ChannelField = + | { key: string; type: 'text' | 'password' | 'number'; placeholder?: string } + | { key: string; type: 'boolean' } + | { key: string; type: 'list'; placeholder?: string }; + +type ChannelDefinition = { + id: ChannelKey; + titleKey: string; + hintKey: string; + fields: ChannelField[]; +}; + +type WhatsAppStatusPayload = { + ok?: boolean; + enabled?: boolean; + bridge_url?: string; + bridge_running?: boolean; + error?: string; + status?: { + state?: string; + connected?: boolean; + logged_in?: boolean; + bridge_addr?: string; + user_jid?: string; + push_name?: string; + platform?: string; + qr_available?: boolean; + qr_code?: string; + last_event?: string; + last_error?: string; + updated_at?: string; + inbound_count?: number; + outbound_count?: number; + read_receipt_count?: number; + last_inbound_at?: string; + last_outbound_at?: string; + last_read_at?: string; + last_inbound_from?: string; + last_outbound_to?: string; + last_inbound_text?: string; + last_outbound_text?: string; + }; +}; + +const channelDefinitions: Record = { + telegram: { + id: 'telegram', + titleKey: 'telegram', + hintKey: 'telegramChannelHint', + fields: [ + { key: 'enabled', type: 'boolean' }, + { key: 'token', type: 'password' }, + { key: 'streaming', type: 'boolean' }, + { key: 'allow_from', type: 'list', placeholder: '123456789' }, + { key: 'allow_chats', type: 'list', placeholder: 'telegram:123456789' }, + { key: 'enable_groups', type: 'boolean' }, + { key: 'require_mention_in_groups', type: 'boolean' }, + ], + }, + whatsapp: { + id: 'whatsapp', + titleKey: 'whatsappBridge', + hintKey: 'whatsappBridgeHint', + fields: [ + { key: 'enabled', type: 'boolean' }, + { key: 'bridge_url', type: 'text', placeholder: 'ws://127.0.0.1:3001' }, + { key: 'allow_from', type: 'list', placeholder: '8613012345678@s.whatsapp.net' }, + { key: 'enable_groups', type: 'boolean' }, + { key: 'require_mention_in_groups', type: 'boolean' }, + ], + }, + discord: { + id: 'discord', + titleKey: 'discord', + hintKey: 'discordChannelHint', + fields: [ + { key: 'enabled', type: 'boolean' }, + { key: 'token', type: 'password' }, + { key: 'allow_from', type: 'list', placeholder: 'discord-user-id' }, + ], + }, + feishu: { + id: 'feishu', + titleKey: 'feishu', + hintKey: 'feishuChannelHint', + fields: [ + { key: 'enabled', type: 'boolean' }, + { key: 'app_id', type: 'text' }, + { key: 'app_secret', type: 'password' }, + { key: 'encrypt_key', type: 'password' }, + { key: 'verification_token', type: 'password' }, + { key: 'allow_from', type: 'list' }, + { key: 'allow_chats', type: 'list' }, + { key: 'enable_groups', type: 'boolean' }, + { key: 'require_mention_in_groups', type: 'boolean' }, + ], + }, + qq: { + id: 'qq', + titleKey: 'qq', + hintKey: 'qqChannelHint', + fields: [ + { key: 'enabled', type: 'boolean' }, + { key: 'app_id', type: 'text' }, + { key: 'app_secret', type: 'password' }, + { key: 'allow_from', type: 'list' }, + ], + }, + dingtalk: { + id: 'dingtalk', + titleKey: 'dingtalk', + hintKey: 'dingtalkChannelHint', + fields: [ + { key: 'enabled', type: 'boolean' }, + { key: 'client_id', type: 'text' }, + { key: 'client_secret', type: 'password' }, + { key: 'allow_from', type: 'list' }, + ], + }, + maixcam: { + id: 'maixcam', + titleKey: 'maixcam', + hintKey: 'maixcamChannelHint', + fields: [ + { key: 'enabled', type: 'boolean' }, + { key: 'host', type: 'text' }, + { key: 'port', type: 'number' }, + { key: 'allow_from', type: 'list' }, + ], + }, +}; + +function clone(value: T): T { + return JSON.parse(JSON.stringify(value)); +} + +function formatList(value: unknown) { + if (!Array.isArray(value)) return ''; + return value.map((item) => String(item ?? '')).join('\n'); +} + +function parseList(text: string) { + return String(text || '') + .split('\n') + .map((line) => line.split(',')) + .flat() + .map((item) => item.trim()) + .filter(Boolean); +} + +function getWhatsAppFieldDescription(t: (key: string) => string, fieldKey: string) { + switch (fieldKey) { + case 'enabled': + return t('whatsappFieldEnabledHint'); + case 'bridge_url': + return t('whatsappFieldBridgeURLHint'); + case 'allow_from': + return t('whatsappFieldAllowFromHint'); + case 'enable_groups': + return t('whatsappFieldEnableGroupsHint'); + case 'require_mention_in_groups': + return t('whatsappFieldRequireMentionHint'); + default: + return ''; + } +} + +function getWhatsAppBooleanIcon(fieldKey: string) { + switch (fieldKey) { + case 'enabled': + return Wifi; + case 'enable_groups': + return Users; + case 'require_mention_in_groups': + return ShieldCheck; + default: + return Check; + } +} + +const ChannelSettings: React.FC = () => { + const { channelId } = useParams(); + const navigate = useNavigate(); + const { t } = useTranslation(); + const ui = useUI(); + const { cfg, setCfg, q, loadConfig } = useAppContext(); + const key = (channelId || 'whatsapp') as ChannelKey; + const definition = channelDefinitions[key]; + + const [draft, setDraft] = useState>({}); + const [saving, setSaving] = useState(false); + const [waStatus, setWaStatus] = useState(null); + + useEffect(() => { + if (!definition) { + navigate('/channels/whatsapp', { replace: true }); + return; + } + const next = clone(((cfg as any)?.channels?.[definition.id] || {}) as Record); + setDraft(next); + }, [cfg, definition, navigate]); + + useEffect(() => { + if (key !== 'whatsapp') return; + let active = true; + const fetchStatus = async () => { + try { + const res = await fetch(`/webui/api/whatsapp/status${q}`); + const json = await res.json(); + if (active) setWaStatus(json); + } catch { + if (active) setWaStatus(null); + } + }; + void fetchStatus(); + const timer = window.setInterval(fetchStatus, 3000); + return () => { + active = false; + window.clearInterval(timer); + }; + }, [key, q]); + + const qrImageURL = useMemo(() => { + const updatedAt = waStatus?.status?.updated_at || Date.now(); + const sep = q ? '&' : '?'; + return `/webui/api/whatsapp/qr.svg${q}${sep}ts=${encodeURIComponent(String(updatedAt))}`; + }, [q, waStatus?.status?.updated_at]); + + if (!definition) return null; + + const saveChannel = async () => { + setSaving(true); + try { + const nextCfg = clone(cfg || {}); + if (!nextCfg.channels || typeof nextCfg.channels !== 'object') { + (nextCfg as any).channels = {}; + } + (nextCfg as any).channels[definition.id] = clone(draft); + const res = await fetch(`/webui/api/config${q}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(nextCfg), + }); + if (!res.ok) { + throw new Error(await res.text()); + } + setCfg(nextCfg); + await loadConfig(true); + await ui.notify(t('configSaved')); + } catch (err: any) { + await ui.notify(String(err?.message || err || 'save failed')); + } finally { + setSaving(false); + } + }; + + const handleLogout = async () => { + const ok = await ui.confirmDialog({ + title: t('whatsappLogoutTitle'), + message: t('whatsappLogoutMessage'), + danger: true, + confirmText: t('logout'), + }); + if (!ok) return; + await ui.withLoading(async () => { + await fetch(`/webui/api/whatsapp/logout${q}`, { method: 'POST' }); + }, t('loading')); + }; + + const renderField = (field: ChannelField) => { + const label = t(`configLabels.${field.key}`); + const value = draft[field.key]; + const isWhatsApp = key === 'whatsapp'; + const helper = isWhatsApp ? getWhatsAppFieldDescription(t, field.key) : ''; + if (field.type === 'boolean') { + if (isWhatsApp) { + const Icon = getWhatsAppBooleanIcon(field.key); + return ( + + ); + } + return ( + + ); + } + if (field.type === 'list') { + return ( +
+
+ + {isWhatsApp && Array.isArray(value) && value.length > 0 && ( + + {t('entries')}: {value.length} + + )} +
+ {helper &&
{helper}
} +