Refactor UI for network cards and improve theming

Redesigned network display cards and related components for a more modern, consistent look, including improved button styles, card layouts, and responsive design. Added support for background images and dynamic theming across cards, tables, and log views. Enhanced input and select components with unified styling. Improved file table responsiveness and log display usability. Refactored OneBot API debug and navigation UI for better usability and mobile support.
This commit is contained in:
手瓜一十雪
2025-12-22 12:27:56 +08:00
parent eb86e4705f
commit a84d5d3976
38 changed files with 919 additions and 565 deletions

View File

@@ -8,7 +8,7 @@ import { useLocalStorage } from '@uidotdev/usehooks';
import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import { IoLink, IoSend, IoSettingsSharp } from 'react-icons/io5';
import { PiCatDuotone } from 'react-icons/pi';
import { TbApi, TbCode } from 'react-icons/tb';
import key from '@/const/key';
import { OneBotHttpApiContent, OneBotHttpApiPath } from '@/const/ob_api';
@@ -89,42 +89,58 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
}, [path]);
return (
<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}
<section className='h-full flex flex-col gap-3 md:gap-4 p-3 md:p-6 bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm rounded-2xl overflow-hidden'>
<div className='flex flex-col md:flex-row md:items-center justify-between border-b border-white/10 pb-3 md:pb-4 gap-3'>
<div className='flex items-center gap-2 md:gap-4 overflow-hidden'>
<h1 className='text-lg md:text-xl font-bold flex items-center gap-2 text-primary-500 flex-shrink-0'>
<TbApi size={24} />
<span className='truncate'>{data.description}</span>
</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' />}
className='bg-white/40 dark:bg-black/20 backdrop-blur-md shadow-sm border border-white/20 hidden md:flex'
symbol={<IoLink size={16} className='inline-block mr-1' />}
tooltipProps={{ content: '点击复制地址' }}
size="sm"
>
{path}
</Snippet>
<Button
size='sm'
variant='ghost'
color='primary'
className='border-primary/20 hover:bg-primary/10'
onPress={() => setIsStructOpen(true)}
>
</Button>
</div>
<div className='flex gap-2 items-center justify-end'>
<div className='flex gap-2 items-center flex-shrink-0'>
<Button
size='sm'
variant='flat'
color='default'
radius='full'
isIconOnly
className='bg-white/40 dark:bg-white/10 md:hidden font-medium text-default-700'
onPress={() => setIsStructOpen(true)}
>
<TbCode className="text-lg" />
</Button>
<Button
size='sm'
variant='flat'
color='default'
radius='full'
className='bg-white/40 dark:bg-white/10 hidden md:flex font-medium text-default-700'
startContent={<TbCode className="text-lg" />}
onPress={() => setIsStructOpen(true)}
>
</Button>
<Popover placement='bottom-end'>
<PopoverTrigger>
<Button
variant='ghost'
size='sm'
variant='flat'
color='default'
isIconOnly
radius='full'
className='border-white/20 hover:bg-white/20 text-default-600'
className='bg-white/40 dark:bg-white/10 text-default-700 font-medium'
startContent={<IoSettingsSharp className="animate-spin-slow-on-hover text-lg" />}
>
<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'>
@@ -159,18 +175,17 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
<Button
onPress={sendRequest}
color='primary'
size='lg'
radius='full'
className='font-bold px-8 shadow-lg shadow-primary/30'
className='font-bold px-6 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'>
<div className='flex-1 grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4 min-h-0 overflow-auto'>
{/* 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'>
@@ -183,7 +198,9 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
<Button
size='sm'
color='primary'
variant='light'
variant='flat'
radius='full'
className="bg-primary/10 text-primary"
onPress={() => setRequestBody(generateDefaultJson(data.request))}
>
@@ -207,7 +224,6 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
</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'>
@@ -217,8 +233,10 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
</div>
<Button
size='sm'
color='success'
variant='light'
color='primary'
variant='flat'
radius='full'
className="bg-primary/10 text-primary"
onPress={() => {
navigator.clipboard.writeText(responseContent);
toast.success('已复制');

View File

@@ -1,8 +1,11 @@
import { Button } from '@heroui/button';
import { Card, CardBody } from '@heroui/card';
import { Input } from '@heroui/input';
import clsx from 'clsx';
import { ScrollShadow } from "@heroui/scroll-shadow";
import { motion } from 'motion/react';
import { useState } from 'react';
import { TbApi, TbLayoutSidebarLeftCollapseFilled, TbSearch } from 'react-icons/tb';
import type { OneBotHttpApi, OneBotHttpApiPath } from '@/const/ob_api';
@@ -11,75 +14,116 @@ export interface OneBotApiNavListProps {
selectedApi: OneBotHttpApiPath;
onSelect: (apiName: OneBotHttpApiPath) => void;
openSideBar: boolean;
onToggle?: (isOpen: boolean) => void;
}
const OneBotApiNavList: React.FC<OneBotApiNavListProps> = (props) => {
const { data, selectedApi, onSelect, openSideBar } = props;
const { data, selectedApi, onSelect, openSideBar, onToggle } = props;
const [searchValue, setSearchValue] = useState('');
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 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={{
type: openSideBar ? 'spring' : 'tween',
stiffness: 150,
damping: 15,
}}
animate={{ width: openSideBar ? '16rem' : '0rem' }}
style={{ overflowY: openSideBar ? 'auto' : 'hidden' }}
>
<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-default-600'
classNames={{
inputWrapper:
'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'
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
isClearable
onClear={() => setSearchValue('')}
<>
{/* Mobile backdrop overlay */}
{openSideBar && (
<div
className="fixed inset-0 bg-black/20 backdrop-blur-[1px] z-10 md:hidden"
onClick={() => onToggle?.(false)}
/>
{Object.entries(data).map(([apiName, api]) => (
<Card
key={apiName}
shadow='none'
className={clsx(
'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) ||
api.description?.includes(searchValue)
),
},
{
'!bg-white/60 dark:!bg-white/10 !border-white/20 shadow-sm !text-primary font-medium':
apiName === selectedApi,
}
)}
isPressable
onPress={() => onSelect(apiName as OneBotHttpApiPath)}
>
<CardBody>
<h2 className='font-bold'>{api.description}</h2>
<div
className={clsx('text-sm text-default-400', {
'!text-primary': apiName === selectedApi,
})}
)}
<motion.div
className={clsx(
'h-full z-20 flex-shrink-0 border border-white/10 dark:border-white/5 bg-white/60 dark:bg-black/60 backdrop-blur-2xl shadow-xl overflow-hidden rounded-2xl',
'fixed md:relative left-0 top-0 md:top-auto md:left-auto'
)}
initial={false}
animate={{ width: openSideBar ? 280 : 0, opacity: openSideBar ? 1 : 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
<div className='w-[280px] h-full flex flex-col'>
<div className='p-3 md:p-4 flex justify-between items-center border-b border-white/10'>
<span className='font-bold text-lg px-2 flex items-center gap-2'>
<TbApi className="text-primary" /> API
</span>
{onToggle && (
<Button
isIconOnly
size='sm'
variant='light'
onPress={() => onToggle(false)}
className="text-default-500 hover:text-default-800"
>
{apiName}
</div>
</CardBody>
</Card>
))}
</div>
</motion.div>
<TbLayoutSidebarLeftCollapseFilled size={20} />
</Button>
)}
</div>
<div className='p-3 pb-0'>
<Input
classNames={{
inputWrapper:
'bg-white/40 dark:bg-white/10 backdrop-blur-md border border-white/20 hover:bg-white/60 dark:hover:bg-white/20 transition-all shadow-sm',
input: 'bg-transparent text-default-700 placeholder:text-default-400',
}}
isClearable
radius='lg'
placeholder='搜索 API...'
startContent={<TbSearch className="text-default-400" />}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onClear={() => setSearchValue('')}
size="sm"
/>
</div>
<ScrollShadow className='flex-1 p-3 flex flex-col gap-2 overflow-y-auto scroll-smooth' size={40}>
{Object.entries(data).map(([apiName, api]) => {
const isMatch = apiName.toLowerCase().includes(searchValue.toLowerCase()) ||
api.description?.toLowerCase().includes(searchValue.toLowerCase());
if (!isMatch) return null;
const isSelected = apiName === selectedApi;
return (
<div
key={apiName}
role="button"
tabIndex={0}
onClick={() => onSelect(apiName as OneBotHttpApiPath)}
onKeyDown={(e) => e.key === 'Enter' && onSelect(apiName as OneBotHttpApiPath)}
className="cursor-pointer focus:outline-none"
>
<Card
shadow='none'
className={clsx(
'w-full border border-transparent transition-all duration-200 group min-h-[60px]',
isSelected
? 'bg-primary/10 border-primary/20 shadow-sm'
: 'bg-transparent hover:bg-white/40 dark:hover:bg-white/5'
)}
>
<CardBody className='p-3 text-left'>
<div className='flex flex-col gap-1'>
<span className={clsx(
'font-medium text-sm transition-colors',
isSelected ? 'text-primary-600 dark:text-primary-400' : 'text-default-700 dark:text-default-200 group-hover:text-default-900'
)}>
{api.description}
</span>
<span className={clsx(
'text-xs font-mono truncate transition-colors',
isSelected ? 'text-primary-400 dark:text-primary-300' : 'text-default-400 group-hover:text-default-500'
)}>
{apiName}
</span>
</div>
</CardBody>
</Card>
</div>
);
})}
</ScrollShadow>
</div>
</motion.div>
</>
);
};