diff --git a/backend/routes/artwork.js b/backend/routes/artwork.js index a9ea801..33b7dfb 100644 --- a/backend/routes/artwork.js +++ b/backend/routes/artwork.js @@ -10,6 +10,7 @@ router.get('/search', async (req, res) => { try { const { keyword, + tags, type = 'all', sort = 'date_desc', duration = 'all', @@ -17,16 +18,27 @@ router.get('/search', async (req, res) => { limit = 30 } = req.query; - if (!keyword) { + // 处理标签参数 + let tagsArray = []; + if (tags) { + if (Array.isArray(tags)) { + tagsArray = tags; + } else { + tagsArray = [tags]; + } + } + + if (!keyword && (!tagsArray || tagsArray.length === 0)) { return res.status(400).json({ success: false, - error: 'Search keyword is required' + error: 'Search keyword or tags are required' }); } const artworkService = new ArtworkService(req.backend.getAuth()); const result = await artworkService.searchArtworks({ keyword, + tags: tagsArray, type, sort, duration, diff --git a/backend/services/artwork.js b/backend/services/artwork.js index 8651d9b..3373ba7 100644 --- a/backend/services/artwork.js +++ b/backend/services/artwork.js @@ -151,6 +151,7 @@ class ArtworkService { try { const { keyword, + tags, type = 'all', sort = 'date_desc', duration = 'all', @@ -159,10 +160,10 @@ class ArtworkService { } = searchOptions; // 验证搜索参数 - if (!keyword || keyword.trim() === '') { + if ((!keyword || keyword.trim() === '') && (!tags || tags.length === 0)) { return { success: false, - error: 'Search keyword is required' + error: 'Search keyword or tags are required' }; } @@ -187,8 +188,17 @@ class ArtworkService { 'within_last_month': 'within_last_month' }; + // 构建搜索关键词 + let searchWord = ''; + if (keyword && keyword.trim()) { + searchWord = keyword.trim(); + } else if (tags && tags.length > 0) { + // 将标签数组转换为搜索关键词,用空格分隔 + searchWord = tags.join(' '); + } + const params = { - word: keyword.trim(), + word: searchWord, search_target: searchTargetMap[type] || 'partial_match_for_tags', sort: sortMap[sort] || 'date_desc', offset: parseInt(offset) || 0, diff --git a/ui/dist.zip b/ui/dist.zip index a385437..87930c0 100644 Binary files a/ui/dist.zip and b/ui/dist.zip differ diff --git a/ui/src/services/artwork.ts b/ui/src/services/artwork.ts index d757cb8..f83f936 100644 --- a/ui/src/services/artwork.ts +++ b/ui/src/services/artwork.ts @@ -56,7 +56,19 @@ class ArtworkService { */ async searchArtworks(params: SearchParams): Promise> { const queryParams = new URLSearchParams(); - queryParams.append('keyword', params.keyword); + + // 处理关键词搜索 + if (params.keyword) { + queryParams.append('keyword', params.keyword); + } + + // 处理标签搜索 + if (params.tags && params.tags.length > 0) { + params.tags.forEach(tag => { + queryParams.append('tags', tag); + }); + } + if (params.type) queryParams.append('type', params.type); if (params.sort) queryParams.append('sort', params.sort); if (params.duration) queryParams.append('duration', params.duration); diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index ac2257b..3884e11 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -114,7 +114,8 @@ export interface DownloadTask { // 搜索参数 export interface SearchParams { - keyword: string; + keyword?: string; + tags?: string[]; type?: 'all' | 'art' | 'manga' | 'novel'; sort?: 'date_desc' | 'date_asc' | 'popular_desc'; duration?: 'all' | 'within_last_day' | 'within_last_week' | 'within_last_month'; diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue index 34c1e03..ad07786 100644 --- a/ui/src/views/ArtworkView.vue +++ b/ui/src/views/ArtworkView.vue @@ -158,13 +158,16 @@

标签

- {{ tag.name }} - +
@@ -534,6 +537,88 @@ const goBackToArtist = () => { } }; +// 选中的标签状态 +const selectedTags = ref([]); +const isCtrlPressed = ref(false); + +// 处理标签点击 +const handleTagClick = (event: MouseEvent, tagName: string) => { + // 阻止默认行为 + event.preventDefault(); + + console.log('标签点击事件:', { + tagName, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + isCtrlPressed: isCtrlPressed.value + }); + + // 如果按住Ctrl键,则添加到选中状态(不跳转) + if (event.ctrlKey || event.metaKey || isCtrlPressed.value) { + console.log('检测到Ctrl键,添加到选中状态'); + + // 切换标签的选中状态 + const index = selectedTags.value.indexOf(tagName); + if (index > -1) { + // 如果已选中,则取消选中 + selectedTags.value.splice(index, 1); + } else { + // 如果未选中,则添加到选中列表 + selectedTags.value.push(tagName); + } + + console.log('当前选中的标签:', selectedTags.value); + + // 保存到sessionStorage + sessionStorage.setItem('currentSearchTags', JSON.stringify(selectedTags.value)); + + // 不跳转,只是更新选中状态 + } else { + console.log('普通点击,执行单标签搜索'); + // 普通点击,只搜索当前标签,清除之前的多标签选择 + selectedTags.value = []; + sessionStorage.removeItem('currentSearchTags'); + + router.push({ + path: '/search', + query: { + mode: 'tags', + tag: tagName + } + }); + } +}; + +// 处理键盘事件 +const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Control' || event.key === 'Meta') { + isCtrlPressed.value = true; + } +}; + +const handleKeyUp = (event: KeyboardEvent) => { + if (event.key === 'Control' || event.key === 'Meta') { + isCtrlPressed.value = false; + + // 当松开Ctrl键时,如果有选中的标签,则跳转到搜索页面 + if (selectedTags.value.length > 0) { + console.log('松开Ctrl键,跳转到搜索页面,标签:', selectedTags.value); + + router.push({ + path: '/search', + query: { + mode: 'tags', + tags: selectedTags.value + } + }); + + // 清空选中状态 + selectedTags.value = []; + sessionStorage.removeItem('currentSearchTags'); + } + } +}; + // 监听路由变化,重新获取作品详情和导航数据 watch(() => route.params.id, () => { // 清理之前的任务状态 @@ -573,11 +658,15 @@ onMounted(() => { // 添加键盘事件监听 document.addEventListener('keydown', handleKeydown); + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); }); // 组件卸载时移除事件监听和清理SSE连接 onUnmounted(() => { document.removeEventListener('keydown', handleKeydown); + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('keyup', handleKeyUp); stopTaskStreaming(); }); @@ -841,6 +930,35 @@ onUnmounted(() => { border-radius: 1rem; font-size: 0.875rem; line-height: 1; + border: none; + cursor: pointer; + transition: all 0.2s; +} + +.tag-clickable { + background: #e0f2fe; + color: #0369a1; + border: 1px solid #bae6fd; +} + +.tag-clickable:hover { + background: #bae6fd; + color: #075985; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.tag-selected { + background: #3b82f6 !important; + color: white !important; + border-color: #2563eb !important; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); +} + +.tag-selected:hover { + background: #2563eb !important; + color: white !important; } .artwork-description { diff --git a/ui/src/views/SearchView.vue b/ui/src/views/SearchView.vue index d52e1c3..9657aff 100644 --- a/ui/src/views/SearchView.vue +++ b/ui/src/views/SearchView.vue @@ -10,6 +10,9 @@ + @@ -30,6 +33,45 @@ + +
+
+ + +
+ + +
+
+ + {{ tag }} + + +
+ +
+
+
\ No newline at end of file