thread-master/apps/web/src/components/usage/UsageMeterBars.tsx

87 lines
3.1 KiB
TypeScript
Raw Normal View History

2026-07-10 05:10:31 +00:00
import { useState } from "react";
import { useI18n } from "../../i18n/I18nContext";
import { METER_META, type UsageMeter } from "../../lib/usageMeter";
export type MeterBarRow = {
meter: UsageMeter;
2026-07-13 03:18:08 +00:00
/** 已用點數platform only */
2026-07-10 05:10:31 +00:00
credits: number;
2026-07-13 03:18:08 +00:00
/** 平台呼叫次數(不含 byok */
2026-07-10 05:10:31 +00:00
count: number;
2026-07-13 03:18:08 +00:00
/** 分項點數硬上限(與 PLANS.soft_caps 同步,單位=點) */
2026-07-10 05:10:31 +00:00
soft_cap: number;
};
type Props = {
rows: MeterBarRow[];
unlimited?: boolean;
};
/**
2026-07-13 03:18:08 +00:00
* = / monthly_credits
* hover
2026-07-10 05:10:31 +00:00
*/
export function UsageMeterBars({ rows, unlimited = false }: Props) {
const { t } = useI18n();
const [active, setActive] = useState<UsageMeter | null>(null);
const ordered = rows
.slice()
.sort((a, b) => METER_META[a.meter].order - METER_META[b.meter].order);
return (
<ul className="hb-usage-mbars" onMouseLeave={() => setActive(null)}>
{ordered.map((row) => {
const label = t(`usage.meter.${row.meter}`);
2026-07-13 03:18:08 +00:00
const over = !unlimited && row.soft_cap > 0 && row.credits > row.soft_cap;
const atCap = !unlimited && row.soft_cap > 0 && row.credits >= row.soft_cap;
2026-07-10 05:10:31 +00:00
const pct =
row.soft_cap > 0
2026-07-13 03:18:08 +00:00
? Math.min(100, Math.round((row.credits / row.soft_cap) * 100))
2026-07-10 05:10:31 +00:00
: 0;
const on = active === row.meter;
return (
<li key={row.meter}>
<button
type="button"
2026-07-13 03:18:08 +00:00
className={`hb-usage-mbar${on ? " is-on" : ""}${over || atCap ? " is-over" : ""}`}
2026-07-10 05:10:31 +00:00
onMouseEnter={() => setActive(row.meter)}
onFocus={() => setActive(row.meter)}
aria-label={t("usage.meter.barAria", {
label,
count: row.count,
credits: row.credits,
cap: row.soft_cap,
})}
>
<span className="hb-usage-mbar__label">{label}</span>
<span className="hb-usage-mbar__track" aria-hidden>
<span
className="hb-usage-mbar__fill"
2026-07-13 03:18:08 +00:00
style={{ width: `${Math.max(pct, row.credits > 0 ? 3 : 0)}%` }}
2026-07-10 05:10:31 +00:00
/>
</span>
<span className="hb-usage-mbar__val">
2026-07-13 03:18:08 +00:00
<strong>{row.credits}</strong>
2026-07-10 05:10:31 +00:00
<span className="text-muted">/{row.soft_cap}</span>
2026-07-13 03:18:08 +00:00
<span className="hb-usage-mbar__unit">{t("usage.meter.pt")}</span>
{on ? (
<span className="hb-usage-mbar__pts">
{t("usage.meter.timesShort", { n: row.count })}
</span>
) : null}
{unlimited ? (
<span className="hb-usage-mbar__inf"></span>
) : over ? (
<span className="hb-usage-mbar__over">{t("usage.meter.over")}</span>
) : atCap ? (
<span className="hb-usage-mbar__over">{t("usage.meter.locked")}</span>
) : null}
2026-07-10 05:10:31 +00:00
</span>
</button>
</li>
);
})}
</ul>
);
}