mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-02 00:30:25 +00:00
refactor: 整体重构 (#1381)
* feat: pnpm new * Refactor build and release workflows, update dependencies Switch build scripts and workflows from npm to pnpm, update build and artifact paths, and simplify release workflow by removing version detection and changelog steps. Add new dependencies (silk-wasm, express, ws, node-pty-prebuilt-multiarch), update exports in package.json files, and add vite config for napcat-framework. Also, rename manifest.json for framework package and fix static asset copying in shell build config.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { Button, ButtonGroup } from '@heroui/button';
|
||||
import { Input } from '@heroui/input';
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} 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
|
||||
}
|
||||
|
||||
export default function CreateFileModal ({
|
||||
isOpen,
|
||||
fileType,
|
||||
newFileName,
|
||||
onTypeChange,
|
||||
onNameChange,
|
||||
onClose,
|
||||
onCreate,
|
||||
}: CreateFileModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>新建</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<ButtonGroup color='primary'>
|
||||
<Button
|
||||
variant={fileType === 'file' ? 'solid' : 'flat'}
|
||||
onPress={() => onTypeChange('file')}
|
||||
>
|
||||
文件
|
||||
</Button>
|
||||
<Button
|
||||
variant={fileType === 'directory' ? 'solid' : 'flat'}
|
||||
onPress={() => onTypeChange('directory')}
|
||||
>
|
||||
目录
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Input label='名称' value={newFileName} onChange={onNameChange} />
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color='primary' variant='flat' onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color='primary' onPress={onCreate}>
|
||||
创建
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Code } from '@heroui/code';
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@heroui/modal';
|
||||
|
||||
import CodeEditor from '@/components/code_editor';
|
||||
|
||||
interface FileEditModalProps {
|
||||
isOpen: boolean
|
||||
file: { path: string; content: string } | null
|
||||
onClose: () => void
|
||||
onSave: () => void
|
||||
onContentChange: (newContent?: string) => void
|
||||
}
|
||||
|
||||
export default function FileEditModal ({
|
||||
isOpen,
|
||||
file,
|
||||
onClose,
|
||||
onSave,
|
||||
onContentChange,
|
||||
}: FileEditModalProps) {
|
||||
// 根据文件后缀返回对应语言
|
||||
const getLanguage = (filePath: string) => {
|
||||
if (filePath.endsWith('.js')) return 'javascript';
|
||||
if (filePath.endsWith('.ts')) return 'typescript';
|
||||
if (filePath.endsWith('.tsx')) return 'tsx';
|
||||
if (filePath.endsWith('.jsx')) return 'jsx';
|
||||
if (filePath.endsWith('.vue')) return 'vue';
|
||||
if (filePath.endsWith('.svelte')) return 'svelte';
|
||||
if (filePath.endsWith('.json')) return 'json';
|
||||
if (filePath.endsWith('.html')) return 'html';
|
||||
if (filePath.endsWith('.css')) return 'css';
|
||||
if (filePath.endsWith('.scss')) return 'scss';
|
||||
if (filePath.endsWith('.less')) return 'less';
|
||||
if (filePath.endsWith('.md')) return 'markdown';
|
||||
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) return 'yaml';
|
||||
if (filePath.endsWith('.xml')) return 'xml';
|
||||
if (filePath.endsWith('.sql')) return 'sql';
|
||||
if (filePath.endsWith('.sh')) return 'shell';
|
||||
if (filePath.endsWith('.bat')) return 'bat';
|
||||
if (filePath.endsWith('.php')) return 'php';
|
||||
if (filePath.endsWith('.java')) return 'java';
|
||||
if (filePath.endsWith('.c')) return 'c';
|
||||
if (filePath.endsWith('.cpp')) return 'cpp';
|
||||
if (filePath.endsWith('.h')) return 'h';
|
||||
if (filePath.endsWith('.hpp')) return 'hpp';
|
||||
if (filePath.endsWith('.go')) return 'go';
|
||||
if (filePath.endsWith('.py')) return 'python';
|
||||
if (filePath.endsWith('.rb')) return 'ruby';
|
||||
if (filePath.endsWith('.cs')) return 'csharp';
|
||||
if (filePath.endsWith('.swift')) return 'swift';
|
||||
if (filePath.endsWith('.vb')) return 'vb';
|
||||
if (filePath.endsWith('.lua')) return 'lua';
|
||||
if (filePath.endsWith('.pl')) return 'perl';
|
||||
if (filePath.endsWith('.r')) return 'r';
|
||||
return 'plaintext';
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size='full' isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader className='flex items-center gap-2 bg-content2 bg-opacity-50'>
|
||||
<span>编辑文件</span>
|
||||
<Code className='text-xs'>{file?.path}</Code>
|
||||
</ModalHeader>
|
||||
<ModalBody className='p-0'>
|
||||
<div className='h-full'>
|
||||
<CodeEditor
|
||||
height='100%'
|
||||
value={file?.content || ''}
|
||||
onChange={onContentChange}
|
||||
options={{ wordWrap: 'on' }}
|
||||
language={file?.path ? getLanguage(file.path) : 'plaintext'}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color='primary' variant='flat' onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color='primary' onPress={onSave}>
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@heroui/modal';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import { useRequest } from 'ahooks';
|
||||
import path from 'path-browserify';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import FileManager from '@/controllers/file_manager';
|
||||
|
||||
interface FilePreviewModalProps {
|
||||
isOpen: boolean
|
||||
filePath: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const videoExts = ['.mp4', '.webm'];
|
||||
export const audioExts = ['.mp3', '.wav'];
|
||||
|
||||
export const supportedPreviewExts = [...videoExts, ...audioExts];
|
||||
|
||||
export default function FilePreviewModal ({
|
||||
isOpen,
|
||||
filePath,
|
||||
onClose,
|
||||
}: FilePreviewModalProps) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const { data, loading, error, run } = useRequest(
|
||||
async () => FileManager.downloadToURL(filePath),
|
||||
{
|
||||
refreshDeps: [filePath],
|
||||
manual: true,
|
||||
refreshDepsAction: () => {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (!filePath || !supportedPreviewExts.includes(ext)) {
|
||||
return;
|
||||
}
|
||||
run();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (filePath) {
|
||||
run();
|
||||
}
|
||||
}, [filePath]);
|
||||
|
||||
let contentElement = null;
|
||||
if (!supportedPreviewExts.includes(ext)) {
|
||||
contentElement = <div>暂不支持预览此文件类型</div>;
|
||||
} else if (error) {
|
||||
contentElement = <div>读取文件失败</div>;
|
||||
} else if (loading || !data) {
|
||||
contentElement = (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
} else if (videoExts.includes(ext)) {
|
||||
contentElement = <video src={data} controls className='max-w-full' />;
|
||||
} else if (audioExts.includes(ext)) {
|
||||
contentElement = <audio src={data} controls className='w-full' />;
|
||||
} else {
|
||||
contentElement = (
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal 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>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import { Button, ButtonGroup } from '@heroui/button';
|
||||
import { Pagination } from '@heroui/pagination';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import {
|
||||
type Selection,
|
||||
type SortDescriptor,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@heroui/table';
|
||||
import path from 'path-browserify';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { BiRename } from 'react-icons/bi';
|
||||
import { FiCopy, FiDownload, FiMove, FiTrash2 } from 'react-icons/fi';
|
||||
import { PhotoSlider } from 'react-photo-view';
|
||||
|
||||
import FileIcon from '@/components/file_icon';
|
||||
|
||||
import type { FileInfo } from '@/controllers/file_manager';
|
||||
|
||||
import { supportedPreviewExts } from './file_preview_modal';
|
||||
import ImageNameButton, { PreviewImage, imageExts } from './image_name_button';
|
||||
|
||||
export interface FileTableProps {
|
||||
files: FileInfo[]
|
||||
currentPath: string
|
||||
loading: boolean
|
||||
sortDescriptor: SortDescriptor
|
||||
onSortChange: (descriptor: SortDescriptor) => void
|
||||
selectedFiles: Selection
|
||||
onSelectionChange: (selected: Selection) => void
|
||||
onDirectoryClick: (dirPath: string) => void
|
||||
onEdit: (filePath: string) => void
|
||||
onPreview: (filePath: string) => void
|
||||
onRenameRequest: (name: string) => void
|
||||
onMoveRequest: (name: string) => void
|
||||
onCopyPath: (fileName: string) => void
|
||||
onDelete: (filePath: string) => void
|
||||
onDownload: (filePath: string) => void
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
export default function FileTable ({
|
||||
files,
|
||||
currentPath,
|
||||
loading,
|
||||
sortDescriptor,
|
||||
onSortChange,
|
||||
selectedFiles,
|
||||
onSelectionChange,
|
||||
onDirectoryClick,
|
||||
onEdit,
|
||||
onPreview,
|
||||
onRenameRequest,
|
||||
onMoveRequest,
|
||||
onCopyPath,
|
||||
onDelete,
|
||||
onDownload,
|
||||
}: FileTableProps) {
|
||||
const [page, setPage] = useState(1);
|
||||
const pages = Math.ceil(files.length / PAGE_SIZE) || 1;
|
||||
const start = (page - 1) * PAGE_SIZE;
|
||||
const end = start + PAGE_SIZE;
|
||||
const displayFiles = files.slice(start, end);
|
||||
const [showImage, setShowImage] = useState(false);
|
||||
const [previewIndex, setPreviewIndex] = useState(0);
|
||||
const [previewImages, setPreviewImages] = useState<PreviewImage[]>([]);
|
||||
|
||||
const addPreviewImage = useCallback((image: PreviewImage) => {
|
||||
setPreviewImages((prev) => {
|
||||
const exists = prev.some((p) => p.key === image.key);
|
||||
if (exists) return prev;
|
||||
return [...prev, image];
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPreviewImages([]);
|
||||
setPreviewIndex(0);
|
||||
setShowImage(false);
|
||||
setPage(1);
|
||||
}, [currentPath]);
|
||||
|
||||
const onPreviewImage = (name: string, images: PreviewImage[]) => {
|
||||
const index = images.findIndex((image) => image.key === name);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
setPreviewIndex(index);
|
||||
setShowImage(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PhotoSlider
|
||||
images={previewImages}
|
||||
visible={showImage}
|
||||
onClose={() => setShowImage(false)}
|
||||
index={previewIndex}
|
||||
onIndexChange={setPreviewIndex}
|
||||
/>
|
||||
<Table
|
||||
aria-label='文件列表'
|
||||
sortDescriptor={sortDescriptor}
|
||||
onSortChange={onSortChange}
|
||||
onSelectionChange={onSelectionChange}
|
||||
defaultSelectedKeys={[]}
|
||||
selectedKeys={selectedFiles}
|
||||
selectionMode='multiple'
|
||||
bottomContent={
|
||||
<div className='flex w-full justify-center'>
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
color='primary'
|
||||
page={page}
|
||||
total={pages}
|
||||
onChange={(page) => setPage(page)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key='name' allowsSorting>
|
||||
名称
|
||||
</TableColumn>
|
||||
<TableColumn key='type' allowsSorting>
|
||||
类型
|
||||
</TableColumn>
|
||||
<TableColumn key='size' allowsSorting>
|
||||
大小
|
||||
</TableColumn>
|
||||
<TableColumn key='mtime' allowsSorting>
|
||||
修改时间
|
||||
</TableColumn>
|
||||
<TableColumn key='actions'>操作</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
isLoading={loading}
|
||||
loadingContent={
|
||||
<div className='flex justify-center items-center h-full'>
|
||||
<Spinner />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{displayFiles.map((file: FileInfo) => {
|
||||
const filePath = path.join(currentPath, file.name);
|
||||
const ext = path.extname(file.name).toLowerCase();
|
||||
const previewable = supportedPreviewExts.includes(ext);
|
||||
const images = previewImages;
|
||||
return (
|
||||
<TableRow key={file.name}>
|
||||
<TableCell>
|
||||
{imageExts.includes(ext)
|
||||
? (
|
||||
<ImageNameButton
|
||||
name={file.name}
|
||||
filePath={filePath}
|
||||
onPreview={() => onPreviewImage(file.name, images)}
|
||||
onAddPreview={addPreviewImage}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
variant='light'
|
||||
onPress={() =>
|
||||
file.isDirectory
|
||||
? onDirectoryClick(file.name)
|
||||
: previewable
|
||||
? onPreview(filePath)
|
||||
: onEdit(filePath)}
|
||||
className='text-left justify-start'
|
||||
startContent={
|
||||
<FileIcon
|
||||
name={file.name}
|
||||
isDirectory={file.isDirectory}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{file.name}
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{file.isDirectory ? '目录' : '文件'}</TableCell>
|
||||
<TableCell>
|
||||
{isNaN(file.size) || file.isDirectory
|
||||
? '-'
|
||||
: `${file.size} 字节`}
|
||||
</TableCell>
|
||||
<TableCell>{new Date(file.mtime).toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<ButtonGroup size='sm'>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
onPress={() => onRenameRequest(file.name)}
|
||||
>
|
||||
<BiRename />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
onPress={() => onMoveRequest(file.name)}
|
||||
>
|
||||
<FiMove />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
onPress={() => onCopyPath(file.name)}
|
||||
>
|
||||
<FiCopy />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
onPress={() => onDownload(filePath)}
|
||||
>
|
||||
<FiDownload />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
color='primary'
|
||||
variant='flat'
|
||||
onPress={() => onDelete(filePath)}
|
||||
>
|
||||
<FiTrash2 />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Image } from '@heroui/image';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import { useRequest } from 'ahooks';
|
||||
import path from 'path-browserify';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import FileManager from '@/controllers/file_manager';
|
||||
|
||||
import FileIcon from '../file_icon';
|
||||
|
||||
export interface PreviewImage {
|
||||
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
|
||||
}
|
||||
|
||||
export default function ImageNameButton ({
|
||||
name,
|
||||
filePath,
|
||||
onPreview,
|
||||
onAddPreview,
|
||||
}: ImageNameButtonProps) {
|
||||
const { data, loading, error, run } = useRequest(
|
||||
async () => FileManager.downloadToURL(filePath),
|
||||
{
|
||||
refreshDeps: [filePath],
|
||||
manual: true,
|
||||
refreshDepsAction: () => {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (!filePath || !imageExts.includes(ext)) {
|
||||
return;
|
||||
}
|
||||
run();
|
||||
},
|
||||
}
|
||||
);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
onAddPreview({
|
||||
key: name,
|
||||
src: data,
|
||||
alt: name,
|
||||
});
|
||||
}
|
||||
}, [data, name, onAddPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filePath) {
|
||||
run();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant='light'
|
||||
className='text-left justify-start'
|
||||
onPress={onPreview}
|
||||
startContent={
|
||||
error
|
||||
? (
|
||||
<FileIcon name={name} isDirectory={false} />
|
||||
)
|
||||
: loading || !data
|
||||
? (
|
||||
<Spinner size='sm' />
|
||||
)
|
||||
: (
|
||||
<Image
|
||||
src={data}
|
||||
alt={name}
|
||||
className='w-8 h-8 flex-shrink-0'
|
||||
classNames={{
|
||||
wrapper: 'w-8 h-8 flex-shrink-0',
|
||||
}}
|
||||
radius='sm'
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@heroui/modal';
|
||||
import { Spinner } from '@heroui/spinner';
|
||||
import clsx from 'clsx';
|
||||
import path from 'path-browserify';
|
||||
import { useState } from 'react';
|
||||
import { IoAdd, IoRemove } from 'react-icons/io5';
|
||||
|
||||
import FileManager from '@/controllers/file_manager';
|
||||
|
||||
interface MoveModalProps {
|
||||
isOpen: boolean;
|
||||
moveTargetPath: string;
|
||||
selectionInfo: string;
|
||||
onClose: () => void;
|
||||
onMove: () => void;
|
||||
onSelect: (dir: string) => void; // 新增回调
|
||||
}
|
||||
|
||||
// 将 DirectoryTree 改为递归组件
|
||||
// 新增 selectedPath 属性,用于标识当前选中的目录
|
||||
function DirectoryTree ({
|
||||
basePath,
|
||||
onSelect,
|
||||
selectedPath,
|
||||
}: {
|
||||
basePath: string;
|
||||
onSelect: (dir: string) => void;
|
||||
selectedPath?: string;
|
||||
}) {
|
||||
const [dirs, setDirs] = useState<string[]>([]);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
// 新增loading状态
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchDirectories = async () => {
|
||||
try {
|
||||
// 直接使用 basePath 调用接口,移除 process.platform 判断
|
||||
const list = await FileManager.listDirectories(basePath);
|
||||
setDirs(list.map((item) => item.name));
|
||||
} catch (_error) {
|
||||
// ...error handling...
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (!expanded) {
|
||||
setExpanded(true);
|
||||
setLoading(true);
|
||||
await fetchDirectories();
|
||||
setLoading(false);
|
||||
} else {
|
||||
setExpanded(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
onSelect(basePath);
|
||||
handleToggle();
|
||||
};
|
||||
|
||||
// 计算显示的名称
|
||||
const getDisplayName = () => {
|
||||
if (basePath === '/') return '/';
|
||||
if (/^[A-Z]:$/i.test(basePath)) return basePath;
|
||||
return path.basename(basePath);
|
||||
};
|
||||
|
||||
// 更新 Button 的 variant 逻辑
|
||||
const isSeleted = selectedPath === basePath;
|
||||
const variant = isSeleted
|
||||
? 'solid'
|
||||
: selectedPath && path.dirname(selectedPath) === basePath
|
||||
? 'flat'
|
||||
: 'light';
|
||||
|
||||
return (
|
||||
<div className='ml-4'>
|
||||
<Button
|
||||
onPress={handleClick}
|
||||
className='py-1 px-2 text-left justify-start min-w-0 min-h-0 h-auto text-sm rounded-md'
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant={variant}
|
||||
startContent={
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-md',
|
||||
isSeleted ? 'bg-primary-600' : 'bg-primary-50'
|
||||
)}
|
||||
>
|
||||
{expanded ? <IoRemove /> : <IoAdd />}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{getDisplayName()}
|
||||
</Button>
|
||||
{expanded && (
|
||||
<div>
|
||||
{loading
|
||||
? (
|
||||
<div className='flex py-1 px-8'>
|
||||
<Spinner size='sm' color='primary' />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
dirs.map((dirName) => {
|
||||
const childPath =
|
||||
basePath === '/' && /^[A-Z]:$/i.test(dirName)
|
||||
? dirName
|
||||
: path.join(basePath, dirName);
|
||||
return (
|
||||
<DirectoryTree
|
||||
key={childPath}
|
||||
basePath={childPath}
|
||||
onSelect={onSelect}
|
||||
selectedPath={selectedPath}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MoveModal ({
|
||||
isOpen,
|
||||
moveTargetPath,
|
||||
selectionInfo,
|
||||
onClose,
|
||||
onMove,
|
||||
onSelect,
|
||||
}: MoveModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>选择目标目录</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='rounded-md p-2 border border-default-300 overflow-auto max-h-60'>
|
||||
<DirectoryTree
|
||||
basePath='/'
|
||||
onSelect={onSelect}
|
||||
selectedPath={moveTargetPath}
|
||||
/>
|
||||
</div>
|
||||
<p className='text-sm text-default-500 mt-2'>
|
||||
当前选择:{moveTargetPath || '未选择'}
|
||||
</p>
|
||||
<p className='text-sm text-default-500'>移动项:{selectionInfo}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color='primary' variant='flat' onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color='primary' onPress={onMove}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Input } from '@heroui/input';
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@heroui/modal';
|
||||
|
||||
interface RenameModalProps {
|
||||
isOpen: boolean
|
||||
newFileName: string
|
||||
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
onClose: () => void
|
||||
onRename: () => void
|
||||
}
|
||||
|
||||
export default function RenameModal ({
|
||||
isOpen,
|
||||
newFileName,
|
||||
onNameChange,
|
||||
onClose,
|
||||
onRename,
|
||||
}: RenameModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>重命名</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input label='新名称' value={newFileName} onChange={onNameChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color='primary' variant='flat' onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color='primary' onPress={onRename}>
|
||||
确定
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user