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:
手瓜一十雪 2025-12-22 10:38:23 +08:00
parent 872a3e0100
commit 8697061a90
19 changed files with 380 additions and 296 deletions

View File

@ -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'>

View File

@ -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}

View File

@ -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 />

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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} />)

View File

@ -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}

View File

@ -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) : '请求响应'}

View File

@ -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>
)}

View File

@ -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 版本'

View File

@ -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'>

View File

@ -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,
};
}

View File

@ -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'
)}
>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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='实时日志'>

View File

@ -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) => (

View File

@ -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}