mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-15 12:50:33 +00:00
feat: 优化webui界面和文件管理器 (#1472)
This commit is contained in:
parent
4e37b002f9
commit
ce9482f19d
@ -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);
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
VITE_DEBUG_BACKEND_URL="http://127.0.0.1:6099"
|
VITE_DEBUG_BACKEND_URL="http://127.0.0.1:6099"
|
||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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> = ({
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user