Files
K-Vault/webdav.html

1357 lines
42 KiB
HTML
Raw Permalink 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>WebDAV 上传中心 | K-Vault</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo.png" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="/theme.css" />
<script src="/theme.js?v=20260305"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css" />
<style>
:root {
--wf-surface: rgba(255, 255, 255, 0.92);
--wf-border: rgba(255, 255, 255, 0.58);
--wf-shadow: 0 10px 28px rgba(20, 32, 55, 0.12);
--wf-shadow-soft: 0 6px 18px rgba(20, 32, 55, 0.1);
--wf-primary: #8a4bff;
--wf-primary-soft: rgba(138, 75, 255, 0.13);
}
html[data-theme="dark"] {
--wf-surface: rgba(16, 26, 43, 0.9);
--wf-border: rgba(120, 145, 192, 0.32);
--wf-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
--wf-shadow-soft: 0 8px 20px rgba(0, 0, 0, 0.3);
--wf-primary: #9aa9ff;
--wf-primary-soft: rgba(154, 169, 255, 0.2);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(60vw 50vh at 12% 8%, rgba(138, 75, 255, 0.16), transparent 70%),
radial-gradient(50vw 45vh at 88% 14%, rgba(255, 166, 104, 0.2), transparent 72%),
linear-gradient(135deg, #ffd7e4 0%, #c8f1ff 100%);
color: #1f2937;
font-family: "Manrope", "Noto Sans SC", "PingFang SC", "Segoe UI", sans-serif;
}
html[data-theme="dark"] body {
color: #e7eeff;
background:
radial-gradient(60vw 50vh at 12% 8%, rgba(64, 129, 255, 0.2), transparent 70%),
radial-gradient(50vw 45vh at 88% 14%, rgba(68, 170, 146, 0.2), transparent 72%),
linear-gradient(140deg, #0a1222 0%, #0d1729 52%, #132237 100%);
}
.page {
max-width: 1360px;
margin: 0 auto;
padding: 20px;
}
.header {
position: sticky;
top: 20px;
z-index: 50;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 24px;
border-radius: 16px;
background: var(--wf-surface);
border: 1px solid var(--wf-border);
box-shadow: var(--wf-shadow-soft);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
margin-bottom: 16px;
}
.header-title {
display: flex;
align-items: center;
gap: 10px;
font-family: "Sora", "Manrope", "Noto Sans SC", sans-serif;
font-size: 1.38rem;
font-weight: 700;
letter-spacing: 0.02em;
color: inherit;
margin: 0;
}
.header-title i {
color: var(--wf-primary);
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.header-logo {
width: 30px;
height: 30px;
border-radius: 8px;
object-fit: cover;
box-shadow: 0 4px 12px rgba(138, 75, 255, 0.2);
background: rgba(255, 255, 255, 0.92);
}
.action-link,
.action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid var(--wf-border);
border-radius: 999px;
padding: 9px 13px;
min-height: 40px;
background: var(--wf-surface);
color: inherit;
text-decoration: none;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
font-weight: 600;
}
.action-link:hover,
.action-btn:hover {
border-color: rgba(138, 75, 255, 0.4);
background: var(--wf-primary-soft);
box-shadow: var(--wf-shadow-soft);
transform: translateY(-1px);
}
.action-link.active {
border-color: rgba(138, 75, 255, 0.5);
background: var(--wf-primary-soft);
}
.layout {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.toolbar-row {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--wf-border);
background: var(--wf-surface);
box-shadow: var(--wf-shadow-soft);
}
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
border: 1px solid var(--wf-border);
background: rgba(255, 255, 255, 0.72);
color: #586173;
font-size: 0.88rem;
font-weight: 600;
}
.status-pill .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #909399;
}
.status-pill.ok {
border-color: rgba(103, 194, 58, 0.45);
background: rgba(103, 194, 58, 0.14);
color: #1f7a46;
}
.status-pill.ok .status-dot {
background: #67c23a;
}
.status-pill.bad {
border-color: rgba(245, 108, 108, 0.45);
background: rgba(245, 108, 108, 0.14);
color: #af3939;
}
.status-pill.bad .status-dot {
background: #f56c6c;
}
.card {
border: 1px solid var(--wf-border);
background: var(--wf-surface);
border-radius: 16px;
box-shadow: var(--wf-shadow-soft);
padding: 16px;
transition: box-shadow 0.2s ease;
}
.card:hover {
box-shadow: var(--wf-shadow);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
margin-bottom: 12px;
}
.card h2 {
margin: 0;
font-size: 1.06rem;
display: inline-flex;
align-items: center;
gap: 8px;
}
.card-sub {
margin-top: 4px;
color: #77819a;
font-size: 0.86rem;
line-height: 1.45;
}
.row {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.upload-file-row {
display: flex;
gap: 10px;
align-items: stretch;
}
.file-picker {
position: relative;
flex: 1;
min-height: 74px;
border-radius: 14px;
border: 1px dashed rgba(138, 75, 255, 0.45);
background:
linear-gradient(140deg, rgba(255, 255, 255, 0.9) 0%, rgba(242, 233, 255, 0.86) 100%);
padding: 12px 14px;
display: flex;
align-items: center;
gap: 12px;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.file-picker:hover {
border-color: rgba(138, 75, 255, 0.68);
box-shadow: 0 8px 20px rgba(138, 75, 255, 0.18);
transform: translateY(-1px);
}
.file-picker.is-dragover {
border-color: rgba(16, 185, 129, 0.82);
box-shadow: 0 10px 22px rgba(16, 185, 129, 0.22);
background:
linear-gradient(140deg, rgba(236, 253, 245, 0.94) 0%, rgba(216, 255, 243, 0.9) 100%);
}
.file-picker.has-files {
border-style: solid;
}
.file-input-native {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.file-choose-btn {
border-radius: 999px;
border: 1px solid rgba(138, 75, 255, 0.36);
background: rgba(255, 255, 255, 0.9);
min-height: 42px;
padding: 9px 14px;
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(138, 75, 255, 0.14);
}
.file-picker-meta {
min-width: 0;
flex: 1;
}
.file-picker-title {
font-size: 0.96rem;
font-weight: 700;
color: #273247;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-picker-hint {
margin-top: 4px;
color: #60708e;
font-size: 0.82rem;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-count-badge {
flex: 0 0 auto;
border-radius: 999px;
border: 1px solid rgba(138, 75, 255, 0.25);
background: rgba(138, 75, 255, 0.12);
color: #5b35b5;
font-size: 0.8rem;
font-weight: 700;
padding: 6px 10px;
}
#uploadFilesBtn {
min-width: 124px;
}
.file-queue-wrap {
margin-top: 12px;
border: 1px solid var(--wf-border);
background: rgba(255, 255, 255, 0.72);
border-radius: 12px;
padding: 10px;
}
.file-queue-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.file-queue-title {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
font-weight: 700;
color: #405072;
}
.file-queue-clear {
border-radius: 999px;
padding: 6px 10px;
font-size: 0.8rem;
border: 1px solid rgba(245, 108, 108, 0.42);
background: rgba(255, 245, 245, 0.92);
color: #d74b4b;
}
.file-queue-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 6px;
}
.file-queue-item {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid rgba(138, 75, 255, 0.18);
border-radius: 10px;
padding: 7px 8px;
background: rgba(255, 255, 255, 0.88);
}
.file-queue-icon {
width: 30px;
height: 30px;
border-radius: 9px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(138, 75, 255, 0.14);
color: #6b3be7;
flex: 0 0 auto;
}
.file-queue-meta {
min-width: 0;
flex: 1;
}
.file-queue-name {
font-size: 0.86rem;
font-weight: 600;
color: #2d3b55;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-queue-size {
margin-top: 2px;
font-size: 0.78rem;
color: #7483a0;
}
.file-queue-more {
text-align: center;
font-size: 0.78rem;
color: #6a7690;
padding: 4px 0 2px;
}
input,
button {
border: 1px solid var(--wf-border);
border-radius: 10px;
background: var(--wf-surface);
color: inherit;
padding: 10px 12px;
}
input[type="text"] {
min-width: 260px;
flex: 1;
}
.input-label {
display: block;
margin-bottom: 8px;
color: #5f6678;
font-size: 0.85rem;
font-weight: 600;
}
button {
font-weight: 600;
cursor: pointer;
}
button:hover {
border-color: rgba(138, 75, 255, 0.45);
}
.btn-primary {
background: linear-gradient(135deg, #8a4bff 0%, #b39ddb 100%);
border-color: transparent;
color: #fff;
}
.btn-primary:hover {
border-color: transparent;
}
.btn-danger {
background: linear-gradient(135deg, #f56c6c 0%, #f89b9b 100%);
border-color: transparent;
color: #fff;
}
.result-tools {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
button[disabled] {
opacity: 0.6;
cursor: wait;
}
.hint {
margin: 10px 0 0;
color: #64748b;
font-size: 0.86rem;
}
html[data-theme="dark"] .hint {
color: #9aa9c7;
}
html[data-theme="dark"] .status-pill,
html[data-theme="dark"] .result-item,
html[data-theme="dark"] .toolbar-row {
background: rgba(20, 29, 49, 0.86);
border-color: rgba(130, 152, 196, 0.35);
color: #dbe6ff;
}
html[data-theme="dark"] .file-picker {
border-color: rgba(130, 152, 196, 0.52);
background:
linear-gradient(140deg, rgba(19, 30, 52, 0.92) 0%, rgba(32, 45, 72, 0.9) 100%);
}
html[data-theme="dark"] .file-picker:hover {
border-color: rgba(154, 169, 255, 0.72);
box-shadow: 0 8px 20px rgba(100, 123, 255, 0.26);
}
html[data-theme="dark"] .file-picker.is-dragover {
border-color: rgba(52, 211, 153, 0.76);
box-shadow: 0 10px 22px rgba(16, 185, 129, 0.24);
background:
linear-gradient(140deg, rgba(13, 49, 47, 0.92) 0%, rgba(23, 68, 62, 0.88) 100%);
}
html[data-theme="dark"] .file-choose-btn {
border-color: rgba(154, 169, 255, 0.45);
background: rgba(26, 37, 63, 0.94);
color: #e4ecff;
}
html[data-theme="dark"] .file-picker-title {
color: #e6eeff;
}
html[data-theme="dark"] .file-picker-hint {
color: #9db0d0;
}
html[data-theme="dark"] .file-count-badge {
border-color: rgba(154, 169, 255, 0.34);
background: rgba(154, 169, 255, 0.22);
color: #d9e4ff;
}
html[data-theme="dark"] .file-queue-wrap {
border-color: rgba(130, 152, 196, 0.34);
background: rgba(20, 29, 49, 0.82);
}
html[data-theme="dark"] .file-queue-title {
color: #cbd8f4;
}
html[data-theme="dark"] .file-queue-clear {
border-color: rgba(244, 114, 114, 0.5);
background: rgba(66, 25, 34, 0.7);
color: #fecaca;
}
html[data-theme="dark"] .file-queue-item {
border-color: rgba(154, 169, 255, 0.26);
background: rgba(28, 40, 64, 0.9);
}
html[data-theme="dark"] .file-queue-icon {
background: rgba(154, 169, 255, 0.2);
color: #dbe5ff;
}
html[data-theme="dark"] .file-queue-name {
color: #e4ecff;
}
html[data-theme="dark"] .file-queue-size,
html[data-theme="dark"] .file-queue-more {
color: #9db0d0;
}
html[data-theme="dark"] .card-sub,
html[data-theme="dark"] .input-label,
html[data-theme="dark"] .result-item .result-meta,
html[data-theme="dark"] .result-empty {
color: #90a0bf;
}
html[data-theme="dark"] .result-empty {
border-color: rgba(130, 152, 196, 0.35);
}
.status-line {
color: #64748b;
font-size: 0.9rem;
}
html[data-theme="dark"] .status-line {
color: #9aa9c7;
}
.status-line.ok {
color: #16a34a;
}
.status-line.bad {
color: #ef4444;
}
.result-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 10px;
}
.result-item {
border: 1px solid var(--wf-border);
border-radius: 12px;
background: rgba(255, 255, 255, 0.74);
padding: 10px 12px;
display: grid;
gap: 4px;
}
.result-item .result-title {
font-size: 0.92rem;
font-weight: 700;
color: inherit;
word-break: break-word;
}
.result-item .result-meta {
font-size: 0.82rem;
color: #7a8399;
}
.result-list a {
color: #0f62fe;
font-size: 0.85rem;
word-break: break-all;
}
.result-list .error {
color: #ef4444;
}
.result-empty {
padding: 20px 16px;
text-align: center;
color: #7a8399;
border: 1px dashed var(--wf-border);
border-radius: 12px;
}
.span-2 {
grid-column: 1 / -1;
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
}
.span-2 {
grid-column: auto;
}
}
@media (max-width: 640px) {
.page {
padding: 10px;
}
.header {
top: 8px;
padding: 10px 12px;
flex-wrap: wrap;
}
.header-title {
width: 100%;
font-size: 1rem;
}
.header-actions {
width: 100%;
justify-content: flex-start;
overflow-x: auto;
scrollbar-width: none;
}
.header-actions::-webkit-scrollbar {
display: none;
}
.action-link,
.action-btn {
flex: 0 0 auto;
font-size: 0.82rem;
}
.toolbar-row {
flex-direction: column;
align-items: flex-start;
}
.upload-file-row {
flex-direction: column;
}
#uploadFilesBtn {
width: 100%;
}
}
</style>
<link rel="stylesheet" href="/mobile-refactor.css" />
</head>
<body>
<main class="page">
<header class="header">
<h1 class="header-title">
<img
src="/logo.png"
alt="K-Vault Logo"
class="header-logo"
loading="eager"
onerror="this.onerror=null;this.src='/favicon.ico';"
/>
<span>WebDAV 上传中心</span>
</h1>
<div class="header-actions">
<a class="action-link" href="./"><i class="fas fa-home"></i><span>首页</span></a>
<a class="action-link" href="./gallery.html"><i class="fas fa-images"></i><span>图片浏览</span></a>
<a class="action-link" href="./admin.html"><i class="fas fa-folder-tree"></i><span>管理后台</span></a>
<a class="action-link active" href="./webdav.html"><i class="fas fa-hard-drive"></i><span>WebDAV</span></a>
<button type="button" class="action-btn" data-theme-toggle aria-label="切换主题">
<i class="fas fa-moon" data-theme-icon></i>
<span data-theme-label>夜间</span>
</button>
</div>
</header>
<section class="layout">
<article class="toolbar-row">
<div class="toolbar-left">
<div class="status-pill" id="statusPill">
<span class="status-dot"></span>
<span id="statusText">正在检测 WebDAV 状态...</span>
</div>
<div class="status-pill">
<i class="fas fa-folder-tree"></i>
<span id="folderPathPreview">当前目录:根目录</span>
</div>
</div>
<div class="toolbar-right">
<button id="copyAllBtn" type="button">复制全部链接</button>
<button id="clearResultBtn" type="button" class="btn-danger">清空记录</button>
</div>
</article>
<article class="card">
<div class="card-header">
<div>
<h2><i class="fas fa-circle-info"></i> WebDAV 状态</h2>
<p class="card-sub">用于确认 WebDAV 配置和连接情况。</p>
</div>
<button id="refreshStatusBtn" type="button"><i class="fas fa-rotate"></i> 刷新状态</button>
</div>
<p id="statusDetailText" class="status-line">等待检测</p>
</article>
<article class="card">
<div class="card-header">
<div>
<h2><i class="fas fa-folder-open"></i> 上传目录</h2>
<p class="card-sub">可填写多级目录例如项目A/一月。</p>
</div>
</div>
<label class="input-label" for="folderPathInput">目录路径</label>
<div class="row">
<input id="folderPathInput" type="text" placeholder="留空则上传到根目录" />
</div>
<p class="hint">后台可继续移动目录,已生成直链不会受影响。</p>
</article>
<article class="card">
<div class="card-header">
<div>
<h2><i class="fas fa-cloud-upload-alt"></i> 文件上传</h2>
<p class="card-sub">支持批量上传到 WebDAV。</p>
</div>
</div>
<div class="upload-file-row">
<div class="file-picker" id="filePickerBox" role="button" tabindex="0" aria-label="选择或拖拽文件">
<input id="fileInput" class="file-input-native" type="file" multiple />
<button id="chooseFileBtn" type="button" class="file-choose-btn">
<i class="fas fa-folder-open"></i>
<span>选择文件</span>
</button>
<div class="file-picker-meta">
<div id="filePickerTitle" class="file-picker-title">未选择任何文件</div>
<div id="filePickerHint" class="file-picker-hint">支持多选、拖拽到此区域,上传后自动记录直链</div>
</div>
<span id="fileCountBadge" class="file-count-badge">0 个</span>
</div>
<button id="uploadFilesBtn" type="button" class="btn-primary">开始上传</button>
</div>
<div id="fileQueueWrap" class="file-queue-wrap" style="display:none;">
<div class="file-queue-head">
<span class="file-queue-title"><i class="fas fa-list-ul"></i> 待上传队列</span>
<button id="clearQueueBtn" type="button" class="file-queue-clear">清空队列</button>
</div>
<ul id="fileQueueList" class="file-queue-list"></ul>
</div>
</article>
<article class="card">
<div class="card-header">
<div>
<h2><i class="fas fa-link"></i> URL 上传</h2>
<p class="card-sub">输入文件地址,服务器将自动拉取并上传。</p>
</div>
</div>
<div class="row">
<input id="urlInput" type="text" placeholder="https://example.com/file.zip" />
<button id="uploadUrlBtn" type="button" class="btn-primary">上传 URL</button>
</div>
</article>
<article class="card span-2">
<div class="card-header">
<h2><i class="fas fa-list-check"></i> 上传结果</h2>
<div class="result-tools">
<span id="resultCount" class="card-sub">0 条</span>
</div>
</div>
<div id="resultEmpty" class="result-empty">暂无上传结果,完成上传后将显示在这里。</div>
<ul id="resultList" class="result-list"></ul>
</article>
</section>
</main>
<script>
(function () {
"use strict";
var statusPill = document.getElementById("statusPill");
var statusText = document.getElementById("statusText");
var statusDetailText = document.getElementById("statusDetailText");
var folderPathPreview = document.getElementById("folderPathPreview");
var folderPathInput = document.getElementById("folderPathInput");
var filePickerBox = document.getElementById("filePickerBox");
var chooseFileBtn = document.getElementById("chooseFileBtn");
var filePickerTitle = document.getElementById("filePickerTitle");
var filePickerHint = document.getElementById("filePickerHint");
var fileCountBadge = document.getElementById("fileCountBadge");
var fileQueueWrap = document.getElementById("fileQueueWrap");
var fileQueueList = document.getElementById("fileQueueList");
var clearQueueBtn = document.getElementById("clearQueueBtn");
var fileInput = document.getElementById("fileInput");
var uploadFilesBtn = document.getElementById("uploadFilesBtn");
var urlInput = document.getElementById("urlInput");
var uploadUrlBtn = document.getElementById("uploadUrlBtn");
var resultList = document.getElementById("resultList");
var resultEmpty = document.getElementById("resultEmpty");
var resultCount = document.getElementById("resultCount");
var refreshStatusBtn = document.getElementById("refreshStatusBtn");
var clearResultBtn = document.getElementById("clearResultBtn");
var copyAllBtn = document.getElementById("copyAllBtn");
var resultLinks = [];
var fallbackSelectedFiles = [];
function normalizeFolderPath(value) {
return String(value || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").trim();
}
function updateFolderPreview() {
var folderPath = normalizeFolderPath(folderPathInput.value);
folderPathPreview.textContent = "当前目录:" + (folderPath || "根目录");
try {
localStorage.setItem("webdavFolderPath", folderPath);
} catch {
// ignore
}
}
function ensureArray(value) {
if (Array.isArray(value)) return value;
if (value == null) return [];
if (typeof value.length === "number") return Array.prototype.slice.call(value);
return [value];
}
function formatFileSize(bytes) {
var value = Number(bytes || 0);
if (!Number.isFinite(value) || value <= 0) return "0 B";
var units = ["B", "KB", "MB", "GB", "TB"];
var index = 0;
while (value >= 1024 && index < units.length - 1) {
value /= 1024;
index += 1;
}
return value.toFixed(value >= 100 || index === 0 ? 0 : 1) + " " + units[index];
}
function resolveFileIconClass(file) {
var name = String((file && file.name) || "").toLowerCase();
var mime = String((file && file.type) || "").toLowerCase();
if (mime.startsWith("image/")) return "fas fa-image";
if (mime.startsWith("video/")) return "fas fa-film";
if (mime.startsWith("audio/")) return "fas fa-music";
if (mime.includes("zip") || mime.includes("rar") || mime.includes("7z") || /\.zip$|\.rar$|\.7z$|\.tar$|\.gz$/i.test(name)) return "fas fa-file-archive";
if (mime.includes("pdf") || /\.pdf$/i.test(name)) return "fas fa-file-pdf";
if (mime.includes("word") || /\.docx?$|\.odt$/i.test(name)) return "fas fa-file-word";
if (mime.includes("excel") || /\.xlsx?$|\.csv$/i.test(name)) return "fas fa-file-excel";
if (mime.includes("text") || /\.txt$|\.md$/i.test(name)) return "fas fa-file-lines";
return "fas fa-file";
}
function renderFileQueue(files) {
var list = ensureArray(files);
if (!list.length) {
fileQueueWrap.style.display = "none";
fileQueueList.innerHTML = "";
return;
}
fileQueueWrap.style.display = "block";
fileQueueList.innerHTML = "";
var maxVisible = 6;
list.slice(0, maxVisible).forEach(function (file) {
var item = document.createElement("li");
item.className = "file-queue-item";
var icon = document.createElement("span");
icon.className = "file-queue-icon";
var iconNode = document.createElement("i");
iconNode.className = resolveFileIconClass(file);
icon.appendChild(iconNode);
var meta = document.createElement("div");
meta.className = "file-queue-meta";
var name = document.createElement("div");
name.className = "file-queue-name";
name.textContent = String(file.name || "未命名文件");
var size = document.createElement("div");
size.className = "file-queue-size";
size.textContent = formatFileSize(file.size || 0);
meta.appendChild(name);
meta.appendChild(size);
item.appendChild(icon);
item.appendChild(meta);
fileQueueList.appendChild(item);
});
if (list.length > maxVisible) {
var more = document.createElement("li");
more.className = "file-queue-more";
more.textContent = "还有 " + (list.length - maxVisible) + " 个文件未展开";
fileQueueList.appendChild(more);
}
}
function getSelectedFiles() {
var chosen = ensureArray(fileInput.files);
if (chosen.length) return chosen;
return ensureArray(fallbackSelectedFiles);
}
function updateFilePickerState(files) {
var picked = ensureArray(files);
var count = picked.length;
if (!count) {
filePickerBox.classList.remove("has-files");
filePickerTitle.textContent = "未选择任何文件";
filePickerHint.textContent = "支持多选、拖拽到此区域,上传后自动记录直链";
fileCountBadge.textContent = "0 个";
renderFileQueue([]);
return;
}
var totalBytes = picked.reduce(function (sum, file) {
return sum + Number((file && file.size) || 0);
}, 0);
var firstName = (picked[0] && picked[0].name) ? picked[0].name : "文件";
filePickerBox.classList.add("has-files");
fileCountBadge.textContent = count + " 个";
if (count === 1) {
filePickerTitle.textContent = firstName;
filePickerHint.textContent = "大小:" + formatFileSize(totalBytes) + ",点击“开始上传”继续";
renderFileQueue(picked);
return;
}
filePickerTitle.textContent = "已选择 " + count + " 个文件";
filePickerHint.textContent = "总大小:" + formatFileSize(totalBytes) + ",首个文件:" + firstName;
renderFileQueue(picked);
}
function syncFilesFromDrop(files) {
var dropped = ensureArray(files).filter(Boolean);
if (!dropped.length) return;
fallbackSelectedFiles = dropped;
try {
if (typeof DataTransfer !== "undefined") {
var dt = new DataTransfer();
dropped.forEach(function (file) {
dt.items.add(file);
});
fileInput.files = dt.files;
fallbackSelectedFiles = [];
}
} catch {
// keep fallbackSelectedFiles for browsers that block assignment
}
updateFilePickerState(getSelectedFiles());
}
function clearSelectedFiles() {
fileInput.value = "";
fallbackSelectedFiles = [];
updateFilePickerState([]);
}
function toAbsoluteUrl(value) {
var raw = String(value || "").trim();
if (!raw) return "";
try {
return new URL(raw, window.location.origin).toString();
} catch {
return raw;
}
}
function renderResultState() {
resultCount.textContent = resultList.children.length + " 条";
resultEmpty.style.display = resultList.children.length > 0 ? "none" : "block";
}
function pushResult(message, kind, link) {
var item = document.createElement("li");
item.className = "result-item";
var title = document.createElement("div");
title.className = "result-title";
title.textContent = message;
var meta = document.createElement("div");
meta.className = "result-meta";
meta.textContent = new Date().toLocaleString("zh-CN", { hour12: false }) + " · " + (normalizeFolderPath(folderPathInput.value) || "根目录");
item.appendChild(title);
item.appendChild(meta);
if (kind === "error") {
title.classList.add("error");
}
if (link) {
var absoluteLink = toAbsoluteUrl(link);
var anchor = document.createElement("a");
anchor.href = absoluteLink;
anchor.target = "_blank";
anchor.rel = "noopener";
anchor.textContent = absoluteLink;
item.appendChild(anchor);
resultLinks.unshift(absoluteLink);
}
resultList.prepend(item);
if (resultList.children.length > 120) {
resultList.removeChild(resultList.lastChild);
}
if (resultLinks.length > 120) {
resultLinks = resultLinks.slice(0, 120);
}
renderResultState();
}
async function request(url, options) {
var response = await fetch(url, Object.assign({ credentials: "include" }, options || {}));
var data = await response.json().catch(function () {
return null;
});
if (!response.ok) {
var message = data && (data.error || data.message || data.errorDetail);
if (!message) {
message = "请求失败(" + response.status + "";
}
throw new Error(message);
}
return data;
}
function setStatus(message, ok) {
statusPill.classList.remove("ok", "bad");
if (ok === true) statusPill.classList.add("ok");
if (ok === false) statusPill.classList.add("bad");
statusText.textContent = message;
statusDetailText.textContent = message;
}
async function checkStatus() {
setStatus("正在检测 WebDAV 状态...", null);
var data = await request("./api/status");
var webdav = data.webdav || {};
var ok = Boolean(webdav.connected && webdav.enabled);
if (ok) {
setStatus("WebDAV 已连接并启用,可正常上传。", true);
} else {
var detail = webdav.message || "未配置";
setStatus("WebDAV 当前不可用:" + detail, false);
}
}
async function uploadFiles() {
var files = getSelectedFiles();
if (!files.length) {
setStatus("请先选择要上传的文件。", false);
return;
}
uploadFilesBtn.disabled = true;
try {
var folderPath = normalizeFolderPath(folderPathInput.value);
for (var i = 0; i < files.length; i += 1) {
setStatus("正在上传第 " + (i + 1) + " / " + files.length + " 个文件...", null);
var form = new FormData();
form.append("file", files[i]);
form.append("storageMode", "webdav");
form.append("folderPath", folderPath);
var payload = await request("./upload", { method: "POST", body: form });
var first = Array.isArray(payload) ? payload[0] : payload;
var src = first && first.src ? String(first.src) : "";
if (src) {
pushResult("文件上传成功", "ok", toAbsoluteUrl(src));
} else {
pushResult("文件上传成功,但未返回直链。", "ok");
}
}
clearSelectedFiles();
setStatus("全部文件上传完成。", true);
} finally {
uploadFilesBtn.disabled = false;
}
}
async function uploadFromUrl() {
var sourceUrl = String(urlInput.value || "").trim();
if (!sourceUrl) {
setStatus("请先输入 URL 地址。", false);
return;
}
uploadUrlBtn.disabled = true;
try {
var folderPath = normalizeFolderPath(folderPathInput.value);
setStatus("正在拉取远程地址并上传...", null);
var payload = await request("./api/upload-from-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: sourceUrl,
storageMode: "webdav",
folderPath: folderPath,
}),
});
var src = payload && (payload.src || payload.url || (payload.file && payload.file.src));
if (src) {
pushResult("URL 上传成功", "ok", toAbsoluteUrl(String(src)));
} else {
pushResult("URL 上传完成。", "ok");
}
setStatus("URL 上传完成。", true);
} finally {
uploadUrlBtn.disabled = false;
}
}
async function ensureAuth() {
var auth = await request("./api/auth/check");
if (auth.authRequired && !auth.authenticated) {
var redirect = encodeURIComponent(window.location.pathname + window.location.search);
window.location.href = "./login.html?redirect=" + redirect;
return false;
}
return true;
}
function onError(prefix, error) {
var message = prefix + "" + ((error && error.message) || "未知错误");
pushResult(message, "error");
setStatus(message, false);
}
folderPathInput.addEventListener("input", updateFolderPreview);
clearQueueBtn.addEventListener("click", function (event) {
event.preventDefault();
clearSelectedFiles();
setStatus("已清空待上传队列。", null);
});
chooseFileBtn.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
fileInput.click();
});
filePickerBox.addEventListener("click", function (event) {
var target = event.target;
if (target && target.id === "chooseFileBtn") return;
fileInput.click();
});
filePickerBox.addEventListener("keydown", function (event) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
fileInput.click();
}
});
filePickerBox.addEventListener("dragover", function (event) {
event.preventDefault();
filePickerBox.classList.add("is-dragover");
});
filePickerBox.addEventListener("dragenter", function (event) {
event.preventDefault();
filePickerBox.classList.add("is-dragover");
});
filePickerBox.addEventListener("dragleave", function (event) {
var next = event.relatedTarget;
if (next && filePickerBox.contains(next)) return;
filePickerBox.classList.remove("is-dragover");
});
filePickerBox.addEventListener("drop", function (event) {
event.preventDefault();
filePickerBox.classList.remove("is-dragover");
var dropped = event.dataTransfer ? ensureArray(event.dataTransfer.files) : [];
if (!dropped.length) return;
syncFilesFromDrop(dropped);
setStatus("已拖入 " + dropped.length + " 个文件,点击“开始上传”即可。", null);
});
fileInput.addEventListener("change", function () {
fallbackSelectedFiles = [];
var files = getSelectedFiles();
updateFilePickerState(files);
if (!files.length) {
setStatus("请选择要上传的文件。", null);
return;
}
setStatus("已选择 " + files.length + " 个文件,点击“开始上传”即可。", null);
});
refreshStatusBtn.addEventListener("click", function () {
checkStatus().catch(function (error) {
setStatus(error.message || "状态检测失败。", false);
});
});
uploadFilesBtn.addEventListener("click", function () {
uploadFiles().catch(function (error) {
onError("文件上传失败", error);
});
});
uploadUrlBtn.addEventListener("click", function () {
uploadFromUrl().catch(function (error) {
onError("URL 上传失败", error);
});
});
urlInput.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
event.preventDefault();
uploadUrlBtn.click();
}
});
clearResultBtn.addEventListener("click", function () {
resultList.innerHTML = "";
resultLinks = [];
renderResultState();
setStatus("已清空上传记录。", null);
});
copyAllBtn.addEventListener("click", function () {
var links = resultLinks.filter(Boolean);
if (!links.length) {
setStatus("当前没有可复制的链接。", false);
return;
}
var text = links.join("\n");
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text)
.then(function () {
setStatus("已复制 " + links.length + " 条链接。", true);
})
.catch(function (error) {
setStatus(error.message || "复制失败。", false);
});
return;
}
var textarea = document.createElement("textarea");
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
var copied = false;
try {
copied = document.execCommand("copy");
} catch {
copied = false;
}
textarea.remove();
setStatus(copied ? ("已复制 " + links.length + " 条链接。") : "复制失败。", copied);
});
(async function init() {
var cachedFolderPath = "";
try {
cachedFolderPath = localStorage.getItem("webdavFolderPath") || "";
} catch {
cachedFolderPath = "";
}
folderPathInput.value = normalizeFolderPath(cachedFolderPath);
updateFolderPreview();
renderResultState();
updateFilePickerState([]);
var authenticated = await ensureAuth();
if (!authenticated) return;
await checkStatus();
})().catch(function (error) {
setStatus(error.message || "页面初始化失败。", false);
});
})();
</script>
</body>
</html>