Files
K-Vault/gallery.html

1380 lines
39 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片浏览器 | K-Vault</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/logo.png">
<link rel="shortcut icon" href="/favicon.ico">
<link rel="stylesheet" href="/theme.css">
<script src="/theme.js?v=20260305"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/theme-chalk/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/all.min.css">
<style>
:root {
--primary: #8a4bff;
--primary-light: #b39ddb;
--primary-dark: #6c3ce0;
--accent: #ffd7e4;
--bg: linear-gradient(135deg, #ffd7e4 0%, #c8f1ff 100%);
--card-bg: rgba(255, 255, 255, 0.92);
--shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
--shadow-hover: 0 8px 24px rgba(138, 75, 255, 0.12);
--transition: all 0.2s ease;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
background-attachment: fixed;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', sans-serif;
}
#app {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 头部 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 28px;
background: var(--card-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 18px;
margin-bottom: 24px;
box-shadow: var(--shadow);
border: 1px solid rgba(255, 255, 255, 0.5);
flex-wrap: wrap;
gap: 16px;
position: sticky;
top: 20px;
z-index: 100;
}
.header-title {
font-size: 1.5em;
font-weight: 700;
color: #333;
display: flex;
align-items: center;
gap: 12px;
}
.header-title i { color: var(--primary); font-size: 1.1em; }
.header-search {
flex: 1;
max-width: 400px;
min-width: 200px;
}
.header-search input {
width: 100%;
padding: 10px 16px;
border: 2px solid #eee;
border-radius: 24px;
font-size: 0.95em;
transition: all 0.3s;
}
.header-search input:focus {
border-color: var(--primary-light);
outline: none;
box-shadow: 0 0 0 3px rgba(138, 75, 255, 0.1);
}
.header-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
}
.header-actions .gallery-theme-toggle {
margin-left: 2px;
border: 1.5px solid rgba(138, 75, 255, 0.25);
background: linear-gradient(
135deg,
rgba(138, 75, 255, 0.16),
rgba(200, 241, 255, 0.42)
);
color: #2f3243;
font-weight: 600;
box-shadow: 0 6px 16px rgba(138, 75, 255, 0.14);
}
.header-actions .gallery-theme-toggle i {
color: var(--primary);
width: 14px;
text-align: center;
}
.header-actions .gallery-theme-toggle:hover {
border-color: var(--primary-light);
transform: translateY(-1px);
}
html[data-theme="dark"] .header-actions .gallery-theme-toggle {
border-color: rgba(154, 169, 255, 0.45);
background: linear-gradient(
135deg,
rgba(154, 169, 255, 0.2),
rgba(84, 106, 156, 0.35)
);
color: #e8eeff;
}
html[data-theme="dark"] .header-actions .gallery-theme-toggle i {
color: #c4d4ff;
}
.nav-btn {
padding: 8px 16px;
border-radius: 8px;
text-decoration: none;
color: #666;
font-size: 0.9em;
transition: all 0.3s;
background: rgba(255, 255, 255, 0.8);
border: 1px solid #eee;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
.nav-btn:hover {
background: var(--accent);
border-color: var(--primary-light);
color: #333;
}
.nav-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* 工具栏 */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 24px;
background: var(--card-bg);
backdrop-filter: blur(20px);
border-radius: 14px;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: var(--shadow);
}
.toolbar-left, .toolbar-right { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.stat-badge {
padding: 8px 16px;
background: linear-gradient(135deg, rgba(138, 75, 255, 0.08), rgba(255, 215, 228, 0.15));
border-radius: 24px;
font-size: 0.88em;
color: #555;
font-weight: 500;
}
.stat-badge strong { color: var(--primary); margin-left: 4px; }
/* 链接格式选择器 */
.format-selector {
display: flex;
gap: 4px;
background: #f5f5f5;
padding: 4px;
border-radius: 8px;
}
.format-btn {
padding: 6px 12px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
font-size: 0.8em;
transition: all 0.2s;
color: #666;
}
.format-btn:hover { background: rgba(138, 75, 255, 0.1); }
.format-btn.active { background: var(--primary); color: white; }
/* 图片网格 */
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
}
.image-card {
position: relative;
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
background: var(--card-bg);
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 1px solid rgba(255, 255, 255, 0.5);
will-change: transform;
contain: layout style paint;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-hover);
}
.image-card.selected {
box-shadow: 0 0 0 3px var(--primary), var(--shadow-hover);
}
.image-card img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.2s ease;
}
.image-card:hover img { transform: scale(1.05); }
.image-card-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0) 50%, rgba(0,0,0,0.7) 100%);
opacity: 0;
transition: opacity 0.3s;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 12px;
}
.image-card:hover .image-card-overlay { opacity: 1; }
.image-card.selected .image-card-overlay { opacity: 1; }
.image-card-name {
color: white;
font-size: 0.8em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
}
.image-card-actions {
display: flex;
gap: 6px;
justify-content: center;
}
.card-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: rgba(255, 255, 255, 0.9);
color: #333;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 0.9em;
}
.card-btn:hover {
background: var(--primary);
color: white;
transform: scale(1.1);
}
.card-btn.delete:hover { background: #f56c6c; }
.image-card-checkbox {
position: absolute;
top: 10px;
left: 10px;
width: 24px;
height: 24px;
background: rgba(255, 255, 255, 0.9);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
cursor: pointer;
}
.image-card:hover .image-card-checkbox,
.image-card.selected .image-card-checkbox { opacity: 1; }
.image-card-checkbox.checked {
background: var(--primary);
color: white;
}
/* 批量操作栏 */
.batch-bar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(51, 51, 51, 0.95);
backdrop-filter: blur(10px);
padding: 12px 24px;
border-radius: 16px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
}
.batch-bar-count {
color: white;
font-size: 0.9em;
}
.batch-bar-count strong { color: var(--accent); }
.batch-btn {
padding: 8px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.85em;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.batch-btn.primary { background: var(--primary); color: white; }
.batch-btn.success { background: #67c23a; color: white; }
.batch-btn.danger { background: #f56c6c; color: white; }
.batch-btn.default { background: rgba(255,255,255,0.2); color: white; }
.batch-btn:hover { transform: scale(1.05); filter: brightness(1.1); }
/* 预览弹窗 - 原生图片 / iframe */
.preview-modal {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.preview-modal img {
max-width: 90%;
max-height: 90%;
border-radius: 8px;
box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
}
/* iframe 容器 - 90% 宽度80% 高度 */
.preview-modal .iframe-container {
width: 90vw;
height: 80vh;
position: relative;
border-radius: 8px;
overflow: hidden;
background: #000;
}
.preview-modal .iframe-container iframe {
width: 100%;
height: 100%;
border: none;
}
.preview-close {
position: absolute;
top: 20px;
right: 20px;
width: 50px;
height: 50px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
color: white;
font-size: 1.5em;
cursor: pointer;
transition: all 0.3s;
}
.preview-close:hover {
background: rgba(255, 255, 255, 0.4);
transform: rotate(90deg);
}
.preview-info {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
padding: 12px 24px;
border-radius: 12px;
color: white;
display: flex;
gap: 16px;
align-items: center;
}
.preview-link {
max-width: 400px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: monospace;
font-size: 0.85em;
}
/* 分页 */
.pagination {
display: flex;
justify-content: center;
margin-top: 24px;
gap: 8px;
}
.page-btn {
width: 40px;
height: 40px;
border: none;
border-radius: 10px;
background: rgba(255, 255, 255, 0.9);
cursor: pointer;
transition: all 0.2s;
font-size: 0.9em;
}
.page-btn:hover { background: var(--accent); }
.page-btn.active { background: var(--primary); color: white; }
.page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* 空状态 */
.empty-state {
text-align: center;
padding: 80px 20px;
color: #999;
}
.empty-state i { font-size: 4em; margin-bottom: 20px; color: #ddd; }
/* 加载状态 */
.loading-state {
text-align: center;
padding: 60px;
color: #666;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #eee;
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Toast */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
.toast {
padding: 12px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
animation: slideIn 0.3s ease;
}
.toast.success { border-left: 4px solid #67c23a; }
.toast.error { border-left: 4px solid #f56c6c; }
.toast.info { border-left: 4px solid var(--primary); }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* 响应式 */
@media (max-width: 768px) {
body { padding: 12px; }
.header {
flex-direction: column;
align-items: stretch;
gap: 12px;
padding: 14px 18px;
}
.header-title {
font-size: 1.2em;
justify-content: center;
}
.header-search {
max-width: none;
order: 3;
}
.header-actions {
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
.header-actions .gallery-theme-toggle {
order: 4;
}
.nav-btn {
padding: 8px 14px;
font-size: 0.85em;
}
.toolbar {
flex-direction: column;
gap: 12px;
align-items: stretch;
padding: 14px;
}
.toolbar-left {
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.toolbar-right {
flex-direction: column;
align-items: center;
gap: 8px;
}
.stat-badge {
padding: 6px 12px;
font-size: 0.82em;
}
.format-selector {
justify-content: center;
}
.format-btn {
padding: 6px 12px;
font-size: 0.82em;
}
.image-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}
.image-card-overlay {
opacity: 1;
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, transparent 100%);
}
.image-card-name {
font-size: 0.75em;
}
.card-btn {
width: 30px;
height: 30px;
font-size: 0.8em;
}
.batch-bar {
flex-wrap: wrap;
width: calc(100% - 24px);
justify-content: center;
gap: 8px;
padding: 12px;
left: 12px;
bottom: 12px;
}
.batch-btn {
padding: 8px 14px;
font-size: 0.85em;
}
/* 预览弹窗优化 */
.preview-overlay .preview-content {
max-width: 95%;
max-height: 85%;
}
.preview-actions {
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
}
@media (max-width: 480px) {
body { padding: 8px; }
.header {
padding: 12px 14px;
border-radius: 12px;
gap: 10px;
}
.header-title {
font-size: 1.05em;
gap: 8px;
}
.header-search input {
height: 36px;
font-size: 0.9em;
padding: 0 12px;
}
.header-actions {
gap: 6px;
}
.header-actions .gallery-theme-toggle {
padding: 6px 9px;
min-width: 38px;
justify-content: center;
}
.header-actions .gallery-theme-toggle [data-theme-label] {
display: none;
}
.nav-btn {
padding: 6px 10px;
font-size: 0.78em;
border-radius: 8px;
}
.nav-btn i {
font-size: 0.9em;
}
.toolbar {
padding: 12px;
border-radius: 12px;
gap: 10px;
}
.toolbar-left {
gap: 6px;
}
.stat-badge {
padding: 5px 10px;
font-size: 0.75em;
border-radius: 6px;
}
.format-btn {
padding: 5px 10px;
font-size: 0.75em;
min-width: 36px;
}
.image-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.image-card {
border-radius: 10px;
}
.image-card-checkbox {
width: 24px;
height: 24px;
font-size: 0.75em;
}
.card-btn {
width: 26px;
height: 26px;
font-size: 0.72em;
}
.image-card-name {
font-size: 0.68em;
padding: 6px 8px;
}
.batch-bar {
flex-direction: column;
width: calc(100% - 16px);
left: 8px;
bottom: 8px;
padding: 10px;
gap: 8px;
border-radius: 12px;
}
.batch-btn {
width: 100%;
justify-content: center;
padding: 10px 16px;
}
.toast {
max-width: 90%;
right: 5%;
padding: 12px 16px;
font-size: 0.85em;
}
/* 加载状态 */
.loading-indicator {
padding: 30px;
}
.loading-indicator i {
font-size: 2em;
}
/* 移动端禁用高消耗效果 */
.header {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: rgba(255, 255, 255, 0.98);
}
.toolbar {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: rgba(255, 255, 255, 0.98);
}
.image-card:hover {
transform: none;
}
.image-card:hover img {
transform: none;
}
.image-card-overlay {
opacity: 1;
}
}
/* 用户偏好减少动画 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
<link rel="stylesheet" href="/mobile-refactor.css">
</head>
<body>
<div id="app" v-cloak>
<!-- 头部 -->
<div class="header">
<div class="header-title">
<i class="fas fa-images"></i>
<span>图片浏览器</span>
</div>
<div class="header-search">
<input type="text" v-model="searchQuery" placeholder="搜索文件名..." @input="handleSearch">
</div>
<div class="header-actions">
<a href="./" class="nav-btn"><i class="fas fa-cloud-upload-alt"></i> 上传</a>
<a href="./admin.html" class="nav-btn"><i class="fas fa-th-large"></i> 管理</a>
<a href="./" class="nav-btn"><i class="fas fa-home"></i> 首页</a>
<button type="button" class="nav-btn gallery-theme-toggle" data-theme-toggle aria-label="切换主题">
<i class="fas fa-moon" data-theme-icon></i>
<span data-theme-label>夜间</span>
</button>
</div>
</div>
<!-- 工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<span class="stat-badge"><i class="fas fa-images"></i> 总数<strong>{{ totalCount }}</strong></span>
<span class="stat-badge"><i class="fas fa-check-square"></i> 已选<strong>{{ selectedCount }}</strong></span>
<button class="nav-btn" @click="selectAll">
<i :class="isAllSelected ? 'fas fa-square' : 'fas fa-check-square'"></i>
{{ isAllSelected ? '取消全选' : '全选' }}
</button>
<button class="nav-btn" @click="loadMore" v-if="hasMore" :disabled="loading">
<i class="fas fa-sync" :class="{ 'fa-spin': loading }"></i>
加载更多
</button>
</div>
<div class="toolbar-right">
<span style="font-size: 0.85em; color: #666;">链接格式:</span>
<div class="format-selector">
<button class="format-btn" :class="{ active: linkFormat === 'url' }" @click="linkFormat = 'url'">URL</button>
<button class="format-btn" :class="{ active: linkFormat === 'markdown' }" @click="linkFormat = 'markdown'">MD</button>
<button class="format-btn" :class="{ active: linkFormat === 'html' }" @click="linkFormat = 'html'">HTML</button>
<button class="format-btn" :class="{ active: linkFormat === 'bbcode' }" @click="linkFormat = 'bbcode'">BB</button>
</div>
</div>
</div>
<!-- 图片网格 -->
<div class="image-grid" v-if="!loading || images.length > 0">
<div class="image-card"
v-for="(img, index) in filteredImages"
:key="img.name"
:class="{ selected: img.selected }"
@click="toggleSelect(img)">
<img :src="'/file/' + img.name"
:alt="img.metadata?.fileName || img.name"
loading="lazy"
@error="handleImageError($event, img)">
<div class="image-card-checkbox"
:class="{ checked: img.selected }"
@click.stop="toggleSelect(img)">
<i :class="img.selected ? 'fas fa-check' : 'far fa-square'"></i>
</div>
<div class="image-card-overlay">
<div class="image-card-name">{{ img.metadata?.fileName || img.name }}</div>
<div class="image-card-actions">
<button class="card-btn" @click.stop="copyLink(img)" title="复制链接">
<i class="fas fa-copy"></i>
</button>
<button class="card-btn" @click.stop="previewImage(img)" title="预览">
<i class="fas fa-expand"></i>
</button>
<button class="card-btn" @click.stop="downloadImage(img)" title="下载">
<i class="fas fa-download"></i>
</button>
<button class="card-btn delete" @click.stop="deleteImage(img, index)" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 加载状态 -->
<div class="loading-state" v-if="loading && images.length === 0">
<div class="loading-spinner"></div>
<div>加载中...</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && filteredImages.length === 0">
<i class="fas fa-inbox"></i>
<div style="font-size: 1.2em; margin-bottom: 8px;">没有找到图片</div>
<div>{{ searchQuery ? '尝试其他搜索词' : '上传一些图片开始使用' }}</div>
<a href="./" class="nav-btn" style="margin-top: 20px; display: inline-flex;">
<i class="fas fa-cloud-upload-alt"></i> 去上传
</a>
</div>
<!-- 分页 -->
<div class="pagination" v-if="totalPages > 1">
<button class="page-btn" @click="currentPage = 1" :disabled="currentPage === 1">
<i class="fas fa-angle-double-left"></i>
</button>
<button class="page-btn" @click="currentPage--" :disabled="currentPage === 1">
<i class="fas fa-angle-left"></i>
</button>
<template v-for="page in displayPages">
<button class="page-btn" :class="{ active: page === currentPage }" @click="currentPage = page" :key="page">
{{ page }}
</button>
</template>
<button class="page-btn" @click="currentPage++" :disabled="currentPage === totalPages">
<i class="fas fa-angle-right"></i>
</button>
<button class="page-btn" @click="currentPage = totalPages" :disabled="currentPage === totalPages">
<i class="fas fa-angle-double-right"></i>
</button>
</div>
<!-- 批量操作栏 -->
<div class="batch-bar" v-if="selectedCount > 0">
<div class="batch-bar-count">已选择 <strong>{{ selectedCount }}</strong> 个文件</div>
<button class="batch-btn primary" @click="batchCopy">
<i class="fas fa-copy"></i> 复制链接
</button>
<button class="batch-btn success" @click="batchDownload">
<i class="fas fa-download"></i> 下载
</button>
<button class="batch-btn danger" @click="batchDelete">
<i class="fas fa-trash"></i> 删除
</button>
<button class="batch-btn default" @click="clearSelection">
<i class="fas fa-times"></i> 取消
</button>
</div>
<!-- 预览弹窗 - 原生图片 / iframe 预览 -->
<div class="preview-modal" v-if="previewData" @click="closePreview">
<button class="preview-close" @click="closePreview">
<i class="fas fa-times"></i>
</button>
<!-- 原生图片预览 -->
<img v-if="previewData.type === 'native-image'" :src="previewData.url" @click.stop>
<!-- iframe 预览(视频/音频/文档) -->
<div v-else-if="previewData.type === 'iframe'" class="iframe-container" @click.stop>
<iframe
ref="previewIframe"
:src="previewData.iframeUrl"
allow="autoplay; fullscreen; encrypted-media"
allowfullscreen>
</iframe>
</div>
<div class="preview-info" @click.stop>
<span class="preview-link">{{ previewData.url }}</span>
<button class="batch-btn primary" @click="copyPreviewLink">
<i class="fas fa-copy"></i> 复制
</button>
</div>
</div>
<!-- Toast 容器 -->
<div class="toast-container">
<div class="toast" :class="toast.type" v-for="(toast, index) in toasts" :key="index">
<i :class="getToastIcon(toast.type)"></i>
<span>{{ toast.message }}</span>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.3/lib/index.js"></script>
<script>
// 统一媒体预览服务基础 URL
const PREVIEW_SERVICE_BASE = 'https://www.katelya.eu.org';
new Vue({
el: '#app',
data: {
baseURL: document.location.origin,
images: [],
loading: true,
searchQuery: '',
linkFormat: 'url',
currentPage: 1,
pageSize: 30,
nextCursor: null,
hasMore: true,
previewData: null, // 新的预览数据对象
toasts: [],
searchTimer: null
},
computed: {
filteredImages() {
let result = this.images;
// 只显示图片类型
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico'];
result = result.filter(img => {
const ext = img.name.split('.').pop().toLowerCase();
return imageExts.includes(ext);
});
// 搜索过滤
if (this.searchQuery.trim()) {
const query = this.searchQuery.toLowerCase();
result = result.filter(img => {
const name = (img.metadata?.fileName || img.name).toLowerCase();
return name.includes(query);
});
}
// 分页
const start = (this.currentPage - 1) * this.pageSize;
return result.slice(start, start + this.pageSize);
},
totalCount() {
return this.images.length;
},
selectedCount() {
return this.images.filter(img => img.selected).length;
},
isAllSelected() {
return this.filteredImages.length > 0 && this.filteredImages.every(img => img.selected);
},
totalPages() {
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico'];
let filtered = this.images.filter(img => {
const ext = img.name.split('.').pop().toLowerCase();
return imageExts.includes(ext);
});
if (this.searchQuery.trim()) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(img => {
const name = (img.metadata?.fileName || img.name).toLowerCase();
return name.includes(query);
});
}
return Math.ceil(filtered.length / this.pageSize);
},
displayPages() {
const pages = [];
const total = this.totalPages;
const current = this.currentPage;
let start = Math.max(1, current - 2);
let end = Math.min(total, current + 2);
if (end - start < 4) {
if (start === 1) end = Math.min(5, total);
else if (end === total) start = Math.max(1, total - 4);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
}
},
methods: {
async loadImages() {
this.loading = true;
try {
const response = await fetch('./api/manage/list?limit=100', {
method: 'GET',
credentials: 'include'
});
if (response.status === 401) {
window.location.href = './api/manage/login';
return;
}
const result = await response.json();
const invalidPrefixes = ['session:', 'chunk:', 'upload:', 'temp:'];
const filtered = (result.keys || []).filter(img => {
if (!img.name) return false;
if (invalidPrefixes.some(prefix => img.name.startsWith(prefix))) return false;
const metadata = img.metadata || {};
return metadata.fileName && metadata.TimeStamp !== undefined && metadata.TimeStamp !== null;
});
this.images = filtered.map(img => ({
...img,
selected: false,
metadata: {
...img.metadata,
fileName: img.metadata?.fileName || img.name
}
}));
this.nextCursor = result.list_complete ? null : result.cursor;
this.hasMore = !result.list_complete;
} catch (error) {
this.showToast('加载失败,请检查网络', 'error');
}
this.loading = false;
},
async loadMore() {
if (!this.nextCursor || this.loading) return;
this.loading = true;
try {
const response = await fetch(`./api/manage/list?limit=100&cursor=${encodeURIComponent(this.nextCursor)}`, {
method: 'GET',
credentials: 'include'
});
const result = await response.json();
const invalidPrefixes = ['session:', 'chunk:', 'upload:', 'temp:'];
const filtered = (result.keys || []).filter(img => {
if (!img.name) return false;
if (invalidPrefixes.some(prefix => img.name.startsWith(prefix))) return false;
const metadata = img.metadata || {};
return metadata.fileName && metadata.TimeStamp !== undefined && metadata.TimeStamp !== null;
});
const newImages = filtered.map(img => ({
...img,
selected: false,
metadata: {
...img.metadata,
fileName: img.metadata?.fileName || img.name
}
}));
this.images = [...this.images, ...newImages];
this.nextCursor = result.list_complete ? null : result.cursor;
this.hasMore = !result.list_complete;
this.showToast(`加载了 ${newImages.length} 张图片`, 'success');
} catch (error) {
this.showToast('加载失败', 'error');
}
this.loading = false;
},
handleSearch() {
clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(() => {
this.currentPage = 1;
}, 300);
},
toggleSelect(img) {
img.selected = !img.selected;
},
selectAll() {
const newState = !this.isAllSelected;
this.filteredImages.forEach(img => img.selected = newState);
},
clearSelection() {
this.images.forEach(img => img.selected = false);
},
formatLink(img) {
const url = `${this.baseURL}/file/${img.name}`;
const name = img.metadata?.fileName || img.name;
switch (this.linkFormat) {
case 'markdown': return `![${name}](${url})`;
case 'html': return `<img src="${url}" alt="${name}">`;
case 'bbcode': return `[img]${url}[/img]`;
default: return url;
}
},
copyLink(img) {
const link = this.formatLink(img);
this.copyToClipboard(link);
this.showToast('链接已复制', 'success');
},
batchCopy() {
const selected = this.images.filter(img => img.selected);
const links = selected.map(img => this.formatLink(img)).join('\n');
this.copyToClipboard(links);
this.showToast(`已复制 ${selected.length} 个链接`, 'success');
},
batchDownload() {
const selected = this.images.filter(img => img.selected);
this.showToast(`开始下载 ${selected.length} 个文件`, 'info');
selected.forEach((img, i) => {
setTimeout(() => {
this.downloadImage(img);
}, i * 500);
});
},
async batchDelete() {
const selected = this.images.filter(img => img.selected);
if (!confirm(`确定要删除这 ${selected.length} 个文件吗?`)) return;
let successCount = 0;
for (const img of selected) {
try {
const response = await fetch(`./api/manage/delete/${img.name}`, {
method: 'GET',
credentials: 'include'
});
if (response.ok) {
const index = this.images.findIndex(i => i.name === img.name);
if (index > -1) this.images.splice(index, 1);
successCount++;
}
} catch (e) {}
}
this.showToast(`成功删除 ${successCount} 个文件`, 'success');
},
previewImage(img) {
// 文件信息
const fileName = img.metadata?.fileName || img.name;
// 构造绝对直链 URL必须是完整的 https:// 链接)
const fileUrl = `${this.baseURL}/file/${img.name}`;
const ext = fileName.split('.').pop().toLowerCase();
// 图片类型 - 保持原生 Lightbox 预览
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico'];
if (imageExts.includes(ext)) {
this.previewData = {
type: 'native-image',
url: `/file/${img.name}`,
fileName: fileName,
img: img
};
return;
}
// 视频/音频/文档类型 - 使用 Blog Preview iframe
const videoExts = ['mp4', 'webm', 'mkv', 'avi', 'mov', 'm3u8'];
const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'm4a'];
const docExts = ['pdf', 'txt', 'doc', 'docx'];
if (videoExts.includes(ext) || audioExts.includes(ext) || docExts.includes(ext)) {
// 构造 iframe URL - 必须对参数进行 encodeURIComponent 编码
const targetUrl = encodeURIComponent(fileUrl);
const targetTitle = encodeURIComponent(fileName);
const iframeSrc = `${PREVIEW_SERVICE_BASE}/preview?url=${targetUrl}&title=${targetTitle}&autoplay=1`;
this.previewData = {
type: 'iframe',
iframeUrl: iframeSrc,
url: fileUrl,
fileName: fileName,
img: img
};
return;
}
// 其他类型(.zip, .exe 等)- 直接下载
this.downloadImage(img);
},
// 关闭预览并销毁 iframe
closePreview() {
if (this.$refs.previewIframe) {
this.$refs.previewIframe.src = 'about:blank';
}
this.previewData = null;
},
// 复制预览链接
copyPreviewLink() {
if (this.previewData) {
this.copyToClipboard(this.previewData.url);
this.showToast('链接已复制', 'success');
}
},
downloadImage(img) {
const link = document.createElement('a');
link.href = `/file/${img.name}`;
link.download = img.metadata?.fileName || img.name;
link.click();
},
async deleteImage(img, index) {
if (!confirm('确定要删除这个文件吗?')) return;
try {
const response = await fetch(`./api/manage/delete/${img.name}`, {
method: 'GET',
credentials: 'include'
});
if (response.ok) {
const realIndex = this.images.findIndex(i => i.name === img.name);
if (realIndex > -1) this.images.splice(realIndex, 1);
this.showToast('删除成功', 'success');
} else {
throw new Error('删除失败');
}
} catch (error) {
this.showToast('删除失败', 'error');
}
},
handleImageError(event, img) {
event.target.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect fill="%23f0f0f0" width="100" height="100"/><text x="50" y="55" text-anchor="middle" fill="%23999" font-size="12">加载失败</text></svg>';
},
copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
const textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.value = text;
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
},
showToast(message, type = 'info') {
const toast = { message, type };
this.toasts.push(toast);
setTimeout(() => {
const index = this.toasts.indexOf(toast);
if (index > -1) this.toasts.splice(index, 1);
}, 3000);
},
getToastIcon(type) {
const icons = {
success: 'fas fa-check-circle',
error: 'fas fa-times-circle',
info: 'fas fa-info-circle'
};
return icons[type] || icons.info;
},
async checkAuth() {
try {
const response = await fetch('/api/auth/check', { credentials: 'include' });
const data = await response.json();
if (data.authRequired && !data.authenticated) {
window.location.href = '/login.html?redirect=' + encodeURIComponent(window.location.pathname);
return false;
}
return true;
} catch (e) {
console.error('Auth check failed:', e);
return true;
}
}
},
async mounted() {
// 先检查认证
const isAuth = await this.checkAuth();
if (!isAuth) return;
this.loadImages();
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.previewData) {
this.closePreview();
}
});
}
});
</script>
</body>
</html>