267 lines
9.6 KiB
JavaScript
267 lines
9.6 KiB
JavaScript
// 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);
|