轻量高效的服务器端PHP文件打包工具:从功能到使用全指南
在日常开发或服务器管理中,我们经常会遇到这样的场景:需要从服务器上下载多个分散的文件(如PHP脚本、HTML页面、配置文件),却不想通过复杂的FTP工具逐个传输;想要快速打包某一目录下的代码文件分享给同事,却发现服务器上没有预装压缩工具;需要核对服务器上的文件结构和内容,避免下载后才发现漏选或错选文件……这时,一款能在服务器端直接运行、可视化操作的文件打包工具就成了刚需。
今天要介绍的这款PHP单文件文件打包工具,由本人开发,只需要新建一个php文件,复制代码进去,通过url访问该文件即可,就可轻易解决这些痛点。它无需复杂部署,只需将单个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), // 用户IDsprintf("%07o", $gid), // 组IDsprintf("%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);// 压缩为gzreturn 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 lang="zh-CN"><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 class="container"><h1>文件打包工具</h1><div class="header-actions"><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 class="panel"><h2>选择要打包的文件</h2><div class="file-list" id="file-list"><div class="file-item"><span>正在加载文件列表...</span></div></div></div><div class="panel"><h2>打包选项</h2><div class="options"><div class="option-group"><h3>输出格式</h3><div id="format-options"><div class="format-option"><input type="radio" name="format" id="format-txt" value="txt" checked><label for="format-txt">TXT (文本预览)</label></div></div></div><div class="option-group"><h3>输出文件名</h3><input type="text" id="output-filename" placeholder="请输入输出文件名" value="打包文件"></div></div><button id="generate-btn">生成文件</button><div class="progress" id="progress-container"><div class="progress-bar" id="progress-bar"></div></div><div class="status" id="status-message"></div><div class="preview-area" id="preview-area"><h3 id="preview-title">文件内容预览</h3><textarea class="preview-content" id="preview-content" readonly></textarea><button id="download-preview" class="btn btn-primary" style="margin-top: 10px;">下载文件</button></div><div class="stats" id="stats-container"><div class="stat-item"><div class="stat-label">总文件数</div><div class="stat-value" id="total-files">0</div></div><div class="stat-item"><div class="stat-label">总文件夹数</div><div class="stat-value" id="total-folders">0</div></div><div class="stat-item"><div class="stat-label">已选择文件</div><div class="stat-value" id="selected-count">0</div></div><div class="stat-item"><div class="stat-label">支持格式</div><div class="stat-value" 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>
本文链接:https://kinber.cn/post/5762.html 转载需授权!
推荐本站淘宝优惠价购买喜欢的宝贝:

支付宝微信扫一扫,打赏作者吧~
