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

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