| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
- <title>KirinCode 管理后台</title>
- <style>
- *{margin:0;padding:0;box-sizing:border-box}
- body{font-family:system-ui,sans-serif;background:#0f172a;color:#ecf0f1;min-height:100vh}
- header{background:#1a1a2e;padding:12px 24px;border-bottom:1px solid #2c3e50;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
- h1{font-size:18px;color:#e74c3c;margin-right:24px}
- .tabs{display:flex;gap:4px;flex-wrap:wrap}
- .tab{padding:6px 14px;border-radius:6px;border:none;font-size:13px;cursor:pointer;background:transparent;color:#7f8c8d}
- .tab.active{background:#e74c3c;color:#fff}
- main{padding:24px;max-width:1200px;margin:0 auto}
- table{width:100%;border-collapse:collapse;margin-top:12px}
- th{color:#7f8c8d;font-size:11px;text-align:left;padding:10px 8px;border-bottom:1px solid #2c3e50}
- td{padding:10px 8px;font-size:13px;border-bottom:1px solid #1e293b}
- tr:hover{background:#1a1a2e22}
- .input{padding:8px 12px;border-radius:6px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:13px}
- .select{padding:6px 10px;border-radius:6px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:12px;cursor:pointer}
- .btn{padding:8px 16px;border-radius:6px;border:none;background:#e74c3c;color:#fff;font-size:13px;cursor:pointer}
- .btn:hover{opacity:.9}.btn-sm{padding:5px 10px;font-size:12px}
- .btn-green{background:#27ae60}.btn-orange{background:#e67e22}.btn-red{background:#c0392b}
- .login-page{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}
- .login-box{width:340px;display:flex;flex-direction:column;gap:12px}
- .logout-btn{margin-left:auto;padding:6px 14px;border-radius:6px;border:1px solid #2c3e50;background:transparent;color:#7f8c8d;cursor:pointer;font-size:12px}
- .msg{color:#e74c3c;font-size:13px;text-align:center;padding:16px}
- .msg-ok{color:#27ae60}
- .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}
- .card-label{color:#7f8c8d;font-size:12px;margin-bottom:8px}
- .card-value{font-size:22px;font-weight:700;color:#f39c12}
- .pagination{display:flex;justify-content:center;gap:8px;margin-top:16px;align-items:center}
- .pagination button{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:13px;background:transparent;color:#7f8c8d}
- .pagination button:disabled{opacity:.5}
- .flex-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
- </style>
- </head>
- <body>
- <div id="login">
- <div class="login-page">
- <div class="login-box">
- <h1 style="color:#e74c3c;text-align:center;margin-bottom:8px">KirinCode 管理后台</h1>
- <input type="email" id="lemail" class="input" placeholder="管理员邮箱" value="admin@kailin.com.cn">
- <input type="password" id="lpass" class="input" placeholder="密码" value="admin123">
- <button class="btn" onclick="doLogin()">登 录</button>
- <div class="msg" id="lerror"></div>
- </div>
- </div>
- </div>
- <div id="app" style="display:none">
- <header>
- <h1>KirinCode 管理后台</h1>
- <div class="tabs">
- <button class="tab active" data-tab="teams" onclick="loadTab('teams')">团队管理</button>
- <button class="tab" data-tab="models" onclick="loadTab('models')">模型配置</button>
- <button class="tab" data-tab="usage" onclick="loadTab('usage')">用量统计</button>
- </div>
- <button class="logout-btn" onclick="logout()">退出</button>
- </header>
- <main id="content"><div class="msg">加载中...</div></main>
- </div>
- <script>
- const API = "http://localhost:4097/api/v1"
- let currentTab = "teams"
- function token(){return localStorage.getItem("kc_admin_token")||""}
- async function api(path, opt){
- const t=token(); const h={Authorization:"Bearer "+t}
- if(opt&&opt.method==="POST")h["Content-Type"]="application/json"
- try{
- const r=await fetch(API+path,{method:opt?opt.method||"GET":"GET",headers:h,body:opt&&opt.body?JSON.stringify(opt.body):undefined})
- return await r.json()
- }catch(e){return {error:"连接失败,请确认 API 服务已启动 (port 4097)"}}
- }
- async function doLogin(){
- const e=document.getElementById("lemail").value
- const p=document.getElementById("lpass").value
- document.getElementById("lerror").innerHTML="登录中..."
- const d=await api("/auth/login",{method:"POST",body:{email:e,password:p}})
- if(d.error){document.getElementById("lerror").innerHTML=d.error;return}
- if(d.user&&d.user.role!=="admin"){document.getElementById("lerror").innerHTML="此账号非管理员";return}
- localStorage.setItem("kc_admin_token",d.token)
- document.getElementById("lerror").innerHTML="<span class='msg-ok'>登录成功</span>"
- setTimeout(showApp,300)
- }
- function showApp(){
- document.getElementById("login").style.display="none"
- document.getElementById("app").style.display="block"
- loadTab("teams")
- }
- async function loadTab(tab){
- currentTab=tab
- document.querySelectorAll(".tab").forEach(e=>e.classList.toggle("active",e.dataset.tab===tab))
- if(tab==="teams")await loadTeams()
- else if(tab==="models")await loadModels()
- else if(tab==="usage")await loadUsage()
- }
- async function loadTeams(){
- document.getElementById("content").innerHTML="<div class='msg'>加载中...</div>"
- try{
- const [users,teams]=await Promise.all([api("/admin/users"),api("/admin/teams")])
- if(users.error){document.getElementById("content").innerHTML="<div class='msg'>"+users.error+"</div>";return}
- let h="<h3 style='margin-bottom:12px'>团队成员</h3>"
- h+="<table><thead><tr><th>邮箱</th><th>名称</th><th>角色</th><th>Token用量</th><th>请求次数</th><th>费用(USD)</th><th>状态</th></tr></thead><tbody>"
- for(const u of users||[]){
- h+=`<tr><td>${u.email}</td><td>${u.display_name||""}</td><td>${u.role}</td>
- <td style="text-align:right">${(u.usage?.input+u.usage?.output||0).toLocaleString()}</td>
- <td style="text-align:right">${u.usage?.requests||0}</td>
- <td style="text-align:right;color:#f39c12">$${(u.usage?.cost||0).toFixed(4)}</td>
- <td>${u.status||"active"}</td></tr>`
- }
- h+="</tbody></table>"
- if(teams&&teams.length)h+="<p style='margin-top:16px;color:#7f8c8d'>团队: "+teams.map(t=>t.name).join(", ")+"</p>"
- document.getElementById("content").innerHTML=h
- }catch(e){document.getElementById("content").innerHTML="<div class='msg'>加载失败: "+e.message+"</div>"}
- }
- async function loadModels(){
- document.getElementById("content").innerHTML="<div class='msg'>加载中...</div>"
- try{
- const models=await api("/admin/models")
- let h="<h3 style='margin-bottom:12px'>企业模型配置</h3>"
- 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>`
- h+="<table><thead><tr><th>Provider</th><th>模型</th><th>API Key</th><th>创建时间</th><th>操作</th></tr></thead><tbody>"
- for(const m of models||[]){
- 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>
- <td><button class='btn btn-sm btn-red' onclick='delModel("${m.id}")'>删除</button></td></tr>`
- }
- h+="</tbody></table>"
- if(!models||!models.length)h+="<p style='color:#7f8c8d;margin-top:12px'>暂无配置的模型,请添加</p>"
- document.getElementById("content").innerHTML=h
- }catch(e){document.getElementById("content").innerHTML="<div class='msg'>加载失败</div>"}
- }
- async function addModel(){
- const p=document.getElementById("mProvider").value
- const m=document.getElementById("mModel").value
- const k=document.getElementById("mKey").value
- if(!p||!m){alert("请填写 Provider 和模型名");return}
- await api("/admin/models",{method:"POST",body:{provider:p,model:m,api_key:k}})
- loadModels()
- }
- async function delModel(id){await api("/admin/models/"+id,{method:"DELETE"});loadModels()}
- async function loadUsage(){
- document.getElementById("content").innerHTML="<div class='msg'>加载中...</div>"
- try{
- const [s,d]=await Promise.all([
- api("/admin/usage/summary"),
- api("/admin/usage/detail?page=1")
- ])
- let h=""
- if(!s.error){
- 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>
- <div class='card'><div class='card-label'>请求次数</div><div class='card-value'>${(s.requests||0).toLocaleString()}</div></div>
- <div class='card'><div class='card-label'>今日费用</div><div class='card-value'>$${(s.cost||0).toFixed(4)}</div></div>
- <div class='card'><div class='card-label'>活跃用户</div><div class='card-value'>${s.users||0}</div></div></div>`
- }
- h+="<button class='btn btn-green' style='margin-bottom:12px' onclick='exportCSV()'>导出 CSV</button>"
- h+="<table><thead><tr><th>时间</th><th>用户</th><th>模型</th><th>输入Token</th><th>输出Token</th><th>费用</th></tr></thead><tbody>"
- for(const r of d.data||[]){
- h+=`<tr><td style='font-size:11px;color:#7f8c8d'>${new Date(r.created_at).toLocaleString("zh-CN")}</td>
- <td>${r.display_name||""}</td><td>${r.provider}/${r.model}</td>
- <td style="text-align:right">${(r.input_tokens||0).toLocaleString()}</td>
- <td style="text-align:right">${(r.output_tokens||0).toLocaleString()}</td>
- <td style="text-align:right;color:#f39c12">$${(r.cost_usd||0).toFixed(5)}</td></tr>`
- }
- h+="</tbody></table>"
- document.getElementById("content").innerHTML=h
- }catch(e){document.getElementById("content").innerHTML="<div class='msg'>加载失败</div>"}
- }
- async function exportCSV(){
- const t=token()
- const r=await fetch(API+"/admin/usage/export",{headers:{Authorization:"Bearer "+t}})
- const b=await r.blob()
- const a=document.createElement("a");a.href=URL.createObjectURL(b);a.download="用量导出.csv";a.click()
- }
- function logout(){localStorage.removeItem("kc_admin_token");location.reload()}
- // Auto-login
- if(localStorage.getItem("kc_admin_token"))showApp()
- </script>
- </body>
- </html>
|