mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 08:10:25 +00:00
feat: 优化webui界面和文件管理器 (#1472)
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> = ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user