feat(usage): 改进用量显示并添加按提供商刷新功能
- 添加 formatToLocal 工具函数统一处理日期本地化显示 - 用量卡片添加重置时间显示和双击刷新功能 - 优化 Gemini/Antigravity/Codex 的用量数据格式化逻辑 - 改进 Codex 用量显示,优先展示周限制信息 - 添加相关国际化文本支持
This commit is contained in:
parent
4406b7d881
commit
3b0c2180d2
8 changed files with 265 additions and 178 deletions
|
|
@ -1468,29 +1468,32 @@ export class AntigravityApiService {
|
|||
};
|
||||
|
||||
const res = await this.authClient.request(requestOptions);
|
||||
logger.info(`[Antigravity] fetchAvailableModels success`);
|
||||
if (res.data && res.data.models) {
|
||||
const modelsData = res.data.models;
|
||||
|
||||
// 遍历模型数据,提取配额信息
|
||||
for (const [modelId, modelData] of Object.entries(modelsData)) {
|
||||
const aliasName = modelName2Alias(modelId);
|
||||
if (aliasName == null || aliasName === '') continue; // 跳过不支持的模型
|
||||
// logger.info(`[Antigravity] fetchAvailableModels success: ${JSON.stringify(res.data)}`);
|
||||
if (res.data) {
|
||||
|
||||
if (res.data.models) {
|
||||
const modelsData = res.data.models;
|
||||
|
||||
const modelInfo = {
|
||||
remaining: 0,
|
||||
resetTime: null,
|
||||
resetTimeRaw: null
|
||||
};
|
||||
|
||||
// 从 quotaInfo 中提取配额信息
|
||||
if (modelData.quotaInfo) {
|
||||
modelInfo.remaining = modelData.quotaInfo.remainingFraction || modelData.quotaInfo.remaining || 0;
|
||||
modelInfo.resetTime = modelData.quotaInfo.resetTime || null;
|
||||
modelInfo.resetTimeRaw = modelData.quotaInfo.resetTime;
|
||||
// 遍历模型数据,提取配额信息
|
||||
for (const [modelId, modelData] of Object.entries(modelsData)) {
|
||||
const aliasName = modelName2Alias(modelId);
|
||||
if (aliasName == null || aliasName === '') continue; // 跳过不支持的模型
|
||||
|
||||
const modelInfo = {
|
||||
remaining: 0,
|
||||
resetTime: null,
|
||||
resetTimeRaw: null
|
||||
};
|
||||
|
||||
// 从 quotaInfo 中提取配额信息
|
||||
if (modelData.quotaInfo) {
|
||||
modelInfo.remaining = modelData.quotaInfo.remainingFraction !== undefined ? modelData.quotaInfo.remainingFraction : (modelData.quotaInfo.remaining || 0);
|
||||
modelInfo.resetTime = modelData.quotaInfo.resetTime || null;
|
||||
modelInfo.resetTimeRaw = modelData.quotaInfo.resetTime;
|
||||
}
|
||||
|
||||
result.models[aliasName] = modelInfo;
|
||||
}
|
||||
|
||||
result.models[aliasName] = modelInfo;
|
||||
}
|
||||
|
||||
// 对模型按名称排序
|
||||
|
|
|
|||
|
|
@ -542,7 +542,7 @@ export class CodexApiService {
|
|||
if (primaryWindow) {
|
||||
// remaining = 1 - (used_percent / 100)
|
||||
const remaining = 1 - (primaryWindow.used_percent || 0) / 100;
|
||||
const resetTime = primaryWindow.reset_at ? new Date(primaryWindow.reset_at * 1000).toISOString() : null;
|
||||
const resetTime = primaryWindow.reset_at ? new Date(primaryWindow.reset_at * 1000).toDateString() : null;
|
||||
|
||||
// 为所有 Codex 模型设置相同的配额信息
|
||||
const codexModels = ['default'];
|
||||
|
|
|
|||
|
|
@ -359,9 +359,9 @@ export class ProviderPoolManager {
|
|||
async _refreshNodeToken(providerType, providerStatus, force = false) {
|
||||
const config = providerStatus.config;
|
||||
|
||||
// 检查刷新次数是否已达上限(最大3次)
|
||||
// 检查刷新次数是否已达上限(最大5次)
|
||||
const currentRefreshCount = config.refreshCount || 0;
|
||||
if (currentRefreshCount >= 3 && !force) {
|
||||
if (currentRefreshCount >= 5 && !force) {
|
||||
this._log('warn', `Node ${providerStatus.uuid} has reached maximum refresh count (3), marking as unhealthy`);
|
||||
// 标记为不健康
|
||||
this.markProviderUnhealthyImmediately(providerType, config, 'Maximum refresh count (3) reached');
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { getProviderPoolManager } from './service-manager.js';
|
||||
import { serviceInstances } from '../providers/adapter.js';
|
||||
import { MODEL_PROVIDER } from '../utils/common.js';
|
||||
import { MODEL_PROVIDER, formatToLocal } from '../utils/common.js';
|
||||
|
||||
/**
|
||||
* 用量查询服务类
|
||||
|
|
@ -314,31 +314,6 @@ export function formatGeminiUsage(usageData) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset
|
||||
|
||||
/**
|
||||
* 将 UTC 时间转换为北京时间
|
||||
* @param {string} utcString - UTC 时间字符串
|
||||
* @returns {string} 北京时间字符串
|
||||
*/
|
||||
function utcToBeijing(utcString) {
|
||||
try {
|
||||
if (!utcString) return '--';
|
||||
const utcDate = new Date(utcString);
|
||||
const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET);
|
||||
return beijingTime
|
||||
.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-');
|
||||
} catch (e) {
|
||||
return '--';
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
// 基本信息 - 映射到 Kiro 结构
|
||||
daysUntilReset: null,
|
||||
|
|
@ -377,16 +352,20 @@ export function formatGeminiUsage(usageData) {
|
|||
|
||||
// 解析模型配额信息
|
||||
if (usageData.models && typeof usageData.models === 'object') {
|
||||
for (const [modelName, modelInfo] of Object.entries(usageData.models)) {
|
||||
// Gemini 返回的数据结构:{ remaining, resetTime, resetTimeRaw }
|
||||
for (const [modelKey, modelInfo] of Object.entries(usageData.models)) {
|
||||
// Gemini 返回的数据结构:{ remaining, resetTime, resetTimeRaw, tokenType }
|
||||
// remaining 是 0-1 之间的比例值,表示剩余配额百分比
|
||||
const remainingPercent = typeof modelInfo.remaining === 'number' ? modelInfo.remaining : 1;
|
||||
const usedPercent = 1 - remainingPercent;
|
||||
|
||||
// 解析 modelKey (modelId:tokenType)
|
||||
const [modelId, tokenType] = modelKey.split(':');
|
||||
const displayName = tokenType ? `${modelId} (${tokenType})` : modelId;
|
||||
|
||||
const item = {
|
||||
resourceType: 'MODEL_USAGE',
|
||||
displayName: modelInfo.displayName || modelName,
|
||||
displayNamePlural: modelInfo.displayName || modelName,
|
||||
displayName: displayName,
|
||||
displayNamePlural: displayName,
|
||||
unit: 'quota',
|
||||
currency: null,
|
||||
|
||||
|
|
@ -411,13 +390,14 @@ export function formatGeminiUsage(usageData) {
|
|||
bonuses: [],
|
||||
|
||||
// 额外的 Gemini 特有信息
|
||||
modelName: modelName,
|
||||
modelName: modelId,
|
||||
tokenType: tokenType,
|
||||
inputTokenLimit: modelInfo.inputTokenLimit || 0,
|
||||
outputTokenLimit: modelInfo.outputTokenLimit || 0,
|
||||
remaining: remainingPercent,
|
||||
remainingPercent: Math.round(remainingPercent * 100), // 剩余百分比
|
||||
resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ?
|
||||
utcToBeijing(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--',
|
||||
formatToLocal(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--',
|
||||
resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null
|
||||
};
|
||||
|
||||
|
|
@ -438,31 +418,6 @@ export function formatAntigravityUsage(usageData) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset
|
||||
|
||||
/**
|
||||
* 将 UTC 时间转换为北京时间
|
||||
* @param {string} utcString - UTC 时间字符串
|
||||
* @returns {string} 北京时间字符串
|
||||
*/
|
||||
function utcToBeijing(utcString) {
|
||||
try {
|
||||
if (!utcString) return '--';
|
||||
const utcDate = new Date(utcString);
|
||||
const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET);
|
||||
return beijingTime
|
||||
.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-');
|
||||
} catch (e) {
|
||||
return '--';
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
// 基本信息 - 映射到 Kiro 结构
|
||||
daysUntilReset: null,
|
||||
|
|
@ -507,6 +462,10 @@ export function formatAntigravityUsage(usageData) {
|
|||
const remainingPercent = typeof modelInfo.remaining === 'number' ? modelInfo.remaining : 1;
|
||||
const usedPercent = 1 - remainingPercent;
|
||||
|
||||
// 优先使用模型自己的重置时间,如果没有则使用全局重置时间
|
||||
const resetTimeRaw = modelInfo.resetTimeRaw || (usageData.quotaInfo ? usageData.quotaInfo.quotaResetTime : null);
|
||||
const resetTimeFormatted = resetTimeRaw ? formatToLocal(resetTimeRaw) : (modelInfo.resetTime || '--');
|
||||
|
||||
const item = {
|
||||
resourceType: 'MODEL_USAGE',
|
||||
displayName: modelInfo.displayName || modelName,
|
||||
|
|
@ -515,7 +474,7 @@ export function formatAntigravityUsage(usageData) {
|
|||
currency: null,
|
||||
|
||||
// 当前用量 - Antigravity 返回的是剩余比例,转换为已用比例(百分比形式)
|
||||
currentUsage: usedPercent * 100,
|
||||
currentUsage: Math.round(usedPercent * 100 * 100) / 100,
|
||||
usageLimit: 100, // 以百分比表示,总量为 100%
|
||||
|
||||
// 超额信息
|
||||
|
|
@ -525,8 +484,7 @@ export function formatAntigravityUsage(usageData) {
|
|||
overageCharges: 0,
|
||||
|
||||
// 下次重置时间
|
||||
nextDateReset: modelInfo.resetTimeRaw ? new Date(modelInfo.resetTimeRaw).toISOString() :
|
||||
(modelInfo.resetTime ? new Date(modelInfo.resetTime).toISOString() : null),
|
||||
nextDateReset: resetTimeRaw ? (typeof resetTimeRaw === 'number' ? new Date(resetTimeRaw * 1000).toISOString() : new Date(resetTimeRaw).toISOString()) : null,
|
||||
|
||||
// 免费试用信息
|
||||
freeTrial: null,
|
||||
|
|
@ -539,10 +497,9 @@ export function formatAntigravityUsage(usageData) {
|
|||
inputTokenLimit: modelInfo.inputTokenLimit || 0,
|
||||
outputTokenLimit: modelInfo.outputTokenLimit || 0,
|
||||
remaining: remainingPercent,
|
||||
remainingPercent: remainingPercent * 100, // 剩余百分比
|
||||
resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ?
|
||||
utcToBeijing(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--',
|
||||
resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null
|
||||
remainingPercent: Math.round(remainingPercent * 100 * 100) / 100, // 剩余百分比
|
||||
resetTime: resetTimeFormatted,
|
||||
resetTimeRaw: resetTimeRaw
|
||||
};
|
||||
|
||||
result.usageBreakdown.push(item);
|
||||
|
|
@ -562,31 +519,6 @@ export function formatCodexUsage(usageData) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const TZ_OFFSET = 8 * 60 * 60 * 1000; // Beijing timezone offset
|
||||
|
||||
/**
|
||||
* 将 UTC 时间转换为北京时间
|
||||
* @param {string} utcString - UTC 时间字符串
|
||||
* @returns {string} 北京时间字符串
|
||||
*/
|
||||
function utcToBeijing(utcString) {
|
||||
try {
|
||||
if (!utcString) return '--';
|
||||
const utcDate = new Date(utcString);
|
||||
const beijingTime = new Date(utcDate.getTime() + TZ_OFFSET);
|
||||
return beijingTime
|
||||
.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
.replace(/\//g, '-');
|
||||
} catch (e) {
|
||||
return '--';
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
// 基本信息 - 映射到 Kiro 结构
|
||||
daysUntilReset: null,
|
||||
|
|
@ -661,7 +593,7 @@ export function formatCodexUsage(usageData) {
|
|||
remaining: remainingPercent,
|
||||
remainingPercent: Math.round(remainingPercent * 100), // 剩余百分比
|
||||
resetTime: (modelInfo.resetTimeRaw || modelInfo.resetTime) ?
|
||||
utcToBeijing(modelInfo.resetTimeRaw ? new Date(modelInfo.resetTimeRaw * 1000).toISOString() : modelInfo.resetTime) : '--',
|
||||
formatToLocal(modelInfo.resetTimeRaw || modelInfo.resetTime) : '--',
|
||||
resetTimeRaw: modelInfo.resetTimeRaw || modelInfo.resetTime || null,
|
||||
|
||||
// 注入 raw 窗口信息以便前端使用
|
||||
|
|
|
|||
|
|
@ -1155,6 +1155,34 @@ export function getMD5Hash(obj) {
|
|||
return crypto.createHash('md5').update(jsonString).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将日期转换为系统本地时间格式
|
||||
* @param {string|number} dateInput - 日期字符串或时间戳
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
export function formatToLocal(dateInput) {
|
||||
try {
|
||||
if (!dateInput) return '--';
|
||||
// 处理数值型时间戳(秒 -> 毫秒)
|
||||
let finalInput = dateInput;
|
||||
if (typeof dateInput === 'number' && dateInput < 10000000000) {
|
||||
finalInput = dateInput * 1000;
|
||||
}
|
||||
const date = new Date(finalInput);
|
||||
if (isNaN(date.getTime())) return '--';
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}).replace(/\//g, '-');
|
||||
} catch (e) {
|
||||
return '--';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建符合 fromProvider 格式的错误响应(非流式)
|
||||
|
|
|
|||
|
|
@ -528,11 +528,22 @@ const translations = {
|
|||
'usage.card.status.healthy': '健康',
|
||||
'usage.card.status.unhealthy': '异常',
|
||||
'usage.card.totalUsage': '总用量',
|
||||
'usage.card.resetAt': '将在 {time} 重置',
|
||||
'usage.card.freeTrial': '免费试用',
|
||||
'usage.card.bonus': '奖励',
|
||||
'usage.card.expires': '到期: {time}',
|
||||
'usage.doubleClickToRefresh': '双击刷新该提供商用量',
|
||||
'usage.refreshingProvider': '正在刷新 {name} 用量...',
|
||||
'usage.group.expandAll': '展开所有卡片',
|
||||
'usage.group.collapseAll': '折叠所有卡片',
|
||||
'usage.failedToLoad': '加载失败',
|
||||
'usage.loadFailed': '获取支持的提供商列表失败',
|
||||
'usage.resetInfo': '将在 {time} 后重置',
|
||||
'usage.weeklyLimit': '每周限制',
|
||||
'usage.time.days': '{days}天{hours}小时',
|
||||
'usage.time.hours': '{hours}小时{minutes}分',
|
||||
'usage.time.minutes': '{minutes}分钟',
|
||||
'usage.time.soon': '即将',
|
||||
|
||||
// Logs
|
||||
'logs.title': '实时日志',
|
||||
|
|
@ -1287,11 +1298,22 @@ const translations = {
|
|||
'usage.card.status.healthy': 'Healthy',
|
||||
'usage.card.status.unhealthy': 'Abnormal',
|
||||
'usage.card.totalUsage': 'Total Usage',
|
||||
'usage.card.resetAt': 'Resets at {time}',
|
||||
'usage.card.freeTrial': 'Free Trial',
|
||||
'usage.card.bonus': 'Bonus',
|
||||
'usage.card.expires': 'Expires: {time}',
|
||||
'usage.doubleClickToRefresh': 'Double click to refresh this provider',
|
||||
'usage.refreshingProvider': 'Refreshing {name} usage...',
|
||||
'usage.group.expandAll': 'Expand All Cards',
|
||||
'usage.group.collapseAll': 'Collapse All Cards',
|
||||
'usage.failedToLoad': 'Failed to load',
|
||||
'usage.loadFailed': 'Failed to load supported providers',
|
||||
'usage.resetInfo': 'Resets in {time}',
|
||||
'usage.weeklyLimit': 'Weekly Limit',
|
||||
'usage.time.days': '{days}d {hours}h',
|
||||
'usage.time.hours': '{hours}h {minutes}m',
|
||||
'usage.time.minutes': '{minutes}m',
|
||||
'usage.time.soon': 'Soon',
|
||||
|
||||
// Logs
|
||||
'logs.title': 'Real-time Logs',
|
||||
|
|
|
|||
|
|
@ -42,11 +42,19 @@ async function loadSupportedProviders() {
|
|||
const tag = document.createElement('span');
|
||||
tag.className = 'provider-tag';
|
||||
tag.textContent = getProviderDisplayName(provider);
|
||||
tag.title = t('usage.doubleClickToRefresh') || '双击刷新该提供商用量';
|
||||
tag.setAttribute('data-i18n-title', 'usage.doubleClickToRefresh');
|
||||
|
||||
// 添加双击事件
|
||||
tag.addEventListener('dblclick', () => {
|
||||
refreshProviderUsage(provider);
|
||||
});
|
||||
|
||||
listEl.appendChild(tag);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取支持的提供商列表失败:', error);
|
||||
listEl.innerHTML = '<span class="error-text">Failed to load</span>';
|
||||
listEl.innerHTML = `<span class="error-text" data-i18n="usage.failedToLoad">${t('usage.failedToLoad')}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +113,7 @@ export async function loadUsage() {
|
|||
errorEl.style.display = 'block';
|
||||
const errorMsgEl = document.getElementById('usageErrorMessage');
|
||||
if (errorMsgEl) {
|
||||
errorMsgEl.textContent = error.message || t('usage.title') + '失败';
|
||||
errorMsgEl.textContent = error.message || (t('usage.title') + t('common.refresh.failed'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,7 +172,7 @@ export async function refreshUsage() {
|
|||
errorEl.style.display = 'block';
|
||||
const errorMsgEl = document.getElementById('usageErrorMessage');
|
||||
if (errorMsgEl) {
|
||||
errorMsgEl.textContent = error.message || t('usage.title') + '失败';
|
||||
errorMsgEl.textContent = error.message || (t('usage.title') + t('common.refresh.failed'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +211,7 @@ function renderUsageData(data, container) {
|
|||
const validInstances = [];
|
||||
for (const instance of providerData.instances) {
|
||||
// 过滤掉服务实例未初始化的
|
||||
if (instance.error === '服务实例未初始化') {
|
||||
if (instance.error === '服务实例未初始化' || instance.error === 'Service instance not initialized') {
|
||||
continue;
|
||||
}
|
||||
// 过滤掉已禁用的提供商
|
||||
|
|
@ -235,6 +243,51 @@ function renderUsageData(data, container) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新特定提供商类型的用量数据
|
||||
* @param {string} providerType - 提供商类型
|
||||
*/
|
||||
export async function refreshProviderUsage(providerType) {
|
||||
const loadingEl = document.getElementById('usageLoading');
|
||||
const refreshBtn = document.getElementById('refreshUsageBtn');
|
||||
const contentEl = document.getElementById('usageContent');
|
||||
|
||||
// 显示加载状态
|
||||
if (loadingEl) loadingEl.style.display = 'block';
|
||||
if (refreshBtn) refreshBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const providerName = getProviderDisplayName(providerType);
|
||||
showToast(t('common.info'), t('usage.refreshingProvider', { name: providerName }), 'info');
|
||||
|
||||
// 调用按提供商刷新的 API
|
||||
const response = await fetch(`/api/usage/${providerType}?refresh=true`, {
|
||||
method: 'GET',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const providerData = await response.json();
|
||||
|
||||
// 获取当前完整数据并更新其中一个提供商的数据
|
||||
// 注意:这里为了保持页面一致性,我们重新获取一次完整数据(走缓存)来重新渲染
|
||||
// 或者手动在当前 DOM 中更新该提供商的部分。
|
||||
// 为了简单可靠,我们重新 loadUsage(),它会读取刚刚更新过的后端缓存
|
||||
await loadUsage();
|
||||
|
||||
showToast(t('common.success'), t('common.refresh.success'), 'success');
|
||||
} catch (error) {
|
||||
console.error(`刷新提供商 ${providerType} 失败:`, error);
|
||||
showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
|
||||
} finally {
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
if (refreshBtn) refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建提供商分组容器
|
||||
* @param {string} providerType - 提供商类型
|
||||
|
|
@ -349,6 +402,10 @@ function createInstanceUsageCard(instance, providerType) {
|
|||
|
||||
// 显示名称:优先自定义名称,其次 uuid
|
||||
const displayName = instance.name || instance.uuid;
|
||||
|
||||
const displayUsageText = totalUsage.isCodex
|
||||
? `${totalUsage.percent.toFixed(1)}%`
|
||||
: `${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}`;
|
||||
|
||||
collapsedSummary.innerHTML = `
|
||||
<div class="collapsed-summary-row collapsed-summary-name-row">
|
||||
|
|
@ -362,8 +419,8 @@ function createInstanceUsageCard(instance, providerType) {
|
|||
<div class="progress-fill" style="width: ${totalUsage.percent}%"></div>
|
||||
</div>
|
||||
<span class="collapsed-percent">${totalUsage.percent.toFixed(1)}%</span>
|
||||
<span class="collapsed-usage-text">${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}</span>
|
||||
` : (instance.error ? `<span class="collapsed-error">${t('common.error')}</span>` : '')}
|
||||
<span class="collapsed-usage-text">${displayUsageText}</span>
|
||||
` : (instance.error ? `<span class="collapsed-error" data-i18n="common.error">${t('common.error')}</span>` : '')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -459,18 +516,43 @@ function renderUsageDetails(usage) {
|
|||
|
||||
const progressClass = totalUsage.percent >= 90 ? 'danger' : (totalUsage.percent >= 70 ? 'warning' : 'normal');
|
||||
|
||||
// 提取第一个有重置时间的条目(通常是总配额)
|
||||
let resetTimeHTML = '';
|
||||
if (totalUsage.isCodex && totalUsage.resetAfterSeconds !== undefined) {
|
||||
const resetTimeText = formatTimeRemaining(totalUsage.resetAfterSeconds);
|
||||
resetTimeHTML = `
|
||||
<div class="total-reset-info" data-i18n="usage.resetInfo" data-i18n-params='{"time":"${resetTimeText}"}'>
|
||||
<i class="fas fa-history"></i> ${t('usage.resetInfo', { time: resetTimeText })}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
const resetTimeEntry = usage.usageBreakdown.find(b => b.resetTime && b.resetTime !== '--');
|
||||
resetTimeHTML = resetTimeEntry ? `
|
||||
<div class="total-reset-info" data-i18n="usage.card.resetAt" data-i18n-params='{"time":"${resetTimeEntry.resetTime}"}'>
|
||||
<i class="fas fa-history"></i> ${t('usage.card.resetAt', { time: resetTimeEntry.resetTime })}
|
||||
</div>
|
||||
` : '';
|
||||
}
|
||||
|
||||
const displayValue = totalUsage.isCodex
|
||||
? `${totalUsage.percent.toFixed(1)}%`
|
||||
: `${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}`;
|
||||
|
||||
totalSection.innerHTML = `
|
||||
<div class="total-usage-header">
|
||||
<span class="total-label">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
<span data-i18n="usage.card.totalUsage">${t('usage.card.totalUsage')}</span>
|
||||
</span>
|
||||
<span class="total-value">${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}</span>
|
||||
<span class="total-value">${displayValue}</span>
|
||||
</div>
|
||||
<div class="progress-bar ${progressClass}">
|
||||
<div class="progress-fill" style="width: ${totalUsage.percent}%"></div>
|
||||
</div>
|
||||
<div class="total-percent">${totalUsage.percent.toFixed(2)}%</div>
|
||||
<div class="total-footer">
|
||||
<div class="total-percent">${totalUsage.percent.toFixed(2)}%</div>
|
||||
${resetTimeHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(totalSection);
|
||||
|
|
@ -522,6 +604,19 @@ function createUsageBreakdownHTML(breakdown) {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// 如果有重置时间,则显示
|
||||
if (breakdown.resetTime && breakdown.resetTime !== '--') {
|
||||
const resetText = t('usage.card.resetAt', { time: breakdown.resetTime });
|
||||
html += `
|
||||
<div class="extra-usage-info reset-time">
|
||||
<span class="extra-label">
|
||||
<i class="fas fa-history"></i>
|
||||
<span data-i18n="usage.card.resetAt" data-i18n-params='${JSON.stringify({ time: breakdown.resetTime })}'>${resetText}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 免费试用信息
|
||||
if (breakdown.freeTrial && breakdown.freeTrial.status === 'ACTIVE') {
|
||||
html += `
|
||||
|
|
@ -559,52 +654,28 @@ function createUsageBreakdownHTML(breakdown) {
|
|||
*/
|
||||
function createCodexUsageBreakdownHTML(breakdown) {
|
||||
const rl = breakdown.rateLimit;
|
||||
const primary = rl.primary_window;
|
||||
const secondary = rl.secondary_window;
|
||||
|
||||
const primaryPercent = primary.used_percent || 0;
|
||||
const primaryProgressClass = primaryPercent >= 90 ? 'danger' : (primaryPercent >= 70 ? 'warning' : 'normal');
|
||||
|
||||
const primaryLimitHours = Math.round(primary.limit_window_seconds / 3600);
|
||||
const primaryResetText = formatTimeRemaining(primary.reset_after_seconds);
|
||||
if (!secondary) return '';
|
||||
|
||||
let html = `
|
||||
const secondaryPercent = secondary.used_percent || 0;
|
||||
const secondaryProgressClass = secondaryPercent >= 90 ? 'danger' : (secondaryPercent >= 70 ? 'warning' : 'normal');
|
||||
const secondaryResetText = formatTimeRemaining(secondary.reset_after_seconds);
|
||||
|
||||
return `
|
||||
<div class="breakdown-item-compact codex-usage-item">
|
||||
<div class="breakdown-header-compact">
|
||||
<span class="breakdown-name"><i class="fas fa-clock"></i> ${primaryLimitHours} 小时限制</span>
|
||||
<span class="breakdown-usage">${primaryPercent}%</span>
|
||||
<span class="breakdown-name" data-i18n="usage.weeklyLimit"><i class="fas fa-calendar-alt"></i> ${t('usage.weeklyLimit')}</span>
|
||||
<span class="breakdown-usage">${secondaryPercent}%</span>
|
||||
</div>
|
||||
<div class="progress-bar-small ${primaryProgressClass}">
|
||||
<div class="progress-fill" style="width: ${primaryPercent}%"></div>
|
||||
<div class="progress-bar-small ${secondaryProgressClass}">
|
||||
<div class="progress-fill" style="width: ${secondaryPercent}%"></div>
|
||||
</div>
|
||||
<div class="codex-reset-info">
|
||||
<i class="fas fa-history"></i> 将在 ${primaryResetText} 后重置
|
||||
<div class="codex-reset-info" data-i18n="usage.resetInfo" data-i18n-params='{"time":"${secondaryResetText}"}'>
|
||||
<i class="fas fa-history"></i> ${t('usage.resetInfo', { time: secondaryResetText })}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (secondary) {
|
||||
const secondaryPercent = secondary.used_percent || 0;
|
||||
const secondaryProgressClass = secondaryPercent >= 90 ? 'danger' : (secondaryPercent >= 70 ? 'warning' : 'normal');
|
||||
const secondaryResetText = formatTimeRemaining(secondary.reset_after_seconds);
|
||||
|
||||
html += `
|
||||
<div class="codex-secondary-usage">
|
||||
<div class="breakdown-header-compact">
|
||||
<span class="breakdown-name"><i class="fas fa-calendar-alt"></i> 每周限制</span>
|
||||
<span class="breakdown-usage">${secondaryPercent}%</span>
|
||||
</div>
|
||||
<div class="progress-bar-small ${secondaryProgressClass}">
|
||||
<div class="progress-fill" style="width: ${secondaryPercent}%"></div>
|
||||
</div>
|
||||
<div class="codex-reset-info">
|
||||
<i class="fas fa-history"></i> 将在 ${secondaryResetText} 后重置
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -613,15 +684,15 @@ function createCodexUsageBreakdownHTML(breakdown) {
|
|||
* @returns {string} 格式化后的时间
|
||||
*/
|
||||
function formatTimeRemaining(seconds) {
|
||||
if (seconds <= 0) return '即将';
|
||||
if (seconds <= 0) return t('usage.time.soon');
|
||||
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) return `${days}天${hours}小时`;
|
||||
if (hours > 0) return `${hours}小时${minutes}分`;
|
||||
return `${minutes}分钟`;
|
||||
if (days > 0) return t('usage.time.days', { days, hours });
|
||||
if (hours > 0) return t('usage.time.hours', { hours, minutes });
|
||||
return t('usage.time.minutes', { minutes });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -634,6 +705,30 @@ function calculateTotalUsage(usageBreakdown) {
|
|||
return { hasData: false, used: 0, limit: 0, percent: 0 };
|
||||
}
|
||||
|
||||
// 特殊处理 Codex
|
||||
const codexEntry = usageBreakdown.find(b => b.rateLimit && b.rateLimit.secondary_window);
|
||||
if (codexEntry) {
|
||||
const secondary = codexEntry.rateLimit.secondary_window;
|
||||
const secondaryPercent = secondary.used_percent || 0;
|
||||
|
||||
// 只有当周限制达到 100% 时,总用量才显示 100%
|
||||
// 否则按正常逻辑计算(或者这里可以理解为非 100% 时不改变原有的总用量逻辑,
|
||||
// 但根据用户反馈,Codex 应该主要关注周限制)
|
||||
// 重新审视需求:达到周限制时,总用量直接100%,重置时间设置为周限制时间
|
||||
|
||||
if (secondaryPercent >= 100) {
|
||||
return {
|
||||
hasData: true,
|
||||
used: 100,
|
||||
limit: 100,
|
||||
percent: 100,
|
||||
isCodex: true,
|
||||
resetAfterSeconds: secondary.reset_after_seconds
|
||||
};
|
||||
}
|
||||
// 如果未达到 100%,则继续执行下面的常规计算逻辑
|
||||
}
|
||||
|
||||
let totalUsed = 0;
|
||||
let totalLimit = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -43,16 +43,6 @@
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-tag {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -315,6 +305,28 @@
|
|||
.total-label { font-size: 0.8rem; font-weight: 600; color: var(--text-primary); display: flex; align-items: center; gap: 0.375rem; }
|
||||
.total-value { font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); font-family: monospace; }
|
||||
|
||||
.total-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.total-percent {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.total-reset-info {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-tertiary);
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.reset-info-compact { background: var(--bg-secondary); padding: 0.5rem 0.75rem; border-radius: 0.375rem; border: 1px solid var(--border-color); }
|
||||
.reset-info-row { display: flex; justify-content: space-between; align-items: center; padding: 0.25rem 0; }
|
||||
.reset-info-row:first-child { border-bottom: 1px solid var(--border-color); padding-bottom: 0.375rem; margin-bottom: 0.25rem; }
|
||||
|
|
@ -408,11 +420,6 @@
|
|||
[data-theme="dark"] .page-btn { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); }
|
||||
[data-theme="dark"] .page-jump-input { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); }
|
||||
|
||||
/* Codex 专用样式 */
|
||||
.codex-usage-item {
|
||||
border-left: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.codex-reset-info {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-tertiary);
|
||||
|
|
|
|||
Loading…
Reference in a new issue