智能荐股系统 v1.0.0:股票池/行情/新闻RAG/机构持仓/多因子评分/AI深度研报
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/* 智能荐股系统 - 公共工具 */
|
||||
const $ = (sel, el) => (el || document).querySelector(sel);
|
||||
const $$ = (sel, el) => Array.from((el || document).querySelectorAll(sel));
|
||||
|
||||
async function api(url, opts) {
|
||||
const res = await fetch(url, opts);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok && data.error) throw new Error(data.error);
|
||||
return data;
|
||||
}
|
||||
|
||||
function fmtPct(v) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
const n = Number(v);
|
||||
return (n > 0 ? '+' : '') + n.toFixed(2) + '%';
|
||||
}
|
||||
|
||||
function pctClass(v) {
|
||||
const n = Number(v);
|
||||
if (n > 0) return 'up';
|
||||
if (n < 0) return 'down';
|
||||
return 'flat';
|
||||
}
|
||||
|
||||
function fmtNum(v, digits) {
|
||||
if (v === null || v === undefined) return '--';
|
||||
const n = Number(v);
|
||||
if (Math.abs(n) >= 10000) return (n / 10000).toFixed(2) + '万';
|
||||
return n.toFixed(digits === undefined ? 2 : digits);
|
||||
}
|
||||
|
||||
function fmtAmountYi(v) {
|
||||
// 万元 -> 亿元
|
||||
const n = Number(v);
|
||||
if (!n) return '--';
|
||||
return (n / 10000).toFixed(2) + '亿';
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s || '').replace(/[&<>"']/g, c => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function mdRender(text) {
|
||||
if (window.marked) {
|
||||
return marked.parse(text || '');
|
||||
}
|
||||
return '<pre style="white-space:pre-wrap">' + escapeHtml(text) + '</pre>';
|
||||
}
|
||||
|
||||
/* 评分环(SVG) */
|
||||
function scoreRing(el, score) {
|
||||
const color = score >= 82 ? '#ef4444' : score >= 68 ? '#f59e0b' : score >= 55 ? '#3b82f6' : '#6e7681';
|
||||
const r = 18, c = 2 * Math.PI * r;
|
||||
const off = c * (1 - Math.min(100, score) / 100);
|
||||
el.innerHTML = `<svg width="46" height="46">
|
||||
<circle cx="23" cy="23" r="${r}" stroke="#262d3a" stroke-width="5" fill="none"/>
|
||||
<circle cx="23" cy="23" r="${r}" stroke="${color}" stroke-width="5" fill="none"
|
||||
stroke-linecap="round" stroke-dasharray="${c}" stroke-dashoffset="${off}"/>
|
||||
</svg><span class="ring-val" style="color:${color}">${score}</span>`;
|
||||
}
|
||||
|
||||
function scoreBars(parts) {
|
||||
const map = [['trend', '趋势'], ['momentum', '动量'], ['technical', '技术'],
|
||||
['volume', '量能'], ['news', '消息'], ['institutional', '机构']];
|
||||
return map.map(([k, label]) => {
|
||||
const v = parts ? (parts[k] || 0) : 0;
|
||||
const pct = Math.min(100, v / (k === 'trend' ? 25 : k === 'momentum' ? 20 : 15) * 100);
|
||||
return `<div class="score-bar">
|
||||
<div class="sb-label">${label}</div>
|
||||
<div class="sb-track"><div class="sb-fill" style="width:${pct}%"></div></div>
|
||||
<div class="sb-val num">${v}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* 导航高亮 */
|
||||
(function nav() {
|
||||
const path = location.pathname;
|
||||
$$('.nav-item').forEach(a => {
|
||||
const n = a.dataset.nav;
|
||||
if (n && (path === n || (n !== '/' && path.startsWith(n)))) a.classList.add('active');
|
||||
});
|
||||
const t = setInterval(() => {
|
||||
const el = $('#sideTime');
|
||||
if (!el) { clearInterval(t); return; }
|
||||
el.textContent = new Date().toLocaleString('zh-CN', { hour12: false });
|
||||
}, 1000);
|
||||
})();
|
||||
|
||||
/* 弹窗 */
|
||||
function openModal(html) {
|
||||
let mask = $('.modal-mask');
|
||||
if (!mask) {
|
||||
mask = document.createElement('div');
|
||||
mask.className = 'modal-mask';
|
||||
mask.innerHTML = `<div class="modal"><span class="modal-close">✕</span><div class="modal-body"></div></div>`;
|
||||
document.body.appendChild(mask);
|
||||
$('.modal-close', mask).onclick = () => mask.classList.remove('show');
|
||||
mask.onclick = e => { if (e.target === mask) mask.classList.remove('show'); };
|
||||
}
|
||||
$('.modal-body', mask).innerHTML = html;
|
||||
mask.classList.add('show');
|
||||
return mask;
|
||||
}
|
||||
|
||||
/* toast */
|
||||
function toast(msg, type) {
|
||||
let box = $('#toastBox');
|
||||
if (!box) {
|
||||
box = document.createElement('div');
|
||||
box.id = 'toastBox';
|
||||
box.style.cssText = 'position:fixed;top:20px;right:20px;z-index:999;display:flex;flex-direction:column;gap:8px;';
|
||||
document.body.appendChild(box);
|
||||
}
|
||||
const d = document.createElement('div');
|
||||
d.style.cssText = 'padding:10px 16px;border-radius:8px;background:#1c2230;border:1px solid #262d3a;color:#e6edf3;font-size:13px;box-shadow:0 4px 16px rgba(0,0,0,.4);';
|
||||
d.textContent = msg;
|
||||
box.appendChild(d);
|
||||
setTimeout(() => d.remove(), 2600);
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
let t;
|
||||
return function () { clearTimeout(t); t = setTimeout(() => fn.apply(this, arguments), ms); };
|
||||
}
|
||||
Reference in New Issue
Block a user