From 8237a9e6dbdb49ccf4c7b5a987566eddb02bcea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=BD=E5=B7=A5=E5=91=98?= <4846a27a-ba87-49c9-939d-7275f63efcc7@agents.multica.local> Date: Mon, 24 Aug 2026 20:13:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(HEL-90):=20P2b=20=E9=AA=8C=E6=94=B6?= =?UTF-8?q?=E4=BA=94=E6=9D=A1=E6=95=B4=E6=94=B9=20+=20=E5=A4=9C=E9=97=B4?= =?UTF-8?q?=E9=BB=91=E5=AD=97=E6=8E=92=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- app/frontend/m/css/features.css | 73 ++++++++++++++++++++++++------- app/frontend/m/css/tokens.css | 1 + app/frontend/m/js/pages.js | 73 ++++++++++++++++++++++--------- app/frontend/pages/ladder/page.js | 3 +- 4 files changed, 111 insertions(+), 39 deletions(-) diff --git a/app/frontend/m/css/features.css b/app/frontend/m/css/features.css index ccd6488..943bc86 100644 --- a/app/frontend/m/css/features.css +++ b/app/frontend/m/css/features.css @@ -1076,7 +1076,7 @@ .m-chart-card { padding: 8px 6px; - height: var(--mobile-chart-height); + height: var(--mobile-chart-height-compact); } .m-chart-card svg { @@ -1148,18 +1148,18 @@ .m-sent-metrics { display: flex; - gap: 8px; + gap: 6px; margin-top: 12px; - flex-wrap: wrap; + flex-wrap: nowrap; } .m-sent-metric { - flex: 1; - min-width: 72px; + flex: 1 1 0; + min-width: 0; display: flex; flex-direction: column; gap: 2px; - padding: 8px; + padding: 8px 6px; border-radius: 6px; background: var(--surface-muted); } @@ -1167,18 +1167,31 @@ .m-sent-metric span { font-size: var(--font-size-aux); color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .m-sent-metric b { font-size: var(--font-size-card-title); font-weight: 600; font-variant-numeric: tabular-nums; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .m-sent-up b { color: var(--market-up); } .m-sent-warn b { color: var(--warning); } .m-sent-down b { color: var(--market-down); } +@media (max-width: 359px) { + .m-sent-metrics { gap: 4px; } + .m-sent-metric { padding: 8px 4px; } + .m-sent-metric span { font-size: 10.5px; } + .m-sent-metric b { font-size: 14px; } +} + .m-sent-advice { margin-top: 10px; font-size: var(--font-size-label); @@ -1337,12 +1350,14 @@ .m-ladder-tier-count { font-size: var(--font-size-label); color: var(--text-secondary); + white-space: nowrap; } .m-ladder-rate { - margin-left: auto; + flex: 0 0 auto; font-size: var(--font-size-aux); color: var(--text-tertiary); + white-space: nowrap; } .m-ladder-stocks { @@ -1426,15 +1441,24 @@ color: var(--text-tertiary); } -.m-ladder-more { - width: 100%; - height: 40px; +.m-ladder-fold { + margin-left: auto; + flex: 0 0 auto; + min-height: 32px; + padding: 0 2px; border: 0; - border-top: 1px solid var(--border-subtle); background: transparent; color: var(--action); - font-size: var(--font-size-label); + font-size: var(--font-size-aux); + white-space: nowrap; cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: transform var(--motion-press-release) var(--ease-press); +} + +.m-ladder-fold:active { + transform: scale(0.94); + transition-duration: var(--motion-press); } .m-rate-list { @@ -1512,16 +1536,17 @@ } .m-rotation-chips { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(2, 1fr); gap: 8px; } .m-rotation-chip { - display: inline-flex; + display: flex; align-items: center; - gap: 6px; - padding: 6px 10px; + gap: 8px; + min-width: 0; + padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border-strong); background: var(--surface); @@ -1536,14 +1561,28 @@ background: var(--surface-hover); } +.m-rotation-copy { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + .m-rotation-chip strong { font-size: var(--font-size-label); font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .m-rotation-chip small { font-size: var(--font-size-aux); color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .m-rotation-chip.heat-strong { diff --git a/app/frontend/m/css/tokens.css b/app/frontend/m/css/tokens.css index f710d13..c53a081 100644 --- a/app/frontend/m/css/tokens.css +++ b/app/frontend/m/css/tokens.css @@ -45,6 +45,7 @@ --mobile-safe-bottom: env(safe-area-inset-bottom, 0px); --mobile-page-padding: 12px; --mobile-chart-height: 240px; + --mobile-chart-height-compact: 168px; --mobile-table-row-height: 44px; --mobile-table-header-height: 40px; --mobile-icon-cell: 78px; diff --git a/app/frontend/m/js/pages.js b/app/frontend/m/js/pages.js index 0f5e8a0..49cc544 100644 --- a/app/frontend/m/js/pages.js +++ b/app/frontend/m/js/pages.js @@ -372,7 +372,7 @@ } const head = ordered.map(function (col, i) { - const sortable = sortableColumn(col); + const sortable = !(opts && opts.noSort) && sortableColumn(col); let extraCls = ""; let extraAttrs = ""; let indicator = ""; @@ -813,7 +813,7 @@ } function sentimentTrendChart(rows) { - const W = 360, H = 240, padL = 8, padR = 30, padT = 12, padB = 22; + const W = 360, H = 168, padL = 8, padR = 30, padT = 10, padB = 20; const pw = W - padL - padR; const ph = H - padT - padB; if (!rows.length) return emptyChart("暂无情绪历史数据"); @@ -927,7 +927,7 @@ return raw.slice().reverse().map(function (r) { return Object.assign({}, r, { trade_date: displayCompactDate(r.trade_date) }); }); - }, {}); + }, { noSort: true }); } function sentMetric(label, valueHtml, tone) { @@ -951,6 +951,7 @@ function loadLadder() { const seq = nextSeq(); + Object.keys(expandedLadder).forEach(function (level) { delete expandedLadder[level]; }); const date = state.requestedDate; global.MobileAPI.request("/api/dashboard?trade_date=" + encodeURIComponent(date)).then(function (payload) { if (seq !== state.seq) return; @@ -974,6 +975,7 @@ const rateHtml = level > 1 && count ? '较' + (level - 1) + "板 " + rate + "%" : ""; let stocksHtml = ""; + let foldBtn = ""; if (gap) { stocksHtml = '
' + (level >= maxLevel ? "断层 · " + escapeHtml(label) + "及以上空缺" : "该层暂时空缺") + "
"; } else { @@ -995,15 +997,16 @@ "" + escapeHtml(s.first_time && s.first_time !== "--" ? s.first_time : "时间待校正") + " · " + escapeHtml(amount) + "" + ""; }).join(""); - const remaining = !expanded && cap && stocks.length > cap ? stocks.length - cap : 0; - if (remaining) { - stocksHtml += '"; + if (cap && stocks.length > cap) { + const remaining = stocks.length - cap; + foldBtn = '"; } } return '
' + '
' + escapeHtml(label) + "" + - '' + count + " 只" + rateHtml + "
" + + '' + count + " 只" + rateHtml + foldBtn + "" + '
' + stocksHtml + "
"; } @@ -1019,7 +1022,6 @@ ladders.forEach(function (g) { groupMap[number(g.level)] = g; }); const yesterday = dash.yesterday_limits || []; const prevMax = yesterday.reduce(function (m, r) { return Math.max(m, number(r.prior_streak)); }, 0); - const spaceStocks = (groupMap[maxLevel] || {}).stocks || []; const spaceChange = prevMax && maxLevel < prevMax ? "较昨日 " + prevMax + " 板 ↓ 压缩" : prevMax && maxLevel > prevMax ? "较昨日 " + prevMax + " 板 ↑ 抬升" : "高度与昨日接近"; const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度,重点看承接。" @@ -1027,7 +1029,6 @@ const apexHtml = '
' + '
空间板' + (maxLevel ? maxLevel + " 板" : "--") + "" + escapeHtml(spaceChange) + "
" + - '

' + (spaceStocks.length ? spaceStocks.map(function (s) { return "" + escapeHtml(s.name) + "(" + escapeHtml(s.sector || "其他") + ")"; }).join(" · ") : "暂无空间板") + "

" + "" + escapeHtml(spaceNote) + "
"; const tiers = []; @@ -1082,8 +1083,10 @@ const heat = strength >= 90 ? "strong" : strength >= 70 ? "warm" : "mild"; return '"; }).join(""); return '
' + @@ -1586,30 +1589,60 @@ function bindSheetDrag(root) { const sheet = root.querySelector(".m-sheet"); + const backdrop = root.querySelector(".m-sheet-backdrop"); const handle = root.querySelector(".m-sheet-handle"); - if (!sheet || !handle) return; + const head = root.querySelector(".m-sheet-head"); + if (!sheet) return; let startY = 0; + let startT = 0; let dragging = false; - handle.addEventListener("touchstart", function (event) { + + function begin(event) { + if (event.target && event.target.closest(".m-sheet-close")) return; dragging = true; - startY = event.touches[0].clientY; + startY = event.clientY; + startT = Date.now(); sheet.style.transition = "none"; - }, { passive: true }); - handle.addEventListener("touchmove", function (event) { + if (backdrop) backdrop.style.transition = "none"; + if (event.currentTarget && event.currentTarget.setPointerCapture) { + try { event.currentTarget.setPointerCapture(event.pointerId); } catch (e) { /* noop */ } + } + } + + function move(event) { if (!dragging) return; - const dy = event.touches[0].clientY - startY; - if (dy > 0) sheet.style.transform = "translateY(" + dy + "px)"; - }, { passive: true }); - handle.addEventListener("touchend", function (event) { + const dy = event.clientY - startY; + if (dy > 0) { + sheet.style.transform = "translateY(" + dy + "px)"; + if (backdrop) { + const ratio = Math.min(1, dy / sheet.offsetHeight); + backdrop.style.opacity = String(Math.max(0, 1 - ratio)); + } + } + } + + function end(event) { if (!dragging) return; dragging = false; sheet.style.transition = ""; - const dy = event.changedTouches[0].clientY - startY; - if (dy > sheet.offsetHeight / 3) { + if (backdrop) { backdrop.style.opacity = ""; backdrop.style.transition = ""; } + const dy = event.clientY - startY; + const dt = Date.now() - startT; + const velocity = dt > 0 ? dy / dt : 0; + if (dy >= sheet.offsetHeight * 0.25 || (velocity >= 0.5 && dy >= 40)) { closeSheet(); } else { sheet.style.transform = ""; } + } + + [handle, head].forEach(function (grip) { + if (!grip) return; + grip.style.touchAction = "none"; + grip.addEventListener("pointerdown", begin); + grip.addEventListener("pointermove", move); + grip.addEventListener("pointerup", end); + grip.addEventListener("pointercancel", end); }); } diff --git a/app/frontend/pages/ladder/page.js b/app/frontend/pages/ladder/page.js index 7a037b8..815bc31 100644 --- a/app/frontend/pages/ladder/page.js +++ b/app/frontend/pages/ladder/page.js @@ -33,7 +33,6 @@ function renderLadderBoard(ladders) { return groupMap.get(level) || { level, label: level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}板`, count: 0, stocks: [] }; }); const total = ordered.reduce((sum, group) => sum + number(group.count), 0); - const spaceStocks = ordered.find((group) => number(group.level) === maxLevel)?.stocks || []; const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value); const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || ""); setText("ladderDateRange", `数据日期 ${currentDate}`); @@ -77,7 +76,7 @@ function renderLadderBoard(ladders) { const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度,重点观察承接而非单看高度。" : maxLevel >= 3 ? "空间位于中段,梯队延续性比绝对高度更重要。" : "高度受到压缩,先观察首板向二板的结构修复。"; const strongestGroup = structureRows.reduce((best, group) => number(group.count) > number(best?.count) ? group : best, structureRows[0]); insights.innerHTML = ` -

空间板

市场高度
${maxLevel ? `${maxLevel} 板` : "--"}${escapeHtml(spaceChange)}

${spaceStocks.length ? spaceStocks.map((stock) => `${escapeHtml(stock.name)}(${escapeHtml(stock.sector || "其他")})`).join(" · ") : "暂无空间板"}

${spaceNote}

+

空间板

市场高度
${maxLevel ? `${maxLevel} 板` : "--"}${escapeHtml(spaceChange)}

${spaceNote}

梯队结构

完整度
${structureRows.map((group) => `
${escapeHtml(group.label || `${number(group.level)}板`)}${number(group.count) ? `${number(group.count)} 只` : "断层"}
`).join("")}

断层越少,梯队从低位向高位传导越连贯。当前腰部为 ${escapeHtml(strongestGroup?.label || "--")}

晋级率参考

昨日梯队 → 今日
${rateRows.length ? rateRows.map((row) => `
${escapeHtml(row.label)}${formatNumber(row.value, 1)}%
`).join("") : '
暂无可比梯队
'}
数据来自“涨停表现”页 · 昨日梯队样本
`; container.querySelectorAll("[data-ladder-level]").forEach((button) => {