mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-28 20:31:26 +08:00
Refactor UI styles for improved consistency and clarity
Unified card backgrounds, borders, and shadows across components for a more consistent look. Enhanced table, tab, and button styles for clarity and accessibility. Improved layout and modal structure in OneBot API debug, added modal for struct display, and optimized WebSocket debug connection logic. Updated file manager, logs, network, and terminal pages for visual consistency. Refactored interface definitions for stricter typing and readability.
This commit is contained in:
parent
872a3e0100
commit
8697061a90
@ -4,19 +4,19 @@ import clsx from 'clsx';
|
||||
import { title } from '../primitives';
|
||||
|
||||
export interface ContainerProps {
|
||||
title: string
|
||||
tag?: React.ReactNode
|
||||
action: React.ReactNode
|
||||
enableSwitch: React.ReactNode
|
||||
children: React.ReactNode
|
||||
title: string;
|
||||
tag?: React.ReactNode;
|
||||
action: React.ReactNode;
|
||||
enableSwitch: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface DisplayCardProps {
|
||||
showType?: boolean
|
||||
onEdit: () => void
|
||||
onEnable: () => Promise<void>
|
||||
onDelete: () => Promise<void>
|
||||
onEnableDebug: () => Promise<void>
|
||||
showType?: boolean;
|
||||
onEdit: () => void;
|
||||
onEnable: () => Promise<void>;
|
||||
onDelete: () => Promise<void>;
|
||||
onEnableDebug: () => Promise<void>;
|
||||
}
|
||||
|
||||
const DisplayCardContainer: React.FC<ContainerProps> = ({
|
||||
@ -27,7 +27,7 @@ const DisplayCardContainer: React.FC<ContainerProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Card className='bg-opacity-50 backdrop-blur-sm'>
|
||||
<Card className='bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm'>
|
||||
<CardHeader className='pb-0 flex items-center'>
|
||||
{tag && (
|
||||
<div className='text-center text-default-400 mb-1 absolute top-0 left-1/2 -translate-x-1/2 text-sm pointer-events-none bg-warning-100 dark:bg-warning-50 px-2 rounded-b'>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { Card, CardBody } from '@heroui/card';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { title } from '@/components/primitives';
|
||||
|
||||
|
||||
export interface NetworkItemDisplayProps {
|
||||
count: number
|
||||
label: string
|
||||
size?: 'sm' | 'md'
|
||||
count: number;
|
||||
label: string;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||
@ -17,35 +17,26 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||
return (
|
||||
<Card
|
||||
className={clsx(
|
||||
'bg-opacity-60 shadow-sm md:rounded-3xl',
|
||||
'bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm transition-all hover:bg-white/70 dark:hover:bg-black/30',
|
||||
size === 'md'
|
||||
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
|
||||
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
||||
? 'col-span-8 md:col-span-2'
|
||||
: 'col-span-2 md:col-span-1'
|
||||
)}
|
||||
shadow='sm'
|
||||
shadow='none'
|
||||
>
|
||||
<CardBody className='items-center md:gap-1 p-1 md:p-2'>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1',
|
||||
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
||||
title({
|
||||
color: size === 'md' ? 'pink' : 'yellow',
|
||||
size,
|
||||
})
|
||||
'flex-1 font-mono font-bold text-default-700 dark:text-gray-200',
|
||||
size === 'md' ? 'text-4xl md:text-5xl' : 'text-2xl md:text-3xl',
|
||||
)}
|
||||
>
|
||||
{count}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'whitespace-nowrap text-nowrap flex-shrink-0',
|
||||
size === 'md' ? 'text-sm md:text-base' : 'text-xs md:text-sm',
|
||||
title({
|
||||
color: size === 'md' ? 'pink' : 'yellow',
|
||||
shadow: true,
|
||||
size: 'xxs',
|
||||
})
|
||||
'whitespace-nowrap text-nowrap flex-shrink-0 font-medium text-default-500',
|
||||
size === 'md' ? 'text-sm' : 'text-xs',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
|
||||
@ -25,21 +25,21 @@ import { supportedPreviewExts } from './file_preview_modal';
|
||||
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button';
|
||||
|
||||
export interface FileTableProps {
|
||||
files: FileInfo[]
|
||||
currentPath: string
|
||||
loading: boolean
|
||||
sortDescriptor: SortDescriptor
|
||||
onSortChange: (descriptor: SortDescriptor) => void
|
||||
selectedFiles: Selection
|
||||
onSelectionChange: (selected: Selection) => void
|
||||
onDirectoryClick: (dirPath: string) => void
|
||||
onEdit: (filePath: string) => void
|
||||
onPreview: (filePath: string) => void
|
||||
onRenameRequest: (name: string) => void
|
||||
onMoveRequest: (name: string) => void
|
||||
onCopyPath: (fileName: string) => void
|
||||
onDelete: (filePath: string) => void
|
||||
onDownload: (filePath: string) => void
|
||||
files: FileInfo[];
|
||||
currentPath: string;
|
||||
loading: boolean;
|
||||
sortDescriptor: SortDescriptor;
|
||||
onSortChange: (descriptor: SortDescriptor) => void;
|
||||
selectedFiles: Selection;
|
||||
onSelectionChange: (selected: Selection) => void;
|
||||
onDirectoryClick: (dirPath: string) => void;
|
||||
onEdit: (filePath: string) => void;
|
||||
onPreview: (filePath: string) => void;
|
||||
onRenameRequest: (name: string) => void;
|
||||
onMoveRequest: (name: string) => void;
|
||||
onCopyPath: (fileName: string) => void;
|
||||
onDelete: (filePath: string) => void;
|
||||
onDownload: (filePath: string) => void;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
@ -112,7 +112,7 @@ export default function FileTable ({
|
||||
selectedKeys={selectedFiles}
|
||||
selectionMode='multiple'
|
||||
bottomContent={
|
||||
<div className='flex w-full justify-center'>
|
||||
<div className='flex w-full justify-center p-2 border-t border-white/10'>
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
@ -121,9 +121,17 @@ export default function FileTable ({
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={(page) => setPage(page)}
|
||||
classNames={{
|
||||
cursor: 'bg-primary shadow-lg',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
classNames={{
|
||||
wrapper: 'bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm p-0',
|
||||
th: 'bg-white/40 dark:bg-white/5 backdrop-blur-md text-default-600',
|
||||
td: 'group-data-[first=true]:first:before:rounded-none group-data-[first=true]:last:before:rounded-none',
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key='name' allowsSorting>
|
||||
@ -180,7 +188,7 @@ export default function FileTable ({
|
||||
name={file.name}
|
||||
isDirectory={file.isDirectory}
|
||||
/>
|
||||
}
|
||||
}
|
||||
>
|
||||
{file.name}
|
||||
</Button>
|
||||
@ -194,43 +202,43 @@ export default function FileTable ({
|
||||
</TableCell>
|
||||
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<ButtonGroup size='sm'>
|
||||
<ButtonGroup size='sm' variant='light'>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
color='default'
|
||||
className='text-default-500 hover:text-primary'
|
||||
onPress={() => onRenameRequest(file.name)}
|
||||
>
|
||||
<BiRename />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
color='default'
|
||||
className='text-default-500 hover:text-primary'
|
||||
onPress={() => onMoveRequest(file.name)}
|
||||
>
|
||||
<FiMove />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
color='default'
|
||||
className='text-default-500 hover:text-primary'
|
||||
onPress={() => onCopyPath(file.name)}
|
||||
>
|
||||
<FiCopy />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
color='default'
|
||||
className='text-default-500 hover:text-primary'
|
||||
onPress={() => onDownload(filePath)}
|
||||
>
|
||||
<FiDownload />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
color='danger'
|
||||
className='text-danger hover:bg-danger/10'
|
||||
onPress={() => onDelete(filePath)}
|
||||
>
|
||||
<FiTrash2 />
|
||||
|
||||
@ -2,6 +2,7 @@ import { Button } from '@heroui/button';
|
||||
import { Tooltip } from '@heroui/tooltip';
|
||||
import { useRequest } from 'ahooks';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IoMdQuote } from 'react-icons/io';
|
||||
import { IoCopy, IoRefresh } from 'react-icons/io5';
|
||||
|
||||
import { request } from '@/utils/request';
|
||||
@ -30,18 +31,21 @@ export default function Hitokoto () {
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div className='relative'>
|
||||
<div className='relative flex flex-col items-center justify-center p-6 min-h-[120px]'>
|
||||
{loading && <PageLoading />}
|
||||
{error
|
||||
? (
|
||||
<div className='text-primary-400'>一言加载失败:{error.message}</div>
|
||||
<div className='text-danger'>一言加载失败:{error.message}</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div>{data?.hitokoto}</div>
|
||||
<div className='text-right'>
|
||||
—— <span className='text-default-400'>{data?.from}</span>{' '}
|
||||
{data?.from_who}
|
||||
<IoMdQuote className="text-4xl text-primary/20 mb-4" />
|
||||
<div className="text-xl font-medium text-default-700 dark:text-gray-200 tracking-wide leading-relaxed italic">
|
||||
“ {data?.hitokoto} ”
|
||||
</div>
|
||||
<div className='mt-4 flex flex-col items-center text-sm'>
|
||||
<span className='font-bold text-primary-500/80'>—— {data?.from}</span>
|
||||
{data?.from_who && <span className="text-default-400 text-xs mt-1">{data?.from_who}</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@ -49,25 +53,25 @@ export default function Hitokoto () {
|
||||
<div className='flex gap-2'>
|
||||
<Tooltip content='刷新' placement='top'>
|
||||
<Button
|
||||
className="text-default-400 hover:text-primary transition-colors"
|
||||
onPress={run}
|
||||
size='sm'
|
||||
isLoading={loading}
|
||||
isIconOnly
|
||||
radius='full'
|
||||
color='primary'
|
||||
variant='flat'
|
||||
variant='light'
|
||||
>
|
||||
<IoRefresh />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content='复制' placement='top'>
|
||||
<Button
|
||||
className="text-default-400 hover:text-success transition-colors"
|
||||
onPress={onCopy}
|
||||
size='sm'
|
||||
isIconOnly
|
||||
radius='full'
|
||||
color='success'
|
||||
variant='flat'
|
||||
variant='light'
|
||||
>
|
||||
<IoCopy />
|
||||
</Button>
|
||||
|
||||
@ -2,11 +2,12 @@ import { Button } from '@heroui/button';
|
||||
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||
import { Input } from '@heroui/input';
|
||||
import { Snippet } from '@heroui/snippet';
|
||||
import { Modal, ModalBody, ModalContent, ModalHeader } from '@heroui/modal';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover';
|
||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||
import { motion } from 'motion/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IoLink, IoSend } from 'react-icons/io5';
|
||||
import { IoLink, IoSend, IoSettingsSharp } from 'react-icons/io5';
|
||||
import { PiCatDuotone } from 'react-icons/pi';
|
||||
|
||||
import key from '@/const/key';
|
||||
@ -38,9 +39,8 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
});
|
||||
const [requestBody, setRequestBody] = useState('{}');
|
||||
const [responseContent, setResponseContent] = useState('');
|
||||
const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false);
|
||||
const [isResponseOpen, setIsResponseOpen] = useState(false);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [isStructOpen, setIsStructOpen] = useState(false);
|
||||
const responseRef = useRef<HTMLDivElement>(null);
|
||||
const parsedRequest = parse(data.request);
|
||||
const parsedResponse = parse(data.response);
|
||||
@ -70,7 +70,6 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false);
|
||||
setIsResponseOpen(true);
|
||||
responseRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
@ -90,149 +89,202 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
}, [path]);
|
||||
|
||||
return (
|
||||
<section className='p-4 pt-14 rounded-lg shadow-md'>
|
||||
<h1 className='text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400'>
|
||||
<PiCatDuotone />
|
||||
{data.description}
|
||||
</h1>
|
||||
<h1 className='text-lg font-bold mb-4'>
|
||||
<Snippet
|
||||
className='bg-default-50 bg-opacity-50 backdrop-blur-md'
|
||||
symbol={<IoLink size={18} className='inline-block mr-1' />}
|
||||
tooltipProps={{
|
||||
content: '点击复制地址',
|
||||
}}
|
||||
>
|
||||
{path}
|
||||
</Snippet>
|
||||
</h1>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Input
|
||||
label='HTTP URL'
|
||||
placeholder='输入 HTTP URL'
|
||||
value={httpConfig.url}
|
||||
onChange={(e) =>
|
||||
setHttpConfig({ ...httpConfig, url: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
label='Token'
|
||||
placeholder='输入 Token'
|
||||
value={httpConfig.token}
|
||||
onChange={(e) =>
|
||||
setHttpConfig({ ...httpConfig, token: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
onPress={sendRequest}
|
||||
color='primary'
|
||||
size='lg'
|
||||
radius='full'
|
||||
isIconOnly
|
||||
isDisabled={isFetching}
|
||||
>
|
||||
<IoSend />
|
||||
</Button>
|
||||
</div>
|
||||
<Card
|
||||
shadow='sm'
|
||||
className='my-4 bg-opacity-50 backdrop-blur-md overflow-visible'
|
||||
>
|
||||
<CardHeader className='font-bold text-lg gap-1 pb-0'>
|
||||
<span className='mr-2'>请求体</span>
|
||||
<section className='p-6 pt-14 rounded-2xl bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm mx-4 mt-4 flex flex-col gap-4 h-[calc(100vh-6rem)] overflow-hidden'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h1 className='text-2xl font-bold flex items-center gap-2 text-primary-500'>
|
||||
<PiCatDuotone />
|
||||
{data.description}
|
||||
</h1>
|
||||
<Snippet
|
||||
className='bg-white/40 dark:bg-black/20 backdrop-blur-md shadow-sm border border-white/20'
|
||||
symbol={<IoLink size={18} className='inline-block mr-1' />}
|
||||
tooltipProps={{ content: '点击复制地址' }}
|
||||
>
|
||||
{path}
|
||||
</Snippet>
|
||||
<Button
|
||||
color='warning'
|
||||
variant='flat'
|
||||
onPress={() => setIsCodeEditorOpen(!isCodeEditorOpen)}
|
||||
size='sm'
|
||||
radius='full'
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='border-primary/20 hover:bg-primary/10'
|
||||
onPress={() => setIsStructOpen(true)}
|
||||
>
|
||||
{isCodeEditorOpen ? '收起' : '展开'}
|
||||
查看数据定义
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<motion.div
|
||||
ref={responseRef}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{
|
||||
opacity: isCodeEditorOpen ? 1 : 0,
|
||||
height: isCodeEditorOpen ? 'auto' : 0,
|
||||
}}
|
||||
>
|
||||
<CodeEditor
|
||||
value={requestBody}
|
||||
onChange={(value) => setRequestBody(value ?? '')}
|
||||
language='json'
|
||||
height='400px'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end gap-1'>
|
||||
<div className='flex gap-2 items-center justify-end'>
|
||||
<Popover placement='bottom-end'>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='default'
|
||||
isIconOnly
|
||||
radius='full'
|
||||
className='border-white/20 hover:bg-white/20 text-default-600'
|
||||
>
|
||||
<IoSettingsSharp className="animate-spin-slow-on-hover" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-[340px] p-4 bg-white/80 dark:bg-black/80 backdrop-blur-xl border border-white/20 shadow-xl rounded-2xl'>
|
||||
<div className='flex flex-col gap-4 w-full'>
|
||||
<h3 className='font-bold text-lg text-default-700'>请求配置</h3>
|
||||
<Input
|
||||
label='HTTP URL'
|
||||
placeholder='输入 HTTP URL'
|
||||
value={httpConfig.url}
|
||||
onChange={(e) => setHttpConfig({ ...httpConfig, url: e.target.value })}
|
||||
variant='bordered'
|
||||
labelPlacement='outside'
|
||||
classNames={{
|
||||
inputWrapper: 'bg-default-100/50 backdrop-blur-sm border-default-200/50',
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label='Token'
|
||||
placeholder='输入 Token'
|
||||
value={httpConfig.token}
|
||||
onChange={(e) => setHttpConfig({ ...httpConfig, token: e.target.value })}
|
||||
variant='bordered'
|
||||
labelPlacement='outside'
|
||||
classNames={{
|
||||
inputWrapper: 'bg-default-100/50 backdrop-blur-sm border-default-200/50',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
onPress={sendRequest}
|
||||
color='primary'
|
||||
size='lg'
|
||||
radius='full'
|
||||
className='font-bold px-8 shadow-lg shadow-primary/30'
|
||||
isLoading={isFetching}
|
||||
startContent={!isFetching && <IoSend />}
|
||||
>
|
||||
发送请求
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 grid grid-cols-1 xl:grid-cols-2 gap-4 min-h-0 overflow-hidden'>
|
||||
{/* Request Column */}
|
||||
<Card className='bg-white/40 dark:bg-white/5 backdrop-blur-md border border-white/20 shadow-sm h-full flex flex-col'>
|
||||
<CardHeader className='font-bold text-lg gap-2 pb-2 px-4 pt-4 border-b border-white/10 flex-shrink-0 justify-between items-center'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='w-2 h-6 rounded-full bg-primary-500'></span>
|
||||
请求体 (Request)
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<ChatInputModal />
|
||||
<Button
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant='flat'
|
||||
onPress={() =>
|
||||
setRequestBody(generateDefaultJson(data.request))}
|
||||
variant='light'
|
||||
onPress={() => setRequestBody(generateDefaultJson(data.request))}
|
||||
>
|
||||
填充示例请求体
|
||||
内置示例
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card
|
||||
shadow='sm'
|
||||
className='my-4 relative bg-opacity-50 backdrop-blur-md'
|
||||
>
|
||||
<PageLoading loading={isFetching} />
|
||||
<CardHeader className='font-bold text-lg gap-1 pb-0'>
|
||||
<span className='mr-2'>响应</span>
|
||||
<Button
|
||||
color='warning'
|
||||
variant='flat'
|
||||
onPress={() => setIsResponseOpen(!isResponseOpen)}
|
||||
size='sm'
|
||||
radius='full'
|
||||
>
|
||||
{isResponseOpen ? '收起' : '展开'}
|
||||
</Button>
|
||||
<Button
|
||||
color='success'
|
||||
variant='flat'
|
||||
onPress={() => {
|
||||
navigator.clipboard.writeText(responseContent);
|
||||
toast.success('响应内容已复制到剪贴板');
|
||||
}}
|
||||
size='sm'
|
||||
radius='full'
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<motion.div
|
||||
className='overflow-y-auto text-sm'
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{
|
||||
opacity: isResponseOpen ? 1 : 0,
|
||||
height: isResponseOpen ? 300 : 0,
|
||||
}}
|
||||
>
|
||||
<pre>
|
||||
<code>
|
||||
{responseContent || (
|
||||
<div className='text-gray-400'>暂无响应</div>
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</motion.div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<div className='p-2 md:p-4 border border-default-50 dark:border-default-200 rounded-lg backdrop-blur-sm'>
|
||||
<h2 className='text-xl font-semibold mb-2'>请求体结构</h2>
|
||||
<DisplayStruct schema={parsedRequest} />
|
||||
<h2 className='text-xl font-semibold mt-4 mb-2'>响应体结构</h2>
|
||||
<DisplayStruct schema={parsedResponse} />
|
||||
</CardHeader>
|
||||
<CardBody className='p-0 flex-1 relative'>
|
||||
<div className='absolute inset-0'>
|
||||
<CodeEditor
|
||||
value={requestBody}
|
||||
onChange={(value) => setRequestBody(value ?? '')}
|
||||
language='json'
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
padding: { top: 10, bottom: 10 },
|
||||
scrollBeyondLastLine: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* Response Column */}
|
||||
<Card className='bg-white/40 dark:bg-white/5 backdrop-blur-md border border-white/20 shadow-sm h-full flex flex-col'>
|
||||
<PageLoading loading={isFetching} />
|
||||
<CardHeader className='font-bold text-lg gap-2 pb-2 px-4 pt-4 border-b border-white/10 flex-shrink-0 justify-between items-center'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='w-2 h-6 rounded-full bg-secondary-500'></span>
|
||||
响应 (Response)
|
||||
</div>
|
||||
<Button
|
||||
size='sm'
|
||||
color='success'
|
||||
variant='light'
|
||||
onPress={() => {
|
||||
navigator.clipboard.writeText(responseContent);
|
||||
toast.success('已复制');
|
||||
}}
|
||||
>
|
||||
复制内容
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody className='p-0 flex-1 relative bg-black/5 dark:bg-black/30'>
|
||||
<div className='absolute inset-0 overflow-auto p-4'>
|
||||
<pre className='text-xs font-mono whitespace-pre-wrap break-all'>
|
||||
{responseContent || <span className='text-default-400 italic'>等待请求响应...</span>}
|
||||
</pre>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Struct Display - maybe put in a modal or separate tab?
|
||||
For now, putting it in a collapsed/compact area at bottom is tricky with "h-[calc(100vh)]".
|
||||
User wants "Thorough optimization".
|
||||
I will make Struct Display a Drawer or Modal, OR put it below if we want scrolling.
|
||||
But I set height to fixed full screen.
|
||||
Let's put Struct Display in a Tab or Toggle at Top?
|
||||
Or just let the main container scroll and remove fixed height?
|
||||
Layout choice: Fixed height editors are good for workflow. Structure is reference.
|
||||
I will leave Struct Display OUT of the fixed view, or add a toggle to show it.
|
||||
Let's add a "View Structure" button in header that opens a Modal.
|
||||
Yes, that's cleaner.
|
||||
*/}
|
||||
<Modal
|
||||
isOpen={isStructOpen}
|
||||
onOpenChange={setIsStructOpen}
|
||||
size='5xl'
|
||||
scrollBehavior='inside'
|
||||
backdrop='blur'
|
||||
classNames={{
|
||||
base: 'bg-white/80 dark:bg-black/80 backdrop-blur-xl border border-white/20',
|
||||
header: 'border-b border-white/10',
|
||||
body: 'p-6',
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
{() => (
|
||||
<>
|
||||
<ModalHeader className='flex flex-col gap-1'>
|
||||
API 数据结构定义
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||
<div>
|
||||
<h2 className='text-xl font-bold mb-4 text-primary-500'>请求体结构 (Request)</h2>
|
||||
<DisplayStruct schema={parsedRequest} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className='text-xl font-bold mb-4 text-secondary-500'>响应体结构 (Response)</h2>
|
||||
<DisplayStruct schema={parsedResponse} />
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</section>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -8,15 +8,15 @@ import { TbSquareRoundedChevronRightFilled } from 'react-icons/tb';
|
||||
import type { LiteralValue, ParsedSchema } from '@/utils/zod';
|
||||
|
||||
interface DisplayStructProps {
|
||||
schema: ParsedSchema | ParsedSchema[]
|
||||
schema: ParsedSchema | ParsedSchema[];
|
||||
}
|
||||
|
||||
const SchemaType = ({
|
||||
type,
|
||||
value,
|
||||
}: {
|
||||
type: string
|
||||
value?: LiteralValue
|
||||
type: string;
|
||||
value?: LiteralValue;
|
||||
}) => {
|
||||
let name = type;
|
||||
switch (type) {
|
||||
@ -57,7 +57,7 @@ const SchemaType = ({
|
||||
};
|
||||
|
||||
const SchemaLabel: React.FC<{
|
||||
schema: ParsedSchema
|
||||
schema: ParsedSchema;
|
||||
}> = ({ schema }) => (
|
||||
<>
|
||||
{Array.isArray(schema.type)
|
||||
@ -81,8 +81,8 @@ const SchemaLabel: React.FC<{
|
||||
);
|
||||
|
||||
const SchemaContainer: React.FC<{
|
||||
schema: ParsedSchema
|
||||
children: React.ReactNode
|
||||
schema: ParsedSchema;
|
||||
children: React.ReactNode;
|
||||
}> = ({ schema, children }) => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
@ -126,7 +126,7 @@ const SchemaContainer: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
|
||||
const RenderSchema: React.FC<{ schema: ParsedSchema; }> = ({ schema }) => {
|
||||
if (schema.type === 'object') {
|
||||
return (
|
||||
<SchemaContainer schema={schema}>
|
||||
@ -193,7 +193,7 @@ const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
|
||||
|
||||
const DisplayStruct: React.FC<DisplayStructProps> = ({ schema }) => {
|
||||
return (
|
||||
<div className='p-4 bg-content2 rounded-lg bg-opacity-50'>
|
||||
<div className=''>
|
||||
{Array.isArray(schema)
|
||||
? (
|
||||
schema.map((s, i) => <RenderSchema key={s.name || i} schema={s} />)
|
||||
|
||||
@ -7,10 +7,10 @@ import { useState } from 'react';
|
||||
import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api';
|
||||
|
||||
export interface OneBotApiNavListProps {
|
||||
data: OneBotHttpApi
|
||||
selectedApi: OneBotHttpApiPath
|
||||
onSelect: (apiName: OneBotHttpApiPath) => void
|
||||
openSideBar: boolean
|
||||
data: OneBotHttpApi;
|
||||
selectedApi: OneBotHttpApiPath;
|
||||
onSelect: (apiName: OneBotHttpApiPath) => void;
|
||||
openSideBar: boolean;
|
||||
}
|
||||
|
||||
const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
@ -19,8 +19,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
return (
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'h-[calc(100vh-3.5rem)] left-0 !overflow-hidden md:w-auto z-20 top-[3.3rem] md:top-[3rem] absolute md:sticky md:float-start',
|
||||
openSideBar && 'bg-background bg-opacity-20 backdrop-blur-md'
|
||||
'h-[calc(100vh-3.5rem)] left-0 !overflow-hidden md:w-auto z-20 top-[3.3rem] md:top-[3rem] absolute md:sticky md:float-start rounded-r-xl border-r border-white/20',
|
||||
openSideBar && 'bg-white/40 dark:bg-black/40 backdrop-blur-2xl border-white/20 shadow-xl'
|
||||
)}
|
||||
initial={{ width: 0 }}
|
||||
transition={{
|
||||
@ -33,11 +33,11 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
>
|
||||
<div className='w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0'>
|
||||
<Input
|
||||
className='sticky top-0 z-10 text-primary-600'
|
||||
className='sticky top-0 z-10 text-default-600'
|
||||
classNames={{
|
||||
inputWrapper:
|
||||
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
||||
input: 'bg-transparent !text-primary-400 !placeholder-primary-400',
|
||||
'bg-white/40 dark:bg-white/10 backdrop-blur-md border border-white/20 mb-2 hover:bg-white/60 dark:hover:bg-white/20 transition-all',
|
||||
input: 'bg-transparent text-default-700 placeholder:text-default-400',
|
||||
}}
|
||||
radius='full'
|
||||
placeholder='搜索 API'
|
||||
@ -51,7 +51,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
key={apiName}
|
||||
shadow='none'
|
||||
className={clsx(
|
||||
'w-full border border-primary-100 rounded-lg mb-1 bg-opacity-30 backdrop-blur-sm text-primary-400',
|
||||
'w-full border border-transparent rounded-xl mb-1 bg-transparent hover:bg-white/40 dark:hover:bg-white/10 transition-all text-default-600 dark:text-gray-300',
|
||||
{
|
||||
hidden: !(
|
||||
apiName.includes(searchValue) ||
|
||||
@ -59,7 +59,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
),
|
||||
},
|
||||
{
|
||||
'!bg-opacity-40 border border-primary-400 bg-primary-50 text-primary-600':
|
||||
'!bg-white/60 dark:!bg-white/10 !border-white/20 shadow-sm !text-primary font-medium':
|
||||
apiName === selectedApi,
|
||||
}
|
||||
)}
|
||||
@ -69,8 +69,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||
<CardBody>
|
||||
<h2 className='font-bold'>{api.description}</h2>
|
||||
<div
|
||||
className={clsx('text-sm text-primary-200', {
|
||||
'!text-primary-400': apiName === selectedApi,
|
||||
className={clsx('text-sm text-default-400', {
|
||||
'!text-primary': apiName === selectedApi,
|
||||
})}
|
||||
>
|
||||
{apiName}
|
||||
|
||||
@ -30,14 +30,14 @@ const itemVariants = {
|
||||
},
|
||||
};
|
||||
|
||||
function RequestComponent ({ data: _ }: { data: OB11Request }) {
|
||||
function RequestComponent ({ data: _ }: { data: OB11Request; }) {
|
||||
return <div>Request消息,暂未适配</div>;
|
||||
}
|
||||
|
||||
export interface OneBotItemRenderProps {
|
||||
data: AllOB11WsResponse[]
|
||||
index: number
|
||||
style: React.CSSProperties
|
||||
data: AllOB11WsResponse[];
|
||||
index: number;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const getItemSize = (event: OB11AllEvent['post_type']) => {
|
||||
@ -90,7 +90,7 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
||||
animate='visible'
|
||||
className='h-full px-2'
|
||||
>
|
||||
<Card className='w-full h-full py-2 bg-opacity-50 backdrop-blur-sm'>
|
||||
<Card className='w-full h-full py-2 bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm'>
|
||||
<CardHeader className='py-0 text-default-500 flex-row gap-2'>
|
||||
<div className='font-bold'>
|
||||
{isEvent ? getEventName(msg.post_type) : '请求响应'}
|
||||
|
||||
@ -8,15 +8,15 @@ import { SelfInfo } from '@/types/user';
|
||||
import PageLoading from './page_loading';
|
||||
|
||||
export interface QQInfoCardProps {
|
||||
data?: SelfInfo
|
||||
error?: Error
|
||||
loading?: boolean
|
||||
data?: SelfInfo;
|
||||
error?: Error;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
return (
|
||||
<Card
|
||||
className='relative bg-primary-100 bg-opacity-60 overflow-hidden flex-shrink-0 shadow-md shadow-primary-300 dark:shadow-primary-50'
|
||||
className='relative bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 overflow-hidden flex-shrink-0 shadow-sm'
|
||||
shadow='none'
|
||||
radius='lg'
|
||||
>
|
||||
@ -31,28 +31,32 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||
</CardBody>
|
||||
)
|
||||
: (
|
||||
<CardBody className='flex-row items-center gap-2 overflow-hidden relative'>
|
||||
<div className='absolute right-0 bottom-0 text-5xl text-primary-400'>
|
||||
<CardBody className='flex-row items-center gap-4 overflow-hidden relative p-4'>
|
||||
<div className='absolute right-[-10px] bottom-[-10px] text-7xl text-default-400/10 rotate-12 pointer-events-none'>
|
||||
<BsTencentQq />
|
||||
</div>
|
||||
<div className='relative flex-shrink-0 z-10'>
|
||||
<Image
|
||||
src={
|
||||
data?.avatarUrl ??
|
||||
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1`
|
||||
}
|
||||
className='shadow-md rounded-full w-12 aspect-square'
|
||||
data?.avatarUrl ??
|
||||
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1`
|
||||
}
|
||||
className='shadow-sm rounded-full w-14 aspect-square ring-2 ring-white/50 dark:ring-white/10'
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
|
||||
data?.online ? 'bg-green-500' : 'bg-gray-500'
|
||||
'w-3.5 h-3.5 rounded-full absolute right-0.5 bottom-0.5 border-2 border-white dark:border-zinc-900 z-10',
|
||||
data?.online ? 'bg-success-500' : 'bg-default-400'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-col justify-center'>
|
||||
<div className='text-lg truncate'>{data?.nick}</div>
|
||||
<div className='text-primary-500 text-sm'>{data?.uin}</div>
|
||||
<div className='flex-col justify-center z-10'>
|
||||
<div className='text-xl font-bold text-default-800 dark:text-gray-100 truncate mb-0.5'>
|
||||
{data?.nick || '未知用户'}
|
||||
</div>
|
||||
<div className='text-default-500 font-mono text-xs tracking-wider opacity-80'>
|
||||
{data?.uin || 'Unknown'}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
)}
|
||||
|
||||
@ -30,10 +30,10 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
|
||||
endContent,
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-100 dark:shadow-primary-100 rounded text-primary-400'>
|
||||
{icon}
|
||||
<div className='w-24'>{title}</div>
|
||||
<div className='text-primary-200'>{value}</div>
|
||||
<div className='flex text-sm gap-2 p-3 items-center rounded-lg text-default-600 dark:text-gray-300 bg-white/50 dark:bg-white/5 border border-white/20 transition-colors hover:bg-white/70 dark:hover:bg-white/10'>
|
||||
<div className="text-lg opacity-80">{icon}</div>
|
||||
<div className='w-24 font-medium'>{title}</div>
|
||||
<div className='text-default-500 text-xs font-mono'>{value}</div>
|
||||
<div className='ml-auto'>{endContent}</div>
|
||||
</div>
|
||||
);
|
||||
@ -303,13 +303,13 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
||||
error: qqVersionError,
|
||||
} = useRequest(WebUIManager.getQQVersion);
|
||||
return (
|
||||
<Card className='bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1'>
|
||||
<CardHeader className='pb-0 items-center gap-1 text-primary-500 font-extrabold'>
|
||||
<FaCircleInfo className='text-lg' />
|
||||
<Card className='bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm overflow-visible flex-1'>
|
||||
<CardHeader className='pb-0 items-center gap-2 text-default-700 dark:text-white font-bold px-4 pt-4'>
|
||||
<FaCircleInfo className='text-lg opacity-80' />
|
||||
<span>系统信息</span>
|
||||
</CardHeader>
|
||||
<CardBody className='flex-1'>
|
||||
<div className='flex flex-col justify-between h-full'>
|
||||
<div className='flex flex-col gap-2 justify-between h-full'>
|
||||
<NapCatVersion />
|
||||
<SystemInfoItem
|
||||
title='QQ 版本'
|
||||
|
||||
@ -9,10 +9,10 @@ import bkg from '@/assets/images/bg/1AD934174C0107F14BAD8776D29C5F90.png';
|
||||
import UsagePie from './usage_pie';
|
||||
|
||||
export interface SystemStatusItemProps {
|
||||
title: string
|
||||
value?: string | number
|
||||
size?: 'md' | 'lg'
|
||||
unit?: string
|
||||
title: string;
|
||||
value?: string | number;
|
||||
size?: 'md' | 'lg';
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
||||
@ -24,21 +24,21 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'shadow-sm shadow-primary-100 p-2 rounded-md text-sm bg-content1 bg-opacity-30',
|
||||
'p-2 rounded-lg text-sm bg-white/50 dark:bg-white/5 border border-white/20 transition-colors hover:bg-white/70 dark:hover:bg-white/10',
|
||||
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
|
||||
)}
|
||||
>
|
||||
<div className='w-24'>{title}</div>
|
||||
<div className='text-default-400'>
|
||||
<div className='w-24 text-default-600 font-medium'>{title}</div>
|
||||
<div className='text-default-500 font-mono text-xs'>
|
||||
{value}
|
||||
{unit}
|
||||
{unit && <span className="ml-0.5 opacity-70">{unit}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface SystemStatusDisplayProps {
|
||||
data?: SystemStatus
|
||||
data?: SystemStatus;
|
||||
}
|
||||
|
||||
const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='bg-opacity-60 shadow-sm shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden'>
|
||||
<Card className='bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm col-span-1 lg:col-span-2 relative overflow-hidden'>
|
||||
<div className='absolute h-full right-0 top-0'>
|
||||
<Image
|
||||
src={bkg}
|
||||
@ -69,8 +69,8 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
</div>
|
||||
<CardBody className='overflow-visible md:flex-row gap-4 items-center justify-stretch z-10'>
|
||||
<div className='flex-1 w-full md:max-w-96'>
|
||||
<h2 className='text-lg font-semibold flex items-center gap-1 text-primary-400'>
|
||||
<GiCpu className='text-xl' />
|
||||
<h2 className='text-lg font-semibold flex items-center gap-2 text-default-700 dark:text-gray-200 mb-2'>
|
||||
<GiCpu className='text-xl opacity-80' />
|
||||
<span>CPU</span>
|
||||
</h2>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
@ -88,8 +88,8 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||
unit='%'
|
||||
/>
|
||||
</div>
|
||||
<h2 className='text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2'>
|
||||
<BiSolidMemoryCard className='text-xl' />
|
||||
<h2 className='text-lg font-semibold flex items-center gap-2 text-default-700 dark:text-gray-200 mb-2 mt-4'>
|
||||
<BiSolidMemoryCard className='text-xl opacity-80' />
|
||||
<span>内存</span>
|
||||
</h2>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { Selection } from '@react-types/shared';
|
||||
import { useReactive } from 'ahooks';
|
||||
import { useCallback, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import useWebSocket, { ReadyState } from 'react-use-websocket';
|
||||
@ -11,8 +10,8 @@ import { isOB11Event, isOB11RequestResponse } from '@/utils/onebot';
|
||||
import type { AllOB11WsResponse } from '@/types/onebot';
|
||||
|
||||
export { ReadyState } from 'react-use-websocket';
|
||||
export function useWebSocketDebug (url: string, token: string) {
|
||||
const messageHistory = useReactive<AllOB11WsResponse[]>([]);
|
||||
export function useWebSocketDebug (url: string, token: string, connectOnMount: boolean = true) {
|
||||
const [messageHistory, setMessageHistory] = useState<AllOB11WsResponse[]>([]);
|
||||
const [filterTypes, setFilterTypes] = useState<Selection>('all');
|
||||
|
||||
const filteredMessages = messageHistory.filter((msg) => {
|
||||
@ -22,11 +21,18 @@ export function useWebSocketDebug (url: string, token: string) {
|
||||
return false;
|
||||
});
|
||||
|
||||
const { sendMessage, readyState } = useWebSocket(url, {
|
||||
const { sendMessage, readyState } = useWebSocket(connectOnMount ? url : null, {
|
||||
share: false,
|
||||
onMessage: useCallback((event: WebSocketEventMap['message']) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
messageHistory.unshift(data);
|
||||
setMessageHistory((prev) => {
|
||||
const newHistory = [data, ...prev];
|
||||
if (newHistory.length > 500) {
|
||||
return newHistory.slice(0, 500);
|
||||
}
|
||||
return newHistory;
|
||||
});
|
||||
} catch (_error) {
|
||||
toast.error('WebSocket 消息解析失败');
|
||||
}
|
||||
@ -39,7 +45,7 @@ export function useWebSocketDebug (url: string, token: string) {
|
||||
console.error('WebSocket error:', event);
|
||||
},
|
||||
onOpen: () => {
|
||||
messageHistory.splice(0, messageHistory.length);
|
||||
setMessageHistory([]);
|
||||
},
|
||||
});
|
||||
|
||||
@ -50,6 +56,10 @@ export function useWebSocketDebug (url: string, token: string) {
|
||||
sendMessage(msg);
|
||||
};
|
||||
|
||||
const clearMessages = useCallback(() => {
|
||||
setMessageHistory([]);
|
||||
}, []);
|
||||
|
||||
const FilterMessagesType = renderFilterMessageType(
|
||||
filterTypes,
|
||||
setFilterTypes
|
||||
@ -63,5 +73,6 @@ export function useWebSocketDebug (url: string, token: string) {
|
||||
filterTypes,
|
||||
setFilterTypes,
|
||||
FilterMessagesType,
|
||||
clearMessages,
|
||||
};
|
||||
}
|
||||
|
||||
@ -99,10 +99,8 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
|
||||
transition={{ duration: 0.4 }}
|
||||
className={clsx(
|
||||
'flex-1 overflow-y-auto',
|
||||
'bg-white/60 dark:bg-black/40 backdrop-blur-xl',
|
||||
'shadow-[0_8px_32px_0_rgba(31,38,135,0.07)]',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
openSideBar ? 'm-3 ml-0 rounded-3xl border border-white/40 dark:border-white/10' : 'm-0 rounded-none',
|
||||
openSideBar ? 'ml-0' : 'ml-0',
|
||||
'pb-10 md:pb-0'
|
||||
)}
|
||||
>
|
||||
|
||||
@ -24,9 +24,10 @@ export default function WSDebug () {
|
||||
});
|
||||
const [inputUrl, setInputUrl] = useState(socketConfig.url);
|
||||
const [inputToken, setInputToken] = useState(socketConfig.token);
|
||||
const [shouldConnect, setShouldConnect] = useState(false);
|
||||
|
||||
const { sendMessage, readyState, FilterMessagesType, filteredMessages } =
|
||||
useWebSocketDebug(socketConfig.url, socketConfig.token);
|
||||
const { sendMessage, readyState, FilterMessagesType, filteredMessages, clearMessages } =
|
||||
useWebSocketDebug(socketConfig.url, socketConfig.token, shouldConnect);
|
||||
|
||||
const handleConnect = useCallback(() => {
|
||||
if (!inputUrl.startsWith('ws://') && !inputUrl.startsWith('wss://')) {
|
||||
@ -37,13 +38,18 @@ export default function WSDebug () {
|
||||
url: inputUrl,
|
||||
token: inputToken,
|
||||
});
|
||||
}, [inputUrl, inputToken]);
|
||||
setShouldConnect(true);
|
||||
}, [inputUrl, inputToken, setSocketConfig]);
|
||||
|
||||
const handleDisconnect = useCallback(() => {
|
||||
setShouldConnect(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>Websocket调试 - NapCat WebUI</title>
|
||||
<div className='h-[calc(100vh-4rem)] overflow-hidden flex flex-col'>
|
||||
<Card className='mx-2 mt-2 flex-shrink-0 bg-opacity-50 backdrop-blur-sm'>
|
||||
<Card className='mx-2 mt-2 flex-shrink-0 bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm'>
|
||||
<CardBody className='gap-2'>
|
||||
<div className='grid gap-2 items-center md:grid-cols-5'>
|
||||
<Input
|
||||
@ -64,23 +70,33 @@ export default function WSDebug () {
|
||||
/>
|
||||
<div className='flex-shrink-0 flex gap-2 col-span-2 md:col-span-1'>
|
||||
<Button
|
||||
color='primary'
|
||||
onPress={handleConnect}
|
||||
onPress={shouldConnect ? handleDisconnect : handleConnect}
|
||||
size='lg'
|
||||
radius='full'
|
||||
color={shouldConnect ? 'danger' : 'primary'}
|
||||
className='w-full md:w-auto'
|
||||
>
|
||||
连接
|
||||
{shouldConnect ? '断开' : '连接'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-2 border border-default-100 bg-content1 bg-opacity-50 rounded-md dark:bg-[rgb(30,30,30)]'>
|
||||
<div className='p-2 rounded-lg bg-white/50 dark:bg-white/5 border border-white/20 transition-colors'>
|
||||
<div className='grid gap-2 md:grid-cols-5 items-center md:w-fit'>
|
||||
<WSStatus state={readyState} />
|
||||
<div className='md:w-64 max-w-full col-span-2'>
|
||||
{FilterMessagesType}
|
||||
</div>
|
||||
<OneBotSendModal sendMessage={sendMessage} />
|
||||
<div className='flex gap-2 justify-end col-span-2 md:col-span-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
color='danger'
|
||||
variant='flat'
|
||||
onPress={clearMessages}
|
||||
>
|
||||
清空日志
|
||||
</Button>
|
||||
<OneBotSendModal sendMessage={sendMessage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
@ -329,8 +329,8 @@ export default function FileManagerPage () {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='p-4'>
|
||||
<div className='mb-4 flex items-center gap-4 sticky top-14 z-10 bg-content1 py-1'>
|
||||
<div className='h-full flex flex-col relative gap-4 w-full p-4'>
|
||||
<div className='mb-4 flex items-center gap-4 sticky top-14 z-10 bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm py-2 px-4 rounded-xl'>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
@ -418,8 +418,8 @@ export default function FileManagerPage () {
|
||||
)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Breadcrumbs className='flex-1 shadow-small px-2 py-2 rounded-lg'>
|
||||
)}
|
||||
<Breadcrumbs className='flex-1 bg-white/40 dark:bg-black/20 backdrop-blur-md shadow-sm border border-white/20 px-2 py-2 rounded-lg'>
|
||||
{currentPath.split('/').map((part, index, parts) => (
|
||||
<BreadcrumbItem
|
||||
key={part}
|
||||
|
||||
@ -105,7 +105,7 @@ const DashboardIndexPage: React.FC = () => {
|
||||
<SystemStatusCard setArchInfo={setArchInfo} />
|
||||
</div>
|
||||
<Networks />
|
||||
<Card className='bg-opacity-60 shadow-sm shadow-primary-100'>
|
||||
<Card className='bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm'>
|
||||
<CardBody>
|
||||
<Hitokoto />
|
||||
</CardBody>
|
||||
|
||||
@ -53,8 +53,8 @@ export default function LogsPage () {
|
||||
classNames={{
|
||||
panel: 'w-full flex-1 h-full py-0 flex flex-col gap-4',
|
||||
base: 'flex-shrink-0 !h-fit',
|
||||
tabList: 'bg-opacity-50 backdrop-blur-sm',
|
||||
cursor: 'bg-opacity-60 backdrop-blur-sm',
|
||||
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||
}}
|
||||
>
|
||||
<Tab title='实时日志'>
|
||||
|
||||
@ -388,8 +388,8 @@ export default function NetworkPage () {
|
||||
className='max-w-full'
|
||||
items={tabs}
|
||||
classNames={{
|
||||
tabList: 'bg-opacity-50 backdrop-blur-sm',
|
||||
cursor: 'bg-opacity-60 backdrop-blur-sm',
|
||||
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
|
||||
@ -112,7 +112,7 @@ export default function TerminalPage () {
|
||||
className='h-full overflow-hidden'
|
||||
>
|
||||
<div className='flex items-center gap-2 flex-shrink-0 flex-grow-0'>
|
||||
<TabList className='flex-1 !overflow-x-auto w-full hide-scrollbar'>
|
||||
<TabList className='flex-1 !overflow-x-auto w-full hide-scrollbar bg-white/40 dark:bg-black/20 backdrop-blur-md p-1 rounded-lg border border-white/20'>
|
||||
<SortableContext
|
||||
items={tabs}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user