880 lines
46 KiB
HTML
880 lines
46 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>NAS 轻量媒体播放器</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: "Microsoft YaHei", Arial, sans-serif; max-width: 1400px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
||
|
||
.logo-container { text-align: center; margin-bottom: 20px; }
|
||
.logo {
|
||
max-width: 200px; height: auto; display: inline-block;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||
border-radius: 8px; padding: 5px; background: white;
|
||
}
|
||
|
||
h1 { color: #333; text-align: center; margin-bottom: 10px; }
|
||
.subtitle { color: #4285F4; text-align: center; margin-bottom: 30px; font-size: 18px; }
|
||
.subtitle a { color: #4285F4; text-decoration: none; }
|
||
.subtitle a:hover { text-decoration: underline; }
|
||
|
||
/* 媒体容器 */
|
||
.media-container {
|
||
width: 100%; background: #000; border-radius: 8px; overflow: hidden;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
display: flex; justify-content: center; align-items: center;
|
||
min-height: 600px; position: relative;
|
||
}
|
||
.media-content {
|
||
width: 100%; height: 100%;
|
||
display: flex; justify-content: center; align-items: center; padding: 20px;
|
||
}
|
||
video, audio {
|
||
width: 100%; height: auto; max-height: 80vh; display: none; background: #000;
|
||
}
|
||
img {
|
||
max-width: 100%; max-height: 80vh; object-fit: contain; display: none; background: #000;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading {
|
||
position: absolute; top: 50%; left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
text-align: center; color: #4285F4; z-index: 10;
|
||
background: rgba(0,0,0,0.7); padding: 20px 30px; border-radius: 8px;
|
||
display: none; /* ← 修复:只用 display:none,不重复声明 */
|
||
}
|
||
.loading-spinner {
|
||
border: 4px solid rgba(66,133,244,0.3); border-radius: 50%;
|
||
border-top: 4px solid #4285F4; width: 40px; height: 40px;
|
||
animation: spin 1s linear infinite; margin: 0 auto 15px;
|
||
}
|
||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||
|
||
.control-panel {
|
||
margin: 20px 0; padding: 15px; background: #fff;
|
||
border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||
}
|
||
.select-group { display: inline-block; margin-right: 20px; margin-bottom: 15px; vertical-align: top; }
|
||
label { font-weight: bold; color: #555; margin-right: 10px; }
|
||
select, input[type="text"], input[type="file"], input[type="password"] {
|
||
padding: 10px 15px; font-size: 16px; border: 1px solid #ddd;
|
||
border-radius: 4px; outline: none; min-width: 250px;
|
||
}
|
||
select:focus, input[type="text"]:focus, input[type="password"]:focus {
|
||
border-color: #4285F4; box-shadow: 0 0 0 2px rgba(66,133,244,0.2);
|
||
}
|
||
|
||
.btn-group { display: inline-block; }
|
||
button {
|
||
padding: 10px 20px; font-size: 16px; margin-right: 10px;
|
||
border: none; border-radius: 4px; cursor: pointer; transition: all 0.3s;
|
||
}
|
||
.btn-primary { background: #4285F4; color: white; }
|
||
.btn-primary:hover { background: #3367D6; }
|
||
.btn-primary:disabled { background: #ccc; cursor: not-allowed; }
|
||
.btn-secondary { background: #28a745; color: white; }
|
||
.btn-secondary:hover { background: #218838; }
|
||
.btn-danger { background: #dc3545; color: white; }
|
||
.btn-danger:hover { background: #c82333; }
|
||
|
||
.media-info { margin-top: 15px; color: #666; font-size: 14px; }
|
||
.dir-navigation { margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 4px; }
|
||
|
||
/* 上传区域 */
|
||
.upload-section {
|
||
margin: 30px 0; padding: 20px; background: #fff;
|
||
border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||
border-top: 3px solid #4285F4;
|
||
}
|
||
.upload-section h2 {
|
||
color: #333; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee;
|
||
}
|
||
.upload-form { display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end; }
|
||
.form-row { flex: 1; min-width: 250px; }
|
||
|
||
/* 进度条 */
|
||
.upload-progress { margin-top: 15px; display: none; }
|
||
.progress-container {
|
||
width: 100%; height: 8px; background-color: #e9ecef;
|
||
border-radius: 4px; overflow: hidden; box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
|
||
}
|
||
.progress-bar {
|
||
height: 100%; background: linear-gradient(90deg, #4285F4, #3367D6);
|
||
border-radius: 4px; width: 0%; transition: width 0.2s ease-in-out; position: relative;
|
||
}
|
||
.progress-bar::after {
|
||
content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: linear-gradient(to right, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0.1) 100%);
|
||
background-size: 200% 100%; animation: shimmer 1.5s infinite; opacity: 0.7;
|
||
}
|
||
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
||
.progress-stats {
|
||
display: flex; justify-content: space-between;
|
||
margin-top: 8px; font-size: 14px; color: #666;
|
||
}
|
||
.speed-indicator { color: #4285F4; font-weight: bold; }
|
||
|
||
/* 消息提示 */
|
||
.message {
|
||
margin-top: 15px; padding: 12px 15px; border-radius: 4px;
|
||
display: none; font-size: 14px;
|
||
}
|
||
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||
.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
||
|
||
.create-dir-form {
|
||
margin-top: 15px; padding-top: 15px; border-top: 1px dashed #ddd;
|
||
}
|
||
.file-info { margin-top: 10px; font-size: 13px; color: #666; }
|
||
|
||
/* 媒体类型标签 */
|
||
.media-type-badge {
|
||
display: inline-block; padding: 3px 8px; border-radius: 4px;
|
||
font-size: 12px; margin-left: 10px;
|
||
}
|
||
.type-video { background-color: #e3f2fd; color: #0d47a1; }
|
||
.type-image { background-color: #e8f5e9; color: #1b5e20; }
|
||
.type-audio { background-color: #fff3e0; color: #e65100; }
|
||
|
||
/* 密码保护模态框 */
|
||
.modal-overlay {
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: rgba(0,0,0,0.7);
|
||
display: none; /* ← 修复:只保留 display:none,去掉多余的 display:flex */
|
||
justify-content: center; align-items: center; z-index: 1000;
|
||
}
|
||
.modal {
|
||
background: white; padding: 30px; border-radius: 8px;
|
||
width: 90%; max-width: 400px; box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||
}
|
||
.modal h3 { margin-bottom: 20px; color: #333; text-align: center; }
|
||
.modal-form { display: flex; flex-direction: column; gap: 15px; }
|
||
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
|
||
.protected-badge {
|
||
display: inline-block; padding: 2px 6px; background: #ffeb3b;
|
||
color: #333; font-size: 12px; border-radius: 3px; margin-left: 5px;
|
||
}
|
||
|
||
/* 支持格式说明 */
|
||
.supported-formats {
|
||
margin: 20px 0; padding: 15px;
|
||
background: #f0f7ff; border-left: 4px solid #4285F4; border-radius: 4px;
|
||
color: #333; font-size: 14px; word-wrap: break-word;
|
||
}
|
||
.supported-formats strong { color: #4285F4; }
|
||
|
||
/* 响应式 */
|
||
@media (max-width: 768px) {
|
||
.media-container { min-height: 400px; }
|
||
.select-group { display: block; margin-right: 0; margin-bottom: 15px; }
|
||
.upload-form { flex-direction: column; align-items: stretch; }
|
||
select, input[type="text"], input[type="password"] { min-width: 100%; width: 100%; }
|
||
.supported-formats { font-size: 13px; padding: 12px; }
|
||
.logo { max-width: 150px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="logo-container">
|
||
<!-- Logo SVG 内嵌 base64(保持原版不变) -->
|
||
<img class="logo" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIxNDJweCIgaGVpZ2h0PSIxMTNweCIgdmlld0JveD0iMCAwIDE0MiAxMTMiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE0MiAxMTMiIHhtbDpzcGFjZT0icHJlc2VydmUiPiAgPGltYWdlIGlkPSJpbWFnZTAiIHdpZHRoPSIxNDIiIGhlaWdodD0iMTEzIiB4PSIwIiB5PSIwIgogICAgaHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFJNEFBQUJ4Q0FNQUFBQTA1MnYyQUFBQUJHZEJUVUVBQUxHUEMveGhCUUFBQUNCalNGSk4KQUFCNkpRQUFnSU1BQVBuL0FBQ0E2UUFBZFRBQUFPcGdBQUE2bUFBQUYyK1NYOFZHQUFBQy9WQk1WRVgvLy8vLy8vLy8vLzM3NHZYKwovZnYvL1BuOC92MzYvZnY5L3ZuNi8vejUvLzM3Ly8vOC9mLysvdi84L1B6OS9QcjgvUDc5Ly83OS8vdi8vL3YvL2Y3Ky9QMzQvZi80Ci8vL3E4L2ZNMHRla3NMeHdoWnBOYUg4L1VHSXhSRjAwU21kQlduRlpjb21LbksyN3h0RGY1T2piM3VFYk9sY0JJMFFDSUQwQkpVa0UKSjAwQkhrSUtMazhrUldWZmVJL3Q5LzBGS0VJQktWRUZMRk1ITDFJS01GUUlMbElITFZJSkxWQUlMMDhGTFV3Z1BWL1MydUhxN081NgpqWjRkTmswR0tWUUpLbEVLTFZNSU1WSUlMVTBHTDBzSEkwNUVZMzJDbEtVVE5GQUpMMVRDemRVR0xGRUZMbEVNTGxNSEtrL2Y3UFExClVXOEpLMU1JTGxNTEpVVWRORklLTEZBUExFb01MMVVITFZRQ0xWRzR3Y2tMTGxNSkxsWUtNRlZvZjVNRkwxTUVMRS85Ky96Ly8vaXQKdThZRExsankvUDRHTWxuNCt2eng5Zk1tUFZRM1YzUU9NRlVMTVZZS01GY0pNVlVLTDFvTk1GWUhNbFVDTDFVSUwxa0xNVmdRTGxvTQpMRmdLTWxVTE0xWUdNbFFOTVZVWU8xME1NVnNOTDFnSUoxVVVOMW82VzNpWXA3VG42ZXZ5OHZIMTlQVDI5dmJ4OGU4TU1sY3BUR3hTCmJZYjcrL3o1K2ZvSUtsWU9NVmY3Ky9rY1FXUU9NbFlKTTFrdFQyeno4dk1JTTFiMzkvZjE5Zk1MTWxrTE0xVDQ5L1VOTkZ2ejlQVVAKTlZvT05GZ1JPVjRsU1dzMFVHWUpORmNGTTFFUE4xdEJYbm43L2Z3eFVtNFZQV1B1N3U4NFVtMzQrUGtNTkZjTk0xajYrdmNKTTFzTwpNVmtMTUZvUE1sZ01NMW9NTkZnUE1scjE5dmtMTWxzS05WZ05OVmdlUldrTk5sWVFNVjBPTVZFUk0xME5OVmtOTkZVS05Gb3ZVM0VRCk0xVUxObGtJTWx3S05sMEpOVno4L1BjTE5Wc0lObGdRTjE0UU9GZ05NbHdNTVYwSk5Wb09OVndJTkZrZFJWNE5NMTRPT0Y0SU5sb0gKTlZrTE5WMFBPbDBMTlY4S05sc0pOMkFMTjE0TU5sdzFWV3dKTmwwTk4xOE9OVjROTjEwTk9Gc01ObUVPTldFTU9GMExPRjhNTjFvTApPbUVRTzJJTk9WNE9PR0FQT1Z3Tk9HSU5PV0FPT21FUE9WOE9PbDhSTjJFR09GczFWV29PT1dNTk9tRUtQVndOTzE4Tk8yVU1QR01HCk1HQURNMXNITjFzSE4xMTlpWkgzQUFBQUFYUlNUbE1BUU9iWVpnQUFBQUZpUzBkRUFJZ0ZIVWdBQUFBSmNFaFpjd0FBQ3hNQUFBc1QKQVFDYW5CZ0FBQUFIZEVsTlJRZm5CQUVORWhZT1VSYnVBQUFaSVVsRVFWUm8zdTJiRFZ4VDU3M0g5eHlPaUNZbjc2YzFnYnVaRUliUgpFdHVDVVF1c2dyMVFhSzJWYm8yTGhBNVphK2NDaGpWYlRrR3VCS0hoWlpoR3dmaEd2QlNzemtSYWM2cVJPVEhPSzJQTVdxL1pCUjNzClFuQ3dpcFFFZHJlNzNiZlAvWjlBQW1wN0s3Mno5M00vSDM5NGtwenpQT2M1MytmLzhqelB5WWxmK2NwRFBkUkRQZFFjaE1MQ1lJdjQKdjBTSndPZEZ6bzlhc0dEQndvVXNOcHZGSmpqei84K1FJakNjaTdONHhFSitKSUhqQ0xIWmJCNHVZUE9Ga1FUMjVjTVFySVVzbGtqSQpRaGcyNHlzUVFSS0VnSVcrWEpoSDVqL0tZckhnNm92RWt1aVl2L2txNkd1THBiSllPUnpEQ0lUNGdpL1JRamppTXlnb1RyYjQ2L0ZMCjRoV0twVUVwbGkxN0xFRVp1eHdKRnJCdzlHVlppUEVJd2dUTEpYL3plTHppaVNjVGsxYW9WQ3JZVmo2MmF2WHExZkhMbmxJbUkwVGkKYk1HWEVkUVJpTTFFUzBycU41YkVQL0gwbXJTVjZXdlhyZ0tscjFyMURQT2lXdk8zOGZFWnNZZ1FRR3cvY0k4SkJmd0ZMT0hDekdmagpGYXZYcmwyNWF2WEt0YXFzRlN1QUkxMjFJdkd4bGF2V1B2YllxbFdLSlRGZ0llSlIzZ04ybUVqQTRpT1VuUkQvNUtyVno4RGY2dFZyClY2OWVOU1hBV0FXR1VxMEMzejJuV0pMS2h1UW5XUStTQnBGTVBrY3ZVenozSFBobXBZcTUvS3JuMDRNMDZTdGdiMFhpcWxXSnExVHIKbmxPOXNPeXJ5U3pFSm9nSFNFT0lXR2o5aTh0ZTJQRGNTbFdPS2lkSEJhOWdpaytYNGlVSndpSUo5b01LYUlFSUVaajRtNG9YdnJVbQpiVU9pNmxOQlZMTU9MMTBXRFZsSXNoOE1EWVlXQ0xEc2w1OTRXcjB5YmMyR2pUa2hxV1krQlVsVVUweXFEZDlXTFpQQ2tJZ3RmQkEwCkJBOG1KczNMaW1mUzB0STJyVm1UdUhJMnlEVFZ0RzJtanEvSVVnRVBRdk9KQjJDZmlFaFNoTEpmWHJyMm1jZlVhYmtiMWp6UFhEMUwKRzFhV2FwYU5HT1ZrSmFvMnFwYmtJWUlnLy9yNWpnbEYyUHB2TG4xbDdjcE5hVG1KYXphb2N1LzBsT29PbnpFdkt6WWtKdVorWjBrbQpZZ254L3oxUEJLUHdIb0xoaHZXMXBXbWIxcTFMVXozM2ltcERibTRPazFiQjRKMks0THVzbzBwTHkxMEJQRXMxS0pJbCtGK2g0REM2CmtJeUk2WGtuSWgrT0tCVzVTZXZVZ0pPbXlrcExUTXo1SE9VQ1VPTHphWXB2cm9jNVZmVEZ6WUlJZGlRZkZ5d1FDUEFvRVR1NG1CSnQKM293SzRyK2JDN1paRjdyY05FL3U5SjRLRHFSdHpFbFQzWTIxTEFQaE9QZUxqajR3K1FraWtRZ1NGRFlCVEpjRUJrc3NISC8xRzRyWAowdFlCejZ3cnJWQ3RTR1BFaEZCV3prYlZwaFVyWmhkUCtXMmpRb0kyNC9nWE13M0JaaEhNRW1MTG9oUzUvSFZZN09FQ3lBeGNnRVV2ClNVcGtMcTBPeGNZR0NKbkUwRTdPY3pCUXE1NS9aZU5HMVN3eFNKQmdUeTJQMmt4OEVSN3dzb0FmK1VoeTZ1THZ2YlFzUHY2WnJiSkYKQkxQOHhKRDg4YlhwU2V1K3IxNjNUaHRLYjUwMnQ3QW9LMG1kQkFleXRQQmhrNjVRblRVcithZWt6b3BQUlZGUlh5QzUwQUl3QjRwZAovUGdTeGRMVjIxNUpXdnVkK01laTF4Tkl5RWN4ajZlbnF4a2NkWGlrMFdvMzZkVnFOWU9pWTNhM0ZXcHpBZXB1bkt6aUh6eTdIdCtNCjVzeURXQXNGYUxsMHllcFhpbC9aK01ZMlEzRngrZzkvOUxjdjU4bGhUUkcvRm5DK3IvNytqSFVLczVLU3RJV0Z1UUREZk5KdEt0UW0KcVF1THRQZmdaQlUvbm9sRmllYUtneDVsYlJZbUp5eFphNlRTaTdUcjNnUzdGK24xUDFxamVGWXBYL3gwWVE1WUI0d1J4bEZyazdLSwpzdUJObTFYQ3VLdW9KTW1nM1hRUGpiWXdTZjFDNlNNWVNjd3R1U0pFanl6QWtyLzVVczZiUlVaakRsaW1zRGc5Zlh0Wm1mcEhmNmQ0CitYSDFHN2xGV1pCWTMxODNxOS9hSFUrVlMxTk5CY255aXAyVlZXL0pTb3FUTnFYZmFSbEc2OUxpQy9ETmN4eWFGL0JaMlBydktkYVoKYzZZalk4b0dhdlhHRFQvYW9NdlpDSE80T20wVC9PWG1ibUl1a2xSZGsxcVFYTVhtMVBLRVNFamdxQzV6UlhwUjdxWmNHTENobU5rMgpKYXBXd041ajZxVXhLR3B1T0lnRmR5T0w0MytzMWpHQkNmK01JQ1lTOU9ha2VyMVdaOURwa2d3R28wNnJLOUVaU3hJeU1ndmlxdW9RCm5wK2ZUKzdpMUhJNUpGZGowUnZVYnh0MWpMUTZuVUd2TnhyMWFpczBwRXQ4ZGpsR3pPbk9BbTRxTVpraVo1dEJyV1p3WnNsb3RScVQKU3ZUR0lrcW4xcXNOUmJyZFNrbnlIZzZaVDdCNElvZ0pMcGVkdnl1U0U1dGdMdFRweW5SR0NvajA5VkRSYURUb2trcEt0SWF5TEVVQgpjenMwSitQdzF6KzdwcUdCUVlGL1JtT0l4bENtWjdwcmdGWkxkSWFTM1htU3VDMjFHRTZTY0dmZXVETXVXNU9DMS9Md3ZhYnFvaEo5CmlhNGhxVWdYYk1HUXBOTlJhcjFOVzZRcjFLY3ZqVVlrTmdjYzdGRVdTbzEvczdnb2Zjb3llbjNZT21hR1NiZlBxTmNWbDBwTjhrcEUKQ2poSXRMOHVXU05UYmswNGtCQUhjOUxCekVOYWZWR1QvYW5ER1RYU3ZPalUxTlJvWlhtcDFxeDlRMi9VcWt1S1h2aDdXUGZjL3pJKwpJb3FGRm4zdk84M3ZGQnUxT3ZXMHBvSzVSRnVvVTVjeDI0dXA0aTIxUXJoZHdmR0syTlNZQkRobTBKYWFHZ2tlR1Z2NllreWVTUktiCkxPZlZjbW81ak1nVXplS2k5Q1NvcE45a1ZIM2pWWXdrNzk4NDJFSk1FLzltZWtOUkF5UGpMQlVYRzVPTURjMHRyYWFLL2NRUkxpZHkKdjF3Uy9lN1JmWWIwNXVLRzRxMlNLcHczajNjc0x1WDFLaExpZzgrZHorVUdjZWFqWGFaOWtBMUd5SXFpTjErRys4QjU5eDNMQ09lagp2T2UzNmQ2RVVEUXkzdGRxaTZaeENvMWFZM0ZUamVRWVFaSWlnbE9seWR1cU5ocTFoV3BEb2M0U0hZZEkzcTc1SkpkRGtQa3NYRVJnCkJHZGEzQ040WnBGQlcxU1VwQzFXRjc4VWkvRDduN2RRSklGKzhtMmJycmd3S1RTRUpVRWFKWldBeDNUcUhURUZiM0ZGbkVqV3dlVE0KMWgwNm1BdDBTZHFrYXFsRWZKd1VrU1NQRFdzUlBqbC9aK1dlUFpVN09WZ2tleDZQaE5Fb3BWV2RwSWZCMjFCVy9PWXlDU0xuM1M4Twp3Z20wcURUWDBXdzAyTXdoT1kyVXVWNXZzN1hVU1Biazg0OFFlS1ZHZWRpMno2eXptZDgySnlnbDhrcFlEQkVuVGxTOUx0YVlaQkM3CkdhMmdqRHlUSEdkemEvTnhqbVNIMldtbzF6dk1CcW8rWHNaSDg0VDNGOGNJbGpTRS9LVzI5QWF6N3IxUVF1bmZQMWxpdHBtcFZra0sKbXlCeDdIV05zclNzeEZ3R01KWWFVeHpKZ1h1V0NuRkJabDdNdXp0Y0JrTjl2Wmt5R0pnUlJ4OWRkWVROM1lWWHRKWVlqRTRqYmRZWApHVDlRWkNJMjk3NiswUkJ4U1JMdTc3T1hOQlNaS2JNK0ZNSTJzODVjYjZpV1ZUeUNjL25jS28yMHRLemVScFhZZG16TmpEMkY3VG9ZClY1QXAzVzFwZWsrdmM5a01VTmRHd1dhZzlFWkQ5SDVpUGtkVUdYMzZiV3U5emd4Rnh2ZCtxSWpHWUdYMytRTXpnbnRkY1AzQzJNWGYKb3VyTnpRME5WRWhXcys2UVVrd0tvempFM214bHFlMmtycUc1MmFLTVBjYmx5VFdwTWRVNksxRG96QlFGaDQwNnM5N3MxTkxOelZUegpickVRNStDOGdsS3FtZElERGxRcSs0QVpCMFVFZ1F2L1o2SW9FZ2tSdHNYMDFhVXZGRkVVclhNM2gyaE9tOTN2U2lyNU9JK1lMOCswCk5PanE5V2J6VnBsNDc1NDRrL0t3M1FaekFkUUZGK21ncnRscE05VHJ5dDR1b1pxYkV6UjFZTzBqMmRXVTFXMXNleHZHWnIzT2NGS1IKQjZzNG5JakVJMG5zczMwR0syR0UyS2FuRkQ5K281NHlObE1uZFdIak9NL2t4V0ZrdmdnL1pub1hJa0JyZHJhYVVvNW5aN2EybTVtQgoydEZzTmxNMnczc0dJMlYxR1psektZUFphRHRacnVHSWVFZHc4VllyRFpheVVycDZvMWxYVnIrbU5YazV3Z1FZTG9TN1V0R25XeWlDClFMZ0lFMjlWUEZsRzA3YjNxWDNRT1ZzSUowR3luNnpsNHZNMU5VWmJVWE94bzFVU2w1eVo4ZE5tYXdNVFlNMUdRRERDR3pQQk01K00KelRxOXNjR1NHbGZINXBGQTR6WmFLYU81bUlLWlhhY3ZLenU3OW1kUExVNk5sY1BpVndpWjgya0dpb2hDRU1LeWwxY1htYzl0TjRSQwpXR2ZZQjhPeW9WVnpndUNRSlBnSkRHV3pialZwVERFV3lrM1JOSVFFaEFyRUROWHNicWFwS1h2U2xNMWhUTWpMM292eU9VU2xwTnBnCnBaeVUyK2hvcG9LRHhudnZ2ZGZ3eXZNS3hZNk1naE93MUJDeCtQY2FhTDl3RnlMeUZCMzE5U2QxdG5ETU5MdVo2SlRLRWNGRGV6V3QKSngzbm5aNkV6SUxVQ3orbmFkclI0WEk2bTV4T204ZERPNXhOOUFHNncrbDBuT21nNkl1V0dKbTRVU1NxSmNqR1RJdmpiYnFEdXVodQpjbG5QME5BZHA5bnMrb2RMMXM2MjE1NVVQQ1Y3RmNmSXlIdFdZNzhRd3AydVZQSCt0cktUKzVxZFRucGFQM1YyMFllaUt6Q2NWN3RUClp2SDhzcU9ManBIbFdad2RiamROaDJ0MU9McVlOK3JNZVFhc2ZXdWVTWHo4QkJaNWhFU25ORFVkWjJ4dXA2T0pwcytmb2MyaE0raDkKNTgwMDVhQUtGUW14TUg2eTJYZnlDS013SEVuamJSOThVRzkydTh5TzhHbG56TjJweDBndUcwK1dYblE1SGE2bWpCaExWd2UwMzlIQgoySUx1b0drM3ZEZ2NZQk9uM2RLcXpKUWtIenVCQ0Z5QWlCT25Zdk1zNTVzb3Q4dlJTZE9laXpUZEhHclc3WURPbktGb3MvdEpSZDZqCk1BcmRzUnlMRUFKTmRQeXZ6R2ZPT0E4MXU1enVFSS9UZGNpMG4rQ3hhOFd0blpTRFBubHhSNmtaQ0tDdkRnb0F3QzVuS0FhbWFjZnUKREtWSmsxeEhjcmg4eE9ZVHFDNUZMS3NwcFR1c2RKT2JEcHJiNFFEeVVMdE9jeE9ZeW5iNi9aTnZMSDEzUFNJWHpyWVBQZzliVUJDLwo2V1JUay9NUW5CUDJBcHdYdlJQdElrNW9xajEwUjhlQjgyNmFnZ2lCVmgwUUpSMzBBZGc2ZnBXUUVXMFNWeHg3NndUbmVHUGp3V01wCjh1d0NXWFROdXhZbjdmWTQzZTVtYVBBODFlRjB6R3JZNlFMWDB1K2YzbTZtWERiRjErVm9vWWdYam1mK1BDRmE5R3lhK1FQekI3WVAKVHRJWHFURE54YndVR0QvM210cWRUSnYwQVdnVFFKem5MMW85SGtpc0pxckQxVkdhQVV1K1BKQXlUeW1OeVNpL1lIRTZ6b01Ob1o0YgpJQnhONXgyZTVpNkh3OVAwODQ2WmZqb2NEcDNUdHAyeVhmNlFYdnIxOWFoMnhqdzRqb2xxNHMyMkR6dHRWOXBPMjl4bndpZEo1VGpKCjNtdXl1QzQ2YkIyT2k0NnBobWlYR1JJSzNwMGRkQlBUWS9CdXNPZ01RRGN4SHp3ZlVXNDNYRHg0MUZOYWZ1RnRwM3RIZEl4bmhvWjIKdlgvWlpuT2RkdGxzbC9YeFgzc1ZzUVRUNjFVWXNMSHMrSDM2RDh2YVBKN09OcXVWb2wxbllLUndkTFhHd2JTdzMyU1p2a3FIbTU2cgpncGk3S3pRMDNXV3BNTTJLSFZkSU5wQWhYb213QmRPM3BsRTRpeFdqTUo4OTU3cnE4WGl1UUxMUzU4MFhmK2s4MlM0bWVGeVIrRER0CjlIUkF0cGtaaTh4TlRKaWZkRlh2ekc1OTBXMlhLTStFanRQMEhUZzJTbEVnaEhVV1E0TUpmeUZNZnJ6bzhybHpsOTJlb0t4V3NMN0wKNGNqY1QzSTRjVEZPWnJCejBCNEloN25pT0duSHZwKzZxMU9PNzBuZVhYMU04dlB3Y2RjTUR2T3kvWjEvZkZVbzRFLzVTb2hTSDI4NAplKzdhNWVZUVRwdlQ3UEpzaFdWVlpHT2VpL0k0emJRUm9DamJYSDFsbzh3ZE5tZDFTcHlNbTFmTmtyakN4N2R2M3c1RzJjN1loc0c1CjNCd3Z3MWhCSEJ3VFB2clZiK25PbnJ2c3VXcWQ0dkYwYm0ranZKSTZIcm1yb0JTQ3FSUE9OYmhzbnRPMnVjclQxa2Jid0M2ZVhkR0gKU2NuTThkTlRUVm10UVJ5Ynk1Yit2ZVdJK1E0VFk3R1ErT1VzOE5VNXQyM2FPQjUzVzV1dC9CZzZRbFNVZDNaMmV0cXVYcjNpdWZycgpmM0pmbmFQYzFwNnJ6Yit1ZnF2Z0tDZjZNSko0UXNldFlYbUNPTzREZEx3RVo2eURXR3hrVWpTY3ZYYnU3RlZYRUliUlZXdWJDWEU1CnRiS09Uay92MW93TFZLZlYxZGxwdlRKSGVUcXBydXVYZDFjVVhOZ1pYWDJ3d0hOUCtaVXBuRE9VKzRsV3hIempqQkVZcWxGc1Azdm8Kck92WFV6Q01oZHFja0ZaSDhMaHkrc3B1eWJHRDhzelNHMTA5WGU2dU9hcmpOeDN1UHNmUjFndnQ1WWZ0NWUvMnpKVDBNSjk3dW01NApHRy9abkIrK1YveXo5Y3hBT0k4UVlydC92UDNzdVhPdWFmTXg5YTUwU2h2WlBFeENXUzBGZFREWkg4eTg2S0Y3NWtyenViclI1Wm1LCnBlM1g5c1VYb09BQ0dUdVZzTzN5NWJPWE84RTRiVEFTUXJVREY3dGtKL0xKUFZKM3AvSTQzRXYrQXNtcmI5eUFzeDhRVHR2N3A2a1gKb29NNE9KS1hYcnQyK2RxMW5rczlqSUpHN3ZERVl2a0VNTGd6ajhNOHdlRld4bmg2UEEvQ09oNG1PRHF0bmdOclloZ2NuRXZJU3orOAozTm5UZVdrR3A2dnJrcGhnMXhhMFhMb1JYY2ZpY0hqc3ZhMWRydjdPdnpyUHREb3ZYcmxjK0Z0V2NGQkdjVHMrWkZpbWFJSTROenA3CjVJaEhtQzUxWGlxUHczZnh5Rjlvamw3NTZKOGZGRTBYR09sYTd1RlREQTQ0YThlMnprdWRuVDFoSEkrbjMxTkI1Sk1taUtKT0pkeG0KczhWYmFVZzN6d1BVYWF0bFVkQTZ3a1ZQQlhHbXhaUjFlVnh4REk3SGJmVmN5WkJKVWc4UERBNzRXZ1o4ZjJYMWh6OE5EVnhKV0E0NApYS0dRbGJBTlBIVzlaOHBkVE9IQWpZL0VSQzFQMHRmVjM5Ly9VVi9IcFVPRGNHNy9GNzdzNStpai92NmhtNjlWTTg0U0loRVdrL3U3CnJyNmIxL29ZWFI5aUtnejdKTHhJUE83d1FIZi9VUC9BVU9pOFlkL3dvUmJmeUtDdmY5QTNNdFF5TXR3LzRQTU9qb3lFK3pneUFEMGUKZ3VLK0VWOExWTzlyNlI4K05QUjdlQjN5OWNGSlF6ZjdSL29INzdFeWcvUG1CV2FCZ1hBY1pTN3R1WDY5NzZQcllaeit2cEc4VTF4aQpUd1pjdFgvazBJeFpEZzEvZkxSOVJwYjJ3YUgrb1pZdzdrRExZUCtnWmFaNHlEZHkvZERJYjBadXRjOCs2VjZmLzM1dzZIYy9DQ1k2CnNUa0t4VDU5dmErbnEyOGFwei9vb0dvNXlUNGhPOUR2NnpuVWRhTS9ySjREVW5sY1dQSmtVL1hIZmYwWFE2WERCL29zcWVKd2NYSm0KZTBkL1g0K3ZSaE0zNnh6VFlQL2RPakFFT0ttTWRTSUV1R2lSNVIrdTl3MzBYSWVacElleFRuOGZFTXVJV2hSM29XVm9zTVhYTjh1cwpvOUpHVEVpSWdqK21RbnpFS2pnNjBCSXU3eCt3eXhwRkJDSUpJVW5BM2RZV2lYMWthRENqQWhFRWd2dHg1c2MrQ0drK3VqZWtmejk4CjdZblk0T0k5S2txSWxFOWU3K3ZzNjV2QzhRYmwyeW9tK0hVeStHUWZ1dTBOYTJ4WWVvcmdzRlBFakNvaTU3UDMxM1IzajRWTFB5bVgKWS9uc0xYVmJqaCt2MjdJcmtrakpHQnYzeWtSYzlxNDRjVWdtN3lmZXUzVjcyRlc2S0lpRDRSaUtmV0d3YTZSbmdORk5hTnZYUFRRNgo3azA5UVlwU2FqNFo2Ujd0RHA4Mk91aVZWdkh5a2JqYVlyRzBhMUErZVR6UFB6UWVLbTdwcmFuaWsvTk5oNE1TNTBkdVVYcEh4eVFrCnB6WmZabUYwbEhueER0MU5NemppVGF3SkxyKytFZ0d4ekhyeHcrdVh1c0k0QUJRWTl4NHVxT1VnemU2eDRZbndhUU5qM2pGcEZjRkIKNHFPVG85MEJqWWdrNjZJSEJtZWF2MTJ6bjUzUGt3VjhmdDl3UzNZK1hxZjBEdmtsT0llRFo5cDlvMTd2OEtoMzVQYjRQY1laLzhpbgpLSWljZXFMTjVGYkIwc0dSbXplSFF6Z2o0OTNlY1crNW5FK2VrQno5MVVqNE5GL0wrSWgwUDhuRHhPMUQ0Mk8zTklnbnFJdjJlUWZDCnpYcHJxdGpjSTdMeDBRSDdXQ0NiUng1WCtzWUdKRVQrZks1RXFsUktZWXRwSDV1OGZRL1BTTVB1VjdFcG5BaWNSSStVYjV3TVRQWU4KamtCWCt6OFpZTWJMZ2I1ZjFWUVFaSjNFTW5MSU4rQWJHaDRZaEpqdUg1WldrUnhDYklFQUJCd09lVHg2OXVEcXF6bUZ3eHB5YU1RMwpOT2pMNXVEN2xRTWp2dFI4TnNuWlg5VUkybis4TWJaOHVqNlRVc3hRNWhzWStPU213b1JDRHdZd0hDZVNTL29taC90dTNyeDVmYlRYCjJ4dndCOFlteGdOMlpRV2ZPQzZwOWs5MEJ5WUNBZi9vMkpnOUFNNDZnc1R0Z1VEZ0tPQnc2cUlEM2tCSTNZR2FLczZSZkZtdjN6OGEKQ0dSejJNZVY5ckd4d3hvZWp3Tjk0SkJzU0dPOGdHbHJTbjZ2Mys4ZDgvci84TTYvc0pCb0dpY0M4aEtUTFowYzhONzJEZC8wZC9zbgpBcmY5dmIwVGYveWp2VVpUeTZyVFpBVHN3REx1dHpOSTByMGNjZ1lIWWljd2kyZThaaTg1SDNDWWhQUm5zOGxHcGQxL2E3ZzlUellsCjB4NDJtNml3aDZ2ZkR0akhvSy9lajFkbmczRkMzM3d6MGNQS2VHNXljdEp1bjV3TWpBWjYvYmZ0UTcwVGdVLzgxYVlLRE1uempvN2IKZS8zMjIvN0E3UW13RG1SV3UzY0toMkJ3WnZSeFRWVVVPR3ZVM3RzOWRpdjdDSEZjNmU4ZG54ejFBN0lQdHZLZExDNnF1RFVhcWo3aApIK3NkdGR0dnJzNkVxVW9ZL2c0RGgyRnQrWVVudTBFQjcrakU1RmpnOXFUZDI5c0wxdmRLWXh2Um5vSVl2OWMrTWRFTEFTbzlSWEx2CnRNNk1jWHA3YXhvak9idGtmd3A0L1JPKzdGMmMvZEpiZ1c2N2Y1d3BoR3JsYnhHNytCWCtLV2N4Rmd4TVROZ0R3OS9OUU1Uc2gvMFIKOC9nSXJhOSs1MS85L2orUGVzR0dvNHhYb1htby9FbTdVbEp4NnFCRTJUN2gvZE9vZlV6YXlPSGVHVHV6Wk0rb2pPVE15d3c2MXg3TAppOXhTSUd2dnJUYUZKRG5JNWlDNTNUNU40NTMwK3IyQlQxNzU3YXN3anMvK0pRUWk4bkhXK3QzUGZQeVhXd0Z2Q3pUV3kvVEc3NThZCjl3ZnNmL0puUkd2a3g4U1pOWmFqdHlha2UySElEK0dRNkE0Yyt4L2Jzd2tvbmJwNEJYNkV4Q29PMnkvc1FjVDA3Nm9GK1lnckNmUk8KMHpDeTkzLzNKOHNSaWQvNXN4NE1waGpoOHBxbHYvbmorTC81Sjd1OUFOUGIyOXM5YVlkZVFpalpqN1puNUtXbXB0YTBqeXZmcWp6WQpPSVVUMjFoWnVUTjFGbzQvME51YXZiT3lzdkxnd2IwSEQxYSsvdnFwZzNIVmdRc3BWWldNOWtEdFUzc0t5cWREQjFoNmgzcEhucWhaClRvZzI0M2Q5bVNzVWlTSVJNdjM4dFgvN3k2MWV1L2MyMDRYZXdCZVdkMVpFM1Nzd2VlKzRIYTVqZisxbk1oYWFKOER2K1dvWkUrV3oKa1doOXpkcTB3TVNmQW9QZDNiZjl2dHYzcWZHNzlIbjFBNzd1MFluQXJjbC9mM3B4TW9ad1lkU25QTjJLUUxnb0VzZVNwVDk3b3JOLwpkUFRQbzhPM2VxYzFrem1mdmo5bmVXL2RIdTByZnI2OFlDRmlDd1NmOFdBVTR6TWhoSkpsaTcvK3hIZi80NTkrZWVNL0dmM2hEMy80Cno4L1JmMDNyczhxbldwaHA1NWV2L2VBSDd4ek95MFlJWTdISXozeUlEUXNrSHZQY0JzbGpaY3FZeGYveTJ3ZWtyVEhSRXZGeWhFRVcKUHZJLy9oNERFeEVremhaaEJIcVE0ak1MUTRMTjU2RFBlb0EwSytYWkdQOEluODlpOGJId2Y4T1kxbWZ0M3kzb05YTzlzTzRweDNHTQp4U09JS1B3K0hxZkQ1ZGdFd1dheitaRmZYSHo0aStSUDYrNUNsb0FVTEJSaTJQMCsyNC9Bd0YyNFFEU3R1M3NaMmcrVjMzMzg4NFF4CkQ0anVFK1doSHVxaEh1cWhIdXFoL3QvcHZ3RzBHTDBDS29ydG9BQUFBQ1YwUlZoMFpHRjBaVHBqY21WaGRHVUFNakF5TXkwd05DMHcKTVZReE16b3hPRG95TWlzd01Eb3dNRlhDVXhrQUFBQWxkRVZZZEdSaGRHVTZiVzlrYVdaNUFESXdNak10TURRdE1ERlVNVE02TVRnNgpNaklyTURBNk1EQWtuK3VsQUFBQUtIUkZXSFJrWVhSbE9uUnBiV1Z6ZEdGdGNBQXlNREl6TFRBMExUQXhWREV6T2pFNE9qSXlLekF3Ck9qQXdjNHJLZWdBQUFBQkpSVTVFcmtKZ2dnPT0iIC8+Cjwvc3ZnPgo=" /><br>
|
||
</div>
|
||
|
||
<h1>NAS 轻量媒体播放器</h1>
|
||
<div class="subtitle"><a href="/static/zhinan.html" target="_blank">设置使用指南</a></div>
|
||
|
||
<div class="media-container">
|
||
<div id="loading" class="loading">
|
||
<div class="loading-spinner"></div>
|
||
<div id="loading-text">加载中...</div>
|
||
</div>
|
||
<div class="media-content">
|
||
<video id="videoPlayer" controls preload="metadata">
|
||
您的浏览器不支持 HTML5 视频播放,请升级浏览器
|
||
</video>
|
||
<audio id="audioPlayer" controls></audio>
|
||
<img id="imageViewer" alt="图片预览" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-panel">
|
||
<div class="select-group">
|
||
<label for="dirSelect">选择目录:</label>
|
||
<select id="dirSelect"></select>
|
||
</div>
|
||
<div class="select-group">
|
||
<label for="mediaSelect">选择媒体:</label>
|
||
<select id="mediaSelect"></select>
|
||
</div>
|
||
<div class="btn-group">
|
||
<button id="prevBtn" class="btn-primary" disabled>上一个</button>
|
||
<button id="nextBtn" class="btn-primary" disabled>下一个</button>
|
||
</div>
|
||
<div class="dir-navigation">
|
||
当前目录(根目录在/mnt/):<span id="currentDirPath">/</span>
|
||
<span id="protectedIndicator" class="protected-badge" style="display:none;">已加密</span>
|
||
</div>
|
||
<div class="media-info">
|
||
当前播放:<span id="currentMediaName">无</span>
|
||
<span id="mediaTypeBadge" class="media-type-badge" style="display:none;"></span> |
|
||
共 <span id="totalMediaCount">0</span> 个媒体文件
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件上传区域 -->
|
||
<div class="upload-section">
|
||
<h2>媒体文件上传</h2>
|
||
<div class="upload-form">
|
||
<div class="form-row">
|
||
<label for="uploadTargetDir">目标目录:</label>
|
||
<select id="uploadTargetDir"><option value="">加载中...</option></select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="mediaFile">选择媒体文件:</label>
|
||
<input type="file" id="mediaFile"
|
||
accept=".mp4,.avi,.mkv,.webm,.mov,.flv,.wmv,.mpeg,.mpg,.m4v,.jpg,.jpeg,.png,.gif,.bmp,.webp,.tiff,.tif,.mp3,.wav,.ogg,.flac,.aac,.m4a,.wma,.ape,.alac" />
|
||
</div>
|
||
<div class="form-row">
|
||
<button id="uploadBtn" class="btn-secondary">上传文件</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="fileInfo" class="file-info"></div>
|
||
|
||
<div class="upload-progress" id="uploadProgress">
|
||
<div class="progress-container">
|
||
<div class="progress-bar" id="progressBar"></div>
|
||
</div>
|
||
<div class="progress-stats">
|
||
<span id="progressPercent">0%</span>
|
||
<span id="progressDetails">0 MB / 0 MB</span>
|
||
<span id="uploadSpeed" class="speed-indicator">0 KB/s</span>
|
||
</div>
|
||
</div>
|
||
<div id="message" class="message"></div>
|
||
|
||
<!-- 创建新目录 -->
|
||
<div class="create-dir-form">
|
||
<h3>创建新目录</h3>
|
||
<div class="form-row">
|
||
<label for="createTargetDir">父目录:</label>
|
||
<select id="createTargetDir"><option value="">加载中...</option></select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="newDirName">新目录名称:</label>
|
||
<input type="text" id="newDirName" placeholder="输入目录名称" />
|
||
</div>
|
||
<div class="form-row">
|
||
<label for="dirPassword">设置访问密码(可选):</label>
|
||
<input type="password" id="dirPassword" placeholder="留空则不设置密码保护" />
|
||
</div>
|
||
<div class="form-row">
|
||
<button id="createDirBtn" class="btn-primary">创建目录</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 密码保护模态框 -->
|
||
<div class="modal-overlay" id="passwordModal">
|
||
<div class="modal">
|
||
<h3>🔒 目录已加密保护</h3>
|
||
<div id="modalMessage" class="info" style="display:block;margin-bottom:15px;">请输入访问密码</div>
|
||
<div class="modal-form">
|
||
<div>
|
||
<label for="modalPassword">密码:</label>
|
||
<input type="password" id="modalPassword" placeholder="请输入访问密码" />
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button id="cancelBtn" class="btn-danger">取消</button>
|
||
<button id="confirmBtn" class="btn-primary">确认</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支持格式说明 -->
|
||
<div class="supported-formats">
|
||
<strong>支持格式:</strong>
|
||
.mp4 .avi .mkv .webm .mov .flv .wmv .mpeg .mpg .m4v
|
||
.jpg .jpeg .png .gif .bmp .webp .tiff .tif
|
||
.mp3 .wav .ogg .flac .aac .m4a .wma .ape .alac
|
||
</div>
|
||
|
||
<script>
|
||
// ── DOM 元素 ────────────────────────────────────────────────────────────────────
|
||
const videoPlayer = document.getElementById('videoPlayer');
|
||
const audioPlayer = document.getElementById('audioPlayer');
|
||
const imageViewer = document.getElementById('imageViewer');
|
||
const dirSelect = document.getElementById('dirSelect');
|
||
const mediaSelect = document.getElementById('mediaSelect');
|
||
const prevBtn = document.getElementById('prevBtn');
|
||
const nextBtn = document.getElementById('nextBtn');
|
||
const loading = document.getElementById('loading');
|
||
const loadingText = document.getElementById('loading-text');
|
||
const currentMediaName = document.getElementById('currentMediaName');
|
||
const mediaTypeBadge = document.getElementById('mediaTypeBadge');
|
||
const totalMediaCount = document.getElementById('totalMediaCount');
|
||
const currentDirPath = document.getElementById('currentDirPath');
|
||
const protectedIndicator = document.getElementById('protectedIndicator');
|
||
|
||
const uploadTargetDir = document.getElementById('uploadTargetDir');
|
||
const createTargetDir = document.getElementById('createTargetDir');
|
||
const mediaFile = document.getElementById('mediaFile');
|
||
const uploadBtn = document.getElementById('uploadBtn');
|
||
const createDirBtn = document.getElementById('createDirBtn');
|
||
const newDirName = document.getElementById('newDirName');
|
||
const dirPassword = document.getElementById('dirPassword');
|
||
const uploadProgress = document.getElementById('uploadProgress');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressPercent = document.getElementById('progressPercent');
|
||
const progressDetails = document.getElementById('progressDetails');
|
||
const uploadSpeed = document.getElementById('uploadSpeed');
|
||
const message = document.getElementById('message');
|
||
const fileInfo = document.getElementById('fileInfo');
|
||
|
||
const passwordModal = document.getElementById('passwordModal');
|
||
const modalPassword = document.getElementById('modalPassword');
|
||
const confirmBtn = document.getElementById('confirmBtn');
|
||
const cancelBtn = document.getElementById('cancelBtn');
|
||
const modalMessage = document.getElementById('modalMessage');
|
||
|
||
// ── 全局状态 ────────────────────────────────────────────────────────────────────
|
||
const LAST_VISITED_DIR_KEY = 'nas_last_visited_dir';
|
||
let mediaList = [];
|
||
let dirList = [];
|
||
let allDirs = [];
|
||
let currentIndex = -1;
|
||
let currentDir = '';
|
||
let currentProtectedDir = null;
|
||
let pendingUploadFile = null;
|
||
let pendingUploadDir = '';
|
||
let uploadStartTime = null;
|
||
let uploadedBytes = 0;
|
||
let totalBytes = 0;
|
||
let speedHistory = [];
|
||
let lastUpdateTime = 0;
|
||
|
||
// ── 工具函数 ────────────────────────────────────────────────────────────────────
|
||
|
||
function getLastVisitedDir() {
|
||
try { return localStorage.getItem(LAST_VISITED_DIR_KEY) || ''; }
|
||
catch (e) { return ''; }
|
||
}
|
||
|
||
function saveLastVisitedDir(dirPath) {
|
||
try { localStorage.setItem(LAST_VISITED_DIR_KEY, dirPath); }
|
||
catch (e) { /* 忽略存储失败 */ }
|
||
}
|
||
|
||
function showLoading(text = '加载中...') {
|
||
loadingText.textContent = text;
|
||
loading.style.display = 'block';
|
||
}
|
||
|
||
function hideLoading() {
|
||
loading.style.display = 'none';
|
||
}
|
||
|
||
/** 显示操作结果消息(自动 3 秒后隐藏) */
|
||
function showMessage(text, type = 'success') {
|
||
message.textContent = text;
|
||
message.className = `message ${type}`;
|
||
message.style.display = 'block';
|
||
setTimeout(() => { message.style.display = 'none'; }, 3000);
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function calculateSpeed(currentBytes) {
|
||
const now = Date.now();
|
||
if (lastUpdateTime === 0) { lastUpdateTime = now; uploadedBytes = currentBytes; return 0; }
|
||
const elapsed = (now - lastUpdateTime) / 1000;
|
||
if (elapsed > 0.5) {
|
||
const speed = (currentBytes - uploadedBytes) / elapsed / 1024;
|
||
speedHistory.push(speed);
|
||
if (speedHistory.length > 10) speedHistory.shift();
|
||
uploadedBytes = currentBytes;
|
||
lastUpdateTime = now;
|
||
return speedHistory.reduce((a, b) => a + b, 0) / speedHistory.length;
|
||
}
|
||
return speedHistory.length > 0 ? speedHistory[speedHistory.length - 1] : 0;
|
||
}
|
||
|
||
// ── 目录管理 ────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* 修复:option 元素不支持内嵌 HTML,使用纯文本拼接保护标识。
|
||
*/
|
||
function populateDirSelect(selectedDir = '') {
|
||
dirSelect.innerHTML = dirList.map(dir => {
|
||
const lock = dir.protected ? ' 🔒' : '';
|
||
const isSelected = dir.path === selectedDir ? 'selected' : '';
|
||
return `<option value="${escAttr(dir.path)}" ${isSelected}>${escText(dir.name)}${lock}</option>`;
|
||
}).join('');
|
||
}
|
||
|
||
function populateUploadDirSelect() {
|
||
const makeOpt = dir => {
|
||
const lock = dir.protected ? ' 🔒' : '';
|
||
const isSel = dir.path === currentDir ? 'selected' : '';
|
||
return `<option value="${escAttr(dir.path)}" ${isSel}>${escText(dir.name)}${lock}</option>`;
|
||
};
|
||
uploadTargetDir.innerHTML = allDirs.map(makeOpt).join('');
|
||
createTargetDir.innerHTML = allDirs.map(makeOpt).join('');
|
||
}
|
||
|
||
/** 简易 HTML attribute 和 text 转义(防 XSS) */
|
||
function escAttr(str) { return String(str).replace(/"/g, '"'); }
|
||
function escText(str) { return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
||
|
||
async function loadAllDirectories() {
|
||
try {
|
||
const resp = await fetch('/api/all-directories');
|
||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||
const data = await resp.json();
|
||
allDirs = data.directories || [];
|
||
populateUploadDirSelect();
|
||
} catch (err) {
|
||
console.error('加载目录失败:', err);
|
||
showMessage('加载目录失败,请重试', 'error');
|
||
}
|
||
}
|
||
|
||
async function loadDirectoryTree() {
|
||
try {
|
||
const resp = await fetch('/api/directories');
|
||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||
const data = await resp.json();
|
||
dirList = flattenDirectories(data.directories || []);
|
||
populateDirSelect(currentDir);
|
||
} catch (err) {
|
||
console.error('加载目录树失败:', err);
|
||
}
|
||
}
|
||
|
||
function flattenDirectories(dirs, parentPath = '', result = []) {
|
||
result.push({ name: parentPath || '主目录', path: parentPath, protected: false });
|
||
dirs.forEach(dir => {
|
||
const fullPath = parentPath ? `${parentPath}/${dir.name}` : dir.name;
|
||
result.push({ name: fullPath, path: fullPath, protected: dir.protected || false });
|
||
if (dir.children && dir.children.length > 0) {
|
||
flattenDirectories(dir.children, fullPath, result);
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
// ── 密码模态框 ──────────────────────────────────────────────────────────────────
|
||
|
||
function showPasswordModal(dirPath, autoPlay = false, isUpload = false, targetDir = '', file = null) {
|
||
currentProtectedDir = dirPath;
|
||
if (isUpload && file && targetDir) {
|
||
pendingUploadFile = file;
|
||
pendingUploadDir = targetDir;
|
||
}
|
||
modalPassword.value = '';
|
||
modalMessage.textContent = '请输入访问密码';
|
||
modalMessage.className = 'info';
|
||
modalMessage.style.display = 'block';
|
||
passwordModal.style.display = 'flex'; // ← 覆盖 display:none,显示为 flex
|
||
modalPassword.focus();
|
||
}
|
||
|
||
function hidePasswordModal() {
|
||
passwordModal.style.display = 'none';
|
||
currentProtectedDir = null;
|
||
modalPassword.value = '';
|
||
}
|
||
|
||
async function verifyDirectoryPassword(password) {
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('dir_path', currentProtectedDir);
|
||
fd.append('password', password);
|
||
const resp = await fetch('/api/verify-dir-password', {
|
||
method: 'POST', body: fd, credentials: 'include'
|
||
});
|
||
const result = await resp.json();
|
||
|
||
if (result.success) {
|
||
hidePasswordModal();
|
||
showMessage('验证成功!正在加载目录...');
|
||
saveLastVisitedDir(currentDir);
|
||
await loadMediaInDir(currentDir, true);
|
||
populateDirSelect(currentDir);
|
||
|
||
if (pendingUploadFile && pendingUploadDir) {
|
||
const f = pendingUploadFile, d = pendingUploadDir;
|
||
pendingUploadFile = null; pendingUploadDir = '';
|
||
performUpload(d, f);
|
||
}
|
||
return true;
|
||
} else {
|
||
modalMessage.textContent = '密码错误,请重试';
|
||
modalMessage.className = 'error';
|
||
return false;
|
||
}
|
||
} catch (err) {
|
||
console.error('验证密码失败:', err);
|
||
modalMessage.textContent = '验证失败:' + err.message;
|
||
modalMessage.className = 'error';
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ── 媒体加载 ────────────────────────────────────────────────────────────────────
|
||
|
||
async function loadMediaInDir(subdir, isAfterAuth = false) {
|
||
try {
|
||
showLoading('加载媒体列表中...');
|
||
currentDir = subdir;
|
||
saveLastVisitedDir(subdir);
|
||
|
||
const url = subdir ? `/api/media?subdir=${encodeURIComponent(subdir)}` : '/api/media';
|
||
const resp = await fetch(url, { credentials: 'include' });
|
||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||
const data = await resp.json();
|
||
mediaList = data.media || [];
|
||
|
||
// 受保护目录且尚未认证
|
||
if (!isAfterAuth && data.protected && mediaList.length === 0) {
|
||
protectedIndicator.style.display = 'inline-block';
|
||
if (data.top_protected_dir) {
|
||
hideLoading();
|
||
showPasswordModal(data.top_protected_dir, false);
|
||
return;
|
||
}
|
||
} else {
|
||
protectedIndicator.style.display = data.protected ? 'inline-block' : 'none';
|
||
}
|
||
|
||
currentDirPath.textContent = currentDir ? `/${currentDir}` : '/';
|
||
totalMediaCount.textContent = mediaList.length;
|
||
|
||
// 填充媒体选择框
|
||
mediaSelect.innerHTML = mediaList.map((media, idx) => {
|
||
const icon = media.type === 'image' ? '🖼️' : (media.type === 'audio' ? '🎵' : '📹');
|
||
const size = media.size ? ` (${formatFileSize(media.size)})` : '';
|
||
return `<option value="${idx}">${icon} ${escText(media.name)}${size}</option>`;
|
||
}).join('');
|
||
|
||
if (mediaList.length > 0) {
|
||
await playMedia(0);
|
||
} else {
|
||
hideAllMedia();
|
||
currentMediaName.textContent = '无';
|
||
mediaTypeBadge.style.display = 'none';
|
||
currentIndex = -1;
|
||
}
|
||
|
||
updateButtons();
|
||
hideLoading();
|
||
} catch (err) {
|
||
console.error('加载媒体失败:', err);
|
||
hideLoading(); // ← 修复:错误时正确隐藏 loading
|
||
showMessage(`加载失败: ${err.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function hideAllMedia() {
|
||
videoPlayer.pause(); audioPlayer.pause();
|
||
videoPlayer.src = ''; audioPlayer.src = ''; imageViewer.src = '';
|
||
videoPlayer.style.display = 'none';
|
||
audioPlayer.style.display = 'none';
|
||
imageViewer.style.display = 'none';
|
||
}
|
||
|
||
/**
|
||
* 构建媒体 URL。
|
||
* 注意:不再对视频/音频 URL 添加时间戳,避免影响断点续传 Range 请求。
|
||
* 图片添加时间戳以防止缓存旧内容。
|
||
*/
|
||
function buildMediaUrl(media) {
|
||
const base = currentDir
|
||
? `/api/media/${encodeURIComponent(currentDir)}/${encodeURIComponent(media.name)}`
|
||
: `/api/media/${encodeURIComponent(media.name)}`;
|
||
return media.type === 'image' ? base + '?t=' + Date.now() : base;
|
||
}
|
||
|
||
async function playMedia(idx) {
|
||
if (idx < 0 || idx >= mediaList.length) return;
|
||
showLoading('加载媒体中...');
|
||
currentIndex = idx;
|
||
const media = mediaList[idx];
|
||
|
||
currentMediaName.textContent = media.name;
|
||
if (media.type === 'video') {
|
||
mediaTypeBadge.textContent = '视频'; mediaTypeBadge.className = 'media-type-badge type-video';
|
||
} else if (media.type === 'audio') {
|
||
mediaTypeBadge.textContent = '音频'; mediaTypeBadge.className = 'media-type-badge type-audio';
|
||
} else {
|
||
mediaTypeBadge.textContent = '图片'; mediaTypeBadge.className = 'media-type-badge type-image';
|
||
}
|
||
mediaTypeBadge.style.display = 'inline-block';
|
||
hideAllMedia();
|
||
|
||
const mediaUrl = buildMediaUrl(media);
|
||
|
||
try {
|
||
if (media.type === 'video') {
|
||
videoPlayer.src = mediaUrl;
|
||
videoPlayer.style.display = 'block';
|
||
videoPlayer.oncanplay = () => hideLoading();
|
||
videoPlayer.onerror = () => {
|
||
hideLoading();
|
||
showMessage('视频加载失败,请检查文件', 'error');
|
||
};
|
||
await videoPlayer.play().catch(err => { console.warn('自动播放失败:', err); hideLoading(); });
|
||
|
||
} else if (media.type === 'audio') {
|
||
audioPlayer.src = mediaUrl;
|
||
audioPlayer.style.display = 'block';
|
||
audioPlayer.oncanplay = () => hideLoading();
|
||
audioPlayer.onerror = () => {
|
||
hideLoading();
|
||
showMessage('音频加载失败,请检查文件', 'error');
|
||
};
|
||
await audioPlayer.play().catch(err => { console.warn('自动播放失败:', err); hideLoading(); });
|
||
|
||
} else {
|
||
imageViewer.style.display = 'block';
|
||
imageViewer.onload = () => hideLoading();
|
||
imageViewer.onerror = () => {
|
||
hideLoading();
|
||
showMessage('图片加载失败,请检查文件或路径', 'error');
|
||
};
|
||
imageViewer.src = mediaUrl;
|
||
}
|
||
|
||
mediaSelect.value = idx;
|
||
updateButtons();
|
||
} catch (err) {
|
||
hideLoading();
|
||
showMessage(`播放失败: ${err.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function updateButtons() {
|
||
prevBtn.disabled = currentIndex <= 0;
|
||
nextBtn.disabled = currentIndex >= mediaList.length - 1;
|
||
}
|
||
|
||
// ── 上传功能 ────────────────────────────────────────────────────────────────────
|
||
|
||
async function performUpload(targetDir, file) {
|
||
uploadBtn.disabled = true;
|
||
uploadProgress.style.display = 'block';
|
||
progressBar.style.width = '0%';
|
||
progressPercent.textContent = '0%';
|
||
progressDetails.textContent = '0 MB / 0 MB';
|
||
uploadSpeed.textContent = '0 KB/s';
|
||
speedHistory = []; lastUpdateTime = 0; uploadedBytes = 0; totalBytes = file.size;
|
||
uploadStartTime = Date.now();
|
||
|
||
return new Promise((resolve) => {
|
||
const fd = new FormData();
|
||
fd.append('target_dir', targetDir);
|
||
fd.append('file', file);
|
||
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('POST', '/api/upload-media');
|
||
|
||
xhr.upload.onprogress = (e) => {
|
||
if (!e.lengthComputable) return;
|
||
const pct = Math.round((e.loaded / e.total) * 100);
|
||
progressBar.style.width = pct + '%';
|
||
progressPercent.textContent = pct + '%';
|
||
progressDetails.textContent = `${formatFileSize(e.loaded)} / ${formatFileSize(e.total)}`;
|
||
const spd = calculateSpeed(e.loaded);
|
||
uploadSpeed.textContent = spd > 1024
|
||
? `${(spd / 1024).toFixed(1)} MB/s`
|
||
: `${Math.round(spd)} KB/s`;
|
||
};
|
||
|
||
xhr.onload = () => {
|
||
uploadProgress.style.display = 'none';
|
||
uploadBtn.disabled = false;
|
||
try {
|
||
const result = JSON.parse(xhr.responseText);
|
||
if (result.success) {
|
||
showMessage(result.message);
|
||
mediaFile.value = '';
|
||
fileInfo.textContent = '';
|
||
// 刷新媒体列表
|
||
loadAllDirectories();
|
||
if (currentDir === targetDir) loadMediaInDir(currentDir, true);
|
||
} else {
|
||
showMessage(result.message || '上传失败', 'error');
|
||
}
|
||
} catch (e) {
|
||
showMessage('上传响应解析失败', 'error');
|
||
}
|
||
resolve();
|
||
};
|
||
|
||
xhr.onerror = () => {
|
||
uploadProgress.style.display = 'none';
|
||
uploadBtn.disabled = false;
|
||
showMessage('网络错误,上传失败', 'error');
|
||
resolve();
|
||
};
|
||
|
||
xhr.send(fd);
|
||
});
|
||
}
|
||
|
||
// ── 创建目录 ────────────────────────────────────────────────────────────────────
|
||
|
||
async function createDirectory() {
|
||
const targetPath = createTargetDir.value || '';
|
||
const dirName = newDirName.value.trim();
|
||
const password = dirPassword.value.trim();
|
||
|
||
if (!dirName) { showMessage('请输入目录名称', 'error'); return; }
|
||
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('target_path', targetPath);
|
||
fd.append('new_dir', dirName);
|
||
if (password) fd.append('dir_password', password);
|
||
|
||
const resp = await fetch('/api/create-directory', { method: 'POST', body: fd });
|
||
const result = await resp.json();
|
||
|
||
if (result.success) {
|
||
showMessage(result.message);
|
||
newDirName.value = '';
|
||
dirPassword.value = '';
|
||
// 刷新所有目录列表
|
||
await Promise.all([loadAllDirectories(), loadDirectoryTree()]);
|
||
} else {
|
||
showMessage(result.message || '创建失败', 'error');
|
||
}
|
||
} catch (err) {
|
||
console.error('创建目录失败:', err);
|
||
showMessage(`创建失败: ${err.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
// ── 事件绑定 ────────────────────────────────────────────────────────────────────
|
||
|
||
dirSelect.addEventListener('change', async () => {
|
||
const selected = dirSelect.value;
|
||
currentDir = selected;
|
||
await loadMediaInDir(selected);
|
||
// 同步上传目标目录选中状态
|
||
uploadTargetDir.value = selected;
|
||
});
|
||
|
||
mediaSelect.addEventListener('change', () => {
|
||
const idx = parseInt(mediaSelect.value, 10);
|
||
if (!isNaN(idx)) playMedia(idx);
|
||
});
|
||
|
||
prevBtn.addEventListener('click', () => { if (currentIndex > 0) playMedia(currentIndex - 1); });
|
||
nextBtn.addEventListener('click', () => { if (currentIndex < mediaList.length - 1) playMedia(currentIndex + 1); });
|
||
|
||
// 视频播放结束后自动下一个
|
||
videoPlayer.addEventListener('ended', () => {
|
||
if (currentIndex < mediaList.length - 1) playMedia(currentIndex + 1);
|
||
});
|
||
audioPlayer.addEventListener('ended', () => {
|
||
if (currentIndex < mediaList.length - 1) playMedia(currentIndex + 1);
|
||
});
|
||
|
||
// 文件选择信息显示
|
||
mediaFile.addEventListener('change', () => {
|
||
if (mediaFile.files.length > 0) {
|
||
const f = mediaFile.files[0];
|
||
fileInfo.textContent = `已选择: ${f.name} (${formatFileSize(f.size)})`;
|
||
} else {
|
||
fileInfo.textContent = '';
|
||
}
|
||
});
|
||
|
||
// 上传按钮
|
||
uploadBtn.addEventListener('click', async () => {
|
||
const file = mediaFile.files[0];
|
||
if (!file) { showMessage('请选择要上传的文件', 'error'); return; }
|
||
const targetDir = uploadTargetDir.value || '';
|
||
|
||
// 检查受保护目录认证
|
||
try {
|
||
const resp = await fetch(`/api/media?subdir=${encodeURIComponent(targetDir)}`, { credentials: 'include' });
|
||
const data = await resp.json();
|
||
if (data.protected && !data.media) {
|
||
// 未认证,弹出密码框
|
||
showPasswordModal(data.top_protected_dir, false, true, targetDir, file);
|
||
return;
|
||
}
|
||
} catch (e) { /* 忽略检查失败,直接尝试上传 */ }
|
||
|
||
performUpload(targetDir, file);
|
||
});
|
||
|
||
// 创建目录按钮
|
||
createDirBtn.addEventListener('click', createDirectory);
|
||
|
||
// 模态框按钮
|
||
confirmBtn.addEventListener('click', () => {
|
||
const pwd = modalPassword.value;
|
||
if (!pwd) { modalMessage.textContent = '请输入密码'; modalMessage.className = 'error'; return; }
|
||
verifyDirectoryPassword(pwd);
|
||
});
|
||
|
||
cancelBtn.addEventListener('click', () => {
|
||
hidePasswordModal();
|
||
pendingUploadFile = null; pendingUploadDir = '';
|
||
});
|
||
|
||
modalPassword.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') confirmBtn.click();
|
||
});
|
||
|
||
// 键盘快捷键(左/右箭头切换媒体)
|
||
document.addEventListener('keydown', (e) => {
|
||
if (passwordModal.style.display === 'flex') return;
|
||
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.tagName)) return;
|
||
if (e.key === 'ArrowLeft' && !prevBtn.disabled) prevBtn.click();
|
||
if (e.key === 'ArrowRight' && !nextBtn.disabled) nextBtn.click();
|
||
});
|
||
|
||
// ── 页面初始化 ──────────────────────────────────────────────────────────────────
|
||
async function init() {
|
||
showLoading('初始化中...');
|
||
try {
|
||
await Promise.all([loadAllDirectories(), loadDirectoryTree()]);
|
||
|
||
// 恢复上次访问的目录
|
||
const lastDir = getLastVisitedDir();
|
||
const targetDir = (lastDir && dirList.some(d => d.path === lastDir)) ? lastDir : '';
|
||
|
||
if (dirSelect.options.length > 0) {
|
||
dirSelect.value = targetDir;
|
||
currentDir = targetDir;
|
||
}
|
||
|
||
await loadMediaInDir(targetDir);
|
||
} catch (err) {
|
||
console.error('初始化失败:', err);
|
||
hideLoading();
|
||
showMessage('初始化失败,请刷新页面重试', 'error');
|
||
}
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|