/* Copyright (c) 2025-2026 马建军. All rights reserved. * 专有软件 — 未经授权禁止复制、传播、转售。 * 详见 LICENSE.zh-CN.txt */ (function () { var recommendedGroupsCache = null; var recommendedGroupsPromise = null; function loadRecommendedGroups() { if (recommendedGroupsCache) { return Promise.resolve(recommendedGroupsCache); } if (recommendedGroupsPromise) { return recommendedGroupsPromise; } recommendedGroupsPromise = fetch('/api/symbols/recommended') .then(function (r) { if (!r.ok) { throw new Error('HTTP ' + r.status); } return r.json(); }) .then(function (groups) { recommendedGroupsCache = Array.isArray(groups) ? groups : []; return recommendedGroupsCache; }) .catch(function () { recommendedGroupsCache = null; throw new Error('load failed'); }) .finally(function () { recommendedGroupsPromise = null; }); return recommendedGroupsPromise; } function formatSub(item) { var sub = '同花顺 ' + item.ths_code + (item.market_code ? ' · ' + item.market_code : '') + ' · ' + (item.exchange || ''); if (item.max_lots != null && item.max_lots > 0) { sub += ' · 最大 ' + item.max_lots + ' 手'; } return sub; } function formatInputLabel(item) { return item.input_label || (item.name + ' ' + item.ths_code); } function itemMatchesQuery(item, qLower) { if (!qLower) return true; var hay = ( item.name + ' ' + item.ths_code + ' ' + (item.display || '') + ' ' + (item.contract || '') + ' ' + (item.exchange || '') ).toLowerCase(); return hay.indexOf(qLower) >= 0; } function groupedHasMatch(groups, qLower) { if (!qLower) return true; return groups.some(function (group) { return group.items.some(function (item) { return itemMatchesQuery(item, qLower); }); }); } function setupSymbolMetaPlacement(wrapper, selectedEl) { if (!selectedEl || wrapper.classList.contains('market-symbol-wrap')) { return; } var label = wrapper.querySelector('label.text-label, label.symbol-field-label'); if (!label) { label = document.createElement('label'); label.className = 'text-label symbol-field-label'; label.appendChild(document.createTextNode('品种')); var input = wrapper.querySelector('.symbol-input'); if (input) { wrapper.insertBefore(label, input); } } if (selectedEl.parentElement !== label) { label.appendChild(selectedEl); } } function initSymbolInput(wrapper) { const input = wrapper.querySelector('.symbol-input'); const hiddenThs = wrapper.querySelector('input[name="symbol"]') || wrapper.querySelector('.symbol-ths-code'); const hiddenName = wrapper.querySelector('input[name="symbol_name"]'); const hiddenMarket = wrapper.querySelector('input[name="market_code"]'); const hiddenSina = wrapper.querySelector('input[name="sina_code"]'); const dropdown = wrapper.querySelector('.symbol-dropdown'); const selectedEl = wrapper.querySelector('.symbol-selected'); setupSymbolMetaPlacement(wrapper, selectedEl); const isMarketPicker = wrapper.classList.contains('market-symbol-wrap'); const useMainsPicker = isMarketPicker || wrapper.classList.contains('symbol-mains'); let timer = null; let abortCtrl = null; const cache = new Map(); let mainsCache = null; function hideDropdown() { dropdown.classList.remove('show'); } function selectItem(item) { const label = formatInputLabel(item); input.value = label; if (hiddenThs) hiddenThs.value = item.ths_code; if (hiddenName) hiddenName.value = item.name; if (hiddenMarket) hiddenMarket.value = item.market_code || ''; if (hiddenSina) hiddenSina.value = item.sina_code || ''; if (selectedEl) selectedEl.textContent = formatSub(item); hideDropdown(); input.dispatchEvent(new CustomEvent('symbol-selected', { detail: item, bubbles: true })); } function buildOptionEl(item) { const div = document.createElement('div'); div.className = 'symbol-option'; if (item.near_expiry) { div.classList.add('near-expiry'); } var label = item.display || (item.name + ' ' + item.ths_code); if (item.near_expiry) { label += ' 临期'; } if (item.has_night_session) { label += ' 夜盘'; } div.innerHTML = label + '
' + formatSub(item) + '
'; div.addEventListener('mousedown', function (e) { e.preventDefault(); selectItem(item); }); return div; } function renderItems(items) { dropdown.innerHTML = ''; if (!items.length) { dropdown.innerHTML = '
无匹配,可输入同花顺代码如 ag2608
'; } else { items.forEach(function (item) { dropdown.appendChild(buildOptionEl(item)); }); } dropdown.classList.add('show'); } function renderGrouped(groups, filterQ) { dropdown.innerHTML = ''; const qLower = (filterQ || '').trim().toLowerCase(); let any = false; groups.forEach(function (group) { const items = group.items.filter(function (item) { return itemMatchesQuery(item, qLower); }); if (!items.length) return; any = true; const head = document.createElement('div'); head.className = 'symbol-group-head'; head.textContent = group.category; dropdown.appendChild(head); items.forEach(function (item) { dropdown.appendChild(buildOptionEl(item)); }); }); if (!any) { dropdown.innerHTML = '
无匹配品种,可输入合约代码如 ag2608
'; } dropdown.classList.add('show'); } function showMarketMains(filterQ, onEmpty) { const q = (filterQ || '').trim(); const qLower = q.toLowerCase(); if (mainsCache) { if (!q || groupedHasMatch(mainsCache, qLower)) { renderGrouped(mainsCache, q); return; } if (typeof onEmpty === 'function') { onEmpty(q); return; } renderGrouped(mainsCache, q); return; } dropdown.innerHTML = '
正在加载推荐品种…
'; dropdown.classList.add('show'); loadRecommendedGroups() .then(function (groups) { mainsCache = groups; if (!groups.length) { dropdown.innerHTML = '
当前资金下暂无推荐品种,可输入合约代码搜索
'; dropdown.classList.add('show'); return; } showMarketMains(filterQ, onEmpty); }) .catch(function () { dropdown.innerHTML = '
推荐品种加载失败,请刷新页面或输入合约代码搜索
'; dropdown.classList.add('show'); }); } function search(q) { if (cache.has(q)) { renderItems(cache.get(q)); return; } if (abortCtrl) { abortCtrl.abort(); } abortCtrl = new AbortController(); fetch('/api/symbols/search?q=' + encodeURIComponent(q), { signal: abortCtrl.signal, }) .then(function (r) { return r.json(); }) .then(function (items) { cache.set(q, items); renderItems(items); }) .catch(function (err) { if (err && err.name === 'AbortError') return; hideDropdown(); }); } function handleQuery(q) { if (useMainsPicker) { showMarketMains(q, function (query) { search(query); }); } else { search(q); } } input.addEventListener('input', function () { if (hiddenThs && hiddenThs !== input) hiddenThs.value = ''; if (hiddenName) hiddenName.value = ''; if (hiddenMarket) hiddenMarket.value = ''; if (hiddenSina) hiddenSina.value = ''; if (selectedEl) selectedEl.textContent = ''; const q = input.value.trim(); if (!q) { if (useMainsPicker) { showMarketMains(''); } else { hideDropdown(); } return; } clearTimeout(timer); timer = setTimeout(function () { handleQuery(q); }, 120); }); input.addEventListener('blur', function () { setTimeout(hideDropdown, 150); }); input.addEventListener('focus', function () { const q = input.value.trim(); if (useMainsPicker) { showMarketMains(q, function (query) { if (query) search(query); }); return; } if (q && hiddenThs && !hiddenThs.value) { search(q); } }); } function initSymbolForms() { document.querySelectorAll('.symbol-wrap').forEach(initSymbolInput); document.querySelectorAll('form').forEach(function (form) { if (!form.querySelector('.symbol-wrap')) return; if (form.id === 'market-form') return; if (form.dataset.symbolGuard) return; form.dataset.symbolGuard = '1'; form.addEventListener('submit', function (e) { const ths = form.querySelector('input[name="symbol"]') || form.querySelector('.symbol-ths-code'); const market = form.querySelector('input[name="market_code"]'); if (ths && !ths.value.trim()) { e.preventDefault(); alert('请从下拉列表选择品种'); return; } if (market && !market.value.trim()) { e.preventDefault(); alert('请从下拉列表选择品种(需含同花顺行情代码)'); } }); }); } if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(initSymbolForms); else document.addEventListener('DOMContentLoaded', initSymbolForms); })();