diff --git a/backend/services/repository.js b/backend/services/repository.js index e2ed921..9418159 100644 --- a/backend/services/repository.js +++ b/backend/services/repository.js @@ -740,18 +740,28 @@ class RepositoryService { // 删除作品 async deleteArtwork(artworkId) { try { - const artwork = await this.findArtworkById(artworkId) + // 优化:直接通过文件系统查找,避免全仓库扫描 + const artwork = await this.findArtworkByIdOptimized(artworkId) if (!artwork) { throw new Error('作品不存在') } await fs.rm(artwork.path, { recursive: true, force: true }) - // 检查作者目录是否为空,如果为空则删除 + // 优化:直接检查作者目录是否为空,避免重复扫描 const artistDir = artwork.artistPath - const artistArtworks = await this.getArtworksByArtist(artwork.artist) - if (artistArtworks.artworks.length === 0) { - await fs.rmdir(artistDir) + try { + const artistEntries = await fs.readdir(artistDir, { withFileTypes: true }) + const hasArtworks = artistEntries.some(entry => + entry.isDirectory() && entry.name.match(/^\d+_/) + ) + + if (!hasArtworks) { + await fs.rmdir(artistDir) + } + } catch (error) { + // 如果读取目录失败,可能目录已经不存在,忽略错误 + logger.warn(`检查作者目录失败: ${error.message}`) } return { success: true, message: '作品删除成功' } @@ -760,6 +770,59 @@ class RepositoryService { } } + // 优化的作品查找方法:直接通过文件系统查找,避免全仓库扫描 + async findArtworkByIdOptimized(artworkId) { + try { + // 确保配置已加载 + await this.loadConfig() + + // 使用当前配置的目录 + const currentBaseDir = this.getCurrentBaseDir() + + // 扫描所有作者目录 + const artistEntries = await fs.readdir(currentBaseDir, { withFileTypes: true }) + + for (const artistEntry of artistEntries) { + if (!artistEntry.isDirectory()) continue + + // 跳过配置文件和隐藏文件 + if (artistEntry.name.startsWith('.') || artistEntry.name === '.repository-config.json') { + continue + } + + const artistName = artistEntry.name + const artistPath = path.join(currentBaseDir, artistName) + + // 扫描作者下的作品目录 + const artworkEntries = await fs.readdir(artistPath, { withFileTypes: true }) + + for (const artworkEntry of artworkEntries) { + if (!artworkEntry.isDirectory()) continue + + // 检查是否是目标作品目录(包含数字ID) + const artworkMatch = artworkEntry.name.match(/^(\d+)_(.+)$/) + if (artworkMatch && artworkMatch[1] === artworkId.toString()) { + const artworkPath = path.join(artistPath, artworkEntry.name) + const title = artworkMatch[2] + + // 找到目标作品,返回基本信息(不需要扫描文件详情) + return { + id: artworkId, + title: title, + artist: artistName, + artistPath: artistPath, + path: artworkPath + } + } + } + } + + return null // 未找到作品 + } catch (error) { + throw new Error(`查找作品失败: ${error.message}`) + } + } + // 加载持久化缓存 async loadPersistentCache() { try { @@ -964,4 +1027,4 @@ class RepositoryService { } } -module.exports = RepositoryService \ No newline at end of file +module.exports = RepositoryService \ No newline at end of file diff --git a/ui/src/assets/theme.css b/ui/src/assets/theme.css index f78f9ae..413f78d 100644 --- a/ui/src/assets/theme.css +++ b/ui/src/assets/theme.css @@ -26,8 +26,9 @@ --color-warning-light: #fef3c7; --color-danger: #ef4444; --color-danger-light: #fee2e2; - --color-info: #06b6d4; - --color-info-light: #cffafe; + --color-danger-dark: #dc2626; + --color-info: #0284c7; + --color-info-light: #e0f2fe; /* 阴影 */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); @@ -130,4 +131,366 @@ .btn-enhanced:active { transform: translateY(0); box-shadow: var(--shadow-sm); +} + +/* 通用按钮样式 */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md) var(--spacing-xl); + border-radius: var(--radius-md); + font-weight: 600; + text-decoration: none; + transition: all var(--transition-normal); + border: none; + cursor: pointer; + font-size: 1rem; + line-height: 1; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-sm { + padding: var(--spacing-sm) var(--spacing-lg); + font-size: 0.875rem; +} + +.btn-lg { + padding: var(--spacing-lg) var(--spacing-2xl); + font-size: 1.125rem; +} + +.btn-primary { + background: var(--color-primary); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background: var(--color-primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + border: 1px solid var(--color-border); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--color-bg-secondary); + border-color: var(--color-border-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.btn-warning { + background: var(--color-warning-light); + color: var(--color-warning); + border: 1px solid var(--color-warning); +} + +.btn-warning:hover:not(:disabled) { + background: var(--color-warning); + color: white; + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-danger { + background: var(--color-danger-light); + color: var(--color-danger); + border: 1px solid var(--color-danger); +} + +.btn-danger:hover:not(:disabled) { + background: var(--color-danger); + color: white; + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-text { + background: none; + color: var(--color-primary); + padding: var(--spacing-sm) var(--spacing-lg); +} + +.btn-text:hover:not(:disabled) { + background: var(--color-bg-tertiary); +} + +/* 卡片容器样式 */ +.card { + background: var(--color-bg-primary); + border-radius: var(--radius-xl); + padding: var(--spacing-2xl); + box-shadow: var(--shadow-md); + border: 1px solid var(--color-border); +} + +.card-sm { + padding: var(--spacing-xl); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-xl); +} + +.card-title { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0; +} + +.card-title-sm { + font-size: 1.25rem; +} + +.card-actions { + display: flex; + gap: var(--spacing-md); +} + +/* 网格布局 */ +.grid { + display: grid; + gap: var(--spacing-2xl); +} + +.grid-auto-fit { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +.grid-auto-fill { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); +} + +.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } +.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } + +/* 状态指示器 */ +.status-indicator { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--radius-lg); + font-size: 0.875rem; + font-weight: 500; +} + +.status-success { + background: var(--color-success-light); + color: var(--color-success); + border: 1px solid var(--color-success); +} + +.status-warning { + background: var(--color-warning-light); + color: var(--color-warning); + border: 1px solid var(--color-warning); +} + +.status-danger { + background: var(--color-danger-light); + color: var(--color-danger); + border: 1px solid var(--color-danger); +} + +.status-info { + background: var(--color-info-light); + color: var(--color-info); + border: 1px solid var(--color-info); +} + +/* 进度条 */ +.progress-bar { + flex: 1; + height: 0.5rem; + background: var(--color-bg-tertiary); + border-radius: var(--radius-sm); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: var(--color-success); + transition: width 0.3s ease; +} + +.progress-fill.progress-warning { + background: var(--color-warning); +} + +.progress-fill.progress-danger { + background: var(--color-danger); +} + +/* 统计项 */ +.stat-item { + background: var(--color-bg-secondary); + padding: var(--spacing-lg); + border-radius: var(--radius-md); + border: 1px solid var(--color-border); +} + +.stat-label { + font-size: 0.875rem; + color: var(--color-text-secondary); + margin-bottom: var(--spacing-sm); + font-weight: 500; +} + +.stat-value { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text-primary); + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +/* 标签样式 */ +.tag { + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + padding: var(--spacing-xs) var(--spacing-md); + border-radius: var(--radius-xl); + font-size: 0.875rem; + line-height: 1; + border: 1px solid var(--color-border); + cursor: pointer; + transition: all var(--transition-normal); +} + +.tag-clickable { + background: var(--color-info-light); + color: var(--color-info); + border-color: var(--color-info); +} + +.tag-clickable:hover { + background: var(--color-info); + color: white; + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.tag-selected { + background: var(--color-primary) !important; + color: white !important; + border-color: var(--color-primary-dark) !important; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); +} + +/* 切换开关 */ +.toggle-switch { + position: relative; + width: 28px; + height: 14px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--color-border); + transition: var(--transition-normal); + border-radius: 14px; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 10px; + width: 10px; + left: 2px; + bottom: 2px; + border-radius: 50%; + background-color: white; + transition: var(--transition-normal); +} + +.toggle-switch input:checked + .toggle-slider { + background-color: var(--color-primary); +} + +.toggle-switch input:focus + .toggle-slider { + box-shadow: 0 0 1px var(--color-primary); +} + +.toggle-switch input:checked + .toggle-slider:before { + transform: translateX(14px); +} + +/* 加载状态 */ +.loading { + text-align: center; + color: var(--color-text-secondary); + padding: var(--spacing-2xl); +} + +.error-message { + background: var(--color-danger-light); + color: var(--color-danger-dark); + padding: var(--spacing-md); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-lg); + border: 1px solid var(--color-danger); +} + +/* 响应式工具类 */ +@media (max-width: 768px) { + .mobile-stack { + flex-direction: column !important; + } + + .mobile-full { + width: 100% !important; + } + + .mobile-hide { + display: none !important; + } + + .grid-auto-fill { + grid-template-columns: 1fr; + } + + .card { + padding: var(--spacing-xl); + } + + .card-header { + flex-direction: column; + gap: var(--spacing-lg); + align-items: stretch; + } + + .card-actions { + flex-direction: column; + } } \ No newline at end of file diff --git a/ui/src/components/artist/ArtistCard.vue b/ui/src/components/artist/ArtistCard.vue index 10600c8..1297e6c 100644 --- a/ui/src/components/artist/ArtistCard.vue +++ b/ui/src/components/artist/ArtistCard.vue @@ -82,7 +82,7 @@ const getImageUrl = getImageProxyUrl \ No newline at end of file diff --git a/ui/src/components/cache/CacheManager.vue b/ui/src/components/cache/CacheManager.vue index 5bcb9ee..c2f5c42 100644 --- a/ui/src/components/cache/CacheManager.vue +++ b/ui/src/components/cache/CacheManager.vue @@ -190,150 +190,105 @@ onMounted(() => { \ No newline at end of file + \ No newline at end of file diff --git a/ui/src/components/home/RandomRecommendations.vue b/ui/src/components/home/RandomRecommendations.vue index d28f429..2d0c39f 100644 --- a/ui/src/components/home/RandomRecommendations.vue +++ b/ui/src/components/home/RandomRecommendations.vue @@ -9,7 +9,9 @@ - +
+
+
刷新推荐 @@ -170,8 +172,8 @@ watch(() => hasFollowingArtists.value, (newValue) => { \ No newline at end of file diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue index cd14907..85081b4 100644 --- a/ui/src/views/ArtworkView.vue +++ b/ui/src/views/ArtworkView.vue @@ -22,12 +22,11 @@ @page-change="currentImagePage = $event" /> - @@ -71,6 +70,7 @@ const loading = ref(false); const error = ref(null); const currentImagePage = ref(0); // 当前图片页面 const downloading = ref(false); +const deleting = ref(false); // 新增删除中状态 const isDownloaded = ref(false); // 下载任务状态 - 使用Pinia store @@ -252,7 +252,7 @@ const checkDownloadStatus = async (artworkId: number, retryCount = 0) => { // 下载作品 const handleDownload = async () => { - if (!artwork.value) return; + if (!artwork.value || downloading.value) return; // 防止重复点击 try { // 清理下载状态 @@ -326,30 +326,109 @@ watch(currentTask, (newTask, oldTask) => { } }, { immediate: true }); -// 收藏/取消收藏 -const handleBookmark = async () => { - if (!artwork.value) return; +// 收藏/取消收藏 - 已注释,功能暂时不可用 +// const handleBookmark = async () => { +// if (!artwork.value) return; + +// try { +// const action = artwork.value.is_bookmarked ? 'remove' : 'add'; +// const response = await artworkService.toggleBookmark(artwork.value.id, action); + +// if (response.success && response.data) { +// // 更新作品状态 +// artwork.value.is_bookmarked = response.data.is_bookmarked; +// artwork.value.total_bookmarks += artwork.value.is_bookmarked ? 1 : -1; + +// // 显示成功消息 +// console.log(response.data.message); +// } else { +// // 显示错误提示给用户 +// bookmarkError.value = response.error || '收藏操作失败'; +// console.error('收藏操作失败:', response.error); +// } +// } catch (err) { +// // 显示错误提示给用户 +// bookmarkError.value = '藏暂时不可用,请去官方收藏或者取消收藏'; +// console.error('收藏操作失败:', err); +// } +// }; + +// 删除作品 +const handleDelete = async () => { + if (!artwork.value || deleting.value) return; // 防止重复点击 + + // 确认删除 + if (!confirm(`确定要删除作品 "${artwork.value.title}" 吗?此操作不可恢复。`)) { + return; + } + + deleting.value = true; // 设置删除中状态 try { - const action = artwork.value.is_bookmarked ? 'remove' : 'add'; - const response = await artworkService.toggleBookmark(artwork.value.id, action); + const response = await repositoryStore.deleteArtwork(artwork.value.id.toString()); - if (response.success && response.data) { - // 更新作品状态 - artwork.value.is_bookmarked = response.data.is_bookmarked; - artwork.value.total_bookmarks += artwork.value.is_bookmarked ? 1 : -1; - - // 显示成功消息 - console.log(response.data.message); + if (response.success) { + // 删除成功后更新本地状态 + isDownloaded.value = false; + + // 显示成功提示(非阻塞) + const successMessage = document.createElement('div'); + successMessage.textContent = '作品删除成功'; + successMessage.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: var(--color-success); + color: white; + padding: 12px 20px; + border-radius: 6px; + z-index: 9999; + font-size: 14px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + `; + document.body.appendChild(successMessage); + + // 3秒后自动移除提示 + setTimeout(() => { + if (successMessage.parentNode) { + successMessage.parentNode.removeChild(successMessage); + } + }, 3000); + + // 不退出页面,保持在当前页面 } else { - // 显示错误提示给用户 - bookmarkError.value = response.error || '收藏操作失败'; - console.error('收藏操作失败:', response.error); + throw new Error(response.error || '删除失败'); } } catch (err) { - // 显示错误提示给用户 - bookmarkError.value = '藏暂时不可用,请去官方收藏或者取消收藏'; - console.error('收藏操作失败:', err); + const errorMessage = err instanceof Error ? err.message : '删除作品失败'; + + // 显示错误提示(非阻塞) + const errorDiv = document.createElement('div'); + errorDiv.textContent = '删除失败: ' + errorMessage; + errorDiv.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: var(--color-danger); + color: white; + padding: 12px 20px; + border-radius: 6px; + z-index: 9999; + font-size: 14px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + `; + document.body.appendChild(errorDiv); + + // 3秒后自动移除提示 + setTimeout(() => { + if (errorDiv.parentNode) { + errorDiv.parentNode.removeChild(errorDiv); + } + }, 3000); + + console.error('删除作品失败:', err); + } finally { + deleting.value = false; // 重置删除状态 } }; @@ -755,7 +834,7 @@ onMounted(() => { // 初始化推荐开关状态 initializeRecommendationsState(); - + // 初始化 Caption 开关状态 initializeCaptionState(); });