diff --git a/napcat.webui/package.json b/napcat.webui/package.json index 07246511..0ef7e2db 100644 --- a/napcat.webui/package.json +++ b/napcat.webui/package.json @@ -36,6 +36,7 @@ "@heroui/spinner": "2.2.7", "@heroui/switch": "2.2.9", "@heroui/system": "2.4.7", + "@heroui/table": "^2.2.9", "@heroui/tabs": "2.2.8", "@heroui/theme": "2.4.6", "@heroui/tooltip": "2.2.8", @@ -56,6 +57,7 @@ "framer-motion": "^12.0.6", "monaco-editor": "^0.52.2", "motion": "^12.0.6", + "path-browserify": "^1.0.1", "qface": "^1.4.1", "qrcode.react": "^4.2.0", "quill": "^2.0.3", @@ -83,6 +85,7 @@ "@types/event-source-polyfill": "^1.0.5", "@types/fabric": "^5.3.9", "@types/node": "^22.12.0", + "@types/path-browserify": "^1.0.3", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@types/react-window": "^1.8.8", diff --git a/napcat.webui/src/components/file_icon.tsx b/napcat.webui/src/components/file_icon.tsx new file mode 100644 index 00000000..9327abe9 --- /dev/null +++ b/napcat.webui/src/components/file_icon.tsx @@ -0,0 +1,166 @@ +import { + FaFile, + FaFileAudio, + FaFileCode, + FaFileCsv, + FaFileExcel, + FaFileImage, + FaFileLines, + FaFilePdf, + FaFilePowerpoint, + FaFileVideo, + FaFileWord, + FaFileZipper, + FaFolderClosed +} from 'react-icons/fa6' + +export interface FileIconProps { + name?: string + isDirectory?: boolean +} + +const FileIcon = (props: FileIconProps) => { + const { name, isDirectory = false } = props + if (isDirectory) { + return + } + + const ext = name?.split('.').pop() || '' + if (ext) { + switch (ext.toLowerCase()) { + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': + case 'svg': + case 'bmp': + case 'ico': + case 'webp': + case 'tiff': + case 'tif': + case 'heic': + case 'heif': + case 'avif': + case 'apng': + case 'flif': + case 'ai': + case 'psd': + case 'xcf': + case 'sketch': + case 'fig': + case 'xd': + case 'svgz': + return + case 'pdf': + return + case 'doc': + case 'docx': + return + case 'xls': + case 'xlsx': + return + case 'csv': + return + case 'ppt': + case 'pptx': + return + case 'zip': + case 'rar': + case '7z': + case 'tar': + case 'gz': + case 'bz2': + case 'xz': + case 'lz': + case 'lzma': + case 'zst': + case 'zstd': + case 'z': + case 'taz': + case 'tz': + case 'tzo': + return + case 'txt': + return + case 'mp3': + case 'wav': + case 'flac': + return + case 'mp4': + case 'avi': + case 'mov': + case 'wmv': + return + case 'html': + case 'css': + case 'js': + case 'ts': + case 'jsx': + case 'tsx': + case 'json': + case 'xml': + case 'yaml': + case 'yml': + case 'md': + case 'sh': + case 'py': + case 'java': + case 'c': + case 'cpp': + case 'cs': + case 'go': + case 'php': + case 'rb': + case 'pl': + case 'swift': + case 'kt': + case 'rs': + case 'sql': + case 'r': + case 'scala': + case 'groovy': + case 'dart': + case 'lua': + case 'perl': + case 'h': + case 'm': + case 'mm': + case 'makefile': + case 'cmake': + case 'dockerfile': + case 'gradle': + case 'properties': + case 'ini': + case 'conf': + case 'env': + case 'bat': + case 'cmd': + case 'ps1': + case 'psm1': + case 'psd1': + case 'ps1xml': + case 'psc1': + case 'pssc': + case 'nuspec': + case 'resx': + case 'resw': + case 'csproj': + case 'vbproj': + case 'vcxproj': + case 'fsproj': + case 'sln': + case 'suo': + case 'user': + case 'userosscache': + case 'sln.docstates': + case 'dll': + return + default: + return + } + } + + return +} + +export default FileIcon diff --git a/napcat.webui/src/controllers/file_manager.ts b/napcat.webui/src/controllers/file_manager.ts new file mode 100644 index 00000000..9af3f0dd --- /dev/null +++ b/napcat.webui/src/controllers/file_manager.ts @@ -0,0 +1,90 @@ +import { serverRequest } from '@/utils/request' + +export interface FileInfo { + name: string + isDirectory: boolean + size: number + mtime: Date +} + +export default class FileManager { + public static async listFiles(path: string = '/') { + const { data } = await serverRequest.get>( + `/File/list?path=${encodeURIComponent(path)}` + ) + return data.data + } + + public static async createDirectory(path: string): Promise { + const { data } = await serverRequest.post>( + '/File/mkdir', + { path } + ) + return data.data + } + + public static async delete(path: string) { + const { data } = await serverRequest.post>( + '/File/delete', + { path } + ) + return data.data + } + + public static async readFile(path: string) { + const { data } = await serverRequest.get>( + `/File/read?path=${encodeURIComponent(path)}` + ) + return data.data + } + + public static async writeFile(path: string, content: string) { + const { data } = await serverRequest.post>( + '/File/write', + { path, content } + ) + return data.data + } + + public static async createFile(path: string): Promise { + const { data } = await serverRequest.post>( + '/File/create', + { path } + ) + return data.data + } + + public static async batchDelete(paths: string[]) { + const { data } = await serverRequest.post>( + '/File/batchDelete', + { paths } + ) + return data.data + } + + public static async rename(oldPath: string, newPath: string) { + const { data } = await serverRequest.post>( + '/File/rename', + { oldPath, newPath } + ) + return data.data + } + + public static async move(sourcePath: string, targetPath: string) { + const { data } = await serverRequest.post>( + '/File/move', + { sourcePath, targetPath } + ) + return data.data + } + + public static async batchMove( + items: { sourcePath: string; targetPath: string }[] + ) { + const { data } = await serverRequest.post>( + '/File/batchMove', + { items } + ) + return data.data + } +} diff --git a/napcat.webui/src/pages/dashboard/file_manager.tsx b/napcat.webui/src/pages/dashboard/file_manager.tsx new file mode 100644 index 00000000..1ed2f057 --- /dev/null +++ b/napcat.webui/src/pages/dashboard/file_manager.tsx @@ -0,0 +1,655 @@ +import { BreadcrumbItem, Breadcrumbs } from '@heroui/breadcrumbs' +import { Button, ButtonGroup } from '@heroui/button' +import { Code } from '@heroui/code' +import { Input } from '@heroui/input' +import { + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader +} from '@heroui/modal' +import { Spinner } from '@heroui/spinner' +import { + SortDescriptor, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow +} from '@heroui/table' +import { Tooltip } from '@heroui/tooltip' +import { Selection } from '@react-types/shared' +import path from 'path-browserify' +import { useEffect, useState } from 'react' +import toast from 'react-hot-toast' +import { FiCopy, FiEdit2, FiMove, FiPlus, FiTrash2 } from 'react-icons/fi' +import { TiArrowBack } from 'react-icons/ti' +import { useLocation, useNavigate } from 'react-router-dom' + +import CodeEditor from '@/components/code_editor' +import FileIcon from '@/components/file_icon' + +import useDialog from '@/hooks/use-dialog' + +import FileManager, { FileInfo } from '@/controllers/file_manager' + +export default function FileManagerPage() { + const [files, setFiles] = useState([]) + const [loading, setLoading] = useState(false) + // 修改初始排序状态 + const [sortDescriptor, setSortDescriptor] = useState({ + column: 'name', + direction: 'ascending' + }) + const dialog = useDialog() + const location = useLocation() + const navigate = useNavigate() + const currentPath = decodeURIComponent(location.hash.slice(1) || '/') + const [editingFile, setEditingFile] = useState<{ + path: string + content: string + } | null>(null) + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false) + const [newFileName, setNewFileName] = useState('') + const [fileType, setFileType] = useState<'file' | 'directory'>('file') + const [selectedFiles, setSelectedFiles] = useState(new Set()) + const [isRenameModalOpen, setIsRenameModalOpen] = useState(false) + const [isMoveModalOpen, setIsMoveModalOpen] = useState(false) + const [renamingFile, setRenamingFile] = useState('') + const [moveTargetPath, setMoveTargetPath] = useState('') + + const sortFiles = (files: FileInfo[], descriptor: SortDescriptor) => { + return [...files].sort((a, b) => { + // 始终保持目录在前面 + if (a.isDirectory !== b.isDirectory) { + return a.isDirectory ? -1 : 1 + } + + const direction = descriptor.direction === 'ascending' ? 1 : -1 + + switch (descriptor.column) { + case 'name': + return direction * a.name.localeCompare(b.name) + case 'type': { + const aType = a.isDirectory ? '目录' : '文件' + const bType = b.isDirectory ? '目录' : '文件' + return direction * aType.localeCompare(bType) + } + case 'size': + return direction * ((a.size || 0) - (b.size || 0)) + case 'mtime': + return ( + direction * + (new Date(a.mtime).getTime() - new Date(b.mtime).getTime()) + ) + default: + return 0 + } + }) + } + + const loadFiles = async () => { + setLoading(true) + try { + const files = await FileManager.listFiles(currentPath) + setFiles(sortFiles(files, sortDescriptor)) + } catch (error) { + toast.error('加载文件列表失败') + setFiles([]) + } + setLoading(false) + } + + useEffect(() => { + loadFiles() + }, [currentPath]) + + const handleSortChange = (descriptor: SortDescriptor) => { + setSortDescriptor(descriptor) + setFiles((prev) => sortFiles(prev, descriptor)) + } + + const handleDirectoryClick = (dirPath: string) => { + // Windows 系统下处理盘符切换 + if (dirPath.match(/^[A-Z]:\\?$/i)) { + navigate(`/file_manager#${encodeURIComponent(dirPath)}`) + return + } + + // 处理返回上级目录 + if (dirPath === '..') { + // 检查是否在盘符根目录(如 C:) + if (/^[A-Z]:$/i.test(currentPath)) { + navigate('/file_manager#/') + return + } + + const parentPath = path.dirname(currentPath) + // 如果已经在根目录,则显示盘符列表(Windows)或保持在根目录(其他系统) + if (parentPath === currentPath) { + navigate('/file_manager#/') + return + } + navigate(`/file_manager#${encodeURIComponent(parentPath)}`) + return + } + + const newPath = path.join(currentPath, dirPath) + navigate(`/file_manager#${encodeURIComponent(newPath)}`) + } + + const handleEdit = async (filePath: string) => { + try { + const content = await FileManager.readFile(filePath) + setEditingFile({ path: filePath, content }) + } catch (error) { + toast.error('打开文件失败') + } + } + + const handleSave = async () => { + if (!editingFile) return + try { + await FileManager.writeFile(editingFile.path, editingFile.content) + toast.success('保存成功') + setEditingFile(null) + loadFiles() + } catch (error) { + toast.error('保存失败') + } + } + + const handleDelete = async (filePath: string) => { + dialog.confirm({ + title: '删除文件', + content: ( +
+ 确定要删除文件 {filePath} 吗? +
+ ), + onConfirm: async () => { + try { + await FileManager.delete(filePath) + toast.success('删除成功') + loadFiles() + } catch (error) { + toast.error('删除失败') + } + } + }) + } + + const handleCreate = async () => { + if (!newFileName) return + const newPath = path.join(currentPath, newFileName) + try { + if (fileType === 'directory') { + const result = await FileManager.createDirectory(newPath) + if (!result) { + toast.error('目录已存在') + return + } + } else { + const result = await FileManager.createFile(newPath) + if (!result) { + toast.error('文件已存在') + return + } + } + toast.success('创建成功') + setIsCreateModalOpen(false) + setNewFileName('') + loadFiles() + } catch (error) { + const err = error as Error + toast.error(err?.message || '创建失败') + } + } + + const handleBatchDelete = async () => { + // 处理 Selection 类型 + const selectedArray = + selectedFiles === 'all' + ? files.map((f) => f.name) + : Array.from(selectedFiles as Set) + + if (selectedArray.length === 0) return + + dialog.confirm({ + title: '批量删除', + content:
确定要删除选中的 {selectedArray.length} 个项目吗?
, + onConfirm: async () => { + try { + const paths = selectedArray.map((key) => path.join(currentPath, key)) + await FileManager.batchDelete(paths) + toast.success('批量删除成功') + setSelectedFiles(new Set()) + loadFiles() + } catch (error) { + toast.error('批量删除失败') + } + } + }) + } + + // 处理重命名 + const handleRename = async () => { + if (!renamingFile || !newFileName) return + try { + const oldPath = path.join(currentPath, renamingFile) + const newPath = path.join(currentPath, newFileName) + await FileManager.rename(oldPath, newPath) + toast.success('重命名成功') + setIsRenameModalOpen(false) + setRenamingFile('') + setNewFileName('') + loadFiles() + } catch (error) { + toast.error('重命名失败') + } + } + + // 处理移动 + const handleMove = async (sourceName: string) => { + if (!moveTargetPath) return + try { + const sourcePath = path.join(currentPath, sourceName) + const targetPath = path.join(moveTargetPath, sourceName) + await FileManager.move(sourcePath, targetPath) + toast.success('移动成功') + setIsMoveModalOpen(false) + setMoveTargetPath('') + loadFiles() + } catch (error) { + toast.error('移动失败') + } + } + + // 处理批量移动 + const handleBatchMove = async () => { + if (!moveTargetPath) return + const selectedArray = + selectedFiles === 'all' + ? files.map((f) => f.name) + : Array.from(selectedFiles as Set) + + if (selectedArray.length === 0) return + + try { + const items = selectedArray.map((name) => ({ + sourcePath: path.join(currentPath, name), + targetPath: path.join(moveTargetPath, name) + })) + await FileManager.batchMove(items) + toast.success('批量移动成功') + setIsMoveModalOpen(false) + setMoveTargetPath('') + setSelectedFiles(new Set()) + loadFiles() + } catch (error) { + toast.error('批量移动失败') + } + } + + // 添加复制路径处理函数 + const handleCopyPath = (fileName: string) => { + const fullPath = path.join(currentPath, fileName) + navigator.clipboard.writeText(fullPath) + toast.success('路径已复制') + } + + // 修改移动按钮的点击处理 + const handleMoveClick = (fileName: string) => { + setRenamingFile(fileName) + setMoveTargetPath('') + setIsMoveModalOpen(true) + } + + return ( +
+
+ + + {(selectedFiles === 'all' || + (selectedFiles as Set).size > 0) && ( + <> + + + + )} + + {currentPath.split('/').map((part, index, parts) => ( + { + const newPath = parts.slice(0, index + 1).join('/') + navigate(`/file_manager#${encodeURIComponent(newPath)}`) + }} + > + {part} + + ))} + +
+ + + + + 名称 + + + 类型 + + + 大小 + + + 修改时间 + + 操作 + + + + + } + items={files} + > + {(file) => ( + + + + + {file.isDirectory ? '目录' : '文件'} + + {isNaN(file.size) || file.isDirectory + ? '-' + : `${file.size} 字节`} + + {new Date(file.mtime).toLocaleString()} + + + + + + + + + + + + + + + + + + )} + +
+ + {/* 文件编辑对话框 */} + setEditingFile(null)} + > + + + 编辑文件 + {editingFile?.path} + + +
+ + setEditingFile((prev) => + prev ? { ...prev, content: value || '' } : null + ) + } + options={{ wordWrap: 'on' }} + /> +
+
+ + + + +
+
+ + {/* 新建文件/目录对话框 */} + setIsCreateModalOpen(false)} + > + + 新建 + +
+ + + + + setNewFileName(e.target.value)} + /> +
+
+ + + + +
+
+ + {/* 重命名对话框 */} + setIsRenameModalOpen(false)} + > + + 重命名 + + setNewFileName(e.target.value)} + /> + + + + + + + + + {/* 移动对话框 */} + setIsMoveModalOpen(false)}> + + 移动到 + +
+ setMoveTargetPath(e.target.value)} + placeholder="请输入完整目标路径" + /> +

+ 当前选择: + {selectedFiles === 'all' || + (selectedFiles as Set).size > 0 + ? `${selectedFiles === 'all' ? files.length : (selectedFiles as Set).size} 个项目` + : renamingFile} +

+
+
+ + + + +
+
+
+ ) +} diff --git a/napcat.webui/src/pages/index.tsx b/napcat.webui/src/pages/index.tsx index 62e53fd5..8a94ec5a 100644 --- a/napcat.webui/src/pages/index.tsx +++ b/napcat.webui/src/pages/index.tsx @@ -1,8 +1,6 @@ import { AnimatePresence, motion } from 'motion/react' import { Route, Routes, useLocation } from 'react-router-dom' -import UnderConstruction from '@/components/under_construction' - import DefaultLayout from '@/layouts/default' import DashboardIndexPage from './dashboard' @@ -11,6 +9,7 @@ import ConfigPage from './dashboard/config' import DebugPage from './dashboard/debug' import HttpDebug from './dashboard/debug/http' import WSDebug from './dashboard/debug/websocket' +import FileManagerPage from './dashboard/file_manager' import LogsPage from './dashboard/logs' import NetworkPage from './dashboard/network' import TerminalPage from './dashboard/terminal' @@ -36,7 +35,7 @@ export default function IndexPage() { } /> } /> - } path="/file_manager" /> + } path="/file_manager" /> } path="/terminal" /> } path="/about" /> diff --git a/src/webui/src/api/File.ts b/src/webui/src/api/File.ts new file mode 100644 index 00000000..6f23510d --- /dev/null +++ b/src/webui/src/api/File.ts @@ -0,0 +1,261 @@ +import type { RequestHandler } from 'express'; +import { sendError, sendSuccess } from '../utils/response'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; + +const isWindows = os.platform() === 'win32'; + +// 获取系统根目录列表(Windows返回盘符列表,其他系统返回['/']) +const getRootDirs = async (): Promise => { + if (!isWindows) return ['/']; + + // Windows 驱动器字母 (A-Z) + const drives: string[] = []; + for (let i = 65; i <= 90; i++) { + const driveLetter = String.fromCharCode(i); + try { + await fs.access(`${driveLetter}:\\`); + drives.push(`${driveLetter}:`); + } catch { + // 如果驱动器不存在或无法访问,跳过 + continue; + } + } + return drives.length > 0 ? drives : ['C:']; +}; + +// 规范化路径 +const normalizePath = (inputPath: string): string => { + if (!inputPath) return isWindows ? 'C:\\' : '/'; + return path.normalize(inputPath); +}; + +interface FileInfo { + name: string; + isDirectory: boolean; + size: number; + mtime: Date; +} + +// 添加系统文件黑名单 +const SYSTEM_FILES = new Set(['pagefile.sys', 'swapfile.sys', 'hiberfil.sys', 'System Volume Information']); + +// 检查文件或目录是否存在 +const checkExists = async (pathToCheck: string): Promise => { + try { + await fs.access(pathToCheck); + return true; + } catch { + return false; + } +}; + +// 检查同类型的文件或目录是否存在 +const checkSameTypeExists = async (pathToCheck: string, isDirectory: boolean): Promise => { + try { + const stat = await fs.stat(pathToCheck); + // 只有当类型相同时才认为是冲突 + return stat.isDirectory() === isDirectory; + } catch { + return false; + } +}; + +// 获取目录内容 +export const ListFilesHandler: RequestHandler = async (req, res) => { + try { + const requestPath = (req.query.path as string) || (isWindows ? 'C:\\' : '/'); + const normalizedPath = normalizePath(requestPath); + + // 如果是根路径且在Windows系统上,返回盘符列表 + if (isWindows && (!requestPath || requestPath === '/' || requestPath === '\\')) { + const drives = await getRootDirs(); + const driveInfos: FileInfo[] = await Promise.all( + drives.map(async (drive) => { + try { + const stat = await fs.stat(`${drive}\\`); + return { + name: drive, + isDirectory: true, + size: 0, + mtime: stat.mtime, + }; + } catch { + return { + name: drive, + isDirectory: true, + size: 0, + mtime: new Date(), + }; + } + }) + ); + return sendSuccess(res, driveInfos); + } + + const files = await fs.readdir(normalizedPath); + const fileInfos: FileInfo[] = []; + + for (const file of files) { + // 跳过系统文件 + if (SYSTEM_FILES.has(file)) continue; + + try { + const fullPath = path.join(normalizedPath, file); + const stat = await fs.stat(fullPath); + fileInfos.push({ + name: file, + isDirectory: stat.isDirectory(), + size: stat.size, + mtime: stat.mtime, + }); + } catch (error) { + // 忽略无法访问的文件 + // console.warn(`无法访问文件 ${file}:`, error); + continue; + } + } + + return sendSuccess(res, fileInfos); + } catch (error) { + // console.error('读取目录失败:', error); + return sendError(res, '读取目录失败'); + } +}; + +// 创建目录 +export const CreateDirHandler: RequestHandler = async (req, res) => { + try { + const { path: dirPath } = req.body; + const normalizedPath = normalizePath(dirPath); + + // 检查是否已存在同类型(目录) + if (await checkSameTypeExists(normalizedPath, true)) { + return sendError(res, '同名目录已存在'); + } + + await fs.mkdir(normalizedPath, { recursive: true }); + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '创建目录失败'); + } +}; + +// 删除文件/目录 +export const DeleteHandler: RequestHandler = async (req, res) => { + try { + const { path: targetPath } = req.body; + const normalizedPath = normalizePath(targetPath); + const stat = await fs.stat(normalizedPath); + if (stat.isDirectory()) { + await fs.rm(normalizedPath, { recursive: true }); + } else { + await fs.unlink(normalizedPath); + } + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '删除失败'); + } +}; + +// 批量删除文件/目录 +export const BatchDeleteHandler: RequestHandler = async (req, res) => { + try { + const { paths } = req.body; + for (const targetPath of paths) { + const normalizedPath = normalizePath(targetPath); + const stat = await fs.stat(normalizedPath); + if (stat.isDirectory()) { + await fs.rm(normalizedPath, { recursive: true }); + } else { + await fs.unlink(normalizedPath); + } + } + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '批量删除失败'); + } +}; + +// 读取文件内容 +export const ReadFileHandler: RequestHandler = async (req, res) => { + try { + const filePath = normalizePath(req.query.path as string); + const content = await fs.readFile(filePath, 'utf-8'); + return sendSuccess(res, content); + } catch (error) { + return sendError(res, '读取文件失败'); + } +}; + +// 写入文件内容 +export const WriteFileHandler: RequestHandler = async (req, res) => { + try { + const { path: filePath, content } = req.body; + const normalizedPath = normalizePath(filePath); + await fs.writeFile(normalizedPath, content, 'utf-8'); + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '写入文件失败'); + } +}; + +// 创建新文件 +export const CreateFileHandler: RequestHandler = async (req, res) => { + try { + const { path: filePath } = req.body; + const normalizedPath = normalizePath(filePath); + + // 检查是否已存在同类型(文件) + if (await checkSameTypeExists(normalizedPath, false)) { + return sendError(res, '同名文件已存在'); + } + + await fs.writeFile(normalizedPath, '', 'utf-8'); + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '创建文件失败'); + } +}; + +// 重命名文件/目录 +export const RenameHandler: RequestHandler = async (req, res) => { + try { + const { oldPath, newPath } = req.body; + const normalizedOldPath = normalizePath(oldPath); + const normalizedNewPath = normalizePath(newPath); + await fs.rename(normalizedOldPath, normalizedNewPath); + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '重命名失败'); + } +}; + +// 移动文件/目录 +export const MoveHandler: RequestHandler = async (req, res) => { + try { + const { sourcePath, targetPath } = req.body; + const normalizedSourcePath = normalizePath(sourcePath); + const normalizedTargetPath = normalizePath(targetPath); + await fs.rename(normalizedSourcePath, normalizedTargetPath); + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '移动失败'); + } +}; + +// 批量移动 +export const BatchMoveHandler: RequestHandler = async (req, res) => { + try { + const { items } = req.body; + for (const { sourcePath, targetPath } of items) { + const normalizedSourcePath = normalizePath(sourcePath); + const normalizedTargetPath = normalizePath(targetPath); + await fs.rename(normalizedSourcePath, normalizedTargetPath); + } + return sendSuccess(res, true); + } catch (error) { + return sendError(res, '批量移动失败'); + } +}; diff --git a/src/webui/src/router/File.ts b/src/webui/src/router/File.ts new file mode 100644 index 00000000..fb906f03 --- /dev/null +++ b/src/webui/src/router/File.ts @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import { + ListFilesHandler, + CreateDirHandler, + DeleteHandler, + ReadFileHandler, + WriteFileHandler, + CreateFileHandler, + BatchDeleteHandler, // 添加这一行 + RenameHandler, + MoveHandler, + BatchMoveHandler, +} from '../api/File'; + +const router = Router(); + +router.get('/list', ListFilesHandler); +router.post('/mkdir', CreateDirHandler); +router.post('/delete', DeleteHandler); +router.get('/read', ReadFileHandler); +router.post('/write', WriteFileHandler); +router.post('/create', CreateFileHandler); +router.post('/batchDelete', BatchDeleteHandler); // 添加这一行 +router.post('/rename', RenameHandler); +router.post('/move', MoveHandler); +router.post('/batchMove', BatchMoveHandler); + +export { router as FileRouter }; diff --git a/src/webui/src/router/index.ts b/src/webui/src/router/index.ts index eaa960d3..8ea50102 100644 --- a/src/webui/src/router/index.ts +++ b/src/webui/src/router/index.ts @@ -12,6 +12,7 @@ import { QQLoginRouter } from '@webapi/router/QQLogin'; import { AuthRouter } from '@webapi/router/auth'; import { LogRouter } from '@webapi/router/Log'; import { BaseRouter } from '@webapi/router/Base'; +import { FileRouter } from './File'; const router = Router(); @@ -32,5 +33,7 @@ router.use('/QQLogin', QQLoginRouter); router.use('/OB11Config', OB11ConfigRouter); // router:日志相关路由 router.use('/Log', LogRouter); +// file:文件相关路由 +router.use('/File', FileRouter); export { router as ALLRouter };