feat(dashboard): 最近请求缓存命中列 + 中文模式人民币计价 + 价格解析记忆化

- 最近请求表新增缓存命中率列(缓存读取/总输入)
- useCurrency:中文界面费用按最新 USD→CNY 汇率显示 ¥,英文保持 $(open.er-api.com,缓存 12h,失败兜底 7.2)
- match_price 记忆化缓存:O(记录数×5910) → O(模型种类×5910),扫描从秒级降到百毫秒级
- 仪表盘右上角显示加载耗时/数据量(get_summary 往返埋点);Rust 侧分段计时
This commit is contained in:
KimiSwitch Dev committed 2026-08-02 00:18:19 +08:00
1 parent de132ffb3a
commit 38a11666ac
9 files changed
+173 -28

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "../i18n";
import { fmtUsd } from "../lib/dashboard-format";
/**
* USD → CNY exchange rate for the dashboard.
* Chinese mode shows prices in CNY (latest available rate), English stays in
* USD. The rate is fetched once and cached in localStorage for 12h; any
* failure falls back to a conservative fixed rate so the UI never blocks.
*/
const FALLBACK_RATE = 7.2;
const CACHE_KEY = "kimi-switch-cny-rate";
const CACHE_TTL = 12 * 60 * 60 * 1000; // 12 hours
function readCachedRate(): number | null {
try {
const raw = localStorage.getItem(CACHE_KEY);
if (!raw) return null;
const { ts, rate } = JSON.parse(raw) as { ts: number; rate: number };
if (Date.now() - ts > CACHE_TTL || !(rate > 1)) return null;
return rate;
} catch {
return null;
}
}
function writeCachedRate(rate: number) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({ ts: Date.now(), rate }));
} catch {
/* ignore */
}
}
export function useCurrency() {
const { lang } = useTranslation();
const [rate, setRate] = useState<number | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
const cached = readCachedRate();
if (cached) {
setRate(cached);
return;
}
try {
const res = await fetch("https://open.er-api.com/v6/latest/USD");
if (res.ok) {
const data = (await res.json()) as { rates?: { CNY?: number } };
const cny = Number(data.rates?.CNY);
if (cny > 1) {
if (!cancelled) setRate(cny);
writeCachedRate(cny);
return;
}
}
} catch {
/* network blocked → fallback */
}
if (!cancelled) setRate(FALLBACK_RATE);
})();
return () => {
cancelled = true;
};
}, []);
/** Format a USD cost for display: CNY in Chinese mode, USD otherwise. */
const money = useCallback(
(usd: number): string => {
if (lang !== "zh") return fmtUsd(usd);
const v = (Number(usd) || 0) * (rate ?? FALLBACK_RATE);
if (v === 0) return "¥0.00";
if (v < 0.1) return "¥" + v.toFixed(3);
if (v < 1) return "¥" + v.toFixed(2);
return "¥" + v.toFixed(2);
},
[lang, rate],
);
return { money, cnyRate: rate ?? FALLBACK_RATE };
}
+8 -1
View File
@@ -22,16 +22,23 @@ export function useDashboard() {
const [data, setData] = useState<SummaryResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/** Round-trip timing for the last get_summary call (user-perceived lag). */
const [loadStats, setLoadStats] = useState<{ ms: number; kb: number } | null>(null);
const refresh = useCallback(async (force = false) => {
setLoading(true);
setError(null);
const start = performance.now();
try {
const result = await invoke<SummaryResult>("get_summary", {
range,
refresh: force,
});
setData(result);
setLoadStats({
ms: Math.round(performance.now() - start),
kb: Math.round(JSON.stringify(result).length / 1024),
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setError(msg);
@@ -53,5 +60,5 @@ export function useDashboard() {
}
}, []);
return { range, changeRange, data, loading, error, refresh };
return { range, changeRange, data, loading, error, refresh, loadStats };
}