paper-burner/js/ui/ui-model-search.js

267 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// js/ui/ui-model-search.js
// 封装通用模型搜索弹窗及其集成逻辑。
(function(global) {
'use strict';
const overlayState = {
initialized: false,
overlayEl: null,
titleEl: null,
inputEl: null,
listEl: null,
emptyEl: null,
closeBtn: null,
items: [],
currentValue: '',
onSelect: null,
emptyMessage: '暂无可选项'
};
function initializeModelSearchOverlay() {
if (overlayState.initialized) return overlayState;
const overlay = document.getElementById('modelSearchOverlay');
const titleEl = document.getElementById('modelSearchTitle');
const inputEl = document.getElementById('modelSearchInput');
const listEl = document.getElementById('modelSearchList');
const emptyEl = document.getElementById('modelSearchEmpty');
const closeBtn = document.getElementById('modelSearchCloseBtn');
if (!overlay || !titleEl || !inputEl || !listEl || !emptyEl || !closeBtn) {
console.warn('[ModelSearchOverlay] 必需的DOM元素缺失搜索弹窗未初始化。');
return overlayState;
}
overlayState.overlayEl = overlay;
overlayState.titleEl = titleEl;
overlayState.inputEl = inputEl;
overlayState.listEl = listEl;
overlayState.emptyEl = emptyEl;
overlayState.closeBtn = closeBtn;
overlayState.initialized = true;
const handleBackgroundClick = (event) => {
if (event.target === overlay) {
closeModelSearchOverlay();
}
};
const handleKeydown = (event) => {
if (event.key === 'Escape' && !overlay.classList.contains('hidden')) {
closeModelSearchOverlay();
}
};
overlay.addEventListener('click', handleBackgroundClick);
closeBtn.addEventListener('click', closeModelSearchOverlay);
inputEl.addEventListener('input', () => renderModelSearchResults(inputEl.value));
document.addEventListener('keydown', handleKeydown);
return overlayState;
}
function openModelSearchOverlay({
title = '选择模型',
items = [],
currentValue = '',
placeholder = '搜索模型...',
emptyMessage = '暂无可选项',
onSelect = null
}) {
const state = initializeModelSearchOverlay();
if (!state.initialized) return;
state.items = (items || []).map(item => {
const value = item.value || '';
const label = item.label || value;
const description = item.description || '';
return {
value,
label,
description,
searchText: `${value} ${label} ${description}`.toLowerCase()
};
});
state.currentValue = currentValue || '';
state.onSelect = typeof onSelect === 'function' ? onSelect : null;
state.emptyMessage = emptyMessage || '暂无可选项';
state.titleEl.textContent = title;
state.inputEl.value = '';
state.inputEl.placeholder = placeholder;
state.overlayEl.classList.remove('hidden');
document.body.classList.add('overflow-hidden');
renderModelSearchResults('');
setTimeout(() => {
state.inputEl.focus();
state.inputEl.select();
}, 50);
}
function closeModelSearchOverlay() {
if (!overlayState.initialized) return;
overlayState.overlayEl.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
overlayState.items = [];
overlayState.currentValue = '';
overlayState.onSelect = null;
}
function renderModelSearchResults(keyword) {
if (!overlayState.initialized) return;
const filter = (keyword || '').trim().toLowerCase();
const { listEl, emptyEl, items, currentValue, onSelect, emptyMessage } = overlayState;
listEl.innerHTML = '';
const matched = filter
? items.filter(item => item.searchText.includes(filter))
: items.slice();
if (matched.length === 0) {
emptyEl.textContent = emptyMessage;
emptyEl.classList.remove('hidden');
return;
}
emptyEl.classList.add('hidden');
listEl.scrollTop = 0;
matched.forEach(item => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'w-full text-left px-3 py-2 border rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500';
if (item.value === currentValue) {
button.classList.add('bg-blue-50', 'border-blue-200', 'text-blue-700', 'font-semibold');
} else {
button.classList.add('border-transparent', 'hover:bg-gray-50', 'text-gray-700');
}
const labelHtml = highlightKeyword(item.label || item.value, filter);
const descHtml = item.description && item.description !== item.label
? highlightKeyword(item.description, filter)
: '';
const labelSpan = document.createElement('span');
labelSpan.className = 'block text-sm';
labelSpan.innerHTML = labelHtml;
button.appendChild(labelSpan);
if (descHtml) {
const descSpan = document.createElement('span');
descSpan.className = 'block text-xs text-gray-500 mt-0.5';
descSpan.innerHTML = descHtml;
button.appendChild(descSpan);
}
button.addEventListener('click', () => {
if (typeof onSelect === 'function') {
onSelect(item.value, item);
}
closeModelSearchOverlay();
});
listEl.appendChild(button);
});
}
const caches = {};
const controllers = {};
function getModelSearchCache(key) {
return caches[key] || [];
}
function setModelSearchCache(key, items) {
caches[key] = Array.isArray(items) ? items : [];
const controller = controllers[key];
if (controller && typeof controller.update === 'function') {
controller.update();
}
}
function registerModelSearchIntegration({
key,
selectEl,
buttonEl,
title = '选择模型',
placeholder = '搜索模型...',
emptyMessage = '暂无可选项',
onEmpty,
onSelect
}) {
if (!key || !buttonEl || !selectEl) return;
const controller = controllers[key] || {};
controller.key = key;
controller.selectEl = selectEl;
controller.buttonEl = buttonEl;
controller.title = title;
controller.placeholder = placeholder;
controller.emptyMessage = emptyMessage;
controller.onSelect = typeof onSelect === 'function'
? onSelect
: (value) => {
if (!selectEl || value == null) return;
if (selectEl.value !== value) {
selectEl.value = value;
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
}
};
controller.onEmpty = typeof onEmpty === 'function' ? onEmpty : null;
controller.maybeDetect = (items) => {
if (Array.isArray(items) && items.length <= 1) {
if (typeof controller.onEmpty === 'function') controller.onEmpty();
return true;
}
return false;
};
controller.update = () => {
const hasItems = (getModelSearchCache(key) || []).length > 0;
buttonEl.disabled = !hasItems;
};
if (buttonEl.dataset.modelSearchRegistered !== 'true') {
buttonEl.addEventListener('click', () => {
const items = getModelSearchCache(key);
if (!items.length) {
if (controller.onEmpty) {
controller.onEmpty();
} else if (typeof global.showNotification === 'function') {
global.showNotification('请先检测可用模型', 'info');
}
return;
}
if (controller.maybeDetect(items)) {
return;
}
const currentValue = controller.selectEl ? controller.selectEl.value : '';
openModelSearchOverlay({
title: controller.title,
items,
currentValue,
placeholder: controller.placeholder,
emptyMessage: controller.emptyMessage,
onSelect: controller.onSelect
});
});
buttonEl.dataset.modelSearchRegistered = 'true';
}
controllers[key] = controller;
controller.update();
return controller;
}
global.initializeModelSearchOverlay = initializeModelSearchOverlay;
global.openModelSearchOverlay = openModelSearchOverlay;
global.closeModelSearchOverlay = closeModelSearchOverlay;
global.renderModelSearchResults = renderModelSearchResults;
global.getModelSearchCache = getModelSearchCache;
global.setModelSearchCache = setModelSearchCache;
global.registerModelSearchIntegration = registerModelSearchIntegration;
})(window);