87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
import { useState } from "react";
|
||
import { useI18n } from "../../i18n/I18nContext";
|
||
import { METER_META, type UsageMeter } from "../../lib/usageMeter";
|
||
|
||
export type MeterBarRow = {
|
||
meter: UsageMeter;
|
||
/** 已用點數(platform only) */
|
||
credits: number;
|
||
/** 平台呼叫次數(不含 byok) */
|
||
count: number;
|
||
/** 分項點數硬上限(與 PLANS.soft_caps 同步,單位=點) */
|
||
soft_cap: number;
|
||
};
|
||
|
||
type Props = {
|
||
rows: MeterBarRow[];
|
||
unlimited?: boolean;
|
||
};
|
||
|
||
/**
|
||
* 分項:數字 = 已用點 / 上限點(與方案 monthly_credits 同一單位)。
|
||
* 超過/達上限標紅;hover 顯示平台呼叫次數。
|
||
*/
|
||
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}`);
|
||
const over = !unlimited && row.soft_cap > 0 && row.credits > row.soft_cap;
|
||
const atCap = !unlimited && row.soft_cap > 0 && row.credits >= row.soft_cap;
|
||
const pct =
|
||
row.soft_cap > 0
|
||
? Math.min(100, Math.round((row.credits / row.soft_cap) * 100))
|
||
: 0;
|
||
const on = active === row.meter;
|
||
return (
|
||
<li key={row.meter}>
|
||
<button
|
||
type="button"
|
||
className={`hb-usage-mbar${on ? " is-on" : ""}${over || atCap ? " is-over" : ""}`}
|
||
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"
|
||
style={{ width: `${Math.max(pct, row.credits > 0 ? 3 : 0)}%` }}
|
||
/>
|
||
</span>
|
||
<span className="hb-usage-mbar__val">
|
||
<strong>{row.credits}</strong>
|
||
<span className="text-muted">/{row.soft_cap}</span>
|
||
<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}
|
||
</span>
|
||
</button>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
);
|
||
}
|