From cc235997763603e4b8b7d64e16870c5a81caa7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sun, 4 Jan 2026 20:38:08 +0800 Subject: [PATCH] Enhance HTTP debug UI with command palette and UI improvements Added a new CommandPalette component for quick API selection and execution (Ctrl/Cmd+K). Refactored the HTTP debug page to use the command palette, improved tab and panel UI, and enhanced the code editor's appearance and theme integration. Updated OneBotApiDebug to support imperative methods for request body and sending, improved response panel resizing, and made various UI/UX refinements across related components. --- .../src/components/code_editor.tsx | 60 ++- .../src/components/command_palette.tsx | 228 +++++++++++ .../file_manage/file_edit_modal.tsx | 12 +- .../src/components/onebot/api/debug.tsx | 355 ++++++++++-------- .../src/components/onebot/api/nav_list.tsx | 12 +- .../src/components/sidebar/index.tsx | 5 +- .../src/const/themes/nc_pink.ts | 2 +- .../src/pages/dashboard/debug/http/index.tsx | 229 ++++++----- 8 files changed, 638 insertions(+), 265 deletions(-) create mode 100644 packages/napcat-webui-frontend/src/components/command_palette.tsx diff --git a/packages/napcat-webui-frontend/src/components/code_editor.tsx b/packages/napcat-webui-frontend/src/components/code_editor.tsx index 4ad5227c..2f11b567 100644 --- a/packages/napcat-webui-frontend/src/components/code_editor.tsx +++ b/packages/napcat-webui-frontend/src/components/code_editor.tsx @@ -30,6 +30,7 @@ export interface CodeEditorRef { const CodeEditor = React.forwardRef((props, ref) => { const { isDark } = useTheme(); + const chromeless = !!props.options?.chromeless; // eslint-disable-next-line @typescript-eslint/no-unused-vars const [val, setVal] = useState(props.value || props.defaultValue || ''); const internalRef = React.useRef(null); @@ -51,36 +52,66 @@ const CodeEditor = React.forwardRef((props, ref) "&": { fontSize: "14px", height: "100% !important", + backgroundColor: 'transparent !important', + }, + "&.cm-editor": { + backgroundColor: 'transparent !important', }, ".cm-scroller": { - fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", + fontFamily: "var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace)", lineHeight: "1.6", overflow: "auto !important", height: "100% !important", + backgroundColor: 'transparent !important', }, ".cm-gutters": { - backgroundColor: "transparent", + backgroundColor: "transparent !important", borderRight: "none", - color: isDark ? "#ffffff50" : "#00000040", + color: isDark + ? 'hsl(var(--heroui-foreground-500) / 0.75)' + : 'hsl(var(--heroui-foreground-500) / 0.65)', }, ".cm-gutterElement": { paddingLeft: "12px", paddingRight: "12px", }, ".cm-activeLineGutter": { - backgroundColor: "transparent", - color: isDark ? "#fff" : "#000", + backgroundColor: 'transparent !important', + color: isDark + ? 'hsl(var(--heroui-foreground) / 0.9) !important' + : 'hsl(var(--heroui-foreground) / 0.8) !important', }, ".cm-content": { - caretColor: isDark ? "#fff" : "#000", + color: 'hsl(var(--heroui-foreground) / 0.9)', + caretColor: 'hsl(var(--heroui-foreground) / 0.9)', paddingTop: "12px", paddingBottom: "12px", + backgroundColor: 'transparent !important', }, ".cm-activeLine": { - backgroundColor: isDark ? "#ffffff10" : "#00000008", + backgroundColor: isDark + ? 'hsl(var(--heroui-foreground) / 0.08)' + : 'hsl(var(--heroui-foreground) / 0.06)', }, ".cm-selectionMatch": { - backgroundColor: isDark ? "#ffffff20" : "#00000010", + backgroundColor: isDark + ? 'hsl(var(--heroui-foreground) / 0.16)' + : 'hsl(var(--heroui-foreground) / 0.12)', + }, + // Syntax highlighting overrides for better readability + ".ͼo": { + // JSON property names - use a softer primary color + color: isDark + ? 'hsl(var(--heroui-primary) / 0.85)' + : 'hsl(var(--heroui-primary) / 0.75)', + }, + ".ͼd": { + // Strings - softer green + color: isDark ? '#98c379cc' : '#50a14fcc', + }, + ".ͼc": { + // Numbers - softer orange + color: isDark ? '#d19a66cc' : '#c18401cc', }, }); @@ -95,17 +126,20 @@ const CodeEditor = React.forwardRef((props, ref)
{ diff --git a/packages/napcat-webui-frontend/src/components/command_palette.tsx b/packages/napcat-webui-frontend/src/components/command_palette.tsx new file mode 100644 index 00000000..bb5f2c4d --- /dev/null +++ b/packages/napcat-webui-frontend/src/components/command_palette.tsx @@ -0,0 +1,228 @@ +import { Button } from '@heroui/button'; +import { Input } from '@heroui/input'; +import { + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@heroui/modal'; +import clsx from 'clsx'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { TbCornerDownLeft, TbSearch } from 'react-icons/tb'; + +export type CommandPaletteCommand = { + id: string; + title: string; + subtitle?: string; + group?: string; +}; + +export type CommandPaletteExecuteMode = 'open' | 'send'; + +export interface CommandPaletteProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + commands: CommandPaletteCommand[]; + onExecute: (commandId: string, mode: CommandPaletteExecuteMode) => void; +} + +const isMobileByViewport = () => { + try { + return window.innerWidth < 768; + } catch { + return false; + } +}; + +export default function CommandPalette (props: CommandPaletteProps) { + const { isOpen, onOpenChange, commands, onExecute } = props; + const inputRef = useRef(null); + + const [query, setQuery] = useState(''); + const [activeIndex, setActiveIndex] = useState(0); + const [mobile, setMobile] = useState(false); + + useEffect(() => { + const update = () => setMobile(isMobileByViewport()); + update(); + window.addEventListener('resize', update); + return () => window.removeEventListener('resize', update); + }, []); + + useEffect(() => { + if (!isOpen) return; + setQuery(''); + setActiveIndex(0); + // 等 Modal 动画挂载后再 focus + const t = window.setTimeout(() => inputRef.current?.focus(), 50); + return () => window.clearTimeout(t); + }, [isOpen]); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + const list = !q + ? commands + : commands.filter((c) => { + const hay = `${c.id} ${c.title} ${c.subtitle ?? ''} ${c.group ?? ''}`.toLowerCase(); + return hay.includes(q); + }); + + // 简单:优先 path 前缀命中 + if (!q) return list; + const starts = list.filter((c) => c.id.toLowerCase().startsWith(q)); + const rest = list.filter((c) => !c.id.toLowerCase().startsWith(q)); + return [...starts, ...rest]; + }, [commands, query]); + + useEffect(() => { + if (activeIndex >= filtered.length) setActiveIndex(0); + }, [filtered.length, activeIndex]); + + const active = filtered[activeIndex]; + + const exec = (mode: CommandPaletteExecuteMode) => { + if (!active) return; + onExecute(active.id, mode); + onOpenChange(false); + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setActiveIndex((i) => Math.min(i + 1, Math.max(0, filtered.length - 1))); + return; + } + if (e.key === 'ArrowUp') { + e.preventDefault(); + setActiveIndex((i) => Math.max(i - 1, 0)); + return; + } + if (e.key === 'Enter') { + e.preventDefault(); + // Shift+Enter 仅打开;Enter 打开并发送 + exec(e.shiftKey ? 'open' : 'send'); + return; + } + if (e.key === 'Escape') { + e.preventDefault(); + onOpenChange(false); + } + }; + + return ( + + + {() => ( + <> + + 命令面板 + Ctrl/Cmd + K + + + } + radius='lg' + variant='flat' + classNames={{ + inputWrapper: 'bg-content2/50 border border-default-200/50 dark:border-default-100/20', + input: 'text-sm', + }} + /> + +
+
+ {filtered.length === 0 && ( +
没有匹配的接口
+ )} + {filtered.map((c, idx) => ( + + ))} +
+
+
+ {mobile && ( + + + + + + )} + + )} +
+
+ ); +} diff --git a/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx b/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx index c479199c..fc689834 100644 --- a/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx +++ b/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx @@ -63,17 +63,17 @@ export default function FileEditModal ({ }; return ( - - - + + + 编辑文件 {file?.path}
Ctrl/Cmd + S 保存
- -
{ + +
{ if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); onSave(); @@ -88,7 +88,7 @@ export default function FileEditModal ({ />
- + diff --git a/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx b/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx index 0125ad7b..36930f9a 100644 --- a/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx +++ b/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx @@ -1,4 +1,5 @@ import { Button } from '@heroui/button'; + import { Input } from '@heroui/input'; import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'; import { Tooltip } from '@heroui/tooltip'; @@ -6,7 +7,7 @@ import { Tab, Tabs } from '@heroui/tabs'; import { Chip } from '@heroui/chip'; import { useLocalStorage } from '@uidotdev/usehooks'; import clsx from 'clsx'; -import { useEffect, useState, useCallback } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useState, useCallback } from 'react'; import toast from 'react-hot-toast'; import { IoChevronDown, IoSend, IoSettingsSharp, IoCopy } from 'react-icons/io5'; import { TbCode, TbMessageCode } from 'react-icons/tb'; @@ -30,14 +31,21 @@ export interface OneBotApiDebugProps { adapterName?: string; } -const OneBotApiDebug: React.FC = (props) => { +export interface OneBotApiDebugRef { + setRequestBody: (value: string) => void; + sendWithBody: (value: string) => void; + focusRequestEditor: () => void; +} + +const OneBotApiDebug = forwardRef((props, ref) => { const { path, data, adapterName } = props; const currentURL = new URL(window.location.origin); currentURL.port = '3000'; const defaultHttpUrl = currentURL.href; + const defaultToken = localStorage.getItem('token') || ''; const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, { url: defaultHttpUrl, - token: '', + token: defaultToken, }); const [requestBody, setRequestBody] = useState('{}'); @@ -46,21 +54,23 @@ const OneBotApiDebug: React.FC = (props) => { const [activeTab, setActiveTab] = useState('request'); const [responseExpanded, setResponseExpanded] = useState(true); const [responseStatus, setResponseStatus] = useState<{ code: number; text: string; } | null>(null); - const [responseHeight, setResponseHeight] = useLocalStorage('napcat_debug_response_height', 240); // 默认高度 + // Height Resizing Logic + const [responseHeight, setResponseHeight] = useState(240); + const [storedHeight, setStoredHeight] = useLocalStorage('napcat_debug_response_height', 240); const parsedRequest = parse(data.request); const parsedResponse = parse(data.response); const [backgroundImage] = useLocalStorage(key.backgroundImage, ''); const hasBackground = !!backgroundImage; - const sendRequest = async () => { + const sendRequest = async (bodyOverride?: string) => { if (isFetching) return; setIsFetching(true); setResponseStatus(null); const r = toast.loading('正在发送请求...'); try { - const parsedRequestBody = JSON.parse(requestBody); + const parsedRequestBody = JSON.parse(bodyOverride ?? requestBody); // 如果有 adapterName,走后端转发 if (adapterName) { @@ -127,93 +137,132 @@ const OneBotApiDebug: React.FC = (props) => { } }; + useImperativeHandle(ref, () => ({ + setRequestBody: (value: string) => { + setActiveTab('request'); + setRequestBody(value); + }, + sendWithBody: (value: string) => { + setActiveTab('request'); + setRequestBody(value); + // 直接用 override 发送,避免 setState 异步导致拿到旧值 + void sendRequest(value); + }, + focusRequestEditor: () => { + setActiveTab('request'); + } + })); + useEffect(() => { setRequestBody(generateDefaultJson(data.request)); setResponseContent(''); setResponseStatus(null); }, [path]); - // Height Resizing Logic + // Sync from storage on mount + useEffect(() => { + setResponseHeight(storedHeight); + }, []); + const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault(); const startY = e.clientY; const startHeight = responseHeight; + let currentH = startHeight; + let frameId: number; const handleMouseMove = (mv: MouseEvent) => { - const delta = startY - mv.clientY; - // 向上拖动 -> 增加高度 - setResponseHeight(Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta))); + if (frameId) cancelAnimationFrame(frameId); + frameId = requestAnimationFrame(() => { + const delta = startY - mv.clientY; + currentH = Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta)); + setResponseHeight(currentH); + }); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); + if (frameId) cancelAnimationFrame(frameId); + setStoredHeight(currentH); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); - }, [responseHeight, setResponseHeight]); + }, [responseHeight, setStoredHeight]); const handleTouchStart = useCallback((e: React.TouchEvent) => { - // 阻止默认滚动行为可能需要谨慎,这里尽量只阻止 handle 上的 - // e.preventDefault(); const touch = e.touches[0]; const startY = touch.clientY; const startHeight = responseHeight; + let currentH = startHeight; + let frameId: number; const handleTouchMove = (mv: TouchEvent) => { - const mvTouch = mv.touches[0]; - const delta = startY - mvTouch.clientY; - setResponseHeight(Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta))); + if (frameId) cancelAnimationFrame(frameId); + frameId = requestAnimationFrame(() => { + const mvTouch = mv.touches[0]; + const delta = startY - mvTouch.clientY; + currentH = Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta)); + setResponseHeight(currentH); + }); }; const handleTouchEnd = () => { document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); + if (frameId) cancelAnimationFrame(frameId); + setStoredHeight(currentH); }; document.addEventListener('touchmove', handleTouchMove); document.addEventListener('touchend', handleTouchEnd); - }, [responseHeight, setResponseHeight]); + }, [responseHeight, setStoredHeight]); return ( -
- {/* URL Bar */} -
-
- POST - {path} + +
+ {/* 1. Top Toolbar: URL & Actions */} +
+ {/* Method & Path */} + {/* Method & Path */} + {/* Method & Path */} +
+
+ {path} +
-
- + {/* Actions */} +
+ - - +

Debug Setup

- setHttpConfig({ ...httpConfig, url: e.target.value })} size='sm' variant='flat' /> - setHttpConfig({ ...httpConfig, token: e.target.value })} size='sm' variant='flat' /> + setHttpConfig({ ...httpConfig, url: e.target.value })} size='sm' variant='bordered' /> + setHttpConfig({ ...httpConfig, token: e.target.value })} size='sm' variant='bordered' />
-
-
- - - - -
- - {(onOpen) => ( - - - - )} - - - - - -
-
- -
+ {/* 2. Main Workspace (Request) - Flexible Height */} +
+
+ {/* Request Toolbar */}
+ + + + + +
+ + {(onOpen) => ( + + + + )} + + + + +
+
+ + {/* Content Area */} +
{activeTab === 'request' ? ( - setRequestBody(value ?? '')} - language='json' - options={{ - minimap: { enabled: false }, - fontSize: 12, - scrollBeyondLastLine: false, - wordWrap: 'on', - padding: { top: 12 }, - lineNumbersMinChars: 3 - }} - /> +
+ setRequestBody(value ?? '')} + language='json' + options={{ + minimap: { enabled: false }, + fontSize: 13, + fontFamily: 'JetBrains Mono, monospace', + scrollBeyondLastLine: false, + wordWrap: 'on', + padding: { top: 16, bottom: 16 }, + lineNumbersMinChars: 3, + chromeless: true, + backgroundColor: 'transparent' + }} + /> +
) : ( -
+
-

Request - 请求数据结构

+

Request Params

-
+
-

Response - 返回数据结构

+

Response Data

@@ -309,73 +352,79 @@ const OneBotApiDebug: React.FC = (props) => {
- {/* Response Area */} -
+ {/* 3. Response Panel (Bottom) */} +
+ {/* Resize Handle / Header */}
setResponseExpanded(!responseExpanded)} > - {/* Header & Resize Handle */} -
setResponseExpanded(!responseExpanded)} - > - {/* Invisble Resize Area that becomes visible/active */} - {responseExpanded && ( -
{ e.stopPropagation(); handleMouseDown(e); }} - onTouchStart={(e) => { e.stopPropagation(); handleTouchStart(e); }} - onClick={(e) => e.stopPropagation()} - > -
-
- )} + {/* Invisible Draggable Area */} + {responseExpanded && ( +
{ e.stopPropagation(); handleMouseDown(e); }} + onTouchStart={(e) => { e.stopPropagation(); handleTouchStart(e); }} + onClick={(e) => e.stopPropagation()} + /> + )} -
- - Response -
-
- {responseStatus && ( - = 200 && responseStatus.code < 300 ? 'success' : 'danger'} className="h-4 text-[9px] font-mono px-1.5 opacity-50"> - {responseStatus.code} - - )} - +
+
+
+ Response + {responseStatus && ( + = 200 && responseStatus.code < 300 ? 'success' : 'danger'} className="h-5 text-[10px] font-mono border-none bg-transparent pl-0"> + {responseStatus.code} {responseStatus.text} + + )}
- {/* Response Content - Code Editor */} - {responseExpanded && ( -
- + +
+ + {/* Response Editor */} + {responseExpanded && ( +
+ +
- )} -
+
+ )}
-
+ +
); -}; +}); export default OneBotApiDebug; diff --git a/packages/napcat-webui-frontend/src/components/onebot/api/nav_list.tsx b/packages/napcat-webui-frontend/src/components/onebot/api/nav_list.tsx index 7b4670cb..7cce84c4 100644 --- a/packages/napcat-webui-frontend/src/components/onebot/api/nav_list.tsx +++ b/packages/napcat-webui-frontend/src/components/onebot/api/nav_list.tsx @@ -143,21 +143,23 @@ const OneBotApiNavList: React.FC = (props) => { key={api.path} onClick={() => onSelect(api.path)} className={clsx( - 'flex flex-col gap-0.5 px-3 py-2 rounded-lg cursor-pointer transition-all border border-transparent select-none', + 'flex flex-col gap-0.5 px-3 py-2 rounded-lg cursor-pointer transition-all border select-none', isSelected - ? (hasBackground ? '' : 'bg-primary/20 border-primary/20 shadow-sm') - : 'hover:bg-white/5' + ? (hasBackground + ? 'bg-white/10 border-white/20' + : 'bg-primary/10 border-primary/20 shadow-sm') + : 'border-transparent hover:bg-white/10 dark:hover:bg-white/5' )} > {api.description} {api.path} diff --git a/packages/napcat-webui-frontend/src/components/sidebar/index.tsx b/packages/napcat-webui-frontend/src/components/sidebar/index.tsx index 4c2c9706..be9719fd 100644 --- a/packages/napcat-webui-frontend/src/components/sidebar/index.tsx +++ b/packages/napcat-webui-frontend/src/components/sidebar/index.tsx @@ -70,7 +70,10 @@ const SideBar: React.FC = (props) => {
-
+
NapCat
diff --git a/packages/napcat-webui-frontend/src/const/themes/nc_pink.ts b/packages/napcat-webui-frontend/src/const/themes/nc_pink.ts index bb2e8831..1210f45c 100644 --- a/packages/napcat-webui-frontend/src/const/themes/nc_pink.ts +++ b/packages/napcat-webui-frontend/src/const/themes/nc_pink.ts @@ -183,7 +183,7 @@ const theme: ThemeConfig = { '--heroui-primary-800': '339.33 86.54% 20.39%', '--heroui-primary-900': '340 84.91% 10.39%', '--heroui-primary-foreground': '0 0% 100%', - '--heroui-primary': '339.2 90.36% 51.18%', + '--heroui-primary': '339.2 90.36% 60%', '--heroui-secondary-50': '270 61.54% 94.9%', '--heroui-secondary-100': '270 59.26% 89.41%', '--heroui-secondary-200': '270 59.26% 78.82%', diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx index ccafb31a..0637ed08 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/debug/http/index.tsx @@ -1,31 +1,43 @@ import { Button } from '@heroui/button'; import { useLocalStorage } from '@uidotdev/usehooks'; import clsx from 'clsx'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { IoClose } from 'react-icons/io5'; -import { TbSquareRoundedChevronLeftFilled } from 'react-icons/tb'; +import { TbSearch } from 'react-icons/tb'; import key from '@/const/key'; import oneBotHttpApi from '@/const/ob_api'; import type { OneBotHttpApiPath } from '@/const/ob_api'; import OneBotApiDebug from '@/components/onebot/api/debug'; -import OneBotApiNavList from '@/components/onebot/api/nav_list'; +import CommandPalette from '@/components/command_palette'; +import type { CommandPaletteCommand, CommandPaletteExecuteMode } from '@/components/command_palette'; + +import { generateDefaultJson } from '@/utils/zod'; +import type { OneBotApiDebugRef } from '@/components/onebot/api/debug'; export default function HttpDebug () { - const [activeApi, setActiveApi] = useState('/set_qq_profile'); - const [openApis, setOpenApis] = useState(['/set_qq_profile']); - const [openSideBar, setOpenSideBar] = useState(true); + const [activeApi, setActiveApi] = useState(null); + const [openApis, setOpenApis] = useState([]); const [backgroundImage] = useLocalStorage(key.backgroundImage, ''); const hasBackground = !!backgroundImage; const [adapterName, setAdapterName] = useState(''); + const [paletteOpen, setPaletteOpen] = useState(false); - // Auto-collapse sidebar on mobile initial load + const debugRefs = useRef(new Map()); + const [pendingRun, setPendingRun] = useState<{ path: OneBotHttpApiPath; body: string; } | null>(null); + + // Ctrl/Cmd + K 打开命令面板 useEffect(() => { - if (window.innerWidth < 768) { - setOpenSideBar(false); - } + const handler = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + setPaletteOpen(true); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); }, []); // Initialize Debug Adapter @@ -64,9 +76,48 @@ export default function HttpDebug () { setOpenApis([...openApis, api]); } setActiveApi(api); - if (window.innerWidth < 768) { - setOpenSideBar(false); + }; + + // 等对应 Debug 组件挂载后再触发发送 + useEffect(() => { + if (!pendingRun) return; + if (activeApi !== pendingRun.path) return; + const ref = debugRefs.current.get(pendingRun.path); + if (!ref) return; + ref.sendWithBody(pendingRun.body); + setPendingRun(null); + }, [activeApi, pendingRun]); + + const commands: CommandPaletteCommand[] = useMemo(() => { + return Object.keys(oneBotHttpApi).map((p) => { + const path = p as OneBotHttpApiPath; + const item = oneBotHttpApi[path]; + // 简单分组:按描述里已有分类不可靠,这里只用 path 前缀推断 + const group = path.startsWith('/get_') ? 'GET' : (path.startsWith('/set_') ? 'SET' : 'API'); + return { + id: path, + title: item?.description || path, + subtitle: item?.request ? '回车发送 · Shift+Enter 仅打开' : undefined, + group, + }; + }); + }, []); + + const executeCommand = (commandId: string, mode: CommandPaletteExecuteMode) => { + const api = commandId as OneBotHttpApiPath; + const item = oneBotHttpApi[api]; + const body = item?.request ? generateDefaultJson(item.request) : '{}'; + + handleSelectApi(api); + // 确保请求参数可见 + const ref = debugRefs.current.get(api); + if (ref) { + if (mode === 'send') ref.sendWithBody(body); + else ref.setRequestBody(body); + return; } + // 若还没挂载,延迟执行 + if (mode === 'send') setPendingRun({ path: api, body }); }; const handleCloseTab = (e: React.MouseEvent, apiToRemove: OneBotHttpApiPath) => { @@ -76,9 +127,6 @@ export default function HttpDebug () { if (activeApi === apiToRemove) { if (newOpenApis.length > 0) { - // Switch to the last opened tab or the previous one? - // Usually the one to the right or left. Let's pick the last one for simplicity or neighbor. - // Finding index of removed api to pick neighbor is better UX, but last one is acceptable. setActiveApi(newOpenApis[newOpenApis.length - 1]); } else { setActiveApi(null); @@ -89,50 +137,24 @@ export default function HttpDebug () { return ( <> HTTP调试 - NapCat WebUI -
+
- {/* Unifed Header */} -
-
- -

接口调试

-
-
- -
- - -
- {/* Tab Bar */} -
+
+
+ {/* Tab List */} +
{openApis.map((api) => { const isActive = api === activeApi; const item = oneBotHttpApi[api]; @@ -141,21 +163,26 @@ export default function HttpDebug () { key={api} onClick={() => setActiveApi(api)} className={clsx( - 'group flex items-center gap-2 px-4 h-9 cursor-pointer border-r border-white/5 select-none transition-all min-w-[120px] max-w-[200px]', + 'group flex items-center gap-2 px-3 h-8 my-1 mr-1 rounded-md cursor-pointer border select-none transition-all min-w-[120px] max-w-[260px]', + hasBackground ? 'border-transparent hover:bg-white/10' : 'border-transparent hover:bg-white/10 dark:hover:bg-white/5', isActive - ? (hasBackground ? 'bg-white/10 text-white' : 'bg-white/40 dark:bg-white/5 text-primary font-medium') - : 'opacity-50 hover:opacity-100 hover:bg-white/5' + ? (hasBackground + ? 'bg-white/15 text-white border-white/20' + : 'bg-default-100 dark:bg-white/15 text-foreground dark:text-white font-medium shadow-sm border-default-200 dark:border-white/10') + : (hasBackground ? 'text-white/70 hover:text-white' : 'text-default-600 dark:text-white/70 hover:text-default-900 dark:hover:text-white') )} > POST {item?.description || api}
handleCloseTab(e, api)} > @@ -163,37 +190,67 @@ export default function HttpDebug () {
); + })}
- {/* Content Panels */} -
- {activeApi === null && ( -
- 选择一个接口开始调试 -
- )} - - {openApis.map((api) => ( -
- -
- ))} + {/* Actions */} +
+
+ + {/* Content Panels */} +
+ {activeApi === null && ( +
+ 使用命令面板选择接口(Ctrl/Cmd + K) +
+ )} + + {openApis.map((api) => ( +
+ { + if (!node) { + debugRefs.current.delete(api); + return; + } + debugRefs.current.set(api, node); + }} + path={api} + data={oneBotHttpApi[api]} + adapterName={adapterName} + /> +
+ ))} +
+ + ); }