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 @@