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

@@ -1,9 +1,11 @@
import { Card, CardBody } from '@heroui/card';
import { Tab, Tabs } from '@heroui/tabs';
import { useLocalStorage } from '@uidotdev/usehooks';
import clsx from 'clsx';
import { useMediaQuery } from 'react-responsive';
import { useNavigate, useSearchParams } from 'react-router-dom';
import key from '@/const/key';
import ChangePasswordCard from './change_password';
import LoginConfigCard from './login';
import OneBotConfigCard from './onebot';
@@ -12,24 +14,29 @@ import ThemeConfigCard from './theme';
import WebUIConfigCard from './webui';
export interface ConfigPageProps {
children?: React.ReactNode
size?: 'sm' | 'md' | 'lg'
children?: React.ReactNode;
size?: 'sm' | 'md' | 'lg';
}
const ConfingPageItem: React.FC<ConfigPageProps> = ({
const ConfigPageItem: React.FC<ConfigPageProps> = ({
children,
size = 'md',
}) => {
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
const hasBackground = !!backgroundImage;
return (
<Card className='bg-opacity-50 backdrop-blur-sm'>
<CardBody className='items-center py-5'>
<div
className={clsx('max-w-full flex flex-col gap-2', {
'w-72': size === 'sm',
'w-96': size === 'md',
'w-[32rem]': size === 'lg',
})}
>
<Card className={clsx(
'w-full mx-auto backdrop-blur-sm border border-white/40 dark:border-white/10 shadow-sm rounded-2xl transition-all',
hasBackground ? 'bg-white/20 dark:bg-black/10' : 'bg-white/60 dark:bg-black/40',
{
'max-w-xl': size === 'sm',
'max-w-3xl': size === 'md',
'max-w-6xl': size === 'lg',
}
)}>
<CardBody className='py-6 px-4 md:py-8 md:px-12'>
<div className='w-full flex flex-col gap-5'>
{children}
</div>
</CardBody>
@@ -38,7 +45,6 @@ const ConfingPageItem: React.FC<ConfigPageProps> = ({
};
export default function ConfigPage () {
const isMediumUp = useMediaQuery({ minWidth: 768 });
const navigate = useNavigate();
const search = useSearchParams({
tab: 'onebot',
@@ -46,53 +52,55 @@ export default function ConfigPage () {
const tab = search.get('tab') ?? 'onebot';
return (
<section className='w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10'>
<section className='w-full max-w-[1200px] mx-auto py-4 md:py-8 px-2 md:px-6 relative'>
<title> - NapCat WebUI</title>
<Tabs
aria-label='config tab'
fullWidth
fullWidth={false}
className='w-full'
isVertical={isMediumUp}
selectedKey={tab}
onSelectionChange={(key) => {
navigate(`/config?tab=${key}`);
}}
classNames={{
tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm',
panel: 'w-full relative',
base: 'md:!w-auto flex-grow-0 flex-shrink-0 mr-0',
cursor: 'bg-opacity-60 backdrop-blur-sm',
base: 'w-full flex-col items-center',
tabList: 'bg-white/40 dark:bg-black/20 backdrop-blur-md rounded-2xl p-1.5 shadow-sm border border-white/20 dark:border-white/5 mb-4 md:mb-8 w-full md:w-fit mx-auto overflow-x-auto hide-scrollbar',
cursor: 'bg-white/80 dark:bg-white/10 backdrop-blur-md shadow-sm rounded-xl',
tab: 'h-9 px-4 md:px-6',
tabContent: 'text-default-600 dark:text-default-300 font-medium group-data-[selected=true]:text-primary',
panel: 'w-full relative p-0',
}}
>
<Tab title='OneBot配置' key='onebot'>
<ConfingPageItem>
<ConfigPageItem>
<OneBotConfigCard />
</ConfingPageItem>
</ConfigPageItem>
</Tab>
<Tab title='服务器配置' key='server'>
<ConfingPageItem>
<ConfigPageItem>
<ServerConfigCard />
</ConfingPageItem>
</ConfigPageItem>
</Tab>
<Tab title='WebUI配置' key='webui'>
<ConfingPageItem>
<ConfigPageItem>
<WebUIConfigCard />
</ConfingPageItem>
</ConfigPageItem>
</Tab>
<Tab title='登录配置' key='login'>
<ConfingPageItem>
<ConfigPageItem>
<LoginConfigCard />
</ConfingPageItem>
</ConfigPageItem>
</Tab>
<Tab title='修改密码' key='token'>
<ConfingPageItem>
<ConfigPageItem size='sm'>
<ChangePasswordCard />
</ConfingPageItem>
</ConfigPageItem>
</Tab>
<Tab title='主题配置' key='theme'>
<ConfingPageItem size='lg'>
<ConfigPageItem size='lg'>
<ThemeConfigCard />
</ConfingPageItem>
</ConfigPageItem>
</Tab>
</Tabs>
</section>

View File

@@ -74,6 +74,11 @@ const OneBotConfigCard = () => {
{...field}
label='音乐签名地址'
placeholder='请输入音乐签名地址'
classNames={{
inputWrapper:
'bg-default-100/50 dark:bg-white/5 backdrop-blur-md border border-transparent hover:bg-default-200/50 dark:hover:bg-white/10 transition-all shadow-sm data-[hover=true]:border-default-300',
input: 'bg-transparent text-default-700 placeholder:text-default-400',
}}
/>
)}
/>

View File

@@ -1,5 +1,4 @@
import { Input } from '@heroui/input';
import { Switch } from '@heroui/switch';
import { useRequest } from 'ahooks';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
@@ -7,6 +6,7 @@ import toast from 'react-hot-toast';
import SaveButtons from '@/components/button/save_buttons';
import PageLoading from '@/components/page_loading';
import SwitchCard from '@/components/switch_card';
import WebUIManager from '@/controllers/webui_manager';
@@ -79,8 +79,8 @@ const ServerConfigCard = () => {
<>
<title> - NapCat WebUI</title>
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2'>
<div className='flex-shrink-0 w-full'></div>
<div className='flex flex-col gap-3'>
<div className='flex-shrink-0 w-full font-bold text-default-600 dark:text-default-400 px-1'></div>
<Controller
control={control}
name='host'
@@ -92,6 +92,11 @@ const ServerConfigCard = () => {
description='服务器监听的IP地址0.0.0.0表示监听所有网卡'
isDisabled={!!configError}
errorMessage={configError ? '获取配置失败' : undefined}
classNames={{
inputWrapper:
'bg-default-100/50 dark:bg-white/5 backdrop-blur-md border border-transparent hover:bg-default-200/50 dark:hover:bg-white/10 transition-all shadow-sm data-[hover=true]:border-default-300',
input: 'bg-transparent text-default-700 placeholder:text-default-400',
}}
/>
)}
/>
@@ -109,6 +114,11 @@ const ServerConfigCard = () => {
isDisabled={!!configError}
errorMessage={configError ? '获取配置失败' : undefined}
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
classNames={{
inputWrapper:
'bg-default-100/50 dark:bg-white/5 backdrop-blur-md border border-transparent hover:bg-default-200/50 dark:hover:bg-white/10 transition-all shadow-sm data-[hover=true]:border-default-300',
input: 'bg-transparent text-gray-800 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500',
}}
/>
)}
/>
@@ -126,47 +136,42 @@ const ServerConfigCard = () => {
isDisabled={!!configError}
errorMessage={configError ? '获取配置失败' : undefined}
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
classNames={{
inputWrapper:
'bg-default-100/50 dark:bg-white/5 backdrop-blur-md border border-transparent hover:bg-default-200/50 dark:hover:bg-white/10 transition-all shadow-sm data-[hover=true]:border-default-300',
input: 'bg-transparent text-gray-800 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500',
}}
/>
)}
/>
</div>
<div className='flex flex-col gap-2'>
<div className='flex-shrink-0 w-full'></div>
<div className='flex flex-col gap-3'>
<div className='flex-shrink-0 w-full font-bold text-default-600 dark:text-default-400 px-1'></div>
<Controller
control={control}
name='disableWebUI'
render={({ field }) => (
<Switch
isSelected={field.value}
onValueChange={(value) => field.onChange(value)}
isDisabled={!!configError}
>
<div className='flex flex-col'>
<span>WebUI</span>
<span className='text-sm text-default-400'>
WebUI服务
</span>
</div>
</Switch>
<SwitchCard
value={field.value}
onValueChange={(value: boolean) => field.onChange(value)}
disabled={!!configError}
label='禁用WebUI'
description='启用后将完全禁用WebUI服务需要重启生效'
/>
)}
/>
<Controller
control={control}
name='disableNonLANAccess'
render={({ field }) => (
<Switch
isSelected={field.value}
onValueChange={(value) => field.onChange(value)}
isDisabled={!!configError}
>
<div className='flex flex-col'>
<span>访</span>
<span className='text-sm text-default-400'>
访WebUI
</span>
</div>
</Switch>
<SwitchCard
value={field.value}
onValueChange={(value: boolean) => field.onChange(value)}
disabled={!!configError}
label='禁用非局域网访问'
description='启用后只允许局域网内的设备访问WebUI提高安全性'
/>
)}
/>
</div>

View File

@@ -93,11 +93,13 @@ const WebUIConfigCard = () => {
<>
<title>WebUI配置 - NapCat WebUI</title>
<div className='flex flex-col gap-2'>
<div className='flex-shrink-0 w-full'>WebUI字体</div>
<div className='flex-shrink-0 w-full font-bold text-default-600 dark:text-default-400 px-1'>WebUI字体</div>
<div className='text-sm text-default-400'>
<FileInput
label='中文字体'
placeholder='选择字体文件'
accept='.ttf,.otf,.woff,.woff2'
onChange={async (file) => {
try {
await FileManager.uploadWebUIFont(file);
@@ -124,26 +126,35 @@ const WebUIConfigCard = () => {
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='flex-shrink-0 w-full'></div>
<div className='flex-shrink-0 w-full font-bold text-default-600 dark:text-default-400 px-1'></div>
<Controller
control={control}
name='background'
render={({ field }) => <ImageInput {...field} />}
render={({ field }) => (
<ImageInput
{...field}
/>
)}
/>
</div>
<div className='flex flex-col gap-2'>
<div></div>
<div className='flex-shrink-0 w-full font-bold text-default-600 dark:text-default-400 px-1'></div>
{siteConfig.navItems.map((item) => (
<Controller
key={item.label}
control={control}
name={`customIcons.${item.label}`}
render={({ field }) => <ImageInput {...field} label={item.label} />}
render={({ field }) => (
<ImageInput
{...field}
label={item.label}
/>
)}
/>
))}
</div>
<div className='flex flex-col gap-2'>
<div className='flex-shrink-0 w-full'>Passkey认证</div>
<div className='flex-shrink-0 w-full font-bold text-default-600 dark:text-default-400 px-1'>Passkey认证</div>
<div className='text-sm text-default-400 mb-2'>
Passkey后便WebUItoken
</div>