refactor: establish standalone application boundary

This commit is contained in:
leefer
2026-08-03 21:42:25 +08:00
parent cc5fb8d73e
commit e1e76cd51e
324 changed files with 63090 additions and 44743 deletions
+199
View File
@@ -0,0 +1,199 @@
async function openEntityDetail(item) {
state.entityDetailItem = item;
state.entityDetailPayload = null;
state.entityDetailIntraday = null;
state.entityDetailChartMode = "daily";
const requestSequence = ++state.entityDetailRequestSequence;
syncDetailChartButtons("entity", "daily");
setText("entityDetailCode", item.code || item.id || "--");
setText("entityDetailName", item.name || "--");
setText("entityDetailValue", "--");
setText("entityDetailChange", "--");
setText("entityDetailType", item.type_label || "--");
setText("entityDetailDate", "正在加载行情");
document.querySelector("#entityDetailChange").className = "";
renderEmptyState("entityDetailMetrics", "正在加载交易数据");
openModalDialog(elements.entityDetailDialog);
clearEntityDetailChart("正在加载日 K 数据");
try {
const params = new URLSearchParams({ type: item.type, id: item.id, trade_date: elements.tradeDate.value });
const payload = await apiRequest(`/api/search/detail?${params}`);
if (requestSequence !== state.entityDetailRequestSequence) return;
state.entityDetailPayload = payload;
const entity = payload.entity || {};
setText("entityDetailCode", entity.code || item.code || "--");
setText("entityDetailName", entity.name || item.name || "--");
setText("entityDetailValue", meaningfulNumber(entity.value) && number(entity.value) !== 0 ? formatNumber(entity.value, 2) : "--");
setText("entityDetailChange", `${signed(entity.change)}%`);
setText("entityDetailType", entity.type_label || item.type_label || "--");
document.querySelector("#entityDetailChange").className = changeClass(entity.change);
renderEntityDetailMetrics(payload.metrics || []);
if (state.entityDetailChartMode === "daily") {
setText("entityDetailDate", `${payload.meta?.realtime ? "实时" : "收盘"} · ${payload.meta?.trade_date || "--"}`);
requestAnimationFrame(() => drawEntityDetailChart(payload.series || []));
}
} catch (error) {
if (requestSequence !== state.entityDetailRequestSequence) return;
setText("entityDetailDate", "行情加载失败");
renderEmptyState("entityDetailMetrics", error.message || "交易数据加载失败");
if (state.entityDetailChartMode === "daily") clearEntityDetailChart(error.message || "行情加载失败");
showToast(error.message || "详情加载失败");
}
}
async function selectEntityDetailChart(mode) {
const selected = mode === "intraday" ? "intraday" : "daily";
state.entityDetailChartMode = selected;
syncDetailChartButtons("entity", selected);
if (selected === "daily") {
const payload = state.entityDetailPayload;
if (payload) {
setText("entityDetailDate", `${payload.meta?.realtime ? "实时" : "收盘"} · ${payload.meta?.trade_date || "--"}`);
requestAnimationFrame(() => drawEntityDetailChart(payload.series || []));
} else clearEntityDetailChart("正在加载日 K 数据");
return;
}
if (state.entityDetailIntraday) {
renderEntityIntraday(state.entityDetailIntraday);
return;
}
const item = state.entityDetailItem;
if (!item) return;
const requestSequence = state.entityDetailRequestSequence;
setText("entityDetailDate", "正在加载分时");
clearEntityDetailChart("正在加载分时数据");
try {
const params = new URLSearchParams({ type: item.type, id: item.id });
const payload = await apiRequest(`/api/chart/intraday?${params}`);
if (requestSequence !== state.entityDetailRequestSequence) return;
state.entityDetailIntraday = payload;
if (state.entityDetailChartMode === "intraday") renderEntityIntraday(payload);
} catch (error) {
if (requestSequence !== state.entityDetailRequestSequence || state.entityDetailChartMode !== "intraday") return;
setText("entityDetailDate", "分时暂不可用");
clearEntityDetailChart(error.message || "分时行情暂不可用");
}
}
function renderEntityIntraday(payload) {
const points = payload.points || [];
if (!points.length) {
setText("entityDetailDate", "分时暂不可用");
clearEntityDetailChart("分时行情暂不可用");
return;
}
setText("entityDetailDate", `分时 · ${payload.meta?.trade_date || "--"}`);
requestAnimationFrame(() => {
if (state.entityDetailChartMode !== "intraday") return;
drawIntradayCanvas(elements.entityDetailChart, points, [], payload.meta?.previous_close);
});
}
function syncDetailChartButtons(scope, mode) {
const selector = scope === "stock" ? "[data-stock-detail-chart]" : "[data-entity-detail-chart]";
const datasetKey = scope === "stock" ? "stockDetailChart" : "entityDetailChart";
document.querySelectorAll(selector).forEach((button) => {
const active = button.dataset[datasetKey] === mode;
button.classList.toggle("active", active);
button.setAttribute("aria-pressed", String(active));
});
}
function renderEntityDetailMetrics(metrics) {
const container = document.querySelector("#entityDetailMetrics");
if (!metrics.length) {
renderEmptyState(container, "暂无交易数据");
return;
}
container.innerHTML = metrics.map((metric) => {
const value = typeof metric.value === "number" ? formatNumber(metric.value, Number.isInteger(metric.value) ? 0 : 2) : String(metric.value ?? "--");
const tone = metric.tone === "change" ? changeClass(metric.value) : "";
return `<div><dt>${escapeHtml(metric.label)}</dt><dd class="${tone}">${escapeHtml(value)}${escapeHtml(metric.unit || "")}</dd></div>`;
}).join("");
}
function drawEntityDetailChart(series, canvas = elements.entityDetailChart) {
const candles = (series || []).filter((item) => number(item.close) > 0).map((item) => {
const close = number(item.close);
const open = number(item.open) || close;
const high = Math.max(number(item.high) || close, open, close);
const low = Math.min(number(item.low) || close, open, close);
return { ...item, open, high, low, close };
});
if (!candles.length) {
clearEntityDetailChart("暂无日 K 数据", canvas);
return;
}
const rect = canvas.getBoundingClientRect();
const ratio = window.devicePixelRatio || 1;
const width = Math.max(320, rect.width);
const height = Math.max(220, rect.height);
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
const context = canvas.getContext("2d");
const palette = currentChartPalette();
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.clearRect(0, 0, width, height);
context.fillStyle = palette.background;
context.fillRect(0, 0, width, height);
const left = 48;
const right = 12;
const top = 14;
const bottom = 22;
const volumeHeight = 54;
const gap = 12;
const priceBottom = height - bottom - volumeHeight - gap;
const plotWidth = width - left - right;
const maximum = Math.max(...candles.map((item) => item.high));
const minimum = Math.min(...candles.map((item) => item.low));
const range = Math.max(maximum - minimum, maximum * 0.01, 0.01);
const maxVolume = Math.max(...candles.map((item) => number(item.volume)), 1);
const priceY = (value) => top + (maximum - value) / range * (priceBottom - top);
const step = plotWidth / candles.length;
const candleWidth = clamp(step * 0.62, 2, 8);
context.strokeStyle = palette.grid;
context.fillStyle = palette.axis;
context.font = "11px Microsoft YaHei";
context.textAlign = "right";
for (let line = 0; line <= 4; line += 1) {
const lineY = top + (priceBottom - top) * line / 4;
context.beginPath();
context.moveTo(left, lineY);
context.lineTo(width - right, lineY);
context.stroke();
context.fillText((maximum - range * line / 4).toFixed(2), left - 6, lineY + 4);
}
candles.forEach((item, index) => {
const x = left + step * index + step / 2;
const color = drawCandlestick(context, x, item, priceY, candleWidth, palette);
const volumeBarHeight = number(item.volume) / maxVolume * volumeHeight;
context.fillStyle = color;
context.globalAlpha = 0.72;
context.fillRect(x - candleWidth / 2, height - bottom - volumeBarHeight, candleWidth, volumeBarHeight);
context.globalAlpha = 1;
});
context.textAlign = "center";
context.fillStyle = palette.axis;
[0, Math.floor((candles.length - 1) / 2), candles.length - 1].forEach((index) => {
const x = left + step * index + step / 2;
context.fillText(String(candles[index].trade_date || "").slice(5), x, height - 5);
});
}
function clearEntityDetailChart(message, canvas = elements.entityDetailChart) {
const rect = canvas.getBoundingClientRect();
const width = Math.max(320, Math.round(rect.width || 680));
const height = Math.max(220, Math.round(rect.height || 300));
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
const palette = currentChartPalette();
context.fillStyle = palette.background;
context.fillRect(0, 0, width, height);
context.fillStyle = palette.axis;
context.font = "13px Microsoft YaHei";
context.textAlign = "center";
context.fillText(message, width / 2, height / 2);
}