personal.html 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
  5. <title>KirinCode — 个人使用</title>
  6. <style>
  7. *{margin:0;padding:0;box-sizing:border-box}
  8. body{font-family:system-ui,sans-serif;background:#0f172a;color:#ecf0f1;min-height:100vh}
  9. header{background:#1a1a2e;padding:12px 24px;border-bottom:1px solid #2c3e50;display:flex;align-items:center;gap:16px}
  10. h1{font-size:18px;color:#e74c3c}
  11. .tabs{display:flex;gap:4px;margin-left:24px}
  12. .tab{padding:6px 14px;border-radius:6px;border:none;font-size:13px;cursor:pointer;background:transparent;color:#7f8c8d}
  13. .tab.active{background:#e74c3c;color:#fff}
  14. main{padding:24px;max-width:1000px;margin:0 auto}
  15. .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px;margin-bottom:24px}
  16. .card{background:#1a1a2e;padding:20px;border-radius:12px;border:1px solid #2c3e50}
  17. .card-label{color:#7f8c8d;font-size:12px;margin-bottom:8px}
  18. .card-value{font-size:22px;font-weight:700;color:#f39c12}
  19. .chart{background:#1a1a2e;padding:16px;border-radius:10px;border:1px solid #2c3e50;margin-bottom:24px}
  20. table{width:100%;border-collapse:collapse}
  21. th{color:#7f8c8d;font-size:11px;text-align:left;padding:10px 8px;border-bottom:1px solid #2c3e50}
  22. td{padding:10px 8px;font-size:13px;border-bottom:1px solid #1e293b}
  23. .login-page{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}
  24. .login-box{width:340px;display:flex;flex-direction:column;gap:12px}
  25. .input{padding:10px 14px;border-radius:8px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:14px;width:100%}
  26. .btn{padding:10px;border-radius:8px;border:none;background:#e74c3c;color:#fff;font-size:14px;cursor:pointer;width:100%}
  27. .btn:hover{opacity:.9}
  28. .link{color:#7f8c8d;text-align:center;cursor:pointer;font-size:13px;margin-top:4px}
  29. .logout-btn{margin-left:auto;padding:6px 14px;border-radius:6px;border:1px solid #2c3e50;background:transparent;color:#7f8c8d;cursor:pointer;font-size:12px}
  30. .pagination{display:flex;justify-content:center;gap:8px;margin-top:16px;align-items:center}
  31. .pagination button{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:13px;background:transparent;color:#7f8c8d}
  32. .pagination button:disabled{opacity:.5}
  33. .error{color:#e74c3c;font-size:13px;text-align:center}
  34. </style>
  35. </head>
  36. <body>
  37. <div id="login" style="display:none">
  38. <div class="login-page">
  39. <div class="login-box">
  40. <h1 style="color:#e74c3c;text-align:center;margin-bottom:12px">KirinCode 个人中心</h1>
  41. <input type="email" id="lemail" class="input" placeholder="企业邮箱">
  42. <input type="password" id="lpass" class="input" placeholder="密码">
  43. <input type="text" id="lname" class="input" placeholder="显示名称(注册)" style="display:none">
  44. <button class="btn" id="lbtn">登 录</button>
  45. <div class="error" id="lerror"></div>
  46. <div class="link" id="ltoggle">没有账号?注册</div>
  47. </div>
  48. </div>
  49. </div>
  50. <div id="app" style="display:none">
  51. <header>
  52. <h1 id="dispName">KirinCode 个人中心</h1>
  53. <div class="tabs">
  54. <button class="tab active" data-tab="summary">今日概览</button>
  55. <button class="tab" data-tab="trend">趋势图表</button>
  56. <button class="tab" data-tab="detail">用量明细</button>
  57. </div>
  58. <button class="logout-btn" onclick="logout()">退出</button>
  59. </header>
  60. <main>
  61. <div id="tab-summary">
  62. <div class="cards" id="cards"></div>
  63. <div class="charts" id="summaryChart"></div>
  64. </div>
  65. <div id="tab-trend" style="display:none">
  66. <div class="chart"><svg id="trendSvg" viewBox="0 0 700 220" style="width:100%;height:auto"></svg></div>
  67. </div>
  68. <div id="tab-detail" style="display:none">
  69. <table><thead><tr><th>时间</th><th>模型</th><th>Token(入)</th><th>Token(出)</th><th>费用</th></tr></thead><tbody id="detailBody"></tbody></table>
  70. <div class="pagination" id="pagination"></div>
  71. </div>
  72. </main>
  73. </div>
  74. <script>
  75. const API = "http://localhost:4097/api/v1"
  76. let isRegister = false, detailPage = 1, currentTab = "summary"
  77. document.getElementById("ltoggle").onclick = () => {
  78. isRegister = !isRegister
  79. document.getElementById("lname").style.display = isRegister ? "block" : "none"
  80. document.getElementById("lbtn").textContent = isRegister ? "注 册" : "登 录"
  81. document.getElementById("ltoggle").textContent = isRegister ? "已有账号?登录" : "没有账号?注册"
  82. }
  83. document.getElementById("lbtn").onclick = async () => {
  84. const email = document.getElementById("lemail").value
  85. const pass = document.getElementById("lpass").value
  86. const name = document.getElementById("lname").value
  87. const ep = isRegister ? "register" : "login"
  88. const body = isRegister ? {email,password:pass,display_name:name} : {email,password:pass}
  89. try {
  90. const r = await fetch(`${API}/auth/${ep}`, {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})
  91. const d = await r.json()
  92. if (d.error) { document.getElementById("lerror").textContent = d.error; return }
  93. localStorage.setItem("kc_token", d.token)
  94. localStorage.setItem("kc_user", JSON.stringify(d.user))
  95. showApp(d.user)
  96. } catch(e) { document.getElementById("lerror").textContent = "连接失败 — 请确认 Admin Server 已启动" }
  97. }
  98. function showApp(user) {
  99. document.getElementById("login").style.display = "none"
  100. document.getElementById("app").style.display = "block"
  101. document.getElementById("dispName").textContent = "👤 " + (user.display_name || user.email)
  102. loadTab("summary")
  103. }
  104. async function api(path) {
  105. const t = localStorage.getItem("kc_token")
  106. const r = await fetch(`${API}${path}`, {headers:{Authorization:`Bearer ${t}`}})
  107. return r.json()
  108. }
  109. async function loadTab(tab) {
  110. currentTab = tab
  111. document.querySelectorAll(".tab").forEach(e => e.classList.toggle("active", e.dataset.tab === tab))
  112. document.querySelectorAll("[id^='tab-']").forEach(e => e.style.display = "none")
  113. document.getElementById("tab-"+tab).style.display = "block"
  114. if (tab === "summary") {
  115. const s = await api("/personal/summary")
  116. document.getElementById("cards").innerHTML = `
  117. <div class="card"><div class="card-label">今日 Token</div><div class="card-value">${((s.input||0)+(s.output||0)).toLocaleString()}</div></div>
  118. <div class="card"><div class="card-label">请求次数</div><div class="card-value">${(s.requests||0).toLocaleString()}</div></div>
  119. <div class="card"><div class="card-label">今日费用</div><div class="card-value">$${(s.cost||0).toFixed(4)}</div></div>`
  120. const t = await api("/personal/trend")
  121. if (t.length) drawTrend(t)
  122. }
  123. if (tab === "trend") {
  124. const t = await api("/personal/trend")
  125. if (t.length) drawTrend(t)
  126. else document.getElementById("trendSvg").outerHTML = '<div style="color:#7f8c8d;text-align:center;padding:40px">暂无数据</div>'
  127. }
  128. if (tab === "detail") {
  129. detailPage = 1; loadDetail()
  130. }
  131. }
  132. async function loadDetail() {
  133. const d = await api(`/personal/detail?page=${detailPage}`)
  134. document.getElementById("detailBody").innerHTML = d.data.map(e => `
  135. <tr><td style="font-size:11px;color:#7f8c8d">${new Date(e.created_at).toLocaleString()}</td>
  136. <td>${e.provider}/${e.model}</td>
  137. <td style="text-align:right">${e.input_tokens.toLocaleString()}</td>
  138. <td style="text-align:right">${e.output_tokens.toLocaleString()}</td>
  139. <td style="text-align:right;color:#f39c12">$${e.cost_usd.toFixed(5)}</td></tr>`).join("")
  140. document.getElementById("pagination").innerHTML = `
  141. <button ${detailPage<=1?"disabled":""} onclick="detailPage--;loadDetail()">上一页</button>
  142. <span style="color:#7f8c8d;font-size:13px">${d.page}/${d.pages} (${d.total}条)</span>
  143. <button ${detailPage>=d.pages?"disabled":""} onclick="detailPage++;loadDetail()">下一页</button>`
  144. }
  145. function drawTrend(data) {
  146. const max = Math.max(...data.map(d=>d.input+d.output),1); const w=700,h=200
  147. const pts = data.map((d,i) => `${(i/(data.length-1))*w},${h-((d.input+d.output)/max)*h}`).join(" ")
  148. const labels = data.filter((_,i)=>i%Math.max(1,Math.floor(data.length/6))===0).map((d,i)=>{
  149. const x = (i*Math.max(1,Math.floor(data.length/6))/(data.length-1))*w
  150. return `<text x="${x}" y="${h+20}" fill="#7f8c8d" font-size="10" text-anchor="middle">${new Date(d.minute).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}</text>`
  151. }).join("")
  152. document.getElementById("trendSvg").innerHTML = `
  153. <line x1="0" y1="0" x2="0" y2="${h}" stroke="#2c3e50" stroke-width="1"/>
  154. <line x1="0" y1="${h}" x2="${w}" y2="${h}" stroke="#2c3e50" stroke-width="1"/>
  155. <polyline points="${pts}" fill="none" stroke="#e74c3c" stroke-width="2"/>
  156. <polygon points="0,${h} ${pts} ${w},${h}" fill="url(#g)" opacity="0.15"/>
  157. <defs><linearGradient id="g"><stop offset="0%" stop-color="#e74c3c"/><stop offset="100%" stop-color="#e74c3c" stop-opacity="0"/></linearGradient></defs>
  158. ${labels}`
  159. }
  160. function logout() {
  161. localStorage.removeItem("kc_token"); localStorage.removeItem("kc_user")
  162. document.getElementById("login").style.display = "block"
  163. document.getElementById("app").style.display = "none"
  164. }
  165. document.querySelectorAll(".tab").forEach(e => e.onclick = () => loadTab(e.dataset.tab))
  166. // Auto-login
  167. if (localStorage.getItem("kc_token")) showApp(JSON.parse(localStorage.getItem("kc_user") || "{}"))
  168. else document.getElementById("login").style.display = "block"
  169. </script>
  170. </body>
  171. </html>