128 lines
4.2 KiB
JavaScript
128 lines
4.2 KiB
JavaScript
/* 智能荐股系统 - 公共工具 */
|
||
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); };
|
||
}
|