背景与需求
我最近使用了一个免费的VPS虚拟主机搭建测试网站,但发现其文件传输功能存在诸多限制:上传和下载均有严格的大小限制,自带的FTP网页工具体验不佳,下载的压缩文件经常损坏。特别是当需要处理大型网站项目(如700MB+的备份文件)时,这些限制使得文件传输几乎无法进行。
为此,我开发了一个专门的文件管理工具,主要解决了以下痛点:
- 自动分块处理大文件上传(默认10MB分块,可配置)
- 支持大文件稳定下载,避免中断问题
- 无需手动拆分压缩包,直接处理完整大文件
核心功能详解
1. 智能文件浏览与导航
- 可视化浏览服务器文件系统,支持无缝目录导航
- 面包屑导航设计,支持快速层级跳转
- 使用直观的emoji图标区分文件与文件夹类型
- 文件夹优先排序,便于快速定位
- 实时显示文件大小等详细信息
2. 先进的大文件上传机制
- 普通上传:支持多文件同时传输
- 智能分块上传:自动识别超过15MB的文件并进行分块处理
- 可配置分块大小(默认10MB,根据服务器限制调整)
- 实时上传进度显示
- 内置断点续传功能,应对网络不稳定情况
- 智能重命名:自动为重名文件添加时间戳避免覆盖
3. 远程文件下载功能
- 支持从外部URL直接下载文件到服务器
- 专用存储目录(yuanchengdownload)管理下载文件
- 完整的URL格式验证机制
- 5分钟下载超时保护
- 自动处理文件名冲突
- HTTP状态码检查确保下载完整性
4. 多功能文件编辑器
- 支持多种文本格式:txt, html, css, js, php, py等20+格式
- 全屏编辑模式,占用95%屏幕空间
- 专业编辑器特性:等宽字体、Tab键支持(插入4空格)
- 快捷键操作(Ctrl+S保存)
- 自动备份功能防止数据丢失
- 安全限制:5MB文件大小限制,仅处理UTF-8/ASCII文本
5. 压缩与解压解决方案
- 智能压缩:支持文件与目录的ZIP格式压缩
- 压缩文件存储在源文件同级目录
- 自动时间戳命名避免覆盖
- 安全解压:支持ZIP格式完整解压
- 递归解压保持目录结构
- 路径安全验证防止遍历攻击
- 详细解压日志记录
- 自动处理重名文件冲突
6. 可视化文件移动功能
- 图形化目录树选择目标位置
- 安全检查防止递归移动(文件夹到自身子目录)
- 智能重命名处理目标位置同名文件
- 实时操作验证确保移动有效性
7. 稳定可靠的文件下载
- 单文件直接下载
- 大文件分块下载(256KB分块)
- 优化传输:移除执行时间限制,禁用输出缓冲
- 支持断点续传
- 实时连接中断检测与恢复
8. 基本文件管理操作
- 新建文件夹与空白文件
- 安全删除功能(支持递归删除目录)
- 文件名合法性验证防止系统冲突
9. 智能右键上下文菜单
- 根据文件类型动态显示可用操作
- 直观的emoji图标标识功能:
- 📦 压缩功能
- 🗂️ 解压(ZIP文件)
- 📝 编辑(支持格式)
- ✂️ 移动/剪切
- 🗑️ 删除
技术优势
此工具专门针对有限制的VPS环境优化,解决了免费主机服务常见的文件传输痛点。通过分块传输、断点续传和智能重命名机制,确保了大规模文件传输的可靠性和稳定性,极大提高了网站管理和备份的效率。
无论是大型网站迁移、定期备份还是日常文件管理,这个工具都能提供稳定高效的支持,彻底解决了免费VPS文件传输的种种限制问题。
![图片[1]-高效服务器文件管理工具网页版-九零社区](https://i0.hdslb.com/bfs/openplatform/04b8394e52d5cef5b4b1d7c262aa06bd4dfafb9f.png)
使用方法
只有一个php页面就上传到网站根目录直接访问这个php页面就可以,
使用方法,把下面的代码复制到一个txt文件中,然后重命名为file-manager.php或者其他名字,直接访问就可以,例如https://www.90ii.cn/file-manager.php,放在网站根目录就可以。
文件代码
<?php
// 错误报告设置
error_reporting(E_ALL);
ini_set('display_errors', 1);
// 脚本最大执行时间(0为不限时)
set_time_limit(0);
ignore_user_abort(true);
// 针对免费服务器优化:禁用输出缓冲
ini_set('output_buffering', 0);
// 自动获取绝对路径
$absolutePath = realpath(__DIR__);
$basePath = $absolutePath;
// 处理AJAX请求
if (isset($_POST['action'])) {
// 设置错误处理,确保返回JSON格式
set_error_handler(function($errno, $errstr, $errfile, $errline) {
die(json_encode(['error' => "PHP Error: $errstr in $errfile on line $errline"]));
});
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if ($_POST['action'] == 'list_dir') {
// 列出目录内容
$dir = isset($_POST['dir']) ? $_POST['dir'] : $basePath;
$dir = realpath($dir);
// 安全检查:确保请求的目录在允许的根目录之下
if (strpos($dir, $basePath) !== 0) {
die(json_encode(['error' => '访问被拒绝:目录不在允许范围内']));
}
$files = scandir($dir);
$result = [];
foreach ($files as $file) {
if ($file == '.' || $file == '..') continue;
$fullPath = $dir . DIRECTORY_SEPARATOR . $file;
$isDir = is_dir($fullPath);
$result[] = [
'name' => $file,
'path' => $fullPath,
'is_dir' => $isDir,
'size' => $isDir ? null : filesize($fullPath)
];
}
// 按文件夹优先排序
usort($result, function($a, $b) {
if ($a['is_dir'] && !$b['is_dir']) return -1;
if (!$a['is_dir'] && $b['is_dir']) return 1;
return strcmp($a['name'], $b['name']);
});
echo json_encode([
'current_dir' => $dir,
'files' => $result
]);
exit;
}
elseif ($_POST['action'] == 'upload_file') {
// 文件上传处理
$uploadDir = isset($_POST['upload_dir']) ? $_POST['upload_dir'] : $basePath;
$uploadDir = realpath($uploadDir);
// 安全检查
if (!$uploadDir || strpos($uploadDir, $basePath) !== 0) {
die(json_encode(['error' => '无效的上传目录']));
}
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
die(json_encode(['error' => '文件上传失败']));
}
$file = $_FILES['file'];
$fileName = basename($file['name']);
$targetPath = $uploadDir . DIRECTORY_SEPARATOR . $fileName;
// 检查文件是否已存在
if (file_exists($targetPath)) {
$fileName = pathinfo($fileName, PATHINFO_FILENAME) . '_' . date('Y-m-d_H-i-s') . '.' . pathinfo($fileName, PATHINFO_EXTENSION);
$targetPath = $uploadDir . DIRECTORY_SEPARATOR . $fileName;
}
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo json_encode([
'success' => true,
'message' => "文件上传成功:$fileName",
'filename' => $fileName
]);
} else {
echo json_encode(['error' => '文件保存失败']);
}
exit;
}
elseif ($_POST['action'] == 'chunk_upload') {
// 分块上传处理
$uploadDir = isset($_POST['upload_dir']) ? $_POST['upload_dir'] : $basePath;
$uploadDir = realpath($uploadDir);
// 安全检查
if (!$uploadDir || strpos($uploadDir, $basePath) !== 0) {
die(json_encode(['error' => '无效的上传目录']));
}
$fileName = isset($_POST['fileName']) ? basename($_POST['fileName']) : '';
$chunkIndex = isset($_POST['chunkIndex']) ? intval($_POST['chunkIndex']) : 0;
$totalChunks = isset($_POST['totalChunks']) ? intval($_POST['totalChunks']) : 1;
$fileId = isset($_POST['fileId']) ? $_POST['fileId'] : '';
if (empty($fileName) || empty($fileId)) {
die(json_encode(['error' => '缺少必要参数']));
}
if (!isset($_FILES['chunk']) || $_FILES['chunk']['error'] !== UPLOAD_ERR_OK) {
die(json_encode(['error' => '分块上传失败']));
}
// 创建临时目录
$tempDir = $uploadDir . DIRECTORY_SEPARATOR . 'temp_uploads';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
// 保存分块文件
$chunkPath = $tempDir . DIRECTORY_SEPARATOR . $fileId . '_' . $chunkIndex;
if (!move_uploaded_file($_FILES['chunk']['tmp_name'], $chunkPath)) {
die(json_encode(['error' => '分块保存失败']));
}
// 检查是否所有分块都已上传
$uploadedChunks = 0;
for ($i = 0; $i < $totalChunks; $i++) {
if (file_exists($tempDir . DIRECTORY_SEPARATOR . $fileId . '_' . $i)) {
$uploadedChunks++;
}
}
if ($uploadedChunks === $totalChunks) {
// 所有分块上传完成,合并文件
$finalPath = $uploadDir . DIRECTORY_SEPARATOR . $fileName;
// 检查文件是否已存在
if (file_exists($finalPath)) {
$fileName = pathinfo($fileName, PATHINFO_FILENAME) . '_' . date('Y-m-d_H-i-s') . '.' . pathinfo($fileName, PATHINFO_EXTENSION);
$finalPath = $uploadDir . DIRECTORY_SEPARATOR . $fileName;
}
$finalFile = fopen($finalPath, 'wb');
if (!$finalFile) {
die(json_encode(['error' => '无法创建最终文件']));
}
// 合并所有分块
for ($i = 0; $i < $totalChunks; $i++) {
$chunkPath = $tempDir . DIRECTORY_SEPARATOR . $fileId . '_' . $i;
if (file_exists($chunkPath)) {
$chunkData = file_get_contents($chunkPath);
fwrite($finalFile, $chunkData);
unlink($chunkPath); // 删除临时分块
}
}
fclose($finalFile);
echo json_encode([
'success' => true,
'completed' => true,
'message' => "大文件上传成功:$fileName",
'filename' => $fileName,
'size' => filesize($finalPath)
]);
} else {
// 返回上传进度
echo json_encode([
'success' => true,
'completed' => false,
'progress' => round(($uploadedChunks / $totalChunks) * 100, 2),
'uploaded' => $uploadedChunks,
'total' => $totalChunks
]);
}
exit;
}
elseif ($_POST['action'] == 'remote_download') {
// 远程下载功能
$url = isset($_POST['url']) ? trim($_POST['url']) : '';
if (empty($url)) {
die(json_encode(['error' => '请输入有效的下载链接']));
}
// 验证URL格式
if (!filter_var($url, FILTER_VALIDATE_URL)) {
die(json_encode(['error' => '无效的URL格式']));
}
// 创建远程下载目录
$downloadDir = $basePath . DIRECTORY_SEPARATOR . 'yuanchengdownload';
if (!is_dir($downloadDir)) {
mkdir($downloadDir, 0755, true);
}
// 获取文件名
$fileName = basename(parse_url($url, PHP_URL_PATH));
if (empty($fileName) || strpos($fileName, '.') === false) {
$fileName = 'download_' . date('Y-m-d_H-i-s') . '.file';
}
$targetPath = $downloadDir . DIRECTORY_SEPARATOR . $fileName;
// 如果文件已存在,添加时间戳
if (file_exists($targetPath)) {
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
$name = pathinfo($fileName, PATHINFO_FILENAME);
$fileName = $name . '_' . date('Y-m-d_H-i-s') . '.' . $ext;
$targetPath = $downloadDir . DIRECTORY_SEPARATOR . $fileName;
}
// 使用cURL下载文件
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 5分钟超时
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
$data = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($data === false || !empty($error)) {
die(json_encode(['error' => '下载失败: ' . $error]));
}
if ($httpCode !== 200) {
die(json_encode(['error' => "下载失败,HTTP状态码: $httpCode"]));
}
if (file_put_contents($targetPath, $data) === false) {
die(json_encode(['error' => '保存文件失败']));
}
echo json_encode([
'success' => true,
'message' => "远程文件下载成功:$fileName",
'filename' => $fileName,
'size' => filesize($targetPath)
]);
exit;
}
elseif ($_POST['action'] == 'create_folder') {
// 创建文件夹
$currentDir = isset($_POST['current_dir']) ? $_POST['current_dir'] : $basePath;
$folderName = isset($_POST['folder_name']) ? trim($_POST['folder_name']) : '';
$currentDir = realpath($currentDir);
// 安全检查
if (!$currentDir || strpos($currentDir, $basePath) !== 0) {
die(json_encode(['error' => '无效的目录路径']));
}
if (empty($folderName)) {
die(json_encode(['error' => '请输入文件夹名称']));
}
// 验证文件夹名称
if (preg_match('/[<>:"|?*]/', $folderName)) {
die(json_encode(['error' => '文件夹名称包含非法字符']));
}
$folderPath = $currentDir . DIRECTORY_SEPARATOR . $folderName;
if (file_exists($folderPath)) {
die(json_encode(['error' => '文件夹已存在']));
}
if (mkdir($folderPath, 0755)) {
echo json_encode([
'success' => true,
'message' => "文件夹创建成功:$folderName"
]);
} else {
echo json_encode(['error' => '文件夹创建失败']);
}
exit;
}
elseif ($_POST['action'] == 'create_file') {
// 创建文件
$currentDir = isset($_POST['current_dir']) ? $_POST['current_dir'] : $basePath;
$fileName = isset($_POST['file_name']) ? trim($_POST['file_name']) : '';
$currentDir = realpath($currentDir);
// 安全检查
if (!$currentDir || strpos($currentDir, $basePath) !== 0) {
die(json_encode(['error' => '无效的目录路径']));
}
if (empty($fileName)) {
die(json_encode(['error' => '请输入文件名称']));
}
// 验证文件名
if (preg_match('/[<>:"|?*]/', $fileName)) {
die(json_encode(['error' => '文件名包含非法字符']));
}
$filePath = $currentDir . DIRECTORY_SEPARATOR . $fileName;
if (file_exists($filePath)) {
die(json_encode(['error' => '文件已存在']));
}
if (file_put_contents($filePath, '') !== false) {
echo json_encode([
'success' => true,
'message' => "文件创建成功:$fileName"
]);
} else {
echo json_encode(['error' => '文件创建失败']);
}
exit;
}
elseif ($_POST['action'] == 'compress_single') {
// 压缩单个文件或目录
$targetPath = isset($_POST['path']) ? $_POST['path'] : '';
$targetPath = realpath($targetPath);
// 安全检查
if (!$targetPath || strpos($targetPath, $basePath) !== 0 || !file_exists($targetPath)) {
die(json_encode(['error' => '无效的文件路径']));
}
// 获取文件/目录所在的目录
$parentDir = dirname($targetPath);
$fileName = basename($targetPath);
$zipFileName = $fileName . '_' . date('Y-m-d_H-i-s') . '.zip';
$zipFilePath = $parentDir . DIRECTORY_SEPARATOR . $zipFileName;
$zip = new ZipArchive();
if ($zip->open($zipFilePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE) {
die(json_encode(['error' => "无法创建ZIP文件: $zipFileName"]));
}
// 递归添加目录到ZIP的函数
function addToZip($path, &$zip, $basePath) {
if (is_file($path)) {
$localPath = basename($path);
$zip->addFile($path, $localPath);
} elseif (is_dir($path)) {
$localPath = basename($path);
$zip->addEmptyDir($localPath);
$handle = opendir($path);
if ($handle) {
while (false !== ($f = readdir($handle))) {
if ($f != '.' && $f != '..') {
$filePath = "$path/$f";
$relativePath = $localPath . '/' . $f;
if (is_file($filePath)) {
$zip->addFile($filePath, $relativePath);
} elseif (is_dir($filePath)) {
$zip->addEmptyDir($relativePath);
addFolderToZipRecursive($filePath, $zip, $relativePath);
}
}
}
closedir($handle);
}
}
}
function addFolderToZipRecursive($folder, &$zip, $localBase) {
$handle = opendir($folder);
if ($handle) {
while (false !== ($f = readdir($handle))) {
if ($f != '.' && $f != '..') {
$filePath = "$folder/$f";
$localPath = $localBase . '/' . $f;
if (is_file($filePath)) {
$zip->addFile($filePath, $localPath);
} elseif (is_dir($filePath)) {
$zip->addEmptyDir($localPath);
addFolderToZipRecursive($filePath, $zip, $localPath);
}
}
}
closedir($handle);
}
}
addToZip($targetPath, $zip, $basePath);
$zip->close();
echo json_encode([
'success' => true,
'message' => "压缩完成!文件已保存为: $zipFileName",
'filename' => $zipFileName,
'size' => filesize($zipFilePath)
]);
exit;
}
elseif ($_POST['action'] == 'download_single') {
// 下载单个文件
$targetPath = isset($_POST['path']) ? $_POST['path'] : '';
$targetPath = realpath($targetPath);
// 安全检查
if (!$targetPath || strpos($targetPath, $basePath) !== 0 || !file_exists($targetPath)) {
header('HTTP/1.1 404 Not Found');
die('文件不存在');
}
// 只允许下载文件,不允许下载目录
if (is_dir($targetPath)) {
die(json_encode(['error' => '请使用压缩功能来下载目录']));
}
$fileName = basename($targetPath);
$fileSize = filesize($targetPath);
// 重新设置执行时间限制和忽略用户中断
set_time_limit(0);
ignore_user_abort(true);
// 清除所有输出缓冲
while (ob_get_level()) {
ob_end_clean();
}
// 禁用Apache的输出压缩(如果可能)
if (function_exists('apache_setenv')) {
apache_setenv('no-gzip', '1');
}
// 设置下载头信息
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Length: ' . $fileSize);
header('Content-Transfer-Encoding: binary');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Connection: close');
$handle = fopen($targetPath, 'rb');
if ($handle === false) {
header('HTTP/1.1 500 Internal Server Error');
die('无法打开文件');
}
// 使用256KB块大小
$chunkSize = 262144;
while (!feof($handle)) {
if (connection_aborted()) {
break;
}
set_time_limit(30);
$data = fread($handle, $chunkSize);
if ($data === false || strlen($data) == 0) {
break;
}
echo $data;
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} else {
flush();
}
}
fclose($handle);
exit;
}
elseif ($_POST['action'] == 'delete_item') {
// 删除文件或目录
$targetPath = isset($_POST['path']) ? $_POST['path'] : '';
$targetPath = realpath($targetPath);
// 安全检查
if (!$targetPath || strpos($targetPath, $basePath) !== 0 || !file_exists($targetPath)) {
die(json_encode(['error' => '无效的文件路径']));
}
// 递归删除目录的函数
function deleteDirectory($dir) {
if (!is_dir($dir)) {
return unlink($dir);
}
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
if (is_dir($filePath)) {
deleteDirectory($filePath);
} else {
unlink($filePath);
}
}
return rmdir($dir);
}
$fileName = basename($targetPath);
$isDir = is_dir($targetPath);
try {
if ($isDir) {
$success = deleteDirectory($targetPath);
$message = $success ? "目录 '$fileName' 删除成功!" : "删除目录失败!";
} else {
$success = unlink($targetPath);
$message = $success ? "文件 '$fileName' 删除成功!" : "删除文件失败!";
}
if ($success) {
echo json_encode([
'success' => true,
'message' => $message
]);
} else {
echo json_encode(['error' => $message]);
}
} catch (Exception $e) {
echo json_encode(['error' => '删除时发生错误: ' . $e->getMessage()]);
}
exit;
}
elseif ($_POST['action'] == 'read_file') {
// 读取文件内容用于编辑
$targetPath = isset($_POST['path']) ? $_POST['path'] : '';
$targetPath = realpath($targetPath);
// 安全检查
if (!$targetPath || strpos($targetPath, $basePath) !== 0 || !file_exists($targetPath)) {
die(json_encode(['error' => '无效的文件路径']));
}
// 只允许编辑文件,不允许编辑目录
if (is_dir($targetPath)) {
die(json_encode(['error' => '无法编辑目录']));
}
// 检查文件大小,避免加载过大的文件
$fileSize = filesize($targetPath);
if ($fileSize > 5 * 1024 * 1024) { // 5MB限制
die(json_encode(['error' => '文件过大,无法编辑(超过5MB)']));
}
// 尝试读取文件内容
$content = file_get_contents($targetPath);
if ($content === false) {
die(json_encode(['error' => '无法读取文件内容']));
}
// 检查是否为二进制文件
if (mb_check_encoding($content, 'UTF-8') === false && !mb_check_encoding($content, 'ASCII')) {
die(json_encode(['error' => '无法编辑二进制文件']));
}
echo json_encode([
'success' => true,
'content' => $content,
'filename' => basename($targetPath),
'size' => $fileSize
]);
exit;
}
elseif ($_POST['action'] == 'save_file') {
// 保存文件内容
$targetPath = isset($_POST['path']) ? $_POST['path'] : '';
$content = isset($_POST['content']) ? $_POST['content'] : '';
$targetPath = realpath($targetPath);
// 安全检查
if (!$targetPath || strpos($targetPath, $basePath) !== 0) {
die(json_encode(['error' => '无效的文件路径']));
}
// 创建备份
$backupPath = $targetPath . '.backup.' . date('Y-m-d_H-i-s');
if (file_exists($targetPath)) {
copy($targetPath, $backupPath);
}
// 保存文件
$result = file_put_contents($targetPath, $content);
if ($result === false) {
die(json_encode(['error' => '保存文件失败']));
}
echo json_encode([
'success' => true,
'message' => '文件保存成功!',
'bytes_written' => $result
]);
exit;
}
elseif ($_POST['action'] == 'move_item') {
// 移动文件或目录
$sourcePath = isset($_POST['source_path']) ? $_POST['source_path'] : '';
$targetDir = isset($_POST['target_dir']) ? $_POST['target_dir'] : '';
$sourcePath = realpath($sourcePath);
$targetDir = realpath($targetDir);
// 安全检查
if (!$sourcePath || !$targetDir ||
strpos($sourcePath, $basePath) !== 0 ||
strpos($targetDir, $basePath) !== 0) {
die(json_encode(['error' => '无效的路径']));
}
if (!file_exists($sourcePath)) {
die(json_encode(['error' => '源文件或目录不存在']));
}
if (!is_dir($targetDir) || !is_writable($targetDir)) {
die(json_encode(['error' => '目标目录无效或无写入权限']));
}
$fileName = basename($sourcePath);
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
// 检查是否试图移动到自身或子目录中(防止递归移动)
if (is_dir($sourcePath)) {
$realSource = realpath($sourcePath);
$realTarget = realpath($targetDir);
if ($realSource === $realTarget || strpos($realTarget, $realSource . DIRECTORY_SEPARATOR) === 0) {
die(json_encode(['error' => '不能将目录移动到自身或其子目录中']));
}
}
// 检查目标位置是否已存在同名文件/目录
if (file_exists($targetPath)) {
// 生成新的文件名
$counter = 1;
$extension = '';
$baseName = $fileName;
if (!is_dir($sourcePath) && strrpos($fileName, '.') !== false) {
$extension = substr($fileName, strrpos($fileName, '.'));
$baseName = substr($fileName, 0, strrpos($fileName, '.'));
}
do {
$newFileName = $baseName . '_' . $counter . $extension;
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $newFileName;
$counter++;
} while (file_exists($targetPath));
$fileName = $newFileName;
}
// 执行移动操作
if (rename($sourcePath, $targetPath)) {
$itemType = is_dir($targetPath) ? '目录' : '文件';
echo json_encode([
'success' => true,
'message' => "{$itemType} '{$fileName}' 已成功移动到目标位置",
'new_name' => $fileName
]);
} else {
echo json_encode(['error' => '移动失败,请检查文件权限']);
}
exit;
}
elseif ($_POST['action'] == 'extract_archive') {
// 解压缩文件
$archivePath = isset($_POST['path']) ? $_POST['path'] : '';
$archivePath = realpath($archivePath);
// 安全检查
if (!$archivePath || strpos($archivePath, $basePath) !== 0 || !file_exists($archivePath)) {
die(json_encode(['error' => '无效的压缩文件路径']));
}
if (is_dir($archivePath)) {
die(json_encode(['error' => '无法解压目录']));
}
$fileName = basename($archivePath);
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$currentDir = dirname($archivePath);
// 检查是否为支持的压缩格式
$supportedFormats = ['zip'];
if (!in_array($fileExtension, $supportedFormats)) {
die(json_encode(['error' => "不支持的压缩格式: .$fileExtension (当前仅支持: " . implode(', ', $supportedFormats) . ")"]));
}
// 检查ZipArchive扩展是否可用
if (!class_exists('ZipArchive')) {
die(json_encode(['error' => 'PHP ZipArchive 扩展不可用']));
}
// 创建解压目录
$extractBaseName = pathinfo($fileName, PATHINFO_FILENAME);
$extractDir = $currentDir . DIRECTORY_SEPARATOR . $extractBaseName;
// 如果解压目录已存在,创建带时间戳的新目录
if (file_exists($extractDir)) {
$extractDir = $currentDir . DIRECTORY_SEPARATOR . $extractBaseName . '_extracted_' . date('Y-m-d_H-i-s');
}
if (!mkdir($extractDir, 0755, true)) {
die(json_encode(['error' => '无法创建解压目录']));
}
// 执行解压操作
$zip = new ZipArchive();
$result = $zip->open($archivePath);
if ($result !== TRUE) {
// 删除创建的空目录
rmdir($extractDir);
$errorMessages = [
ZipArchive::ER_OK => '没有错误',
ZipArchive::ER_MULTIDISK => '不支持多磁盘ZIP文件',
ZipArchive::ER_RENAME => '重命名临时文件失败',
ZipArchive::ER_CLOSE => '关闭ZIP文件失败',
ZipArchive::ER_SEEK => '查找错误',
ZipArchive::ER_READ => '读取错误',
ZipArchive::ER_WRITE => '写入错误',
ZipArchive::ER_CRC => 'CRC错误',
ZipArchive::ER_ZIPCLOSED => 'ZIP文件已关闭',
ZipArchive::ER_NOENT => 'ZIP文件不存在',
ZipArchive::ER_EXISTS => '文件已存在',
ZipArchive::ER_OPEN => '无法打开文件',
ZipArchive::ER_TMPOPEN => '无法创建临时文件',
ZipArchive::ER_ZLIB => 'ZLIB错误',
ZipArchive::ER_MEMORY => '内存分配失败',
ZipArchive::ER_CHANGED => 'ZIP文件已被修改',
ZipArchive::ER_COMPNOTSUPP => '不支持的压缩方法',
ZipArchive::ER_EOF => '过早的EOF',
ZipArchive::ER_INVAL => '无效参数',
ZipArchive::ER_NOZIP => '不是一个ZIP文件',
ZipArchive::ER_INTERNAL => '内部错误',
ZipArchive::ER_INCONS => 'ZIP文件不一致',
ZipArchive::ER_REMOVE => '无法删除文件',
ZipArchive::ER_DELETED => '文件已被删除'
];
$errorMsg = isset($errorMessages[$result]) ? $errorMessages[$result] : "未知错误 (代码: $result)";
die(json_encode(['error' => "打开ZIP文件失败: $errorMsg"]));
}
// 获取ZIP文件信息
$numFiles = $zip->numFiles;
$extractedFiles = 0;
$skippedDirectories = 0;
$errors = [];
$detailedLog = [];
// 逐个解压文件
for ($i = 0; $i < $numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
if ($fileInfo === false) {
$errors[] = "无法获取文件信息 (索引: $i)";
$detailedLog[] = "ERROR: 无法获取文件信息 (索引: $i)";
continue;
}
$fileName = $fileInfo['name'];
$detailedLog[] = "处理文件: $fileName";
// 安全检查:防止路径遍历攻击
// 检查文件名中是否包含危险的路径模式
if (strpos($fileName, '../') !== false || strpos($fileName, '..\') !== false) {
$errors[] = "危险的文件路径(包含上级目录): $fileName";
$detailedLog[] = "ERROR: 危险路径 - $fileName";
continue;
}
// 检查是否为绝对路径
if (substr($fileName, 0, 1) === '/' || (strlen($fileName) > 1 && substr($fileName, 1, 1) === ':')) {
$errors[] = "危险的文件路径(绝对路径): $fileName";
$detailedLog[] = "ERROR: 绝对路径 - $fileName";
continue;
}
$targetPath = $extractDir . DIRECTORY_SEPARATOR . $fileName;
// 规范化路径分隔符
$targetPath = str_replace(['/', '\'], DIRECTORY_SEPARATOR, $targetPath);
// 如果是目录,跳过(目录会在创建文件时自动创建)
if (substr($fileName, -1) === '/') {
$skippedDirectories++;
$detailedLog[] = "SKIP: 目录 - $fileName";
continue;
}
// 确保目标路径在解压目录内
$realExtractDir = realpath($extractDir);
$targetDirPath = dirname($targetPath);
// 创建目录结构
if (!is_dir($targetDirPath)) {
if (!mkdir($targetDirPath, 0755, true)) {
$errors[] = "无法创建目录: $targetDirPath (文件: $fileName)";
$detailedLog[] = "ERROR: 创建目录失败 - $targetDirPath";
continue;
}
$detailedLog[] = "SUCCESS: 创建目录 - $targetDirPath";
}
// 再次验证路径安全性(在目录创建后)
$realTargetDir = realpath($targetDirPath);
if ($realTargetDir === false || strpos($realTargetDir, $realExtractDir) !== 0) {
$errors[] = "危险的文件路径(超出解压范围): $fileName";
$detailedLog[] = "ERROR: 路径验证失败 - $fileName";
continue;
}
// 解压文件
$fileContent = $zip->getFromIndex($i);
if ($fileContent === false) {
$errors[] = "无法提取文件内容: $fileName";
$detailedLog[] = "ERROR: 提取内容失败 - $fileName";
continue;
}
// 检查文件内容长度
if ($fileContent === '') {
// 空文件是正常的,继续处理
$detailedLog[] = "INFO: 空文件 - $fileName";
}
if (file_put_contents($targetPath, $fileContent) === false) {
$errors[] = "无法写入文件: $fileName (目标: $targetPath)";
$detailedLog[] = "ERROR: 写入失败 - $fileName";
continue;
}
$extractedFiles++;
$detailedLog[] = "SUCCESS: 解压成功 - $fileName";
}
$zip->close();
// 计算统计信息
$totalProcessed = $extractedFiles + $skippedDirectories + count($errors);
$actualFiles = $numFiles - $skippedDirectories; // 实际文件数(排除目录)
// 生成结果消息
$resultMessage = "解压完成!\n";
$resultMessage .= "压缩文件: $fileName\n";
$resultMessage .= "解压目录: " . basename($extractDir) . "\n";
$resultMessage .= "ZIP中总条目数: $numFiles 个\n";
$resultMessage .= "其中目录条目: $skippedDirectories 个\n";
$resultMessage .= "实际文件数: $actualFiles 个\n";
$resultMessage .= "成功解压文件: $extractedFiles 个\n";
$resultMessage .= "失败文件数: " . count($errors) . " 个";
if (!empty($errors)) {
$resultMessage .= "\n\n解压失败的文件:\n";
// 只显示前10个错误,避免消息过长
$displayErrors = array_slice($errors, 0, 10);
foreach ($displayErrors as $error) {
$resultMessage .= "• $error\n";
}
if (count($errors) > 10) {
$resultMessage .= "... 还有 " . (count($errors) - 10) . " 个错误未显示";
}
}
// 保存详细日志到文件(可选,用于调试)
$logFile = $extractDir . DIRECTORY_SEPARATOR . 'extraction_log.txt';
file_put_contents($logFile, implode("\n", $detailedLog));
echo json_encode([
'success' => true,
'message' => $resultMessage,
'extracted_files' => $extractedFiles,
'total_files' => $numFiles,
'actual_files' => $actualFiles,
'skipped_directories' => $skippedDirectories,
'extract_dir' => basename($extractDir),
'errors' => $errors,
'error_count' => count($errors),
'log_file' => 'extraction_log.txt'
]);
exit;
}
elseif ($_POST['action'] == 'get_directory_tree') {
// 获取目录树用于移动操作的目标选择
$rootDir = isset($_POST['root_dir']) ? $_POST['root_dir'] : $basePath;
$rootDir = realpath($rootDir);
// 安全检查
if (!$rootDir || strpos($rootDir, $basePath) !== 0) {
die(json_encode(['error' => '无效的根目录路径']));
}
function buildDirectoryTree($dir, $basePath, $maxDepth = 3, $currentDepth = 0) {
if ($currentDepth >= $maxDepth) {
return [];
}
$tree = [];
try {
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$itemPath = $dir . DIRECTORY_SEPARATOR . $item;
if (is_dir($itemPath)) {
$relativePath = str_replace($basePath, '', $itemPath);
$relativePath = ltrim($relativePath, DIRECTORY_SEPARATOR);
$node = [
'name' => $item,
'path' => $itemPath,
'relative_path' => $relativePath,
'children' => buildDirectoryTree($itemPath, $basePath, $maxDepth, $currentDepth + 1)
];
$tree[] = $node;
}
}
} catch (Exception $e) {
// 忽略权限错误等异常
}
// 按名称排序
usort($tree, function($a, $b) {
return strcmp($a['name'], $b['name']);
});
return $tree;
}
$tree = buildDirectoryTree($rootDir, $basePath);
echo json_encode([
'success' => true,
'root_path' => $rootDir,
'tree' => $tree
]);
exit;
}
// 如果没有匹配的action,返回错误
die(json_encode(['error' => '未知的操作类型']));
}
// 获取文件图标的函数
function getFileIcon($fileName) {
$ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$iconMap = [
// 文档类
'txt' => '📄', 'doc' => '📄', 'docx' => '📄', 'pdf' => '📕',
'rtf' => '📄', 'odt' => '📄',
// 表格类
'xls' => '📊', 'xlsx' => '📊', 'csv' => '📊', 'ods' => '📊',
// 演示文稿
'ppt' => '📊', 'pptx' => '📊', 'odp' => '📊',
// 图像类
'jpg' => '🖼️', 'jpeg' => '🖼️', 'png' => '🖼️', 'gif' => '🖼️',
'bmp' => '🖼️', 'svg' => '🖼️', 'ico' => '🖼️', 'webp' => '🖼️',
'tiff' => '🖼️', 'tif' => '🖼️',
// 音频类
'mp3' => '🎵', 'wav' => '🎵', 'flac' => '🎵', 'aac' => '🎵',
'ogg' => '🎵', 'm4a' => '🎵', 'wma' => '🎵',
// 视频类
'mp4' => '🎬', 'avi' => '🎬', 'mkv' => '🎬', 'mov' => '🎬',
'wmv' => '🎬', 'flv' => '🎬', 'webm' => '🎬', '3gp' => '🎬',
// 压缩文件
'zip' => '📦', 'rar' => '📦', '7z' => '📦', 'tar' => '📦',
'gz' => '📦', 'bz2' => '📦', 'xz' => '📦',
// 代码文件
'html' => '🌐', 'htm' => '🌐', 'css' => '🎨', 'js' => '⚡',
'php' => '🐘', 'py' => '🐍', 'java' => '☕', 'cpp' => '⚙️',
'c' => '⚙️', 'cs' => '⚙️', 'go' => '🐹', 'rs' => '🦀',
'rb' => '💎', 'swift' => '🦉', 'kt' => '🎯',
// 配置文件
'json' => '📋', 'xml' => '📋', 'yaml' => '📋', 'yml' => '📋',
'ini' => '⚙️', 'conf' => '⚙️', 'cfg' => '⚙️',
// 数据库
'sql' => '🗃️', 'db' => '🗃️', 'sqlite' => '🗃️',
// 日志文件
'log' => '📜',
// 字体文件
'ttf' => '🔤', 'otf' => '🔤', 'woff' => '🔤', 'woff2' => '🔤',
// 可执行文件
'exe' => '⚡', 'msi' => '⚡', 'deb' => '⚡', 'rpm' => '⚡',
'dmg' => '⚡', 'app' => '⚡',
// 其他常见格式
'md' => '📝', 'readme' => '📖', 'license' => '📜',
'gitignore' => '🚫', 'htaccess' => '⚙️'
];
return isset($iconMap[$ext]) ? $iconMap[$ext] : '📄';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网站文件压缩下载工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
}
.path-info {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
color: white;
word-break: break-all;
}
.main-content {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
}
/* 工具栏样式 */
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
align-items: center;
padding: 15px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10px;
border: 1px solid #dee2e6;
}
.toolbar-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 16px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
min-height: 38px;
}
.toolbar-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.toolbar-btn:active {
transform: translateY(0);
}
.toolbar-btn.success {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
}
.toolbar-btn.success:hover {
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
}
.toolbar-btn.warning {
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
color: #212529;
}
.toolbar-btn.warning:hover {
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.4);
}
.toolbar-btn.info {
background: linear-gradient(135deg, #17a2b8 0%, #6f42c1 100%);
}
.toolbar-btn.info:hover {
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.4);
}
.toolbar-btn.danger {
background: linear-gradient(135deg, #dc3545 0%, #e83e8c 100%);
}
.toolbar-btn.danger:hover {
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
}
.toolbar-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
/* 文件上传样式 */
.upload-input {
display: none;
}
.file-explorer {
position: relative;
}
.file-list {
list-style: none;
max-height: 600px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
}
.file-item {
display: flex;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.file-item:last-child {
border-bottom: none;
}
.file-item:hover {
background: #e6f7ff;
transform: translateX(3px);
}
.file-item.directory {
font-weight: 600;
color: #1890ff;
}
.file-item.directory:hover {
background: #f0f9ff;
}
.file-item.selected {
background: #bae7ff;
border-left: 4px solid #1890ff;
}
.file-icon {
margin-right: 10px;
font-size: 1.2em;
min-width: 25px;
}
.file-name {
flex: 1;
margin-right: 15px;
word-break: break-all;
}
.file-size {
color: #666;
font-size: 0.9em;
min-width: 80px;
text-align: right;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 25px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
margin: 5px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
}
.status {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
display: none;
animation: slideIn 0.3s ease;
}
.status.success {
background: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
}
.status.error {
background: #fff2f0;
border: 1px solid #ffccc7;
color: #ff4d4f;
}
.loading {
display: none;
text-align: center;
margin: 50px 0;
color: #666;
}
.loading.large-upload {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
.loading.large-upload .spinner {
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid white;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.breadcrumb {
display: flex;
align-items: center;
margin-bottom: 15px;
flex-wrap: wrap;
}
.breadcrumb-item {
cursor: pointer;
color: #1890ff;
}
.breadcrumb-item:hover {
text-decoration: underline;
}
.breadcrumb-separator {
margin: 0 8px;
color: #999;
}
/* 右键菜单样式 */
.context-menu {
position: fixed;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 5px 0;
z-index: 1000;
display: none;
min-width: 150px;
}
.context-menu-item {
padding: 10px 15px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 14px;
}
.context-menu-item:hover {
background-color: #f5f5f5;
}
.context-menu-item.disabled {
color: #ccc;
cursor: not-allowed;
}
.context-menu-item.disabled:hover {
background-color: transparent;
}
/* 编辑弹窗样式 */
.edit-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
}
.edit-modal-content {
background-color: white;
margin: 2.5% auto;
padding: 0;
border-radius: 8px;
width: 95%;
height: 95%;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
position: relative;
}
.edit-modal-header {
padding: 15px 20px;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
color: white;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 60px;
flex-shrink: 0;
}
.edit-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.edit-modal-close {
font-size: 32px;
font-weight: bold;
cursor: pointer;
line-height: 1;
padding: 5px;
border-radius: 50%;
transition: all 0.3s ease;
}
.edit-modal-close:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.edit-modal-body {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#file-content {
width: 100%;
height: 100%;
border: 2px solid #e0e0e0;
border-radius: 6px;
padding: 15px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
resize: none;
outline: none;
background-color: #f8f9fa;
transition: all 0.3s ease;
}
#file-content:focus {
border-color: #1890ff;
box-shadow: 0 0 10px rgba(24, 144, 255, 0.3);
background-color: #ffffff;
}
.edit-modal-footer {
padding: 15px 20px;
border-top: 2px solid #f0f0f0;
display: flex;
gap: 15px;
justify-content: flex-end;
flex-shrink: 0;
background-color: #fafafa;
border-radius: 0 0 8px 8px;
}
.edit-modal-footer .btn {
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.edit-modal-footer .btn-primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
}
.edit-modal-footer .btn-primary:hover {
background: linear-gradient(135deg, #096dd9 0%, #0050b3 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
}
.edit-modal-footer .btn:not(.btn-primary) {
background-color: #f5f5f5;
color: #666;
border: 1px solid #d9d9d9;
}
.edit-modal-footer .btn:not(.btn-primary):hover {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
/* 统一消息弹窗样式 */
.message-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 10000;
display: none;
justify-content: center;
align-items: center;
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
}
.message-modal {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
transform: scale(0.8);
animation: modalSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.message-header {
padding: 25px 30px 15px;
border-bottom: 2px solid #f0f0f0;
display: flex;
align-items: center;
gap: 15px;
}
.message-icon {
font-size: 32px;
min-width: 40px;
}
.message-icon.success { color: #52c41a; }
.message-icon.error { color: #ff4d4f; }
.message-icon.warning { color: #faad14; }
.message-icon.info { color: #1890ff; }
.message-icon.confirm { color: #722ed1; }
.message-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
}
.message-body {
padding: 20px 30px;
font-size: 16px;
line-height: 1.6;
color: #666;
white-space: pre-wrap;
word-wrap: break-word;
}
.message-footer {
padding: 15px 30px 25px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.message-btn {
padding: 10px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 80px;
}
.message-btn-primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
}
.message-btn-primary:hover {
background: linear-gradient(135deg, #096dd9 0%, #0050b3 100%);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
}
.message-btn-success {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: white;
}
.message-btn-success:hover {
background: linear-gradient(135deg, #389e0d 0%, #237804 100%);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(82, 196, 26, 0.4);
}
.message-btn-danger {
background: linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%);
color: white;
}
.message-btn-danger:hover {
background: linear-gradient(135deg, #cf1322 0%, #a8071a 100%);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(255, 77, 79, 0.4);
}
.message-btn-secondary {
background: #f5f5f5;
color: #666;
border: 1px solid #d9d9d9;
}
.message-btn-secondary:hover {
background: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
transform: translateY(-1px);
}
.message-btn:active {
transform: translateY(0);
}
/* 动画效果 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes modalSlideOut {
from {
opacity: 1;
transform: scale(1) translateY(0);
}
to {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
}
.message-modal.closing {
animation: modalSlideOut 0.3s ease forwards;
}
/* 加载动画 */
.message-loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-radius: 50%;
border-top-color: #1890ff;
animation: spin 1s ease-in-out infinite;
margin-right: 8px;
}
/* 输入模态框样式 */
.input-modal {
display: none;
position: fixed;
z-index: 10001;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
}
.input-modal-content {
background: white;
margin: 10% auto;
padding: 0;
border-radius: 16px;
width: 90%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.input-modal-header {
padding: 25px 30px 15px;
border-bottom: 2px solid #f0f0f0;
display: flex;
align-items: center;
gap: 15px;
}
.input-modal-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0;
}
.input-modal-body {
padding: 20px 30px;
}
.input-group {
margin-bottom: 20px;
}
.input-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.input-field {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
outline: none;
}
.input-field:focus {
border-color: #1890ff;
box-shadow: 0 0 10px rgba(24, 144, 255, 0.3);
}
.input-modal-footer {
padding: 15px 30px 25px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 目录选择弹窗样式 */
.directory-modal {
display: none;
position: fixed;
z-index: 9998;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
}
.directory-modal-content {
background-color: white;
margin: 5% auto;
padding: 0;
border-radius: 12px;
width: 90%;
max-width: 700px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
position: relative;
}
.directory-modal-header {
padding: 20px 25px;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: white;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 60px;
flex-shrink: 0;
}
.directory-modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.directory-modal-close {
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
padding: 5px;
border-radius: 50%;
transition: all 0.3s ease;
}
.directory-modal-close:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.directory-modal-body {
flex: 1;
padding: 20px 25px;
overflow-y: auto;
min-height: 300px;
max-height: 50vh;
}
.directory-tree {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
}
.loading-tree {
text-align: center;
padding: 40px 20px;
color: #666;
}
.loading-tree .spinner {
width: 30px;
height: 30px;
margin: 0 auto 15px;
}
.tree-node {
margin: 2px 0;
user-select: none;
}
.tree-item {
display: flex;
align-items: center;
padding: 8px 12px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s ease;
position: relative;
}
.tree-item:hover {
background-color: #f0f9ff;
transform: translateX(3px);
}
.tree-item.selected {
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
border-left: 4px solid #1890ff;
color: #0050b3;
font-weight: 600;
}
.tree-toggle {
margin-right: 8px;
font-size: 12px;
cursor: pointer;
transition: transform 0.2s ease;
color: #666;
min-width: 16px;
text-align: center;
}
.tree-toggle.expanded {
transform: rotate(90deg);
}
.tree-toggle.no-children {
visibility: hidden;
}
.tree-icon {
margin-right: 8px;
font-size: 16px;
}
.tree-name {
flex: 1;
}
.tree-children {
margin-left: 24px;
border-left: 1px dotted #d9d9d9;
padding-left: 8px;
display: none;
}
.tree-children.expanded {
display: block;
}
.directory-modal-footer {
padding: 20px 25px;
border-top: 2px solid #f0f0f0;
background-color: #fafafa;
border-radius: 0 0 12px 12px;
flex-shrink: 0;
}
.selected-path {
margin-bottom: 15px;
padding: 12px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
}
.selected-path strong {
color: #333;
}
.selected-path span {
color: #1890ff;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
word-break: break-all;
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.modal-buttons .btn {
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.modal-buttons .btn-primary {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: white;
}
.modal-buttons .btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, #389e0d 0%, #237804 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4);
}
.modal-buttons .btn-primary:disabled {
background: #f5f5f5;
color: #bfbfbf;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.modal-buttons .btn-secondary {
background-color: #f5f5f5;
color: #666;
border: 1px solid #d9d9d9;
}
.modal-buttons .btn-secondary:hover {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
transform: translateY(-1px);
}
/* 响应式设计 */
[url=home.php?mod=space&uid=945662]@media[/url] (max-width: 768px) {
.toolbar {
flex-direction: column;
gap: 8px;
}
.toolbar-btn {
width: 100%;
justify-content: center;
}
.edit-modal-content {
width: 98%;
height: 98%;
margin: 1% auto;
}
.edit-modal-header {
padding: 10px 15px;
min-height: 50px;
}
.edit-modal-header h3 {
font-size: 16px;
}
.edit-modal-close {
font-size: 28px;
}
.edit-modal-body {
padding: 15px;
}
#file-content {
font-size: 12px;
padding: 10px;
}
.edit-modal-footer {
padding: 10px 15px;
flex-direction: column;
}
.edit-modal-footer .btn {
width: 100%;
margin-bottom: 10px;
}
.message-modal {
width: 95%;
margin: 10px;
}
.message-header {
padding: 20px 20px 10px;
}
.message-icon {
font-size: 28px;
min-width: 32px;
}
.message-title {
font-size: 18px;
}
.message-body {
padding: 15px 20px;
font-size: 15px;
}
.message-footer {
padding: 10px 20px 20px;
flex-direction: column;
}
.message-btn {
width: 100%;
margin-bottom: 8px;
}
.message-btn:last-child {
margin-bottom: 0;
}
.input-modal-content {
width: 95%;
margin: 5% auto;
}
.directory-modal-content {
width: 95%;
height: 90vh;
margin: 5vh auto;
}
.directory-modal-header {
padding: 15px 20px;
}
.directory-modal-body {
padding: 15px 20px;
max-height: 60vh;
}
.directory-modal-footer {
padding: 15px 20px;
}
.modal-buttons {
flex-direction: column;
}
.modal-buttons .btn {
width: 100%;
margin-bottom: 10px;
}
.modal-buttons .btn:last-child {
margin-bottom: 0;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>网站文件压缩下载工具</h1>
<p class="subtitle">文件管理、上传下载、编辑压缩一体化工具</p>
</header>
<div class="path-info">
<strong>当前服务器绝对路径:</strong> <?php echo $absolutePath; ?>
</div>
<div class="main-content">
<div class="file-explorer">
<!-- 工具栏 -->
<div class="toolbar">
<button class="toolbar-btn success" id="upload-btn">
📤 上传文件
</button>
<button class="toolbar-btn" id="download-selected-btn" disabled>
📥 下载选中
</button>
<button class="toolbar-btn info" id="remote-download-btn">
🌐 远程下载
</button>
<button class="toolbar-btn warning" id="create-folder-btn">
📁 新建文件夹
</button>
<button class="toolbar-btn warning" id="create-file-btn">
📄 新建文件
</button>
</div>
<div class="breadcrumb" id="breadcrumb">
<span class="breadcrumb-item" data-path="<?php echo $basePath; ?>">根目录</span>
</div>
<ul class="file-list" id="file-list">
<li>加载中...</li>
</ul>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>处理中,请稍候...</p>
</div>
<div class="status" id="status"></div>
</div>
</div>
<!-- 文件上传隐藏输入框 -->
<input type="file" id="upload-input" class="upload-input" multiple>
<!-- 右键菜单 -->
<div class="context-menu" id="context-menu">
<div class="context-menu-item" id="compress-item">📦 压缩</div>
<div class="context-menu-item" id="extract-item">📂 解压</div>
<div class="context-menu-item" id="download-item">⬇️ 下载</div>
<div class="context-menu-item" id="edit-item">📝 编辑</div>
<div class="context-menu-item" id="move-item">✂️ 移动</div>
<div class="context-menu-item" id="delete-item">🗑️ 删除</div>
</div>
<!-- 目录选择弹窗(用于移动操作) -->
<div class="directory-modal" id="directory-modal" tabindex="-1">
<div class="directory-modal-content">
<div class="directory-modal-header">
<h3 id="directory-modal-title">选择目标目录</h3>
<span class="directory-modal-close" id="directory-modal-close" title="关闭 (ESC)">×</span>
</div>
<div class="directory-modal-body">
<div class="directory-tree" id="directory-tree">
<div class="loading-tree">
<div class="spinner"></div>
<p>正在加载目录树...</p>
</div>
</div>
</div>
<div class="directory-modal-footer">
<div class="selected-path">
<strong>选中路径:</strong>
<span id="selected-target-path">未选择</span>
</div>
<div class="modal-buttons">
<button class="btn btn-secondary" id="cancel-move" title="取消移动 (ESC)">❌ 取消</button>
<button class="btn btn-primary" id="confirm-move" title="确认移动" disabled>✂️ 移动到此位置</button>
</div>
</div>
</div>
</div>
<!-- 文件编辑弹窗 -->
<div class="edit-modal" id="edit-modal" tabindex="-1">
<div class="edit-modal-content">
<div class="edit-modal-header">
<h3 id="edit-file-name">编辑文件</h3>
<span class="edit-modal-close" id="edit-modal-close" title="关闭 (ESC)">×</span>
</div>
<div class="edit-modal-body">
<textarea id="file-content" placeholder="文件内容将在此处显示..." spellcheck="false"></textarea>
</div>
<div class="edit-modal-footer">
<button class="btn btn-primary" id="save-file" title="保存文件 (Ctrl+S)">💾 保存</button>
<button class="btn" id="cancel-edit" title="取消编辑 (ESC)">❌ 取消</button>
</div>
</div>
</div>
<!-- 统一消息弹窗 -->
<div class="message-overlay" id="messageOverlay">
<div class="message-modal" id="messageModal">
<div class="message-header">
<span class="message-icon" id="messageIcon">ℹ️</span>
<h3 class="message-title" id="messageTitle">提示</h3>
</div>
<div class="message-body" id="messageBody">
消息内容
</div>
<div class="message-footer" id="messageFooter">
<button class="message-btn message-btn-primary" id="messageOkBtn">确定</button>
</div>
</div>
</div>
<!-- 输入弹窗 -->
<div class="input-modal" id="inputModal">
<div class="input-modal-content">
<div class="input-modal-header">
<span class="message-icon" id="inputIcon">📝</span>
<h3 class="input-modal-title" id="inputTitle">输入信息</h3>
</div>
<div class="input-modal-body">
<div class="input-group">
<label class="input-label" id="inputLabel">请输入:</label>
<input type="text" class="input-field" id="inputField" placeholder="请输入...">
</div>
</div>
<div class="input-modal-footer">
<button class="message-btn message-btn-secondary" id="inputCancelBtn">取消</button>
<button class="message-btn message-btn-primary" id="inputOkBtn">确定</button>
</div>
</div>
</div>
</div>
<script>
// 统一消息弹窗系统
class MessageModal {
constructor() {
this.overlay = document.getElementById('messageOverlay');
this.modal = document.getElementById('messageModal');
this.icon = document.getElementById('messageIcon');
this.title = document.getElementById('messageTitle');
this.body = document.getElementById('messageBody');
this.footer = document.getElementById('messageFooter');
this.okBtn = document.getElementById('messageOkBtn');
this.currentResolve = null;
this.isVisible = false;
this.init();
}
init() {
// 点击遮罩层关闭(可选)
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.close(false);
}
});
// ESC键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isVisible) {
e.preventDefault();
this.close(false);
}
});
}
show(options = {}) {
return new Promise((resolve) => {
this.currentResolve = resolve;
const {
type = 'info',
title = '提示',
message = '',
showCancel = false,
okText = '确定',
cancelText = '取消',
allowEscapeKey = true
} = options;
// 设置图标和样式
const iconMap = {
success: { icon: '✅', class: 'success' },
error: { icon: '❌', class: 'error' },
warning: { icon: '⚠️', class: 'warning' },
info: { icon: 'ℹ️', class: 'info' },
confirm: { icon: '❓', class: 'confirm' }
};
const iconInfo = iconMap[type] || iconMap.info;
this.icon.textContent = iconInfo.icon;
this.icon.className = `message-icon ${iconInfo.class}`;
// 设置标题和内容
this.title.textContent = title;
this.body.textContent = message;
// 设置按钮
this.footer.innerHTML = '';
if (showCancel) {
const cancelBtn = document.createElement('button');
cancelBtn.className = 'message-btn message-btn-secondary';
cancelBtn.textContent = cancelText;
cancelBtn.onclick = () => this.close(false);
this.footer.appendChild(cancelBtn);
}
const okBtn = document.createElement('button');
okBtn.className = `message-btn ${type === 'error' ? 'message-btn-danger' :
type === 'success' ? 'message-btn-success' :
'message-btn-primary'}`;
okBtn.textContent = okText;
okBtn.onclick = () => this.close(true);
this.footer.appendChild(okBtn);
// 显示弹窗
this.overlay.style.display = 'flex';
this.isVisible = true;
// 聚焦到确定按钮
setTimeout(() => {
okBtn.focus();
}, 100);
});
}
close(result) {
if (!this.isVisible) return;
this.modal.classList.add('closing');
setTimeout(() => {
this.overlay.style.display = 'none';
this.modal.classList.remove('closing');
this.isVisible = false;
if (this.currentResolve) {
this.currentResolve(result);
this.currentResolve = null;
}
}, 300);
}
// 简化的方法
alert(message, title = '提示', type = 'info') {
return this.show({
type,
title,
message,
showCancel: false
});
}
confirm(message, title = '确认', type = 'confirm') {
return this.show({
type,
title,
message,
showCancel: true,
okText: '确定',
cancelText: '取消'
});
}
success(message, title = '成功') {
return this.alert(message, title, 'success');
}
error(message, title = '错误') {
return this.alert(message, title, 'error');
}
warning(message, title = '警告') {
return this.alert(message, title, 'warning');
}
}
// 输入弹窗系统
class InputModal {
constructor() {
this.modal = document.getElementById('inputModal');
this.icon = document.getElementById('inputIcon');
this.title = document.getElementById('inputTitle');
this.label = document.getElementById('inputLabel');
this.field = document.getElementById('inputField');
this.okBtn = document.getElementById('inputOkBtn');
this.cancelBtn = document.getElementById('inputCancelBtn');
this.currentResolve = null;
this.isVisible = false;
this.init();
}
init() {
// 点击取消按钮
this.cancelBtn.addEventListener('click', () => this.close(null));
// 点击确定按钮
this.okBtn.addEventListener('click', () => {
const value = this.field.value.trim();
this.close(value);
});
// 回车键确定
this.field.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const value = this.field.value.trim();
this.close(value);
} else if (e.key === 'Escape') {
e.preventDefault();
this.close(null);
}
});
// 点击遮罩层关闭
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) {
this.close(null);
}
});
}
show(options = {}) {
return new Promise((resolve) => {
this.currentResolve = resolve;
const {
title = '输入信息',
label = '请输入:',
placeholder = '请输入...',
defaultValue = '',
icon = '📝'
} = options;
// 设置内容
this.title.textContent = title;
this.label.textContent = label;
this.field.placeholder = placeholder;
this.field.value = defaultValue;
this.icon.textContent = icon;
// 显示弹窗
this.modal.style.display = 'flex';
this.isVisible = true;
// 聚焦到输入框
setTimeout(() => {
this.field.focus();
this.field.select();
}, 100);
});
}
close(result) {
if (!this.isVisible) return;
this.modal.style.display = 'none';
this.isVisible = false;
this.field.value = '';
if (this.currentResolve) {
this.currentResolve(result);
this.currentResolve = null;
}
}
}
// 创建全局实例
const messageModal = new MessageModal();
const inputModal = new InputModal();
// 重写全局alert和confirm函数
window.alert = function(message) {
return messageModal.alert(message);
};
window.confirm = function(message) {
return messageModal.confirm(message);
};
document.addEventListener('DOMContentLoaded', function() {
const fileList = document.getElementById('file-list');
const breadcrumb = document.getElementById('breadcrumb');
const loading = document.getElementById('loading');
const status = document.getElementById('status');
const contextMenu = document.getElementById('context-menu');
const compressItem = document.getElementById('compress-item');
const extractItem = document.getElementById('extract-item');
const downloadItem = document.getElementById('download-item');
const editItem = document.getElementById('edit-item');
const deleteItem = document.getElementById('delete-item');
const moveItem = document.getElementById('move-item');
// 工具栏按钮
const uploadBtn = document.getElementById('upload-btn');
const downloadSelectedBtn = document.getElementById('download-selected-btn');
const remoteDownloadBtn = document.getElementById('remote-download-btn');
const createFolderBtn = document.getElementById('create-folder-btn');
const createFileBtn = document.getElementById('create-file-btn');
const uploadInput = document.getElementById('upload-input');
// 编辑相关元素
const editModal = document.getElementById('edit-modal');
const editFileName = document.getElementById('edit-file-name');
const fileContent = document.getElementById('file-content');
const saveFileBtn = document.getElementById('save-file');
const cancelEditBtn = document.getElementById('cancel-edit');
const editModalClose = document.getElementById('edit-modal-close');
// 目录选择相关元素
const directoryModal = document.getElementById('directory-modal');
const directoryModalTitle = document.getElementById('directory-modal-title');
const directoryTree = document.getElementById('directory-tree');
const selectedTargetPath = document.getElementById('selected-target-path');
const confirmMoveBtn = document.getElementById('confirm-move');
const cancelMoveBtn = document.getElementById('cancel-move');
const directoryModalClose = document.getElementById('directory-modal-close');
let currentPath = '<?php echo $basePath; ?>';
let currentSelectedPath = '';
let currentSelectedIsDir = false;
let selectedFiles = new Set();
let selectedMoveTarget = '';
let moveSourcePath = '';
let moveSourceIsDir = false;
// 工具栏按钮事件
uploadBtn.addEventListener('click', () => {
uploadInput.click();
});
uploadInput.addEventListener('change', handleFileUpload);
downloadSelectedBtn.addEventListener('click', () => {
if (selectedFiles.size === 1) {
const filePath = Array.from(selectedFiles)[0];
downloadFile(filePath);
}
});
remoteDownloadBtn.addEventListener('click', handleRemoteDownload);
createFolderBtn.addEventListener('click', handleCreateFolder);
createFileBtn.addEventListener('click', handleCreateFile);
// 文件上传处理
function handleFileUpload(event) {
const files = event.target.files;
if (files.length === 0) return;
Array.from(files).forEach(file => {
// 判断文件大小,大于15MB使用分块上传
if (file.size > 15 * 1024 * 1024) {
uploadLargeFile(file);
} else {
uploadSingleFile(file);
}
});
// 清空input
uploadInput.value = '';
}
function uploadSingleFile(file) {
const formData = new FormData();
formData.append('action', 'upload_file');
formData.append('upload_dir', currentPath);
formData.append('file', file);
loading.style.display = 'block';
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
if (data.error) {
messageModal.error('上传失败:' + data.error);
return;
}
messageModal.success(data.message, '上传成功').then(() => {
loadDirectory(currentPath);
});
})
.catch(error => {
loading.style.display = 'none';
messageModal.error('上传时出错: ' + error);
});
}
// 大文件分块上传需要分多少M上传在这里设置就行
function uploadLargeFile(file) {
const chunkSize = 10 * 1024 * 1024; // 10MB 分块大小 可以分30等等
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
messageModal.show({
type: 'info',
title: '大文件上传',
message: `文件大小: ${formatFileSize(file.size)}\n将采用分块上传模式,请耐心等待...`,
showCancel: false,
okText: '确定'
}).then(() => {
startChunkUpload(file, chunkSize, totalChunks, fileId, 0);
});
}
function startChunkUpload(file, chunkSize, totalChunks, fileId, chunkIndex) {
if (chunkIndex >= totalChunks) {
return;
}
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('action', 'chunk_upload');
formData.append('upload_dir', currentPath);
formData.append('fileName', file.name);
formData.append('fileId', fileId);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('chunk', chunk);
// 显示上传进度
loading.style.display = 'block';
const progressText = `上传进度: ${chunkIndex + 1}/${totalChunks} (${Math.round(((chunkIndex + 1) / totalChunks) * 100)}%)`;
document.querySelector('#loading p').textContent = progressText;
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
loading.style.display = 'none';
messageModal.error('分块上传失败:' + data.error);
return;
}
if (data.completed) {
// 上传完成
loading.style.display = 'none';
document.querySelector('#loading p').textContent = '处理中,请稍候...';
messageModal.success(data.message + '\n文件大小: ' + formatFileSize(data.size), '上传完成').then(() => {
loadDirectory(currentPath);
});
} else {
// 继续上传下一个分块
setTimeout(() => {
startChunkUpload(file, chunkSize, totalChunks, fileId, chunkIndex + 1);
}, 100); // 稍作延迟避免服务器压力
}
})
.catch(error => {
loading.style.display = 'none';
document.querySelector('#loading p').textContent = '处理中,请稍候...';
messageModal.error('分块上传时出错: ' + error);
});
}
// 远程下载处理
function handleRemoteDownload() {
inputModal.show({
title: '远程下载',
label: '请输入下载链接:',
placeholder: 'https://example.com/file.zip',
icon: '🌐'
}).then((url) => {
if (!url) return;
const formData = new FormData();
formData.append('action', 'remote_download');
formData.append('url', url);
loading.style.display = 'block';
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
if (data.error) {
messageModal.error('下载失败:' + data.error);
return;
}
messageModal.success(data.message, '下载成功').then(() => {
loadDirectory(currentPath);
});
})
.catch(error => {
loading.style.display = 'none';
messageModal.error('下载时出错: ' + error);
});
});
}
// 创建文件夹处理
function handleCreateFolder() {
inputModal.show({
title: '新建文件夹',
label: '文件夹名称:',
placeholder: '新建文件夹',
icon: '📁'
}).then((folderName) => {
if (!folderName) return;
const formData = new FormData();
formData.append('action', 'create_folder');
formData.append('current_dir', currentPath);
formData.append('folder_name', folderName);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
messageModal.error('创建失败:' + data.error);
return;
}
messageModal.success(data.message, '创建成功').then(() => {
loadDirectory(currentPath);
});
})
.catch(error => {
messageModal.error('创建时出错: ' + error);
});
});
}
// 创建文件处理
function handleCreateFile() {
inputModal.show({
title: '新建文件',
label: '文件名称:',
placeholder: 'newfile.txt',
icon: '📄'
}).then((fileName) => {
if (!fileName) return;
const formData = new FormData();
formData.append('action', 'create_file');
formData.append('current_dir', currentPath);
formData.append('file_name', fileName);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
messageModal.error('创建失败:' + data.error);
return;
}
messageModal.success(data.message, '创建成功').then(() => {
loadDirectory(currentPath);
});
})
.catch(error => {
messageModal.error('创建时出错: ' + error);
});
});
}
// 更新下载按钮状态
function updateDownloadButtonState() {
if (selectedFiles.size === 1) {
const filePath = Array.from(selectedFiles)[0];
// 检查是否为文件(非目录)
const fileItem = document.querySelector(`[data-path="${filePath}"]`);
const isDir = fileItem ? fileItem.getAttribute('data-is-dir') === 'true' : false;
downloadSelectedBtn.disabled = isDir;
downloadSelectedBtn.textContent = isDir ? '📥 下载选中 (目录请用压缩)' : '📥 下载选中';
} else {
downloadSelectedBtn.disabled = true;
downloadSelectedBtn.textContent = '📥 下载选中';
}
}
// 隐藏右键菜单
function hideContextMenu() {
contextMenu.style.display = 'none';
}
// 显示右键菜单
function showContextMenu(e, path, isDir) {
e.preventDefault();
currentSelectedPath = path;
currentSelectedIsDir = isDir;
// 设置菜单项状态
compressItem.classList.remove('disabled');
deleteItem.classList.remove('disabled');
moveItem.classList.remove('disabled');
// 检查是否为压缩文件
const fileName = path.split(/[\/]/).pop();
const ext = getFileExtension(fileName);
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'];
const isArchive = !isDir && archiveExts.includes(ext);
if (isArchive) {
extractItem.classList.remove('disabled');
if (ext === 'zip') {
extractItem.textContent = '📂 解压';
} else {
extractItem.textContent = `📂 解压 (仅支持ZIP)`;
extractItem.classList.add('disabled');
}
} else {
extractItem.classList.add('disabled');
extractItem.textContent = '📂 解压 (非压缩文件)';
}
if (isDir) {
downloadItem.classList.add('disabled');
downloadItem.textContent = '⬇️ 下载 (目录请用压缩)';
editItem.classList.add('disabled');
editItem.textContent = '📝 编辑 (不支持目录)';
} else {
downloadItem.classList.remove('disabled');
downloadItem.textContent = '⬇️ 下载';
// 检查是否为可编辑的文件类型
const editableExts = ['txt', 'html', 'htm', 'css', 'js', 'php', 'py', 'java', 'cpp', 'c', 'cs', 'xml', 'json', 'md', 'log', 'sql', 'ini', 'conf', 'yaml', 'yml'];
if (editableExts.includes(ext)) {
editItem.classList.remove('disabled');
editItem.textContent = '📝 编辑';
} else {
editItem.classList.add('disabled');
editItem.textContent = '📝 编辑 (不支持此格式)';
}
}
// 先显示菜单以获取其尺寸
contextMenu.style.display = 'block';
contextMenu.style.visibility = 'hidden';
// 获取菜单尺寸和视窗尺寸
const menuWidth = contextMenu.offsetWidth;
const menuHeight = contextMenu.offsetHeight;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 计算菜单位置
let left = e.clientX + 10; // 鼠标右侧10px
let top = e.clientY - 10; // 鼠标位置稍微向上
// 防止菜单超出右边界
if (left + menuWidth > windowWidth) {
left = e.clientX - menuWidth - 10; // 显示在鼠标左侧
}
// 防止菜单超出下边界
if (top + menuHeight > windowHeight) {
top = windowHeight - menuHeight - 10;
}
// 防止菜单超出上边界
if (top < 0) {
top = 10;
}
// 防止菜单超出左边界
if (left < 0) {
left = 10;
}
// 设置最终位置并显示菜单
contextMenu.style.left = left + 'px';
contextMenu.style.top = top + 'px';
contextMenu.style.visibility = 'visible';
}
// 点击其他地方隐藏菜单
document.addEventListener('click', hideContextMenu);
// 右键菜单项点击事件
compressItem.addEventListener('click', function() {
if (currentSelectedPath) {
compressFile(currentSelectedPath);
}
hideContextMenu();
});
extractItem.addEventListener('click', function() {
if (currentSelectedPath && !currentSelectedIsDir && !extractItem.classList.contains('disabled')) {
extractArchive(currentSelectedPath);
}
hideContextMenu();
});
downloadItem.addEventListener('click', function() {
if (currentSelectedPath && !currentSelectedIsDir) {
downloadFile(currentSelectedPath);
}
hideContextMenu();
});
editItem.addEventListener('click', function() {
if (currentSelectedPath && !currentSelectedIsDir && !editItem.classList.contains('disabled')) {
editFile(currentSelectedPath);
}
hideContextMenu();
});
moveItem.addEventListener('click', function() {
if (currentSelectedPath) {
showMoveDialog(currentSelectedPath, currentSelectedIsDir);
}
hideContextMenu();
});
deleteItem.addEventListener('click', function() {
if (currentSelectedPath) {
deleteItem_func(currentSelectedPath, currentSelectedIsDir);
}
hideContextMenu();
});
// 编辑弹窗事件
editModalClose.addEventListener('click', closeEditModal);
cancelEditBtn.addEventListener('click', closeEditModal);
saveFileBtn.addEventListener('click', saveFile);
// 目录选择弹窗事件
directoryModalClose.addEventListener('click', closeMoveDialog);
cancelMoveBtn.addEventListener('click', closeMoveDialog);
confirmMoveBtn.addEventListener('click', performMove);
// 添加键盘快捷键支持
document.addEventListener('keydown', function(e) {
// ESC键关闭模态框
if (e.key === 'Escape' && editModal.style.display === 'block') {
e.preventDefault();
closeEditModal();
}
// ESC键关闭移动对话框
if (e.key === 'Escape' && directoryModal.style.display === 'block') {
e.preventDefault();
closeMoveDialog();
}
// Ctrl+S保存文件
if (e.ctrlKey && e.key === 's' && editModal.style.display === 'block') {
e.preventDefault();
saveFile();
}
});
// 点击弹窗外部关闭
editModal.addEventListener('click', function(e) {
if (e.target === editModal) {
closeEditModal();
}
});
// 点击目录选择弹窗外部关闭
directoryModal.addEventListener('click', function(e) {
if (e.target === directoryModal) {
closeMoveDialog();
}
});
// 为文本框添加Tab键支持(插入4个空格)
fileContent.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
// 插入4个空格
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
// 移动光标
this.selectionStart = this.selectionEnd = start + 4;
}
});
// 解压文件
function extractArchive(path) {
const fileName = path.split(/[\/]/).pop();
messageModal.confirm(
`确定要解压文件 "${fileName}" 吗?\n\n文件将被解压到当前目录中,如果存在同名文件夹会自动重命名。`,
'确认解压',
'confirm'
).then((confirmed) => {
if (!confirmed) return;
loading.style.display = 'block';
status.style.display = 'none';
const formData = new FormData();
formData.append('action', 'extract_archive');
formData.append('path', path);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
if (data.error) {
messageModal.error('解压失败:' + data.error);
return;
}
// 显示详细的解压结果
let resultMessage = data.message;
if (data.errors && data.errors.length > 0) {
messageModal.warning(resultMessage, '解压完成(部分文件失败)').then(() => {
loadDirectory(currentPath);
});
} else {
messageModal.success(resultMessage, '解压成功').then(() => {
loadDirectory(currentPath);
});
}
})
.catch(error => {
loading.style.display = 'none';
messageModal.error('解压时出错: ' + error);
});
});
}
// 压缩文件/目录
function compressFile(path) {
loading.style.display = 'block';
status.style.display = 'none';
const formData = new FormData();
formData.append('action', 'compress_single');
formData.append('path', path);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
if (data.error) {
messageModal.error(data.error);
return;
}
messageModal.success(data.message).then(() => {
// 刷新当前目录以显示新创建的压缩文件
loadDirectory(currentPath);
});
})
.catch(error => {
loading.style.display = 'none';
messageModal.error('压缩时出错: ' + error);
});
}
// 显示移动对话框
function showMoveDialog(sourcePath, isDir) {
moveSourcePath = sourcePath;
moveSourceIsDir = isDir;
selectedMoveTarget = '';
const fileName = sourcePath.split(/[\/]/).pop();
const itemType = isDir ? '文件夹' : '文件';
directoryModalTitle.textContent = `移动${itemType}: ${fileName}`;
selectedTargetPath.textContent = '未选择';
confirmMoveBtn.disabled = true;
// 显示模态框
directoryModal.style.display = 'block';
document.body.style.overflow = 'hidden';
// 加载目录树
loadDirectoryTree();
// 聚焦到模态框
setTimeout(() => {
directoryModal.focus();
}, 100);
}
// 加载目录树
function loadDirectoryTree() {
directoryTree.innerHTML = `
<div class="loading-tree">
<div class="spinner"></div>
<p>正在加载目录树...</p>
</div>
`;
const formData = new FormData();
formData.append('action', 'get_directory_tree');
formData.append('root_dir', '<?php echo $basePath; ?>');
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
directoryTree.innerHTML = `
<div style="text-align: center; padding: 20px; color: #ff4d4f;">
<p>❌ 加载目录树失败</p>
<p>${data.error}</p>
</div>
`;
return;
}
renderDirectoryTree(data.tree, data.root_path);
})
.catch(error => {
directoryTree.innerHTML = `
<div style="text-align: center; padding: 20px; color: #ff4d4f;">
<p>❌ 加载目录树时出错</p>
<p>${error}</p>
</div>
`;
});
}
// 渲染目录树
function renderDirectoryTree(tree, rootPath) {
// 添加根目录选项
let html = `
<div class="tree-node">
<div class="tree-item" data-path="${rootPath}" data-level="0">
<span class="tree-toggle no-children">📁</span>
<span class="tree-icon">🏠</span>
<span class="tree-name">根目录</span>
</div>
</div>
`;
// 递归渲染子目录
html += renderTreeNodes(tree, 1);
directoryTree.innerHTML = html;
// 添加点击事件
directoryTree.querySelectorAll('.tree-item').forEach(item => {
const path = item.getAttribute('data-path');
item.addEventListener('click', function(e) {
e.stopPropagation();
// 移除所有选中状态
directoryTree.querySelectorAll('.tree-item').forEach(i => i.classList.remove('selected'));
// 选中当前项
this.classList.add('selected');
selectedMoveTarget = path;
// 显示相对路径
const basePath = '<?php echo $basePath; ?>';
const relativePath = path === basePath ? '根目录' : path.replace(basePath, '').replace(/^[\/]+/, '');
selectedTargetPath.textContent = relativePath || '根目录';
// 检查是否可以移动(防止移动到自身或子目录)
let canMove = true;
if (moveSourceIsDir && path !== moveSourcePath) {
// 检查是否试图移动到自身的子目录
if (path.startsWith(moveSourcePath + '/') || path.startsWith(moveSourcePath + '\')) {
canMove = false;
}
}
// 不能移动到自身
if (path === moveSourcePath) {
canMove = false;
}
confirmMoveBtn.disabled = !canMove;
if (!canMove) {
selectedTargetPath.innerHTML = `<span style="color: #ff4d4f;">${relativePath || '根目录'} (不能移动到此位置)</span>`;
}
});
});
// 添加展开/折叠功能
directoryTree.querySelectorAll('.tree-toggle:not(.no-children)').forEach(toggle => {
toggle.addEventListener('click', function(e) {
e.stopPropagation();
const treeNode = this.closest('.tree-node');
const children = treeNode.querySelector('.tree-children');
if (children) {
const isExpanded = children.classList.contains('expanded');
if (isExpanded) {
children.classList.remove('expanded');
this.classList.remove('expanded');
} else {
children.classList.add('expanded');
this.classList.add('expanded');
}
}
});
});
}
// 递归渲染树节点
function renderTreeNodes(nodes, level) {
if (!nodes || nodes.length === 0) return '';
return nodes.map(node => {
const hasChildren = node.children && node.children.length > 0;
const toggleClass = hasChildren ? 'tree-toggle' : 'tree-toggle no-children';
const childrenHtml = hasChildren ? `
<div class="tree-children">
${renderTreeNodes(node.children, level + 1)}
</div>
` : '';
return `
<div class="tree-node">
<div class="tree-item" data-path="${node.path}" data-level="${level}">
<span class="${toggleClass}">▶</span>
<span class="tree-icon">📁</span>
<span class="tree-name">${node.name}</span>
</div>
${childrenHtml}
</div>
`;
}).join('');
}
// 执行移动操作
function performMove() {
if (!selectedMoveTarget || !moveSourcePath) return;
const fileName = moveSourcePath.split(/[\/]/).pop();
const itemType = moveSourceIsDir ? '文件夹' : '文件';
messageModal.confirm(
`确定要将${itemType} "${fileName}" 移动到选中的目录吗?\n\n如果目标位置存在同名文件,系统会自动重命名。`,
'确认移动',
'confirm'
).then((confirmed) => {
if (!confirmed) return;
confirmMoveBtn.disabled = true;
confirmMoveBtn.innerHTML = '<span class="message-loading"></span>移动中...';
const formData = new FormData();
formData.append('action', 'move_item');
formData.append('source_path', moveSourcePath);
formData.append('target_dir', selectedMoveTarget);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
confirmMoveBtn.disabled = false;
confirmMoveBtn.innerHTML = '✂️ 移动到此位置';
if (data.error) {
messageModal.error('移动失败:' + data.error);
return;
}
messageModal.success(data.message, '移动成功').then(() => {
closeMoveDialog();
// 刷新当前目录
loadDirectory(currentPath);
});
})
.catch(error => {
confirmMoveBtn.disabled = false;
confirmMoveBtn.innerHTML = '✂️ 移动到此位置';
messageModal.error('移动时出错: ' + error);
});
});
}
// 关闭移动对话框
function closeMoveDialog() {
directoryModal.style.display = 'none';
document.body.style.overflow = 'auto';
selectedMoveTarget = '';
moveSourcePath = '';
moveSourceIsDir = false;
selectedTargetPath.textContent = '未选择';
confirmMoveBtn.disabled = true;
confirmMoveBtn.innerHTML = '✂️ 移动到此位置';
}
// 下载文件
function downloadFile(path) {
messageModal.success('正在准备下载...');
// 创建隐藏表单进行下载
const form = document.createElement('form');
form.method = 'POST';
form.action = '';
form.style.display = 'none';
const actionInput = document.createElement('input');
actionInput.type = 'hidden';
actionInput.name = 'action';
actionInput.value = 'download_single';
const pathInput = document.createElement('input');
pathInput.type = 'hidden';
pathInput.name = 'path';
pathInput.value = path;
form.appendChild(actionInput);
form.appendChild(pathInput);
document.body.appendChild(form);
// 提交表单开始下载
form.submit();
// 清理表单
setTimeout(() => {
if (document.body.contains(form)) {
document.body.removeChild(form);
}
messageModal.success('下载已开始...');
}, 1000);
}
// 编辑文件
function editFile(path) {
const fileName = path.split(/[\/]/).pop();
editFileName.textContent = `编辑文件: ${fileName}`;
fileContent.value = '正在加载文件内容,请稍候...';
fileContent.disabled = true;
// 显示模态框
editModal.style.display = 'block';
document.body.style.overflow = 'hidden'; // 防止背景滚动
// 聚焦到模态框
setTimeout(() => {
editModal.focus();
}, 100);
const formData = new FormData();
formData.append('action', 'read_file');
formData.append('path', path);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
messageModal.error('读取文件失败:' + data.error).then(() => {
closeEditModal();
});
return;
}
fileContent.value = data.content;
fileContent.disabled = false;
// 添加文件信息显示
const fileInfo = `文件大小: ${formatFileSize(data.size)} | 编码: UTF-8`;
editFileName.textContent = `编辑文件: ${fileName} (${fileInfo})`;
// 聚焦到文本框
setTimeout(() => {
fileContent.focus();
fileContent.setSelectionRange(0, 0); // 光标移到开始
}, 100);
})
.catch(error => {
messageModal.error('读取文件时出错: ' + error).then(() => {
closeEditModal();
});
});
}
// 保存文件
function saveFile() {
if (!currentSelectedPath) return;
// 确认保存
messageModal.confirm('确定要保存文件吗?\n注意:保存前会自动创建备份文件。', '确认保存').then((confirmed) => {
if (!confirmed) return;
saveFileBtn.disabled = true;
saveFileBtn.innerHTML = '<span class="message-loading"></span>保存中...';
const formData = new FormData();
formData.append('action', 'save_file');
formData.append('path', currentSelectedPath);
formData.append('content', fileContent.value);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
saveFileBtn.disabled = false;
saveFileBtn.innerHTML = '💾 保存';
if (data.error) {
messageModal.error('保存失败:' + data.error);
return;
}
// 使用美化的成功消息
messageModal.success(`${data.message}\n已写入 ${data.bytes_written} 字节`, '保存成功').then(() => {
closeEditModal();
// 刷新当前目录
loadDirectory(currentPath);
});
})
.catch(error => {
saveFileBtn.disabled = false;
saveFileBtn.innerHTML = '💾 保存';
messageModal.error('保存时出错: ' + error);
});
});
}
// 关闭编辑弹窗
function closeEditModal() {
// 检查是否有未保存的更改
if (fileContent.value && !fileContent.disabled) {
messageModal.confirm('有未保存的更改,确定要关闭吗?', '确认关闭').then((confirmed) => {
if (confirmed) {
performCloseModal();
}
});
} else {
performCloseModal();
}
}
// 执行关闭模态框
function performCloseModal() {
editModal.style.display = 'none';
document.body.style.overflow = 'auto'; // 恢复背景滚动
fileContent.value = '';
fileContent.disabled = false;
currentSelectedPath = '';
editFileName.textContent = '编辑文件';
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 字节';
const k = 1024;
const sizes = ['字节', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 删除文件/目录
function deleteItem_func(path, isDir) {
const fileName = path.split(/[\/]/).pop();
const itemType = isDir ? '目录' : '文件';
const message = `确定要删除${itemType} "${fileName}" 吗?${isDir ? '\n注意:这将删除目录及其所有内容!' : ''}`;
messageModal.confirm(message, '确认删除', 'warning').then((confirmed) => {
if (!confirmed) return;
loading.style.display = 'block';
status.style.display = 'none';
const formData = new FormData();
formData.append('action', 'delete_item');
formData.append('path', path);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
if (data.error) {
messageModal.error('删除失败:' + data.error);
return;
}
messageModal.success(data.message, '删除成功').then(() => {
// 刷新当前目录
loadDirectory(currentPath);
});
})
.catch(error => {
loading.style.display = 'none';
messageModal.error('删除时出错: ' + error);
});
});
}
// 加载目录内容
function loadDirectory(path) {
loading.style.display = 'block';
const formData = new FormData();
formData.append('action', 'list_dir');
formData.append('dir', path);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
loading.style.display = 'none';
if (data.error) {
messageModal.error(data.error);
return;
}
currentPath = data.current_dir;
selectedFiles.clear();
updateDownloadButtonState();
updateBreadcrumb(currentPath);
renderFileList(data.files);
})
.catch(error => {
loading.style.display = 'none';
messageModal.error('加载目录时出错: ' + error);
});
}
// 更新面包屑导航
function updateBreadcrumb(path) {
const basePath = '<?php echo $basePath; ?>';
const parts = path.replace(basePath, '').split(/[\/]/).filter(part => part !== '');
let html = `<span class="breadcrumb-item" data-path="${basePath}">根目录</span>`;
let current = basePath;
for (const part of parts) {
current += '/' + part;
html += `<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item" data-path="${current}">${part}</span>`;
}
breadcrumb.innerHTML = html;
// 添加面包屑点击事件
breadcrumb.querySelectorAll('.breadcrumb-item').forEach(item => {
item.addEventListener('click', function() {
const targetPath = this.getAttribute('data-path');
loadDirectory(targetPath);
});
});
}
// 渲染文件列表
function renderFileList(files) {
if (files.length === 0) {
fileList.innerHTML = '<li style="text-align: center; color: #999; padding: 40px;">此目录为空</li>';
return;
}
const fileItems = files.map(file => {
const icon = file.is_dir ? '📁' : getFileIcon(file.name);
const sizeText = file.is_dir ? '' : formatFileSize(file.size || 0);
const itemClass = file.is_dir ? 'directory' : '';
return `
<li class="file-item ${itemClass}" data-path="${file.path}" data-is-dir="${file.is_dir}">
<span class="file-icon">${icon}</span>
<span class="file-name">${file.name}</span>
<span class="file-size">${sizeText}</span>
</li>
`;
}).join('');
fileList.innerHTML = fileItems;
// 添加文件项事件监听
fileList.querySelectorAll('.file-item').forEach(item => {
const path = item.getAttribute('data-path');
const isDir = item.getAttribute('data-is-dir') === 'true';
// 单击选中文件
item.addEventListener('click', function(e) {
// 清除其他选中状态
fileList.querySelectorAll('.file-item').forEach(i => i.classList.remove('selected'));
// 选中当前项
this.classList.add('selected');
selectedFiles.clear();
selectedFiles.add(path);
updateDownloadButtonState();
});
// 双击进入目录
item.addEventListener('dblclick', function() {
if (isDir) {
loadDirectory(path);
}
});
// 右键菜单
item.addEventListener('contextmenu', function(e) {
showContextMenu(e, path, isDir);
});
});
}
// 获取文件扩展名
function getFileExtension(fileName) {
return fileName.split('.').pop().toLowerCase();
}
// 获取文件图标
function getFileIcon(fileName) {
const ext = getFileExtension(fileName);
const iconMap = {
// 文档类
'txt': '📄', 'doc': '📄', 'docx': '📄', 'pdf': '📕',
'rtf': '📄', 'odt': '📄',
// 表格类
'xls': '📊', 'xlsx': '📊', 'csv': '📊', 'ods': '📊',
// 演示文稿
'ppt': '📊', 'pptx': '📊', 'odp': '📊',
// 图像类
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️',
'bmp': '🖼️', 'svg': '🖼️', 'ico': '🖼️', 'webp': '🖼️',
'tiff': '🖼️', 'tif': '🖼️',
// 音频类
'mp3': '🎵', 'wav': '🎵', 'flac': '🎵', 'aac': '🎵',
'ogg': '🎵', 'm4a': '🎵', 'wma': '🎵',
// 视频类
'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬', 'mov': '🎬',
'wmv': '🎬', 'flv': '🎬', 'webm': '🎬', '3gp': '🎬',
// 压缩文件
'zip': '📦', 'rar': '📦', '7z': '📦', 'tar': '📦',
'gz': '📦', 'bz2': '📦', 'xz': '📦',
// 代码文件
'html': '🌐', 'htm': '🌐', 'css': '🎨', 'js': '⚡',
'php': '🐘', 'py': '🐍', 'java': '☕', 'cpp': '⚙️',
'c': '⚙️', 'cs': '⚙️', 'go': '🐹', 'rs': '🦀',
'rb': '💎', 'swift': '🦉', 'kt': '🎯',
// 配置文件
'json': '📋', 'xml': '📋', 'yaml': '📋', 'yml': '📋',
'ini': '⚙️', 'conf': '⚙️', 'cfg': '⚙️',
// 数据库
'sql': '🗃️', 'db': '🗃️', 'sqlite': '🗃️',
// 日志文件
'log': '📜',
// 字体文件
'ttf': '🔤', 'otf': '🔤', 'woff': '🔤', 'woff2': '🔤',
// 可执行文件
'exe': '⚡', 'msi': '⚡', 'deb': '⚡', 'rpm': '⚡',
'dmg': '⚡', 'app': '⚡',
// 其他常见格式
'md': '📝', 'readme': '📖', 'license': '📜',
'gitignore': '🚫', 'htaccess': '⚙️'
};
return iconMap[ext] || '📄';
}
// 显示状态消息
function showStatus(message, type = 'success') {
status.textContent = message;
status.className = `status ${type}`;
status.style.display = 'block';
// 3秒后自动隐藏
setTimeout(() => {
status.style.display = 'none';
}, 3000);
}
// 初始加载
loadDirectory(currentPath);
});
</script>
</body>
</html>
© 版权声明
1. 本主题所有言论和图片纯属会员个人意见,与本站立场无关。一切关于该内容及资源商业行为与本站无关。
2. 本站的所有内容都不保证其准确性,有效性,时间性。阅读本站内容因误导等因素而造成的损失本站不承担连带责任。
3. 本站提供的一切软件、教程和内容信息等仅限用于学习和研究目的,不得用于商业或者游戏以及其它非法用途,否则,一切后果请用户自负。
4. 本站资源均来自网络收集整理,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容,如果您喜欢该程序和内容,请支持正版。
5. 本站本着互联网分享学习精神,本站大部分内容转载于其他网站和媒体,如内容涉及版权等问题,请联系本站进行删除或修改处理,敬请谅解!
6. 如有侵犯您版权的内容,请邮件与我们取得联系删除(E-mail:283532303@qq.com)本站将及时改正。
2. 本站的所有内容都不保证其准确性,有效性,时间性。阅读本站内容因误导等因素而造成的损失本站不承担连带责任。
3. 本站提供的一切软件、教程和内容信息等仅限用于学习和研究目的,不得用于商业或者游戏以及其它非法用途,否则,一切后果请用户自负。
4. 本站资源均来自网络收集整理,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容,如果您喜欢该程序和内容,请支持正版。
5. 本站本着互联网分享学习精神,本站大部分内容转载于其他网站和媒体,如内容涉及版权等问题,请联系本站进行删除或修改处理,敬请谅解!
6. 如有侵犯您版权的内容,请邮件与我们取得联系删除(E-mail:283532303@qq.com)本站将及时改正。
THE END















暂无评论内容