Files
crypto_okx_hub/static/hub.js
T
2026-10-05 06:47:49 +08:00

833 lines
27 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.
(function (global) {
function fmtPnl(v) {
if (v == null || v === "") return "—";
const n = Number(v);
if (!Number.isFinite(n)) return "—";
return (n > 0 ? "+" : "") + n.toFixed(2) + " U";
}
function pnlClass(v) {
const n = Number(v);
if (!Number.isFinite(n) || n === 0) return "pnl-flat";
return n > 0 ? "pnl-up" : "pnl-down";
}
function fmtPnlHtml(v) {
return '<span class="' + pnlClass(v) + '">' + escapeHtml(fmtPnl(v)) + "</span>";
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formatExitBrief(p) {
if (p && p.target_monitor_text && String(p.target_monitor_text).trim() && p.target_monitor_text !== "—") {
return String(p.target_monitor_text).trim();
}
const parts = [];
const tgt = Number(p && p.target_index);
if (Number.isFinite(tgt) && tgt > 0) {
const ot = String((p && p.opt_type) || "").toUpperCase();
const side = ot === "P" ? "Put≤" : ot === "C" ? "Call≥" : "目标";
parts.push(side + " " + tgt);
}
if (p && p.profit_exit_enabled) {
const m = Number(p.profit_exit_mult);
if (Number.isFinite(m) && m > 0) {
parts.push(Math.abs(m - Math.round(m)) < 1e-9 ? Math.round(m) + "倍" : m + "倍");
} else {
parts.push("利润出场");
}
}
return parts.length ? parts.join(" · ") : "未设出场";
}
function activeCondOrder(snap) {
const list = snap && snap.cond_orders_active;
if (Array.isArray(list) && list.length && list[0]) return list[0];
if (snap && snap.cond_order) return snap.cond_order;
if (snap && snap.cond_order_locked) return { status: "armed" };
return null;
}
function condStatusLabel(st) {
const map = { armed: "待命", triggering: "触发中", open: "持仓中" };
return map[st] || "占用";
}
function condStatusClass(st) {
if (st === "open") return "is-open";
if (st === "triggering") return "is-trig";
return "is-armed";
}
function condTagHtml(cond) {
if (!cond) return "";
const st = String(cond.status || "armed");
return (
'<span class="overview-cond-tag ' +
condStatusClass(st) +
'">条件单 · ' +
escapeHtml(condStatusLabel(st)) +
"</span>"
);
}
function condWaitLine(cond) {
if (!cond) return "条件单占用";
const put = String(cond.opt_type || "").toUpperCase() !== "C";
const uly = String(cond.underlying || "").trim();
const side =
String(cond.trigger_side || "").toLowerCase() === "lte"
? "下穿 ≤"
: put
? "上穿 ≥"
: "下穿 ≤";
const px = cond.trigger_px != null && cond.trigger_px !== "" ? String(cond.trigger_px) : "";
const bits = [];
if (uly) bits.push(uly + " " + (put ? "Put" : "Call"));
if (px) bits.push(side + " " + px);
return bits.length ? bits.join(" · ") : "条件单占用";
}
function positionSummary(brief, cond) {
const list = brief || [];
if (!list.length) {
if (cond) {
return (
'<div class="overview-cond-wait">' +
'<div class="overview-cond-wait-line">' +
escapeHtml(condWaitLine(cond)) +
"</div></div>"
);
}
return '<span class="muted">暂无持仓</span>';
}
const opened = cond && String(cond.opened_inst_id || "").trim();
const tagOpen = !!(cond && String(cond.status || "") === "open");
const items = list.slice(0, 8).map(function (p) {
const id = p.inst_id || "";
const sheets = escapeHtml(String(p.sheets != null ? p.sheets : "—"));
const lev = Number(p.contract_leverage != null ? p.contract_leverage : p.leverage);
const levTxt =
Number.isFinite(lev) && lev > 0
? " · 约" + lev.toFixed(1) + "×"
: "";
const fromCond = tagOpen && (!opened || opened === id);
return (
"<li>" +
escapeHtml(id) +
" · " +
sheets +
"张" +
escapeHtml(levTxt) +
" · " +
fmtPnlHtml(p.pnl_usdt) +
(fromCond ? '<span class="overview-cond-src">条件单</span>' : "") +
"</li>"
);
});
const more =
list.length > 8
? '<li class="muted">…共 ' + list.length + " 笔</li>"
: "";
return "<ul class=\"pos-brief\">" + items.join("") + more + "</ul>";
}
function exitSummary(brief) {
const list = brief || [];
if (!list.length) return '<span class="muted">—</span>';
const items = list.slice(0, 8).map(function (p) {
const exitText = formatExitBrief(p);
const exitCls = exitText === "未设出场" ? "exit-unset" : "exit-set";
return '<li class="' + exitCls + '">' + escapeHtml(exitText) + "</li>";
});
const more =
list.length > 8
? '<li class="muted">…共 ' + list.length + " 笔</li>"
: "";
return '<ul class="pos-brief exit-brief">' + items.join("") + more + "</ul>";
}
function closeActionButtons(brief, instanceId, online) {
const list = (brief || []).filter(function (p) {
return !!(p && p.inst_id);
});
if (!online) {
return (
'<button type="button" class="btn btn-close-disabled" disabled title="实例离线">平仓</button>'
);
}
if (!list.length) {
return (
'<button type="button" class="btn btn-close-disabled" disabled title="暂无持仓">平仓</button>'
);
}
return list
.slice(0, 8)
.map(function (p) {
const id = p.inst_id || "";
const invalid = !!(
p.bid_invalid ||
(p.bid_px == null && (p.bid_sz == null || p.bid_sz === ""))
);
const reason = (p.bid_invalid_reason || "买一无效,禁止平仓").toString();
const tip = list.length > 1 ? id + " · 买一限价全平" : "买一限价全平";
if (invalid) {
return (
'<button type="button" class="btn btn-danger" disabled title="' +
escapeHtml(reason) +
'">平仓</button>'
);
}
return (
'<button type="button" class="btn btn-danger" data-close-inst="' +
encodeURIComponent(String(instanceId)) +
'" data-close-opt="' +
encodeURIComponent(id) +
'" title="' +
escapeHtml(tip) +
'">平仓</button>'
);
})
.join("");
}
function fmtBidPx(v) {
if (v == null || v === "") return "—";
const n = Number(v);
if (!Number.isFinite(n)) return "—";
let s = n.toFixed(4).replace(/\.?0+$/, "");
return s || "0";
}
function isLiquidityWeak(p) {
if (!p) return false;
if (p.liquidity_ok === false || p.bid_invalid) return true;
const uncovered = Number(p.uncovered_sheets);
return Number.isFinite(uncovered) && uncovered > 0;
}
function bidLiquidityLine(p) {
if (p.bid_invalid || (p.bid_px == null && (p.bid_sz == null || p.bid_sz === ""))) {
const reason = (p.bid_invalid_reason || "无买一/残档").toString();
const short = reason.length > 18 ? reason.slice(0, 18) + "…" : reason;
return { text: short, weak: true };
}
const px = fmtBidPx(p.bid_px);
const sz = p.bid_sz != null ? String(p.bid_sz) : "—";
const uncovered = Number(p.uncovered_sheets);
let text = px + " / " + sz + "张";
const weak = isLiquidityWeak(p);
if (Number.isFinite(uncovered) && uncovered > 0) {
text += "(差" + uncovered + ")";
}
return { text: text, weak: weak };
}
function bidLiquiditySummary(brief) {
const list = brief || [];
if (!list.length) return '<span class="muted">—</span>';
const items = list.slice(0, 8).map(function (p) {
const line = bidLiquidityLine(p);
const cls = line.weak ? "liq-warn" : "liq-ok";
return '<li class="' + cls + '">' + escapeHtml(line.text) + "</li>";
});
const more =
list.length > 8
? '<li class="muted">…共 ' + list.length + " 笔</li>"
: "";
const anyWeak = list.some(isLiquidityWeak);
const wrapCls = anyWeak ? "pos-brief liq-brief liq-brief--warn" : "pos-brief liq-brief";
return '<ul class="' + wrapCls + '">' + items.join("") + more + "</ul>";
}
function fmtMoneyU(v) {
if (v == null || v === "") return "—";
const n = Number(v);
if (!Number.isFinite(n)) return "—";
return n.toFixed(2) + " U";
}
function fmtRatioPct(v) {
if (v == null || v === "") return "—";
const n = Number(v);
if (!Number.isFinite(n)) return "—";
return n.toFixed(2) + "%";
}
function premiumCell(snap) {
const prem = snap.premium || {};
const paid = prem.paid_usdt;
const equity = prem.equity_usdt;
const cash = prem.cash_usdt;
const tipParts = [];
if (cash != null) tipParts.push("余额 " + fmtMoneyU(cash));
if (paid != null) tipParts.push("开仓占用 " + fmtMoneyU(paid));
if (equity != null) tipParts.push("合计(余额+占用) " + fmtMoneyU(equity));
const tip = tipParts.join(" · ") || "账户资金暂无";
return (
'<span title="' +
escapeHtml(tip) +
'">' +
escapeHtml(fmtMoneyU(paid)) +
"</span>"
);
}
function premiumRatioCell(snap) {
const prem = snap.premium || {};
const ratio = prem.ratio_pct;
const tip =
prem.paid_usdt != null && prem.equity_usdt != null
? "开仓占用 " +
fmtMoneyU(prem.paid_usdt) +
" / (余额+占用) " +
fmtMoneyU(prem.equity_usdt)
: "";
return (
'<span title="' +
escapeHtml(tip) +
'">' +
escapeHtml(fmtRatioPct(ratio)) +
"</span>"
);
}
function renderRow(row) {
const snap = row.snapshot || {};
const inst = snap.instance || {};
const pnl =
snap.pnl && snap.pnl.unrealized_usdt != null
? snap.pnl.unrealized_usdt
: snap.upl_total_usdc;
const brief = snap.positions_brief || snap.positions || [];
const cond = activeCondOrder(snap);
const online = !!row.online;
const status = online
? '<span class="status-on">在线</span>'
: '<span class="status-off">离线</span>';
const err = row.error
? '<div class="muted">错误: ' + escapeHtml(row.error) + "</div>"
: "";
return (
"<tr>" +
"<td><strong>" +
escapeHtml(row.name || "") +
"</strong>" +
condTagHtml(cond) +
err +
"</td>" +
"<td>" +
status +
"</td>" +
"<td>" +
(function () {
const m = String(inst.mode || "").toLowerCase();
if (m === "sim" || m === "live") {
return (
'<span class="overview-mode-tag overview-mode-tag--' +
m +
'">' +
escapeHtml(m) +
"</span>"
);
}
return escapeHtml(inst.mode || "—");
})() +
"</td>" +
"<td>" +
fmtPnlHtml(pnl) +
"</td>" +
"<td>" +
premiumCell(snap) +
"</td>" +
"<td>" +
premiumRatioCell(snap) +
"</td>" +
"<td>" +
positionSummary(brief, cond) +
"</td>" +
"<td>" +
exitSummary(brief) +
"</td>" +
"<td>" +
bidLiquiditySummary(brief) +
"</td>" +
'<td><div class="overview-actions">' +
'<a class="btn" href="/desk?id=' +
encodeURIComponent(row.id) +
'">打开</a>' +
closeActionButtons(brief, row.id, online) +
"</div></td>" +
"</tr>"
);
}
async function requestClose(btn) {
const instanceId = decodeURIComponent(btn.getAttribute("data-close-inst") || "");
const optId = decodeURIComponent(btn.getAttribute("data-close-opt") || "");
if (!instanceId || !optId) return;
if (
!confirm(
"确认向该客户端下发买一限价全平?\n\n合约: " +
optId +
"\n\n无效买一将被拒绝;下单在客户端本地执行。"
)
) {
return;
}
const oldText = btn.textContent;
btn.disabled = true;
btn.textContent = "平仓中…";
try {
const rr = await fetch("/api/instances/" + encodeURIComponent(instanceId) + "/options/close", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ inst_id: optId }),
});
let dd = {};
try {
dd = await rr.json();
} catch (e) {
dd = {};
}
if (dd && dd.ok) {
let msg = dd.msg || "已提交买一全平";
if (dd.submitted_sheets != null) msg += " · " + dd.submitted_sheets + " 张";
if (dd.fully_closed) msg += " · 已全平";
alert(msg);
await refresh();
} else {
alert((dd && dd.msg) || "平仓失败");
btn.disabled = false;
btn.textContent = oldText;
}
} catch (e) {
alert((e && e.message) || "请求失败");
btn.disabled = false;
btn.textContent = oldText;
}
}
function numOrNull(v) {
if (v == null || v === "") return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
function emptyAgg() {
return {
machines: 0,
online: 0,
offline: 0,
positions: 0,
pnl: 0,
premium: 0,
cash: 0,
hasPnl: false,
hasPremium: false,
hasCash: false,
};
}
function addOnlineMetrics(bucket, snap) {
const brief = snap.positions_brief || snap.positions || [];
bucket.positions += brief.length;
const pnl =
snap.pnl && snap.pnl.unrealized_usdt != null
? numOrNull(snap.pnl.unrealized_usdt)
: numOrNull(snap.upl_total_usdc);
if (pnl != null) {
bucket.pnl += pnl;
bucket.hasPnl = true;
}
const prem = snap.premium || {};
const paid = numOrNull(prem.paid_usdt);
const cash = numOrNull(prem.cash_usdt);
if (paid != null) {
bucket.premium += paid;
bucket.hasPremium = true;
}
if (cash != null) {
bucket.cash += cash;
bucket.hasCash = true;
}
}
function summarizeOverview(list) {
const all = emptyAgg();
const sim = emptyAgg();
const live = emptyAgg();
(list || []).forEach(function (row) {
const snap = row.snapshot || {};
const mode = String((snap.instance || {}).mode || "").toLowerCase();
const online = !!row.online;
const targets = [all];
if (mode === "sim") targets.push(sim);
else if (mode === "live") targets.push(live);
targets.forEach(function (b) {
b.machines += 1;
if (online) b.online += 1;
else b.offline += 1;
});
if (!online) return;
addOnlineMetrics(all, snap);
if (mode === "sim") addOnlineMetrics(sim, snap);
else if (mode === "live") addOnlineMetrics(live, snap);
});
return { all: all, sim: sim, live: live };
}
function fmtMoneyPlain(v, known) {
if (!known || v == null || !Number.isFinite(Number(v))) return "—";
return Number(v).toFixed(2) + " U";
}
function renderOverviewSummary(list) {
const box = document.getElementById("overview-summary");
if (!box) return;
if (!list || !list.length) {
box.hidden = true;
box.innerHTML = "";
return;
}
const s = summarizeOverview(list);
function modeRow(label, tag, bucket) {
return (
"<tr>" +
'<td><span class="overview-mode-tag overview-mode-tag--' +
tag +
'">' +
escapeHtml(label) +
"</span></td>" +
'<td class="' +
(bucket.online > 0 ? "status-on" : "") +
'">' +
bucket.online +
"</td>" +
'<td class="' +
(bucket.offline > 0 ? "status-off" : "") +
'">' +
bucket.offline +
"</td>" +
"<td>" +
bucket.positions +
"</td>" +
'<td class="' +
(bucket.hasPnl ? pnlClass(bucket.pnl) : "") +
'">' +
escapeHtml(bucket.hasPnl ? fmtPnl(bucket.pnl) : "—") +
"</td>" +
"<td>" +
escapeHtml(fmtMoneyPlain(bucket.premium, bucket.hasPremium)) +
"</td>" +
"<td>" +
escapeHtml(fmtMoneyPlain(bucket.cash, bucket.hasCash)) +
"</td>" +
"</tr>"
);
}
const rows = [];
if (s.sim.machines > 0) rows.push(modeRow("sim", "sim", s.sim));
if (s.live.machines > 0) rows.push(modeRow("live", "live", s.live));
if (!rows.length) {
box.hidden = true;
box.innerHTML = "";
return;
}
box.hidden = false;
box.innerHTML =
'<div class="overview-mode-wrap">' +
'<div class="overview-mode-head"><strong>实时汇总</strong><span class="muted">仅统计在线机资金与仓位</span></div>' +
'<table class="overview-mode-table">' +
"<thead><tr>" +
"<th>模式</th><th>在线</th><th>离线</th><th>仓位</th><th>浮盈</th><th>权利金</th><th>余额</th>" +
"</tr></thead><tbody>" +
rows.join("") +
"</tbody></table></div>";
}
function modeSortKey(mode) {
const m = String(mode || "").toLowerCase();
if (m === "sim") return 0;
if (m === "live") return 1;
return 2;
}
function sortInstancesByMode(list) {
return (list || []).slice().sort(function (a, b) {
const ma = modeSortKey((((a || {}).snapshot || {}).instance || {}).mode);
const mb = modeSortKey((((b || {}).snapshot || {}).instance || {}).mode);
if (ma !== mb) return ma - mb;
const ia = Number((a && a.id) || 0);
const ib = Number((b && b.id) || 0);
return ia - ib;
});
}
function latestPulledAt(list) {
let best = "";
(list || []).forEach(function (row) {
const t = String((row && row.pulled_at) || "").trim();
if (t && t !== "—" && t > best) best = t;
});
return best;
}
async function refresh() {
const status = document.getElementById("overview-status");
const grid = document.getElementById("overview-grid");
if (!grid) return;
if (status) status.textContent = "刷新中…";
try {
const r = await fetch("/api/overview");
const d = await r.json();
if (!d.ok) throw new Error(d.msg || "失败");
const list = sortInstancesByMode(d.instances || []);
renderOverviewSummary(list);
if (global.HubQuotes && HubQuotes.paint) HubQuotes.paint(list);
if (!list.length) {
grid.innerHTML =
'<p class="muted">暂无启用实例,请先到「实例」添加.</p>';
} else {
grid.innerHTML =
'<div class="overview-table-wrap"><table class="overview-table">' +
"<thead><tr>" +
"<th>名称</th><th>状态</th><th>模式</th><th>浮盈</th><th>权利金</th><th>资金占比</th><th>持仓</th><th>出场</th><th>买一/流动性</th><th>操作</th>" +
"</tr></thead><tbody>" +
list.map(renderRow).join("") +
"</tbody></table></div>";
grid.querySelectorAll("[data-close-opt]").forEach(function (btn) {
btn.addEventListener("click", function () {
requestClose(btn);
});
});
}
if (status) {
const s = summarizeOverview(list);
const pulled = latestPulledAt(list);
status.textContent =
"已更新 · 共 " +
list.length +
" 台 · 在线 " +
s.all.online +
" · 离线 " +
s.all.offline +
(pulled ? " · 拉取时间 " + pulled : "");
}
} catch (e) {
if (status) status.textContent = e.message || "加载失败";
const box = document.getElementById("overview-summary");
if (box) {
box.hidden = true;
box.innerHTML = "";
}
if (global.HubQuotes && HubQuotes.paint) HubQuotes.paint([]);
}
}
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
const LUNAR_MONTHS = ["正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "冬", "腊"];
const LUNAR_INFO = [
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
0x0d520,
];
function lunarLeapMonth(year) {
return LUNAR_INFO[year - 1900] & 0xf;
}
function lunarLeapDays(year) {
if (!lunarLeapMonth(year)) return 0;
return LUNAR_INFO[year - 1900] & 0x10000 ? 30 : 29;
}
function lunarMonthDays(year, month) {
return LUNAR_INFO[year - 1900] & (0x10000 >> month) ? 30 : 29;
}
function lunarYearDays(year) {
let sum = 348;
for (let i = 0x8000; i > 0x8; i >>= 1) {
sum += LUNAR_INFO[year - 1900] & i ? 1 : 0;
}
return sum + lunarLeapDays(year);
}
function lunarDayName(day) {
const n = ["日", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
if (day === 10) return "初十";
if (day === 20) return "二十";
if (day === 30) return "三十";
if (day < 11) return "初" + n[day];
if (day < 20) return "十" + n[day - 10];
return "廿" + n[day - 20];
}
function solarToLunarText(year, month, day) {
if (year < 1900 || year > 2100) return "";
let offset = Math.round(
(Date.UTC(year, month - 1, day) - Date.UTC(1900, 0, 31)) / 86400000
);
let i;
let temp = 0;
for (i = 1900; i < 2101 && offset > 0; i++) {
temp = lunarYearDays(i);
offset -= temp;
}
if (offset < 0) {
offset += temp;
i--;
}
const lunarYear = i;
const leap = lunarLeapMonth(lunarYear);
let isLeap = false;
for (i = 1; i < 13 && offset > 0; i++) {
if (leap > 0 && i === leap + 1 && !isLeap) {
i--;
isLeap = true;
temp = lunarLeapDays(lunarYear);
} else {
temp = lunarMonthDays(lunarYear, i);
}
if (isLeap && i === leap + 1) isLeap = false;
offset -= temp;
}
if (offset === 0 && leap > 0 && i === leap + 1) {
if (isLeap) {
isLeap = false;
} else {
isLeap = true;
i--;
}
}
if (offset < 0) {
offset += temp;
i--;
}
const monthName = (isLeap ? "闰" : "") + LUNAR_MONTHS[i - 1] + "月";
return "农历" + monthName + lunarDayName(offset + 1);
}
function beijingParts() {
try {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Shanghai",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).formatToParts(new Date());
const get = function (type) {
const hit = parts.find(function (p) {
return p.type === type;
});
return hit ? hit.value : "";
};
const y = Number(get("year"));
const m = Number(get("month"));
const d = Number(get("day"));
const weekdayIndex = new Date(Date.UTC(y, m - 1, d)).getUTCDay();
return {
time:
get("year") +
"-" +
get("month") +
"-" +
get("day") +
" " +
get("hour") +
":" +
get("minute") +
":" +
get("second"),
weekdayIndex: weekdayIndex,
weekday: WEEKDAYS[weekdayIndex] || "",
lunar: solarToLunarText(y, m, d),
};
} catch (e) {
const now = new Date();
const utc = now.getTime() + now.getTimezoneOffset() * 60000;
const bj = new Date(utc + 8 * 3600000);
const p = function (n) {
return String(n).padStart(2, "0");
};
const y = bj.getFullYear();
const m = bj.getMonth() + 1;
const d = bj.getDate();
return {
time:
y +
"-" +
p(m) +
"-" +
p(d) +
" " +
p(bj.getHours()) +
":" +
p(bj.getMinutes()) +
":" +
p(bj.getSeconds()),
weekdayIndex: bj.getDay(),
weekday: WEEKDAYS[bj.getDay()] || "",
lunar: solarToLunarText(y, m, d),
};
}
}
function tickBeijingClock() {
const el = document.getElementById("overview-bj-time");
if (!el) return;
const parts = beijingParts();
el.textContent = parts.time;
el.setAttribute("datetime", parts.time.replace(" ", "T") + "+08:00");
const weekend = parts.weekdayIndex === 0 || parts.weekdayIndex === 6;
const clock = document.getElementById("overview-bj-clock");
if (clock) clock.classList.toggle("is-weekend", weekend);
const meta = document.getElementById("overview-bj-meta");
if (meta) {
meta.innerHTML =
(parts.weekday ? '<span class="overview-bj-weekday">' + parts.weekday + "</span>" : "") +
(parts.lunar ? '<span class="overview-bj-lunar">' + parts.lunar + "</span>" : "");
}
}
function boot() {
const btn = document.getElementById("btn-refresh");
if (btn) btn.addEventListener("click", refresh);
tickBeijingClock();
setInterval(tickBeijingClock, 1000);
refresh();
setInterval(refresh, 10000);
}
global.HubOverview = { boot: boot, refresh: refresh };
})(window);