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

128 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 智能荐股系统 - 公共工具 */
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); };
}