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} + /> +
+ ))} +
+ + ); }