手中的剑为什么而挥动
只有靠你自己去寻找答案----盖聂

文件管理系统-简单单页php

文件管理系统-简单单页php下载
命名:index.php
下载:download

文件管理系统 – 密码访问版

<?php
/**
* 文件管理系统 - 安全增强版 v2.7
* 
* 功能特性:
* - 密码保护(可开关)
* - 文件浏览、预览、下载
* - 多级目录支持(默认折叠)
* - 文件优先排序(文件夹置底)
* - 响应式设计(PC/平板/手机)
* - 密码状态显示
* - 自适应 HTTP/HTTPS
* - 安全强化:哈希密码、XSS防护、路径遍历保护、脚本执行拦截
* 
* @author FileManager
* @version 2.7
*/

// ==================== 协议自适应 ====================
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
|| (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on');

// 安全头设置(部分仅在 HTTPS 下发)
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
if ($isHttps) {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}

// ==================== 会话初始化 ====================
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $isHttps, // 仅在 HTTPS 下设置 secure
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();

// ==================== 配置文件路径 ====================
$configFile = __DIR__ . '/config.json';

// ==================== 管理员密码(已哈希,生产环境可设为环境变量) ====================
$adminPwdHash = '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi'; // 密码: ruoqq.cn
// 如需更安全,可改为从环境变量获取: $adminPwdHash = getenv('ADMIN_PASSWORD_HASH') ?: '...';

// ==================== 默认访问密码 ====================
$defaultPwd = '123';

// ==================== 初始化配置文件 ====================
if (!file_exists($configFile)) {
file_put_contents($configFile, json_encode([
'need_pwd' => true,
'password_hash' => password_hash($defaultPwd, PASSWORD_DEFAULT) // 直接存储哈希
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}

$cfg = json_decode(file_get_contents($configFile), true);

// 确保必要字段存在(注意:不再兼容旧明文密码)
if (!isset($cfg['password_hash'])) {
$cfg['password_hash'] = password_hash($defaultPwd, PASSWORD_DEFAULT);
}
if (!isset($cfg['need_pwd'])) {
$cfg['need_pwd'] = true;
}

// ==================== 验证登录状态 ====================
$tokenTimeout = 1800;
$isLoggedIn = false;

if (isset($_SESSION['logged']) && $_SESSION['logged'] === true) {
if (isset($_SESSION['token_time']) && time() - $_SESSION['token_time'] <= $tokenTimeout) {
// 验证 User-Agent 一致性(防止会话劫持)
if (isset($_SESSION['user_agent']) && $_SESSION['user_agent'] === ($_SERVER['HTTP_USER_AGENT'] ?? '')) {
$_SESSION['token_time'] = time();
$isLoggedIn = true;
} else {
// UA 变化,安全退出
$_SESSION = [];
session_destroy();
header('Location: index.php');
exit;
}
} else {
// 超时退出
$_SESSION = [];
session_destroy();
}
}

// ==================== 保存设置处理 ====================
$setErr = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_setting'])) {
$inputAdminPwd = trim($_POST['admin_pwd'] ?? '');

if (!password_verify($inputAdminPwd, $adminPwdHash)) {
$setErr = '管理员密码错误';
} else {
$cfg['need_pwd'] = !empty($_POST['need_pwd']);
$newPwd = trim($_POST['new_user_pwd'] ?? '');
if (!empty($newPwd)) {
$cfg['password_hash'] = password_hash($newPwd, PASSWORD_DEFAULT);
}
file_put_contents($configFile, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
header('Location: index.php?saved=1');
exit;
}
}

// ==================== 退出登录处理 ====================
if (isset($_GET['logout'])) {
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
header('Location: index.php');
exit;
}

// ==================== 登录验证处理 ====================
$loginErr = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login_submit'])) {
$inputPwd = $_POST['login_pwd'] ?? '';
if (password_verify($inputPwd, $cfg['password_hash'])) {
$_SESSION['logged'] = true;
$_SESSION['token_time'] = time();
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
session_regenerate_id(true);
header('Location: index.php');
exit;
} else {
$loginErr = '密码错误,请重试';
}
}

// ==================== 判断是否需要登录 ====================
$needLogin = $cfg['need_pwd'] && !$isLoggedIn;

// ==================== 文件系统配置 ====================
define('BASE_DIR', __DIR__ . '/download');

if (!is_dir(BASE_DIR)) {
mkdir(BASE_DIR, 0755, true);
}

// ---------- 自动生成 .htaccess(禁止脚本执行) ----------
$htaccessPath = BASE_DIR . '/.htaccess';
$htaccessContent = "# Auto-generated security rule\n";
$htaccessContent .= "<FilesMatch \"\.(php|phtml|phar|php3|php4|php5|php7|pl|py|cgi)$\">\n Deny from all\n</FilesMatch>\n";
$htaccessContent .= "RemoveHandler .php .phtml .phar\n";
if (!file_exists($htaccessPath)) {
file_put_contents($htaccessPath, $htaccessContent);
@chmod($htaccessPath, 0444);
}

// ---------- 危险脚本直接访问拦截 ----------
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$dangerousExt = ['php', 'phtml', 'phar', 'php3', 'php4', 'php5', 'php7', 'pl', 'py', 'cgi'];
if (preg_match('#^/download/.+\.(' . implode('|', $dangerousExt) . ')$#i', $requestUri)) {
http_response_code(403);
die('403 Forbidden - Access to this file type is denied.');
}

$host = ($isHttps ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'];

$preview_types = [
'image' => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'],
'video' => ['mp4', 'webm', 'ogg', 'mov', 'avi'],
'audio' => ['mp3', 'wav', 'ogg', 'flac', 'm4a'],
'text' => ['txt', 'html', 'css', 'js', 'md', 'json', 'xml', 'log'],
'pdf' => ['pdf']
];

$allow_sort = ['name', 'type', 'size', 'time'];
$sort = isset($_GET['sort']) && in_array($_GET['sort'], $allow_sort) ? $_GET['sort'] : 'name';
$order = isset($_GET['order']) && $_GET['order'] === 'desc' ? 'desc' : 'asc';

// ==================== 核心函数 ====================

function scan_dir($path) {
$result = [];
if (!is_dir($path)) return $result;
$d = opendir($path);
if (!$d) return $result;
while (($f = readdir($d)) !== false) {
if ($f === '.' || $f === '..') continue;
if ($f[0] === '.') continue;
$fp = $path . '/' . $f;

// 修复漏洞:忽略符号链接,防止路径遍历
if (is_link($fp)) continue;

// 过滤危险脚本扩展名
$ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
$dangerous = ['php', 'phtml', 'phar', 'php3', 'php4', 'php5', 'php7', 'pl', 'py', 'cgi'];
if (in_array($ext, $dangerous)) continue;

$isDir = is_dir($fp);
$result[] = [
'name' => $f,
'is_dir' => $isDir,
'size' => $isDir ? 0 : filesize($fp),
'time' => filemtime($fp),
'children' => $isDir ? scan_dir($fp) : []
];
}
closedir($d);
return $result;
}

function sort_items(&$items, $sort, $order) {
usort($items, function($a, $b) use ($sort, $order) {
if (!$a['is_dir'] && $b['is_dir']) return -1;
if ($a['is_dir'] && !$b['is_dir']) return 1;
$v1 = $v2 = '';
switch ($sort) {
case 'type':
if ($a['is_dir']) { $v1 = $a['name']; $v2 = $b['name']; }
else { $v1 = strtolower(pathinfo($a['name'], PATHINFO_EXTENSION)); $v2 = strtolower(pathinfo($b['name'], PATHINFO_EXTENSION)); }
break;
case 'size': $v1 = $a['size']; $v2 = $b['size']; break;
case 'time': $v1 = $a['time']; $v2 = $b['time']; break;
default: $v1 = $a['name']; $v2 = $b['name'];
}
$r = is_string($v1) ? strnatcasecmp($v1, $v2) : ($v1 - $v2);
return $order === 'desc' ? -$r : $r;
});
foreach ($items as &$item) {
if ($item['is_dir'] && !empty($item['children'])) sort_items($item['children'], $sort, $order);
}
}

function format_size($bytes) {
if ($bytes == 0) return '0 B';
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = floor(log($bytes, 1024));
$i = min($i, count($units) - 1);
return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
}

function get_preview_type($filename, $types) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
foreach ($types as $type => $extensions) {
if (in_array($ext, $extensions)) return $type;
}
return null;
}

function render($list, $types, $host, $rel = '', $level = 0) {
static $folderId = 0;
if (empty($list)) return;
$paddingLeft = $level > 0 ? '16px' : '0';
echo '<ul class="file-list" style="padding-left:' . $paddingLeft . ';">';
foreach ($list as $item) {
$name = htmlspecialchars($item['name'], ENT_QUOTES, 'UTF-8');
$path = trim($rel . '/' . $name, '/');
$url = 'download/' . $path;
$fullUrl = rtrim($host, '/') . '/' . ltrim($url, '/');
if ($item['is_dir']) {
$folderId++;
$folderIdStr = 'folder_' . $folderId;
$fileCount = countFiles($item['children']);
echo '<li class="folder-item"><div class="folder-header" onclick="toggleFolder(\'' . $folderIdStr . '\')" title="点击展开/折叠文件夹(' . $fileCount . ' 个文件)"><span class="folder-arrow" id="arrow_' . $folderIdStr . '">▶</span><span class="folder-icon">📁</span><span class="folder-name">' . $name . '</span><span class="folder-count">' . $fileCount . '</span></div><div class="folder-content" id="' . $folderIdStr . '" style="display:none;">';
if (!empty($item['children'])) render($item['children'], $types, $host, $path, $level + 1);
else echo '<div class="empty-folder">空文件夹</div>';
echo '</div></li>';
} else {
$size = format_size($item['size']);
$time = date('Y-m-d H:i', $item['time']);
$previewType = get_preview_type($name, $types);
echo '<li class="file-item"><div class="file-container"><span class="file-icon">' . getFileIcon($name) . '</span><a href="' . $url . '" download class="file-name" title="下载文件:' . $name . '&#10;大小:' . $size . '&#10;修改时间:' . $time . '">' . $name . '</a><span class="file-meta">' . $size . '</span><span class="file-time">' . $time . '</span><div class="file-actions"><button class="copy-btn" data-url="' . htmlspecialchars($fullUrl) . '" title="复制文件链接">复制链接</button>';
if ($previewType) echo '<button class="prev-btn" onclick="pv(\'' . $url . '\',\'' . $previewType . '\')" title="预览文件">预览</button>';
echo '</div></div></li>';
}
}
echo '</ul>';
}

function countFiles($items) {
$count = 0;
foreach ($items as $item) {
if ($item['is_dir']) $count += countFiles($item['children']);
else $count++;
}
return $count;
}

function getFileIcon($filename) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$icons = [
'jpg' => '🖼', 'jpeg' => '🖼', 'png' => '🖼', 'gif' => '🖼', 'bmp' => '🖼', 'webp' => '🖼', 'svg' => '🖼',
'mp4' => '🎬', 'webm' => '🎬', 'ogg' => '🎬', 'mov' => '🎬', 'avi' => '🎬',
'mp3' => '🎵', 'wav' => '🎵', 'flac' => '🎵', 'm4a' => '🎵',
'pdf' => '📕', 'doc' => '📘', 'docx' => '📘', 'xls' => '📗', 'xlsx' => '📗', 'ppt' => '📙', 'pptx' => '📙',
'html' => '💻', 'css' => '🎨', 'js' => '📜', 'php' => '🐘', 'py' => '🐍', 'json' => '📋', 'md' => '📝',
'zip' => '📦', 'rar' => '📦', '7z' => '📦', 'tar' => '📦', 'gz' => '📦',
'txt' => '📄', 'log' => '📄',
];
return isset($icons[$ext]) ? $icons[$ext] : '📄';
}

$list = scan_dir(BASE_DIR);
sort_items($list, $sort, $order);

// ==================== 登录页面 ====================
if ($needLogin) {
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="robots" content="noindex, nofollow">
<title>访问验证 - 文件管理系统</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;}
html{font-size:12.8px;}
body{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:16px;font-size:1rem;}
.login-box{background:#fff;padding:32px 24px;border-radius:14px;width:100%;max-width:340px;text-align:center;box-shadow:0 16px 48px rgba(0,0,0,0.3);animation:fadeInUp 0.5s ease;}
@keyframes fadeInUp{from{opacity:0;transform:translateY(16px);}to{opacity:1;transform:translateY(0);}}
.login-box h2{margin-bottom:6px;color:#333;font-size:1.6rem;}
.login-status{display:inline-block;background:#fed7d7;color:#c53030;padding:5px 14px;border-radius:16px;font-size:0.9rem;margin-bottom:20px;border:1px solid #fc8181;font-weight:600;}
.login-input{width:100%;padding:11px 14px;border:2px solid #e0e0e0;border-radius:7px;font-size:1.05rem;margin-bottom:14px;outline:none;transition:border-color 0.3s,box-shadow 0.3s;text-align:center;}
.login-input:focus{border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,0.1);}
.login-btn{width:100%;padding:11px;background:#667eea;color:#fff;border:none;border-radius:7px;font-size:1.05rem;cursor:pointer;transition:all 0.3s;font-weight:600;}
.login-btn:hover{background:#5a6fd6;transform:translateY(-1px);box-shadow:0 4px 10px rgba(102,126,234,0.4);}
.login-btn:active{transform:translateY(0);}
.err{color:#e53e3e;margin-top:14px;font-size:0.95rem;font-weight:500;animation:shake 0.5s ease;padding:7px;background:#fff5f5;border-radius:5px;}
@keyframes shake{0%,100%{transform:translateX(0);}25%{transform:translateX(-4px);}75%{transform:translateX(4px);}}
.login-note{margin-top:12px;font-size:0.8rem;color:#a0aec0;}
.login-note p{margin:4px 0;}
</style>
</head>
<body>
<div class="login-box">
<h2>文件管理系统</h2>
<div class="login-status">密码保护模式</div>
<form method="post" autocomplete="off">
<input type="password" name="login_pwd" class="login-input" placeholder="请输入访问密码" autofocus autocomplete="new-password">
<button type="submit" name="login_submit" class="login-btn">验证登录</button>
</form>
<?php if ($loginErr): ?>
<div class="err"><?=htmlspecialchars($loginErr, ENT_QUOTES, 'UTF-8')?></div>
<?php endif; ?>
<div class="login-note">
<p>关闭浏览器页面后需重新登录</p>
<p>30分钟无操作自动退出</p>
</div>
</div>
</body>
</html>
<?php exit; } ?>

<!-- ==================== 主管理页面 ==================== -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="robots" content="noindex, nofollow">
<title>文件管理中心</title>
<style>
:root{--primary:#667eea;--primary-hover:#5a6fd6;--success:#10b981;--success-hover:#059669;--danger:#e53e3e;--danger-bg:#ffeaea;--warning:#f6ad55;--warning-bg:#fffbf0;--bg:#f0f2f5;--card-bg:#ffffff;--border:#e2e8f0;--text:#333333;--text-light:#718096;--radius:10px;--shadow:0 2px 16px rgba(0,0,0,0.06);}
html{font-size:12.8px;}
*{box-sizing:border-box;}
body{margin:0;padding:clamp(8px,2.4vw,16px);background:var(--bg);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;-webkit-text-size-adjust:100%;color:var(--text);line-height:1.5;font-size:1rem;}
.main{max-width:1200px;margin:0 auto;background:var(--card-bg);padding:clamp(14px,3.2vw,20px);border-radius:14px;box-shadow:var(--shadow);}
.head{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:10px;padding-bottom:14px;border-bottom:2px solid #f0f0f0;}
.head-left{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.head h2{font-size:1.5rem;margin:0;white-space:nowrap;}
.pwd-status{display:inline-flex;align-items:center;padding:5px 12px;border-radius:16px;font-size:0.85rem;font-weight:600;white-space:nowrap;}
.pwd-status.locked{background:#fed7d7;color:#c53030;border:1px solid #fc8181;}
.pwd-status.unlocked{background:#c6f6d5;color:#276749;border:1px solid #68d391;}
.logout{color:var(--danger);background:var(--danger-bg);padding:6px 13px;border-radius:5px;text-decoration:none;font-weight:500;font-size:0.95rem;white-space:nowrap;transition:all 0.2s;display:<?=$cfg['need_pwd']?'inline-block':'none'?>;}
.logout:hover{background:#fecaca;transform:translateY(-1px);}
.save-success{background:#c6f6d5;color:#276749;padding:8px 14px;border-radius:7px;margin-bottom:14px;font-size:0.95rem;font-weight:500;animation:fadeIn 0.3s ease;display:<?=isset($_GET['saved'])?'block':'none'?>;}
@keyframes fadeIn{from{opacity:0;transform:translateY(-8px);}to{opacity:1;transform:translateY(0);}}
.set{background:#f9f9f9;padding:clamp(10px,2.4vw,14px);border-radius:var(--radius);margin-bottom:14px;border:1px solid var(--border);}
.set-title{font-weight:600;margin-bottom:10px;color:#4a5568;font-size:0.95rem;}
.set-form-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;}
.set label{display:flex;align-items:center;gap:6px;font-size:0.95rem;white-space:nowrap;cursor:pointer;font-weight:500;}
.set input[type="checkbox"]{width:15px;height:15px;cursor:pointer;accent-color:var(--primary);}
.set input[type="text"],.set input[type="password"]{padding:6px 10px;border:1px solid #cbd5e0;border-radius:5px;font-size:0.9rem;min-width:0;flex:1;transition:border-color 0.2s,box-shadow 0.2s;}
.set input:focus{border-color:var(--primary);outline:none;box-shadow:0 0 0 3px rgba(102,126,234,0.1);}
.set .btn-save{padding:6px 16px;background:var(--primary);color:white;border:none;border-radius:5px;cursor:pointer;font-size:0.9rem;font-weight:500;white-space:nowrap;transition:all 0.2s;}
.set .btn-save:hover{background:var(--primary-hover);transform:translateY(-1px);}
.err-tip{color:var(--danger);margin-top:8px;font-size:0.85rem;font-weight:500;padding:6px 10px;background:#fff5f5;border-radius:5px;border-left:3px solid var(--danger);}
.sort{background:#f9f9f9;padding:clamp(8px,2.4vw,10px) 14px;border-radius:var(--radius);margin-bottom:14px;border:1px solid var(--border);display:flex;align-items:center;flex-wrap:wrap;gap:8px;}
.sort-label{font-size:0.95rem;color:var(--text-light);white-space:nowrap;font-weight:500;}
.sort form{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
.sort select{padding:6px 10px;border:1px solid #cbd5e0;border-radius:5px;font-size:0.9rem;background:white;cursor:pointer;transition:border-color 0.2s;}
.sort select:focus{border-color:var(--primary);outline:none;}
.sort .btn-sort{padding:6px 14px;background:#48bb78;color:white;border:none;border-radius:5px;cursor:pointer;font-size:0.9rem;white-space:nowrap;transition:all 0.2s;font-weight:500;}
.sort .btn-sort:hover{background:#38a169;transform:translateY(-1px);}
.file-list{list-style:none;padding:0;margin:0;}
.folder-header{cursor:pointer;padding:10px 12px;background:var(--warning-bg);border-radius:7px;border-left:3px solid var(--warning);display:flex;align-items:center;gap:8px;transition:all 0.2s;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;}
.folder-header:hover{background:#ffe8cc;border-left-color:#ed8936;}
.folder-header:active{transform:scale(0.99);}
.folder-arrow{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;transition:transform 0.3s cubic-bezier(0.4,0,0.2,1);font-size:10px;color:#c05621;flex-shrink:0;font-weight:bold;}
.folder-arrow.open{transform:rotate(90deg);}
.folder-icon{font-size:16px;flex-shrink:0;}
.folder-name{font-weight:600;color:#c05621;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:0.95rem;}
.folder-count{background:#f6ad55;color:white;padding:2px 7px;border-radius:8px;font-size:0.75rem;font-weight:600;flex-shrink:0;min-width:18px;text-align:center;}
.empty-folder{padding:8px 16px;color:#a0aec0;font-size:0.85rem;font-style:italic;}
.file-item{margin:2px 0;padding:8px 12px;background:#fff;border-radius:7px;border:1px solid transparent;transition:all 0.2s;list-style:none;}
.file-item:hover{background:#f7fafc;border-color:var(--border);box-shadow:0 1px 2px rgba(0,0,0,0.04);}
.file-container{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;}
.file-icon{flex-shrink:0;font-size:15px;}
.file-name{color:#2b6cb0;text-decoration:none;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;transition:color 0.2s;font-size:0.95rem;}
.file-name:hover{color:#1a4971;text-decoration:underline;}
.file-meta{color:var(--text-light);font-size:0.85rem;white-space:nowrap;flex-shrink:0;}
.file-time{color:#a0aec0;font-size:0.8rem;white-space:nowrap;flex-shrink:0;}
.file-actions{display:flex;gap:5px;flex-shrink:0;}
.copy-btn{padding:5px 12px;background:var(--success);color:white;border:none;border-radius:5px;font-size:0.85rem;cursor:pointer;white-space:nowrap;transition:all 0.2s;font-weight:500;}
.copy-btn:hover{background:var(--success-hover);transform:translateY(-1px);}
.copy-btn.copied{background:#047857;}
.prev-btn{padding:5px 12px;background:var(--primary);color:white;border:none;border-radius:5px;font-size:0.85rem;cursor:pointer;white-space:nowrap;transition:all 0.2s;font-weight:500;}
.prev-btn:hover{background:var(--primary-hover);transform:translateY(-1px);}
#mask{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:1000;align-items:center;justify-content:center;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);}
#mask.active{display:flex;}
.mask-close{position:absolute;top:16px;right:16px;color:white;font-size:24px;cursor:pointer;z-index:1001;width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.1);border-radius:50%;transition:all 0.3s;border:none;line-height:1;}
.mask-close:hover{background:rgba(255,255,255,0.25);transform:rotate(90deg);}
#mask-content{max-width:95vw;max-height:90vh;animation:zoomIn 0.3s ease;}
@keyframes zoomIn{from{transform:scale(0.9);opacity:0;}to{transform:scale(1);opacity:1;}}
@media (max-width:768px){html{font-size:12px;}.head{flex-direction:column;align-items:flex-start;}.head-left{width:100%;justify-content:space-between;}.set-form-row{flex-direction:column;align-items:stretch;}.set input[type="text"],.set input[type="password"]{width:100%;}.sort{flex-direction:column;align-items:stretch;}.sort form{flex-direction:column;}.sort select,.sort .btn-sort{width:100%;text-align:center;}.file-time{display:none;}}
@media (max-width:480px){html{font-size:13px;}body{padding:3px;}.main{padding:10px;border-radius:7px;}.file-container{flex-direction:column;align-items:flex-start;gap:5px;}.file-name{width:100%;font-size:0.95rem;}.file-meta,.file-time{font-size:0.8rem;width:100%;}.file-actions{width:100%;justify-content:stretch;}.copy-btn,.prev-btn{flex:1;text-align:center;padding:7px 10px;font-size:0.85rem;}.folder-header{padding:8px;}.folder-name{font-size:0.9rem;}.mask-close{top:8px;right:8px;width:30px;height:30px;font-size:20px;}}
</style>
</head>
<body>

<div class="main">

<div class="save-success">设置已成功保存!</div>

<div class="head">
<div class="head-left">
<h2>文件管理中心</h2>
<?php if ($cfg['need_pwd']): ?>
<span class="pwd-status locked" title="需要密码访问">密码保护</span>
<?php else: ?>
<span class="pwd-status unlocked" title="无需密码">开放访问</span>
<?php endif; ?>
</div>
<a href="?logout" class="logout" title="退出登录">退出登录</a>
</div>

<div class="set">
<div class="set-title">系统设置</div>
<form method="post">
<div class="set-form-row">
<label title="开启后需要密码才能访问">
<input type="checkbox" name="need_pwd" <?=$cfg['need_pwd']?'checked':''?>>
开启访问密码验证
</label>
<input type="text" name="new_user_pwd" placeholder="新访问密码(留空不变)" title="留空则不修改密码">
<input type="password" name="admin_pwd" placeholder="管理员密码(必填)" required title="输入管理员密码">
<button type="submit" name="save_setting" class="btn-save">保存设置</button>
</div>
</form>
<?php if (!empty($setErr)): ?>
<div class="err-tip"><?=htmlspecialchars($setErr,ENT_QUOTES,'UTF-8')?></div>
<?php endif; ?>
</div>

<div class="sort">
<span class="sort-label">排序方式:</span>
<form method="get">
<select name="sort">
<option value="name" <?=$sort=='name'?'selected':''?>>按名称</option>
<option value="type" <?=$sort=='type'?'selected':''?>>按类型</option>
<option value="size" <?=$sort=='size'?'selected':''?>>按大小</option>
<option value="time" <?=$sort=='time'?'selected':''?>>按修改时间</option>
</select>
<button type="submit" class="btn-sort"><?=$order=='asc'?'升序排列':'降序排列'?></button>
<input type="hidden" name="order" value="<?=$order=='asc'?'desc':'asc'?>">
</form>
</div>

<?php render($list, $preview_types, $host); ?>

</div>

<div id="mask">
<button class="mask-close" title="关闭预览">&times;</button>
<div id="mask-content"></div>
</div>

<script>
// ==================== 页面关闭自动退出 ====================
var isPageActive = true;

window.addEventListener('beforeunload', function() {
navigator.sendBeacon('?logout');
});

document.addEventListener('visibilitychange', function() {
if (document.hidden) {
isPageActive = false;
if (/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
navigator.sendBeacon('?logout');
}
} else {
if (!isPageActive) location.reload();
isPageActive = true;
}
});

window.addEventListener('pagehide', function() {
navigator.sendBeacon('?logout');
});

window.addEventListener('freeze', function() {
navigator.sendBeacon('?logout');
});

// ==================== 空闲退出 ====================
var idleTimer;
function resetIdleTimer() {
clearTimeout(idleTimer);
idleTimer = setTimeout(function() {
alert('由于30分钟未操作,即将退出登录');
window.location.href = '?logout';
}, 30 * 60 * 1000);
}
['mousemove','keypress','click','scroll','touchstart','touchmove'].forEach(function(e) {
document.addEventListener(e, resetIdleTimer);
});
resetIdleTimer();

// ==================== 文件夹折叠/展开 ====================
function toggleFolder(folderId) {
var content = document.getElementById(folderId);
var arrow = document.getElementById('arrow_' + folderId);
if (!content || !arrow) return;
var isHidden = content.style.display === 'none' || content.style.display === '';
content.style.display = isHidden ? 'block' : 'none';
arrow.classList.toggle('open', isHidden);
}

// ==================== 预览功能(已修复XSS) ====================
function pv(url, type) {
var mask = document.getElementById('mask');
var mc = document.getElementById('mask-content');
mask.classList.add('active');
document.body.style.overflow = 'hidden';
var html = '';
switch (type) {
case 'image':
html = '<img src="'+url+'" style="max-width:95vw;max-height:85vh;border-radius:8px;object-fit:contain;" alt="预览" onerror="this.innerHTML=\'<div style=color:white>加载失败</div>\'">';
break;
case 'video':
html = '<video controls autoplay src="'+url+'" style="max-width:95vw;max-height:85vh;border-radius:8px;background:#000;" playsinline></video>';
break;
case 'audio':
var fname = url.split('/').pop().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
html = '<div style="background:#fff;padding:32px;border-radius:14px;text-align:center;min-width:280px;">' +
'<div style="font-size:40px;margin-bottom:16px;">🎵</div>' +
'<div style="font-size:13px;color:#666;margin-bottom:14px;">'+fname+'</div>' +
'<audio controls autoplay src="'+url+'" style="width:100%;max-width:360px;"></audio></div>';
break;
case 'text':
html = '<div style="background:#fff;padding:16px;border-radius:7px;max-height:80vh;overflow:auto;max-width:90vw;min-width:280px;"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;font-size:13px;color:#333;">加载中...</pre></div>';
mc.innerHTML = html;
fetch(url).then(function(r){return r.text();}).then(function(t){
var escaped = t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
mc.innerHTML = '<div style="background:#fff;padding:16px;border-radius:7px;max-height:80vh;overflow:auto;max-width:90vw;min-width:280px;"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;font-size:13px;color:#333;">'+escaped+'</pre></div>';
}).catch(function(){mc.innerHTML='<div style="color:white;text-align:center;">无法加载文件内容</div>';});
return;
case 'pdf':
html = '<iframe src="'+url+'" width="95%" height="85vh" style="border-radius:7px;border:none;background:white;"></iframe>';
break;
default:
html = '<div style="color:white;font-size:16px;">暂不支持预览</div>';
}
mc.innerHTML = html;
}

function closePreview() {
document.getElementById('mask').classList.remove('active');
document.getElementById('mask-content').innerHTML = '';
document.body.style.overflow = '';
}

document.querySelector('.mask-close').addEventListener('click', closePreview);
document.getElementById('mask').addEventListener('click', function(e){if(e.target===this)closePreview();});
document.addEventListener('keydown', function(e){if(e.key==='Escape'||e.keyCode===27)closePreview();});

// ==================== 复制链接 ====================
document.querySelectorAll('.copy-btn').forEach(function(btn){
btn.addEventListener('click', function(e){
e.stopPropagation();
var url = this.getAttribute('data-url');
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(url).then(function(){showFB(btn);}).catch(function(){fallbackCopy(url,btn);});
} else {
fallbackCopy(url,btn);
}
});
});

function showFB(btn){
var orig = btn.textContent;
btn.textContent = '已复制';
btn.classList.add('copied');
setTimeout(function(){btn.textContent=orig;btn.classList.remove('copied');},1500);
}

function fallbackCopy(text,btn){
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;left:-9999px;';
document.body.appendChild(ta);
ta.contentEditable = true;
ta.readOnly = true;
var range = document.createRange();
range.selectNodeContents(ta);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
ta.setSelectionRange(0,999999);
try{document.execCommand('copy');showFB(btn);}catch(e){prompt('请手动复制:',text);}
sel.removeAllRanges();
document.body.removeChild(ta);
}

// 移动端优化
document.querySelectorAll('.folder-header').forEach(function(header){
header.addEventListener('touchend', function(e){e.preventDefault();this.click();});
});
document.getElementById('mask').addEventListener('touchmove', function(e){e.preventDefault();},{passive:false});

// 自动隐藏提示
var msg = document.querySelector('.save-success');
if (msg && msg.style.display !== 'none') {
setTimeout(function(){
msg.style.transition = 'opacity 0.5s';
msg.style.opacity = '0';
setTimeout(function(){msg.style.display='none';},500);
},3000);
}
</script>
</body>
</html>


文件管理系统 – 无密码访问版

<?php
/**
* 文件管理系统 - 无密码访问版(全安全增强)
* 
* @version 2.5.5 (Final Secure)
*/

// ==================== 协议自动识别 ====================
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
|| (isset($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on');

// ==================== 安全头设置 ====================
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
if ($isHttps) {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}

// ==================== 危险脚本直接访问拦截 ====================
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$dangerousExt = ['php', 'phtml', 'phar', 'php3', 'php4', 'php5', 'php7', 'pl', 'py', 'cgi'];
if (preg_match('#^/download/.+\.(' . implode('|', $dangerousExt) . ')$#i', $requestUri)) {
http_response_code(403);
die('403 Forbidden - Access to this file type is denied.');
}

// ==================== 文件系统配置 ====================
define('BASE_DIR', __DIR__ . '/download');

if (!is_dir(BASE_DIR)) {
mkdir(BASE_DIR, 0755, true);
}

// ---------- 自动生成 .htaccess(禁止脚本执行) ----------
$htaccessPath = BASE_DIR . '/.htaccess';
$htaccessContent = "# 自动生成的安全规则 - 禁止执行服务器端脚本\n";
$htaccessContent .= "<FilesMatch \"\.(php|phtml|phar|php3|php4|php5|php7|pl|py|cgi)$\">\n";
$htaccessContent .= " Deny from all\n";
$htaccessContent .= "</FilesMatch>\n";
$htaccessContent .= "RemoveHandler .php .phtml .phar\n";

if (!file_exists($htaccessPath)) {
file_put_contents($htaccessPath, $htaccessContent);
@chmod($htaccessPath, 0444); // 只读,防止被篡改
}
// ---------------------------------------------------

$host = ($isHttps ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'];

// ==================== 预览类型 ====================
$preview_types = [
'image' => ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'],
'video' => ['mp4', 'webm', 'ogg', 'mov', 'avi'],
'audio' => ['mp3', 'wav', 'ogg', 'flac', 'm4a'],
'text' => ['txt', 'html', 'css', 'js', 'md', 'json', 'xml', 'log'],
'pdf' => ['pdf']
];

// ==================== 排序参数白名单 ====================
$allow_sort = ['name', 'type', 'size', 'time'];
$sort = isset($_GET['sort']) && in_array($_GET['sort'], $allow_sort) ? $_GET['sort'] : 'name';
$order = isset($_GET['order']) && $_GET['order'] === 'desc' ? 'desc' : 'asc';

// ==================== 核心函数 ====================

/**
* 递归扫描目录(已过滤危险文件、隐藏文件、符号链接)
*/
function scan_dir($path) {
$result = [];
if (!is_dir($path)) return $result;
$d = opendir($path);
if (!$d) return $result;
while (($f = readdir($d)) !== false) {
if ($f === '.' || $f === '..') continue;
if ($f[0] === '.') continue; // 隐藏文件/文件夹
$fp = $path . '/' . $f;

if (is_link($fp)) continue; // 禁止符号链接(路径遍历)

$ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
$dangerous = ['php', 'phtml', 'phar', 'php3', 'php4', 'php5', 'php7', 'pl', 'py', 'cgi'];
if (in_array($ext, $dangerous)) continue; // 过滤可执行脚本

$isDir = is_dir($fp);
$result[] = [
'name' => $f,
'is_dir' => $isDir,
'size' => $isDir ? 0 : filesize($fp),
'time' => filemtime($fp),
'children' => $isDir ? scan_dir($fp) : []
];
}
closedir($d);
return $result;
}

/**
* 自定义排序(文件优先,文件夹置底)
*/
function sort_items(&$items, $sort, $order) {
usort($items, function($a, $b) use ($sort, $order) {
if (!$a['is_dir'] && $b['is_dir']) return -1;
if ($a['is_dir'] && !$b['is_dir']) return 1;
$v1 = $v2 = '';
switch ($sort) {
case 'type':
if ($a['is_dir']) { $v1 = $a['name']; $v2 = $b['name']; }
else { $v1 = strtolower(pathinfo($a['name'], PATHINFO_EXTENSION)); $v2 = strtolower(pathinfo($b['name'], PATHINFO_EXTENSION)); }
break;
case 'size': $v1 = $a['size']; $v2 = $b['size']; break;
case 'time': $v1 = $a['time']; $v2 = $b['time']; break;
default: $v1 = $a['name']; $v2 = $b['name'];
}
$r = is_string($v1) ? strnatcasecmp($v1, $v2) : ($v1 - $v2);
return $order === 'desc' ? -$r : $r;
});
foreach ($items as &$item) {
if ($item['is_dir'] && !empty($item['children'])) sort_items($item['children'], $sort, $order);
}
}

/**
* 格式化文件大小
*/
function format_size($bytes) {
if ($bytes == 0) return '0 B';
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = floor(log($bytes, 1024));
$i = min($i, count($units) - 1);
return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i];
}

/**
* 获取预览类型
*/
function get_preview_type($filename, $types) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
foreach ($types as $type => $extensions) {
if (in_array($ext, $extensions)) return $type;
}
return null;
}

/**
* 渲染文件列表(带安全转义)
*/
function render($list, $types, $host, $rel = '', $level = 0) {
static $folderId = 0;
if (empty($list)) return;
$paddingLeft = $level > 0 ? '16px' : '0';
echo '<ul class="file-list" style="padding-left:' . $paddingLeft . ';">';
foreach ($list as $item) {
$name = htmlspecialchars($item['name'], ENT_QUOTES, 'UTF-8');
$path = trim($rel . '/' . $name, '/');
$url = 'download/' . $path;
$fullUrl = rtrim($host, '/') . '/' . ltrim($url, '/');
if ($item['is_dir']) {
$folderId++;
$folderIdStr = 'folder_' . $folderId;
$fileCount = countFiles($item['children']);
echo '<li class="folder-item"><div class="folder-header" onclick="toggleFolder(\'' . $folderIdStr . '\')" title="点击展开/折叠(' . $fileCount . '个文件)"><span class="folder-arrow" id="arrow_' . $folderIdStr . '">▶</span><span class="folder-icon">📁</span><span class="folder-name">' . $name . '</span><span class="folder-count">' . $fileCount . '</span></div><div class="folder-content" id="' . $folderIdStr . '" style="display:none;">';
if (!empty($item['children'])) render($item['children'], $types, $host, $path, $level + 1);
else echo '<div class="empty-folder">空文件夹</div>';
echo '</div></li>';
} else {
$size = format_size($item['size']);
$time = date('Y-m-d H:i', $item['time']);
$previewType = get_preview_type($name, $types);
echo '<li class="file-item"><div class="file-container"><span class="file-icon">' . getFileIcon($name) . '</span><a href="' . $url . '" download class="file-name" title="下载:' . $name . '&#10;大小:' . $size . '&#10;时间:' . $time . '">' . $name . '</a><span class="file-meta">' . $size . '</span><span class="file-time">' . $time . '</span><div class="file-actions"><button class="copy-btn" data-url="' . htmlspecialchars($fullUrl) . '" title="复制链接">复制链接</button>';
if ($previewType) echo '<button class="prev-btn" onclick="pv(\'' . $url . '\',\'' . $previewType . '\')" title="预览">预览</button>';
echo '</div></div></li>';
}
}
echo '</ul>';
}

/**
* 统计文件夹内文件数量
*/
function countFiles($items) {
$count = 0;
foreach ($items as $item) {
if ($item['is_dir']) $count += countFiles($item['children']);
else $count++;
}
return $count;
}

/**
* 根据扩展名返回表情图标
*/
function getFileIcon($filename) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$icons = [
'jpg' => '🖼', 'jpeg' => '🖼', 'png' => '🖼', 'gif' => '🖼', 'bmp' => '🖼', 'webp' => '🖼', 'svg' => '🖼',
'mp4' => '🎬', 'webm' => '🎬', 'ogg' => '🎬', 'mov' => '🎬', 'avi' => '🎬',
'mp3' => '🎵', 'wav' => '🎵', 'flac' => '🎵', 'm4a' => '🎵',
'pdf' => '📕', 'doc' => '📘', 'docx' => '📘', 'xls' => '📗', 'xlsx' => '📗', 'ppt' => '📙', 'pptx' => '📙',
'html' => '💻', 'css' => '🎨', 'js' => '📜', 'json' => '📋', 'md' => '📝',
'zip' => '📦', 'rar' => '📦', '7z' => '📦', 'tar' => '📦', 'gz' => '📦',
'txt' => '📄', 'log' => '📄',
];
return isset($icons[$ext]) ? $icons[$ext] : '📄';
}

// ==================== 加载文件列表 ====================
$list = scan_dir(BASE_DIR);
sort_items($list, $sort, $order);

// ==================== 前端页面(完整HTML/CSS/JS) ====================
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="robots" content="noindex, nofollow">
<title>文件管理中心 - 开放访问</title>
<style>
:root{--primary:#667eea;--primary-hover:#5a6fd6;--success:#10b981;--success-hover:#059669;--danger:#e53e3e;--danger-bg:#ffeaea;--warning:#f6ad55;--warning-bg:#fffbf0;--bg:#f0f2f5;--card-bg:#ffffff;--border:#e2e8f0;--text:#333333;--text-light:#718096;--radius:10px;--shadow:0 2px 16px rgba(0,0,0,0.06);}
html{font-size:12.8px;}
*{box-sizing:border-box;}
body{margin:0;padding:clamp(8px,2.4vw,16px);background:var(--bg);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;-webkit-text-size-adjust:100%;color:var(--text);line-height:1.5;font-size:1rem;}
.main{max-width:1200px;margin:0 auto;background:var(--card-bg);padding:clamp(14px,3.2vw,20px);border-radius:14px;box-shadow:var(--shadow);}
.head{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:10px;padding-bottom:14px;border-bottom:2px solid #f0f0f0;}
.head-left{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.head h2{font-size:1.5rem;margin:0;white-space:nowrap;}
.pwd-status{display:inline-flex;align-items:center;padding:5px 12px;border-radius:16px;font-size:0.85rem;font-weight:600;white-space:nowrap;background:#c6f6d5;color:#276749;border:1px solid #68d391;}
.sort{background:#f9f9f9;padding:clamp(8px,2.4vw,10px) 14px;border-radius:var(--radius);margin-bottom:14px;border:1px solid var(--border);display:flex;align-items:center;flex-wrap:wrap;gap:8px;}
.sort-label{font-size:0.95rem;color:var(--text-light);white-space:nowrap;font-weight:500;}
.sort form{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
.sort select{padding:6px 10px;border:1px solid #cbd5e0;border-radius:5px;font-size:0.9rem;background:white;cursor:pointer;transition:border-color 0.2s;}
.sort select:focus{border-color:var(--primary);outline:none;}
.sort .btn-sort{padding:6px 14px;background:#48bb78;color:white;border:none;border-radius:5px;cursor:pointer;font-size:0.9rem;white-space:nowrap;transition:all 0.2s;font-weight:500;}
.sort .btn-sort:hover{background:#38a169;transform:translateY(-1px);}
.file-list{list-style:none;padding:0;margin:0;}
.folder-header{cursor:pointer;padding:10px 12px;background:var(--warning-bg);border-radius:7px;border-left:3px solid var(--warning);display:flex;align-items:center;gap:8px;transition:all 0.2s;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;}
.folder-header:hover{background:#ffe8cc;border-left-color:#ed8936;}
.folder-header:active{transform:scale(0.99);}
.folder-arrow{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;transition:transform 0.3s cubic-bezier(0.4,0,0.2,1);font-size:10px;color:#c05621;flex-shrink:0;font-weight:bold;}
.folder-arrow.open{transform:rotate(90deg);}
.folder-icon{font-size:16px;flex-shrink:0;}
.folder-name{font-weight:600;color:#c05621;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:0.95rem;}
.folder-count{background:#f6ad55;color:white;padding:2px 7px;border-radius:8px;font-size:0.75rem;font-weight:600;flex-shrink:0;min-width:18px;text-align:center;}
.empty-folder{padding:8px 16px;color:#a0aec0;font-size:0.85rem;font-style:italic;}
.file-item{margin:2px 0;padding:8px 12px;background:#fff;border-radius:7px;border:1px solid transparent;transition:all 0.2s;list-style:none;}
.file-item:hover{background:#f7fafc;border-color:var(--border);box-shadow:0 1px 2px rgba(0,0,0,0.04);}
.file-container{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;}
.file-icon{flex-shrink:0;font-size:15px;}
.file-name{color:#2b6cb0;text-decoration:none;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;transition:color 0.2s;font-size:0.95rem;}
.file-name:hover{color:#1a4971;text-decoration:underline;}
.file-meta{color:var(--text-light);font-size:0.85rem;white-space:nowrap;flex-shrink:0;}
.file-time{color:#a0aec0;font-size:0.8rem;white-space:nowrap;flex-shrink:0;}
.file-actions{display:flex;gap:5px;flex-shrink:0;}
.copy-btn{padding:5px 12px;background:var(--success);color:white;border:none;border-radius:5px;font-size:0.85rem;cursor:pointer;white-space:nowrap;transition:all 0.2s;font-weight:500;}
.copy-btn:hover{background:var(--success-hover);transform:translateY(-1px);}
.copy-btn.copied{background:#047857;}
.prev-btn{padding:5px 12px;background:var(--primary);color:white;border:none;border-radius:5px;font-size:0.85rem;cursor:pointer;white-space:nowrap;transition:all 0.2s;font-weight:500;}
.prev-btn:hover{background:var(--primary-hover);transform:translateY(-1px);}
#mask{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:1000;align-items:center;justify-content:center;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);}
#mask.active{display:flex;}
.mask-close{position:absolute;top:16px;right:16px;color:white;font-size:24px;cursor:pointer;z-index:1001;width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.1);border-radius:50%;transition:all 0.3s;border:none;line-height:1;}
.mask-close:hover{background:rgba(255,255,255,0.25);transform:rotate(90deg);}
#mask-content{max-width:95vw;max-height:90vh;animation:zoomIn 0.3s ease;}
@keyframes zoomIn{from{transform:scale(0.9);opacity:0;}to{transform:scale(1);opacity:1;}}
@media (max-width:768px){html{font-size:12px;}.head{flex-direction:column;align-items:flex-start;}.head-left{width:100%;justify-content:space-between;}.sort{flex-direction:column;align-items:stretch;}.sort form{flex-direction:column;}.sort select,.sort .btn-sort{width:100%;text-align:center;}.file-time{display:none;}}
@media (max-width:480px){html{font-size:13px;}body{padding:3px;}.main{padding:10px;border-radius:7px;}.file-container{flex-direction:column;align-items:flex-start;gap:5px;}.file-name{width:100%;font-size:0.95rem;}.file-meta,.file-time{font-size:0.8rem;width:100%;}.file-actions{width:100%;justify-content:stretch;}.copy-btn,.prev-btn{flex:1;text-align:center;padding:7px 10px;font-size:0.85rem;}.folder-header{padding:8px;}.folder-name{font-size:0.9rem;}.mask-close{top:8px;right:8px;width:30px;height:30px;font-size:20px;}}
</style>
</head>
<body>

<div class="main">

<div class="head">
<div class="head-left">
<h2>文件管理中心</h2>
<span class="pwd-status" title="无需密码即可访问">开放访问</span>
</div>
</div>

<div class="sort">
<span class="sort-label">排序方式:</span>
<form method="get">
<select name="sort">
<option value="name" <?=$sort=='name'?'selected':''?>>按名称</option>
<option value="type" <?=$sort=='type'?'selected':''?>>按类型</option>
<option value="size" <?=$sort=='size'?'selected':''?>>按大小</option>
<option value="time" <?=$sort=='time'?'selected':''?>>按修改时间</option>
</select>
<button type="submit" class="btn-sort"><?=$order=='asc'?'升序排列':'降序排列'?></button>
<input type="hidden" name="order" value="<?=$order=='asc'?'desc':'asc'?>">
</form>
</div>

<!-- 文件列表 -->
<?php render($list, $preview_types, $host); ?>

</div>

<!-- 预览遮罩 -->
<div id="mask">
<button class="mask-close" title="关闭预览">&times;</button>
<div id="mask-content"></div>
</div>

<script>
// ==================== 文件夹折叠/展开 ====================
function toggleFolder(folderId) {
var content = document.getElementById(folderId);
var arrow = document.getElementById('arrow_' + folderId);
if (!content || !arrow) return;
var isHidden = content.style.display === 'none' || content.style.display === '';
if (isHidden) {
content.style.display = 'block';
arrow.classList.add('open');
} else {
content.style.display = 'none';
arrow.classList.remove('open');
}
}

// ==================== 预览功能 ====================
function pv(url, type) {
var mask = document.getElementById('mask');
var mc = document.getElementById('mask-content');
mask.classList.add('active');
document.body.style.overflow = 'hidden';
switch (type) {
case 'image':
mc.innerHTML = '<img src="'+url+'" style="max-width:95vw;max-height:85vh;border-radius:8px;object-fit:contain;" alt="预览" onerror="this.parentElement.innerHTML=\'<div style=color:white;font-size:16px>图片加载失败</div>\'">';
break;
case 'video':
mc.innerHTML = '<video controls autoplay src="'+url+'" style="max-width:95vw;max-height:85vh;border-radius:8px;background:#000;" playsinline webkit-playsinline></video>';
break;
case 'audio':
var fname = url.split('/').pop().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
mc.innerHTML = '<div style="background:#fff;padding:32px;border-radius:14px;text-align:center;min-width:280px;"><div style="font-size:40px;margin-bottom:16px;">🎵</div><div style="font-size:13px;color:#666;margin-bottom:14px;">'+fname+'</div><audio controls autoplay src="'+url+'" style="width:100%;max-width:360px;"></audio></div>';
break;
case 'text':
mc.innerHTML = '<div style="background:#fff;padding:16px;border-radius:7px;max-height:80vh;overflow:auto;max-width:90vw;min-width:280px;"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;font-size:13px;color:#333;">加载中...</pre></div>';
fetch(url).then(function(r){return r.text();}).then(function(t){
mc.innerHTML = '<div style="background:#fff;padding:16px;border-radius:7px;max-height:80vh;overflow:auto;max-width:90vw;min-width:280px;"><pre style="margin:0;white-space:pre-wrap;font-size:13px;">'+t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre></div>';
}).catch(function(){mc.innerHTML='<div style="color:white;font-size:16px;text-align:center;">无法加载</div>';});
break;
case 'pdf':
mc.innerHTML = '<iframe src="'+url+'" width="95%" height="85vh" style="border-radius:7px;border:none;background:white;"></iframe>';
break;
default:
mc.innerHTML = '<div style="color:white;font-size:16px;">暂不支持预览</div>';
}
}

function closePreview() {
document.getElementById('mask').classList.remove('active');
document.getElementById('mask-content').innerHTML = '';
document.body.style.overflow = '';
}

document.querySelector('.mask-close').addEventListener('click', closePreview);
document.getElementById('mask').addEventListener('click', function(e){if(e.target===this)closePreview();});
document.addEventListener('keydown', function(e){if(e.key==='Escape'||e.keyCode===27)closePreview();});

// ==================== 复制链接 ====================
document.querySelectorAll('.copy-btn').forEach(function(btn){
btn.addEventListener('click', function(e){
e.stopPropagation();
var url = this.getAttribute('data-url');
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(url).then(function(){showCopyFeedback(btn);}).catch(function(){fallbackCopy(url,btn);});
} else {
fallbackCopy(url,btn);
}
});
});

function showCopyFeedback(btn){
var orig = btn.textContent;
btn.textContent = '已复制';
btn.classList.add('copied');
setTimeout(function(){btn.textContent=orig;btn.classList.remove('copied');},1500);
}

function fallbackCopy(text,btn){
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;left:-9999px;';
document.body.appendChild(ta);
ta.contentEditable = 'true';
ta.readOnly = true;
var range = document.createRange();
range.selectNodeContents(ta);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
ta.setSelectionRange(0,999999);
try{document.execCommand('copy');showCopyFeedback(btn);}catch(e){prompt('请手动复制:',text);}
sel.removeAllRanges();
document.body.removeChild(ta);
}

// ==================== 移动端优化 ====================
document.querySelectorAll('.folder-header').forEach(function(header){
header.addEventListener('touchend', function(e){e.preventDefault();this.click();});
});
document.getElementById('mask').addEventListener('touchmove', function(e){e.preventDefault();},{passive:false});
</script>
</body>
</html>

									
赞(0) 打赏
未经允许不得转载:云浅 » 文件管理系统-简单单页php

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫