feat(HEL-90): P2b 验收五条整改 + 夜间黑字排查

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
施工员
2026-08-24 20:13:39 +08:00
co-authored by multica-agent
parent 0e6c102ab2
commit 8237a9e6db
4 changed files with 111 additions and 39 deletions
+56 -17
View File
@@ -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 {
+1
View File
@@ -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;
+53 -20
View File
@@ -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 ? '<span class="m-ladder-rate">较' + (level - 1) + "板 " + rate + "%</span>" : "";
let stocksHtml = "";
let foldBtn = "";
if (gap) {
stocksHtml = '<div class="m-ladder-gap">' + (level >= maxLevel ? "断层 · " + escapeHtml(label) + "及以上空缺" : "该层暂时空缺") + "</div>";
} else {
@@ -995,15 +997,16 @@
"<small>" + escapeHtml(s.first_time && s.first_time !== "--" ? s.first_time : "时间待校正") + " · " + escapeHtml(amount) + "</small></span>" +
"</button>";
}).join("");
const remaining = !expanded && cap && stocks.length > cap ? stocks.length - cap : 0;
if (remaining) {
stocksHtml += '<button class="m-ladder-more" type="button" data-ladder-expand="' + level + '">展开剩余 ' + remaining + " 只</button>";
if (cap && stocks.length > cap) {
const remaining = stocks.length - cap;
foldBtn = '<button class="m-ladder-fold" type="button" data-ladder-expand="' + level + '">' +
(expanded ? "收起 \u25b4" : "展开剩余 " + remaining + " 只 \u25be") + "</button>";
}
}
return '<section class="m-ladder-tier' + (gap ? " is-gap" : "") + '">' +
'<div class="m-ladder-tier-head"><span class="m-ladder-tier-level" style="color:' + color + '">' + escapeHtml(label) + "</span>" +
'<span class="m-ladder-tier-count">' + count + " 只</span>" + rateHtml + "</div>" +
'<span class="m-ladder-tier-count">' + count + " 只</span>" + rateHtml + foldBtn + "</div>" +
'<div class="m-ladder-stocks">' + stocksHtml + "</div></section>";
}
@@ -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 = '<div class="m-apex">' +
'<div class="m-apex-head"><span>空间板</span><strong>' + (maxLevel ? maxLevel + " 板" : "--") + "</strong><em>" + escapeHtml(spaceChange) + "</em></div>" +
'<p>' + (spaceStocks.length ? spaceStocks.map(function (s) { return "<b>" + escapeHtml(s.name) + "</b>" + escapeHtml(s.sector || "其他") + ""; }).join(" · ") : "暂无空间板") + "</p>" +
"<small>" + escapeHtml(spaceNote) + "</small></div>";
const tiers = [];
@@ -1082,8 +1083,10 @@
const heat = strength >= 90 ? "strong" : strength >= 70 ? "warm" : "mild";
return '<button class="m-rotation-chip ' + heat + '" type="button" data-rotation-sector="' + escapeHtml(sector.name) + '" data-rotation-date="' + escapeHtml(day.trade_date) + '">' +
'<span class="m-rotation-rank rank-' + Math.min(number(sector.rank), 4) + '">' + number(sector.rank) + "</span>" +
'<span class="m-rotation-copy">' +
"<strong>" + escapeHtml(sector.name) + "</strong>" +
"<small>" + number(sector.count) + " 家 · " + formatNumber(number(sector.strength), 0) + "</small>" +
"</span>" +
"</button>";
}).join("");
return '<section class="m-rotation-day' + (day.trade_date === latestDate ? " is-latest" : "") + '">' +
@@ -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);
});
}