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>[] {
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>) => {
return acc ?? msg.buildContent();
}, undefined);

View File

@ -1 +1 @@
VITE_DEBUG_BACKEND_URL="http://127.0.0.1:6099"
VITE_DEBUG_BACKEND_URL="http://127.0.0.1:6099"

View File

@ -37,6 +37,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
onEnable,
onDelete,
onEnableDebug,
showType,
}: NetworkDisplayCardProps<T>) => {
const { name, enable, debug } = data;
const [editing, setEditing] = useState(false);
@ -60,15 +61,16 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
return (
<DisplayCardContainer
className="w-full max-w-[420px]"
className='w-full max-w-[420px]'
tag={showType ? typeLabel : undefined}
action={
<div className="flex gap-2 w-full">
<div className='flex gap-2 w-full'>
<Button
fullWidth
radius='full'
size='sm'
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} />}
onPress={onEdit}
isDisabled={editing}
@ -82,10 +84,10 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
size='sm'
variant='flat'
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
? "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-secondary/20 hover:text-secondary data-[hover=true]:text-secondary'
: 'hover:bg-success/20 hover:text-success data-[hover=true]:text-success'
)}
startContent={<CgDebug size={16} />}
onPress={handleEnableDebug}
@ -113,11 +115,11 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
isSelected={enable}
onChange={handleEnable}
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'>
{(() => {
@ -125,29 +127,30 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
if (targetFullField) {
// 模式1存在全宽字段如URL布局为
// Row 1: 名称 (全宽)
// Row 1: 类型 (全宽)
// Row 2: 全宽字段 (全宽)
return (
<>
<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'
>
<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">
{name}
<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'>
{typeLabel}
</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'
>
<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.value)
: (
<span className={clsx(
typeof targetFullField.value === 'string' && (targetFullField.value.startsWith('http') || targetFullField.value.includes('.') || targetFullField.value.includes(':')) ? 'font-mono' : ''
)}>
)}
>
{String(targetFullField.value)}
</span>
)}
@ -157,7 +160,7 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
);
} else {
// 模式2无全宽字段布局为 4 个小块 (2行 x 2列)
// Row 1: 名称 | Field 0
// Row 1: 类型 | Field 0
// Row 2: Field 1 | Field 2
const displayFields = fields.slice(0, 3);
return (
@ -165,9 +168,9 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
<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'
>
<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">
{name}
<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'>
{typeLabel}
</div>
</div>
{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'
>
<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.value)
@ -184,7 +187,8 @@ const NetworkDisplayCard = <T extends keyof NetworkType> ({
: (
<span className={clsx(
typeof field.value === 'string' && (field.value.startsWith('http') || field.value.includes('.') || field.value.includes(':')) ? 'font-mono' : ''
)}>
)}
>
{String(field.value)}
</span>
)}

View File

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

View File

@ -63,11 +63,11 @@ export default function FileEditModal ({
};
return (
<Modal size='full' isOpen={isOpen} onClose={onClose}>
<Modal radius='sm' size='full' isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalHeader className='flex items-center gap-2 border-b border-default-200/50'>
<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">
<span className="px-1 py-0.5 rounded border border-default-300 bg-default-100">Ctrl/Cmd + S</span>
</div>
@ -89,10 +89,10 @@ export default function FileEditModal ({
</div>
</ModalBody>
<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 color='primary' onPress={onSave}>
<Button radius='sm' color='primary' onPress={onSave}>
</Button>
</ModalFooter>

View File

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

View File

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

View File

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

View File

@ -83,15 +83,16 @@ function DirectoryTree ({
return (
<div className='ml-4'>
<Button
radius='sm'
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'
color='primary'
variant={variant}
startContent={
<div
className={clsx(
'rounded-md',
'rounded-sm',
isSeleted ? 'bg-primary-600' : 'bg-primary-50'
)}
>
@ -140,11 +141,11 @@ export default function MoveModal ({
onSelect,
}: MoveModalProps) {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal radius='sm' isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalHeader></ModalHeader>
<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
basePath='/'
onSelect={onSelect}
@ -157,10 +158,10 @@ export default function MoveModal ({
<p className='text-sm text-default-500'>{selectionInfo}</p>
</ModalBody>
<ModalFooter>
<Button color='primary' variant='flat' onPress={onClose}>
<Button radius='sm' color='primary' variant='flat' onPress={onClose}>
</Button>
<Button color='primary' onPress={onMove}>
<Button radius='sm' color='primary' onPress={onMove}>
</Button>
</ModalFooter>

View File

@ -9,11 +9,11 @@ import {
} from '@heroui/modal';
interface RenameModalProps {
isOpen: boolean
newFileName: string
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void
onClose: () => void
onRename: () => void
isOpen: boolean;
newFileName: string;
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClose: () => void;
onRename: () => void;
}
export default function RenameModal ({
@ -24,17 +24,17 @@ export default function RenameModal ({
onRename,
}: RenameModalProps) {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal radius='sm' isOpen={isOpen} onClose={onClose}>
<ModalContent>
<ModalHeader></ModalHeader>
<ModalBody>
<Input label='新名称' value={newFileName} onChange={onNameChange} />
<Input radius='sm' label='新名称' value={newFileName} onChange={onNameChange} />
</ModalBody>
<ModalFooter>
<Button color='primary' variant='flat' onPress={onClose}>
<Button radius='sm' color='primary' variant='flat' onPress={onClose}>
</Button>
<Button color='primary' onPress={onRename}>
<Button radius='sm' color='primary' onPress={onRename}>
</Button>
</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 { Tooltip } from '@heroui/tooltip';
import { useLocalStorage } from '@uidotdev/usehooks';
@ -40,30 +42,36 @@ export default function Hitokoto () {
}
};
return (
<div>
<div className='relative flex flex-col items-center justify-center p-6 min-h-[120px]'>
<div className='overflow-hidden'>
<div className='relative flex flex-col items-center justify-center p-4 md:p-6'>
{loading && !data && <PageLoading />}
{data && (
<>
<IoMdQuote className={clsx(
"text-4xl mb-4",
hasBackground ? "text-white/30" : "text-primary/20"
)} />
'text-4xl mb-4',
hasBackground ? 'text-white/30' : 'text-primary/20'
)}
/>
<div className={clsx(
"text-xl font-medium tracking-wide leading-relaxed italic",
hasBackground ? "text-white drop-shadow-sm" : "text-default-700 dark:text-gray-200"
)}>
'text-xl font-medium tracking-wide leading-relaxed italic',
hasBackground ? 'text-white drop-shadow-sm' : 'text-default-700 dark:text-gray-200'
)}
>
" {data?.hitokoto} "
</div>
<div className='mt-4 flex flex-col items-center text-sm'>
<span className={clsx(
'font-bold',
hasBackground ? 'text-white/90' : 'text-primary-500/80'
)}> {data?.from}</span>
)}
> {data?.from}
</span>
{data?.from_who && <span className={clsx(
"text-xs mt-1",
hasBackground ? "text-white/70" : "text-default-400"
)}>{data?.from_who}</span>}
'text-xs mt-1',
hasBackground ? 'text-white/70' : 'text-default-400'
)}
> {data?.from_who}
</span>}
</div>
</>
)}
@ -72,8 +80,8 @@ export default function Hitokoto () {
<Tooltip content='刷新' placement='top'>
<Button
className={clsx(
"transition-colors",
hasBackground ? "text-white/60 hover:text-white" : "text-default-400 hover:text-primary"
'transition-colors',
hasBackground ? 'text-white/60 hover:text-white' : 'text-default-400 hover:text-primary'
)}
onPress={run}
size='sm'
@ -88,8 +96,8 @@ export default function Hitokoto () {
<Tooltip content='复制' placement='top'>
<Button
className={clsx(
"transition-colors",
hasBackground ? "text-white/60 hover:text-white" : "text-default-400 hover:text-success"
'transition-colors',
hasBackground ? 'text-white/60 hover:text-white' : 'text-default-400 hover:text-success'
)}
onPress={onCopy}
size='sm'

View File

@ -13,18 +13,18 @@ import type {
import { renderMessageContent } from '../render_message';
export interface OneBotMessageProps {
data: OB11Message
data: OB11Message;
}
export interface OneBotMessageGroupProps {
data: OB11GroupMessage
data: OB11GroupMessage;
}
export interface OneBotMessagePrivateProps {
data: OB11PrivateMessage
data: OB11PrivateMessage;
}
const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
const MessageContent: React.FC<{ data: OB11Message; }> = ({ data }) => {
return (
<div className='h-full flex flex-col overflow-hidden flex-1'>
<div className='flex gap-2 items-center flex-shrink-0'>
@ -35,8 +35,8 @@ const MessageContent: React.FC<{ data: OB11Message }> = ({ data }) => {
<span
className={clsx(
isOB11GroupMessage(data) &&
data.sender.card &&
'text-default-400 font-normal'
data.sender.card &&
'text-default-400 font-normal'
)}
>
{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='flex items-center p-1 flex-shrink-0'>
<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='群头像'
size='sm'
className='flex-shrink-0 mr-2'

View File

@ -48,7 +48,7 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
<Image
src={
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'
/>
@ -63,13 +63,15 @@ const QQInfoCard: React.FC<QQInfoCardProps> = ({ data, error, loading }) => {
<div className={clsx(
'text-xl font-bold truncate mb-0.5',
hasBackground ? 'text-white drop-shadow-sm' : 'text-default-800 dark:text-gray-100'
)}>
)}
>
{data?.nick || '未知用户'}
</div>
<div className={clsx(
'font-mono text-xs tracking-wider',
hasBackground ? 'text-white/80' : 'text-default-500 opacity-80'
)}>
)}
>
{data?.uin || 'Unknown'}
</div>
</div>

View File

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

View File

@ -1,5 +1,6 @@
import React, { useMemo } from 'react';
import clsx from 'clsx';
import { Tooltip } from '@heroui/tooltip';
import { useTheme } from '@/hooks/use-theme';
@ -18,9 +19,13 @@ const UsagePie: React.FC<UsagePieProps> = ({
}) => {
const { theme } = useTheme();
// Ensure values are clean
const cleanSystem = Math.min(Math.max(systemUsage || 0, 0), 100);
const cleanProcess = Math.min(Math.max(processUsage || 0, 0), cleanSystem);
// Ensure values are clean and consistent
// Process usage cannot exceed system usage, and system usage cannot be less than process usage.
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
const size = 100;
@ -47,75 +52,102 @@ const UsagePie: React.FC<UsagePieProps> = ({
return `${(cleanProcess / 100) * circumference} ${circumference}`;
}, [cleanProcess, circumference]);
return (
<div className="relative w-36 h-36 flex items-center justify-center">
<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"
/>
// 计算其他进程占用(系统总占用 - QQ占用
const otherUsage = Math.max(cleanSystem - cleanProcess, 0);
{/* 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>
// Tooltip 内容
const tooltipContent = (
<div className='flex flex-col gap-1 p-1 text-xs'>
<div className='flex items-center gap-2'>
<span className='w-2 h-2 rounded-full' style={{ backgroundColor: colors.qq }} />
<span>QQ进程: {cleanProcess.toFixed(1)}%</span>
</div>
<div className='flex items-center gap-2'>
<span className='w-2 h-2 rounded-full' style={{ backgroundColor: colors.other }} />
<span>: {otherUsage.toFixed(1)}%</span>
</div>
<div className='flex items-center gap-2'>
<span className='w-2 h-2 rounded-full' style={{ backgroundColor: colors.track }} />
<span>: {(100 - cleanSystem).toFixed(1)}%</span>
</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;

View File

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

View File

@ -337,7 +337,7 @@ export default function FileManagerPage () {
return (
<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',
'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
? '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'
@ -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'>
<Button
radius='sm'
color='primary'
size='sm'
isIconOnly
@ -356,6 +357,7 @@ export default function FileManagerPage () {
</Button>
<Button
radius='sm'
color='primary'
size='sm'
isIconOnly
@ -367,6 +369,7 @@ export default function FileManagerPage () {
</Button>
<Button
radius='sm'
color='primary'
isLoading={loading}
size='sm'
@ -378,6 +381,7 @@ export default function FileManagerPage () {
<MdRefresh />
</Button>
<Button
radius='sm'
color='primary'
size='sm'
isIconOnly
@ -392,6 +396,7 @@ export default function FileManagerPage () {
selectedFiles === 'all') && (
<>
<Button
radius='sm'
color='primary'
size='sm'
variant='flat'
@ -404,6 +409,7 @@ export default function FileManagerPage () {
)
</Button>
<Button
radius='sm'
color='primary'
size='sm'
variant='flat'
@ -419,6 +425,7 @@ export default function FileManagerPage () {
)
</Button>
<Button
radius='sm'
color='primary'
size='sm'
variant='flat'
@ -435,7 +442,10 @@ export default function FileManagerPage () {
</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'>
<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) => (
<BreadcrumbItem
key={part}
@ -450,6 +460,7 @@ export default function FileManagerPage () {
))}
</Breadcrumbs>
<Input
radius='sm'
type='text'
placeholder='输入跳转路径'
value={jumpPath}
@ -472,7 +483,7 @@ export default function FileManagerPage () {
animate={{ height: showUpload ? 'auto' : 0 }}
transition={{ duration: 0.2 }}
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',
showUpload ? 'mb-4 border-2' : 'border-none'
)}
@ -486,7 +497,7 @@ export default function FileManagerPage () {
<div className='flex flex-col items-center gap-2'>
<FiUpload className='text-3xl text-primary' />
<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>
</div>

View File

@ -102,7 +102,7 @@ const DashboardIndexPage: React.FC = () => {
return (
<>
<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='flex flex-col gap-2'>
<QQInfo />
@ -112,10 +112,11 @@ const DashboardIndexPage: React.FC = () => {
</div>
<Networks />
<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'
)}>
<CardBody>
)}
>
<CardBody className='overflow-hidden'>
<Hitokoto />
</CardBody>
</Card>

View File

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