diff --git a/frontend/m/css/features.css b/frontend/m/css/features.css
index ccd6488..943bc86 100644
--- a/frontend/m/css/features.css
+++ b/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/frontend/m/css/tokens.css b/frontend/m/css/tokens.css
index f710d13..c53a081 100644
--- a/frontend/m/css/tokens.css
+++ b/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/frontend/m/js/pages.js b/frontend/m/js/pages.js
index 0f5e8a0..49cc544 100644
--- a/frontend/m/js/pages.js
+++ b/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/frontend/pages/ladder/page.js b/frontend/pages/ladder/page.js
index 7a037b8..815bc31 100644
--- a/frontend/pages/ladder/page.js
+++ b/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) => {