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:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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后,您可以更便捷地登录WebUI,无需每次输入token
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'motion/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { TbSquareRoundedChevronLeftFilled } from 'react-icons/tb';
|
||||
|
||||
@@ -27,36 +26,39 @@ export default function HttpDebug () {
|
||||
return (
|
||||
<>
|
||||
<title>HTTP调试 - NapCat WebUI</title>
|
||||
<OneBotApiNavList
|
||||
data={oneBotHttpApi}
|
||||
selectedApi={selectedApi}
|
||||
onSelect={setSelectedApi}
|
||||
openSideBar={openSideBar}
|
||||
/>
|
||||
<div ref={contentRef} className='flex-1 h-full overflow-x-hidden'>
|
||||
<motion.div
|
||||
className='absolute top-16 z-30 md:!ml-4'
|
||||
animate={{ marginLeft: openSideBar ? '16rem' : '1rem' }}
|
||||
transition={{ type: 'spring', stiffness: 150, damping: 15 }}
|
||||
<div className='flex h-[calc(100vh-3.5rem)] overflow-hidden relative p-2 md:p-4 gap-2 md:gap-4'>
|
||||
<OneBotApiNavList
|
||||
data={oneBotHttpApi}
|
||||
selectedApi={selectedApi}
|
||||
onSelect={(api) => {
|
||||
setSelectedApi(api);
|
||||
// Auto-close sidebar on mobile after selection
|
||||
if (window.innerWidth < 768) {
|
||||
setOpenSideBar(false);
|
||||
}
|
||||
}}
|
||||
openSideBar={openSideBar}
|
||||
onToggle={setOpenSideBar}
|
||||
/>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className='flex-1 h-full overflow-hidden flex flex-col relative'
|
||||
>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
radius='md'
|
||||
variant='shadow'
|
||||
size='sm'
|
||||
onPress={() => setOpenSideBar(!openSideBar)}
|
||||
>
|
||||
<TbSquareRoundedChevronLeftFilled
|
||||
size={24}
|
||||
className={clsx(
|
||||
'transition-transform',
|
||||
openSideBar ? '' : 'transform rotate-180'
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</motion.div>
|
||||
<OneBotApiDebug path={selectedApi} data={data} />
|
||||
{/* Toggle Button Container - positioned on top-left of content if sidebar is closed */}
|
||||
<div className='absolute top-2 left-2 z-30'>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="flat"
|
||||
className={clsx("bg-white/40 dark:bg-black/40 backdrop-blur-md transition-opacity rounded-full shadow-sm", openSideBar ? "opacity-0 pointer-events-none md:opacity-0" : "opacity-100")}
|
||||
onPress={() => setOpenSideBar(true)}
|
||||
>
|
||||
<TbSquareRoundedChevronLeftFilled className="transform rotate-180" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<OneBotApiDebug path={selectedApi} data={data} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -48,8 +48,8 @@ export default function WSDebug () {
|
||||
return (
|
||||
<>
|
||||
<title>Websocket调试 - NapCat WebUI</title>
|
||||
<div className='h-[calc(100vh-4rem)] overflow-hidden flex flex-col'>
|
||||
<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'>
|
||||
<div className='h-[calc(100vh-4rem)] overflow-hidden flex flex-col p-2 md:p-0'>
|
||||
<Card className='md:mx-2 md: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'>
|
||||
<div className='grid gap-2 items-center md:grid-cols-5'>
|
||||
<Input
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BreadcrumbItem, Breadcrumbs } from '@heroui/breadcrumbs';
|
||||
import { Button } from '@heroui/button';
|
||||
import { Input } from '@heroui/input';
|
||||
import type { Selection, SortDescriptor } from '@react-types/shared';
|
||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||
import clsx from 'clsx';
|
||||
import { motion } from 'motion/react';
|
||||
import path from 'path-browserify';
|
||||
@@ -14,6 +15,7 @@ import { TbTrash } from 'react-icons/tb';
|
||||
import { TiArrowBack } from 'react-icons/ti';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import key from '@/const/key';
|
||||
import CreateFileModal from '@/components/file_manage/create_file_modal';
|
||||
import FileEditModal from '@/components/file_manage/file_edit_modal';
|
||||
import FilePreviewModal from '@/components/file_manage/file_preview_modal';
|
||||
@@ -328,123 +330,139 @@ export default function FileManagerPage () {
|
||||
useFsAccessApi: false, // 添加此选项以避免某些浏览器的文件系统API问题
|
||||
});
|
||||
|
||||
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
||||
const hasBackground = !!backgroundImage;
|
||||
|
||||
return (
|
||||
<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-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
|
||||
color='primary'
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={() => handleDirectoryClick('..')}
|
||||
className='text-lg'
|
||||
>
|
||||
<TiArrowBack />
|
||||
</Button>
|
||||
<div className='h-full flex flex-col relative gap-4 w-full p-2 md:p-4'>
|
||||
<div className={clsx(
|
||||
'mb-4 flex flex-col md:flex-row items-stretch md:items-center gap-4 sticky top-14 z-10 backdrop-blur-sm shadow-sm py-2 px-4 rounded-xl transition-colors',
|
||||
hasBackground
|
||||
? 'bg-white/20 dark:bg-black/10 border border-white/40 dark:border-white/10'
|
||||
: 'bg-white/60 dark:bg-black/40 border border-white/40 dark:border-white/10'
|
||||
)}>
|
||||
<div className='flex items-center gap-2 overflow-x-auto hide-scrollbar pb-1 md:pb-0'>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={() => handleDirectoryClick('..')}
|
||||
className='text-lg min-w-8'
|
||||
>
|
||||
<TiArrowBack />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={() => setIsCreateModalOpen(true)}
|
||||
className='text-lg'
|
||||
>
|
||||
<FiPlus />
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={() => setIsCreateModalOpen(true)}
|
||||
className='text-lg min-w-8'
|
||||
>
|
||||
<FiPlus />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color='primary'
|
||||
isLoading={loading}
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={loadFiles}
|
||||
className='text-lg'
|
||||
>
|
||||
<MdRefresh />
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={() => setShowUpload((prev) => !prev)}
|
||||
className='text-lg'
|
||||
>
|
||||
<FiUpload />
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
isLoading={loading}
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={loadFiles}
|
||||
className='text-lg min-w-8'
|
||||
>
|
||||
<MdRefresh />
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
isIconOnly
|
||||
variant='flat'
|
||||
onPress={() => setShowUpload((prev) => !prev)}
|
||||
className='text-lg min-w-8'
|
||||
>
|
||||
<FiUpload />
|
||||
</Button>
|
||||
|
||||
{((selectedFiles instanceof Set && selectedFiles.size > 0) ||
|
||||
selectedFiles === 'all') && (
|
||||
<>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
onPress={handleBatchDelete}
|
||||
className='text-sm'
|
||||
startContent={<TbTrash className='text-lg' />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
{((selectedFiles instanceof Set && selectedFiles.size > 0) ||
|
||||
selectedFiles === 'all') && (
|
||||
<>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
onPress={handleBatchDelete}
|
||||
className='text-sm px-2 min-w-fit'
|
||||
startContent={<TbTrash className='text-lg' />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
onPress={() => {
|
||||
setMoveTargetPath('');
|
||||
setIsMoveModalOpen(true);
|
||||
}}
|
||||
className='text-sm px-2 min-w-fit'
|
||||
startContent={<FiMove className='text-lg' />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
onPress={handleBatchDownload}
|
||||
className='text-sm px-2 min-w-fit'
|
||||
startContent={<FiDownload className='text-lg' />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col md:flex-row flex-1 gap-2 overflow-hidden items-stretch md:items-center'>
|
||||
<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 overflow-x-auto hide-scrollbar whitespace-nowrap'>
|
||||
{currentPath.split('/').map((part, index, parts) => (
|
||||
<BreadcrumbItem
|
||||
key={part}
|
||||
isCurrent={index === parts.length - 1}
|
||||
onPress={() => {
|
||||
setMoveTargetPath('');
|
||||
setIsMoveModalOpen(true);
|
||||
const newPath = parts.slice(0, index + 1).join('/');
|
||||
navigate(`/file_manager#${encodeURIComponent(newPath)}`);
|
||||
}}
|
||||
className='text-sm'
|
||||
startContent={<FiMove className='text-lg' />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
onPress={handleBatchDownload}
|
||||
className='text-sm'
|
||||
startContent={<FiDownload className='text-lg' />}
|
||||
>
|
||||
(
|
||||
{selectedFiles instanceof Set ? selectedFiles.size : files.length}
|
||||
)
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<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) => (
|
||||
<BreadcrumbItem
|
||||
key={part}
|
||||
isCurrent={index === parts.length - 1}
|
||||
onPress={() => {
|
||||
const newPath = parts.slice(0, index + 1).join('/');
|
||||
navigate(`/file_manager#${encodeURIComponent(newPath)}`);
|
||||
}}
|
||||
>
|
||||
{part}
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='输入跳转路径'
|
||||
value={jumpPath}
|
||||
onChange={(e) => setJumpPath(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && jumpPath.trim() !== '') {
|
||||
navigate(`/file_manager#${encodeURIComponent(jumpPath.trim())}`);
|
||||
}
|
||||
}}
|
||||
className='ml-auto w-64'
|
||||
/>
|
||||
{part}
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='输入跳转路径'
|
||||
value={jumpPath}
|
||||
onChange={(e) => setJumpPath(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && jumpPath.trim() !== '') {
|
||||
navigate(`/file_manager#${encodeURIComponent(jumpPath.trim())}`);
|
||||
}
|
||||
}}
|
||||
className='w-full md:w-64'
|
||||
classNames={{
|
||||
inputWrapper: 'bg-white/40 dark:bg-black/20 backdrop-blur-md',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Card, CardBody } from '@heroui/card';
|
||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||
import { useRequest } from 'ahooks';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import key from '@/const/key';
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -92,6 +95,9 @@ const SystemStatusCard: React.FC<SystemStatusCardProps> = ({ setArchInfo }) => {
|
||||
|
||||
const DashboardIndexPage: React.FC = () => {
|
||||
const [archInfo, setArchInfo] = useState<string>();
|
||||
// @ts-ignore
|
||||
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
||||
const hasBackground = !!backgroundImage;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -105,7 +111,10 @@ const DashboardIndexPage: React.FC = () => {
|
||||
<SystemStatusCard setArchInfo={setArchInfo} />
|
||||
</div>
|
||||
<Networks />
|
||||
<Card className='bg-white/60 dark:bg-black/40 backdrop-blur-xl border border-white/40 dark:border-white/10 shadow-sm'>
|
||||
<Card className={clsx(
|
||||
'backdrop-blur-sm border border-white/40 dark:border-white/10 shadow-sm transition-all',
|
||||
hasBackground ? 'bg-white/10 dark:bg-black/10' : 'bg-white/60 dark:bg-black/40'
|
||||
)}>
|
||||
<CardBody>
|
||||
<Hitokoto />
|
||||
</CardBody>
|
||||
|
||||
@@ -375,9 +375,8 @@ export default function NetworkPage () {
|
||||
<AddButton onOpen={handleClickCreate} />
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
|
||||
radius='full'
|
||||
variant='flat'
|
||||
onPress={refresh}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
|
||||
@@ -12,10 +12,13 @@ import {
|
||||
horizontalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Button } from '@heroui/button';
|
||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IoAdd, IoClose } from 'react-icons/io5';
|
||||
|
||||
import key from '@/const/key';
|
||||
import { TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||
import { SortableTab } from '@/components/tabs/sortable_tab.tsx';
|
||||
import { TerminalInstance } from '@/components/terminal/terminal-instance';
|
||||
@@ -30,6 +33,8 @@ interface TerminalTab {
|
||||
export default function TerminalPage () {
|
||||
const [tabs, setTabs] = useState<TerminalTab[]>([]);
|
||||
const [selectedTab, setSelectedTab] = useState<string>('');
|
||||
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
|
||||
const hasBackground = !!backgroundImage;
|
||||
|
||||
useEffect(() => {
|
||||
// 获取已存在的终端列表
|
||||
@@ -112,35 +117,40 @@ export default function TerminalPage () {
|
||||
className='h-full overflow-hidden'
|
||||
>
|
||||
<div className='flex items-center gap-2 flex-shrink-0 flex-grow-0'>
|
||||
<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
|
||||
items={tabs}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<SortableTab
|
||||
key={tab.id}
|
||||
id={tab.id}
|
||||
value={tab.id}
|
||||
isSelected={selectedTab === tab.id}
|
||||
className='flex gap-2 items-center flex-shrink-0'
|
||||
>
|
||||
{tab.title}
|
||||
<Button
|
||||
isIconOnly
|
||||
radius='full'
|
||||
variant='flat'
|
||||
size='sm'
|
||||
className='min-w-0 w-4 h-4 flex-shrink-0'
|
||||
onPress={() => closeTerminal(tab.id)}
|
||||
color={selectedTab === tab.id ? 'primary' : 'default'}
|
||||
{tabs.length > 0 && (
|
||||
<TabList className={clsx(
|
||||
'flex-1 !overflow-x-auto w-full hide-scrollbar backdrop-blur-sm p-1 rounded-lg border border-white/20',
|
||||
hasBackground ? 'bg-white/20 dark:bg-black/10' : 'bg-white/40 dark:bg-black/20'
|
||||
)}>
|
||||
<SortableContext
|
||||
items={tabs}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<SortableTab
|
||||
key={tab.id}
|
||||
id={tab.id}
|
||||
value={tab.id}
|
||||
isSelected={selectedTab === tab.id}
|
||||
className='flex gap-2 items-center flex-shrink-0'
|
||||
>
|
||||
<IoClose />
|
||||
</Button>
|
||||
</SortableTab>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TabList>
|
||||
{tab.title}
|
||||
<Button
|
||||
isIconOnly
|
||||
radius='full'
|
||||
variant='flat'
|
||||
size='sm'
|
||||
className='min-w-0 w-4 h-4 flex-shrink-0'
|
||||
onPress={() => closeTerminal(tab.id)}
|
||||
color={selectedTab === tab.id ? 'primary' : 'default'}
|
||||
>
|
||||
<IoClose />
|
||||
</Button>
|
||||
</SortableTab>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TabList>
|
||||
)}
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
|
||||
Reference in New Issue
Block a user