修复扫描速度慢,优化仓库显示

This commit is contained in:
2025-10-22 16:25:42 +08:00
parent 92e307d3e5
commit 0e8766c0b4
12 changed files with 2501 additions and 446 deletions
@@ -1,20 +1,76 @@
<template>
<div class="artists-view">
<div class="artists-grid">
<div v-for="artist in artists" :key="artist.name" class="artist-card"
@click="$emit('select-artist', artist.name)">
<div class="artist-avatar">
<span class="avatar-text">{{ artist.name.charAt(0).toUpperCase() }}</span>
</div>
<div class="artist-info">
<h4>{{ artist.name }}</h4>
<p>{{ artist.artworkCount }} 个作品</p>
<p>{{ formatFileSize(artist.totalSize) }}</p>
</div>
<div class="artist-actions">
<button @click.stop="$emit('view-artist-works', artist.name)" class="action-btn">
查看作品
</button>
<!-- 空状态 -->
<div v-if="artists.length === 0" class="empty-state">
<div class="empty-icon">👤</div>
<h3>暂无作者</h3>
<p>这里还没有任何作者信息</p>
</div>
<!-- 作者网格 -->
<div v-else class="artists-grid">
<div v-for="artist in artists" :key="artist.name" class="artist-card">
<!-- 卡片内容 -->
<div class="card-content" @click="$emit('view-artist-works', artist.name)">
<!-- 头像和背景 -->
<div class="artist-header">
<div class="artist-avatar">
<span class="avatar-text">{{ artist.name.charAt(0).toUpperCase() }}</span>
</div>
<div class="artist-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
</div>
<!-- 作者信息 -->
<div class="artist-details">
<h3 class="artist-name" :title="artist.name">{{ artist.name }}</h3>
<!-- 统计信息 -->
<div class="artist-stats">
<div class="stat-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
<span class="stat-value">{{ artist.artworkCount }}</span>
<span class="stat-label">作品</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span class="stat-value">{{ formatFileSize(artist.totalSize) }}</span>
<span class="stat-label">大小</span>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="artist-actions">
<button @click.stop="$emit('view-artist-works', artist.name)" class="action-btn primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<span>查看作品</span>
</button>
<button @click.stop="$emit('select-artist', artist.name)" class="action-btn secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
</button>
</div>
</div>
</div>
</div>
@@ -39,86 +95,293 @@ defineEmits<Emits>()
</script>
<style scoped>
.artists-view {
width: 100%;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6b7280;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
margin: 0 0 0.5rem 0;
color: #1f2937;
font-size: 1.5rem;
}
.empty-state p {
margin: 0;
font-size: 1rem;
}
/* 作者网格 */
.artists-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* 作者卡片 */
.artist-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1.5rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 1rem;
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.artist-card:hover {
border-color: #3b82f6;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
}
.card-content {
display: flex;
flex-direction: column;
height: 100%;
cursor: pointer;
}
/* 作者头部 */
.artist-header {
position: relative;
height: 120px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.artist-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><defs><pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse"><path d="M 20 0 L 0 0 0 20" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
opacity: 0.3;
}
.artist-avatar {
width: 60px;
height: 60px;
background: #3b82f6;
position: relative;
width: 80px;
height: 80px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1;
}
.artist-info {
.avatar-text {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.artist-badge {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: 2rem;
height: 2rem;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
z-index: 1;
}
.artist-badge svg {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
}
/* 作者详情 */
.artist-details {
padding: 1.25rem;
flex: 1;
}
.artist-info h4 {
margin: 0 0 0.5rem 0;
.artist-name {
margin: 0 0 1rem 0;
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 统计信息 */
.artist-stats {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.375rem;
}
.stat-item svg {
width: 1.25rem;
height: 1.25rem;
color: #6b7280;
stroke-width: 2;
}
.stat-value {
font-size: 1rem;
font-weight: 600;
color: #1f2937;
}
.artist-info p {
margin: 0.25rem 0;
.stat-label {
font-size: 0.75rem;
color: #6b7280;
font-size: 0.875rem;
}
.stat-divider {
width: 1px;
height: 2.5rem;
background: #e5e7eb;
}
/* 操作按钮 */
.artist-actions {
display: flex;
gap: 0.5rem;
padding: 1rem 1.25rem 1.25rem;
}
.action-btn {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
background: white;
border-radius: 0.375rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
.action-btn svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.action-btn.primary {
flex: 1;
background: #3b82f6;
color: white;
}
.action-btn.primary:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
.action-btn.secondary {
background: #f3f4f6;
border-color: #3b82f6;
color: #4b5563;
padding: 0.75rem;
}
.action-btn.secondary:hover {
background: #e5e7eb;
color: #1f2937;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.artists-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
}
}
@media (max-width: 768px) {
.artists-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
.artist-card {
flex-direction: column;
text-align: center;
.artist-header {
height: 100px;
}
.artist-avatar {
width: 64px;
height: 64px;
}
.avatar-text {
font-size: 1.5rem;
}
.artist-details {
padding: 1rem;
}
.artist-name {
font-size: 1rem;
}
.action-btn.primary span {
display: none;
}
.action-btn.primary {
justify-content: center;
}
}
@media (max-width: 480px) {
.artists-grid {
grid-template-columns: 1fr;
}
.stat-value {
font-size: 0.875rem;
}
.stat-label {
font-size: 0.6875rem;
}
}
</style>
@@ -1,31 +1,102 @@
<template>
<div class="artworks-view">
<div class="artworks-grid">
<div v-for="artwork in artworks" :key="artwork.id" class="artwork-card" @click="$emit('view-artwork', artwork)">
<div class="artwork-preview" v-if="artwork.files.length > 0">
<img :src="getPreviewUrl(artwork.files[0].path)" :alt="artwork.title" class="preview-image"
@click.stop="$emit('open-image-viewer', artwork, 0)" />
<div class="artwork-overlay">
<button @click.stop="$emit('open-image-viewer', artwork, 0)" class="view-btn-overlay">
👁 查看大图
</button>
<!-- 无作品提示 -->
<div v-if="artworks.length === 0" class="empty-state">
<div class="empty-icon">🎨</div>
<h3>暂无作品</h3>
<p>这里还没有任何作品快去下载一些吧</p>
</div>
<!-- 作品网格 -->
<div v-else class="artworks-grid">
<div v-for="artwork in artworks" :key="artwork.id" class="artwork-card">
<!-- 图片容器 - 点击预览大图 -->
<div class="artwork-image-wrapper" @click="$emit('open-image-viewer', artwork, 0)">
<div v-if="artwork.files.length > 0" class="artwork-image-container">
<img
:src="getPreviewUrl(artwork.files[0].path)"
:alt="artwork.title"
class="artwork-image"
loading="lazy"
/>
<!-- 多图标识 -->
<div v-if="artwork.files.length > 1" class="multi-image-badge">
<svg class="badge-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<span>{{ artwork.files.length }}</span>
</div>
<!-- 悬浮遮罩 -->
<div class="artwork-hover-overlay">
<button class="quick-view-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span>查看大图</span>
</button>
</div>
</div>
<div v-else class="artwork-no-image">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<span>暂无预览</span>
</div>
</div>
<div class="artwork-info">
<h4>{{ artwork.title }}</h4>
<p class="artist-name" @click.stop="$emit('select-artist', artwork.artist)">
👤 {{ artwork.artist }}
</p>
<p>{{ formatFileSize(artwork.size) }}</p>
<p class="file-count">{{ artwork.files.length }} 个文件</p>
</div>
<div class="artwork-actions">
<button @click.stop="$emit('view-artwork', artwork)" class="action-btn">
详情
</button>
<button @click.stop="$emit('open-image-viewer', artwork, 0)" class="action-btn">
预览
</button>
<!-- 作品信息 -->
<div class="artwork-content">
<h3 class="artwork-title" :title="artwork.title">{{ artwork.title }}</h3>
<!-- 作者信息 -->
<div class="artwork-artist" @click.stop="$emit('select-artist', artwork.artist)">
<div class="artist-avatar">
<span>{{ artwork.artist.charAt(0).toUpperCase() }}</span>
</div>
<span class="artist-name">{{ artwork.artist }}</span>
</div>
<!-- 元数据 -->
<div class="artwork-meta">
<div class="meta-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span>{{ formatFileSize(artwork.size) }}</span>
</div>
<div class="meta-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
<polyline points="13 2 13 9 20 9"/>
</svg>
<span>{{ artwork.files.length }} 个文件</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="artwork-actions">
<button @click.stop="$emit('view-artwork', artwork)" class="action-btn action-btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
查看详情
</button>
<button @click.stop="$emit('open-image-viewer', artwork, 0)" class="action-btn action-btn-secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
</div>
</div>
</div>
</div>
@@ -51,128 +122,372 @@ defineEmits<Emits>()
</script>
<style scoped>
.artworks-view {
width: 100%;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6b7280;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
margin: 0 0 0.5rem 0;
color: #1f2937;
font-size: 1.5rem;
}
.empty-state p {
margin: 0;
font-size: 1rem;
}
/* 作品网格 - 响应式布局 */
.artworks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* 作品卡片 */
.artwork-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
border-radius: 0.75rem;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
}
.artwork-card:hover {
border-color: #3b82f6;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
}
.artwork-preview {
/* 图片容器 */
.artwork-image-wrapper {
position: relative;
height: 200px;
overflow: hidden;
width: 100%;
cursor: pointer;
background: #f9fafb;
}
.preview-image {
.artwork-image-container {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
overflow: hidden;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.artwork-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.2s;
object-fit: contain;
transition: transform 0.3s ease;
background: white;
}
.artwork-card:hover .preview-image {
transform: scale(1.05);
.artwork-card:hover .artwork-image {
transform: scale(1.02);
}
.artwork-overlay {
/* 多图标识 */
.multi-image-badge {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
color: white;
padding: 0.375rem 0.625rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
z-index: 2;
}
.badge-icon {
width: 1rem;
height: 1rem;
stroke-width: 2;
}
/* 悬浮遮罩 */
.artwork-hover-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
transition: opacity 0.3s ease;
z-index: 1;
}
.artwork-card:hover .artwork-overlay {
.artwork-image-wrapper:hover .artwork-hover-overlay {
opacity: 1;
}
.view-btn-overlay {
.quick-view-btn {
background: white;
color: #1f2937;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.artwork-info {
padding: 1rem;
.quick-view-btn:hover {
background: #3b82f6;
color: white;
transform: scale(1.05);
}
.artwork-info h4 {
margin: 0 0 0.5rem 0;
.quick-view-btn svg {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
}
/* 无图片状态 */
.artwork-no-image {
width: 100%;
aspect-ratio: 4 / 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f3f4f6;
color: #9ca3af;
gap: 0.75rem;
}
.artwork-no-image svg {
width: 3rem;
height: 3rem;
stroke-width: 1.5;
}
.artwork-no-image span {
font-size: 0.875rem;
font-weight: 500;
}
/* 作品内容 */
.artwork-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
flex: 1;
}
.artwork-title {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #1f2937;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-clamp: 2;
display: box;
box-orient: vertical;
}
/* 作者信息 */
.artwork-artist {
display: flex;
align-items: center;
gap: 0.625rem;
cursor: pointer;
transition: all 0.2s;
padding: 0.5rem;
margin: -0.5rem;
border-radius: 0.5rem;
}
.artwork-artist:hover {
background: #f3f4f6;
}
.artist-avatar {
width: 2rem;
height: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.875rem;
font-weight: 600;
flex-shrink: 0;
}
.artist-name {
color: #3b82f6 !important;
cursor: pointer;
color: #4b5563;
font-size: 0.875rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s;
}
.artist-name:hover {
text-decoration: underline;
.artwork-artist:hover .artist-name {
color: #3b82f6;
}
.artwork-info p {
margin: 0.25rem 0;
font-size: 0.75rem;
/* 元数据 */
.artwork-meta {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.375rem;
color: #6b7280;
font-size: 0.8125rem;
}
.file-count {
font-weight: 500;
color: #6b5563 !important;
.meta-item svg {
width: 1rem;
height: 1rem;
stroke-width: 2;
flex-shrink: 0;
}
/* 操作按钮 */
.artwork-actions {
padding: 0 1rem 1rem;
display: flex;
gap: 0.5rem;
margin-top: auto;
padding-top: 0.75rem;
border-top: 1px solid #f3f4f6;
}
.action-btn {
flex: 1;
padding: 0.5rem;
border: 1px solid #d1d5db;
background: white;
border-radius: 0.375rem;
padding: 0.625rem 1rem;
border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.action-btn:hover {
.action-btn svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.action-btn-primary {
background: #3b82f6;
color: white;
}
.action-btn-primary:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
.action-btn-secondary {
background: #f3f4f6;
border-color: #3b82f6;
color: #4b5563;
}
.action-btn-secondary:hover {
background: #e5e7eb;
color: #1f2937;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.artworks-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.25rem;
}
}
@media (max-width: 768px) {
.artworks-grid {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.artwork-content {
padding: 1rem;
}
.artwork-title {
font-size: 0.9375rem;
}
.action-btn span {
display: none;
}
.action-btn svg {
margin: 0;
}
}
@media (max-width: 480px) {
.artworks-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.75rem;
}
.multi-image-badge {
top: 0.5rem;
right: 0.5rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.badge-icon {
width: 0.875rem;
height: 0.875rem;
}
}
</style>
@@ -1,51 +1,121 @@
<template>
<div class="gallery-view">
<div class="gallery-controls">
<div class="zoom-controls">
<button @click="zoomOut" class="zoom-btn" :disabled="zoomLevel <= 0.5">
🔍-
</button>
<span class="zoom-level">{{ Math.round(zoomLevel * 100) }}%</span>
<button @click="zoomIn" class="zoom-btn" :disabled="zoomLevel >= 3">
🔍+
</button>
</div>
<div class="view-controls">
<button @click="setGridSize('small')" :class="['size-btn', { active: gridSize === 'small' }]">
</button>
<button @click="setGridSize('medium')" :class="['size-btn', { active: gridSize === 'medium' }]">
</button>
<button @click="setGridSize('large')" :class="['size-btn', { active: gridSize === 'large' }]">
</button>
</div>
<!-- 空状态 -->
<div v-if="artworks.length === 0" class="empty-state">
<div class="empty-icon">🖼</div>
<h3>暂无作品</h3>
<p>这里还没有任何作品</p>
</div>
<div class="gallery-grid" :class="`grid-${gridSize}`">
<div v-for="artwork in artworks" :key="artwork.id" class="gallery-item"
@click="$emit('open-image-viewer', artwork, 0)">
<div class="gallery-image-container">
<img :src="getPreviewUrl(artwork.files[0].path)" :alt="artwork.title" class="gallery-image"
@load="onImageLoad" @error="onImageError" />
<div class="gallery-overlay">
<div class="overlay-content">
<h4>{{ artwork.title }}</h4>
<p>{{ artwork.artist }}</p>
<div class="overlay-actions">
<button @click.stop="$emit('open-image-viewer', artwork, 0)" class="overlay-btn">
👁 查看大图
</button>
<button @click.stop="$emit('view-artwork', artwork)" class="overlay-btn">
📋 详情
</button>
<template v-else>
<!-- 画廊控制栏 -->
<div class="gallery-controls">
<div class="controls-section">
<label class="control-label">网格大小</label>
<div class="control-buttons">
<button @click="setGridSize('small')" :class="['control-btn', { active: gridSize === 'small' }]">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
<span></span>
</button>
<button @click="setGridSize('medium')" :class="['control-btn', { active: gridSize === 'medium' }]">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="8" height="8"/>
<rect x="3" y="13" width="8" height="8"/>
<rect x="13" y="3" width="8" height="8"/>
<rect x="13" y="13" width="8" height="8"/>
</svg>
<span></span>
</button>
<button @click="setGridSize('large')" :class="['control-btn', { active: gridSize === 'large' }]">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="7"/>
<rect x="3" y="14" width="18" height="7"/>
</svg>
<span></span>
</button>
</div>
</div>
<div class="controls-section">
<label class="control-label">图片适应</label>
<div class="control-buttons">
<button @click="setFitMode('contain')" :class="['control-btn', { active: fitMode === 'contain' }]">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<rect x="7" y="7" width="10" height="10"/>
</svg>
<span>完整</span>
</button>
<button @click="setFitMode('cover')" :class="['control-btn', { active: fitMode === 'cover' }]">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<rect x="2" y="2" width="20" height="20"/>
</svg>
<span>填充</span>
</button>
</div>
</div>
</div>
<!-- 画廊网格 -->
<div class="gallery-grid" :class="`grid-${gridSize}`">
<div v-for="artwork in artworks" :key="artwork.id" class="gallery-item">
<div class="gallery-card" @click="$emit('open-image-viewer', artwork, 0)">
<!-- 图片容器 -->
<div class="image-container" :class="`fit-${fitMode}`">
<img
:src="getPreviewUrl(artwork.files[0].path)"
:alt="artwork.title"
class="gallery-image"
loading="lazy"
@load="onImageLoad"
@error="onImageError"
/>
<!-- 多图徽章 -->
<div v-if="artwork.files.length > 1" class="image-count-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<span>{{ artwork.files.length }}</span>
</div>
<!-- 悬浮信息遮罩 -->
<div class="image-overlay">
<div class="overlay-top">
<h4 class="overlay-title">{{ artwork.title }}</h4>
<p class="overlay-artist">{{ artwork.artist }}</p>
</div>
<div class="overlay-bottom">
<button @click.stop="$emit('open-image-viewer', artwork, 0)" class="overlay-action-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
查看大图
</button>
<button @click.stop="$emit('view-artwork', artwork)" class="overlay-action-btn secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
详情
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
@@ -67,239 +137,432 @@ defineProps<Props>()
defineEmits<Emits>()
// 画廊模式相关
const zoomLevel = ref(1)
const gridSize = ref('medium')
const gridSize = ref<'small' | 'medium' | 'large'>('medium')
const fitMode = ref<'contain' | 'cover'>('contain')
// 画廊模式图片缩放
const zoomIn = () => {
zoomLevel.value = Math.min(zoomLevel.value + 0.1, 3)
}
const zoomOut = () => {
zoomLevel.value = Math.max(zoomLevel.value - 0.1, 0.5)
}
// 画廊模式网格大小
// 设置网格大小
const setGridSize = (size: 'small' | 'medium' | 'large') => {
gridSize.value = size
}
// 画廊模式图片加载和错误处理
const onImageLoad = () => {
// 图片加载成功后可以进行一些操作,例如调整布局
// 设置图片适应模式
const setFitMode = (mode: 'contain' | 'cover') => {
fitMode.value = mode
}
// 图片加载成功
const onImageLoad = (event: Event) => {
const img = event.target as HTMLImageElement
img.classList.add('loaded')
}
// 图片加载失败
const onImageError = (event: Event) => {
console.error('图片加载失败:', (event.target as HTMLImageElement).src)
// 可以显示一个错误提示或替换图片
const img = event.target as HTMLImageElement
console.error('图片加载失败:', img.src)
img.classList.add('error')
// 显示占位图
img.style.display = 'none'
const container = img.parentElement
if (container) {
container.classList.add('has-error')
}
}
</script>
<style scoped>
.gallery-view {
position: relative;
margin-top: 2rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
width: 100%;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6b7280;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
margin: 0 0 0.5rem 0;
color: #1f2937;
font-size: 1.5rem;
}
.empty-state p {
margin: 0;
font-size: 1rem;
}
/* 画廊控制栏 */
.gallery-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 0.5rem 1rem;
flex-wrap: wrap;
gap: 1.5rem;
margin-bottom: 1.5rem;
padding: 1.25rem;
background: white;
border-radius: 0.375rem;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.zoom-controls {
.controls-section {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.75rem;
}
.zoom-btn {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
cursor: pointer;
.control-label {
font-size: 0.875rem;
transition: all 0.2s;
}
.zoom-btn:hover:not(:disabled) {
background: #2563eb;
}
.zoom-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.zoom-level {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
color: #4b5563;
white-space: nowrap;
}
.view-controls {
.control-buttons {
display: flex;
gap: 0.5rem;
background: #f3f4f6;
padding: 0.25rem;
border-radius: 0.5rem;
}
.size-btn {
background: #3b82f6;
color: white;
.control-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border: none;
padding: 0.5rem 0.75rem;
background: transparent;
color: #6b7280;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.size-btn:hover:not(:disabled) {
.control-btn svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.control-btn:hover {
color: #1f2937;
background: #e5e7eb;
}
.control-btn.active {
background: #3b82f6;
color: white;
}
.control-btn.active:hover {
background: #2563eb;
}
.size-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 画廊网格 */
.gallery-grid {
display: grid;
gap: 1rem;
margin-bottom: 2rem;
}
.grid-small .gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
.gallery-grid.grid-small {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
.grid-medium .gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
.gallery-grid.grid-medium {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.grid-large .gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
.gallery-grid.grid-large {
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}
/* 画廊项目 */
.gallery-item {
position: relative;
height: 250px;
border-radius: 0.5rem;
}
.gallery-card {
position: relative;
background: white;
border-radius: 0.75rem;
overflow: hidden;
cursor: pointer;
background: white;
border: 1px solid #e5e7eb;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.gallery-item:hover {
border-color: #3b82f6;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
.gallery-card:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
}
.gallery-image-container {
/* 图片容器 */
.image-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f9fafb;
aspect-ratio: 4 / 3;
overflow: hidden;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.gallery-image {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
transform: scale(v-bind(zoomLevel));
}
.gallery-item:hover .gallery-image {
transform: scale(v-bind(zoomLevel) * 1.05);
}
.gallery-overlay {
.image-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
background: white;
z-index: 0;
}
.gallery-item:hover .gallery-overlay {
.gallery-image {
position: relative;
width: 100%;
height: 100%;
transition: all 0.3s ease;
opacity: 0;
z-index: 1;
}
.gallery-image.loaded {
opacity: 1;
}
.overlay-content {
text-align: center;
color: white;
padding: 1rem;
/* 图片适应模式 */
.image-container.fit-contain .gallery-image {
object-fit: contain;
}
.overlay-content h4 {
margin: 0 0 0.25rem 0;
.image-container.fit-cover .gallery-image {
object-fit: cover;
}
.gallery-card:hover .gallery-image {
transform: scale(1.05);
}
/* 错误状态 */
.image-container.has-error::after {
content: '图片加载失败';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #9ca3af;
font-size: 0.875rem;
text-align: center;
z-index: 2;
}
.overlay-content p {
/* 多图徽章 */
.image-count-badge {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
color: white;
padding: 0.375rem 0.625rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
z-index: 3;
}
.image-count-badge svg {
width: 1rem;
height: 1rem;
stroke-width: 2;
}
/* 悬浮遮罩 */
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.6) 0%,
rgba(0, 0, 0, 0) 30%,
rgba(0, 0, 0, 0) 70%,
rgba(0, 0, 0, 0.8) 100%
);
backdrop-filter: blur(4px);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1rem;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 2;
}
.gallery-card:hover .image-overlay {
opacity: 1;
}
.overlay-top {
color: white;
}
.overlay-title {
margin: 0 0 0.25rem 0;
font-size: 1rem;
font-weight: 600;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-clamp: 2;
display: box;
box-orient: vertical;
}
.overlay-artist {
margin: 0;
font-size: 0.75rem;
opacity: 0.8;
font-size: 0.875rem;
opacity: 0.9;
}
.overlay-actions {
.overlay-bottom {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.overlay-btn {
.overlay-action-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.625rem 1rem;
background: white;
color: #1f2937;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
}
.overlay-btn:hover {
background: #f3f4f6;
.overlay-action-btn svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.overlay-action-btn:hover {
background: #3b82f6;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
.overlay-action-btn.secondary {
background: rgba(255, 255, 255, 0.9);
}
.overlay-action-btn.secondary:hover {
background: rgba(255, 255, 255, 1);
color: #3b82f6;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.gallery-grid.grid-small {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.gallery-grid.grid-medium {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
.gallery-grid.grid-large {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
}
@media (max-width: 768px) {
.gallery-controls {
flex-direction: column;
gap: 1rem;
}
.zoom-controls,
.view-controls {
.controls-section {
width: 100%;
flex-direction: column;
align-items: stretch;
}
.control-buttons {
justify-content: stretch;
}
.control-btn {
flex: 1;
justify-content: center;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
.gallery-grid.grid-small {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.gallery-grid.grid-medium {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.gallery-grid.grid-large {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
.overlay-action-btn span {
display: none;
}
.overlay-action-btn svg {
margin: 0;
}
}
@media (max-width: 480px) {
.image-count-badge {
top: 0.5rem;
right: 0.5rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.image-count-badge svg {
width: 0.875rem;
height: 0.875rem;
}
.overlay-top {
display: none;
}
.image-overlay {
justify-content: flex-end;
}
}
</style>
@@ -1,25 +1,74 @@
<template>
<div class="pagination" v-if="totalPages > 1">
<button @click="$emit('change-page', 1)" :disabled="currentPage <= 1" class="page-btn">
首页
</button>
<button @click="$emit('change-page', currentPage - 1)" :disabled="currentPage <= 1" class="page-btn">
上一页
<!-- 首页 -->
<button
@click="$emit('change-page', 1)"
:disabled="currentPage <= 1"
class="page-btn nav-btn"
title="首页"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="11 17 6 12 11 7"/>
<polyline points="18 17 13 12 18 7"/>
</svg>
</button>
<!-- 上一页 -->
<button
@click="$emit('change-page', currentPage - 1)"
:disabled="currentPage <= 1"
class="page-btn nav-btn"
title="上一页"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="15 18 9 12 15 6"/>
</svg>
</button>
<!-- 页码 -->
<div class="page-numbers">
<button v-for="page in visiblePages" :key="page" @click="$emit('change-page', page)"
:class="['page-btn', { active: page === currentPage }]">
<button
v-for="page in visiblePages"
:key="page"
@click="$emit('change-page', page)"
:class="['page-btn', 'number-btn', { active: page === currentPage }]"
:title="`第 ${page} 页`"
>
{{ page }}
</button>
</div>
<button @click="$emit('change-page', currentPage + 1)" :disabled="currentPage >= totalPages" class="page-btn">
下一页
<!-- 下一页 -->
<button
@click="$emit('change-page', currentPage + 1)"
:disabled="currentPage >= totalPages"
class="page-btn nav-btn"
title="下一页"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="9 18 15 12 9 6"/>
</svg>
</button>
<button @click="$emit('change-page', totalPages)" :disabled="currentPage >= totalPages" class="page-btn">
末页
<!-- 末页 -->
<button
@click="$emit('change-page', totalPages)"
:disabled="currentPage >= totalPages"
class="page-btn nav-btn"
title="末页"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="13 17 18 12 13 7"/>
<polyline points="6 17 11 12 6 7"/>
</svg>
</button>
<!-- 页码信息 -->
<div class="page-info">
<span class="current-page">{{ currentPage }}</span>
<span class="divider">/</span>
<span class="total-pages">{{ totalPages }}</span>
</div>
</div>
</template>
@@ -63,49 +112,148 @@ const visiblePages = computed(() => {
align-items: center;
gap: 0.5rem;
margin-top: 2rem;
padding: 1.5rem 0;
}
.page-btn {
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
display: flex;
align-items: center;
justify-content: center;
min-width: 2.5rem;
height: 2.5rem;
padding: 0.5rem;
border: none;
background: white;
border-radius: 0.375rem;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
color: #4b5563;
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.page-btn:hover:not(:disabled) {
.page-btn svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.page-btn:hover:not(:disabled):not(.active) {
background: #f3f4f6;
border-color: #3b82f6;
color: #1f2937;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.page-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
.page-btn:active:not(:disabled) {
transform: translateY(0);
}
.page-btn:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
box-shadow: none;
}
/* 导航按钮 */
.nav-btn {
min-width: 2.5rem;
}
/* 页码按钮 */
.number-btn {
min-width: 2.5rem;
font-weight: 600;
}
.number-btn.active {
background: #3b82f6;
color: white;
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
.number-btn.active:hover {
background: #2563eb;
transform: translateY(-1px);
}
.page-numbers {
display: flex;
gap: 0.25rem;
gap: 0.375rem;
}
/* 页码信息 */
.page-info {
display: flex;
align-items: center;
gap: 0.375rem;
margin-left: 0.5rem;
padding: 0.5rem 0.875rem;
background: #f9fafb;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: #6b7280;
}
.current-page {
color: #3b82f6;
font-weight: 600;
}
.divider {
color: #d1d5db;
margin: 0 0.125rem;
}
.total-pages {
color: #9ca3af;
}
/* 响应式设计 */
@media (max-width: 768px) {
.pagination {
flex-wrap: wrap;
gap: 0.375rem;
padding: 1rem 0;
}
.page-btn {
min-width: 2.25rem;
height: 2.25rem;
}
.page-numbers {
order: 3;
order: -1;
width: 100%;
justify-content: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
gap: 0.25rem;
}
.page-info {
order: -2;
width: 100%;
justify-content: center;
margin: 0 0 0.75rem 0;
}
.nav-btn {
flex: 1;
}
}
@media (max-width: 480px) {
.page-btn {
min-width: 2rem;
height: 2rem;
font-size: 0.8125rem;
}
.page-btn svg {
width: 1rem;
height: 1rem;
}
}
</style>
@@ -1,26 +1,65 @@
<template>
<div class="search-panel">
<div class="search-filters">
<div class="search-container">
<!-- 搜索框 -->
<div class="search-box">
<input v-model="searchQuery" type="text" placeholder="搜索作品标题、作者名称..." class="search-input"
@input="debounceSearch" />
<button @click="clearSearch" class="clear-btn" v-if="searchQuery">
<div class="search-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
</div>
<input
v-model="searchQuery"
type="text"
placeholder="搜索作品标题、作者名称..."
class="search-input"
@input="debounceSearch"
@keyup.enter="emit('search', searchQuery)"
/>
<button v-if="searchQuery" @click="clearSearch" class="clear-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<!-- 筛选控制 -->
<div class="filter-controls">
<select v-model="sortBy" @change="handleSortChange" class="filter-select">
<option value="date">按日期排序</option>
<option value="name">按名称排序</option>
<option value="size">按大小排序</option>
</select>
<div class="filter-group">
<label class="filter-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="4" y1="21" x2="4" y2="14"/>
<line x1="4" y1="10" x2="4" y2="3"/>
<line x1="12" y1="21" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12" y2="3"/>
<line x1="20" y1="21" x2="20" y2="16"/>
<line x1="20" y1="12" x2="20" y2="3"/>
<line x1="1" y1="14" x2="7" y2="14"/>
<line x1="9" y1="8" x2="15" y2="8"/>
<line x1="17" y1="16" x2="23" y2="16"/>
</svg>
</label>
<select v-model="sortBy" @change="handleSortChange" class="filter-select">
<option value="date">按日期</option>
<option value="name">按名称</option>
<option value="size">按大小</option>
</select>
</div>
<select v-model="filterBy" @change="handleFilterChange" class="filter-select">
<option value="all">全部</option>
<option value="images">仅图片</option>
<option value="videos">仅视频</option>
</select>
<div class="filter-group">
<label class="filter-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
</svg>
</label>
<select v-model="filterBy" @change="handleFilterChange" class="filter-select">
<option value="all">全部类型</option>
<option value="images">仅图片</option>
<option value="videos">仅视频</option>
</select>
</div>
</div>
</div>
</div>
@@ -85,75 +124,184 @@ watch(() => props.initialQuery, (newQuery) => {
<style scoped>
.search-panel {
margin-bottom: 2rem;
margin-bottom: 1.5rem;
}
.search-filters {
.search-container {
display: flex;
gap: 1rem;
align-items: center;
align-items: stretch;
background: white;
padding: 1rem;
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* 搜索框 */
.search-box {
position: relative;
flex: 1;
max-width: 400px;
display: flex;
align-items: center;
background: #f9fafb;
border-radius: 0.5rem;
border: 2px solid transparent;
transition: all 0.2s;
}
.search-box:focus-within {
background: white;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.search-icon {
display: flex;
align-items: center;
padding: 0 0.75rem;
color: #6b7280;
}
.search-icon svg {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
}
.search-input {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 0.875rem;
flex: 1;
padding: 0.75rem 0;
border: none;
background: transparent;
font-size: 0.9375rem;
outline: none;
color: #1f2937;
}
.search-input::placeholder {
color: #9ca3af;
}
.clear-btn {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
margin-right: 0.5rem;
background: transparent;
border: none;
color: #6b7280;
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
border-radius: 0.375rem;
transition: all 0.2s;
}
.clear-btn svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.clear-btn:hover {
background: #f3f4f6;
background: #e5e7eb;
color: #1f2937;
}
/* 筛选控制 */
.filter-controls {
display: flex;
gap: 0.75rem;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
background: #f9fafb;
padding: 0.5rem;
border-radius: 0.5rem;
border: 2px solid transparent;
transition: all 0.2s;
}
.filter-group:focus-within {
background: white;
border-color: #3b82f6;
}
.filter-label {
display: flex;
align-items: center;
color: #6b7280;
padding: 0 0.25rem;
}
.filter-label svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.filter-select {
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: white;
padding: 0.375rem 0.75rem;
border: none;
background: transparent;
font-size: 0.875rem;
font-weight: 500;
color: #1f2937;
cursor: pointer;
outline: none;
appearance: none;
padding-right: 1.5rem;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 5L6 8L9 5' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.25rem center;
}
@media (max-width: 768px) {
.search-filters {
flex-direction: column;
width: 100%;
.filter-select:hover {
color: #3b82f6;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.search-container {
flex-wrap: wrap;
}
.search-box {
max-width: none;
min-width: 100%;
}
.filter-controls {
flex: 1;
}
}
@media (max-width: 768px) {
.search-container {
flex-direction: column;
gap: 0.75rem;
}
.filter-controls {
width: 100%;
flex-wrap: wrap;
}
.filter-select {
.filter-group {
flex: 1;
min-width: calc(50% - 0.375rem);
}
}
@media (max-width: 480px) {
.filter-controls {
flex-direction: column;
}
.filter-group {
min-width: 100%;
}
}
</style>
@@ -1,19 +1,47 @@
<template>
<div class="view-toggle">
<button class="view-btn" :class="{ active: modelValue === 'artists' }"
@click="$emit('update:modelValue', 'artists')">
<span class="btn-icon">👥</span>
按作者浏览
<button
class="view-btn"
:class="{ active: modelValue === 'artists' }"
@click="$emit('update:modelValue', 'artists')"
:title="'按作者浏览'"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="btn-text">作者</span>
</button>
<button class="view-btn" :class="{ active: modelValue === 'artworks' }"
@click="$emit('update:modelValue', 'artworks')">
<span class="btn-icon">🖼</span>
所有作品
<button
class="view-btn"
:class="{ active: modelValue === 'artworks' }"
@click="$emit('update:modelValue', 'artworks')"
:title="'作品列表'"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
</svg>
<span class="btn-text">列表</span>
</button>
<button class="view-btn" :class="{ active: modelValue === 'gallery' }"
@click="$emit('update:modelValue', 'gallery')">
<span class="btn-icon">🎨</span>
画廊模式
<button
class="view-btn"
:class="{ active: modelValue === 'gallery' }"
@click="$emit('update:modelValue', 'gallery')"
:title="'画廊模式'"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<span class="btn-text">画廊</span>
</button>
</div>
</template>
@@ -33,38 +61,72 @@ defineEmits<Emits>()
<style scoped>
.view-toggle {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
display: inline-flex;
gap: 0;
background: #f3f4f6;
padding: 0.25rem;
border-radius: 0.625rem;
margin-bottom: 1.5rem;
}
.view-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border: 1px solid #d1d5db;
background: white;
border-radius: 0.375rem;
padding: 0.625rem 1.125rem;
border: none;
background: transparent;
color: #6b7280;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
}
.view-btn svg {
width: 1.125rem;
height: 1.125rem;
stroke-width: 2;
}
.view-btn:hover:not(.active) {
color: #1f2937;
background: rgba(255, 255, 255, 0.6);
}
.view-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
background: white;
color: #3b82f6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.btn-icon {
font-size: 1rem;
.btn-text {
font-weight: 500;
}
@media (max-width: 768px) {
.view-toggle {
width: 100%;
justify-content: center;
display: flex;
}
.view-btn {
flex: 1;
padding: 0.75rem 0.875rem;
}
.btn-text {
display: none;
}
}
@media (max-width: 480px) {
.view-btn svg {
width: 1.25rem;
height: 1.25rem;
}
}
</style>
+29
View File
@@ -209,6 +209,32 @@ export const useRepositoryStore = defineStore('repository', () => {
})
}
// 快速扫描 - 仅获取基本信息
const quickScan = async () => {
return await apiCall('/quick-scan')
}
// 完整扫描 - 支持并发和缓存
const scanRepository = async (options: {
maxConcurrency?: number
useCache?: boolean
forceRefresh?: boolean
} = {}) => {
return await apiCall('/scan', {
method: 'POST',
body: JSON.stringify({
maxConcurrency: options.maxConcurrency || 5, // 减少默认并发数
useCache: options.useCache !== false,
forceRefresh: options.forceRefresh === true,
}),
})
}
// 清除扫描缓存
const clearScanCache = async () => {
return await apiCall('/clear-scan-cache', { method: 'POST' })
}
return {
// 状态
config,
@@ -232,5 +258,8 @@ export const useRepositoryStore = defineStore('repository', () => {
checkArtworkDownloaded,
checkDirectoryExists,
migrateFromOldToNew,
quickScan,
scanRepository,
clearScanCache,
}
})
+155
View File
@@ -20,6 +20,26 @@
</button>
</div>
<!-- 扫描控制面板 -->
<div class="scan-controls">
<div class="scan-buttons">
<button @click="performFullScan" :disabled="isScanning" class="scan-button">
{{ isScanning ? '扫描中...' : '完整扫描' }}
</button>
<button @click="clearCache" class="clear-cache-button">
清除缓存
</button>
</div>
<!-- 扫描进度 -->
<div v-if="isScanning" class="scan-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: scanProgress + '%' }"></div>
</div>
<div class="progress-text">{{ scanStatus }}</div>
</div>
</div>
<!-- 配置管理 -->
<div v-if="activeTab === 'config'" class="tab-content">
<RepositoryConfigComponent :config="config" :migrating="migrating" :migration-progress="migrationProgress"
@@ -65,6 +85,11 @@ const config = ref<RepositoryConfig>({
migrationRules: []
})
// 扫描相关状态
const isScanning = ref(false)
const scanProgress = ref(0)
const scanStatus = ref('')
// 浏览相关
const searchQuery = ref('')
const viewMode = ref('artworks') // 默认显示作品模式
@@ -96,6 +121,15 @@ onMounted(async () => {
// 加载统计信息
const loadStats = async () => {
try {
// 首先尝试快速扫描获取基本信息
try {
const quickResult = await repositoryStore.quickScan()
console.log('快速扫描结果:', quickResult)
} catch (error) {
console.warn('快速扫描失败,使用传统方法:', error)
}
// 然后获取完整统计信息
stats.value = await repositoryStore.getStats()
} catch (error: any) {
console.error('加载统计信息失败:', error)
@@ -408,6 +442,49 @@ const handleConfigSaved = async () => {
console.error('配置保存后刷新数据失败:', error)
}
}
// 执行完整扫描
const performFullScan = async () => {
try {
isScanning.value = true
scanProgress.value = 0
scanStatus.value = '开始扫描...'
const result = await repositoryStore.scanRepository({
maxConcurrency: 5, // 减少并发数,避免文件句柄过多
useCache: true,
forceRefresh: true
})
scanStatus.value = '扫描完成'
scanProgress.value = 100
// 重新加载数据
await loadStats()
await loadArtists()
await loadAllArtworks(1)
alert(`扫描完成!发现 ${result.artworks.length} 个作品,${result.artists.length} 个作者`)
} catch (error: any) {
console.error('扫描失败:', error)
alert('扫描失败: ' + error.message)
} finally {
isScanning.value = false
scanStatus.value = ''
scanProgress.value = 0
}
}
// 清除扫描缓存
const clearCache = async () => {
try {
await repositoryStore.clearScanCache()
alert('扫描缓存已清除')
} catch (error: any) {
console.error('清除缓存失败:', error)
alert('清除缓存失败: ' + error.message)
}
}
</script>
<style scoped>
@@ -471,9 +548,87 @@ const handleConfigSaved = async () => {
padding: 2rem;
}
.scan-controls {
background: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.scan-buttons {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.scan-button {
background: #3b82f6;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.scan-button:hover:not(:disabled) {
background: #2563eb;
}
.scan-button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.clear-cache-button {
background: #6b7280;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.clear-cache-button:hover {
background: #4b5563;
}
.scan-progress {
margin-top: 1rem;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.875rem;
color: #6b7280;
text-align: center;
}
@media (max-width: 768px) {
.container {
padding: 0 1rem;
}
.scan-buttons {
flex-direction: column;
}
}
</style>