admin.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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;flex-wrap:wrap}
  10. h1{font-size:18px;color:#e74c3c;margin-right:24px}
  11. .tabs{display:flex;gap:4px;flex-wrap:wrap}
  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:1200px;margin:0 auto}
  15. table{width:100%;border-collapse:collapse;margin-top:12px}
  16. th{color:#7f8c8d;font-size:11px;text-align:left;padding:10px 8px;border-bottom:1px solid #2c3e50}
  17. td{padding:10px 8px;font-size:13px;border-bottom:1px solid #1e293b}
  18. tr:hover{background:#1a1a2e22}
  19. .input{padding:8px 12px;border-radius:6px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:13px}
  20. .select{padding:6px 10px;border-radius:6px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:12px;cursor:pointer}
  21. .btn{padding:8px 16px;border-radius:6px;border:none;background:#e74c3c;color:#fff;font-size:13px;cursor:pointer}
  22. .btn:hover{opacity:.9}.btn-sm{padding:5px 10px;font-size:12px}
  23. .btn-green{background:#27ae60}.btn-orange{background:#e67e22}.btn-red{background:#c0392b}
  24. .login-page{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}
  25. .login-box{width:340px;display:flex;flex-direction:column;gap:12px}
  26. .logout-btn{margin-left:auto;padding:6px 14px;border-radius:6px;border:1px solid #2c3e50;background:transparent;color:#7f8c8d;cursor:pointer;font-size:12px}
  27. .msg{color:#e74c3c;font-size:13px;text-align:center;padding:16px}
  28. .msg-ok{color:#27ae60}
  29. .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px;margin-bottom:24px}
  30. .card{background:#1a1a2e;padding:20px;border-radius:12px;border:1px solid #2c3e50}
  31. .card-label{color:#7f8c8d;font-size:12px;margin-bottom:8px}
  32. .card-value{font-size:22px;font-weight:700;color:#f39c12}
  33. .pagination{display:flex;justify-content:center;gap:8px;margin-top:16px;align-items:center}
  34. .pagination button{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:13px;background:transparent;color:#7f8c8d}
  35. .pagination button:disabled{opacity:.5}
  36. .flex-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
  37. </style>
  38. </head>
  39. <body>
  40. <div id="login">
  41. <div class="login-page">
  42. <div class="login-box">
  43. <h1 style="color:#e74c3c;text-align:center;margin-bottom:8px">KirinCode 管理后台</h1>
  44. <input type="email" id="lemail" class="input" placeholder="管理员邮箱" value="admin@kailin.com.cn">
  45. <input type="password" id="lpass" class="input" placeholder="密码" value="admin123">
  46. <button class="btn" onclick="doLogin()">登 录</button>
  47. <div class="msg" id="lerror"></div>
  48. </div>
  49. </div>
  50. </div>
  51. <div id="app" style="display:none">
  52. <header>
  53. <h1>KirinCode 管理后台</h1>
  54. <div class="tabs">
  55. <button class="tab active" data-tab="teams" onclick="loadTab('teams')">团队管理</button>
  56. <button class="tab" data-tab="models" onclick="loadTab('models')">模型配置</button>
  57. <button class="tab" data-tab="usage" onclick="loadTab('usage')">用量统计</button>
  58. </div>
  59. <button class="logout-btn" onclick="logout()">退出</button>
  60. </header>
  61. <main id="content"><div class="msg">加载中...</div></main>
  62. </div>
  63. <script>
  64. const API = "http://localhost:4097/api/v1"
  65. let currentTab = "teams"
  66. function token(){return localStorage.getItem("kc_admin_token")||""}
  67. async function api(path, opt){
  68. const t=token(); const h={Authorization:"Bearer "+t}
  69. if(opt&&opt.method==="POST")h["Content-Type"]="application/json"
  70. try{
  71. const r=await fetch(API+path,{method:opt?opt.method||"GET":"GET",headers:h,body:opt&&opt.body?JSON.stringify(opt.body):undefined})
  72. return await r.json()
  73. }catch(e){return {error:"连接失败,请确认 API 服务已启动 (port 4097)"}}
  74. }
  75. async function doLogin(){
  76. const e=document.getElementById("lemail").value
  77. const p=document.getElementById("lpass").value
  78. document.getElementById("lerror").innerHTML="登录中..."
  79. const d=await api("/auth/login",{method:"POST",body:{email:e,password:p}})
  80. if(d.error){document.getElementById("lerror").innerHTML=d.error;return}
  81. if(d.user&&d.user.role!=="admin"){document.getElementById("lerror").innerHTML="此账号非管理员";return}
  82. localStorage.setItem("kc_admin_token",d.token)
  83. document.getElementById("lerror").innerHTML="<span class='msg-ok'>登录成功</span>"
  84. setTimeout(showApp,300)
  85. }
  86. function showApp(){
  87. document.getElementById("login").style.display="none"
  88. document.getElementById("app").style.display="block"
  89. loadTab("teams")
  90. }
  91. async function loadTab(tab){
  92. currentTab=tab
  93. document.querySelectorAll(".tab").forEach(e=>e.classList.toggle("active",e.dataset.tab===tab))
  94. if(tab==="teams")await loadTeams()
  95. else if(tab==="models")await loadModels()
  96. else if(tab==="usage")await loadUsage()
  97. }
  98. async function loadTeams(){
  99. document.getElementById("content").innerHTML="<div class='msg'>加载中...</div>"
  100. try{
  101. const [users,teams]=await Promise.all([api("/admin/users"),api("/admin/teams")])
  102. if(users.error){document.getElementById("content").innerHTML="<div class='msg'>"+users.error+"</div>";return}
  103. let h="<h3 style='margin-bottom:12px'>团队成员</h3>"
  104. h+="<table><thead><tr><th>邮箱</th><th>名称</th><th>角色</th><th>Token用量</th><th>请求次数</th><th>费用(USD)</th><th>状态</th></tr></thead><tbody>"
  105. for(const u of users||[]){
  106. h+=`<tr><td>${u.email}</td><td>${u.display_name||""}</td><td>${u.role}</td>
  107. <td style="text-align:right">${(u.usage?.input+u.usage?.output||0).toLocaleString()}</td>
  108. <td style="text-align:right">${u.usage?.requests||0}</td>
  109. <td style="text-align:right;color:#f39c12">$${(u.usage?.cost||0).toFixed(4)}</td>
  110. <td>${u.status||"active"}</td></tr>`
  111. }
  112. h+="</tbody></table>"
  113. if(teams&&teams.length)h+="<p style='margin-top:16px;color:#7f8c8d'>团队: "+teams.map(t=>t.name).join(", ")+"</p>"
  114. document.getElementById("content").innerHTML=h
  115. }catch(e){document.getElementById("content").innerHTML="<div class='msg'>加载失败: "+e.message+"</div>"}
  116. }
  117. async function loadModels(){
  118. document.getElementById("content").innerHTML="<div class='msg'>加载中...</div>"
  119. try{
  120. const models=await api("/admin/models")
  121. let h="<h3 style='margin-bottom:12px'>企业模型配置</h3>"
  122. h+=`<div class='flex-row'><input class='input' id='mProvider' placeholder='Provider (如 anthropic)'><input class='input' id='mModel' placeholder='模型名'><input class='input' id='mKey' placeholder='API Key'><button class='btn' onclick='addModel()'>添加模型</button></div>`
  123. h+="<table><thead><tr><th>Provider</th><th>模型</th><th>API Key</th><th>创建时间</th><th>操作</th></tr></thead><tbody>"
  124. for(const m of models||[]){
  125. h+=`<tr><td>${m.provider}</td><td>${m.model}</td><td>${"*".repeat(Math.min(m.api_key?.length||0,12))||"未设置"}</td><td>${new Date(m.created_at).toLocaleDateString("zh-CN")}</td>
  126. <td><button class='btn btn-sm btn-red' onclick='delModel("${m.id}")'>删除</button></td></tr>`
  127. }
  128. h+="</tbody></table>"
  129. if(!models||!models.length)h+="<p style='color:#7f8c8d;margin-top:12px'>暂无配置的模型,请添加</p>"
  130. document.getElementById("content").innerHTML=h
  131. }catch(e){document.getElementById("content").innerHTML="<div class='msg'>加载失败</div>"}
  132. }
  133. async function addModel(){
  134. const p=document.getElementById("mProvider").value
  135. const m=document.getElementById("mModel").value
  136. const k=document.getElementById("mKey").value
  137. if(!p||!m){alert("请填写 Provider 和模型名");return}
  138. await api("/admin/models",{method:"POST",body:{provider:p,model:m,api_key:k}})
  139. loadModels()
  140. }
  141. async function delModel(id){await api("/admin/models/"+id,{method:"DELETE"});loadModels()}
  142. async function loadUsage(){
  143. document.getElementById("content").innerHTML="<div class='msg'>加载中...</div>"
  144. try{
  145. const [s,d]=await Promise.all([
  146. api("/admin/usage/summary"),
  147. api("/admin/usage/detail?page=1")
  148. ])
  149. let h=""
  150. if(!s.error){
  151. h+=`<div class='cards'><div class='card'><div class='card-label'>全队 Token</div><div class='card-value'>${((s.input||0)+(s.output||0)).toLocaleString()}</div></div>
  152. <div class='card'><div class='card-label'>请求次数</div><div class='card-value'>${(s.requests||0).toLocaleString()}</div></div>
  153. <div class='card'><div class='card-label'>今日费用</div><div class='card-value'>$${(s.cost||0).toFixed(4)}</div></div>
  154. <div class='card'><div class='card-label'>活跃用户</div><div class='card-value'>${s.users||0}</div></div></div>`
  155. }
  156. h+="<button class='btn btn-green' style='margin-bottom:12px' onclick='exportCSV()'>导出 CSV</button>"
  157. h+="<table><thead><tr><th>时间</th><th>用户</th><th>模型</th><th>输入Token</th><th>输出Token</th><th>费用</th></tr></thead><tbody>"
  158. for(const r of d.data||[]){
  159. h+=`<tr><td style='font-size:11px;color:#7f8c8d'>${new Date(r.created_at).toLocaleString("zh-CN")}</td>
  160. <td>${r.display_name||""}</td><td>${r.provider}/${r.model}</td>
  161. <td style="text-align:right">${(r.input_tokens||0).toLocaleString()}</td>
  162. <td style="text-align:right">${(r.output_tokens||0).toLocaleString()}</td>
  163. <td style="text-align:right;color:#f39c12">$${(r.cost_usd||0).toFixed(5)}</td></tr>`
  164. }
  165. h+="</tbody></table>"
  166. document.getElementById("content").innerHTML=h
  167. }catch(e){document.getElementById("content").innerHTML="<div class='msg'>加载失败</div>"}
  168. }
  169. async function exportCSV(){
  170. const t=token()
  171. const r=await fetch(API+"/admin/usage/export",{headers:{Authorization:"Bearer "+t}})
  172. const b=await r.blob()
  173. const a=document.createElement("a");a.href=URL.createObjectURL(b);a.download="用量导出.csv";a.click()
  174. }
  175. function logout(){localStorage.removeItem("kc_admin_token");location.reload()}
  176. // Auto-login
  177. if(localStorage.getItem("kc_admin_token"))showApp()
  178. </script>
  179. </body>
  180. </html>