From 730ee2004855f9437b65b79bb25d384c0c4d3b2b Mon Sep 17 00:00:00 2001 From: RhenCloud Date: Sat, 15 Nov 2025 12:45:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=A8=A1=E5=9D=97=E5=8C=96?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E4=BB=A3=E7=A0=81=20-=20=E5=B0=86=20main.js?= =?UTF-8?q?=20=E5=92=8C=20main.css=20=E6=8B=86=E5=88=86=E4=B8=BA=E4=B8=93?= =?UTF-8?q?=E7=94=A8=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 main.js 并替换为8个 JS 模块 - 移除 main.css 并替换为7个 CSS 模块 - 更新 base.html 以加载模块化文件 - 通过 index.css 保持完全向后兼容 - 改进代码组织、可维护性和可复用性 --- static/css/buttons.css | 149 +++++ static/css/grid.css | 169 ++++++ static/css/index.css | 22 + static/css/main.css | 999 ------------------------------ static/css/modals.css | 339 +++++++++++ static/css/table.css | 197 ++++++ static/css/utilities.css | 85 +++ static/css/variables.css | 95 +++ static/js/dialog.js | 235 ++++++++ static/js/download.js | 27 + static/js/file-operations.js | 408 +++++++++++++ static/js/main.js | 1099 ---------------------------------- static/js/preview.js | 189 ++++++ static/js/selection.js | 178 ++++++ static/js/theme.js | 98 +++ static/js/ui-utils.js | 44 ++ static/js/utilities.js | 68 +++ templates/base.html | 21 +- 18 files changed, 2322 insertions(+), 2100 deletions(-) create mode 100644 static/css/buttons.css create mode 100644 static/css/grid.css create mode 100644 static/css/index.css delete mode 100644 static/css/main.css create mode 100644 static/css/modals.css create mode 100644 static/css/table.css create mode 100644 static/css/utilities.css create mode 100644 static/css/variables.css create mode 100644 static/js/dialog.js create mode 100644 static/js/download.js create mode 100644 static/js/file-operations.js delete mode 100644 static/js/main.js create mode 100644 static/js/preview.js create mode 100644 static/js/selection.js create mode 100644 static/js/theme.js create mode 100644 static/js/ui-utils.js create mode 100644 static/js/utilities.js diff --git a/static/css/buttons.css b/static/css/buttons.css new file mode 100644 index 0000000..53fc9fe --- /dev/null +++ b/static/css/buttons.css @@ -0,0 +1,149 @@ +/** + * 按钮样式 + */ + +/* 通用按钮基类 */ +.btn { + background: none; + border: none; + cursor: pointer; + padding: 8px; + border-radius: 6px; + font-size: 1em; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; + font-family: inherit; +} + +.btn:hover { + background-color: var(--hover-bg); + transform: translateY(-1px); +} + +.btn:active { + transform: translateY(0); +} + +/* 导航栏按钮 */ +.nav-btn { + color: var(--secondary-text); + font-size: 1.1em; + padding: 8px 10px; +} + +.nav-btn:hover { + color: var(--text-color); +} + +/* 主题/视图切换按钮 */ +.theme-toggle, +.view-toggle { + background: none; + border: none; + color: var(--secondary-text); + cursor: pointer; + padding: 8px 10px; + border-radius: 6px; + font-size: 1.1em; + transition: all 0.2s ease; +} + +.theme-toggle:hover, +.view-toggle:hover { + background-color: var(--hover-bg); + color: var(--text-color); + transform: translateY(-1px); +} + +.theme-toggle:active, +.view-toggle:active { + transform: translateY(0); +} + +/* 删除按钮 */ +.delete-btn { + background: none; + border: none; + color: var(--danger-color); + cursor: pointer; + padding: 6px 10px; + border-radius: 6px; + font-size: 0.9em; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.delete-btn:hover { + background-color: rgba(220, 53, 69, 0.1); + color: var(--danger-hover-color); + transform: translateY(-1px); +} + +.delete-btn:active { + transform: translateY(0); +} + +/* 下载按钮 */ +.download-btn { + background: none; + border: none; + color: var(--success-color); + cursor: pointer; + padding: 6px 10px; + border-radius: 6px; + font-size: 0.9em; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.download-btn:hover { + background-color: rgba(40, 167, 69, 0.1); + color: #218838; + transform: translateY(-1px); +} + +.download-btn:active { + transform: translateY(0); +} + +/* 操作链接按钮 */ +.action-link { + color: var(--link-color); + text-decoration: none; + padding: 4px 8px; + border-radius: 4px; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.action-link:hover { + background-color: var(--hover-bg); + text-decoration: none; +} + +button.action-link { + border: none; + background: none; + cursor: pointer; + font-size: 0.9em; + font-family: inherit; +} + +/* 重命名按钮 */ +.rename-btn, +.grid-action-btn.rename { + color: var(--warning-color); +} + +.rename-btn:hover, +.grid-action-btn.rename:hover { + color: var(--warning-hover-color); +} diff --git a/static/css/grid.css b/static/css/grid.css new file mode 100644 index 0000000..624b575 --- /dev/null +++ b/static/css/grid.css @@ -0,0 +1,169 @@ +/** + * 网格视图样式 + */ + +/* Grid view 容器 */ +.grid-container { + display: none; + margin-top: 20px; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; +} + +:root[data-view="grid"] .files-table { + display: none; +} + +:root[data-view="grid"] .grid-container { + display: grid; +} + +/* 网格卡片 */ +.grid-card { + background: var(--container-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 10px; + text-align: center; + box-shadow: 0 1px 2px var(--shadow-color); +} + +.grid-checkbox { + text-align: left; + margin-bottom: 8px; +} + +.grid-checkbox input { + transform: scale(1.05); + accent-color: var(--link-color); +} + +/* 网格缩略图 */ +.grid-thumb { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 6px; + margin-bottom: 8px; + background-color: var(--icon-bg); + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.grid-thumb:hover { + transform: scale(1.02); + box-shadow: 0 2px 8px var(--shadow-color); +} + +/* 网格图标容器 */ +.grid-icon { + width: 100%; + height: 100px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; + border-radius: 6px; + background-color: var(--icon-bg); + cursor: pointer; + transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; +} + +.grid-icon:hover { + transform: scale(1.02); + background-color: var(--hover-bg); + box-shadow: 0 2px 8px var(--shadow-color); +} + +.grid-icon i { + font-size: 64px; +} + +/* 网格卡片文件名 */ +.grid-name { + display: block; + font-size: 0.95em; + color: var(--text-color); + margin-bottom: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 网格卡片操作按钮 */ +.grid-actions { + display: flex; + gap: 4px; + justify-content: center; + margin-top: 8px; +} + +.grid-action-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px 6px; + border-radius: 4px; + font-size: 0.85em; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 3px; +} + +.grid-action-btn.preview { + color: var(--link-color); +} + +.grid-action-btn.preview:hover { + background-color: rgba(0, 123, 255, 0.1); +} + +.grid-action-btn.download { + color: var(--success-color); +} + +.grid-action-btn.download:hover { + background-color: rgba(40, 167, 69, 0.1); +} + +.grid-action-btn.delete { + color: var(--danger-color); +} + +.grid-action-btn.delete:hover { + background-color: rgba(220, 53, 69, 0.1); +} + +@media (max-width: 768px) { + .grid-container { + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + } + + .grid-icon i { + font-size: 48px; + } +} + +@media (max-width: 480px) { + .grid-container { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; + } + + .grid-card { + padding: 8px; + } + + .grid-icon i { + font-size: 40px; + } + + .grid-name { + font-size: 0.9em; + } + + .grid-actions { + gap: 2px; + } +} diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..be1b324 --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,22 @@ +/** + * Cloud-Index 主样式表 + * 这个文件导入所有模块化的 CSS 文件 + */ + +/* 1. 变量和基础样式 */ +@import url("variables.css"); + +/* 2. 按钮样式 */ +@import url("buttons.css"); + +/* 3. 表格视图 */ +@import url("table.css"); + +/* 4. 网格视图 */ +@import url("grid.css"); + +/* 5. 模态框和对话框 */ +@import url("modals.css"); + +/* 6. 实用工具样式 */ +@import url("utilities.css"); diff --git a/static/css/main.css b/static/css/main.css deleted file mode 100644 index 451c877..0000000 --- a/static/css/main.css +++ /dev/null @@ -1,999 +0,0 @@ -:root { - --bg-color: #f5f5f5; - --container-bg: white; - --text-color: #2c3e50; - --border-color: #eee; - --hover-bg: #f8f9fa; - --secondary-text: #6c757d; - --link-color: #007bff; - --folder-color: #ffc107; - --file-color: #6c757d; - --shadow-color: rgba(0, 0, 0, 0.1); - --icon-bg: #f0f0f0; -} - -[data-theme="dark"] { - --bg-color: #1a1a1a; - --container-bg: #2d2d2d; - --text-color: #e1e1e1; - --border-color: #404040; - --hover-bg: #363636; - --secondary-text: #a0a0a0; - --link-color: #66b3ff; - --folder-color: #ffd54f; - --file-color: #b0b0b0; - --shadow-color: rgba(0, 0, 0, 0.3); - --icon-bg: #3a3a3a; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - padding: 20px; - background-color: var(--bg-color); - color: var(--text-color); - transition: background-color 0.3s ease, color 0.3s ease; -} - -.container { - background-color: var(--container-bg); - border-radius: 8px; - box-shadow: 0 2px 4px var(--shadow-color); - padding: 20px; - width: calc(100% - 40px); - max-width: 1200px; - margin: 0 auto; -} - -h1 { - color: var(--text-color); - margin-bottom: 30px; - border-bottom: 2px solid var(--border-color); - padding-bottom: 10px; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 10px; -} - -h1 > div { - display: flex; - gap: 8px; - align-items: center; -} - -/* 通用按钮样式 */ -.btn { - background: none; - border: none; - cursor: pointer; - padding: 8px; - border-radius: 6px; - font-size: 1em; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - gap: 6px; -} - -.btn:hover { - background-color: var(--hover-bg); - transform: translateY(-1px); -} - -.btn:active { - transform: translateY(0); -} - -/* 导航栏按钮 */ -.nav-btn { - color: var(--secondary-text); - font-size: 1.1em; - padding: 8px 10px; -} - -.nav-btn:hover { - color: var(--text-color); -} - -/* 兼容旧的类名 */ -.theme-toggle, -.view-toggle { - background: none; - border: none; - color: var(--secondary-text); - cursor: pointer; - padding: 8px 10px; - border-radius: 6px; - font-size: 1.1em; - transition: all 0.2s ease; -} - -.theme-toggle:hover, -.view-toggle:hover { - background-color: var(--hover-bg); - color: var(--text-color); - transform: translateY(-1px); -} - -.theme-toggle:active, -.view-toggle:active { - transform: translateY(0); -} - -/* 导航栏删除按钮特殊样式 */ -.view-toggle[style*="color: #dc3545"]:hover { - color: #c82333 !important; - background-color: rgba(220, 53, 69, 0.1); -} - -/* Grid view styles */ -.grid-container { - display: none; - margin-top: 20px; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: 12px; -} - -.grid-card { - background: var(--container-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 10px; - text-align: center; - box-shadow: 0 1px 2px var(--shadow-color); -} - -.grid-checkbox { - text-align: left; - margin-bottom: 8px; -} - -.grid-checkbox input { - transform: scale(1.05); - accent-color: var(--link-color); -} - -.grid-thumb { - width: 100%; - height: 100px; - object-fit: cover; - border-radius: 6px; - margin-bottom: 8px; - background-color: var(--icon-bg); - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.grid-thumb:hover { - transform: scale(1.02); - box-shadow: 0 2px 8px var(--shadow-color); -} - -/* 网格卡片中的图标容器 */ -.grid-icon { - width: 100%; - height: 100px; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 8px; - border-radius: 6px; - background-color: var(--icon-bg); - cursor: pointer; - transition: transform 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; -} - -.grid-icon:hover { - transform: scale(1.02); - background-color: var(--hover-bg); - box-shadow: 0 2px 8px var(--shadow-color); -} - -.grid-icon i { - font-size: 64px; -} - -.grid-name { - display: block; - font-size: 0.95em; - color: var(--text-color); - margin-bottom: 6px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* 网格卡片操作按钮 */ -.grid-actions { - display: flex; - gap: 4px; - justify-content: center; - margin-top: 8px; -} - -.grid-action-btn { - background: none; - border: none; - cursor: pointer; - padding: 4px 6px; - border-radius: 4px; - font-size: 0.85em; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - gap: 3px; -} - -.grid-action-btn.preview { - color: var(--link-color); -} - -.grid-action-btn.preview:hover { - background-color: rgba(0, 123, 255, 0.1); -} - -.grid-action-btn.download { - color: #28a745; -} - -.grid-action-btn.download:hover { - background-color: rgba(40, 167, 69, 0.1); -} - -.grid-action-btn.delete { - color: #dc3545; -} - -.grid-action-btn.delete:hover { - background-color: rgba(220, 53, 69, 0.1); -} - -/* Toggle visibility based on data-view on root */ -:root[data-view="grid"] .files-table { - display: none; -} - -:root[data-view="grid"] .grid-container { - display: grid; -} - -.files-table { - width: 100%; - border-collapse: collapse; - margin-top: 20px; -} - -.checkbox-col { - width: 48px; - text-align: center; -} - -.checkbox-col input { - transform: scale(1.05); - accent-color: var(--link-color); -} - -.files-table th, -.files-table td { - padding: 12px; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.files-table th { - background-color: var(--hover-bg); - color: var(--text-color); - font-weight: 600; -} - -.files-table tr:hover { - background-color: var(--hover-bg); -} - -.file-name-col { - cursor: pointer; - user-select: none; -} - -.file-name-col:hover { - background-color: var(--hover-bg); -} - -.file-icon { - margin-right: 8px; - pointer-events: none; -} - -.folder { - color: var(--folder-color); -} - -.file { - color: var(--file-color); -} - -a { - color: var(--link-color); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -.file-size { - color: var(--secondary-text); - font-size: 0.9em; -} - -.last-modified { - color: var(--secondary-text); - font-size: 0.9em; -} - -.empty-message { - text-align: center; - padding: 40px; - color: var(--secondary-text); -} - -.breadcrumb { - margin-bottom: 12px; - font-size: 0.95em; - color: var(--secondary-text); - background-color: var(--container-bg); - padding: 8px 12px; - border-radius: 4px; -} - -/* 页脚样式 */ -.site-footer { - margin-top: 20px; - text-align: center; - color: var(--secondary-text); - font-size: 0.9em; - padding: 14px 12px; -} - -.site-footer a { - color: var(--link-color); - text-decoration: none; - margin: 0 6px; -} - -.site-footer a:hover { - text-decoration: underline; -} - -@media (max-width: 600px) { - .site-footer { - font-size: 0.85em; - padding: 12px 8px; - } -} - -/* 文件上传输入框 */ -.upload-input { - display: none; -} - -/* 删除按钮 */ -.delete-btn { - background: none; - border: none; - color: #dc3545; - cursor: pointer; - padding: 6px 10px; - border-radius: 6px; - font-size: 0.9em; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - gap: 4px; -} - -.delete-btn:hover { - background-color: rgba(220, 53, 69, 0.1); - color: #c82333; - transform: translateY(-1px); -} - -.delete-btn:active { - transform: translateY(0); -} - -/* 操作链接按钮 */ -.action-link { - color: var(--link-color); - text-decoration: none; - padding: 4px 8px; - border-radius: 4px; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - gap: 4px; -} - -.action-link:hover { - background-color: var(--hover-bg); - text-decoration: none; -} - -/* 预览按钮(button标签样式) */ -button.action-link { - border: none; - background: none; - cursor: pointer; - font-size: 0.9em; - font-family: inherit; -} - -/* 下载按钮 */ -.download-btn { - background: none; - border: none; - color: #28a745; - cursor: pointer; - padding: 6px 10px; - border-radius: 6px; - font-size: 0.9em; - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - gap: 4px; -} - -.download-btn:hover { - background-color: rgba(40, 167, 69, 0.1); - color: #218838; - transform: translateY(-1px); -} - -.download-btn:active { - transform: translateY(0); -} - -/* 预览模态框 */ -.preview-modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.9); - z-index: 1000; - opacity: 0; - transition: opacity 0.3s ease; -} - -.preview-modal.show { - display: flex; - opacity: 1; - justify-content: center; - align-items: center; -} - -.preview-container { - position: relative; - max-width: 90%; - max-height: 90%; - display: flex; - flex-direction: column; - align-items: center; -} - -.preview-content { - max-width: 100%; - max-height: 85vh; - object-fit: contain; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); -} - -.preview-controls { - position: absolute; - top: 20px; - right: 20px; - display: flex; - gap: 10px; - z-index: 1001; -} - -.preview-btn { - background-color: rgba(255, 255, 255, 0.9); - border: none; - color: #333; - width: 40px; - height: 40px; - border-radius: 50%; - cursor: pointer; - font-size: 1.2em; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); -} - -.preview-btn:hover { - background-color: white; - transform: scale(1.1); -} - -.preview-info { - margin-top: 15px; - color: white; - text-align: center; - font-size: 0.95em; - background-color: rgba(0, 0, 0, 0.7); - padding: 10px 20px; - border-radius: 20px; - max-width: 80%; - word-break: break-all; -} - -.preview-loading { - color: white; - font-size: 1.2em; - text-align: center; -} - -.preview-error { - color: #ff6b6b; - font-size: 1.1em; - text-align: center; - padding: 20px; - background-color: rgba(255, 255, 255, 0.1); - border-radius: 8px; -} - -video.preview-content { - background-color: #000; -} - -/* 音频预览样式 */ -.preview-audio-wrapper { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px; - background-color: rgba(0, 0, 0, 0.5); - border-radius: 12px; - min-width: 400px; -} - -.preview-audio { - width: 100%; - max-width: 500px; - margin-top: 20px; -} - -/* 文本预览样式 */ -.preview-text { - background-color: rgba(255, 255, 255, 0.95); - color: #2c3e50; - padding: 20px; - border-radius: 8px; - max-width: 90vw; - max-height: 85vh; - overflow: auto; - font-family: "Courier New", Courier, monospace; - font-size: 14px; - line-height: 1.6; - white-space: pre-wrap; - word-wrap: break-word; -} - -[data-theme="dark"] .preview-text { - background-color: rgba(45, 45, 45, 0.95); - color: #e1e1e1; -} - -/* 不支持预览的文件样式 */ -.preview-unsupported { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 60px 40px; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 12px; - color: white; - text-align: center; - min-width: 400px; -} - -.preview-unsupported p { - margin: 0; -} - -.preview-unsupported .action-link { - background-color: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.3); - color: white; -} - -.preview-unsupported .action-link:hover { - background-color: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.5); -} - -@media (max-width: 768px) { - .preview-container { - max-width: 95%; - } - - .preview-controls { - top: 10px; - right: 10px; - } - - .preview-btn { - width: 35px; - height: 35px; - font-size: 1em; - } - - .preview-info { - font-size: 0.85em; - padding: 8px 15px; - } - - .preview-audio-wrapper { - min-width: 300px; - padding: 30px 20px; - } - - .preview-unsupported { - min-width: 300px; - padding: 40px 20px; - } - - .preview-text { - font-size: 12px; - padding: 15px; - } - - .files-table .action-text { - display: none; - } - - .files-table .action-link { - padding: 6px; - font-size: 1.1em; - } - - .files-table td, - .files-table th { - padding: 8px 4px; - } -} - -/* 上传进度提示 */ -.upload-status { - padding: 10px; - border-radius: 4px; - margin-bottom: 10px; - display: none; -} - -.upload-status.success { - background-color: #d4edda; - color: #155724; - border: 1px solid #c3e6cb; - display: block; -} - -.upload-status.error { - background-color: #f8d7da; - color: #721c24; - border: 1px solid #f5c6cb; - display: block; -} - -/* 自定义对话框样式 */ -.app-dialog[hidden] { - display: none; -} - -.app-dialog { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background-color: rgba(0, 0, 0, 0.35); - backdrop-filter: blur(2px); - z-index: 1100; - opacity: 0; - pointer-events: none; - transition: opacity 0.2s ease; -} - -.app-dialog.is-visible { - opacity: 1; - pointer-events: auto; -} - -.app-dialog__backdrop { - position: absolute; - inset: 0; -} - -.app-dialog__panel { - position: relative; - background-color: var(--container-bg); - color: var(--text-color); - border-radius: 12px; - box-shadow: 0 18px 60px rgba(0, 0, 0, 0.25); - padding: 24px; - min-width: 320px; - max-width: min(420px, 90vw); - transform: translateY(20px); - transition: transform 0.2s ease; -} - -.app-dialog.is-visible .app-dialog__panel { - transform: translateY(0); -} - -.app-dialog__title { - margin: 0 0 12px; - font-size: 1.15em; - font-weight: 600; -} - -.app-dialog__message { - margin-bottom: 18px; - line-height: 1.5; - color: var(--text-color); - word-break: break-word; -} - -.app-dialog__input { - margin-bottom: 18px; - display: block; - width: 100%; -} - -.app-dialog__input input { - width: 100%; - padding: 10px 12px; - border-radius: 6px; - border: 1px solid var(--border-color); - background-color: var(--container-bg); - color: var(--text-color); - font-size: 0.95em; - box-sizing: border-box; - font-family: inherit; -} - -.app-dialog__input input::placeholder { - color: var(--secondary-text); -} - -.app-dialog__input input:focus { - outline: none; - border-color: var(--link-color); - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15); -} - -.app-dialog__actions { - display: flex; - justify-content: flex-end; - gap: 10px; -} - -.app-dialog__btn { - padding: 8px 16px; - border-radius: 6px; - border: none; - cursor: pointer; - font-size: 0.95em; - transition: transform 0.15s ease, background-color 0.2s ease; -} - -.app-dialog__btn:hover { - transform: translateY(-1px); -} - -.app-dialog__cancel { - background-color: var(--hover-bg); - color: var(--secondary-text); -} - -.app-dialog__cancel:hover { - background-color: rgba(0, 0, 0, 0.05); -} - -.app-dialog__confirm { - background-color: #dc3545; - color: #fff; -} - -.app-dialog__confirm:hover { - background-color: #c82333; -} - -@media (max-width: 480px) { - .app-dialog__panel { - width: calc(100vw - 40px); - padding: 20px; - } -} - -.action-link:hover, -.grid-action-btn:hover { - color: var(--link-hover-color); - background-color: var(--hover-bg-color); -} - -#deleteTrigger, -.delete-btn, -.grid-action-btn.delete { - color: var(--danger-color, #dc3545); -} - -#deleteTrigger:hover, -.delete-btn:hover, -.grid-action-btn.delete:hover { - color: var(--danger-hover-color, #a0202d); -} - -.rename-btn, -.grid-action-btn.rename { - color: var(--warning-color, #ffc107); -} - -.rename-btn:hover, -.grid-action-btn.rename:hover { - color: var(--warning-hover-color, #e0a800); -} - -.file-name-col { - width: auto; - max-width: 300px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.file-name-col a { - display: inline-block; - max-width: calc(100% - 30px); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - vertical-align: middle; -} - -.file-size-col { - width: 100px; - min-width: 100px; - white-space: nowrap; -} - -.last-modified-col { - width: 180px; - min-width: 180px; - white-space: nowrap; -} - -.actions-col { - white-space: nowrap; -} - -.actions-col > * { - display: inline-flex; - align-items: center; - gap: 4px; - margin-left: 4px; -} - -@media (max-width: 1200px) { - .files-table .action-text { - display: none; - } - .files-table .action-link { - padding: 6px 8px; - } -} - -@media (max-width: 992px) { - .last-modified-col { - display: none !important; - } - .file-name-col { - max-width: 200px; - } -} - -@media (max-width: 768px) { - .file-size-col { - display: none !important; - } - .file-name-col { - max-width: 150px; - } - - .files-table .action-text { - display: none; - } - - .files-table .action-link, - .files-table button.action-link { - padding: 6px; - font-size: 1em; - margin-left: 2px; - } - - .files-table td, - .files-table th { - padding: 8px 4px; - } - - .actions-col > * { - margin-left: 2px; - gap: 2px; - } -} - -@media (max-width: 480px) { - body { - padding: 10px; - } - - .container { - padding: 12px; - width: calc(100% - 20px); - } - - h1 { - margin-bottom: 20px; - padding-bottom: 8px; - justify-content: flex-start; - } - - h1 > div { - width: 100%; - justify-content: flex-start; - } - - .files-table td, - .files-table th { - padding: 6px 2px; - font-size: 0.9em; - } - - .checkbox-col { - width: 30px; - } - - .file-name-col { - max-width: 120px; - font-size: 0.85em; - } - - .files-table .action-link, - .files-table button.action-link { - padding: 4px; - font-size: 0.9em; - margin-left: 1px; - } - - .actions-col > * { - margin-left: 1px; - gap: 1px; - } -} - font-size: 1.1em; - } - - .files-table td, - .files-table th { - padding: 8px 4px; - } -} diff --git a/static/css/modals.css b/static/css/modals.css new file mode 100644 index 0000000..74822b3 --- /dev/null +++ b/static/css/modals.css @@ -0,0 +1,339 @@ +/** + * 预览模态框和对话框样式 + */ + +/* 预览模态框 */ +.preview-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; +} + +.preview-modal.show { + display: flex; + opacity: 1; + justify-content: center; + align-items: center; +} + +.preview-container { + position: relative; + max-width: 90%; + max-height: 90%; + display: flex; + flex-direction: column; + align-items: center; +} + +.preview-content { + max-width: 100%; + max-height: 85vh; + object-fit: contain; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); +} + +video.preview-content { + background-color: #000; +} + +.preview-controls { + position: absolute; + top: 20px; + right: 20px; + display: flex; + gap: 10px; + z-index: 1001; +} + +.preview-btn { + background-color: rgba(255, 255, 255, 0.9); + border: none; + color: #333; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + font-size: 1.2em; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.preview-btn:hover { + background-color: white; + transform: scale(1.1); +} + +.preview-info { + margin-top: 15px; + color: white; + text-align: center; + font-size: 0.95em; + background-color: rgba(0, 0, 0, 0.7); + padding: 10px 20px; + border-radius: 20px; + max-width: 80%; + word-break: break-all; +} + +.preview-loading { + color: white; + font-size: 1.2em; + text-align: center; +} + +.preview-error { + color: #ff6b6b; + font-size: 1.1em; + text-align: center; + padding: 20px; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 8px; +} + +/* 音频预览样式 */ +.preview-audio-wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 12px; + min-width: 400px; +} + +.preview-audio { + width: 100%; + max-width: 500px; + margin-top: 20px; +} + +/* 文本预览样式 */ +.preview-text { + background-color: rgba(255, 255, 255, 0.95); + color: #2c3e50; + padding: 20px; + border-radius: 8px; + max-width: 90vw; + max-height: 85vh; + overflow: auto; + font-family: "Courier New", Courier, monospace; + font-size: 14px; + line-height: 1.6; + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; +} + +[data-theme="dark"] .preview-text { + background-color: rgba(45, 45, 45, 0.95); + color: #e1e1e1; +} + +/* 不支持预览的文件样式 */ +.preview-unsupported { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 40px; + background-color: rgba(0, 0, 0, 0.7); + border-radius: 12px; + color: white; + text-align: center; + min-width: 400px; +} + +.preview-unsupported p { + margin: 0; +} + +.preview-unsupported .action-link { + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; +} + +.preview-unsupported .action-link:hover { + background-color: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.5); +} + +@media (max-width: 768px) { + .preview-container { + max-width: 95%; + } + + .preview-controls { + top: 10px; + right: 10px; + } + + .preview-btn { + width: 35px; + height: 35px; + font-size: 1em; + } + + .preview-info { + font-size: 0.85em; + padding: 8px 15px; + } + + .preview-audio-wrapper { + min-width: 300px; + padding: 30px 20px; + } + + .preview-unsupported { + min-width: 300px; + padding: 40px 20px; + } + + .preview-text { + font-size: 12px; + padding: 15px; + } +} + +/* 自定义对话框 */ +.app-dialog[hidden] { + display: none; +} + +.app-dialog { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.35); + backdrop-filter: blur(2px); + z-index: 1100; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; +} + +.app-dialog.is-visible { + opacity: 1; + pointer-events: auto; +} + +.app-dialog__backdrop { + position: absolute; + inset: 0; +} + +.app-dialog__panel { + position: relative; + background-color: var(--container-bg); + color: var(--text-color); + border-radius: 12px; + box-shadow: 0 18px 60px rgba(0, 0, 0, 0.25); + padding: 24px; + min-width: 320px; + max-width: min(420px, 90vw); + transform: translateY(20px); + transition: transform 0.2s ease; +} + +.app-dialog.is-visible .app-dialog__panel { + transform: translateY(0); +} + +.app-dialog__title { + margin: 0 0 12px; + font-size: 1.15em; + font-weight: 600; +} + +.app-dialog__message { + margin-bottom: 18px; + line-height: 1.5; + color: var(--text-color); + word-break: break-word; +} + +.app-dialog__input { + margin-bottom: 18px; + display: block; + width: 100%; +} + +.app-dialog__input input { + width: 100%; + padding: 10px 12px; + border-radius: 6px; + border: 1px solid var(--border-color); + background-color: var(--container-bg); + color: var(--text-color); + font-size: 0.95em; + box-sizing: border-box; + font-family: inherit; +} + +.app-dialog__input input::placeholder { + color: var(--secondary-text); +} + +.app-dialog__input input:focus { + outline: none; + border-color: var(--link-color); + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15); +} + +.app-dialog__actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.app-dialog__btn { + padding: 8px 16px; + border-radius: 6px; + border: none; + cursor: pointer; + font-size: 0.95em; + transition: transform 0.15s ease, background-color 0.2s ease; +} + +.app-dialog__btn:hover { + transform: translateY(-1px); +} + +.app-dialog__cancel { + background-color: var(--hover-bg); + color: var(--secondary-text); +} + +.app-dialog__cancel:hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.app-dialog__confirm { + background-color: var(--danger-color); + color: #fff; +} + +.app-dialog__confirm:hover { + background-color: var(--danger-hover-color); +} + +@media (max-width: 480px) { + .app-dialog__panel { + width: calc(100vw - 40px); + padding: 20px; + } +} diff --git a/static/css/table.css b/static/css/table.css new file mode 100644 index 0000000..98bd08e --- /dev/null +++ b/static/css/table.css @@ -0,0 +1,197 @@ +/** + * 表格和列表视图样式 + */ + +.files-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +.checkbox-col { + width: 48px; + text-align: center; +} + +.checkbox-col input { + transform: scale(1.05); + accent-color: var(--link-color); +} + +.files-table th, +.files-table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.files-table th { + background-color: var(--hover-bg); + color: var(--text-color); + font-weight: 600; +} + +.files-table tr:hover { + background-color: var(--hover-bg); +} + +.file-name-col { + width: auto; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + user-select: none; +} + +.file-name-col a { + display: inline-block; + max-width: calc(100% - 30px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; +} + +.file-name-col:hover { + background-color: var(--hover-bg); +} + +.file-icon { + margin-right: 8px; + pointer-events: none; +} + +.folder { + color: var(--folder-color); +} + +.file { + color: var(--file-color); +} + +.file-size { + color: var(--secondary-text); + font-size: 0.9em; +} + +.file-size-col { + width: 100px; + min-width: 100px; + white-space: nowrap; +} + +.last-modified { + color: var(--secondary-text); + font-size: 0.9em; +} + +.last-modified-col { + width: 180px; + min-width: 180px; + white-space: nowrap; +} + +.actions-col { + white-space: nowrap; +} + +.actions-col > * { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: 4px; +} + +.breadcrumb { + margin-bottom: 12px; + font-size: 0.95em; + color: var(--secondary-text); + background-color: var(--container-bg); + padding: 8px 12px; + border-radius: 4px; +} + +.empty-message { + text-align: center; + padding: 40px; + color: var(--secondary-text); +} + +@media (max-width: 1200px) { + .files-table .action-text { + display: none; + } + .files-table .action-link { + padding: 6px 8px; + } +} + +@media (max-width: 992px) { + .last-modified-col { + display: none !important; + } + .file-name-col { + max-width: 200px; + } +} + +@media (max-width: 768px) { + .file-size-col { + display: none !important; + } + .file-name-col { + max-width: 150px; + } + + .files-table .action-text { + display: none; + } + + .files-table .action-link, + .files-table button.action-link { + padding: 6px; + font-size: 1em; + margin-left: 2px; + } + + .files-table td, + .files-table th { + padding: 8px 4px; + } + + .actions-col > * { + margin-left: 2px; + gap: 2px; + } +} + +@media (max-width: 480px) { + .files-table td, + .files-table th { + padding: 6px 2px; + font-size: 0.9em; + } + + .checkbox-col { + width: 30px; + } + + .file-name-col { + max-width: 120px; + font-size: 0.85em; + } + + .files-table .action-link, + .files-table button.action-link { + padding: 4px; + font-size: 0.9em; + margin-left: 1px; + } + + .actions-col > * { + margin-left: 1px; + gap: 1px; + } +} diff --git a/static/css/utilities.css b/static/css/utilities.css new file mode 100644 index 0000000..f51bbe5 --- /dev/null +++ b/static/css/utilities.css @@ -0,0 +1,85 @@ +/** + * 状态消息和其他实用样式 + */ + +/* 上传进度提示 */ +.upload-status { + padding: 10px; + border-radius: 4px; + margin-bottom: 10px; + display: none; +} + +.upload-status.success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.upload-status.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + +/* 页脚样式 */ +.site-footer { + margin-top: 20px; + text-align: center; + color: var(--secondary-text); + font-size: 0.9em; + padding: 14px 12px; +} + +.site-footer a { + color: var(--link-color); + text-decoration: none; + margin: 0 6px; +} + +.site-footer a:hover { + text-decoration: underline; +} + +/* 上传输入框 */ +.upload-input { + display: none; +} + +/* 禁用状态 */ +.is-disabled { + opacity: 0.6; + cursor: not-allowed !important; + pointer-events: none; +} + +@media (max-width: 600px) { + .site-footer { + font-size: 0.85em; + padding: 12px 8px; + } +} + +@media (max-width: 480px) { + body { + padding: 10px; + } + + .container { + padding: 12px; + width: calc(100% - 20px); + } + + h1 { + margin-bottom: 20px; + padding-bottom: 8px; + justify-content: flex-start; + } + + h1 > div { + width: 100%; + justify-content: flex-start; + } +} diff --git a/static/css/variables.css b/static/css/variables.css new file mode 100644 index 0000000..56206ee --- /dev/null +++ b/static/css/variables.css @@ -0,0 +1,95 @@ +/** + * CSS 变量和主题定义 + */ + +:root { + --bg-color: #f5f5f5; + --container-bg: white; + --text-color: #2c3e50; + --border-color: #eee; + --hover-bg: #f8f9fa; + --secondary-text: #6c757d; + --link-color: #007bff; + --folder-color: #ffc107; + --file-color: #6c757d; + --shadow-color: rgba(0, 0, 0, 0.1); + --icon-bg: #f0f0f0; + --danger-color: #dc3545; + --danger-hover-color: #a0202d; + --warning-color: #ffc107; + --warning-hover-color: #e0a800; + --success-color: #28a745; +} + +[data-theme="dark"] { + --bg-color: #1a1a1a; + --container-bg: #2d2d2d; + --text-color: #e1e1e1; + --border-color: #404040; + --hover-bg: #363636; + --secondary-text: #a0a0a0; + --link-color: #66b3ff; + --folder-color: #ffd54f; + --file-color: #b0b0b0; + --shadow-color: rgba(0, 0, 0, 0.3); + --icon-bg: #3a3a3a; +} + +/** + * 基础样式 + */ + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + padding: 20px; + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.3s ease, color 0.3s ease; + margin: 0; +} + +.container { + background-color: var(--container-bg); + border-radius: 8px; + box-shadow: 0 2px 4px var(--shadow-color); + padding: 20px; + width: calc(100% - 40px); + max-width: 1200px; + margin: 0 auto; +} + +h1 { + color: var(--text-color); + margin-bottom: 30px; + border-bottom: 2px solid var(--border-color); + padding-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} + +h1 > div { + display: flex; + gap: 8px; + align-items: center; +} + +a { + color: var(--link-color); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} diff --git a/static/js/dialog.js b/static/js/dialog.js new file mode 100644 index 0000000..2011611 --- /dev/null +++ b/static/js/dialog.js @@ -0,0 +1,235 @@ +/** + * 对话框(Dialog)相关功能 + * 包括确认、输入、提示等对话框 + */ + +const dialogState = { + container: null, + title: null, + message: null, + inputWrapper: null, + input: null, + confirmBtn: null, + cancelBtn: null, + resolve: null, + options: null, + previousActiveElement: null, +}; + +/** + * 检查对话框是否打开 + * @returns {boolean} + */ +function isDialogOpen() { + return Boolean(dialogState.container && !dialogState.container.hasAttribute("hidden")); +} + +/** + * 关闭对话框 + * @param {boolean} confirmed - 是否确认 + */ +function closeDialog(confirmed) { + if (!dialogState.resolve || !dialogState.container) { + return; + } + + const showInput = dialogState.options?.showInput; + const value = confirmed && showInput && dialogState.input ? dialogState.input.value : undefined; + + dialogState.container.classList.remove("is-visible"); + dialogState.container.setAttribute("aria-hidden", "true"); + + window.setTimeout(() => { + if (!dialogState.container.classList.contains("is-visible")) { + dialogState.container.setAttribute("hidden", ""); + } + }, 200); + + if (dialogState.inputWrapper) { + dialogState.inputWrapper.hidden = true; + } + + const resolve = dialogState.resolve; + dialogState.resolve = null; + const options = dialogState.options || {}; + dialogState.options = null; + + if (dialogState.previousActiveElement && typeof dialogState.previousActiveElement.focus === "function") { + dialogState.previousActiveElement.focus({ preventScroll: true }); + } + dialogState.previousActiveElement = null; + + resolve({ + confirmed, + value: value !== undefined ? value : undefined, + options, + }); +} + +/** + * 打开对话框 + * @param {object} options - 对话框选项 + * @returns {Promise} + */ +function openDialog(options) { + if (!dialogState.container) { + return Promise.resolve({ confirmed: false }); + } + + return new Promise((resolve) => { + dialogState.resolve = resolve; + dialogState.options = options; + dialogState.previousActiveElement = + document.activeElement instanceof HTMLElement ? document.activeElement : null; + + dialogState.container.removeAttribute("hidden"); + dialogState.container.setAttribute("aria-hidden", "false"); + + if (dialogState.title) { + dialogState.title.textContent = options.title || ""; + dialogState.title.hidden = !options.title; + } + + if (dialogState.message) { + dialogState.message.textContent = options.message || ""; + } + + if (dialogState.inputWrapper && dialogState.input) { + dialogState.inputWrapper.hidden = !options.showInput; + dialogState.input.value = options.defaultValue || ""; + dialogState.input.placeholder = options.placeholder || ""; + } + + if (dialogState.confirmBtn) { + dialogState.confirmBtn.textContent = options.confirmLabel || "确定"; + } + + if (dialogState.cancelBtn) { + dialogState.cancelBtn.textContent = options.cancelLabel || "取消"; + dialogState.cancelBtn.hidden = options.hideCancel || false; + } + + window.requestAnimationFrame(() => { + if (!dialogState.container) { + return; + } + dialogState.container.classList.add("is-visible"); + + if (options.showInput && dialogState.input) { + dialogState.input.focus(); + dialogState.input.select(); + } else if (dialogState.confirmBtn) { + dialogState.confirmBtn.focus(); + } + }); + }); +} + +/** + * 初始化对话框 + */ +function initDialog() { + const container = document.getElementById("appDialog"); + if (!container || container.dataset.initialized === "true") { + return; + } + + container.dataset.initialized = "true"; + dialogState.container = container; + dialogState.title = document.getElementById("appDialogTitle"); + dialogState.message = document.getElementById("appDialogMessage"); + dialogState.inputWrapper = document.getElementById("appDialogInputWrapper"); + dialogState.input = document.getElementById("appDialogInput"); + dialogState.confirmBtn = document.getElementById("appDialogConfirm"); + dialogState.cancelBtn = document.getElementById("appDialogCancel"); + + if (dialogState.confirmBtn) { + dialogState.confirmBtn.addEventListener("click", () => closeDialog(true)); + } + + if (dialogState.cancelBtn) { + dialogState.cancelBtn.addEventListener("click", () => closeDialog(false)); + } + + container.addEventListener("click", (event) => { + if ( + event.target === container || + (event.target instanceof HTMLElement && event.target.dataset.dialogDismiss === "true") + ) { + closeDialog(false); + } + }); + + document.addEventListener("keydown", (event) => { + if (!isDialogOpen()) { + return; + } + + if (event.key === "Escape") { + event.preventDefault(); + closeDialog(false); + return; + } + + if (event.key === "Enter" && dialogState.options?.showInput) { + const active = document.activeElement; + if (active === dialogState.input) { + event.preventDefault(); + closeDialog(true); + } + } + }); +} + +/** + * 显示确认对话框 + * @param {string} message - 消息 + * @param {object} options - 选项 + * @returns {Promise} + */ +function showConfirm(message, options = {}) { + return openDialog({ + title: options.title || "确认操作", + message, + confirmLabel: options.confirmLabel || "确定", + cancelLabel: options.cancelLabel || "取消", + hideCancel: options.hideCancel || false, + }).then((result) => Boolean(result.confirmed)); +} + +/** + * 显示输入对话框 + * @param {string} message - 消息 + * @param {object} options - 选项 + * @returns {Promise} + */ +async function showPrompt(message, options = {}) { + const result = await openDialog({ + title: options.title || "请输入内容", + message, + confirmLabel: options.confirmLabel || "确定", + cancelLabel: options.cancelLabel || "取消", + showInput: true, + defaultValue: options.defaultValue || "", + placeholder: options.placeholder || "", + }); + + if (!result.confirmed) { + return null; + } + + const value = typeof result.value === "string" ? result.value.trim() : ""; + return value === "" ? null : value; +} + +/** + * 导出到全局作用域 + */ +window.DialogUtils = { + isDialogOpen, + closeDialog, + openDialog, + initDialog, + showConfirm, + showPrompt, +}; diff --git a/static/js/download.js b/static/js/download.js new file mode 100644 index 0000000..ecd6887 --- /dev/null +++ b/static/js/download.js @@ -0,0 +1,27 @@ +/** + * 下载按钮事件处理 + */ + +/** + * 绑定下载按钮监听器 + */ +function attachDownloadButtonListeners() { + const downloadButtons = document.querySelectorAll("[data-download-key]"); + downloadButtons.forEach((button) => { + if (!button.dataset.listenerAttached) { + button.addEventListener("click", () => { + const key = button.dataset.downloadKey; + const name = button.dataset.downloadName; + downloadFile(`/download/${key}`, name); + }); + button.dataset.listenerAttached = "true"; + } + }); +} + +/** + * 导出到全局作用域 + */ +window.DownloadUtils = { + attachDownloadButtonListeners, +}; diff --git a/static/js/file-operations.js b/static/js/file-operations.js new file mode 100644 index 0000000..bc71eb9 --- /dev/null +++ b/static/js/file-operations.js @@ -0,0 +1,408 @@ +/** + * 文件操作相关功能 + * 包括上传、下载、删除、重命名等 + */ + +/** + * 上传文件 + * @param {FileList} files - 文件列表 + */ +async function uploadFiles(files) { + const currentPrefix = document.body.dataset.currentPrefix || ""; + + for (const file of files) { + const formData = new FormData(); + formData.append("file", file); + formData.append("prefix", currentPrefix); + + try { + updateStatus(`正在上传: ${file.name}...`, null); + + const response = await fetch("/upload", { + method: "POST", + body: formData, + }); + + const result = await response.json(); + + if (result.success) { + const statusDiv = updateStatus(`✓ ${file.name} 上传成功!`, "success"); + hideStatusLater(statusDiv); + + setTimeout(() => { + window.location.reload(); + }, 2000); + } else { + updateStatus(`✗ ${file.name} 上传失败: ${result.error}`, "error"); + } + } catch (error) { + updateStatus(`✗ ${file.name} 上传失败: ${error.message}`, "error"); + } + } +} + +/** + * 提示删除文件 + */ +async function promptDelete() { + const suggested = ""; + const path = await showPrompt("请输入要删除的文件路径(相对于存储桶),例如:folder/file.jpg", { + title: "删除文件", + defaultValue: suggested, + placeholder: "folder/file.jpg", + confirmLabel: "删除", + }); + + if (path) { + await deleteFile(path); + } +} + +/** + * 删除文件夹 + * @param {string} prefix - 文件夹前缀 + */ +async function deleteFolder(prefix) { + const confirmed = await showConfirm(`确定要删除文件夹 "${prefix}" 及其所有内容吗?此操作不可逆!`, { + title: "删除文件夹", + confirmLabel: "确认删除", + }); + + if (!confirmed) { + return; + } + + updateStatus(`正在删除文件夹: ${prefix}...`, null); + + try { + const response = await fetch(`/delete_folder/${prefix}`, { + method: "DELETE", + }); + + const result = await response.json(); + + if (result.success) { + const statusDiv = updateStatus("✓ 文件夹删除成功!", "success"); + hideStatusLater(statusDiv); + + setTimeout(() => { + const parentPath = prefix.split("/").slice(0, -2).join("/"); + window.location.href = parentPath ? `/${parentPath}` : "/"; + }, 1500); + } else { + updateStatus(`✗ 删除失败: ${result.error}`, "error"); + } + } catch (error) { + updateStatus(`✗ 删除失败: ${error.message}`, "error"); + } +} + +/** + * 删除单个文件 + * @param {string} filePath - 文件路径 + * @param {object} options - 选项 + * @returns {Promise} + */ +async function deleteFile(filePath, options = {}) { + const { skipConfirm = false, suppressReload = false, suppressStatus = false } = options; + + if (!skipConfirm) { + const confirmed = await showConfirm(`确定要删除 "${filePath}" 吗?`, { + title: "删除文件", + confirmLabel: "删除", + }); + + if (!confirmed) { + return false; + } + } + + if (!suppressStatus) { + updateStatus(`正在删除: ${filePath}...`, null); + } + + try { + const response = await fetch(`/delete/${filePath}`, { + method: "DELETE", + }); + + const result = await response.json(); + + if (result.success) { + if (!suppressStatus) { + const statusDiv = updateStatus("✓ 文件删除成功!", "success"); + hideStatusLater(statusDiv); + } + + if (!suppressReload) { + setTimeout(() => { + window.location.reload(); + }, 2000); + } + + return true; + } + + if (!suppressStatus) { + updateStatus(`✗ 删除失败: ${result.error}`, "error"); + } + return false; + } catch (error) { + if (!suppressStatus) { + updateStatus(`✗ 删除失败: ${error.message}`, "error"); + } + return false; + } +} + +/** + * 下载文件 + * @param {string} url - 下载 URL + * @param {string} filename - 文件名 + */ +function downloadFile(url, filename) { + if (!url) { + updateStatus("✗ 无法下载:缺少下载链接", "error"); + return; + } + + if (url.startsWith("/download/")) { + fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.blob(); + }) + .then((blob) => { + const link = document.createElement("a"); + const blobUrl = URL.createObjectURL(blob); + link.href = blobUrl; + link.download = filename || "file"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(blobUrl); + + const statusDiv = updateStatus(`✓ 开始下载: ${filename || ""}`, "success"); + hideStatusLater(statusDiv); + }) + .catch((error) => { + console.error("Download error:", error); + updateStatus(`✗ 下载失败: ${error.message}`, "error"); + }); + } else { + const link = document.createElement("a"); + link.href = url; + link.download = filename || ""; + link.target = "_blank"; + link.rel = "noopener"; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + const statusDiv = updateStatus(`✓ 开始下载: ${filename || ""}`, "success"); + hideStatusLater(statusDiv); + } +} + +/** + * 重命名文件 + * @param {string} oldKey - 旧的文件键 + * @param {string} newName - 新名称 + */ +async function renameFile(oldKey, newName) { + updateStatus(`正在重命名: ${oldKey}...`, null); + + try { + const response = await fetch(`/rename/${oldKey}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ newName: newName }), + }); + + const result = await response.json(); + + if (result.success) { + const statusDiv = updateStatus("✓ 文件重命名成功!", "success"); + hideStatusLater(statusDiv); + + setTimeout(() => { + window.location.reload(); + }, 1500); + } else { + updateStatus(`✗ 重命名失败: ${result.error}`, "error"); + } + } catch (error) { + updateStatus(`✗ 重命名失败: ${error.message}`, "error"); + } +} + +/** + * 重命名文件夹 + * @param {string} oldPrefix - 旧的文件夹前缀 + * @param {string} newName - 新名称 + */ +async function renameFolder(oldPrefix, newName) { + updateStatus(`正在重命名文件夹: ${oldPrefix}...`, null); + + try { + const response = await fetch(`/rename_folder/${oldPrefix}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ newName: newName }), + }); + + const result = await response.json(); + + if (result.success) { + const statusDiv = updateStatus("✓ 文件夹重命名成功!", "success"); + hideStatusLater(statusDiv); + + setTimeout(() => { + window.location.reload(); + }, 1500); + } else { + updateStatus(`✗ 重命名失败: ${result.error}`, "error"); + } + } catch (error) { + updateStatus(`✗ 重命名失败: ${error.message}`, "error"); + } +} + +/** + * 提示重命名 + * @param {string} oldKey - 旧键 + * @param {string} oldName - 旧名称 + * @param {boolean} isFolder - 是否是文件夹 + */ +async function promptRename(oldKey, oldName, isFolder = false) { + const title = isFolder ? "重命名文件夹" : "重命名文件"; + const newName = await showPrompt(`请输入新的名称:`, { + title: title, + defaultValue: oldName, + confirmLabel: "重命名", + }); + + if (newName && newName !== oldName) { + if (isFolder) { + await renameFolder(oldKey, newName); + } else { + await renameFile(oldKey, newName); + } + } +} + +/** + * 复制项目 + * @param {string} source - 源路径 + * @param {string} destination - 目标路径 + * @param {boolean} isFolder - 是否是文件夹 + */ +async function copyItem(source, destination, isFolder) { + updateStatus(`正在复制...`, null); + await performOperation("/copy", "复制", { source, destination, is_folder: isFolder }); +} + +/** + * 移动项目 + * @param {string} source - 源路径 + * @param {string} destination - 目标路径 + * @param {boolean} isFolder - 是否是文件夹 + */ +async function moveItem(source, destination, isFolder) { + updateStatus(`正在移动...`, null); + await performOperation("/move", "移动", { source, destination, is_folder: isFolder }); +} + +/** + * 提示复制或移动 + * @param {string} source - 源路径 + * @param {boolean} isFolder - 是否是文件夹 + * @param {string} operation - 操作类型 ('copy' 或 'move') + */ +async function promptCopyOrMove(source, isFolder, operation) { + const opText = operation === "copy" ? "复制" : "移动"; + const itemText = isFolder ? "文件夹" : "文件"; + const dest = await showPrompt(`请输入目标目录路径:`, { + title: `${opText}${itemText}`, + confirmLabel: opText, + }); + + if (dest) { + let normalizedDest = dest; + if (!normalizedDest.endsWith("/")) { + normalizedDest += "/"; + } + let name = source + .split("/") + .filter((p) => p) + .pop(); + if (isFolder) { + name += "/"; + } + const fullDest = normalizedDest + name; + + if (operation === "copy") { + await copyItem(source, fullDest, isFolder); + } else { + await moveItem(source, fullDest, isFolder); + } + } +} + +/** + * 执行复制/移动操作 + * @param {string} endpoint - API 端点 + * @param {string} opText - 操作文本 + * @param {object} body - 请求体 + */ +async function performOperation(endpoint, opText, body) { + try { + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + const result = await response.json(); + + if (result.success) { + const statusDiv = updateStatus(`✓ ${opText}成功!`, "success"); + hideStatusLater(statusDiv); + + setTimeout(() => { + window.location.reload(); + }, 1500); + } else { + updateStatus(`✗ ${opText}失败: ${result.error}`, "error"); + } + } catch (error) { + updateStatus(`✗ ${opText}失败: ${error.message}`, "error"); + } +} + +/** + * 导出到全局作用域 + */ +window.FileOps = { + uploadFiles, + promptDelete, + deleteFolder, + deleteFile, + downloadFile, + renameFile, + renameFolder, + promptRename, + copyItem, + moveItem, + promptCopyOrMove, + performOperation, +}; diff --git a/static/js/main.js b/static/js/main.js deleted file mode 100644 index 8f65c35..0000000 --- a/static/js/main.js +++ /dev/null @@ -1,1099 +0,0 @@ -(function () { - function updateStatus(message, state) { - const statusDiv = document.getElementById("uploadStatus"); - if (!statusDiv) { - return null; - } - - statusDiv.textContent = message; - statusDiv.className = "upload-status" + (state ? ` ${state}` : ""); - statusDiv.style.display = "block"; - return statusDiv; - } - - function hideStatusLater(statusDiv, delay = 2000) { - if (!statusDiv) { - return; - } - setTimeout(() => { - statusDiv.style.display = "none"; - }, delay); - } - - const dialogState = { - container: null, - title: null, - message: null, - inputWrapper: null, - input: null, - confirmBtn: null, - cancelBtn: null, - resolve: null, - options: null, - previousActiveElement: null, - }; - - function isDialogOpen() { - return Boolean(dialogState.container && !dialogState.container.hasAttribute("hidden")); - } - - function closeDialog(confirmed) { - if (!dialogState.resolve || !dialogState.container) { - return; - } - - const showInput = dialogState.options?.showInput; - const value = confirmed && showInput && dialogState.input ? dialogState.input.value : undefined; - - dialogState.container.classList.remove("is-visible"); - dialogState.container.setAttribute("aria-hidden", "true"); - - window.setTimeout(() => { - if (!dialogState.container.classList.contains("is-visible")) { - dialogState.container.setAttribute("hidden", ""); - } - }, 200); - - if (dialogState.inputWrapper) { - dialogState.inputWrapper.hidden = true; - } - - const resolve = dialogState.resolve; - dialogState.resolve = null; - const options = dialogState.options || {}; - dialogState.options = null; - - if (dialogState.previousActiveElement && typeof dialogState.previousActiveElement.focus === "function") { - dialogState.previousActiveElement.focus({ preventScroll: true }); - } - dialogState.previousActiveElement = null; - - resolve({ - confirmed, - value: value !== undefined ? value : undefined, - options, - }); - } - - function openDialog(options) { - if (!dialogState.container) { - return Promise.resolve({ confirmed: false }); - } - - return new Promise((resolve) => { - dialogState.resolve = resolve; - dialogState.options = options; - dialogState.previousActiveElement = - document.activeElement instanceof HTMLElement ? document.activeElement : null; - - dialogState.container.removeAttribute("hidden"); - dialogState.container.setAttribute("aria-hidden", "false"); - - if (dialogState.title) { - dialogState.title.textContent = options.title || ""; - dialogState.title.hidden = !options.title; - } - - if (dialogState.message) { - dialogState.message.textContent = options.message || ""; - } - - if (dialogState.inputWrapper && dialogState.input) { - dialogState.inputWrapper.hidden = !options.showInput; - dialogState.input.value = options.defaultValue || ""; - dialogState.input.placeholder = options.placeholder || ""; - } - - if (dialogState.confirmBtn) { - dialogState.confirmBtn.textContent = options.confirmLabel || "确定"; - } - - if (dialogState.cancelBtn) { - dialogState.cancelBtn.textContent = options.cancelLabel || "取消"; - dialogState.cancelBtn.hidden = options.hideCancel || false; - } - - window.requestAnimationFrame(() => { - if (!dialogState.container) { - return; - } - dialogState.container.classList.add("is-visible"); - - if (options.showInput && dialogState.input) { - dialogState.input.focus(); - dialogState.input.select(); - } else if (dialogState.confirmBtn) { - dialogState.confirmBtn.focus(); - } - }); - }); - } - - function initDialog() { - const container = document.getElementById("appDialog"); - if (!container || container.dataset.initialized === "true") { - return; - } - - container.dataset.initialized = "true"; - dialogState.container = container; - dialogState.title = document.getElementById("appDialogTitle"); - dialogState.message = document.getElementById("appDialogMessage"); - dialogState.inputWrapper = document.getElementById("appDialogInputWrapper"); - dialogState.input = document.getElementById("appDialogInput"); - dialogState.confirmBtn = document.getElementById("appDialogConfirm"); - dialogState.cancelBtn = document.getElementById("appDialogCancel"); - - if (dialogState.confirmBtn) { - dialogState.confirmBtn.addEventListener("click", () => closeDialog(true)); - } - - if (dialogState.cancelBtn) { - dialogState.cancelBtn.addEventListener("click", () => closeDialog(false)); - } - - container.addEventListener("click", (event) => { - if ( - event.target === container || - (event.target instanceof HTMLElement && event.target.dataset.dialogDismiss === "true") - ) { - closeDialog(false); - } - }); - - document.addEventListener("keydown", (event) => { - if (!isDialogOpen()) { - return; - } - - if (event.key === "Escape") { - event.preventDefault(); - closeDialog(false); - return; - } - - if (event.key === "Enter" && dialogState.options?.showInput) { - const active = document.activeElement; - if (active === dialogState.input) { - event.preventDefault(); - closeDialog(true); - } - } - }); - } - - function showConfirm(message, options = {}) { - return openDialog({ - title: options.title || "确认操作", - message, - confirmLabel: options.confirmLabel || "确定", - cancelLabel: options.cancelLabel || "取消", - hideCancel: options.hideCancel || false, - }).then((result) => Boolean(result.confirmed)); - } - - async function showPrompt(message, options = {}) { - const result = await openDialog({ - title: options.title || "请输入内容", - message, - confirmLabel: options.confirmLabel || "确定", - cancelLabel: options.cancelLabel || "取消", - showInput: true, - defaultValue: options.defaultValue || "", - placeholder: options.placeholder || "", - }); - - if (!result.confirmed) { - return null; - } - - const value = typeof result.value === "string" ? result.value.trim() : ""; - return value === "" ? null : value; - } - - function initThemeAndView() { - const themeToggle = document.getElementById("themeToggle"); - const viewToggle = document.getElementById("viewToggle"); - - if (!themeToggle || !viewToggle) { - return; - } - - const themeIcon = themeToggle.querySelector("i"); - const viewIcon = viewToggle.querySelector("i"); - - const savedTheme = localStorage.getItem("theme"); - if (savedTheme === "dark") { - document.documentElement.setAttribute("data-theme", "dark"); - if (themeIcon) { - themeIcon.classList.remove("fa-moon"); - themeIcon.classList.add("fa-sun"); - } - } - - const savedView = localStorage.getItem("view") || "list"; - document.documentElement.setAttribute("data-view", savedView); - if (viewIcon) { - if (savedView === "grid") { - viewIcon.classList.remove("fa-th-large"); - viewIcon.classList.add("fa-th-list"); - } else { - viewIcon.classList.remove("fa-th-list"); - viewIcon.classList.add("fa-th-large"); - } - } - - viewToggle.addEventListener("click", () => { - const current = document.documentElement.getAttribute("data-view") || "list"; - const next = current === "grid" ? "list" : "grid"; - document.documentElement.setAttribute("data-view", next); - localStorage.setItem("view", next); - - if (!viewIcon) { - return; - } - - if (next === "grid") { - viewIcon.classList.remove("fa-th-large"); - viewIcon.classList.add("fa-th-list"); - } else { - viewIcon.classList.remove("fa-th-list"); - viewIcon.classList.add("fa-th-large"); - } - }); - - themeToggle.addEventListener("click", () => { - const currentTheme = document.documentElement.getAttribute("data-theme"); - const newTheme = currentTheme === "dark" ? "light" : "dark"; - - document.documentElement.setAttribute("data-theme", newTheme); - localStorage.setItem("theme", newTheme); - - if (!themeIcon) { - return; - } - - if (newTheme === "dark") { - themeIcon.classList.remove("fa-moon"); - themeIcon.classList.add("fa-sun"); - } else { - themeIcon.classList.remove("fa-sun"); - themeIcon.classList.add("fa-moon"); - } - }); - - if (!savedTheme) { - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - if (prefersDark) { - document.documentElement.setAttribute("data-theme", "dark"); - if (themeIcon) { - themeIcon.classList.remove("fa-moon"); - themeIcon.classList.add("fa-sun"); - } - localStorage.setItem("theme", "dark"); - } - } - } - - function unregisterServiceWorker() { - if (!("serviceWorker" in navigator)) { - return; - } - - window.addEventListener("load", () => { - navigator.serviceWorker - .getRegistrations() - .then((registrations) => { - registrations.forEach((registration) => { - registration.unregister().then(() => { - console.log("Service Worker unregistered"); - }); - }); - }) - .catch((error) => { - console.log("Error unregistering Service Worker:", error); - }); - - // 清理 Service Worker 相关的缓存 - if ("caches" in window) { - caches.keys().then((cacheNames) => { - cacheNames.forEach((cacheName) => { - caches.delete(cacheName).then(() => { - console.log("Cache deleted:", cacheName); - }); - }); - }); - } - }); - } - - function registerModalHandlers() { - const modal = document.getElementById("previewModal"); - if (!modal) { - return; - } - - modal.addEventListener("click", (event) => { - if (event.target === modal) { - closePreview(); - } - }); - - document.addEventListener("keydown", (event) => { - if (event.key === "Escape" && !isDialogOpen()) { - closePreview(); - } - }); - } - - async function uploadFiles(files) { - const currentPrefix = document.body.dataset.currentPrefix || ""; - - for (const file of files) { - const formData = new FormData(); - formData.append("file", file); - formData.append("prefix", currentPrefix); - - try { - updateStatus(`正在上传: ${file.name}...`, null); - - const response = await fetch("/upload", { - method: "POST", - body: formData, - }); - - const result = await response.json(); - - if (result.success) { - const statusDiv = updateStatus(`✓ ${file.name} 上传成功!`, "success"); - hideStatusLater(statusDiv); - - setTimeout(() => { - window.location.reload(); - }, 2000); - } else { - updateStatus(`✗ ${file.name} 上传失败: ${result.error}`, "error"); - } - } catch (error) { - updateStatus(`✗ ${file.name} 上传失败: ${error.message}`, "error"); - } - } - } - - async function promptDelete() { - const suggested = ""; - const path = await showPrompt("请输入要删除的文件路径(相对于存储桶),例如:folder/file.jpg", { - title: "删除文件", - defaultValue: suggested, - placeholder: "folder/file.jpg", - confirmLabel: "删除", - }); - - if (path) { - await deleteFile(path); - } - } - - async function deleteFolder(prefix) { - const confirmed = await showConfirm(`确定要删除文件夹 "${prefix}" 及其所有内容吗?此操作不可逆!`, { - title: "删除文件夹", - confirmLabel: "确认删除", - }); - - if (!confirmed) { - return; - } - - updateStatus(`正在删除文件夹: ${prefix}...`, null); - - try { - const response = await fetch(`/delete_folder/${prefix}`, { - method: "DELETE", - }); - - const result = await response.json(); - - if (result.success) { - const statusDiv = updateStatus("✓ 文件夹删除成功!", "success"); - hideStatusLater(statusDiv); - - setTimeout(() => { - // 返回上一级目录 - const parentPath = prefix.split("/").slice(0, -2).join("/"); - window.location.href = parentPath ? `/${parentPath}` : "/"; - }, 1500); - } else { - updateStatus(`✗ 删除失败: ${result.error}`, "error"); - } - } catch (error) { - updateStatus(`✗ 删除失败: ${error.message}`, "error"); - } - } - - async function deleteFile(filePath, options = {}) { - const { skipConfirm = false, suppressReload = false, suppressStatus = false } = options; - - if (!skipConfirm) { - const confirmed = await showConfirm(`确定要删除 "${filePath}" 吗?`, { - title: "删除文件", - confirmLabel: "删除", - }); - - if (!confirmed) { - return false; - } - } - - if (!suppressStatus) { - updateStatus(`正在删除: ${filePath}...`, null); - } - - try { - const response = await fetch(`/delete/${filePath}`, { - method: "DELETE", - }); - - const result = await response.json(); - - if (result.success) { - if (!suppressStatus) { - const statusDiv = updateStatus("✓ 文件删除成功!", "success"); - hideStatusLater(statusDiv); - } - - if (!suppressReload) { - setTimeout(() => { - window.location.reload(); - }, 2000); - } - - return true; - } - - if (!suppressStatus) { - updateStatus(`✗ 删除失败: ${result.error}`, "error"); - } - return false; - } catch (error) { - if (!suppressStatus) { - updateStatus(`✗ 删除失败: ${error.message}`, "error"); - } - return false; - } - } - - function getEntryCheckboxes() { - return document.querySelectorAll(".entry-checkbox"); - } - - function updateSelectAllState() { - const master = document.getElementById("selectAll"); - if (!master) { - return; - } - - const checkboxes = Array.from(getEntryCheckboxes()); - if (checkboxes.length === 0) { - master.checked = false; - master.indeterminate = false; - return; - } - - const checkedCount = checkboxes.filter((checkbox) => checkbox.checked).length; - - if (checkedCount === 0) { - master.checked = false; - master.indeterminate = false; - } else if (checkedCount === checkboxes.length) { - master.checked = true; - master.indeterminate = false; - } else { - master.checked = false; - master.indeterminate = true; - } - } - - function toggleSelectAll(master) { - const checkboxes = getEntryCheckboxes(); - const desiredState = Boolean(master.checked); - - checkboxes.forEach((checkbox) => { - checkbox.checked = desiredState; - }); - - master.indeterminate = false; - updateSelectAllState(); - } - - function attachEntryCheckboxListeners() { - const master = document.getElementById("selectAll"); - if (master && !master.dataset.listenerAttached) { - master.addEventListener("change", () => toggleSelectAll(master)); - master.dataset.listenerAttached = "true"; - } - - getEntryCheckboxes().forEach((checkbox) => { - if (!checkbox.dataset.listenerAttached) { - checkbox.addEventListener("change", updateSelectAllState); - checkbox.dataset.listenerAttached = "true"; - } - }); - - updateSelectAllState(); - } - - function attachDownloadButtonListeners() { - const downloadButtons = document.querySelectorAll("[data-download-key]"); - downloadButtons.forEach((button) => { - if (!button.dataset.listenerAttached) { - button.addEventListener("click", () => { - const key = button.dataset.downloadKey; - const name = button.dataset.downloadName; - downloadFile(`/download/${key}`, name); - }); - button.dataset.listenerAttached = "true"; - } - }); - } - - function downloadFile(url, filename) { - if (!url) { - updateStatus("✗ 无法下载:缺少下载链接", "error"); - return; - } - - // 对于 /download/ 路径,使用 fetch 以更好地处理大文件和错误 - if (url.startsWith("/download/")) { - fetch(url) - .then((response) => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.blob(); - }) - .then((blob) => { - const link = document.createElement("a"); - const blobUrl = URL.createObjectURL(blob); - link.href = blobUrl; - link.download = filename || "file"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(blobUrl); - - const statusDiv = updateStatus(`✓ 开始下载: ${filename || ""}`, "success"); - hideStatusLater(statusDiv); - }) - .catch((error) => { - console.error("Download error:", error); - updateStatus(`✗ 下载失败: ${error.message}`, "error"); - }); - } else { - // 对于外部 URL,使用传统方法 - const link = document.createElement("a"); - link.href = url; - link.download = filename || ""; - link.target = "_blank"; - link.rel = "noopener"; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - const statusDiv = updateStatus(`✓ 开始下载: ${filename || ""}`, "success"); - hideStatusLater(statusDiv); - } - } - - async function deleteSelectedEntries() { - const selected = Array.from(getEntryCheckboxes()).filter((checkbox) => checkbox.checked); - - if (selected.length === 0) { - const statusDiv = updateStatus("✗ 请先选择要删除的项目", "error"); - hideStatusLater(statusDiv); - return; - } - - const directories = selected.filter((checkbox) => checkbox.dataset.type === "dir"); - if (directories.length > 0) { - const statusDiv = updateStatus("✗ 暂不支持批量删除文件夹,请仅选择文件", "error"); - hideStatusLater(statusDiv); - return; - } - - const files = selected.map((checkbox) => checkbox.value); - const confirmMessage = - files.length === 1 ? `确定要删除 "${files[0]}" 吗?` : `确定要删除选中的 ${files.length} 个文件吗?`; - - const confirmed = await showConfirm(confirmMessage, { - title: "批量删除", - confirmLabel: "删除", - }); - - if (!confirmed) { - return; - } - - const deleteButton = document.getElementById("deleteTrigger"); - if (deleteButton) { - deleteButton.disabled = true; - deleteButton.classList.add("is-disabled"); - } - - const inProgressStatus = updateStatus(`正在删除 ${files.length} 个文件...`, null); - - const failures = []; - let successCount = 0; - - for (const filePath of files) { - // 跳过额外提示和页面刷新,在批量完成后统一处理 - const result = await deleteFile(filePath, { - skipConfirm: true, - suppressReload: true, - suppressStatus: true, - }); - - if (result) { - successCount += 1; - } else { - failures.push(filePath); - } - } - - if (deleteButton) { - deleteButton.disabled = false; - deleteButton.classList.remove("is-disabled"); - } - - if (inProgressStatus) { - inProgressStatus.style.display = "none"; - } - - if (failures.length === 0 && successCount > 0) { - const statusDiv = updateStatus(`✓ 已删除 ${successCount} 个文件`, "success"); - hideStatusLater(statusDiv, 3000); - - setTimeout(() => { - window.location.reload(); - }, 1500); - return; - } - - if (failures.length > 0) { - const message = - failures.length === files.length - ? "✗ 删除失败,请稍后重试" - : `删除部分文件失败:${failures.join(", ")}`; - const statusDiv = updateStatus(message, "error"); - hideStatusLater(statusDiv, 4000); - - if (successCount > 0) { - setTimeout(() => { - window.location.reload(); - }, 1500); - } - } - } - - async function promptRename(oldKey, oldName, isFolder = false) { - const title = isFolder ? "重命名文件夹" : "重命名文件"; - const newName = await showPrompt(`请输入新的名称:`, { - title: title, - defaultValue: oldName, - confirmLabel: "重命名", - }); - - if (newName && newName !== oldName) { - if (isFolder) { - await renameFolder(oldKey, newName); - } else { - await renameFile(oldKey, newName); - } - } - } - - async function renameFile(oldKey, newName) { - updateStatus(`正在重命名: ${oldKey}...`, null); - - try { - const response = await fetch(`/rename/${oldKey}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ newName: newName }), - }); - - const result = await response.json(); - - if (result.success) { - const statusDiv = updateStatus("✓ 文件重命名成功!", "success"); - hideStatusLater(statusDiv); - - setTimeout(() => { - window.location.reload(); - }, 1500); - } else { - updateStatus(`✗ 重命名失败: ${result.error}`, "error"); - } - } catch (error) { - updateStatus(`✗ 重命名失败: ${error.message}`, "error"); - } - } - - async function renameFolder(oldPrefix, newName) { - updateStatus(`正在重命名文件夹: ${oldPrefix}...`, null); - - try { - const response = await fetch(`/rename_folder/${oldPrefix}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ newName: newName }), - }); - - const result = await response.json(); - - if (result.success) { - const statusDiv = updateStatus("✓ 文件夹重命名成功!", "success"); - hideStatusLater(statusDiv); - - setTimeout(() => { - window.location.reload(); - }, 1500); - } else { - updateStatus(`✗ 重命名失败: ${result.error}`, "error"); - } - } catch (error) { - updateStatus(`✗ 重命名失败: ${error.message}`, "error"); - } - } - - async function promptCopyOrMove(source, isFolder, operation) { - const opText = operation === "copy" ? "复制" : "移动"; - const itemText = isFolder ? "文件夹" : "文件"; - const dest = await showPrompt(`请输入目标目录路径:`, { - title: `${opText}${itemText}`, - confirmLabel: opText, - }); - - if (dest) { - // 确保 dest 以 / 结尾 - let normalizedDest = dest; - if (!normalizedDest.endsWith("/")) { - normalizedDest += "/"; - } - // 提取源名称 - let name = source - .split("/") - .filter((p) => p) - .pop(); - if (isFolder) { - name += "/"; - } - const fullDest = normalizedDest + name; - - if (operation === "copy") { - await copyItem(source, fullDest, isFolder); - } else { - await moveItem(source, fullDest, isFolder); - } - } - } - - async function copyItem(source, destination, isFolder) { - updateStatus(`正在复制...`, null); - await performOperation("/copy", "复制", { source, destination, is_folder: isFolder }); - } - - async function moveItem(source, destination, isFolder) { - updateStatus(`正在移动...`, null); - await performOperation("/move", "移动", { source, destination, is_folder: isFolder }); - } - - async function performOperation(endpoint, opText, body) { - try { - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - - const result = await response.json(); - - if (result.success) { - const statusDiv = updateStatus(`✓ ${opText}成功!`, "success"); - hideStatusLater(statusDiv); - - setTimeout(() => { - window.location.reload(); - }, 1500); - } else { - updateStatus(`✗ ${opText}失败: ${result.error}`, "error"); - } - } catch (error) { - updateStatus(`✗ ${opText}失败: ${error.message}`, "error"); - } - } - - function getFileType(filename) { - const extension = filename.toLowerCase().split(".").pop(); - const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "ico"]; - const videoExtensions = ["mp4", "webm", "ogg", "mov", "avi", "mkv", "m4v"]; - const audioExtensions = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus", "weba"]; - const pdfExtensions = ["pdf"]; - const textExtensions = [ - "txt", - "log", - "md", - "json", - "xml", - "csv", - "js", - "css", - "html", - "py", - "java", - "c", - "cpp", - "h", - "hpp", - "sh", - "bat", - "yaml", - "yml", - "toml", - "ini", - "conf", - ]; - - if (imageExtensions.includes(extension)) { - return "image"; - } - if (videoExtensions.includes(extension)) { - return "video"; - } - if (audioExtensions.includes(extension)) { - return "audio"; - } - if (pdfExtensions.includes(extension)) { - return "pdf"; - } - if (textExtensions.includes(extension)) { - return "text"; - } - return "unsupported"; - } - - function openPreview(url, filename) { - const modal = document.getElementById("previewModal"); - const container = document.getElementById("previewContainer"); - const info = document.getElementById("previewInfo"); - - if (!modal || !container || !info) { - window.open(url, "_blank"); - return; - } - - const fileType = getFileType(filename); - - // 对于不支持预览的文件类型,显示提示信息 - if (fileType === "unsupported") { - container.innerHTML = ` -
- -

该文件不支持预览

-

文件名: ${filename}

-
- - -
-
- `; - info.textContent = filename; - modal.classList.add("show"); - document.body.style.overflow = "hidden"; - return; - } - - container.innerHTML = '
加载中...
'; - info.textContent = filename; - modal.classList.add("show"); - document.body.style.overflow = "hidden"; - - setTimeout(() => { - if (fileType === "image") { - const image = document.createElement("img"); - image.className = "preview-content"; - image.src = url; - image.alt = filename; - - image.onload = () => { - container.innerHTML = ""; - container.appendChild(image); - }; - - image.onerror = () => { - container.innerHTML = '
图片加载失败
'; - }; - } else if (fileType === "video") { - const video = document.createElement("video"); - video.className = "preview-content"; - video.src = url; - video.controls = true; - video.autoplay = false; - - video.onloadedmetadata = () => { - container.innerHTML = ""; - container.appendChild(video); - }; - - video.onerror = () => { - container.innerHTML = '
视频加载失败
'; - }; - } else if (fileType === "audio") { - const audioWrapper = document.createElement("div"); - audioWrapper.className = "preview-audio-wrapper"; - audioWrapper.innerHTML = ` - - - `; - container.innerHTML = ""; - container.appendChild(audioWrapper); - } else if (fileType === "pdf") { - const iframe = document.createElement("iframe"); - iframe.className = "preview-content"; - iframe.src = url; - iframe.style.width = "100%"; - iframe.style.height = "100%"; - iframe.style.border = "none"; - - container.innerHTML = ""; - container.appendChild(iframe); - } else if (fileType === "text") { - container.innerHTML = '
加载文本内容...
'; - - fetch(url) - .then((response) => { - if (!response.ok) throw new Error("加载失败"); - return response.text(); - }) - .then((text) => { - const pre = document.createElement("pre"); - pre.className = "preview-text"; - pre.textContent = text; - container.innerHTML = ""; - container.appendChild(pre); - }) - .catch((error) => { - container.innerHTML = '
文本加载失败: ' + error.message + "
"; - }); - } - }, 100); - } - - function closePreview() { - const modal = document.getElementById("previewModal"); - const container = document.getElementById("previewContainer"); - - if (!modal || !container) { - return; - } - - modal.classList.remove("show"); - document.body.style.overflow = ""; - - setTimeout(() => { - container.innerHTML = ""; - }, 300); - } - - function downloadPreview() { - const container = document.getElementById("previewContainer"); - const info = document.getElementById("previewInfo"); - - if (!container || !info) { - return; - } - - const media = container.querySelector(".preview-content"); - if (media && media.src) { - downloadFile(media.src, info.textContent); - } - } - - document.addEventListener("DOMContentLoaded", () => { - initDialog(); - initThemeAndView(); - registerModalHandlers(); - unregisterServiceWorker(); - attachEntryCheckboxListeners(); - attachDownloadButtonListeners(); - }); - - window.uploadFiles = uploadFiles; - window.promptDelete = promptDelete; - window.deleteFile = deleteFile; - window.deleteSelectedEntries = deleteSelectedEntries; - window.toggleSelectAll = (master) => toggleSelectAll(master); - window.downloadFile = downloadFile; - window.openPreview = openPreview; - window.closePreview = closePreview; - window.downloadPreview = downloadPreview; - window.promptRename = promptRename; - window.deleteFolder = deleteFolder; - window.promptCopyOrMove = promptCopyOrMove; - - async function promptCreateFolder() { - const currentPrefix = document.body.dataset.currentPrefix || ""; - const folderName = await showPrompt("请输入新文件夹的名称:", { - title: "新建文件夹", - confirmLabel: "创建", - }); - - if (folderName) { - const path = currentPrefix + folderName; - await createFolder(path); - } - } - - async function createFolder(path) { - updateStatus(`正在创建文件夹: ${path}...`, null); - - try { - const response = await fetch(`/create_folder`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ path: path }), - }); - - const result = await response.json(); - - if (result.success) { - const statusDiv = updateStatus("✓ 文件夹创建成功!", "success"); - hideStatusLater(statusDiv); - - setTimeout(() => { - window.location.reload(); - }, 1500); - } else { - updateStatus(`✗ 创建失败: ${result.error}`, "error"); - } - } catch (error) { - updateStatus(`✗ 创建失败: ${error.message}`, "error"); - } - } - window.promptCreateFolder = promptCreateFolder; -})(); diff --git a/static/js/preview.js b/static/js/preview.js new file mode 100644 index 0000000..7298917 --- /dev/null +++ b/static/js/preview.js @@ -0,0 +1,189 @@ +/** + * 文件预览功能 + * 包括图片、视频、音频、PDF、文本等预览 + */ + +/** + * 获取文件类型 + * @param {string} filename - 文件名 + * @returns {string} 文件类型 + */ +function getFileType(filename) { + const extension = filename.toLowerCase().split(".").pop(); + const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "ico"]; + const videoExtensions = ["mp4", "webm", "ogg", "mov", "avi", "mkv", "m4v"]; + const audioExtensions = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus", "weba"]; + const pdfExtensions = ["pdf"]; + const textExtensions = [ + "txt", + "log", + "md", + "json", + "xml", + "csv", + "js", + "css", + "html", + "py", + "java", + "c", + "cpp", + "h", + "hpp", + "sh", + "bat", + "yaml", + "yml", + "toml", + "ini", + "conf", + ]; + + if (imageExtensions.includes(extension)) { + return "image"; + } + if (videoExtensions.includes(extension)) { + return "video"; + } + if (audioExtensions.includes(extension)) { + return "audio"; + } + if (pdfExtensions.includes(extension)) { + return "pdf"; + } + if (textExtensions.includes(extension)) { + return "text"; + } + return "unsupported"; +} + +/** + * 关闭预览 + */ +function closePreview() { + const modal = document.getElementById("previewModal"); + if (modal) { + modal.classList.remove("show"); + document.body.style.overflow = ""; + } +} + +/** + * 打开预览 + * @param {string} url - 文件 URL + * @param {string} filename - 文件名 + */ +function openPreview(url, filename) { + const modal = document.getElementById("previewModal"); + const container = document.getElementById("previewContainer"); + const info = document.getElementById("previewInfo"); + + if (!modal || !container || !info) { + window.open(url, "_blank"); + return; + } + + const fileType = getFileType(filename); + + // 对于不支持预览的文件类型,显示提示信息 + if (fileType === "unsupported") { + container.innerHTML = ` +
+ +

该文件不支持预览

+

文件名: ${filename}

+
+ + +
+
+ `; + info.textContent = filename; + modal.classList.add("show"); + document.body.style.overflow = "hidden"; + return; + } + + container.innerHTML = '
加载中...
'; + info.textContent = filename; + modal.classList.add("show"); + document.body.style.overflow = "hidden"; + + setTimeout(() => { + if (fileType === "image") { + const image = document.createElement("img"); + image.className = "preview-content"; + image.src = url; + image.alt = filename; + image.style.maxWidth = "100%"; + image.style.maxHeight = "100%"; + image.onerror = () => { + container.innerHTML = '
加载失败
'; + }; + container.innerHTML = ""; + container.appendChild(image); + } else if (fileType === "video") { + const video = document.createElement("video"); + video.className = "preview-content"; + video.controls = true; + video.style.maxWidth = "100%"; + video.style.maxHeight = "100%"; + const source = document.createElement("source"); + source.src = url; + source.type = `video/${url.split(".").pop()}`; + video.appendChild(source); + container.innerHTML = ""; + container.appendChild(video); + } else if (fileType === "audio") { + const audio = document.createElement("audio"); + audio.className = "preview-content"; + audio.controls = true; + audio.style.width = "100%"; + const source = document.createElement("source"); + source.src = url; + source.type = `audio/${url.split(".").pop()}`; + audio.appendChild(source); + container.innerHTML = ""; + container.appendChild(audio); + } else if (fileType === "pdf") { + container.innerHTML = ` + + `; + } else if (fileType === "text") { + fetch(url) + .then((response) => response.text()) + .then((text) => { + const pre = document.createElement("pre"); + pre.className = "preview-text"; + pre.textContent = text; + pre.style.margin = "0"; + pre.style.padding = "16px"; + pre.style.overflow = "auto"; + pre.style.whiteSpace = "pre-wrap"; + pre.style.wordWrap = "break-word"; + container.innerHTML = ""; + container.appendChild(pre); + }) + .catch(() => { + container.innerHTML = '
加载失败
'; + }); + } + }, 100); +} + +/** + * 导出到全局作用域 + */ +window.PreviewUtils = { + getFileType, + closePreview, + openPreview, +}; diff --git a/static/js/selection.js b/static/js/selection.js new file mode 100644 index 0000000..3cb6b70 --- /dev/null +++ b/static/js/selection.js @@ -0,0 +1,178 @@ +/** + * 文件选择和批量操作相关功能 + */ + +/** + * 获取所有条目复选框 + * @returns {NodeListOf} + */ +function getEntryCheckboxes() { + return document.querySelectorAll(".entry-checkbox"); +} + +/** + * 更新全选状态 + */ +function updateSelectAllState() { + const master = document.getElementById("selectAll"); + if (!master) { + return; + } + + const checkboxes = Array.from(getEntryCheckboxes()); + if (checkboxes.length === 0) { + master.checked = false; + master.indeterminate = false; + return; + } + + const checkedCount = checkboxes.filter((checkbox) => checkbox.checked).length; + + if (checkedCount === 0) { + master.checked = false; + master.indeterminate = false; + } else if (checkedCount === checkboxes.length) { + master.checked = true; + master.indeterminate = false; + } else { + master.checked = false; + master.indeterminate = true; + } +} + +/** + * 切换全选 + * @param {HTMLInputElement} master - 主复选框 + */ +function toggleSelectAll(master) { + const checkboxes = getEntryCheckboxes(); + const desiredState = Boolean(master.checked); + + checkboxes.forEach((checkbox) => { + checkbox.checked = desiredState; + }); + + master.indeterminate = false; + updateSelectAllState(); +} + +/** + * 绑定复选框监听器 + */ +function attachEntryCheckboxListeners() { + const master = document.getElementById("selectAll"); + if (master && !master.dataset.listenerAttached) { + master.addEventListener("change", () => toggleSelectAll(master)); + master.dataset.listenerAttached = "true"; + } + + getEntryCheckboxes().forEach((checkbox) => { + if (!checkbox.dataset.listenerAttached) { + checkbox.addEventListener("change", updateSelectAllState); + checkbox.dataset.listenerAttached = "true"; + } + }); + + updateSelectAllState(); +} + +/** + * 删除选定的条目 + */ +async function deleteSelectedEntries() { + const selected = Array.from(getEntryCheckboxes()).filter((checkbox) => checkbox.checked); + + if (selected.length === 0) { + const statusDiv = updateStatus("✗ 请先选择要删除的项目", "error"); + hideStatusLater(statusDiv); + return; + } + + const directories = selected.filter((checkbox) => checkbox.dataset.type === "dir"); + if (directories.length > 0) { + const statusDiv = updateStatus("✗ 暂不支持批量删除文件夹,请仅选择文件", "error"); + hideStatusLater(statusDiv); + return; + } + + const files = selected.map((checkbox) => checkbox.value); + const confirmMessage = + files.length === 1 ? `确定要删除 "${files[0]}" 吗?` : `确定要删除选中的 ${files.length} 个文件吗?`; + + const confirmed = await showConfirm(confirmMessage, { + title: "批量删除", + confirmLabel: "删除", + }); + + if (!confirmed) { + return; + } + + const deleteButton = document.getElementById("deleteTrigger"); + if (deleteButton) { + deleteButton.disabled = true; + deleteButton.classList.add("is-disabled"); + } + + const inProgressStatus = updateStatus(`正在删除 ${files.length} 个文件...`, null); + + const failures = []; + let successCount = 0; + + for (const filePath of files) { + const result = await deleteFile(filePath, { + skipConfirm: true, + suppressReload: true, + suppressStatus: true, + }); + + if (result) { + successCount += 1; + } else { + failures.push(filePath); + } + } + + if (deleteButton) { + deleteButton.disabled = false; + deleteButton.classList.remove("is-disabled"); + } + + if (inProgressStatus) { + inProgressStatus.style.display = "none"; + } + + if (failures.length === 0 && successCount > 0) { + const statusDiv = updateStatus(`✓ 已删除 ${successCount} 个文件`, "success"); + hideStatusLater(statusDiv, 3000); + + setTimeout(() => { + window.location.reload(); + }, 1500); + return; + } + + if (failures.length > 0) { + const message = + failures.length === files.length ? "✗ 删除失败,请稍后重试" : `删除部分文件失败:${failures.join(", ")}`; + const statusDiv = updateStatus(message, "error"); + hideStatusLater(statusDiv, 4000); + + if (successCount > 0) { + setTimeout(() => { + window.location.reload(); + }, 1500); + } + } +} + +/** + * 导出到全局作用域 + */ +window.SelectionUtils = { + getEntryCheckboxes, + updateSelectAllState, + toggleSelectAll, + attachEntryCheckboxListeners, + deleteSelectedEntries, +}; diff --git a/static/js/theme.js b/static/js/theme.js new file mode 100644 index 0000000..c680732 --- /dev/null +++ b/static/js/theme.js @@ -0,0 +1,98 @@ +/** + * 主题和视图切换功能 + * 包括深色/浅色主题切换、列表/网格视图切换 + */ + +/** + * 初始化主题和视图 + */ +function initThemeAndView() { + const themeToggle = document.getElementById("themeToggle"); + const viewToggle = document.getElementById("viewToggle"); + + if (!themeToggle || !viewToggle) { + return; + } + + const themeIcon = themeToggle.querySelector("i"); + const viewIcon = viewToggle.querySelector("i"); + + const savedTheme = localStorage.getItem("theme"); + if (savedTheme === "dark") { + document.documentElement.setAttribute("data-theme", "dark"); + if (themeIcon) { + themeIcon.classList.remove("fa-moon"); + themeIcon.classList.add("fa-sun"); + } + } + + const savedView = localStorage.getItem("view") || "list"; + document.documentElement.setAttribute("data-view", savedView); + if (viewIcon) { + if (savedView === "grid") { + viewIcon.classList.remove("fa-th-large"); + viewIcon.classList.add("fa-th-list"); + } else { + viewIcon.classList.remove("fa-th-list"); + viewIcon.classList.add("fa-th-large"); + } + } + + viewToggle.addEventListener("click", () => { + const current = document.documentElement.getAttribute("data-view") || "list"; + const next = current === "grid" ? "list" : "grid"; + document.documentElement.setAttribute("data-view", next); + localStorage.setItem("view", next); + + if (!viewIcon) { + return; + } + + if (next === "grid") { + viewIcon.classList.remove("fa-th-large"); + viewIcon.classList.add("fa-th-list"); + } else { + viewIcon.classList.remove("fa-th-list"); + viewIcon.classList.add("fa-th-large"); + } + }); + + themeToggle.addEventListener("click", () => { + const currentTheme = document.documentElement.getAttribute("data-theme"); + const newTheme = currentTheme === "dark" ? "light" : "dark"; + + document.documentElement.setAttribute("data-theme", newTheme); + localStorage.setItem("theme", newTheme); + + if (!themeIcon) { + return; + } + + if (newTheme === "dark") { + themeIcon.classList.remove("fa-moon"); + themeIcon.classList.add("fa-sun"); + } else { + themeIcon.classList.remove("fa-sun"); + themeIcon.classList.add("fa-moon"); + } + }); + + if (!savedTheme) { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + if (prefersDark) { + document.documentElement.setAttribute("data-theme", "dark"); + if (themeIcon) { + themeIcon.classList.remove("fa-moon"); + themeIcon.classList.add("fa-sun"); + } + localStorage.setItem("theme", "dark"); + } + } +} + +/** + * 导出到全局作用域 + */ +window.ThemeUtils = { + initThemeAndView, +}; diff --git a/static/js/ui-utils.js b/static/js/ui-utils.js new file mode 100644 index 0000000..2e05686 --- /dev/null +++ b/static/js/ui-utils.js @@ -0,0 +1,44 @@ +/** + * UI 相关的全局工具函数 + * 包括状态提示、对话框等 + */ + +/** + * 更新状态消息 + * @param {string} message - 消息内容 + * @param {string} state - 状态类型 ('success', 'error', 'warning', 或空) + * @returns {HTMLElement|null} 状态元素 + */ +function updateStatus(message, state) { + const statusDiv = document.getElementById("uploadStatus"); + if (!statusDiv) { + return null; + } + + statusDiv.textContent = message; + statusDiv.className = "upload-status" + (state ? ` ${state}` : ""); + statusDiv.style.display = "block"; + return statusDiv; +} + +/** + * 延迟隐藏状态消息 + * @param {HTMLElement} statusDiv - 状态元素 + * @param {number} delay - 延迟时间(毫秒) + */ +function hideStatusLater(statusDiv, delay = 2000) { + if (!statusDiv) { + return; + } + setTimeout(() => { + statusDiv.style.display = "none"; + }, delay); +} + +/** + * 导出到全局作用域 + */ +window.UIUtils = { + updateStatus, + hideStatusLater, +}; diff --git a/static/js/utilities.js b/static/js/utilities.js new file mode 100644 index 0000000..49a402f --- /dev/null +++ b/static/js/utilities.js @@ -0,0 +1,68 @@ +/** + * Service Worker 和其他工具函数 + */ + +/** + * 注销 Service Worker + */ +function unregisterServiceWorker() { + if (!("serviceWorker" in navigator)) { + return; + } + + window.addEventListener("load", () => { + navigator.serviceWorker + .getRegistrations() + .then((registrations) => { + registrations.forEach((registration) => { + registration.unregister().then(() => { + console.log("Service Worker unregistered"); + }); + }); + }) + .catch((error) => { + console.log("Error unregistering Service Worker:", error); + }); + + // 清理 Service Worker 相关的缓存 + if ("caches" in window) { + caches.keys().then((cacheNames) => { + cacheNames.forEach((cacheName) => { + caches.delete(cacheName).then(() => { + console.log("Cache deleted:", cacheName); + }); + }); + }); + } + }); +} + +/** + * 注册模态框处理程序 + */ +function registerModalHandlers() { + const modal = document.getElementById("previewModal"); + if (!modal) { + return; + } + + modal.addEventListener("click", (event) => { + if (event.target === modal) { + closePreview(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !isDialogOpen()) { + closePreview(); + } + }); +} + +/** + * 导出到全局作用域 + */ +window.UtilityFuncs = { + unregisterServiceWorker, + registerModalHandlers, +}; diff --git a/templates/base.html b/templates/base.html index 249362b..d0ef2e3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,7 +6,7 @@ {% block title %}Cloud Index{% endblock %} - + {% block head_extra %}{% endblock %} @@ -27,7 +27,24 @@ - + + + + + + + + + {% block scripts %}{% endblock %}