154 lines
5.0 KiB
JavaScript
154 lines
5.0 KiB
JavaScript
const metricAnimationFrames = new WeakMap();
|
|
|
|
let rowAnimationObserver = null;
|
|
|
|
|
|
function changeClass(value) {
|
|
return number(value) > 0 ? "up" : number(value) < 0 ? "down" : "";
|
|
}
|
|
|
|
|
|
function streakLabel(streak) {
|
|
const value = Math.max(1, number(streak));
|
|
return value === 1 ? "首板" : `${value}板`;
|
|
}
|
|
|
|
function signed(value) {
|
|
const parsed = number(value);
|
|
return `${parsed > 0 ? "+" : ""}${formatNumber(parsed, 2)}`;
|
|
}
|
|
|
|
|
|
function formatMoneyMillion(value) {
|
|
const parsed = number(value);
|
|
const sign = parsed > 0 ? "+" : "";
|
|
if (Math.abs(parsed) >= 100) return `${sign}${formatNumber(parsed / 100, 2)} 亿`;
|
|
return `${sign}${formatNumber(parsed * 100, 0)} 万`;
|
|
}
|
|
|
|
async function apiRequest(url, method = "GET", body = null, requestOptions = {}) {
|
|
return window.XiaobaiAPI.request(url, method, body, requestOptions);
|
|
}
|
|
|
|
function setLoading(loading, text = "正在加载复盘数据", context = "default") {
|
|
elements.loading.hidden = !loading;
|
|
elements.loading.dataset.context = loading ? context : "default";
|
|
setText("loadingTitle", text);
|
|
setText(
|
|
"loadingHint",
|
|
context === "screener"
|
|
? "正在完成因子筛选、候选排序与历史样本回测,这通常需要一点时间"
|
|
: "请稍候",
|
|
);
|
|
}
|
|
|
|
function setStatus(text) {
|
|
applicationShell.setStatus(text);
|
|
}
|
|
|
|
let toastTimer;
|
|
function showToast(message) {
|
|
clearTimeout(toastTimer);
|
|
elements.toast.textContent = message;
|
|
elements.toast.hidden = false;
|
|
toastTimer = setTimeout(() => { elements.toast.hidden = true; }, 3600);
|
|
}
|
|
|
|
function setText(id, value) {
|
|
const element = document.getElementById(id);
|
|
if (element) element.textContent = value;
|
|
}
|
|
|
|
function motionEnabled() {
|
|
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
}
|
|
|
|
function refreshIcons() {
|
|
if (!window.lucide?.createIcons) return;
|
|
window.lucide.createIcons({ attrs: { "aria-hidden": "true" } });
|
|
}
|
|
|
|
function toggleHeaderCommandMenu(force) {
|
|
applicationShell.toggleHeaderCommandMenu(force);
|
|
}
|
|
|
|
|
|
function animateMetric(id, rawValue, formatter = (value) => value) {
|
|
const element = document.getElementById(id);
|
|
const target = Number(rawValue);
|
|
if (!element || !Number.isFinite(target)) {
|
|
setText(id, formatter(rawValue));
|
|
return;
|
|
}
|
|
const storedValue = Number(element.dataset.metricValue);
|
|
const previous = Number.isFinite(storedValue) ? storedValue : 0;
|
|
element.dataset.metricValue = String(target);
|
|
const existingFrame = metricAnimationFrames.get(element);
|
|
if (existingFrame) cancelAnimationFrame(existingFrame);
|
|
if (!motionEnabled() || previous === target) {
|
|
element.textContent = formatter(target);
|
|
return;
|
|
}
|
|
element.classList.remove("metric-changed");
|
|
void element.offsetWidth;
|
|
element.classList.add("metric-changed");
|
|
const startedAt = performance.now();
|
|
const duration = 560;
|
|
const update = (now) => {
|
|
const progress = Math.min(1, (now - startedAt) / duration);
|
|
const eased = 1 - (1 - progress) ** 3;
|
|
element.textContent = formatter(previous + (target - previous) * eased);
|
|
if (progress < 1) {
|
|
metricAnimationFrames.set(element, requestAnimationFrame(update));
|
|
} else {
|
|
element.textContent = formatter(target);
|
|
metricAnimationFrames.delete(element);
|
|
setTimeout(() => element.classList.remove("metric-changed"), 80);
|
|
}
|
|
};
|
|
metricAnimationFrames.set(element, requestAnimationFrame(update));
|
|
}
|
|
|
|
function animateRows(container) {
|
|
if (!container) return;
|
|
const rows = [...container.children].filter((item) => item.matches("tr, [data-code]"));
|
|
if (!motionEnabled()) {
|
|
rows.forEach((row) => row.classList.remove("row-pending", "row-enter"));
|
|
return;
|
|
}
|
|
const unseenRows = rows.filter((row) => row.dataset.motionSeen !== "1");
|
|
unseenRows.slice(0, 12).forEach((row, index) => {
|
|
row.dataset.motionSeen = "1";
|
|
row.classList.remove("row-pending", "row-enter");
|
|
row.style.setProperty("--row-delay", `${index * 24}ms`);
|
|
requestAnimationFrame(() => row.classList.add("row-enter"));
|
|
row.addEventListener("animationend", () => row.classList.remove("row-enter"), { once: true });
|
|
});
|
|
if (!("IntersectionObserver" in window)) {
|
|
unseenRows.slice(12).forEach((row) => { row.dataset.motionSeen = "1"; });
|
|
return;
|
|
}
|
|
if (!rowAnimationObserver) {
|
|
rowAnimationObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
if (!entry.isIntersecting) return;
|
|
const row = entry.target;
|
|
rowAnimationObserver.unobserve(row);
|
|
row.dataset.motionSeen = "1";
|
|
row.classList.remove("row-pending");
|
|
row.style.setProperty("--row-delay", "0ms");
|
|
requestAnimationFrame(() => row.classList.add("row-enter"));
|
|
row.addEventListener("animationend", () => row.classList.remove("row-enter"), { once: true });
|
|
});
|
|
}, { threshold: 0.08, rootMargin: "0px 0px 40px 0px" });
|
|
}
|
|
unseenRows.slice(12).forEach((row) => {
|
|
row.classList.add("row-pending");
|
|
rowAnimationObserver.observe(row);
|
|
});
|
|
}
|
|
|
|
function waitForMotion(duration) {
|
|
return new Promise((resolve) => setTimeout(resolve, motionEnabled() ? duration : 0));
|
|
}
|