import { createSignal, createResource, onMount, Show, For } from "solid-js" import { useNavigate } from "@solidjs/router" const API_BASE = "http://localhost:4097/api/v1" const LS_KEY = "kirincode_personal_token" async function api(path: string, method = "GET", body?: any): Promise { const token = localStorage.getItem(LS_KEY) const headers: Record = { "Content-Type": "application/json" } if (token) headers["Authorization"] = `Bearer ${token}` const res = await fetch(`${API_BASE}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined }) return res.json() } export function PersonalDashboard() { const navigate = useNavigate() const [token, setToken] = createSignal(localStorage.getItem(LS_KEY) || "") const [loggedIn, setLoggedIn] = createSignal(!!token()) const [email, setEmail] = createSignal("") const [password, setPassword] = createSignal("") const [displayName, setDisplayName] = createSignal("") const [isRegister, setIsRegister] = createSignal(false) const [error, setError] = createSignal("") const [user, setUser] = createSignal(null) const [tab, setTab] = createSignal<"summary" | "trend" | "detail">("summary") const [detailPage, setDetailPage] = createSignal(1) async function doAuth() { setError("") const endpoint = isRegister() ? "/auth/register" : "/auth/login" const body = isRegister() ? { email: email(), password: password(), display_name: displayName() } : { email: email(), password: password() } try { const data = await api(endpoint, "POST", body) if (data.error) { setError(data.error); return } localStorage.setItem(LS_KEY, data.token) setToken(data.token) setUser(data.user) setLoggedIn(true) } catch { setError("连接失败 — 请确认 Admin Server 已启动 (port 4097)") } } function logout() { localStorage.removeItem(LS_KEY) setToken(""); setLoggedIn(false); setUser(null) } const [summary] = createResource(loggedIn, () => api("/personal/summary")) const [trend] = createResource(() => loggedIn() && tab() === "trend", () => api("/personal/trend")) const [details] = createResource(() => loggedIn() && tab() === "detail", () => api(`/personal/detail?page=${detailPage()}`)) onMount(() => { const i = setInterval(() => { if (loggedIn() && tab() === "summary") api("/personal/summary") }, 30000) return () => clearInterval(i) }) if (!loggedIn()) { return (

KirinCode 个人中心

setEmail(e.currentTarget.value)} style={styles.input} /> setPassword(e.currentTarget.value)} style={styles.input} onKeyDown={e => e.key === "Enter" && doAuth()} /> setDisplayName(e.currentTarget.value)} style={styles.input} />
{error()}
) } const tabs = [ { key: "summary" as const, label: "今日概览" }, { key: "trend" as const, label: "趋势图表" }, { key: "detail" as const, label: "用量明细" }, ] return (
👤 {user()?.display_name || user()?.email || "个人中心"}
{tabs.map(t => ( ))}

今日Token消耗

分钟级消耗趋势(近24小时)

用量明细

第 {details()?.page}/{details()?.pages} 页
) } function Card(p: { label: string; value: string }) { return
{p.label}
{p.value}
} function DetailTable(p: { data: any[] }) { return ( {(row: any) => ( )}
时间模型Token(入)Token(出)费用
{new Date(row.created_at).toLocaleString()} {row.provider}/{row.model} {row.input_tokens?.toLocaleString()} {row.output_tokens?.toLocaleString()} ${row.cost_usd?.toFixed(5)}
) } function TrendChart(p: { data: any[] }) { if (p.data.length < 2) return
数据不足
const max = Math.max(...p.data.map((d: any) => d.input + d.output), 1) const w = 700; const h = 200 const pts = p.data.map((d: any, i: number) => `${(i / (p.data.length - 1)) * w},${h - ((d.input + d.output) / max) * h}`).join(" ") return ( ) } const styles: Record = { loginPage: { display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif" }, title: { color: "#e74c3c", "margin-bottom": "24px" }, input: { padding: "10px 14px", "border-radius": "8px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "14px" }, btn: { padding: "10px", "border-radius": "8px", border: "none", background: "#e74c3c", color: "#fff", "font-size": "14px", cursor: "pointer", width: "100%" }, error: { color: "#e74c3c", "font-size": "13px", "text-align": "center" }, page: { display: "flex", "flex-direction": "column", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif", overflow: "auto" }, header: { display: "flex", "align-items": "center", padding: "12px 24px", background: "#1a1a2e", "border-bottom": "1px solid #2c3e50", gap: "16px" }, headerTitle: { "font-size": "18px", "font-weight": "700", color: "#e74c3c" }, backBtn: { padding: "6px 14px", "border-radius": "6px", border: "1px solid #2c3e50", background: "transparent", color: "#7f8c8d", cursor: "pointer", "font-size": "13px", position: "absolute", left: "24px", top: "12px" }, tabBtn: { padding: "6px 14px", "border-radius": "6px", border: "none", "font-size": "13px", cursor: "pointer", whiteSpace: "nowrap" }, logoutBtn: { padding: "6px 14px", "border-radius": "6px", border: "1px solid #2c3e50", background: "transparent", color: "#7f8c8d", cursor: "pointer", "font-size": "12px" }, content: { flex: 1, padding: "24px", overflow: "auto" }, sectionTitle: { "font-size": "16px", "font-weight": "600", "margin-bottom": "16px" }, cards: { display: "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", gap: "16px", "margin-bottom": "24px" }, card: { background: "#1a1a2e", padding: "20px", "border-radius": "12px", border: "1px solid #2c3e50" }, cardLabel: { color: "#7f8c8d", "font-size": "12px", "margin-bottom": "8px" }, cardValue: { "font-size": "22px", "font-weight": "700", color: "#f39c12" }, chart: { background: "#1a1a2e", padding: "16px", "border-radius": "10px", border: "1px solid #2c3e50" }, }