feat: 优化webui界面和文件管理器 (#1472)

This commit is contained in:
时瑾 2026-01-01 21:40:39 +08:00 committed by GitHub
parent 4e37b002f9
commit ce9482f19d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 281 additions and 217 deletions

View File

@ -14,7 +14,7 @@ export class PacketMsgBuilder {
buildFakeMsg (selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] { buildFakeMsg (selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => { return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`; const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=0&img_type=jpg`;
const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => { const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => {
return acc ?? msg.buildContent(); return acc ?? msg.buildContent();
}, undefined); }, undefined);

View File

@ -37,6 +37,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
onEnable, onEnable,
onDelete, onDelete,
onEnableDebug, onEnableDebug,
showType,
}: NetworkDisplayCardProps<T>) => { }: NetworkDisplayCardProps<T>) => {
const { name, enable, debug } = data; const { name, enable, debug } = data;
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
@ -60,15 +61,16 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
return ( return (
<DisplayCardContainer <DisplayCardContainer
className="w-full max-w-[420px]" className='w-full max-w-[420px]'
tag={showType ? typeLabel : undefined}
action={ action={
<div className="flex gap-2 w-full"> <div className='flex gap-2 w-full'>
<Button <Button
fullWidth fullWidth
radius='full' radius='full'
size='sm' size='sm'
variant='flat' variant='flat'
className="flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-warning/20 hover:text-warning transition-colors" className='flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-warning/20 hover:text-warning transition-colors'
startContent={<FiEdit3 size={16} />} startContent={<FiEdit3 size={16} />}
onPress={onEdit} onPress={onEdit}
isDisabled={editing} isDisabled={editing}
@ -82,10 +84,10 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
size='sm' size='sm'
variant='flat' variant='flat'
className={clsx( className={clsx(
"flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium transition-colors", 'flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium transition-colors',
debug debug
? "hover:bg-secondary/20 hover:text-secondary data-[hover=true]:text-secondary" ? 'hover:bg-secondary/20 hover:text-secondary data-[hover=true]:text-secondary'
: "hover:bg-success/20 hover:text-success data-[hover=true]:text-success" : 'hover:bg-success/20 hover:text-success data-[hover=true]:text-success'
)} )}
startContent={<CgDebug size={16} />} startContent={<CgDebug size={16} />}
onPress={handleEnableDebug} onPress={handleEnableDebug}
@ -113,11 +115,11 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
isSelected={enable} isSelected={enable}
onChange={handleEnable} onChange={handleEnable}
classNames={{ classNames={{
wrapper: "group-data-[selected=true]:bg-primary-400", wrapper: 'group-data-[selected=true]:bg-primary-400',
}} }}
/> />
} }
title={typeLabel} title={name}
> >
<div className='grid grid-cols-2 gap-3'> <div className='grid grid-cols-2 gap-3'>
{(() => { {(() => {
@ -125,29 +127,30 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
if (targetFullField) { if (targetFullField) {
// 模式1存在全宽字段如URL布局为 // 模式1存在全宽字段如URL布局为
// Row 1: 名称 (全宽) // Row 1: 类型 (全宽)
// Row 2: 全宽字段 (全宽) // Row 2: 全宽字段 (全宽)
return ( return (
<> <>
<div <div
className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors col-span-2' className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors col-span-2'
> >
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'></span> <span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'></span>
<div className="text-sm font-medium text-default-700 dark:text-white/90 truncate"> <div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{name} {typeLabel}
</div> </div>
</div> </div>
<div <div
className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors col-span-2' className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors col-span-2'
> >
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>{targetFullField.label}</span> <span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>{targetFullField.label}</span>
<div className="text-sm font-medium text-default-700 dark:text-white/90 truncate"> <div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{targetFullField.render {targetFullField.render
? targetFullField.render(targetFullField.value) ? targetFullField.render(targetFullField.value)
: ( : (
<span className={clsx( <span className={clsx(
typeof targetFullField.value === 'string' && (targetFullField.value.startsWith('http') || targetFullField.value.includes('.') || targetFullField.value.includes(':')) ? 'font-mono' : '' typeof targetFullField.value === 'string' && (targetFullField.value.startsWith('http') || targetFullField.value.includes('.') || targetFullField.value.includes(':')) ? 'font-mono' : ''
)}> )}
>
{String(targetFullField.value)} {String(targetFullField.value)}
</span> </span>
)} )}
@ -157,7 +160,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
); );
} else { } else {
// 模式2无全宽字段布局为 4 个小块 (2行 x 2列) // 模式2无全宽字段布局为 4 个小块 (2行 x 2列)
// Row 1: 名称 | Field 0 // Row 1: 类型 | Field 0
// Row 2: Field 1 | Field 2 // Row 2: Field 1 | Field 2
const displayFields = fields.slice(0, 3); const displayFields = fields.slice(0, 3);
return ( return (
@ -165,9 +168,9 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
<div <div
className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors' className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'
> >
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'></span> <span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'></span>
<div className="text-sm font-medium text-default-700 dark:text-white/90 truncate"> <div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{name} {typeLabel}
</div> </div>
</div> </div>
{displayFields.map((field, index) => ( {displayFields.map((field, index) => (
@ -176,7 +179,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors' className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'
> >
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>{field.label}</span> <span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>{field.label}</span>
<div className="text-sm font-medium text-default-700 dark:text-white/90 truncate"> <div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
{field.render {field.render
? ( ? (
field.render(field.value) field.render(field.value)
@ -184,7 +187,8 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
: ( : (
<span className={clsx( <span className={clsx(
typeof field.value === 'string' && (field.value.startsWith('http') || field.value.includes('.') || field.value.includes(':')) ? 'font-mono' : '' typeof field.value === 'string' && (field.value.startsWith('http') || field.value.includes('.') || field.value.includes(':')) ? 'font-mono' : ''
)}> )}
>
{String(field.value)} {String(field.value)}
</span> </span>
)} )}

View File

@ -9,13 +9,13 @@ import {
} from '@heroui/modal'; } from '@heroui/modal';
interface CreateFileModalProps { interface CreateFileModalProps {
isOpen: boolean isOpen: boolean;
fileType: 'file' | 'directory' fileType: 'file' | 'directory';
newFileName: string newFileName: string;
onTypeChange: (type: 'file' | 'directory') => void onTypeChange: (type: 'file' | 'directory') => void;
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClose: () => void onClose: () => void;
onCreate: () => void onCreate: () => void;
} }
export default function CreateFileModal ({ export default function CreateFileModal ({
@ -28,12 +28,12 @@ export default function CreateFileModal ({
onCreate, onCreate,
}: CreateFileModalProps) { }: CreateFileModalProps) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal radius='sm' isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody> <ModalBody>
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
<ButtonGroup color='primary'> <ButtonGroup radius='sm' color='primary'>
<Button <Button
variant={fileType === 'file' ? 'solid' : 'flat'} variant={fileType === 'file' ? 'solid' : 'flat'}
onPress={() => onTypeChange('file')} onPress={() => onTypeChange('file')}
@ -47,14 +47,14 @@ export default function CreateFileModal ({
</Button> </Button>
</ButtonGroup> </ButtonGroup>
<Input label='名称' value={newFileName} onChange={onNameChange} /> <Input radius='sm' label='名称' value={newFileName} onChange={onNameChange} />
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color='primary' variant='flat' onPress={onClose}> <Button radius='sm' color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color='primary' onPress={onCreate}> <Button radius='sm' color='primary' onPress={onCreate}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@ -63,11 +63,11 @@ export default function FileEditModal ({
}; };
return ( return (
<Modal size='full' isOpen={isOpen} onClose={onClose}> <Modal radius='sm' size='full' isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader className='flex items-center gap-2 border-b border-default-200/50'> <ModalHeader className='flex items-center gap-2 border-b border-default-200/50'>
<span></span> <span></span>
<Code className='text-xs'>{file?.path}</Code> <Code radius='sm' className='text-xs'>{file?.path}</Code>
<div className="ml-auto text-xs text-default-400 font-normal px-2"> <div className="ml-auto text-xs text-default-400 font-normal px-2">
<span className="px-1 py-0.5 rounded border border-default-300 bg-default-100">Ctrl/Cmd + S</span> <span className="px-1 py-0.5 rounded border border-default-300 bg-default-100">Ctrl/Cmd + S</span>
</div> </div>
@ -89,10 +89,10 @@ export default function FileEditModal ({
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter className="border-t border-default-200/50"> <ModalFooter className="border-t border-default-200/50">
<Button color='primary' variant='flat' onPress={onClose}> <Button radius='sm' color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color='primary' onPress={onSave}> <Button radius='sm' color='primary' onPress={onSave}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@ -14,9 +14,9 @@ import { useEffect } from 'react';
import FileManager from '@/controllers/file_manager'; import FileManager from '@/controllers/file_manager';
interface FilePreviewModalProps { interface FilePreviewModalProps {
isOpen: boolean isOpen: boolean;
filePath: string filePath: string;
onClose: () => void onClose: () => void;
} }
export const videoExts = ['.mp4', '.webm']; export const videoExts = ['.mp4', '.webm'];
@ -75,14 +75,14 @@ export default function FilePreviewModal ({
} }
return ( return (
<Modal isOpen={isOpen} onClose={onClose} scrollBehavior='inside' size='3xl'> <Modal radius='sm' isOpen={isOpen} onClose={onClose} scrollBehavior='inside' size='3xl'>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody className='flex justify-center items-center'> <ModalBody className='flex justify-center items-center'>
{contentElement} {contentElement}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color='primary' variant='flat' onPress={onClose}> <Button radius='sm' color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@ -105,6 +105,7 @@ export default function FileTable ({
/> />
<Table <Table
aria-label='文件列表' aria-label='文件列表'
radius='sm'
sortDescriptor={sortDescriptor} sortDescriptor={sortDescriptor}
onSortChange={onSortChange} onSortChange={onSortChange}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
@ -175,6 +176,7 @@ export default function FileTable ({
) )
: ( : (
<Button <Button
radius='sm'
variant='light' variant='light'
onPress={() => onPress={() =>
file.isDirectory file.isDirectory
@ -202,7 +204,7 @@ export default function FileTable ({
</TableCell> </TableCell>
<TableCell className='hidden md:table-cell'>{new Date(file.mtime).toLocaleString()}</TableCell> <TableCell className='hidden md:table-cell'>{new Date(file.mtime).toLocaleString()}</TableCell>
<TableCell> <TableCell>
<ButtonGroup size='sm' variant='light'> <ButtonGroup radius='sm' size='sm' variant='light'>
<Button <Button
isIconOnly isIconOnly
color='default' color='default'

View File

@ -10,17 +10,17 @@ import FileManager from '@/controllers/file_manager';
import FileIcon from '../file_icon'; import FileIcon from '../file_icon';
export interface PreviewImage { export interface PreviewImage {
key: string key: string;
src: string src: string;
alt: string alt: string;
} }
export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp']; export const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'];
export interface ImageNameButtonProps { export interface ImageNameButtonProps {
name: string name: string;
filePath: string filePath: string;
onPreview: () => void onPreview: () => void;
onAddPreview: (image: PreviewImage) => void onAddPreview: (image: PreviewImage) => void;
} }
export default function ImageNameButton ({ export default function ImageNameButton ({
@ -61,6 +61,7 @@ export default function ImageNameButton ({
return ( return (
<Button <Button
radius='sm'
variant='light' variant='light'
className='text-left justify-start' className='text-left justify-start'
onPress={onPreview} onPress={onPreview}

View File

@ -83,15 +83,16 @@ function DirectoryTree ({
return ( return (
<div className='ml-4'> <div className='ml-4'>
<Button <Button
radius='sm'
onPress={handleClick} onPress={handleClick}
className='py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md' className='py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-sm'
size='sm' size='sm'
color='primary' color='primary'
variant={variant} variant={variant}
startContent={ startContent={
<div <div
className={clsx( className={clsx(
'rounded-md', 'rounded-sm',
isSeleted ? 'bg-primary-600' : 'bg-primary-50' isSeleted ? 'bg-primary-600' : 'bg-primary-50'
)} )}
> >
@ -140,11 +141,11 @@ export default function MoveModal ({
onSelect, onSelect,
}: MoveModalProps) { }: MoveModalProps) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal radius='sm' isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody> <ModalBody>
<div className='rounded-md p-2 border border-default-300 overflow-auto max-h-60'> <div className='rounded-sm p-2 border border-default-300 overflow-auto max-h-60'>
<DirectoryTree <DirectoryTree
basePath='/' basePath='/'
onSelect={onSelect} onSelect={onSelect}
@ -157,10 +158,10 @@ export default function MoveModal ({
<p className='text-sm text-default-500'>{selectionInfo}</p> <p className='text-sm text-default-500'>{selectionInfo}</p>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color='primary' variant='flat' onPress={onClose}> <Button radius='sm' color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color='primary' onPress={onMove}> <Button radius='sm' color='primary' onPress={onMove}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@ -9,11 +9,11 @@ import {
} from '@heroui/modal'; } from '@heroui/modal';
interface RenameModalProps { interface RenameModalProps {
isOpen: boolean isOpen: boolean;
newFileName: string newFileName: string;
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClose: () => void onClose: () => void;
onRename: () => void onRename: () => void;
} }
export default function RenameModal ({ export default function RenameModal ({
@ -24,17 +24,17 @@ export default function RenameModal ({
onRename, onRename,
}: RenameModalProps) { }: RenameModalProps) {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <Modal radius='sm' isOpen={isOpen} onClose={onClose}>
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalBody> <ModalBody>
<Input label='新名称' value={newFileName} onChange={onNameChange} /> <Input radius='sm' label='新名称' value={newFileName} onChange={onNameChange} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color='primary' variant='flat' onPress={onClose}> <Button radius='sm' color='primary' variant='flat' onPress={onClose}>
</Button> </Button>
<Button color='primary' onPress={onRename}> <Button radius='sm' color='primary' onPress={onRename}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@ -1,3 +1,5 @@
/* eslint-disable @stylistic/jsx-closing-bracket-location */
/* eslint-disable @stylistic/jsx-closing-tag-location */
import { Button } from '@heroui/button'; import { Button } from '@heroui/button';
import { Tooltip } from '@heroui/tooltip'; import { Tooltip } from '@heroui/tooltip';
import { useLocalStorage } from '@uidotdev/usehooks'; import { useLocalStorage } from '@uidotdev/usehooks';
@ -40,30 +42,36 @@ export default function Hitokoto () {
} }
}; };
return ( return (
<div> <div className='overflow-hidden'>
<div className='relative flex flex-col items-center justify-center p-6 min-h-[120px]'> <div className='relative flex flex-col items-center justify-center p-4 md:p-6'>
{loading && !data && <PageLoading />} {loading && !data && <PageLoading />}
{data && ( {data && (
<> <>
<IoMdQuote className={clsx( <IoMdQuote className={clsx(
"text-4xl mb-4", 'text-4xl mb-4',
hasBackground ? "text-white/30" : "text-primary/20" hasBackground ? 'text-white/30' : 'text-primary/20'
)} /> )}
/>
<div className={clsx( <div className={clsx(
"text-xl font-medium tracking-wide leading-relaxed italic", 'text-xl font-medium tracking-wide leading-relaxed italic',
hasBackground ? "text-white drop-shadow-sm" : "text-default-700 dark:text-gray-200" hasBackground ? 'text-white drop-shadow-sm' : 'text-default-700 dark:text-gray-200'
)}> )}
>
" {data?.hitokoto} " " {data?.hitokoto} "
</div> </div>
<div className='mt-4 flex flex-col items-center text-sm'> <div className='mt-4 flex flex-col items-center text-sm'>
<span className={clsx( <span className={clsx(
'font-bold', 'font-bold',
hasBackground ? 'text-white/90' : 'text-primary-500/80' hasBackground ? 'text-white/90' : 'text-primary-500/80'
)}> {data?.from}</span> )}
> {data?.from}
</span>
{data?.from_who && <span className={clsx( {data?.from_who && <span className={clsx(
"text-xs mt-1", 'text-xs mt-1',
hasBackground ? "text-white/70" : "text-default-400" hasBackground ? 'text-white/70' : 'text-default-400'
)}>{data?.from_who}</span>} )}
> {data?.from_who}
</span>}
</div> </div>
</> </>
)} )}
@ -72,8 +80,8 @@ export default function Hitokoto () {
<Tooltip content='刷新' placement='top'> <Tooltip content='刷新' placement='top'>
<Button <Button
className={clsx( className={clsx(
"transition-colors", 'transition-colors',
hasBackground ? "text-white/60 hover:text-white" : "text-default-400 hover:text-primary" hasBackground ? 'text-white/60 hover:text-white' : 'text-default-400 hover:text-primary'
)} )}
onPress={run} onPress={run}
size='sm' size='sm'
@ -88,8 +96,8 @@ export default function Hitokoto () {
<Tooltip content='复制' placement='top'> <Tooltip content='复制' placement='top'>
<Button <Button
className={clsx( className={clsx(
"transition-colors", 'transition-colors',
hasBackground ? "text-white/60 hover:text-white" : "text-default-400 hover:text-success" hasBackground ? 'text-white/60 hover:text-white' : 'text-default-400 hover:text-success'
)} )}
onPress={onCopy} onPress={onCopy}
size='sm' size='sm'

View File

@ -13,18 +13,18 @@ import type {
import { renderMessageContent } from '../render_message'; import { renderMessageContent } from '../render_message';
export interface OneBotMessageProps { export interface OneBotMessageProps {
data: OB11Message data: OB11Message;
} }
export interface OneBotMessageGroupProps { export interface OneBotMessageGroupProps {
data: OB11GroupMessage data: OB11GroupMessage;
} }
export interface OneBotMessagePrivateProps { export interface OneBotMessagePrivateProps {
data: OB11PrivateMessage data: OB11PrivateMessage;
} }
const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => { const MessageContent: React.FC<{ data: OB11Message; }> = ({ data }) => {
return ( return (
<div className='h-full flex flex-col overflow-hidden flex-1'> <div className='h-full flex flex-col overflow-hidden flex-1'>
<div className='flex gap-2 items-center flex-shrink-0'> <div className='flex gap-2 items-center flex-shrink-0'>
@ -35,8 +35,8 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
<span <span
className={clsx( className={clsx(
isOB11GroupMessage(data) && isOB11GroupMessage(data) &&
data.sender.card && data.sender.card &&
'text-default-400 font-normal' 'text-default-400 font-normal'
)} )}
> >
{data.sender.nickname} {data.sender.nickname}
@ -73,7 +73,7 @@ const OneBotMessageGroup: React.FC<OneBotMessageGroupProps> = ({ data }) => {
<div className='h-full overflow-hidden flex flex-col w-full'> <div className='h-full overflow-hidden flex flex-col w-full'>
<div className='flex items-center p-1 flex-shrink-0'> <div className='flex items-center p-1 flex-shrink-0'>
<Avatar <Avatar
src={`https://p.qlogo.cn/gh/${data.group_id}/${data.group_id}/640/`} src={`https://p.qlogo.cn/gh/${data.group_id}/${data.group_id}/0/`}
alt='群头像' alt='群头像'
size='sm' size='sm'
className='flex-shrink-0 mr-2' className='flex-shrink-0 mr-2'

View File

@ -48,7 +48,7 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
<Image <Image
src={ src={
data?.avatarUrl ?? data?.avatarUrl ??
`https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=1` `https://q1.qlogo.cn/g?b=qq&nk=${data?.uin}&s=0`
} }
className='shadow-sm rounded-full w-14 aspect-square ring-2 ring-white/50 dark:ring-white/10' className='shadow-sm rounded-full w-14 aspect-square ring-2 ring-white/50 dark:ring-white/10'
/> />
@ -63,13 +63,15 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
<div className={clsx( <div className={clsx(
'text-xl font-bold truncate mb-0.5', 'text-xl font-bold truncate mb-0.5',
hasBackground ? 'text-white drop-shadow-sm' : 'text-default-800 dark:text-gray-100' hasBackground ? 'text-white drop-shadow-sm' : 'text-default-800 dark:text-gray-100'
)}> )}
>
{data?.nick || '未知用户'} {data?.nick || '未知用户'}
</div> </div>
<div className={clsx( <div className={clsx(
'font-mono text-xs tracking-wider', 'font-mono text-xs tracking-wider',
hasBackground ? 'text-white/80' : 'text-default-500 opacity-80' hasBackground ? 'text-white/80' : 'text-default-500 opacity-80'
)}> )}
>
{data?.uin || 'Unknown'} {data?.uin || 'Unknown'}
</div> </div>
</div> </div>

View File

@ -7,17 +7,17 @@ import { IoMdRefresh } from 'react-icons/io';
import { isQQQuickNewItem } from '@/utils/qq'; import { isQQQuickNewItem } from '@/utils/qq';
export interface QQItem { export interface QQItem {
uin: string uin: string;
} }
interface QuickLoginProps { interface QuickLoginProps {
qqList: (QQItem | LoginListItem)[] qqList: (QQItem | LoginListItem)[];
refresh: boolean refresh: boolean;
isLoading: boolean isLoading: boolean;
selectedQQ: string selectedQQ: string;
onUpdateQQList: () => void onUpdateQQList: () => void;
handleSelectionChange: React.ChangeEventHandler<HTMLSelectElement> handleSelectionChange: React.ChangeEventHandler<HTMLSelectElement>;
onSubmit: () => void onSubmit: () => void;
} }
const QuickLogin: React.FC<QuickLoginProps> = ({ const QuickLogin: React.FC<QuickLoginProps> = ({

View File

@ -1,5 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { Tooltip } from '@heroui/tooltip';
import { useTheme } from '@/hooks/use-theme'; import { useTheme } from '@/hooks/use-theme';
@ -18,9 +19,13 @@ const UsagePie: React.FC<UsagePieProps> = ({
}) => { }) => {
const { theme } = useTheme(); const { theme } = useTheme();
// Ensure values are clean // Ensure values are clean and consistent
const cleanSystem = Math.min(Math.max(systemUsage || 0, 0), 100); // Process usage cannot exceed system usage, and system usage cannot be less than process usage.
const cleanProcess = Math.min(Math.max(processUsage || 0, 0), cleanSystem); const rawSystem = Math.max(systemUsage || 0, 0);
const rawProcess = Math.max(processUsage || 0, 0);
const cleanSystem = Math.min(Math.max(rawSystem, rawProcess), 100);
const cleanProcess = Math.min(rawProcess, cleanSystem);
// SVG Config // SVG Config
const size = 100; const size = 100;
@ -47,75 +52,102 @@ const UsagePie: React.FC<UsagePieProps> = ({
return `${(cleanProcess / 100) * circumference} ${circumference}`; return `${(cleanProcess / 100) * circumference} ${circumference}`;
}, [cleanProcess, circumference]); }, [cleanProcess, circumference]);
return ( // 计算其他进程占用(系统总占用 - QQ占用
<div className="relative w-36 h-36 flex items-center justify-center"> const otherUsage = Math.max(cleanSystem - cleanProcess, 0);
<svg
className="w-full h-full -rotate-90"
viewBox={`0 0 ${size} ${size}`}
>
{/* Track / Free Space */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke={colors.track}
strokeWidth={strokeWidth}
strokeLinecap="round"
/>
{/* System Usage (Background for QQ) - effectively "Others" + "QQ" */} // Tooltip 内容
<circle const tooltipContent = (
cx={center} <div className='flex flex-col gap-1 p-1 text-xs'>
cy={center} <div className='flex items-center gap-2'>
r={radius} <span className='w-2 h-2 rounded-full' style={{ backgroundColor: colors.qq }} />
fill="none" <span>QQ进程: {cleanProcess.toFixed(1)}%</span>
stroke={colors.other} </div>
strokeWidth={strokeWidth} <div className='flex items-center gap-2'>
strokeLinecap="round" <span className='w-2 h-2 rounded-full' style={{ backgroundColor: colors.other }} />
strokeDasharray={systemDash} <span>: {otherUsage.toFixed(1)}%</span>
className="transition-all duration-700 ease-out" </div>
/> <div className='flex items-center gap-2'>
<span className='w-2 h-2 rounded-full' style={{ backgroundColor: colors.track }} />
{/* QQ Usage - Layered on top */} <span>: {(100 - cleanSystem).toFixed(1)}%</span>
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke={colors.qq}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={processDash}
className="transition-all duration-700 ease-out"
/>
</svg>
{/* Center Content */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none">
{title && (
<span className={clsx(
"text-[10px] font-medium mb-0.5 opacity-80 uppercase tracking-widest scale-90",
hasBackground ? 'text-white/80' : 'text-default-500 dark:text-default-400'
)}>
{title}
</span>
)}
<div className="flex items-baseline gap-0.5">
<span className={clsx(
"text-2xl font-bold font-mono tracking-tight",
hasBackground ? 'text-white' : 'text-default-900 dark:text-white'
)}>
{Math.round(cleanSystem)}
</span>
<span className={clsx(
"text-xs font-bold",
hasBackground ? 'text-white/60' : 'text-default-400 dark:text-default-500'
)}>%</span>
</div>
</div> </div>
</div> </div>
); );
return (
<Tooltip content={tooltipContent} placement='top'>
<div className='relative w-36 h-36 flex items-center justify-center cursor-pointer'>
<svg
className='w-full h-full -rotate-90'
viewBox={`0 0 ${size} ${size}`}
>
{/* Track / Free Space */}
<circle
cx={center}
cy={center}
r={radius}
fill='none'
stroke={colors.track}
strokeWidth={strokeWidth}
strokeLinecap='round'
/>
{/* System Usage (Background for QQ) - effectively "Others" + "QQ" */}
<circle
cx={center}
cy={center}
r={radius}
fill='none'
stroke={colors.other}
strokeWidth={strokeWidth}
strokeLinecap='round'
strokeDasharray={systemDash}
className='transition-all duration-700 ease-out'
/>
{/* QQ Usage - Layered on top */}
<circle
cx={center}
cy={center}
r={radius}
fill='none'
stroke={colors.qq}
strokeWidth={strokeWidth}
strokeLinecap='round'
strokeDasharray={processDash}
className='transition-all duration-700 ease-out'
/>
</svg>
{/* Center Content */}
<div className='absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none'>
{title && (
<span className={clsx(
'text-[10px] font-medium mb-0.5 opacity-80 uppercase tracking-widest scale-90',
hasBackground ? 'text-white/80' : 'text-default-500 dark:text-default-400'
)}
>
{title}
</span>
)}
<div className='flex items-baseline gap-0.5'>
<span className={clsx(
'text-2xl font-bold font-mono tracking-tight',
hasBackground ? 'text-white' : 'text-default-900 dark:text-white'
)}
>
{Math.round(cleanSystem)}
</span>
<span className={clsx(
'text-xs font-bold',
hasBackground ? 'text-white/60' : 'text-default-400 dark:text-default-500'
)}
>%
</span>
</div>
</div>
</div>
</Tooltip>
);
}; };
export default UsagePie; export default UsagePie;

View File

@ -10,9 +10,8 @@ import { Controller, useForm, useWatch } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { FaFont, FaUserAstronaut, FaCheck } from 'react-icons/fa'; import { FaFont, FaUserAstronaut, FaCheck } from 'react-icons/fa';
import { FaPaintbrush } from 'react-icons/fa6'; import { FaPaintbrush } from 'react-icons/fa6';
import { IoIosColorPalette } from 'react-icons/io'; import { IoIosColorPalette, IoMdRefresh } from 'react-icons/io';
import { MdDarkMode, MdLightMode } from 'react-icons/md'; import { MdDarkMode, MdLightMode } from 'react-icons/md';
import { IoMdRefresh } from 'react-icons/io';
import themes from '@/const/themes'; import themes from '@/const/themes';
@ -77,8 +76,8 @@ function PreviewThemeCard ({ theme, onPreview, isSelected }: PreviewThemeCardPro
)} )}
> >
{isSelected && ( {isSelected && (
<div className="absolute top-1 right-1 z-10"> <div className='absolute top-1 right-1 z-10'>
<Chip size="sm" color="primary" variant="solid"> <Chip size='sm' color='primary' variant='solid'>
<FaCheck size={10} /> <FaCheck size={10} />
</Chip> </Chip>
</div> </div>
@ -91,20 +90,20 @@ function PreviewThemeCard ({ theme, onPreview, isSelected }: PreviewThemeCardPro
<FaUserAstronaut /> <FaUserAstronaut />
{theme.author ?? '未知'} {theme.author ?? '未知'}
</div> </div>
<div className='text-xs text-primary-200'>{theme.description}</div> <div className='text-xs text-primary-200 whitespace-nowrap overflow-hidden text-ellipsis w-full'>{theme.description}</div>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
{colors.map((color) => ( {colors.map((color) => (
<div className='flex gap-1 items-center flex-wrap' key={color}> <div className='flex gap-1 items-center flex-nowrap' key={color}>
<div className='text-xs w-4 text-right'> <div className='text-xs w-4 text-right flex-shrink-0'>
{color[0].toUpperCase()} {color[0].toUpperCase()}
</div> </div>
{values.map((value) => ( {values.map((value) => (
<div <div
key={value} key={value}
className={clsx( className={clsx(
'w-2 h-2 rounded-full shadow-small', 'w-2 h-2 rounded-full shadow-small flex-shrink-0',
`bg-${color}${value}` `bg-${color}${value}`
)} )}
/> />
@ -135,9 +134,9 @@ const isThemeColorsEqual = (a: ThemeConfig, b: ThemeConfig): boolean => {
// 字体模式显示名称映射 // 字体模式显示名称映射
const fontModeNames: Record<string, string> = { const fontModeNames: Record<string, string> = {
'aacute': 'Aa 偷吃可爱长大的', aacute: 'Aa 偷吃可爱长大的',
'system': '系统默认', system: '系统默认',
'custom': '自定义字体', custom: '自定义字体',
}; };
const ThemeConfigCard = () => { const ThemeConfigCard = () => {
@ -169,11 +168,16 @@ const ThemeConfigCard = () => {
const originalDataRef = useRef<ThemeConfig | null>(null); const originalDataRef = useRef<ThemeConfig | null>(null);
// 在组件挂载时创建 style 标签,并在卸载时清理 // 在组件挂载时创建 style 标签,并在卸载时清理
// 同时在卸载时恢复字体到已保存的状态(避免"伪自动保存"问题)
useEffect(() => { useEffect(() => {
const styleTag = document.createElement('style'); const styleTag = document.createElement('style');
document.head.appendChild(styleTag); document.head.appendChild(styleTag);
styleTagRef.current = styleTag; styleTagRef.current = styleTag;
return () => { return () => {
// 组件卸载时,恢复到已保存的字体设置
if (originalDataRef.current?.fontMode) {
applyFont(originalDataRef.current.fontMode);
}
if (styleTagRef.current) { if (styleTagRef.current) {
document.head.removeChild(styleTagRef.current); document.head.removeChild(styleTagRef.current);
} }
@ -259,14 +263,12 @@ const ThemeConfigCard = () => {
const savedThemeName = useMemo(() => { const savedThemeName = useMemo(() => {
if (!originalDataRef.current) return null; if (!originalDataRef.current) return null;
return themes.find(t => isThemeColorsEqual(t.theme, originalDataRef.current!))?.name || '自定义'; return themes.find(t => isThemeColorsEqual(t.theme, originalDataRef.current!))?.name || '自定义';
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataLoaded, hasUnsavedChanges]); }, [dataLoaded, hasUnsavedChanges]);
// 已保存的字体模式显示名称 // 已保存的字体模式显示名称
const savedFontModeDisplayName = useMemo(() => { const savedFontModeDisplayName = useMemo(() => {
const mode = originalDataRef.current?.fontMode || 'aacute'; const mode = originalDataRef.current?.fontMode || 'aacute';
return fontModeNames[mode] || mode; return fontModeNames[mode] || mode;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataLoaded, hasUnsavedChanges]); }, [dataLoaded, hasUnsavedChanges]);
if (loading) return <PageLoading loading />; if (loading) return <PageLoading loading />;
@ -282,33 +284,33 @@ const ThemeConfigCard = () => {
<title> - NapCat WebUI</title> <title> - NapCat WebUI</title>
{/* 顶部操作栏 */} {/* 顶部操作栏 */}
<div className="sticky top-0 z-20 bg-background/80 backdrop-blur-md border-b border-divider"> <div className='sticky top-0 z-20 bg-background/80 backdrop-blur-md border-b border-divider'>
<div className="flex items-center justify-between p-4"> <div className='flex items-center justify-between p-4'>
<div className="flex items-center gap-3 flex-wrap"> <div className='flex items-center gap-3 flex-wrap'>
<div className="flex items-center gap-2 text-sm"> <div className='flex items-center gap-2 text-sm'>
<span className="text-default-400">:</span> <span className='text-default-400'>:</span>
<Chip size="sm" color="primary" variant="flat"> <Chip size='sm' color='primary' variant='flat'>
{savedThemeName || '加载中...'} {savedThemeName || '加载中...'}
</Chip> </Chip>
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className='flex items-center gap-2 text-sm'>
<span className="text-default-400">:</span> <span className='text-default-400'>:</span>
<Chip size="sm" color="secondary" variant="flat"> <Chip size='sm' color='secondary' variant='flat'>
{savedFontModeDisplayName} {savedFontModeDisplayName}
</Chip> </Chip>
</div> </div>
{hasUnsavedChanges && ( {hasUnsavedChanges && (
<Chip size="sm" color="warning" variant="solid"> <Chip size='sm' color='warning' variant='solid'>
</Chip> </Chip>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className='flex items-center gap-2'>
<Button <Button
size="sm" size='sm'
radius="full" radius='full'
variant="flat" variant='flat'
className="font-medium bg-default-100 text-default-600 dark:bg-default-50/50" className='font-medium bg-default-100 text-default-600 dark:bg-default-50/50'
onPress={() => { onPress={() => {
reset(); reset();
toast.success('已重置'); toast.success('已重置');
@ -318,10 +320,10 @@ const ThemeConfigCard = () => {
</Button> </Button>
<Button <Button
size="sm" size='sm'
color='primary' color='primary'
radius="full" radius='full'
className="font-medium shadow-md shadow-primary/20" className='font-medium shadow-md shadow-primary/20'
isLoading={isSubmitting} isLoading={isSubmitting}
onPress={() => onSubmit()} onPress={() => onSubmit()}
isDisabled={!hasUnsavedChanges} isDisabled={!hasUnsavedChanges}
@ -329,11 +331,11 @@ const ThemeConfigCard = () => {
</Button> </Button>
<Button <Button
size="sm" size='sm'
isIconOnly isIconOnly
radius='full' radius='full'
variant='flat' variant='flat'
className="text-default-500 bg-default-100 dark:bg-default-50/50" className='text-default-500 bg-default-100 dark:bg-default-50/50'
onPress={onRefresh} onPress={onRefresh}
> >
<IoMdRefresh size={18} /> <IoMdRefresh size={18} />
@ -342,7 +344,7 @@ const ThemeConfigCard = () => {
</div> </div>
</div> </div>
<div className="p-4"> <div className='p-4'>
<Accordion variant='splitted' defaultExpandedKeys={['font', 'select']}> <Accordion variant='splitted' defaultExpandedKeys={['font', 'select']}>
<AccordionItem <AccordionItem
key='font' key='font'
@ -355,18 +357,18 @@ const ThemeConfigCard = () => {
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
<Controller <Controller
control={control} control={control}
name="theme.fontMode" name='theme.fontMode'
render={({ field }) => ( render={({ field }) => (
<Select <Select
label="字体预设" label='字体预设'
selectedKeys={field.value ? [field.value] : ['aacute']} selectedKeys={field.value ? [field.value] : ['aacute']}
onChange={(e) => field.onChange(e.target.value)} onChange={(e) => field.onChange(e.target.value)}
className="max-w-xs" className='max-w-xs'
disallowEmptySelection disallowEmptySelection
> >
<SelectItem key="aacute">Aa </SelectItem> <SelectItem key='aacute'>Aa </SelectItem>
<SelectItem key="system"></SelectItem> <SelectItem key='system'></SelectItem>
<SelectItem key="custom"></SelectItem> <SelectItem key='custom'></SelectItem>
</Select> </Select>
)} )}
/> />

View File

@ -337,7 +337,7 @@ export default function FileManagerPage () {
return ( return (
<div className='h-full flex flex-col relative gap-4 w-full p-2 md:p-4'> <div className='h-full flex flex-col relative gap-4 w-full p-2 md:p-4'>
<div className={clsx( <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', '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-sm transition-colors',
hasBackground hasBackground
? 'bg-white/20 dark:bg-black/10 border border-white/40 dark:border-white/10' ? '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' : 'bg-white/60 dark:bg-black/40 border border-white/40 dark:border-white/10'
@ -345,6 +345,7 @@ export default function FileManagerPage () {
> >
<div className='flex items-center gap-2 overflow-x-auto hide-scrollbar pb-1 md:pb-0'> <div className='flex items-center gap-2 overflow-x-auto hide-scrollbar pb-1 md:pb-0'>
<Button <Button
radius='sm'
color='primary' color='primary'
size='sm' size='sm'
isIconOnly isIconOnly
@ -356,6 +357,7 @@ export default function FileManagerPage () {
</Button> </Button>
<Button <Button
radius='sm'
color='primary' color='primary'
size='sm' size='sm'
isIconOnly isIconOnly
@ -367,6 +369,7 @@ export default function FileManagerPage () {
</Button> </Button>
<Button <Button
radius='sm'
color='primary' color='primary'
isLoading={loading} isLoading={loading}
size='sm' size='sm'
@ -378,6 +381,7 @@ export default function FileManagerPage () {
<MdRefresh /> <MdRefresh />
</Button> </Button>
<Button <Button
radius='sm'
color='primary' color='primary'
size='sm' size='sm'
isIconOnly isIconOnly
@ -392,6 +396,7 @@ export default function FileManagerPage () {
selectedFiles === 'all') && ( selectedFiles === 'all') && (
<> <>
<Button <Button
radius='sm'
color='primary' color='primary'
size='sm' size='sm'
variant='flat' variant='flat'
@ -404,6 +409,7 @@ export default function FileManagerPage () {
) )
</Button> </Button>
<Button <Button
radius='sm'
color='primary' color='primary'
size='sm' size='sm'
variant='flat' variant='flat'
@ -419,6 +425,7 @@ export default function FileManagerPage () {
) )
</Button> </Button>
<Button <Button
radius='sm'
color='primary' color='primary'
size='sm' size='sm'
variant='flat' variant='flat'
@ -435,7 +442,10 @@ export default function FileManagerPage () {
</div> </div>
<div className='flex flex-col md:flex-row flex-1 gap-2 overflow-hidden items-stretch md:items-center'> <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'> <Breadcrumbs
radius='sm'
className='flex-1 bg-white/40 dark:bg-black/20 backdrop-blur-md shadow-sm border border-white/20 px-2 py-2 rounded-sm overflow-x-auto hide-scrollbar whitespace-nowrap'
>
{currentPath.split('/').map((part, index, parts) => ( {currentPath.split('/').map((part, index, parts) => (
<BreadcrumbItem <BreadcrumbItem
key={part} key={part}
@ -450,6 +460,7 @@ export default function FileManagerPage () {
))} ))}
</Breadcrumbs> </Breadcrumbs>
<Input <Input
radius='sm'
type='text' type='text'
placeholder='输入跳转路径' placeholder='输入跳转路径'
value={jumpPath} value={jumpPath}
@ -472,7 +483,7 @@ export default function FileManagerPage () {
animate={{ height: showUpload ? 'auto' : 0 }} animate={{ height: showUpload ? 'auto' : 0 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className={clsx( className={clsx(
'border-dashed rounded-lg text-center overflow-hidden', 'border-dashed rounded-sm text-center overflow-hidden',
isDragActive ? 'border-primary bg-primary/10' : 'border-default-300', isDragActive ? 'border-primary bg-primary/10' : 'border-default-300',
showUpload ? 'mb-4 border-2' : 'border-none' showUpload ? 'mb-4 border-2' : 'border-none'
)} )}
@ -486,7 +497,7 @@ export default function FileManagerPage () {
<div className='flex flex-col items-center gap-2'> <div className='flex flex-col items-center gap-2'>
<FiUpload className='text-3xl text-primary' /> <FiUpload className='text-3xl text-primary' />
<p className='text-default-600'></p> <p className='text-default-600'></p>
<Button color='primary' size='sm' variant='flat' onPress={open}> <Button radius='sm' color='primary' size='sm' variant='flat' onPress={open}>
</Button> </Button>
</div> </div>

View File

@ -102,7 +102,7 @@ const DashboardIndexPage: React.FC = () => {
return ( return (
<> <>
<title> - NapCat WebUI</title> <title> - NapCat WebUI</title>
<section className='w-full p-2 md:p-4 md:max-w-[1000px] mx-auto'> <section className='w-full p-2 md:p-4 md:max-w-[1000px] mx-auto overflow-hidden'>
<div className='grid grid-cols-1 lg:grid-cols-3 gap-4 items-stretch'> <div className='grid grid-cols-1 lg:grid-cols-3 gap-4 items-stretch'>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<QQInfo /> <QQInfo />
@ -112,10 +112,11 @@ const DashboardIndexPage: React.FC = () => {
</div> </div>
<Networks /> <Networks />
<Card className={clsx( <Card className={clsx(
'backdrop-blur-sm border border-white/40 dark:border-white/10 shadow-sm transition-all', 'backdrop-blur-sm border border-white/40 dark:border-white/10 shadow-sm transition-all overflow-hidden',
hasBackground ? 'bg-white/10 dark:bg-black/10' : 'bg-white/60 dark:bg-black/40' hasBackground ? 'bg-white/10 dark:bg-black/10' : 'bg-white/60 dark:bg-black/40'
)}> )}
<CardBody> >
<CardBody className='overflow-hidden'>
<Hitokoto /> <Hitokoto />
</CardBody> </CardBody>
</Card> </Card>

View File

@ -158,7 +158,7 @@ export default function TerminalPage () {
variant='flat' variant='flat'
onPress={createNewTerminal} onPress={createNewTerminal}
startContent={<IoAdd />} startContent={<IoAdd />}
className='text-xl' className='text-xl ml-auto'
/> />
</div> </div>
<div className='flex-grow overflow-hidden'> <div className='flex-grow overflow-hidden'>