mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-14 20:30:34 +00:00
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.
This commit is contained in:
parent
c6ec2126e0
commit
cc23599776
@ -30,6 +30,7 @@ export interface CodeEditorRef {
|
|||||||
|
|
||||||
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
|
const chromeless = !!props.options?.chromeless;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [val, setVal] = useState(props.value || props.defaultValue || '');
|
const [val, setVal] = useState(props.value || props.defaultValue || '');
|
||||||
const internalRef = React.useRef<ReactCodeMirrorRef>(null);
|
const internalRef = React.useRef<ReactCodeMirrorRef>(null);
|
||||||
@ -51,36 +52,66 @@ const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>((props, ref)
|
|||||||
"&": {
|
"&": {
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
height: "100% !important",
|
height: "100% !important",
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
|
},
|
||||||
|
"&.cm-editor": {
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
},
|
},
|
||||||
".cm-scroller": {
|
".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",
|
lineHeight: "1.6",
|
||||||
overflow: "auto !important",
|
overflow: "auto !important",
|
||||||
height: "100% !important",
|
height: "100% !important",
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
},
|
},
|
||||||
".cm-gutters": {
|
".cm-gutters": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent !important",
|
||||||
borderRight: "none",
|
borderRight: "none",
|
||||||
color: isDark ? "#ffffff50" : "#00000040",
|
color: isDark
|
||||||
|
? 'hsl(var(--heroui-foreground-500) / 0.75)'
|
||||||
|
: 'hsl(var(--heroui-foreground-500) / 0.65)',
|
||||||
},
|
},
|
||||||
".cm-gutterElement": {
|
".cm-gutterElement": {
|
||||||
paddingLeft: "12px",
|
paddingLeft: "12px",
|
||||||
paddingRight: "12px",
|
paddingRight: "12px",
|
||||||
},
|
},
|
||||||
".cm-activeLineGutter": {
|
".cm-activeLineGutter": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: 'transparent !important',
|
||||||
color: isDark ? "#fff" : "#000",
|
color: isDark
|
||||||
|
? 'hsl(var(--heroui-foreground) / 0.9) !important'
|
||||||
|
: 'hsl(var(--heroui-foreground) / 0.8) !important',
|
||||||
},
|
},
|
||||||
".cm-content": {
|
".cm-content": {
|
||||||
caretColor: isDark ? "#fff" : "#000",
|
color: 'hsl(var(--heroui-foreground) / 0.9)',
|
||||||
|
caretColor: 'hsl(var(--heroui-foreground) / 0.9)',
|
||||||
paddingTop: "12px",
|
paddingTop: "12px",
|
||||||
paddingBottom: "12px",
|
paddingBottom: "12px",
|
||||||
|
backgroundColor: 'transparent !important',
|
||||||
},
|
},
|
||||||
".cm-activeLine": {
|
".cm-activeLine": {
|
||||||
backgroundColor: isDark ? "#ffffff10" : "#00000008",
|
backgroundColor: isDark
|
||||||
|
? 'hsl(var(--heroui-foreground) / 0.08)'
|
||||||
|
: 'hsl(var(--heroui-foreground) / 0.06)',
|
||||||
},
|
},
|
||||||
".cm-selectionMatch": {
|
".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<CodeEditorRef, CodeEditorProps>((props, ref)
|
|||||||
<div
|
<div
|
||||||
style={{ fontSize: props.options?.fontSize || 14, height: props.height || '100%', display: 'flex', flexDirection: 'column' }}
|
style={{ fontSize: props.options?.fontSize || 14, height: props.height || '100%', display: 'flex', flexDirection: 'column' }}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-xl border overflow-hidden transition-colors',
|
chromeless
|
||||||
isDark
|
? 'overflow-hidden transition-colors bg-transparent'
|
||||||
? 'border-white/10 bg-[#282c34]'
|
: 'rounded-xl border overflow-hidden transition-colors backdrop-blur-sm',
|
||||||
: 'border-default-200 bg-white'
|
!chromeless && (isDark
|
||||||
|
? 'border-white/10 bg-white/5 text-default-100'
|
||||||
|
: 'border-white/40 dark:border-white/10 bg-white/60 dark:bg-black/20 text-default-700')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
ref={internalRef}
|
ref={internalRef}
|
||||||
value={props.value ?? props.defaultValue}
|
value={props.value ?? props.defaultValue}
|
||||||
height="100%"
|
height="100%"
|
||||||
className="h-full w-full"
|
className="h-full w-full [&_.cm-editor]:!bg-transparent [&_.cm-scroller]:!bg-transparent"
|
||||||
|
style={{ backgroundColor: 'transparent' }}
|
||||||
theme={isDark ? oneDark : 'light'}
|
theme={isDark ? oneDark : 'light'}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
|||||||
@ -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<HTMLInputElement | null>(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 (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
size={mobile ? 'full' : '2xl'}
|
||||||
|
radius={mobile ? 'none' : 'lg'}
|
||||||
|
scrollBehavior='inside'
|
||||||
|
backdrop='blur'
|
||||||
|
>
|
||||||
|
<ModalContent>
|
||||||
|
{() => (
|
||||||
|
<>
|
||||||
|
<ModalHeader className={clsx(
|
||||||
|
'flex items-center gap-2',
|
||||||
|
mobile ? 'border-b border-default-200/50' : ''
|
||||||
|
)}>
|
||||||
|
<span className='text-sm font-semibold'>命令面板</span>
|
||||||
|
<span className='text-xs text-default-400 font-normal hidden md:inline'>Ctrl/Cmd + K</span>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className={clsx('gap-3', mobile ? 'p-3' : 'p-4')}>
|
||||||
|
<Input
|
||||||
|
ref={inputRef as any}
|
||||||
|
autoFocus
|
||||||
|
value={query}
|
||||||
|
onValueChange={setQuery}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder='输入 /set_xxx 或 描述… Enter:打开并发送,Shift+Enter:仅打开'
|
||||||
|
startContent={<TbSearch className='opacity-40' size={16} />}
|
||||||
|
radius='lg'
|
||||||
|
variant='flat'
|
||||||
|
classNames={{
|
||||||
|
inputWrapper: 'bg-content2/50 border border-default-200/50 dark:border-default-100/20',
|
||||||
|
input: 'text-sm',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={clsx(
|
||||||
|
'rounded-xl border border-default-200/50 dark:border-default-100/20 overflow-hidden',
|
||||||
|
mobile ? 'flex-1 min-h-0' : 'max-h-[420px]'
|
||||||
|
)}>
|
||||||
|
<div className={clsx(
|
||||||
|
'divide-y divide-default-200/50 dark:divide-default-100/20 overflow-y-auto no-scrollbar',
|
||||||
|
mobile ? 'h-full' : 'max-h-[420px]'
|
||||||
|
)}>
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<div className='p-6 text-sm text-default-400'>没有匹配的接口</div>
|
||||||
|
)}
|
||||||
|
{filtered.map((c, idx) => (
|
||||||
|
<button
|
||||||
|
key={c.id}
|
||||||
|
type='button'
|
||||||
|
className={clsx(
|
||||||
|
'w-full text-left px-4 py-3 transition-colors flex items-center gap-3',
|
||||||
|
idx === activeIndex
|
||||||
|
? 'bg-primary/10'
|
||||||
|
: 'hover:bg-default-100/50 dark:hover:bg-default-50/10'
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => setActiveIndex(idx)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveIndex(idx);
|
||||||
|
exec('open');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='min-w-0 flex-1'>
|
||||||
|
<div className='flex items-center gap-2 min-w-0'>
|
||||||
|
<span className='text-xs font-mono text-default-500 truncate'>{c.id}</span>
|
||||||
|
{c.group && (
|
||||||
|
<span className='text-[10px] px-2 py-0.5 rounded-full bg-default-100/60 dark:bg-default-50/20 text-default-500'>
|
||||||
|
{c.group}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='text-sm text-default-700 dark:text-default-200 truncate'>{c.title}</div>
|
||||||
|
{c.subtitle && (
|
||||||
|
<div className='text-xs text-default-400 truncate'>{c.subtitle}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-2 flex-shrink-0'>
|
||||||
|
<span className='hidden md:inline text-[10px] text-default-400'>Enter</span>
|
||||||
|
<TbCornerDownLeft className='opacity-40' size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
{mobile && (
|
||||||
|
<ModalFooter className='border-t border-default-200/50'>
|
||||||
|
<Button radius='full' variant='flat' onPress={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
radius='full'
|
||||||
|
variant='flat'
|
||||||
|
color='primary'
|
||||||
|
isDisabled={!active}
|
||||||
|
onPress={() => exec('open')}
|
||||||
|
>
|
||||||
|
打开
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
radius='full'
|
||||||
|
color='primary'
|
||||||
|
isDisabled={!active}
|
||||||
|
onPress={() => exec('send')}
|
||||||
|
>
|
||||||
|
打开并发送
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -63,17 +63,17 @@ export default function FileEditModal ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal radius='sm' size='full' isOpen={isOpen} onClose={onClose}>
|
<Modal radius='sm' size='full' isOpen={isOpen} onClose={onClose} scrollBehavior="inside">
|
||||||
<ModalContent>
|
<ModalContent className="flex flex-col h-full max-h-[100dvh]">
|
||||||
<ModalHeader className='flex items-center gap-2 border-b border-default-200/50'>
|
<ModalHeader className='flex items-center gap-2 border-b border-default-200/50 flex-shrink-0'>
|
||||||
<span>编辑文件</span>
|
<span>编辑文件</span>
|
||||||
<Code radius='sm' className='text-xs'>{file?.path}</Code>
|
<Code radius='sm' className='text-xs'>{file?.path}</Code>
|
||||||
<div className="ml-auto text-xs text-default-400 font-normal px-2">
|
<div className="ml-auto text-xs text-default-400 font-normal px-2">
|
||||||
按 <span className="px-1 py-0.5 rounded border border-default-300 bg-default-100">Ctrl/Cmd + S</span> 保存
|
按 <span className="px-1 py-0.5 rounded border border-default-300 bg-default-100">Ctrl/Cmd + S</span> 保存
|
||||||
</div>
|
</div>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody className='p-4 bg-content2/50'>
|
<ModalBody className='p-4 bg-content2/50 flex-1 min-h-0 overflow-hidden'>
|
||||||
<div className='h-full' onKeyDown={(e) => {
|
<div className='h-full w-full overflow-auto' onKeyDown={(e) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSave();
|
onSave();
|
||||||
@ -88,7 +88,7 @@ export default function FileEditModal ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="border-t border-default-200/50">
|
<ModalFooter className="border-t border-default-200/50 flex-shrink-0">
|
||||||
<Button radius='sm' color='primary' variant='flat' onPress={onClose}>
|
<Button radius='sm' color='primary' variant='flat' onPress={onClose}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Button } from '@heroui/button';
|
import { Button } from '@heroui/button';
|
||||||
|
|
||||||
import { Input } from '@heroui/input';
|
import { Input } from '@heroui/input';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||||
import { Tooltip } from '@heroui/tooltip';
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
@ -6,7 +7,7 @@ import { Tab, Tabs } from '@heroui/tabs';
|
|||||||
import { Chip } from '@heroui/chip';
|
import { Chip } from '@heroui/chip';
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx';
|
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 toast from 'react-hot-toast';
|
||||||
import { IoChevronDown, IoSend, IoSettingsSharp, IoCopy } from 'react-icons/io5';
|
import { IoChevronDown, IoSend, IoSettingsSharp, IoCopy } from 'react-icons/io5';
|
||||||
import { TbCode, TbMessageCode } from 'react-icons/tb';
|
import { TbCode, TbMessageCode } from 'react-icons/tb';
|
||||||
@ -30,14 +31,21 @@ export interface OneBotApiDebugProps {
|
|||||||
adapterName?: string;
|
adapterName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
export interface OneBotApiDebugRef {
|
||||||
|
setRequestBody: (value: string) => void;
|
||||||
|
sendWithBody: (value: string) => void;
|
||||||
|
focusRequestEditor: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OneBotApiDebug = forwardRef<OneBotApiDebugRef, OneBotApiDebugProps>((props, ref) => {
|
||||||
const { path, data, adapterName } = props;
|
const { path, data, adapterName } = props;
|
||||||
const currentURL = new URL(window.location.origin);
|
const currentURL = new URL(window.location.origin);
|
||||||
currentURL.port = '3000';
|
currentURL.port = '3000';
|
||||||
const defaultHttpUrl = currentURL.href;
|
const defaultHttpUrl = currentURL.href;
|
||||||
|
const defaultToken = localStorage.getItem('token') || '';
|
||||||
const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, {
|
const [httpConfig, setHttpConfig] = useLocalStorage(key.httpDebugConfig, {
|
||||||
url: defaultHttpUrl,
|
url: defaultHttpUrl,
|
||||||
token: '',
|
token: defaultToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [requestBody, setRequestBody] = useState('{}');
|
const [requestBody, setRequestBody] = useState('{}');
|
||||||
@ -46,21 +54,23 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
const [activeTab, setActiveTab] = useState<any>('request');
|
const [activeTab, setActiveTab] = useState<any>('request');
|
||||||
const [responseExpanded, setResponseExpanded] = useState(true);
|
const [responseExpanded, setResponseExpanded] = useState(true);
|
||||||
const [responseStatus, setResponseStatus] = useState<{ code: number; text: string; } | null>(null);
|
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 parsedRequest = parse(data.request);
|
||||||
const parsedResponse = parse(data.response);
|
const parsedResponse = parse(data.response);
|
||||||
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
||||||
const hasBackground = !!backgroundImage;
|
const hasBackground = !!backgroundImage;
|
||||||
|
|
||||||
const sendRequest = async () => {
|
const sendRequest = async (bodyOverride?: string) => {
|
||||||
if (isFetching) return;
|
if (isFetching) return;
|
||||||
setIsFetching(true);
|
setIsFetching(true);
|
||||||
setResponseStatus(null);
|
setResponseStatus(null);
|
||||||
const r = toast.loading('正在发送请求...');
|
const r = toast.loading('正在发送请求...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedRequestBody = JSON.parse(requestBody);
|
const parsedRequestBody = JSON.parse(bodyOverride ?? requestBody);
|
||||||
|
|
||||||
// 如果有 adapterName,走后端转发
|
// 如果有 adapterName,走后端转发
|
||||||
if (adapterName) {
|
if (adapterName) {
|
||||||
@ -127,93 +137,132 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (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(() => {
|
useEffect(() => {
|
||||||
setRequestBody(generateDefaultJson(data.request));
|
setRequestBody(generateDefaultJson(data.request));
|
||||||
setResponseContent('');
|
setResponseContent('');
|
||||||
setResponseStatus(null);
|
setResponseStatus(null);
|
||||||
}, [path]);
|
}, [path]);
|
||||||
|
|
||||||
// Height Resizing Logic
|
// Sync from storage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
setResponseHeight(storedHeight);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const startY = e.clientY;
|
const startY = e.clientY;
|
||||||
const startHeight = responseHeight;
|
const startHeight = responseHeight;
|
||||||
|
let currentH = startHeight;
|
||||||
|
let frameId: number;
|
||||||
|
|
||||||
const handleMouseMove = (mv: MouseEvent) => {
|
const handleMouseMove = (mv: MouseEvent) => {
|
||||||
const delta = startY - mv.clientY;
|
if (frameId) cancelAnimationFrame(frameId);
|
||||||
// 向上拖动 -> 增加高度
|
frameId = requestAnimationFrame(() => {
|
||||||
setResponseHeight(Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta)));
|
const delta = startY - mv.clientY;
|
||||||
|
currentH = Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta));
|
||||||
|
setResponseHeight(currentH);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
if (frameId) cancelAnimationFrame(frameId);
|
||||||
|
setStoredHeight(currentH);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
}, [responseHeight, setResponseHeight]);
|
}, [responseHeight, setStoredHeight]);
|
||||||
|
|
||||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||||
// 阻止默认滚动行为可能需要谨慎,这里尽量只阻止 handle 上的
|
|
||||||
// e.preventDefault();
|
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
const startY = touch.clientY;
|
const startY = touch.clientY;
|
||||||
const startHeight = responseHeight;
|
const startHeight = responseHeight;
|
||||||
|
let currentH = startHeight;
|
||||||
|
let frameId: number;
|
||||||
|
|
||||||
const handleTouchMove = (mv: TouchEvent) => {
|
const handleTouchMove = (mv: TouchEvent) => {
|
||||||
const mvTouch = mv.touches[0];
|
if (frameId) cancelAnimationFrame(frameId);
|
||||||
const delta = startY - mvTouch.clientY;
|
frameId = requestAnimationFrame(() => {
|
||||||
setResponseHeight(Math.max(100, Math.min(window.innerHeight - 200, startHeight + delta)));
|
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 = () => {
|
const handleTouchEnd = () => {
|
||||||
document.removeEventListener('touchmove', handleTouchMove);
|
document.removeEventListener('touchmove', handleTouchMove);
|
||||||
document.removeEventListener('touchend', handleTouchEnd);
|
document.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
if (frameId) cancelAnimationFrame(frameId);
|
||||||
|
setStoredHeight(currentH);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('touchmove', handleTouchMove);
|
document.addEventListener('touchmove', handleTouchMove);
|
||||||
document.addEventListener('touchend', handleTouchEnd);
|
document.addEventListener('touchend', handleTouchEnd);
|
||||||
}, [responseHeight, setResponseHeight]);
|
}, [responseHeight, setStoredHeight]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='h-full flex flex-col overflow-hidden bg-transparent'>
|
|
||||||
{/* URL Bar */}
|
<div className='flex flex-col h-full w-full relative overflow-hidden'>
|
||||||
<div className='flex flex-wrap md:flex-nowrap items-center gap-2 p-2 md:p-4 pb-2 flex-shrink-0'>
|
{/* 1. Top Toolbar: URL & Actions */}
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex-grow flex items-center gap-2 px-3 md:px-4 h-10 rounded-xl transition-all w-full md:w-auto',
|
'flex items-center gap-4 px-4 py-2 border-b flex-shrink-0 z-10',
|
||||||
hasBackground ? 'bg-white/5' : 'bg-black/5 dark:bg-white/5'
|
hasBackground ? 'border-white/10 bg-white/5' : 'border-black/5 dark:border-white/10 bg-white/40 dark:bg-black/20'
|
||||||
)}>
|
)}>
|
||||||
<Chip size="sm" variant="shadow" color="primary" className="font-bold text-[10px] h-5 min-w-[40px]">POST</Chip>
|
{/* Method & Path */}
|
||||||
<span className={clsx(
|
{/* Method & Path */}
|
||||||
'text-xs font-mono truncate select-all flex-1 opacity-50',
|
{/* Method & Path */}
|
||||||
hasBackground ? 'text-white' : 'text-default-600'
|
<div className="flex items-center gap-3 flex-1 min-w-0 pl-1">
|
||||||
)}>{path}</span>
|
<div className={clsx(
|
||||||
|
'text-sm font-mono truncate select-all px-2 py-1 rounded-md transition-colors',
|
||||||
|
hasBackground ? 'text-white/90 bg-black/10' : 'text-foreground dark:text-white/90 bg-default-100/50'
|
||||||
|
)}>
|
||||||
|
{path}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex items-center gap-2 flex-shrink-0 ml-auto'>
|
{/* Actions */}
|
||||||
<Popover placement='bottom-end' backdrop='blur'>
|
<div className='flex items-center gap-2'>
|
||||||
|
<Popover placement='bottom-end' backdrop='transparent'>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button size='sm' variant='light' radius='full' isIconOnly className='h-10 w-10 opacity-40 hover:opacity-100'>
|
<Button size='sm' variant='light' radius='sm' isIconOnly className='opacity-60 hover:opacity-100'>
|
||||||
<IoSettingsSharp className="text-lg" />
|
<IoSettingsSharp className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className='w-[260px] p-3 rounded-xl border border-white/10 shadow-2xl bg-white/80 dark:bg-black/80 backdrop-blur-xl'>
|
<PopoverContent className='w-[260px] p-3 rounded-md border border-white/10 shadow-2xl bg-white/80 dark:bg-black/80 backdrop-blur-xl'>
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<p className='text-[10px] font-bold opacity-30 uppercase tracking-widest'>Debug Setup</p>
|
<p className='text-[10px] font-bold opacity-30 uppercase tracking-widest'>Debug Setup</p>
|
||||||
<Input label='Base URL' value={httpConfig.url} onChange={(e) => setHttpConfig({ ...httpConfig, url: e.target.value })} size='sm' variant='flat' />
|
<Input label='Base URL' labelPlacement="outside" placeholder="http://..." value={httpConfig.url} onChange={(e) => setHttpConfig({ ...httpConfig, url: e.target.value })} size='sm' variant='bordered' />
|
||||||
<Input label='Token' value={httpConfig.token} onChange={(e) => setHttpConfig({ ...httpConfig, token: e.target.value })} size='sm' variant='flat' />
|
<Input label='Token' labelPlacement="outside" placeholder="access_token" value={httpConfig.token} onChange={(e) => setHttpConfig({ ...httpConfig, token: e.target.value })} size='sm' variant='bordered' />
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onPress={sendRequest}
|
onPress={() => sendRequest()}
|
||||||
color='primary'
|
color='primary'
|
||||||
radius='full'
|
radius='sm'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='h-10 px-6 font-bold shadow-md shadow-primary/20 hover:scale-[1.02] active:scale-[0.98]'
|
className='font-bold shadow-sm px-4'
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
startContent={!isFetching && <IoSend className="text-xs" />}
|
startContent={!isFetching && <IoSend className="text-xs" />}
|
||||||
>
|
>
|
||||||
@ -222,85 +271,79 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex-1 flex flex-col min-h-0 bg-transparent'>
|
{/* 2. Main Workspace (Request) - Flexible Height */}
|
||||||
<div className='px-4 flex flex-wrap items-center justify-between flex-shrink-0 min-h-[36px] gap-2 py-1'>
|
<div className='flex-1 min-h-0 flex flex-col relative'>
|
||||||
<Tabs
|
<div className='flex-1 flex flex-col overflow-hidden relative'>
|
||||||
size="sm"
|
{/* Request Toolbar */}
|
||||||
variant="underlined"
|
|
||||||
selectedKey={activeTab}
|
|
||||||
onSelectionChange={setActiveTab}
|
|
||||||
classNames={{
|
|
||||||
cursor: 'bg-primary h-0.5',
|
|
||||||
tab: 'px-0 mr-5 h-8',
|
|
||||||
tabList: 'p-0 border-none',
|
|
||||||
tabContent: 'text-[11px] font-bold opacity-30 group-data-[selected=true]:opacity-80 transition-opacity'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tab key="request" title="请求参数" />
|
|
||||||
<Tab key="docs" title="接口定义" />
|
|
||||||
</Tabs>
|
|
||||||
<div className='flex items-center gap-1 ml-auto'>
|
|
||||||
<ChatInputModal>
|
|
||||||
{(onOpen) => (
|
|
||||||
<Tooltip content="构造消息 (CQ码)" closeDelay={0}>
|
|
||||||
<Button
|
|
||||||
isIconOnly
|
|
||||||
size='sm'
|
|
||||||
variant='light'
|
|
||||||
radius='full'
|
|
||||||
className='h-7 w-7 text-primary/80 bg-primary/10 hover:bg-primary/20'
|
|
||||||
onPress={onOpen}
|
|
||||||
>
|
|
||||||
<TbMessageCode size={16} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</ChatInputModal>
|
|
||||||
|
|
||||||
<Tooltip content="生成示例参数" closeDelay={0}>
|
|
||||||
<Button
|
|
||||||
isIconOnly
|
|
||||||
size='sm'
|
|
||||||
variant='light'
|
|
||||||
radius='full'
|
|
||||||
className='h-7 w-7 text-default-400 hover:text-primary hover:bg-default-100/50'
|
|
||||||
onPress={() => setRequestBody(generateDefaultJson(data.request))}
|
|
||||||
>
|
|
||||||
<TbCode size={16} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-1 min-h-0 relative px-3 pb-2 mt-1'>
|
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'h-full transition-all',
|
'px-4 flex items-center justify-between h-10 flex-shrink-0 border-b',
|
||||||
activeTab !== 'request' && 'rounded-xl overflow-y-auto no-scrollbar',
|
hasBackground ? 'border-white/10' : 'border-default-100 dark:border-white/10'
|
||||||
hasBackground ? 'bg-transparent' : (activeTab !== 'request' && 'bg-white/10 dark:bg-black/10')
|
|
||||||
)}>
|
)}>
|
||||||
|
<Tabs
|
||||||
|
aria-label="Request Options"
|
||||||
|
size="sm"
|
||||||
|
variant="underlined"
|
||||||
|
selectedKey={activeTab}
|
||||||
|
onSelectionChange={setActiveTab}
|
||||||
|
classNames={{
|
||||||
|
tabList: 'p-0 gap-6 bg-transparent',
|
||||||
|
cursor: 'w-full bg-foreground dark:bg-white h-[2px]',
|
||||||
|
tab: 'px-0 h-full',
|
||||||
|
tabContent: 'text-xs font-medium text-default-500 dark:text-white/50 group-data-[selected=true]:text-foreground dark:group-data-[selected=true]:text-white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab key="request" title="请求体" />
|
||||||
|
<Tab key="docs" title="接口文档" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-1 opacity-70'>
|
||||||
|
<ChatInputModal>
|
||||||
|
{(onOpen) => (
|
||||||
|
<Tooltip content="构造 CQ 码" closeDelay={0}>
|
||||||
|
<Button isIconOnly size='sm' variant='light' radius='sm' className='w-8 h-8' onPress={onOpen}>
|
||||||
|
<TbMessageCode size={16} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</ChatInputModal>
|
||||||
|
<Tooltip content="生成示例" closeDelay={0}>
|
||||||
|
<Button isIconOnly size='sm' variant='light' radius='sm' className='w-8 h-8' onPress={() => setRequestBody(generateDefaultJson(data.request))}>
|
||||||
|
<TbCode size={16} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<div className='flex-1 relative overflow-hidden'>
|
||||||
{activeTab === 'request' ? (
|
{activeTab === 'request' ? (
|
||||||
<CodeEditor
|
<div className="absolute inset-0">
|
||||||
value={requestBody}
|
<CodeEditor
|
||||||
onChange={(value) => setRequestBody(value ?? '')}
|
value={requestBody}
|
||||||
language='json'
|
onChange={(value) => setRequestBody(value ?? '')}
|
||||||
options={{
|
language='json'
|
||||||
minimap: { enabled: false },
|
options={{
|
||||||
fontSize: 12,
|
minimap: { enabled: false },
|
||||||
scrollBeyondLastLine: false,
|
fontSize: 13,
|
||||||
wordWrap: 'on',
|
fontFamily: 'JetBrains Mono, monospace',
|
||||||
padding: { top: 12 },
|
scrollBeyondLastLine: false,
|
||||||
lineNumbersMinChars: 3
|
wordWrap: 'on',
|
||||||
}}
|
padding: { top: 16, bottom: 16 },
|
||||||
/>
|
lineNumbersMinChars: 3,
|
||||||
|
chromeless: true,
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='p-6 space-y-10'>
|
<div className='p-6 space-y-8 overflow-y-auto h-full scrollbar-hide'>
|
||||||
<section>
|
<section>
|
||||||
<h3 className='text-[10px] font-bold opacity-20 uppercase tracking-[0.2em] mb-4'>Request - 请求数据结构</h3>
|
<h3 className='text-[10px] font-bold text-default-700 dark:text-default-50 uppercase tracking-widest mb-4'>Request Params</h3>
|
||||||
<DisplayStruct schema={parsedRequest} />
|
<DisplayStruct schema={parsedRequest} />
|
||||||
</section>
|
</section>
|
||||||
<div className='h-px bg-white/5 w-full' />
|
<div className='h-px bg-white/10 w-full' />
|
||||||
<section>
|
<section>
|
||||||
<h3 className='text-[10px] font-bold opacity-20 uppercase tracking-[0.2em] mb-4'>Response - 返回数据结构</h3>
|
<h3 className='text-[10px] font-bold text-default-700 dark:text-default-50 uppercase tracking-widest mb-4'>Response Data</h3>
|
||||||
<DisplayStruct schema={parsedResponse} />
|
<DisplayStruct schema={parsedResponse} />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -309,73 +352,79 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Response Area */}
|
{/* 3. Response Panel (Bottom) */}
|
||||||
<div className='flex-shrink-0 px-3 pb-3'>
|
<div
|
||||||
|
className='flex-shrink-0 flex flex-col overflow-hidden relative'
|
||||||
|
style={{ height: responseExpanded ? undefined : 'auto' }}
|
||||||
|
>
|
||||||
|
{/* Resize Handle / Header */}
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-xl transition-all overflow-hidden border border-white/5 flex flex-col',
|
'flex items-center justify-between px-4 py-1.5 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 transition-colors select-none group relative border-t',
|
||||||
hasBackground ? 'bg-white/5' : 'bg-white/5 dark:bg-black/5'
|
hasBackground ? 'border-white/10' : 'border-default-100 dark:border-white/10'
|
||||||
)}
|
)}
|
||||||
|
onClick={() => setResponseExpanded(!responseExpanded)}
|
||||||
>
|
>
|
||||||
{/* Header & Resize Handle */}
|
{/* Invisible Draggable Area */}
|
||||||
<div
|
{responseExpanded && (
|
||||||
className='flex items-center justify-between px-4 py-2 cursor-pointer hover:bg-white/5 transition-all select-none relative group'
|
<div
|
||||||
onClick={() => setResponseExpanded(!responseExpanded)}
|
className="absolute -top-1.5 left-0 w-full h-4 cursor-ns-resize z-20"
|
||||||
>
|
onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e); }}
|
||||||
{/* Invisble Resize Area that becomes visible/active */}
|
onTouchStart={(e) => { e.stopPropagation(); handleTouchStart(e); }}
|
||||||
{responseExpanded && (
|
onClick={(e) => e.stopPropagation()}
|
||||||
<div
|
/>
|
||||||
className="absolute -top-1 left-0 w-full h-3 cursor-ns-resize z-50 flex items-center justify-center opacity-0 hover:opacity-100 group-hover:opacity-100 transition-opacity"
|
)}
|
||||||
onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e); }}
|
|
||||||
onTouchStart={(e) => { e.stopPropagation(); handleTouchStart(e); }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="w-12 h-1 bg-white/20 rounded-full" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<IoChevronDown className={clsx('text-[10px] transition-transform duration-300 opacity-20', !responseExpanded && '-rotate-90')} />
|
<div className={clsx('transition-transform duration-200', !responseExpanded && '-rotate-90')}>
|
||||||
<span className='text-[10px] font-semibold tracking-wide opacity-30 uppercase'>Response</span>
|
<IoChevronDown size={14} className="opacity-50" />
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
{responseStatus && (
|
|
||||||
<Chip size="sm" variant="flat" color={responseStatus.code >= 200 && responseStatus.code < 300 ? 'success' : 'danger'} className="h-4 text-[9px] font-mono px-1.5 opacity-50">
|
|
||||||
{responseStatus.code}
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
<Button size='sm' variant='light' isIconOnly radius='full' className='h-6 w-6 opacity-20 hover:opacity-80 transition-opacity' onClick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(responseContent); toast.success('已复制'); }}>
|
|
||||||
<IoCopy size={10} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className={clsx(
|
||||||
|
'text-[10px] font-bold tracking-widest uppercase',
|
||||||
|
hasBackground ? 'text-white' : 'text-foreground dark:text-white'
|
||||||
|
)}>Response</span>
|
||||||
|
{responseStatus && (
|
||||||
|
<Chip size="sm" variant="dot" color={responseStatus.code >= 200 && responseStatus.code < 300 ? 'success' : 'danger'} className="h-5 text-[10px] font-mono border-none bg-transparent pl-0">
|
||||||
|
{responseStatus.code} {responseStatus.text}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Response Content - Code Editor */}
|
<Button size='sm' variant='light' isIconOnly radius='sm' className='h-6 w-6 opacity-40 hover:opacity-100' onClick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(responseContent); toast.success('已复制'); }}>
|
||||||
{responseExpanded && (
|
<IoCopy size={12} />
|
||||||
<div style={{ height: responseHeight }} className="relative bg-transparent">
|
</Button>
|
||||||
<PageLoading loading={isFetching} />
|
</div>
|
||||||
|
|
||||||
|
{/* Response Editor */}
|
||||||
|
{responseExpanded && (
|
||||||
|
<div style={{ height: responseHeight }} className="relative bg-transparent">
|
||||||
|
<PageLoading loading={isFetching} />
|
||||||
|
<div className="absolute inset-0">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={responseContent || '// Waiting for response...'}
|
value={responseContent || '// Waiting for response...'}
|
||||||
language='json'
|
language='json'
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
|
fontFamily: 'JetBrains Mono, monospace',
|
||||||
lineNumbers: 'off',
|
lineNumbers: 'off',
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
wordWrap: 'on',
|
wordWrap: 'on',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
folding: true,
|
folding: true,
|
||||||
padding: { top: 8, bottom: 8 },
|
padding: { top: 12, bottom: 12 },
|
||||||
renderLineHighlight: 'none',
|
renderLineHighlight: 'none',
|
||||||
automaticLayout: true
|
chromeless: true,
|
||||||
|
backgroundColor: 'transparent'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default OneBotApiDebug;
|
export default OneBotApiDebug;
|
||||||
|
|||||||
@ -143,21 +143,23 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
key={api.path}
|
key={api.path}
|
||||||
onClick={() => onSelect(api.path)}
|
onClick={() => onSelect(api.path)}
|
||||||
className={clsx(
|
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
|
isSelected
|
||||||
? (hasBackground ? '' : 'bg-primary/20 border-primary/20 shadow-sm')
|
? (hasBackground
|
||||||
: 'hover:bg-white/5'
|
? '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'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'text-[12px] font-medium transition-colors truncate',
|
'text-[12px] font-medium transition-colors truncate',
|
||||||
isSelected ? 'text-primary' : 'opacity-60'
|
isSelected ? 'text-primary' : 'opacity-70'
|
||||||
)}>
|
)}>
|
||||||
{api.description}
|
{api.description}
|
||||||
</span>
|
</span>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'text-[10px] font-mono truncate transition-all',
|
'text-[10px] font-mono truncate transition-all',
|
||||||
isSelected ? 'text-primary/60' : 'opacity-20'
|
isSelected ? 'text-primary/60' : 'opacity-30'
|
||||||
)}>
|
)}>
|
||||||
{api.path}
|
{api.path}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -70,7 +70,10 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
|||||||
<motion.div className='w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right p-4'>
|
<motion.div className='w-64 flex flex-col items-stretch h-full transition-transform duration-300 ease-in-out z-30 relative float-right p-4'>
|
||||||
<div className='flex items-center justify-start gap-3 px-2 my-8 ml-2'>
|
<div className='flex items-center justify-start gap-3 px-2 my-8 ml-2'>
|
||||||
<div className="h-5 w-1 bg-primary rounded-full shadow-sm" />
|
<div className="h-5 w-1 bg-primary rounded-full shadow-sm" />
|
||||||
<div className="text-xl font-bold text-default-900 dark:text-white tracking-wide select-none">
|
<div className={clsx(
|
||||||
|
"text-xl font-bold tracking-wide select-none",
|
||||||
|
hasBackground ? 'text-white' : 'text-default-900 dark:text-white'
|
||||||
|
)}>
|
||||||
NapCat
|
NapCat
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -183,7 +183,7 @@ const theme: ThemeConfig = {
|
|||||||
'--heroui-primary-800': '339.33 86.54% 20.39%',
|
'--heroui-primary-800': '339.33 86.54% 20.39%',
|
||||||
'--heroui-primary-900': '340 84.91% 10.39%',
|
'--heroui-primary-900': '340 84.91% 10.39%',
|
||||||
'--heroui-primary-foreground': '0 0% 100%',
|
'--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-50': '270 61.54% 94.9%',
|
||||||
'--heroui-secondary-100': '270 59.26% 89.41%',
|
'--heroui-secondary-100': '270 59.26% 89.41%',
|
||||||
'--heroui-secondary-200': '270 59.26% 78.82%',
|
'--heroui-secondary-200': '270 59.26% 78.82%',
|
||||||
|
|||||||
@ -1,31 +1,43 @@
|
|||||||
import { Button } from '@heroui/button';
|
import { Button } from '@heroui/button';
|
||||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { IoClose } from 'react-icons/io5';
|
import { IoClose } from 'react-icons/io5';
|
||||||
import { TbSquareRoundedChevronLeftFilled } from 'react-icons/tb';
|
import { TbSearch } from 'react-icons/tb';
|
||||||
|
|
||||||
import key from '@/const/key';
|
import key from '@/const/key';
|
||||||
import oneBotHttpApi from '@/const/ob_api';
|
import oneBotHttpApi from '@/const/ob_api';
|
||||||
import type { OneBotHttpApiPath } from '@/const/ob_api';
|
import type { OneBotHttpApiPath } from '@/const/ob_api';
|
||||||
|
|
||||||
import OneBotApiDebug from '@/components/onebot/api/debug';
|
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 () {
|
export default function HttpDebug () {
|
||||||
const [activeApi, setActiveApi] = useState<OneBotHttpApiPath | null>('/set_qq_profile');
|
const [activeApi, setActiveApi] = useState<OneBotHttpApiPath | null>(null);
|
||||||
const [openApis, setOpenApis] = useState<OneBotHttpApiPath[]>(['/set_qq_profile']);
|
const [openApis, setOpenApis] = useState<OneBotHttpApiPath[]>([]);
|
||||||
const [openSideBar, setOpenSideBar] = useState(true);
|
|
||||||
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
||||||
const hasBackground = !!backgroundImage;
|
const hasBackground = !!backgroundImage;
|
||||||
|
|
||||||
const [adapterName, setAdapterName] = useState<string>('');
|
const [adapterName, setAdapterName] = useState<string>('');
|
||||||
|
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||||
|
|
||||||
// Auto-collapse sidebar on mobile initial load
|
const debugRefs = useRef(new Map<string, OneBotApiDebugRef>());
|
||||||
|
const [pendingRun, setPendingRun] = useState<{ path: OneBotHttpApiPath; body: string; } | null>(null);
|
||||||
|
|
||||||
|
// Ctrl/Cmd + K 打开命令面板
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.innerWidth < 768) {
|
const handler = (e: KeyboardEvent) => {
|
||||||
setOpenSideBar(false);
|
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
|
// Initialize Debug Adapter
|
||||||
@ -64,9 +76,48 @@ export default function HttpDebug () {
|
|||||||
setOpenApis([...openApis, api]);
|
setOpenApis([...openApis, api]);
|
||||||
}
|
}
|
||||||
setActiveApi(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) => {
|
const handleCloseTab = (e: React.MouseEvent, apiToRemove: OneBotHttpApiPath) => {
|
||||||
@ -76,9 +127,6 @@ export default function HttpDebug () {
|
|||||||
|
|
||||||
if (activeApi === apiToRemove) {
|
if (activeApi === apiToRemove) {
|
||||||
if (newOpenApis.length > 0) {
|
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]);
|
setActiveApi(newOpenApis[newOpenApis.length - 1]);
|
||||||
} else {
|
} else {
|
||||||
setActiveApi(null);
|
setActiveApi(null);
|
||||||
@ -89,50 +137,24 @@ export default function HttpDebug () {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>HTTP调试 - NapCat WebUI</title>
|
<title>HTTP调试 - NapCat WebUI</title>
|
||||||
<div className='h-[calc(100vh-3.5rem)] p-0 md:p-4'>
|
<div className='h-[calc(100vh-3.5rem)] pt-2 px-0 md:px-4'>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'h-full flex flex-col overflow-hidden transition-all relative',
|
'h-full flex flex-col overflow-hidden transition-all relative',
|
||||||
'rounded-none md:rounded-2xl',
|
// 'rounded-none md:rounded-2xl border', // Removing the main border/radius
|
||||||
hasBackground
|
// hasBackground
|
||||||
? 'bg-white/5 dark:bg-black/5 backdrop-blur-sm'
|
// ? 'bg-white/5 dark:bg-black/5 backdrop-blur-sm border-white/10'
|
||||||
: 'bg-white/20 dark:bg-black/10 backdrop-blur-sm shadow-sm'
|
// : 'bg-white/40 dark:bg-black/20 backdrop-blur-md shadow-sm border-white/40 dark:border-white/10'
|
||||||
|
'bg-transparent'
|
||||||
)}>
|
)}>
|
||||||
{/* Unifed Header */}
|
<div className='flex-1 flex flex-col overflow-hidden relative'>
|
||||||
<div className='h-12 border-b border-white/10 flex items-center justify-between px-4 z-50 bg-white/5 flex-shrink-0'>
|
<div className={clsx(
|
||||||
<div className='flex items-center gap-3'>
|
'flex items-center w-full flex-shrink-0 pr-2 md:pl-4 py-1 relative z-20 rounded-md',
|
||||||
<Button
|
hasBackground
|
||||||
isIconOnly
|
? 'bg-white/5'
|
||||||
size="sm"
|
: 'bg-white/30 dark:bg-white/5'
|
||||||
variant="light"
|
)}>
|
||||||
className={clsx(
|
{/* Tab List */}
|
||||||
"opacity-50 hover:opacity-100 transition-all",
|
<div className="flex-1 overflow-x-auto no-scrollbar flex items-center">
|
||||||
openSideBar && "text-primary opacity-100"
|
|
||||||
)}
|
|
||||||
onPress={() => setOpenSideBar(!openSideBar)}
|
|
||||||
>
|
|
||||||
<TbSquareRoundedChevronLeftFilled className={clsx("text-lg transform transition-transform", !openSideBar && "rotate-180")} />
|
|
||||||
</Button>
|
|
||||||
<h1 className={clsx(
|
|
||||||
'text-sm font-bold tracking-tight',
|
|
||||||
hasBackground ? 'text-white/80' : 'text-default-700 dark:text-gray-200'
|
|
||||||
)}>接口调试</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-1 flex flex-row overflow-hidden relative'>
|
|
||||||
<OneBotApiNavList
|
|
||||||
data={oneBotHttpApi}
|
|
||||||
selectedApi={activeApi || '' as any}
|
|
||||||
onSelect={handleSelectApi}
|
|
||||||
openSideBar={openSideBar}
|
|
||||||
onToggle={setOpenSideBar}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className='flex-1 h-full overflow-hidden flex flex-col relative'
|
|
||||||
>
|
|
||||||
{/* Tab Bar */}
|
|
||||||
<div className='flex items-center w-full overflow-x-auto no-scrollbar border-b border-white/5 bg-white/5 flex-shrink-0'>
|
|
||||||
{openApis.map((api) => {
|
{openApis.map((api) => {
|
||||||
const isActive = api === activeApi;
|
const isActive = api === activeApi;
|
||||||
const item = oneBotHttpApi[api];
|
const item = oneBotHttpApi[api];
|
||||||
@ -141,21 +163,26 @@ export default function HttpDebug () {
|
|||||||
key={api}
|
key={api}
|
||||||
onClick={() => setActiveApi(api)}
|
onClick={() => setActiveApi(api)}
|
||||||
className={clsx(
|
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
|
isActive
|
||||||
? (hasBackground ? 'bg-white/10 text-white' : 'bg-white/40 dark:bg-white/5 text-primary font-medium')
|
? (hasBackground
|
||||||
: 'opacity-50 hover:opacity-100 hover:bg-white/5'
|
? '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')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'text-[10px] font-bold uppercase tracking-wider',
|
'text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded-sm',
|
||||||
isActive ? 'opacity-100' : 'opacity-50'
|
isActive
|
||||||
|
? 'bg-success/20 text-success'
|
||||||
|
: 'opacity-60 bg-default-200/50 dark:bg-white/10'
|
||||||
)}>POST</span>
|
)}>POST</span>
|
||||||
<span className='text-xs truncate flex-1'>{item?.description || api}</span>
|
<span className='text-xs truncate flex-1'>{item?.description || api}</span>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'p-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/20 transition-opacity',
|
'p-0.5 rounded-sm hover:bg-black/10 dark:hover:bg-white/20 transition-opacity',
|
||||||
isActive ? 'opacity-50 hover:opacity-100' : 'opacity-0 group-hover:opacity-50'
|
isActive ? 'opacity-40 hover:opacity-100' : 'opacity-0 group-hover:opacity-40'
|
||||||
)}
|
)}
|
||||||
onClick={(e) => handleCloseTab(e, api)}
|
onClick={(e) => handleCloseTab(e, api)}
|
||||||
>
|
>
|
||||||
@ -163,37 +190,67 @@ export default function HttpDebug () {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Panels */}
|
{/* Actions */}
|
||||||
<div className='flex-1 relative overflow-hidden'>
|
<div className='flex items-center gap-2 pl-2 border-l border-white/5 flex-shrink-0'>
|
||||||
{activeApi === null && (
|
<Button
|
||||||
<div className='h-full flex items-center justify-center text-default-400 text-sm opacity-50 select-none'>
|
isIconOnly
|
||||||
选择一个接口开始调试
|
size='sm'
|
||||||
</div>
|
radius='sm'
|
||||||
)}
|
variant='light'
|
||||||
|
className='text-default-500 hover:text-primary w-10 h-10 min-w-10'
|
||||||
{openApis.map((api) => (
|
onClick={() => setPaletteOpen(true)}
|
||||||
<div
|
onPress={() => setPaletteOpen(true)}
|
||||||
key={api}
|
>
|
||||||
className={clsx(
|
<TbSearch size={18} />
|
||||||
'h-full w-full absolute top-0 left-0 transition-opacity duration-200',
|
</Button>
|
||||||
api === activeApi ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<OneBotApiDebug
|
|
||||||
path={api}
|
|
||||||
data={oneBotHttpApi[api]}
|
|
||||||
adapterName={adapterName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Content Panels */}
|
||||||
|
<div className='flex-1 relative overflow-hidden'>
|
||||||
|
{activeApi === null && (
|
||||||
|
<div className='h-full flex items-center justify-center text-default-400 text-sm opacity-50 select-none'>
|
||||||
|
使用命令面板选择接口(Ctrl/Cmd + K)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{openApis.map((api) => (
|
||||||
|
<div
|
||||||
|
key={api}
|
||||||
|
className={clsx(
|
||||||
|
'h-full w-full absolute top-0 left-0 transition-opacity duration-200',
|
||||||
|
api === activeApi ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<OneBotApiDebug
|
||||||
|
ref={(node) => {
|
||||||
|
if (!node) {
|
||||||
|
debugRefs.current.delete(api);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugRefs.current.set(api, node);
|
||||||
|
}}
|
||||||
|
path={api}
|
||||||
|
data={oneBotHttpApi[api]}
|
||||||
|
adapterName={adapterName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CommandPalette
|
||||||
|
isOpen={paletteOpen}
|
||||||
|
onOpenChange={setPaletteOpen}
|
||||||
|
commands={commands}
|
||||||
|
onExecute={executeCommand}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user