245b85ad27
Co-authored-by: Cursor <cursoragent@cursor.com>
338 lines
9.3 KiB
JavaScript
338 lines
9.3 KiB
JavaScript
/**
|
|
* 账户流水:资金/交易 Tab · 分页 10 · SSE 自动刷新 · 时间窗跟随顶栏预设.
|
|
*/
|
|
(function (global) {
|
|
const PAGE_SIZE = 10;
|
|
let account = "funding";
|
|
let page = 1;
|
|
let pages = 0;
|
|
let localVersion = 0;
|
|
let es = null;
|
|
let reconnectTimer = null;
|
|
let loading = false;
|
|
let booted = false;
|
|
|
|
function root() {
|
|
const active = document.querySelector('.embed-tab-pane.is-active-pane [data-account-ledger="1"]');
|
|
if (active) return active;
|
|
return document.getElementById("account-ledger-root");
|
|
}
|
|
|
|
function $(id) {
|
|
const r = root();
|
|
return (r && r.querySelector("#" + id)) || document.getElementById(id);
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s == null ? "" : s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function listWindowQs() {
|
|
if (typeof global.listWindowQueryString === "function") {
|
|
const q = global.listWindowQueryString();
|
|
return q ? (q.charAt(0) === "?" ? q.slice(1) : q) : "";
|
|
}
|
|
try {
|
|
return new URLSearchParams(location.search).toString();
|
|
} catch (_) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function fmtBj(ms) {
|
|
const n = Number(ms);
|
|
if (!Number.isFinite(n) || n <= 0) return "—";
|
|
try {
|
|
const d = new Date(n);
|
|
const parts = new Intl.DateTimeFormat("zh-CN", {
|
|
timeZone: "Asia/Shanghai",
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
hour12: false,
|
|
}).formatToParts(d);
|
|
const get = (t) => (parts.find((p) => p.type === t) || {}).value || "";
|
|
return (
|
|
get("year") +
|
|
"-" +
|
|
get("month") +
|
|
"-" +
|
|
get("day") +
|
|
" " +
|
|
get("hour") +
|
|
":" +
|
|
get("minute") +
|
|
":" +
|
|
get("second")
|
|
);
|
|
} catch (_) {
|
|
return "—";
|
|
}
|
|
}
|
|
|
|
function fmtAmt(v) {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return "—";
|
|
const cls = n > 0 ? "account-ledger-amt-pos" : n < 0 ? "account-ledger-amt-neg" : "";
|
|
const sign = n > 0 ? "+" : "";
|
|
return '<span class="' + cls + '">' + sign + n.toFixed(6).replace(/\.?0+$/, "") + "</span>";
|
|
}
|
|
|
|
function fmtBal(v) {
|
|
if (v == null || v === "") return "—";
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return "—";
|
|
return n.toFixed(6).replace(/\.?0+$/, "");
|
|
}
|
|
|
|
function setStatus(msg, isErr) {
|
|
const el = $("account-ledger-status");
|
|
if (!el) return;
|
|
el.textContent = msg || "";
|
|
el.style.color = isErr ? "#f07178" : "";
|
|
}
|
|
|
|
function setSyncLabel(data) {
|
|
const el = $("account-ledger-sync");
|
|
if (!el) return;
|
|
const ts = data && data.last_sync_at;
|
|
if (!ts) {
|
|
el.textContent = "尚未同步";
|
|
return;
|
|
}
|
|
el.textContent = "同步 " + fmtBj(Number(ts) * 1000);
|
|
}
|
|
|
|
function renderRows(items) {
|
|
const tbody = $("account-ledger-tbody");
|
|
if (!tbody) return;
|
|
if (!items || !items.length) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="muted">当前时间窗暂无流水</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = items
|
|
.map(function (it) {
|
|
const note = [it.symbol, it.note, it.raw_type].filter(Boolean).join(" · ");
|
|
return (
|
|
"<tr>" +
|
|
"<td>" +
|
|
escapeHtml(fmtBj(it.ts_ms)) +
|
|
"</td>" +
|
|
"<td>" +
|
|
escapeHtml(it.ccy || "") +
|
|
"</td>" +
|
|
"<td>" +
|
|
escapeHtml(it.kind_label || it.kind || "") +
|
|
"</td>" +
|
|
"<td>" +
|
|
fmtAmt(it.amount) +
|
|
"</td>" +
|
|
"<td>" +
|
|
escapeHtml(fmtBal(it.balance_after)) +
|
|
"</td>" +
|
|
"<td>" +
|
|
escapeHtml(note || "—") +
|
|
"</td>" +
|
|
"</tr>"
|
|
);
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function renderPager(data) {
|
|
pages = Number(data.pages || 0);
|
|
page = Number(data.page || 1);
|
|
const info = $("account-ledger-page-info");
|
|
const prev = $("account-ledger-prev");
|
|
const next = $("account-ledger-next");
|
|
if (info) {
|
|
info.textContent =
|
|
"第 " + page + " / " + (pages || 1) + " 页 · 共 " + (data.total || 0) + " 条 · 每页 " + PAGE_SIZE;
|
|
}
|
|
if (prev) prev.disabled = page <= 1;
|
|
if (next) next.disabled = !pages || page >= pages;
|
|
}
|
|
|
|
async function loadList(opts) {
|
|
const r = root();
|
|
if (!r) return;
|
|
if (loading) return;
|
|
loading = true;
|
|
const force = opts && opts.force;
|
|
try {
|
|
if (!force) setStatus("加载中…");
|
|
const qs = new URLSearchParams(listWindowQs());
|
|
qs.set("account", account);
|
|
qs.set("page", String(page));
|
|
const res = await fetch("/api/account_ledger?" + qs.toString(), {
|
|
credentials: "same-origin",
|
|
});
|
|
const data = await res.json().catch(function () {
|
|
return {};
|
|
});
|
|
if (!res.ok || data.ok === false) {
|
|
throw new Error(data.msg || res.statusText || "加载失败");
|
|
}
|
|
if (data.ledger_version != null) localVersion = Number(data.ledger_version) || localVersion;
|
|
renderRows(data.items || []);
|
|
renderPager(data);
|
|
setSyncLabel(data);
|
|
const winLabel = (data.window && data.window.label) || "";
|
|
const err = data.last_error ? " · 同步提示: " + data.last_error : "";
|
|
setStatus(
|
|
(winLabel ? "时间窗 " + winLabel + " · " : "") +
|
|
(account === "trading" ? "交易账户" : "资金账户") +
|
|
err,
|
|
!!data.last_error
|
|
);
|
|
} catch (e) {
|
|
setStatus(e.message || String(e), true);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
async function refreshNow() {
|
|
setStatus("正在从交易所同步…");
|
|
try {
|
|
const res = await fetch("/api/account_ledger/refresh", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "{}",
|
|
});
|
|
const data = await res.json().catch(function () {
|
|
return {};
|
|
});
|
|
if (!res.ok || data.ok === false) {
|
|
throw new Error(data.msg || "同步失败");
|
|
}
|
|
await loadList({ force: true });
|
|
} catch (e) {
|
|
setStatus(e.message || String(e), true);
|
|
}
|
|
}
|
|
|
|
function bindUi() {
|
|
const r = root();
|
|
if (!r || r.getAttribute("data-ledger-bound") === "1") return;
|
|
r.setAttribute("data-ledger-bound", "1");
|
|
r.querySelectorAll(".account-ledger-tab").forEach(function (btn) {
|
|
btn.addEventListener("click", function () {
|
|
const acc = btn.getAttribute("data-ledger-account") || "funding";
|
|
if (acc === account) return;
|
|
account = acc;
|
|
page = 1;
|
|
r.querySelectorAll(".account-ledger-tab").forEach(function (b) {
|
|
const on = b.getAttribute("data-ledger-account") === account;
|
|
b.classList.toggle("active", on);
|
|
b.setAttribute("aria-selected", on ? "true" : "false");
|
|
});
|
|
loadList();
|
|
});
|
|
});
|
|
const prev = $("account-ledger-prev");
|
|
const next = $("account-ledger-next");
|
|
const ref = $("account-ledger-refresh");
|
|
if (prev)
|
|
prev.addEventListener("click", function () {
|
|
if (page > 1) {
|
|
page -= 1;
|
|
loadList();
|
|
}
|
|
});
|
|
if (next)
|
|
next.addEventListener("click", function () {
|
|
if (!pages || page < pages) {
|
|
page += 1;
|
|
loadList();
|
|
}
|
|
});
|
|
if (ref) ref.addEventListener("click", refreshNow);
|
|
}
|
|
|
|
function connectSse() {
|
|
if (es) {
|
|
try {
|
|
es.close();
|
|
} catch (_) {}
|
|
es = null;
|
|
}
|
|
if (typeof EventSource === "undefined") return;
|
|
try {
|
|
es = new EventSource("/api/account_ledger/stream");
|
|
es.addEventListener("ledger", function (ev) {
|
|
let data = {};
|
|
try {
|
|
data = JSON.parse(ev.data || "{}");
|
|
} catch (_) {}
|
|
const ver = Number(data.ledger_version || 0);
|
|
if (ver && ver !== localVersion) {
|
|
localVersion = ver;
|
|
loadList({ force: true });
|
|
}
|
|
});
|
|
es.onerror = function () {
|
|
try {
|
|
es.close();
|
|
} catch (_) {}
|
|
es = null;
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
reconnectTimer = setTimeout(connectSse, 5000);
|
|
};
|
|
} catch (_) {}
|
|
}
|
|
|
|
function boot() {
|
|
const r = root();
|
|
if (!r) return;
|
|
bindUi();
|
|
if (!booted) {
|
|
booted = true;
|
|
connectSse();
|
|
}
|
|
loadList();
|
|
}
|
|
|
|
function onTabActivated(tab) {
|
|
if (tab !== "account_ledger") return;
|
|
boot();
|
|
}
|
|
|
|
global.AccountLedgerPage = {
|
|
boot: boot,
|
|
onTabActivated: onTabActivated,
|
|
reload: function () {
|
|
page = 1;
|
|
loadList();
|
|
},
|
|
};
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const page =
|
|
(document.body && document.body.getAttribute("data-page")) ||
|
|
(document.body && document.body.getAttribute("data-initial-tab")) ||
|
|
"";
|
|
if (page === "account_ledger" || root()) {
|
|
// embed 延后到 tab 激活;独立页直接 boot
|
|
if (!document.body || document.body.getAttribute("data-embed-shell") !== "1") {
|
|
boot();
|
|
} else if (page === "account_ledger") {
|
|
boot();
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener("instance-embed-tab-activated", function (ev) {
|
|
const tab = ev && ev.detail && ev.detail.tab;
|
|
onTabActivated(tab);
|
|
});
|
|
})(window);
|