Initial commit: gamer project
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// 联系人自动补全模块
|
||||
const _acCache = {};
|
||||
|
||||
async function acFetchNames(field) {
|
||||
if (_acCache[field] && Date.now() - _acCache[field].ts < 60000) return _acCache[field].data;
|
||||
try {
|
||||
const res = await fetch(API + '/api/contacts?field=' + field, { headers: headers() });
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
_acCache[field] = { data, ts: Date.now() };
|
||||
return data;
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function acInvalidateCache(field) {
|
||||
if (field) delete _acCache[field];
|
||||
else Object.keys(_acCache).forEach(k => delete _acCache[k]);
|
||||
}
|
||||
|
||||
function acSetup(inputId, field) {
|
||||
const input = document.getElementById(inputId);
|
||||
if (!input || input._acBound) return;
|
||||
input._acBound = true;
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'ac-wrap';
|
||||
input.parentNode.insertBefore(wrap, input);
|
||||
wrap.appendChild(input);
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'ac-list';
|
||||
wrap.appendChild(list);
|
||||
|
||||
let names = [];
|
||||
let activeIdx = -1;
|
||||
|
||||
async function loadNames() {
|
||||
names = await acFetchNames(field);
|
||||
}
|
||||
|
||||
function render(query) {
|
||||
const q = query.trim().toLowerCase();
|
||||
const filtered = q ? names.filter(n => n.toLowerCase().includes(q)) : names;
|
||||
if (filtered.length === 0) { list.classList.remove('show'); return; }
|
||||
activeIdx = -1;
|
||||
list.innerHTML = filtered.slice(0, 10).map((n, i) => {
|
||||
let display = n;
|
||||
if (q) {
|
||||
const idx = n.toLowerCase().indexOf(q);
|
||||
if (idx >= 0) {
|
||||
display = n.slice(0, idx) + '<span class="ac-highlight">' + n.slice(idx, idx + q.length) + '</span>' + n.slice(idx + q.length);
|
||||
}
|
||||
}
|
||||
return `<div class="ac-item" data-idx="${i}" data-value="${n.replace(/"/g, '"')}">${display}</div>`;
|
||||
}).join('');
|
||||
list.classList.add('show');
|
||||
}
|
||||
|
||||
function pick(value) {
|
||||
input.value = value;
|
||||
list.classList.remove('show');
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
input.addEventListener('focus', async () => {
|
||||
await loadNames();
|
||||
render(input.value);
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => { render(input.value); });
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
const items = list.querySelectorAll('.ac-item');
|
||||
if (!list.classList.contains('show') || items.length === 0) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
activeIdx = Math.min(activeIdx + 1, items.length - 1);
|
||||
items.forEach((el, i) => el.classList.toggle('active', i === activeIdx));
|
||||
items[activeIdx].scrollIntoView({ block: 'nearest' });
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
activeIdx = Math.max(activeIdx - 1, 0);
|
||||
items.forEach((el, i) => el.classList.toggle('active', i === activeIdx));
|
||||
items[activeIdx].scrollIntoView({ block: 'nearest' });
|
||||
} else if (e.key === 'Enter' && activeIdx >= 0) {
|
||||
e.preventDefault();
|
||||
pick(items[activeIdx].dataset.value);
|
||||
} else if (e.key === 'Escape') {
|
||||
list.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
list.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault(); // 防止 input 失焦
|
||||
const item = e.target.closest('.ac-item');
|
||||
if (item) pick(item.dataset.value);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!wrap.contains(e.target)) list.classList.remove('show');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user