/* 智能荐股系统 - 公共工具 */ 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 '
' + escapeHtml(text) + '
'; } /* 评分环(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 = ` ${score}`; } 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 `
${label}
${v}
`; }).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 = ``; 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); }; }