Restructure into modules/ with single-process CTP and config/ layout.
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
/* 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 += ' <span class="near-expiry-tag">临期</span>';
|
||||
}
|
||||
if (item.has_night_session) {
|
||||
label += ' <span class="night-session-tag">夜盘</span>';
|
||||
}
|
||||
div.innerHTML = label +
|
||||
'<div class="sub">' + formatSub(item) + '</div>';
|
||||
div.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
selectItem(item);
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
function renderItems(items) {
|
||||
dropdown.innerHTML = '';
|
||||
if (!items.length) {
|
||||
dropdown.innerHTML = '<div class="symbol-option">无匹配,可输入同花顺代码如 ag2608</div>';
|
||||
} 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 = '<div class="symbol-option">无匹配品种,可输入合约代码如 ag2608</div>';
|
||||
}
|
||||
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 = '<div class="symbol-option">正在加载推荐品种…</div>';
|
||||
dropdown.classList.add('show');
|
||||
loadRecommendedGroups()
|
||||
.then(function (groups) {
|
||||
mainsCache = groups;
|
||||
if (!groups.length) {
|
||||
dropdown.innerHTML =
|
||||
'<div class="symbol-option">当前资金下暂无推荐品种,可输入合约代码搜索</div>';
|
||||
dropdown.classList.add('show');
|
||||
return;
|
||||
}
|
||||
showMarketMains(filterQ, onEmpty);
|
||||
})
|
||||
.catch(function () {
|
||||
dropdown.innerHTML =
|
||||
'<div class="symbol-option">推荐品种加载失败,请刷新页面或输入合约代码搜索</div>';
|
||||
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);
|
||||
})();
|
||||
Reference in New Issue
Block a user