diff --git a/scripts/data-transfer.js b/scripts/data-transfer.js
index 19950958..39ca10d3 100644
--- a/scripts/data-transfer.js
+++ b/scripts/data-transfer.js
@@ -117,7 +117,7 @@ const CSV_FIELD_MAPPING = {
concurrencyLimit: '并发限制',
dailyCostLimit: '日费用限制(美元)',
totalCostLimit: '总费用限制(美元)',
- weeklyOpusCostLimit: '周Opus费用限制(美元)',
+ weeklyOpusCostLimit: '周Claude费用限制(美元)',
// 账户绑定
claudeAccountId: 'Claude专属账户',
diff --git a/src/routes/admin/apiKeys.js b/src/routes/admin/apiKeys.js
index f2149ee8..0c763c03 100644
--- a/src/routes/admin/apiKeys.js
+++ b/src/routes/admin/apiKeys.js
@@ -1029,6 +1029,7 @@ router.post('/api-keys/batch-stats', authenticateAdmin, async (req, res) => {
cost: 0,
formattedCost: '$0.00',
dailyCost: 0,
+ weeklyOpusCost: 0,
currentWindowCost: 0,
windowRemainingSeconds: null,
allTimeCost: 0,
@@ -1109,6 +1110,7 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
// 获取实时限制数据(窗口数据不受时间范围筛选影响,始终获取当前窗口状态)
let dailyCost = 0
+ let weeklyOpusCost = 0 // 字段名沿用 weeklyOpusCost*,语义为“Claude 周费用”
let currentWindowCost = 0
let windowRemainingSeconds = null
let windowStartTime = null
@@ -1121,6 +1123,7 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
const rateLimitWindow = parseInt(apiKey?.rateLimitWindow) || 0
const dailyCostLimit = parseFloat(apiKey?.dailyCostLimit) || 0
const totalCostLimit = parseFloat(apiKey?.totalCostLimit) || 0
+ const weeklyOpusCostLimit = parseFloat(apiKey?.weeklyOpusCostLimit) || 0
// 只在启用了每日费用限制时查询
if (dailyCostLimit > 0) {
@@ -1133,6 +1136,11 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
allTimeCost = parseFloat((await client.get(totalCostKey)) || '0')
}
+ // 只在启用了 Claude 周费用限制时查询(字段名沿用 weeklyOpusCostLimit)
+ if (weeklyOpusCostLimit > 0) {
+ weeklyOpusCost = await redis.getWeeklyOpusCost(keyId)
+ }
+
// 🔧 FIX: 对于 "全部时间" 时间范围,直接使用 allTimeCost
// 因为 usage:*:model:daily:* 键有 30 天 TTL,旧数据已经过期
if (timeRange === 'all' && allTimeCost > 0) {
@@ -1149,6 +1157,7 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
formattedCost: CostCalculator.formatCost(allTimeCost),
// 实时限制数据(始终返回,不受时间范围影响)
dailyCost,
+ weeklyOpusCost,
currentWindowCost,
windowRemainingSeconds,
windowStartTime,
@@ -1199,6 +1208,7 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
formattedCost: '$0.00',
// 实时限制数据(始终返回,不受时间范围影响)
dailyCost,
+ weeklyOpusCost,
currentWindowCost,
windowRemainingSeconds,
windowStartTime,
@@ -1317,6 +1327,7 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) {
formattedCost: CostCalculator.formatCost(totalCost),
// 实时限制数据
dailyCost,
+ weeklyOpusCost,
currentWindowCost,
windowRemainingSeconds,
windowStartTime,
diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js
index 9eb44c4d..0ca80b9f 100644
--- a/src/services/apiKeyService.js
+++ b/src/services/apiKeyService.js
@@ -762,7 +762,7 @@ class ApiKeyService {
for (const key of apiKeys) {
key.usage = await redis.getUsageStats(key.id)
const costStats = await redis.getCostStats(key.id)
- // Add cost information to usage object for frontend compatibility
+ // 为前端兼容性:把费用信息同步到 usage 对象里
if (key.usage && costStats) {
key.usage.total = key.usage.total || {}
key.usage.total.cost = costStats.total
diff --git a/web/admin-spa/src/components/apikeys/LimitProgressBar.vue b/web/admin-spa/src/components/apikeys/LimitProgressBar.vue
index 6f2975e6..f84e1db2 100644
--- a/web/admin-spa/src/components/apikeys/LimitProgressBar.vue
+++ b/web/admin-spa/src/components/apikeys/LimitProgressBar.vue
@@ -2,7 +2,7 @@
@@ -18,7 +18,7 @@
{{ label }}
${{ current.toFixed(2) }} / ${{ limit.toFixed(2) }}${{ currentValue.toFixed(2) }} / ${{ limitValue.toFixed(2) }}
@@ -57,7 +57,7 @@
- ${{ current.toFixed(2) }} / ${{ limit.toFixed(2) }}
+ ${{ currentValue.toFixed(2) }} / ${{ limitValue.toFixed(2) }}
@@ -95,11 +95,11 @@ const props = defineProps({
required: true
},
current: {
- type: Number,
+ type: [Number, String],
default: 0
},
limit: {
- type: Number,
+ type: [Number, String],
required: true
},
showShine: {
@@ -109,10 +109,18 @@ const props = defineProps({
})
const isCompact = computed(() => props.variant === 'compact')
+const currentValue = computed(() => {
+ const n = Number(props.current)
+ return Number.isFinite(n) ? n : 0
+})
+const limitValue = computed(() => {
+ const n = Number(props.limit)
+ return Number.isFinite(n) ? n : 0
+})
const progress = computed(() => {
// 无限制时不显示进度条
- if (!props.limit || props.limit <= 0) return 0
- const percentage = (props.current / props.limit) * 100
+ if (!limitValue.value || limitValue.value <= 0) return 0
+ const percentage = (currentValue.value / limitValue.value) * 100
return Math.min(percentage, 100)
})
diff --git a/web/admin-spa/src/components/apikeys/UsageDetailModal.vue b/web/admin-spa/src/components/apikeys/UsageDetailModal.vue
index 06ac3d36..871c9c3f 100644
--- a/web/admin-spa/src/components/apikeys/UsageDetailModal.vue
+++ b/web/admin-spa/src/components/apikeys/UsageDetailModal.vue
@@ -155,11 +155,11 @@
限制设置
-
-
-
+
@@ -195,7 +195,7 @@
并发限制
@@ -204,11 +204,25 @@
-
+
时间窗口限制
+
+ 未设置窗口时长(rateLimitWindow=0),窗口限制不会生效。
+
+
+
+
+
+
+ 访问控制
+
+
+
+
+ 服务权限
+
+ {{ permissionsDisplay }}
+
+
+
+
+
+
+ 模型限制(禁用列表)
+
+ {{ restrictedModels.length }}
+
+
+
+
+ {{ model }}
+
+
+
未配置具体模型
+
+
+
+
+ 客户端限制(允许列表)
+
+ {{ allowedClients.length }}
+
+
+
+
+ {{ client }}
+
+
+
未配置客户端
+
+
@@ -280,14 +357,48 @@ const cacheReadTokens = computed(() => props.apiKey.usage?.total?.cacheReadToken
const rpm = computed(() => props.apiKey.usage?.averages?.rpm || 0)
const tpm = computed(() => props.apiKey.usage?.averages?.tpm || 0)
+const enableModelRestriction = computed(
+ () =>
+ props.apiKey.enableModelRestriction === true || props.apiKey.enableModelRestriction === 'true'
+)
+const restrictedModels = computed(() =>
+ Array.isArray(props.apiKey.restrictedModels) ? props.apiKey.restrictedModels : []
+)
+const enableClientRestriction = computed(
+ () =>
+ props.apiKey.enableClientRestriction === true || props.apiKey.enableClientRestriction === 'true'
+)
+const allowedClients = computed(() =>
+ Array.isArray(props.apiKey.allowedClients) ? props.apiKey.allowedClients : []
+)
+const permissions = computed(() => props.apiKey.permissions || [])
+
+const permissionsDisplay = computed(() => {
+ if (!Array.isArray(permissions.value) || permissions.value.length === 0) {
+ return '全部'
+ }
+ return permissions.value.join(', ')
+})
+
+const hasAccessRestrictions = computed(() => {
+ return (
+ enableModelRestriction.value ||
+ enableClientRestriction.value ||
+ (Array.isArray(permissions.value) && permissions.value.length > 0)
+ )
+})
+
const hasLimits = computed(() => {
return (
- props.apiKey.dailyCostLimit > 0 ||
- props.apiKey.totalCostLimit > 0 ||
- props.apiKey.concurrencyLimit > 0 ||
- props.apiKey.weeklyOpusCostLimit > 0 ||
- props.apiKey.rateLimitWindow > 0 ||
- props.apiKey.tokenLimit > 0
+ Number(props.apiKey.dailyCostLimit) > 0 ||
+ Number(props.apiKey.totalCostLimit) > 0 ||
+ Number(props.apiKey.concurrencyLimit) > 0 ||
+ Number(props.apiKey.weeklyOpusCostLimit) > 0 ||
+ Number(props.apiKey.rateLimitWindow) > 0 ||
+ Number(props.apiKey.rateLimitRequests) > 0 ||
+ Number(props.apiKey.rateLimitCost) > 0 ||
+ Number(props.apiKey.tokenLimit) > 0 ||
+ hasAccessRestrictions.value
)
})
diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue
index a68ca4e4..aceeb197 100644
--- a/web/admin-spa/src/views/ApiKeysView.vue
+++ b/web/admin-spa/src/views/ApiKeysView.vue
@@ -4261,6 +4261,7 @@ const showUsageDetails = (apiKey) => {
const enrichedApiKey = {
...apiKey,
dailyCost: cachedStats?.dailyCost ?? apiKey.dailyCost ?? 0,
+ weeklyOpusCost: cachedStats?.weeklyOpusCost ?? apiKey.weeklyOpusCost ?? 0,
currentWindowCost: cachedStats?.currentWindowCost ?? apiKey.currentWindowCost ?? 0,
windowRemainingSeconds: cachedStats?.windowRemainingSeconds ?? apiKey.windowRemainingSeconds,
windowStartTime: cachedStats?.windowStartTime ?? apiKey.windowStartTime ?? null,