diff --git a/routes/api_routes.py b/routes/api_routes.py index 3682c61e..5c54a871 100644 --- a/routes/api_routes.py +++ b/routes/api_routes.py @@ -107,6 +107,7 @@ class ApiRoutes: page_size = int(request.query.get('page_size', '20')) sort_by = request.query.get('sort_by', 'name') folder = request.query.get('folder') + search = request.query.get('search', '').lower() # Validate parameters if page < 1 or page_size < 1 or page_size > 100: @@ -119,12 +120,13 @@ class ApiRoutes: 'error': 'Invalid sort parameter' }, status=400) - # Get paginated data + # Get paginated data with search result = await self.scanner.get_paginated_data( page=page, page_size=page_size, sort_by=sort_by, - folder=folder + folder=folder, + search=search ) # Format the response data @@ -133,6 +135,10 @@ class ApiRoutes: for item in result['items'] ] + logger.info(f"API response - Total items: {result['total']}, " + f"Page items: {len(formatted_items)}, " + f"Total pages: {result['total_pages']}") + return web.json_response({ 'items': formatted_items, 'total': result['total'], @@ -141,13 +147,8 @@ class ApiRoutes: 'total_pages': result['total_pages'] }) - except ValueError as e: - return web.json_response({ - 'error': 'Invalid parameters' - }, status=400) - except Exception as e: - logger.error(f"Error fetching loras: {e}", exc_info=True) + logger.error(f"Error in get_loras: {str(e)}", exc_info=True) return web.json_response({ 'error': 'Internal server error' }, status=500) diff --git a/services/lora_scanner.py b/services/lora_scanner.py index 92c875ea..7c67dffb 100644 --- a/services/lora_scanner.py +++ b/services/lora_scanner.py @@ -95,31 +95,39 @@ class LoraScanner: page: int, page_size: int, sort_by: str = 'date', - folder: Optional[str] = None) -> Dict: - """Get paginated LoRA data""" - # 确保缓存已初始化 + folder: Optional[str] = None, + search: Optional[str] = None) -> Dict: + """Get paginated LoRA data with search support""" cache = await self.get_cached_data() - # Select sorted data based on sort_by parameter - data = (cache.sorted_by_date if sort_by == 'date' - else cache.sorted_by_name) + # 先获取基础数据集 + data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name - # Apply folder filter if specified + # 应用文件夹过滤 if folder is not None: data = [item for item in data if item['folder'] == folder] - # Calculate pagination + # 应用搜索过滤(只匹配model_name) + if search: + search = search.lower().strip() + before_search = len(data) + data = [item for item in data + if search in item['model_name'].lower()] + + # 计算分页 total_items = len(data) start_idx = (page - 1) * page_size end_idx = min(start_idx + page_size, total_items) - return { + result = { 'items': data[start_idx:end_idx], 'total': total_items, 'page': page, 'page_size': page_size, 'total_pages': (total_items + page_size - 1) // page_size } + + return result def invalidate_cache(self): """Invalidate the current cache""" diff --git a/static/css/style.css b/static/css/style.css index 06672f39..addcc414 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -875,4 +875,63 @@ body.modal-open .toast-info { /* 使用已有的loading-spinner样式 */ .initialization-notice .loading-spinner { margin-bottom: var(--space-2); +} + +/* Search Container Styles */ +.controls { + display: flex; + flex-direction: column; + gap: var(--space-2); + margin-bottom: var(--space-2); +} + +.actions { + display: flex; + align-items: center; + gap: var(--space-2); + flex-wrap: nowrap; /* 防止内容换行 */ + width: 100%; /* 确保占满容器宽度 */ +} + +.search-container { + position: relative; + width: 250px; + margin-left: auto; + flex-shrink: 0; /* 防止搜索框被压缩 */ +} + +/* 调整搜索框样式以匹配其他控件 */ +.search-container input { + width: 100%; + padding: 6px 32px 6px 12px; + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); + background: var(--lora-surface); + color: var(--text-color); + font-size: 0.9em; + height: 32px; + box-sizing: border-box; /* 确保padding不会增加总宽度 */ +} + +.search-icon { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: oklch(var(--text-color) / 0.5); + pointer-events: none; + line-height: 1; /* 防止图标影响容器高度 */ +} + +@media (max-width: 768px) { + .actions { + flex-wrap: wrap; + gap: var(--space-1); + } + + .search-container { + width: 100%; + order: -1; /* 在移动端将搜索框移到顶部 */ + margin-left: 0; + } } \ No newline at end of file diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index b34efc77..2951a586 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -199,13 +199,17 @@ export async function replacePreview(filePath) { input.click(); } -function appendLoraCards(loras) { +export function appendLoraCards(loras) { const grid = document.getElementById('loraGrid'); const sentinel = document.getElementById('scroll-sentinel'); loras.forEach(lora => { const card = createLoraCard(lora); - grid.insertBefore(card, sentinel); + if (sentinel) { + grid.insertBefore(card, sentinel); + } else { + grid.appendChild(card); + } }); } diff --git a/static/js/main.js b/static/js/main.js index 26e06033..3a6472dd 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -7,6 +7,7 @@ import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReloa import { showToast, lazyLoadImages, restoreFolderFilter, initTheme, toggleTheme, toggleFolder, copyTriggerWord } from './utils/uiHelpers.js'; import { initializeInfiniteScroll } from './utils/infiniteScroll.js'; import { showDeleteModal, confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; +import { SearchManager } from './utils/search.js'; // Export all functions that need global access window.loadMoreLoras = loadMoreLoras; @@ -33,20 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { restoreFolderFilter(); initializeLoraCards(); initTheme(); - - // Search handler - const searchHandler = debounce(term => { - document.querySelectorAll('.lora-card').forEach(card => { - card.style.display = [card.dataset.name, card.dataset.folder] - .some(text => text.toLowerCase().includes(term)) - ? 'block' - : 'none'; - }); - }, 250); - - document.getElementById('searchInput')?.addEventListener('input', e => { - searchHandler(e.target.value.toLowerCase()); - }); + window.searchManager = new SearchManager(); }); // Initialize event listeners diff --git a/static/js/utils/search.js b/static/js/utils/search.js new file mode 100644 index 00000000..e8765f12 --- /dev/null +++ b/static/js/utils/search.js @@ -0,0 +1,87 @@ +import { appendLoraCards } from '../api/loraApi.js'; +import { state } from '../state/index.js'; +import { resetAndReload } from '../api/loraApi.js'; +import { showToast } from './uiHelpers.js'; + +export class SearchManager { + constructor() { + this.searchInput = document.getElementById('searchInput'); + this.searchDebounceTimeout = null; + this.currentSearchTerm = ''; + this.isSearching = false; + + if (this.searchInput) { + this.searchInput.addEventListener('input', this.handleSearch.bind(this)); + } + } + + handleSearch(event) { + if (this.searchDebounceTimeout) { + clearTimeout(this.searchDebounceTimeout); + } + + this.searchDebounceTimeout = setTimeout(async () => { + const searchTerm = event.target.value.trim().toLowerCase(); + + if (searchTerm !== this.currentSearchTerm && !this.isSearching) { + this.currentSearchTerm = searchTerm; + await this.performSearch(searchTerm); + } + }, 250); + } + + async performSearch(searchTerm) { + const grid = document.getElementById('loraGrid'); + + if (!searchTerm) { + state.currentPage = 1; + await resetAndReload(); + return; + } + + try { + this.isSearching = true; + state.loadingManager.showSimpleLoading('Searching...'); + + state.currentPage = 1; + state.hasMore = true; + + const url = new URL('/api/loras', window.location.origin); + url.searchParams.set('page', '1'); + url.searchParams.set('page_size', '20'); + url.searchParams.set('sort_by', state.sortBy); + url.searchParams.set('search', searchTerm); + + if (state.activeFolder) { + url.searchParams.set('folder', state.activeFolder); + } + + const response = await fetch(url); + + if (!response.ok) { + throw new Error('Search failed'); + } + + const data = await response.json(); + + if (searchTerm === this.currentSearchTerm) { + grid.innerHTML = ''; + + if (data.items.length === 0) { + grid.innerHTML = '