fix(ui): 统计区间文案在日视图恒为空、天数按小时桶误报

v1.9.0 加的「统计区间:起 → 止 (Nd)」正是用来解释"周 > 月"的,但它自己
在日视图上从来不出现在屏幕上。

原因:区间终点从最后一个 bucket 的标签拼出来,而 bucket 粒度随视图变化
—— 日视图按小时("2026-10-03T09"),周/月按天("2026-10-01")。代码无条件
拼 "T00:00:00Z",日视图就成了 "2026-10-03T09T00:00:00Z",Date 拒绝该值,
toISOString() 抛 RangeError,整个赋值语句丢失。页面无任何报错,只是那行
字不见了——恰好是用户最常看的"今日"视图。

第二个问题同源:天数直接用了 st.buckets.length。日视图 5 个桶是 5 个小时,
界面却显示 "(5d)"。改为按窗口起止算天数。

CDP 实测(修复后):
  day   统计区间: 2026-10-03 → 2026-10-03 (1d)   ← 原为空,且曾误报 5d
  week  统计区间: 2026-09-28 → 2026-10-03 (6d)
  month 统计区间: 2026-10-01 → 2026-10-03 (3d)
  all  (空,正确:终身累计没有窗口)

判据一条 + 变异:恢复原来的盲目拼接 → 判红。

Co-Authored-By: ModelRouter <noreply@modelrouter.dev>
This commit is contained in:
JianFeeeee
2026-10-03 12:48:17 +08:00
parent 97864895d4
commit dac0b4ce93
2 changed files with 56 additions and 10 deletions

View File

@ -2016,16 +2016,41 @@ const pnote = $("#period-note");
// like a bug unless the dates are on screen — so print them. // like a bug unless the dates are on screen — so print them.
const prange = $("#period-range"); const prange = $("#period-range");
if (prange && st.period && st.period !== "all" && st.from) { if (prange && st.period && st.period !== "all" && st.from) {
const d0 = new Date(st.from); // Bucket labels are hourly for a day ("2026-10-03T09") and daily
const d1 = st.buckets && st.buckets.length // for a week or month ("2026-10-01"). Appending "T00:00:00Z" to
? new Date(st.buckets[st.buckets.length - 1].bucket + "T00:00:00Z") // the hourly form builds "2026-10-03T09T00:00:00Z", which Date
: d0; // rejects — toISOString() then throws and the whole assignment is
const f = (d) => // lost, leaving the range line silently empty on the day view (the
d.toISOString().slice(0, 10); // one people look at most).
prange.textContent = const f = (d) => d.toISOString().slice(0, 10);
t("periodRange") + const label = st.buckets && st.buckets.length
" " + f(d0) + " → " + f(d1) + ? st.buckets[st.buckets.length - 1].bucket
(st.buckets ? " (" + st.buckets.length + "d)" : ""); : "";
let endMs = 0;
if (/^\d{4}-\d{2}-\d{2}$/.test(label)) {
endMs = new Date(label + "T00:00:00Z").getTime();
} else if (/^\d{4}-\d{2}-\d{2}T\d{2}$/.test(label)) {
// An hourly bucket covers its hour, so the day it belongs to is
// what the range should end on.
endMs = new Date(label + ":00:00Z").getTime();
}
const from = new Date(st.from);
const end = endMs ? new Date(endMs) : from;
// The bucket count is not a day count: the day view buckets by
// HOUR, so "(5d)" for five hours of today was simply wrong. Days
// covered comes from the window itself, which is what the reader
// is trying to establish.
let days = 1;
if (endMs) {
days = Math.max(
1,
Math.round((endMs - from.getTime()) / 86400000) + 1,
);
}
const tail = " (" + days + "d)";
prange.textContent = endMs
? t("periodRange") + " " + f(from) + " → " + f(end) + tail
: t("periodRange") + " " + f(from) + tail;
} else if (prange) { } else if (prange) {
prange.textContent = ""; prange.textContent = "";
} }

View File

@ -266,3 +266,24 @@ func TestUIKeyAutoEditsDoNotAutoPersist(t *testing.T) {
} }
} }
} }
// The stats range line formats its end date from the LAST bucket label.
// Bucket labels are hourly for the day view ("2026-10-03T09") and daily for
// week/month ("2026-10-01"), so blindly appending "T00:00:00Z" to the hourly
// form builds "2026-10-03T09T00:00:00Z" — Date rejects it, toISOString()
// throws, and the assignment is lost. The symptom is an empty range line on
// exactly the view people look at most, with no error anywhere.
func TestUIStatsPeriodRangeHandlesHourlyBuckets(t *testing.T) {
src := uiSource(t)
el := readFuncBody(t, src, "paintStats")
// It must branch on the label shape instead of concatenating blindly.
if !strings.Contains(el, `T\d{2}$`) && !strings.Contains(el, `\d{2}-\d{2}T\d{2}`) {
t.Fatal("paintStats must recognise the hourly bucket label shape " +
"(YYYY-MM-DDTHH); concatenating a time onto it yields an invalid Date")
}
// And it must not contain the naive form that broke it.
if strings.Contains(el, `bucket + "T00:00:00Z"`) {
t.Fatal("paintStats still appends T00:00:00Z to the bucket label — " +
"this throws on the day view and leaves the range line empty")
}
}