mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-16 13:20:33 +00: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';
|
import { title } from '../primitives';
|
||||||
|
|
||||||
export interface ContainerProps {
|
export interface ContainerProps {
|
||||||
title: string
|
title: string;
|
||||||
tag?: React.ReactNode
|
tag?: React.ReactNode;
|
||||||
action: React.ReactNode
|
action: React.ReactNode;
|
||||||
enableSwitch: React.ReactNode
|
enableSwitch: React.ReactNode;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DisplayCardProps {
|
export interface DisplayCardProps {
|
||||||
showType?: boolean
|
showType?: boolean;
|
||||||
onEdit: () => void
|
onEdit: () => void;
|
||||||
onEnable: () => Promise<void>
|
onEnable: () => Promise<void>;
|
||||||
onDelete: () => Promise<void>
|
onDelete: () => Promise<void>;
|
||||||
onEnableDebug: () => Promise<void>
|
onEnableDebug: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisplayCardContainer: React.FC<ContainerProps> = ({
|
const DisplayCardContainer: React.FC<ContainerProps> = ({
|
||||||
@ -27,7 +27,7 @@ const DisplayCardContainer: React.FC<ContainerProps> = ({
|
|||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
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'>
|
<CardHeader className='pb-0 flex items-center'>
|
||||||
{tag && (
|
{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'>
|
<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 { Card, CardBody } from '@heroui/card';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { title } from '@/components/primitives';
|
|
||||||
|
|
||||||
export interface NetworkItemDisplayProps {
|
export interface NetworkItemDisplayProps {
|
||||||
count: number
|
count: number;
|
||||||
label: string
|
label: string;
|
||||||
size?: 'sm' | 'md'
|
size?: 'sm' | 'md';
|
||||||
}
|
}
|
||||||
|
|
||||||
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
||||||
@ -17,35 +17,26 @@ const NetworkItemDisplay: React.FC<NetworkItemDisplayProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={clsx(
|
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'
|
size === 'md'
|
||||||
? 'col-span-8 md:col-span-2 bg-primary-50 shadow-primary-100'
|
? 'col-span-8 md:col-span-2'
|
||||||
: 'col-span-2 md:col-span-1 bg-warning-100 shadow-warning-200'
|
: 'col-span-2 md:col-span-1'
|
||||||
)}
|
)}
|
||||||
shadow='sm'
|
shadow='none'
|
||||||
>
|
>
|
||||||
<CardBody className='items-center md:gap-1 p-1 md:p-2'>
|
<CardBody className='items-center md:gap-1 p-1 md:p-2'>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1',
|
'flex-1 font-mono font-bold text-default-700 dark:text-gray-200',
|
||||||
size === 'md' ? 'text-2xl md:text-3xl' : 'text-xl md:text-2xl',
|
size === 'md' ? 'text-4xl md:text-5xl' : 'text-2xl md:text-3xl',
|
||||||
title({
|
|
||||||
color: size === 'md' ? 'pink' : 'yellow',
|
|
||||||
size,
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'whitespace-nowrap text-nowrap flex-shrink-0',
|
'whitespace-nowrap text-nowrap flex-shrink-0 font-medium text-default-500',
|
||||||
size === 'md' ? 'text-sm md:text-base' : 'text-xs md:text-sm',
|
size === 'md' ? 'text-sm' : 'text-xs',
|
||||||
title({
|
|
||||||
color: size === 'md' ? 'pink' : 'yellow',
|
|
||||||
shadow: true,
|
|
||||||
size: 'xxs',
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@ -25,21 +25,21 @@ import { supportedPreviewExts } from './file_preview_modal';
|
|||||||
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button';
|
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button';
|
||||||
|
|
||||||
export interface FileTableProps {
|
export interface FileTableProps {
|
||||||
files: FileInfo[]
|
files: FileInfo[];
|
||||||
currentPath: string
|
currentPath: string;
|
||||||
loading: boolean
|
loading: boolean;
|
||||||
sortDescriptor: SortDescriptor
|
sortDescriptor: SortDescriptor;
|
||||||
onSortChange: (descriptor: SortDescriptor) => void
|
onSortChange: (descriptor: SortDescriptor) => void;
|
||||||
selectedFiles: Selection
|
selectedFiles: Selection;
|
||||||
onSelectionChange: (selected: Selection) => void
|
onSelectionChange: (selected: Selection) => void;
|
||||||
onDirectoryClick: (dirPath: string) => void
|
onDirectoryClick: (dirPath: string) => void;
|
||||||
onEdit: (filePath: string) => void
|
onEdit: (filePath: string) => void;
|
||||||
onPreview: (filePath: string) => void
|
onPreview: (filePath: string) => void;
|
||||||
onRenameRequest: (name: string) => void
|
onRenameRequest: (name: string) => void;
|
||||||
onMoveRequest: (name: string) => void
|
onMoveRequest: (name: string) => void;
|
||||||
onCopyPath: (fileName: string) => void
|
onCopyPath: (fileName: string) => void;
|
||||||
onDelete: (filePath: string) => void
|
onDelete: (filePath: string) => void;
|
||||||
onDownload: (filePath: string) => void
|
onDownload: (filePath: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
@ -112,7 +112,7 @@ export default function FileTable ({
|
|||||||
selectedKeys={selectedFiles}
|
selectedKeys={selectedFiles}
|
||||||
selectionMode='multiple'
|
selectionMode='multiple'
|
||||||
bottomContent={
|
bottomContent={
|
||||||
<div className='flex w-full justify-center'>
|
<div className='flex w-full justify-center p-2 border-t border-white/10'>
|
||||||
<Pagination
|
<Pagination
|
||||||
isCompact
|
isCompact
|
||||||
showControls
|
showControls
|
||||||
@ -121,9 +121,17 @@ export default function FileTable ({
|
|||||||
page={page}
|
page={page}
|
||||||
total={pages}
|
total={pages}
|
||||||
onChange={(page) => setPage(page)}
|
onChange={(page) => setPage(page)}
|
||||||
|
classNames={{
|
||||||
|
cursor: 'bg-primary shadow-lg',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<TableHeader>
|
||||||
<TableColumn key='name' allowsSorting>
|
<TableColumn key='name' allowsSorting>
|
||||||
@ -180,7 +188,7 @@ export default function FileTable ({
|
|||||||
name={file.name}
|
name={file.name}
|
||||||
isDirectory={file.isDirectory}
|
isDirectory={file.isDirectory}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{file.name}
|
{file.name}
|
||||||
</Button>
|
</Button>
|
||||||
@ -194,43 +202,43 @@ export default function FileTable ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<ButtonGroup size='sm'>
|
<ButtonGroup size='sm' variant='light'>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color='primary'
|
color='default'
|
||||||
variant='flat'
|
className='text-default-500 hover:text-primary'
|
||||||
onPress={() => onRenameRequest(file.name)}
|
onPress={() => onRenameRequest(file.name)}
|
||||||
>
|
>
|
||||||
<BiRename />
|
<BiRename />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color='primary'
|
color='default'
|
||||||
variant='flat'
|
className='text-default-500 hover:text-primary'
|
||||||
onPress={() => onMoveRequest(file.name)}
|
onPress={() => onMoveRequest(file.name)}
|
||||||
>
|
>
|
||||||
<FiMove />
|
<FiMove />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color='primary'
|
color='default'
|
||||||
variant='flat'
|
className='text-default-500 hover:text-primary'
|
||||||
onPress={() => onCopyPath(file.name)}
|
onPress={() => onCopyPath(file.name)}
|
||||||
>
|
>
|
||||||
<FiCopy />
|
<FiCopy />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color='primary'
|
color='default'
|
||||||
variant='flat'
|
className='text-default-500 hover:text-primary'
|
||||||
onPress={() => onDownload(filePath)}
|
onPress={() => onDownload(filePath)}
|
||||||
>
|
>
|
||||||
<FiDownload />
|
<FiDownload />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color='primary'
|
color='danger'
|
||||||
variant='flat'
|
className='text-danger hover:bg-danger/10'
|
||||||
onPress={() => onDelete(filePath)}
|
onPress={() => onDelete(filePath)}
|
||||||
>
|
>
|
||||||
<FiTrash2 />
|
<FiTrash2 />
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Button } from '@heroui/button';
|
|||||||
import { Tooltip } from '@heroui/tooltip';
|
import { Tooltip } from '@heroui/tooltip';
|
||||||
import { useRequest } from 'ahooks';
|
import { useRequest } from 'ahooks';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import { IoMdQuote } from 'react-icons/io';
|
||||||
import { IoCopy, IoRefresh } from 'react-icons/io5';
|
import { IoCopy, IoRefresh } from 'react-icons/io5';
|
||||||
|
|
||||||
import { request } from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
@ -30,18 +31,21 @@ export default function Hitokoto () {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='relative'>
|
<div className='relative flex flex-col items-center justify-center p-6 min-h-[120px]'>
|
||||||
{loading && <PageLoading />}
|
{loading && <PageLoading />}
|
||||||
{error
|
{error
|
||||||
? (
|
? (
|
||||||
<div className='text-primary-400'>一言加载失败:{error.message}</div>
|
<div className='text-danger'>一言加载失败:{error.message}</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<div>{data?.hitokoto}</div>
|
<IoMdQuote className="text-4xl text-primary/20 mb-4" />
|
||||||
<div className='text-right'>
|
<div className="text-xl font-medium text-default-700 dark:text-gray-200 tracking-wide leading-relaxed italic">
|
||||||
—— <span className='text-default-400'>{data?.from}</span>{' '}
|
“ {data?.hitokoto} ”
|
||||||
{data?.from_who}
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -49,25 +53,25 @@ export default function Hitokoto () {
|
|||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Tooltip content='刷新' placement='top'>
|
<Tooltip content='刷新' placement='top'>
|
||||||
<Button
|
<Button
|
||||||
|
className="text-default-400 hover:text-primary transition-colors"
|
||||||
onPress={run}
|
onPress={run}
|
||||||
size='sm'
|
size='sm'
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius='full'
|
radius='full'
|
||||||
color='primary'
|
variant='light'
|
||||||
variant='flat'
|
|
||||||
>
|
>
|
||||||
<IoRefresh />
|
<IoRefresh />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content='复制' placement='top'>
|
<Tooltip content='复制' placement='top'>
|
||||||
<Button
|
<Button
|
||||||
|
className="text-default-400 hover:text-success transition-colors"
|
||||||
onPress={onCopy}
|
onPress={onCopy}
|
||||||
size='sm'
|
size='sm'
|
||||||
isIconOnly
|
isIconOnly
|
||||||
radius='full'
|
radius='full'
|
||||||
color='success'
|
variant='light'
|
||||||
variant='flat'
|
|
||||||
>
|
>
|
||||||
<IoCopy />
|
<IoCopy />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import { Button } from '@heroui/button';
|
|||||||
import { Card, CardBody, CardHeader } from '@heroui/card';
|
import { Card, CardBody, CardHeader } from '@heroui/card';
|
||||||
import { Input } from '@heroui/input';
|
import { Input } from '@heroui/input';
|
||||||
import { Snippet } from '@heroui/snippet';
|
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 { useLocalStorage } from '@uidotdev/usehooks';
|
||||||
import { motion } from 'motion/react';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
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 { PiCatDuotone } from 'react-icons/pi';
|
||||||
|
|
||||||
import key from '@/const/key';
|
import key from '@/const/key';
|
||||||
@ -38,9 +39,8 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
});
|
});
|
||||||
const [requestBody, setRequestBody] = useState('{}');
|
const [requestBody, setRequestBody] = useState('{}');
|
||||||
const [responseContent, setResponseContent] = useState('');
|
const [responseContent, setResponseContent] = useState('');
|
||||||
const [isCodeEditorOpen, setIsCodeEditorOpen] = useState(false);
|
|
||||||
const [isResponseOpen, setIsResponseOpen] = useState(false);
|
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
const [isStructOpen, setIsStructOpen] = useState(false);
|
||||||
const responseRef = useRef<HTMLDivElement>(null);
|
const responseRef = useRef<HTMLDivElement>(null);
|
||||||
const parsedRequest = parse(data.request);
|
const parsedRequest = parse(data.request);
|
||||||
const parsedResponse = parse(data.response);
|
const parsedResponse = parse(data.response);
|
||||||
@ -70,7 +70,6 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
setIsResponseOpen(true);
|
|
||||||
responseRef.current?.scrollIntoView({
|
responseRef.current?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'start',
|
block: 'start',
|
||||||
@ -90,149 +89,202 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
|||||||
}, [path]);
|
}, [path]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='p-4 pt-14 rounded-lg shadow-md'>
|
<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'>
|
||||||
<h1 className='text-2xl font-bold mb-4 flex items-center gap-1 text-primary-400'>
|
<div className='flex flex-col gap-4'>
|
||||||
<PiCatDuotone />
|
<div className='flex items-center justify-between'>
|
||||||
{data.description}
|
<h1 className='text-2xl font-bold flex items-center gap-2 text-primary-500'>
|
||||||
</h1>
|
<PiCatDuotone />
|
||||||
<h1 className='text-lg font-bold mb-4'>
|
{data.description}
|
||||||
<Snippet
|
</h1>
|
||||||
className='bg-default-50 bg-opacity-50 backdrop-blur-md'
|
<Snippet
|
||||||
symbol={<IoLink size={18} className='inline-block mr-1' />}
|
className='bg-white/40 dark:bg-black/20 backdrop-blur-md shadow-sm border border-white/20'
|
||||||
tooltipProps={{
|
symbol={<IoLink size={18} className='inline-block mr-1' />}
|
||||||
content: '点击复制地址',
|
tooltipProps={{ content: '点击复制地址' }}
|
||||||
}}
|
>
|
||||||
>
|
{path}
|
||||||
{path}
|
</Snippet>
|
||||||
</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>
|
|
||||||
<Button
|
<Button
|
||||||
color='warning'
|
|
||||||
variant='flat'
|
|
||||||
onPress={() => setIsCodeEditorOpen(!isCodeEditorOpen)}
|
|
||||||
size='sm'
|
size='sm'
|
||||||
radius='full'
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='border-primary/20 hover:bg-primary/10'
|
||||||
|
onPress={() => setIsStructOpen(true)}
|
||||||
>
|
>
|
||||||
{isCodeEditorOpen ? '收起' : '展开'}
|
查看数据定义
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</div>
|
||||||
<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 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 />
|
<ChatInputModal />
|
||||||
<Button
|
<Button
|
||||||
|
size='sm'
|
||||||
color='primary'
|
color='primary'
|
||||||
variant='flat'
|
variant='light'
|
||||||
onPress={() =>
|
onPress={() => setRequestBody(generateDefaultJson(data.request))}
|
||||||
setRequestBody(generateDefaultJson(data.request))}
|
|
||||||
>
|
>
|
||||||
填充示例请求体
|
内置示例
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</CardHeader>
|
||||||
</CardBody>
|
<CardBody className='p-0 flex-1 relative'>
|
||||||
</Card>
|
<div className='absolute inset-0'>
|
||||||
<Card
|
<CodeEditor
|
||||||
shadow='sm'
|
value={requestBody}
|
||||||
className='my-4 relative bg-opacity-50 backdrop-blur-md'
|
onChange={(value) => setRequestBody(value ?? '')}
|
||||||
>
|
language='json'
|
||||||
<PageLoading loading={isFetching} />
|
options={{
|
||||||
<CardHeader className='font-bold text-lg gap-1 pb-0'>
|
minimap: { enabled: false },
|
||||||
<span className='mr-2'>响应</span>
|
fontSize: 13,
|
||||||
<Button
|
padding: { top: 10, bottom: 10 },
|
||||||
color='warning'
|
scrollBeyondLastLine: false,
|
||||||
variant='flat'
|
}}
|
||||||
onPress={() => setIsResponseOpen(!isResponseOpen)}
|
/>
|
||||||
size='sm'
|
</div>
|
||||||
radius='full'
|
</CardBody>
|
||||||
>
|
</Card>
|
||||||
{isResponseOpen ? '收起' : '展开'}
|
|
||||||
</Button>
|
{/* Response Column */}
|
||||||
<Button
|
<Card className='bg-white/40 dark:bg-white/5 backdrop-blur-md border border-white/20 shadow-sm h-full flex flex-col'>
|
||||||
color='success'
|
<PageLoading loading={isFetching} />
|
||||||
variant='flat'
|
<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'>
|
||||||
onPress={() => {
|
<div className='flex items-center gap-2'>
|
||||||
navigator.clipboard.writeText(responseContent);
|
<span className='w-2 h-6 rounded-full bg-secondary-500'></span>
|
||||||
toast.success('响应内容已复制到剪贴板');
|
响应 (Response)
|
||||||
}}
|
</div>
|
||||||
size='sm'
|
<Button
|
||||||
radius='full'
|
size='sm'
|
||||||
>
|
color='success'
|
||||||
复制
|
variant='light'
|
||||||
</Button>
|
onPress={() => {
|
||||||
</CardHeader>
|
navigator.clipboard.writeText(responseContent);
|
||||||
<CardBody>
|
toast.success('已复制');
|
||||||
<motion.div
|
}}
|
||||||
className='overflow-y-auto text-sm'
|
>
|
||||||
initial={{ opacity: 0, height: 0 }}
|
复制内容
|
||||||
animate={{
|
</Button>
|
||||||
opacity: isResponseOpen ? 1 : 0,
|
</CardHeader>
|
||||||
height: isResponseOpen ? 300 : 0,
|
<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'>
|
||||||
<pre>
|
{responseContent || <span className='text-default-400 italic'>等待请求响应...</span>}
|
||||||
<code>
|
</pre>
|
||||||
{responseContent || (
|
</div>
|
||||||
<div className='text-gray-400'>暂无响应</div>
|
</CardBody>
|
||||||
)}
|
</Card>
|
||||||
</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} />
|
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,15 +8,15 @@ import { TbSquareRoundedChevronRightFilled } from 'react-icons/tb';
|
|||||||
import type { LiteralValue, ParsedSchema } from '@/utils/zod';
|
import type { LiteralValue, ParsedSchema } from '@/utils/zod';
|
||||||
|
|
||||||
interface DisplayStructProps {
|
interface DisplayStructProps {
|
||||||
schema: ParsedSchema | ParsedSchema[]
|
schema: ParsedSchema | ParsedSchema[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SchemaType = ({
|
const SchemaType = ({
|
||||||
type,
|
type,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
type: string
|
type: string;
|
||||||
value?: LiteralValue
|
value?: LiteralValue;
|
||||||
}) => {
|
}) => {
|
||||||
let name = type;
|
let name = type;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -57,7 +57,7 @@ const SchemaType = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SchemaLabel: React.FC<{
|
const SchemaLabel: React.FC<{
|
||||||
schema: ParsedSchema
|
schema: ParsedSchema;
|
||||||
}> = ({ schema }) => (
|
}> = ({ schema }) => (
|
||||||
<>
|
<>
|
||||||
{Array.isArray(schema.type)
|
{Array.isArray(schema.type)
|
||||||
@ -81,8 +81,8 @@ const SchemaLabel: React.FC<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SchemaContainer: React.FC<{
|
const SchemaContainer: React.FC<{
|
||||||
schema: ParsedSchema
|
schema: ParsedSchema;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}> = ({ schema, children }) => {
|
}> = ({ schema, children }) => {
|
||||||
const [expanded, setExpanded] = useState(false);
|
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') {
|
if (schema.type === 'object') {
|
||||||
return (
|
return (
|
||||||
<SchemaContainer schema={schema}>
|
<SchemaContainer schema={schema}>
|
||||||
@ -193,7 +193,7 @@ const RenderSchema: React.FC<{ schema: ParsedSchema }> = ({ schema }) => {
|
|||||||
|
|
||||||
const DisplayStruct: React.FC<DisplayStructProps> = ({ schema }) => {
|
const DisplayStruct: React.FC<DisplayStructProps> = ({ schema }) => {
|
||||||
return (
|
return (
|
||||||
<div className='p-4 bg-content2 rounded-lg bg-opacity-50'>
|
<div className=''>
|
||||||
{Array.isArray(schema)
|
{Array.isArray(schema)
|
||||||
? (
|
? (
|
||||||
schema.map((s, i) => <RenderSchema key={s.name || i} schema={s} />)
|
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';
|
import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api';
|
||||||
|
|
||||||
export interface OneBotApiNavListProps {
|
export interface OneBotApiNavListProps {
|
||||||
data: OneBotHttpApi
|
data: OneBotHttpApi;
|
||||||
selectedApi: OneBotHttpApiPath
|
selectedApi: OneBotHttpApiPath;
|
||||||
onSelect: (apiName: OneBotHttpApiPath) => void
|
onSelect: (apiName: OneBotHttpApiPath) => void;
|
||||||
openSideBar: boolean
|
openSideBar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
||||||
@ -19,8 +19,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={clsx(
|
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',
|
'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-background bg-opacity-20 backdrop-blur-md'
|
openSideBar && 'bg-white/40 dark:bg-black/40 backdrop-blur-2xl border-white/20 shadow-xl'
|
||||||
)}
|
)}
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
transition={{
|
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'>
|
<div className='w-64 h-full overflow-y-auto px-2 pt-2 pb-10 md:pb-0'>
|
||||||
<Input
|
<Input
|
||||||
className='sticky top-0 z-10 text-primary-600'
|
className='sticky top-0 z-10 text-default-600'
|
||||||
classNames={{
|
classNames={{
|
||||||
inputWrapper:
|
inputWrapper:
|
||||||
'bg-opacity-30 bg-primary-50 backdrop-blur-sm border border-primary-300 mb-2',
|
'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-primary-400 !placeholder-primary-400',
|
input: 'bg-transparent text-default-700 placeholder:text-default-400',
|
||||||
}}
|
}}
|
||||||
radius='full'
|
radius='full'
|
||||||
placeholder='搜索 API'
|
placeholder='搜索 API'
|
||||||
@ -51,7 +51,7 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
key={apiName}
|
key={apiName}
|
||||||
shadow='none'
|
shadow='none'
|
||||||
className={clsx(
|
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: !(
|
hidden: !(
|
||||||
apiName.includes(searchValue) ||
|
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,
|
apiName === selectedApi,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@ -69,8 +69,8 @@ const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<h2 className='font-bold'>{api.description}</h2>
|
<h2 className='font-bold'>{api.description}</h2>
|
||||||
<div
|
<div
|
||||||
className={clsx('text-sm text-primary-200', {
|
className={clsx('text-sm text-default-400', {
|
||||||
'!text-primary-400': apiName === selectedApi,
|
'!text-primary': apiName === selectedApi,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{apiName}
|
{apiName}
|
||||||
|
|||||||
@ -30,14 +30,14 @@ const itemVariants = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function RequestComponent ({ data: _ }: { data: OB11Request }) {
|
function RequestComponent ({ data: _ }: { data: OB11Request; }) {
|
||||||
return <div>Request消息,暂未适配</div>;
|
return <div>Request消息,暂未适配</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OneBotItemRenderProps {
|
export interface OneBotItemRenderProps {
|
||||||
data: AllOB11WsResponse[]
|
data: AllOB11WsResponse[];
|
||||||
index: number
|
index: number;
|
||||||
style: React.CSSProperties
|
style: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getItemSize = (event: OB11AllEvent['post_type']) => {
|
export const getItemSize = (event: OB11AllEvent['post_type']) => {
|
||||||
@ -90,7 +90,7 @@ const OneBotItemRender = ({ data, index, style }: OneBotItemRenderProps) => {
|
|||||||
animate='visible'
|
animate='visible'
|
||||||
className='h-full px-2'
|
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'>
|
<CardHeader className='py-0 text-default-500 flex-row gap-2'>
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{isEvent ? getEventName(msg.post_type) : '请求响应'}
|
{isEvent ? getEventName(msg.post_type) : '请求响应'}
|
||||||
|
|||||||
@ -8,15 +8,15 @@ import { SelfInfo } from '@/types/user';
|
|||||||
import PageLoading from './page_loading';
|
import PageLoading from './page_loading';
|
||||||
|
|
||||||
export interface QQInfoCardProps {
|
export interface QQInfoCardProps {
|
||||||
data?: SelfInfo
|
data?: SelfInfo;
|
||||||
error?: Error
|
error?: Error;
|
||||||
loading?: boolean
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<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'
|
shadow='none'
|
||||||
radius='lg'
|
radius='lg'
|
||||||
>
|
>
|
||||||
@ -31,28 +31,32 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<CardBody className='flex-row items-center gap-2 overflow-hidden relative'>
|
<CardBody className='flex-row items-center gap-4 overflow-hidden relative p-4'>
|
||||||
<div className='absolute right-0 bottom-0 text-5xl text-primary-400'>
|
<div className='absolute right-[-10px] bottom-[-10px] text-7xl text-default-400/10 rotate-12 pointer-events-none'>
|
||||||
<BsTencentQq />
|
<BsTencentQq />
|
||||||
</div>
|
</div>
|
||||||
<div className='relative flex-shrink-0 z-10'>
|
<div className='relative flex-shrink-0 z-10'>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
data?.avatarUrl ??
|
data?.avatarUrl ??
|
||||||
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1`
|
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1`
|
||||||
}
|
}
|
||||||
className='shadow-md rounded-full w-12 aspect-square'
|
className='shadow-sm rounded-full w-14 aspect-square ring-2 ring-white/50 dark:ring-white/10'
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-4 h-4 rounded-full absolute right-0.5 bottom-0 border-2 border-primary-100 z-10',
|
'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-green-500' : 'bg-gray-500'
|
data?.online ? 'bg-success-500' : 'bg-default-400'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-col justify-center'>
|
<div className='flex-col justify-center z-10'>
|
||||||
<div className='text-lg truncate'>{data?.nick}</div>
|
<div className='text-xl font-bold text-default-800 dark:text-gray-100 truncate mb-0.5'>
|
||||||
<div className='text-primary-500 text-sm'>{data?.uin}</div>
|
{data?.nick || '未知用户'}
|
||||||
|
</div>
|
||||||
|
<div className='text-default-500 font-mono text-xs tracking-wider opacity-80'>
|
||||||
|
{data?.uin || 'Unknown'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -30,10 +30,10 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
|
|||||||
endContent,
|
endContent,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
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'>
|
<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'>
|
||||||
{icon}
|
<div className="text-lg opacity-80">{icon}</div>
|
||||||
<div className='w-24'>{title}</div>
|
<div className='w-24 font-medium'>{title}</div>
|
||||||
<div className='text-primary-200'>{value}</div>
|
<div className='text-default-500 text-xs font-mono'>{value}</div>
|
||||||
<div className='ml-auto'>{endContent}</div>
|
<div className='ml-auto'>{endContent}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -303,13 +303,13 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
|
|||||||
error: qqVersionError,
|
error: qqVersionError,
|
||||||
} = useRequest(WebUIManager.getQQVersion);
|
} = useRequest(WebUIManager.getQQVersion);
|
||||||
return (
|
return (
|
||||||
<Card className='bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1'>
|
<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-1 text-primary-500 font-extrabold'>
|
<CardHeader className='pb-0 items-center gap-2 text-default-700 dark:text-white font-bold px-4 pt-4'>
|
||||||
<FaCircleInfo className='text-lg' />
|
<FaCircleInfo className='text-lg opacity-80' />
|
||||||
<span>系统信息</span>
|
<span>系统信息</span>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className='flex-1'>
|
<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 />
|
<NapCatVersion />
|
||||||
<SystemInfoItem
|
<SystemInfoItem
|
||||||
title='QQ 版本'
|
title='QQ 版本'
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import bkg from '@/assets/images/bg/1AD934174C0107F14BAD8776D29C5F90.png';
|
|||||||
import UsagePie from './usage_pie';
|
import UsagePie from './usage_pie';
|
||||||
|
|
||||||
export interface SystemStatusItemProps {
|
export interface SystemStatusItemProps {
|
||||||
title: string
|
title: string;
|
||||||
value?: string | number
|
value?: string | number;
|
||||||
size?: 'md' | 'lg'
|
size?: 'md' | 'lg';
|
||||||
unit?: string
|
unit?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
||||||
@ -24,21 +24,21 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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'
|
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='w-24'>{title}</div>
|
<div className='w-24 text-default-600 font-medium'>{title}</div>
|
||||||
<div className='text-default-400'>
|
<div className='text-default-500 font-mono text-xs'>
|
||||||
{value}
|
{value}
|
||||||
{unit}
|
{unit && <span className="ml-0.5 opacity-70">{unit}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SystemStatusDisplayProps {
|
export interface SystemStatusDisplayProps {
|
||||||
data?: SystemStatus
|
data?: SystemStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
||||||
@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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'>
|
<div className='absolute h-full right-0 top-0'>
|
||||||
<Image
|
<Image
|
||||||
src={bkg}
|
src={bkg}
|
||||||
@ -69,8 +69,8 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
|||||||
</div>
|
</div>
|
||||||
<CardBody className='overflow-visible md:flex-row gap-4 items-center justify-stretch z-10'>
|
<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'>
|
<div className='flex-1 w-full md:max-w-96'>
|
||||||
<h2 className='text-lg font-semibold flex items-center gap-1 text-primary-400'>
|
<h2 className='text-lg font-semibold flex items-center gap-2 text-default-700 dark:text-gray-200 mb-2'>
|
||||||
<GiCpu className='text-xl' />
|
<GiCpu className='text-xl opacity-80' />
|
||||||
<span>CPU</span>
|
<span>CPU</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className='grid grid-cols-2 gap-2'>
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
@ -88,8 +88,8 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
|
|||||||
unit='%'
|
unit='%'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className='text-lg font-semibold flex items-center gap-1 text-primary-400 mt-2'>
|
<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' />
|
<BiSolidMemoryCard className='text-xl opacity-80' />
|
||||||
<span>内存</span>
|
<span>内存</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className='grid grid-cols-2 gap-2'>
|
<div className='grid grid-cols-2 gap-2'>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import type { Selection } from '@react-types/shared';
|
import type { Selection } from '@react-types/shared';
|
||||||
import { useReactive } from 'ahooks';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useWebSocket, { ReadyState } from 'react-use-websocket';
|
import useWebSocket, { ReadyState } from 'react-use-websocket';
|
||||||
@ -11,8 +10,8 @@ import { isOB11Event, isOB11RequestResponse } from '@/utils/onebot';
|
|||||||
import type { AllOB11WsResponse } from '@/types/onebot';
|
import type { AllOB11WsResponse } from '@/types/onebot';
|
||||||
|
|
||||||
export { ReadyState } from 'react-use-websocket';
|
export { ReadyState } from 'react-use-websocket';
|
||||||
export function useWebSocketDebug (url: string, token: string) {
|
export function useWebSocketDebug (url: string, token: string, connectOnMount: boolean = true) {
|
||||||
const messageHistory = useReactive<AllOB11WsResponse[]>([]);
|
const [messageHistory, setMessageHistory] = useState<AllOB11WsResponse[]>([]);
|
||||||
const [filterTypes, setFilterTypes] = useState<Selection>('all');
|
const [filterTypes, setFilterTypes] = useState<Selection>('all');
|
||||||
|
|
||||||
const filteredMessages = messageHistory.filter((msg) => {
|
const filteredMessages = messageHistory.filter((msg) => {
|
||||||
@ -22,11 +21,18 @@ export function useWebSocketDebug (url: string, token: string) {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sendMessage, readyState } = useWebSocket(url, {
|
const { sendMessage, readyState } = useWebSocket(connectOnMount ? url : null, {
|
||||||
|
share: false,
|
||||||
onMessage: useCallback((event: WebSocketEventMap['message']) => {
|
onMessage: useCallback((event: WebSocketEventMap['message']) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
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) {
|
} catch (_error) {
|
||||||
toast.error('WebSocket 消息解析失败');
|
toast.error('WebSocket 消息解析失败');
|
||||||
}
|
}
|
||||||
@ -39,7 +45,7 @@ export function useWebSocketDebug (url: string, token: string) {
|
|||||||
console.error('WebSocket error:', event);
|
console.error('WebSocket error:', event);
|
||||||
},
|
},
|
||||||
onOpen: () => {
|
onOpen: () => {
|
||||||
messageHistory.splice(0, messageHistory.length);
|
setMessageHistory([]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,6 +56,10 @@ export function useWebSocketDebug (url: string, token: string) {
|
|||||||
sendMessage(msg);
|
sendMessage(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearMessages = useCallback(() => {
|
||||||
|
setMessageHistory([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const FilterMessagesType = renderFilterMessageType(
|
const FilterMessagesType = renderFilterMessageType(
|
||||||
filterTypes,
|
filterTypes,
|
||||||
setFilterTypes
|
setFilterTypes
|
||||||
@ -63,5 +73,6 @@ export function useWebSocketDebug (url: string, token: string) {
|
|||||||
filterTypes,
|
filterTypes,
|
||||||
setFilterTypes,
|
setFilterTypes,
|
||||||
FilterMessagesType,
|
FilterMessagesType,
|
||||||
|
clearMessages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,10 +99,8 @@ const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
|
|||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 overflow-y-auto',
|
'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',
|
'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'
|
'pb-10 md:pb-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -24,9 +24,10 @@ export default function WSDebug () {
|
|||||||
});
|
});
|
||||||
const [inputUrl, setInputUrl] = useState(socketConfig.url);
|
const [inputUrl, setInputUrl] = useState(socketConfig.url);
|
||||||
const [inputToken, setInputToken] = useState(socketConfig.token);
|
const [inputToken, setInputToken] = useState(socketConfig.token);
|
||||||
|
const [shouldConnect, setShouldConnect] = useState(false);
|
||||||
|
|
||||||
const { sendMessage, readyState, FilterMessagesType, filteredMessages } =
|
const { sendMessage, readyState, FilterMessagesType, filteredMessages, clearMessages } =
|
||||||
useWebSocketDebug(socketConfig.url, socketConfig.token);
|
useWebSocketDebug(socketConfig.url, socketConfig.token, shouldConnect);
|
||||||
|
|
||||||
const handleConnect = useCallback(() => {
|
const handleConnect = useCallback(() => {
|
||||||
if (!inputUrl.startsWith('ws://') && !inputUrl.startsWith('wss://')) {
|
if (!inputUrl.startsWith('ws://') && !inputUrl.startsWith('wss://')) {
|
||||||
@ -37,13 +38,18 @@ export default function WSDebug () {
|
|||||||
url: inputUrl,
|
url: inputUrl,
|
||||||
token: inputToken,
|
token: inputToken,
|
||||||
});
|
});
|
||||||
}, [inputUrl, inputToken]);
|
setShouldConnect(true);
|
||||||
|
}, [inputUrl, inputToken, setSocketConfig]);
|
||||||
|
|
||||||
|
const handleDisconnect = useCallback(() => {
|
||||||
|
setShouldConnect(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>Websocket调试 - NapCat WebUI</title>
|
<title>Websocket调试 - NapCat WebUI</title>
|
||||||
<div className='h-[calc(100vh-4rem)] overflow-hidden flex flex-col'>
|
<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'>
|
<CardBody className='gap-2'>
|
||||||
<div className='grid gap-2 items-center md:grid-cols-5'>
|
<div className='grid gap-2 items-center md:grid-cols-5'>
|
||||||
<Input
|
<Input
|
||||||
@ -64,23 +70,33 @@ export default function WSDebug () {
|
|||||||
/>
|
/>
|
||||||
<div className='flex-shrink-0 flex gap-2 col-span-2 md:col-span-1'>
|
<div className='flex-shrink-0 flex gap-2 col-span-2 md:col-span-1'>
|
||||||
<Button
|
<Button
|
||||||
color='primary'
|
onPress={shouldConnect ? handleDisconnect : handleConnect}
|
||||||
onPress={handleConnect}
|
|
||||||
size='lg'
|
size='lg'
|
||||||
radius='full'
|
radius='full'
|
||||||
|
color={shouldConnect ? 'danger' : 'primary'}
|
||||||
className='w-full md:w-auto'
|
className='w-full md:w-auto'
|
||||||
>
|
>
|
||||||
连接
|
{shouldConnect ? '断开' : '连接'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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'>
|
<div className='grid gap-2 md:grid-cols-5 items-center md:w-fit'>
|
||||||
<WSStatus state={readyState} />
|
<WSStatus state={readyState} />
|
||||||
<div className='md:w-64 max-w-full col-span-2'>
|
<div className='md:w-64 max-w-full col-span-2'>
|
||||||
{FilterMessagesType}
|
{FilterMessagesType}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@ -329,8 +329,8 @@ export default function FileManagerPage () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-4'>
|
<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-content1 py-1'>
|
<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
|
<Button
|
||||||
color='primary'
|
color='primary'
|
||||||
size='sm'
|
size='sm'
|
||||||
@ -418,8 +418,8 @@ export default function FileManagerPage () {
|
|||||||
)
|
)
|
||||||
</Button>
|
</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) => (
|
{currentPath.split('/').map((part, index, parts) => (
|
||||||
<BreadcrumbItem
|
<BreadcrumbItem
|
||||||
key={part}
|
key={part}
|
||||||
|
|||||||
@ -105,7 +105,7 @@ const DashboardIndexPage: React.FC = () => {
|
|||||||
<SystemStatusCard setArchInfo={setArchInfo} />
|
<SystemStatusCard setArchInfo={setArchInfo} />
|
||||||
</div>
|
</div>
|
||||||
<Networks />
|
<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>
|
<CardBody>
|
||||||
<Hitokoto />
|
<Hitokoto />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@ -53,8 +53,8 @@ export default function LogsPage () {
|
|||||||
classNames={{
|
classNames={{
|
||||||
panel: 'w-full flex-1 h-full py-0 flex flex-col gap-4',
|
panel: 'w-full flex-1 h-full py-0 flex flex-col gap-4',
|
||||||
base: 'flex-shrink-0 !h-fit',
|
base: 'flex-shrink-0 !h-fit',
|
||||||
tabList: 'bg-opacity-50 backdrop-blur-sm',
|
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||||
cursor: 'bg-opacity-60 backdrop-blur-sm',
|
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab title='实时日志'>
|
<Tab title='实时日志'>
|
||||||
|
|||||||
@ -388,8 +388,8 @@ export default function NetworkPage () {
|
|||||||
className='max-w-full'
|
className='max-w-full'
|
||||||
items={tabs}
|
items={tabs}
|
||||||
classNames={{
|
classNames={{
|
||||||
tabList: 'bg-opacity-50 backdrop-blur-sm',
|
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||||
cursor: 'bg-opacity-60 backdrop-blur-sm',
|
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export default function TerminalPage () {
|
|||||||
className='h-full overflow-hidden'
|
className='h-full overflow-hidden'
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-2 flex-shrink-0 flex-grow-0'>
|
<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
|
<SortableContext
|
||||||
items={tabs}
|
items={tabs}
|
||||||
strategy={horizontalListSortingStrategy}
|
strategy={horizontalListSortingStrategy}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user