mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge branch 'main' of https://github.com/Wei-Shaw/claude-relay-service
This commit is contained in:
@@ -739,10 +739,28 @@ class ClaudeAccountService {
|
|||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取或创建会话窗口
|
||||||
|
const updatedAccountData = await this.updateSessionWindow(accountId, accountData);
|
||||||
|
|
||||||
// 设置限流状态和时间
|
// 设置限流状态和时间
|
||||||
accountData.rateLimitedAt = new Date().toISOString();
|
updatedAccountData.rateLimitedAt = new Date().toISOString();
|
||||||
accountData.rateLimitStatus = 'limited';
|
updatedAccountData.rateLimitStatus = 'limited';
|
||||||
await redis.setClaudeAccount(accountId, accountData);
|
|
||||||
|
// 限流结束时间 = 会话窗口结束时间
|
||||||
|
if (updatedAccountData.sessionWindowEnd) {
|
||||||
|
updatedAccountData.rateLimitEndAt = updatedAccountData.sessionWindowEnd;
|
||||||
|
const windowEnd = new Date(updatedAccountData.sessionWindowEnd);
|
||||||
|
const now = new Date();
|
||||||
|
const minutesUntilEnd = Math.ceil((windowEnd - now) / (1000 * 60));
|
||||||
|
logger.warn(`🚫 Account marked as rate limited until session window ends: ${accountData.name} (${accountId}) - ${minutesUntilEnd} minutes remaining`);
|
||||||
|
} else {
|
||||||
|
// 如果没有会话窗口,使用默认1小时(兼容旧逻辑)
|
||||||
|
const oneHourLater = new Date(Date.now() + 60 * 60 * 1000);
|
||||||
|
updatedAccountData.rateLimitEndAt = oneHourLater.toISOString();
|
||||||
|
logger.warn(`🚫 Account marked as rate limited (1 hour default): ${accountData.name} (${accountId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.setClaudeAccount(accountId, updatedAccountData);
|
||||||
|
|
||||||
// 如果有会话哈希,删除粘性会话映射
|
// 如果有会话哈希,删除粘性会话映射
|
||||||
if (sessionHash) {
|
if (sessionHash) {
|
||||||
@@ -750,7 +768,6 @@ class ClaudeAccountService {
|
|||||||
logger.info(`🗑️ Deleted sticky session mapping for rate limited account: ${accountId}`);
|
logger.info(`🗑️ Deleted sticky session mapping for rate limited account: ${accountId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warn(`🚫 Account marked as rate limited: ${accountData.name} (${accountId})`);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to mark account as rate limited: ${accountId}`, error);
|
logger.error(`❌ Failed to mark account as rate limited: ${accountId}`, error);
|
||||||
@@ -769,6 +786,7 @@ class ClaudeAccountService {
|
|||||||
// 清除限流状态
|
// 清除限流状态
|
||||||
delete accountData.rateLimitedAt;
|
delete accountData.rateLimitedAt;
|
||||||
delete accountData.rateLimitStatus;
|
delete accountData.rateLimitStatus;
|
||||||
|
delete accountData.rateLimitEndAt; // 清除限流结束时间
|
||||||
await redis.setClaudeAccount(accountId, accountData);
|
await redis.setClaudeAccount(accountId, accountData);
|
||||||
|
|
||||||
logger.success(`✅ Rate limit removed for account: ${accountData.name} (${accountId})`);
|
logger.success(`✅ Rate limit removed for account: ${accountData.name} (${accountId})`);
|
||||||
@@ -789,17 +807,32 @@ class ClaudeAccountService {
|
|||||||
|
|
||||||
// 检查是否有限流状态
|
// 检查是否有限流状态
|
||||||
if (accountData.rateLimitStatus === 'limited' && accountData.rateLimitedAt) {
|
if (accountData.rateLimitStatus === 'limited' && accountData.rateLimitedAt) {
|
||||||
const rateLimitedAt = new Date(accountData.rateLimitedAt);
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const hoursSinceRateLimit = (now - rateLimitedAt) / (1000 * 60 * 60);
|
|
||||||
|
|
||||||
// 如果限流超过1小时,自动解除
|
// 优先使用 rateLimitEndAt(基于会话窗口)
|
||||||
if (hoursSinceRateLimit >= 1) {
|
if (accountData.rateLimitEndAt) {
|
||||||
await this.removeAccountRateLimit(accountId);
|
const rateLimitEndAt = new Date(accountData.rateLimitEndAt);
|
||||||
return false;
|
|
||||||
|
// 如果当前时间超过限流结束时间,自动解除
|
||||||
|
if (now >= rateLimitEndAt) {
|
||||||
|
await this.removeAccountRateLimit(accountId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// 兼容旧数据:使用1小时限流
|
||||||
|
const rateLimitedAt = new Date(accountData.rateLimitedAt);
|
||||||
|
const hoursSinceRateLimit = (now - rateLimitedAt) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
// 如果限流超过1小时,自动解除
|
||||||
|
if (hoursSinceRateLimit >= 1) {
|
||||||
|
await this.removeAccountRateLimit(accountId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -821,13 +854,29 @@ class ClaudeAccountService {
|
|||||||
const rateLimitedAt = new Date(accountData.rateLimitedAt);
|
const rateLimitedAt = new Date(accountData.rateLimitedAt);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const minutesSinceRateLimit = Math.floor((now - rateLimitedAt) / (1000 * 60));
|
const minutesSinceRateLimit = Math.floor((now - rateLimitedAt) / (1000 * 60));
|
||||||
const minutesRemaining = Math.max(0, 60 - minutesSinceRateLimit);
|
|
||||||
|
let minutesRemaining;
|
||||||
|
let rateLimitEndAt;
|
||||||
|
|
||||||
|
// 优先使用 rateLimitEndAt(基于会话窗口)
|
||||||
|
if (accountData.rateLimitEndAt) {
|
||||||
|
rateLimitEndAt = accountData.rateLimitEndAt;
|
||||||
|
const endTime = new Date(accountData.rateLimitEndAt);
|
||||||
|
minutesRemaining = Math.max(0, Math.ceil((endTime - now) / (1000 * 60)));
|
||||||
|
} else {
|
||||||
|
// 兼容旧数据:使用1小时限流
|
||||||
|
minutesRemaining = Math.max(0, 60 - minutesSinceRateLimit);
|
||||||
|
// 计算预期的结束时间
|
||||||
|
const endTime = new Date(rateLimitedAt.getTime() + 60 * 60 * 1000);
|
||||||
|
rateLimitEndAt = endTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isRateLimited: minutesRemaining > 0,
|
isRateLimited: minutesRemaining > 0,
|
||||||
rateLimitedAt: accountData.rateLimitedAt,
|
rateLimitedAt: accountData.rateLimitedAt,
|
||||||
minutesSinceRateLimit,
|
minutesSinceRateLimit,
|
||||||
minutesRemaining
|
minutesRemaining,
|
||||||
|
rateLimitEndAt // 新增:限流结束时间
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,7 +884,8 @@ class ClaudeAccountService {
|
|||||||
isRateLimited: false,
|
isRateLimited: false,
|
||||||
rateLimitedAt: null,
|
rateLimitedAt: null,
|
||||||
minutesSinceRateLimit: 0,
|
minutesSinceRateLimit: 0,
|
||||||
minutesRemaining: 0
|
minutesRemaining: 0,
|
||||||
|
rateLimitEndAt: null
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to get rate limit info for account: ${accountId}`, error);
|
logger.error(`❌ Failed to get rate limit info for account: ${accountId}`, error);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
pre[data-v-2c02f1f7]{white-space:pre-wrap;word-wrap:break-word}.tab-content[data-v-61e76aa7]{min-height:calc(100vh - 300px)}.table-container[data-v-61e76aa7]{overflow-x:auto;border-radius:12px;border:1px solid rgba(0,0,0,.05)}.table-row[data-v-61e76aa7]{transition:all .2s ease}.table-row[data-v-61e76aa7]:hover{background-color:#00000005}.loading-spinner[data-v-61e76aa7]{width:24px;height:24px;border:2px solid #e5e7eb;border-top:2px solid #3b82f6;border-radius:50%;animation:spin-61e76aa7 1s linear infinite}@keyframes spin-61e76aa7{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.api-key-date-picker[data-v-61e76aa7] .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-61e76aa7] .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-61e76aa7] .el-range-separator{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}
|
|
||||||
9
web/admin-spa/dist/assets/ApiKeysView-DUk0IljI.js
vendored
Normal file
9
web/admin-spa/dist/assets/ApiKeysView-DUk0IljI.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/admin-spa/dist/assets/ApiKeysView-Dw8A7_uy.css
vendored
Normal file
1
web/admin-spa/dist/assets/ApiKeysView-Dw8A7_uy.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pre[data-v-2c02f1f7]{white-space:pre-wrap;word-wrap:break-word}.tab-content[data-v-277a81a9]{min-height:calc(100vh - 300px)}.table-container[data-v-277a81a9]{overflow-x:auto;border-radius:12px;border:1px solid rgba(0,0,0,.05)}.table-row[data-v-277a81a9]{transition:all .2s ease}.table-row[data-v-277a81a9]:hover{background-color:#00000005}.loading-spinner[data-v-277a81a9]{width:24px;height:24px;border:2px solid #e5e7eb;border-top:2px solid #3b82f6;border-radius:50%;animation:spin-277a81a9 1s linear infinite}@keyframes spin-277a81a9{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.api-key-date-picker[data-v-277a81a9] .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-277a81a9] .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-277a81a9] .el-range-separator{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
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-Ch5822Og.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",{class:"space-y-6",onSubmit:_(p,["prevent"])},[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-82195a01"]]);export{P as default};
|
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-9AMT1Op2.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",{class:"space-y-6",onSubmit:_(p,["prevent"])},[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-82195a01"]]);export{P as default};
|
||||||
@@ -1 +1 @@
|
|||||||
/* empty css */import{_ as r}from"./index-Ch5822Og.js";import{x as t,y as s,z as o,Q as d,L as a,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=l=>{l.target.style.display="none"};return(l,p)=>(s(),t("div",u,[o("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))]),o("div",x,[o("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)):a("",!0),c(l.$slots,"after-title",{},void 0,!0)]),e.subtitle?(s(),t("p",_,i(e.subtitle),1)):a("",!0)])]))}},C=r(S,[["__scopeId","data-v-718feedc"]]);export{C as L};
|
/* empty css */import{_ as r}from"./index-9AMT1Op2.js";import{x as t,y as s,z as o,Q as d,L as a,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=l=>{l.target.style.display="none"};return(l,p)=>(s(),t("div",u,[o("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))]),o("div",x,[o("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)):a("",!0),c(l.$slots,"after-title",{},void 0,!0)]),e.subtitle?(s(),t("p",_,i(e.subtitle),1)):a("",!0)])]))}},C=r(S,[["__scopeId","data-v-718feedc"]]);export{C as L};
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
|||||||
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-Ch5822Og.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(`确定要重置为默认设置吗?
|
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-9AMT1Op2.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",{class:"text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors",onClick:I},t[5]||(t[5]=[e("i",{class:"fas fa-trash mr-1"},null,-1),v("删除 ",-1)]))])):_("",!0),e("div",null,[e("input",{ref_key:"iconFileInput",ref:w,type:"file",accept:".ico,.png,.jpg,.jpeg,.svg",class:"hidden",onChange:h},null,544),e("button",{class:"btn btn-success px-4 py-2",onClick:t[1]||(t[1]=u=>n.$refs.iconFileInput.click())},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",{disabled:i(d),class:j(["btn btn-primary px-6 py-3",{"opacity-50 cursor-not-allowed":i(d)}]),onClick:f},[i(d)?(g(),m("div",X)):(g(),m("i",Y)),v(" "+S(i(d)?"保存中...":"保存设置"),1)],10,Q),e("button",{class:"btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3",disabled:i(d),onClick:b},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-d29d5f49"]]);export{le as default};
|
这将清除所有自定义的网站名称和图标设置。`))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",{class:"text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors",onClick:I},t[5]||(t[5]=[e("i",{class:"fas fa-trash mr-1"},null,-1),v("删除 ",-1)]))])):_("",!0),e("div",null,[e("input",{ref_key:"iconFileInput",ref:w,type:"file",accept:".ico,.png,.jpg,.jpeg,.svg",class:"hidden",onChange:h},null,544),e("button",{class:"btn btn-success px-4 py-2",onClick:t[1]||(t[1]=u=>n.$refs.iconFileInput.click())},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",{disabled:i(d),class:j(["btn btn-primary px-6 py-3",{"opacity-50 cursor-not-allowed":i(d)}]),onClick:f},[i(d)?(g(),m("div",X)):(g(),m("i",Y)),v(" "+S(i(d)?"保存中...":"保存设置"),1)],10,Q),e("button",{class:"btn bg-gray-100 text-gray-700 hover:bg-gray-200 px-6 py-3",disabled:i(d),onClick:b},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-d29d5f49"]]);export{le as default};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
web/admin-spa/dist/index.html
vendored
4
web/admin-spa/dist/index.html
vendored
@@ -18,12 +18,12 @@
|
|||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
|
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
|
||||||
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
||||||
<script type="module" crossorigin src="/admin-next/assets/index-Ch5822Og.js"></script>
|
<script type="module" crossorigin src="/admin-next/assets/index-9AMT1Op2.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/vue-vendor-CKToUHZx.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/vue-vendor-CKToUHZx.js">
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/vendor-BDiMbLwQ.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/vendor-BDiMbLwQ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/element-plus-B8Fs_0jW.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/element-plus-B8Fs_0jW.js">
|
||||||
<link rel="stylesheet" crossorigin href="/admin-next/assets/element-plus-CPnoEkWW.css">
|
<link rel="stylesheet" crossorigin href="/admin-next/assets/element-plus-CPnoEkWW.css">
|
||||||
<link rel="stylesheet" crossorigin href="/admin-next/assets/index-CANUYAyV.css">
|
<link rel="stylesheet" crossorigin href="/admin-next/assets/index-V6aqLFfH.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<!-- Token统计时间范围选择 -->
|
<!-- Token统计时间范围选择 -->
|
||||||
<select
|
<select
|
||||||
v-model="apiKeyStatsTimeRange"
|
v-model="apiKeyStatsTimeRange"
|
||||||
class="form-input px-3 py-2 text-sm"
|
class="px-2 py-1 text-sm text-gray-700 bg-white border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent hover:border-gray-300 transition-colors"
|
||||||
@change="loadApiKeys()"
|
@change="loadApiKeys()"
|
||||||
>
|
>
|
||||||
<option value="today">
|
<option value="today">
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
<!-- 标签筛选器 -->
|
<!-- 标签筛选器 -->
|
||||||
<select
|
<select
|
||||||
v-model="selectedTagFilter"
|
v-model="selectedTagFilter"
|
||||||
class="form-input px-3 py-2 text-sm"
|
class="px-2 py-1 text-sm text-gray-700 bg-white border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent hover:border-gray-300 transition-colors"
|
||||||
|
@change="currentPage = 1"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
所有标签
|
所有标签
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary px-6 py-3 flex items-center gap-2"
|
class="btn btn-primary px-4 py-1.5 text-sm flex items-center gap-2"
|
||||||
@click.stop="openCreateApiKeyModal"
|
@click.stop="openCreateApiKeyModal"
|
||||||
>
|
>
|
||||||
<i class="fas fa-plus" />创建新 Key
|
<i class="fas fa-plus" />创建新 Key
|
||||||
@@ -460,10 +461,10 @@
|
|||||||
:default-time="defaultTime"
|
:default-time="defaultTime"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 280px;"
|
style="width: 280px;"
|
||||||
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
|
|
||||||
class="api-key-date-picker"
|
class="api-key-date-picker"
|
||||||
:clearable="true"
|
:clearable="true"
|
||||||
:unlink-panels="false"
|
:unlink-panels="false"
|
||||||
|
@update:model-value="(value) => onApiKeyCustomDateRangeChange(key.id, value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,6 +604,99 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<div
|
||||||
|
v-if="filteredAndSortedApiKeys.length > 0"
|
||||||
|
class="mt-6 flex flex-col sm:flex-row justify-between items-center gap-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-sm text-gray-600">
|
||||||
|
共 {{ filteredAndSortedApiKeys.length }} 条记录
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-600">每页显示</span>
|
||||||
|
<select
|
||||||
|
v-model="pageSize"
|
||||||
|
class="px-2 py-1 text-sm text-gray-700 bg-white border border-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent hover:border-gray-300 transition-colors"
|
||||||
|
@change="currentPage = 1"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="size in pageSizeOptions"
|
||||||
|
:key="size"
|
||||||
|
:value="size"
|
||||||
|
>
|
||||||
|
{{ size }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span class="text-sm text-gray-600">条</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 上一页 -->
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
@click="currentPage--"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-left" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 页码 -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<!-- 第一页 -->
|
||||||
|
<button
|
||||||
|
v-if="currentPage > 3"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||||
|
@click="currentPage = 1"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-if="currentPage > 4"
|
||||||
|
class="px-2 text-gray-500"
|
||||||
|
>...</span>
|
||||||
|
|
||||||
|
<!-- 中间页码 -->
|
||||||
|
<button
|
||||||
|
v-for="page in pageNumbers"
|
||||||
|
:key="page"
|
||||||
|
:class="[
|
||||||
|
'px-3 py-1 text-sm font-medium rounded-md',
|
||||||
|
page === currentPage
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50'
|
||||||
|
]"
|
||||||
|
@click="currentPage = page"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 最后一页 -->
|
||||||
|
<span
|
||||||
|
v-if="currentPage < totalPages - 3"
|
||||||
|
class="px-2 text-gray-500"
|
||||||
|
>...</span>
|
||||||
|
<button
|
||||||
|
v-if="totalPages > 1 && currentPage < totalPages - 2"
|
||||||
|
class="px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||||
|
@click="currentPage = totalPages"
|
||||||
|
>
|
||||||
|
{{ totalPages }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下一页 -->
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:disabled="currentPage === totalPages || totalPages === 0"
|
||||||
|
@click="currentPage++"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chevron-right" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模态框组件 -->
|
<!-- 模态框组件 -->
|
||||||
@@ -659,6 +753,11 @@ const apiKeyDateFilters = ref({})
|
|||||||
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)])
|
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)])
|
||||||
const accounts = ref({ claude: [], gemini: [] })
|
const accounts = ref({ claude: [], gemini: [] })
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const pageSizeOptions = [5, 10, 20, 50, 100]
|
||||||
|
|
||||||
// 标签相关
|
// 标签相关
|
||||||
const selectedTagFilter = ref('')
|
const selectedTagFilter = ref('')
|
||||||
const availableTags = ref([])
|
const availableTags = ref([])
|
||||||
@@ -672,8 +771,8 @@ const editingApiKey = ref(null)
|
|||||||
const renewingApiKey = ref(null)
|
const renewingApiKey = ref(null)
|
||||||
const newApiKeyData = ref(null)
|
const newApiKeyData = ref(null)
|
||||||
|
|
||||||
// 计算排序后的API Keys
|
// 计算筛选和排序后的API Keys(未分页)
|
||||||
const sortedApiKeys = computed(() => {
|
const filteredAndSortedApiKeys = computed(() => {
|
||||||
// 先进行标签筛选
|
// 先进行标签筛选
|
||||||
let filteredKeys = apiKeys.value
|
let filteredKeys = apiKeys.value
|
||||||
if (selectedTagFilter.value) {
|
if (selectedTagFilter.value) {
|
||||||
@@ -710,6 +809,37 @@ const sortedApiKeys = computed(() => {
|
|||||||
return sorted
|
return sorted
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
return Math.ceil(filteredAndSortedApiKeys.value.length / pageSize.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算当前页显示的API Keys
|
||||||
|
const sortedApiKeys = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return filteredAndSortedApiKeys.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算要显示的页码
|
||||||
|
const pageNumbers = computed(() => {
|
||||||
|
const pages = []
|
||||||
|
const maxPagesToShow = 5
|
||||||
|
let startPage = Math.max(1, currentPage.value - Math.floor(maxPagesToShow / 2))
|
||||||
|
let endPage = Math.min(totalPages.value, startPage + maxPagesToShow - 1)
|
||||||
|
|
||||||
|
// 调整起始页
|
||||||
|
if (endPage - startPage < maxPagesToShow - 1) {
|
||||||
|
startPage = Math.max(1, endPage - maxPagesToShow + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
})
|
||||||
|
|
||||||
// 加载账户列表
|
// 加载账户列表
|
||||||
const loadAccounts = async () => {
|
const loadAccounts = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -771,6 +901,9 @@ const loadApiKeys = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
availableTags.value = Array.from(tagsSet).sort()
|
availableTags.value = Array.from(tagsSet).sort()
|
||||||
|
|
||||||
|
// 重置到第一页
|
||||||
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('加载 API Keys 失败', 'error')
|
showToast('加载 API Keys 失败', 'error')
|
||||||
@@ -787,6 +920,8 @@ const sortApiKeys = (field) => {
|
|||||||
apiKeysSortBy.value = field
|
apiKeysSortBy.value = field
|
||||||
apiKeysSortOrder.value = 'asc'
|
apiKeysSortOrder.value = 'asc'
|
||||||
}
|
}
|
||||||
|
// 排序时重置到第一页
|
||||||
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化数字
|
// 格式化数字
|
||||||
@@ -1093,7 +1228,7 @@ const deleteApiKey = async (keyId) => {
|
|||||||
const copyApiStatsLink = (apiKey) => {
|
const copyApiStatsLink = (apiKey) => {
|
||||||
// 构建统计页面的完整URL
|
// 构建统计页面的完整URL
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
const statsUrl = `${baseUrl}/admin/api-stats?apiId=${apiKey.id}`
|
const statsUrl = `${baseUrl}/admin-next/api-stats?apiId=${apiKey.id}`
|
||||||
|
|
||||||
// 使用传统的textarea方法复制到剪贴板
|
// 使用传统的textarea方法复制到剪贴板
|
||||||
const textarea = document.createElement('textarea')
|
const textarea = document.createElement('textarea')
|
||||||
@@ -1173,4 +1308,5 @@ onMounted(async () => {
|
|||||||
.api-key-date-picker :deep(.el-range-separator) {
|
.api-key-date-picker :deep(.el-range-separator) {
|
||||||
@apply text-gray-500;
|
@apply text-gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user