@@ -155,58 +173,22 @@ export function Chart({
/>
))}
- {spec.lines &&
}
+ {blank > 0 && (
+
+ {(blank / n) * w >= 56 && (
+ {t.unrecorded}
+ )}
+
+ )}
-
- {grid.at.map((at, i) => {
- const ph = phase(i);
- const hot = i === cursor;
- let bars: ReactNode = null;
- if (spec.bars && ph !== "future") {
- const a = spec.bars.a[i] ?? 0;
- const b = spec.bars.b?.[i] ?? 0;
- // 有量的那一段至少看得见
- const ah = a > 0 ? Math.max(1.2, pctOf(a)) : 0;
- const bh = b > 0 ? Math.max(1.2, pctOf(b)) : 0;
- const bar = cn(
- "absolute bottom-0 left-1/2 w-4 max-w-[70%] -translate-x-1/2",
- !live && "motion-bar",
- ph === "ongoing" && "opacity-50",
- );
- bars = (
- <>
- {ah > 0 && (
-
- )}
- {bh > 0 && (
-
0 ? `calc(${ah}% + 2px)` : 0,
- background: spec.bars.colorB,
- }}
- />
- )}
- >
- );
- }
- return (
-
- {bars}
-
- );
- })}
-
+
{/* 线:指着的那一格画竖线,每条线上一个点 */}
- {spec.lines && cursor !== null && (
+ {cursor !== null && (
<>
{spec.lines.map((l, k) => {
- const v = l.values[cursor];
+ const v = cursor < blank ? null : l.values[cursor];
if (v == null) return null;
return (
;
+ lines: ChartLine[];
n: number;
top: number;
}) {
@@ -312,6 +294,8 @@ function Lines({
fill="none"
stroke={l.color}
strokeWidth={2}
+ strokeDasharray={l.dash ? "3 4" : undefined}
+ opacity={l.dash ? 0.7 : undefined}
strokeLinejoin="round"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
@@ -342,7 +326,8 @@ function Lines({
function TipBox({ id, title, body, x, right }: { id: string; title: string; body: TipBody; x: number; right: boolean }) {
const row = (r: TipRow, k: number) => (
-
+ {/* 图里对得上的那一样才有图例;没有的不留位置,和标题左对齐 */}
+ {r.key && }
{r.v}
{r.k}
diff --git a/src/overview/OverviewPage.tsx b/src/overview/OverviewPage.tsx
index 2e5a24bf..ba76a220 100644
--- a/src/overview/OverviewPage.tsx
+++ b/src/overview/OverviewPage.tsx
@@ -11,17 +11,17 @@ import type { Dashboard, Overview } from "@/types";
import { useText } from "@/i18n";
import { FirstRequestHint } from "@/guide/FirstRequestHint";
import { SetupGuide } from "@/guide/SetupGuide";
-import { bucketOf, useOverview, type Shown } from "./useOverview";
+import { bucketOf, rangeId, useOverview, type Shown } from "./useOverview";
import { OverviewStatus } from "./OverviewStatus";
import { OverviewSkeleton } from "./OverviewSkeleton";
-import { AttentionStrip } from "./Attention";
import { Hero } from "./Hero";
import { Spend } from "./Spend";
import { Quota, hasQuota } from "./Quota";
-import { Year } from "./Year";
-import { Breakdown } from "./Breakdown";
+import { Year, useUsageHistory } from "./Year";
+import { Breakdown, type Dim } from "./Breakdown";
import type { Metric } from "./metric";
import type { YearMetric } from "./yearGrid";
+import { useHold } from "./useHold";
import { overviewText } from "./overview.i18n";
/**
@@ -43,9 +43,9 @@ function neverUsed(d: Dashboard): boolean {
* 这一页的每一个数字都受那条约束:**绝不让估算值混进精确数字里假装准确。**所以
* 金额旁边永远跟着它的限定词。
*
- * 版面:页头(状态一行、时间范围);需要处理的事(没有就不出现);用量(六个指标并排,
+ * 版面:页头(状态一行、时间范围);用量(六个指标并排,
* 选中的那个画成下面的大图);去向和额度与余额并排;最近一年的热力图;按模型、上游、
- * 密钥分的明细表。
+ * 密钥分的明细表。去向和明细按什么分是同一个选择,切一处两处一起换。
*
* **跟着时间范围走的放在一起**:用量、去向、明细。额度与余额是此刻的状态,最近一年固定
* 一年,各自标着自己的口径。
@@ -67,10 +67,20 @@ export default function OverviewPage({
const t = useText(overviewText);
const [range, setRange] = useRange();
const o = useOverview(range, tick);
- // 选中的指标放在这一层:换时间范围时下面整块重挂,选择不该跟着丢
+ // 选中的指标、按什么分放在这一层:几块卡片共用一个选择
const [metric, setMetric] = useState("tokens");
const [yearMetric, setYearMetric] = useState("tokens");
- const pickDay = useCallback((day: Date) => setRange(dayRange(day)), [setRange]);
+ const [dim, setDim] = useState("model");
+ const history = useUsageHistory(tick);
+ const hold = useHold(o.shown?.id);
+ const pickDay = useCallback(
+ (day: Date, from: HTMLElement) => {
+ const r = dayRange(day);
+ hold.start(rangeId(r), from);
+ setRange(r);
+ },
+ [setRange, hold.start],
+ );
const landed = useRef(onLanded);
landed.current = onLanded;
@@ -94,12 +104,15 @@ export default function OverviewPage({
shown={o.shown}
ov={ov}
switching={o.switching}
- tick={tick}
metric={metric}
onMetric={setMetric}
yearMetric={yearMetric}
onYearMetric={setYearMetric}
onPickDay={pickDay}
+ dim={dim}
+ onDim={setDim}
+ history={history}
+ pad={hold.pad}
/>
)}
@@ -110,23 +123,30 @@ function Body({
shown,
ov,
switching,
- tick,
metric,
onMetric,
yearMetric,
onYearMetric,
onPickDay,
+ dim,
+ onDim,
+ history,
+ pad,
}: {
shown: Shown;
ov: Overview | null;
/** 选中的区间还在取,画的是上一个区间的 */
switching: boolean;
- tick: number;
metric: Metric;
onMetric: (m: Metric) => void;
yearMetric: YearMetric;
onYearMetric: (m: YearMetric) => void;
- onPickDay: (day: Date) => void;
+ onPickDay: (day: Date, from: HTMLElement) => void;
+ dim: Dim;
+ onDim: (d: Dim) => void;
+ history: ReturnType;
+ /** 页尾临时补的空白(`useHold`) */
+ pad: number;
}) {
const t = useText(overviewText);
const { data: d, range, id } = shown;
@@ -137,10 +157,10 @@ function Body({
return (
) : (
<>
-
-
+
-
+
-
-
+
+
+ {pad > 0 && }
>
)}
);
}
+
diff --git a/src/overview/OverviewSkeleton.tsx b/src/overview/OverviewSkeleton.tsx
index 0011afe5..90e134ba 100644
--- a/src/overview/OverviewSkeleton.tsx
+++ b/src/overview/OverviewSkeleton.tsx
@@ -85,12 +85,12 @@ export function OverviewSkeleton() {