mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-02 00:30:25 +00:00
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:
@@ -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('已复制');
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user