refactor: establish standalone application boundary
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user