From 53d6f77753e809bcabbefbcfb5394c51d8b3cc0f Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 08:54:43 +0800 Subject: [PATCH 01/13] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=B6=8B=E5=8A=BF=E5=9B=BE=E8=A1=A8=E6=8C=89?= =?UTF-8?q?=E5=B0=8F=E6=97=B6=E7=B2=92=E5=BA=A6=E4=B8=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/assets/AccountsView-BpzHtqkX.js | 9 ------ .../dist/assets/ApiKeysView-8a5XQ0yG.js | 9 ------ .../dist/assets/ApiStatsView-D_Y193CX.js | 1 - .../dist/assets/DashboardView-B-EZhqex.js | 1 - .../dist/assets/LoginView-DCXHRAtT.js | 1 - .../dist/assets/LogoTitle-CIeppYUK.js | 1 - .../dist/assets/MainLayout-Df9D9BdR.js | 1 - .../dist/assets/SettingsView-BD-f8NQ2.js | 3 -- .../dist/assets/TutorialView-PeOC7oRL.js | 1 - web/admin-spa/dist/assets/index-DhITICXu.js | 2 -- web/admin-spa/dist/index.html | 2 +- .../src/components/dashboard/UsageTrend.vue | 8 +++-- web/admin-spa/src/stores/dashboard.js | 13 +++++--- web/admin-spa/src/views/DashboardView.vue | 31 +++++++++++++++++-- 14 files changed, 44 insertions(+), 39 deletions(-) delete mode 100644 web/admin-spa/dist/assets/AccountsView-BpzHtqkX.js delete mode 100644 web/admin-spa/dist/assets/ApiKeysView-8a5XQ0yG.js delete mode 100644 web/admin-spa/dist/assets/ApiStatsView-D_Y193CX.js delete mode 100644 web/admin-spa/dist/assets/DashboardView-B-EZhqex.js delete mode 100644 web/admin-spa/dist/assets/LoginView-DCXHRAtT.js delete mode 100644 web/admin-spa/dist/assets/LogoTitle-CIeppYUK.js delete mode 100644 web/admin-spa/dist/assets/MainLayout-Df9D9BdR.js delete mode 100644 web/admin-spa/dist/assets/SettingsView-BD-f8NQ2.js delete mode 100644 web/admin-spa/dist/assets/TutorialView-PeOC7oRL.js delete mode 100644 web/admin-spa/dist/assets/index-DhITICXu.js diff --git a/web/admin-spa/dist/assets/AccountsView-BpzHtqkX.js b/web/admin-spa/dist/assets/AccountsView-BpzHtqkX.js deleted file mode 100644 index 408d69c5..00000000 --- a/web/admin-spa/dist/assets/AccountsView-BpzHtqkX.js +++ /dev/null @@ -1,9 +0,0 @@ -import{r as k,aR as $e,o as K,V as Te,x as i,y as r,z as e,L as A,K as C,al as fe,aY as Z,aX as ge,aq as z,aZ as Ce,C as D,O as b,c as ee,P as j,I as te,a5 as be,R as ae,an as Y,u as E,q as Ae,Y as Ue,Q as _e,ac as Ie,B as je}from"./vue-vendor-CKToUHZx.js";import{s as S}from"./toast-BvwA7Mwb.js";import{a as M,_ as Ve}from"./index-DhITICXu.js";import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";const le=k(!1),xe=k({title:"",message:"",confirmText:"继续",cancelText:"取消"}),Q=k(null);function we(){return{showConfirmModal:le,confirmOptions:xe,showConfirm:(y,x,U="继续",$="取消")=>new Promise(h=>{xe.value={title:y,message:x,confirmText:U,cancelText:$},Q.value=h,le.value=!0}),handleConfirm:()=>{le.value=!1,Q.value&&(Q.value(!0),Q.value=null)},handleCancel:()=>{le.value=!1,Q.value&&(Q.value(!1),Q.value=null)}}}const he=$e("accounts",()=>{const R=k([]),L=k([]),o=k(!1),y=k(null),x=k(""),U=k("asc"),$=async()=>{o.value=!0,y.value=null;try{const f=await M.get("/admin/claude-accounts");if(f.success)R.value=f.data||[];else throw new Error(f.message||"获取Claude账户失败")}catch(f){throw y.value=f.message,f}finally{o.value=!1}},h=async()=>{o.value=!0,y.value=null;try{const f=await M.get("/admin/gemini-accounts");if(f.success)L.value=f.data||[];else throw new Error(f.message||"获取Gemini账户失败")}catch(f){throw y.value=f.message,f}finally{o.value=!1}};return{claudeAccounts:R,geminiAccounts:L,loading:o,error:y,sortBy:x,sortOrder:U,fetchClaudeAccounts:$,fetchGeminiAccounts:h,fetchAllAccounts:async()=>{o.value=!0,y.value=null;try{await Promise.all([$(),h()])}catch(f){throw y.value=f.message,f}finally{o.value=!1}},createClaudeAccount:async f=>{o.value=!0,y.value=null;try{const d=await M.post("/admin/claude-accounts",f);if(d.success)return await $(),d.data;throw new Error(d.message||"创建Claude账户失败")}catch(d){throw y.value=d.message,d}finally{o.value=!1}},createGeminiAccount:async f=>{o.value=!0,y.value=null;try{const d=await M.post("/admin/gemini-accounts",f);if(d.success)return await h(),d.data;throw new Error(d.message||"创建Gemini账户失败")}catch(d){throw y.value=d.message,d}finally{o.value=!1}},updateClaudeAccount:async(f,d)=>{o.value=!0,y.value=null;try{const T=await M.put(`/admin/claude-accounts/${f}`,d);if(T.success)return await $(),T;throw new Error(T.message||"更新Claude账户失败")}catch(T){throw y.value=T.message,T}finally{o.value=!1}},updateGeminiAccount:async(f,d)=>{o.value=!0,y.value=null;try{const T=await M.put(`/admin/gemini-accounts/${f}`,d);if(T.success)return await h(),T;throw new Error(T.message||"更新Gemini账户失败")}catch(T){throw y.value=T.message,T}finally{o.value=!1}},toggleAccount:async(f,d)=>{o.value=!0,y.value=null;try{const T=f==="claude"?`/admin/claude-accounts/${d}/toggle`:`/admin/gemini-accounts/${d}/toggle`,W=await M.put(T);if(W.success)return f==="claude"?await $():await h(),W;throw new Error(W.message||"切换状态失败")}catch(T){throw y.value=T.message,T}finally{o.value=!1}},deleteAccount:async(f,d)=>{o.value=!0,y.value=null;try{const T=f==="claude"?`/admin/claude-accounts/${d}`:`/admin/gemini-accounts/${d}`,W=await M.delete(T);if(W.success)return f==="claude"?await $():await h(),W;throw new Error(W.message||"删除失败")}catch(T){throw y.value=T.message,T}finally{o.value=!1}},refreshClaudeToken:async f=>{o.value=!0,y.value=null;try{const d=await M.post(`/admin/claude-accounts/${f}/refresh`);if(d.success)return await $(),d;throw new Error(d.message||"Token刷新失败")}catch(d){throw y.value=d.message,d}finally{o.value=!1}},generateClaudeAuthUrl:async f=>{try{const d=await M.post("/admin/claude-accounts/generate-auth-url",f);if(d.success)return d.data;throw new Error(d.message||"生成授权URL失败")}catch(d){throw y.value=d.message,d}},exchangeClaudeCode:async f=>{try{const d=await M.post("/admin/claude-accounts/exchange-code",f);if(d.success)return d.data;throw new Error(d.message||"交换授权码失败")}catch(d){throw y.value=d.message,d}},generateGeminiAuthUrl:async f=>{try{const d=await M.post("/admin/gemini-accounts/generate-auth-url",f);if(d.success)return d.data;throw new Error(d.message||"生成授权URL失败")}catch(d){throw y.value=d.message,d}},exchangeGeminiCode:async f=>{try{const d=await M.post("/admin/gemini-accounts/exchange-code",f);if(d.success)return d.data;throw new Error(d.message||"交换授权码失败")}catch(d){throw y.value=d.message,d}},sortAccounts:f=>{x.value===f?U.value=U.value==="asc"?"desc":"asc":(x.value=f,U.value="asc")},reset:()=>{R.value=[],L.value=[],o.value=!1,y.value=null,x.value="",U.value="asc"}}}),Se={class:"space-y-4"},Ge={class:"flex items-center justify-between"},Re={class:"flex items-center cursor-pointer"},Me={key:0,class:"bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-4"},Pe={class:"grid grid-cols-2 gap-4"},Oe={class:"space-y-4"},Le={class:"flex items-center"},ze={key:0,class:"grid grid-cols-2 gap-4"},De={class:"relative"},Ee=["type"],ve={__name:"ProxyConfig",props:{modelValue:{type:Object,default:()=>({enabled:!1,type:"socks5",host:"",port:"",username:"",password:""})}},emits:["update:modelValue"],setup(R,{emit:L}){const o=R,y=L,x=k({...o.modelValue}),U=k(!!(x.value.username||x.value.password)),$=k(!1);K(()=>o.modelValue,v=>{JSON.stringify(v)!==JSON.stringify(x.value)&&(x.value={...v},U.value=!!(v.username||v.password))},{deep:!0}),K(()=>x.value.enabled,v=>{_()}),K(()=>x.value.type,v=>{_()}),K(()=>x.value.host,v=>{_()}),K(()=>x.value.port,v=>{_()}),K(()=>x.value.username,v=>{_()}),K(()=>x.value.password,v=>{_()}),K(U,v=>{v||(x.value.username="",x.value.password="",_())});let h=null;function _(){h&&clearTimeout(h),h=setTimeout(()=>{const v={...x.value};U.value||(v.username="",v.password=""),y("update:modelValue",v)},100)}return Te(()=>{h&&clearTimeout(h)}),(v,c)=>(r(),i("div",Se,[e("div",Ge,[c[9]||(c[9]=e("h4",{class:"text-sm font-semibold text-gray-700"},"代理设置 (可选)",-1)),e("label",Re,[C(e("input",{type:"checkbox","onUpdate:modelValue":c[0]||(c[0]=I=>x.value.enabled=I),class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[fe,x.value.enabled]]),c[8]||(c[8]=e("span",{class:"ml-2 text-sm text-gray-700"},"启用代理",-1))])]),x.value.enabled?(r(),i("div",Me,[c[17]||(c[17]=Z('

配置代理以访问受限的网络资源。支持 SOCKS5 和 HTTP 代理。

请确保代理服务器稳定可用,否则会影响账户的正常使用。

',1)),e("div",null,[c[11]||(c[11]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"代理类型",-1)),C(e("select",{"onUpdate:modelValue":c[1]||(c[1]=I=>x.value.type=I),class:"form-input w-full"},c[10]||(c[10]=[e("option",{value:"socks5"},"SOCKS5",-1),e("option",{value:"http"},"HTTP",-1),e("option",{value:"https"},"HTTPS",-1)]),512),[[ge,x.value.type]])]),e("div",Pe,[e("div",null,[c[12]||(c[12]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"主机地址",-1)),C(e("input",{"onUpdate:modelValue":c[2]||(c[2]=I=>x.value.host=I),type:"text",placeholder:"例如: 192.168.1.100",class:"form-input w-full"},null,512),[[z,x.value.host]])]),e("div",null,[c[13]||(c[13]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"端口",-1)),C(e("input",{"onUpdate:modelValue":c[3]||(c[3]=I=>x.value.port=I),type:"number",placeholder:"例如: 1080",class:"form-input w-full"},null,512),[[z,x.value.port]])])]),e("div",Oe,[e("div",Le,[C(e("input",{type:"checkbox","onUpdate:modelValue":c[4]||(c[4]=I=>U.value=I),id:"proxyAuth",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[fe,U.value]]),c[14]||(c[14]=e("label",{for:"proxyAuth",class:"ml-2 text-sm text-gray-700 cursor-pointer"}," 需要身份验证 ",-1))]),U.value?(r(),i("div",ze,[e("div",null,[c[15]||(c[15]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"用户名",-1)),C(e("input",{"onUpdate:modelValue":c[5]||(c[5]=I=>x.value.username=I),type:"text",placeholder:"代理用户名",class:"form-input w-full"},null,512),[[z,x.value.username]])]),e("div",null,[c[16]||(c[16]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"密码",-1)),e("div",De,[C(e("input",{"onUpdate:modelValue":c[6]||(c[6]=I=>x.value.password=I),type:$.value?"text":"password",placeholder:"代理密码",class:"form-input w-full pr-10"},null,8,Ee),[[Ce,x.value.password]]),e("button",{type:"button",onClick:c[7]||(c[7]=I=>$.value=!$.value),class:"absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"},[e("i",{class:D($.value?"fas fa-eye-slash":"fas fa-eye")},null,2)])])])])):A("",!0)]),c[18]||(c[18]=e("div",{class:"bg-blue-50 p-3 rounded-lg border border-blue-200"},[e("p",{class:"text-xs text-blue-700"},[e("i",{class:"fas fa-info-circle mr-1"}),e("strong",null,"提示:"),b("代理设置将用于所有与此账户相关的API请求。请确保代理服务器支持HTTPS流量转发。 ")])],-1))])):A("",!0)]))}},Ke={class:"space-y-6"},We={key:0},qe={class:"bg-blue-50 p-6 rounded-lg border border-blue-200"},Be={class:"flex items-start gap-4"},Ne={class:"flex-1"},Fe={class:"space-y-4"},He={class:"bg-white/80 rounded-lg p-4 border border-blue-300"},Je={class:"flex items-start gap-3"},Ye={class:"flex-1"},Qe=["disabled"],Xe={key:0,class:"fas fa-link mr-2"},Ze={key:1,class:"loading-spinner mr-2"},et={key:1,class:"space-y-3"},tt={class:"flex items-center gap-2"},st=["value"],lt={class:"bg-white/80 rounded-lg p-4 border border-blue-300"},at={class:"flex items-start gap-3"},ot={class:"flex-1"},nt={class:"space-y-3"},rt={key:1},it={class:"bg-green-50 p-6 rounded-lg border border-green-200"},ut={class:"flex items-start gap-4"},dt={class:"flex-1"},ct={class:"space-y-4"},mt={class:"bg-white/80 rounded-lg p-4 border border-green-300"},pt={class:"flex items-start gap-3"},ft={class:"flex-1"},xt=["disabled"],vt={key:0,class:"fas fa-link mr-2"},yt={key:1,class:"loading-spinner mr-2"},gt={key:1,class:"space-y-3"},bt={class:"flex items-center gap-2"},wt=["value"],ht={class:"bg-white/80 rounded-lg p-4 border border-green-300"},kt={class:"flex items-start gap-3"},$t={class:"flex-1"},Tt={class:"space-y-3"},Ct={class:"flex gap-3 pt-4"},At=["disabled"],Ut={key:0,class:"loading-spinner mr-2"},_t={__name:"OAuthFlow",props:{platform:{type:String,required:!0},proxy:{type:Object,default:null}},emits:["success","back"],setup(R,{emit:L}){const o=R,y=L,x=he(),U=k(!1),$=k(!1),h=k(""),_=k(""),v=k(!1),c=k(""),I=ee(()=>h.value&&_.value.trim());K(_,g=>{if(!g||typeof g!="string")return;const a=g.trim();if(!a)return;if(a.startsWith("http://")||a.startsWith("https://"))if(a.startsWith("http://localhost:45462"))try{const q=new URL(a).searchParams.get("code");q?(_.value=q,S("成功提取授权码!","success"),console.log("Successfully extracted authorization code from URL")):S("URL 中未找到授权码参数,请检查链接是否正确","error")}catch(N){console.error("Failed to parse URL:",N),S("链接格式错误,请检查是否为完整的 URL","error")}else if(o.platform==="gemini")try{const q=new URL(a).searchParams.get("code");q&&(_.value=q,S("成功提取授权码!","success"))}catch{}else S("请粘贴以 http://localhost:45462 开头的链接","error")});const P=async()=>{var g;U.value=!0;try{const a=(g=o.proxy)!=null&&g.enabled?{proxy:{type:o.proxy.type,host:o.proxy.host,port:parseInt(o.proxy.port),username:o.proxy.username||null,password:o.proxy.password||null}}:{};if(o.platform==="claude"){const G=await x.generateClaudeAuthUrl(a);h.value=G.authUrl,c.value=G.sessionId}else if(o.platform==="gemini"){const G=await x.generateGeminiAuthUrl(a);h.value=G.authUrl,c.value=G.sessionId}}catch(a){S(a.message||"生成授权链接失败","error")}finally{U.value=!1}},V=()=>{h.value="",_.value="",P()},F=async()=>{try{await navigator.clipboard.writeText(h.value),v.value=!0,S("链接已复制","success"),setTimeout(()=>{v.value=!1},2e3)}catch{const a=document.createElement("input");a.value=h.value,document.body.appendChild(a),a.select(),document.execCommand("copy"),document.body.removeChild(a),v.value=!0,S("链接已复制","success"),setTimeout(()=>{v.value=!1},2e3)}},s=async()=>{var g;if(I.value){$.value=!0;try{let a={};o.platform==="claude"?a={sessionId:c.value,callbackUrl:_.value.trim()}:o.platform==="gemini"&&(a={code:_.value.trim(),sessionId:c.value}),(g=o.proxy)!=null&&g.enabled&&(a.proxy={type:o.proxy.type,host:o.proxy.host,port:parseInt(o.proxy.port),username:o.proxy.username||null,password:o.proxy.password||null});let G;o.platform==="claude"?G=await x.exchangeClaudeCode(a):o.platform==="gemini"&&(G=await x.exchangeGeminiCode(a)),y("success",G)}catch(a){S(a.message||"授权失败,请检查授权码是否正确","error")}finally{$.value=!1}}};return(g,a)=>(r(),i("div",Ke,[R.platform==="claude"?(r(),i("div",We,[e("div",qe,[e("div",Be,[a[14]||(a[14]=e("div",{class:"w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0"},[e("i",{class:"fas fa-link text-white"})],-1)),e("div",Ne,[a[12]||(a[12]=e("h4",{class:"font-semibold text-blue-900 mb-3"},"Claude 账户授权",-1)),a[13]||(a[13]=e("p",{class:"text-sm text-blue-800 mb-4"}," 请按照以下步骤完成 Claude 账户的授权: ",-1)),e("div",Fe,[e("div",He,[e("div",Je,[a[5]||(a[5]=e("div",{class:"w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"1",-1)),e("div",Ye,[a[4]||(a[4]=e("p",{class:"font-medium text-blue-900 mb-2"},"点击下方按钮生成授权链接",-1)),h.value?(r(),i("div",et,[e("div",tt,[e("input",{type:"text",value:h.value,readonly:"",class:"form-input flex-1 text-xs font-mono bg-gray-50"},null,8,st),e("button",{onClick:F,class:"px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors",title:"复制链接"},[e("i",{class:D(v.value?"fas fa-check text-green-500":"fas fa-copy")},null,2)])]),e("button",{onClick:V,class:"text-xs text-blue-600 hover:text-blue-700"},a[3]||(a[3]=[e("i",{class:"fas fa-sync-alt mr-1"},null,-1),b("重新生成 ",-1)]))])):(r(),i("button",{key:0,onClick:P,disabled:U.value,class:"btn btn-primary px-4 py-2 text-sm"},[U.value?(r(),i("div",Ze)):(r(),i("i",Xe)),b(" "+j(U.value?"生成中...":"生成授权链接"),1)],8,Qe))])])]),a[11]||(a[11]=Z('
2

在浏览器中打开链接并完成授权

请在新标签页中打开授权链接,登录您的 Claude 账户并授权。

注意:如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。

',1)),e("div",lt,[e("div",at,[a[10]||(a[10]=e("div",{class:"w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"3",-1)),e("div",ot,[a[8]||(a[8]=e("p",{class:"font-medium text-blue-900 mb-2"},"输入 Authorization Code",-1)),a[9]||(a[9]=e("p",{class:"text-sm text-blue-700 mb-3"},[b(" 授权完成后,页面会显示一个 "),e("strong",null,"Authorization Code"),b(",请将其复制并粘贴到下方输入框: ")],-1)),e("div",nt,[e("div",null,[a[6]||(a[6]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},[e("i",{class:"fas fa-key text-blue-500 mr-2"}),b("Authorization Code ")],-1)),C(e("textarea",{"onUpdate:modelValue":a[0]||(a[0]=G=>_.value=G),rows:"3",class:"form-input w-full resize-none font-mono text-sm",placeholder:"粘贴从Claude页面获取的Authorization Code..."},null,512),[[z,_.value]])]),a[7]||(a[7]=e("p",{class:"text-xs text-gray-500 mt-2"},[e("i",{class:"fas fa-info-circle mr-1"}),b(" 请粘贴从Claude页面复制的Authorization Code ")],-1))])])])])])])])])])):R.platform==="gemini"?(r(),i("div",rt,[e("div",it,[e("div",ut,[a[26]||(a[26]=e("div",{class:"w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0"},[e("i",{class:"fas fa-robot text-white"})],-1)),e("div",dt,[a[24]||(a[24]=e("h4",{class:"font-semibold text-green-900 mb-3"},"Gemini 账户授权",-1)),a[25]||(a[25]=e("p",{class:"text-sm text-green-800 mb-4"}," 请按照以下步骤完成 Gemini 账户的授权: ",-1)),e("div",ct,[e("div",mt,[e("div",pt,[a[17]||(a[17]=e("div",{class:"w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"1",-1)),e("div",ft,[a[16]||(a[16]=e("p",{class:"font-medium text-green-900 mb-2"},"点击下方按钮生成授权链接",-1)),h.value?(r(),i("div",gt,[e("div",bt,[e("input",{type:"text",value:h.value,readonly:"",class:"form-input flex-1 text-xs font-mono bg-gray-50"},null,8,wt),e("button",{onClick:F,class:"px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors",title:"复制链接"},[e("i",{class:D(v.value?"fas fa-check text-green-500":"fas fa-copy")},null,2)])]),e("button",{onClick:V,class:"text-xs text-green-600 hover:text-green-700"},a[15]||(a[15]=[e("i",{class:"fas fa-sync-alt mr-1"},null,-1),b("重新生成 ",-1)]))])):(r(),i("button",{key:0,onClick:P,disabled:U.value,class:"btn btn-primary px-4 py-2 text-sm"},[U.value?(r(),i("div",yt)):(r(),i("i",vt)),b(" "+j(U.value?"生成中...":"生成授权链接"),1)],8,xt))])])]),a[23]||(a[23]=Z('
2

在浏览器中打开链接并完成授权

  1. 点击上方的授权链接,在新页面中完成Google账号登录
  2. 点击“登录”按钮后可能会加载很慢(这是正常的)
  3. 如果超过1分钟还在加载,请按 F5 刷新页面
  4. 授权完成后会跳转到 http://localhost:45462 (可能显示无法访问)

提示:如果页面一直无法跳转,可以打开浏览器开发者工具(F12),F5刷新一下授权页再点击页面的登录按钮,在“网络”标签中找到以 localhost:45462 开头的请求,复制其完整URL。

',1)),e("div",ht,[e("div",kt,[a[22]||(a[22]=e("div",{class:"w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"3",-1)),e("div",$t,[a[20]||(a[20]=e("p",{class:"font-medium text-green-900 mb-2"},"复制oauth后的链接",-1)),a[21]||(a[21]=e("p",{class:"text-sm text-green-700 mb-3"}," 复制浏览器地址栏的完整链接并粘贴到下方输入框: ",-1)),e("div",Tt,[e("div",null,[a[18]||(a[18]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},[e("i",{class:"fas fa-key text-green-500 mr-2"}),b("复制oauth后的链接 ")],-1)),C(e("textarea",{"onUpdate:modelValue":a[1]||(a[1]=G=>_.value=G),rows:"3",class:"form-input w-full resize-none font-mono text-sm",placeholder:"粘贴以 http://localhost:45462 开头的完整链接..."},null,512),[[z,_.value]])]),a[19]||(a[19]=Z('

支持粘贴完整链接,系统会自动提取授权码

也可以直接粘贴授权码(code参数的值)

',1))])])])])])])])])])):A("",!0),e("div",Ct,[e("button",{type:"button",onClick:a[2]||(a[2]=G=>g.$emit("back")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 上一步 "),e("button",{type:"button",onClick:s,disabled:!I.value||$.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[$.value?(r(),i("div",Ut)):A("",!0),b(" "+j($.value?"验证中...":"完成授权"),1)],8,At)])]))}},It={key:0,class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},jt={class:"modal-content w-full max-w-md p-6 mx-auto"},Vt={class:"flex items-start gap-4 mb-6"},St={class:"flex-1"},Gt={class:"text-lg font-bold text-gray-900 mb-2"},Rt={class:"text-gray-600 text-sm leading-relaxed whitespace-pre-line"},Mt={class:"flex gap-3"},ke={__name:"ConfirmModal",props:{show:{type:Boolean,required:!0},title:{type:String,default:""},message:{type:String,default:""},confirmText:{type:String,default:"继续"},cancelText:{type:String,default:"取消"}},emits:["confirm","cancel"],setup(R){return(L,o)=>(r(),te(be,{to:"body"},[R.show?(r(),i("div",It,[e("div",jt,[e("div",Vt,[o[2]||(o[2]=e("div",{class:"w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center flex-shrink-0"},[e("i",{class:"fas fa-exclamation text-white text-xl"})],-1)),e("div",St,[e("h3",Gt,j(R.title),1),e("p",Rt,j(R.message),1)])]),e("div",Mt,[e("button",{onClick:o[0]||(o[0]=y=>L.$emit("cancel")),class:"flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors"},j(R.cancelText),1),e("button",{onClick:o[1]||(o[1]=y=>L.$emit("confirm")),class:"flex-1 px-4 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-xl font-medium hover:from-yellow-600 hover:to-orange-600 transition-colors shadow-sm"},j(R.confirmText),1)])])])):A("",!0)]))}},Pt={key:0,class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},Ot={class:"modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar"},Lt={class:"flex items-center justify-between mb-6"},zt={class:"flex items-center gap-3"},Dt={class:"text-xl font-bold text-gray-900"},Et={key:0,class:"flex items-center justify-center mb-8"},Kt={class:"flex items-center space-x-4"},Wt={class:"flex items-center"},qt={class:"flex items-center"},Bt={key:1},Nt={class:"space-y-6"},Ft={key:0},Ht={class:"flex gap-4"},Jt={class:"flex items-center cursor-pointer"},Yt={class:"flex items-center cursor-pointer"},Qt={key:1},Xt={class:"flex gap-4"},Zt={class:"flex items-center cursor-pointer"},es={class:"flex items-center cursor-pointer"},ts={key:0,class:"text-red-500 text-xs mt-1"},ss={class:"flex gap-4"},ls={class:"flex items-center cursor-pointer"},as={class:"flex items-center cursor-pointer"},os={key:2},ns={key:3,class:"space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200"},rs={class:"flex items-start gap-3 mb-4"},is={key:0,class:"text-sm text-blue-800 mb-2"},us={key:1,class:"text-sm text-blue-800 mb-2"},ds={class:"bg-white/80 rounded-lg p-3 mt-2 mb-2 border border-blue-300"},cs={key:0,class:"text-xs text-blue-800"},ms={key:1,class:"text-xs text-blue-800"},ps={key:0,class:"text-red-500 text-xs mt-1"},fs={class:"flex gap-3 pt-4"},xs=["disabled"],vs=["disabled"],ys={key:0,class:"loading-spinner mr-2"},gs={key:3,class:"space-y-6"},bs={class:"flex gap-4"},ws={class:"flex items-center cursor-pointer"},hs={class:"flex items-center cursor-pointer"},ks={key:0},$s={class:"bg-amber-50 p-4 rounded-lg border border-amber-200"},Ts={class:"space-y-4"},Cs={class:"flex gap-3 pt-4"},As=["disabled"],Us={key:0,class:"loading-spinner mr-2"},ye={__name:"AccountForm",props:{account:{type:Object,default:null}},emits:["close","success"],setup(R,{emit:L}){var f,d,T,W,se;const o=R,y=L,x=he(),{showConfirmModal:U,confirmOptions:$,showConfirm:h,handleConfirm:_,handleCancel:v}=we(),c=ee(()=>!!o.account),I=k(!0),P=k(1),V=k(!1),F=()=>{var p;return(p=o.account)!=null&&p.proxy&&o.account.proxy.host&&o.account.proxy.port?{enabled:!0,type:o.account.proxy.type||"socks5",host:o.account.proxy.host,port:o.account.proxy.port,username:o.account.proxy.username||"",password:o.account.proxy.password||""}:{enabled:!1,type:"socks5",host:"",port:"",username:"",password:""}},s=k({platform:((f=o.account)==null?void 0:f.platform)||"claude",addType:"oauth",name:((d=o.account)==null?void 0:d.name)||"",description:((T=o.account)==null?void 0:T.description)||"",accountType:((W=o.account)==null?void 0:W.accountType)||"shared",projectId:((se=o.account)==null?void 0:se.projectId)||"",accessToken:"",refreshToken:"",proxy:F()}),g=k({name:"",accessToken:""}),a=ee(()=>{var p;return((p=s.value.name)==null?void 0:p.trim())&&s.value.platform});ee(()=>{var p,t,m;return s.value.addType==="manual"?((p=s.value.name)==null?void 0:p.trim())&&((t=s.value.accessToken)==null?void 0:t.trim()):(m=s.value.name)==null?void 0:m.trim()});const G=async()=>{if(g.value.name="",!a.value){(!s.value.name||s.value.name.trim()==="")&&(g.value.name="请填写账户名称");return}s.value.platform==="gemini"&&P.value===1&&s.value.addType==="oauth"&&(!s.value.projectId||s.value.projectId.trim()==="")&&!await h("项目编号未填写",`您尚未填写项目编号。 - -如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。 -如果您使用的是普通个人账号,可以继续不填写。`,"继续","返回填写")||(P.value=2)},N=async p=>{V.value=!0;try{const t={name:s.value.name,description:s.value.description,accountType:s.value.accountType,proxy:s.value.proxy.enabled?{type:s.value.proxy.type,host:s.value.proxy.host,port:parseInt(s.value.proxy.port),username:s.value.proxy.username||null,password:s.value.proxy.password||null}:null};s.value.platform==="claude"?t.claudeAiOauth=p.claudeAiOauth||p:s.value.platform==="gemini"&&(t.geminiOauth=p.tokens||p,s.value.projectId&&(t.projectId=s.value.projectId));let m;s.value.platform==="claude"?m=await x.createClaudeAccount(t):m=await x.createGeminiAccount(t),y("success",m)}catch(t){S(t.message||"账户创建失败","error")}finally{V.value=!1}},q=async()=>{g.value.name="",g.value.accessToken="";let p=!1;if((!s.value.name||s.value.name.trim()==="")&&(g.value.name="请填写账户名称",p=!0),s.value.addType==="manual"&&(!s.value.accessToken||s.value.accessToken.trim()==="")&&(g.value.accessToken="请填写 Access Token",p=!0),!p){V.value=!0;try{const t={name:s.value.name,description:s.value.description,accountType:s.value.accountType,proxy:s.value.proxy.enabled?{type:s.value.proxy.type,host:s.value.proxy.host,port:parseInt(s.value.proxy.port),username:s.value.proxy.username||null,password:s.value.proxy.password||null}:null};if(s.value.platform==="claude"){const u=s.value.refreshToken?6e5:31536e6;t.claudeAiOauth={accessToken:s.value.accessToken,refreshToken:s.value.refreshToken||"",expiresAt:Date.now()+u,scopes:["user:inference"]}}else if(s.value.platform==="gemini"){const u=s.value.refreshToken?6e5:31536e6;t.geminiOauth={access_token:s.value.accessToken,refresh_token:s.value.refreshToken||"",scope:"https://www.googleapis.com/auth/cloud-platform",token_type:"Bearer",expiry_date:Date.now()+u},s.value.projectId&&(t.projectId=s.value.projectId)}let m;s.value.platform==="claude"?m=await x.createClaudeAccount(t):m=await x.createGeminiAccount(t),y("success",m)}catch(t){S(t.message||"账户创建失败","error")}finally{V.value=!1}}},X=async()=>{if(g.value.name="",!s.value.name||s.value.name.trim()===""){g.value.name="请填写账户名称";return}if(!(s.value.platform==="gemini"&&(!s.value.projectId||s.value.projectId.trim()==="")&&!await h("项目编号未填写",`您尚未填写项目编号。 - -如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。 -如果您使用的是普通个人账号,可以继续不填写。`,"继续保存","返回填写"))){V.value=!0;try{const p={name:s.value.name,description:s.value.description,accountType:s.value.accountType,proxy:s.value.proxy.enabled?{type:s.value.proxy.type,host:s.value.proxy.host,port:parseInt(s.value.proxy.port),username:s.value.proxy.username||null,password:s.value.proxy.password||null}:null};if(s.value.accessToken||s.value.refreshToken){if(o.account.platform==="claude"){const t=s.value.refreshToken?6e5:31536e6;p.claudeAiOauth={accessToken:s.value.accessToken||"",refreshToken:s.value.refreshToken||"",expiresAt:Date.now()+t,scopes:["user:inference"]}}else if(o.account.platform==="gemini"){const t=s.value.refreshToken?6e5:31536e6;p.geminiOauth={access_token:s.value.accessToken||"",refresh_token:s.value.refreshToken||"",scope:"https://www.googleapis.com/auth/cloud-platform",token_type:"Bearer",expiry_date:Date.now()+t}}}o.account.platform==="gemini"&&s.value.projectId&&(p.projectId=s.value.projectId),o.account.platform==="claude"?await x.updateClaudeAccount(o.account.id,p):await x.updateGeminiAccount(o.account.id,p),y("success")}catch(p){S(p.message||"账户更新失败","error")}finally{V.value=!1}}};return K(()=>s.value.name,()=>{var p;g.value.name&&((p=s.value.name)!=null&&p.trim())&&(g.value.name="")}),K(()=>s.value.accessToken,()=>{var p;g.value.accessToken&&((p=s.value.accessToken)!=null&&p.trim())&&(g.value.accessToken="")}),K(()=>o.account,p=>{if(p){const t=p.proxy&&p.proxy.host&&p.proxy.port?{enabled:!0,type:p.proxy.type||"socks5",host:p.proxy.host,port:p.proxy.port,username:p.proxy.username||"",password:p.proxy.password||""}:{enabled:!1,type:"socks5",host:"",port:"",username:"",password:""};s.value={platform:p.platform,addType:"oauth",name:p.name,description:p.description||"",accountType:p.accountType||"shared",projectId:p.projectId||"",accessToken:"",refreshToken:"",proxy:t}}},{immediate:!0}),(p,t)=>(r(),te(be,{to:"body"},[I.value?(r(),i("div",Pt,[e("div",Ot,[e("div",Lt,[e("div",zt,[t[24]||(t[24]=e("div",{class:"w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center"},[e("i",{class:"fas fa-user-circle text-white"})],-1)),e("h3",Dt,j(c.value?"编辑账户":"添加账户"),1)]),e("button",{onClick:t[0]||(t[0]=m=>p.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},t[25]||(t[25]=[e("i",{class:"fas fa-times text-xl"},null,-1)]))]),!c.value&&s.value.addType==="oauth"?(r(),i("div",Et,[e("div",Kt,[e("div",Wt,[e("div",{class:D(["w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold",P.value>=1?"bg-blue-500 text-white":"bg-gray-200 text-gray-500"])}," 1 ",2),t[26]||(t[26]=e("span",{class:"ml-2 text-sm font-medium text-gray-700"},"基本信息",-1))]),t[28]||(t[28]=e("div",{class:"w-8 h-0.5 bg-gray-300"},null,-1)),e("div",qt,[e("div",{class:D(["w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold",P.value>=2?"bg-blue-500 text-white":"bg-gray-200 text-gray-500"])}," 2 ",2),t[27]||(t[27]=e("span",{class:"ml-2 text-sm font-medium text-gray-700"},"授权认证",-1))])])])):A("",!0),P.value===1&&!c.value?(r(),i("div",Bt,[e("div",Nt,[c.value?A("",!0):(r(),i("div",Ft,[t[31]||(t[31]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"平台",-1)),e("div",Ht,[e("label",Jt,[C(e("input",{type:"radio","onUpdate:modelValue":t[1]||(t[1]=m=>s.value.platform=m),value:"claude",class:"mr-2"},null,512),[[Y,s.value.platform]]),t[29]||(t[29]=e("span",{class:"text-sm text-gray-700"},"Claude",-1))]),e("label",Yt,[C(e("input",{type:"radio","onUpdate:modelValue":t[2]||(t[2]=m=>s.value.platform=m),value:"gemini",class:"mr-2"},null,512),[[Y,s.value.platform]]),t[30]||(t[30]=e("span",{class:"text-sm text-gray-700"},"Gemini",-1))])])])),c.value?A("",!0):(r(),i("div",Qt,[t[34]||(t[34]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"添加方式",-1)),e("div",Xt,[e("label",Zt,[C(e("input",{type:"radio","onUpdate:modelValue":t[3]||(t[3]=m=>s.value.addType=m),value:"oauth",class:"mr-2"},null,512),[[Y,s.value.addType]]),t[32]||(t[32]=e("span",{class:"text-sm text-gray-700"},"OAuth 授权 (推荐)",-1))]),e("label",es,[C(e("input",{type:"radio","onUpdate:modelValue":t[4]||(t[4]=m=>s.value.addType=m),value:"manual",class:"mr-2"},null,512),[[Y,s.value.addType]]),t[33]||(t[33]=e("span",{class:"text-sm text-gray-700"},"手动输入 Access Token",-1))])])])),e("div",null,[t[35]||(t[35]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户名称",-1)),C(e("input",{"onUpdate:modelValue":t[5]||(t[5]=m=>s.value.name=m),type:"text",required:"",class:D(["form-input w-full",{"border-red-500":g.value.name}]),placeholder:"为账户设置一个易识别的名称"},null,2),[[z,s.value.name]]),g.value.name?(r(),i("p",ts,j(g.value.name),1)):A("",!0)]),e("div",null,[t[36]||(t[36]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"描述 (可选)",-1)),C(e("textarea",{"onUpdate:modelValue":t[6]||(t[6]=m=>s.value.description=m),rows:"3",class:"form-input w-full resize-none",placeholder:"账户用途说明..."},null,512),[[z,s.value.description]])]),e("div",null,[t[39]||(t[39]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户类型",-1)),e("div",ss,[e("label",ls,[C(e("input",{type:"radio","onUpdate:modelValue":t[7]||(t[7]=m=>s.value.accountType=m),value:"shared",class:"mr-2"},null,512),[[Y,s.value.accountType]]),t[37]||(t[37]=e("span",{class:"text-sm text-gray-700"},"共享账户",-1))]),e("label",as,[C(e("input",{type:"radio","onUpdate:modelValue":t[8]||(t[8]=m=>s.value.accountType=m),value:"dedicated",class:"mr-2"},null,512),[[Y,s.value.accountType]]),t[38]||(t[38]=e("span",{class:"text-sm text-gray-700"},"专属账户",-1))])]),t[40]||(t[40]=e("p",{class:"text-xs text-gray-500 mt-2"}," 共享账户:供所有API Key使用;专属账户:仅供特定API Key使用 ",-1))]),s.value.platform==="gemini"?(r(),i("div",os,[t[41]||(t[41]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"项目编号 (可选)",-1)),C(e("input",{"onUpdate:modelValue":t[9]||(t[9]=m=>s.value.projectId=m),type:"text",class:"form-input w-full",placeholder:"例如:123456789012(纯数字)"},null,512),[[z,s.value.projectId]]),t[42]||(t[42]=e("div",{class:"mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"},[e("div",{class:"flex items-start gap-2"},[e("i",{class:"fas fa-info-circle text-yellow-600 mt-0.5"}),e("div",{class:"text-xs text-yellow-700"},[e("p",{class:"font-medium mb-1"},"Google Cloud/Workspace 账号需要提供项目编号"),e("p",null,"某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目编号。"),e("div",{class:"mt-2 p-2 bg-white rounded border border-yellow-300"},[e("p",{class:"font-medium mb-1"},"如何获取项目编号:"),e("ol",{class:"list-decimal list-inside space-y-1 ml-2"},[e("li",null,[b("访问 "),e("a",{href:"https://console.cloud.google.com/welcome",target:"_blank",class:"text-blue-600 hover:underline font-medium"},"Google Cloud Console")]),e("li",null,[b("复制"),e("span",{class:"font-semibold text-red-600"},"项目编号(Project Number)"),b(",通常是12位纯数字")]),e("li",{class:"text-red-600"},"⚠️ 注意:不要复制项目ID(Project ID),要复制项目编号!")])]),e("p",{class:"mt-2"},[e("strong",null,"提示:"),b("如果您的账号是普通个人账号(未绑定 Google Cloud),请留空此字段。")])])])],-1))])):A("",!0),s.value.addType==="manual"?(r(),i("div",ns,[e("div",rs,[t[48]||(t[48]=e("div",{class:"w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1"},[e("i",{class:"fas fa-info text-white text-sm"})],-1)),e("div",null,[t[46]||(t[46]=e("h5",{class:"font-semibold text-blue-900 mb-2"},"手动输入 Token",-1)),s.value.platform==="claude"?(r(),i("p",is," 请输入有效的 Claude Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。 ")):s.value.platform==="gemini"?(r(),i("p",us," 请输入有效的 Gemini Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。 ")):A("",!0),e("div",ds,[t[45]||(t[45]=e("p",{class:"text-sm text-blue-900 font-medium mb-1"},[e("i",{class:"fas fa-folder-open mr-1"}),b(" 获取 Access Token 的方法: ")],-1)),s.value.platform==="claude"?(r(),i("p",cs,t[43]||(t[43]=[b(" 请从已登录 Claude Code 的机器上获取 ",-1),e("code",{class:"bg-blue-100 px-1 py-0.5 rounded font-mono"},"~/.claude/.credentials.json",-1),b(" 文件中的凭证, 请勿使用 Claude 官网 API Keys 页面的密钥。 ",-1)]))):s.value.platform==="gemini"?(r(),i("p",ms,t[44]||(t[44]=[b(" 请从已登录 Gemini CLI 的机器上获取 ",-1),e("code",{class:"bg-blue-100 px-1 py-0.5 rounded font-mono"},"~/.config/gemini/credentials.json",-1),b(" 文件中的凭证。 ",-1)]))):A("",!0)]),t[47]||(t[47]=e("p",{class:"text-xs text-blue-600"},"💡 如果未填写 Refresh Token,Token 过期后需要手动更新。",-1))])]),e("div",null,[t[49]||(t[49]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"Access Token *",-1)),C(e("textarea",{"onUpdate:modelValue":t[10]||(t[10]=m=>s.value.accessToken=m),rows:"4",required:"",class:D(["form-input w-full resize-none font-mono text-xs",{"border-red-500":g.value.accessToken}]),placeholder:"请输入 Access Token..."},null,2),[[z,s.value.accessToken]]),g.value.accessToken?(r(),i("p",ps,j(g.value.accessToken),1)):A("",!0)]),e("div",null,[t[50]||(t[50]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"Refresh Token (可选)",-1)),C(e("textarea",{"onUpdate:modelValue":t[11]||(t[11]=m=>s.value.refreshToken=m),rows:"4",class:"form-input w-full resize-none font-mono text-xs",placeholder:"请输入 Refresh Token..."},null,512),[[z,s.value.refreshToken]])])])):A("",!0),ae(ve,{modelValue:s.value.proxy,"onUpdate:modelValue":t[12]||(t[12]=m=>s.value.proxy=m)},null,8,["modelValue"]),e("div",fs,[e("button",{type:"button",onClick:t[13]||(t[13]=m=>p.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),s.value.addType==="oauth"?(r(),i("button",{key:0,type:"button",onClick:G,disabled:V.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"}," 下一步 ",8,xs)):(r(),i("button",{key:1,type:"button",onClick:q,disabled:V.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[V.value?(r(),i("div",ys)):A("",!0),b(" "+j(V.value?"创建中...":"创建"),1)],8,vs))])])])):A("",!0),P.value===2&&s.value.addType==="oauth"?(r(),te(_t,{key:2,platform:s.value.platform,proxy:s.value.proxy,onSuccess:N,onBack:t[14]||(t[14]=m=>P.value=1)},null,8,["platform","proxy"])):A("",!0),c.value?(r(),i("div",gs,[e("div",null,[t[51]||(t[51]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户名称",-1)),C(e("input",{"onUpdate:modelValue":t[15]||(t[15]=m=>s.value.name=m),type:"text",required:"",class:"form-input w-full",placeholder:"为账户设置一个易识别的名称"},null,512),[[z,s.value.name]])]),e("div",null,[t[52]||(t[52]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"描述 (可选)",-1)),C(e("textarea",{"onUpdate:modelValue":t[16]||(t[16]=m=>s.value.description=m),rows:"3",class:"form-input w-full resize-none",placeholder:"账户用途说明..."},null,512),[[z,s.value.description]])]),e("div",null,[t[55]||(t[55]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户类型",-1)),e("div",bs,[e("label",ws,[C(e("input",{type:"radio","onUpdate:modelValue":t[17]||(t[17]=m=>s.value.accountType=m),value:"shared",class:"mr-2"},null,512),[[Y,s.value.accountType]]),t[53]||(t[53]=e("span",{class:"text-sm text-gray-700"},"共享账户",-1))]),e("label",hs,[C(e("input",{type:"radio","onUpdate:modelValue":t[18]||(t[18]=m=>s.value.accountType=m),value:"dedicated",class:"mr-2"},null,512),[[Y,s.value.accountType]]),t[54]||(t[54]=e("span",{class:"text-sm text-gray-700"},"专属账户",-1))])]),t[56]||(t[56]=e("p",{class:"text-xs text-gray-500 mt-2"}," 共享账户:供所有API Key使用;专属账户:仅供特定API Key使用 ",-1))]),s.value.platform==="gemini"?(r(),i("div",ks,[t[57]||(t[57]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"项目编号 (可选)",-1)),C(e("input",{"onUpdate:modelValue":t[19]||(t[19]=m=>s.value.projectId=m),type:"text",class:"form-input w-full",placeholder:"例如:123456789012(纯数字)"},null,512),[[z,s.value.projectId]]),t[58]||(t[58]=e("p",{class:"text-xs text-gray-500 mt-2"}," Google Cloud/Workspace 账号可能需要提供项目编号 ",-1))])):A("",!0),e("div",$s,[t[61]||(t[61]=e("div",{class:"flex items-start gap-3 mb-4"},[e("div",{class:"w-8 h-8 bg-amber-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1"},[e("i",{class:"fas fa-key text-white text-sm"})]),e("div",null,[e("h5",{class:"font-semibold text-amber-900 mb-2"},"更新 Token"),e("p",{class:"text-sm text-amber-800 mb-2"},"可以更新 Access Token 和 Refresh Token。为了安全起见,不会显示当前的 Token 值。"),e("p",{class:"text-xs text-amber-600"},"💡 留空表示不更新该字段。")])],-1)),e("div",Ts,[e("div",null,[t[59]||(t[59]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"新的 Access Token",-1)),C(e("textarea",{"onUpdate:modelValue":t[20]||(t[20]=m=>s.value.accessToken=m),rows:"4",class:"form-input w-full resize-none font-mono text-xs",placeholder:"留空表示不更新..."},null,512),[[z,s.value.accessToken]])]),e("div",null,[t[60]||(t[60]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"新的 Refresh Token",-1)),C(e("textarea",{"onUpdate:modelValue":t[21]||(t[21]=m=>s.value.refreshToken=m),rows:"4",class:"form-input w-full resize-none font-mono text-xs",placeholder:"留空表示不更新..."},null,512),[[z,s.value.refreshToken]])])])]),ae(ve,{modelValue:s.value.proxy,"onUpdate:modelValue":t[22]||(t[22]=m=>s.value.proxy=m)},null,8,["modelValue"]),e("div",Cs,[e("button",{type:"button",onClick:t[23]||(t[23]=m=>p.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),e("button",{type:"button",onClick:X,disabled:V.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[V.value?(r(),i("div",Us)):A("",!0),b(" "+j(V.value?"更新中...":"更新"),1)],8,As)])])):A("",!0)])])):A("",!0),ae(ke,{show:E(U),title:E($).title,message:E($).message,"confirm-text":E($).confirmText,"cancel-text":E($).cancelText,onConfirm:E(_),onCancel:E(v)},null,8,["show","title","message","confirm-text","cancel-text","onConfirm","onCancel"])]))}},_s={class:"accounts-container"},Is={class:"card p-6"},js={class:"flex flex-col md:flex-row justify-between items-center gap-4 mb-6"},Vs={class:"flex gap-2"},Ss={key:0,class:"text-center py-12"},Gs={key:1,class:"text-center py-12"},Rs={key:2,class:"table-container"},Ms={class:"min-w-full"},Ps={class:"bg-gray-50/80 backdrop-blur-sm"},Os={key:1,class:"fas fa-sort ml-1 text-gray-400"},Ls={key:1,class:"fas fa-sort ml-1 text-gray-400"},zs={key:1,class:"fas fa-sort ml-1 text-gray-400"},Ds={key:1,class:"fas fa-sort ml-1 text-gray-400"},Es={class:"divide-y divide-gray-200/50"},Ks={class:"px-6 py-4 whitespace-nowrap"},Ws={class:"flex items-center"},qs={class:"flex items-center gap-2"},Bs={class:"text-sm font-semibold text-gray-900"},Ns={key:0,class:"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"},Fs={key:1,class:"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"},Hs={class:"text-xs text-gray-500"},Js={class:"px-6 py-4 whitespace-nowrap"},Ys={key:0,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"},Qs={key:1,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800"},Xs={class:"px-6 py-4 whitespace-nowrap"},Zs={key:0,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"},el={key:1,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800"},tl={class:"px-6 py-4 whitespace-nowrap"},sl={class:"flex flex-col gap-1"},ll={key:0,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"},al={key:1,class:"text-xs text-gray-500"},ol={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-600"},nl={key:0,class:"text-xs bg-blue-50 px-2 py-1 rounded font-mono"},rl={key:1,class:"text-gray-400"},il={class:"px-6 py-4 whitespace-nowrap text-sm"},ul={key:0,class:"space-y-1"},dl={class:"flex items-center gap-2"},cl={class:"text-sm font-medium text-gray-900"},ml={class:"flex items-center gap-2"},pl={class:"text-xs text-gray-600"},fl={key:0,class:"text-xs text-gray-500"},xl={key:1,class:"text-gray-400 text-xs"},vl={class:"px-6 py-4 whitespace-nowrap"},yl={key:0,class:"space-y-2"},gl={class:"flex items-center gap-2"},bl={class:"w-24 bg-gray-200 rounded-full h-2"},wl={class:"text-xs text-gray-700 font-medium min-w-[32px]"},hl={class:"text-xs text-gray-600"},kl={key:0,class:"text-indigo-600 font-medium"},$l={key:1,class:"text-gray-400 text-sm"},Tl={key:2,class:"text-gray-400 text-sm"},Cl={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-600"},Al={class:"px-6 py-4 whitespace-nowrap text-sm font-medium"},Ul={class:"flex items-center gap-2"},_l=["onClick","disabled","title"],Il=["onClick"],jl=["onClick"],Vl={__name:"AccountsView",setup(R){const{showConfirmModal:L,confirmOptions:o,showConfirm:y,handleConfirm:x,handleCancel:U}=we(),$=k([]),h=k(!1),_=k("name"),v=k(""),c=k("asc"),I=k([]),P=k(!1),V=k(!1),F=k(null),s=ee(()=>v.value?[...$.value].sort((l,n)=>{var B,H,J,oe,ne,re,ie,ue,de,ce,me,pe;let w=l[v.value],O=n[v.value];return v.value==="dailyTokens"?(w=((H=(B=l.usage)==null?void 0:B.daily)==null?void 0:H.allTokens)||0,O=((oe=(J=n.usage)==null?void 0:J.daily)==null?void 0:oe.allTokens)||0):v.value==="dailyRequests"?(w=((re=(ne=l.usage)==null?void 0:ne.daily)==null?void 0:re.requests)||0,O=((ue=(ie=n.usage)==null?void 0:ie.daily)==null?void 0:ue.requests)||0):v.value==="totalTokens"&&(w=((ce=(de=l.usage)==null?void 0:de.total)==null?void 0:ce.allTokens)||0,O=((pe=(me=n.usage)==null?void 0:me.total)==null?void 0:pe.allTokens)||0),v.value==="lastUsed"&&(w=l.lastUsedAt?new Date(l.lastUsedAt).getTime():0,O=n.lastUsedAt?new Date(n.lastUsedAt).getTime():0),v.value==="status"&&(w=l.isActive?1:0,O=n.isActive?1:0),wO?c.value==="asc"?1:-1:0}):$.value),g=async()=>{h.value=!0;try{const[u,l,n]=await Promise.all([M.get("/admin/claude-accounts"),M.get("/admin/gemini-accounts"),M.get("/admin/api-keys")]);n.success&&(I.value=n.data||[]);const w=[];if(u.success){const O=(u.data||[]).map(B=>{const H=I.value.filter(J=>J.claudeAccountId===B.id).length;return{...B,platform:"claude",boundApiKeysCount:H}});w.push(...O)}if(l.success){const O=(l.data||[]).map(B=>{const H=I.value.filter(J=>J.geminiAccountId===B.id).length;return{...B,platform:"gemini",boundApiKeysCount:H}});w.push(...O)}$.value=w}catch{S("加载账户失败","error")}finally{h.value=!1}},a=u=>{u&&(v.value===u?c.value=c.value==="asc"?"desc":"asc":(v.value=u,c.value="asc"))},G=u=>{if(u==null)return"0";const l=Number(u);return l>=1e6?Math.floor(l/1e6).toLocaleString()+"M":l.toLocaleString()},N=u=>{if(!u)return"从未使用";const l=new Date(u),w=new Date-l;return w<6e4?"刚刚":w<36e5?`${Math.floor(w/6e4)} 分钟前`:w<864e5?`${Math.floor(w/36e5)} 小时前`:w<6048e5?`${Math.floor(w/864e5)} 天前`:l.toLocaleDateString("zh-CN")},q=async()=>{try{const u=await M.get("/admin/api-keys");u.success&&(I.value=u.data)}catch(u){console.error("Failed to load API keys:",u)}},X=u=>{if(!u||!u.host||!u.port)return null;let l=`${u.type}://${u.host}:${u.port}`;if(u.username){const n=u.username.length>2?u.username[0]+"***"+u.username[u.username.length-1]:"***",w=u.password?"****":"";l=`${u.type}://${n}:${w}@${u.host}:${u.port}`}return l},f=(u,l)=>{if(!u||!l)return"--";const n=new Date(u),w=new Date(l),O=n.getHours().toString().padStart(2,"0"),B=n.getMinutes().toString().padStart(2,"0"),H=w.getHours().toString().padStart(2,"0"),J=w.getMinutes().toString().padStart(2,"0");return`${O}:${B} - ${H}:${J}`},d=u=>{if(!u||u<=0)return"已结束";const l=Math.floor(u/60),n=u%60;return l>0?`${l}小时${n}分钟`:`${n}分钟`},T=()=>{P.value=!0},W=u=>{F.value=u,V.value=!0},se=async u=>{const l=I.value.filter(w=>w.claudeAccountId===u.id||w.geminiAccountId===u.id).length;if(l>0){S(`无法删除此账号,有 ${l} 个API Key绑定到此账号,请先解绑所有API Key`,"error");return}if(await y("删除账户",`确定要删除账户 "${u.name}" 吗? - -此操作不可恢复。`,"删除","取消"))try{const w=u.platform==="claude"?`/admin/claude-accounts/${u.id}`:`/admin/gemini-accounts/${u.id}`,O=await M.delete(w);O.success?(S("账户已删除","success"),g()):S(O.message||"删除失败","error")}catch{S("删除失败","error")}},p=async u=>{if(!u.isRefreshing)try{u.isRefreshing=!0;const l=await M.post(`/admin/claude-accounts/${u.id}/refresh`);l.success?(S("Token刷新成功","success"),g()):S(l.message||"Token刷新失败","error")}catch{S("Token刷新失败","error")}finally{u.isRefreshing=!1}},t=()=>{P.value=!1,S("账户创建成功","success"),g()},m=()=>{V.value=!1,S("账户更新成功","success"),g()};return K(_,u=>{const l={name:"name",dailyTokens:"dailyTokens",dailyRequests:"dailyRequests",totalTokens:"totalTokens",lastUsed:"lastUsed"};l[u]&&a(l[u])}),Ae(()=>{g(),q()}),(u,l)=>(r(),i("div",_s,[e("div",Is,[e("div",js,[l[10]||(l[10]=e("div",null,[e("h3",{class:"text-xl font-bold text-gray-900 mb-2"},"账户管理"),e("p",{class:"text-gray-600"},"管理您的 Claude 和 Gemini 账户及代理配置")],-1)),e("div",Vs,[C(e("select",{"onUpdate:modelValue":l[0]||(l[0]=n=>_.value=n),onChange:l[1]||(l[1]=n=>a()),class:"form-input px-3 py-2 text-sm"},l[8]||(l[8]=[Z('',5)]),544),[[ge,_.value]]),e("button",{onClick:Ue(T,["stop"]),class:"btn btn-success px-6 py-3 flex items-center gap-2"},l[9]||(l[9]=[e("i",{class:"fas fa-plus"},null,-1),b("添加账户 ",-1)]))])]),h.value?(r(),i("div",Ss,l[11]||(l[11]=[e("div",{class:"loading-spinner mx-auto mb-4"},null,-1),e("p",{class:"text-gray-500"},"正在加载账户...",-1)]))):s.value.length===0?(r(),i("div",Gs,l[12]||(l[12]=[e("div",{class:"w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"},[e("i",{class:"fas fa-user-circle text-gray-400 text-xl"})],-1),e("p",{class:"text-gray-500 text-lg"},"暂无账户",-1),e("p",{class:"text-gray-400 text-sm mt-2"},"点击上方按钮添加您的第一个账户",-1)]))):(r(),i("div",Rs,[e("table",Ms,[e("thead",Ps,[e("tr",null,[e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[2]||(l[2]=n=>a("name"))},[l[13]||(l[13]=b(" 名称 ",-1)),v.value==="name"?(r(),i("i",{key:0,class:D(["fas",c.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(r(),i("i",Os))]),e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[3]||(l[3]=n=>a("platform"))},[l[14]||(l[14]=b(" 平台 ",-1)),v.value==="platform"?(r(),i("i",{key:0,class:D(["fas",c.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(r(),i("i",Ls))]),e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[4]||(l[4]=n=>a("accountType"))},[l[15]||(l[15]=b(" 类型 ",-1)),v.value==="accountType"?(r(),i("i",{key:0,class:D(["fas",c.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(r(),i("i",zs))]),e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[5]||(l[5]=n=>a("status"))},[l[16]||(l[16]=b(" 状态 ",-1)),v.value==="status"?(r(),i("i",{key:0,class:D(["fas",c.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(r(),i("i",Ds))]),l[17]||(l[17]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"代理",-1)),l[18]||(l[18]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"今日使用",-1)),l[19]||(l[19]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"会话窗口",-1)),l[20]||(l[20]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"最后使用",-1)),l[21]||(l[21]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"操作",-1))])]),e("tbody",Es,[(r(!0),i(_e,null,Ie(s.value,n=>(r(),i("tr",{key:n.id,class:"table-row"},[e("td",Ks,[e("div",Ws,[l[24]||(l[24]=e("div",{class:"w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3"},[e("i",{class:"fas fa-user-circle text-white text-xs"})],-1)),e("div",null,[e("div",qs,[e("div",Bs,j(n.name),1),n.accountType==="dedicated"?(r(),i("span",Ns,l[22]||(l[22]=[e("i",{class:"fas fa-lock mr-1"},null,-1),b("专属 ",-1)]))):(r(),i("span",Fs,l[23]||(l[23]=[e("i",{class:"fas fa-share-alt mr-1"},null,-1),b("共享 ",-1)])))]),e("div",Hs,j(n.id),1)])])]),e("td",Js,[n.platform==="gemini"?(r(),i("span",Ys,l[25]||(l[25]=[e("i",{class:"fas fa-robot mr-1"},null,-1),b("Gemini ",-1)]))):(r(),i("span",Qs,l[26]||(l[26]=[e("i",{class:"fas fa-brain mr-1"},null,-1),b("Claude ",-1)])))]),e("td",Xs,[n.scopes&&n.scopes.length>0?(r(),i("span",Zs,l[27]||(l[27]=[e("i",{class:"fas fa-lock mr-1"},null,-1),b("OAuth ",-1)]))):(r(),i("span",el,l[28]||(l[28]=[e("i",{class:"fas fa-key mr-1"},null,-1),b("传统 ",-1)])))]),e("td",tl,[e("div",sl,[e("span",{class:D(["inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold",n.isActive?"bg-green-100 text-green-800":"bg-red-100 text-red-800"])},[e("div",{class:D(["w-2 h-2 rounded-full mr-2",n.isActive?"bg-green-500":"bg-red-500"])},null,2),b(" "+j(n.isActive?"正常":"异常"),1)],2),n.rateLimitStatus&&n.rateLimitStatus.isRateLimited?(r(),i("span",ll,[l[29]||(l[29]=e("i",{class:"fas fa-exclamation-triangle mr-1"},null,-1)),b(" 限流中 ("+j(n.rateLimitStatus.minutesRemaining)+"分钟) ",1)])):A("",!0),n.accountType==="dedicated"?(r(),i("span",al," 绑定: "+j(n.boundApiKeysCount||0)+" 个API Key ",1)):A("",!0)])]),e("td",ol,[X(n.proxy)?(r(),i("div",nl,j(X(n.proxy)),1)):(r(),i("div",rl,"无代理"))]),e("td",il,[n.usage&&n.usage.daily?(r(),i("div",ul,[e("div",dl,[l[30]||(l[30]=e("div",{class:"w-2 h-2 bg-green-500 rounded-full"},null,-1)),e("span",cl,j(n.usage.daily.requests||0)+" 次",1)]),e("div",ml,[l[31]||(l[31]=e("div",{class:"w-2 h-2 bg-blue-500 rounded-full"},null,-1)),e("span",pl,j(G(n.usage.daily.allTokens||0))+" tokens",1)]),n.usage.averages&&n.usage.averages.rpm>0?(r(),i("div",fl," 平均 "+j(n.usage.averages.rpm.toFixed(2))+" RPM ",1)):A("",!0)])):(r(),i("div",xl,"暂无数据"))]),e("td",vl,[n.platform==="claude"&&n.sessionWindow&&n.sessionWindow.hasActiveWindow?(r(),i("div",yl,[e("div",gl,[e("div",bl,[e("div",{class:"bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300",style:je({width:n.sessionWindow.progress+"%"})},null,4)]),e("span",wl,j(n.sessionWindow.progress)+"% ",1)]),e("div",hl,[e("div",null,j(f(n.sessionWindow.windowStart,n.sessionWindow.windowEnd)),1),n.sessionWindow.remainingTime>0?(r(),i("div",kl," 剩余 "+j(d(n.sessionWindow.remainingTime)),1)):A("",!0)])])):n.platform==="claude"?(r(),i("div",$l,l[32]||(l[32]=[e("i",{class:"fas fa-minus"},null,-1)]))):(r(),i("div",Tl,l[33]||(l[33]=[e("span",{class:"text-xs"},"N/A",-1)])))]),e("td",Cl,j(N(n.lastUsedAt)),1),e("td",Al,[e("div",Ul,[n.platform==="claude"&&n.scopes?(r(),i("button",{key:0,onClick:w=>p(n),disabled:n.isRefreshing,class:D(["px-3 py-1.5 rounded-lg text-xs font-medium transition-colors",n.isRefreshing?"bg-gray-100 text-gray-400 cursor-not-allowed":"bg-blue-100 text-blue-700 hover:bg-blue-200"]),title:n.isRefreshing?"刷新中...":"刷新Token"},[e("i",{class:D(["fas fa-sync-alt",n.isRefreshing?"animate-spin":""])},null,2)],10,_l)):A("",!0),e("button",{onClick:w=>W(n),class:"px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors"},l[34]||(l[34]=[e("i",{class:"fas fa-edit"},null,-1)]),8,Il),e("button",{onClick:w=>se(n),class:"px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors"},l[35]||(l[35]=[e("i",{class:"fas fa-trash"},null,-1)]),8,jl)])])]))),128))])])]))]),P.value?(r(),te(ye,{key:0,onClose:l[6]||(l[6]=n=>P.value=!1),onSuccess:t})):A("",!0),V.value?(r(),te(ye,{key:1,account:F.value,onClose:l[7]||(l[7]=n=>V.value=!1),onSuccess:m},null,8,["account"])):A("",!0),ae(ke,{show:E(L),title:E(o).title,message:E(o).message,"confirm-text":E(o).confirmText,"cancel-text":E(o).cancelText,onConfirm:E(x),onCancel:E(U)},null,8,["show","title","message","confirm-text","cancel-text","onConfirm","onCancel"])]))}},Ol=Ve(Vl,[["__scopeId","data-v-aec45f3e"]]);export{Ol as default}; diff --git a/web/admin-spa/dist/assets/ApiKeysView-8a5XQ0yG.js b/web/admin-spa/dist/assets/ApiKeysView-8a5XQ0yG.js deleted file mode 100644 index 1d63066f..00000000 --- a/web/admin-spa/dist/assets/ApiKeysView-8a5XQ0yG.js +++ /dev/null @@ -1,9 +0,0 @@ -import{E as Mt}from"./element-plus-B8Fs_0jW.js";import{aR as Rt,r as C,_ as it,q as at,c as rt,I as z,y as i,z as t,Y as H,K as b,x as r,L as $,O as g,aq as R,C as U,P as u,Q as _,ac as T,aa as et,aX as N,an as B,al as G,a5 as st,R as St,B as Pt}from"./vue-vendor-CKToUHZx.js";import{s as I}from"./toast-BvwA7Mwb.js";import{a as W,_ as J,u as dt}from"./index-DhITICXu.js";import"./vendor-BDiMbLwQ.js";const ut=Rt("clients",{state:()=>({supportedClients:[],loading:!1,error:null}),actions:{async loadSupportedClients(){if(this.supportedClients.length>0)return this.supportedClients;this.loading=!0,this.error=null;try{const k=await W.get("/admin/supported-clients");return k.success?this.supportedClients=k.data||[]:(this.error=k.message||"加载支持的客户端失败",console.error("Failed to load supported clients:",this.error)),this.supportedClients}catch(k){return this.error=k.message||"加载支持的客户端失败",console.error("Error loading supported clients:",k),[]}finally{this.loading=!1}}}}),_t={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},Tt={class:"modal-content w-full max-w-lg p-6 mx-auto max-h-[90vh] flex flex-col"},Et={class:"flex items-center justify-between mb-4"},Vt={key:0,class:"text-red-500 text-xs mt-1"},jt={class:"space-y-2"},Ut={key:0,class:"flex flex-wrap gap-2"},qt=["onClick"],Ft={class:"flex gap-2"},Ot=["onKeypress"],Wt={class:"bg-blue-50 border border-blue-200 rounded-lg p-3"},Yt={class:"space-y-2"},zt={class:"grid grid-cols-1 lg:grid-cols-3 gap-2"},Nt={class:"space-y-2"},Ht={class:"flex gap-2"},Bt={key:0,class:"mt-3"},Gt=["min"],Qt={key:1,class:"text-xs text-gray-500 mt-2"},Xt={class:"flex gap-4"},Jt={class:"flex items-center cursor-pointer"},Zt={class:"flex items-center cursor-pointer"},te={class:"flex items-center cursor-pointer"},ee={class:"grid grid-cols-1 gap-3"},se=["disabled"],le=["value"],oe=["disabled"],ne=["value"],ie={class:"flex items-center mb-2"},ae={key:0,class:"space-y-2 bg-red-50 border border-red-200 rounded-lg p-3"},re={class:"flex flex-wrap gap-1 mb-2 min-h-[24px]"},de=["onClick"],ue={key:0,class:"text-gray-400 text-xs"},me={class:"flex gap-2"},pe=["onKeydown"],ce={class:"flex items-center mb-2"},xe={key:0,class:"bg-green-50 border border-green-200 rounded-lg p-3"},fe={class:"space-y-1"},ge=["id","value"],ye=["for"],be={class:"text-sm font-medium text-gray-700"},ve={class:"text-xs text-gray-500 block"},we={class:"flex gap-3 pt-2"},$e=["disabled"],Ce={key:0,class:"loading-spinner mr-2"},Ae={key:1,class:"fas fa-plus mr-2"},Ke={__name:"CreateApiKeyModal",props:{accounts:{type:Object,default:()=>({claude:[],gemini:[]})}},emits:["close","success"],setup(k,{emit:O}){const w=O;dt();const P=ut(),D=C(!1),f=C({name:""}),L=C(""),h=C([]),e=it({name:"",description:"",tokenLimit:"",rateLimitWindow:"",rateLimitRequests:"",concurrencyLimit:"",dailyCostLimit:"",expireDuration:"",customExpireDate:"",expiresAt:null,permissions:"all",claudeAccountId:"",geminiAccountId:"",enableModelRestriction:!1,restrictedModels:[],modelInput:"",enableClientRestriction:!1,allowedClients:[],tags:[]});at(async()=>{h.value=await P.loadSupportedClients()});const V=rt(()=>{const v=new Date;return v.setMinutes(v.getMinutes()+1),v.toISOString().slice(0,16)}),A=()=>{if(!e.expireDuration){e.expiresAt=null;return}if(e.expireDuration==="custom")return;const v=new Date,d=e.expireDuration.match(/(\d+)([dhmy])/);if(d){const[,q,Q]=d,F=parseInt(q);switch(Q){case"d":v.setDate(v.getDate()+F);break;case"h":v.setHours(v.getHours()+F);break;case"m":v.setMonth(v.getMonth()+F);break;case"y":v.setFullYear(v.getFullYear()+F);break}e.expiresAt=v.toISOString()}},c=()=>{e.customExpireDate&&(e.expiresAt=new Date(e.customExpireDate).toISOString())},y=v=>new Date(v).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),S=()=>{e.modelInput&&!e.restrictedModels.includes(e.modelInput)&&(e.restrictedModels.push(e.modelInput),e.modelInput="")},K=v=>{e.restrictedModels.splice(v,1)},n=()=>{if(L.value&&L.value.trim()){const v=L.value.trim();e.tags.includes(v)||e.tags.push(v),L.value=""}},m=v=>{e.tags.splice(v,1)},j=async()=>{if(f.value.name="",!e.name||!e.name.trim()){f.value.name="请输入API Key名称";return}D.value=!0;try{const v={name:e.name,description:e.description||void 0,tokenLimit:e.tokenLimit!==""&&e.tokenLimit!==null?parseInt(e.tokenLimit):null,rateLimitWindow:e.rateLimitWindow!==""&&e.rateLimitWindow!==null?parseInt(e.rateLimitWindow):null,rateLimitRequests:e.rateLimitRequests!==""&&e.rateLimitRequests!==null?parseInt(e.rateLimitRequests):null,concurrencyLimit:e.concurrencyLimit!==""&&e.concurrencyLimit!==null?parseInt(e.concurrencyLimit):0,dailyCostLimit:e.dailyCostLimit!==""&&e.dailyCostLimit!==null?parseFloat(e.dailyCostLimit):0,expiresAt:e.expiresAt||void 0,permissions:e.permissions,claudeAccountId:e.claudeAccountId||void 0,geminiAccountId:e.geminiAccountId||void 0,tags:e.tags.length>0?e.tags:void 0};v.enableModelRestriction=e.enableModelRestriction,v.restrictedModels=e.restrictedModels,v.enableClientRestriction=e.enableClientRestriction,v.allowedClients=e.allowedClients;const o=await W.post("/admin/api-keys",v);o.success?(I("API Key 创建成功","success"),w("success",o.data),w("close")):I(o.message||"创建失败","error")}catch{I("创建失败","error")}finally{D.value=!1}};return(v,o)=>(i(),z(st,{to:"body"},[t("div",_t,[t("div",Tt,[t("div",Et,[o[27]||(o[27]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-key text-white"})]),t("h3",{class:"text-xl font-bold text-gray-900"},"创建新的 API Key")],-1)),t("button",{onClick:o[0]||(o[0]=d=>v.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},o[26]||(o[26]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),t("form",{onSubmit:H(j,["prevent"]),class:"space-y-4 modal-scroll-content custom-scrollbar flex-1"},[t("div",null,[o[28]||(o[28]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},[g("名称 "),t("span",{class:"text-red-500"},"*")],-1)),b(t("input",{"onUpdate:modelValue":o[1]||(o[1]=d=>e.name=d),type:"text",required:"",class:U(["form-input w-full",{"border-red-500":f.value.name}]),placeholder:"为您的 API Key 取一个名称",onInput:o[2]||(o[2]=d=>f.value.name="")},null,34),[[R,e.name]]),f.value.name?(i(),r("p",Vt,u(f.value.name),1)):$("",!0)]),t("div",null,[o[32]||(o[32]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"标签",-1)),t("div",jt,[e.tags.length>0?(i(),r("div",Ut,[(i(!0),r(_,null,T(e.tags,(d,q)=>(i(),r("span",{key:q,class:"inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"},[g(u(d)+" ",1),t("button",{type:"button",onClick:Q=>m(q),class:"ml-1 hover:text-blue-900"},o[29]||(o[29]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,qt)]))),128))])):$("",!0),t("div",Ft,[b(t("input",{"onUpdate:modelValue":o[3]||(o[3]=d=>L.value=d),type:"text",class:"form-input flex-1",placeholder:"输入新标签名称",onKeypress:et(H(n,["prevent"]),["enter"])},null,40,Ot),[[R,L.value]]),t("button",{type:"button",onClick:n,class:"px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"},o[30]||(o[30]=[t("i",{class:"fas fa-plus"},null,-1)]))]),o[31]||(o[31]=t("p",{class:"text-xs text-gray-500"},"用于标记不同团队或用途,方便筛选管理",-1))])]),t("div",Wt,[o[40]||(o[40]=t("div",{class:"flex items-center gap-2 mb-2"},[t("div",{class:"w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0"},[t("i",{class:"fas fa-tachometer-alt text-white text-xs"})]),t("h4",{class:"font-semibold text-gray-800 text-sm"},"速率限制设置 (可选)")],-1)),t("div",Yt,[t("div",zt,[t("div",null,[o[33]||(o[33]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"时间窗口 (分钟)",-1)),b(t("input",{"onUpdate:modelValue":o[4]||(o[4]=d=>e.rateLimitWindow=d),type:"number",min:"1",placeholder:"无限制",class:"form-input w-full text-sm"},null,512),[[R,e.rateLimitWindow]]),o[34]||(o[34]=t("p",{class:"text-xs text-gray-500 mt-0.5"},"时间段单位",-1))]),t("div",null,[o[35]||(o[35]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"请求次数限制",-1)),b(t("input",{"onUpdate:modelValue":o[5]||(o[5]=d=>e.rateLimitRequests=d),type:"number",min:"1",placeholder:"无限制",class:"form-input w-full text-sm"},null,512),[[R,e.rateLimitRequests]]),o[36]||(o[36]=t("p",{class:"text-xs text-gray-500 mt-0.5"},"窗口内最大请求",-1))]),t("div",null,[o[37]||(o[37]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"Token 限制",-1)),b(t("input",{"onUpdate:modelValue":o[6]||(o[6]=d=>e.tokenLimit=d),type:"number",placeholder:"无限制",class:"form-input w-full text-sm"},null,512),[[R,e.tokenLimit]]),o[38]||(o[38]=t("p",{class:"text-xs text-gray-500 mt-0.5"},"窗口内最大Token",-1))])]),o[39]||(o[39]=t("div",{class:"bg-blue-100 rounded-lg p-2"},[t("h5",{class:"text-xs font-semibold text-blue-800 mb-1"},"💡 使用示例"),t("div",{class:"text-xs text-blue-700 space-y-0.5"},[t("div",null,[t("strong",null,"示例1:"),g(" 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求")]),t("div",null,[t("strong",null,"示例2:"),g(" 时间窗口=1,Token=10000 → 每分钟最多10,000个Token")]),t("div",null,[t("strong",null,"示例3:"),g(" 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token")])])],-1))])]),t("div",null,[o[42]||(o[42]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"每日费用限制 (美元)",-1)),t("div",Nt,[t("div",Ht,[t("button",{type:"button",onClick:o[7]||(o[7]=d=>e.dailyCostLimit="50"),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"$50"),t("button",{type:"button",onClick:o[8]||(o[8]=d=>e.dailyCostLimit="100"),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"$100"),t("button",{type:"button",onClick:o[9]||(o[9]=d=>e.dailyCostLimit="200"),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"$200"),t("button",{type:"button",onClick:o[10]||(o[10]=d=>e.dailyCostLimit=""),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"自定义")]),b(t("input",{"onUpdate:modelValue":o[11]||(o[11]=d=>e.dailyCostLimit=d),type:"number",min:"0",step:"0.01",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.dailyCostLimit]]),o[41]||(o[41]=t("p",{class:"text-xs text-gray-500"},"设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制",-1))])]),t("div",null,[o[43]||(o[43]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"并发限制 (可选)",-1)),b(t("input",{"onUpdate:modelValue":o[12]||(o[12]=d=>e.concurrencyLimit=d),type:"number",min:"0",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.concurrencyLimit]]),o[44]||(o[44]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制",-1))]),t("div",null,[o[45]||(o[45]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"备注 (可选)",-1)),b(t("textarea",{"onUpdate:modelValue":o[13]||(o[13]=d=>e.description=d),rows:"2",class:"form-input w-full resize-none text-sm",placeholder:"描述此 API Key 的用途..."},null,512),[[R,e.description]])]),t("div",null,[o[47]||(o[47]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"有效期限",-1)),b(t("select",{"onUpdate:modelValue":o[14]||(o[14]=d=>e.expireDuration=d),onChange:A,class:"form-input w-full"},o[46]||(o[46]=[t("option",{value:""},"永不过期",-1),t("option",{value:"1d"},"1 天",-1),t("option",{value:"7d"},"7 天",-1),t("option",{value:"30d"},"30 天",-1),t("option",{value:"90d"},"90 天",-1),t("option",{value:"180d"},"180 天",-1),t("option",{value:"365d"},"365 天",-1),t("option",{value:"custom"},"自定义日期",-1)]),544),[[N,e.expireDuration]]),e.expireDuration==="custom"?(i(),r("div",Bt,[b(t("input",{"onUpdate:modelValue":o[15]||(o[15]=d=>e.customExpireDate=d),type:"datetime-local",class:"form-input w-full",min:V.value,onChange:c},null,40,Gt),[[R,e.customExpireDate]])])):$("",!0),e.expiresAt?(i(),r("p",Qt," 将于 "+u(y(e.expiresAt))+" 过期 ",1)):$("",!0)]),t("div",null,[o[51]||(o[51]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"服务权限",-1)),t("div",Xt,[t("label",Jt,[b(t("input",{type:"radio","onUpdate:modelValue":o[16]||(o[16]=d=>e.permissions=d),value:"all",class:"mr-2"},null,512),[[B,e.permissions]]),o[48]||(o[48]=t("span",{class:"text-sm text-gray-700"},"全部服务",-1))]),t("label",Zt,[b(t("input",{type:"radio","onUpdate:modelValue":o[17]||(o[17]=d=>e.permissions=d),value:"claude",class:"mr-2"},null,512),[[B,e.permissions]]),o[49]||(o[49]=t("span",{class:"text-sm text-gray-700"},"仅 Claude",-1))]),t("label",te,[b(t("input",{type:"radio","onUpdate:modelValue":o[18]||(o[18]=d=>e.permissions=d),value:"gemini",class:"mr-2"},null,512),[[B,e.permissions]]),o[50]||(o[50]=t("span",{class:"text-sm text-gray-700"},"仅 Gemini",-1))])]),o[52]||(o[52]=t("p",{class:"text-xs text-gray-500 mt-2"},"控制此 API Key 可以访问哪些服务",-1))]),t("div",null,[o[57]||(o[57]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"专属账号绑定 (可选)",-1)),t("div",ee,[t("div",null,[o[54]||(o[54]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Claude 专属账号",-1)),b(t("select",{"onUpdate:modelValue":o[19]||(o[19]=d=>e.claudeAccountId=d),class:"form-input w-full",disabled:e.permissions==="gemini"},[o[53]||(o[53]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.claude.filter(d=>d.isDedicated),d=>(i(),r("option",{key:d.id,value:d.id},u(d.name)+" ("+u(d.status==="active"?"正常":"异常")+") ",9,le))),128))],8,se),[[N,e.claudeAccountId]])]),t("div",null,[o[56]||(o[56]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Gemini 专属账号",-1)),b(t("select",{"onUpdate:modelValue":o[20]||(o[20]=d=>e.geminiAccountId=d),class:"form-input w-full",disabled:e.permissions==="claude"},[o[55]||(o[55]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.gemini.filter(d=>d.isDedicated),d=>(i(),r("option",{key:d.id,value:d.id},u(d.name)+" ("+u(d.status==="active"?"正常":"异常")+") ",9,ne))),128))],8,oe),[[N,e.geminiAccountId]])])]),o[58]||(o[58]=t("p",{class:"text-xs text-gray-500 mt-2"},"选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池",-1))]),t("div",null,[t("div",ie,[b(t("input",{type:"checkbox","onUpdate:modelValue":o[21]||(o[21]=d=>e.enableModelRestriction=d),id:"enableModelRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableModelRestriction]]),o[59]||(o[59]=t("label",{for:"enableModelRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用模型限制 ",-1))]),e.enableModelRestriction?(i(),r("div",ae,[t("div",null,[o[62]||(o[62]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"限制的模型列表",-1)),t("div",re,[(i(!0),r(_,null,T(e.restrictedModels,(d,q)=>(i(),r("span",{key:q,class:"inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800"},[g(u(d)+" ",1),t("button",{type:"button",onClick:Q=>K(q),class:"ml-1 text-red-600 hover:text-red-800"},o[60]||(o[60]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,de)]))),128)),e.restrictedModels.length===0?(i(),r("span",ue," 暂无限制的模型 ")):$("",!0)]),t("div",me,[b(t("input",{"onUpdate:modelValue":o[22]||(o[22]=d=>e.modelInput=d),onKeydown:et(H(S,["prevent"]),["enter"]),type:"text",placeholder:"输入模型名称,按回车添加",class:"form-input flex-1 text-sm"},null,40,pe),[[R,e.modelInput]]),t("button",{type:"button",onClick:S,class:"px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"},o[61]||(o[61]=[t("i",{class:"fas fa-plus"},null,-1)]))]),o[63]||(o[63]=t("p",{class:"text-xs text-gray-500 mt-1"},"例如:claude-opus-4-20250514",-1))])])):$("",!0)]),t("div",null,[t("div",ce,[b(t("input",{type:"checkbox","onUpdate:modelValue":o[23]||(o[23]=d=>e.enableClientRestriction=d),id:"enableClientRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableClientRestriction]]),o[64]||(o[64]=t("label",{for:"enableClientRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用客户端限制 ",-1))]),e.enableClientRestriction?(i(),r("div",xe,[t("div",null,[o[65]||(o[65]=t("label",{class:"block text-xs font-medium text-gray-700 mb-2"},"允许的客户端",-1)),t("div",fe,[(i(!0),r(_,null,T(h.value,d=>(i(),r("div",{key:d.id,class:"flex items-start"},[b(t("input",{type:"checkbox",id:`client_${d.id}`,value:d.id,"onUpdate:modelValue":o[24]||(o[24]=q=>e.allowedClients=q),class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"},null,8,ge),[[G,e.allowedClients]]),t("label",{for:`client_${d.id}`,class:"ml-2 flex-1 cursor-pointer"},[t("span",be,u(d.name),1),t("span",ve,u(d.description),1)],8,ye)]))),128))])])])):$("",!0)]),t("div",we,[t("button",{type:"button",onClick:o[25]||(o[25]=d=>v.$emit("close")),class:"flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"}," 取消 "),t("button",{type:"submit",disabled:D.value,class:"btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"},[D.value?(i(),r("div",Ce)):(i(),r("i",Ae)),g(" "+u(D.value?"创建中...":"创建"),1)],8,$e)])],32)])])]))}},ke=J(Ke,[["__scopeId","data-v-060c3f39"]]),Le={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},De={class:"modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col"},Ie={class:"flex items-center justify-between mb-6"},he=["value"],Me={class:"space-y-3"},Re={key:0,class:"flex flex-wrap gap-2"},Se=["onClick"],Pe={class:"flex gap-2"},_e=["onKeypress"],Te={class:"bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-4"},Ee={class:"space-y-3"},Ve={class:"flex gap-2"},je={class:"flex gap-4"},Ue={class:"flex items-center cursor-pointer"},qe={class:"flex items-center cursor-pointer"},Fe={class:"flex items-center cursor-pointer"},Oe={class:"grid grid-cols-1 gap-3"},We=["disabled"],Ye=["value"],ze=["disabled"],Ne=["value"],He={class:"flex items-center mb-3"},Be={key:0,class:"space-y-3"},Ge={class:"flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200"},Qe=["onClick"],Xe={key:0,class:"text-gray-400 text-sm"},Je={class:"flex gap-2"},Ze=["onKeydown"],ts={class:"flex items-center mb-3"},es={key:0,class:"space-y-3"},ss={class:"space-y-2"},ls=["id","value"],os=["for"],ns={class:"text-sm font-medium text-gray-700"},is={class:"text-xs text-gray-500 block"},as={class:"flex gap-3 pt-4"},rs=["disabled"],ds={key:0,class:"loading-spinner mr-2"},us={key:1,class:"fas fa-save mr-2"},ms={__name:"EditApiKeyModal",props:{apiKey:{type:Object,required:!0},accounts:{type:Object,default:()=>({claude:[],gemini:[]})}},emits:["close","success"],setup(k,{emit:O}){const w=k,P=O;dt();const D=ut(),f=C(!1),L=C([]),h=C(""),e=it({name:"",tokenLimit:"",rateLimitWindow:"",rateLimitRequests:"",concurrencyLimit:"",dailyCostLimit:"",permissions:"all",claudeAccountId:"",geminiAccountId:"",enableModelRestriction:!1,restrictedModels:[],modelInput:"",enableClientRestriction:!1,allowedClients:[],tags:[]}),V=()=>{e.modelInput&&!e.restrictedModels.includes(e.modelInput)&&(e.restrictedModels.push(e.modelInput),e.modelInput="")},A=K=>{e.restrictedModels.splice(K,1)},c=()=>{if(h.value&&h.value.trim()){const K=h.value.trim();e.tags.includes(K)||e.tags.push(K),h.value=""}},y=K=>{e.tags.splice(K,1)},S=async()=>{f.value=!0;try{const K={tokenLimit:e.tokenLimit!==""&&e.tokenLimit!==null?parseInt(e.tokenLimit):0,rateLimitWindow:e.rateLimitWindow!==""&&e.rateLimitWindow!==null?parseInt(e.rateLimitWindow):0,rateLimitRequests:e.rateLimitRequests!==""&&e.rateLimitRequests!==null?parseInt(e.rateLimitRequests):0,concurrencyLimit:e.concurrencyLimit!==""&&e.concurrencyLimit!==null?parseInt(e.concurrencyLimit):0,dailyCostLimit:e.dailyCostLimit!==""&&e.dailyCostLimit!==null?parseFloat(e.dailyCostLimit):0,permissions:e.permissions,claudeAccountId:e.claudeAccountId||null,geminiAccountId:e.geminiAccountId||null,tags:e.tags};K.enableModelRestriction=e.enableModelRestriction,K.restrictedModels=e.restrictedModels,K.enableClientRestriction=e.enableClientRestriction,K.allowedClients=e.allowedClients;const n=await W.put(`/admin/api-keys/${w.apiKey.id}`,K);n.success?(P("success"),P("close")):I(n.message||"更新失败","error")}catch{I("更新失败","error")}finally{f.value=!1}};return at(async()=>{L.value=await D.loadSupportedClients(),e.name=w.apiKey.name,e.tokenLimit=w.apiKey.tokenLimit||"",e.rateLimitWindow=w.apiKey.rateLimitWindow||"",e.rateLimitRequests=w.apiKey.rateLimitRequests||"",e.concurrencyLimit=w.apiKey.concurrencyLimit||"",e.dailyCostLimit=w.apiKey.dailyCostLimit||"",e.permissions=w.apiKey.permissions||"all",e.claudeAccountId=w.apiKey.claudeAccountId||"",e.geminiAccountId=w.apiKey.geminiAccountId||"",e.restrictedModels=w.apiKey.restrictedModels||[],e.allowedClients=w.apiKey.allowedClients||[],e.tags=w.apiKey.tags||[],e.enableModelRestriction=e.restrictedModels.length>0,e.enableClientRestriction=e.allowedClients.length>0}),(K,n)=>(i(),z(st,{to:"body"},[t("div",Le,[t("div",De,[t("div",Ie,[n[22]||(n[22]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-edit text-white"})]),t("h3",{class:"text-xl font-bold text-gray-900"},"编辑 API Key")],-1)),t("button",{onClick:n[0]||(n[0]=m=>K.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},n[21]||(n[21]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),t("form",{onSubmit:H(S,["prevent"]),class:"space-y-6 modal-scroll-content custom-scrollbar flex-1"},[t("div",null,[n[23]||(n[23]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"名称",-1)),t("input",{value:e.name,type:"text",disabled:"",class:"form-input w-full bg-gray-100 cursor-not-allowed"},null,8,he),n[24]||(n[24]=t("p",{class:"text-xs text-gray-500 mt-2"},"名称不可修改",-1))]),t("div",null,[n[28]||(n[28]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"标签",-1)),t("div",Me,[e.tags.length>0?(i(),r("div",Re,[(i(!0),r(_,null,T(e.tags,(m,j)=>(i(),r("span",{key:j,class:"inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"},[g(u(m)+" ",1),t("button",{type:"button",onClick:v=>y(j),class:"ml-1 hover:text-blue-900"},n[25]||(n[25]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,Se)]))),128))])):$("",!0),t("div",Pe,[b(t("input",{"onUpdate:modelValue":n[1]||(n[1]=m=>h.value=m),type:"text",class:"form-input flex-1",placeholder:"输入新标签名称",onKeypress:et(H(c,["prevent"]),["enter"])},null,40,_e),[[R,h.value]]),t("button",{type:"button",onClick:c,class:"px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"},n[26]||(n[26]=[t("i",{class:"fas fa-plus"},null,-1)]))]),n[27]||(n[27]=t("p",{class:"text-xs text-gray-500"},"用于标记不同团队或用途,方便筛选管理",-1))])]),t("div",Te,[n[35]||(n[35]=t("div",{class:"flex items-start gap-3 mb-3"},[t("div",{class:"w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0"},[t("i",{class:"fas fa-tachometer-alt text-white text-sm"})]),t("div",{class:"flex-1"},[t("h4",{class:"font-semibold text-gray-800 mb-1"},"速率限制设置"),t("p",{class:"text-sm text-gray-600"},"控制 API Key 的使用频率和资源消耗")])],-1)),t("div",null,[n[29]||(n[29]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"时间窗口 (分钟)",-1)),b(t("input",{"onUpdate:modelValue":n[2]||(n[2]=m=>e.rateLimitWindow=m),type:"number",min:"1",placeholder:"留空表示无限制",class:"form-input w-full"},null,512),[[R,e.rateLimitWindow]]),n[30]||(n[30]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置一个时间段(以分钟为单位),用于计算速率限制",-1))]),t("div",null,[n[31]||(n[31]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"时间窗口内的请求次数限制",-1)),b(t("input",{"onUpdate:modelValue":n[3]||(n[3]=m=>e.rateLimitRequests=m),type:"number",min:"1",placeholder:"留空表示无限制",class:"form-input w-full"},null,512),[[R,e.rateLimitRequests]]),n[32]||(n[32]=t("p",{class:"text-xs text-gray-500 mt-2"},"在时间窗口内允许的最大请求次数(需要先设置时间窗口)",-1))]),t("div",null,[n[33]||(n[33]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"时间窗口内的 Token 使用量限制",-1)),b(t("input",{"onUpdate:modelValue":n[4]||(n[4]=m=>e.tokenLimit=m),type:"number",min:"0",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.tokenLimit]]),n[34]||(n[34]=t("p",{class:"text-xs text-gray-500 mt-2"},"在时间窗口内允许消耗的最大 Token 数量(需要先设置时间窗口),0 或留空表示无限制",-1))]),n[36]||(n[36]=t("div",{class:"bg-blue-100 rounded-lg p-3 mt-3"},[t("h5",{class:"text-sm font-semibold text-blue-800 mb-2"},"💡 使用示例"),t("div",{class:"text-xs text-blue-700 space-y-1"},[t("p",null,[t("strong",null,"示例1:"),g(" 时间窗口=60,请求次数限制=100")]),t("p",{class:"ml-4"},"→ 每60分钟内最多允许100次请求"),t("p",{class:"mt-2"},[t("strong",null,"示例2:"),g(" 时间窗口=10,Token限制=50000")]),t("p",{class:"ml-4"},"→ 每10分钟内最多消耗50,000个Token"),t("p",{class:"mt-2"},[t("strong",null,"示例3:"),g(" 时间窗口=30,请求次数限制=50,Token限制=100000")]),t("p",{class:"ml-4"},"→ 每30分钟内最多50次请求且总Token不超过100,000")])],-1))]),t("div",null,[n[38]||(n[38]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"每日费用限制 (美元)",-1)),t("div",Ee,[t("div",Ve,[t("button",{type:"button",onClick:n[5]||(n[5]=m=>e.dailyCostLimit="50"),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"$50"),t("button",{type:"button",onClick:n[6]||(n[6]=m=>e.dailyCostLimit="100"),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"$100"),t("button",{type:"button",onClick:n[7]||(n[7]=m=>e.dailyCostLimit="200"),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"$200"),t("button",{type:"button",onClick:n[8]||(n[8]=m=>e.dailyCostLimit=""),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"自定义")]),b(t("input",{"onUpdate:modelValue":n[9]||(n[9]=m=>e.dailyCostLimit=m),type:"number",min:"0",step:"0.01",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.dailyCostLimit]]),n[37]||(n[37]=t("p",{class:"text-xs text-gray-500"},"设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制",-1))])]),t("div",null,[n[39]||(n[39]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"并发限制",-1)),b(t("input",{"onUpdate:modelValue":n[10]||(n[10]=m=>e.concurrencyLimit=m),type:"number",min:"0",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.concurrencyLimit]]),n[40]||(n[40]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置此 API Key 可同时处理的最大请求数",-1))]),t("div",null,[n[44]||(n[44]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"服务权限",-1)),t("div",je,[t("label",Ue,[b(t("input",{type:"radio","onUpdate:modelValue":n[11]||(n[11]=m=>e.permissions=m),value:"all",class:"mr-2"},null,512),[[B,e.permissions]]),n[41]||(n[41]=t("span",{class:"text-sm text-gray-700"},"全部服务",-1))]),t("label",qe,[b(t("input",{type:"radio","onUpdate:modelValue":n[12]||(n[12]=m=>e.permissions=m),value:"claude",class:"mr-2"},null,512),[[B,e.permissions]]),n[42]||(n[42]=t("span",{class:"text-sm text-gray-700"},"仅 Claude",-1))]),t("label",Fe,[b(t("input",{type:"radio","onUpdate:modelValue":n[13]||(n[13]=m=>e.permissions=m),value:"gemini",class:"mr-2"},null,512),[[B,e.permissions]]),n[43]||(n[43]=t("span",{class:"text-sm text-gray-700"},"仅 Gemini",-1))])]),n[45]||(n[45]=t("p",{class:"text-xs text-gray-500 mt-2"},"控制此 API Key 可以访问哪些服务",-1))]),t("div",null,[n[50]||(n[50]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"专属账号绑定",-1)),t("div",Oe,[t("div",null,[n[47]||(n[47]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Claude 专属账号",-1)),b(t("select",{"onUpdate:modelValue":n[14]||(n[14]=m=>e.claudeAccountId=m),class:"form-input w-full",disabled:e.permissions==="gemini"},[n[46]||(n[46]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.claude,m=>(i(),r("option",{key:m.id,value:m.id},u(m.name)+" ("+u(m.status==="active"?"正常":"异常")+") ",9,Ye))),128))],8,We),[[N,e.claudeAccountId]])]),t("div",null,[n[49]||(n[49]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Gemini 专属账号",-1)),b(t("select",{"onUpdate:modelValue":n[15]||(n[15]=m=>e.geminiAccountId=m),class:"form-input w-full",disabled:e.permissions==="claude"},[n[48]||(n[48]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.gemini,m=>(i(),r("option",{key:m.id,value:m.id},u(m.name)+" ("+u(m.status==="active"?"正常":"异常")+") ",9,Ne))),128))],8,ze),[[N,e.geminiAccountId]])])]),n[51]||(n[51]=t("p",{class:"text-xs text-gray-500 mt-2"},"修改绑定账号将影响此API Key的请求路由",-1))]),t("div",null,[t("div",He,[b(t("input",{type:"checkbox","onUpdate:modelValue":n[16]||(n[16]=m=>e.enableModelRestriction=m),id:"editEnableModelRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableModelRestriction]]),n[52]||(n[52]=t("label",{for:"editEnableModelRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用模型限制 ",-1))]),e.enableModelRestriction?(i(),r("div",Be,[t("div",null,[n[55]||(n[55]=t("label",{class:"block text-sm font-medium text-gray-600 mb-2"},"限制的模型列表",-1)),t("div",Ge,[(i(!0),r(_,null,T(e.restrictedModels,(m,j)=>(i(),r("span",{key:j,class:"inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"},[g(u(m)+" ",1),t("button",{type:"button",onClick:v=>A(j),class:"ml-2 text-red-600 hover:text-red-800"},n[53]||(n[53]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,Qe)]))),128)),e.restrictedModels.length===0?(i(),r("span",Xe," 暂无限制的模型 ")):$("",!0)]),t("div",Je,[b(t("input",{"onUpdate:modelValue":n[17]||(n[17]=m=>e.modelInput=m),onKeydown:et(H(V,["prevent"]),["enter"]),type:"text",placeholder:"输入模型名称,按回车添加",class:"form-input flex-1"},null,40,Ze),[[R,e.modelInput]]),t("button",{type:"button",onClick:V,class:"px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"},n[54]||(n[54]=[t("i",{class:"fas fa-plus"},null,-1)]))]),n[56]||(n[56]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置此API Key无法访问的模型,例如:claude-opus-4-20250514",-1))])])):$("",!0)]),t("div",null,[t("div",ts,[b(t("input",{type:"checkbox","onUpdate:modelValue":n[18]||(n[18]=m=>e.enableClientRestriction=m),id:"editEnableClientRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableClientRestriction]]),n[57]||(n[57]=t("label",{for:"editEnableClientRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用客户端限制 ",-1))]),e.enableClientRestriction?(i(),r("div",es,[t("div",null,[n[58]||(n[58]=t("label",{class:"block text-sm font-medium text-gray-600 mb-2"},"允许的客户端",-1)),n[59]||(n[59]=t("p",{class:"text-xs text-gray-500 mb-3"},"勾选允许使用此API Key的客户端",-1)),t("div",ss,[(i(!0),r(_,null,T(L.value,m=>(i(),r("div",{key:m.id,class:"flex items-start"},[b(t("input",{type:"checkbox",id:`edit_client_${m.id}`,value:m.id,"onUpdate:modelValue":n[19]||(n[19]=j=>e.allowedClients=j),class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"},null,8,ls),[[G,e.allowedClients]]),t("label",{for:`edit_client_${m.id}`,class:"ml-2 flex-1 cursor-pointer"},[t("span",ns,u(m.name),1),t("span",is,u(m.description),1)],8,os)]))),128))])])])):$("",!0)]),t("div",as,[t("button",{type:"button",onClick:n[20]||(n[20]=m=>K.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),t("button",{type:"submit",disabled:f.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[f.value?(i(),r("div",ds)):(i(),r("i",us)),g(" "+u(f.value?"保存中...":"保存修改"),1)],8,rs)])],32)])])]))}},ps=J(ms,[["__scopeId","data-v-35ae96db"]]),cs={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},xs={class:"modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col"},fs={class:"flex items-center justify-between mb-6"},gs={class:"space-y-6 modal-scroll-content custom-scrollbar flex-1"},ys={class:"bg-blue-50 border border-blue-200 rounded-lg p-4"},bs={class:"flex items-start gap-3"},vs={class:"text-sm text-gray-700"},ws={class:"text-xs text-gray-600 mt-1"},$s={key:0,class:"mt-3"},Cs=["min"],As={key:1,class:"text-xs text-gray-500 mt-2"},Ks={class:"flex gap-3 pt-4"},ks=["disabled"],Ls={key:0,class:"loading-spinner mr-2"},Ds={key:1,class:"fas fa-clock mr-2"},Is={__name:"RenewApiKeyModal",props:{apiKey:{type:Object,required:!0}},emits:["close","success"],setup(k,{emit:O}){const w=k,P=O;dt();const D=C(!1),f=it({renewDuration:"30d",customExpireDate:"",newExpiresAt:null}),L=rt(()=>{const c=new Date;return w.apiKey.expiresAt&&new Date(w.apiKey.expiresAt)>c?new Date(w.apiKey.expiresAt).toISOString().slice(0,16):(c.setMinutes(c.getMinutes()+1),c.toISOString().slice(0,16))}),h=c=>new Date(c).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),e=()=>{if(!f.renewDuration){f.newExpiresAt=null;return}if(f.renewDuration==="permanent"){f.newExpiresAt=null;return}if(f.renewDuration==="custom")return;const c=w.apiKey.expiresAt&&new Date(w.apiKey.expiresAt)>new Date?new Date(w.apiKey.expiresAt):new Date,S=f.renewDuration.match(/(\d+)([dhmy])/);if(S){const[,K,n]=S,m=parseInt(K);switch(n){case"d":c.setDate(c.getDate()+m);break;case"h":c.setHours(c.getHours()+m);break;case"m":c.setMonth(c.getMonth()+m);break;case"y":c.setFullYear(c.getFullYear()+m);break}f.newExpiresAt=c.toISOString()}},V=()=>{f.customExpireDate&&(f.newExpiresAt=new Date(f.customExpireDate).toISOString())},A=async()=>{D.value=!0;try{const c={expiresAt:f.renewDuration==="permanent"?null:f.newExpiresAt},y=await W.put(`/admin/api-keys/${w.apiKey.id}/renew`,c);y.success?(I("API Key 续期成功","success"),P("success"),P("close")):I(y.message||"续期失败","error")}catch{I("续期失败","error")}finally{D.value=!1}};return e(),(c,y)=>(i(),z(st,{to:"body"},[t("div",cs,[t("div",xs,[t("div",fs,[y[5]||(y[5]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-clock text-white"})]),t("h3",{class:"text-xl font-bold text-gray-900"},"续期 API Key")],-1)),t("button",{onClick:y[0]||(y[0]=S=>c.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},y[4]||(y[4]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),t("div",gs,[t("div",ys,[t("div",bs,[y[7]||(y[7]=t("div",{class:"w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0"},[t("i",{class:"fas fa-info text-white text-sm"})],-1)),t("div",null,[y[6]||(y[6]=t("h4",{class:"font-semibold text-gray-800 mb-1"},"API Key 信息",-1)),t("p",vs,u(k.apiKey.name),1),t("p",ws," 当前过期时间:"+u(k.apiKey.expiresAt?h(k.apiKey.expiresAt):"永不过期"),1)])])]),t("div",null,[y[9]||(y[9]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"续期时长",-1)),b(t("select",{"onUpdate:modelValue":y[1]||(y[1]=S=>f.renewDuration=S),onChange:e,class:"form-input w-full"},y[8]||(y[8]=[t("option",{value:"7d"},"延长 7 天",-1),t("option",{value:"30d"},"延长 30 天",-1),t("option",{value:"90d"},"延长 90 天",-1),t("option",{value:"180d"},"延长 180 天",-1),t("option",{value:"365d"},"延长 365 天",-1),t("option",{value:"custom"},"自定义日期",-1),t("option",{value:"permanent"},"设为永不过期",-1)]),544),[[N,f.renewDuration]]),f.renewDuration==="custom"?(i(),r("div",$s,[b(t("input",{"onUpdate:modelValue":y[2]||(y[2]=S=>f.customExpireDate=S),type:"datetime-local",class:"form-input w-full",min:L.value,onChange:V},null,40,Cs),[[R,f.customExpireDate]])])):$("",!0),f.newExpiresAt?(i(),r("p",As," 新的过期时间:"+u(h(f.newExpiresAt)),1)):$("",!0)])]),t("div",Ks,[t("button",{type:"button",onClick:y[3]||(y[3]=S=>c.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),t("button",{type:"button",onClick:A,disabled:D.value||!f.renewDuration,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[D.value?(i(),r("div",Ls)):(i(),r("i",Ds)),g(" "+u(D.value?"续期中...":"确认续期"),1)],8,ks)])])])]))}},hs=J(Is,[["__scopeId","data-v-5486ec2f"]]),Ms={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},Rs={class:"modal-content w-full max-w-lg p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar"},Ss={class:"space-y-4 mb-6"},Ps={class:"p-3 bg-gray-50 rounded-lg border"},_s={class:"text-gray-900 font-medium"},Ts={key:0},Es={class:"p-3 bg-gray-50 rounded-lg border"},Vs={class:"text-gray-700"},js={class:"relative"},Us={class:"p-4 pr-14 bg-gray-900 rounded-lg border font-mono text-sm text-white break-all min-h-[60px] flex items-center"},qs={class:"absolute top-3 right-3"},Fs=["title"],Os={__name:"NewApiKeyModal",props:{apiKey:{type:Object,required:!0}},emits:["close"],setup(k,{emit:O}){const w=k,P=O,D=C(!1),f=()=>{D.value=!D.value},L=()=>{const A=w.apiKey.apiKey||w.apiKey.key||"";return A?D.value||A.length<=12?A:A.substring(0,8)+"●".repeat(Math.max(0,A.length-12))+A.substring(A.length-4):""},h=async()=>{const A=w.apiKey.apiKey||w.apiKey.key||"";if(!A){I("API Key 不存在","error");return}try{await navigator.clipboard.writeText(A),I("API Key 已复制到剪贴板","success")}catch(c){console.error("Failed to copy:",c);const y=document.createElement("textarea");y.value=A,document.body.appendChild(y),y.select();try{document.execCommand("copy"),I("API Key 已复制到剪贴板","success")}catch{I("复制失败,请手动复制","error")}finally{document.body.removeChild(y)}}},e=async()=>{window.showConfirm?await window.showConfirm("关闭提醒",`关闭后将无法再次查看完整的API Key,请确保已经妥善保存。 - -确定要关闭吗?`,"确定关闭","取消")&&P("close"):confirm(`关闭后将无法再次查看完整的API Key,请确保已经妥善保存。 - -确定要关闭吗?`)&&P("close")},V=async()=>{window.showConfirm?await window.showConfirm("确定要关闭吗?",`您还没有保存API Key,关闭后将无法再次查看。 - -建议您先复制API Key再关闭。`,"仍然关闭","返回复制")&&P("close"):confirm(`您还没有保存API Key,关闭后将无法再次查看。 - -确定要关闭吗?`)&&P("close")};return(A,c)=>(i(),z(st,{to:"body"},[t("div",Ms,[t("div",Rs,[t("div",{class:"flex items-center justify-between mb-6"},[c[1]||(c[1]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-check text-white text-lg"})]),t("div",null,[t("h3",{class:"text-xl font-bold text-gray-900"},"API Key 创建成功"),t("p",{class:"text-sm text-gray-600"},"请妥善保存您的 API Key")])],-1)),t("button",{onClick:V,class:"text-gray-400 hover:text-gray-600 transition-colors",title:"直接关闭(不推荐)"},c[0]||(c[0]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),c[7]||(c[7]=t("div",{class:"bg-amber-50 border-l-4 border-amber-400 p-4 mb-6"},[t("div",{class:"flex items-start"},[t("div",{class:"w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5"},[t("i",{class:"fas fa-exclamation-triangle text-white text-sm"})]),t("div",{class:"ml-3"},[t("h5",{class:"font-semibold text-amber-900 mb-1"},"重要提醒"),t("p",{class:"text-sm text-amber-800"}," 这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。 ")])])],-1)),t("div",Ss,[t("div",null,[c[2]||(c[2]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"API Key 名称",-1)),t("div",Ps,[t("span",_s,u(k.apiKey.name),1)])]),k.apiKey.description?(i(),r("div",Ts,[c[3]||(c[3]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"备注",-1)),t("div",Es,[t("span",Vs,u(k.apiKey.description||"无描述"),1)])])):$("",!0),t("div",null,[c[4]||(c[4]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"API Key",-1)),t("div",js,[t("div",Us,u(L()),1),t("div",qs,[t("button",{onClick:f,type:"button",class:"btn-icon-sm hover:bg-gray-800 bg-gray-700",title:D.value?"隐藏API Key":"显示完整API Key"},[t("i",{class:U(["fas",D.value?"fa-eye-slash":"fa-eye","text-gray-300"])},null,2)],8,Fs)])]),c[5]||(c[5]=t("p",{class:"text-xs text-gray-500 mt-2"}," 点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key ",-1))])]),t("div",{class:"flex gap-3"},[t("button",{onClick:h,class:"flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"},c[6]||(c[6]=[t("i",{class:"fas fa-copy"},null,-1),g(" 复制 API Key ",-1)])),t("button",{onClick:e,class:"px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"}," 我已保存 ")])])])]))}},Ws=J(Os,[["__scopeId","data-v-3d91f349"]]),Ys={class:"tab-content"},zs={class:"card p-6"},Ns={class:"flex flex-col md:flex-row justify-between items-center gap-4 mb-6"},Hs={class:"flex items-center gap-3"},Bs=["value"],Gs={key:0,class:"text-center py-12"},Qs={key:1,class:"text-center py-12"},Xs={key:2,class:"table-container"},Js={class:"min-w-full"},Zs={class:"bg-gray-50/80 backdrop-blur-sm"},tl={key:1,class:"fas fa-sort ml-1 text-gray-400"},el={key:1,class:"fas fa-sort ml-1 text-gray-400"},sl={class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},ll={key:1,class:"fas fa-sort ml-1 text-gray-400"},ol={key:1,class:"fas fa-sort ml-1 text-gray-400"},nl={key:1,class:"fas fa-sort ml-1 text-gray-400"},il={class:"divide-y divide-gray-200/50"},al={class:"table-row"},rl={class:"px-6 py-4 whitespace-nowrap"},dl={class:"flex items-center"},ul={class:"text-sm font-semibold text-gray-900"},ml={class:"text-xs text-gray-500"},pl={class:"text-xs text-gray-500 mt-1"},cl={key:0},xl={key:1},fl={class:"px-6 py-4"},gl={class:"flex flex-wrap gap-1"},yl={key:0,class:"text-xs text-gray-400"},bl={class:"px-6 py-4 whitespace-nowrap"},vl={class:"text-sm font-mono text-gray-600 bg-gray-50 px-3 py-1 rounded-lg"},wl={class:"px-6 py-4 whitespace-nowrap"},$l={class:"px-6 py-4"},Cl={class:"space-y-1"},Al={class:"flex justify-between text-sm"},Kl={class:"font-medium text-gray-900"},kl={class:"flex justify-between text-sm"},Ll={class:"font-medium text-gray-900"},Dl={class:"flex justify-between text-sm"},Il={class:"font-medium text-green-600"},hl={key:0,class:"flex justify-between text-sm"},Ml={class:"flex justify-between text-sm"},Rl={class:"font-medium text-purple-600"},Sl={class:"flex justify-between text-sm"},Pl={key:0,class:"text-xs text-gray-500"},_l={key:1,class:"flex justify-between text-sm"},Tl={class:"font-medium text-indigo-600"},El={key:2,class:"flex justify-between text-sm"},Vl={class:"font-medium text-indigo-600"},jl={class:"flex justify-between text-xs text-gray-500"},Ul={key:3,class:"flex justify-between text-xs text-orange-500"},ql={class:"flex justify-between text-xs text-blue-600"},Fl={class:"pt-1 border-t border-gray-100"},Ol={class:"flex justify-between text-xs text-green-600"},Wl={class:"pt-2"},Yl=["onClick"],zl={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Nl={class:"px-6 py-4 whitespace-nowrap text-sm"},Hl={key:0},Bl={key:0,class:"text-red-600"},Gl={key:1,class:"text-orange-600"},Ql={key:2,class:"text-gray-600"},Xl={key:1,class:"text-gray-400"},Jl={class:"px-6 py-4 whitespace-nowrap text-sm"},Zl={class:"flex gap-2"},to=["onClick"],eo=["onClick"],so=["onClick"],lo=["onClick"],oo={key:0},no={colspan:"7",class:"px-6 py-4 bg-gray-50"},io={key:0,class:"text-center py-4"},ao={class:"space-y-4"},ro={class:"flex items-center justify-between mb-4"},uo={class:"flex items-center gap-2"},mo={key:0,class:"text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full"},po={class:"flex gap-1 items-center"},co={class:"flex gap-1 bg-gray-100 rounded p-1"},xo=["onClick"],fo={key:0,class:"text-center py-8"},go={class:"flex items-center justify-center gap-2 mb-3"},yo=["onClick"],bo={key:1,class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"},vo={class:"flex justify-between items-start mb-3"},wo={class:"flex-1"},$o={class:"text-sm font-semibold text-gray-800 block mb-1"},Co={class:"text-xs text-gray-500 bg-blue-50 px-2 py-1 rounded-full"},Ao={class:"space-y-2 mb-3"},Ko={class:"flex justify-between items-center text-sm"},ko={class:"font-semibold text-gray-900"},Lo={class:"flex justify-between items-center text-sm"},Do={class:"font-semibold text-green-600"},Io={class:"pt-2 mt-2 border-t border-gray-100"},ho={class:"flex justify-between items-center text-xs text-gray-500"},Mo={class:"font-medium"},Ro={class:"flex justify-between items-center text-xs text-gray-500"},So={class:"font-medium"},Po={key:0,class:"flex justify-between items-center text-xs text-purple-600"},_o={class:"font-medium"},To={key:1,class:"flex justify-between items-center text-xs text-purple-600"},Eo={class:"font-medium"},Vo={class:"w-full bg-gray-200 rounded-full h-2 mt-3"},jo={class:"text-right mt-1"},Uo={class:"text-xs font-medium text-indigo-600"},qo={key:2,class:"mt-4 p-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border border-indigo-100"},Fo={class:"flex items-center justify-between text-sm"},Oo={class:"flex gap-4 text-xs"},Wo={class:"text-gray-600"},Yo={class:"font-semibold text-gray-800"},zo={class:"text-gray-600"},No={class:"font-semibold text-gray-800"},Ho={__name:"ApiKeysView",setup(k){const O=ut(),w=C([]),P=C(!1),D=C("today"),f=C(""),L=C("asc"),h=C({}),e=C({}),V=C({}),A=C([new Date(2e3,1,1,0,0,0),new Date(2e3,2,1,23,59,59)]),c=C({claude:[],gemini:[]}),y=C(""),S=C([]),K=C(!1),n=C(!1),m=C(!1),j=C(!1),v=C(null),o=C(null),d=C(null),q=rt(()=>{let a=w.value;return y.value&&(a=w.value.filter(x=>x.tags&&x.tags.includes(y.value))),f.value?[...a].sort((x,l)=>{let p=x[f.value],M=l[f.value];return f.value==="status"?(p=x.isActive?1:0,M=l.isActive?1:0):f.value==="cost"?(p=parseFloat(lt(x.usage).replace("$","")),M=parseFloat(lt(l.usage).replace("$",""))):(f.value==="createdAt"||f.value==="expiresAt")&&(p=p?new Date(p).getTime():0,M=M?new Date(M).getTime():0),pM?L.value==="asc"?1:-1:0}):a}),Q=async()=>{try{const[a,s]=await Promise.all([W.get("/admin/claude-accounts"),W.get("/admin/gemini-accounts")]);a.success&&(c.value.claude=a.data||[]),s.success&&(c.value.gemini=s.data||[])}catch(a){console.error("加载账户列表失败:",a)}},F=async()=>{P.value=!0;try{const a=await W.get(`/admin/api-keys?timeRange=${D.value}`);if(a.success){w.value=a.data||[];const s=new Set;w.value.forEach(x=>{x.tags&&Array.isArray(x.tags)&&x.tags.forEach(l=>s.add(l))}),S.value=Array.from(s).sort()}}catch{I("加载 API Keys 失败","error")}finally{P.value=!1}},X=a=>{f.value===a?L.value=L.value==="asc"?"desc":"asc":(f.value=a,L.value="asc")},E=a=>!a&&a!==0?"0":a.toLocaleString("zh-CN"),lt=a=>!a||!a.total?"$0.0000":`$${(a.total.cost||0).toFixed(4)}`,gt=a=>{if(!a)return"未知账户";const s=c.value.claude.find(l=>l.id===a);if(s)return s.name;const x=c.value.gemini.find(l=>l.id===a);return x?x.name:`账户-${a.substring(0,8)}`},ot=a=>a?new Date(a)!a||ot(a)?!1:(new Date(a)-new Date)/(1e3*60*60*24)<=7,pt=a=>a?new Date(a).toLocaleDateString("zh-CN"):"",yt=async a=>{h.value[a]?h.value[a]=!1:(h.value[a]=!0,V.value[a]||xt(a),await Z(a,!0))},Z=async(a,s=!1)=>{if(!s&&e.value[a]&&e.value[a].length>0)return;const x=Y(a);try{let l=`/admin/api-keys/${a}/model-stats`;const p=new URLSearchParams;if(x.customStart&&x.customEnd)p.append("startDate",x.customStart),p.append("endDate",x.customEnd),p.append("period","custom");else{const tt=x.preset==="today"?"daily":"monthly";p.append("period",tt)}l+="?"+p.toString();const M=await W.get(l);M.success&&(e.value[a]=M.data||[])}catch{I("加载模型统计失败","error"),e.value[a]=[]}},ct=(a,s)=>{const x=s.reduce((l,p)=>l+(p.allTokens||0),0);return x===0?0:Math.round(a/x*100)},bt=a=>a.formatted&&a.formatted.total?a.formatted.total:a.cost!==void 0?`$${a.cost.toFixed(6)}`:"$0.000000",xt=a=>{const s=new Date,x=new Date(s);x.setDate(s.getDate()-6),V.value[a]={type:"preset",preset:"7days",customStart:x.toISOString().split("T")[0],customEnd:s.toISOString().split("T")[0],customRange:null,presetOptions:[{value:"today",label:"今日",days:1},{value:"7days",label:"7天",days:7},{value:"30days",label:"30天",days:30}]}},Y=a=>(V.value[a]||xt(a),V.value[a]),ft=(a,s)=>{const x=Y(s);x.type="preset",x.preset=a;const l=x.presetOptions.find(p=>p.value===a);if(l){const p=new Date,M=new Date(p);M.setDate(p.getDate()-(l.days-1)),x.customStart=M.toISOString().split("T")[0],x.customEnd=p.toISOString().split("T")[0];const tt=nt=>nt.getFullYear()+"-"+String(nt.getMonth()+1).padStart(2,"0")+"-"+String(nt.getDate()).padStart(2,"0")+" 00:00:00";x.customRange=[tt(M),tt(p)]}Z(s,!0)},vt=(a,s)=>{const x=Y(a);s&&s.length===2?(x.type="custom",x.preset="",x.customRange=s,x.customStart=s[0].split(" ")[0],x.customEnd=s[1].split(" ")[0],Z(a,!0)):s===null&&ft("7days",a)},wt=a=>a>new Date,$t=a=>{const s=Y(a);s.type="preset",s.preset="7days";const x=new Date,l=new Date(x);l.setDate(x.getDate()-6),s.customStart=l.toISOString().split("T")[0],s.customEnd=x.toISOString().split("T")[0],s.customRange=null,Z(a,!0),I("已重置筛选条件并刷新数据","info")},Ct=()=>{K.value=!0},At=a=>{v.value=a,n.value=!0},Kt=a=>{o.value=a,m.value=!0},kt=a=>{K.value=!1,d.value=a,j.value=!0,F()},Lt=()=>{n.value=!1,I("API Key 更新成功","success"),F()},Dt=()=>{m.value=!1,I("API Key 续期成功","success"),F()},It=async a=>{let s=!1;if(window.showConfirm?s=await window.showConfirm("删除 API Key","确定要删除这个 API Key 吗?此操作不可恢复。","确定删除","取消"):s=confirm("确定要删除这个 API Key 吗?此操作不可恢复。"),!!s)try{const x=await W.delete(`/admin/api-keys/${a}`);x.success?(I("API Key 已删除","success"),F()):I(x.message||"删除失败","error")}catch{I("删除失败","error")}},ht=a=>{const x=`${window.location.origin}/admin/api-stats?apiId=${a.id}`,l=document.createElement("textarea");l.value=x,l.style.position="fixed",l.style.opacity="0",l.style.left="-9999px",document.body.appendChild(l),l.select(),l.setSelectionRange(0,99999);try{document.execCommand("copy")?I("已复制统计页面链接","success"):(I("复制失败,请手动复制","error"),console.log("统计页面链接:",x))}catch(p){I("复制失败,请手动复制","error"),console.error("复制错误:",p),console.log("统计页面链接:",x)}finally{document.body.removeChild(l)}};return at(async()=>{await Promise.all([O.loadSupportedClients(),Q(),F()])}),(a,s)=>{const x=Mt;return i(),r("div",Ys,[t("div",zs,[t("div",Ns,[s[15]||(s[15]=t("div",null,[t("h3",{class:"text-xl font-bold text-gray-900 mb-2"},"API Keys 管理"),t("p",{class:"text-gray-600"},"管理和监控您的 API 密钥")],-1)),t("div",Hs,[b(t("select",{"onUpdate:modelValue":s[0]||(s[0]=l=>D.value=l),onChange:s[1]||(s[1]=l=>F()),class:"form-input px-3 py-2 text-sm"},s[12]||(s[12]=[t("option",{value:"today"},"今日",-1),t("option",{value:"7days"},"最近7天",-1),t("option",{value:"monthly"},"本月",-1),t("option",{value:"all"},"全部时间",-1)]),544),[[N,D.value]]),b(t("select",{"onUpdate:modelValue":s[2]||(s[2]=l=>y.value=l),class:"form-input px-3 py-2 text-sm"},[s[13]||(s[13]=t("option",{value:""},"所有标签",-1)),(i(!0),r(_,null,T(S.value,l=>(i(),r("option",{key:l,value:l},u(l),9,Bs))),128))],512),[[N,y.value]]),t("button",{onClick:H(Ct,["stop"]),class:"btn btn-primary px-6 py-3 flex items-center gap-2"},s[14]||(s[14]=[t("i",{class:"fas fa-plus"},null,-1),g("创建新 Key ",-1)]))])]),P.value?(i(),r("div",Gs,s[16]||(s[16]=[t("div",{class:"loading-spinner mx-auto mb-4"},null,-1),t("p",{class:"text-gray-500"},"正在加载 API Keys...",-1)]))):w.value.length===0?(i(),r("div",Qs,s[17]||(s[17]=[t("div",{class:"w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"},[t("i",{class:"fas fa-key text-gray-400 text-xl"})],-1),t("p",{class:"text-gray-500 text-lg"},"暂无 API Keys",-1),t("p",{class:"text-gray-400 text-sm mt-2"},"点击上方按钮创建您的第一个 API Key",-1)]))):(i(),r("div",Xs,[t("table",Js,[t("thead",Zs,[t("tr",null,[t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[3]||(s[3]=l=>X("name"))},[s[18]||(s[18]=g(" 名称 ",-1)),f.value==="name"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",tl))]),s[25]||(s[25]=t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"标签",-1)),s[26]||(s[26]=t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"API Key",-1)),t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[4]||(s[4]=l=>X("status"))},[s[19]||(s[19]=g(" 状态 ",-1)),f.value==="status"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",el))]),t("th",sl,[s[22]||(s[22]=g(" 使用统计 ",-1)),t("span",{class:"cursor-pointer hover:bg-gray-100 px-2 py-1 rounded",onClick:s[5]||(s[5]=l=>X("cost"))},[s[20]||(s[20]=g(" (费用 ",-1)),f.value==="cost"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",ll)),s[21]||(s[21]=g(") ",-1))])]),t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[6]||(s[6]=l=>X("createdAt"))},[s[23]||(s[23]=g(" 创建时间 ",-1)),f.value==="createdAt"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",ol))]),t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[7]||(s[7]=l=>X("expiresAt"))},[s[24]||(s[24]=g(" 过期时间 ",-1)),f.value==="expiresAt"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",nl))]),s[27]||(s[27]=t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"操作",-1))])]),t("tbody",il,[(i(!0),r(_,null,T(q.value,l=>(i(),r(_,{key:l.id},[t("tr",al,[t("td",rl,[t("div",dl,[s[30]||(s[30]=t("div",{class:"w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3"},[t("i",{class:"fas fa-key text-white text-xs"})],-1)),t("div",null,[t("div",ul,u(l.name),1),t("div",ml,u(l.id),1),t("div",pl,[l.claudeAccountId?(i(),r("span",cl,[s[28]||(s[28]=t("i",{class:"fas fa-link mr-1"},null,-1)),g(" 绑定: "+u(gt(l.claudeAccountId)),1)])):(i(),r("span",xl,s[29]||(s[29]=[t("i",{class:"fas fa-share-alt mr-1"},null,-1),g(" 使用共享池 ",-1)])))])])])]),t("td",fl,[t("div",gl,[(i(!0),r(_,null,T(l.tags||[],p=>(i(),r("span",{key:p,class:"inline-flex items-center px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full"},u(p),1))),128)),!l.tags||l.tags.length===0?(i(),r("span",yl,"无标签")):$("",!0)])]),t("td",bl,[t("div",vl,u((l.apiKey||"").substring(0,20))+"... ",1)]),t("td",wl,[t("span",{class:U(["inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold",l.isActive?"bg-green-100 text-green-800":"bg-red-100 text-red-800"])},[t("div",{class:U(["w-2 h-2 rounded-full mr-2",l.isActive?"bg-green-500":"bg-red-500"])},null,2),g(" "+u(l.isActive?"活跃":"禁用"),1)],2)]),t("td",$l,[t("div",Cl,[t("div",Al,[s[31]||(s[31]=t("span",{class:"text-gray-600"},"请求数:",-1)),t("span",Kl,u(E(l.usage&&l.usage.total&&l.usage.total.requests||0)),1)]),t("div",kl,[s[32]||(s[32]=t("span",{class:"text-gray-600"},"Token:",-1)),t("span",Ll,u(E(l.usage&&l.usage.total&&l.usage.total.tokens||0)),1)]),t("div",Dl,[s[33]||(s[33]=t("span",{class:"text-gray-600"},"费用:",-1)),t("span",Il,u(lt(l.usage)),1)]),l.dailyCostLimit>0?(i(),r("div",hl,[s[34]||(s[34]=t("span",{class:"text-gray-600"},"今日费用:",-1)),t("span",{class:U(["font-medium",(l.dailyCost||0)>=l.dailyCostLimit?"text-red-600":"text-blue-600"])}," $"+u((l.dailyCost||0).toFixed(2))+" / $"+u(l.dailyCostLimit.toFixed(2)),3)])):$("",!0),t("div",Ml,[s[35]||(s[35]=t("span",{class:"text-gray-600"},"并发限制:",-1)),t("span",Rl,u(l.concurrencyLimit>0?l.concurrencyLimit:"无限制"),1)]),t("div",Sl,[s[36]||(s[36]=t("span",{class:"text-gray-600"},"当前并发:",-1)),t("span",{class:U(["font-medium",l.currentConcurrency>0?"text-orange-600":"text-gray-600"])},[g(u(l.currentConcurrency||0)+" ",1),l.concurrencyLimit>0?(i(),r("span",Pl,"/ "+u(l.concurrencyLimit),1)):$("",!0)],2)]),l.rateLimitWindow>0?(i(),r("div",_l,[s[37]||(s[37]=t("span",{class:"text-gray-600"},"时间窗口:",-1)),t("span",Tl,u(l.rateLimitWindow)+" 分钟",1)])):$("",!0),l.rateLimitRequests>0?(i(),r("div",El,[s[38]||(s[38]=t("span",{class:"text-gray-600"},"请求限制:",-1)),t("span",Vl,u(l.rateLimitRequests)+" 次/窗口",1)])):$("",!0),t("div",jl,[t("span",null,"输入: "+u(E(l.usage&&l.usage.total&&l.usage.total.inputTokens||0)),1),t("span",null,"输出: "+u(E(l.usage&&l.usage.total&&l.usage.total.outputTokens||0)),1)]),(l.usage&&l.usage.total&&l.usage.total.cacheCreateTokens||0)>0||(l.usage&&l.usage.total&&l.usage.total.cacheReadTokens||0)>0?(i(),r("div",Ul,[t("span",null,"缓存创建: "+u(E(l.usage&&l.usage.total&&l.usage.total.cacheCreateTokens||0)),1),t("span",null,"缓存读取: "+u(E(l.usage&&l.usage.total&&l.usage.total.cacheReadTokens||0)),1)])):$("",!0),t("div",ql,[t("span",null,"RPM: "+u(l.usage&&l.usage.averages&&l.usage.averages.rpm||0),1),t("span",null,"TPM: "+u(l.usage&&l.usage.averages&&l.usage.averages.tpm||0),1)]),t("div",Fl,[t("div",Ol,[t("span",null,"今日: "+u(E(l.usage&&l.usage.daily&&l.usage.daily.requests||0))+"次",1),t("span",null,u(E(l.usage&&l.usage.daily&&l.usage.daily.tokens||0))+"T",1)])]),t("div",Wl,[l&&l.id?(i(),r("button",{key:0,onClick:p=>yt(l.id),class:"text-xs text-indigo-600 hover:text-indigo-800 font-medium"},[t("i",{class:U(["fas",h.value[l.id]?"fa-chevron-up":"fa-chevron-down","mr-1"])},null,2),s[39]||(s[39]=g(" 模型使用分布 ",-1))],8,Yl)):$("",!0)])])]),t("td",zl,u(new Date(l.createdAt).toLocaleDateString()),1),t("td",Nl,[l.expiresAt?(i(),r("div",Hl,[ot(l.expiresAt)?(i(),r("div",Bl,s[40]||(s[40]=[t("i",{class:"fas fa-exclamation-circle mr-1"},null,-1),g(" 已过期 ",-1)]))):mt(l.expiresAt)?(i(),r("div",Gl,[s[41]||(s[41]=t("i",{class:"fas fa-clock mr-1"},null,-1)),g(" "+u(pt(l.expiresAt)),1)])):(i(),r("div",Ql,u(pt(l.expiresAt)),1))])):(i(),r("div",Xl,s[42]||(s[42]=[t("i",{class:"fas fa-infinity mr-1"},null,-1),g(" 永不过期 ",-1)])))]),t("td",Jl,[t("div",Zl,[t("button",{onClick:p=>ht(l),class:"text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-3 py-1 rounded-lg transition-colors",title:"复制统计页面链接"},s[43]||(s[43]=[t("i",{class:"fas fa-chart-bar mr-1"},null,-1),g("统计 ",-1)]),8,to),t("button",{onClick:p=>At(l),class:"text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"},s[44]||(s[44]=[t("i",{class:"fas fa-edit mr-1"},null,-1),g("编辑 ",-1)]),8,eo),l.expiresAt&&(ot(l.expiresAt)||mt(l.expiresAt))?(i(),r("button",{key:0,onClick:p=>Kt(l),class:"text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-3 py-1 rounded-lg transition-colors"},s[45]||(s[45]=[t("i",{class:"fas fa-clock mr-1"},null,-1),g("续期 ",-1)]),8,so)):$("",!0),t("button",{onClick:p=>It(l.id),class:"text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"},s[46]||(s[46]=[t("i",{class:"fas fa-trash mr-1"},null,-1),g("删除 ",-1)]),8,lo)])])]),l&&l.id&&h.value[l.id]?(i(),r("tr",oo,[t("td",no,[e.value[l.id]?$("",!0):(i(),r("div",io,s[47]||(s[47]=[t("div",{class:"loading-spinner mx-auto"},null,-1),t("p",{class:"text-sm text-gray-500 mt-2"},"加载模型统计...",-1)]))),t("div",ao,[t("div",ro,[s[48]||(s[48]=t("h5",{class:"text-sm font-semibold text-gray-700 flex items-center"},[t("i",{class:"fas fa-chart-pie text-indigo-500 mr-2"}),g(" 模型使用分布 ")],-1)),t("div",uo,[e.value[l.id]&&e.value[l.id].length>0?(i(),r("span",mo,u(e.value[l.id].length)+" 个模型 ",1)):$("",!0),t("div",po,[t("div",co,[(i(!0),r(_,null,T(Y(l.id).presetOptions,p=>(i(),r("button",{key:p.value,onClick:M=>ft(p.value,l.id),class:U(["px-2 py-1 rounded text-xs font-medium transition-colors",Y(l.id).preset===p.value&&Y(l.id).type==="preset"?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:text-gray-900"])},u(p.label),11,xo))),128))]),St(x,{"model-value":Y(l.id).customRange,"onUpdate:modelValue":p=>vt(l.id,p),type:"datetimerange","range-separator":"至","start-placeholder":"开始日期","end-placeholder":"结束日期",format:"YYYY-MM-DD HH:mm:ss","value-format":"YYYY-MM-DD HH:mm:ss","disabled-date":wt,"default-time":A.value,size:"small",style:{width:"280px"},class:"api-key-date-picker",clearable:!0,"unlink-panels":!1},null,8,["model-value","onUpdate:modelValue","default-time"])])])]),e.value[l.id]&&e.value[l.id].length===0?(i(),r("div",fo,[t("div",go,[s[50]||(s[50]=t("i",{class:"fas fa-chart-line text-gray-400 text-lg"},null,-1)),s[51]||(s[51]=t("p",{class:"text-sm text-gray-500"},"暂无模型使用数据",-1)),t("button",{onClick:p=>$t(l.id),class:"text-blue-500 hover:text-blue-700 text-sm ml-2 flex items-center gap-1 transition-colors",title:"重置筛选条件并刷新"},s[49]||(s[49]=[t("i",{class:"fas fa-sync-alt text-xs"},null,-1),t("span",{class:"text-xs"},"刷新",-1)]),8,yo)]),s[52]||(s[52]=t("p",{class:"text-xs text-gray-400"},"尝试调整时间范围或点击刷新重新加载数据",-1))])):e.value[l.id]&&e.value[l.id].length>0?(i(),r("div",bo,[(i(!0),r(_,null,T(e.value[l.id],p=>(i(),r("div",{key:p.model,class:"bg-gradient-to-br from-white to-gray-50 rounded-xl p-4 border border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-200"},[t("div",vo,[t("div",wo,[t("span",$o,u(p.model),1),t("span",Co,u(p.requests)+" 次请求",1)])]),t("div",Ao,[t("div",Ko,[s[53]||(s[53]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-coins text-yellow-500 mr-1 text-xs"}),g(" 总Token: ")],-1)),t("span",ko,u(E(p.allTokens)),1)]),t("div",Lo,[s[54]||(s[54]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-dollar-sign text-green-500 mr-1 text-xs"}),g(" 费用: ")],-1)),t("span",Do,u(bt(p)),1)]),t("div",Io,[t("div",ho,[s[55]||(s[55]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-arrow-down text-green-500 mr-1"}),g(" 输入: ")],-1)),t("span",Mo,u(E(p.inputTokens)),1)]),t("div",Ro,[s[56]||(s[56]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-arrow-up text-blue-500 mr-1"}),g(" 输出: ")],-1)),t("span",So,u(E(p.outputTokens)),1)]),p.cacheCreateTokens>0?(i(),r("div",Po,[s[57]||(s[57]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-save mr-1"}),g(" 缓存创建: ")],-1)),t("span",_o,u(E(p.cacheCreateTokens)),1)])):$("",!0),p.cacheReadTokens>0?(i(),r("div",To,[s[58]||(s[58]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-download mr-1"}),g(" 缓存读取: ")],-1)),t("span",Eo,u(E(p.cacheReadTokens)),1)])):$("",!0)])]),t("div",Vo,[t("div",{class:"bg-gradient-to-r from-indigo-500 to-purple-600 h-2 rounded-full transition-all duration-500",style:Pt({width:ct(p.allTokens,e.value[l.id])+"%"})},null,4)]),t("div",jo,[t("span",Uo,u(ct(p.allTokens,e.value[l.id]))+"% ",1)])]))),128))])):$("",!0),e.value[l.id]&&e.value[l.id].length>0?(i(),r("div",qo,[t("div",Fo,[s[61]||(s[61]=t("span",{class:"font-semibold text-gray-700 flex items-center"},[t("i",{class:"fas fa-calculator text-indigo-500 mr-2"}),g(" 总计统计 ")],-1)),t("div",Oo,[t("span",Wo,[s[59]||(s[59]=g(" 总请求: ",-1)),t("span",Yo,u(e.value[l.id].reduce((p,M)=>p+M.requests,0)),1)]),t("span",zo,[s[60]||(s[60]=g(" 总Token: ",-1)),t("span",No,u(E(e.value[l.id].reduce((p,M)=>p+M.allTokens,0))),1)])])])])):$("",!0)])])])):$("",!0)],64))),128))])])]))]),K.value?(i(),z(ke,{key:0,accounts:c.value,onClose:s[8]||(s[8]=l=>K.value=!1),onSuccess:kt},null,8,["accounts"])):$("",!0),n.value?(i(),z(ps,{key:1,apiKey:v.value,accounts:c.value,onClose:s[9]||(s[9]=l=>n.value=!1),onSuccess:Lt},null,8,["apiKey","accounts"])):$("",!0),m.value?(i(),z(hs,{key:2,apiKey:o.value,onClose:s[10]||(s[10]=l=>m.value=!1),onSuccess:Dt},null,8,["apiKey"])):$("",!0),j.value?(i(),z(Ws,{key:3,apiKey:d.value,onClose:s[11]||(s[11]=l=>j.value=!1)},null,8,["apiKey"])):$("",!0)])}}},Zo=J(Ho,[["__scopeId","data-v-518dfd83"]]);export{Zo as default}; diff --git a/web/admin-spa/dist/assets/ApiStatsView-D_Y193CX.js b/web/admin-spa/dist/assets/ApiStatsView-D_Y193CX.js deleted file mode 100644 index 55c13e6c..00000000 --- a/web/admin-spa/dist/assets/ApiStatsView-D_Y193CX.js +++ /dev/null @@ -1 +0,0 @@ -import{aR as B,r as $,c as J,aW as A,x as f,y as m,z as t,O as y,K as H,aq as Q,u as s,aa as G,f as X,P as r,C as P,L as C,Q as O,ac as F,q as Z,aU as tt,V as st,o as et,R as S,J as at,av as it}from"./vue-vendor-CKToUHZx.js";import{L as ot}from"./LogoTitle-CIeppYUK.js";import{_ as K}from"./index-DhITICXu.js";import{b as nt}from"./vendor-BDiMbLwQ.js";import lt from"./TutorialView-PeOC7oRL.js";/* empty css */import"./element-plus-B8Fs_0jW.js";class rt{constructor(){this.baseURL=window.location.origin,this.isDev=!1}async request(n,a={}){try{this.isDev&&n.startsWith("/admin")&&(n="/webapi"+n);const l=await fetch(`${this.baseURL}${n}`,{headers:{"Content-Type":"application/json",...a.headers},...a}),c=await l.json();if(!l.ok)throw new Error(c.message||`请求失败: ${l.status}`);return c}catch(l){throw console.error("API Stats request error:",l),l}}async getKeyId(n){return this.request("/apiStats/api/get-key-id",{method:"POST",body:JSON.stringify({apiKey:n})})}async getUserStats(n){return this.request("/apiStats/api/user-stats",{method:"POST",body:JSON.stringify({apiId:n})})}async getUserModelStats(n,a="daily"){return this.request("/apiStats/api/user-model-stats",{method:"POST",body:JSON.stringify({apiId:n,period:a})})}async getOemSettings(){try{return await this.request("/admin/oem-settings")}catch(n){return console.error("Failed to load OEM settings:",n),{success:!0,data:{siteName:"Claude Relay Service",siteIcon:"",siteIconData:""}}}}}const D=new rt,M=B("apistats",()=>{const w=$(""),n=$(null),a=$(!1),l=$(!1),c=$(!0),e=$(""),i=$("daily"),d=$(null),v=$([]),_=$(null),h=$(null),x=$({siteName:"",siteIcon:"",siteIconData:""}),u=J(()=>{const o={requests:0,inputTokens:0,outputTokens:0,cacheCreateTokens:0,cacheReadTokens:0,allTokens:0,cost:0,formattedCost:"$0.000000"};return i.value==="daily"?_.value||o:h.value||o}),U=J(()=>{if(!d.value||!u.value)return{tokenUsage:0,costUsage:0,requestUsage:0};const o=u.value,g=d.value.limits;return{tokenUsage:g.tokenLimit>0?Math.min(o.allTokens/g.tokenLimit*100,100):0,costUsage:g.dailyCostLimit>0?Math.min(o.cost/g.dailyCostLimit*100,100):0,requestUsage:g.rateLimitRequests>0?Math.min(o.requests/g.rateLimitRequests*100,100):0}});async function T(){if(!w.value.trim()){e.value="请输入 API Key";return}a.value=!0,e.value="",d.value=null,v.value=[],n.value=null;try{const o=await D.getKeyId(w.value);if(o.success){n.value=o.data.id;const g=await D.getUserStats(n.value);if(g.success)d.value=g.data,await R(),e.value="",j();else throw new Error(g.message||"查询失败")}else throw new Error(o.message||"获取 API Key ID 失败")}catch(o){console.error("Query stats error:",o),e.value=o.message||"查询统计数据失败,请检查您的 API Key 是否正确",d.value=null,v.value=[],n.value=null}finally{a.value=!1}}async function R(){n.value&&(await Promise.all([L("daily"),L("monthly")]),await q(i.value))}async function L(o){try{const g=await D.getUserModelStats(n.value,o);if(g.success){const z=g.data||[],k={requests:0,inputTokens:0,outputTokens:0,cacheCreateTokens:0,cacheReadTokens:0,allTokens:0,cost:0,formattedCost:"$0.000000"};z.forEach(I=>{var W;k.requests+=I.requests||0,k.inputTokens+=I.inputTokens||0,k.outputTokens+=I.outputTokens||0,k.cacheCreateTokens+=I.cacheCreateTokens||0,k.cacheReadTokens+=I.cacheReadTokens||0,k.allTokens+=I.allTokens||0,k.cost+=((W=I.costs)==null?void 0:W.total)||0}),k.formattedCost=N(k.cost),o==="daily"?_.value=k:h.value=k}else console.warn(`Failed to load ${o} stats:`,g.message)}catch(g){console.error(`Load ${o} stats error:`,g)}}async function q(o="daily"){if(n.value){l.value=!0;try{const g=await D.getUserModelStats(n.value,o);if(g.success)v.value=g.data||[];else throw new Error(g.message||"加载模型统计失败")}catch(g){console.error("Load model stats error:",g),v.value=[]}finally{l.value=!1}}}async function E(o){i.value===o||l.value||(i.value=o,(o==="daily"&&!_.value||o==="monthly"&&!h.value)&&await L(o),await q(o))}async function b(){if(n.value){a.value=!0,e.value="",d.value=null,v.value=[];try{const o=await D.getUserStats(n.value);if(o.success)d.value=o.data,await R(),e.value="";else throw new Error(o.message||"查询失败")}catch(o){console.error("Load stats with apiId error:",o),e.value=o.message||"查询统计数据失败",d.value=null,v.value=[]}finally{a.value=!1}}}async function p(){c.value=!0;try{const o=await D.getOemSettings();o&&o.success&&o.data&&(x.value={...x.value,...o.data})}catch(o){console.error("Error loading OEM settings:",o),x.value={siteName:"Claude Relay Service",siteIcon:"",siteIconData:""}}finally{c.value=!1}}function N(o){return typeof o!="number"||o===0?"$0.000000":o>=1?"$"+o.toFixed(2):o>=.01?"$"+o.toFixed(4):"$"+o.toFixed(6)}function j(){if(n.value){const o=new URL(window.location);o.searchParams.set("apiId",n.value),window.history.pushState({},"",o)}}function V(){d.value=null,v.value=[],_.value=null,h.value=null,e.value="",i.value="daily",n.value=null}function Y(){w.value="",V()}return{apiKey:w,apiId:n,loading:a,modelStatsLoading:l,oemLoading:c,error:e,statsPeriod:i,statsData:d,modelStats:v,dailyStats:_,monthlyStats:h,oemSettings:x,currentPeriodData:u,usagePercentages:U,queryStats:T,loadAllPeriodStats:R,loadPeriodStats:L,loadModelStats:q,switchPeriod:E,loadStatsWithApiId:b,loadOemSettings:p,clearData:V,reset:Y}}),dt={class:"api-input-wide-card glass-strong rounded-3xl p-6 mb-8 shadow-xl"},ct={class:"max-w-4xl mx-auto"},ut={class:"api-input-grid grid grid-cols-1 lg:grid-cols-4"},ft={class:"lg:col-span-3"},mt=["disabled"],xt={class:"lg:col-span-1"},yt=["disabled"],pt={key:0,class:"fas fa-spinner loading-spinner"},gt={key:1,class:"fas fa-search"},vt={__name:"ApiKeyInput",setup(w){const n=M(),{apiKey:a,loading:l}=A(n),{queryStats:c}=n;return(e,i)=>(m(),f("div",dt,[i[6]||(i[6]=t("div",{class:"wide-card-title text-center mb-6"},[t("h2",{class:"text-2xl font-bold mb-2"},[t("i",{class:"fas fa-chart-line mr-3"}),y(" 使用统计查询 ")]),t("p",{class:"text-base text-gray-600"},"查询您的 API Key 使用情况和统计数据")],-1)),t("div",ct,[t("div",ut,[t("div",ft,[i[3]||(i[3]=t("label",{class:"block text-sm font-medium mb-2 text-gray-700"},[t("i",{class:"fas fa-key mr-2"}),y(" 输入您的 API Key ")],-1)),H(t("input",{"onUpdate:modelValue":i[0]||(i[0]=d=>X(a)?a.value=d:null),type:"password",placeholder:"请输入您的 API Key (cr_...)",class:"wide-card-input w-full",onKeyup:i[1]||(i[1]=G((...d)=>s(c)&&s(c)(...d),["enter"])),disabled:s(l)},null,40,mt),[[Q,s(a)]])]),t("div",xt,[i[4]||(i[4]=t("label",{class:"hidden lg:block text-sm font-medium mb-2 text-gray-700"},"   ",-1)),t("button",{onClick:i[2]||(i[2]=(...d)=>s(c)&&s(c)(...d)),disabled:s(l)||!s(a).trim(),class:"btn btn-primary btn-query w-full h-full flex items-center justify-center gap-2"},[s(l)?(m(),f("i",pt)):(m(),f("i",gt)),y(" "+r(s(l)?"查询中...":"查询统计"),1)],8,yt)])]),i[5]||(i[5]=t("div",{class:"security-notice mt-4"},[t("i",{class:"fas fa-shield-alt mr-2"}),y(" 您的 API Key 仅用于查询自己的统计数据,不会被存储或用于其他用途 ")],-1))])]))}},_t=K(vt,[["__scopeId","data-v-a4be9e3c"]]),bt={class:"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"},ht={class:"card p-6"},$t={class:"space-y-3"},wt={class:"flex justify-between items-center"},kt={class:"font-medium text-gray-900"},St={class:"flex justify-between items-center"},Tt={class:"flex justify-between items-center"},It={class:"font-medium text-gray-900"},Ct={class:"flex justify-between items-center"},Lt={class:"font-medium text-gray-900"},Dt={class:"flex justify-between items-center"},Pt={key:0},At={key:0,class:"text-red-600 font-medium"},Kt={key:1,class:"text-orange-600 font-medium"},Mt={key:2,class:"text-gray-900 font-medium"},Rt={key:1,class:"text-gray-400 font-medium"},qt={class:"card p-6"},jt={class:"text-xl font-bold mb-4 flex items-center text-gray-900"},Ut={class:"text-sm font-normal text-gray-600 ml-2"},Et={class:"grid grid-cols-2 gap-4"},Nt={class:"stat-card text-center"},Ot={class:"text-3xl font-bold text-green-600"},Ft={class:"text-sm text-gray-600"},Vt={class:"stat-card text-center"},Wt={class:"text-3xl font-bold text-blue-600"},Jt={class:"text-sm text-gray-600"},Yt={class:"stat-card text-center"},zt={class:"text-3xl font-bold text-purple-600"},Bt={class:"text-sm text-gray-600"},Ht={class:"stat-card text-center"},Qt={class:"text-3xl font-bold text-yellow-600"},Gt={class:"text-sm text-gray-600"},Xt={__name:"StatsOverview",setup(w){const n=M(),{statsData:a,statsPeriod:l,currentPeriodData:c}=A(n),e=x=>{if(!x)return"无";try{return nt(x).format("YYYY年MM月DD日 HH:mm")}catch{return"格式错误"}},i=x=>x?new Date(x).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}):"",d=x=>x?new Date(x){if(!x)return!1;const T=(new Date(x)-new Date)/(1e3*60*60*24);return T>0&&T<=7},_=x=>(typeof x!="number"&&(x=parseInt(x)||0),x===0?"0":x>=1e6?(x/1e6).toFixed(1)+"M":x>=1e3?(x/1e3).toFixed(1)+"K":x.toLocaleString()),h=x=>({claude:"Claude",gemini:"Gemini",all:"全部模型"})[x]||x||"未知";return(x,u)=>(m(),f("div",bt,[t("div",ht,[u[8]||(u[8]=t("h3",{class:"text-xl font-bold mb-4 flex items-center text-gray-900"},[t("i",{class:"fas fa-info-circle mr-3 text-blue-500"}),y(" API Key 信息 ")],-1)),t("div",$t,[t("div",wt,[u[0]||(u[0]=t("span",{class:"text-gray-600"},"名称",-1)),t("span",kt,r(s(a).name),1)]),t("div",St,[u[1]||(u[1]=t("span",{class:"text-gray-600"},"状态",-1)),t("span",{class:P([s(a).isActive?"text-green-600":"text-red-600","font-medium"])},[t("i",{class:P([s(a).isActive?"fas fa-check-circle":"fas fa-times-circle","mr-1"])},null,2),y(" "+r(s(a).isActive?"活跃":"已停用"),1)],2)]),t("div",Tt,[u[2]||(u[2]=t("span",{class:"text-gray-600"},"权限",-1)),t("span",It,r(h(s(a).permissions)),1)]),t("div",Ct,[u[3]||(u[3]=t("span",{class:"text-gray-600"},"创建时间",-1)),t("span",Lt,r(e(s(a).createdAt)),1)]),t("div",Dt,[u[7]||(u[7]=t("span",{class:"text-gray-600"},"过期时间",-1)),s(a).expiresAt?(m(),f("div",Pt,[d(s(a).expiresAt)?(m(),f("div",At,u[4]||(u[4]=[t("i",{class:"fas fa-exclamation-circle mr-1"},null,-1),y(" 已过期 ",-1)]))):v(s(a).expiresAt)?(m(),f("div",Kt,[u[5]||(u[5]=t("i",{class:"fas fa-clock mr-1"},null,-1)),y(" "+r(i(s(a).expiresAt)),1)])):(m(),f("div",Mt,r(i(s(a).expiresAt)),1))])):(m(),f("div",Rt,u[6]||(u[6]=[t("i",{class:"fas fa-infinity mr-1"},null,-1),y(" 永不过期 ",-1)])))])])]),t("div",qt,[t("h3",jt,[u[9]||(u[9]=t("i",{class:"fas fa-chart-bar mr-3 text-green-500"},null,-1)),u[10]||(u[10]=y(" 使用统计概览 ",-1)),t("span",Ut,"("+r(s(l)==="daily"?"今日":"本月")+")",1)]),t("div",Et,[t("div",Nt,[t("div",Ot,r(_(s(c).requests)),1),t("div",Ft,r(s(l)==="daily"?"今日":"本月")+"请求数",1)]),t("div",Vt,[t("div",Wt,r(_(s(c).allTokens)),1),t("div",Jt,r(s(l)==="daily"?"今日":"本月")+"Token数",1)]),t("div",Yt,[t("div",zt,r(s(c).formattedCost||"$0.000000"),1),t("div",Bt,r(s(l)==="daily"?"今日":"本月")+"费用",1)]),t("div",Ht,[t("div",Qt,r(_(s(c).inputTokens)),1),t("div",Gt,r(s(l)==="daily"?"今日":"本月")+"输入Token",1)])])])]))}},Zt=K(Xt,[["__scopeId","data-v-957fdbf9"]]),ts={class:"card p-6"},ss={class:"text-xl font-bold mb-4 flex items-center text-gray-900"},es={class:"text-sm font-normal text-gray-600 ml-2"},as={class:"space-y-3"},is={class:"flex justify-between items-center"},os={class:"font-medium text-gray-900"},ns={class:"flex justify-between items-center"},ls={class:"font-medium text-gray-900"},rs={class:"flex justify-between items-center"},ds={class:"font-medium text-gray-900"},cs={class:"flex justify-between items-center"},us={class:"font-medium text-gray-900"},fs={class:"mt-4 pt-4 border-t border-gray-200"},ms={class:"flex justify-between items-center font-bold text-gray-900"},xs={class:"text-xl"},ys={__name:"TokenDistribution",setup(w){const n=M(),{statsPeriod:a,currentPeriodData:l}=A(n),c=e=>(typeof e!="number"&&(e=parseInt(e)||0),e===0?"0":e>=1e6?(e/1e6).toFixed(1)+"M":e>=1e3?(e/1e3).toFixed(1)+"K":e.toLocaleString());return(e,i)=>(m(),f("div",ts,[t("h3",ss,[i[0]||(i[0]=t("i",{class:"fas fa-coins mr-3 text-yellow-500"},null,-1)),i[1]||(i[1]=y(" Token 使用分布 ",-1)),t("span",es,"("+r(s(a)==="daily"?"今日":"本月")+")",1)]),t("div",as,[t("div",is,[i[2]||(i[2]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-arrow-right mr-2 text-green-500"}),y(" 输入 Token ")],-1)),t("span",os,r(c(s(l).inputTokens)),1)]),t("div",ns,[i[3]||(i[3]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-arrow-left mr-2 text-blue-500"}),y(" 输出 Token ")],-1)),t("span",ls,r(c(s(l).outputTokens)),1)]),t("div",rs,[i[4]||(i[4]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-save mr-2 text-purple-500"}),y(" 缓存创建 Token ")],-1)),t("span",ds,r(c(s(l).cacheCreateTokens)),1)]),t("div",cs,[i[5]||(i[5]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-download mr-2 text-orange-500"}),y(" 缓存读取 Token ")],-1)),t("span",us,r(c(s(l).cacheReadTokens)),1)])]),t("div",fs,[t("div",ms,[t("span",null,r(s(a)==="daily"?"今日":"本月")+"总计",1),t("span",xs,r(c(s(l).allTokens)),1)])])]))}},ps=K(ys,[["__scopeId","data-v-db41efab"]]),gs={class:"card p-6"},vs={class:"space-y-3"},_s={class:"flex justify-between items-center"},bs={class:"font-medium text-gray-900"},hs={class:"flex justify-between items-center"},$s={class:"font-medium text-gray-900"},ws={class:"flex justify-between items-center"},ks={class:"font-medium text-gray-900"},Ss={class:"flex justify-between items-center"},Ts={class:"font-medium text-gray-900"},Is={class:"flex justify-between items-center"},Cs={class:"font-medium text-gray-900"},Ls={key:0,class:"text-orange-600"},Ds={key:1,class:"text-green-600"},Ps={class:"flex justify-between items-center"},As={class:"font-medium text-gray-900"},Ks={key:0,class:"text-orange-600"},Ms={key:1,class:"text-green-600"},Rs={key:0,class:"card p-6 mt-6"},qs={class:"grid grid-cols-1 lg:grid-cols-2 gap-6"},js={key:0,class:"bg-amber-50 border border-amber-200 rounded-lg p-4"},Us={class:"space-y-2"},Es={class:"text-gray-800"},Ns={key:1,class:"bg-blue-50 border border-blue-200 rounded-lg p-4"},Os={class:"space-y-2"},Fs={class:"text-gray-800"},Vs={__name:"LimitConfig",setup(w){const n=M(),{statsData:a}=A(n),l=c=>(typeof c!="number"&&(c=parseInt(c)||0),c===0?"0":c>=1e6?(c/1e6).toFixed(1)+"M":c>=1e3?(c/1e3).toFixed(1)+"K":c.toLocaleString());return(c,e)=>(m(),f("div",null,[t("div",gs,[e[10]||(e[10]=t("h3",{class:"text-xl font-bold mb-4 flex items-center text-gray-900"},[t("i",{class:"fas fa-shield-alt mr-3 text-red-500"}),y(" 限制配置 ")],-1)),t("div",vs,[t("div",_s,[e[0]||(e[0]=t("span",{class:"text-gray-600"},"Token 限制",-1)),t("span",bs,r(s(a).limits.tokenLimit>0?l(s(a).limits.tokenLimit):"无限制"),1)]),t("div",hs,[e[1]||(e[1]=t("span",{class:"text-gray-600"},"并发限制",-1)),t("span",$s,r(s(a).limits.concurrencyLimit>0?s(a).limits.concurrencyLimit:"无限制"),1)]),t("div",ws,[e[2]||(e[2]=t("span",{class:"text-gray-600"},"速率限制",-1)),t("span",ks,r(s(a).limits.rateLimitRequests>0&&s(a).limits.rateLimitWindow>0?`${s(a).limits.rateLimitRequests}次/${s(a).limits.rateLimitWindow}分钟`:"无限制"),1)]),t("div",Ss,[e[3]||(e[3]=t("span",{class:"text-gray-600"},"每日费用限制",-1)),t("span",Ts,r(s(a).limits.dailyCostLimit>0?"$"+s(a).limits.dailyCostLimit:"无限制"),1)]),t("div",Is,[e[6]||(e[6]=t("span",{class:"text-gray-600"},"模型限制",-1)),t("span",Cs,[s(a).restrictions.enableModelRestriction&&s(a).restrictions.restrictedModels.length>0?(m(),f("span",Ls,[e[4]||(e[4]=t("i",{class:"fas fa-exclamation-triangle mr-1"},null,-1)),y(" 限制 "+r(s(a).restrictions.restrictedModels.length)+" 个模型 ",1)])):(m(),f("span",Ds,e[5]||(e[5]=[t("i",{class:"fas fa-check-circle mr-1"},null,-1),y(" 允许所有模型 ",-1)])))])]),t("div",Ps,[e[9]||(e[9]=t("span",{class:"text-gray-600"},"客户端限制",-1)),t("span",As,[s(a).restrictions.enableClientRestriction&&s(a).restrictions.allowedClients.length>0?(m(),f("span",Ks,[e[7]||(e[7]=t("i",{class:"fas fa-exclamation-triangle mr-1"},null,-1)),y(" 限制 "+r(s(a).restrictions.allowedClients.length)+" 个客户端 ",1)])):(m(),f("span",Ms,e[8]||(e[8]=[t("i",{class:"fas fa-check-circle mr-1"},null,-1),y(" 允许所有客户端 ",-1)])))])])])]),s(a).restrictions.enableModelRestriction&&s(a).restrictions.restrictedModels.length>0||s(a).restrictions.enableClientRestriction&&s(a).restrictions.allowedClients.length>0?(m(),f("div",Rs,[e[17]||(e[17]=t("h3",{class:"text-xl font-bold mb-4 flex items-center text-gray-900"},[t("i",{class:"fas fa-list-alt mr-3 text-amber-500"}),y(" 详细限制信息 ")],-1)),t("div",qs,[s(a).restrictions.enableModelRestriction&&s(a).restrictions.restrictedModels.length>0?(m(),f("div",js,[e[12]||(e[12]=t("h4",{class:"font-bold text-amber-800 mb-3 flex items-center"},[t("i",{class:"fas fa-robot mr-2"}),y(" 受限模型列表 ")],-1)),t("div",Us,[(m(!0),f(O,null,F(s(a).restrictions.restrictedModels,i=>(m(),f("div",{key:i,class:"bg-white rounded px-3 py-2 text-sm border border-amber-200"},[e[11]||(e[11]=t("i",{class:"fas fa-ban mr-2 text-red-500"},null,-1)),t("span",Es,r(i),1)]))),128))]),e[13]||(e[13]=t("p",{class:"text-xs text-amber-700 mt-3"},[t("i",{class:"fas fa-info-circle mr-1"}),y(" 此 API Key 不能访问以上列出的模型 ")],-1))])):C("",!0),s(a).restrictions.enableClientRestriction&&s(a).restrictions.allowedClients.length>0?(m(),f("div",Ns,[e[15]||(e[15]=t("h4",{class:"font-bold text-blue-800 mb-3 flex items-center"},[t("i",{class:"fas fa-desktop mr-2"}),y(" 允许的客户端 ")],-1)),t("div",Os,[(m(!0),f(O,null,F(s(a).restrictions.allowedClients,i=>(m(),f("div",{key:i,class:"bg-white rounded px-3 py-2 text-sm border border-blue-200"},[e[14]||(e[14]=t("i",{class:"fas fa-check mr-2 text-green-500"},null,-1)),t("span",Fs,r(i),1)]))),128))]),e[16]||(e[16]=t("p",{class:"text-xs text-blue-700 mt-3"},[t("i",{class:"fas fa-info-circle mr-1"}),y(" 此 API Key 只能被以上列出的客户端使用 ")],-1))])):C("",!0)])])):C("",!0)]))}},Ws=K(Vs,[["__scopeId","data-v-3163241d"]]),Js={class:"card p-6"},Ys={class:"mb-6"},zs={class:"text-xl font-bold flex items-center text-gray-900"},Bs={class:"text-sm font-normal text-gray-600 ml-2"},Hs={key:0,class:"text-center py-8"},Qs={key:1,class:"space-y-4"},Gs={class:"flex justify-between items-start mb-3"},Xs={class:"font-bold text-lg text-gray-900"},Zs={class:"text-gray-600 text-sm"},te={class:"text-right"},se={class:"text-lg font-bold text-green-600"},ee={class:"grid grid-cols-2 md:grid-cols-4 gap-3 text-sm"},ae={class:"bg-gray-50 rounded p-2"},ie={class:"font-medium text-gray-900"},oe={class:"bg-gray-50 rounded p-2"},ne={class:"font-medium text-gray-900"},le={class:"bg-gray-50 rounded p-2"},re={class:"font-medium text-gray-900"},de={class:"bg-gray-50 rounded p-2"},ce={class:"font-medium text-gray-900"},ue={key:2,class:"text-center py-8 text-gray-500"},fe={__name:"ModelUsageStats",setup(w){const n=M(),{statsPeriod:a,modelStats:l,modelStatsLoading:c}=A(n),e=i=>(typeof i!="number"&&(i=parseInt(i)||0),i===0?"0":i>=1e6?(i/1e6).toFixed(1)+"M":i>=1e3?(i/1e3).toFixed(1)+"K":i.toLocaleString());return(i,d)=>(m(),f("div",Js,[t("div",Ys,[t("h3",zs,[d[0]||(d[0]=t("i",{class:"fas fa-robot mr-3 text-indigo-500"},null,-1)),d[1]||(d[1]=y(" 模型使用统计 ",-1)),t("span",Bs,"("+r(s(a)==="daily"?"今日":"本月")+")",1)])]),s(c)?(m(),f("div",Hs,d[2]||(d[2]=[t("i",{class:"fas fa-spinner loading-spinner text-2xl mb-2 text-gray-600"},null,-1),t("p",{class:"text-gray-600"},"加载模型统计数据中...",-1)]))):s(l).length>0?(m(),f("div",Qs,[(m(!0),f(O,null,F(s(l),(v,_)=>{var h;return m(),f("div",{key:_,class:"model-usage-item"},[t("div",Gs,[t("div",null,[t("h4",Xs,r(v.model),1),t("p",Zs,r(v.requests)+" 次请求",1)]),t("div",te,[t("div",se,r(((h=v.formatted)==null?void 0:h.total)||"$0.000000"),1),d[3]||(d[3]=t("div",{class:"text-sm text-gray-600"},"总费用",-1))])]),t("div",ee,[t("div",ae,[d[4]||(d[4]=t("div",{class:"text-gray-600"},"输入 Token",-1)),t("div",ie,r(e(v.inputTokens)),1)]),t("div",oe,[d[5]||(d[5]=t("div",{class:"text-gray-600"},"输出 Token",-1)),t("div",ne,r(e(v.outputTokens)),1)]),t("div",le,[d[6]||(d[6]=t("div",{class:"text-gray-600"},"缓存创建",-1)),t("div",re,r(e(v.cacheCreateTokens)),1)]),t("div",de,[d[7]||(d[7]=t("div",{class:"text-gray-600"},"缓存读取",-1)),t("div",ce,r(e(v.cacheReadTokens)),1)])])])}),128))])):(m(),f("div",ue,[d[8]||(d[8]=t("i",{class:"fas fa-chart-pie text-3xl mb-3"},null,-1)),t("p",null,"暂无"+r(s(a)==="daily"?"今日":"本月")+"模型使用数据",1)]))]))}},me=K(fe,[["__scopeId","data-v-b5e42c0c"]]),xe={class:"min-h-screen gradient-bg p-6"},ye={class:"glass-strong rounded-3xl p-6 mb-8 shadow-xl"},pe={class:"flex flex-col md:flex-row justify-between items-center gap-4"},ge={class:"flex items-center gap-3"},ve={class:"mb-8"},_e={class:"flex justify-center"},be={class:"inline-flex bg-white/10 backdrop-blur-xl rounded-full p-1 shadow-lg border border-white/20"},he={key:0,class:"tab-content"},$e={key:0,class:"mb-8"},we={class:"bg-red-500/20 border border-red-500/30 rounded-xl p-4 text-red-800 backdrop-blur-sm"},ke={key:1,class:"fade-in"},Se={class:"glass-strong rounded-3xl p-6 shadow-xl"},Te={class:"mb-6 pb-6 border-b border-gray-200"},Ie={class:"flex flex-col md:flex-row items-start md:items-center justify-between gap-4"},Ce={class:"flex gap-2"},Le=["disabled"],De=["disabled"],Pe={class:"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"},Ae={key:1,class:"tab-content"},Ke={class:"glass-strong rounded-3xl shadow-xl"},Me={__name:"ApiStatsView",setup(w){const n=tt(),a=M(),l=$("stats"),{apiKey:c,apiId:e,loading:i,modelStatsLoading:d,oemLoading:v,error:_,statsPeriod:h,statsData:x,oemSettings:u}=A(a),{queryStats:U,switchPeriod:T,loadStatsWithApiId:R,loadOemSettings:L,reset:q}=a,E=b=>{(b.ctrlKey||b.metaKey)&&b.key==="Enter"&&(!i.value&&c.value.trim()&&U(),b.preventDefault()),b.key==="Escape"&&q()};return Z(()=>{console.log("API Stats Page loaded"),L();const b=n.query.apiId,p=n.query.apiKey;b&&b.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)?(e.value=b,R()):p&&p.length>10&&(c.value=p),document.addEventListener("keydown",E)}),st(()=>{document.removeEventListener("keydown",E)}),et(c,b=>{b||a.clearData()}),(b,p)=>{const N=it("router-link");return m(),f("div",xe,[t("div",ye,[t("div",pe,[S(ot,{loading:s(v),title:s(u).siteName,subtitle:l.value==="stats"?"API Key 使用统计":"使用教程","logo-src":s(u).siteIconData||s(u).siteIcon},null,8,["loading","title","subtitle","logo-src"]),t("div",ge,[S(N,{to:"/dashboard",class:"admin-button rounded-xl px-4 py-2 text-white transition-all duration-300 flex items-center gap-2"},{default:at(()=>p[4]||(p[4]=[t("i",{class:"fas fa-cog text-sm"},null,-1),t("span",{class:"text-sm font-medium"},"管理后台",-1)])),_:1,__:[4]})])])]),t("div",ve,[t("div",_e,[t("div",be,[t("button",{onClick:p[0]||(p[0]=j=>l.value="stats"),class:P(["tab-pill-button",l.value==="stats"?"active":""])},p[5]||(p[5]=[t("i",{class:"fas fa-chart-line mr-2"},null,-1),t("span",null,"统计查询",-1)]),2),t("button",{onClick:p[1]||(p[1]=j=>l.value="tutorial"),class:P(["tab-pill-button",l.value==="tutorial"?"active":""])},p[6]||(p[6]=[t("i",{class:"fas fa-graduation-cap mr-2"},null,-1),t("span",null,"使用教程",-1)]),2)])])]),l.value==="stats"?(m(),f("div",he,[S(_t),s(_)?(m(),f("div",$e,[t("div",we,[p[7]||(p[7]=t("i",{class:"fas fa-exclamation-triangle mr-2"},null,-1)),y(" "+r(s(_)),1)])])):C("",!0),s(x)?(m(),f("div",ke,[t("div",Se,[t("div",Te,[t("div",Ie,[p[10]||(p[10]=t("div",{class:"flex items-center gap-3"},[t("i",{class:"fas fa-clock text-blue-500 text-lg"}),t("span",{class:"text-lg font-medium text-gray-700"},"统计时间范围")],-1)),t("div",Ce,[t("button",{onClick:p[2]||(p[2]=j=>s(T)("daily")),class:P([["period-btn",{active:s(h)==="daily"}],"px-6 py-2 text-sm font-medium flex items-center gap-2"]),disabled:s(i)||s(d)},p[8]||(p[8]=[t("i",{class:"fas fa-calendar-day"},null,-1),y(" 今日 ",-1)]),10,Le),t("button",{onClick:p[3]||(p[3]=j=>s(T)("monthly")),class:P([["period-btn",{active:s(h)==="monthly"}],"px-6 py-2 text-sm font-medium flex items-center gap-2"]),disabled:s(i)||s(d)},p[9]||(p[9]=[t("i",{class:"fas fa-calendar-alt"},null,-1),y(" 本月 ",-1)]),10,De)])])]),S(Zt),t("div",Pe,[S(ps),S(Ws)]),S(me)])])):C("",!0)])):C("",!0),l.value==="tutorial"?(m(),f("div",Ae,[t("div",Ke,[S(lt)])])):C("",!0)])}}},Fe=K(Me,[["__scopeId","data-v-4e1404f4"]]);export{Fe as default}; diff --git a/web/admin-spa/dist/assets/DashboardView-B-EZhqex.js b/web/admin-spa/dist/assets/DashboardView-B-EZhqex.js deleted file mode 100644 index 6b775d03..00000000 --- a/web/admin-spa/dist/assets/DashboardView-B-EZhqex.js +++ /dev/null @@ -1 +0,0 @@ -import{E as it}from"./element-plus-B8Fs_0jW.js";import{aR as dt,r as k,c as ut,aW as ct,o as st,q as pt,D as E,x as b,z as t,P as d,u as s,O as v,L as I,Q as lt,ac as rt,C as j,R as yt,y as h}from"./vue-vendor-CKToUHZx.js";import{a as O,_ as mt}from"./index-DhITICXu.js";import{s as at}from"./toast-BvwA7Mwb.js";import{C as ot}from"./chart-Cor9iTVD.js";import"./vendor-BDiMbLwQ.js";const ft=dt("dashboard",()=>{const L=k(!1),U=k({totalApiKeys:0,activeApiKeys:0,totalAccounts:0,activeAccounts:0,rateLimitedAccounts:0,todayRequests:0,totalRequests:0,todayTokens:0,todayInputTokens:0,todayOutputTokens:0,totalTokens:0,totalInputTokens:0,totalOutputTokens:0,totalCacheCreateTokens:0,totalCacheReadTokens:0,todayCacheCreateTokens:0,todayCacheReadTokens:0,systemRPM:0,systemTPM:0,systemStatus:"正常",uptime:0}),i=k({todayCosts:{totalCost:0,formatted:{totalCost:"$0.000000"}},totalCosts:{totalCost:0,formatted:{totalCost:"$0.000000"}}}),V=k([]),w=k([]),q=k([]),T=k({data:[],topApiKeys:[],totalApiKeys:0}),l=k({type:"preset",preset:"7days",customStart:"",customEnd:"",customRange:null,presetOptions:[{value:"today",label:"今日",days:1},{value:"7days",label:"7天",days:7},{value:"30days",label:"30天",days:30}]}),g=k("day"),R=k("requests"),_=k([new Date(2e3,1,1,0,0,0),new Date(2e3,2,1,23,59,59)]),Z=ut(()=>{const a=U.value.uptime,r=Math.floor(a/86400),n=Math.floor(a%86400/3600),o=Math.floor(a%3600/60);return r>0?`${r}天 ${n}小时`:n>0?`${n}小时 ${o}分钟`:`${o}分钟`});async function J(){L.value=!0;try{const[a,r,n]=await Promise.all([O.get("/admin/dashboard"),O.get("/admin/usage-costs?period=today"),O.get("/admin/usage-costs?period=all")]);if(a.success){const o=a.data.overview||{},c=a.data.recentActivity||{},m=a.data.systemAverages||{},C=a.data.systemHealth||{};U.value={totalApiKeys:o.totalApiKeys||0,activeApiKeys:o.activeApiKeys||0,totalAccounts:o.totalClaudeAccounts||0,activeAccounts:o.activeClaudeAccounts||0,rateLimitedAccounts:o.rateLimitedClaudeAccounts||0,todayRequests:c.requestsToday||0,totalRequests:o.totalRequestsUsed||0,todayTokens:c.tokensToday||0,todayInputTokens:c.inputTokensToday||0,todayOutputTokens:c.outputTokensToday||0,totalTokens:o.totalTokensUsed||0,totalInputTokens:o.totalInputTokensUsed||0,totalOutputTokens:o.totalOutputTokensUsed||0,totalCacheCreateTokens:o.totalCacheCreateTokensUsed||0,totalCacheReadTokens:o.totalCacheReadTokensUsed||0,todayCacheCreateTokens:c.cacheCreateTokensToday||0,todayCacheReadTokens:c.cacheReadTokensToday||0,systemRPM:m.rpm||0,systemTPM:m.tpm||0,systemStatus:C.redisConnected?"正常":"异常",uptime:C.uptime||0}}r.success&&n.success&&(i.value={todayCosts:r.data.totalCosts||{totalCost:0,formatted:{totalCost:"$0.000000"}},totalCosts:n.data.totalCosts||{totalCost:0,formatted:{totalCost:"$0.000000"}}})}catch(a){console.error("加载仪表板数据失败:",a)}finally{L.value=!1}}async function X(a=7,r="day"){try{let n="/admin/usage-trend?";r==="hour"?(n+="granularity=hour",l.value.customRange&&l.value.customRange.length===2&&(n+=`&startDate=${encodeURIComponent(l.value.customRange[0])}`,n+=`&endDate=${encodeURIComponent(l.value.customRange[1])}`)):n+=`granularity=day&days=${a}`;const o=await O.get(n);o.success&&(w.value=o.data)}catch(n){console.error("加载使用趋势失败:",n)}}async function tt(a="daily"){try{const r=await O.get(`/admin/model-stats?period=${a}`);r.success&&(q.value=r.data)}catch(r){console.error("加载模型统计失败:",r)}}async function Y(a="requests"){try{let r="/admin/api-keys-usage-trend?";if(g.value==="hour")r+="granularity=hour",l.value.customRange&&l.value.customRange.length===2&&(r+=`&startDate=${encodeURIComponent(l.value.customRange[0])}`,r+=`&endDate=${encodeURIComponent(l.value.customRange[1])}`);else{const o=l.value.type==="preset"?l.value.preset==="today"?1:l.value.preset==="7days"?7:30:z(l.value.customStart,l.value.customEnd);r+=`granularity=day&days=${o}`}r+=`&metric=${a}`;const n=await O.get(r);n.success&&(T.value={data:n.data||[],topApiKeys:n.topApiKeys||[],totalApiKeys:n.totalApiKeys||0})}catch(r){console.error("加载API Keys趋势失败:",r)}}function K(a){l.value.type="preset",l.value.preset=a;const r=l.value.presetOptions.find(n=>n.value===a);if(r){const n=new Date;let o,c;if(g.value==="hour")switch(a){case"last24h":o=new Date(n.getTime()-24*60*60*1e3),c=n;break;case"yesterday":o=new Date(n),o.setDate(n.getDate()-1),o.setHours(0,0,0,0),c=new Date(o),c.setHours(23,59,59,999);break;case"dayBefore":o=new Date(n),o.setDate(n.getDate()-2),o.setHours(0,0,0,0),c=new Date(o),c.setHours(23,59,59,999);break}else o=new Date(n),c=new Date(n),a==="today"?(o.setHours(0,0,0,0),c.setHours(23,59,59,999)):(o.setDate(n.getDate()-(r.days-1)),o.setHours(0,0,0,0),c.setHours(23,59,59,999));l.value.customStart=o.toISOString().split("T")[0],l.value.customEnd=c.toISOString().split("T")[0];const m=C=>{const G=C.getFullYear(),W=String(C.getMonth()+1).padStart(2,"0"),H=String(C.getDate()).padStart(2,"0"),Q=String(C.getHours()).padStart(2,"0"),p=String(C.getMinutes()).padStart(2,"0"),e=String(C.getSeconds()).padStart(2,"0");return`${G}-${W}-${H} ${Q}:${p}:${e}`};l.value.customRange=[m(o),m(c)]}$()}function et(a){if(a&&a.length===2){l.value.type="custom",l.value.preset="",l.value.customRange=a,l.value.customStart=a[0].split(" ")[0],l.value.customEnd=a[1].split(" ")[0];const r=new Date(a[0]),n=new Date(a[1]);if(g.value==="hour"){if((n-r)/36e5>24){at("小时粒度下日期范围不能超过24小时","warning");return}}else if(Math.ceil((n-r)/864e5)+1>31){at("日期范围不能超过 31 天","warning");return}$()}else a===null&&K((g.value==="hour","7days"))}function N(a){if(g.value=a,a==="hour"){if(l.value.presetOptions=[{value:"last24h",label:"近24小时",hours:24},{value:"yesterday",label:"昨天",hours:24},{value:"dayBefore",label:"前天",hours:24}],l.value.type==="custom"&&l.value.customRange&&l.value.customRange.length===2){const r=new Date(l.value.customRange[0]);if((new Date(l.value.customRange[1])-r)/(1e3*60*60)>24){at("小时粒度下日期范围不能超过24小时,已切换到近24小时","warning"),K("last24h");return}}if(["today","7days","30days"].includes(l.value.preset)){K("last24h");return}}else if(l.value.presetOptions=[{value:"today",label:"今日",days:1},{value:"7days",label:"7天",days:7},{value:"30days",label:"30天",days:30}],["last24h","yesterday","dayBefore"].includes(l.value.preset)){K("7days");return}$()}async function $(){let a,r="monthly";if(l.value.type==="preset"){const n=l.value.presetOptions.find(o=>o.value===l.value.preset);g.value==="hour"?(a=1,r="daily"):(a=n?n.days:7,l.value.preset==="today"?r="daily":r="monthly")}else{if(g.value==="hour"){const n=new Date(l.value.customRange[0]),o=new Date(l.value.customRange[1]),c=Math.ceil((o-n)/(1e3*60*60));a=Math.ceil(c/24)||1}else a=z(l.value.customStart,l.value.customEnd);r="daily"}await Promise.all([X(a,g.value),tt(r),Y(R.value)])}function z(a,r){if(!a||!r)return 7;const n=new Date(a),o=new Date(r),c=Math.abs(o-n);return Math.ceil(c/(1e3*60*60*24))||7}function B(a){return a>new Date}return{loading:L,dashboardData:U,costsData:i,modelStats:V,trendData:w,dashboardModelStats:q,apiKeysTrendData:T,dateFilter:l,trendGranularity:g,apiKeysTrendMetric:R,defaultTime:_,formattedUptime:Z,loadDashboardData:J,loadUsageTrend:X,loadModelStats:tt,loadApiKeysTrend:Y,setDateFilterPreset:K,onCustomDateRangeChange:et,setTrendGranularity:N,refreshChartsData:$,disabledDate:B}}),gt={class:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8"},xt={class:"stat-card"},bt={class:"flex items-center justify-between"},vt={class:"text-3xl font-bold text-gray-900"},ht={class:"text-xs text-gray-500 mt-1"},kt={class:"stat-card"},Ct={class:"flex items-center justify-between"},Tt={class:"text-3xl font-bold text-gray-900"},_t={class:"text-xs text-gray-500 mt-1"},Dt={key:0,class:"text-yellow-600"},wt={class:"stat-card"},Rt={class:"flex items-center justify-between"},At={class:"text-3xl font-bold text-gray-900"},St={class:"text-xs text-gray-500 mt-1"},Kt={class:"stat-card"},$t={class:"flex items-center justify-between"},Mt={class:"text-3xl font-bold text-green-600"},Ut={class:"text-xs text-gray-500 mt-1"},Pt={class:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mb-8"},Ft={class:"stat-card"},It={class:"flex items-center justify-between"},Ot={class:"flex-1 mr-8"},qt={class:"flex items-baseline gap-2 mb-2 flex-wrap"},Bt={class:"text-3xl font-bold text-blue-600"},Ht={class:"text-sm text-green-600 font-medium"},Et={class:"text-xs text-gray-500"},jt={class:"flex justify-between items-center flex-wrap gap-x-4"},Lt={class:"font-medium"},Vt={class:"font-medium"},Yt={key:0,class:"text-purple-600"},Nt={class:"font-medium"},zt={key:1,class:"text-purple-600"},Gt={class:"font-medium"},Wt={class:"stat-card"},Qt={class:"flex items-center justify-between"},Zt={class:"flex-1 mr-8"},Jt={class:"flex items-baseline gap-2 mb-2 flex-wrap"},Xt={class:"text-3xl font-bold text-emerald-600"},te={class:"text-sm text-green-600 font-medium"},ee={class:"text-xs text-gray-500"},se={class:"flex justify-between items-center flex-wrap gap-x-4"},ae={class:"font-medium"},oe={class:"font-medium"},ne={key:0,class:"text-purple-600"},le={class:"font-medium"},re={key:1,class:"text-purple-600"},ie={class:"font-medium"},de={class:"stat-card"},ue={class:"flex items-center justify-between"},ce={class:"text-3xl font-bold text-orange-600"},pe={class:"stat-card"},ye={class:"flex items-center justify-between"},me={class:"text-3xl font-bold text-rose-600"},fe={class:"mb-8"},ge={class:"flex items-center justify-between mb-6"},xe={class:"flex gap-2 items-center"},be={class:"flex gap-1 bg-gray-100 rounded-lg p-1"},ve=["onClick"],he={class:"flex gap-1 bg-gray-100 rounded-lg p-1"},ke={class:"flex items-center gap-2"},Ce={key:0,class:"text-xs text-orange-600"},Te={class:"grid grid-cols-1 lg:grid-cols-2 gap-6"},_e={class:"card p-6"},De={class:"relative",style:{height:"300px"}},we={class:"card p-6"},Re={key:0,class:"text-center py-8"},Ae={key:1,class:"overflow-auto max-h-[300px]"},Se={class:"min-w-full"},Ke={class:"divide-y divide-gray-200"},$e={class:"px-4 py-2 text-sm text-gray-900"},Me={class:"px-4 py-2 text-sm text-gray-600 text-right"},Ue={class:"px-4 py-2 text-sm text-gray-600 text-right"},Pe={class:"px-4 py-2 text-sm text-green-600 text-right font-medium"},Fe={class:"px-4 py-2 text-sm font-medium text-right"},Ie={class:"inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"},Oe={class:"mb-8"},qe={class:"card p-6"},Be={style:{height:"300px"}},He={class:"mb-8"},Ee={class:"card p-6"},je={class:"flex items-center justify-between mb-4"},Le={class:"flex gap-1 bg-gray-100 rounded-lg p-1"},Ve={class:"mb-4 text-sm text-gray-600"},Ye={key:0},Ne={key:1},ze={style:{height:"350px"}},Ge={__name:"DashboardView",setup(L){const U=ft(),{dashboardData:i,costsData:V,dashboardModelStats:w,trendData:q,apiKeysTrendData:T,formattedUptime:l,dateFilter:g,trendGranularity:R,apiKeysTrendMetric:_,defaultTime:Z}=ct(U),{loadDashboardData:J,loadUsageTrend:X,loadModelStats:tt,loadApiKeysTrend:Y,setDateFilterPreset:K,onCustomDateRangeChange:et,setTrendGranularity:N,refreshChartsData:$,disabledDate:z}=U,B=k(null),a=k(null),r=k(null);let n=null,o=null,c=null;function m(p){return p>=1e6?(p/1e6).toFixed(2)+"M":p>=1e3?(p/1e3).toFixed(2)+"K":p.toString()}function C(p,e){if(!e||e.length===0)return 0;const f=e.reduce((u,A)=>u+A.allTokens,0);return f===0?0:(p/f*100).toFixed(1)}function G(){if(!B.value)return;n&&n.destroy();const p=w.value||[],e={labels:p.map(f=>f.model),datasets:[{data:p.map(f=>f.allTokens),backgroundColor:["#3B82F6","#10B981","#F59E0B","#EF4444","#8B5CF6","#EC4899","#14B8A6","#F97316","#6366F1","#84CC16"],borderWidth:0}]};n=new ot(B.value,{type:"doughnut",data:e,options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"bottom",labels:{padding:15,usePointStyle:!0,font:{size:12}}},tooltip:{callbacks:{label:function(f){const u=f.label||"",A=m(f.parsed),P=C(f.parsed,p);return`${u}: ${A} (${P}%)`}}}}}})}function W(){if(!a.value)return;o&&o.destroy();const p=q.value||[],e=p.map(y=>y.inputTokens||0),f=p.map(y=>y.outputTokens||0),u=p.map(y=>y.cacheCreateTokens||0),A=p.map(y=>y.cacheReadTokens||0),P=p.map(y=>y.requests||0),x=p.map(y=>y.cost||0),F={labels:p.map(y=>y.date),datasets:[{label:"输入Token",data:e,borderColor:"rgb(102, 126, 234)",backgroundColor:"rgba(102, 126, 234, 0.1)",tension:.3},{label:"输出Token",data:f,borderColor:"rgb(240, 147, 251)",backgroundColor:"rgba(240, 147, 251, 0.1)",tension:.3},{label:"缓存创建Token",data:u,borderColor:"rgb(59, 130, 246)",backgroundColor:"rgba(59, 130, 246, 0.1)",tension:.3},{label:"缓存读取Token",data:A,borderColor:"rgb(147, 51, 234)",backgroundColor:"rgba(147, 51, 234, 0.1)",tension:.3},{label:"费用 (USD)",data:x,borderColor:"rgb(34, 197, 94)",backgroundColor:"rgba(34, 197, 94, 0.1)",tension:.3,yAxisID:"y2"},{label:"请求数",data:P,borderColor:"rgb(16, 185, 129)",backgroundColor:"rgba(16, 185, 129, 0.1)",tension:.3,yAxisID:"y1"}]};o=new ot(a.value,{type:"line",data:F,options:{responsive:!0,maintainAspectRatio:!1,interaction:{mode:"index",intersect:!1},plugins:{title:{display:!0,text:"Token使用趋势",font:{size:16,weight:"bold"}},legend:{position:"top"},tooltip:{mode:"index",intersect:!1,callbacks:{label:function(y){const D=y.dataset.label||"";let S=y.parsed.y;return D==="费用 (USD)"?S<.01?D+": $"+S.toFixed(6):D+": $"+S.toFixed(4):D==="请求数"?D+": "+S.toLocaleString()+" 次":D+": "+S.toLocaleString()+" tokens"}}}},scales:{x:{type:"category",display:!0,title:{display:!0,text:R.value==="hour"?"时间":"日期"}},y:{type:"linear",display:!0,position:"left",title:{display:!0,text:"Token数量"},ticks:{callback:function(y){return m(y)}}},y1:{type:"linear",display:!0,position:"right",title:{display:!0,text:"请求数"},grid:{drawOnChartArea:!1},ticks:{callback:function(y){return y.toLocaleString()}}},y2:{type:"linear",display:!1,position:"right"}}}})}function H(){var P;if(!r.value)return;c&&c.destroy();const p=T.value.data||[],e=_.value,f=["#3B82F6","#10B981","#F59E0B","#EF4444","#8B5CF6","#EC4899","#14B8A6","#F97316","#6366F1","#84CC16"],u=((P=T.value.topApiKeys)==null?void 0:P.map((x,F)=>{var S,nt;const y=T.value.data.map(M=>!M.apiKeys||!M.apiKeys[x]?0:e==="tokens"?M.apiKeys[x].tokens:M.apiKeys[x].requests||0);return{label:((nt=(S=T.value.data.find(M=>M.apiKeys&&M.apiKeys[x]))==null?void 0:S.apiKeys[x])==null?void 0:nt.name)||`API Key ${x}`,data:y,borderColor:f[F%f.length],backgroundColor:f[F%f.length]+"20",tension:.4,fill:!1}}))||[],A={labels:p.map(x=>x.date),datasets:u};c=new ot(r.value,{type:"line",data:A,options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{position:"bottom",labels:{padding:20,usePointStyle:!0,font:{size:12}}},tooltip:{mode:"index",intersect:!1,callbacks:{label:function(x){const F=x.dataset.label||"",y=x.parsed.y,D=_.value==="tokens"?" tokens":" 次";return F+": "+y.toLocaleString()+D}}}},scales:{x:{type:"category",display:!0,title:{display:!0,text:R.value==="hour"?"时间":"日期"}},y:{beginAtZero:!0,title:{display:!0,text:_.value==="tokens"?"Token 数量":"请求次数"},ticks:{callback:function(x){return m(x)}}}}}})}async function Q(){await Y(_.value),await E(),H()}return st(w,()=>{E(()=>G())}),st(q,()=>{E(()=>W())}),st(T,()=>{E(()=>H())}),pt(async()=>{await Promise.all([J(),$()]),await E(),G(),W(),H()}),(p,e)=>{const f=it;return h(),b("div",null,[t("div",gt,[t("div",xt,[t("div",bt,[t("div",null,[e[6]||(e[6]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"总API Keys",-1)),t("p",vt,d(s(i).totalApiKeys),1),t("p",ht,"活跃: "+d(s(i).activeApiKeys||0),1)]),e[7]||(e[7]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-blue-500 to-blue-600"},[t("i",{class:"fas fa-key"})],-1))])]),t("div",kt,[t("div",Ct,[t("div",null,[e[8]||(e[8]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"服务账户",-1)),t("p",Tt,d(s(i).totalAccounts),1),t("p",_t,[v(" 活跃: "+d(s(i).activeAccounts||0)+" ",1),s(i).rateLimitedAccounts>0?(h(),b("span",Dt," | 限流: "+d(s(i).rateLimitedAccounts),1)):I("",!0)])]),e[9]||(e[9]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-green-500 to-green-600"},[t("i",{class:"fas fa-user-circle"})],-1))])]),t("div",wt,[t("div",Rt,[t("div",null,[e[10]||(e[10]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"今日请求",-1)),t("p",At,d(s(i).todayRequests),1),t("p",St,"总请求: "+d(m(s(i).totalRequests||0)),1)]),e[11]||(e[11]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-purple-500 to-purple-600"},[t("i",{class:"fas fa-chart-line"})],-1))])]),t("div",Kt,[t("div",$t,[t("div",null,[e[12]||(e[12]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"系统状态",-1)),t("p",Mt,d(s(i).systemStatus),1),t("p",Ut,"运行时间: "+d(s(l)),1)]),e[13]||(e[13]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-yellow-500 to-orange-500"},[t("i",{class:"fas fa-heartbeat"})],-1))])])]),t("div",Pt,[t("div",Ft,[t("div",It,[t("div",Ot,[e[18]||(e[18]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"今日Token",-1)),t("div",qt,[t("p",Bt,d(m((s(i).todayInputTokens||0)+(s(i).todayOutputTokens||0)+(s(i).todayCacheCreateTokens||0)+(s(i).todayCacheReadTokens||0))),1),t("span",Ht,"/ "+d(s(V).todayCosts.formatted.totalCost),1)]),t("div",Et,[t("div",jt,[t("span",null,[e[14]||(e[14]=v("输入: ",-1)),t("span",Lt,d(m(s(i).todayInputTokens||0)),1)]),t("span",null,[e[15]||(e[15]=v("输出: ",-1)),t("span",Vt,d(m(s(i).todayOutputTokens||0)),1)]),(s(i).todayCacheCreateTokens||0)>0?(h(),b("span",Yt,[e[16]||(e[16]=v("缓存创建: ",-1)),t("span",Nt,d(m(s(i).todayCacheCreateTokens||0)),1)])):I("",!0),(s(i).todayCacheReadTokens||0)>0?(h(),b("span",zt,[e[17]||(e[17]=v("缓存读取: ",-1)),t("span",Gt,d(m(s(i).todayCacheReadTokens||0)),1)])):I("",!0)])])]),e[19]||(e[19]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-indigo-500 to-indigo-600"},[t("i",{class:"fas fa-coins"})],-1))])]),t("div",Wt,[t("div",Qt,[t("div",Zt,[e[24]||(e[24]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"总Token消耗",-1)),t("div",Jt,[t("p",Xt,d(m((s(i).totalInputTokens||0)+(s(i).totalOutputTokens||0)+(s(i).totalCacheCreateTokens||0)+(s(i).totalCacheReadTokens||0))),1),t("span",te,"/ "+d(s(V).totalCosts.formatted.totalCost),1)]),t("div",ee,[t("div",se,[t("span",null,[e[20]||(e[20]=v("输入: ",-1)),t("span",ae,d(m(s(i).totalInputTokens||0)),1)]),t("span",null,[e[21]||(e[21]=v("输出: ",-1)),t("span",oe,d(m(s(i).totalOutputTokens||0)),1)]),(s(i).totalCacheCreateTokens||0)>0?(h(),b("span",ne,[e[22]||(e[22]=v("缓存创建: ",-1)),t("span",le,d(m(s(i).totalCacheCreateTokens||0)),1)])):I("",!0),(s(i).totalCacheReadTokens||0)>0?(h(),b("span",re,[e[23]||(e[23]=v("缓存读取: ",-1)),t("span",ie,d(m(s(i).totalCacheReadTokens||0)),1)])):I("",!0)])])]),e[25]||(e[25]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-emerald-500 to-emerald-600"},[t("i",{class:"fas fa-database"})],-1))])]),t("div",de,[t("div",ue,[t("div",null,[e[26]||(e[26]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"平均RPM",-1)),t("p",ce,d(s(i).systemRPM||0),1),e[27]||(e[27]=t("p",{class:"text-xs text-gray-500 mt-1"},"每分钟请求数",-1))]),e[28]||(e[28]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-orange-500 to-orange-600"},[t("i",{class:"fas fa-tachometer-alt"})],-1))])]),t("div",pe,[t("div",ye,[t("div",null,[e[29]||(e[29]=t("p",{class:"text-sm font-semibold text-gray-600 mb-1"},"平均TPM",-1)),t("p",me,d(s(i).systemTPM||0),1),e[30]||(e[30]=t("p",{class:"text-xs text-gray-500 mt-1"},"每分钟Token数",-1))]),e[31]||(e[31]=t("div",{class:"stat-icon flex-shrink-0 bg-gradient-to-br from-rose-500 to-rose-600"},[t("i",{class:"fas fa-rocket"})],-1))])])]),t("div",fe,[t("div",ge,[e[36]||(e[36]=t("h3",{class:"text-xl font-bold text-gray-900"},"模型使用分布与Token使用趋势",-1)),t("div",xe,[t("div",be,[(h(!0),b(lt,null,rt(s(g).presetOptions,u=>(h(),b("button",{key:u.value,onClick:A=>s(K)(u.value),class:j(["px-3 py-1 rounded-md text-sm font-medium transition-colors",s(g).preset===u.value&&s(g).type==="preset"?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:text-gray-900"])},d(u.label),11,ve))),128))]),t("div",he,[t("button",{onClick:e[0]||(e[0]=u=>s(N)("day")),class:j(["px-3 py-1 rounded-md text-sm font-medium transition-colors",s(R)==="day"?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:text-gray-900"])},e[32]||(e[32]=[t("i",{class:"fas fa-calendar-day mr-1"},null,-1),v("按天 ",-1)]),2),t("button",{onClick:e[1]||(e[1]=u=>s(N)("hour")),class:j(["px-3 py-1 rounded-md text-sm font-medium transition-colors",s(R)==="hour"?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:text-gray-900"])},e[33]||(e[33]=[t("i",{class:"fas fa-clock mr-1"},null,-1),v("按小时 ",-1)]),2)]),t("div",ke,[yt(f,{"default-time":s(Z),modelValue:s(g).customRange,"onUpdate:modelValue":e[2]||(e[2]=u=>s(g).customRange=u),type:"datetimerange","range-separator":"至","start-placeholder":"开始日期","end-placeholder":"结束日期",format:"YYYY-MM-DD HH:mm:ss","value-format":"YYYY-MM-DD HH:mm:ss",onChange:s(et),"disabled-date":s(z),size:"default",style:{width:"400px"},class:"custom-date-picker"},null,8,["default-time","modelValue","onChange","disabled-date"]),s(R)==="hour"?(h(),b("span",Ce,e[34]||(e[34]=[t("i",{class:"fas fa-info-circle"},null,-1),v(" 最多24小时 ",-1)]))):I("",!0)]),t("button",{onClick:e[3]||(e[3]=u=>s($)()),class:"btn btn-primary px-4 py-2 flex items-center gap-2"},e[35]||(e[35]=[t("i",{class:"fas fa-sync-alt"},null,-1),v("刷新 ",-1)]))])]),t("div",Te,[t("div",_e,[e[37]||(e[37]=t("h4",{class:"text-lg font-semibold text-gray-800 mb-4"},"Token使用分布",-1)),t("div",De,[t("canvas",{ref_key:"modelUsageChart",ref:B},null,512)])]),t("div",we,[e[40]||(e[40]=t("h4",{class:"text-lg font-semibold text-gray-800 mb-4"},"详细统计数据",-1)),s(w).length===0?(h(),b("div",Re,e[38]||(e[38]=[t("p",{class:"text-gray-500"},"暂无模型使用数据",-1)]))):(h(),b("div",Ae,[t("table",Se,[e[39]||(e[39]=t("thead",{class:"bg-gray-50 sticky top-0"},[t("tr",null,[t("th",{class:"px-4 py-2 text-left text-xs font-medium text-gray-700"},"模型"),t("th",{class:"px-4 py-2 text-right text-xs font-medium text-gray-700"},"请求数"),t("th",{class:"px-4 py-2 text-right text-xs font-medium text-gray-700"},"总Token"),t("th",{class:"px-4 py-2 text-right text-xs font-medium text-gray-700"},"费用"),t("th",{class:"px-4 py-2 text-right text-xs font-medium text-gray-700"},"占比")])],-1)),t("tbody",Ke,[(h(!0),b(lt,null,rt(s(w),u=>(h(),b("tr",{key:u.model,class:"hover:bg-gray-50"},[t("td",$e,d(u.model),1),t("td",Me,d(m(u.requests)),1),t("td",Ue,d(m(u.allTokens)),1),t("td",Pe,d(u.formatted?u.formatted.total:"$0.000000"),1),t("td",Fe,[t("span",Ie,d(C(u.allTokens,s(w)))+"% ",1)])]))),128))])])]))])])]),t("div",Oe,[t("div",qe,[t("div",Be,[t("canvas",{ref_key:"usageTrendChart",ref:a},null,512)])])]),t("div",He,[t("div",Ee,[t("div",je,[e[43]||(e[43]=t("h3",{class:"text-lg font-semibold text-gray-900"},"API Keys 使用趋势",-1)),t("div",Le,[t("button",{onClick:e[4]||(e[4]=u=>{_.value="requests",Q()}),class:j(["px-3 py-1 rounded-md text-sm font-medium transition-colors",s(_)==="requests"?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:text-gray-900"])},e[41]||(e[41]=[t("i",{class:"fas fa-exchange-alt mr-1"},null,-1),v("请求次数 ",-1)]),2),t("button",{onClick:e[5]||(e[5]=u=>{_.value="tokens",Q()}),class:j(["px-3 py-1 rounded-md text-sm font-medium transition-colors",s(_)==="tokens"?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:text-gray-900"])},e[42]||(e[42]=[t("i",{class:"fas fa-coins mr-1"},null,-1),v("Token 数量 ",-1)]),2)])]),t("div",Ve,[s(T).totalApiKeys>10?(h(),b("span",Ye," 共 "+d(s(T).totalApiKeys)+" 个 API Key,显示使用量前 10 个 ",1)):(h(),b("span",Ne," 共 "+d(s(T).totalApiKeys)+" 个 API Key ",1))]),t("div",ze,[t("canvas",{ref_key:"apiKeysUsageTrendChart",ref:r},null,512)])])])])}}},es=mt(Ge,[["__scopeId","data-v-5170898f"]]);export{es as default}; diff --git a/web/admin-spa/dist/assets/LoginView-DCXHRAtT.js b/web/admin-spa/dist/assets/LoginView-DCXHRAtT.js deleted file mode 100644 index a1edfb3e..00000000 --- a/web/admin-spa/dist/assets/LoginView-DCXHRAtT.js +++ /dev/null @@ -1 +0,0 @@ -import{c as b,r as x,q as f,x as a,z as s,L as i,Q as y,u as o,P as m,Y as _,K as u,aq as c,O as g,y as n}from"./vue-vendor-CKToUHZx.js";import{_ as v,u as w}from"./index-DhITICXu.js";/* empty css */import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";const h={class:"flex items-center justify-center min-h-screen p-6"},k={class:"glass-strong rounded-3xl p-10 w-full max-w-md shadow-2xl"},L={class:"text-center mb-8"},S={class:"w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-2xl flex items-center justify-center backdrop-blur-sm overflow-hidden"},V=["src"],I={key:1,class:"fas fa-cloud text-3xl text-gray-700"},N={key:1,class:"w-12 h-12 bg-gray-300/50 rounded animate-pulse"},q={key:0,class:"text-3xl font-bold text-white mb-2 header-title"},D={key:1,class:"h-9 w-64 bg-gray-300/50 rounded animate-pulse mx-auto mb-2"},E=["disabled"],j={key:0,class:"fas fa-sign-in-alt mr-2"},B={key:1,class:"loading-spinner mr-2"},M={key:0,class:"mt-6 p-4 bg-red-500/20 border border-red-500/30 rounded-xl text-red-800 text-sm text-center backdrop-blur-sm"},F={__name:"LoginView",setup(O){const e=w(),d=b(()=>e.oemLoading),l=x({username:"",password:""});f(()=>{e.loadOemSettings()});const p=async()=>{await e.login(l.value)};return(T,t)=>(n(),a("div",h,[s("div",k,[s("div",L,[s("div",S,[d.value?(n(),a("div",N)):(n(),a(y,{key:0},[o(e).oemSettings.siteIconData||o(e).oemSettings.siteIcon?(n(),a("img",{key:0,src:o(e).oemSettings.siteIconData||o(e).oemSettings.siteIcon,alt:"Logo",class:"w-12 h-12 object-contain",onError:t[0]||(t[0]=r=>r.target.style.display="none")},null,40,V)):(n(),a("i",I))],64))]),!d.value&&o(e).oemSettings.siteName?(n(),a("h1",q,m(o(e).oemSettings.siteName),1)):d.value?(n(),a("div",D)):i("",!0),t[3]||(t[3]=s("p",{class:"text-gray-600 text-lg"},"管理后台",-1))]),s("form",{onSubmit:_(p,["prevent"]),class:"space-y-6"},[s("div",null,[t[4]||(t[4]=s("label",{class:"block text-sm font-semibold text-gray-900 mb-3"},"用户名",-1)),u(s("input",{"onUpdate:modelValue":t[1]||(t[1]=r=>l.value.username=r),type:"text",required:"",class:"form-input w-full",placeholder:"请输入用户名"},null,512),[[c,l.value.username]])]),s("div",null,[t[5]||(t[5]=s("label",{class:"block text-sm font-semibold text-gray-900 mb-3"},"密码",-1)),u(s("input",{"onUpdate:modelValue":t[2]||(t[2]=r=>l.value.password=r),type:"password",required:"",class:"form-input w-full",placeholder:"请输入密码"},null,512),[[c,l.value.password]])]),s("button",{type:"submit",disabled:o(e).loginLoading,class:"btn btn-primary w-full py-4 px-6 text-lg font-semibold"},[o(e).loginLoading?i("",!0):(n(),a("i",j)),o(e).loginLoading?(n(),a("div",B)):i("",!0),g(" "+m(o(e).loginLoading?"登录中...":"登录"),1)],8,E)],32),o(e).loginError?(n(),a("div",M,[t[6]||(t[6]=s("i",{class:"fas fa-exclamation-triangle mr-2"},null,-1)),g(m(o(e).loginError),1)])):i("",!0)])]))}},P=v(F,[["__scopeId","data-v-4a19afbe"]]);export{P as default}; diff --git a/web/admin-spa/dist/assets/LogoTitle-CIeppYUK.js b/web/admin-spa/dist/assets/LogoTitle-CIeppYUK.js deleted file mode 100644 index 33cde9b9..00000000 --- a/web/admin-spa/dist/assets/LogoTitle-CIeppYUK.js +++ /dev/null @@ -1 +0,0 @@ -/* empty css */import{_ as r}from"./index-DhITICXu.js";import{x as t,y as s,z as l,Q as d,L as o,A as c,C as g,P as i}from"./vue-vendor-CKToUHZx.js";const u={class:"flex items-center gap-4"},f={class:"w-12 h-12 bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-gray-300/30 rounded-xl flex items-center justify-center backdrop-blur-sm flex-shrink-0 overflow-hidden"},y=["src"],m={key:1,class:"fas fa-cloud text-xl text-gray-700"},h={key:1,class:"w-8 h-8 bg-gray-300/50 rounded animate-pulse"},x={class:"flex flex-col justify-center min-h-[48px]"},b={class:"flex items-center gap-3"},k={key:1,class:"h-8 w-64 bg-gray-300/50 rounded animate-pulse"},_={key:0,class:"text-gray-600 text-sm leading-tight mt-0.5"},S={__name:"LogoTitle",props:{loading:{type:Boolean,default:!1},title:{type:String,default:""},subtitle:{type:String,default:""},logoSrc:{type:String,default:""},titleClass:{type:String,default:"text-gray-900"}},setup(e){const n=a=>{a.target.style.display="none"};return(a,p)=>(s(),t("div",u,[l("div",f,[e.loading?(s(),t("div",h)):(s(),t(d,{key:0},[e.logoSrc?(s(),t("img",{key:0,src:e.logoSrc,alt:"Logo",class:"w-8 h-8 object-contain",onError:n},null,40,y)):(s(),t("i",m))],64))]),l("div",x,[l("div",b,[!e.loading&&e.title?(s(),t("h1",{key:0,class:g(["text-2xl font-bold header-title leading-tight",e.titleClass])},i(e.title),3)):e.loading?(s(),t("div",k)):o("",!0),c(a.$slots,"after-title",{},void 0,!0)]),e.subtitle?(s(),t("p",_,i(e.subtitle),1)):o("",!0)])]))}},C=r(S,[["__scopeId","data-v-a75bf797"]]);export{C as L}; diff --git a/web/admin-spa/dist/assets/MainLayout-Df9D9BdR.js b/web/admin-spa/dist/assets/MainLayout-Df9D9BdR.js deleted file mode 100644 index 485d4549..00000000 --- a/web/admin-spa/dist/assets/MainLayout-Df9D9BdR.js +++ /dev/null @@ -1 +0,0 @@ -import{c as M,r as w,_ as O,q as E,V as R,x as o,y as n,z as t,L as T,R as y,J as U,P as b,C as V,Y as $,O as v,T as D,K as k,aq as _,Q as N,aT as q,ac as F,o as J,av as H,aU as Q,I as A,aV as Y,M as G}from"./vue-vendor-CKToUHZx.js";import{_ as L,u as W,a as j}from"./index-DhITICXu.js";import{s as g}from"./toast-BvwA7Mwb.js";import{L as X}from"./LogoTitle-CIeppYUK.js";import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";/* empty css */const Z={class:"glass-strong rounded-3xl p-6 mb-8 shadow-xl",style:{"z-index":"10",position:"relative"}},ee={class:"flex flex-col md:flex-row justify-between items-center gap-4"},te={class:"flex items-center gap-4"},se={class:"flex items-center gap-2"},ae={class:"text-sm text-gray-400 font-mono"},ne=["href"],oe={class:"relative user-menu-container"},le={class:"px-4 py-3 border-b border-gray-100"},re={class:"flex items-center justify-between text-sm"},ie={class:"font-mono text-gray-700"},ue={key:0,class:"mt-2"},de={class:"flex items-center justify-between text-sm mb-2"},ce={class:"font-mono text-green-600"},me=["href"],fe={key:1,class:"mt-2 text-center text-xs text-gray-500"},pe={key:2,class:"mt-2 text-center"},ve={key:"message",class:"px-3 py-1.5 bg-green-100 border border-green-200 rounded-lg inline-block"},xe={key:0,class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},ge={class:"modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col"},be=["value"],ye={class:"flex gap-3 pt-4"},we=["disabled"],he={key:0,class:"loading-spinner mr-2"},ke={key:1,class:"fas fa-save mr-2"},_e={__name:"AppHeader",setup(C){const x=q(),d=W(),m=M(()=>d.user||{username:"Admin"}),l=M(()=>d.oemSettings||{}),h=M(()=>d.oemLoading),s=w({current:"...",latest:"",hasUpdate:!1,checkingUpdate:!1,lastChecked:null,releaseInfo:null,noUpdateMessage:!1}),r=w(!1),f=w(!1),p=w(!1),a=O({currentPassword:"",newPassword:"",confirmPassword:"",newUsername:""}),P=async()=>{if(!s.value.checkingUpdate){s.value.checkingUpdate=!0;try{const u=await j.get("/admin/check-updates");if(u.success){const e=u.data;s.value.current=e.current,s.value.latest=e.latest,s.value.hasUpdate=e.hasUpdate,s.value.releaseInfo=e.releaseInfo,s.value.lastChecked=new Date,localStorage.setItem("versionInfo",JSON.stringify({current:e.current,latest:e.latest,lastChecked:s.value.lastChecked,hasUpdate:e.hasUpdate,releaseInfo:e.releaseInfo})),e.hasUpdate||(s.value.noUpdateMessage=!0,setTimeout(()=>{s.value.noUpdateMessage=!1},3e3))}}catch(u){console.error("Error checking for updates:",u);const e=localStorage.getItem("versionInfo");if(e){const c=JSON.parse(e);s.value.current=c.current||s.value.current,s.value.latest=c.latest,s.value.hasUpdate=c.hasUpdate,s.value.releaseInfo=c.releaseInfo,s.value.lastChecked=new Date(c.lastChecked)}}finally{s.value.checkingUpdate=!1}}},z=()=>{a.currentPassword="",a.newPassword="",a.confirmPassword="",a.newUsername="",f.value=!0,r.value=!1},I=()=>{f.value=!1},B=async()=>{if(a.newPassword!==a.confirmPassword){g("两次输入的密码不一致","error");return}if(a.newPassword.length<8){g("新密码长度至少8位","error");return}p.value=!0;try{const u=await j.post("/admin/change-password",{currentPassword:a.currentPassword,newPassword:a.newPassword,newUsername:a.newUsername||void 0});if(u.success){const e=a.newUsername?"账户信息修改成功,请重新登录":"密码修改成功,请重新登录";g(e,"success"),I(),setTimeout(()=>{d.logout(),x.push("/login")},1500)}else g(u.message||"修改失败","error")}catch{g("修改密码失败","error")}finally{p.value=!1}},K=()=>{confirm("确定要退出登录吗?")&&(d.logout(),x.push("/login"),g("已安全退出","success")),r.value=!1},S=u=>{!u.target.closest(".user-menu-container")&&r.value&&(r.value=!1)};return E(()=>{P(),setInterval(()=>{P()},36e5),document.addEventListener("click",S)}),R(()=>{document.removeEventListener("click",S)}),(u,e)=>{var c;return n(),o(N,null,[t("div",Z,[t("div",ee,[t("div",te,[y(X,{loading:h.value,title:l.value.siteName,subtitle:"管理后台","logo-src":l.value.siteIconData||l.value.siteIcon,"title-class":"text-white"},{"after-title":U(()=>{var i;return[t("div",se,[t("span",ae,"v"+b(s.value.current||"..."),1),s.value.hasUpdate?(n(),o("a",{key:0,href:((i=s.value.releaseInfo)==null?void 0:i.htmlUrl)||"#",target:"_blank",class:"inline-flex items-center gap-1 px-2 py-0.5 bg-green-500 border border-green-600 rounded-full text-xs text-white hover:bg-green-600 transition-colors animate-pulse",title:"有新版本可用"},e[7]||(e[7]=[t("i",{class:"fas fa-arrow-up text-[10px]"},null,-1),t("span",null,"新版本",-1)]),8,ne)):T("",!0)])]}),_:1},8,["loading","title","logo-src"])]),t("div",oe,[t("button",{onClick:e[0]||(e[0]=i=>r.value=!r.value),class:"btn btn-primary px-4 py-3 flex items-center gap-2 relative"},[e[8]||(e[8]=t("i",{class:"fas fa-user-circle"},null,-1)),t("span",null,b(m.value.username||"Admin"),1),t("i",{class:V(["fas fa-chevron-down text-xs transition-transform duration-200",{"rotate-180":r.value}])},null,2)]),r.value?(n(),o("div",{key:0,class:"absolute right-0 top-full mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 py-2 user-menu-dropdown",style:{"z-index":"999999"},onClick:e[2]||(e[2]=$(()=>{},["stop"]))},[t("div",le,[t("div",re,[e[9]||(e[9]=t("span",{class:"text-gray-500"},"当前版本",-1)),t("span",ie,"v"+b(s.value.current||"..."),1)]),s.value.hasUpdate?(n(),o("div",ue,[t("div",de,[e[10]||(e[10]=t("span",{class:"text-green-600 font-medium"},[t("i",{class:"fas fa-arrow-up mr-1"}),v("有新版本 ")],-1)),t("span",ce,"v"+b(s.value.latest),1)]),t("a",{href:((c=s.value.releaseInfo)==null?void 0:c.htmlUrl)||"#",target:"_blank",class:"block w-full text-center px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600 transition-colors"},e[11]||(e[11]=[t("i",{class:"fas fa-external-link-alt mr-1"},null,-1),v("查看更新 ",-1)]),8,me)])):s.value.checkingUpdate?(n(),o("div",fe,e[12]||(e[12]=[t("i",{class:"fas fa-spinner fa-spin mr-1"},null,-1),v("检查更新中... ",-1)]))):(n(),o("div",pe,[y(D,{name:"fade",mode:"out-in"},{default:U(()=>[s.value.noUpdateMessage?(n(),o("div",ve,e[13]||(e[13]=[t("p",{class:"text-xs text-green-700 font-medium"},[t("i",{class:"fas fa-check-circle mr-1"}),v("当前已是最新版本 ")],-1)]))):(n(),o("button",{key:"button",onClick:e[1]||(e[1]=i=>P()),class:"text-xs text-blue-500 hover:text-blue-700 transition-colors"},e[14]||(e[14]=[t("i",{class:"fas fa-sync-alt mr-1"},null,-1),v("检查更新 ",-1)])))]),_:1})]))]),t("button",{onClick:z,class:"w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"},e[15]||(e[15]=[t("i",{class:"fas fa-key text-blue-500"},null,-1),t("span",null,"修改账户信息",-1)])),e[17]||(e[17]=t("hr",{class:"my-2 border-gray-200"},null,-1)),t("button",{onClick:K,class:"w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"},e[16]||(e[16]=[t("i",{class:"fas fa-sign-out-alt text-red-500"},null,-1),t("span",null,"退出登录",-1)]))])):T("",!0)])])]),f.value?(n(),o("div",xe,[t("div",ge,[t("div",{class:"flex items-center justify-between mb-6"},[e[19]||(e[19]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-key text-white"})]),t("h3",{class:"text-xl font-bold text-gray-900"},"修改账户信息")],-1)),t("button",{onClick:I,class:"text-gray-400 hover:text-gray-600 transition-colors"},e[18]||(e[18]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),t("form",{onSubmit:$(B,["prevent"]),class:"space-y-6 modal-scroll-content custom-scrollbar flex-1"},[t("div",null,[e[20]||(e[20]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"当前用户名",-1)),t("input",{value:m.value.username||"Admin",type:"text",disabled:"",class:"form-input w-full bg-gray-100 cursor-not-allowed"},null,8,be),e[21]||(e[21]=t("p",{class:"text-xs text-gray-500 mt-2"},"当前用户名,输入新用户名以修改",-1))]),t("div",null,[e[22]||(e[22]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"新用户名",-1)),k(t("input",{"onUpdate:modelValue":e[3]||(e[3]=i=>a.newUsername=i),type:"text",class:"form-input w-full",placeholder:"输入新用户名(留空保持不变)"},null,512),[[_,a.newUsername]]),e[23]||(e[23]=t("p",{class:"text-xs text-gray-500 mt-2"},"留空表示不修改用户名",-1))]),t("div",null,[e[24]||(e[24]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"当前密码",-1)),k(t("input",{"onUpdate:modelValue":e[4]||(e[4]=i=>a.currentPassword=i),type:"password",required:"",class:"form-input w-full",placeholder:"请输入当前密码"},null,512),[[_,a.currentPassword]])]),t("div",null,[e[25]||(e[25]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"新密码",-1)),k(t("input",{"onUpdate:modelValue":e[5]||(e[5]=i=>a.newPassword=i),type:"password",required:"",class:"form-input w-full",placeholder:"请输入新密码"},null,512),[[_,a.newPassword]]),e[26]||(e[26]=t("p",{class:"text-xs text-gray-500 mt-2"},"密码长度至少8位",-1))]),t("div",null,[e[27]||(e[27]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"确认新密码",-1)),k(t("input",{"onUpdate:modelValue":e[6]||(e[6]=i=>a.confirmPassword=i),type:"password",required:"",class:"form-input w-full",placeholder:"请再次输入新密码"},null,512),[[_,a.confirmPassword]])]),t("div",ye,[t("button",{type:"button",onClick:I,class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),t("button",{type:"submit",disabled:p.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[p.value?(n(),o("div",he)):(n(),o("i",ke)),v(" "+b(p.value?"保存中...":"保存修改"),1)],8,we)])],32)])])):T("",!0)],64)}}},Ue=L(_e,[["__scopeId","data-v-9c2dcb55"]]),Ce={class:"flex flex-wrap gap-2 mb-6 bg-white/10 rounded-2xl p-2 backdrop-blur-sm"},Pe=["onClick"],Ie={__name:"TabBar",props:{activeTab:{type:String,required:!0}},emits:["tab-change"],setup(C){const x=[{key:"dashboard",name:"仪表板",icon:"fas fa-tachometer-alt"},{key:"apiKeys",name:"API Keys",icon:"fas fa-key"},{key:"accounts",name:"账户管理",icon:"fas fa-user-circle"},{key:"tutorial",name:"使用教程",icon:"fas fa-graduation-cap"},{key:"settings",name:"其他设置",icon:"fas fa-cogs"}];return(d,m)=>(n(),o("div",Ce,[(n(),o(N,null,F(x,l=>t("button",{key:l.key,onClick:h=>d.$emit("tab-change",l.key),class:V(["tab-btn flex-1 py-3 px-6 text-sm font-semibold transition-all duration-300",C.activeTab===l.key?"active":"text-gray-700 hover:bg-white/10 hover:text-gray-900"])},[t("i",{class:V(l.icon+" mr-2")},null,2),v(b(l.name),1)],10,Pe)),64))]))}},Me=L(Ie,[["__scopeId","data-v-0593e69b"]]),Te={class:"min-h-screen p-6"},Ve={class:"glass-strong rounded-3xl p-6 shadow-xl",style:{"z-index":"1","min-height":"calc(100vh - 240px)"}},Le={class:"tab-content"},Se={__name:"MainLayout",setup(C){const x=Q(),d=q(),m=w("dashboard"),l={dashboard:"/dashboard",apiKeys:"/api-keys",accounts:"/accounts",tutorial:"/tutorial",settings:"/settings"};J(()=>x.path,s=>{const r=Object.keys(l).find(f=>l[f]===s);r&&(m.value=r)},{immediate:!0});const h=s=>{m.value=s,d.push(l[s])};return(s,r)=>{const f=H("router-view");return n(),o("div",Te,[y(Ue),t("div",Ve,[y(Me,{"active-tab":m.value,onTabChange:h},null,8,["active-tab"]),t("div",Le,[y(f,null,{default:U(({Component:p})=>[y(D,{name:"slide-up",mode:"out-in"},{default:U(()=>[(n(),A(Y,{include:["DashboardView","ApiKeysView"]},[(n(),A(G(p)))],1024))]),_:2},1024)]),_:1})])])])}}},Be=L(Se,[["__scopeId","data-v-92ae9a03"]]);export{Be as default}; diff --git a/web/admin-spa/dist/assets/SettingsView-BD-f8NQ2.js b/web/admin-spa/dist/assets/SettingsView-BD-f8NQ2.js deleted file mode 100644 index f9de7861..00000000 --- a/web/admin-spa/dist/assets/SettingsView-BD-f8NQ2.js +++ /dev/null @@ -1,3 +0,0 @@ -import{aR as k,r as x,aW as C,q as O,x as m,z as e,u as i,K as T,aq as N,L as _,O as v,C as j,P as S,y as g}from"./vue-vendor-CKToUHZx.js";import{s as c}from"./toast-BvwA7Mwb.js";import{a as D,_ as F}from"./index-DhITICXu.js";import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";const E=k("settings",()=>{const l=x({siteName:"Claude Relay Service",siteIcon:"",siteIconData:"",updatedAt:null}),r=x(!1),p=x(!1),d=async()=>{r.value=!0;try{const s=await D.get("/admin/oem-settings");return s&&s.success&&(l.value={...l.value,...s.data},f()),s}catch(s){throw console.error("Failed to load OEM settings:",s),s}finally{r.value=!1}},a=async s=>{p.value=!0;try{const o=await D.put("/admin/oem-settings",s);return o&&o.success&&(l.value={...l.value,...o.data},f()),o}catch(o){throw console.error("Failed to save OEM settings:",o),o}finally{p.value=!1}},w=async()=>{const s={siteName:"Claude Relay Service",siteIcon:"",siteIconData:"",updatedAt:null};return l.value={...s},await a(s)},f=()=>{if(l.value.siteName&&(document.title=`${l.value.siteName} - 管理后台`),l.value.siteIconData||l.value.siteIcon){const s=document.querySelector('link[rel="icon"]')||document.createElement("link");s.rel="icon",s.href=l.value.siteIconData||l.value.siteIcon,document.querySelector('link[rel="icon"]')||document.head.appendChild(s)}};return{oemSettings:l,loading:r,saving:p,loadOemSettings:d,saveOemSettings:a,resetOemSettings:w,applyOemSettings:f,formatDateTime:s=>s?new Date(s).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}):"",validateIconFile:s=>{const o=[];return s.size>350*1024&&o.push("图标文件大小不能超过 350KB"),["image/x-icon","image/png","image/jpeg","image/jpg","image/svg+xml"].includes(s.type)||o.push("不支持的文件类型,请选择 .ico, .png, .jpg 或 .svg 文件"),{isValid:o.length===0,errors:o}},fileToBase64:s=>new Promise((o,n)=>{const t=new FileReader;t.onload=u=>o(u.target.result),t.onerror=n,t.readAsDataURL(s)})}}),B={class:"settings-container"},V={class:"card p-6"},R={key:0,class:"text-center py-12"},M={key:1,class:"table-container"},A={class:"min-w-full"},q={class:"divide-y divide-gray-200/50"},z={class:"table-row"},K={class:"px-6 py-4"},L={class:"table-row"},U={class:"px-6 py-4"},$={class:"space-y-3"},P={key:0,class:"inline-flex items-center gap-3 p-3 bg-gray-50 rounded-lg"},W=["src"],G={class:"px-6 py-6",colspan:"2"},H={class:"flex items-center justify-between"},J={class:"flex gap-3"},Q=["disabled"],X={key:0,class:"loading-spinner mr-2"},Y={key:1,class:"fas fa-save mr-2"},Z=["disabled"],ee={key:0,class:"text-sm text-gray-500"},te={__name:"SettingsView",setup(l){const r=E(),{loading:p,saving:d,oemSettings:a}=C(r),w=x();O(async()=>{try{await r.loadOemSettings()}catch{c("加载设置失败","error")}});const f=async()=>{try{const n={siteName:a.value.siteName,siteIcon:a.value.siteIcon,siteIconData:a.value.siteIconData},t=await r.saveOemSettings(n);t&&t.success?c("OEM设置保存成功","success"):c((t==null?void 0:t.message)||"保存失败","error")}catch{c("保存OEM设置失败","error")}},b=async()=>{if(confirm(`确定要重置为默认设置吗? - -这将清除所有自定义的网站名称和图标设置。`))try{const n=await r.resetOemSettings();n&&n.success?c("已重置为默认设置","success"):c("重置失败","error")}catch{c("重置失败","error")}},h=async n=>{const t=n.target.files[0];if(!t)return;const u=r.validateIconFile(t);if(!u.isValid){u.errors.forEach(y=>c(y,"error"));return}try{const y=await r.fileToBase64(t);a.value.siteIconData=y}catch{c("文件读取失败","error")}n.target.value=""},I=()=>{a.value.siteIcon="",a.value.siteIconData=""},s=()=>{console.warn("Icon failed to load")},o=r.formatDateTime;return(n,t)=>(g(),m("div",B,[e("div",V,[t[12]||(t[12]=e("div",{class:"flex flex-col md:flex-row justify-between items-center gap-4 mb-6"},[e("div",null,[e("h3",{class:"text-xl font-bold text-gray-900 mb-2"},"其他设置"),e("p",{class:"text-gray-600"},"自定义网站名称和图标")])],-1)),i(p)?(g(),m("div",R,t[2]||(t[2]=[e("div",{class:"loading-spinner mx-auto mb-4"},null,-1),e("p",{class:"text-gray-500"},"正在加载设置...",-1)]))):(g(),m("div",M,[e("table",A,[e("tbody",q,[e("tr",z,[t[4]||(t[4]=e("td",{class:"px-6 py-4 whitespace-nowrap w-48"},[e("div",{class:"flex items-center"},[e("div",{class:"w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3"},[e("i",{class:"fas fa-font text-white text-xs"})]),e("div",null,[e("div",{class:"text-sm font-semibold text-gray-900"},"网站名称"),e("div",{class:"text-xs text-gray-500"},"品牌标识")])])],-1)),e("td",K,[T(e("input",{"onUpdate:modelValue":t[0]||(t[0]=u=>i(a).siteName=u),type:"text",class:"form-input w-full max-w-md",placeholder:"Claude Relay Service",maxlength:"100"},null,512),[[N,i(a).siteName]]),t[3]||(t[3]=e("p",{class:"text-xs text-gray-500 mt-1"},"将显示在浏览器标题和页面头部",-1))])]),e("tr",L,[t[9]||(t[9]=e("td",{class:"px-6 py-4 whitespace-nowrap w-48"},[e("div",{class:"flex items-center"},[e("div",{class:"w-8 h-8 bg-gradient-to-br from-purple-500 to-purple-600 rounded-lg flex items-center justify-center mr-3"},[e("i",{class:"fas fa-image text-white text-xs"})]),e("div",null,[e("div",{class:"text-sm font-semibold text-gray-900"},"网站图标"),e("div",{class:"text-xs text-gray-500"},"Favicon")])])],-1)),e("td",U,[e("div",$,[i(a).siteIconData||i(a).siteIcon?(g(),m("div",P,[e("img",{src:i(a).siteIconData||i(a).siteIcon,alt:"图标预览",class:"w-8 h-8",onError:s},null,40,W),t[6]||(t[6]=e("span",{class:"text-sm text-gray-600"},"当前图标",-1)),e("button",{onClick:I,class:"text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"},t[5]||(t[5]=[e("i",{class:"fas fa-trash mr-1"},null,-1),v("删除 ",-1)]))])):_("",!0),e("div",null,[e("input",{type:"file",ref_key:"iconFileInput",ref:w,onChange:h,accept:".ico,.png,.jpg,.jpeg,.svg",class:"hidden"},null,544),e("button",{onClick:t[1]||(t[1]=u=>n.$refs.iconFileInput.click()),class:"btn btn-success px-4 py-2"},t[7]||(t[7]=[e("i",{class:"fas fa-upload mr-2"},null,-1),v(" 上传图标 ",-1)])),t[8]||(t[8]=e("span",{class:"text-xs text-gray-500 ml-3"},"支持 .ico, .png, .jpg, .svg 格式,最大 350KB",-1))])])])]),e("tr",null,[e("td",G,[e("div",H,[e("div",J,[e("button",{onClick:f,disabled:i(d),class:j(["btn btn-primary px-6 py-3",{"opacity-50 cursor-not-allowed":i(d)}])},[i(d)?(g(),m("div",X)):(g(),m("i",Y)),v(" "+S(i(d)?"保存中...":"保存设置"),1)],10,Q),e("button",{onClick:b,class:"btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3",disabled:i(d)},t[10]||(t[10]=[e("i",{class:"fas fa-undo mr-2"},null,-1),v(" 重置为默认 ",-1)]),8,Z)]),i(a).updatedAt?(g(),m("div",ee,[t[11]||(t[11]=e("i",{class:"fas fa-clock mr-1"},null,-1)),v(" 最后更新:"+S(i(o)(i(a).updatedAt)),1)])):_("",!0)])])])])])]))])]))}},le=F(te,[["__scopeId","data-v-3508e0de"]]);export{le as default}; diff --git a/web/admin-spa/dist/assets/TutorialView-PeOC7oRL.js b/web/admin-spa/dist/assets/TutorialView-PeOC7oRL.js deleted file mode 100644 index 58e0107d..00000000 --- a/web/admin-spa/dist/assets/TutorialView-PeOC7oRL.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as f}from"./index-DhITICXu.js";import{r as g,c as x,x as i,z as d,L as c,O as t,Q as b,ac as u,aY as l,P as a,y as v,C as n}from"./vue-vendor-CKToUHZx.js";import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";const p={class:"card p-6"},y={class:"mb-8"},h={class:"flex flex-wrap gap-2 p-2 bg-gray-100 rounded-xl"},C=["onClick"],w={key:0,class:"tutorial-content"},A={class:"mb-10"},T={class:"bg-gradient-to-r from-orange-50 to-yellow-50 rounded-xl p-6 border border-orange-100 mb-6"},P={class:"space-y-4"},N={class:"bg-white rounded-lg p-4 border border-orange-200"},S={class:"bg-gray-900 text-green-400 p-3 rounded font-mono text-sm"},H={class:"text-gray-300"},O={class:"bg-white rounded-lg p-4 border border-orange-200"},I={class:"mt-3 space-y-2"},R={class:"bg-gray-100 p-2 rounded text-sm"},B={class:"font-mono"},U={class:"bg-blue-50 border border-blue-200 rounded-lg p-4 mt-6"},j={class:"mt-3 space-y-2"},E={class:"bg-gray-100 p-2 rounded text-sm font-mono"},L={key:1,class:"tutorial-content"},_={class:"mb-10"},q={class:"bg-gradient-to-r from-orange-50 to-yellow-50 rounded-xl p-6 border border-orange-100 mb-6"},k={class:"space-y-4"},G={class:"bg-white rounded-lg p-4 border border-orange-200"},K={class:"bg-gray-900 text-green-400 p-3 rounded font-mono text-sm"},z={class:"text-gray-300"},D={class:"bg-white rounded-lg p-4 border border-orange-200"},W={class:"bg-gray-900 text-green-400 p-3 rounded font-mono text-sm mb-3"},$={class:"text-gray-300"},M={class:"bg-gray-900 text-green-400 p-3 rounded font-mono text-sm"},V={class:"text-gray-300"},F={key:2,class:"tutorial-content"},Q={class:"mb-10"},Y={class:"bg-gradient-to-r from-orange-50 to-yellow-50 rounded-xl p-6 border border-orange-100 mb-6"},J={class:"space-y-4"},X={class:"bg-white rounded-lg p-4 border border-orange-200"},Z={class:"bg-gray-900 text-green-400 p-3 rounded font-mono text-sm"},ee={class:"text-gray-300"},de={class:"bg-white rounded-lg p-4 border border-orange-200"},te={class:"bg-gray-900 text-green-400 p-3 rounded font-mono text-sm mb-3"},ae={class:"text-gray-300"},se={class:"bg-gray-900 text-green-400 p-3 rounded font-mono text-sm"},le={class:"text-gray-300"},re={__name:"TutorialView",setup(oe){const r=g("windows"),m=[{key:"windows",name:"Windows",icon:"fab fa-windows"},{key:"macos",name:"macOS",icon:"fab fa-apple"},{key:"linux",name:"Linux / WSL2",icon:"fab fa-linux"}];return x(()=>window.location.origin),(s,e)=>(v(),i("div",p,[e[55]||(e[55]=d("div",{class:"mb-8"},[d("h3",{class:"text-2xl font-bold text-gray-900 mb-4 flex items-center"},[d("i",{class:"fas fa-graduation-cap text-blue-600 mr-3"}),t(" Claude Code 使用教程 ")]),d("p",{class:"text-gray-600 text-lg"},"跟着这个教程,你可以轻松在自己的电脑上安装并使用 Claude Code。")],-1)),d("div",y,[d("div",h,[(v(),i(b,null,u(m,o=>d("button",{key:o.key,onClick:ie=>r.value=o.key,class:n(["flex-1 py-3 px-6 text-sm font-semibold rounded-lg transition-all duration-300 flex items-center justify-center gap-2",r.value===o.key?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:bg-white/50 hover:text-gray-900"])},[d("i",{class:n(o.icon)},null,2),t(" "+a(o.name),1)],10,C)),64))])]),r.value==="windows"?(v(),i("div",w,[e[19]||(e[19]=l('

1 安装 Node.js 环境

Claude Code 需要 Node.js 环境才能运行。

Windows 安装方法

方法一:官网下载(推荐)

  1. 打开浏览器访问 https://nodejs.org/
  2. 点击 "LTS" 版本进行下载(推荐长期支持版本)
  3. 下载完成后双击 .msi 文件
  4. 按照安装向导完成安装,保持默认设置即可

方法二:使用包管理器

如果你安装了 Chocolatey 或 Scoop,可以使用命令行安装:

# 使用 Chocolatey
choco install nodejs
# 或使用 Scoop
scoop install nodejs
Windows 注意事项
  • • 建议使用 PowerShell 而不是 CMD
  • • 如果遇到权限问题,尝试以管理员身份运行
  • • 某些杀毒软件可能会误报,需要添加白名单
验证安装是否成功

安装完成后,打开 PowerShell 或 CMD,输入以下命令:

node --version
npm --version

如果显示版本号,说明安装成功了!

2 安装 Git Bash

Windows 环境下需要使用 Git Bash 安装Claude code。安装完成后,环境变量设置和使用 Claude Code 仍然在普通的 PowerShell 或 CMD 中进行。

下载并安装 Git for Windows
  1. 访问 https://git-scm.com/downloads/win
  2. 点击 "Download for Windows" 下载安装包
  3. 运行下载的 .exe 安装文件
  4. 在安装过程中保持默认设置,直接点击 "Next" 完成安装
安装完成后
  • • 在任意文件夹右键可以看到 "Git Bash Here" 选项
  • • 也可以从开始菜单启动 "Git Bash"
  • • 只需要在 Git Bash 中运行 npm install 命令
  • • 后续的环境变量设置和使用都在 PowerShell/CMD 中
验证 Git Bash 安装

打开 Git Bash,输入以下命令验证:

git --version

如果显示 Git 版本号,说明安装成功!

3 安装 Claude Code

安装 Claude Code

打开 Git Bash(重要:不要使用 PowerShell),运行以下命令:

# 在 Git Bash 中全局安装 Claude Code
npm install -g @anthropic-ai/claude-code

这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code。

重要提醒
  • • 必须在 Git Bash 中运行,不要在 PowerShell 中运行
  • • 如果遇到权限问题,可以尝试在 Git Bash 中使用 sudo 命令
验证 Claude Code 安装

安装完成后,输入以下命令检查是否安装成功:

claude --version

如果显示版本号,恭喜你!Claude Code 已经成功安装了。

',3)),d("div",A,[e[18]||(e[18]=d("h4",{class:"text-xl font-semibold text-gray-800 mb-4 flex items-center"},[d("span",{class:"w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3"},"4"),t(" 设置环境变量 ")],-1)),d("div",T,[e[12]||(e[12]=d("h5",{class:"text-lg font-semibold text-gray-800 mb-3 flex items-center"},[d("i",{class:"fas fa-cog text-orange-600 mr-2"}),t(" 配置 Claude Code 环境变量 ")],-1)),e[13]||(e[13]=d("p",{class:"text-gray-700 mb-4"},"为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:",-1)),d("div",P,[d("div",N,[e[1]||(e[1]=d("h6",{class:"font-medium text-gray-800 mb-2"},"方法一:PowerShell 临时设置(推荐)",-1)),e[2]||(e[2]=d("p",{class:"text-gray-600 text-sm mb-3"},"在 PowerShell 中运行以下命令:",-1)),d("div",S,[d("div",H,'$env:ANTHROPIC_BASE_URL = "'+a(s.currentBaseUrl)+'"',1),e[0]||(e[0]=d("div",{class:"text-gray-300"},'$env:ANTHROPIC_AUTH_TOKEN = "你的API密钥"',-1))]),e[3]||(e[3]=d("p",{class:"text-yellow-700 text-xs mt-2"},'💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。',-1))]),d("div",O,[e[10]||(e[10]=d("h6",{class:"font-medium text-gray-800 mb-2"},"方法二:系统环境变量(永久设置)",-1)),e[11]||(e[11]=d("ol",{class:"text-gray-600 text-sm space-y-1 list-decimal list-inside"},[d("li",null,'右键"此电脑" → "属性" → "高级系统设置"'),d("li",null,'点击"环境变量"按钮'),d("li",null,'在"用户变量"或"系统变量"中点击"新建"'),d("li",null,"添加以下两个变量:")],-1)),d("div",I,[d("div",R,[e[4]||(e[4]=d("strong",null,"变量名:",-1)),e[5]||(e[5]=t(" ANTHROPIC_BASE_URL",-1)),e[6]||(e[6]=d("br",null,null,-1)),e[7]||(e[7]=d("strong",null,"变量值:",-1)),e[8]||(e[8]=t()),d("span",B,a(s.currentBaseUrl),1)]),e[9]||(e[9]=d("div",{class:"bg-gray-100 p-2 rounded text-sm"},[d("strong",null,"变量名:"),t(" ANTHROPIC_AUTH_TOKEN"),d("br"),d("strong",null,"变量值:"),t(),d("span",{class:"font-mono"},"你的API密钥")],-1))])])])]),d("div",U,[e[17]||(e[17]=l('
验证环境变量设置

设置完环境变量后,可以通过以下命令验证是否设置成功:

在 PowerShell 中验证:
echo $env:ANTHROPIC_BASE_URL
echo $env:ANTHROPIC_AUTH_TOKEN
在 CMD 中验证:
echo %ANTHROPIC_BASE_URL%
echo %ANTHROPIC_AUTH_TOKEN%
',3)),d("div",j,[e[15]||(e[15]=d("p",{class:"text-blue-700 text-sm"},[d("strong",null,"预期输出示例:")],-1)),d("div",E,[d("div",null,a(s.currentBaseUrl),1),e[14]||(e[14]=d("div",null,"cr_xxxxxxxxxxxxxxxxxx",-1))]),e[16]||(e[16]=d("p",{class:"text-blue-700 text-xs"}," 💡 如果输出为空或显示变量名本身,说明环境变量设置失败,请重新设置。 ",-1))])])]),e[20]||(e[20]=l('

5 开始使用 Claude Code

现在你可以开始使用 Claude Code 了!

启动 Claude Code
claude
在特定项目中使用
# 进入你的项目目录
cd C:\\path\\to\\your\\project
# 启动 Claude Code
claude

Windows 常见问题解决

安装时提示 "permission denied" 错误

这通常是权限问题,尝试以下解决方法:

  • 以管理员身份运行 PowerShell
  • 或者配置 npm 使用用户目录:npm config set prefix %APPDATA%\\npm
PowerShell 执行策略错误

如果遇到执行策略限制,运行:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
环境变量设置后不生效

设置永久环境变量后需要:

  • 重新启动 PowerShell 或 CMD
  • 或者注销并重新登录 Windows
  • 验证设置:echo $env:ANTHROPIC_BASE_URL
',2))])):r.value==="macos"?(v(),i("div",L,[e[36]||(e[36]=l('

1 安装 Node.js 环境

Claude Code 需要 Node.js 环境才能运行。

macOS 安装方法

方法一:使用 Homebrew(推荐)

如果你已经安装了 Homebrew,使用它安装 Node.js 会更方便:

# 更新 Homebrew
brew update
# 安装 Node.js
brew install node

方法二:官网下载

  1. 访问 https://nodejs.org/
  2. 下载适合 macOS 的 LTS 版本
  3. 打开下载的 .pkg 文件
  4. 按照安装程序指引完成安装
macOS 注意事项
  • • 如果遇到权限问题,可能需要使用 sudo
  • • 首次运行可能需要在系统偏好设置中允许
  • • 建议使用 Terminal 或 iTerm2
验证安装是否成功

安装完成后,打开 Terminal,输入以下命令:

node --version
npm --version

如果显示版本号,说明安装成功了!

2 安装 Claude Code

安装 Claude Code

打开 Terminal,运行以下命令:

# 全局安装 Claude Code
npm install -g @anthropic-ai/claude-code

如果遇到权限问题,可以使用 sudo:

sudo npm install -g @anthropic-ai/claude-code
验证 Claude Code 安装

安装完成后,输入以下命令检查是否安装成功:

claude --version

如果显示版本号,恭喜你!Claude Code 已经成功安装了。

',2)),d("div",_,[e[35]||(e[35]=d("h4",{class:"text-xl font-semibold text-gray-800 mb-4 flex items-center"},[d("span",{class:"w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3"},"3"),t(" 设置环境变量 ")],-1)),d("div",q,[e[33]||(e[33]=d("h5",{class:"text-lg font-semibold text-gray-800 mb-3 flex items-center"},[d("i",{class:"fas fa-cog text-orange-600 mr-2"}),t(" 配置 Claude Code 环境变量 ")],-1)),e[34]||(e[34]=d("p",{class:"text-gray-700 mb-4"},"为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:",-1)),d("div",k,[d("div",G,[e[22]||(e[22]=d("h6",{class:"font-medium text-gray-800 mb-2"},"方法一:临时设置(当前会话)",-1)),e[23]||(e[23]=d("p",{class:"text-gray-600 text-sm mb-3"},"在 Terminal 中运行以下命令:",-1)),d("div",K,[d("div",z,'export ANTHROPIC_BASE_URL="'+a(s.currentBaseUrl)+'"',1),e[21]||(e[21]=d("div",{class:"text-gray-300"},'export ANTHROPIC_AUTH_TOKEN="你的API密钥"',-1))]),e[24]||(e[24]=d("p",{class:"text-yellow-700 text-xs mt-2"},'💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。',-1))]),d("div",D,[e[31]||(e[31]=d("h6",{class:"font-medium text-gray-800 mb-2"},"方法二:永久设置",-1)),e[32]||(e[32]=d("p",{class:"text-gray-600 text-sm mb-3"},"编辑你的 shell 配置文件(根据你使用的 shell):",-1)),d("div",W,[e[25]||(e[25]=d("div",{class:"mb-2"},"# 对于 zsh (默认)",-1)),d("div",$,`echo 'export ANTHROPIC_BASE_URL="`+a(s.currentBaseUrl)+`"' >> ~/.zshrc`,1),e[26]||(e[26]=d("div",{class:"text-gray-300"},`echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc`,-1)),e[27]||(e[27]=d("div",{class:"text-gray-300"},"source ~/.zshrc",-1))]),d("div",M,[e[28]||(e[28]=d("div",{class:"mb-2"},"# 对于 bash",-1)),d("div",V,`echo 'export ANTHROPIC_BASE_URL="`+a(s.currentBaseUrl)+`"' >> ~/.bash_profile`,1),e[29]||(e[29]=d("div",{class:"text-gray-300"},`echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bash_profile`,-1)),e[30]||(e[30]=d("div",{class:"text-gray-300"},"source ~/.bash_profile",-1))])])])])]),e[37]||(e[37]=l('

4 开始使用 Claude Code

现在你可以开始使用 Claude Code 了!

启动 Claude Code
claude
在特定项目中使用
# 进入你的项目目录
cd /path/to/your/project
# 启动 Claude Code
claude

macOS 常见问题解决

安装时提示权限错误

尝试以下解决方法:

  • 使用 sudo 安装:sudo npm install -g @anthropic-ai/claude-code
  • 或者配置 npm 使用用户目录:npm config set prefix ~/.npm-global
macOS 安全设置阻止运行

如果系统阻止运行 Claude Code:

  • 打开"系统偏好设置" → "安全性与隐私"
  • 点击"仍要打开"或"允许"
  • 或者在 Terminal 中运行:sudo spctl --master-disable
环境变量不生效

检查以下几点:

  • 确认修改了正确的配置文件(.zshrc 或 .bash_profile)
  • 重新启动 Terminal
  • 验证设置:echo $ANTHROPIC_BASE_URL
',2))])):r.value==="linux"?(v(),i("div",F,[e[53]||(e[53]=l('

1 安装 Node.js 环境

Claude Code 需要 Node.js 环境才能运行。

Linux 安装方法

方法一:使用官方仓库(推荐)

# 添加 NodeSource 仓库
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
# 安装 Node.js
sudo apt-get install -y nodejs

方法二:使用系统包管理器

虽然版本可能不是最新的,但对于基本使用已经足够:

# Ubuntu/Debian
sudo apt update
sudo apt install nodejs npm
# CentOS/RHEL/Fedora
sudo dnf install nodejs npm
Linux 注意事项
  • • 某些发行版可能需要安装额外的依赖
  • • 如果遇到权限问题,使用 sudo
  • • 确保你的用户在 npm 的全局目录有写权限
验证安装是否成功

安装完成后,打开终端,输入以下命令:

node --version
npm --version

如果显示版本号,说明安装成功了!

2 安装 Claude Code

安装 Claude Code

打开终端,运行以下命令:

# 全局安装 Claude Code
npm install -g @anthropic-ai/claude-code

如果遇到权限问题,可以使用 sudo:

sudo npm install -g @anthropic-ai/claude-code
验证 Claude Code 安装

安装完成后,输入以下命令检查是否安装成功:

claude --version

如果显示版本号,恭喜你!Claude Code 已经成功安装了。

',2)),d("div",Q,[e[52]||(e[52]=d("h4",{class:"text-xl font-semibold text-gray-800 mb-4 flex items-center"},[d("span",{class:"w-8 h-8 bg-orange-500 text-white rounded-full flex items-center justify-center text-sm font-bold mr-3"},"3"),t(" 设置环境变量 ")],-1)),d("div",Y,[e[50]||(e[50]=d("h5",{class:"text-lg font-semibold text-gray-800 mb-3 flex items-center"},[d("i",{class:"fas fa-cog text-orange-600 mr-2"}),t(" 配置 Claude Code 环境变量 ")],-1)),e[51]||(e[51]=d("p",{class:"text-gray-700 mb-4"},"为了让 Claude Code 连接到你的中转服务,需要设置两个环境变量:",-1)),d("div",J,[d("div",X,[e[39]||(e[39]=d("h6",{class:"font-medium text-gray-800 mb-2"},"方法一:临时设置(当前会话)",-1)),e[40]||(e[40]=d("p",{class:"text-gray-600 text-sm mb-3"},"在终端中运行以下命令:",-1)),d("div",Z,[d("div",ee,'export ANTHROPIC_BASE_URL="'+a(s.currentBaseUrl)+'"',1),e[38]||(e[38]=d("div",{class:"text-gray-300"},'export ANTHROPIC_AUTH_TOKEN="你的API密钥"',-1))]),e[41]||(e[41]=d("p",{class:"text-yellow-700 text-xs mt-2"},'💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。',-1))]),d("div",de,[e[48]||(e[48]=d("h6",{class:"font-medium text-gray-800 mb-2"},"方法二:永久设置",-1)),e[49]||(e[49]=d("p",{class:"text-gray-600 text-sm mb-3"},"编辑你的 shell 配置文件:",-1)),d("div",te,[e[42]||(e[42]=d("div",{class:"mb-2"},"# 对于 bash (默认)",-1)),d("div",ae,`echo 'export ANTHROPIC_BASE_URL="`+a(s.currentBaseUrl)+`"' >> ~/.bashrc`,1),e[43]||(e[43]=d("div",{class:"text-gray-300"},`echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bashrc`,-1)),e[44]||(e[44]=d("div",{class:"text-gray-300"},"source ~/.bashrc",-1))]),d("div",se,[e[45]||(e[45]=d("div",{class:"mb-2"},"# 对于 zsh",-1)),d("div",le,`echo 'export ANTHROPIC_BASE_URL="`+a(s.currentBaseUrl)+`"' >> ~/.zshrc`,1),e[46]||(e[46]=d("div",{class:"text-gray-300"},`echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc`,-1)),e[47]||(e[47]=d("div",{class:"text-gray-300"},"source ~/.zshrc",-1))])])])])]),e[54]||(e[54]=l('

4 开始使用 Claude Code

现在你可以开始使用 Claude Code 了!

启动 Claude Code
claude
在特定项目中使用
# 进入你的项目目录
cd /path/to/your/project
# 启动 Claude Code
claude

Linux 常见问题解决

安装时提示权限错误

尝试以下解决方法:

  • 使用 sudo 安装:sudo npm install -g @anthropic-ai/claude-code
  • 或者配置 npm 使用用户目录:npm config set prefix ~/.npm-global
  • 然后添加到 PATH:export PATH=~/.npm-global/bin:$PATH
缺少依赖库

某些 Linux 发行版需要安装额外依赖:

# Ubuntu/Debian
sudo apt install build-essential
# CentOS/RHEL
sudo dnf groupinstall "Development Tools"
环境变量不生效

检查以下几点:

  • 确认修改了正确的配置文件(.bashrc 或 .zshrc)
  • 重新启动终端或运行 source ~/.bashrc
  • 验证设置:echo $ANTHROPIC_BASE_URL
',2))])):c("",!0),e[56]||(e[56]=d("div",{class:"bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl p-6 text-center"},[d("h5",{class:"text-xl font-semibold mb-2"},"🎉 恭喜你!"),d("p",{class:"text-blue-100 mb-4"},"你已经成功安装并配置了 Claude Code,现在可以开始享受 AI 编程助手带来的便利了。"),d("p",{class:"text-sm text-blue-200"},"如果在使用过程中遇到任何问题,可以查看官方文档或社区讨论获取帮助。")],-1))]))}},ge=f(re,[["__scopeId","data-v-58e5d8f5"]]);export{ge as default}; diff --git a/web/admin-spa/dist/assets/index-DhITICXu.js b/web/admin-spa/dist/assets/index-DhITICXu.js deleted file mode 100644 index 58b0ab02..00000000 --- a/web/admin-spa/dist/assets/index-DhITICXu.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/LoginView-DCXHRAtT.js","assets/vue-vendor-CKToUHZx.js","assets/element-plus-B8Fs_0jW.js","assets/vendor-BDiMbLwQ.js","assets/element-plus-CPnoEkWW.css","assets/LoginView-tn0RQdqM.css","assets/LogoTitle-zkjZaSZH.css","assets/MainLayout-Df9D9BdR.js","assets/toast-BvwA7Mwb.js","assets/LogoTitle-CIeppYUK.js","assets/MainLayout-BSYYg59M.css","assets/DashboardView-B-EZhqex.js","assets/chart-Cor9iTVD.js","assets/DashboardView-BNhlTn49.css","assets/ApiKeysView-8a5XQ0yG.js","assets/ApiKeysView-TPhICvaG.css","assets/AccountsView-BpzHtqkX.js","assets/AccountsView-CzMsrxvN.css","assets/TutorialView-PeOC7oRL.js","assets/TutorialView-DrYIAHe5.css","assets/SettingsView-BD-f8NQ2.js","assets/SettingsView-rXAPQgqk.css","assets/ApiStatsView-D_Y193CX.js","assets/ApiStatsView-onZNVLml.css"])))=>i.map(i=>d[i]); -import{aP as K,aQ as W,aR as F,r as _,c as L,q as R,V as D,I as q,z as h,x as T,ac as M,Q as G,a5 as N,y,C as S,L as x,P as b,Y as U,B as H,R as P,J,T as Q,O as X,av as Y,aM as Z,aS as ee}from"./vue-vendor-CKToUHZx.js";import{i as te,z as oe}from"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))o(s);new MutationObserver(s=>{for(const a of s)if(a.type==="childList")for(const n of a.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&o(n)}).observe(document,{childList:!0,subtree:!0});function e(s){const a={};return s.integrity&&(a.integrity=s.integrity),s.referrerPolicy&&(a.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?a.credentials="include":s.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function o(s){if(s.ep)return;s.ep=!0;const a=e(s);fetch(s.href,a)}})();const se="modulepreload",ne=function(r){return"/admin-next/"+r},$={},w=function(t,e,o){let s=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const n=document.querySelector("meta[property=csp-nonce]"),p=(n==null?void 0:n.nonce)||(n==null?void 0:n.getAttribute("nonce"));s=Promise.allSettled(e.map(c=>{if(c=ne(c),c in $)return;$[c]=!0;const l=c.endsWith(".css"),d=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${d}`))return;const i=document.createElement("link");if(i.rel=l?"stylesheet":se,l||(i.as="script"),i.crossOrigin="",i.href=c,p&&i.setAttribute("nonce",p),document.head.appendChild(i),l)return new Promise((m,f)=>{i.addEventListener("load",m),i.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${c}`)))})}))}function a(n){const p=new Event("vite:preloadError",{cancelable:!0});if(p.payload=n,window.dispatchEvent(p),!p.defaultPrevented)throw n}return s.then(n=>{for(const p of n||[])p.status==="rejected"&&a(p.reason);return t().catch(a)})},V={basePath:"/admin-next/",apiPrefix:""};function ae(r=""){return r&&!r.startsWith("/")&&(r="/"+r),V.basePath+(r.startsWith("#")?r:"#"+r)}function re(){return ae("/login")}const ie=()=>w(()=>import("./LoginView-DCXHRAtT.js"),__vite__mapDeps([0,1,2,3,4,5,6])),E=()=>w(()=>import("./MainLayout-Df9D9BdR.js"),__vite__mapDeps([7,1,8,9,6,2,3,4,10])),ce=()=>w(()=>import("./DashboardView-B-EZhqex.js"),__vite__mapDeps([11,2,1,3,4,8,12,13])),le=()=>w(()=>import("./ApiKeysView-8a5XQ0yG.js"),__vite__mapDeps([14,2,1,3,4,8,15])),ue=()=>w(()=>import("./AccountsView-BpzHtqkX.js"),__vite__mapDeps([16,1,8,2,3,4,17])),de=()=>w(()=>import("./TutorialView-PeOC7oRL.js"),__vite__mapDeps([18,1,2,3,4,19])),fe=()=>w(()=>import("./SettingsView-BD-f8NQ2.js"),__vite__mapDeps([20,1,8,2,3,4,21])),he=()=>w(()=>import("./ApiStatsView-D_Y193CX.js"),__vite__mapDeps([22,1,9,6,3,18,2,4,19,23])),me=[{path:"/",redirect:"/api-stats"},{path:"/login",name:"Login",component:ie,meta:{requiresAuth:!1}},{path:"/api-stats",name:"ApiStats",component:he,meta:{requiresAuth:!1}},{path:"/dashboard",component:E,meta:{requiresAuth:!0},children:[{path:"",name:"Dashboard",component:ce}]},{path:"/api-keys",component:E,meta:{requiresAuth:!0},children:[{path:"",name:"ApiKeys",component:le}]},{path:"/accounts",component:E,meta:{requiresAuth:!0},children:[{path:"",name:"Accounts",component:ue}]},{path:"/tutorial",component:E,meta:{requiresAuth:!0},children:[{path:"",name:"Tutorial",component:de}]},{path:"/settings",component:E,meta:{requiresAuth:!0},children:[{path:"",name:"Settings",component:fe}]}],C=K({history:W(V.basePath),routes:me});C.beforeEach((r,t,e)=>{const o=B();console.log("路由导航:",{to:r.path,from:t.path,requiresAuth:r.meta.requiresAuth,isAuthenticated:o.isAuthenticated}),r.path==="/api-stats"||r.path.startsWith("/api-stats")?e():r.meta.requiresAuth&&!o.isAuthenticated?e("/login"):r.path==="/login"&&o.isAuthenticated?e("/dashboard"):e()});const z=V.apiPrefix;function A(r){return r.startsWith("/")||(r="/"+r),z+r}class pe{constructor(){this.baseURL=z}getAuthToken(){return localStorage.getItem("authToken")||null}buildConfig(t={}){const e={headers:{"Content-Type":"application/json",...t.headers},...t},o=this.getAuthToken();return o&&(e.headers.Authorization=`Bearer ${o}`),e}async handleResponse(t){if(t.status===401){const o=window.location.pathname+window.location.hash;throw o.includes("/login")||o.endsWith("/")||(localStorage.removeItem("authToken"),window.location.href=re()),new Error("Unauthorized")}const e=t.headers.get("content-type");if(e&&e.includes("application/json")){const o=await t.json();if(!t.ok)throw new Error(o.message||`HTTP ${t.status}`);return o}if(!t.ok)throw new Error(`HTTP ${t.status}: ${t.statusText}`);return t}async get(t,e={}){const o=A(t),s=this.buildConfig({...e,method:"GET"});try{const a=await fetch(o,s);return await this.handleResponse(a)}catch(a){throw console.error("API GET Error:",a),a}}async post(t,e=null,o={}){const s=A(t),a=this.buildConfig({...o,method:"POST",body:e?JSON.stringify(e):void 0});try{const n=await fetch(s,a);return await this.handleResponse(n)}catch(n){throw console.error("API POST Error:",n),n}}async put(t,e=null,o={}){const s=A(t),a=this.buildConfig({...o,method:"PUT",body:e?JSON.stringify(e):void 0});try{const n=await fetch(s,a);return await this.handleResponse(n)}catch(n){throw console.error("API PUT Error:",n),n}}async delete(t,e={}){const o=A(t),s=this.buildConfig({...e,method:"DELETE"});try{const a=await fetch(o,s);return await this.handleResponse(a)}catch(a){throw console.error("API DELETE Error:",a),a}}}const k=new pe,B=F("auth",()=>{const r=_(!1),t=_(localStorage.getItem("authToken")||""),e=_(""),o=_(""),s=_(!1),a=_({siteName:"Claude Relay Service",siteIcon:"",siteIconData:"",faviconData:""}),n=_(!0),p=L(()=>!!t.value&&r.value),c=L(()=>t.value),l=L(()=>({username:e.value}));async function d(u){s.value=!0,o.value="";try{const g=await k.post("/web/auth/login",u);g.success?(t.value=g.token,e.value=g.username||u.username,r.value=!0,localStorage.setItem("authToken",g.token),await C.push("/dashboard")):o.value=g.message||"登录失败"}catch(g){o.value=g.message||"登录失败,请检查用户名和密码"}finally{s.value=!1}}function i(){r.value=!1,t.value="",e.value="",localStorage.removeItem("authToken"),C.push("/login")}function m(){t.value&&(r.value=!0,f())}async function f(){try{const u=await k.get("/web/auth/user");u.success&&u.user&&(e.value=u.user.username),(await k.get("/admin/dashboard")).success||i()}catch{i()}}async function v(){n.value=!0;try{const u=await k.get("/admin/oem-settings");if(u.success&&u.data){if(a.value={...a.value,...u.data},u.data.siteIconData||u.data.siteIcon){const g=document.querySelector("link[rel*='icon']")||document.createElement("link");g.type="image/x-icon",g.rel="shortcut icon",g.href=u.data.siteIconData||u.data.siteIcon,document.getElementsByTagName("head")[0].appendChild(g)}u.data.siteName&&(document.title=`${u.data.siteName} - 管理后台`)}}catch(u){console.error("加载OEM设置失败:",u)}finally{n.value=!1}}return{isLoggedIn:r,authToken:t,username:e,loginError:o,loginLoading:s,oemSettings:a,oemLoading:n,isAuthenticated:p,token:c,user:l,login:d,logout:i,checkAuth:m,loadOemSettings:v}}),O=(r,t)=>{const e=r.__vccOpts||r;for(const[o,s]of t)e[o]=s;return e},_e={class:"toast-container"},ge=["onClick"],ve={class:"toast-content"},ye={class:"toast-icon"},we={class:"toast-body"},Te={key:0,class:"toast-title"},be={class:"toast-message"},Ee=["onClick"],Ae={__name:"ToastNotification",setup(r,{expose:t}){const e=_([]);let o=0;const s=l=>{const d={success:"fas fa-check-circle",error:"fas fa-exclamation-circle",warning:"fas fa-exclamation-triangle",info:"fas fa-info-circle"};return d[l]||d.info},a=(l,d="info",i=null,m=5e3)=>{const f=++o,v={id:f,message:l,type:d,title:i,duration:m,isVisible:!1};return e.value.push(v),setTimeout(()=>{v.isVisible=!0},10),m>0&&setTimeout(()=>{n(f)},m),f},n=l=>{const d=e.value.findIndex(i=>i.id===l);if(d>-1){const i=e.value[d];i.isVisible=!1,setTimeout(()=>{const m=e.value.findIndex(f=>f.id===l);m>-1&&e.value.splice(m,1)},300)}},p=()=>{e.value.forEach(l=>{l.isVisible=!1}),setTimeout(()=>{e.value.length=0},300)},c=(l,d="info",i=null,m=5e3)=>a(l,d,i,m);return R(()=>{window.showToast=c}),D(()=>{window.showToast===c&&delete window.showToast}),t({showToast:c,removeToast:n,clearAllToasts:p}),(l,d)=>(y(),q(N,{to:"body"},[h("div",_e,[(y(!0),T(G,null,M(e.value,i=>(y(),T("div",{key:i.id,class:S(["toast",`toast-${i.type}`,i.isVisible?"toast-show":"toast-hide"]),onClick:m=>n(i.id)},[h("div",ve,[h("div",ye,[h("i",{class:S(s(i.type))},null,2)]),h("div",we,[i.title?(y(),T("div",Te,b(i.title),1)):x("",!0),h("div",be,b(i.message),1)]),h("button",{class:"toast-close",onClick:U(m=>n(i.id),["stop"])},d[0]||(d[0]=[h("i",{class:"fas fa-times"},null,-1)]),8,Ee)]),i.duration>0?(y(),T("div",{key:0,class:"toast-progress",style:H({animationDuration:`${i.duration}ms`})},null,4)):x("",!0)],10,ge))),128))])]))}},ke=O(Ae,[["__scopeId","data-v-383c6781"]]),Pe={class:"modal-content w-full max-w-md p-6 mx-auto"},xe={class:"flex items-start gap-4 mb-6"},Ce={class:"flex-1"},Ie={class:"text-lg font-semibold text-gray-900 mb-2"},Le={class:"text-gray-600 leading-relaxed whitespace-pre-line"},Se={class:"flex items-center justify-end gap-3"},Re=["disabled"],Ve=["disabled"],Oe={key:0,class:"loading-spinner mr-2"},$e={__name:"ConfirmDialog",setup(r,{expose:t}){const e=_(!1),o=_(!1),s=_(""),a=_(""),n=_("确认"),p=_("取消");let c=null;const l=(f,v,u="确认",g="取消")=>new Promise(j=>{s.value=f,a.value=v,n.value=u,p.value=g,e.value=!0,o.value=!1,c=j}),d=()=>{o.value||(o.value=!0,setTimeout(()=>{e.value=!1,o.value=!1,c&&(c(!0),c=null)},200))},i=()=>{o.value||(e.value=!1,c&&(c(!1),c=null))},m=f=>{e.value&&(f.key==="Escape"?i():f.key==="Enter"&&!f.shiftKey&&!f.ctrlKey&&!f.altKey&&d())};return R(()=>{window.showConfirm=l,document.addEventListener("keydown",m)}),D(()=>{window.showConfirm===l&&delete window.showConfirm,document.removeEventListener("keydown",m)}),t({showConfirm:l}),(f,v)=>(y(),q(N,{to:"body"},[P(Q,{name:"modal",appear:""},{default:J(()=>[e.value?(y(),T("div",{key:0,class:"fixed inset-0 modal z-[100] flex items-center justify-center p-4",onClick:U(i,["self"])},[h("div",Pe,[h("div",xe,[v[0]||(v[0]=h("div",{class:"flex-shrink-0 w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-xl flex items-center justify-center"},[h("i",{class:"fas fa-exclamation-triangle text-white text-lg"})],-1)),h("div",Ce,[h("h3",Ie,b(s.value),1),h("div",Le,b(a.value),1)])]),h("div",Se,[h("button",{onClick:i,class:"btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3",disabled:o.value},b(p.value),9,Re),h("button",{onClick:d,class:S(["btn btn-warning px-6 py-3",{"opacity-50 cursor-not-allowed":o.value}]),disabled:o.value},[o.value?(y(),T("div",Oe)):x("",!0),X(" "+b(n.value),1)],10,Ve)])])])):x("",!0)]),_:1})]))}},De=O($e,[["__scopeId","data-v-70e604e3"]]),qe={id:"app"},Ne={__name:"App",setup(r){const t=B(),e=_(),o=_();return R(()=>{t.checkAuth(),t.loadOemSettings()}),(s,a)=>{const n=Y("router-view");return y(),T("div",qe,[P(n),P(ke,{ref_key:"toastRef",ref:e},null,512),P(De,{ref_key:"confirmRef",ref:o},null,512)])}}},Ue=O(Ne,[["__scopeId","data-v-5cba45f5"]]),I=Z(Ue),ze=ee();I.use(ze);I.use(C);I.use(te,{locale:oe});I.mount("#app");export{O as _,k as a,B as u}; diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index bee0f376..1c362b70 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/components/dashboard/UsageTrend.vue b/web/admin-spa/src/components/dashboard/UsageTrend.vue index 682a3605..e72c3675 100644 --- a/web/admin-spa/src/components/dashboard/UsageTrend.vue +++ b/web/admin-spa/src/components/dashboard/UsageTrend.vue @@ -55,8 +55,12 @@ const createChart = () => { const labels = dashboardStore.trendData.map(item => { if (granularity.value === 'hour') { - const date = new Date(item.date) - return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:00` + // 小时粒度使用hour字段 + const date = new Date(item.hour) + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hour = String(date.getHours()).padStart(2, '0') + return `${month}/${day} ${hour}:00` } return item.date }) diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 4535b548..1165f84e 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -141,12 +141,15 @@ export const useDashboardStore = defineStore('dashboard', () => { let url = '/admin/usage-trend?' if (granularity === 'hour') { - // 小时粒度,传递开始和结束时间 + // 小时粒度,计算时间范围 url += `granularity=hour` - if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { - url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}` - url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}` - } + + // 根据days参数计算时间范围 + const endTime = new Date() + const startTime = new Date(endTime.getTime() - days * 24 * 60 * 60 * 1000) + + url += `&startDate=${encodeURIComponent(startTime.toISOString())}` + url += `&endDate=${encodeURIComponent(endTime.toISOString())}` } else { // 天粒度,传递天数 url += `granularity=day&days=${days}` diff --git a/web/admin-spa/src/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index 38cc0e2d..c114249c 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -441,8 +441,22 @@ function createUsageTrendChart() { const requestsData = data.map(d => d.requests || 0) const costData = data.map(d => d.cost || 0) + // 根据数据类型确定标签字段和格式 + const labelField = data[0]?.date ? 'date' : 'hour' + const labels = data.map(d => { + if (labelField === 'hour') { + // 格式化小时显示 + const date = new Date(d.hour) + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hour = String(date.getHours()).padStart(2, '0') + return `${month}/${day} ${hour}:00` + } + return d.date + }) + const chartData = { - labels: data.map(d => d.date), + labels: labels, datasets: [ { label: '输入Token', @@ -628,8 +642,21 @@ function createApiKeysUsageTrendChart() { } }) || [] + // 根据数据类型确定标签字段 + const labelField = data[0]?.date ? 'date' : 'hour' + const chartData = { - labels: data.map(d => d.date), + labels: data.map(d => { + if (labelField === 'hour') { + // 格式化小时显示 + const date = new Date(d.hour) + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + const hour = String(date.getHours()).padStart(2, '0') + return `${month}/${day} ${hour}:00` + } + return d.date + }), datasets: datasets } From 61b1a0ec3250d895ad15e9832981c358456b5b82 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 09:18:55 +0800 Subject: [PATCH 02/13] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E9=97=AE=E9=A2=98=E5=AF=BC=E8=87=B4=E7=9A=84?= =?UTF-8?q?=E5=9B=BE=E8=A1=A8=E6=97=B6=E9=97=B4=E6=98=BE=E7=A4=BA=E4=B8=8D?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E5=92=8C=E4=BB=8A=E6=97=A5=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin.js | 35 ++++++---- web/admin-spa/dist/index.html | 2 +- web/admin-spa/src/stores/dashboard.js | 94 ++++++++++++++++++++++++--- 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 1273370d..e3d118e8 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1123,8 +1123,9 @@ router.get('/usage-stats', authenticateAdmin, async (req, res) => { router.get('/model-stats', authenticateAdmin, async (req, res) => { try { const { period = 'daily' } = req.query; // daily, monthly - const today = new Date().toISOString().split('T')[0]; - const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const today = redis.getDateStringInTimezone(); + const tzDate = redis.getDateInTimezone(); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`); @@ -1265,8 +1266,10 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { currentHour.setMinutes(0, 0, 0); while (currentHour <= endTime) { - const dateStr = currentHour.toISOString().split('T')[0]; - const hour = String(currentHour.getHours()).padStart(2, '0'); + // 使用时区转换后的时间来生成键 + const tzCurrentHour = redis.getDateInTimezone(currentHour); + const dateStr = redis.getDateStringInTimezone(currentHour); + const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); const hourKey = `${dateStr}:${hour}`; // 获取当前小时的模型统计数据 @@ -1338,7 +1341,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { } trendData.push({ - date: hourKey, + date: dateStr, // 保持日期格式一致 hour: currentHour.toISOString(), inputTokens: hourInputTokens, outputTokens: hourOutputTokens, @@ -1362,7 +1365,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { for (let i = 0; i < daysCount; i++) { const date = new Date(today); date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; + const dateStr = redis.getDateStringInTimezone(date); // 汇总当天所有API Key的使用数据 const pattern = `usage:daily:*:${dateStr}`; @@ -1478,8 +1481,9 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = logger.info(`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`); const client = redis.getClientSafe(); - const today = new Date().toISOString().split('T')[0]; - const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const today = redis.getDateStringInTimezone(); + const tzDate = redis.getDateInTimezone(); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; let searchPatterns = []; @@ -1501,7 +1505,7 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = // 生成日期范围内所有日期的搜索模式 for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { - const dateStr = d.toISOString().split('T')[0]; + const dateStr = redis.getDateStringInTimezone(d); searchPatterns.push(`usage:${keyId}:model:daily:*:${dateStr}`); } @@ -1695,7 +1699,11 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { currentHour.setMinutes(0, 0, 0); while (currentHour <= endTime) { - const hourKey = currentHour.toISOString().split(':')[0].replace('T', ':'); + // 使用时区转换后的时间来生成键 + const tzCurrentHour = redis.getDateInTimezone(currentHour); + const dateStr = redis.getDateStringInTimezone(currentHour); + const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); + const hourKey = `${dateStr}:${hour}`; // 获取这个小时所有API Key的数据 const pattern = `usage:hourly:*:${hourKey}`; @@ -1740,7 +1748,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { for (let i = 0; i < daysCount; i++) { const date = new Date(today); date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; + const dateStr = redis.getDateStringInTimezone(date); // 获取这一天所有API Key的数据 const pattern = `usage:daily:*:${dateStr}`; @@ -1832,8 +1840,9 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { // 按模型统计费用 const client = redis.getClientSafe(); - const today = new Date().toISOString().split('T')[0]; - const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const today = redis.getDateStringInTimezone(); + const tzDate = redis.getDateInTimezone(); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; let pattern; if (period === 'today') { diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index 1c362b70..4132f9b1 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 1165f84e..23df0b0e 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -144,12 +144,49 @@ export const useDashboardStore = defineStore('dashboard', () => { // 小时粒度,计算时间范围 url += `granularity=hour` - // 根据days参数计算时间范围 - const endTime = new Date() - const startTime = new Date(endTime.getTime() - days * 24 * 60 * 60 * 1000) - - url += `&startDate=${encodeURIComponent(startTime.toISOString())}` - url += `&endDate=${encodeURIComponent(endTime.toISOString())}` + if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { + // 使用自定义时间范围 + url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}` + url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}` + } else { + // 使用预设计算时间范围,与loadApiKeysTrend保持一致 + const now = new Date() + let startTime, endTime + + if (dateFilter.value.type === 'preset') { + switch (dateFilter.value.preset) { + case 'last24h': + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + break + case 'yesterday': + startTime = new Date(now) + startTime.setDate(now.getDate() - 1) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + case 'dayBefore': + startTime = new Date(now) + startTime.setDate(now.getDate() - 2) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + default: + // 默认近24小时 + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + } + } else { + // 默认使用days参数计算 + startTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000) + endTime = now + } + + url += `&startDate=${encodeURIComponent(startTime.toISOString())}` + url += `&endDate=${encodeURIComponent(endTime.toISOString())}` + } } else { // 天粒度,传递天数 url += `granularity=day&days=${days}` @@ -178,17 +215,58 @@ export const useDashboardStore = defineStore('dashboard', () => { async function loadApiKeysTrend(metric = 'requests') { try { let url = '/admin/api-keys-usage-trend?' + let days = 7 if (trendGranularity.value === 'hour') { - // 小时粒度,传递开始和结束时间 + // 小时粒度,计算时间范围 url += `granularity=hour` + if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { + // 使用自定义时间范围 url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}` url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}` + } else { + // 使用预设计算时间范围,与setDateFilterPreset保持一致 + const now = new Date() + let startTime, endTime + + if (dateFilter.value.type === 'preset') { + switch (dateFilter.value.preset) { + case 'last24h': + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + break + case 'yesterday': + startTime = new Date(now) + startTime.setDate(now.getDate() - 1) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + case 'dayBefore': + startTime = new Date(now) + startTime.setDate(now.getDate() - 2) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + default: + // 默认近24小时 + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + } + } else { + // 默认近24小时 + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + } + + url += `&startDate=${encodeURIComponent(startTime.toISOString())}` + url += `&endDate=${encodeURIComponent(endTime.toISOString())}` } } else { // 天粒度,传递天数 - const days = dateFilter.value.type === 'preset' + days = dateFilter.value.type === 'preset' ? (dateFilter.value.preset === 'today' ? 1 : dateFilter.value.preset === '7days' ? 7 : 30) : calculateDaysBetween(dateFilter.value.customStart, dateFilter.value.customEnd) url += `granularity=day&days=${days}` From 5503004b6628c1fbf33456472ac46b9ace5db3c2 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 09:42:09 +0800 Subject: [PATCH 03/13] =?UTF-8?q?fix(admin):=20=E4=BF=AE=E5=A4=8D=E6=97=B6?= =?UTF-8?q?=E5=8C=BA=E8=BD=AC=E6=8D=A2=E5=92=8C=E6=97=B6=E9=97=B4=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复小时粒度下时间显示为完整日期的问题 - 修复时区转换逻辑,使用正确的UTC偏移计算 - 统一时间标签格式:小时粒度显示MM/DD HH:00,天粒度显示MM/DD - 修复图表时间显示比实际时间快7小时的问题 --- src/models/redis.js | 5 +++-- src/routes/admin.js | 6 +++--- web/admin-spa/src/views/DashboardView.vue | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/models/redis.js b/src/models/redis.js index 9473724b..29efdca1 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -5,8 +5,9 @@ const logger = require('../utils/logger'); // 时区辅助函数 function getDateInTimezone(date = new Date()) { const offset = config.system.timezoneOffset || 8; // 默认UTC+8 - const utcTime = date.getTime() + (date.getTimezoneOffset() * 60000); - const targetTime = new Date(utcTime + (offset * 3600000)); + // 直接基于UTC时间计算目标时区时间 + // 不需要考虑本地时区,因为我们总是基于UTC + const targetTime = new Date(date.getTime() + (offset * 3600000)); return targetTime; } diff --git a/src/routes/admin.js b/src/routes/admin.js index e3d118e8..b4a4a088 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1341,8 +1341,8 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { } trendData.push({ - date: dateStr, // 保持日期格式一致 - hour: currentHour.toISOString(), + // 对于小时粒度,只返回hour字段,不返回date字段 + hour: tzCurrentHour.toISOString(), // 使用转换后的时区时间 inputTokens: hourInputTokens, outputTokens: hourOutputTokens, requests: hourRequests, @@ -1710,7 +1710,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { const keys = await client.keys(pattern); const hourData = { - hour: currentHour.toISOString(), + hour: tzCurrentHour.toISOString(), // 使用转换后的时区时间 apiKeys: {} }; diff --git a/web/admin-spa/src/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index c114249c..014d976c 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -452,6 +452,14 @@ function createUsageTrendChart() { const hour = String(date.getHours()).padStart(2, '0') return `${month}/${day} ${hour}:00` } + // 按天显示时,只显示月/日,不显示年份 + const dateStr = d.date + if (dateStr && dateStr.includes('-')) { + const parts = dateStr.split('-') + if (parts.length >= 3) { + return `${parts[1]}/${parts[2]}` + } + } return d.date }) @@ -655,6 +663,14 @@ function createApiKeysUsageTrendChart() { const hour = String(date.getHours()).padStart(2, '0') return `${month}/${day} ${hour}:00` } + // 按天显示时,只显示月/日,不显示年份 + const dateStr = d.date + if (dateStr && dateStr.includes('-')) { + const parts = dateStr.split('-') + if (parts.length >= 3) { + return `${parts[1]}/${parts[2]}` + } + } return d.date }), datasets: datasets From 4c64e6df4b3c3bd82d107a5e6b57ff6b1dd106fd Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 10:07:25 +0800 Subject: [PATCH 04/13] =?UTF-8?q?fix(timezone):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=86=99=E5=85=A5=E6=97=B6=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E5=8C=BA=E9=94=99=E8=AF=AF=EF=BC=88=E5=85=B3=E9=94=AE=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 redis.js 中所有时区相关的日期获取方法 - 使用 getUTC* 方法替代 get* 方法获取正确的时区日期 - 影响:incrementTokenUsage, incrementAccountUsage, incrementDailyCost 等 - 修复 admin.js 中查询数据时的日期键生成 - 确保所有 Redis 键格式一致: - 日期:YYYY-MM-DD - 月份:YYYY-MM - 小时:YYYY-MM-DD:HH - 添加服务端时间标签,避免前端时区转换问题 这是核心修复,确保数据从源头就是正确的。 --- src/models/redis.js | 34 ++++++++++-------- src/routes/admin.js | 36 +++++++++++++------ .../dist/assets/DashboardView-BNhlTn49.css | 1 - web/admin-spa/dist/index.html | 2 +- web/admin-spa/src/views/DashboardView.vue | 10 ++++++ 5 files changed, 56 insertions(+), 27 deletions(-) delete mode 100644 web/admin-spa/dist/assets/DashboardView-BNhlTn49.css diff --git a/src/models/redis.js b/src/models/redis.js index 29efdca1..f8f10723 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -3,24 +3,30 @@ const config = require('../../config/config'); const logger = require('../utils/logger'); // 时区辅助函数 +// 注意:这个函数的目的是获取某个时间点在目标时区的"本地"表示 +// 例如:UTC时间 2025-07-30 01:00:00 在 UTC+8 时区表示为 2025-07-30 09:00:00 function getDateInTimezone(date = new Date()) { const offset = config.system.timezoneOffset || 8; // 默认UTC+8 - // 直接基于UTC时间计算目标时区时间 - // 不需要考虑本地时区,因为我们总是基于UTC - const targetTime = new Date(date.getTime() + (offset * 3600000)); - return targetTime; + + // 方法:创建一个偏移后的Date对象,使其getUTCXXX方法返回目标时区的值 + // 这样我们可以用getUTCFullYear()等方法获取目标时区的年月日时分秒 + const offsetMs = offset * 3600000; // 时区偏移的毫秒数 + const adjustedTime = new Date(date.getTime() + offsetMs); + + return adjustedTime; } // 获取配置时区的日期字符串 (YYYY-MM-DD) function getDateStringInTimezone(date = new Date()) { const tzDate = getDateInTimezone(date); - return `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`; + // 使用UTC方法获取偏移后的日期部分 + return `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`; } // 获取配置时区的小时 (0-23) function getHourInTimezone(date = new Date()) { const tzDate = getDateInTimezone(date); - return tzDate.getHours(); + return tzDate.getUTCHours(); } class RedisClient { @@ -163,7 +169,7 @@ class RedisClient { const now = new Date(); const today = getDateStringInTimezone(now); const tzDate = getDateInTimezone(now); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`; // 新增小时级别 const daily = `usage:daily:${keyId}:${today}`; @@ -288,7 +294,7 @@ class RedisClient { const now = new Date(); const today = getDateStringInTimezone(now); const tzDate = getDateInTimezone(now); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`; // 账户级别统计的键 @@ -386,7 +392,7 @@ class RedisClient { const today = getDateStringInTimezone(); const dailyKey = `usage:daily:${keyId}:${today}`; const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const monthlyKey = `usage:monthly:${keyId}:${currentMonth}`; const [total, daily, monthly] = await Promise.all([ @@ -482,8 +488,8 @@ class RedisClient { async incrementDailyCost(keyId, amount) { const today = getDateStringInTimezone(); const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; - const currentHour = `${today}:${String(getHourInTimezone()).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; + const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`; const dailyKey = `usage:cost:daily:${keyId}:${today}`; const monthlyKey = `usage:cost:monthly:${keyId}:${currentMonth}`; @@ -510,8 +516,8 @@ class RedisClient { async getCostStats(keyId) { const today = getDateStringInTimezone(); const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; - const currentHour = `${today}:${String(getHourInTimezone()).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; + const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`; const [daily, monthly, hourly, total] = await Promise.all([ this.client.get(`usage:cost:daily:${keyId}:${today}`), @@ -534,7 +540,7 @@ class RedisClient { const today = getDateStringInTimezone(); const accountDailyKey = `account_usage:daily:${accountId}:${today}`; const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const accountMonthlyKey = `account_usage:monthly:${accountId}:${currentMonth}`; const [total, daily, monthly] = await Promise.all([ diff --git a/src/routes/admin.js b/src/routes/admin.js index b4a4a088..0166f759 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -63,7 +63,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { // 今日 - 使用时区日期 const redis = require('../models/redis'); const tzDate = redis.getDateInTimezone(now); - const dateStr = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`; + const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`; searchPatterns.push(`usage:daily:*:${dateStr}`); } else if (timeRange === '7days') { // 最近7天 @@ -72,14 +72,14 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const date = new Date(now); date.setDate(date.getDate() - i); const tzDate = redis.getDateInTimezone(date); - const dateStr = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`; + const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`; searchPatterns.push(`usage:daily:*:${dateStr}`); } } else if (timeRange === 'monthly') { // 本月 const redis = require('../models/redis'); const tzDate = redis.getDateInTimezone(now); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; searchPatterns.push(`usage:monthly:*:${currentMonth}`); } @@ -189,7 +189,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const redis = require('../models/redis'); const tzToday = redis.getDateStringInTimezone(now); const tzDate = redis.getDateInTimezone(now); - const tzMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const modelKeys = timeRange === 'today' ? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`) @@ -1125,7 +1125,7 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { const { period = 'daily' } = req.query; // daily, monthly const today = redis.getDateStringInTimezone(); const tzDate = redis.getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`); @@ -1269,7 +1269,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { // 使用时区转换后的时间来生成键 const tzCurrentHour = redis.getDateInTimezone(currentHour); const dateStr = redis.getDateStringInTimezone(currentHour); - const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); + const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0'); const hourKey = `${dateStr}:${hour}`; // 获取当前小时的模型统计数据 @@ -1340,9 +1340,16 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { hourCost = costResult.costs.total; } + // 格式化时间标签 + const tzDate = redis.getDateInTimezone(currentHour); + const month = String(tzDate.getUTCMonth() + 1).padStart(2, '0'); + const day = String(tzDate.getUTCDate()).padStart(2, '0'); + const hourStr = String(tzDate.getUTCHours()).padStart(2, '0'); + trendData.push({ // 对于小时粒度,只返回hour字段,不返回date字段 - hour: tzCurrentHour.toISOString(), // 使用转换后的时区时间 + hour: currentHour.toISOString(), // 保留原始ISO时间用于排序 + label: `${month}/${day} ${hourStr}:00`, // 添加格式化的标签 inputTokens: hourInputTokens, outputTokens: hourOutputTokens, requests: hourRequests, @@ -1483,7 +1490,7 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = const client = redis.getClientSafe(); const today = redis.getDateStringInTimezone(); const tzDate = redis.getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; let searchPatterns = []; @@ -1702,15 +1709,22 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { // 使用时区转换后的时间来生成键 const tzCurrentHour = redis.getDateInTimezone(currentHour); const dateStr = redis.getDateStringInTimezone(currentHour); - const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); + const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0'); const hourKey = `${dateStr}:${hour}`; // 获取这个小时所有API Key的数据 const pattern = `usage:hourly:*:${hourKey}`; const keys = await client.keys(pattern); + // 格式化时间标签 + const tzDateForLabel = redis.getDateInTimezone(currentHour); + const monthLabel = String(tzDateForLabel.getUTCMonth() + 1).padStart(2, '0'); + const dayLabel = String(tzDateForLabel.getUTCDate()).padStart(2, '0'); + const hourLabel = String(tzDateForLabel.getUTCHours()).padStart(2, '0'); + const hourData = { - hour: tzCurrentHour.toISOString(), // 使用转换后的时区时间 + hour: currentHour.toISOString(), // 使用原始时间,不进行时区转换 + label: `${monthLabel}/${dayLabel} ${hourLabel}:00`, // 添加格式化的标签 apiKeys: {} }; @@ -1842,7 +1856,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { const client = redis.getClientSafe(); const today = redis.getDateStringInTimezone(); const tzDate = redis.getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; let pattern; if (period === 'today') { diff --git a/web/admin-spa/dist/assets/DashboardView-BNhlTn49.css b/web/admin-spa/dist/assets/DashboardView-BNhlTn49.css deleted file mode 100644 index 8b643d67..00000000 --- a/web/admin-spa/dist/assets/DashboardView-BNhlTn49.css +++ /dev/null @@ -1 +0,0 @@ -.custom-date-picker[data-v-5170898f] .el-input__inner{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.custom-date-picker[data-v-5170898f] .el-input__inner:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1));--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.custom-date-picker[data-v-5170898f] .el-input__inner{font-size:13px;padding:0 10px}.custom-date-picker[data-v-5170898f] .el-range-separator{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1));padding:0 2px}.custom-date-picker[data-v-5170898f] .el-range-input{font-size:13px} diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index 4132f9b1..b9de86c8 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index 014d976c..68b4e2a3 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -444,6 +444,11 @@ function createUsageTrendChart() { // 根据数据类型确定标签字段和格式 const labelField = data[0]?.date ? 'date' : 'hour' const labels = data.map(d => { + // 优先使用后端提供的label字段 + if (d.label) { + return d.label + } + if (labelField === 'hour') { // 格式化小时显示 const date = new Date(d.hour) @@ -655,6 +660,11 @@ function createApiKeysUsageTrendChart() { const chartData = { labels: data.map(d => { + // 优先使用后端提供的label字段 + if (d.label) { + return d.label + } + if (labelField === 'hour') { // 格式化小时显示 const date = new Date(d.hour) From 496569d1109e9b99ec6b7056c074293d7c292855 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 10:34:20 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E6=9D=BF=E6=8A=98=E7=BA=BF=E5=9B=BE=E6=97=B6=E9=97=B4=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E8=AE=A1=E7=AE=97=E9=94=99=E8=AF=AF=EF=BC=88UTC+8?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dist/assets/AccountsView-B3VuC3-p.js | 15 ---- .../dist/assets/ApiKeysView-DemJuKGs.js | 9 --- web/admin-spa/dist/index.html | 2 +- web/admin-spa/src/stores/dashboard.js | 75 +++++++++++++------ 4 files changed, 55 insertions(+), 46 deletions(-) delete mode 100644 web/admin-spa/dist/assets/AccountsView-B3VuC3-p.js delete mode 100644 web/admin-spa/dist/assets/ApiKeysView-DemJuKGs.js diff --git a/web/admin-spa/dist/assets/AccountsView-B3VuC3-p.js b/web/admin-spa/dist/assets/AccountsView-B3VuC3-p.js deleted file mode 100644 index d0c60d61..00000000 --- a/web/admin-spa/dist/assets/AccountsView-B3VuC3-p.js +++ /dev/null @@ -1,15 +0,0 @@ -import{r as T,aR as Te,o as O,V as Ae,x as u,y as i,z as e,L as $,K as w,al as xe,aY as te,aX as we,aq as j,aZ as Ue,C as G,O as h,c as se,P as U,I as le,a5 as ke,R as ne,an as H,u as W,q as Ve,Y as Ie,Q as je,ac as Se,B as ve}from"./vue-vendor-CKToUHZx.js";import{s as V}from"./toast-BvwA7Mwb.js";import{a as K,_ as Me}from"./index-hS0iOrU-.js";import"./element-plus-B8Fs_0jW.js";import"./vendor-BDiMbLwQ.js";const oe=T(!1),ye=T({title:"",message:"",confirmText:"继续",cancelText:"取消"}),J=T(null);function he(){return{showConfirmModal:oe,confirmOptions:ye,showConfirm:(g,d,S="继续",I="取消")=>new Promise(C=>{ye.value={title:g,message:d,confirmText:S,cancelText:I},J.value=C,oe.value=!0}),handleConfirm:()=>{oe.value=!1,J.value&&(J.value(!0),J.value=null)},handleCancel:()=>{oe.value=!1,J.value&&(J.value(!1),J.value=null)}}}const $e=Te("accounts",()=>{const L=T([]),E=T([]),m=T([]),g=T(!1),d=T(null),S=T(""),I=T("asc"),C=async()=>{g.value=!0,d.value=null;try{const x=await K.get("/admin/claude-accounts");if(x.success)L.value=x.data||[];else throw new Error(x.message||"获取Claude账户失败")}catch(x){throw d.value=x.message,x}finally{g.value=!1}},A=async()=>{g.value=!0,d.value=null;try{const x=await K.get("/admin/claude-console-accounts");if(x.success)E.value=x.data||[];else throw new Error(x.message||"获取Claude Console账户失败")}catch(x){throw d.value=x.message,x}finally{g.value=!1}},y=async()=>{g.value=!0,d.value=null;try{const x=await K.get("/admin/gemini-accounts");if(x.success)m.value=x.data||[];else throw new Error(x.message||"获取Gemini账户失败")}catch(x){throw d.value=x.message,x}finally{g.value=!1}};return{claudeAccounts:L,claudeConsoleAccounts:E,geminiAccounts:m,loading:g,error:d,sortBy:S,sortOrder:I,fetchClaudeAccounts:C,fetchClaudeConsoleAccounts:A,fetchGeminiAccounts:y,fetchAllAccounts:async()=>{g.value=!0,d.value=null;try{await Promise.all([C(),A(),y()])}catch(x){throw d.value=x.message,x}finally{g.value=!1}},createClaudeAccount:async x=>{g.value=!0,d.value=null;try{const c=await K.post("/admin/claude-accounts",x);if(c.success)return await C(),c.data;throw new Error(c.message||"创建Claude账户失败")}catch(c){throw d.value=c.message,c}finally{g.value=!1}},createClaudeConsoleAccount:async x=>{g.value=!0,d.value=null;try{const c=await K.post("/admin/claude-console-accounts",x);if(c.success)return await A(),c.data;throw new Error(c.message||"创建Claude Console账户失败")}catch(c){throw d.value=c.message,c}finally{g.value=!1}},createGeminiAccount:async x=>{g.value=!0,d.value=null;try{const c=await K.post("/admin/gemini-accounts",x);if(c.success)return await y(),c.data;throw new Error(c.message||"创建Gemini账户失败")}catch(c){throw d.value=c.message,c}finally{g.value=!1}},updateClaudeAccount:async(x,c)=>{g.value=!0,d.value=null;try{const b=await K.put(`/admin/claude-accounts/${x}`,c);if(b.success)return await C(),b;throw new Error(b.message||"更新Claude账户失败")}catch(b){throw d.value=b.message,b}finally{g.value=!1}},updateClaudeConsoleAccount:async(x,c)=>{g.value=!0,d.value=null;try{const b=await K.put(`/admin/claude-console-accounts/${x}`,c);if(b.success)return await A(),b;throw new Error(b.message||"更新Claude Console账户失败")}catch(b){throw d.value=b.message,b}finally{g.value=!1}},updateGeminiAccount:async(x,c)=>{g.value=!0,d.value=null;try{const b=await K.put(`/admin/gemini-accounts/${x}`,c);if(b.success)return await y(),b;throw new Error(b.message||"更新Gemini账户失败")}catch(b){throw d.value=b.message,b}finally{g.value=!1}},toggleAccount:async(x,c)=>{g.value=!0,d.value=null;try{let b;x==="claude"?b=`/admin/claude-accounts/${c}/toggle`:x==="claude-console"?b=`/admin/claude-console-accounts/${c}/toggle`:b=`/admin/gemini-accounts/${c}/toggle`;const q=await K.put(b);if(q.success)return x==="claude"?await C():x==="claude-console"?await A():await y(),q;throw new Error(q.message||"切换状态失败")}catch(b){throw d.value=b.message,b}finally{g.value=!1}},deleteAccount:async(x,c)=>{g.value=!0,d.value=null;try{let b;x==="claude"?b=`/admin/claude-accounts/${c}`:x==="claude-console"?b=`/admin/claude-console-accounts/${c}`:b=`/admin/gemini-accounts/${c}`;const q=await K.delete(b);if(q.success)return x==="claude"?await C():x==="claude-console"?await A():await y(),q;throw new Error(q.message||"删除失败")}catch(b){throw d.value=b.message,b}finally{g.value=!1}},refreshClaudeToken:async x=>{g.value=!0,d.value=null;try{const c=await K.post(`/admin/claude-accounts/${x}/refresh`);if(c.success)return await C(),c;throw new Error(c.message||"Token刷新失败")}catch(c){throw d.value=c.message,c}finally{g.value=!1}},generateClaudeAuthUrl:async x=>{try{const c=await K.post("/admin/claude-accounts/generate-auth-url",x);if(c.success)return c.data;throw new Error(c.message||"生成授权URL失败")}catch(c){throw d.value=c.message,c}},exchangeClaudeCode:async x=>{try{const c=await K.post("/admin/claude-accounts/exchange-code",x);if(c.success)return c.data;throw new Error(c.message||"交换授权码失败")}catch(c){throw d.value=c.message,c}},generateGeminiAuthUrl:async x=>{try{const c=await K.post("/admin/gemini-accounts/generate-auth-url",x);if(c.success)return c.data;throw new Error(c.message||"生成授权URL失败")}catch(c){throw d.value=c.message,c}},exchangeGeminiCode:async x=>{try{const c=await K.post("/admin/gemini-accounts/exchange-code",x);if(c.success)return c.data;throw new Error(c.message||"交换授权码失败")}catch(c){throw d.value=c.message,c}},sortAccounts:x=>{S.value===x?I.value=I.value==="asc"?"desc":"asc":(S.value=x,I.value="asc")},reset:()=>{L.value=[],E.value=[],m.value=[],g.value=!1,d.value=null,S.value="",I.value="asc"}}}),Ke={class:"space-y-4"},Pe={class:"flex items-center justify-between"},Re={class:"flex items-center cursor-pointer"},Ge={key:0,class:"bg-gray-50 p-4 rounded-lg border border-gray-200 space-y-4"},Le={class:"grid grid-cols-2 gap-4"},De={class:"space-y-4"},Oe={class:"flex items-center"},Ee={key:0,class:"grid grid-cols-2 gap-4"},ze={class:"relative"},We=["type"],ge={__name:"ProxyConfig",props:{modelValue:{type:Object,default:()=>({enabled:!1,type:"socks5",host:"",port:"",username:"",password:""})}},emits:["update:modelValue"],setup(L,{emit:E}){const m=L,g=E,d=T({...m.modelValue}),S=T(!!(d.value.username||d.value.password)),I=T(!1);O(()=>m.modelValue,y=>{JSON.stringify(y)!==JSON.stringify(d.value)&&(d.value={...y},S.value=!!(y.username||y.password))},{deep:!0}),O(()=>d.value.enabled,y=>{A()}),O(()=>d.value.type,y=>{A()}),O(()=>d.value.host,y=>{A()}),O(()=>d.value.port,y=>{A()}),O(()=>d.value.username,y=>{A()}),O(()=>d.value.password,y=>{A()}),O(S,y=>{y||(d.value.username="",d.value.password="",A())});let C=null;function A(){C&&clearTimeout(C),C=setTimeout(()=>{const y={...d.value};S.value||(y.username="",y.password=""),g("update:modelValue",y)},100)}return Ae(()=>{C&&clearTimeout(C)}),(y,f)=>(i(),u("div",Ke,[e("div",Pe,[f[9]||(f[9]=e("h4",{class:"text-sm font-semibold text-gray-700"},"代理设置 (可选)",-1)),e("label",Re,[w(e("input",{type:"checkbox","onUpdate:modelValue":f[0]||(f[0]=M=>d.value.enabled=M),class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[xe,d.value.enabled]]),f[8]||(f[8]=e("span",{class:"ml-2 text-sm text-gray-700"},"启用代理",-1))])]),d.value.enabled?(i(),u("div",Ge,[f[17]||(f[17]=te('

配置代理以访问受限的网络资源。支持 SOCKS5 和 HTTP 代理。

请确保代理服务器稳定可用,否则会影响账户的正常使用。

',1)),e("div",null,[f[11]||(f[11]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"代理类型",-1)),w(e("select",{"onUpdate:modelValue":f[1]||(f[1]=M=>d.value.type=M),class:"form-input w-full"},f[10]||(f[10]=[e("option",{value:"socks5"},"SOCKS5",-1),e("option",{value:"http"},"HTTP",-1),e("option",{value:"https"},"HTTPS",-1)]),512),[[we,d.value.type]])]),e("div",Le,[e("div",null,[f[12]||(f[12]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"主机地址",-1)),w(e("input",{"onUpdate:modelValue":f[2]||(f[2]=M=>d.value.host=M),type:"text",placeholder:"例如: 192.168.1.100",class:"form-input w-full"},null,512),[[j,d.value.host]])]),e("div",null,[f[13]||(f[13]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"端口",-1)),w(e("input",{"onUpdate:modelValue":f[3]||(f[3]=M=>d.value.port=M),type:"number",placeholder:"例如: 1080",class:"form-input w-full"},null,512),[[j,d.value.port]])])]),e("div",De,[e("div",Oe,[w(e("input",{type:"checkbox","onUpdate:modelValue":f[4]||(f[4]=M=>S.value=M),id:"proxyAuth",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[xe,S.value]]),f[14]||(f[14]=e("label",{for:"proxyAuth",class:"ml-2 text-sm text-gray-700 cursor-pointer"}," 需要身份验证 ",-1))]),S.value?(i(),u("div",Ee,[e("div",null,[f[15]||(f[15]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"用户名",-1)),w(e("input",{"onUpdate:modelValue":f[5]||(f[5]=M=>d.value.username=M),type:"text",placeholder:"代理用户名",class:"form-input w-full"},null,512),[[j,d.value.username]])]),e("div",null,[f[16]||(f[16]=e("label",{class:"block text-sm font-medium text-gray-700 mb-2"},"密码",-1)),e("div",ze,[w(e("input",{"onUpdate:modelValue":f[6]||(f[6]=M=>d.value.password=M),type:I.value?"text":"password",placeholder:"代理密码",class:"form-input w-full pr-10"},null,8,We),[[Ue,d.value.password]]),e("button",{type:"button",onClick:f[7]||(f[7]=M=>I.value=!I.value),class:"absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600"},[e("i",{class:G(I.value?"fas fa-eye-slash":"fas fa-eye")},null,2)])])])])):$("",!0)]),f[18]||(f[18]=e("div",{class:"bg-blue-50 p-3 rounded-lg border border-blue-200"},[e("p",{class:"text-xs text-blue-700"},[e("i",{class:"fas fa-info-circle mr-1"}),e("strong",null,"提示:"),h("代理设置将用于所有与此账户相关的API请求。请确保代理服务器支持HTTPS流量转发。 ")])],-1))])):$("",!0)]))}},qe={class:"space-y-6"},Be={key:0},Ne={class:"bg-blue-50 p-6 rounded-lg border border-blue-200"},Fe={class:"flex items-start gap-4"},He={class:"flex-1"},_e={class:"space-y-4"},Je={class:"bg-white/80 rounded-lg p-4 border border-blue-300"},Ye={class:"flex items-start gap-3"},Qe={class:"flex-1"},Xe=["disabled"],Ze={key:0,class:"fas fa-link mr-2"},et={key:1,class:"loading-spinner mr-2"},tt={key:1,class:"space-y-3"},st={class:"flex items-center gap-2"},lt=["value"],at={class:"bg-white/80 rounded-lg p-4 border border-blue-300"},ot={class:"flex items-start gap-3"},nt={class:"flex-1"},rt={class:"space-y-3"},it={key:1},ut={class:"bg-green-50 p-6 rounded-lg border border-green-200"},dt={class:"flex items-start gap-4"},pt={class:"flex-1"},ct={class:"space-y-4"},mt={class:"bg-white/80 rounded-lg p-4 border border-green-300"},ft={class:"flex items-start gap-3"},xt={class:"flex-1"},vt=["disabled"],yt={key:0,class:"fas fa-link mr-2"},gt={key:1,class:"loading-spinner mr-2"},bt={key:1,class:"space-y-3"},wt={class:"flex items-center gap-2"},kt=["value"],ht={class:"bg-white/80 rounded-lg p-4 border border-green-300"},$t={class:"flex items-start gap-3"},Ct={class:"flex-1"},Tt={class:"space-y-3"},At={class:"flex gap-3 pt-4"},Ut=["disabled"],Vt={key:0,class:"loading-spinner mr-2"},It={__name:"OAuthFlow",props:{platform:{type:String,required:!0},proxy:{type:Object,default:null}},emits:["success","back"],setup(L,{emit:E}){const m=L,g=E,d=$e(),S=T(!1),I=T(!1),C=T(""),A=T(""),y=T(!1),f=T(""),M=se(()=>C.value&&A.value.trim());O(A,v=>{if(!v||typeof v!="string")return;const r=v.trim();if(!r)return;if(r.startsWith("http://")||r.startsWith("https://"))if(r.startsWith("http://localhost:45462"))try{const B=new URL(r).searchParams.get("code");B?(A.value=B,V("成功提取授权码!","success"),console.log("Successfully extracted authorization code from URL")):V("URL 中未找到授权码参数,请检查链接是否正确","error")}catch(N){console.error("Failed to parse URL:",N),V("链接格式错误,请检查是否为完整的 URL","error")}else if(m.platform==="gemini")try{const B=new URL(r).searchParams.get("code");B&&(A.value=B,V("成功提取授权码!","success"))}catch{}else V("请粘贴以 http://localhost:45462 开头的链接","error")});const D=async()=>{var v;S.value=!0;try{const r=(v=m.proxy)!=null&&v.enabled?{proxy:{type:m.proxy.type,host:m.proxy.host,port:parseInt(m.proxy.port),username:m.proxy.username||null,password:m.proxy.password||null}}:{};if(m.platform==="claude"){const R=await d.generateClaudeAuthUrl(r);C.value=R.authUrl,f.value=R.sessionId}else if(m.platform==="gemini"){const R=await d.generateGeminiAuthUrl(r);C.value=R.authUrl,f.value=R.sessionId}}catch(r){V(r.message||"生成授权链接失败","error")}finally{S.value=!1}},P=()=>{C.value="",A.value="",D()},_=async()=>{try{await navigator.clipboard.writeText(C.value),y.value=!0,V("链接已复制","success"),setTimeout(()=>{y.value=!1},2e3)}catch{const r=document.createElement("input");r.value=C.value,document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),y.value=!0,V("链接已复制","success"),setTimeout(()=>{y.value=!1},2e3)}},s=async()=>{var v;if(M.value){I.value=!0;try{let r={};m.platform==="claude"?r={sessionId:f.value,callbackUrl:A.value.trim()}:m.platform==="gemini"&&(r={code:A.value.trim(),sessionId:f.value}),(v=m.proxy)!=null&&v.enabled&&(r.proxy={type:m.proxy.type,host:m.proxy.host,port:parseInt(m.proxy.port),username:m.proxy.username||null,password:m.proxy.password||null});let R;m.platform==="claude"?R=await d.exchangeClaudeCode(r):m.platform==="gemini"&&(R=await d.exchangeGeminiCode(r)),g("success",R)}catch(r){V(r.message||"授权失败,请检查授权码是否正确","error")}finally{I.value=!1}}};return(v,r)=>(i(),u("div",qe,[L.platform==="claude"?(i(),u("div",Be,[e("div",Ne,[e("div",Fe,[r[14]||(r[14]=e("div",{class:"w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0"},[e("i",{class:"fas fa-link text-white"})],-1)),e("div",He,[r[12]||(r[12]=e("h4",{class:"font-semibold text-blue-900 mb-3"},"Claude 账户授权",-1)),r[13]||(r[13]=e("p",{class:"text-sm text-blue-800 mb-4"}," 请按照以下步骤完成 Claude 账户的授权: ",-1)),e("div",_e,[e("div",Je,[e("div",Ye,[r[5]||(r[5]=e("div",{class:"w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"1",-1)),e("div",Qe,[r[4]||(r[4]=e("p",{class:"font-medium text-blue-900 mb-2"},"点击下方按钮生成授权链接",-1)),C.value?(i(),u("div",tt,[e("div",st,[e("input",{type:"text",value:C.value,readonly:"",class:"form-input flex-1 text-xs font-mono bg-gray-50"},null,8,lt),e("button",{onClick:_,class:"px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors",title:"复制链接"},[e("i",{class:G(y.value?"fas fa-check text-green-500":"fas fa-copy")},null,2)])]),e("button",{onClick:P,class:"text-xs text-blue-600 hover:text-blue-700"},r[3]||(r[3]=[e("i",{class:"fas fa-sync-alt mr-1"},null,-1),h("重新生成 ",-1)]))])):(i(),u("button",{key:0,onClick:D,disabled:S.value,class:"btn btn-primary px-4 py-2 text-sm"},[S.value?(i(),u("div",et)):(i(),u("i",Ze)),h(" "+U(S.value?"生成中...":"生成授权链接"),1)],8,Xe))])])]),r[11]||(r[11]=te('
2

在浏览器中打开链接并完成授权

请在新标签页中打开授权链接,登录您的 Claude 账户并授权。

注意:如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。

',1)),e("div",at,[e("div",ot,[r[10]||(r[10]=e("div",{class:"w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"3",-1)),e("div",nt,[r[8]||(r[8]=e("p",{class:"font-medium text-blue-900 mb-2"},"输入 Authorization Code",-1)),r[9]||(r[9]=e("p",{class:"text-sm text-blue-700 mb-3"},[h(" 授权完成后,页面会显示一个 "),e("strong",null,"Authorization Code"),h(",请将其复制并粘贴到下方输入框: ")],-1)),e("div",rt,[e("div",null,[r[6]||(r[6]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},[e("i",{class:"fas fa-key text-blue-500 mr-2"}),h("Authorization Code ")],-1)),w(e("textarea",{"onUpdate:modelValue":r[0]||(r[0]=R=>A.value=R),rows:"3",class:"form-input w-full resize-none font-mono text-sm",placeholder:"粘贴从Claude页面获取的Authorization Code..."},null,512),[[j,A.value]])]),r[7]||(r[7]=e("p",{class:"text-xs text-gray-500 mt-2"},[e("i",{class:"fas fa-info-circle mr-1"}),h(" 请粘贴从Claude页面复制的Authorization Code ")],-1))])])])])])])])])])):L.platform==="gemini"?(i(),u("div",it,[e("div",ut,[e("div",dt,[r[26]||(r[26]=e("div",{class:"w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center flex-shrink-0"},[e("i",{class:"fas fa-robot text-white"})],-1)),e("div",pt,[r[24]||(r[24]=e("h4",{class:"font-semibold text-green-900 mb-3"},"Gemini 账户授权",-1)),r[25]||(r[25]=e("p",{class:"text-sm text-green-800 mb-4"}," 请按照以下步骤完成 Gemini 账户的授权: ",-1)),e("div",ct,[e("div",mt,[e("div",ft,[r[17]||(r[17]=e("div",{class:"w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"1",-1)),e("div",xt,[r[16]||(r[16]=e("p",{class:"font-medium text-green-900 mb-2"},"点击下方按钮生成授权链接",-1)),C.value?(i(),u("div",bt,[e("div",wt,[e("input",{type:"text",value:C.value,readonly:"",class:"form-input flex-1 text-xs font-mono bg-gray-50"},null,8,kt),e("button",{onClick:_,class:"px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors",title:"复制链接"},[e("i",{class:G(y.value?"fas fa-check text-green-500":"fas fa-copy")},null,2)])]),e("button",{onClick:P,class:"text-xs text-green-600 hover:text-green-700"},r[15]||(r[15]=[e("i",{class:"fas fa-sync-alt mr-1"},null,-1),h("重新生成 ",-1)]))])):(i(),u("button",{key:0,onClick:D,disabled:S.value,class:"btn btn-primary px-4 py-2 text-sm"},[S.value?(i(),u("div",gt)):(i(),u("i",yt)),h(" "+U(S.value?"生成中...":"生成授权链接"),1)],8,vt))])])]),r[23]||(r[23]=te('
2

在浏览器中打开链接并完成授权

  1. 点击上方的授权链接,在新页面中完成Google账号登录
  2. 点击“登录”按钮后可能会加载很慢(这是正常的)
  3. 如果超过1分钟还在加载,请按 F5 刷新页面
  4. 授权完成后会跳转到 http://localhost:45462 (可能显示无法访问)

提示:如果页面一直无法跳转,可以打开浏览器开发者工具(F12),F5刷新一下授权页再点击页面的登录按钮,在“网络”标签中找到以 localhost:45462 开头的请求,复制其完整URL。

',1)),e("div",ht,[e("div",$t,[r[22]||(r[22]=e("div",{class:"w-6 h-6 bg-green-600 text-white rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"},"3",-1)),e("div",Ct,[r[20]||(r[20]=e("p",{class:"font-medium text-green-900 mb-2"},"复制oauth后的链接",-1)),r[21]||(r[21]=e("p",{class:"text-sm text-green-700 mb-3"}," 复制浏览器地址栏的完整链接并粘贴到下方输入框: ",-1)),e("div",Tt,[e("div",null,[r[18]||(r[18]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},[e("i",{class:"fas fa-key text-green-500 mr-2"}),h("复制oauth后的链接 ")],-1)),w(e("textarea",{"onUpdate:modelValue":r[1]||(r[1]=R=>A.value=R),rows:"3",class:"form-input w-full resize-none font-mono text-sm",placeholder:"粘贴以 http://localhost:45462 开头的完整链接..."},null,512),[[j,A.value]])]),r[19]||(r[19]=te('

支持粘贴完整链接,系统会自动提取授权码

也可以直接粘贴授权码(code参数的值)

',1))])])])])])])])])])):$("",!0),e("div",At,[e("button",{type:"button",onClick:r[2]||(r[2]=R=>v.$emit("back")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 上一步 "),e("button",{type:"button",onClick:s,disabled:!M.value||I.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[I.value?(i(),u("div",Vt)):$("",!0),h(" "+U(I.value?"验证中...":"完成授权"),1)],8,Ut)])]))}},jt={key:0,class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},St={class:"modal-content w-full max-w-md p-6 mx-auto"},Mt={class:"flex items-start gap-4 mb-6"},Kt={class:"flex-1"},Pt={class:"text-lg font-bold text-gray-900 mb-2"},Rt={class:"text-gray-600 text-sm leading-relaxed whitespace-pre-line"},Gt={class:"flex gap-3"},Ce={__name:"ConfirmModal",props:{show:{type:Boolean,required:!0},title:{type:String,default:""},message:{type:String,default:""},confirmText:{type:String,default:"继续"},cancelText:{type:String,default:"取消"}},emits:["confirm","cancel"],setup(L){return(E,m)=>(i(),le(ke,{to:"body"},[L.show?(i(),u("div",jt,[e("div",St,[e("div",Mt,[m[2]||(m[2]=e("div",{class:"w-12 h-12 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center flex-shrink-0"},[e("i",{class:"fas fa-exclamation text-white text-xl"})],-1)),e("div",Kt,[e("h3",Pt,U(L.title),1),e("p",Rt,U(L.message),1)])]),e("div",Gt,[e("button",{onClick:m[0]||(m[0]=g=>E.$emit("cancel")),class:"flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-xl font-medium hover:bg-gray-200 transition-colors"},U(L.cancelText),1),e("button",{onClick:m[1]||(m[1]=g=>E.$emit("confirm")),class:"flex-1 px-4 py-2.5 bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-xl font-medium hover:from-yellow-600 hover:to-orange-600 transition-colors shadow-sm"},U(L.confirmText),1)])])])):$("",!0)]))}},Lt={key:0,class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},Dt={class:"modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar"},Ot={class:"flex items-center justify-between mb-6"},Et={class:"flex items-center gap-3"},zt={class:"text-xl font-bold text-gray-900"},Wt={key:0,class:"flex items-center justify-center mb-8"},qt={class:"flex items-center space-x-4"},Bt={class:"flex items-center"},Nt={class:"flex items-center"},Ft={key:1},Ht={class:"space-y-6"},_t={key:0},Jt={class:"flex gap-4"},Yt={class:"flex items-center cursor-pointer"},Qt={class:"flex items-center cursor-pointer"},Xt={class:"flex items-center cursor-pointer"},Zt={key:1},es={class:"flex gap-4"},ts={class:"flex items-center cursor-pointer"},ss={class:"flex items-center cursor-pointer"},ls={key:0,class:"text-red-500 text-xs mt-1"},as={class:"flex gap-4"},os={class:"flex items-center cursor-pointer"},ns={class:"flex items-center cursor-pointer"},rs={key:2},is={key:3,class:"space-y-4"},us={key:0,class:"text-red-500 text-xs mt-1"},ds={key:0,class:"text-red-500 text-xs mt-1"},ps={class:"mb-2 flex gap-2"},cs={key:4},ms={key:5,class:"space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200"},fs={class:"flex items-start gap-3 mb-4"},xs={key:0,class:"text-sm text-blue-800 mb-2"},vs={key:1,class:"text-sm text-blue-800 mb-2"},ys={class:"bg-white/80 rounded-lg p-3 mt-2 mb-2 border border-blue-300"},gs={key:0,class:"text-xs text-blue-800"},bs={key:1,class:"text-xs text-blue-800"},ws={key:0,class:"text-red-500 text-xs mt-1"},ks={class:"flex gap-3 pt-4"},hs=["disabled"],$s=["disabled"],Cs={key:0,class:"loading-spinner mr-2"},Ts={key:3,class:"space-y-6"},As={class:"flex gap-4"},Us={class:"flex items-center cursor-pointer"},Vs={class:"flex items-center cursor-pointer"},Is={key:0},js={key:1},Ss={key:2,class:"space-y-4"},Ms={class:"mb-2 flex gap-2"},Ks={key:3,class:"bg-amber-50 p-4 rounded-lg border border-amber-200"},Ps={class:"space-y-4"},Rs={class:"flex gap-3 pt-4"},Gs=["disabled"],Ls={key:0,class:"loading-spinner mr-2"},be={__name:"AccountForm",props:{account:{type:Object,default:null}},emits:["close","success"],setup(L,{emit:E}){var X,Z,ee,x,c,b,q,ae,p,l,o,k;const m=L,g=E,d=$e(),{showConfirmModal:S,confirmOptions:I,showConfirm:C,handleConfirm:A,handleCancel:y}=he(),f=se(()=>!!m.account),M=T(!0),D=T(1),P=T(!1),_=()=>{var n;return(n=m.account)!=null&&n.proxy&&m.account.proxy.host&&m.account.proxy.port?{enabled:!0,type:m.account.proxy.type||"socks5",host:m.account.proxy.host,port:m.account.proxy.port,username:m.account.proxy.username||"",password:m.account.proxy.password||""}:{enabled:!1,type:"socks5",host:"",port:"",username:"",password:""}},s=T({platform:((X=m.account)==null?void 0:X.platform)||"claude",addType:"oauth",name:((Z=m.account)==null?void 0:Z.name)||"",description:((ee=m.account)==null?void 0:ee.description)||"",accountType:((x=m.account)==null?void 0:x.accountType)||"shared",projectId:((c=m.account)==null?void 0:c.projectId)||"",accessToken:"",refreshToken:"",proxy:_(),apiUrl:((b=m.account)==null?void 0:b.apiUrl)||"",apiKey:((q=m.account)==null?void 0:q.apiKey)||"",priority:((ae=m.account)==null?void 0:ae.priority)||50,supportedModels:((l=(p=m.account)==null?void 0:p.supportedModels)==null?void 0:l.join(` -`))||"",userAgent:((o=m.account)==null?void 0:o.userAgent)||"",rateLimitDuration:((k=m.account)==null?void 0:k.rateLimitDuration)||60}),v=T({name:"",accessToken:"",apiUrl:"",apiKey:""}),r=se(()=>{var n;return((n=s.value.name)==null?void 0:n.trim())&&s.value.platform});se(()=>{var n,t,a;return s.value.addType==="manual"?((n=s.value.name)==null?void 0:n.trim())&&((t=s.value.accessToken)==null?void 0:t.trim()):(a=s.value.name)==null?void 0:a.trim()});const R=async()=>{if(v.value.name="",!r.value){(!s.value.name||s.value.name.trim()==="")&&(v.value.name="请填写账户名称");return}s.value.platform==="gemini"&&D.value===1&&s.value.addType==="oauth"&&(!s.value.projectId||s.value.projectId.trim()==="")&&!await C("项目编号未填写",`您尚未填写项目编号。 - -如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。 -如果您使用的是普通个人账号,可以继续不填写。`,"继续","返回填写")||(D.value=2)},N=async n=>{P.value=!0;try{const t={name:s.value.name,description:s.value.description,accountType:s.value.accountType,proxy:s.value.proxy.enabled?{type:s.value.proxy.type,host:s.value.proxy.host,port:parseInt(s.value.proxy.port),username:s.value.proxy.username||null,password:s.value.proxy.password||null}:null};s.value.platform==="claude"?(t.claudeAiOauth=n.claudeAiOauth||n,t.priority=s.value.priority||50):s.value.platform==="gemini"&&(t.geminiOauth=n.tokens||n,s.value.projectId&&(t.projectId=s.value.projectId));let a;s.value.platform==="claude"?a=await d.createClaudeAccount(t):a=await d.createGeminiAccount(t),g("success",a)}catch(t){V(t.message||"账户创建失败","error")}finally{P.value=!1}},B=async()=>{v.value.name="",v.value.accessToken="",v.value.apiUrl="",v.value.apiKey="";let n=!1;if((!s.value.name||s.value.name.trim()==="")&&(v.value.name="请填写账户名称",n=!0),s.value.platform==="claude-console"?((!s.value.apiUrl||s.value.apiUrl.trim()==="")&&(v.value.apiUrl="请填写 API URL",n=!0),(!s.value.apiKey||s.value.apiKey.trim()==="")&&(v.value.apiKey="请填写 API Key",n=!0)):s.value.addType==="manual"&&(!s.value.accessToken||s.value.accessToken.trim()==="")&&(v.value.accessToken="请填写 Access Token",n=!0),!n){P.value=!0;try{const t={name:s.value.name,description:s.value.description,accountType:s.value.accountType,proxy:s.value.proxy.enabled?{type:s.value.proxy.type,host:s.value.proxy.host,port:parseInt(s.value.proxy.port),username:s.value.proxy.username||null,password:s.value.proxy.password||null}:null};if(s.value.platform==="claude"){const z=s.value.refreshToken?6e5:31536e6;t.claudeAiOauth={accessToken:s.value.accessToken,refreshToken:s.value.refreshToken||"",expiresAt:Date.now()+z,scopes:["user:inference"]},t.priority=s.value.priority||50}else if(s.value.platform==="gemini"){const z=s.value.refreshToken?6e5:31536e6;t.geminiOauth={access_token:s.value.accessToken,refresh_token:s.value.refreshToken||"",scope:"https://www.googleapis.com/auth/cloud-platform",token_type:"Bearer",expiry_date:Date.now()+z},s.value.projectId&&(t.projectId=s.value.projectId)}else s.value.platform==="claude-console"&&(t.apiUrl=s.value.apiUrl,t.apiKey=s.value.apiKey,t.priority=s.value.priority||50,t.supportedModels=s.value.supportedModels?s.value.supportedModels.split(` -`).filter(z=>z.trim()):[],t.userAgent=s.value.userAgent||null,t.rateLimitDuration=s.value.rateLimitDuration||60);let a;s.value.platform==="claude"?a=await d.createClaudeAccount(t):s.value.platform==="claude-console"?a=await d.createClaudeConsoleAccount(t):a=await d.createGeminiAccount(t),g("success",a)}catch(t){V(t.message||"账户创建失败","error")}finally{P.value=!1}}},Q=async()=>{if(v.value.name="",!s.value.name||s.value.name.trim()===""){v.value.name="请填写账户名称";return}if(!(s.value.platform==="gemini"&&(!s.value.projectId||s.value.projectId.trim()==="")&&!await C("项目编号未填写",`您尚未填写项目编号。 - -如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。 -如果您使用的是普通个人账号,可以继续不填写。`,"继续保存","返回填写"))){P.value=!0;try{const n={name:s.value.name,description:s.value.description,accountType:s.value.accountType,proxy:s.value.proxy.enabled?{type:s.value.proxy.type,host:s.value.proxy.host,port:parseInt(s.value.proxy.port),username:s.value.proxy.username||null,password:s.value.proxy.password||null}:null};if(s.value.accessToken||s.value.refreshToken){if(m.account.platform==="claude"){const t=s.value.refreshToken?6e5:31536e6;n.claudeAiOauth={accessToken:s.value.accessToken||"",refreshToken:s.value.refreshToken||"",expiresAt:Date.now()+t,scopes:["user:inference"]}}else if(m.account.platform==="gemini"){const t=s.value.refreshToken?6e5:31536e6;n.geminiOauth={access_token:s.value.accessToken||"",refresh_token:s.value.refreshToken||"",scope:"https://www.googleapis.com/auth/cloud-platform",token_type:"Bearer",expiry_date:Date.now()+t}}}m.account.platform==="gemini"&&s.value.projectId&&(n.projectId=s.value.projectId),m.account.platform==="claude"&&(n.priority=s.value.priority||50),m.account.platform==="claude-console"&&(n.apiUrl=s.value.apiUrl,s.value.apiKey&&(n.apiKey=s.value.apiKey),n.priority=s.value.priority||50,n.supportedModels=s.value.supportedModels?s.value.supportedModels.split(` -`).filter(t=>t.trim()):[],n.userAgent=s.value.userAgent||null,n.rateLimitDuration=s.value.rateLimitDuration||60),m.account.platform==="claude"?await d.updateClaudeAccount(m.account.id,n):m.account.platform==="claude-console"?await d.updateClaudeConsoleAccount(m.account.id,n):await d.updateGeminiAccount(m.account.id,n),g("success")}catch(n){V(n.message||"账户更新失败","error")}finally{P.value=!1}}};O(()=>s.value.name,()=>{var n;v.value.name&&((n=s.value.name)!=null&&n.trim())&&(v.value.name="")}),O(()=>s.value.accessToken,()=>{var n;v.value.accessToken&&((n=s.value.accessToken)!=null&&n.trim())&&(v.value.accessToken="")}),O(()=>s.value.apiUrl,()=>{var n;v.value.apiUrl&&((n=s.value.apiUrl)!=null&&n.trim())&&(v.value.apiUrl="")}),O(()=>s.value.apiKey,()=>{var n;v.value.apiKey&&((n=s.value.apiKey)!=null&&n.trim())&&(v.value.apiKey="")}),O(()=>s.value.platform,n=>{n==="claude-console"&&(s.value.addType="manual")});const F=n=>{const t=s.value.supportedModels?s.value.supportedModels.split(` -`).filter(a=>a.trim()):[];if(t.includes(n)){V(`模型 ${n} 已存在`,"info");return}t.push(n),s.value.supportedModels=t.join(` -`),V(`已添加模型 ${n}`,"success")};return O(()=>m.account,n=>{var t;if(n){const a=n.proxy&&n.proxy.host&&n.proxy.port?{enabled:!0,type:n.proxy.type||"socks5",host:n.proxy.host,port:n.proxy.port,username:n.proxy.username||"",password:n.proxy.password||""}:{enabled:!1,type:"socks5",host:"",port:"",username:"",password:""};s.value={platform:n.platform,addType:"oauth",name:n.name,description:n.description||"",accountType:n.accountType||"shared",projectId:n.projectId||"",accessToken:"",refreshToken:"",proxy:a,apiUrl:n.apiUrl||"",apiKey:"",priority:n.priority||50,supportedModels:((t=n.supportedModels)==null?void 0:t.join(` -`))||"",userAgent:n.userAgent||"",rateLimitDuration:n.rateLimitDuration||60}}},{immediate:!0}),(n,t)=>(i(),le(ke,{to:"body"},[M.value?(i(),u("div",Lt,[e("div",Dt,[e("div",Ot,[e("div",Et,[t[43]||(t[43]=e("div",{class:"w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center"},[e("i",{class:"fas fa-user-circle text-white"})],-1)),e("h3",zt,U(f.value?"编辑账户":"添加账户"),1)]),e("button",{onClick:t[0]||(t[0]=a=>n.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},t[44]||(t[44]=[e("i",{class:"fas fa-times text-xl"},null,-1)]))]),!f.value&&s.value.addType==="oauth"?(i(),u("div",Wt,[e("div",qt,[e("div",Bt,[e("div",{class:G(["w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold",D.value>=1?"bg-blue-500 text-white":"bg-gray-200 text-gray-500"])}," 1 ",2),t[45]||(t[45]=e("span",{class:"ml-2 text-sm font-medium text-gray-700"},"基本信息",-1))]),t[47]||(t[47]=e("div",{class:"w-8 h-0.5 bg-gray-300"},null,-1)),e("div",Nt,[e("div",{class:G(["w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold",D.value>=2?"bg-blue-500 text-white":"bg-gray-200 text-gray-500"])}," 2 ",2),t[46]||(t[46]=e("span",{class:"ml-2 text-sm font-medium text-gray-700"},"授权认证",-1))])])])):$("",!0),D.value===1&&!f.value?(i(),u("div",Ft,[e("div",Ht,[f.value?$("",!0):(i(),u("div",_t,[t[51]||(t[51]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"平台",-1)),e("div",Jt,[e("label",Yt,[w(e("input",{type:"radio","onUpdate:modelValue":t[1]||(t[1]=a=>s.value.platform=a),value:"claude",class:"mr-2"},null,512),[[H,s.value.platform]]),t[48]||(t[48]=e("span",{class:"text-sm text-gray-700"},"Claude",-1))]),e("label",Qt,[w(e("input",{type:"radio","onUpdate:modelValue":t[2]||(t[2]=a=>s.value.platform=a),value:"claude-console",class:"mr-2"},null,512),[[H,s.value.platform]]),t[49]||(t[49]=e("span",{class:"text-sm text-gray-700"},"Claude Console",-1))]),e("label",Xt,[w(e("input",{type:"radio","onUpdate:modelValue":t[3]||(t[3]=a=>s.value.platform=a),value:"gemini",class:"mr-2"},null,512),[[H,s.value.platform]]),t[50]||(t[50]=e("span",{class:"text-sm text-gray-700"},"Gemini",-1))])])])),!f.value&&s.value.platform!=="claude-console"?(i(),u("div",Zt,[t[54]||(t[54]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"添加方式",-1)),e("div",es,[e("label",ts,[w(e("input",{type:"radio","onUpdate:modelValue":t[4]||(t[4]=a=>s.value.addType=a),value:"oauth",class:"mr-2"},null,512),[[H,s.value.addType]]),t[52]||(t[52]=e("span",{class:"text-sm text-gray-700"},"OAuth 授权 (推荐)",-1))]),e("label",ss,[w(e("input",{type:"radio","onUpdate:modelValue":t[5]||(t[5]=a=>s.value.addType=a),value:"manual",class:"mr-2"},null,512),[[H,s.value.addType]]),t[53]||(t[53]=e("span",{class:"text-sm text-gray-700"},"手动输入 Access Token",-1))])])])):$("",!0),e("div",null,[t[55]||(t[55]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户名称",-1)),w(e("input",{"onUpdate:modelValue":t[6]||(t[6]=a=>s.value.name=a),type:"text",required:"",class:G(["form-input w-full",{"border-red-500":v.value.name}]),placeholder:"为账户设置一个易识别的名称"},null,2),[[j,s.value.name]]),v.value.name?(i(),u("p",ls,U(v.value.name),1)):$("",!0)]),e("div",null,[t[56]||(t[56]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"描述 (可选)",-1)),w(e("textarea",{"onUpdate:modelValue":t[7]||(t[7]=a=>s.value.description=a),rows:"3",class:"form-input w-full resize-none",placeholder:"账户用途说明..."},null,512),[[j,s.value.description]])]),e("div",null,[t[59]||(t[59]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户类型",-1)),e("div",as,[e("label",os,[w(e("input",{type:"radio","onUpdate:modelValue":t[8]||(t[8]=a=>s.value.accountType=a),value:"shared",class:"mr-2"},null,512),[[H,s.value.accountType]]),t[57]||(t[57]=e("span",{class:"text-sm text-gray-700"},"共享账户",-1))]),e("label",ns,[w(e("input",{type:"radio","onUpdate:modelValue":t[9]||(t[9]=a=>s.value.accountType=a),value:"dedicated",class:"mr-2"},null,512),[[H,s.value.accountType]]),t[58]||(t[58]=e("span",{class:"text-sm text-gray-700"},"专属账户",-1))])]),t[60]||(t[60]=e("p",{class:"text-xs text-gray-500 mt-2"}," 共享账户:供所有API Key使用;专属账户:仅供特定API Key使用 ",-1))]),s.value.platform==="gemini"?(i(),u("div",rs,[t[61]||(t[61]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"项目编号 (可选)",-1)),w(e("input",{"onUpdate:modelValue":t[10]||(t[10]=a=>s.value.projectId=a),type:"text",class:"form-input w-full",placeholder:"例如:123456789012(纯数字)"},null,512),[[j,s.value.projectId]]),t[62]||(t[62]=e("div",{class:"mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg"},[e("div",{class:"flex items-start gap-2"},[e("i",{class:"fas fa-info-circle text-yellow-600 mt-0.5"}),e("div",{class:"text-xs text-yellow-700"},[e("p",{class:"font-medium mb-1"},"Google Cloud/Workspace 账号需要提供项目编号"),e("p",null,"某些 Google 账号(特别是绑定了 Google Cloud 的账号)会被识别为 Workspace 账号,需要提供额外的项目编号。"),e("div",{class:"mt-2 p-2 bg-white rounded border border-yellow-300"},[e("p",{class:"font-medium mb-1"},"如何获取项目编号:"),e("ol",{class:"list-decimal list-inside space-y-1 ml-2"},[e("li",null,[h("访问 "),e("a",{href:"https://console.cloud.google.com/welcome",target:"_blank",class:"text-blue-600 hover:underline font-medium"},"Google Cloud Console")]),e("li",null,[h("复制"),e("span",{class:"font-semibold text-red-600"},"项目编号(Project Number)"),h(",通常是12位纯数字")]),e("li",{class:"text-red-600"},"⚠️ 注意:不要复制项目ID(Project ID),要复制项目编号!")])]),e("p",{class:"mt-2"},[e("strong",null,"提示:"),h("如果您的账号是普通个人账号(未绑定 Google Cloud),请留空此字段。")])])])],-1))])):$("",!0),s.value.platform==="claude-console"&&!f.value?(i(),u("div",is,[e("div",null,[t[63]||(t[63]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"API URL *",-1)),w(e("input",{"onUpdate:modelValue":t[11]||(t[11]=a=>s.value.apiUrl=a),type:"text",required:"",class:G(["form-input w-full",{"border-red-500":v.value.apiUrl}]),placeholder:"例如:https://api.example.com"},null,2),[[j,s.value.apiUrl]]),v.value.apiUrl?(i(),u("p",us,U(v.value.apiUrl),1)):$("",!0)]),e("div",null,[t[64]||(t[64]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"API Key *",-1)),w(e("input",{"onUpdate:modelValue":t[12]||(t[12]=a=>s.value.apiKey=a),type:"password",required:"",class:G(["form-input w-full",{"border-red-500":v.value.apiKey}]),placeholder:"请输入API Key"},null,2),[[j,s.value.apiKey]]),v.value.apiKey?(i(),u("p",ds,U(v.value.apiKey),1)):$("",!0)]),e("div",null,[t[65]||(t[65]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"支持的模型 (可选)--注意,ClaudeCode必须加上hiku模型!",-1)),e("div",ps,[e("button",{type:"button",onClick:t[13]||(t[13]=a=>F("claude-sonnet-4-20250514")),class:"px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"}," + claude-sonnet-4-20250514 "),e("button",{type:"button",onClick:t[14]||(t[14]=a=>F("claude-opus-4-20250514")),class:"px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"}," + claude-opus-4-20250514 "),e("button",{type:"button",onClick:t[15]||(t[15]=a=>F("claude-3-5-haiku-20241022")),class:"px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"}," + claude-3-5-haiku-20241022 ")]),w(e("textarea",{"onUpdate:modelValue":t[16]||(t[16]=a=>s.value.supportedModels=a),rows:"3",class:"form-input w-full resize-none",placeholder:"每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"},null,512),[[j,s.value.supportedModels]]),t[66]||(t[66]=e("p",{class:"text-xs text-gray-500 mt-1"},"留空表示支持所有模型。如果指定模型,请求中的模型不在列表内将不会调度到此账号",-1))]),e("div",null,[t[67]||(t[67]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"自定义 User-Agent (可选)",-1)),w(e("input",{"onUpdate:modelValue":t[17]||(t[17]=a=>s.value.userAgent=a),type:"text",class:"form-input w-full",placeholder:"默认:claude-cli/1.0.61 (console, cli)"},null,512),[[j,s.value.userAgent]])]),e("div",null,[t[68]||(t[68]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"限流时间 (分钟)",-1)),w(e("input",{"onUpdate:modelValue":t[18]||(t[18]=a=>s.value.rateLimitDuration=a),type:"number",min:"1",class:"form-input w-full",placeholder:"默认60分钟"},null,512),[[j,s.value.rateLimitDuration,void 0,{number:!0}]]),t[69]||(t[69]=e("p",{class:"text-xs text-gray-500 mt-1"},"当账号返回429错误时,暂停调度的时间(分钟)",-1))])])):$("",!0),s.value.platform==="claude"||s.value.platform==="claude-console"?(i(),u("div",cs,[t[70]||(t[70]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"调度优先级 (1-100)",-1)),w(e("input",{"onUpdate:modelValue":t[19]||(t[19]=a=>s.value.priority=a),type:"number",min:"1",max:"100",class:"form-input w-full",placeholder:"数字越小优先级越高,默认50"},null,512),[[j,s.value.priority,void 0,{number:!0}]]),t[71]||(t[71]=e("p",{class:"text-xs text-gray-500 mt-1"},"数字越小优先级越高,建议范围:1-100",-1))])):$("",!0),s.value.addType==="manual"&&s.value.platform!=="claude-console"?(i(),u("div",ms,[e("div",fs,[t[77]||(t[77]=e("div",{class:"w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1"},[e("i",{class:"fas fa-info text-white text-sm"})],-1)),e("div",null,[t[75]||(t[75]=e("h5",{class:"font-semibold text-blue-900 mb-2"},"手动输入 Token",-1)),s.value.platform==="claude"?(i(),u("p",xs," 请输入有效的 Claude Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。 ")):s.value.platform==="gemini"?(i(),u("p",vs," 请输入有效的 Gemini Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。 ")):$("",!0),e("div",ys,[t[74]||(t[74]=e("p",{class:"text-sm text-blue-900 font-medium mb-1"},[e("i",{class:"fas fa-folder-open mr-1"}),h(" 获取 Access Token 的方法: ")],-1)),s.value.platform==="claude"?(i(),u("p",gs,t[72]||(t[72]=[h(" 请从已登录 Claude Code 的机器上获取 ",-1),e("code",{class:"bg-blue-100 px-1 py-0.5 rounded font-mono"},"~/.claude/.credentials.json",-1),h(" 文件中的凭证, 请勿使用 Claude 官网 API Keys 页面的密钥。 ",-1)]))):s.value.platform==="gemini"?(i(),u("p",bs,t[73]||(t[73]=[h(" 请从已登录 Gemini CLI 的机器上获取 ",-1),e("code",{class:"bg-blue-100 px-1 py-0.5 rounded font-mono"},"~/.config/gemini/credentials.json",-1),h(" 文件中的凭证。 ",-1)]))):$("",!0)]),t[76]||(t[76]=e("p",{class:"text-xs text-blue-600"},"💡 如果未填写 Refresh Token,Token 过期后需要手动更新。",-1))])]),e("div",null,[t[78]||(t[78]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"Access Token *",-1)),w(e("textarea",{"onUpdate:modelValue":t[20]||(t[20]=a=>s.value.accessToken=a),rows:"4",required:"",class:G(["form-input w-full resize-none font-mono text-xs",{"border-red-500":v.value.accessToken}]),placeholder:"请输入 Access Token..."},null,2),[[j,s.value.accessToken]]),v.value.accessToken?(i(),u("p",ws,U(v.value.accessToken),1)):$("",!0)]),e("div",null,[t[79]||(t[79]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"Refresh Token (可选)",-1)),w(e("textarea",{"onUpdate:modelValue":t[21]||(t[21]=a=>s.value.refreshToken=a),rows:"4",class:"form-input w-full resize-none font-mono text-xs",placeholder:"请输入 Refresh Token..."},null,512),[[j,s.value.refreshToken]])])])):$("",!0),ne(ge,{modelValue:s.value.proxy,"onUpdate:modelValue":t[22]||(t[22]=a=>s.value.proxy=a)},null,8,["modelValue"]),e("div",ks,[e("button",{type:"button",onClick:t[23]||(t[23]=a=>n.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),s.value.addType==="oauth"&&s.value.platform!=="claude-console"?(i(),u("button",{key:0,type:"button",onClick:R,disabled:P.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"}," 下一步 ",8,hs)):(i(),u("button",{key:1,type:"button",onClick:B,disabled:P.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[P.value?(i(),u("div",Cs)):$("",!0),h(" "+U(P.value?"创建中...":"创建"),1)],8,$s))])])])):$("",!0),D.value===2&&s.value.addType==="oauth"?(i(),le(It,{key:2,platform:s.value.platform,proxy:s.value.proxy,onSuccess:N,onBack:t[24]||(t[24]=a=>D.value=1)},null,8,["platform","proxy"])):$("",!0),f.value?(i(),u("div",Ts,[e("div",null,[t[80]||(t[80]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户名称",-1)),w(e("input",{"onUpdate:modelValue":t[25]||(t[25]=a=>s.value.name=a),type:"text",required:"",class:"form-input w-full",placeholder:"为账户设置一个易识别的名称"},null,512),[[j,s.value.name]])]),e("div",null,[t[81]||(t[81]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"描述 (可选)",-1)),w(e("textarea",{"onUpdate:modelValue":t[26]||(t[26]=a=>s.value.description=a),rows:"3",class:"form-input w-full resize-none",placeholder:"账户用途说明..."},null,512),[[j,s.value.description]])]),e("div",null,[t[84]||(t[84]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"账户类型",-1)),e("div",As,[e("label",Us,[w(e("input",{type:"radio","onUpdate:modelValue":t[27]||(t[27]=a=>s.value.accountType=a),value:"shared",class:"mr-2"},null,512),[[H,s.value.accountType]]),t[82]||(t[82]=e("span",{class:"text-sm text-gray-700"},"共享账户",-1))]),e("label",Vs,[w(e("input",{type:"radio","onUpdate:modelValue":t[28]||(t[28]=a=>s.value.accountType=a),value:"dedicated",class:"mr-2"},null,512),[[H,s.value.accountType]]),t[83]||(t[83]=e("span",{class:"text-sm text-gray-700"},"专属账户",-1))])]),t[85]||(t[85]=e("p",{class:"text-xs text-gray-500 mt-2"}," 共享账户:供所有API Key使用;专属账户:仅供特定API Key使用 ",-1))]),s.value.platform==="gemini"?(i(),u("div",Is,[t[86]||(t[86]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"项目编号 (可选)",-1)),w(e("input",{"onUpdate:modelValue":t[29]||(t[29]=a=>s.value.projectId=a),type:"text",class:"form-input w-full",placeholder:"例如:123456789012(纯数字)"},null,512),[[j,s.value.projectId]]),t[87]||(t[87]=e("p",{class:"text-xs text-gray-500 mt-2"}," Google Cloud/Workspace 账号可能需要提供项目编号 ",-1))])):$("",!0),s.value.platform==="claude"||s.value.platform==="claude-console"?(i(),u("div",js,[t[88]||(t[88]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"调度优先级 (1-100)",-1)),w(e("input",{"onUpdate:modelValue":t[30]||(t[30]=a=>s.value.priority=a),type:"number",min:"1",max:"100",class:"form-input w-full",placeholder:"数字越小优先级越高"},null,512),[[j,s.value.priority,void 0,{number:!0}]]),t[89]||(t[89]=e("p",{class:"text-xs text-gray-500 mt-1"},"数字越小优先级越高,建议范围:1-100",-1))])):$("",!0),s.value.platform==="claude-console"?(i(),u("div",Ss,[e("div",null,[t[90]||(t[90]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"API URL",-1)),w(e("input",{"onUpdate:modelValue":t[31]||(t[31]=a=>s.value.apiUrl=a),type:"text",required:"",class:"form-input w-full",placeholder:"例如:https://api.example.com"},null,512),[[j,s.value.apiUrl]])]),e("div",null,[t[91]||(t[91]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"API Key",-1)),w(e("input",{"onUpdate:modelValue":t[32]||(t[32]=a=>s.value.apiKey=a),type:"password",class:"form-input w-full",placeholder:"留空表示不更新"},null,512),[[j,s.value.apiKey]]),t[92]||(t[92]=e("p",{class:"text-xs text-gray-500 mt-1"},"留空表示不更新 API Key",-1))]),e("div",null,[t[93]||(t[93]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"支持的模型 (可选)",-1)),e("div",Ms,[e("button",{type:"button",onClick:t[33]||(t[33]=a=>F("claude-sonnet-4-20250514")),class:"px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"}," + claude-sonnet-4-20250514 "),e("button",{type:"button",onClick:t[34]||(t[34]=a=>F("claude-opus-4-20250514")),class:"px-3 py-1 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"}," + claude-opus-4-20250514 "),e("button",{type:"button",onClick:t[35]||(t[35]=a=>F("claude-3-5-haiku-20241022")),class:"px-3 py-1 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-purple-200 transition-colors"}," + claude-3-5-haiku-20241022 ")]),w(e("textarea",{"onUpdate:modelValue":t[36]||(t[36]=a=>s.value.supportedModels=a),rows:"3",class:"form-input w-full resize-none",placeholder:"每行一个模型,留空表示支持所有模型。特别注意,ClaudeCode必须加上hiku模型!"},null,512),[[j,s.value.supportedModels]])]),e("div",null,[t[94]||(t[94]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"自定义 User-Agent (可选)",-1)),w(e("input",{"onUpdate:modelValue":t[37]||(t[37]=a=>s.value.userAgent=a),type:"text",class:"form-input w-full",placeholder:"默认:claude-cli/1.0.61 (console, cli)"},null,512),[[j,s.value.userAgent]])]),e("div",null,[t[95]||(t[95]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"限流时间 (分钟)",-1)),w(e("input",{"onUpdate:modelValue":t[38]||(t[38]=a=>s.value.rateLimitDuration=a),type:"number",min:"1",class:"form-input w-full"},null,512),[[j,s.value.rateLimitDuration,void 0,{number:!0}]])])])):$("",!0),s.value.platform!=="claude-console"?(i(),u("div",Ks,[t[98]||(t[98]=e("div",{class:"flex items-start gap-3 mb-4"},[e("div",{class:"w-8 h-8 bg-amber-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1"},[e("i",{class:"fas fa-key text-white text-sm"})]),e("div",null,[e("h5",{class:"font-semibold text-amber-900 mb-2"},"更新 Token"),e("p",{class:"text-sm text-amber-800 mb-2"},"可以更新 Access Token 和 Refresh Token。为了安全起见,不会显示当前的 Token 值。"),e("p",{class:"text-xs text-amber-600"},"💡 留空表示不更新该字段。")])],-1)),e("div",Ps,[e("div",null,[t[96]||(t[96]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"新的 Access Token",-1)),w(e("textarea",{"onUpdate:modelValue":t[39]||(t[39]=a=>s.value.accessToken=a),rows:"4",class:"form-input w-full resize-none font-mono text-xs",placeholder:"留空表示不更新..."},null,512),[[j,s.value.accessToken]])]),e("div",null,[t[97]||(t[97]=e("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"新的 Refresh Token",-1)),w(e("textarea",{"onUpdate:modelValue":t[40]||(t[40]=a=>s.value.refreshToken=a),rows:"4",class:"form-input w-full resize-none font-mono text-xs",placeholder:"留空表示不更新..."},null,512),[[j,s.value.refreshToken]])])])])):$("",!0),ne(ge,{modelValue:s.value.proxy,"onUpdate:modelValue":t[41]||(t[41]=a=>s.value.proxy=a)},null,8,["modelValue"]),e("div",Rs,[e("button",{type:"button",onClick:t[42]||(t[42]=a=>n.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),e("button",{type:"button",onClick:Q,disabled:P.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[P.value?(i(),u("div",Ls)):$("",!0),h(" "+U(P.value?"更新中...":"更新"),1)],8,Gs)])])):$("",!0)])])):$("",!0),ne(Ce,{show:W(S),title:W(I).title,message:W(I).message,"confirm-text":W(I).confirmText,"cancel-text":W(I).cancelText,onConfirm:W(A),onCancel:W(y)},null,8,["show","title","message","confirm-text","cancel-text","onConfirm","onCancel"])]))}},Ds={class:"accounts-container"},Os={class:"card p-6"},Es={class:"flex flex-col md:flex-row justify-between items-center gap-4 mb-6"},zs={class:"flex gap-2"},Ws={key:0,class:"text-center py-12"},qs={key:1,class:"text-center py-12"},Bs={key:2,class:"table-container"},Ns={class:"min-w-full"},Fs={class:"bg-gray-50/80 backdrop-blur-sm"},Hs={key:1,class:"fas fa-sort ml-1 text-gray-400"},_s={key:1,class:"fas fa-sort ml-1 text-gray-400"},Js={key:1,class:"fas fa-sort ml-1 text-gray-400"},Ys={key:1,class:"fas fa-sort ml-1 text-gray-400"},Qs={key:1,class:"fas fa-sort ml-1 text-gray-400"},Xs={class:"divide-y divide-gray-200/50"},Zs={class:"px-6 py-4 whitespace-nowrap"},el={class:"flex items-center"},tl={class:"flex items-center gap-2"},sl={class:"text-sm font-semibold text-gray-900"},ll={key:0,class:"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"},al={key:1,class:"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"},ol={class:"text-xs text-gray-500"},nl={class:"px-6 py-4 whitespace-nowrap"},rl={key:0,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"},il={key:1,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-purple-100 text-purple-800"},ul={key:2,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800"},dl={class:"px-6 py-4 whitespace-nowrap"},pl={key:0,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800"},cl={key:1,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"},ml={key:2,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-orange-100 text-orange-800"},fl={class:"px-6 py-4 whitespace-nowrap"},xl={class:"flex flex-col gap-1"},vl={key:0,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800"},yl={key:1,class:"inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700"},gl=["title"],bl={key:3,class:"text-xs text-gray-500"},wl={class:"px-6 py-4 whitespace-nowrap"},kl={key:0,class:"flex items-center gap-2"},hl={class:"w-16 bg-gray-200 rounded-full h-2"},$l={class:"text-xs text-gray-700 font-medium min-w-[20px]"},Cl={key:1,class:"text-gray-400 text-sm"},Tl={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-600"},Al={key:0,class:"text-xs bg-blue-50 px-2 py-1 rounded font-mono"},Ul={key:1,class:"text-gray-400"},Vl={class:"px-6 py-4 whitespace-nowrap text-sm"},Il={key:0,class:"space-y-1"},jl={class:"flex items-center gap-2"},Sl={class:"text-sm font-medium text-gray-900"},Ml={class:"flex items-center gap-2"},Kl={class:"text-xs text-gray-600"},Pl={key:0,class:"text-xs text-gray-500"},Rl={key:1,class:"text-gray-400 text-xs"},Gl={class:"px-6 py-4 whitespace-nowrap"},Ll={key:0,class:"space-y-2"},Dl={class:"flex items-center gap-2"},Ol={class:"w-24 bg-gray-200 rounded-full h-2"},El={class:"text-xs text-gray-700 font-medium min-w-[32px]"},zl={class:"text-xs text-gray-600"},Wl={key:0,class:"text-indigo-600 font-medium"},ql={key:1,class:"text-gray-400 text-sm"},Bl={key:2,class:"text-gray-400 text-sm"},Nl={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-600"},Fl={class:"px-6 py-4 whitespace-nowrap text-sm font-medium"},Hl={class:"flex items-center gap-2"},_l=["onClick","disabled","title"],Jl=["onClick","disabled","title"],Yl=["onClick"],Ql=["onClick"],Xl={__name:"AccountsView",setup(L){const{showConfirmModal:E,confirmOptions:m,showConfirm:g,handleConfirm:d,handleCancel:S}=he(),I=T([]),C=T(!1),A=T("name"),y=T(""),f=T("asc"),M=T([]),D=T(!1),P=T(!1),_=T(null),s=se(()=>y.value?[...I.value].sort((l,o)=>{var t,a,z,Y,re,ie,ue,de,pe,ce,me,fe;let k=l[y.value],n=o[y.value];return y.value==="dailyTokens"?(k=((a=(t=l.usage)==null?void 0:t.daily)==null?void 0:a.allTokens)||0,n=((Y=(z=o.usage)==null?void 0:z.daily)==null?void 0:Y.allTokens)||0):y.value==="dailyRequests"?(k=((ie=(re=l.usage)==null?void 0:re.daily)==null?void 0:ie.requests)||0,n=((de=(ue=o.usage)==null?void 0:ue.daily)==null?void 0:de.requests)||0):y.value==="totalTokens"&&(k=((ce=(pe=l.usage)==null?void 0:pe.total)==null?void 0:ce.allTokens)||0,n=((fe=(me=o.usage)==null?void 0:me.total)==null?void 0:fe.allTokens)||0),y.value==="lastUsed"&&(k=l.lastUsedAt?new Date(l.lastUsedAt).getTime():0,n=o.lastUsedAt?new Date(o.lastUsedAt).getTime():0),y.value==="status"&&(k=l.isActive?1:0,n=o.isActive?1:0),kn?f.value==="asc"?1:-1:0}):I.value),v=async()=>{C.value=!0;try{const[p,l,o,k]=await Promise.all([K.get("/admin/claude-accounts"),K.get("/admin/claude-console-accounts"),K.get("/admin/gemini-accounts"),K.get("/admin/api-keys")]);k.success&&(M.value=k.data||[]);const n=[];if(p.success){const t=(p.data||[]).map(a=>{const z=M.value.filter(Y=>Y.claudeAccountId===a.id).length;return{...a,platform:"claude",boundApiKeysCount:z}});n.push(...t)}if(l.success){const t=(l.data||[]).map(a=>({...a,platform:"claude-console",boundApiKeysCount:0}));n.push(...t)}if(o.success){const t=(o.data||[]).map(a=>{const z=M.value.filter(Y=>Y.geminiAccountId===a.id).length;return{...a,platform:"gemini",boundApiKeysCount:z}});n.push(...t)}I.value=n}catch{V("加载账户失败","error")}finally{C.value=!1}},r=p=>{p&&(y.value===p?f.value=f.value==="asc"?"desc":"asc":(y.value=p,f.value="asc"))},R=p=>{if(p==null)return"0";const l=Number(p);return l>=1e6?Math.floor(l/1e6).toLocaleString()+"M":l.toLocaleString()},N=p=>{if(!p)return"从未使用";const l=new Date(p),k=new Date-l;return k<6e4?"刚刚":k<36e5?`${Math.floor(k/6e4)} 分钟前`:k<864e5?`${Math.floor(k/36e5)} 小时前`:k<6048e5?`${Math.floor(k/864e5)} 天前`:l.toLocaleDateString("zh-CN")},B=async()=>{try{const p=await K.get("/admin/api-keys");p.success&&(M.value=p.data)}catch(p){console.error("Failed to load API keys:",p)}},Q=p=>{if(!p||!p.host||!p.port)return null;let l=`${p.type}://${p.host}:${p.port}`;if(p.username){const o=p.username.length>2?p.username[0]+"***"+p.username[p.username.length-1]:"***",k=p.password?"****":"";l=`${p.type}://${o}:${k}@${p.host}:${p.port}`}return l},F=(p,l)=>{if(!p||!l)return"--";const o=new Date(p),k=new Date(l),n=o.getHours().toString().padStart(2,"0"),t=o.getMinutes().toString().padStart(2,"0"),a=k.getHours().toString().padStart(2,"0"),z=k.getMinutes().toString().padStart(2,"0");return`${n}:${t} - ${a}:${z}`},X=p=>{if(!p||p<=0)return"已结束";const l=Math.floor(p/60),o=p%60;return l>0?`${l}小时${o}分钟`:`${o}分钟`},Z=()=>{D.value=!0},ee=p=>{_.value=p,P.value=!0},x=async p=>{const l=M.value.filter(k=>k.claudeAccountId===p.id||k.geminiAccountId===p.id).length;if(l>0){V(`无法删除此账号,有 ${l} 个API Key绑定到此账号,请先解绑所有API Key`,"error");return}if(await g("删除账户",`确定要删除账户 "${p.name}" 吗? - -此操作不可恢复。`,"删除","取消"))try{let k;p.platform==="claude"?k=`/admin/claude-accounts/${p.id}`:p.platform==="claude-console"?k=`/admin/claude-console-accounts/${p.id}`:k=`/admin/gemini-accounts/${p.id}`;const n=await K.delete(k);n.success?(V("账户已删除","success"),v()):V(n.message||"删除失败","error")}catch{V("删除失败","error")}},c=async p=>{if(!p.isRefreshing)try{p.isRefreshing=!0;const l=await K.post(`/admin/claude-accounts/${p.id}/refresh`);l.success?(V("Token刷新成功","success"),v()):V(l.message||"Token刷新失败","error")}catch{V("Token刷新失败","error")}finally{p.isRefreshing=!1}},b=async p=>{if(!p.isTogglingSchedulable)try{p.isTogglingSchedulable=!0;let l;if(p.platform==="claude")l=`/admin/claude-accounts/${p.id}/toggle-schedulable`;else if(p.platform==="claude-console")l=`/admin/claude-console-accounts/${p.id}/toggle-schedulable`;else{V("Gemini账户暂不支持调度控制","warning");return}const o=await K.put(l);o.success?(p.schedulable=o.schedulable,V(o.schedulable?"已启用调度":"已禁用调度","success")):V(o.message||"操作失败","error")}catch{V("切换调度状态失败","error")}finally{p.isTogglingSchedulable=!1}},q=()=>{D.value=!1,V("账户创建成功","success"),v()},ae=()=>{P.value=!1,V("账户更新成功","success"),v()};return O(A,p=>{const l={name:"name",dailyTokens:"dailyTokens",dailyRequests:"dailyRequests",totalTokens:"totalTokens",lastUsed:"lastUsed"};l[p]&&r(l[p])}),Ve(()=>{v(),B()}),(p,l)=>(i(),u("div",Ds,[e("div",Os,[e("div",Es,[l[11]||(l[11]=e("div",null,[e("h3",{class:"text-xl font-bold text-gray-900 mb-2"},"账户管理"),e("p",{class:"text-gray-600"},"管理您的 Claude 和 Gemini 账户及代理配置")],-1)),e("div",zs,[w(e("select",{"onUpdate:modelValue":l[0]||(l[0]=o=>A.value=o),onChange:l[1]||(l[1]=o=>r()),class:"form-input px-3 py-2 text-sm"},l[9]||(l[9]=[te('',5)]),544),[[we,A.value]]),e("button",{onClick:Ie(Z,["stop"]),class:"btn btn-success px-6 py-3 flex items-center gap-2"},l[10]||(l[10]=[e("i",{class:"fas fa-plus"},null,-1),h("添加账户 ",-1)]))])]),C.value?(i(),u("div",Ws,l[12]||(l[12]=[e("div",{class:"loading-spinner mx-auto mb-4"},null,-1),e("p",{class:"text-gray-500"},"正在加载账户...",-1)]))):s.value.length===0?(i(),u("div",qs,l[13]||(l[13]=[e("div",{class:"w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"},[e("i",{class:"fas fa-user-circle text-gray-400 text-xl"})],-1),e("p",{class:"text-gray-500 text-lg"},"暂无账户",-1),e("p",{class:"text-gray-400 text-sm mt-2"},"点击上方按钮添加您的第一个账户",-1)]))):(i(),u("div",Bs,[e("table",Ns,[e("thead",Fs,[e("tr",null,[e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[2]||(l[2]=o=>r("name"))},[l[14]||(l[14]=h(" 名称 ",-1)),y.value==="name"?(i(),u("i",{key:0,class:G(["fas",f.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),u("i",Hs))]),e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[3]||(l[3]=o=>r("platform"))},[l[15]||(l[15]=h(" 平台 ",-1)),y.value==="platform"?(i(),u("i",{key:0,class:G(["fas",f.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),u("i",_s))]),e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[4]||(l[4]=o=>r("accountType"))},[l[16]||(l[16]=h(" 类型 ",-1)),y.value==="accountType"?(i(),u("i",{key:0,class:G(["fas",f.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),u("i",Js))]),e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[5]||(l[5]=o=>r("status"))},[l[17]||(l[17]=h(" 状态 ",-1)),y.value==="status"?(i(),u("i",{key:0,class:G(["fas",f.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),u("i",Ys))]),e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:l[6]||(l[6]=o=>r("priority"))},[l[18]||(l[18]=h(" 优先级 ",-1)),y.value==="priority"?(i(),u("i",{key:0,class:G(["fas",f.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),u("i",Qs))]),l[19]||(l[19]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"代理",-1)),l[20]||(l[20]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"今日使用",-1)),l[21]||(l[21]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"会话窗口",-1)),l[22]||(l[22]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"最后使用",-1)),l[23]||(l[23]=e("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"操作",-1))])]),e("tbody",Xs,[(i(!0),u(je,null,Se(s.value,o=>(i(),u("tr",{key:o.id,class:"table-row"},[e("td",Zs,[e("div",el,[l[26]||(l[26]=e("div",{class:"w-8 h-8 bg-gradient-to-br from-green-500 to-green-600 rounded-lg flex items-center justify-center mr-3"},[e("i",{class:"fas fa-user-circle text-white text-xs"})],-1)),e("div",null,[e("div",tl,[e("div",sl,U(o.name),1),o.accountType==="dedicated"?(i(),u("span",ll,l[24]||(l[24]=[e("i",{class:"fas fa-lock mr-1"},null,-1),h("专属 ",-1)]))):(i(),u("span",al,l[25]||(l[25]=[e("i",{class:"fas fa-share-alt mr-1"},null,-1),h("共享 ",-1)])))]),e("div",ol,U(o.id),1)])])]),e("td",nl,[o.platform==="gemini"?(i(),u("span",rl,l[27]||(l[27]=[e("i",{class:"fas fa-robot mr-1"},null,-1),h("Gemini ",-1)]))):o.platform==="claude-console"?(i(),u("span",il,l[28]||(l[28]=[e("i",{class:"fas fa-terminal mr-1"},null,-1),h("Claude Console ",-1)]))):(i(),u("span",ul,l[29]||(l[29]=[e("i",{class:"fas fa-brain mr-1"},null,-1),h("Claude ",-1)])))]),e("td",dl,[o.platform==="claude-console"?(i(),u("span",pl,l[30]||(l[30]=[e("i",{class:"fas fa-key mr-1"},null,-1),h("API Key ",-1)]))):o.scopes&&o.scopes.length>0?(i(),u("span",cl,l[31]||(l[31]=[e("i",{class:"fas fa-lock mr-1"},null,-1),h("OAuth ",-1)]))):(i(),u("span",ml,l[32]||(l[32]=[e("i",{class:"fas fa-key mr-1"},null,-1),h("传统 ",-1)])))]),e("td",fl,[e("div",xl,[e("span",{class:G(["inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold",o.status==="blocked"?"bg-orange-100 text-orange-800":o.isActive?"bg-green-100 text-green-800":"bg-red-100 text-red-800"])},[e("div",{class:G(["w-2 h-2 rounded-full mr-2",o.status==="blocked"?"bg-orange-500":o.isActive?"bg-green-500":"bg-red-500"])},null,2),h(" "+U(o.status==="blocked"?"已封锁":o.isActive?"正常":"异常"),1)],2),o.rateLimitStatus&&o.rateLimitStatus.isRateLimited?(i(),u("span",vl,[l[33]||(l[33]=e("i",{class:"fas fa-exclamation-triangle mr-1"},null,-1)),h(" 限流中 ("+U(o.rateLimitStatus.minutesRemaining)+"分钟) ",1)])):$("",!0),o.schedulable===!1?(i(),u("span",yl,l[34]||(l[34]=[e("i",{class:"fas fa-pause-circle mr-1"},null,-1),h(" 不可调度 ",-1)]))):$("",!0),o.status==="blocked"&&o.errorMessage?(i(),u("span",{key:2,class:"text-xs text-gray-500 mt-1 max-w-xs truncate",title:o.errorMessage},U(o.errorMessage),9,gl)):$("",!0),o.accountType==="dedicated"?(i(),u("span",bl," 绑定: "+U(o.boundApiKeysCount||0)+" 个API Key ",1)):$("",!0)])]),e("td",wl,[o.platform==="claude"||o.platform==="claude-console"?(i(),u("div",kl,[e("div",hl,[e("div",{class:"bg-gradient-to-r from-green-500 to-blue-600 h-2 rounded-full transition-all duration-300",style:ve({width:101-(o.priority||50)+"%"})},null,4)]),e("span",$l,U(o.priority||50),1)])):(i(),u("div",Cl,l[35]||(l[35]=[e("span",{class:"text-xs"},"N/A",-1)])))]),e("td",Tl,[Q(o.proxy)?(i(),u("div",Al,U(Q(o.proxy)),1)):(i(),u("div",Ul,"无代理"))]),e("td",Vl,[o.usage&&o.usage.daily?(i(),u("div",Il,[e("div",jl,[l[36]||(l[36]=e("div",{class:"w-2 h-2 bg-green-500 rounded-full"},null,-1)),e("span",Sl,U(o.usage.daily.requests||0)+" 次",1)]),e("div",Ml,[l[37]||(l[37]=e("div",{class:"w-2 h-2 bg-blue-500 rounded-full"},null,-1)),e("span",Kl,U(R(o.usage.daily.allTokens||0))+" tokens",1)]),o.usage.averages&&o.usage.averages.rpm>0?(i(),u("div",Pl," 平均 "+U(o.usage.averages.rpm.toFixed(2))+" RPM ",1)):$("",!0)])):(i(),u("div",Rl,"暂无数据"))]),e("td",Gl,[o.platform==="claude"&&o.sessionWindow&&o.sessionWindow.hasActiveWindow?(i(),u("div",Ll,[e("div",Dl,[e("div",Ol,[e("div",{class:"bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all duration-300",style:ve({width:o.sessionWindow.progress+"%"})},null,4)]),e("span",El,U(o.sessionWindow.progress)+"% ",1)]),e("div",zl,[e("div",null,U(F(o.sessionWindow.windowStart,o.sessionWindow.windowEnd)),1),o.sessionWindow.remainingTime>0?(i(),u("div",Wl," 剩余 "+U(X(o.sessionWindow.remainingTime)),1)):$("",!0)])])):o.platform==="claude"?(i(),u("div",ql,l[38]||(l[38]=[e("i",{class:"fas fa-minus"},null,-1)]))):(i(),u("div",Bl,l[39]||(l[39]=[e("span",{class:"text-xs"},"N/A",-1)])))]),e("td",Nl,U(N(o.lastUsedAt)),1),e("td",Fl,[e("div",Hl,[o.platform==="claude"&&o.scopes?(i(),u("button",{key:0,onClick:k=>c(o),disabled:o.isRefreshing,class:G(["px-3 py-1.5 rounded-lg text-xs font-medium transition-colors",o.isRefreshing?"bg-gray-100 text-gray-400 cursor-not-allowed":"bg-blue-100 text-blue-700 hover:bg-blue-200"]),title:o.isRefreshing?"刷新中...":"刷新Token"},[e("i",{class:G(["fas fa-sync-alt",o.isRefreshing?"animate-spin":""])},null,2)],10,_l)):$("",!0),e("button",{onClick:k=>b(o),disabled:o.isTogglingSchedulable,class:G(["px-3 py-1.5 rounded-lg text-xs font-medium transition-colors",o.isTogglingSchedulable?"bg-gray-100 text-gray-400 cursor-not-allowed":o.schedulable?"bg-green-100 text-green-700 hover:bg-green-200":"bg-gray-100 text-gray-700 hover:bg-gray-200"]),title:o.schedulable?"点击禁用调度":"点击启用调度"},[e("i",{class:G(["fas",o.schedulable?"fa-toggle-on":"fa-toggle-off"])},null,2)],10,Jl),e("button",{onClick:k=>ee(o),class:"px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-xs font-medium hover:bg-blue-200 transition-colors"},l[40]||(l[40]=[e("i",{class:"fas fa-edit"},null,-1)]),8,Yl),e("button",{onClick:k=>x(o),class:"px-3 py-1.5 bg-red-100 text-red-700 rounded-lg text-xs font-medium hover:bg-red-200 transition-colors"},l[41]||(l[41]=[e("i",{class:"fas fa-trash"},null,-1)]),8,Ql)])])]))),128))])])]))]),D.value?(i(),le(be,{key:0,onClose:l[7]||(l[7]=o=>D.value=!1),onSuccess:q})):$("",!0),P.value?(i(),le(be,{key:1,account:_.value,onClose:l[8]||(l[8]=o=>P.value=!1),onSuccess:ae},null,8,["account"])):$("",!0),ne(Ce,{show:W(E),title:W(m).title,message:W(m).message,"confirm-text":W(m).confirmText,"cancel-text":W(m).cancelText,onConfirm:W(d),onCancel:W(S)},null,8,["show","title","message","confirm-text","cancel-text","onConfirm","onCancel"])]))}},aa=Me(Xl,[["__scopeId","data-v-d6375019"]]);export{aa as default}; diff --git a/web/admin-spa/dist/assets/ApiKeysView-DemJuKGs.js b/web/admin-spa/dist/assets/ApiKeysView-DemJuKGs.js deleted file mode 100644 index f07c95b2..00000000 --- a/web/admin-spa/dist/assets/ApiKeysView-DemJuKGs.js +++ /dev/null @@ -1,9 +0,0 @@ -import{E as Mt}from"./element-plus-B8Fs_0jW.js";import{aR as Rt,r as C,_ as it,q as at,c as rt,I as z,y as i,z as t,Y as H,K as b,x as r,L as $,O as g,aq as R,C as U,P as u,Q as _,ac as T,aa as et,aX as N,an as B,al as G,a5 as st,R as St,B as Pt}from"./vue-vendor-CKToUHZx.js";import{s as I}from"./toast-BvwA7Mwb.js";import{a as W,_ as J,u as dt}from"./index-hS0iOrU-.js";import"./vendor-BDiMbLwQ.js";const ut=Rt("clients",{state:()=>({supportedClients:[],loading:!1,error:null}),actions:{async loadSupportedClients(){if(this.supportedClients.length>0)return this.supportedClients;this.loading=!0,this.error=null;try{const k=await W.get("/admin/supported-clients");return k.success?this.supportedClients=k.data||[]:(this.error=k.message||"加载支持的客户端失败",console.error("Failed to load supported clients:",this.error)),this.supportedClients}catch(k){return this.error=k.message||"加载支持的客户端失败",console.error("Error loading supported clients:",k),[]}finally{this.loading=!1}}}}),_t={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},Tt={class:"modal-content w-full max-w-lg p-6 mx-auto max-h-[90vh] flex flex-col"},Et={class:"flex items-center justify-between mb-4"},Vt={key:0,class:"text-red-500 text-xs mt-1"},jt={class:"space-y-2"},Ut={key:0,class:"flex flex-wrap gap-2"},qt=["onClick"],Ft={class:"flex gap-2"},Ot=["onKeypress"],Wt={class:"bg-blue-50 border border-blue-200 rounded-lg p-3"},Yt={class:"space-y-2"},zt={class:"grid grid-cols-1 lg:grid-cols-3 gap-2"},Nt={class:"space-y-2"},Ht={class:"flex gap-2"},Bt={key:0,class:"mt-3"},Gt=["min"],Qt={key:1,class:"text-xs text-gray-500 mt-2"},Xt={class:"flex gap-4"},Jt={class:"flex items-center cursor-pointer"},Zt={class:"flex items-center cursor-pointer"},te={class:"flex items-center cursor-pointer"},ee={class:"grid grid-cols-1 gap-3"},se=["disabled"],le=["value"],oe=["disabled"],ne=["value"],ie={class:"flex items-center mb-2"},ae={key:0,class:"space-y-2 bg-red-50 border border-red-200 rounded-lg p-3"},re={class:"flex flex-wrap gap-1 mb-2 min-h-[24px]"},de=["onClick"],ue={key:0,class:"text-gray-400 text-xs"},me={class:"flex gap-2"},pe=["onKeydown"],ce={class:"flex items-center mb-2"},xe={key:0,class:"bg-green-50 border border-green-200 rounded-lg p-3"},fe={class:"space-y-1"},ge=["id","value"],ye=["for"],be={class:"text-sm font-medium text-gray-700"},ve={class:"text-xs text-gray-500 block"},we={class:"flex gap-3 pt-2"},$e=["disabled"],Ce={key:0,class:"loading-spinner mr-2"},Ae={key:1,class:"fas fa-plus mr-2"},Ke={__name:"CreateApiKeyModal",props:{accounts:{type:Object,default:()=>({claude:[],gemini:[]})}},emits:["close","success"],setup(k,{emit:O}){const w=O;dt();const P=ut(),D=C(!1),f=C({name:""}),L=C(""),h=C([]),e=it({name:"",description:"",tokenLimit:"",rateLimitWindow:"",rateLimitRequests:"",concurrencyLimit:"",dailyCostLimit:"",expireDuration:"",customExpireDate:"",expiresAt:null,permissions:"all",claudeAccountId:"",geminiAccountId:"",enableModelRestriction:!1,restrictedModels:[],modelInput:"",enableClientRestriction:!1,allowedClients:[],tags:[]});at(async()=>{h.value=await P.loadSupportedClients()});const V=rt(()=>{const v=new Date;return v.setMinutes(v.getMinutes()+1),v.toISOString().slice(0,16)}),A=()=>{if(!e.expireDuration){e.expiresAt=null;return}if(e.expireDuration==="custom")return;const v=new Date,d=e.expireDuration.match(/(\d+)([dhmy])/);if(d){const[,q,Q]=d,F=parseInt(q);switch(Q){case"d":v.setDate(v.getDate()+F);break;case"h":v.setHours(v.getHours()+F);break;case"m":v.setMonth(v.getMonth()+F);break;case"y":v.setFullYear(v.getFullYear()+F);break}e.expiresAt=v.toISOString()}},c=()=>{e.customExpireDate&&(e.expiresAt=new Date(e.customExpireDate).toISOString())},y=v=>new Date(v).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),S=()=>{e.modelInput&&!e.restrictedModels.includes(e.modelInput)&&(e.restrictedModels.push(e.modelInput),e.modelInput="")},K=v=>{e.restrictedModels.splice(v,1)},n=()=>{if(L.value&&L.value.trim()){const v=L.value.trim();e.tags.includes(v)||e.tags.push(v),L.value=""}},m=v=>{e.tags.splice(v,1)},j=async()=>{if(f.value.name="",!e.name||!e.name.trim()){f.value.name="请输入API Key名称";return}D.value=!0;try{const v={name:e.name,description:e.description||void 0,tokenLimit:e.tokenLimit!==""&&e.tokenLimit!==null?parseInt(e.tokenLimit):null,rateLimitWindow:e.rateLimitWindow!==""&&e.rateLimitWindow!==null?parseInt(e.rateLimitWindow):null,rateLimitRequests:e.rateLimitRequests!==""&&e.rateLimitRequests!==null?parseInt(e.rateLimitRequests):null,concurrencyLimit:e.concurrencyLimit!==""&&e.concurrencyLimit!==null?parseInt(e.concurrencyLimit):0,dailyCostLimit:e.dailyCostLimit!==""&&e.dailyCostLimit!==null?parseFloat(e.dailyCostLimit):0,expiresAt:e.expiresAt||void 0,permissions:e.permissions,claudeAccountId:e.claudeAccountId||void 0,geminiAccountId:e.geminiAccountId||void 0,tags:e.tags.length>0?e.tags:void 0};v.enableModelRestriction=e.enableModelRestriction,v.restrictedModels=e.restrictedModels,v.enableClientRestriction=e.enableClientRestriction,v.allowedClients=e.allowedClients;const o=await W.post("/admin/api-keys",v);o.success?(I("API Key 创建成功","success"),w("success",o.data),w("close")):I(o.message||"创建失败","error")}catch{I("创建失败","error")}finally{D.value=!1}};return(v,o)=>(i(),z(st,{to:"body"},[t("div",_t,[t("div",Tt,[t("div",Et,[o[27]||(o[27]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-key text-white"})]),t("h3",{class:"text-xl font-bold text-gray-900"},"创建新的 API Key")],-1)),t("button",{onClick:o[0]||(o[0]=d=>v.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},o[26]||(o[26]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),t("form",{onSubmit:H(j,["prevent"]),class:"space-y-4 modal-scroll-content custom-scrollbar flex-1"},[t("div",null,[o[28]||(o[28]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},[g("名称 "),t("span",{class:"text-red-500"},"*")],-1)),b(t("input",{"onUpdate:modelValue":o[1]||(o[1]=d=>e.name=d),type:"text",required:"",class:U(["form-input w-full",{"border-red-500":f.value.name}]),placeholder:"为您的 API Key 取一个名称",onInput:o[2]||(o[2]=d=>f.value.name="")},null,34),[[R,e.name]]),f.value.name?(i(),r("p",Vt,u(f.value.name),1)):$("",!0)]),t("div",null,[o[32]||(o[32]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"标签",-1)),t("div",jt,[e.tags.length>0?(i(),r("div",Ut,[(i(!0),r(_,null,T(e.tags,(d,q)=>(i(),r("span",{key:q,class:"inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"},[g(u(d)+" ",1),t("button",{type:"button",onClick:Q=>m(q),class:"ml-1 hover:text-blue-900"},o[29]||(o[29]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,qt)]))),128))])):$("",!0),t("div",Ft,[b(t("input",{"onUpdate:modelValue":o[3]||(o[3]=d=>L.value=d),type:"text",class:"form-input flex-1",placeholder:"输入新标签名称",onKeypress:et(H(n,["prevent"]),["enter"])},null,40,Ot),[[R,L.value]]),t("button",{type:"button",onClick:n,class:"px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"},o[30]||(o[30]=[t("i",{class:"fas fa-plus"},null,-1)]))]),o[31]||(o[31]=t("p",{class:"text-xs text-gray-500"},"用于标记不同团队或用途,方便筛选管理",-1))])]),t("div",Wt,[o[40]||(o[40]=t("div",{class:"flex items-center gap-2 mb-2"},[t("div",{class:"w-6 h-6 bg-blue-500 rounded flex items-center justify-center flex-shrink-0"},[t("i",{class:"fas fa-tachometer-alt text-white text-xs"})]),t("h4",{class:"font-semibold text-gray-800 text-sm"},"速率限制设置 (可选)")],-1)),t("div",Yt,[t("div",zt,[t("div",null,[o[33]||(o[33]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"时间窗口 (分钟)",-1)),b(t("input",{"onUpdate:modelValue":o[4]||(o[4]=d=>e.rateLimitWindow=d),type:"number",min:"1",placeholder:"无限制",class:"form-input w-full text-sm"},null,512),[[R,e.rateLimitWindow]]),o[34]||(o[34]=t("p",{class:"text-xs text-gray-500 mt-0.5"},"时间段单位",-1))]),t("div",null,[o[35]||(o[35]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"请求次数限制",-1)),b(t("input",{"onUpdate:modelValue":o[5]||(o[5]=d=>e.rateLimitRequests=d),type:"number",min:"1",placeholder:"无限制",class:"form-input w-full text-sm"},null,512),[[R,e.rateLimitRequests]]),o[36]||(o[36]=t("p",{class:"text-xs text-gray-500 mt-0.5"},"窗口内最大请求",-1))]),t("div",null,[o[37]||(o[37]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"Token 限制",-1)),b(t("input",{"onUpdate:modelValue":o[6]||(o[6]=d=>e.tokenLimit=d),type:"number",placeholder:"无限制",class:"form-input w-full text-sm"},null,512),[[R,e.tokenLimit]]),o[38]||(o[38]=t("p",{class:"text-xs text-gray-500 mt-0.5"},"窗口内最大Token",-1))])]),o[39]||(o[39]=t("div",{class:"bg-blue-100 rounded-lg p-2"},[t("h5",{class:"text-xs font-semibold text-blue-800 mb-1"},"💡 使用示例"),t("div",{class:"text-xs text-blue-700 space-y-0.5"},[t("div",null,[t("strong",null,"示例1:"),g(" 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求")]),t("div",null,[t("strong",null,"示例2:"),g(" 时间窗口=1,Token=10000 → 每分钟最多10,000个Token")]),t("div",null,[t("strong",null,"示例3:"),g(" 窗口=30,请求=50,Token=100000 → 每30分钟50次请求且不超10万Token")])])],-1))])]),t("div",null,[o[42]||(o[42]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"每日费用限制 (美元)",-1)),t("div",Nt,[t("div",Ht,[t("button",{type:"button",onClick:o[7]||(o[7]=d=>e.dailyCostLimit="50"),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"$50"),t("button",{type:"button",onClick:o[8]||(o[8]=d=>e.dailyCostLimit="100"),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"$100"),t("button",{type:"button",onClick:o[9]||(o[9]=d=>e.dailyCostLimit="200"),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"$200"),t("button",{type:"button",onClick:o[10]||(o[10]=d=>e.dailyCostLimit=""),class:"px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs font-medium"},"自定义")]),b(t("input",{"onUpdate:modelValue":o[11]||(o[11]=d=>e.dailyCostLimit=d),type:"number",min:"0",step:"0.01",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.dailyCostLimit]]),o[41]||(o[41]=t("p",{class:"text-xs text-gray-500"},"设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制",-1))])]),t("div",null,[o[43]||(o[43]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"并发限制 (可选)",-1)),b(t("input",{"onUpdate:modelValue":o[12]||(o[12]=d=>e.concurrencyLimit=d),type:"number",min:"0",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.concurrencyLimit]]),o[44]||(o[44]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制",-1))]),t("div",null,[o[45]||(o[45]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"备注 (可选)",-1)),b(t("textarea",{"onUpdate:modelValue":o[13]||(o[13]=d=>e.description=d),rows:"2",class:"form-input w-full resize-none text-sm",placeholder:"描述此 API Key 的用途..."},null,512),[[R,e.description]])]),t("div",null,[o[47]||(o[47]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"有效期限",-1)),b(t("select",{"onUpdate:modelValue":o[14]||(o[14]=d=>e.expireDuration=d),onChange:A,class:"form-input w-full"},o[46]||(o[46]=[t("option",{value:""},"永不过期",-1),t("option",{value:"1d"},"1 天",-1),t("option",{value:"7d"},"7 天",-1),t("option",{value:"30d"},"30 天",-1),t("option",{value:"90d"},"90 天",-1),t("option",{value:"180d"},"180 天",-1),t("option",{value:"365d"},"365 天",-1),t("option",{value:"custom"},"自定义日期",-1)]),544),[[N,e.expireDuration]]),e.expireDuration==="custom"?(i(),r("div",Bt,[b(t("input",{"onUpdate:modelValue":o[15]||(o[15]=d=>e.customExpireDate=d),type:"datetime-local",class:"form-input w-full",min:V.value,onChange:c},null,40,Gt),[[R,e.customExpireDate]])])):$("",!0),e.expiresAt?(i(),r("p",Qt," 将于 "+u(y(e.expiresAt))+" 过期 ",1)):$("",!0)]),t("div",null,[o[51]||(o[51]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"服务权限",-1)),t("div",Xt,[t("label",Jt,[b(t("input",{type:"radio","onUpdate:modelValue":o[16]||(o[16]=d=>e.permissions=d),value:"all",class:"mr-2"},null,512),[[B,e.permissions]]),o[48]||(o[48]=t("span",{class:"text-sm text-gray-700"},"全部服务",-1))]),t("label",Zt,[b(t("input",{type:"radio","onUpdate:modelValue":o[17]||(o[17]=d=>e.permissions=d),value:"claude",class:"mr-2"},null,512),[[B,e.permissions]]),o[49]||(o[49]=t("span",{class:"text-sm text-gray-700"},"仅 Claude",-1))]),t("label",te,[b(t("input",{type:"radio","onUpdate:modelValue":o[18]||(o[18]=d=>e.permissions=d),value:"gemini",class:"mr-2"},null,512),[[B,e.permissions]]),o[50]||(o[50]=t("span",{class:"text-sm text-gray-700"},"仅 Gemini",-1))])]),o[52]||(o[52]=t("p",{class:"text-xs text-gray-500 mt-2"},"控制此 API Key 可以访问哪些服务",-1))]),t("div",null,[o[57]||(o[57]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"专属账号绑定 (可选)",-1)),t("div",ee,[t("div",null,[o[54]||(o[54]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Claude 专属账号",-1)),b(t("select",{"onUpdate:modelValue":o[19]||(o[19]=d=>e.claudeAccountId=d),class:"form-input w-full",disabled:e.permissions==="gemini"},[o[53]||(o[53]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.claude.filter(d=>d.isDedicated),d=>(i(),r("option",{key:d.id,value:d.id},u(d.name)+" ("+u(d.status==="active"?"正常":"异常")+") ",9,le))),128))],8,se),[[N,e.claudeAccountId]])]),t("div",null,[o[56]||(o[56]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Gemini 专属账号",-1)),b(t("select",{"onUpdate:modelValue":o[20]||(o[20]=d=>e.geminiAccountId=d),class:"form-input w-full",disabled:e.permissions==="claude"},[o[55]||(o[55]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.gemini.filter(d=>d.isDedicated),d=>(i(),r("option",{key:d.id,value:d.id},u(d.name)+" ("+u(d.status==="active"?"正常":"异常")+") ",9,ne))),128))],8,oe),[[N,e.geminiAccountId]])])]),o[58]||(o[58]=t("p",{class:"text-xs text-gray-500 mt-2"},"选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池",-1))]),t("div",null,[t("div",ie,[b(t("input",{type:"checkbox","onUpdate:modelValue":o[21]||(o[21]=d=>e.enableModelRestriction=d),id:"enableModelRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableModelRestriction]]),o[59]||(o[59]=t("label",{for:"enableModelRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用模型限制 ",-1))]),e.enableModelRestriction?(i(),r("div",ae,[t("div",null,[o[62]||(o[62]=t("label",{class:"block text-xs font-medium text-gray-700 mb-1"},"限制的模型列表",-1)),t("div",re,[(i(!0),r(_,null,T(e.restrictedModels,(d,q)=>(i(),r("span",{key:q,class:"inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800"},[g(u(d)+" ",1),t("button",{type:"button",onClick:Q=>K(q),class:"ml-1 text-red-600 hover:text-red-800"},o[60]||(o[60]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,de)]))),128)),e.restrictedModels.length===0?(i(),r("span",ue," 暂无限制的模型 ")):$("",!0)]),t("div",me,[b(t("input",{"onUpdate:modelValue":o[22]||(o[22]=d=>e.modelInput=d),onKeydown:et(H(S,["prevent"]),["enter"]),type:"text",placeholder:"输入模型名称,按回车添加",class:"form-input flex-1 text-sm"},null,40,pe),[[R,e.modelInput]]),t("button",{type:"button",onClick:S,class:"px-3 py-1.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors text-sm"},o[61]||(o[61]=[t("i",{class:"fas fa-plus"},null,-1)]))]),o[63]||(o[63]=t("p",{class:"text-xs text-gray-500 mt-1"},"例如:claude-opus-4-20250514",-1))])])):$("",!0)]),t("div",null,[t("div",ce,[b(t("input",{type:"checkbox","onUpdate:modelValue":o[23]||(o[23]=d=>e.enableClientRestriction=d),id:"enableClientRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableClientRestriction]]),o[64]||(o[64]=t("label",{for:"enableClientRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用客户端限制 ",-1))]),e.enableClientRestriction?(i(),r("div",xe,[t("div",null,[o[65]||(o[65]=t("label",{class:"block text-xs font-medium text-gray-700 mb-2"},"允许的客户端",-1)),t("div",fe,[(i(!0),r(_,null,T(h.value,d=>(i(),r("div",{key:d.id,class:"flex items-start"},[b(t("input",{type:"checkbox",id:`client_${d.id}`,value:d.id,"onUpdate:modelValue":o[24]||(o[24]=q=>e.allowedClients=q),class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"},null,8,ge),[[G,e.allowedClients]]),t("label",{for:`client_${d.id}`,class:"ml-2 flex-1 cursor-pointer"},[t("span",be,u(d.name),1),t("span",ve,u(d.description),1)],8,ye)]))),128))])])])):$("",!0)]),t("div",we,[t("button",{type:"button",onClick:o[25]||(o[25]=d=>v.$emit("close")),class:"flex-1 px-4 py-2.5 bg-gray-100 text-gray-700 rounded-lg font-semibold hover:bg-gray-200 transition-colors text-sm"}," 取消 "),t("button",{type:"submit",disabled:D.value,class:"btn btn-primary flex-1 py-2.5 px-4 font-semibold text-sm"},[D.value?(i(),r("div",Ce)):(i(),r("i",Ae)),g(" "+u(D.value?"创建中...":"创建"),1)],8,$e)])],32)])])]))}},ke=J(Ke,[["__scopeId","data-v-060c3f39"]]),Le={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},De={class:"modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col"},Ie={class:"flex items-center justify-between mb-6"},he=["value"],Me={class:"space-y-3"},Re={key:0,class:"flex flex-wrap gap-2"},Se=["onClick"],Pe={class:"flex gap-2"},_e=["onKeypress"],Te={class:"bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-4"},Ee={class:"space-y-3"},Ve={class:"flex gap-2"},je={class:"flex gap-4"},Ue={class:"flex items-center cursor-pointer"},qe={class:"flex items-center cursor-pointer"},Fe={class:"flex items-center cursor-pointer"},Oe={class:"grid grid-cols-1 gap-3"},We=["disabled"],Ye=["value"],ze=["disabled"],Ne=["value"],He={class:"flex items-center mb-3"},Be={key:0,class:"space-y-3"},Ge={class:"flex flex-wrap gap-2 mb-3 min-h-[32px] p-2 bg-gray-50 rounded-lg border border-gray-200"},Qe=["onClick"],Xe={key:0,class:"text-gray-400 text-sm"},Je={class:"flex gap-2"},Ze=["onKeydown"],ts={class:"flex items-center mb-3"},es={key:0,class:"space-y-3"},ss={class:"space-y-2"},ls=["id","value"],os=["for"],ns={class:"text-sm font-medium text-gray-700"},is={class:"text-xs text-gray-500 block"},as={class:"flex gap-3 pt-4"},rs=["disabled"],ds={key:0,class:"loading-spinner mr-2"},us={key:1,class:"fas fa-save mr-2"},ms={__name:"EditApiKeyModal",props:{apiKey:{type:Object,required:!0},accounts:{type:Object,default:()=>({claude:[],gemini:[]})}},emits:["close","success"],setup(k,{emit:O}){const w=k,P=O;dt();const D=ut(),f=C(!1),L=C([]),h=C(""),e=it({name:"",tokenLimit:"",rateLimitWindow:"",rateLimitRequests:"",concurrencyLimit:"",dailyCostLimit:"",permissions:"all",claudeAccountId:"",geminiAccountId:"",enableModelRestriction:!1,restrictedModels:[],modelInput:"",enableClientRestriction:!1,allowedClients:[],tags:[]}),V=()=>{e.modelInput&&!e.restrictedModels.includes(e.modelInput)&&(e.restrictedModels.push(e.modelInput),e.modelInput="")},A=K=>{e.restrictedModels.splice(K,1)},c=()=>{if(h.value&&h.value.trim()){const K=h.value.trim();e.tags.includes(K)||e.tags.push(K),h.value=""}},y=K=>{e.tags.splice(K,1)},S=async()=>{f.value=!0;try{const K={tokenLimit:e.tokenLimit!==""&&e.tokenLimit!==null?parseInt(e.tokenLimit):0,rateLimitWindow:e.rateLimitWindow!==""&&e.rateLimitWindow!==null?parseInt(e.rateLimitWindow):0,rateLimitRequests:e.rateLimitRequests!==""&&e.rateLimitRequests!==null?parseInt(e.rateLimitRequests):0,concurrencyLimit:e.concurrencyLimit!==""&&e.concurrencyLimit!==null?parseInt(e.concurrencyLimit):0,dailyCostLimit:e.dailyCostLimit!==""&&e.dailyCostLimit!==null?parseFloat(e.dailyCostLimit):0,permissions:e.permissions,claudeAccountId:e.claudeAccountId||null,geminiAccountId:e.geminiAccountId||null,tags:e.tags};K.enableModelRestriction=e.enableModelRestriction,K.restrictedModels=e.restrictedModels,K.enableClientRestriction=e.enableClientRestriction,K.allowedClients=e.allowedClients;const n=await W.put(`/admin/api-keys/${w.apiKey.id}`,K);n.success?(P("success"),P("close")):I(n.message||"更新失败","error")}catch{I("更新失败","error")}finally{f.value=!1}};return at(async()=>{L.value=await D.loadSupportedClients(),e.name=w.apiKey.name,e.tokenLimit=w.apiKey.tokenLimit||"",e.rateLimitWindow=w.apiKey.rateLimitWindow||"",e.rateLimitRequests=w.apiKey.rateLimitRequests||"",e.concurrencyLimit=w.apiKey.concurrencyLimit||"",e.dailyCostLimit=w.apiKey.dailyCostLimit||"",e.permissions=w.apiKey.permissions||"all",e.claudeAccountId=w.apiKey.claudeAccountId||"",e.geminiAccountId=w.apiKey.geminiAccountId||"",e.restrictedModels=w.apiKey.restrictedModels||[],e.allowedClients=w.apiKey.allowedClients||[],e.tags=w.apiKey.tags||[],e.enableModelRestriction=e.restrictedModels.length>0,e.enableClientRestriction=e.allowedClients.length>0}),(K,n)=>(i(),z(st,{to:"body"},[t("div",Le,[t("div",De,[t("div",Ie,[n[22]||(n[22]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-edit text-white"})]),t("h3",{class:"text-xl font-bold text-gray-900"},"编辑 API Key")],-1)),t("button",{onClick:n[0]||(n[0]=m=>K.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},n[21]||(n[21]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),t("form",{onSubmit:H(S,["prevent"]),class:"space-y-6 modal-scroll-content custom-scrollbar flex-1"},[t("div",null,[n[23]||(n[23]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"名称",-1)),t("input",{value:e.name,type:"text",disabled:"",class:"form-input w-full bg-gray-100 cursor-not-allowed"},null,8,he),n[24]||(n[24]=t("p",{class:"text-xs text-gray-500 mt-2"},"名称不可修改",-1))]),t("div",null,[n[28]||(n[28]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"标签",-1)),t("div",Me,[e.tags.length>0?(i(),r("div",Re,[(i(!0),r(_,null,T(e.tags,(m,j)=>(i(),r("span",{key:j,class:"inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 text-sm rounded-full"},[g(u(m)+" ",1),t("button",{type:"button",onClick:v=>y(j),class:"ml-1 hover:text-blue-900"},n[25]||(n[25]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,Se)]))),128))])):$("",!0),t("div",Pe,[b(t("input",{"onUpdate:modelValue":n[1]||(n[1]=m=>h.value=m),type:"text",class:"form-input flex-1",placeholder:"输入新标签名称",onKeypress:et(H(c,["prevent"]),["enter"])},null,40,_e),[[R,h.value]]),t("button",{type:"button",onClick:c,class:"px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"},n[26]||(n[26]=[t("i",{class:"fas fa-plus"},null,-1)]))]),n[27]||(n[27]=t("p",{class:"text-xs text-gray-500"},"用于标记不同团队或用途,方便筛选管理",-1))])]),t("div",Te,[n[35]||(n[35]=t("div",{class:"flex items-start gap-3 mb-3"},[t("div",{class:"w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0"},[t("i",{class:"fas fa-tachometer-alt text-white text-sm"})]),t("div",{class:"flex-1"},[t("h4",{class:"font-semibold text-gray-800 mb-1"},"速率限制设置"),t("p",{class:"text-sm text-gray-600"},"控制 API Key 的使用频率和资源消耗")])],-1)),t("div",null,[n[29]||(n[29]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"时间窗口 (分钟)",-1)),b(t("input",{"onUpdate:modelValue":n[2]||(n[2]=m=>e.rateLimitWindow=m),type:"number",min:"1",placeholder:"留空表示无限制",class:"form-input w-full"},null,512),[[R,e.rateLimitWindow]]),n[30]||(n[30]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置一个时间段(以分钟为单位),用于计算速率限制",-1))]),t("div",null,[n[31]||(n[31]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"时间窗口内的请求次数限制",-1)),b(t("input",{"onUpdate:modelValue":n[3]||(n[3]=m=>e.rateLimitRequests=m),type:"number",min:"1",placeholder:"留空表示无限制",class:"form-input w-full"},null,512),[[R,e.rateLimitRequests]]),n[32]||(n[32]=t("p",{class:"text-xs text-gray-500 mt-2"},"在时间窗口内允许的最大请求次数(需要先设置时间窗口)",-1))]),t("div",null,[n[33]||(n[33]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"时间窗口内的 Token 使用量限制",-1)),b(t("input",{"onUpdate:modelValue":n[4]||(n[4]=m=>e.tokenLimit=m),type:"number",min:"0",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.tokenLimit]]),n[34]||(n[34]=t("p",{class:"text-xs text-gray-500 mt-2"},"在时间窗口内允许消耗的最大 Token 数量(需要先设置时间窗口),0 或留空表示无限制",-1))]),n[36]||(n[36]=t("div",{class:"bg-blue-100 rounded-lg p-3 mt-3"},[t("h5",{class:"text-sm font-semibold text-blue-800 mb-2"},"💡 使用示例"),t("div",{class:"text-xs text-blue-700 space-y-1"},[t("p",null,[t("strong",null,"示例1:"),g(" 时间窗口=60,请求次数限制=100")]),t("p",{class:"ml-4"},"→ 每60分钟内最多允许100次请求"),t("p",{class:"mt-2"},[t("strong",null,"示例2:"),g(" 时间窗口=10,Token限制=50000")]),t("p",{class:"ml-4"},"→ 每10分钟内最多消耗50,000个Token"),t("p",{class:"mt-2"},[t("strong",null,"示例3:"),g(" 时间窗口=30,请求次数限制=50,Token限制=100000")]),t("p",{class:"ml-4"},"→ 每30分钟内最多50次请求且总Token不超过100,000")])],-1))]),t("div",null,[n[38]||(n[38]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"每日费用限制 (美元)",-1)),t("div",Ee,[t("div",Ve,[t("button",{type:"button",onClick:n[5]||(n[5]=m=>e.dailyCostLimit="50"),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"$50"),t("button",{type:"button",onClick:n[6]||(n[6]=m=>e.dailyCostLimit="100"),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"$100"),t("button",{type:"button",onClick:n[7]||(n[7]=m=>e.dailyCostLimit="200"),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"$200"),t("button",{type:"button",onClick:n[8]||(n[8]=m=>e.dailyCostLimit=""),class:"px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm font-medium"},"自定义")]),b(t("input",{"onUpdate:modelValue":n[9]||(n[9]=m=>e.dailyCostLimit=m),type:"number",min:"0",step:"0.01",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.dailyCostLimit]]),n[37]||(n[37]=t("p",{class:"text-xs text-gray-500"},"设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制",-1))])]),t("div",null,[n[39]||(n[39]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"并发限制",-1)),b(t("input",{"onUpdate:modelValue":n[10]||(n[10]=m=>e.concurrencyLimit=m),type:"number",min:"0",placeholder:"0 表示无限制",class:"form-input w-full"},null,512),[[R,e.concurrencyLimit]]),n[40]||(n[40]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置此 API Key 可同时处理的最大请求数",-1))]),t("div",null,[n[44]||(n[44]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"服务权限",-1)),t("div",je,[t("label",Ue,[b(t("input",{type:"radio","onUpdate:modelValue":n[11]||(n[11]=m=>e.permissions=m),value:"all",class:"mr-2"},null,512),[[B,e.permissions]]),n[41]||(n[41]=t("span",{class:"text-sm text-gray-700"},"全部服务",-1))]),t("label",qe,[b(t("input",{type:"radio","onUpdate:modelValue":n[12]||(n[12]=m=>e.permissions=m),value:"claude",class:"mr-2"},null,512),[[B,e.permissions]]),n[42]||(n[42]=t("span",{class:"text-sm text-gray-700"},"仅 Claude",-1))]),t("label",Fe,[b(t("input",{type:"radio","onUpdate:modelValue":n[13]||(n[13]=m=>e.permissions=m),value:"gemini",class:"mr-2"},null,512),[[B,e.permissions]]),n[43]||(n[43]=t("span",{class:"text-sm text-gray-700"},"仅 Gemini",-1))])]),n[45]||(n[45]=t("p",{class:"text-xs text-gray-500 mt-2"},"控制此 API Key 可以访问哪些服务",-1))]),t("div",null,[n[50]||(n[50]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"专属账号绑定",-1)),t("div",Oe,[t("div",null,[n[47]||(n[47]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Claude 专属账号",-1)),b(t("select",{"onUpdate:modelValue":n[14]||(n[14]=m=>e.claudeAccountId=m),class:"form-input w-full",disabled:e.permissions==="gemini"},[n[46]||(n[46]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.claude,m=>(i(),r("option",{key:m.id,value:m.id},u(m.name)+" ("+u(m.status==="active"?"正常":"异常")+") ",9,Ye))),128))],8,We),[[N,e.claudeAccountId]])]),t("div",null,[n[49]||(n[49]=t("label",{class:"block text-sm font-medium text-gray-600 mb-1"},"Gemini 专属账号",-1)),b(t("select",{"onUpdate:modelValue":n[15]||(n[15]=m=>e.geminiAccountId=m),class:"form-input w-full",disabled:e.permissions==="claude"},[n[48]||(n[48]=t("option",{value:""},"使用共享账号池",-1)),(i(!0),r(_,null,T(k.accounts.gemini,m=>(i(),r("option",{key:m.id,value:m.id},u(m.name)+" ("+u(m.status==="active"?"正常":"异常")+") ",9,Ne))),128))],8,ze),[[N,e.geminiAccountId]])])]),n[51]||(n[51]=t("p",{class:"text-xs text-gray-500 mt-2"},"修改绑定账号将影响此API Key的请求路由",-1))]),t("div",null,[t("div",He,[b(t("input",{type:"checkbox","onUpdate:modelValue":n[16]||(n[16]=m=>e.enableModelRestriction=m),id:"editEnableModelRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableModelRestriction]]),n[52]||(n[52]=t("label",{for:"editEnableModelRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用模型限制 ",-1))]),e.enableModelRestriction?(i(),r("div",Be,[t("div",null,[n[55]||(n[55]=t("label",{class:"block text-sm font-medium text-gray-600 mb-2"},"限制的模型列表",-1)),t("div",Ge,[(i(!0),r(_,null,T(e.restrictedModels,(m,j)=>(i(),r("span",{key:j,class:"inline-flex items-center px-3 py-1 rounded-full text-sm bg-red-100 text-red-800"},[g(u(m)+" ",1),t("button",{type:"button",onClick:v=>A(j),class:"ml-2 text-red-600 hover:text-red-800"},n[53]||(n[53]=[t("i",{class:"fas fa-times text-xs"},null,-1)]),8,Qe)]))),128)),e.restrictedModels.length===0?(i(),r("span",Xe," 暂无限制的模型 ")):$("",!0)]),t("div",Je,[b(t("input",{"onUpdate:modelValue":n[17]||(n[17]=m=>e.modelInput=m),onKeydown:et(H(V,["prevent"]),["enter"]),type:"text",placeholder:"输入模型名称,按回车添加",class:"form-input flex-1"},null,40,Ze),[[R,e.modelInput]]),t("button",{type:"button",onClick:V,class:"px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"},n[54]||(n[54]=[t("i",{class:"fas fa-plus"},null,-1)]))]),n[56]||(n[56]=t("p",{class:"text-xs text-gray-500 mt-2"},"设置此API Key无法访问的模型,例如:claude-opus-4-20250514",-1))])])):$("",!0)]),t("div",null,[t("div",ts,[b(t("input",{type:"checkbox","onUpdate:modelValue":n[18]||(n[18]=m=>e.enableClientRestriction=m),id:"editEnableClientRestriction",class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"},null,512),[[G,e.enableClientRestriction]]),n[57]||(n[57]=t("label",{for:"editEnableClientRestriction",class:"ml-2 text-sm font-semibold text-gray-700 cursor-pointer"}," 启用客户端限制 ",-1))]),e.enableClientRestriction?(i(),r("div",es,[t("div",null,[n[58]||(n[58]=t("label",{class:"block text-sm font-medium text-gray-600 mb-2"},"允许的客户端",-1)),n[59]||(n[59]=t("p",{class:"text-xs text-gray-500 mb-3"},"勾选允许使用此API Key的客户端",-1)),t("div",ss,[(i(!0),r(_,null,T(L.value,m=>(i(),r("div",{key:m.id,class:"flex items-start"},[b(t("input",{type:"checkbox",id:`edit_client_${m.id}`,value:m.id,"onUpdate:modelValue":n[19]||(n[19]=j=>e.allowedClients=j),class:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 mt-0.5"},null,8,ls),[[G,e.allowedClients]]),t("label",{for:`edit_client_${m.id}`,class:"ml-2 flex-1 cursor-pointer"},[t("span",ns,u(m.name),1),t("span",is,u(m.description),1)],8,os)]))),128))])])])):$("",!0)]),t("div",as,[t("button",{type:"button",onClick:n[20]||(n[20]=m=>K.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),t("button",{type:"submit",disabled:f.value,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[f.value?(i(),r("div",ds)):(i(),r("i",us)),g(" "+u(f.value?"保存中...":"保存修改"),1)],8,rs)])],32)])])]))}},ps=J(ms,[["__scopeId","data-v-35ae96db"]]),cs={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},xs={class:"modal-content w-full max-w-md p-8 mx-auto max-h-[90vh] flex flex-col"},fs={class:"flex items-center justify-between mb-6"},gs={class:"space-y-6 modal-scroll-content custom-scrollbar flex-1"},ys={class:"bg-blue-50 border border-blue-200 rounded-lg p-4"},bs={class:"flex items-start gap-3"},vs={class:"text-sm text-gray-700"},ws={class:"text-xs text-gray-600 mt-1"},$s={key:0,class:"mt-3"},Cs=["min"],As={key:1,class:"text-xs text-gray-500 mt-2"},Ks={class:"flex gap-3 pt-4"},ks=["disabled"],Ls={key:0,class:"loading-spinner mr-2"},Ds={key:1,class:"fas fa-clock mr-2"},Is={__name:"RenewApiKeyModal",props:{apiKey:{type:Object,required:!0}},emits:["close","success"],setup(k,{emit:O}){const w=k,P=O;dt();const D=C(!1),f=it({renewDuration:"30d",customExpireDate:"",newExpiresAt:null}),L=rt(()=>{const c=new Date;return w.apiKey.expiresAt&&new Date(w.apiKey.expiresAt)>c?new Date(w.apiKey.expiresAt).toISOString().slice(0,16):(c.setMinutes(c.getMinutes()+1),c.toISOString().slice(0,16))}),h=c=>new Date(c).toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),e=()=>{if(!f.renewDuration){f.newExpiresAt=null;return}if(f.renewDuration==="permanent"){f.newExpiresAt=null;return}if(f.renewDuration==="custom")return;const c=w.apiKey.expiresAt&&new Date(w.apiKey.expiresAt)>new Date?new Date(w.apiKey.expiresAt):new Date,S=f.renewDuration.match(/(\d+)([dhmy])/);if(S){const[,K,n]=S,m=parseInt(K);switch(n){case"d":c.setDate(c.getDate()+m);break;case"h":c.setHours(c.getHours()+m);break;case"m":c.setMonth(c.getMonth()+m);break;case"y":c.setFullYear(c.getFullYear()+m);break}f.newExpiresAt=c.toISOString()}},V=()=>{f.customExpireDate&&(f.newExpiresAt=new Date(f.customExpireDate).toISOString())},A=async()=>{D.value=!0;try{const c={expiresAt:f.renewDuration==="permanent"?null:f.newExpiresAt},y=await W.put(`/admin/api-keys/${w.apiKey.id}/renew`,c);y.success?(I("API Key 续期成功","success"),P("success"),P("close")):I(y.message||"续期失败","error")}catch{I("续期失败","error")}finally{D.value=!1}};return e(),(c,y)=>(i(),z(st,{to:"body"},[t("div",cs,[t("div",xs,[t("div",fs,[y[5]||(y[5]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-10 h-10 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-clock text-white"})]),t("h3",{class:"text-xl font-bold text-gray-900"},"续期 API Key")],-1)),t("button",{onClick:y[0]||(y[0]=S=>c.$emit("close")),class:"text-gray-400 hover:text-gray-600 transition-colors"},y[4]||(y[4]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),t("div",gs,[t("div",ys,[t("div",bs,[y[7]||(y[7]=t("div",{class:"w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0"},[t("i",{class:"fas fa-info text-white text-sm"})],-1)),t("div",null,[y[6]||(y[6]=t("h4",{class:"font-semibold text-gray-800 mb-1"},"API Key 信息",-1)),t("p",vs,u(k.apiKey.name),1),t("p",ws," 当前过期时间:"+u(k.apiKey.expiresAt?h(k.apiKey.expiresAt):"永不过期"),1)])])]),t("div",null,[y[9]||(y[9]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-3"},"续期时长",-1)),b(t("select",{"onUpdate:modelValue":y[1]||(y[1]=S=>f.renewDuration=S),onChange:e,class:"form-input w-full"},y[8]||(y[8]=[t("option",{value:"7d"},"延长 7 天",-1),t("option",{value:"30d"},"延长 30 天",-1),t("option",{value:"90d"},"延长 90 天",-1),t("option",{value:"180d"},"延长 180 天",-1),t("option",{value:"365d"},"延长 365 天",-1),t("option",{value:"custom"},"自定义日期",-1),t("option",{value:"permanent"},"设为永不过期",-1)]),544),[[N,f.renewDuration]]),f.renewDuration==="custom"?(i(),r("div",$s,[b(t("input",{"onUpdate:modelValue":y[2]||(y[2]=S=>f.customExpireDate=S),type:"datetime-local",class:"form-input w-full",min:L.value,onChange:V},null,40,Cs),[[R,f.customExpireDate]])])):$("",!0),f.newExpiresAt?(i(),r("p",As," 新的过期时间:"+u(h(f.newExpiresAt)),1)):$("",!0)])]),t("div",Ks,[t("button",{type:"button",onClick:y[3]||(y[3]=S=>c.$emit("close")),class:"flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"}," 取消 "),t("button",{type:"button",onClick:A,disabled:D.value||!f.renewDuration,class:"btn btn-primary flex-1 py-3 px-6 font-semibold"},[D.value?(i(),r("div",Ls)):(i(),r("i",Ds)),g(" "+u(D.value?"续期中...":"确认续期"),1)],8,ks)])])])]))}},hs=J(Is,[["__scopeId","data-v-5486ec2f"]]),Ms={class:"fixed inset-0 modal z-50 flex items-center justify-center p-4"},Rs={class:"modal-content w-full max-w-lg p-8 mx-auto max-h-[90vh] overflow-y-auto custom-scrollbar"},Ss={class:"space-y-4 mb-6"},Ps={class:"p-3 bg-gray-50 rounded-lg border"},_s={class:"text-gray-900 font-medium"},Ts={key:0},Es={class:"p-3 bg-gray-50 rounded-lg border"},Vs={class:"text-gray-700"},js={class:"relative"},Us={class:"p-4 pr-14 bg-gray-900 rounded-lg border font-mono text-sm text-white break-all min-h-[60px] flex items-center"},qs={class:"absolute top-3 right-3"},Fs=["title"],Os={__name:"NewApiKeyModal",props:{apiKey:{type:Object,required:!0}},emits:["close"],setup(k,{emit:O}){const w=k,P=O,D=C(!1),f=()=>{D.value=!D.value},L=()=>{const A=w.apiKey.apiKey||w.apiKey.key||"";return A?D.value||A.length<=12?A:A.substring(0,8)+"●".repeat(Math.max(0,A.length-12))+A.substring(A.length-4):""},h=async()=>{const A=w.apiKey.apiKey||w.apiKey.key||"";if(!A){I("API Key 不存在","error");return}try{await navigator.clipboard.writeText(A),I("API Key 已复制到剪贴板","success")}catch(c){console.error("Failed to copy:",c);const y=document.createElement("textarea");y.value=A,document.body.appendChild(y),y.select();try{document.execCommand("copy"),I("API Key 已复制到剪贴板","success")}catch{I("复制失败,请手动复制","error")}finally{document.body.removeChild(y)}}},e=async()=>{window.showConfirm?await window.showConfirm("关闭提醒",`关闭后将无法再次查看完整的API Key,请确保已经妥善保存。 - -确定要关闭吗?`,"确定关闭","取消")&&P("close"):confirm(`关闭后将无法再次查看完整的API Key,请确保已经妥善保存。 - -确定要关闭吗?`)&&P("close")},V=async()=>{window.showConfirm?await window.showConfirm("确定要关闭吗?",`您还没有保存API Key,关闭后将无法再次查看。 - -建议您先复制API Key再关闭。`,"仍然关闭","返回复制")&&P("close"):confirm(`您还没有保存API Key,关闭后将无法再次查看。 - -确定要关闭吗?`)&&P("close")};return(A,c)=>(i(),z(st,{to:"body"},[t("div",Ms,[t("div",Rs,[t("div",{class:"flex items-center justify-between mb-6"},[c[1]||(c[1]=t("div",{class:"flex items-center gap-3"},[t("div",{class:"w-12 h-12 bg-gradient-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center"},[t("i",{class:"fas fa-check text-white text-lg"})]),t("div",null,[t("h3",{class:"text-xl font-bold text-gray-900"},"API Key 创建成功"),t("p",{class:"text-sm text-gray-600"},"请妥善保存您的 API Key")])],-1)),t("button",{onClick:V,class:"text-gray-400 hover:text-gray-600 transition-colors",title:"直接关闭(不推荐)"},c[0]||(c[0]=[t("i",{class:"fas fa-times text-xl"},null,-1)]))]),c[7]||(c[7]=t("div",{class:"bg-amber-50 border-l-4 border-amber-400 p-4 mb-6"},[t("div",{class:"flex items-start"},[t("div",{class:"w-6 h-6 bg-amber-400 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5"},[t("i",{class:"fas fa-exclamation-triangle text-white text-sm"})]),t("div",{class:"ml-3"},[t("h5",{class:"font-semibold text-amber-900 mb-1"},"重要提醒"),t("p",{class:"text-sm text-amber-800"}," 这是您唯一能看到完整 API Key 的机会。关闭此窗口后,系统将不再显示完整的 API Key。请立即复制并妥善保存。 ")])])],-1)),t("div",Ss,[t("div",null,[c[2]||(c[2]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"API Key 名称",-1)),t("div",Ps,[t("span",_s,u(k.apiKey.name),1)])]),k.apiKey.description?(i(),r("div",Ts,[c[3]||(c[3]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"备注",-1)),t("div",Es,[t("span",Vs,u(k.apiKey.description||"无描述"),1)])])):$("",!0),t("div",null,[c[4]||(c[4]=t("label",{class:"block text-sm font-semibold text-gray-700 mb-2"},"API Key",-1)),t("div",js,[t("div",Us,u(L()),1),t("div",qs,[t("button",{onClick:f,type:"button",class:"btn-icon-sm hover:bg-gray-800 bg-gray-700",title:D.value?"隐藏API Key":"显示完整API Key"},[t("i",{class:U(["fas",D.value?"fa-eye-slash":"fa-eye","text-gray-300"])},null,2)],8,Fs)])]),c[5]||(c[5]=t("p",{class:"text-xs text-gray-500 mt-2"}," 点击眼睛图标切换显示模式,使用下方按钮复制完整 API Key ",-1))])]),t("div",{class:"flex gap-3"},[t("button",{onClick:h,class:"flex-1 btn btn-primary py-3 px-6 font-semibold flex items-center justify-center gap-2"},c[6]||(c[6]=[t("i",{class:"fas fa-copy"},null,-1),g(" 复制 API Key ",-1)])),t("button",{onClick:e,class:"px-6 py-3 bg-gray-200 text-gray-800 rounded-xl font-semibold hover:bg-gray-300 transition-colors border border-gray-300"}," 我已保存 ")])])])]))}},Ws=J(Os,[["__scopeId","data-v-3d91f349"]]),Ys={class:"tab-content"},zs={class:"card p-6"},Ns={class:"flex flex-col md:flex-row justify-between items-center gap-4 mb-6"},Hs={class:"flex items-center gap-3"},Bs=["value"],Gs={key:0,class:"text-center py-12"},Qs={key:1,class:"text-center py-12"},Xs={key:2,class:"table-container"},Js={class:"min-w-full"},Zs={class:"bg-gray-50/80 backdrop-blur-sm"},tl={key:1,class:"fas fa-sort ml-1 text-gray-400"},el={key:1,class:"fas fa-sort ml-1 text-gray-400"},sl={class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},ll={key:1,class:"fas fa-sort ml-1 text-gray-400"},ol={key:1,class:"fas fa-sort ml-1 text-gray-400"},nl={key:1,class:"fas fa-sort ml-1 text-gray-400"},il={class:"divide-y divide-gray-200/50"},al={class:"table-row"},rl={class:"px-6 py-4 whitespace-nowrap"},dl={class:"flex items-center"},ul={class:"text-sm font-semibold text-gray-900"},ml={class:"text-xs text-gray-500"},pl={class:"text-xs text-gray-500 mt-1"},cl={key:0},xl={key:1},fl={class:"px-6 py-4"},gl={class:"flex flex-wrap gap-1"},yl={key:0,class:"text-xs text-gray-400"},bl={class:"px-6 py-4 whitespace-nowrap"},vl={class:"text-sm font-mono text-gray-600 bg-gray-50 px-3 py-1 rounded-lg"},wl={class:"px-6 py-4 whitespace-nowrap"},$l={class:"px-6 py-4"},Cl={class:"space-y-1"},Al={class:"flex justify-between text-sm"},Kl={class:"font-medium text-gray-900"},kl={class:"flex justify-between text-sm"},Ll={class:"font-medium text-gray-900"},Dl={class:"flex justify-between text-sm"},Il={class:"font-medium text-green-600"},hl={key:0,class:"flex justify-between text-sm"},Ml={class:"flex justify-between text-sm"},Rl={class:"font-medium text-purple-600"},Sl={class:"flex justify-between text-sm"},Pl={key:0,class:"text-xs text-gray-500"},_l={key:1,class:"flex justify-between text-sm"},Tl={class:"font-medium text-indigo-600"},El={key:2,class:"flex justify-between text-sm"},Vl={class:"font-medium text-indigo-600"},jl={class:"flex justify-between text-xs text-gray-500"},Ul={key:3,class:"flex justify-between text-xs text-orange-500"},ql={class:"flex justify-between text-xs text-blue-600"},Fl={class:"pt-1 border-t border-gray-100"},Ol={class:"flex justify-between text-xs text-green-600"},Wl={class:"pt-2"},Yl=["onClick"],zl={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Nl={class:"px-6 py-4 whitespace-nowrap text-sm"},Hl={key:0},Bl={key:0,class:"text-red-600"},Gl={key:1,class:"text-orange-600"},Ql={key:2,class:"text-gray-600"},Xl={key:1,class:"text-gray-400"},Jl={class:"px-6 py-4 whitespace-nowrap text-sm"},Zl={class:"flex gap-2"},to=["onClick"],eo=["onClick"],so=["onClick"],lo=["onClick"],oo={key:0},no={colspan:"7",class:"px-6 py-4 bg-gray-50"},io={key:0,class:"text-center py-4"},ao={class:"space-y-4"},ro={class:"flex items-center justify-between mb-4"},uo={class:"flex items-center gap-2"},mo={key:0,class:"text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full"},po={class:"flex gap-1 items-center"},co={class:"flex gap-1 bg-gray-100 rounded p-1"},xo=["onClick"],fo={key:0,class:"text-center py-8"},go={class:"flex items-center justify-center gap-2 mb-3"},yo=["onClick"],bo={key:1,class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"},vo={class:"flex justify-between items-start mb-3"},wo={class:"flex-1"},$o={class:"text-sm font-semibold text-gray-800 block mb-1"},Co={class:"text-xs text-gray-500 bg-blue-50 px-2 py-1 rounded-full"},Ao={class:"space-y-2 mb-3"},Ko={class:"flex justify-between items-center text-sm"},ko={class:"font-semibold text-gray-900"},Lo={class:"flex justify-between items-center text-sm"},Do={class:"font-semibold text-green-600"},Io={class:"pt-2 mt-2 border-t border-gray-100"},ho={class:"flex justify-between items-center text-xs text-gray-500"},Mo={class:"font-medium"},Ro={class:"flex justify-between items-center text-xs text-gray-500"},So={class:"font-medium"},Po={key:0,class:"flex justify-between items-center text-xs text-purple-600"},_o={class:"font-medium"},To={key:1,class:"flex justify-between items-center text-xs text-purple-600"},Eo={class:"font-medium"},Vo={class:"w-full bg-gray-200 rounded-full h-2 mt-3"},jo={class:"text-right mt-1"},Uo={class:"text-xs font-medium text-indigo-600"},qo={key:2,class:"mt-4 p-3 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg border border-indigo-100"},Fo={class:"flex items-center justify-between text-sm"},Oo={class:"flex gap-4 text-xs"},Wo={class:"text-gray-600"},Yo={class:"font-semibold text-gray-800"},zo={class:"text-gray-600"},No={class:"font-semibold text-gray-800"},Ho={__name:"ApiKeysView",setup(k){const O=ut(),w=C([]),P=C(!1),D=C("today"),f=C(""),L=C("asc"),h=C({}),e=C({}),V=C({}),A=C([new Date(2e3,1,1,0,0,0),new Date(2e3,2,1,23,59,59)]),c=C({claude:[],gemini:[]}),y=C(""),S=C([]),K=C(!1),n=C(!1),m=C(!1),j=C(!1),v=C(null),o=C(null),d=C(null),q=rt(()=>{let a=w.value;return y.value&&(a=w.value.filter(x=>x.tags&&x.tags.includes(y.value))),f.value?[...a].sort((x,l)=>{let p=x[f.value],M=l[f.value];return f.value==="status"?(p=x.isActive?1:0,M=l.isActive?1:0):f.value==="cost"?(p=parseFloat(lt(x.usage).replace("$","")),M=parseFloat(lt(l.usage).replace("$",""))):(f.value==="createdAt"||f.value==="expiresAt")&&(p=p?new Date(p).getTime():0,M=M?new Date(M).getTime():0),pM?L.value==="asc"?1:-1:0}):a}),Q=async()=>{try{const[a,s]=await Promise.all([W.get("/admin/claude-accounts"),W.get("/admin/gemini-accounts")]);a.success&&(c.value.claude=a.data||[]),s.success&&(c.value.gemini=s.data||[])}catch(a){console.error("加载账户列表失败:",a)}},F=async()=>{P.value=!0;try{const a=await W.get(`/admin/api-keys?timeRange=${D.value}`);if(a.success){w.value=a.data||[];const s=new Set;w.value.forEach(x=>{x.tags&&Array.isArray(x.tags)&&x.tags.forEach(l=>s.add(l))}),S.value=Array.from(s).sort()}}catch{I("加载 API Keys 失败","error")}finally{P.value=!1}},X=a=>{f.value===a?L.value=L.value==="asc"?"desc":"asc":(f.value=a,L.value="asc")},E=a=>!a&&a!==0?"0":a.toLocaleString("zh-CN"),lt=a=>!a||!a.total?"$0.0000":`$${(a.total.cost||0).toFixed(4)}`,gt=a=>{if(!a)return"未知账户";const s=c.value.claude.find(l=>l.id===a);if(s)return s.name;const x=c.value.gemini.find(l=>l.id===a);return x?x.name:`账户-${a.substring(0,8)}`},ot=a=>a?new Date(a)!a||ot(a)?!1:(new Date(a)-new Date)/(1e3*60*60*24)<=7,pt=a=>a?new Date(a).toLocaleDateString("zh-CN"):"",yt=async a=>{h.value[a]?h.value[a]=!1:(h.value[a]=!0,V.value[a]||xt(a),await Z(a,!0))},Z=async(a,s=!1)=>{if(!s&&e.value[a]&&e.value[a].length>0)return;const x=Y(a);try{let l=`/admin/api-keys/${a}/model-stats`;const p=new URLSearchParams;if(x.customStart&&x.customEnd)p.append("startDate",x.customStart),p.append("endDate",x.customEnd),p.append("period","custom");else{const tt=x.preset==="today"?"daily":"monthly";p.append("period",tt)}l+="?"+p.toString();const M=await W.get(l);M.success&&(e.value[a]=M.data||[])}catch{I("加载模型统计失败","error"),e.value[a]=[]}},ct=(a,s)=>{const x=s.reduce((l,p)=>l+(p.allTokens||0),0);return x===0?0:Math.round(a/x*100)},bt=a=>a.formatted&&a.formatted.total?a.formatted.total:a.cost!==void 0?`$${a.cost.toFixed(6)}`:"$0.000000",xt=a=>{const s=new Date,x=new Date(s);x.setDate(s.getDate()-6),V.value[a]={type:"preset",preset:"7days",customStart:x.toISOString().split("T")[0],customEnd:s.toISOString().split("T")[0],customRange:null,presetOptions:[{value:"today",label:"今日",days:1},{value:"7days",label:"7天",days:7},{value:"30days",label:"30天",days:30}]}},Y=a=>(V.value[a]||xt(a),V.value[a]),ft=(a,s)=>{const x=Y(s);x.type="preset",x.preset=a;const l=x.presetOptions.find(p=>p.value===a);if(l){const p=new Date,M=new Date(p);M.setDate(p.getDate()-(l.days-1)),x.customStart=M.toISOString().split("T")[0],x.customEnd=p.toISOString().split("T")[0];const tt=nt=>nt.getFullYear()+"-"+String(nt.getMonth()+1).padStart(2,"0")+"-"+String(nt.getDate()).padStart(2,"0")+" 00:00:00";x.customRange=[tt(M),tt(p)]}Z(s,!0)},vt=(a,s)=>{const x=Y(a);s&&s.length===2?(x.type="custom",x.preset="",x.customRange=s,x.customStart=s[0].split(" ")[0],x.customEnd=s[1].split(" ")[0],Z(a,!0)):s===null&&ft("7days",a)},wt=a=>a>new Date,$t=a=>{const s=Y(a);s.type="preset",s.preset="7days";const x=new Date,l=new Date(x);l.setDate(x.getDate()-6),s.customStart=l.toISOString().split("T")[0],s.customEnd=x.toISOString().split("T")[0],s.customRange=null,Z(a,!0),I("已重置筛选条件并刷新数据","info")},Ct=()=>{K.value=!0},At=a=>{v.value=a,n.value=!0},Kt=a=>{o.value=a,m.value=!0},kt=a=>{K.value=!1,d.value=a,j.value=!0,F()},Lt=()=>{n.value=!1,I("API Key 更新成功","success"),F()},Dt=()=>{m.value=!1,I("API Key 续期成功","success"),F()},It=async a=>{let s=!1;if(window.showConfirm?s=await window.showConfirm("删除 API Key","确定要删除这个 API Key 吗?此操作不可恢复。","确定删除","取消"):s=confirm("确定要删除这个 API Key 吗?此操作不可恢复。"),!!s)try{const x=await W.delete(`/admin/api-keys/${a}`);x.success?(I("API Key 已删除","success"),F()):I(x.message||"删除失败","error")}catch{I("删除失败","error")}},ht=a=>{const x=`${window.location.origin}/admin/api-stats?apiId=${a.id}`,l=document.createElement("textarea");l.value=x,l.style.position="fixed",l.style.opacity="0",l.style.left="-9999px",document.body.appendChild(l),l.select(),l.setSelectionRange(0,99999);try{document.execCommand("copy")?I("已复制统计页面链接","success"):(I("复制失败,请手动复制","error"),console.log("统计页面链接:",x))}catch(p){I("复制失败,请手动复制","error"),console.error("复制错误:",p),console.log("统计页面链接:",x)}finally{document.body.removeChild(l)}};return at(async()=>{await Promise.all([O.loadSupportedClients(),Q(),F()])}),(a,s)=>{const x=Mt;return i(),r("div",Ys,[t("div",zs,[t("div",Ns,[s[15]||(s[15]=t("div",null,[t("h3",{class:"text-xl font-bold text-gray-900 mb-2"},"API Keys 管理"),t("p",{class:"text-gray-600"},"管理和监控您的 API 密钥")],-1)),t("div",Hs,[b(t("select",{"onUpdate:modelValue":s[0]||(s[0]=l=>D.value=l),onChange:s[1]||(s[1]=l=>F()),class:"form-input px-3 py-2 text-sm"},s[12]||(s[12]=[t("option",{value:"today"},"今日",-1),t("option",{value:"7days"},"最近7天",-1),t("option",{value:"monthly"},"本月",-1),t("option",{value:"all"},"全部时间",-1)]),544),[[N,D.value]]),b(t("select",{"onUpdate:modelValue":s[2]||(s[2]=l=>y.value=l),class:"form-input px-3 py-2 text-sm"},[s[13]||(s[13]=t("option",{value:""},"所有标签",-1)),(i(!0),r(_,null,T(S.value,l=>(i(),r("option",{key:l,value:l},u(l),9,Bs))),128))],512),[[N,y.value]]),t("button",{onClick:H(Ct,["stop"]),class:"btn btn-primary px-6 py-3 flex items-center gap-2"},s[14]||(s[14]=[t("i",{class:"fas fa-plus"},null,-1),g("创建新 Key ",-1)]))])]),P.value?(i(),r("div",Gs,s[16]||(s[16]=[t("div",{class:"loading-spinner mx-auto mb-4"},null,-1),t("p",{class:"text-gray-500"},"正在加载 API Keys...",-1)]))):w.value.length===0?(i(),r("div",Qs,s[17]||(s[17]=[t("div",{class:"w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"},[t("i",{class:"fas fa-key text-gray-400 text-xl"})],-1),t("p",{class:"text-gray-500 text-lg"},"暂无 API Keys",-1),t("p",{class:"text-gray-400 text-sm mt-2"},"点击上方按钮创建您的第一个 API Key",-1)]))):(i(),r("div",Xs,[t("table",Js,[t("thead",Zs,[t("tr",null,[t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[3]||(s[3]=l=>X("name"))},[s[18]||(s[18]=g(" 名称 ",-1)),f.value==="name"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",tl))]),s[25]||(s[25]=t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"标签",-1)),s[26]||(s[26]=t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"API Key",-1)),t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[4]||(s[4]=l=>X("status"))},[s[19]||(s[19]=g(" 状态 ",-1)),f.value==="status"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",el))]),t("th",sl,[s[22]||(s[22]=g(" 使用统计 ",-1)),t("span",{class:"cursor-pointer hover:bg-gray-100 px-2 py-1 rounded",onClick:s[5]||(s[5]=l=>X("cost"))},[s[20]||(s[20]=g(" (费用 ",-1)),f.value==="cost"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",ll)),s[21]||(s[21]=g(") ",-1))])]),t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[6]||(s[6]=l=>X("createdAt"))},[s[23]||(s[23]=g(" 创建时间 ",-1)),f.value==="createdAt"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",ol))]),t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider cursor-pointer hover:bg-gray-100",onClick:s[7]||(s[7]=l=>X("expiresAt"))},[s[24]||(s[24]=g(" 过期时间 ",-1)),f.value==="expiresAt"?(i(),r("i",{key:0,class:U(["fas",L.value==="asc"?"fa-sort-up":"fa-sort-down","ml-1"])},null,2)):(i(),r("i",nl))]),s[27]||(s[27]=t("th",{class:"px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"},"操作",-1))])]),t("tbody",il,[(i(!0),r(_,null,T(q.value,l=>(i(),r(_,{key:l.id},[t("tr",al,[t("td",rl,[t("div",dl,[s[30]||(s[30]=t("div",{class:"w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3"},[t("i",{class:"fas fa-key text-white text-xs"})],-1)),t("div",null,[t("div",ul,u(l.name),1),t("div",ml,u(l.id),1),t("div",pl,[l.claudeAccountId?(i(),r("span",cl,[s[28]||(s[28]=t("i",{class:"fas fa-link mr-1"},null,-1)),g(" 绑定: "+u(gt(l.claudeAccountId)),1)])):(i(),r("span",xl,s[29]||(s[29]=[t("i",{class:"fas fa-share-alt mr-1"},null,-1),g(" 使用共享池 ",-1)])))])])])]),t("td",fl,[t("div",gl,[(i(!0),r(_,null,T(l.tags||[],p=>(i(),r("span",{key:p,class:"inline-flex items-center px-2 py-0.5 bg-blue-100 text-blue-800 text-xs rounded-full"},u(p),1))),128)),!l.tags||l.tags.length===0?(i(),r("span",yl,"无标签")):$("",!0)])]),t("td",bl,[t("div",vl,u((l.apiKey||"").substring(0,20))+"... ",1)]),t("td",wl,[t("span",{class:U(["inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold",l.isActive?"bg-green-100 text-green-800":"bg-red-100 text-red-800"])},[t("div",{class:U(["w-2 h-2 rounded-full mr-2",l.isActive?"bg-green-500":"bg-red-500"])},null,2),g(" "+u(l.isActive?"活跃":"禁用"),1)],2)]),t("td",$l,[t("div",Cl,[t("div",Al,[s[31]||(s[31]=t("span",{class:"text-gray-600"},"请求数:",-1)),t("span",Kl,u(E(l.usage&&l.usage.total&&l.usage.total.requests||0)),1)]),t("div",kl,[s[32]||(s[32]=t("span",{class:"text-gray-600"},"Token:",-1)),t("span",Ll,u(E(l.usage&&l.usage.total&&l.usage.total.tokens||0)),1)]),t("div",Dl,[s[33]||(s[33]=t("span",{class:"text-gray-600"},"费用:",-1)),t("span",Il,u(lt(l.usage)),1)]),l.dailyCostLimit>0?(i(),r("div",hl,[s[34]||(s[34]=t("span",{class:"text-gray-600"},"今日费用:",-1)),t("span",{class:U(["font-medium",(l.dailyCost||0)>=l.dailyCostLimit?"text-red-600":"text-blue-600"])}," $"+u((l.dailyCost||0).toFixed(2))+" / $"+u(l.dailyCostLimit.toFixed(2)),3)])):$("",!0),t("div",Ml,[s[35]||(s[35]=t("span",{class:"text-gray-600"},"并发限制:",-1)),t("span",Rl,u(l.concurrencyLimit>0?l.concurrencyLimit:"无限制"),1)]),t("div",Sl,[s[36]||(s[36]=t("span",{class:"text-gray-600"},"当前并发:",-1)),t("span",{class:U(["font-medium",l.currentConcurrency>0?"text-orange-600":"text-gray-600"])},[g(u(l.currentConcurrency||0)+" ",1),l.concurrencyLimit>0?(i(),r("span",Pl,"/ "+u(l.concurrencyLimit),1)):$("",!0)],2)]),l.rateLimitWindow>0?(i(),r("div",_l,[s[37]||(s[37]=t("span",{class:"text-gray-600"},"时间窗口:",-1)),t("span",Tl,u(l.rateLimitWindow)+" 分钟",1)])):$("",!0),l.rateLimitRequests>0?(i(),r("div",El,[s[38]||(s[38]=t("span",{class:"text-gray-600"},"请求限制:",-1)),t("span",Vl,u(l.rateLimitRequests)+" 次/窗口",1)])):$("",!0),t("div",jl,[t("span",null,"输入: "+u(E(l.usage&&l.usage.total&&l.usage.total.inputTokens||0)),1),t("span",null,"输出: "+u(E(l.usage&&l.usage.total&&l.usage.total.outputTokens||0)),1)]),(l.usage&&l.usage.total&&l.usage.total.cacheCreateTokens||0)>0||(l.usage&&l.usage.total&&l.usage.total.cacheReadTokens||0)>0?(i(),r("div",Ul,[t("span",null,"缓存创建: "+u(E(l.usage&&l.usage.total&&l.usage.total.cacheCreateTokens||0)),1),t("span",null,"缓存读取: "+u(E(l.usage&&l.usage.total&&l.usage.total.cacheReadTokens||0)),1)])):$("",!0),t("div",ql,[t("span",null,"RPM: "+u(l.usage&&l.usage.averages&&l.usage.averages.rpm||0),1),t("span",null,"TPM: "+u(l.usage&&l.usage.averages&&l.usage.averages.tpm||0),1)]),t("div",Fl,[t("div",Ol,[t("span",null,"今日: "+u(E(l.usage&&l.usage.daily&&l.usage.daily.requests||0))+"次",1),t("span",null,u(E(l.usage&&l.usage.daily&&l.usage.daily.tokens||0))+"T",1)])]),t("div",Wl,[l&&l.id?(i(),r("button",{key:0,onClick:p=>yt(l.id),class:"text-xs text-indigo-600 hover:text-indigo-800 font-medium"},[t("i",{class:U(["fas",h.value[l.id]?"fa-chevron-up":"fa-chevron-down","mr-1"])},null,2),s[39]||(s[39]=g(" 模型使用分布 ",-1))],8,Yl)):$("",!0)])])]),t("td",zl,u(new Date(l.createdAt).toLocaleDateString()),1),t("td",Nl,[l.expiresAt?(i(),r("div",Hl,[ot(l.expiresAt)?(i(),r("div",Bl,s[40]||(s[40]=[t("i",{class:"fas fa-exclamation-circle mr-1"},null,-1),g(" 已过期 ",-1)]))):mt(l.expiresAt)?(i(),r("div",Gl,[s[41]||(s[41]=t("i",{class:"fas fa-clock mr-1"},null,-1)),g(" "+u(pt(l.expiresAt)),1)])):(i(),r("div",Ql,u(pt(l.expiresAt)),1))])):(i(),r("div",Xl,s[42]||(s[42]=[t("i",{class:"fas fa-infinity mr-1"},null,-1),g(" 永不过期 ",-1)])))]),t("td",Jl,[t("div",Zl,[t("button",{onClick:p=>ht(l),class:"text-purple-600 hover:text-purple-900 font-medium hover:bg-purple-50 px-3 py-1 rounded-lg transition-colors",title:"复制统计页面链接"},s[43]||(s[43]=[t("i",{class:"fas fa-chart-bar mr-1"},null,-1),g("统计 ",-1)]),8,to),t("button",{onClick:p=>At(l),class:"text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"},s[44]||(s[44]=[t("i",{class:"fas fa-edit mr-1"},null,-1),g("编辑 ",-1)]),8,eo),l.expiresAt&&(ot(l.expiresAt)||mt(l.expiresAt))?(i(),r("button",{key:0,onClick:p=>Kt(l),class:"text-green-600 hover:text-green-900 font-medium hover:bg-green-50 px-3 py-1 rounded-lg transition-colors"},s[45]||(s[45]=[t("i",{class:"fas fa-clock mr-1"},null,-1),g("续期 ",-1)]),8,so)):$("",!0),t("button",{onClick:p=>It(l.id),class:"text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"},s[46]||(s[46]=[t("i",{class:"fas fa-trash mr-1"},null,-1),g("删除 ",-1)]),8,lo)])])]),l&&l.id&&h.value[l.id]?(i(),r("tr",oo,[t("td",no,[e.value[l.id]?$("",!0):(i(),r("div",io,s[47]||(s[47]=[t("div",{class:"loading-spinner mx-auto"},null,-1),t("p",{class:"text-sm text-gray-500 mt-2"},"加载模型统计...",-1)]))),t("div",ao,[t("div",ro,[s[48]||(s[48]=t("h5",{class:"text-sm font-semibold text-gray-700 flex items-center"},[t("i",{class:"fas fa-chart-pie text-indigo-500 mr-2"}),g(" 模型使用分布 ")],-1)),t("div",uo,[e.value[l.id]&&e.value[l.id].length>0?(i(),r("span",mo,u(e.value[l.id].length)+" 个模型 ",1)):$("",!0),t("div",po,[t("div",co,[(i(!0),r(_,null,T(Y(l.id).presetOptions,p=>(i(),r("button",{key:p.value,onClick:M=>ft(p.value,l.id),class:U(["px-2 py-1 rounded text-xs font-medium transition-colors",Y(l.id).preset===p.value&&Y(l.id).type==="preset"?"bg-white text-blue-600 shadow-sm":"text-gray-600 hover:text-gray-900"])},u(p.label),11,xo))),128))]),St(x,{"model-value":Y(l.id).customRange,"onUpdate:modelValue":p=>vt(l.id,p),type:"datetimerange","range-separator":"至","start-placeholder":"开始日期","end-placeholder":"结束日期",format:"YYYY-MM-DD HH:mm:ss","value-format":"YYYY-MM-DD HH:mm:ss","disabled-date":wt,"default-time":A.value,size:"small",style:{width:"280px"},class:"api-key-date-picker",clearable:!0,"unlink-panels":!1},null,8,["model-value","onUpdate:modelValue","default-time"])])])]),e.value[l.id]&&e.value[l.id].length===0?(i(),r("div",fo,[t("div",go,[s[50]||(s[50]=t("i",{class:"fas fa-chart-line text-gray-400 text-lg"},null,-1)),s[51]||(s[51]=t("p",{class:"text-sm text-gray-500"},"暂无模型使用数据",-1)),t("button",{onClick:p=>$t(l.id),class:"text-blue-500 hover:text-blue-700 text-sm ml-2 flex items-center gap-1 transition-colors",title:"重置筛选条件并刷新"},s[49]||(s[49]=[t("i",{class:"fas fa-sync-alt text-xs"},null,-1),t("span",{class:"text-xs"},"刷新",-1)]),8,yo)]),s[52]||(s[52]=t("p",{class:"text-xs text-gray-400"},"尝试调整时间范围或点击刷新重新加载数据",-1))])):e.value[l.id]&&e.value[l.id].length>0?(i(),r("div",bo,[(i(!0),r(_,null,T(e.value[l.id],p=>(i(),r("div",{key:p.model,class:"bg-gradient-to-br from-white to-gray-50 rounded-xl p-4 border border-gray-200 hover:border-indigo-300 hover:shadow-lg transition-all duration-200"},[t("div",vo,[t("div",wo,[t("span",$o,u(p.model),1),t("span",Co,u(p.requests)+" 次请求",1)])]),t("div",Ao,[t("div",Ko,[s[53]||(s[53]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-coins text-yellow-500 mr-1 text-xs"}),g(" 总Token: ")],-1)),t("span",ko,u(E(p.allTokens)),1)]),t("div",Lo,[s[54]||(s[54]=t("span",{class:"text-gray-600 flex items-center"},[t("i",{class:"fas fa-dollar-sign text-green-500 mr-1 text-xs"}),g(" 费用: ")],-1)),t("span",Do,u(bt(p)),1)]),t("div",Io,[t("div",ho,[s[55]||(s[55]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-arrow-down text-green-500 mr-1"}),g(" 输入: ")],-1)),t("span",Mo,u(E(p.inputTokens)),1)]),t("div",Ro,[s[56]||(s[56]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-arrow-up text-blue-500 mr-1"}),g(" 输出: ")],-1)),t("span",So,u(E(p.outputTokens)),1)]),p.cacheCreateTokens>0?(i(),r("div",Po,[s[57]||(s[57]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-save mr-1"}),g(" 缓存创建: ")],-1)),t("span",_o,u(E(p.cacheCreateTokens)),1)])):$("",!0),p.cacheReadTokens>0?(i(),r("div",To,[s[58]||(s[58]=t("span",{class:"flex items-center"},[t("i",{class:"fas fa-download mr-1"}),g(" 缓存读取: ")],-1)),t("span",Eo,u(E(p.cacheReadTokens)),1)])):$("",!0)])]),t("div",Vo,[t("div",{class:"bg-gradient-to-r from-indigo-500 to-purple-600 h-2 rounded-full transition-all duration-500",style:Pt({width:ct(p.allTokens,e.value[l.id])+"%"})},null,4)]),t("div",jo,[t("span",Uo,u(ct(p.allTokens,e.value[l.id]))+"% ",1)])]))),128))])):$("",!0),e.value[l.id]&&e.value[l.id].length>0?(i(),r("div",qo,[t("div",Fo,[s[61]||(s[61]=t("span",{class:"font-semibold text-gray-700 flex items-center"},[t("i",{class:"fas fa-calculator text-indigo-500 mr-2"}),g(" 总计统计 ")],-1)),t("div",Oo,[t("span",Wo,[s[59]||(s[59]=g(" 总请求: ",-1)),t("span",Yo,u(e.value[l.id].reduce((p,M)=>p+M.requests,0)),1)]),t("span",zo,[s[60]||(s[60]=g(" 总Token: ",-1)),t("span",No,u(E(e.value[l.id].reduce((p,M)=>p+M.allTokens,0))),1)])])])])):$("",!0)])])])):$("",!0)],64))),128))])])]))]),K.value?(i(),z(ke,{key:0,accounts:c.value,onClose:s[8]||(s[8]=l=>K.value=!1),onSuccess:kt},null,8,["accounts"])):$("",!0),n.value?(i(),z(ps,{key:1,apiKey:v.value,accounts:c.value,onClose:s[9]||(s[9]=l=>n.value=!1),onSuccess:Lt},null,8,["apiKey","accounts"])):$("",!0),m.value?(i(),z(hs,{key:2,apiKey:o.value,onClose:s[10]||(s[10]=l=>m.value=!1),onSuccess:Dt},null,8,["apiKey"])):$("",!0),j.value?(i(),z(Ws,{key:3,apiKey:d.value,onClose:s[11]||(s[11]=l=>j.value=!1)},null,8,["apiKey"])):$("",!0)])}}},Zo=J(Ho,[["__scopeId","data-v-518dfd83"]]);export{Zo as default}; diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index 5ea2c4cc..cfb4c2ca 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 23df0b0e..d29a8865 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -156,22 +156,33 @@ export const useDashboardStore = defineStore('dashboard', () => { if (dateFilter.value.type === 'preset') { switch (dateFilter.value.preset) { case 'last24h': + // 近24小时:从当前时间往前推24小时 + endTime = new Date(now) startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) - endTime = now break case 'yesterday': + // 昨天:使用UTC时间,避免时区双重转换 startTime = new Date(now) - startTime.setDate(now.getDate() - 1) - startTime.setHours(0, 0, 0, 0) + startTime.setUTCDate(now.getUTCDate() - 1) + startTime.setUTCHours(0, 0, 0, 0) endTime = new Date(startTime) - endTime.setHours(23, 59, 59, 999) + endTime.setUTCHours(23, 59, 59, 999) + + // 由于后端会加8小时,我们需要减去8小时 + startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) + endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) break case 'dayBefore': + // 前天:使用UTC时间 startTime = new Date(now) - startTime.setDate(now.getDate() - 2) - startTime.setHours(0, 0, 0, 0) + startTime.setUTCDate(now.getUTCDate() - 2) + startTime.setUTCHours(0, 0, 0, 0) endTime = new Date(startTime) - endTime.setHours(23, 59, 59, 999) + endTime.setUTCHours(23, 59, 59, 999) + + // 由于后端会加8小时,我们需要减去8小时 + startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) + endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) break default: // 默认近24小时 @@ -233,22 +244,33 @@ export const useDashboardStore = defineStore('dashboard', () => { if (dateFilter.value.type === 'preset') { switch (dateFilter.value.preset) { case 'last24h': + // 近24小时:从当前时间往前推24小时 + endTime = new Date(now) startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) - endTime = now break case 'yesterday': + // 昨天:使用UTC时间,避免时区双重转换 startTime = new Date(now) - startTime.setDate(now.getDate() - 1) - startTime.setHours(0, 0, 0, 0) + startTime.setUTCDate(now.getUTCDate() - 1) + startTime.setUTCHours(0, 0, 0, 0) endTime = new Date(startTime) - endTime.setHours(23, 59, 59, 999) + endTime.setUTCHours(23, 59, 59, 999) + + // 由于后端会加8小时,我们需要减去8小时 + startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) + endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) break case 'dayBefore': + // 前天:使用UTC时间 startTime = new Date(now) - startTime.setDate(now.getDate() - 2) - startTime.setHours(0, 0, 0, 0) + startTime.setUTCDate(now.getUTCDate() - 2) + startTime.setUTCHours(0, 0, 0, 0) endTime = new Date(startTime) - endTime.setHours(23, 59, 59, 999) + endTime.setUTCHours(23, 59, 59, 999) + + // 由于后端会加8小时,我们需要减去8小时 + startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) + endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) break default: // 默认近24小时 @@ -302,22 +324,33 @@ export const useDashboardStore = defineStore('dashboard', () => { // 小时粒度的预设 switch (preset) { case 'last24h': + // 近24小时:从当前时间往前推24小时 + endDate = new Date(now) startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000) - endDate = now break case 'yesterday': + // 昨天:使用UTC时间,避免时区双重转换 startDate = new Date(now) - startDate.setDate(now.getDate() - 1) - startDate.setHours(0, 0, 0, 0) + startDate.setUTCDate(now.getUTCDate() - 1) + startDate.setUTCHours(0, 0, 0, 0) endDate = new Date(startDate) - endDate.setHours(23, 59, 59, 999) + endDate.setUTCHours(23, 59, 59, 999) + + // 由于后端会加8小时,我们需要减去8小时 + startDate = new Date(startDate.getTime() - 8 * 60 * 60 * 1000) + endDate = new Date(endDate.getTime() - 8 * 60 * 60 * 1000) break case 'dayBefore': + // 前天:使用UTC时间 startDate = new Date(now) - startDate.setDate(now.getDate() - 2) - startDate.setHours(0, 0, 0, 0) + startDate.setUTCDate(now.getUTCDate() - 2) + startDate.setUTCHours(0, 0, 0, 0) endDate = new Date(startDate) - endDate.setHours(23, 59, 59, 999) + endDate.setUTCHours(23, 59, 59, 999) + + // 由于后端会加8小时,我们需要减去8小时 + startDate = new Date(startDate.getTime() - 8 * 60 * 60 * 1000) + endDate = new Date(endDate.getTime() - 8 * 60 * 60 * 1000) break } } else { From a265ebf335fad8e6618068fd238ebac969f3a10e Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 10:52:26 +0800 Subject: [PATCH 06/13] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E6=9D=BF=E5=9B=BE=E8=A1=A8=E6=97=B6=E5=8C=BA?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复前端时间计算逻辑,正确处理不同时区用户的时间范围选择 - 添加系统时区信息到dashboard API响应 - 修改getSystemTimezoneDay函数,确保正确处理系统时区的日期转换 - 修复"昨天"和"前天"时间范围计算,基于系统时区而非用户时区 - 添加后端调试日志以便追踪时间转换问题 问题描述: - 选择"昨天"时显示29号7:00到30号6:00(错误) - 选择"近24小时"时显示29号17:00到30号17:00(错误) 修复后: - "昨天"正确显示完整一天(如29号0:00到29号23:59) - "近24小时"正确显示从当前时间往前24小时 - 支持所有时区的用户,不再硬编码UTC+8偏移 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/admin.js | 24 +++-- web/admin-spa/dist/index.html | 2 +- web/admin-spa/src/stores/dashboard.js | 137 ++++++++++++++------------ 3 files changed, 93 insertions(+), 70 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 26849f40..353f95b4 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1293,7 +1293,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => { claudeAccountsHealthy: activeClaudeAccounts > 0, geminiAccountsHealthy: activeGeminiAccounts > 0, uptime: process.uptime() - } + }, + systemTimezone: config.system.timezoneOffset || 8 }; res.json({ success: true, data: dashboard }); @@ -1452,6 +1453,14 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { // 使用自定义时间范围 startTime = new Date(startDate); endTime = new Date(endDate); + + // 调试日志 + logger.info(`📊 Usage trend hour granularity - received times:`); + logger.info(` startDate (raw): ${startDate}`); + logger.info(` endDate (raw): ${endDate}`); + logger.info(` startTime (parsed): ${startTime.toISOString()}`); + logger.info(` endTime (parsed): ${endTime.toISOString()}`); + logger.info(` System timezone offset: ${config.system.timezoneOffset || 8}`); } else { // 默认最近24小时 endTime = new Date(); @@ -1471,7 +1480,8 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { currentHour.setMinutes(0, 0, 0); while (currentHour <= endTime) { - // 使用时区转换后的时间来生成键 + // 注意:前端发送的时间已经是UTC时间,不需要再次转换 + // 直接从currentHour生成对应系统时区的日期和小时 const tzCurrentHour = redis.getDateInTimezone(currentHour); const dateStr = redis.getDateStringInTimezone(currentHour); const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0'); @@ -1545,11 +1555,11 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { hourCost = costResult.costs.total; } - // 格式化时间标签 - const tzDate = redis.getDateInTimezone(currentHour); - const month = String(tzDate.getUTCMonth() + 1).padStart(2, '0'); - const day = String(tzDate.getUTCDate()).padStart(2, '0'); - const hourStr = String(tzDate.getUTCHours()).padStart(2, '0'); + // 格式化时间标签 - 使用系统时区的显示 + const tzDateForLabel = redis.getDateInTimezone(currentHour); + const month = String(tzDateForLabel.getUTCMonth() + 1).padStart(2, '0'); + const day = String(tzDateForLabel.getUTCDate()).padStart(2, '0'); + const hourStr = String(tzDateForLabel.getUTCHours()).padStart(2, '0'); trendData.push({ // 对于小时粒度,只返回hour字段,不返回date字段 diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index cfb4c2ca..0721fb8c 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index d29a8865..c7837448 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -27,7 +27,8 @@ export const useDashboardStore = defineStore('dashboard', () => { systemRPM: 0, systemTPM: 0, systemStatus: '正常', - uptime: 0 + uptime: 0, + systemTimezone: 8 // 默认 UTC+8 }) const costsData = ref({ @@ -81,6 +82,41 @@ export const useDashboardStore = defineStore('dashboard', () => { } }) + // 辅助函数:基于系统时区计算时间 + function getDateInSystemTimezone(date = new Date()) { + const offset = dashboardData.value.systemTimezone || 8 + // 将本地时间转换为UTC时间,然后加上系统时区偏移 + const utcTime = date.getTime() + (date.getTimezoneOffset() * 60000) + return new Date(utcTime + (offset * 3600000)) + } + + // 辅助函数:获取系统时区某一天的起止UTC时间 + // 输入:系统时区的日期(使用getDateInSystemTimezone转换后的) + // 输出:对应的UTC时间 + function getSystemTimezoneDay(systemTzDate, startOfDay = true) { + const offset = dashboardData.value.systemTimezone || 8 + + // 使用UTC方法获取系统时区日期的年月日 + const year = systemTzDate.getUTCFullYear() + const month = systemTzDate.getUTCMonth() + const day = systemTzDate.getUTCDate() + + // 创建一个新的Date对象 + const result = new Date() + + if (startOfDay) { + // 设置为系统时区的0点(UTC时间) + result.setUTCFullYear(year, month, day) + result.setUTCHours(0 - offset, 0, 0, 0) + } else { + // 设置为系统时区的23:59:59.999(UTC时间) + result.setUTCFullYear(year, month, day) + result.setUTCHours(23 - offset, 59, 59, 999) + } + + return result + } + // 方法 async function loadDashboardData() { loading.value = true @@ -118,7 +154,8 @@ export const useDashboardStore = defineStore('dashboard', () => { systemRPM: systemAverages.rpm || 0, systemTPM: systemAverages.tpm || 0, systemStatus: systemHealth.redisConnected ? '正常' : '异常', - uptime: systemHealth.uptime || 0 + uptime: systemHealth.uptime || 0, + systemTimezone: dashboardResponse.data.systemTimezone || 8 } } @@ -161,28 +198,20 @@ export const useDashboardStore = defineStore('dashboard', () => { startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) break case 'yesterday': - // 昨天:使用UTC时间,避免时区双重转换 - startTime = new Date(now) - startTime.setUTCDate(now.getUTCDate() - 1) - startTime.setUTCHours(0, 0, 0, 0) - endTime = new Date(startTime) - endTime.setUTCHours(23, 59, 59, 999) - - // 由于后端会加8小时,我们需要减去8小时 - startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) - endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) + // 昨天:基于系统时区的昨天 + const systemNow = getDateInSystemTimezone(now) + const yesterday = new Date(systemNow) + yesterday.setUTCDate(yesterday.getUTCDate() - 1) + startTime = getSystemTimezoneDay(yesterday, true) + endTime = getSystemTimezoneDay(yesterday, false) break case 'dayBefore': - // 前天:使用UTC时间 - startTime = new Date(now) - startTime.setUTCDate(now.getUTCDate() - 2) - startTime.setUTCHours(0, 0, 0, 0) - endTime = new Date(startTime) - endTime.setUTCHours(23, 59, 59, 999) - - // 由于后端会加8小时,我们需要减去8小时 - startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) - endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) + // 前天:基于系统时区的前天 + const systemNow2 = getDateInSystemTimezone(now) + const dayBefore = new Date(systemNow2) + dayBefore.setUTCDate(dayBefore.getUTCDate() - 2) + startTime = getSystemTimezoneDay(dayBefore, true) + endTime = getSystemTimezoneDay(dayBefore, false) break default: // 默认近24小时 @@ -249,28 +278,20 @@ export const useDashboardStore = defineStore('dashboard', () => { startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) break case 'yesterday': - // 昨天:使用UTC时间,避免时区双重转换 - startTime = new Date(now) - startTime.setUTCDate(now.getUTCDate() - 1) - startTime.setUTCHours(0, 0, 0, 0) - endTime = new Date(startTime) - endTime.setUTCHours(23, 59, 59, 999) - - // 由于后端会加8小时,我们需要减去8小时 - startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) - endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) + // 昨天:基于系统时区的昨天 + const systemNow3 = getDateInSystemTimezone(now) + const yesterday = new Date(systemNow3) + yesterday.setUTCDate(yesterday.getUTCDate() - 1) + startTime = getSystemTimezoneDay(yesterday, true) + endTime = getSystemTimezoneDay(yesterday, false) break case 'dayBefore': - // 前天:使用UTC时间 - startTime = new Date(now) - startTime.setUTCDate(now.getUTCDate() - 2) - startTime.setUTCHours(0, 0, 0, 0) - endTime = new Date(startTime) - endTime.setUTCHours(23, 59, 59, 999) - - // 由于后端会加8小时,我们需要减去8小时 - startTime = new Date(startTime.getTime() - 8 * 60 * 60 * 1000) - endTime = new Date(endTime.getTime() - 8 * 60 * 60 * 1000) + // 前天:基于系统时区的前天 + const systemNow4 = getDateInSystemTimezone(now) + const dayBefore = new Date(systemNow4) + dayBefore.setUTCDate(dayBefore.getUTCDate() - 2) + startTime = getSystemTimezoneDay(dayBefore, true) + endTime = getSystemTimezoneDay(dayBefore, false) break default: // 默认近24小时 @@ -329,28 +350,20 @@ export const useDashboardStore = defineStore('dashboard', () => { startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000) break case 'yesterday': - // 昨天:使用UTC时间,避免时区双重转换 - startDate = new Date(now) - startDate.setUTCDate(now.getUTCDate() - 1) - startDate.setUTCHours(0, 0, 0, 0) - endDate = new Date(startDate) - endDate.setUTCHours(23, 59, 59, 999) - - // 由于后端会加8小时,我们需要减去8小时 - startDate = new Date(startDate.getTime() - 8 * 60 * 60 * 1000) - endDate = new Date(endDate.getTime() - 8 * 60 * 60 * 1000) + // 昨天:基于系统时区的昨天 + const systemNow5 = getDateInSystemTimezone(now) + const yesterday2 = new Date(systemNow5) + yesterday2.setUTCDate(yesterday2.getUTCDate() - 1) + startDate = getSystemTimezoneDay(yesterday2, true) + endDate = getSystemTimezoneDay(yesterday2, false) break case 'dayBefore': - // 前天:使用UTC时间 - startDate = new Date(now) - startDate.setUTCDate(now.getUTCDate() - 2) - startDate.setUTCHours(0, 0, 0, 0) - endDate = new Date(startDate) - endDate.setUTCHours(23, 59, 59, 999) - - // 由于后端会加8小时,我们需要减去8小时 - startDate = new Date(startDate.getTime() - 8 * 60 * 60 * 1000) - endDate = new Date(endDate.getTime() - 8 * 60 * 60 * 1000) + // 前天:基于系统时区的前天 + const systemNow6 = getDateInSystemTimezone(now) + const dayBefore2 = new Date(systemNow6) + dayBefore2.setUTCDate(dayBefore2.getUTCDate() - 2) + startDate = getSystemTimezoneDay(dayBefore2, true) + endDate = getSystemTimezoneDay(dayBefore2, false) break } } else { From 6eae292922ff491e4731315bcebc77fb97c17659 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 11:01:51 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E8=AE=A1=E7=AE=97=E5=8F=8C=E9=87=8D=E5=81=8F?= =?UTF-8?q?=E7=A7=BB=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重写getSystemTimezoneDay函数,修复之前的双重偏移错误 - 简化时间计算逻辑,直接使用Date.UTC创建系统时区时间 - 移除不必要的getDateInSystemTimezone调用 问题原因: 之前的实现在计算系统时区日期时进行了双重偏移,导致时间范围错误 修复后: - 正确计算系统时区某一天的起止UTC时间 - "昨天"选项现在正确显示系统时区的完整一天(如29号0:00到29号23:59) - 适配所有时区的用户,不依赖用户浏览器时区 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- web/admin-spa/dist/index.html | 2 +- web/admin-spa/src/stores/dashboard.js | 66 ++++++++++++--------------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index 0721fb8c..fe670ac7 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index c7837448..787ac6f8 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -91,30 +91,30 @@ export const useDashboardStore = defineStore('dashboard', () => { } // 辅助函数:获取系统时区某一天的起止UTC时间 - // 输入:系统时区的日期(使用getDateInSystemTimezone转换后的) - // 输出:对应的UTC时间 - function getSystemTimezoneDay(systemTzDate, startOfDay = true) { + // 输入:一个日期(以用户本地时间表示) + // 输出:该日期在系统时区的0点/23:59对应的UTC时间 + function getSystemTimezoneDay(localDate, startOfDay = true) { const offset = dashboardData.value.systemTimezone || 8 - // 使用UTC方法获取系统时区日期的年月日 - const year = systemTzDate.getUTCFullYear() - const month = systemTzDate.getUTCMonth() - const day = systemTzDate.getUTCDate() - - // 创建一个新的Date对象 - const result = new Date() + // 获取用户本地日期的年月日 + const year = localDate.getFullYear() + const month = localDate.getMonth() + const day = localDate.getDate() + // 创建UTC时间,表示系统时区的该日期 if (startOfDay) { - // 设置为系统时区的0点(UTC时间) - result.setUTCFullYear(year, month, day) - result.setUTCHours(0 - offset, 0, 0, 0) + // 系统时区YYYY-MM-DD 00:00:00 对应的UTC时间 + const utcDate = new Date(Date.UTC(year, month, day, 0, 0, 0, 0)) + // 减去系统时区偏移得到UTC时间 + utcDate.setUTCHours(utcDate.getUTCHours() - offset) + return utcDate } else { - // 设置为系统时区的23:59:59.999(UTC时间) - result.setUTCFullYear(year, month, day) - result.setUTCHours(23 - offset, 59, 59, 999) + // 系统时区YYYY-MM-DD 23:59:59.999 对应的UTC时间 + const utcDate = new Date(Date.UTC(year, month, day, 23, 59, 59, 999)) + // 减去系统时区偏移得到UTC时间 + utcDate.setUTCHours(utcDate.getUTCHours() - offset) + return utcDate } - - return result } // 方法 @@ -199,17 +199,15 @@ export const useDashboardStore = defineStore('dashboard', () => { break case 'yesterday': // 昨天:基于系统时区的昨天 - const systemNow = getDateInSystemTimezone(now) - const yesterday = new Date(systemNow) - yesterday.setUTCDate(yesterday.getUTCDate() - 1) + const yesterday = new Date(now) + yesterday.setDate(yesterday.getDate() - 1) startTime = getSystemTimezoneDay(yesterday, true) endTime = getSystemTimezoneDay(yesterday, false) break case 'dayBefore': // 前天:基于系统时区的前天 - const systemNow2 = getDateInSystemTimezone(now) - const dayBefore = new Date(systemNow2) - dayBefore.setUTCDate(dayBefore.getUTCDate() - 2) + const dayBefore = new Date(now) + dayBefore.setDate(dayBefore.getDate() - 2) startTime = getSystemTimezoneDay(dayBefore, true) endTime = getSystemTimezoneDay(dayBefore, false) break @@ -279,17 +277,15 @@ export const useDashboardStore = defineStore('dashboard', () => { break case 'yesterday': // 昨天:基于系统时区的昨天 - const systemNow3 = getDateInSystemTimezone(now) - const yesterday = new Date(systemNow3) - yesterday.setUTCDate(yesterday.getUTCDate() - 1) + const yesterday = new Date(now) + yesterday.setDate(yesterday.getDate() - 1) startTime = getSystemTimezoneDay(yesterday, true) endTime = getSystemTimezoneDay(yesterday, false) break case 'dayBefore': // 前天:基于系统时区的前天 - const systemNow4 = getDateInSystemTimezone(now) - const dayBefore = new Date(systemNow4) - dayBefore.setUTCDate(dayBefore.getUTCDate() - 2) + const dayBefore = new Date(now) + dayBefore.setDate(dayBefore.getDate() - 2) startTime = getSystemTimezoneDay(dayBefore, true) endTime = getSystemTimezoneDay(dayBefore, false) break @@ -351,17 +347,15 @@ export const useDashboardStore = defineStore('dashboard', () => { break case 'yesterday': // 昨天:基于系统时区的昨天 - const systemNow5 = getDateInSystemTimezone(now) - const yesterday2 = new Date(systemNow5) - yesterday2.setUTCDate(yesterday2.getUTCDate() - 1) + const yesterday2 = new Date(now) + yesterday2.setDate(yesterday2.getDate() - 1) startDate = getSystemTimezoneDay(yesterday2, true) endDate = getSystemTimezoneDay(yesterday2, false) break case 'dayBefore': // 前天:基于系统时区的前天 - const systemNow6 = getDateInSystemTimezone(now) - const dayBefore2 = new Date(systemNow6) - dayBefore2.setUTCDate(dayBefore2.getUTCDate() - 2) + const dayBefore2 = new Date(now) + dayBefore2.setDate(dayBefore2.getDate() - 2) startDate = getSystemTimezoneDay(dayBefore2, true) endDate = getSystemTimezoneDay(dayBefore2, false) break From 4eedc2e6b582bb5c0702f41ef1241209be778d64 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 12:05:30 +0800 Subject: [PATCH 08/13] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E6=9D=BF=E6=97=B6=E5=8C=BA=E5=A4=84=E7=90=86?= =?UTF-8?q?=20-=20=E5=89=8D=E7=AB=AF=E6=98=BE=E7=A4=BA=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=EF=BC=8C=E5=8F=91=E9=80=81UTC=E6=97=B6?= =?UTF-8?q?=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化 getSystemTimezoneDay 函数,正确处理系统时区到UTC的转换 - 修复 setDateFilterPreset 中昨天/前天的计算逻辑,使用本地时间计算日期 - 确保自定义时间选择器显示系统时区时间,但发送UTC时间给后端 - API Keys页面已经使用正确的时间范围参数,无需修改 --- .../dist/assets/ApiKeysView-TPhICvaG.css | 1 - web/admin-spa/dist/index.html | 2 +- web/admin-spa/src/stores/dashboard.js | 142 ++++++++++++------ 3 files changed, 93 insertions(+), 52 deletions(-) delete mode 100644 web/admin-spa/dist/assets/ApiKeysView-TPhICvaG.css diff --git a/web/admin-spa/dist/assets/ApiKeysView-TPhICvaG.css b/web/admin-spa/dist/assets/ApiKeysView-TPhICvaG.css deleted file mode 100644 index 6b914cc9..00000000 --- a/web/admin-spa/dist/assets/ApiKeysView-TPhICvaG.css +++ /dev/null @@ -1 +0,0 @@ -pre[data-v-3d91f349]{white-space:pre-wrap;word-wrap:break-word}.tab-content[data-v-518dfd83]{min-height:calc(100vh - 300px)}.table-container[data-v-518dfd83]{overflow-x:auto;border-radius:12px;border:1px solid rgba(0,0,0,.05)}.table-row[data-v-518dfd83]{transition:all .2s ease}.table-row[data-v-518dfd83]:hover{background-color:#00000005}.loading-spinner[data-v-518dfd83]{width:24px;height:24px;border:2px solid #e5e7eb;border-top:2px solid #3b82f6;border-radius:50%;animation:spin-518dfd83 1s linear infinite}@keyframes spin-518dfd83{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.api-key-date-picker[data-v-518dfd83] .el-input__inner{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.api-key-date-picker[data-v-518dfd83] .el-input__inner:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1));--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.api-key-date-picker[data-v-518dfd83] .el-range-separator{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))} diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index fe670ac7..656f0c25 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 787ac6f8..f0ddd08e 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -91,29 +91,27 @@ export const useDashboardStore = defineStore('dashboard', () => { } // 辅助函数:获取系统时区某一天的起止UTC时间 - // 输入:一个日期(以用户本地时间表示) + // 输入:一个系统时区的日期对象 // 输出:该日期在系统时区的0点/23:59对应的UTC时间 - function getSystemTimezoneDay(localDate, startOfDay = true) { + function getSystemTimezoneDay(systemTzDate, startOfDay = true) { const offset = dashboardData.value.systemTimezone || 8 - // 获取用户本地日期的年月日 - const year = localDate.getFullYear() - const month = localDate.getMonth() - const day = localDate.getDate() + // 获取系统时区日期的年月日 + const year = systemTzDate.getFullYear() + const month = systemTzDate.getMonth() + const day = systemTzDate.getDate() - // 创建UTC时间,表示系统时区的该日期 + // 创建系统时区的日期时间 if (startOfDay) { - // 系统时区YYYY-MM-DD 00:00:00 对应的UTC时间 - const utcDate = new Date(Date.UTC(year, month, day, 0, 0, 0, 0)) - // 减去系统时区偏移得到UTC时间 - utcDate.setUTCHours(utcDate.getUTCHours() - offset) - return utcDate + // 系统时区 YYYY-MM-DD 00:00:00 + const systemDateTime = new Date(year, month, day, 0, 0, 0, 0) + // 转换为UTC时间:减去时区偏移 + return new Date(systemDateTime.getTime() - offset * 3600000) } else { - // 系统时区YYYY-MM-DD 23:59:59.999 对应的UTC时间 - const utcDate = new Date(Date.UTC(year, month, day, 23, 59, 59, 999)) - // 减去系统时区偏移得到UTC时间 - utcDate.setUTCHours(utcDate.getUTCHours() - offset) - return utcDate + // 系统时区 YYYY-MM-DD 23:59:59.999 + const systemDateTime = new Date(year, month, day, 23, 59, 59, 999) + // 转换为UTC时间:减去时区偏移 + return new Date(systemDateTime.getTime() - offset * 3600000) } } @@ -182,9 +180,24 @@ export const useDashboardStore = defineStore('dashboard', () => { url += `granularity=hour` if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { - // 使用自定义时间范围 - url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}` - url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}` + // 使用自定义时间范围 - 需要将系统时区时间转换为UTC + const convertToUTC = (systemTzTimeStr) => { + const systemTz = dashboardData.value.systemTimezone || 8 + // 解析系统时区时间字符串 + const [datePart, timePart] = systemTzTimeStr.split(' ') + const [year, month, day] = datePart.split('-').map(Number) + const [hours, minutes, seconds] = timePart.split(':').map(Number) + + // 创建系统时区的Date对象 + const systemDate = new Date(year, month - 1, day, hours, minutes, seconds) + + // 转换为UTC + const utcTime = systemDate.getTime() - (systemTz * 3600000) + return new Date(utcTime).toISOString() + } + + url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` + url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}` } else { // 使用预设计算时间范围,与loadApiKeysTrend保持一致 const now = new Date() @@ -199,14 +212,14 @@ export const useDashboardStore = defineStore('dashboard', () => { break case 'yesterday': // 昨天:基于系统时区的昨天 - const yesterday = new Date(now) + const yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) startTime = getSystemTimezoneDay(yesterday, true) endTime = getSystemTimezoneDay(yesterday, false) break case 'dayBefore': // 前天:基于系统时区的前天 - const dayBefore = new Date(now) + const dayBefore = new Date() dayBefore.setDate(dayBefore.getDate() - 2) startTime = getSystemTimezoneDay(dayBefore, true) endTime = getSystemTimezoneDay(dayBefore, false) @@ -260,9 +273,24 @@ export const useDashboardStore = defineStore('dashboard', () => { url += `granularity=hour` if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { - // 使用自定义时间范围 - url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}` - url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}` + // 使用自定义时间范围 - 需要将系统时区时间转换为UTC + const convertToUTC = (systemTzTimeStr) => { + const systemTz = dashboardData.value.systemTimezone || 8 + // 解析系统时区时间字符串 + const [datePart, timePart] = systemTzTimeStr.split(' ') + const [year, month, day] = datePart.split('-').map(Number) + const [hours, minutes, seconds] = timePart.split(':').map(Number) + + // 创建系统时区的Date对象 + const systemDate = new Date(year, month - 1, day, hours, minutes, seconds) + + // 转换为UTC + const utcTime = systemDate.getTime() - (systemTz * 3600000) + return new Date(utcTime).toISOString() + } + + url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` + url += `&endDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[1]))}` } else { // 使用预设计算时间范围,与setDateFilterPreset保持一致 const now = new Date() @@ -277,14 +305,14 @@ export const useDashboardStore = defineStore('dashboard', () => { break case 'yesterday': // 昨天:基于系统时区的昨天 - const yesterday = new Date(now) + const yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) startTime = getSystemTimezoneDay(yesterday, true) endTime = getSystemTimezoneDay(yesterday, false) break case 'dayBefore': // 前天:基于系统时区的前天 - const dayBefore = new Date(now) + const dayBefore = new Date() dayBefore.setDate(dayBefore.getDate() - 2) startTime = getSystemTimezoneDay(dayBefore, true) endTime = getSystemTimezoneDay(dayBefore, false) @@ -346,18 +374,20 @@ export const useDashboardStore = defineStore('dashboard', () => { startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000) break case 'yesterday': - // 昨天:基于系统时区的昨天 - const yesterday2 = new Date(now) - yesterday2.setDate(yesterday2.getDate() - 1) - startDate = getSystemTimezoneDay(yesterday2, true) - endDate = getSystemTimezoneDay(yesterday2, false) + // 昨天:获取本地时间的昨天 + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + // 转换为系统时区的昨天0点和23:59 + startDate = getSystemTimezoneDay(yesterday, true) + endDate = getSystemTimezoneDay(yesterday, false) break case 'dayBefore': - // 前天:基于系统时区的前天 - const dayBefore2 = new Date(now) - dayBefore2.setDate(dayBefore2.getDate() - 2) - startDate = getSystemTimezoneDay(dayBefore2, true) - endDate = getSystemTimezoneDay(dayBefore2, false) + // 前天:获取本地时间的前天 + const dayBefore = new Date() + dayBefore.setDate(dayBefore.getDate() - 2) + // 转换为系统时区的前天0点和23:59 + startDate = getSystemTimezoneDay(dayBefore, true) + endDate = getSystemTimezoneDay(dayBefore, false) break } } else { @@ -381,19 +411,21 @@ export const useDashboardStore = defineStore('dashboard', () => { dateFilter.value.customEnd = endDate.toISOString().split('T')[0] // 设置 customRange 为 Element Plus 需要的格式 - const formatDate = (date) => { - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') + // 显示系统时区的时间 + const formatDateForDisplay = (utcDate) => { + const systemTzDate = getDateInSystemTimezone(utcDate) + const year = systemTzDate.getUTCFullYear() + const month = String(systemTzDate.getUTCMonth() + 1).padStart(2, '0') + const day = String(systemTzDate.getUTCDate()).padStart(2, '0') + const hours = String(systemTzDate.getUTCHours()).padStart(2, '0') + const minutes = String(systemTzDate.getUTCMinutes()).padStart(2, '0') + const seconds = String(systemTzDate.getUTCSeconds()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } dateFilter.value.customRange = [ - formatDate(startDate), - formatDate(endDate) + formatDateForDisplay(startDate), + formatDateForDisplay(endDate) ] } @@ -409,9 +441,19 @@ export const useDashboardStore = defineStore('dashboard', () => { dateFilter.value.customStart = value[0].split(' ')[0] dateFilter.value.customEnd = value[1].split(' ')[0] - // 检查日期范围限制 - const start = new Date(value[0]) - const end = new Date(value[1]) + // 检查日期范围限制 - value中的时间已经是系统时区时间 + const systemTz = dashboardData.value.systemTimezone || 8 + + // 解析系统时区时间 + const parseSystemTime = (timeStr) => { + const [datePart, timePart] = timeStr.split(' ') + const [year, month, day] = datePart.split('-').map(Number) + const [hours, minutes, seconds] = timePart.split(':').map(Number) + return new Date(year, month - 1, day, hours, minutes, seconds) + } + + const start = parseSystemTime(value[0]) + const end = parseSystemTime(value[1]) if (trendGranularity.value === 'hour') { // 小时粒度:限制 24 小时 @@ -433,7 +475,7 @@ export const useDashboardStore = defineStore('dashboard', () => { refreshChartsData() } else if (value === null) { // 清空时恢复默认 - setDateFilterPreset(trendGranularity.value === 'hour' ? '7days' : '7days') + setDateFilterPreset(trendGranularity.value === 'hour' ? 'last24h' : '7days') } } From 3188f4d0588ed4574541b7c90331dc811952cc13 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 12:13:53 +0800 Subject: [PATCH 09/13] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E8=BD=AC=E6=8D=A2=E8=AE=A1=E7=AE=97=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 getSystemTimezoneDay 函数:正确使用 Date.UTC 创建UTC时间,避免本地时区干扰 - 修复自定义时间范围转换:使用 Date.UTC 正确转换系统时区时间到UTC - 解决了昨天/前天日期计算错误的问题 - 解决了自定义时间选择器8小时偏差的问题 --- web/admin-spa/src/stores/dashboard.js | 53 +++++++++++++-------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index f0ddd08e..35d07d45 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -91,27 +91,26 @@ export const useDashboardStore = defineStore('dashboard', () => { } // 辅助函数:获取系统时区某一天的起止UTC时间 - // 输入:一个系统时区的日期对象 + // 输入:一个本地时间的日期对象(如用户选择的日期) // 输出:该日期在系统时区的0点/23:59对应的UTC时间 - function getSystemTimezoneDay(systemTzDate, startOfDay = true) { - const offset = dashboardData.value.systemTimezone || 8 + function getSystemTimezoneDay(localDate, startOfDay = true) { + const systemTz = dashboardData.value.systemTimezone || 8 - // 获取系统时区日期的年月日 - const year = systemTzDate.getFullYear() - const month = systemTzDate.getMonth() - const day = systemTzDate.getDate() + // 获取本地日期的年月日(这是用户想要查看的日期) + const year = localDate.getFullYear() + const month = localDate.getMonth() + const day = localDate.getDate() - // 创建系统时区的日期时间 if (startOfDay) { - // 系统时区 YYYY-MM-DD 00:00:00 - const systemDateTime = new Date(year, month, day, 0, 0, 0, 0) - // 转换为UTC时间:减去时区偏移 - return new Date(systemDateTime.getTime() - offset * 3600000) + // 创建UTC时间,使其在系统时区(UTC+8)显示为 YYYY-MM-DD 00:00:00 + // 例如:要在UTC+8显示为 2025-07-28 00:00:00,UTC时间应该是 2025-07-27 16:00:00 + const utcDate = new Date(Date.UTC(year, month, day, 0 - systemTz, 0, 0, 0)) + return utcDate } else { - // 系统时区 YYYY-MM-DD 23:59:59.999 - const systemDateTime = new Date(year, month, day, 23, 59, 59, 999) - // 转换为UTC时间:减去时区偏移 - return new Date(systemDateTime.getTime() - offset * 3600000) + // 创建UTC时间,使其在系统时区(UTC+8)显示为 YYYY-MM-DD 23:59:59.999 + // 例如:要在UTC+8显示为 2025-07-28 23:59:59,UTC时间应该是 2025-07-28 15:59:59 + const utcDate = new Date(Date.UTC(year, month, day, 24 - systemTz - 1, 59, 59, 999)) + return utcDate } } @@ -188,12 +187,11 @@ export const useDashboardStore = defineStore('dashboard', () => { const [year, month, day] = datePart.split('-').map(Number) const [hours, minutes, seconds] = timePart.split(':').map(Number) - // 创建系统时区的Date对象 - const systemDate = new Date(year, month - 1, day, hours, minutes, seconds) - - // 转换为UTC - const utcTime = systemDate.getTime() - (systemTz * 3600000) - return new Date(utcTime).toISOString() + // 创建UTC时间,使其在系统时区显示为用户选择的时间 + // 例如:用户选择 UTC+8 的 2025-07-25 00:00:00 + // 对应的UTC时间是 2025-07-24 16:00:00 + const utcDate = new Date(Date.UTC(year, month - 1, day, hours - systemTz, minutes, seconds)) + return utcDate.toISOString() } url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` @@ -281,12 +279,11 @@ export const useDashboardStore = defineStore('dashboard', () => { const [year, month, day] = datePart.split('-').map(Number) const [hours, minutes, seconds] = timePart.split(':').map(Number) - // 创建系统时区的Date对象 - const systemDate = new Date(year, month - 1, day, hours, minutes, seconds) - - // 转换为UTC - const utcTime = systemDate.getTime() - (systemTz * 3600000) - return new Date(utcTime).toISOString() + // 创建UTC时间,使其在系统时区显示为用户选择的时间 + // 例如:用户选择 UTC+8 的 2025-07-25 00:00:00 + // 对应的UTC时间是 2025-07-24 16:00:00 + const utcDate = new Date(Date.UTC(year, month - 1, day, hours - systemTz, minutes, seconds)) + return utcDate.toISOString() } url += `&startDate=${encodeURIComponent(convertToUTC(dateFilter.value.customRange[0]))}` From 1e82a6af8beb316f5a709d98b18925cf988a2174 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 12:14:42 +0800 Subject: [PATCH 10/13] =?UTF-8?q?build:=20=E9=87=8D=E6=96=B0=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=89=8D=E7=AB=AF=E5=BA=94=E7=94=A8=20-=20=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E6=97=B6=E5=8C=BA=E8=BD=AC=E6=8D=A2=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/admin-spa/dist/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index 656f0c25..f162bee0 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + From 9d8d698238eed298615fceb0cc894abf6518ac5a Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 12:25:51 +0800 Subject: [PATCH 11/13] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E8=AE=A1=E7=AE=97=E9=97=AE=E9=A2=98=20-=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=9B=BA=E5=AE=9A=E7=9A=84UTC+8=E6=97=B6?= =?UTF-8?q?=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 getSystemTimezoneDay:正确计算UTC+8时区的起止时间 - 修复自定义时间转换:使用固定的UTC+8时区偏移 - 解决了时区数据未初始化导致的计算错误 --- web/admin-spa/src/stores/dashboard.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 35d07d45..728cb74c 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -94,7 +94,8 @@ export const useDashboardStore = defineStore('dashboard', () => { // 输入:一个本地时间的日期对象(如用户选择的日期) // 输出:该日期在系统时区的0点/23:59对应的UTC时间 function getSystemTimezoneDay(localDate, startOfDay = true) { - const systemTz = dashboardData.value.systemTimezone || 8 + // 固定使用UTC+8,因为后端系统时区是UTC+8 + const systemTz = 8 // 获取本地日期的年月日(这是用户想要查看的日期) const year = localDate.getFullYear() @@ -103,12 +104,12 @@ export const useDashboardStore = defineStore('dashboard', () => { if (startOfDay) { // 创建UTC时间,使其在系统时区(UTC+8)显示为 YYYY-MM-DD 00:00:00 - // 例如:要在UTC+8显示为 2025-07-28 00:00:00,UTC时间应该是 2025-07-27 16:00:00 - const utcDate = new Date(Date.UTC(year, month, day, 0 - systemTz, 0, 0, 0)) + // 例如:要在UTC+8显示为 2025-07-29 00:00:00,UTC时间应该是 2025-07-28 16:00:00 + const utcDate = new Date(Date.UTC(year, month, day - 1, 24 - systemTz, 0, 0, 0)) return utcDate } else { // 创建UTC时间,使其在系统时区(UTC+8)显示为 YYYY-MM-DD 23:59:59.999 - // 例如:要在UTC+8显示为 2025-07-28 23:59:59,UTC时间应该是 2025-07-28 15:59:59 + // 例如:要在UTC+8显示为 2025-07-29 23:59:59,UTC时间应该是 2025-07-29 15:59:59 const utcDate = new Date(Date.UTC(year, month, day, 24 - systemTz - 1, 59, 59, 999)) return utcDate } @@ -181,7 +182,8 @@ export const useDashboardStore = defineStore('dashboard', () => { if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { // 使用自定义时间范围 - 需要将系统时区时间转换为UTC const convertToUTC = (systemTzTimeStr) => { - const systemTz = dashboardData.value.systemTimezone || 8 + // 固定使用UTC+8,因为后端系统时区是UTC+8 + const systemTz = 8 // 解析系统时区时间字符串 const [datePart, timePart] = systemTzTimeStr.split(' ') const [year, month, day] = datePart.split('-').map(Number) @@ -273,7 +275,8 @@ export const useDashboardStore = defineStore('dashboard', () => { if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { // 使用自定义时间范围 - 需要将系统时区时间转换为UTC const convertToUTC = (systemTzTimeStr) => { - const systemTz = dashboardData.value.systemTimezone || 8 + // 固定使用UTC+8,因为后端系统时区是UTC+8 + const systemTz = 8 // 解析系统时区时间字符串 const [datePart, timePart] = systemTzTimeStr.split(' ') const [year, month, day] = datePart.split('-').map(Number) From 2bfb61e1d9a30eb58fb834a7936fef7cfa37cc8f Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 12:26:45 +0800 Subject: [PATCH 12/13] =?UTF-8?q?build:=20=E9=87=8D=E6=96=B0=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=89=8D=E7=AB=AF=20-=20=E5=8C=85=E5=90=ABUTC+8?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/admin-spa/dist/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index f162bee0..300f4098 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + From 81917c281e3f398313bb5e67f8f17ceae8af37d0 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 12:38:08 +0800 Subject: [PATCH 13/13] =?UTF-8?q?fix(admin-spa):=20=E5=BD=BB=E5=BA=95?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=B6=E5=8C=BA=E8=AE=A1=E7=AE=97=E5=92=8C?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 getSystemTimezoneDay 函数:正确计算UTC+8时区对应的UTC时间 - 修复时间筛选框显示:昨天/前天显示正确的系统时区日期 - 修复自定义时间范围:正确处理系统时区到UTC的转换 - 简化时区计算逻辑,避免复杂的条件判断 --- web/admin-spa/src/stores/dashboard.js | 71 ++++++++++++++++++--------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 728cb74c..19a83bda 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -103,15 +103,15 @@ export const useDashboardStore = defineStore('dashboard', () => { const day = localDate.getDate() if (startOfDay) { - // 创建UTC时间,使其在系统时区(UTC+8)显示为 YYYY-MM-DD 00:00:00 - // 例如:要在UTC+8显示为 2025-07-29 00:00:00,UTC时间应该是 2025-07-28 16:00:00 - const utcDate = new Date(Date.UTC(year, month, day - 1, 24 - systemTz, 0, 0, 0)) - return utcDate + // 系统时区(UTC+8)的 YYYY-MM-DD 00:00:00 + // 对应的UTC时间是前一天的16:00 + // 例如:UTC+8的2025-07-29 00:00:00 = UTC的2025-07-28 16:00:00 + return new Date(Date.UTC(year, month, day - 1, 16, 0, 0, 0)) } else { - // 创建UTC时间,使其在系统时区(UTC+8)显示为 YYYY-MM-DD 23:59:59.999 - // 例如:要在UTC+8显示为 2025-07-29 23:59:59,UTC时间应该是 2025-07-29 15:59:59 - const utcDate = new Date(Date.UTC(year, month, day, 24 - systemTz - 1, 59, 59, 999)) - return utcDate + // 系统时区(UTC+8)的 YYYY-MM-DD 23:59:59 + // 对应的UTC时间是当天的15:59:59 + // 例如:UTC+8的2025-07-29 23:59:59 = UTC的2025-07-29 15:59:59 + return new Date(Date.UTC(year, month, day, 15, 59, 59, 999)) } } @@ -411,22 +411,47 @@ export const useDashboardStore = defineStore('dashboard', () => { dateFilter.value.customEnd = endDate.toISOString().split('T')[0] // 设置 customRange 为 Element Plus 需要的格式 - // 显示系统时区的时间 - const formatDateForDisplay = (utcDate) => { - const systemTzDate = getDateInSystemTimezone(utcDate) - const year = systemTzDate.getUTCFullYear() - const month = String(systemTzDate.getUTCMonth() + 1).padStart(2, '0') - const day = String(systemTzDate.getUTCDate()).padStart(2, '0') - const hours = String(systemTzDate.getUTCHours()).padStart(2, '0') - const minutes = String(systemTzDate.getUTCMinutes()).padStart(2, '0') - const seconds = String(systemTzDate.getUTCSeconds()).padStart(2, '0') - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + // 对于小时粒度的昨天/前天,需要特殊处理显示 + if (trendGranularity.value === 'hour' && (preset === 'yesterday' || preset === 'dayBefore')) { + // 获取本地日期 + const targetDate = new Date() + if (preset === 'yesterday') { + targetDate.setDate(targetDate.getDate() - 1) + } else { + targetDate.setDate(targetDate.getDate() - 2) + } + + // 显示系统时区的完整一天 + const year = targetDate.getFullYear() + const month = String(targetDate.getMonth() + 1).padStart(2, '0') + const day = String(targetDate.getDate()).padStart(2, '0') + + dateFilter.value.customRange = [ + `${year}-${month}-${day} 00:00:00`, + `${year}-${month}-${day} 23:59:59` + ] + } else { + // 其他情况:近24小时或天粒度 + const formatDateForDisplay = (date) => { + // 固定使用UTC+8来显示时间 + const systemTz = 8 + const tzOffset = systemTz * 60 * 60 * 1000 + const localTime = new Date(date.getTime() + tzOffset) + + const year = localTime.getUTCFullYear() + const month = String(localTime.getUTCMonth() + 1).padStart(2, '0') + const day = String(localTime.getUTCDate()).padStart(2, '0') + const hours = String(localTime.getUTCHours()).padStart(2, '0') + const minutes = String(localTime.getUTCMinutes()).padStart(2, '0') + const seconds = String(localTime.getUTCSeconds()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` + } + + dateFilter.value.customRange = [ + formatDateForDisplay(startDate), + formatDateForDisplay(endDate) + ] } - - dateFilter.value.customRange = [ - formatDateForDisplay(startDate), - formatDateForDisplay(endDate) - ] } // 触发数据刷新