在日常开发或服务器管理中,我们经常会遇到这样的场景:需要从服务器上下载多个分散的文件(如PHP脚本、HTML页面、配置文件),却不想通过复杂的FTP工具逐个传输;想要快速打包某一目录下的代码文件分享给同事,却发现服务器上没有预装压缩工具;需要核对服务器上的文件结构和内容,避免下载后才发现漏选或错选文件……这时,一款能在服务器端直接运行、可视化操作的文件打包工具就成了刚需。
今天要介绍的这款PHP单文件文件打包工具,是本人原创设计的,正是为解决这些痛点而生。它无需复杂部署,只需将单个PHP文件上传到服务器目录,通过浏览器访问即可实现目录扫描、文件选择、多格式打包、内容预览等功能,尤其适合开发者、运维人员在无本地压缩工具或FTP权限受限的场景下使用。下面,我们将从工具核心功能、使用方法、优势亮点、注意事项等方面,进行全面且通俗的讲解。

一、工具核心功能:贴合实际需求,覆盖打包全流程
这款工具的所有功能都基于PHP开发,完全在服务器端运行,无需依赖本地软件。从代码逻辑来看,它的核心能力可分为“文件扫描与管理”“多格式打包”“内容预览与下载”“状态监控与统计”四大模块,每个模块都精准对应实际使用中的需求。
1. 智能目录扫描:自动识别文件,跳过冗余内容
工具的第一步是“找到文件”,这依赖于代码中的 scanDirectoryForStructure() 函数。当你访问工具页面时,它会递归扫描当前目录及所有子目录,自动识别文件和文件夹,同时规避不必要的冗余内容——这是它的一大贴心设计。
自动跳过关键文件:代码中预设了 $skipItems 数组,会自动跳过工具自身相关的文件(如生成的打包文件 files.txt 、工具本体 file.php 、服务器配置文件 .user.ini ),避免打包时把工具文件也包含进去,减少冗余。 兼容不同服务器环境:扫描过程中会先判断目录是否“存在且可读”(通过 is_dir() 和 is_readable() 函数),如果某目录因权限问题无法访问,会自动跳过并继续扫描其他目录,不会导致工具崩溃。 清晰区分文件类型:扫描后会给每个条目标记“file”(文件)或“folder”(文件夹)类型,同时记录文件的大小(通过 filesize() 函数),后续在界面上会用不同图标区分(如文件夹用📁,PHP文件用🐘,图片用🖼️),直观易懂。
举个例子:如果你的服务器目录下有“src”“config”“public”三个子目录,以及“index.php”“README.md”两个文件,工具会自动把这些内容全部扫描出来,并用层级结构展示,就像在本地文件管理器里一样清晰。

2. 灵活的文件选择:批量操作,减少重复劳动
找到文件后,下一步就是“选对文件”。工具提供了多种文件选择方式,彻底告别“逐个勾选”的繁琐,这部分功能由前端JS逻辑与后端PHP判断协同实现。
全选/取消全选:界面顶部的“全选”按钮会自动勾选当前扫描到的所有文件(包括子目录下的文件),“取消全选”则一键清空选择,适合需要打包大部分或全部文件的场景。 文件夹级选择:勾选文件夹前的复选框时,工具会自动选中该文件夹下的所有子文件和子目录(通过 handleFolderSelection() 函数实现),比如勾选“src”文件夹,就会自动选中“src”下的 api.php “utils”子目录及其中的 helper.php ,无需逐个展开勾选。 折叠/展开目录:文件夹前的“▶”“▼”按钮可以折叠或展开子目录,当目录层级较多(如嵌套5-6层)时,折叠无关目录能让界面更简洁,避免视觉混乱。 精准筛选文件:如果只需打包某几个零散文件(如“config/db.php”“public/style.css”),可以直接展开对应目录,单独勾选目标文件,灵活度拉满。
比如你需要打包“config”目录下的所有配置文件和“public”目录下的HTML、CSS文件,只需勾选“config”文件夹和“public”文件夹,工具会自动处理其中的所有子文件,无需手动逐个选择。
3. 多格式打包:适配不同场景,兼容多数服务器
打包功能是工具的核心,代码中的 getSupportedFormats() 函数会先检测服务器环境,自动启用支持的压缩格式,避免出现“点击打包却提示不支持”的尴尬。目前工具支持的格式分为“基础格式”和“扩展格式”两类,覆盖绝大多数使用场景。
(1)默认支持:TXT文本格式(带内容预览)
TXT格式是工具的“保底选项”,无论服务器环境如何,都会支持。它的特殊之处在于不仅能打包文件列表,还能包含文件的具体内容,对应代码中的 generateTxtFile() 函数。
生成的TXT文件会分为三个部分:
文件结构:列出所有选中文件的路径和总数量,比如“总文件数: 3”“- config/db.php”“- public/index.html”,让你快速了解打包范围; 文件内容:按路径顺序展示每个文件的具体内容,比如“config/db.php”的数据库配置代码、“public/index.html”的HTML结构,中间用“==...==”分隔,清晰区分不同文件; 处理统计:最后会显示“总计处理文件: 3 个”,确认是否所有选中文件都已成功处理。
更实用的是,TXT格式支持浏览器预览——生成后不会直接下载,而是在页面上显示预览框,你可以先核对文件内容是否正确(比如确认配置文件没有敏感信息),再点击“下载文件”保存到本地,避免下载后才发现漏选或内容错误。
(2)扩展支持:多种压缩格式(直接下载)
除了TXT,工具还会根据服务器环境自动启用压缩格式,常见的包括:
ZIP格式:最通用的压缩格式,几乎所有操作系统(Windows、macOS、Linux)都能直接解压。代码中通过 ZipArchive 类实现,只要服务器PHP环境开启了该扩展(大部分主流服务器都会开启),就能使用; GZ格式(tar.gz):在Linux/macOS环境下常用的压缩格式,代码中会先将文件打包成tar格式,再通过 gzencode() 函数压缩为tar.gz,适合需要在服务器端进一步处理压缩包的场景; RAR格式:需服务器安装RAR扩展( RarArchive 类),压缩率略高于ZIP,适合习惯使用WinRAR的用户; 7Z格式:压缩率最高的格式之一,但需要服务器上预装7z二进制文件(通过 exec() 函数检测“which 7z”),适合打包大型文件(如多个图片、视频)以节省存储空间。
这些压缩格式的打包逻辑各有优化,比如ZIP格式会保留文件的相对路径(打包后解压仍能保持“config/db.php”的层级),GZ格式会严格遵循tar标准以确保兼容性,避免出现“解压后文件混乱”的问题。
4. 可视化状态监控:进度清晰,统计直观
很多打包工具在生成文件时会让用户“盲目等待”,而这款工具通过“进度条”和“统计面板”,让整个过程可视化,对应代码中的 showStats() 和进度条模拟逻辑。
实时进度条:点击“生成文件”后,页面会显示一个从0%到100%的进度条,虽然是模拟进度(实际生成速度取决于文件大小和数量),但能有效缓解“不知道还要等多久”的焦虑,尤其在打包大量文件时更实用; 核心统计面板:界面下方的统计区域会实时显示4个关键数据:
总文件数:当前目录下所有可识别的文件总数(不含文件夹); 总文件夹数:当前目录下所有子目录的总数; 已选择文件:你当前勾选的文件数量,避免漏选或多选; 支持格式:服务器当前支持的打包格式数量(如显示“3”,代表支持TXT、ZIP、GZ)。
比如你扫描到服务器上有12个文件、3个文件夹,勾选了8个文件,支持4种格式,这些数据会实时显示在统计面板上,让你对当前操作范围和工具能力一目了然。
5. 人性化下载体验:自定义文件名,适配不同需求
工具在下载环节也做了细节优化,避免“每次下载都是默认名”的麻烦:
自定义输出文件名:在“打包选项”中,你可以输入自定义文件名(如“20240510_网站代码备份”),工具会自动添加对应格式的后缀(如ZIP格式会变成“20240510_网站代码备份.zip”,TXT格式会变成“20240510_网站代码备份.txt”); 区分下载逻辑:TXT格式会先预览再提供下载,压缩格式(ZIP、GZ等)则在生成后直接触发浏览器下载,无需额外点击; 兼容性下载:代码中通过 Blob 对象和 URL.createObjectURL() 实现前端下载,支持Chrome、Firefox、Edge、Safari等主流浏览器,不会出现“下载按钮无效”的问题。 
二、工具使用教程:3步上手,无需技术背景
这款工具的最大优势之一就是“零门槛使用”,无论你是资深开发者还是刚接触服务器的新手,只需3步就能完成从部署到打包的全过程。
第一步:部署工具到服务器
准备工具文件:将工具的PHP文件(假设文件名为 filepacker.php ,即代码中的 file.php )保存到本地; 上传文件:通过FTP工具(如FileZilla)或服务器面板(如宝塔面板),将 filepacker.php 上传到你需要打包文件的目标目录(比如你想打包“/www/wwwroot/mywebsite”下的文件,就把 filepacker.php 上传到这个目录); 确认权限:确保 filepacker.php 所在目录有“读取”权限(服务器上通常默认开启),否则工具无法扫描目录下的文件。
第二步:扫描并选择文件
访问工具:打开浏览器,在地址栏输入“服务器域名/目录路径/filepacker.php”(比如“http://www.mywebsite.com/filepacker.php”),加载完成后工具会自动开始扫描目录; 查看文件列表:扫描完成后,页面会以层级结构显示所有文件和文件夹,文件夹前有“▶”按钮,点击可展开子目录,文件旁会显示大小(如“index.php 2.1 KB”); 选择目标文件:
如需打包全部文件:点击顶部“全选”按钮; 如需打包部分目录:勾选目标文件夹(如“src”“config”),工具会自动选中子文件; 如需打包零散文件:展开对应目录,单独勾选目标文件(如“public/style.css”); 选错了?点击“取消全选”重新选择,或直接取消单个文件的勾选。
第三步:设置打包选项并下载
选择输出格式:在“打包选项”的“输出格式”中,选择你需要的格式:
想预览内容或只需简单打包:选“TXT (文本预览)”; 想压缩节省空间或分享给他人:选“ZIP”(兼容性最好); 服务器环境支持且需要高压缩率:选“7Z”或“GZ”;
自定义文件名:在“输出文件名”输入框中,填写你想要的文件名(如“mywebsite_backup_202405”),无需手动加后缀; 生成文件:点击“生成文件”按钮,此时会显示进度条,等待进度条完成; 下载文件:
若选择TXT格式:进度条完成后会显示“文件内容预览”,核对内容无误后,点击“下载文件”保存; 若选择压缩格式(ZIP/GZ等):进度条完成后会自动触发下载,文件会保存到浏览器默认的下载目录。
三、工具优势亮点:为什么选择这款工具?
在众多文件打包方案(如本地压缩后上传、服务器命令行压缩、FTP逐个下载)中,这款PHP工具的优势非常明显,尤其适合特定场景下的需求:
1. 单文件部署:无需安装,即传即用
工具仅包含一个PHP文件,无需解压、配置环境变量或安装依赖(除部分压缩格式需服务器预装扩展外),上传到目标目录后直接通过浏览器访问即可使用。相比需要部署后端服务的工具(如基于Node.js的打包工具),它的部署成本几乎为零,哪怕是新手也能在1分钟内完成部署。
2. 服务器端运行:突破本地环境限制
所有操作都在服务器端完成,无需依赖本地电脑的软件环境:
无需安装压缩软件(如WinRAR、7-Zip),哪怕你在公共电脑上操作,只要能访问浏览器就能打包; 避免FTP传输的繁琐:如果需要打包100个小文件,用FTP逐个下载需要反复点击,而用工具打包后只需下载一个压缩包,节省大量时间; 适配弱网环境:压缩后的文件体积更小,在网络速度较慢时,下载一个10MB的ZIP包比下载100个100KB的零散文件快得多。
3. 可视化操作:比命令行更易用
对于不熟悉服务器命令行(如Linux的 zip / tar 命令)的用户,可视化界面的优势不言而喻:
无需记忆复杂命令(如 zip -r backup.zip src/ config/ ),用鼠标点击就能完成选择和打包; 实时反馈状态:文件是否选中、格式是否支持、生成进度如何,都能在界面上直观看到,避免命令行“黑盒操作”导致的错误(如输错目录路径导致打包失败)。
4. 安全跳过关键文件:避免泄露敏感信息
代码中预设了跳过工具自身文件和服务器配置文件(如 .user.ini )的逻辑,这些文件通常包含工具运行信息或服务器配置,打包后如果分享给他人,可能存在安全风险。工具的自动跳过功能,能减少“误打包敏感文件”的概率,提升使用安全性。
5. 兼容性强:适配多数PHP环境
工具基于PHP 5.6+开发(兼容PHP 7.x、8.x),支持绝大多数主流服务器环境(如Apache、Nginx、IIS),只要服务器开启了基础的PHP运行环境(这是网站服务器的标配),就能正常使用。对于压缩格式的依赖(如 ZipArchive 类),大部分云服务器(如阿里云、腾讯云)的默认PHP配置都会开启,无需额外手动配置。
四、注意事项与常见问题
虽然工具易用且稳定,但在使用过程中,仍有一些细节需要注意,以避免出现问题;同时,我们也整理了一些常见问题的解决方案:
注意事项
1. 权限问题:确保目录可读取
工具扫描文件时需要目录有“读取权限”(服务器上通常用 chmod 755 设置目录权限, chmod 644 设置文件权限)。如果某目录显示“没有找到文件或目录不可读”,可能是该目录权限不足,需要通过服务器面板或FTP工具调整权限。
2. 敏感文件:谨慎选择打包范围
服务器目录中可能包含数据库密码(如 config/db.php )、API密钥等敏感信息,打包前务必确认选中的文件中没有这些内容,避免将压缩包分享给他人时泄露信息。如果需要分享代码,建议先删除敏感信息或替换为占位符。
3. 文件大小:避免打包超大文件
虽然工具支持打包大文件,但生成压缩包时会占用服务器内存和磁盘空间。如果需要打包单个超过1GB的文件(如视频、大型数据库备份),建议直接通过FTP下载,避免因服务器内存不足导致打包失败。
4. 安全访问:限制工具访问权限
工具能扫描和打包目录下的文件,存在一定的安全风险(如被未授权用户访问,导致文件泄露)。使用完成后,建议及时删除服务器上的 filepacker.php 文件;如果需要长期使用,可通过服务器面板设置访问密码(如Nginx的 auth_basic 认证),仅允许指定用户访问。
常见问题(FAQ)
Q1:为什么界面上没有显示RAR/7Z格式的选项?
A:这是因为服务器环境不支持对应的格式:
没有RAR选项:服务器PHP未安装 rar 扩展,需联系服务器管理员开启; 没有7Z选项:服务器未预装7z二进制文件(Linux需通过 yum install p7zip 或 apt install p7zip-full 安装,Windows需手动安装并配置环境变量)。 如果无法修改服务器配置,建议选择ZIP格式(兼容性最好)。
Q2:扫描文件时提示“没有找到文件或目录不可读”,怎么办?
A:首先检查工具所在目录的权限,确保目录有“读取”权限;其次,确认该目录下确实有文件(如果是刚创建的空目录,扫描结果也会为空);最后,如果目录下有符号链接(软链接),工具可能无法识别,建议直接访问实际目录。
Q3:生成TXT文件后,预览内容显示乱码,怎么解决?
A:这是因为文件编码与浏览器默认编码不一致(如文件是GBK编码,而浏览器用UTF-8解码)。解决方案:生成TXT文件后,用记事本打开,点击“文件”→“另存为”,在“编码”选项中选择“UTF-8”,保存后即可正常显示。
Q4:生成ZIP文件后,解压时提示“压缩包损坏”,是什么原因?
A:可能有两种原因:
生成过程中网络中断:导致下载的压缩包不完整,重新生成并下载即可; 服务器内存不足:打包大量文件时,服务器内存不够导致压缩过程中断,建议分多次打包(如先打包“src”目录,再打包“config”目录),或删除部分不必要的文件后再打包。
Q5:可以自定义跳过的文件吗?比如我想跳过“log”目录下的日志文件。
A:目前工具的跳过列表(
五、总结
这款PHP文件打包工具虽然轻量,但功能却十分精准地覆盖了服务器端文件打包的核心需求:从智能扫描目录、灵活选择文件,到多格式打包、可视化预览,再到人性化的下载体验,每一个功能都围绕“简单、高效、易用”的目标设计。
无论是开发者需要备份服务器上的代码文件,运维人员需要打包日志文件,还是普通用户需要分享服务器上的零散文件,这款工具都能成为得力助手。它无需复杂部署,突破本地环境限制,让文件打包从“繁琐操作”变成“一键完成”,尤其适合在无本地工具、弱网环境或命令行不熟练的场景下使用。
如果你经常需要在服务器上处理文件打包需求,不妨试试这款工具——只需一个PHP文件,就能解决你的所有烦恼。代码如下:
<?php
// 设置输出文件名
$outputFile = 'files.txt';
// 要跳过的文件和文件夹列表
$skipItems = [
$outputFile,
'file.php',
'.user.ini',
];
// 检查服务器支持的压缩格式
function getSupportedFormats() {
$formats = ['txt']; // TXT总是支持的
// 检查Zip支持
if (class_exists('ZipArchive')) {
$formats[] = 'zip';
}
// 检查Gz支持
if (function_exists('gzencode')) {
$formats[] = 'gz';
}
// 检查Rar支持(需要rar扩展)
if (class_exists('RarArchive')) {
$formats[] = 'rar';
}
// 检查7z支持(需要7zip二进制文件)
if (function_exists('exec')) {
$output = [];
$returnCode = 0;
@exec('which 7z 2>/dev/null', $output, $returnCode);
if ($returnCode === 0 && !empty($output)) {
$formats[] = '7z';
}
}
return $formats;
}
// 递归遍历目录并收集文件结构
function scanDirectoryForStructure($dir, $baseDir = '', $skipItems = []) {
$structure = [];
// 检查目录是否存在且可读
if (!is_dir($dir) || !is_readable($dir)) {
return $structure;
}
$files = @scandir($dir);
if ($files === false) {
return $structure;
}
foreach ($files as $file) {
if ($file == '.' || $file == '..') {
continue;
}
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
$relativePath = ($baseDir ? $baseDir . DIRECTORY_SEPARATOR : '') . $file;
// 检查是否需要跳过该文件或文件夹
$shouldSkip = false;
foreach ($skipItems as $skipItem) {
if (basename($filePath) === $skipItem) {
$shouldSkip = true;
break;
}
}
if ($shouldSkip) {
continue;
}
if (is_dir($filePath)) {
$item = [
'name' => $file,
'path' => $relativePath,
'type' => 'folder',
'children' => scanDirectoryForStructure($filePath, $relativePath, $skipItems)
];
$structure[] = $item;
} else if (is_file($filePath) && is_readable($filePath)) {
$item = [
'name' => $file,
'path' => $relativePath,
'type' => 'file',
'size' => filesize($filePath)
];
$structure[] = $item;
}
}
return $structure;
}
// 根据选择的文件生成TXT文件
function generateTxtFile($selectedFiles) {
global $skipItems;
$outputContent = "=== 文件结构 ===\n";
$outputContent .= "总文件数: " . count($selectedFiles) . "\n\n";
foreach ($selectedFiles as $file) {
$outputContent .= "- " . $file . "\n";
}
$outputContent .= "\n=== 文件内容 ===\n\n";
$processedCount = 0;
foreach ($selectedFiles as $file) {
if (is_file($file) && is_readable($file)) {
$outputContent .= "==...==\n";
$outputContent .= $file . "\n";
$content = @file_get_contents($file);
if ($content !== false) {
$outputContent .= $content . "\n\n";
$processedCount++;
}
}
}
$outputContent .= "=== 文件内容结束 ===\n";
$outputContent .= "总计处理文件: " . $processedCount . " 个\n";
return $outputContent;
}
// 修复的ZIP文件生成函数
function generateZipFile($selectedFiles) {
$zip = new ZipArchive();
// 创建内存中的ZIP文件
$tempFile = tempnam(sys_get_temp_dir(), 'zip');
if ($zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
foreach ($selectedFiles as $file) {
if (is_file($file) && is_readable($file)) {
// 使用相对路径作为ZIP内的路径
$zip->addFile($file, $file);
}
}
if (!$zip->close()) {
return false;
}
// 读取文件内容
$content = file_get_contents($tempFile);
unlink($tempFile);
return $content;
}
if (file_exists($tempFile)) {
unlink($tempFile);
}
return false;
}
// 修复的GZ文件生成函数 - 使用更标准的tar格式
function generateGzFile($selectedFiles) {
$tarContent = '';
foreach ($selectedFiles as $file) {
if (is_file($file) && is_readable($file)) {
$content = file_get_contents($file);
if ($content !== false) {
$fileInfo = stat($file);
$mode = 0644;
$uid = 0;
$gid = 0;
$size = strlen($content);
$mtime = time();
$typeflag = '0'; // 普通文件
$linkname = '';
$magic = "ustar";
$version = "00";
$uname = "root";
$gname = "root";
$devmajor = 0;
$devminor = 0;
$prefix = '';
// 文件名处理(不超过100字符)
$name = $file;
if (strlen($name) > 100) {
$prefix = substr($name, 0, 155);
$name = substr($name, -100);
}
// 创建tar头部
$header = pack("a100a8a8a8a12a12a8a1a100a6a2a32a32a8a8a155a12",
$name, // 文件名
sprintf("%07o", $mode), // 文件模式
sprintf("%07o", $uid), // 用户ID
sprintf("%07o", $gid), // 组ID
sprintf("%011o", $size), // 文件大小
sprintf("%011o", $mtime), // 修改时间
" ", // 校验和占位符
$typeflag, // 文件类型
$linkname, // 链接名
$magic, // magic
$version, // 版本
$uname, // 用户名
$gname, // 组名
sprintf("%07o", $devmajor), // 主设备号
sprintf("%07o", $devminor), // 次设备号
$prefix, // 前缀
"" // 填充
);
// 计算校验和
$checksum = 0;
for ($i = 0; $i < 512; $i++) {
$checksum += ord($header[$i]);
}
// 写入校验和
$header = substr_replace($header, sprintf("%07o", $checksum) . "\0", 148, 8);
$tarContent .= $header;
$tarContent .= $content;
// 填充到512字节边界
$padding = 512 - ($size % 512);
if ($padding < 512) {
$tarContent .= str_repeat("\0", $padding);
}
}
}
}
// 添加结束标记(两个512字节的零块)
$tarContent .= str_repeat("\0", 1024);
// 压缩为gz
return gzencode($tarContent, 9);
}
// 处理AJAX请求
function handleAjaxRequest() {
global $skipItems;
if (isset($_GET['action']) && $_GET['action'] === 'scan') {
// 返回文件结构
$fileStructure = scanDirectoryForStructure('.', '', $GLOBALS['skipItems']);
$supportedFormats = getSupportedFormats();
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'fileStructure' => $fileStructure,
'supportedFormats' => $supportedFormats
]);
return true;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['action']) && $input['action'] === 'generate') {
$selectedFiles = $input['files'] ?? [];
$format = $input['format'] ?? 'txt';
$filename = $input['filename'] ?? '打包文件';
if (empty($selectedFiles)) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '没有选择文件']);
return true;
}
$content = '';
$contentType = '';
$downloadFilename = '';
switch ($format) {
case 'txt':
$content = generateTxtFile($selectedFiles);
$contentType = 'text/plain; charset=utf-8';
$downloadFilename = $filename . '.txt';
break;
case 'zip':
if (class_exists('ZipArchive')) {
$content = generateZipFile($selectedFiles);
if ($content === false) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '生成ZIP文件失败']);
return true;
}
$contentType = 'application/zip';
$downloadFilename = $filename . '.zip';
} else {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '服务器不支持ZIP格式']);
return true;
}
break;
case 'gz':
if (function_exists('gzencode')) {
$content = generateGzFile($selectedFiles);
$contentType = 'application/gzip';
$downloadFilename = $filename . '.tar.gz';
} else {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '服务器不支持GZ格式']);
return true;
}
break;
default:
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '不支持的格式: ' . $format]);
return true;
}
if ($content === false) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'message' => '生成文件失败']);
return true;
}
// 对于TXT格式,返回JSON包含内容用于预览
if ($format === 'txt') {
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'format' => 'txt',
'filename' => $downloadFilename,
'content' => $content
]);
} else {
// 对于压缩格式,直接输出文件
header('Content-Type: ' . $contentType);
header('Content-Disposition: attachment; filename="' . $downloadFilename . '"');
header('Content-Length: ' . strlen($content));
echo $content;
}
return true;
}
}
return false;
}
// 主执行逻辑
try {
// 首先处理AJAX请求
if (handleAjaxRequest()) {
exit;
}
} catch (Exception $e) {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => '服务器错误: ' . $e->getMessage()
]);
exit;
}
// HTML界面保持不变...
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件打包工具</title>
<style>
/* 样式保持不变... */
:root {
--primary-color: #3498db;
--secondary-color: #2980b9;
--success-color: #2ecc71;
--danger-color: #e74c3c;
--light-color: #f8f9fa;
--dark-color: #343a40;
--border-radius: 4px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f7fa;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: var(--border-radius);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px;
}
h1, h2, h3 {
color: var(--dark-color);
margin-bottom: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: var(--primary-color);
}
.panel {
background: var(--light-color);
border-radius: var(--border-radius);
padding: 20px;
margin-bottom: 20px;
}
.file-list {
max-height: 500px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: var(--border-radius);
padding: 15px;
background: white;
}
.file-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.file-item:last-child {
border-bottom: none;
}
.file-item input[type="checkbox"] {
margin-right: 10px;
}
.file-icon {
margin-right: 8px;
color: var(--primary-color);
}
.folder > .file-name {
font-weight: bold;
color: var(--secondary-color);
}
.folder .children {
margin-left: 20px;
display: none;
}
.folder.expanded .children {
display: block;
}
.folder-toggle {
cursor: pointer;
margin-right: 5px;
color: var(--secondary-color);
}
.options {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 20px 0;
}
.option-group {
flex: 1;
min-width: 200px;
}
select, button, input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
font-size: 16px;
}
button {
background-color: var(--primary-color);
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: var(--secondary-color);
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.format-option {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.format-option input {
margin-right: 10px;
}
.status {
margin-top: 20px;
padding: 15px;
border-radius: var(--border-radius);
display: none;
}
.status.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.progress {
width: 100%;
height: 20px;
background-color: #e9ecef;
border-radius: var(--border-radius);
margin: 10px 0;
overflow: hidden;
display: none;
}
.progress-bar {
height: 100%;
background-color: var(--primary-color);
width: 0%;
transition: width 0.3s;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 20px;
}
.stat-item {
background: var(--light-color);
padding: 15px;
border-radius: var(--border-radius);
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
}
.header-actions {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.btn {
padding: 8px 15px;
border-radius: var(--border-radius);
cursor: pointer;
border: none;
font-size: 14px;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.preview-area {
margin-top: 20px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
padding: 15px;
background: white;
display: none;
}
.preview-content {
width: 100%;
height: 300px;
font-family: monospace;
border: 1px solid #ddd;
border-radius: var(--border-radius);
padding: 10px;
resize: vertical;
}
@media (max-width: 768px) {
.options {
flex-direction: column;
}
.header-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div>
<h1>文件打包工具</h1>
<div>
<button id="select-all" class="btn btn-primary">全选</button>
<button id="deselect-all" class="btn btn-secondary">取消全选</button>
<button id="expand-all" class="btn btn-secondary">展开所有</button>
<button id="collapse-all" class="btn btn-secondary">折叠所有</button>
</div>
<div>
<h2>选择要打包的文件</h2>
<div id="file-list">
<div>
<span>正在加载文件列表...</span>
</div>
</div>
</div>
<div>
<h2>打包选项</h2>
<div>
<div>
<h3>输出格式</h3>
<div id="format-options">
<div>
<input type="radio" name="format" id="format-txt" value="txt" checked>
<label for="format-txt">TXT (文本预览)</label>
</div>
</div>
</div>
<div>
<h3>输出文件名</h3>
<input type="text" id="output-filename" placeholder="请输入输出文件名" value="打包文件">
</div>
</div>
<button id="generate-btn">生成文件</button>
<div id="progress-container">
<div id="progress-bar"></div>
</div>
<div id="status-message"></div>
<div id="preview-area">
<h3 id="preview-title">文件内容预览</h3>
<textarea id="preview-content" readonly></textarea>
<button id="download-preview" class="btn btn-primary" style="margin-top: 10px;">下载文件</button>
</div>
<div id="stats-container">
<div>
<div>总文件数</div>
<div id="total-files">0</div>
</div>
<div>
<div>总文件夹数</div>
<div id="total-folders">0</div>
</div>
<div>
<div>已选择文件</div>
<div id="selected-count">0</div>
</div>
<div>
<div>支持格式</div>
<div id="supported-formats">1</div>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let fileStructure = [];
let selectedFiles = [];
let supportedFormats = [];
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 获取文件结构
fetchFileStructure();
// 绑定事件
document.getElementById('select-all').addEventListener('click', selectAll);
document.getElementById('deselect-all').addEventListener('click', deselectAll);
document.getElementById('expand-all').addEventListener('click', expandAll);
document.getElementById('collapse-all').addEventListener('click', collapseAll);
document.getElementById('generate-btn').addEventListener('click', generateFile);
document.getElementById('download-preview').addEventListener('click', downloadPreview);
});
// 获取文件结构
function fetchFileStructure() {
showStatus('正在扫描文件结构...', 'info');
// 使用AJAX获取文件结构
fetch('?action=scan')
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => {
if (data.success) {
fileStructure = data.fileStructure;
supportedFormats = data.supportedFormats;
// 渲染文件列表
renderFileList();
// 渲染格式选项
renderFormatOptions();
// 显示统计信息
showStats();
showStatus('文件结构扫描完成', 'success');
} else {
throw new Error(data.message || '扫描失败');
}
})
.catch(error => {
console.error('Error:', error);
showStatus('扫描文件结构时出错: ' + error.message, 'error');
});
}
// 渲染文件列表
function renderFileList() {
const fileListContainer = document.getElementById('file-list');
fileListContainer.innerHTML = '';
if (fileStructure.length === 0) {
fileListContainer.innerHTML = '<div><span>没有找到文件或目录不可读</span></div>';
return;
}
fileStructure.forEach(item => {
fileListContainer.appendChild(createFileItem(item));
});
}
// 创建文件项
function createFileItem(item, level = 0) {
const itemDiv = document.createElement('div');
itemDiv.className = 'file-item';
itemDiv.style.paddingLeft = (level * 20) + 'px';
if (item.type === 'folder') {
itemDiv.classList.add('folder');
const toggleSpan = document.createElement('span');
toggleSpan.className = 'folder-toggle';
toggleSpan.innerHTML = '▶';
toggleSpan.addEventListener('click', function() {
itemDiv.classList.toggle('expanded');
toggleSpan.innerHTML = itemDiv.classList.contains('expanded') ? '▼' : '▶';
});
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'item-' + item.path;
checkbox.dataset.path = item.path;
checkbox.dataset.type = 'folder';
checkbox.addEventListener('change', handleFolderSelection);
const iconSpan = document.createElement('span');
iconSpan.className = 'file-icon';
iconSpan.innerHTML = '📁';
const nameSpan = document.createElement('span');
nameSpan.className = 'file-name';
nameSpan.textContent = item.name;
itemDiv.appendChild(toggleSpan);
itemDiv.appendChild(checkbox);
itemDiv.appendChild(iconSpan);
itemDiv.appendChild(nameSpan);
// 添加子项容器
const childrenDiv = document.createElement('div');
childrenDiv.className = 'children';
if (item.children && item.children.length > 0) {
item.children.forEach(child => {
childrenDiv.appendChild(createFileItem(child, level + 1));
});
}
itemDiv.appendChild(childrenDiv);
} else {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'item-' + item.path;
checkbox.dataset.path = item.path;
checkbox.dataset.type = 'file';
checkbox.addEventListener('change', handleFileSelection);
const iconSpan = document.createElement('span');
iconSpan.className = 'file-icon';
iconSpan.innerHTML = getFileIcon(item.name);
const nameSpan = document.createElement('span');
nameSpan.className = 'file-name';
nameSpan.textContent = item.name;
// 添加文件大小
if (item.size !== undefined) {
const sizeSpan = document.createElement('span');
sizeSpan.style.marginLeft = '10px';
sizeSpan.style.color = '#666';
sizeSpan.style.fontSize = '0.9em';
sizeSpan.textContent = formatFileSize(item.size);
nameSpan.appendChild(sizeSpan);
}
itemDiv.appendChild(checkbox);
itemDiv.appendChild(iconSpan);
itemDiv.appendChild(nameSpan);
}
return itemDiv;
}
// 获取文件图标
function getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const iconMap = {
'php': '🐘',
'html': '🌐',
'css': '🎨',
'js': '📜',
'json': '📋',
'txt': '📄',
'md': '📝',
'xml': '📰',
'sql': '🗄️',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️',
'pdf': '📕',
'zip': '📦',
'rar': '📦',
'7z': '📦'
};
return iconMap[ext] || '📄';
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', '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 handleFolderSelection(event) {
const checkbox = event.target;
const folderPath = checkbox.dataset.path;
const isChecked = checkbox.checked;
// 找到所有属于这个文件夹的子项
const children = document.querySelectorAll(`[data-path^="${folderPath}/"]`);
children.forEach(child => {
child.checked = isChecked;
if (child.dataset.type === 'file') {
updateSelectedFiles(child.dataset.path, isChecked);
}
});
updateSelectedFiles(folderPath, isChecked);
}
// 处理文件选择
function handleFileSelection(event) {
const checkbox = event.target;
const filePath = checkbox.dataset.path;
const isChecked = checkbox.checked;
updateSelectedFiles(filePath, isChecked);
}
// 更新选中的文件列表
function updateSelectedFiles(path, isSelected) {
if (isSelected) {
if (!selectedFiles.includes(path)) {
selectedFiles.push(path);
}
} else {
const index = selectedFiles.indexOf(path);
if (index > -1) {
selectedFiles.splice(index, 1);
}
}
updateStats();
}
// 全选
function selectAll() {
const checkboxes = document.querySelectorAll('#file-list input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
if (checkbox.dataset.type === 'file') {
updateSelectedFiles(checkbox.dataset.path, true);
}
});
updateStats();
}
// 取消全选
function deselectAll() {
const checkboxes = document.querySelectorAll('#file-list input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
if (checkbox.dataset.type === 'file') {
updateSelectedFiles(checkbox.dataset.path, false);
}
});
updateStats();
}
// 展开所有
function expandAll() {
const folders = document.querySelectorAll('.folder');
const toggles = document.querySelectorAll('.folder-toggle');
folders.forEach(folder => {
folder.classList.add('expanded');
});
toggles.forEach(toggle => {
toggle.innerHTML = '▼';
});
}
// 折叠所有
function collapseAll() {
const folders = document.querySelectorAll('.folder');
const toggles = document.querySelectorAll('.folder-toggle');
folders.forEach(folder => {
folder.classList.remove('expanded');
});
toggles.forEach(toggle => {
toggle.innerHTML = '▶';
});
}
// 渲染格式选项
function renderFormatOptions() {
const formatContainer = document.getElementById('format-options');
formatContainer.innerHTML = '';
supportedFormats.forEach(format => {
const optionDiv = document.createElement('div');
optionDiv.className = 'format-option';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'format';
radio.id = 'format-' + format;
radio.value = format;
if (format === 'txt') radio.checked = true;
const label = document.createElement('label');
label.htmlFor = 'format-' + format;
label.textContent = format.toUpperCase() + (format === 'txt' ? ' (文本预览)' : ' (直接下载)');
optionDiv.appendChild(radio);
optionDiv.appendChild(label);
formatContainer.appendChild(optionDiv);
});
// 更新支持的格式数量
document.getElementById('supported-formats').textContent = supportedFormats.length;
}
// 生成文件
function generateFile() {
if (selectedFiles.length === 0) {
showStatus('请至少选择一个文件', 'error');
return;
}
const format = document.querySelector('input[name="format"]:checked').value;
const filename = document.getElementById('output-filename').value || '打包文件';
showStatus('正在生成文件...', 'info');
document.getElementById('progress-container').style.display = 'block';
document.getElementById('preview-area').style.display = 'none';
// 模拟进度条
let progress = 0;
const progressBar = document.getElementById('progress-bar');
const progressInterval = setInterval(() => {
progress += 5;
progressBar.style.width = progress + '%';
if (progress >= 100) {
clearInterval(progressInterval);
// 实际生成文件
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'generate',
files: selectedFiles,
format: format,
filename: filename
})
})
.then(response => {
if (format === 'txt') {
return response.json();
} else {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message); });
}
return response.blob();
}
})
.then(data => {
if (format === 'txt') {
if (data.success) {
// 对于TXT文件,显示内容预览
showFilePreview(data.content, data.filename);
showStatus('文件生成成功,请查看预览', 'success');
} else {
throw new Error(data.message);
}
} else {
// 对于压缩文件,直接下载
downloadFile(data, `${filename}.${format === 'gz' ? 'tar.gz' : format}`);
showStatus('文件已生成并开始下载', 'success');
}
document.getElementById('progress-container').style.display = 'none';
})
.catch(error => {
console.error('Error:', error);
showStatus('生成文件时出错: ' + error.message, 'error');
document.getElementById('progress-container').style.display = 'none';
});
}
}, 100);
}
// 显示文件内容预览(TXT格式)
function showFilePreview(content, filename) {
const previewArea = document.getElementById('preview-area');
const previewTitle = document.getElementById('preview-title');
const previewContent = document.getElementById('preview-content');
previewTitle.textContent = filename + ' 内容预览';
previewContent.value = content;
previewArea.style.display = 'block';
// 滚动到预览区域
previewArea.scrollIntoView({ behavior: 'smooth' });
}
// 下载预览文件
function downloadPreview() {
const content = document.getElementById('preview-content').value;
const filename = document.getElementById('output-filename').value || '打包文件';
downloadTxt(content, filename + '.txt');
}
// 下载TXT文件
function downloadTxt(content, filename) {
const blob = new Blob([content], { type: 'text/plain; charset=utf-8' });
downloadFile(blob, filename);
}
// 下载文件
function downloadFile(blob, filename) {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
// 显示状态消息
function showStatus(message, type) {
const statusDiv = document.getElementById('status-message');
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
statusDiv.style.display = 'block';
// 自动隐藏成功消息
if (type === 'success') {
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
}
// 显示统计信息
function showStats() {
const totalFiles = countFiles(fileStructure);
const totalFolders = countFolders(fileStructure);
document.getElementById('total-files').textContent = totalFiles;
document.getElementById('total-folders').textContent = totalFolders;
updateStats();
}
// 更新统计信息
function updateStats() {
const selectedCount = document.getElementById('selected-count');
selectedCount.textContent = selectedFiles.length;
}
// 计算文件数量
function countFiles(structure) {
let count = 0;
structure.forEach(item => {
if (item.type === 'file') {
count++;
} else if (item.type === 'folder' && item.children) {
count += countFiles(item.children);
}
});
return count;
}
// 计算文件夹数量
function countFolders(structure) {
let count = 0;
structure.forEach(item => {
if (item.type === 'folder') {
count++;
if (item.children) {
count += countFolders(item.children);
}
}
});
return count;
}
</script>
</body>
</html>







