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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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