Files
nas-media-player/index.html
2026-04-19 05:07:07 +08:00

1287 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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样式 */
.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;
}
.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: flex;
justify-content: center;
align-items: center;
z-index: 1000;
display: none;
}
.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;
white-space: normal;
word-wrap: break-word;
overflow-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>
<!-- 居中Logo容器 -->
<div class="logo-container">
<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" style="display: none;">
<div class="loading-spinner"></div>
<div id="loading-text">加载中...</div>
</div>
<div class="media-content">
<video id="videoPlayer" controls autoplay 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">
当前目录:<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 = 'last_visited_dir';
let mediaList = [];
let dirList = [];
let allDirs = [];
let protectedDirs = [];
let currentIndex = -1;
let currentDir = "";
let currentProtectedDir = null;
let shouldAutoPlayAfterAuth = false;
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) {
console.warn('获取本地存储失败:', e);
return "";
}
}
// 工具函数:保存上次访问目录到本地存储
function saveLastVisitedDir(dirPath) {
try {
localStorage.setItem(LAST_VISITED_DIR_KEY, dirPath);
} catch (e) {
console.warn('保存本地存储失败:', e);
}
}
// 显示加载状态
function showLoading(text = '加载中...') {
loadingText.textContent = text;
loading.style.display = 'block';
}
// 隐藏加载状态
function hideLoading() {
loading.style.display = 'none';
}
// 显示消息
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) { // 至少0.5秒更新一次
const speed = (currentBytes - uploadedBytes) / elapsed / 1024; // KB/s
speedHistory.push(speed);
if (speedHistory.length > 10) {
speedHistory.shift();
}
const avgSpeed = speedHistory.reduce((a, b) => a + b, 0) / speedHistory.length;
uploadedBytes = currentBytes;
lastUpdateTime = now;
return avgSpeed;
}
return speedHistory.length > 0 ? speedHistory[speedHistory.length - 1] : 0;
}
// 扁平化目录结构
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 populateDirSelect(selectedDir = "") {
dirSelect.innerHTML = dirList.map(dir => {
const protectedBadge = dir.protected ? '<span class="protected-badge">🔒</span>' : '';
const isSelected = dir.path === selectedDir ? 'selected' : '';
return `<option value="${dir.path}" ${isSelected}>${dir.name}${protectedBadge}</option>`;
}).join('');
}
// 加载目录列表
async function loadAllDirectories() {
try {
const response = await fetch('/api/all-directories');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
allDirs = data.directories || [];
// 填充上传目录选择框
uploadTargetDir.innerHTML = allDirs.map(dir => {
const protectedBadge = dir.protected ? '<span class="protected-badge">🔒</span>' : '';
const isSelected = dir.path === currentDir ? 'selected' : '';
return `<option value="${dir.path}" ${isSelected}>${dir.name}${protectedBadge}</option>`;
}).join('');
// 填充创建目录的父目录选择框
createTargetDir.innerHTML = allDirs.map(dir => {
const protectedBadge = dir.protected ? '<span class="protected-badge">🔒</span>' : '';
return `<option value="${dir.path}">${dir.name}${protectedBadge}</option>`;
}).join('');
} catch (err) {
console.error('加载目录失败:', err);
showMessage('加载目录失败,请重试', 'error');
}
}
// 加载受保护目录列表
async function loadProtectedDirectories() {
try {
const response = await fetch('/api/protected-directories');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
protectedDirs = data.protected_dirs || [];
} catch (err) {
console.error('加载受保护目录失败:', err);
}
}
// 显示密码模态框(支持上传场景)
function showPasswordModal(dirPath, autoPlay = false, isUpload = false, targetDir = "", file = null) {
console.log('showPasswordModal:', { dirPath, isUpload, targetDir, file: !!file });
currentProtectedDir = dirPath;
shouldAutoPlayAfterAuth = autoPlay;
// 保存上传信息到全局变量
if (isUpload && file && targetDir) {
pendingUploadFile = file;
pendingUploadDir = targetDir;
console.log('保存上传信息:', { fileName: file.name, targetDir });
}
modalPassword.value = '';
modalMessage.textContent = '请输入访问密码';
modalMessage.className = 'info';
modalMessage.style.display = 'block';
passwordModal.style.display = 'flex';
modalPassword.focus();
}
// 隐藏密码模态框
function hidePasswordModal() {
passwordModal.style.display = 'none';
currentProtectedDir = null;
shouldAutoPlayAfterAuth = false;
modalPassword.value = '';
}
// 验证目录密码(核心修复:保持目录状态,不刷新页面)
async function verifyDirectoryPassword(password) {
try {
console.log('验证密码:', { dirPath: currentProtectedDir, currentDir });
const formData = new FormData();
formData.append('dir_path', currentProtectedDir);
formData.append('password', password);
const response = await fetch('/api/verify-dir-password', {
method: 'POST',
body: formData,
credentials: 'include'
});
const result = await response.json();
console.log('验证结果:', result);
if (result.success) {
hidePasswordModal();
showMessage('验证成功!正在加载目录...');
// 关键修复1保存当前目录到本地存储持久化状态
saveLastVisitedDir(currentDir);
// 关键修复2重新加载当前目录强制刷新媒体列表
await loadMediaInDir(currentDir, true); // 第二个参数表示"验证后加载",跳过密码检查
// 关键修复3更新目录选择框确保选中当前目录
populateDirSelect(currentDir);
// 检查是否有等待上传的文件
if (pendingUploadFile && pendingUploadDir) {
console.log('开始上传加密目录文件:', {
fileName: pendingUploadFile.name,
targetDir: pendingUploadDir
});
performUpload(pendingUploadDir, pendingUploadFile);
pendingUploadFile = null;
pendingUploadDir = "";
}
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('加载媒体列表中...');
// 关键修复1保存当前目录到全局变量和本地存储
currentDir = subdir;
saveLastVisitedDir(subdir);
const url = subdir ? `/api/media?subdir=${encodeURIComponent(subdir)}` : '/api/media';
const response = await fetch(url, {
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
mediaList = data.media || [];
// 关键修复2验证后加载时跳过密码模态框已验证通过
if (!isAfterAuth && data.protected) {
protectedIndicator.style.display = 'inline-block';
if (data.top_protected_dir) {
showPasswordModal(data.top_protected_dir, mediaList.length > 0);
hideLoading();
return;
}
} else {
protectedIndicator.style.display = data.protected ? 'inline-block' : 'none';
}
// 更新UI
currentDirPath.textContent = currentDir ? `/${currentDir}` : '/';
totalMediaCount.textContent = mediaList.length;
// 填充媒体选择框
mediaSelect.innerHTML = mediaList.map((media, idx) => {
let typeIcon = '📹';
if (media.type === 'image') typeIcon = '🖼️';
if (media.type === 'audio') typeIcon = '🎵';
const sizeInfo = media.size ? ` (${formatFileSize(media.size)})` : '';
return `<option value="${idx}">${typeIcon} ${media.name}${sizeInfo}</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);
showLoading('加载媒体失败,请重试', true);
showMessage(`加载失败: ${err.message}`, 'error');
}
}
// 隐藏所有媒体播放器
function hideAllMedia() {
videoPlayer.style.display = 'none';
audioPlayer.style.display = 'none';
imageViewer.style.display = 'none';
videoPlayer.pause();
audioPlayer.pause();
videoPlayer.src = "";
audioPlayer.src = "";
imageViewer.src = "";
}
// 构建媒体URL
function buildMediaUrl(media) {
let url;
if (currentDir) {
url = `/api/media/${encodeURIComponent(currentDir)}/${encodeURIComponent(media.name)}`;
} else {
url = `/api/media/${encodeURIComponent(media.name)}`;
}
// 添加时间戳防止缓存
return url + '?t=' + Date.now();
}
// 播放媒体文件
async function playMedia(idx) {
if (idx < 0 || idx >= mediaList.length) return;
showLoading('加载媒体中...');
currentIndex = idx;
const media = mediaList[currentIndex];
try {
// 更新信息显示
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();
// 构建URL
const mediaUrl = buildMediaUrl(media);
if (media.type === 'video') {
// 视频处理
videoPlayer.src = mediaUrl;
videoPlayer.style.display = 'block';
videoPlayer.oncanplay = function() {
hideLoading();
};
videoPlayer.onerror = function(e) {
console.error('Video error:', e);
showLoading('视频加载失败', true);
showMessage('视频加载失败,请检查文件', 'error');
setTimeout(hideLoading, 2000);
};
await videoPlayer.play().catch(err => {
console.warn('自动播放失败:', err);
hideLoading();
});
} else if (media.type === 'audio') {
// 音频处理
audioPlayer.src = mediaUrl;
audioPlayer.style.display = 'block';
audioPlayer.oncanplay = function() {
hideLoading();
};
audioPlayer.onerror = function(e) {
console.error('Audio error:', e);
showLoading('音频加载失败', true);
showMessage('音频加载失败,请检查文件', 'error');
setTimeout(hideLoading, 2000);
};
await audioPlayer.play().catch(err => {
console.warn('自动播放失败:', err);
hideLoading();
});
} else {
// 图片处理
imageViewer.style.display = 'block';
imageViewer.onload = function() {
hideLoading();
console.log('图片加载成功:', mediaUrl);
};
imageViewer.onerror = function(e) {
console.error('图片加载失败:', e, mediaUrl);
showLoading('图片加载失败', true);
showMessage('图片加载失败,请检查文件或路径', 'error');
setTimeout(hideLoading, 2000);
};
imageViewer.src = '';
imageViewer.src = mediaUrl;
}
mediaSelect.value = idx;
updateButtons();
} catch (err) {
console.error('播放媒体失败:', err);
showLoading('播放失败', true);
showMessage(`播放失败: ${err.message}`, 'error');
setTimeout(hideLoading, 2000);
}
}
// 更新按钮状态
function updateButtons() {
prevBtn.disabled = currentIndex <= 0;
nextBtn.disabled = currentIndex >= mediaList.length - 1;
}
// 创建目录
async function createDirectory() {
const targetPath = createTargetDir.value || "";
const dirName = newDirName.value.trim();
const password = dirPassword.value.trim();
if (!dirName) {
showMessage('请输入目录名称', 'error');
return;
}
try {
const formData = new FormData();
formData.append('target_path', targetPath);
formData.append('new_dir', dirName);
if (password) {
formData.append('dir_password', password);
}
const response = await fetch('/api/create-directory', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
showMessage(result.message);
newDirName.value = '';
dirPassword.value = '';
// 重新加载目录列表,并选中新创建的目录
await loadAllDirectories();
await loadProtectedDirectories();
const dirResponse = await fetch('/api/directories');
const dirData = await dirResponse.json();
dirList = flattenDirectories(dirData.directories);
populateDirSelect(result.path); // 选中新目录
// 加载新目录的媒体列表
await loadMediaInDir(result.path);
} else {
showMessage(result.message || '创建目录失败', 'error');
}
} catch (err) {
console.error('创建目录失败:', err);
showMessage('创建目录失败:' + err.message, 'error');
}
}
// 检查目录是否受保护
function isDirectoryProtected(dirPath) {
return protectedDirs.some(pdir => dirPath === pdir || dirPath.startsWith(pdir + '/'));
}
// 获取顶级受保护目录
function getTopProtectedDirectory(dirPath) {
for (const pdir of protectedDirs) {
if (dirPath === pdir || dirPath.startsWith(pdir + '/')) {
return pdir;
}
}
return null;
}
// 检查是否有认证Cookie
function hasAuthCookie(dirPath) {
const topDir = getTopProtectedDirectory(dirPath);
if (!topDir) return true;
const cookieName = `auth_${encodeURIComponent(topDir)}`;
return document.cookie.includes(cookieName);
}
// 上传媒体文件
async function uploadMedia() {
const file = mediaFile.files[0];
if (!file) {
showMessage('请选择要上传的媒体文件', 'error');
return;
}
const targetDir = uploadTargetDir.value;
if (!targetDir) {
showMessage('请选择目标目录', 'error');
return;
}
console.log('准备上传:', { fileName: file.name, targetDir, size: file.size });
if (isDirectoryProtected(targetDir) && !hasAuthCookie(targetDir)) {
const topDir = getTopProtectedDirectory(targetDir);
showMessage('目标目录需要密码验证,请输入密码', 'info');
showPasswordModal(topDir, false, true, targetDir, file);
return;
}
performUpload(targetDir, file);
}
// 实际执行上传函数
function performUpload(targetDir, file) {
if (!file || !targetDir) {
showMessage('上传参数缺失', 'error');
console.error('上传参数缺失:', { hasFile: !!file, hasDir: !!targetDir });
return;
}
console.log('开始上传:', {
targetDir,
fileName: file.name,
size: formatFileSize(file.size),
type: file.type
});
fileInfo.textContent = `文件名: ${file.name} | 大小: ${formatFileSize(file.size)}`;
uploadProgress.style.display = 'block';
uploadStartTime = Date.now();
uploadedBytes = 0;
totalBytes = file.size;
speedHistory = [];
lastUpdateTime = 0;
const formData = new FormData();
formData.append('target_dir', targetDir);
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload-media', true);
xhr.withCredentials = true;
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressBar.style.width = `${percent}%`;
progressPercent.textContent = `${Math.round(percent)}%`;
progressDetails.textContent = `${formatFileSize(e.loaded)} / ${formatFileSize(e.total)}`;
const speed = calculateSpeed(e.loaded);
if (speed > 1024) {
uploadSpeed.textContent = `${(speed / 1024).toFixed(2)} MB/s`;
} else {
uploadSpeed.textContent = `${speed.toFixed(0)} KB/s`;
}
if (percent > 0 && percent < 100) {
const elapsed = (Date.now() - uploadStartTime) / 1000;
const totalTime = elapsed / (percent / 100);
const remaining = totalTime - elapsed;
if (remaining > 60) {
uploadSpeed.textContent += ` | 剩余: ${Math.round(remaining / 60)} 分钟`;
} else {
uploadSpeed.textContent += ` | 剩余: ${Math.round(remaining)}`;
}
}
}
});
xhr.addEventListener('load', () => {
console.log('上传响应:', { status: xhr.status, response: xhr.responseText });
try {
const result = JSON.parse(xhr.responseText);
if (result.success) {
progressBar.style.width = '100%';
progressPercent.textContent = '100%';
uploadSpeed.textContent = '上传完成!';
showMessage(result.message);
mediaFile.value = '';
fileInfo.textContent = '';
if (targetDir === currentDir) {
setTimeout(() => {
loadMediaInDir(currentDir);
}, 1000);
}
} else {
showMessage(result.message || '上传失败', 'error');
}
} catch (err) {
showMessage('上传完成但处理响应失败: ' + err.message, 'error');
console.error('解析响应失败:', err, xhr.responseText);
}
setTimeout(() => {
uploadProgress.style.display = 'none';
}, 3000);
});
xhr.addEventListener('error', (e) => {
console.error('上传错误:', e);
showMessage('上传失败:网络错误', 'error');
uploadProgress.style.display = 'none';
});
xhr.addEventListener('abort', () => {
console.log('上传被取消');
showMessage('上传已取消', 'info');
uploadProgress.style.display = 'none';
});
xhr.send(formData);
}
// 初始化应用(核心修复:优先加载上次访问目录)
async function initApp() {
try {
showLoading('初始化中...');
// 关键修复1获取上次访问的目录优先于默认根目录
const lastDir = getLastVisitedDir();
console.log('初始化 - 上次访问目录:', lastDir);
// 加载目录列表
const dirResponse = await fetch('/api/directories');
const dirData = await dirResponse.json();
dirList = flattenDirectories(dirData.directories);
// 关键修复2初始化目录选择框时选中上次访问的目录
populateDirSelect(lastDir);
// 加载所有目录(用于上传)
await loadAllDirectories();
// 加载受保护目录列表
await loadProtectedDirectories();
// 关键修复3优先加载上次访问的目录而不是默认根目录
const initDir = lastDir || "";
await loadMediaInDir(initDir);
hideLoading();
} catch (err) {
console.error('初始化失败:', err);
showMessage('初始化失败:' + err.message, 'error');
hideLoading();
}
}
// 事件监听器
dirSelect.addEventListener('change', (e) => {
const selectedDir = e.target.value;
loadMediaInDir(selectedDir);
});
mediaSelect.addEventListener('change', (e) => {
playMedia(parseInt(e.target.value));
});
prevBtn.addEventListener('click', () => {
if (currentIndex > 0) {
playMedia(currentIndex - 1);
}
});
nextBtn.addEventListener('click', () => {
if (currentIndex < mediaList.length - 1) {
playMedia(currentIndex + 1);
}
});
uploadBtn.addEventListener('click', uploadMedia);
createDirBtn.addEventListener('click', createDirectory);
// 密码模态框事件
confirmBtn.addEventListener('click', () => {
const password = modalPassword.value.trim();
if (!password) {
modalMessage.textContent = '请输入密码';
modalMessage.className = 'error';
return;
}
verifyDirectoryPassword(password);
});
cancelBtn.addEventListener('click', hidePasswordModal);
// ESC键关闭模态框
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
hidePasswordModal();
}
});
// 点击模态框外部关闭
passwordModal.addEventListener('click', (e) => {
if (e.target === passwordModal) {
hidePasswordModal();
}
});
// 媒体播放结束自动播放下一个
videoPlayer.addEventListener('ended', () => {
if (currentIndex < mediaList.length - 1) {
playMedia(currentIndex + 1);
}
});
audioPlayer.addEventListener('ended', () => {
if (currentIndex < mediaList.length - 1) {
playMedia(currentIndex + 1);
}
});
// 初始化应用
document.addEventListener('DOMContentLoaded', initApp);
</script>
</body>
</html>