mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
feat: API Keys页面添加全部时间选项和UI改进
- 添加"全部时间"选项到时间范围下拉菜单,可查看所有历史使用数据 - 统一费用显示列,根据选择的时间范围动态显示对应标签 - 支持自定义日期范围查询(最多31天) - 优化日期选择器高度与其他控件对齐(38px) - 使用更通用的标签名称(累计费用、总费用等) - 移除调试console.log语句 后端改进: - 添加自定义日期范围查询支持 - 日期范围验证和31天限制 - 支持all时间范围查询 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -122,7 +122,7 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) =>
|
|||||||
// 获取所有API Keys
|
// 获取所有API Keys
|
||||||
router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { timeRange = 'all' } = req.query // all, 7days, monthly
|
const { timeRange = 'all', startDate, endDate } = req.query // all, 7days, monthly, custom
|
||||||
const apiKeys = await apiKeyService.getAllApiKeys()
|
const apiKeys = await apiKeyService.getAllApiKeys()
|
||||||
|
|
||||||
// 获取用户服务来补充owner信息
|
// 获取用户服务来补充owner信息
|
||||||
@@ -132,7 +132,32 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const searchPatterns = []
|
const searchPatterns = []
|
||||||
|
|
||||||
if (timeRange === 'today') {
|
if (timeRange === 'custom' && startDate && endDate) {
|
||||||
|
// 自定义日期范围
|
||||||
|
const redisClient = require('../models/redis')
|
||||||
|
const start = new Date(startDate)
|
||||||
|
const end = new Date(endDate)
|
||||||
|
|
||||||
|
// 确保日期范围有效
|
||||||
|
if (start > end) {
|
||||||
|
return res.status(400).json({ error: 'Start date must be before or equal to end date' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制最大范围为31天
|
||||||
|
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||||
|
if (daysDiff > 31) {
|
||||||
|
return res.status(400).json({ error: 'Date range cannot exceed 31 days' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成日期范围内每天的搜索模式
|
||||||
|
const currentDate = new Date(start)
|
||||||
|
while (currentDate <= end) {
|
||||||
|
const tzDate = redisClient.getDateInTimezone(currentDate)
|
||||||
|
const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`
|
||||||
|
searchPatterns.push(`usage:daily:*:${dateStr}`)
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
|
}
|
||||||
|
} else if (timeRange === 'today') {
|
||||||
// 今日 - 使用时区日期
|
// 今日 - 使用时区日期
|
||||||
const redisClient = require('../models/redis')
|
const redisClient = require('../models/redis')
|
||||||
const tzDate = redisClient.getDateInTimezone(now)
|
const tzDate = redisClient.getDateInTimezone(now)
|
||||||
@@ -233,7 +258,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost)
|
apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 7天或本月:重新计算统计数据
|
// 7天、本月或自定义日期范围:重新计算统计数据
|
||||||
const tempUsage = {
|
const tempUsage = {
|
||||||
requests: 0,
|
requests: 0,
|
||||||
tokens: 0,
|
tokens: 0,
|
||||||
@@ -274,12 +299,28 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
const tzDate = redisClient.getDateInTimezone(now)
|
const tzDate = redisClient.getDateInTimezone(now)
|
||||||
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`
|
||||||
|
|
||||||
const modelKeys =
|
let modelKeys = []
|
||||||
|
if (timeRange === 'custom' && startDate && endDate) {
|
||||||
|
// 自定义日期范围:获取范围内所有日期的模型统计
|
||||||
|
const start = new Date(startDate)
|
||||||
|
const end = new Date(endDate)
|
||||||
|
const currentDate = new Date(start)
|
||||||
|
|
||||||
|
while (currentDate <= end) {
|
||||||
|
const tzDateForKey = redisClient.getDateInTimezone(currentDate)
|
||||||
|
const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}`
|
||||||
|
const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`)
|
||||||
|
modelKeys = modelKeys.concat(dayKeys)
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
modelKeys =
|
||||||
timeRange === 'today'
|
timeRange === 'today'
|
||||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`)
|
||||||
: timeRange === '7days'
|
: timeRange === '7days'
|
||||||
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
? await client.keys(`usage:${apiKey.id}:model:daily:*:*`)
|
||||||
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
: await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`)
|
||||||
|
}
|
||||||
|
|
||||||
const modelStatsMap = new Map()
|
const modelStatsMap = new Map()
|
||||||
|
|
||||||
@@ -295,8 +336,8 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (timeRange === 'today') {
|
} else if (timeRange === 'today' || timeRange === 'custom') {
|
||||||
// today选项已经在查询时过滤了,不需要额外处理
|
// today和custom选项已经在查询时过滤了,不需要额外处理
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelMatch = key.match(
|
const modelMatch = key.match(
|
||||||
|
|||||||
@@ -64,12 +64,33 @@
|
|||||||
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
|
||||||
></div>
|
></div>
|
||||||
<CustomDropdown
|
<CustomDropdown
|
||||||
v-model="apiKeyStatsTimeRange"
|
v-model="globalDateFilter.preset"
|
||||||
icon="fa-calendar-alt"
|
icon="fa-calendar-alt"
|
||||||
icon-color="text-blue-500"
|
icon-color="text-blue-500"
|
||||||
:options="timeRangeOptions"
|
:options="timeRangeDropdownOptions"
|
||||||
placeholder="选择时间范围"
|
placeholder="选择时间范围"
|
||||||
@change="loadApiKeys()"
|
@change="handleTimeRangeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义日期范围选择器 - 在选择自定义时显示 -->
|
||||||
|
<div v-if="globalDateFilter.type === 'custom'" class="flex items-center">
|
||||||
|
<el-date-picker
|
||||||
|
class="api-key-date-picker custom-date-range-picker"
|
||||||
|
:clearable="true"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
:disabled-date="disabledDate"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:model-value="globalDateFilter.customRange"
|
||||||
|
range-separator="至"
|
||||||
|
size="small"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
style="width: 320px"
|
||||||
|
type="datetimerange"
|
||||||
|
:unlink-panels="false"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
@update:model-value="onGlobalCustomDateRangeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -245,29 +266,13 @@
|
|||||||
class="w-[17%] min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="w-[17%] min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>使用统计</span>
|
|
||||||
<span
|
<span
|
||||||
class="cursor-pointer rounded px-1.5 py-0.5 text-xs normal-case hover:bg-gray-100 dark:hover:bg-gray-600"
|
class="cursor-pointer rounded px-1.5 py-0.5 text-xs normal-case hover:bg-gray-100 dark:hover:bg-gray-600"
|
||||||
@click="sortApiKeys('dailyCost')"
|
@click="sortApiKeys('periodCost')"
|
||||||
>
|
>
|
||||||
今日费用
|
使用统计(按费用排序)
|
||||||
<i
|
<i
|
||||||
v-if="apiKeysSortBy === 'dailyCost'"
|
v-if="apiKeysSortBy === 'periodCost'"
|
||||||
:class="[
|
|
||||||
'fas',
|
|
||||||
apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down',
|
|
||||||
'ml-0.5 text-[10px]'
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<i v-else class="fas fa-sort ml-0.5 text-[10px] text-gray-400" />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="cursor-pointer rounded px-1.5 py-0.5 text-xs normal-case hover:bg-gray-100 dark:hover:bg-gray-600"
|
|
||||||
@click="sortApiKeys('totalCost')"
|
|
||||||
>
|
|
||||||
总费用
|
|
||||||
<i
|
|
||||||
v-if="apiKeysSortBy === 'totalCost'"
|
|
||||||
:class="[
|
:class="[
|
||||||
'fas',
|
'fas',
|
||||||
apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down',
|
apiKeysSortOrder === 'asc' ? 'fa-sort-up' : 'fa-sort-down',
|
||||||
@@ -471,21 +476,19 @@
|
|||||||
<!-- 今日使用统计 -->
|
<!-- 今日使用统计 -->
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div class="mb-1 flex items-center justify-between text-sm">
|
<div class="mb-1 flex items-center justify-between text-sm">
|
||||||
<span class="text-gray-600 dark:text-gray-400">今日请求</span>
|
<span class="text-gray-600 dark:text-gray-400">{{
|
||||||
|
getPeriodRequestLabel()
|
||||||
|
}}</span>
|
||||||
<span class="font-semibold text-gray-900 dark:text-gray-100"
|
<span class="font-semibold text-gray-900 dark:text-gray-100"
|
||||||
>{{ formatNumber(key.usage?.daily?.requests || 0) }}次</span
|
>{{ formatNumber(key.usage?.daily?.requests || 0) }}次</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-gray-600 dark:text-gray-400">今日费用</span>
|
<span class="text-gray-600 dark:text-gray-400">{{
|
||||||
<span class="font-semibold text-green-600"
|
getPeriodCostLabel()
|
||||||
>${{ (key.dailyCost || 0).toFixed(4) }}</span
|
}}</span>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between text-sm">
|
|
||||||
<span class="text-gray-600 dark:text-gray-400">总费用</span>
|
|
||||||
<span class="font-semibold text-blue-600"
|
<span class="font-semibold text-blue-600"
|
||||||
>${{ (key.totalCost || 0).toFixed(4) }}</span
|
>${{ getPeriodCost(key).toFixed(4) }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
@@ -1078,7 +1081,9 @@
|
|||||||
<!-- 今日使用 -->
|
<!-- 今日使用 -->
|
||||||
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-700">
|
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-700">
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
<span class="text-xs text-gray-600 dark:text-gray-400">今日使用</span>
|
<span class="text-xs text-gray-600 dark:text-gray-400">{{
|
||||||
|
globalDateFilter.type === 'custom' ? '累计统计' : '今日使用'
|
||||||
|
}}</span>
|
||||||
<button
|
<button
|
||||||
class="text-xs text-blue-600 hover:text-blue-800"
|
class="text-xs text-blue-600 hover:text-blue-800"
|
||||||
@click="showUsageDetails(key)"
|
@click="showUsageDetails(key)"
|
||||||
@@ -1588,7 +1593,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { showToast } from '@/utils/toast'
|
import { showToast } from '@/utils/toast'
|
||||||
import { apiClient } from '@/config/api'
|
import { apiClient } from '@/config/api'
|
||||||
import { useClientsStore } from '@/stores/clients'
|
import { useClientsStore } from '@/stores/clients'
|
||||||
@@ -1619,11 +1624,43 @@ const isIndeterminate = ref(false)
|
|||||||
const apiKeysLoading = ref(false)
|
const apiKeysLoading = ref(false)
|
||||||
const apiKeyStatsTimeRange = ref('today')
|
const apiKeyStatsTimeRange = ref('today')
|
||||||
|
|
||||||
|
// 全局日期筛选器
|
||||||
|
const globalDateFilter = reactive({
|
||||||
|
type: 'preset',
|
||||||
|
preset: '7days',
|
||||||
|
customStart: '',
|
||||||
|
customEnd: '',
|
||||||
|
customRange: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 时间范围下拉选项
|
||||||
|
const timeRangeDropdownOptions = computed(() => [
|
||||||
|
{ value: '7days', label: '最近7天', icon: 'fa-calendar-week' },
|
||||||
|
{ value: '30days', label: '最近30天', icon: 'fa-calendar-alt' },
|
||||||
|
{ value: 'all', label: '全部时间', icon: 'fa-infinity' },
|
||||||
|
{ value: 'custom', label: '自定义范围', icon: 'fa-calendar-check' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 全局预设选项(用于内部计算)
|
||||||
|
const globalPresetOptions = [
|
||||||
|
{ value: 'today', label: '今日', days: 1 },
|
||||||
|
{ value: '3days', label: '3天', days: 3 },
|
||||||
|
{ value: '7days', label: '7天', days: 7 },
|
||||||
|
{ value: '14days', label: '14天', days: 14 },
|
||||||
|
{ value: '30days', label: '30天', days: 30 },
|
||||||
|
{ value: '90days', label: '90天', days: 90 },
|
||||||
|
{ value: 'thisWeek', label: '本周', days: -1 },
|
||||||
|
{ value: 'thisMonth', label: '本月', days: -2 },
|
||||||
|
{ value: 'lastMonth', label: '上月', days: -3 },
|
||||||
|
{ value: 'custom', label: '自定义', days: -1 },
|
||||||
|
{ value: 'all', label: '全部', days: -99 }
|
||||||
|
]
|
||||||
|
|
||||||
// Tab management
|
// Tab management
|
||||||
const activeTab = ref('active')
|
const activeTab = ref('active')
|
||||||
const deletedApiKeys = ref([])
|
const deletedApiKeys = ref([])
|
||||||
const deletedApiKeysLoading = ref(false)
|
const deletedApiKeysLoading = ref(false)
|
||||||
const apiKeysSortBy = ref('dailyCost')
|
const apiKeysSortBy = ref('periodCost')
|
||||||
const apiKeysSortOrder = ref('desc')
|
const apiKeysSortOrder = ref('desc')
|
||||||
const expandedApiKeys = ref({})
|
const expandedApiKeys = ref({})
|
||||||
const apiKeyModelStats = ref({})
|
const apiKeyModelStats = ref({})
|
||||||
@@ -1651,12 +1688,7 @@ const availableTags = ref([])
|
|||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
// 下拉选项数据
|
// 下拉选项数据
|
||||||
const timeRangeOptions = ref([
|
// Removed timeRangeOptions as we now use globalPresetOptions
|
||||||
{ value: 'today', label: '今日', icon: 'fa-clock' },
|
|
||||||
{ value: '7days', label: '最近7天', icon: 'fa-calendar-week' },
|
|
||||||
{ value: 'monthly', label: '本月', icon: 'fa-calendar' },
|
|
||||||
{ value: 'all', label: '全部时间', icon: 'fa-infinity' }
|
|
||||||
])
|
|
||||||
|
|
||||||
const tagOptions = computed(() => {
|
const tagOptions = computed(() => {
|
||||||
const options = [{ value: '', label: '所有标签', icon: 'fa-asterisk' }]
|
const options = [{ value: '', label: '所有标签', icon: 'fa-asterisk' }]
|
||||||
@@ -1729,6 +1761,9 @@ const sortedApiKeys = computed(() => {
|
|||||||
if (apiKeysSortBy.value === 'status') {
|
if (apiKeysSortBy.value === 'status') {
|
||||||
aVal = a.isActive ? 1 : 0
|
aVal = a.isActive ? 1 : 0
|
||||||
bVal = b.isActive ? 1 : 0
|
bVal = b.isActive ? 1 : 0
|
||||||
|
} else if (apiKeysSortBy.value === 'periodCost') {
|
||||||
|
aVal = calculatePeriodCost(a)
|
||||||
|
bVal = calculatePeriodCost(b)
|
||||||
} else if (apiKeysSortBy.value === 'dailyCost') {
|
} else if (apiKeysSortBy.value === 'dailyCost') {
|
||||||
aVal = a.dailyCost || 0
|
aVal = a.dailyCost || 0
|
||||||
bVal = b.dailyCost || 0
|
bVal = b.dailyCost || 0
|
||||||
@@ -1867,10 +1902,26 @@ const loadAccounts = async () => {
|
|||||||
const loadApiKeys = async () => {
|
const loadApiKeys = async () => {
|
||||||
apiKeysLoading.value = true
|
apiKeysLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await apiClient.get(`/admin/api-keys?timeRange=${apiKeyStatsTimeRange.value}`)
|
// 构建请求参数
|
||||||
|
let params = {}
|
||||||
|
if (
|
||||||
|
globalDateFilter.type === 'custom' &&
|
||||||
|
globalDateFilter.customStart &&
|
||||||
|
globalDateFilter.customEnd
|
||||||
|
) {
|
||||||
|
params.startDate = globalDateFilter.customStart
|
||||||
|
params.endDate = globalDateFilter.customEnd
|
||||||
|
params.timeRange = 'custom'
|
||||||
|
} else if (globalDateFilter.preset === 'all') {
|
||||||
|
params.timeRange = 'all'
|
||||||
|
} else {
|
||||||
|
params.timeRange = globalDateFilter.preset
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = new URLSearchParams(params).toString()
|
||||||
|
const data = await apiClient.get(`/admin/api-keys?${queryString}`)
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
apiKeys.value = data.data || []
|
apiKeys.value = data.data || []
|
||||||
|
|
||||||
// 更新可用标签列表
|
// 更新可用标签列表
|
||||||
const tagsSet = new Set()
|
const tagsSet = new Set()
|
||||||
apiKeys.value.forEach((key) => {
|
apiKeys.value.forEach((key) => {
|
||||||
@@ -2157,6 +2208,184 @@ const calculateModelCost = (stat) => {
|
|||||||
return '$0.000000'
|
return '$0.000000'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取费用统计标签
|
||||||
|
const getPeriodCostLabel = () => {
|
||||||
|
if (
|
||||||
|
globalDateFilter.type === 'custom' &&
|
||||||
|
globalDateFilter.customStart &&
|
||||||
|
globalDateFilter.customEnd
|
||||||
|
) {
|
||||||
|
return '累计费用'
|
||||||
|
}
|
||||||
|
if (globalDateFilter.preset === 'all') {
|
||||||
|
return '总费用'
|
||||||
|
}
|
||||||
|
const preset = globalPresetOptions.find((opt) => opt.value === globalDateFilter.preset)
|
||||||
|
return preset ? `${preset.label}费用` : '费用统计'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取请求数量标签
|
||||||
|
const getPeriodRequestLabel = () => {
|
||||||
|
if (
|
||||||
|
globalDateFilter.type === 'custom' &&
|
||||||
|
globalDateFilter.customStart &&
|
||||||
|
globalDateFilter.customEnd
|
||||||
|
) {
|
||||||
|
return '累计请求'
|
||||||
|
}
|
||||||
|
if (globalDateFilter.preset === 'all') {
|
||||||
|
return '总请求'
|
||||||
|
}
|
||||||
|
return '今日请求'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日期范围内的费用
|
||||||
|
const getPeriodCost = (key) => {
|
||||||
|
// 根据全局日期筛选器返回对应的费用
|
||||||
|
if (globalDateFilter.type === 'custom') {
|
||||||
|
// 自定义日期范围,使用服务器返回的 usage['custom'].cost
|
||||||
|
if (key.usage) {
|
||||||
|
if (key.usage['custom'] && key.usage['custom'].cost !== undefined) {
|
||||||
|
return key.usage['custom'].cost
|
||||||
|
}
|
||||||
|
if (key.usage.total && key.usage.total.cost !== undefined) {
|
||||||
|
return key.usage.total.cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else if (globalDateFilter.preset === 'today') {
|
||||||
|
return key.dailyCost || 0
|
||||||
|
} else if (globalDateFilter.preset === '7days') {
|
||||||
|
// 使用 usage['7days'].cost
|
||||||
|
if (key.usage && key.usage['7days'] && key.usage['7days'].cost !== undefined) {
|
||||||
|
return key.usage['7days'].cost
|
||||||
|
}
|
||||||
|
return key.weeklyCost || key.periodCost || 0
|
||||||
|
} else if (globalDateFilter.preset === '30days') {
|
||||||
|
// 使用 usage['30days'].cost 或 usage.monthly.cost
|
||||||
|
if (key.usage) {
|
||||||
|
if (key.usage['30days'] && key.usage['30days'].cost !== undefined) {
|
||||||
|
return key.usage['30days'].cost
|
||||||
|
}
|
||||||
|
if (key.usage.monthly && key.usage.monthly.cost !== undefined) {
|
||||||
|
return key.usage.monthly.cost
|
||||||
|
}
|
||||||
|
if (key.usage.total && key.usage.total.cost !== undefined) {
|
||||||
|
return key.usage.total.cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key.monthlyCost || key.periodCost || 0
|
||||||
|
} else if (globalDateFilter.preset === 'all') {
|
||||||
|
// 全部时间,返回 usage['all'].cost 或 totalCost
|
||||||
|
if (key.usage && key.usage['all'] && key.usage['all'].cost !== undefined) {
|
||||||
|
return key.usage['all'].cost
|
||||||
|
}
|
||||||
|
return key.totalCost || 0
|
||||||
|
} else {
|
||||||
|
// 默认返回 usage.total.cost
|
||||||
|
return key.periodCost || key.totalCost || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算日期范围内的总费用(用于展开的详细统计)
|
||||||
|
const calculatePeriodCost = (key) => {
|
||||||
|
// 如果没有展开,使用缓存的费用数据
|
||||||
|
if (!apiKeyModelStats.value[key.id]) {
|
||||||
|
return getPeriodCost(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算所有模型的费用总和
|
||||||
|
const stats = apiKeyModelStats.value[key.id] || []
|
||||||
|
let totalCost = 0
|
||||||
|
|
||||||
|
stats.forEach((stat) => {
|
||||||
|
if (stat.cost !== undefined) {
|
||||||
|
totalCost += stat.cost
|
||||||
|
} else if (stat.formatted && stat.formatted.total) {
|
||||||
|
// 尝试从格式化的字符串中提取数字
|
||||||
|
const costStr = stat.formatted.total.replace('$', '').replace(',', '')
|
||||||
|
const cost = parseFloat(costStr)
|
||||||
|
if (!isNaN(cost)) {
|
||||||
|
totalCost += cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return totalCost
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理时间范围下拉框变化
|
||||||
|
const handleTimeRangeChange = (value) => {
|
||||||
|
setGlobalDateFilterPreset(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置全局日期预设
|
||||||
|
const setGlobalDateFilterPreset = (preset) => {
|
||||||
|
globalDateFilter.preset = preset
|
||||||
|
|
||||||
|
if (preset === 'custom') {
|
||||||
|
// 自定义选项,不自动设置日期,等待用户选择
|
||||||
|
globalDateFilter.type = 'custom'
|
||||||
|
// 如果没有自定义范围,设置默认为最近7天
|
||||||
|
if (!globalDateFilter.customRange) {
|
||||||
|
const today = new Date()
|
||||||
|
const startDate = new Date(today)
|
||||||
|
startDate.setDate(today.getDate() - 6)
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return (
|
||||||
|
date.getFullYear() +
|
||||||
|
'-' +
|
||||||
|
String(date.getMonth() + 1).padStart(2, '0') +
|
||||||
|
'-' +
|
||||||
|
String(date.getDate()).padStart(2, '0') +
|
||||||
|
' 00:00:00'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalDateFilter.customRange = [formatDate(startDate), formatDate(today)]
|
||||||
|
globalDateFilter.customStart = startDate.toISOString().split('T')[0]
|
||||||
|
globalDateFilter.customEnd = today.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
} else if (preset === 'all') {
|
||||||
|
// 全部时间选项
|
||||||
|
globalDateFilter.type = 'preset'
|
||||||
|
globalDateFilter.customStart = null
|
||||||
|
globalDateFilter.customEnd = null
|
||||||
|
} else {
|
||||||
|
// 预设选项(7天或30天)
|
||||||
|
globalDateFilter.type = 'preset'
|
||||||
|
const today = new Date()
|
||||||
|
const startDate = new Date(today)
|
||||||
|
|
||||||
|
if (preset === '7days') {
|
||||||
|
startDate.setDate(today.getDate() - 6)
|
||||||
|
} else if (preset === '30days') {
|
||||||
|
startDate.setDate(today.getDate() - 29)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalDateFilter.customStart = startDate.toISOString().split('T')[0]
|
||||||
|
globalDateFilter.customEnd = today.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
loadApiKeys()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局自定义日期范围变化
|
||||||
|
const onGlobalCustomDateRangeChange = (value) => {
|
||||||
|
if (value && value.length === 2) {
|
||||||
|
globalDateFilter.type = 'custom'
|
||||||
|
globalDateFilter.preset = 'custom'
|
||||||
|
globalDateFilter.customRange = value
|
||||||
|
globalDateFilter.customStart = value[0].split(' ')[0]
|
||||||
|
globalDateFilter.customEnd = value[1].split(' ')[0]
|
||||||
|
loadApiKeys()
|
||||||
|
} else if (value === null) {
|
||||||
|
// 清空时恢复默认7天
|
||||||
|
setGlobalDateFilterPreset('7days')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化API Key的日期筛选器
|
// 初始化API Key的日期筛选器
|
||||||
const initApiKeyDateFilter = (keyId) => {
|
const initApiKeyDateFilter = (keyId) => {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@@ -2172,7 +2401,8 @@ const initApiKeyDateFilter = (keyId) => {
|
|||||||
presetOptions: [
|
presetOptions: [
|
||||||
{ value: 'today', label: '今日', days: 1 },
|
{ value: 'today', label: '今日', days: 1 },
|
||||||
{ value: '7days', label: '7天', days: 7 },
|
{ value: '7days', label: '7天', days: 7 },
|
||||||
{ value: '30days', label: '30天', days: 30 }
|
{ value: '30days', label: '30天', days: 30 },
|
||||||
|
{ value: 'custom', label: '自定义', days: -1 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2193,6 +2423,32 @@ const setApiKeyDateFilterPreset = (preset, keyId) => {
|
|||||||
|
|
||||||
const option = filter.presetOptions.find((opt) => opt.value === preset)
|
const option = filter.presetOptions.find((opt) => opt.value === preset)
|
||||||
if (option) {
|
if (option) {
|
||||||
|
if (preset === 'custom') {
|
||||||
|
// 自定义选项,不自动设置日期,等待用户选择
|
||||||
|
filter.type = 'custom'
|
||||||
|
// 如果没有自定义范围,设置默认为最近7天
|
||||||
|
if (!filter.customRange) {
|
||||||
|
const today = new Date()
|
||||||
|
const startDate = new Date(today)
|
||||||
|
startDate.setDate(today.getDate() - 6)
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return (
|
||||||
|
date.getFullYear() +
|
||||||
|
'-' +
|
||||||
|
String(date.getMonth() + 1).padStart(2, '0') +
|
||||||
|
'-' +
|
||||||
|
String(date.getDate()).padStart(2, '0') +
|
||||||
|
' 00:00:00'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.customRange = [formatDate(startDate), formatDate(today)]
|
||||||
|
filter.customStart = startDate.toISOString().split('T')[0]
|
||||||
|
filter.customEnd = today.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 预设选项
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const startDate = new Date(today)
|
const startDate = new Date(today)
|
||||||
startDate.setDate(today.getDate() - (option.days - 1))
|
startDate.setDate(today.getDate() - (option.days - 1))
|
||||||
@@ -2213,6 +2469,7 @@ const setApiKeyDateFilterPreset = (preset, keyId) => {
|
|||||||
|
|
||||||
filter.customRange = [formatDate(startDate), formatDate(today)]
|
filter.customRange = [formatDate(startDate), formatDate(today)]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadApiKeyModelStats(keyId, true)
|
loadApiKeyModelStats(keyId, true)
|
||||||
}
|
}
|
||||||
@@ -2223,7 +2480,7 @@ const onApiKeyCustomDateRangeChange = (keyId, value) => {
|
|||||||
|
|
||||||
if (value && value.length === 2) {
|
if (value && value.length === 2) {
|
||||||
filter.type = 'custom'
|
filter.type = 'custom'
|
||||||
filter.preset = ''
|
filter.preset = 'custom'
|
||||||
filter.customRange = value
|
filter.customRange = value
|
||||||
filter.customStart = value[0].split(' ')[0]
|
filter.customStart = value[0].split(' ')[0]
|
||||||
filter.customEnd = value[1].split(' ')[0]
|
filter.customEnd = value[1].split(' ')[0]
|
||||||
@@ -2638,12 +2895,9 @@ const copyApiStatsLink = (apiKey) => {
|
|||||||
showToast(`已复制统计页面链接`, 'success')
|
showToast(`已复制统计页面链接`, 'success')
|
||||||
} else {
|
} else {
|
||||||
showToast('复制失败,请手动复制', 'error')
|
showToast('复制失败,请手动复制', 'error')
|
||||||
console.log('统计页面链接:', statsUrl)
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('复制失败,请手动复制', 'error')
|
showToast('复制失败,请手动复制', 'error')
|
||||||
console.error('复制错误:', err)
|
|
||||||
console.log('统计页面链接:', statsUrl)
|
|
||||||
} finally {
|
} finally {
|
||||||
document.body.removeChild(textarea)
|
document.body.removeChild(textarea)
|
||||||
}
|
}
|
||||||
@@ -2865,4 +3119,19 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义日期范围选择器高度对齐 */
|
||||||
|
.custom-date-range-picker :deep(.el-input__wrapper) {
|
||||||
|
@apply h-[38px] rounded-lg border border-gray-200 bg-white shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md dark:border-gray-600 dark:bg-gray-800;
|
||||||
|
}
|
||||||
|
.custom-date-range-picker :deep(.el-input__inner) {
|
||||||
|
@apply h-full py-2 text-sm font-medium text-gray-700 dark:text-gray-200;
|
||||||
|
}
|
||||||
|
.custom-date-range-picker :deep(.el-input__prefix),
|
||||||
|
.custom-date-range-picker :deep(.el-input__suffix) {
|
||||||
|
@apply flex items-center;
|
||||||
|
}
|
||||||
|
.custom-date-range-picker :deep(.el-range-separator) {
|
||||||
|
@apply mx-2 text-gray-500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user