refactor: integrate spec system into all 9 tabs — ?§ buttons, native health monitoring
This commit is contained in:
+243
-6
@@ -19,6 +19,17 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
.stock-row:hover { background: #22222e; cursor: pointer; }
|
||||
.loading { opacity: 0.5; pointer-events: none; }
|
||||
.modal-overlay { background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); }
|
||||
.spec-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 18px; height: 18px; border-radius: 50%;
|
||||
font-size: 11px; font-weight: 700; line-height: 1;
|
||||
cursor: pointer; transition: background 0.15s; user-select: none;
|
||||
}
|
||||
.spec-btn.help { color: #58a6ff; background: rgba(88,166,255,0.1); }
|
||||
.spec-btn.help:hover { background: rgba(88,166,255,0.25); }
|
||||
.spec-btn.ai { color: #d29922; background: rgba(210,153,34,0.1); }
|
||||
.spec-btn.ai:hover { background: rgba(210,153,34,0.25); }
|
||||
.spec-btns { display: inline-flex; gap: 4px; }
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: #0f0f13; }
|
||||
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
||||
@@ -67,9 +78,7 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
<div id="tab-prompts" class="tab-content hidden"></div>
|
||||
<div id="tab-market" class="tab-content hidden"></div>
|
||||
<div id="tab-signals" class="tab-content hidden"></div>
|
||||
<div id="tab-health" class="tab-content hidden">
|
||||
<iframe src="/dashboard" style="width:100%;height:100vh;border:none;background:#0d1117;border-radius:8px;"></iframe>
|
||||
</div>
|
||||
<div id="tab-health" class="tab-content hidden"></div>
|
||||
<div id="tab-dashboard" class="tab-content hidden">
|
||||
<iframe src="/dashboard" style="width:100%;height:100vh;border:none;background:#0d1117;border-radius:8px;"></iframe>
|
||||
</div>
|
||||
@@ -82,6 +91,20 @@ body { background: #0f0f13; color: #e2e8f0; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spec Modal -->
|
||||
<div id="specModal" class="fixed inset-0 modal-overlay hidden items-center justify-center z-50" onclick="if(event.target===this)closeSpecModal()">
|
||||
<div class="bg-[#161b22] border border-[#30363d] rounded-2xl w-full max-w-[700px] max-h-[80vh] overflow-y-auto mx-4 p-6" onclick="event.stopPropagation()">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h2 id="specModalTitle" class="text-lg font-bold text-[#c9d1d9]">—</h2>
|
||||
<p id="specModalSubtitle" class="text-xs text-[#8b949e] mt-1">—</p>
|
||||
</div>
|
||||
<button onclick="closeSpecModal()" class="text-[#8b949e] hover:text-white text-xl">×</button>
|
||||
</div>
|
||||
<div id="specModalBody" class="text-sm text-[#c9d1d9] leading-relaxed"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stock Detail Modal -->
|
||||
<div id="stockModal" class="fixed inset-0 modal-overlay hidden items-center justify-center z-50" onclick="if(event.target===this)closeStock()">
|
||||
<div class="bg-[#1a1a24] border border-slate-700 rounded-2xl w-full max-w-2xl max-h-[85vh] overflow-y-auto mx-4 p-6" onclick="event.stopPropagation()">
|
||||
@@ -131,6 +154,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (btn.tagName === 'A') return; // 上传链接保持原行为
|
||||
const name = btn.dataset.tab;
|
||||
if (!name) return;
|
||||
// 切走 health 时清理定时器
|
||||
if (name !== 'health' && healthRefreshInterval) {
|
||||
clearInterval(healthRefreshInterval);
|
||||
healthRefreshInterval = null;
|
||||
}
|
||||
if (name === 'health' && !healthRefreshInterval) {
|
||||
healthRefreshInterval = setInterval(renderHealth, 10000);
|
||||
}
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById('tab-' + name).classList.remove('hidden');
|
||||
@@ -143,6 +174,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
|
||||
function switchTab(name) {
|
||||
// 切走 health 时清理定时器
|
||||
if (name !== 'health' && healthRefreshInterval) {
|
||||
clearInterval(healthRefreshInterval);
|
||||
healthRefreshInterval = null;
|
||||
}
|
||||
// 进入 health 时确保定时器运行
|
||||
if (name === 'health' && !healthRefreshInterval) {
|
||||
healthRefreshInterval = setInterval(renderHealth, 10000);
|
||||
}
|
||||
const btn = document.querySelector(`[data-tab="${name}"]`);
|
||||
if (btn) {
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||
@@ -163,6 +203,7 @@ function renderTab(name) {
|
||||
else if (name === 'prompts') renderPrompts();
|
||||
else if (name === 'market') renderMarket();
|
||||
else if (name === 'signals') renderSignals();
|
||||
else if (name === 'health') renderHealth();
|
||||
else if (name === 'watch') renderWatch();
|
||||
}
|
||||
|
||||
@@ -213,7 +254,7 @@ function renderOverview() {
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="card p-4">
|
||||
<h3 class="text-sm font-semibold mb-3">⚡ 大幅异动</h3>
|
||||
<h3 class="text-sm font-semibold mb-3">⚡ 大幅异动 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('portfolio','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('portfolio','ai')">§</span></span></h3>
|
||||
<div class="space-y-2">${alertHtml}</div>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
@@ -256,6 +297,7 @@ function renderHoldings() {
|
||||
<div class="card overflow-hidden">
|
||||
<div class="p-3 border-b border-slate-800/50 flex justify-between items-center">
|
||||
<span class="text-sm font-semibold">持仓 · ${holdings.length} 只</span>
|
||||
<span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('portfolio','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('portfolio','ai')">§</span></span>
|
||||
<span class="text-xs text-slate-500">点击查看详情</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
@@ -326,6 +368,7 @@ function renderWatchlist() {
|
||||
<div class="card overflow-hidden">
|
||||
<div class="p-3 border-b border-slate-800/50 flex justify-between items-center">
|
||||
<span class="text-sm font-semibold">自选 · ${stocks.length} 只</span>
|
||||
<span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('watchlist','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('watchlist','ai')">§</span></span>
|
||||
<span class="text-xs text-slate-500">按推荐价值排序</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
@@ -386,6 +429,10 @@ function renderReports() {
|
||||
return;
|
||||
}
|
||||
el.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-semibold">📋 报告列表</span>
|
||||
<span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('reports','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('reports','ai')">§</span></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
${reportsData.map(r => `
|
||||
<div class="card p-4 hover:border-slate-600 transition" onclick="viewReport('${r.id}')">
|
||||
@@ -433,6 +480,9 @@ function refreshMarketData() {
|
||||
const cands = candidates || [];
|
||||
const trend = sigs.filter(s => s.source && s.source !== 'xiaoguo');
|
||||
el.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-semibold">🌐 市场全景 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('market','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('market','ai')">§</span><span class="spec-btn help" onclick="showModuleHelp('signals','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('signals','ai')">§</span></span></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-4">
|
||||
<div class="card p-4">
|
||||
<h3 class="text-sm font-semibold mb-2">🏭 行业热点</h3>
|
||||
@@ -686,7 +736,7 @@ function renderDecisionsFiltered() {
|
||||
el.innerHTML = `
|
||||
${tagPills}
|
||||
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<span class="text-sm font-semibold">策略决策库</span>
|
||||
<span class="text-sm font-semibold">策略决策库 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('decisions','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('decisions','ai')">§</span></span></span>
|
||||
<span class="text-xs text-slate-500">· ${withStrategy.length} 只 ${matched}</span>
|
||||
<button id="recommendFilterBtn" onclick="toggleRecommendFilter()"
|
||||
class="text-xs px-2 py-1 rounded-full border transition ${decisionsRecommendOnly ? 'bg-amber-900/30 border-amber-400 text-amber-300' : 'bg-slate-800 border-slate-700 text-slate-400 hover:bg-slate-700'}">
|
||||
@@ -881,7 +931,7 @@ function renderEvaluation() {
|
||||
const p2 = stats.phase2 || {};
|
||||
el.innerHTML = `
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="text-sm font-semibold">策略双维度评估</span>
|
||||
<span class="text-sm font-semibold">策略双维度评估 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('evaluation','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('evaluation','ai')">§</span></span></span>
|
||||
<span class="text-xs bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded-full">${evals.length} 只已评估</span>
|
||||
<button class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 ml-auto" onclick="triggerEval()">🔄 重新评估</button>
|
||||
</div>
|
||||
@@ -1046,6 +1096,7 @@ function renderPrompts() {
|
||||
const categories = data.categories || {};
|
||||
|
||||
let html = '<div class="space-y-4">';
|
||||
html += '<div class="flex items-center justify-between mb-2"><span class="text-sm font-semibold">📝 提示词管理 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp(\'prompts\',\'human\')">?</span><span class="spec-btn ai" onclick="showModuleHelp(\'prompts\',\'ai\')">§</span></span></span></div>';
|
||||
|
||||
// 分类筛选按钮
|
||||
const catSet = {};
|
||||
@@ -1230,6 +1281,9 @@ async function renderSignals() {
|
||||
]);
|
||||
const sourceToday = scan.source_today || {};
|
||||
let html = `
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm font-semibold">🔍 信号 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('signals','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('signals','ai')">§</span></span></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
<div class="card p-4">
|
||||
<div class="text-xs text-slate-500 mb-1">今日信号来源</div>
|
||||
@@ -1280,6 +1334,189 @@ async function renderSignals() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spec System ──
|
||||
let _specCache = {};
|
||||
|
||||
function closeSpecModal() {
|
||||
const el = document.getElementById('specModal');
|
||||
if (el) { el.classList.add('hidden'); el.classList.remove('flex'); }
|
||||
}
|
||||
|
||||
function showModuleHelp(module, type) {
|
||||
const cacheKey = module + ':' + type;
|
||||
const cached = _specCache[cacheKey];
|
||||
const modal = document.getElementById('specModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
|
||||
// Show loading
|
||||
document.getElementById('specModalTitle').textContent = '加载中...';
|
||||
document.getElementById('specModalSubtitle').textContent = '';
|
||||
document.getElementById('specModalBody').innerHTML = '<div class="text-slate-500">正在获取规范...</div>';
|
||||
|
||||
const url = `/api/module-spec/${module}`;
|
||||
const doRender = function(spec) {
|
||||
if (spec.error) {
|
||||
document.getElementById('specModalBody').innerHTML = `<div class="text-red-400">获取失败: ${spec.error}</div>`;
|
||||
return;
|
||||
}
|
||||
_specCache[cacheKey] = spec;
|
||||
_renderSpecModal(spec, type);
|
||||
};
|
||||
|
||||
if (cached) {
|
||||
doRender(cached);
|
||||
} else {
|
||||
fetchJSON(url).then(spec => {
|
||||
if (spec && typeof spec === 'object') {
|
||||
_specCache[cacheKey] = spec;
|
||||
doRender(spec);
|
||||
} else {
|
||||
document.getElementById('specModalBody').innerHTML = '<div class="text-slate-500">无规范数据</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _renderSpecModal(spec, type) {
|
||||
const titleEl = document.getElementById('specModalTitle');
|
||||
const subEl = document.getElementById('specModalSubtitle');
|
||||
const bodyEl = document.getElementById('specModalBody');
|
||||
|
||||
let html = '';
|
||||
|
||||
if (type === 'human') {
|
||||
const h = spec.human_help || {};
|
||||
titleEl.textContent = h.title || spec.module || module;
|
||||
subEl.textContent = 'Human Help · 使用指南';
|
||||
|
||||
if (h.description && h.description.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#58a6ff] mb-1">说明</div><p class="text-[#c9d1d9]">' + h.description.join('<br>') + '</p></div>';
|
||||
}
|
||||
if (h.usage && h.usage.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#3fb950] mb-1">用法</div><ul class="list-disc list-inside text-[#c9d1d9] space-y-1">';
|
||||
h.usage.forEach(u => { html += '<li>' + u + '</li>'; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (h.troubleshooting && h.troubleshooting.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#d29922] mb-1">故障排除</div><ul class="list-disc list-inside text-[#c9d1d9] space-y-1">';
|
||||
h.troubleshooting.forEach(t => { html += '<li>' + t + '</li>'; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
} else {
|
||||
const a = spec.ai_spec || {};
|
||||
titleEl.textContent = spec.module || module;
|
||||
subEl.textContent = 'AI Spec · 模块规范';
|
||||
|
||||
if (a.apis && a.apis.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#58a6ff] mb-1">APIs</div>';
|
||||
a.apis.forEach(api => {
|
||||
const method = api.method || 'GET';
|
||||
const color = method === 'GET' ? 'text-[#3fb950]' : method === 'POST' ? 'text-[#d29922]' : method === 'DELETE' ? 'text-[#f85149]' : 'text-[#58a6ff]';
|
||||
html += '<div class="bg-[#0d1117] rounded p-2 mb-1 text-xs"><span class="font-mono font-bold ' + color + '">' + method + '</span> <span class="font-mono text-[#c9d1d9]">' + (api.path || '') + '</span>';
|
||||
if (api.description) html += '<div class="text-[#8b949e] mt-0.5">' + api.description + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
if (a.dependencies && a.dependencies.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#3fb950] mb-1">依赖</div><ul class="list-disc list-inside text-[#c9d1d9] space-y-1">';
|
||||
a.dependencies.forEach(d => { html += '<li>' + d + '</li>'; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (a.constraints && a.constraints.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#d29922] mb-1">约束</div><ul class="list-disc list-inside text-[#c9d1d9] space-y-1">';
|
||||
a.constraints.forEach(c => { html += '<li>' + c + '</li>'; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (a.must_not && a.must_not.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#f85149] mb-1">禁止</div><ul class="list-disc list-inside text-[#c9d1d9] space-y-1">';
|
||||
a.must_not.forEach(m => { html += '<li>' + m + '</li>'; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
if (a.related_files && a.related_files.length) {
|
||||
html += '<div class="mb-4"><div class="text-xs font-semibold text-[#58a6ff] mb-1">相关文件</div><ul class="list-disc list-inside text-xs font-mono text-[#8b949e] space-y-1">';
|
||||
a.related_files.forEach(f => { html += '<li>' + f + '</li>'; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = '<div class="text-slate-500">该模块暂无规范数据</div>';
|
||||
}
|
||||
|
||||
bodyEl.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Health Tab (native) ──
|
||||
let healthRefreshInterval = null;
|
||||
|
||||
async function renderHealth() {
|
||||
const el = document.getElementById('tab-health');
|
||||
el.innerHTML = '<div class="text-slate-500 py-8">加载中...</div>';
|
||||
try {
|
||||
const [services, monitor] = await Promise.all([
|
||||
fetchJSON('/api/services'),
|
||||
fetchJSON('/api/monitor')
|
||||
]);
|
||||
|
||||
const svcs = services.services || [];
|
||||
const summary = services.summary || { ok: 0, total: 0 };
|
||||
const tasks = monitor.tasks || [];
|
||||
const tier1 = monitor.tier1 || {};
|
||||
const tier2 = monitor.tier2 || {};
|
||||
|
||||
// Summary cards
|
||||
let html = '<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-[#3fb950] font-mono">' + (summary.ok || 0) + '</div><div class="text-xs text-slate-500">正常服务</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-white font-mono">' + (summary.total || 0) + '</div><div class="text-xs text-slate-500">总服务数</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-[#d29922] font-mono">' + (tier1.summary?.total || svcs.length) + '</div><div class="text-xs text-slate-500">一级服务</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-[#58a6ff] font-mono">' + (tier2.summary?.total || 0) + '</div><div class="text-xs text-slate-500">二级服务</div></div>';
|
||||
html += '</div>';
|
||||
|
||||
// Services by layer
|
||||
const layers = {};
|
||||
svcs.forEach(s => {
|
||||
const layer = s.layer || 'other';
|
||||
if (!layers[layer]) layers[layer] = [];
|
||||
layers[layer].push(s);
|
||||
});
|
||||
|
||||
html += '<div class="card overflow-hidden mb-4"><table class="w-full text-xs"><thead><tr class="text-slate-500 border-b border-slate-800/50"><th class="text-left p-3">层级</th><th class="text-left p-3">服务</th><th class="text-left p-3">端口</th><th class="text-left p-3">状态</th></tr></thead><tbody>';
|
||||
Object.keys(layers).sort().forEach(layer => {
|
||||
layers[layer].forEach((s, i) => {
|
||||
const ok = s.health?.ok;
|
||||
const statusIcon = ok ? '🟢' : (ok === false ? '🔴' : '🟡');
|
||||
const statusColor = ok ? 'text-[#3fb950]' : (ok === false ? 'text-[#f85149]' : 'text-[#d29922]');
|
||||
const label = s.label || s.name || s.type || '';
|
||||
const port = s.port || '-';
|
||||
if (i === 0) {
|
||||
html += '<tr class="border-b border-slate-800/30"><td class="p-3 font-semibold text-slate-300" rowspan="' + layers[layer].length + '">' + layer + '</td><td class="p-3">' + s.name + ' <span class="text-slate-500">' + label + '</span></td><td class="p-3 font-mono">' + port + '</td><td class="p-3 ' + statusColor + '">' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + '</td></tr>';
|
||||
} else {
|
||||
html += '<tr class="border-b border-slate-800/30"><td class="p-3">' + s.name + ' <span class="text-slate-500">' + label + '</span></td><td class="p-3 font-mono">' + port + '</td><td class="p-3 ' + statusColor + '">' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + '</td></tr>';
|
||||
}
|
||||
});
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
|
||||
// Task status
|
||||
if (tasks.length) {
|
||||
html += '<div class="card overflow-hidden"><table class="w-full text-xs"><thead><tr class="text-slate-500 border-b border-slate-800/50"><th class="text-left p-3">任务</th><th class="text-left p-3">状态</th><th class="text-left p-3">详情</th></tr></thead><tbody>';
|
||||
tasks.forEach(t => {
|
||||
const st = t.status || '';
|
||||
const sc = st === 'ok' || st === 'running' ? 'text-[#3fb950]' : st === 'error' ? 'text-[#f85149]' : st === 'pending' ? 'text-[#d29922]' : 'text-slate-500';
|
||||
html += '<tr class="border-b border-slate-800/30"><td class="p-3">' + (t.name || '') + '</td><td class="p-3 ' + sc + '">' + st + '</td><td class="p-3 text-slate-500">' + (t.detail || '') + '</td></tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
html += '<div class="text-xs text-slate-600 mt-2">更新于 ' + (monitor.generated_at || new Date().toLocaleTimeString()) + ' · 每10秒刷新</div>';
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="text-red-400">加载健康状态失败: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ── 盯盘 ──
|
||||
let watchTimer = null;
|
||||
async function renderWatch() {
|
||||
|
||||
Reference in New Issue
Block a user