Files
stock-advisor/static/js/common.js
T

127 lines
4.2 KiB
JavaScript
Raw Normal View History

/* 智能荐股系统 - 公共工具 */
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 => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[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); };
}