mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-07 08:52:26 +00:00
后端 - BatchUserUsageStats / UserDashboardStats 新增 ByPlatform 字段 复用 ops 路径 COALESCE(g.platform, a.platform) 语义,不冗余 DB 字段 - 抽出 usageLogEffectivePlatformExpr 常量供管理员与用户两路径共用 - GetBatchUsersUsage cacheKey 加 v=2 + 当日日期,修复跨午夜旧缓存兼容新字段 前端 - 新建 PlatformUsageBreakdown:管理员用量列 hover tooltip 展示各平台 today/total - 新建 PlatformCostCell:单平台 today/total 紧凑单元格 - UsersView 列设置新增 Claude/OpenAI/Gemini/Antigravity 四个平台子列,默认隐藏可手动启用 - 普通用户 Dashboard 新增 Row 3 平台拆分卡片,受 isSimple 控制 - 平台之和 < 总值时显式展示"其他"行,避免数字对不齐 - last_active_at 从 FORCED_VISIBLE_COLUMNS 移除,允许用户隐藏并持久化 - 列设置加 schema 版本号 + 迁移机制,老用户升级时新增默认隐藏列自动应用 - UsersView 用量列(汇总 + 4 平台子列)加入前端单页排序:列头单按钮 + 弹出菜单 切换"今日 / 近30天",三态循环 desc → asc → off;菜单底部备注"仅对本页数据排序" - sortedUsers computed 在 server-side-sort 结果之上叠加本地排序,缺失值按 0 处理; usageSort 状态独立 localStorage 持久化,互不干扰后端 sort_by - i18n 新增 admin.users.sortBy / sortCurrentPageOnly Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
3.5 KiB
Vue
104 lines
3.5 KiB
Vue
<template>
|
|
<div class="group/usage relative text-sm">
|
|
<div class="flex items-center gap-1.5">
|
|
<span class="text-gray-500 dark:text-gray-400">{{ t('admin.users.today') }}:</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">${{ today.toFixed(4) }}</span>
|
|
<Icon
|
|
v-if="hasBreakdown"
|
|
name="infoCircle"
|
|
size="xs"
|
|
class="text-gray-400 dark:text-gray-500"
|
|
/>
|
|
</div>
|
|
<div class="mt-0.5 flex items-center gap-1.5">
|
|
<span class="text-gray-500 dark:text-gray-400">{{ t('admin.users.total') }}:</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">${{ total.toFixed(4) }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-if="hasBreakdown"
|
|
class="pointer-events-none absolute left-full top-0 z-50 ml-2 min-w-[220px] whitespace-nowrap rounded-md bg-gray-900 px-3 py-2 text-xs text-white opacity-0 shadow-xl transition-opacity duration-100 group-hover/usage:opacity-100 dark:bg-dark-600"
|
|
>
|
|
<div class="mb-1.5 flex items-center justify-between gap-3 border-b border-white/10 pb-1 text-[11px] opacity-80">
|
|
<span>{{ t('admin.users.platformBreakdown') }}</span>
|
|
<span class="font-mono">{{ t('admin.users.today') }} / {{ t('admin.users.total') }}</span>
|
|
</div>
|
|
<div
|
|
v-for="item in sortedBreakdown"
|
|
:key="item.platform"
|
|
class="flex items-center justify-between gap-3 py-0.5"
|
|
:class="{ 'opacity-70 italic': item.isOther }"
|
|
>
|
|
<span class="capitalize">
|
|
{{ item.isOther ? t('admin.users.platformOther') : platformLabel(item.platform) }}
|
|
</span>
|
|
<span class="font-mono">
|
|
${{ item.today_actual_cost.toFixed(4) }}
|
|
<span class="opacity-50">/</span>
|
|
${{ item.total_actual_cost.toFixed(4) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import Icon from '@/components/icons/Icon.vue'
|
|
import type { PlatformUsage } from '@/api/admin/dashboard'
|
|
|
|
const props = defineProps<{
|
|
today: number
|
|
total: number
|
|
byPlatform?: PlatformUsage[]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
// 与 UserDashboardStats 保持一致:把"总值 - 各平台之和"的差作为"其他"行展示,
|
|
// 避免 tooltip 内各平台费用加总与列首总值对不上。
|
|
const OTHER_THRESHOLD = 0.0001
|
|
|
|
interface BreakdownRow {
|
|
platform: string
|
|
today_actual_cost: number
|
|
total_actual_cost: number
|
|
isOther?: boolean
|
|
}
|
|
|
|
const sortedBreakdown = computed<BreakdownRow[]>(() => {
|
|
const list = props.byPlatform ?? []
|
|
const rows: BreakdownRow[] = [...list]
|
|
.sort((a, b) => b.total_actual_cost - a.total_actual_cost)
|
|
.map((p) => ({ ...p }))
|
|
|
|
const sumTotal = rows.reduce((s, r) => s + r.total_actual_cost, 0)
|
|
const sumToday = rows.reduce((s, r) => s + r.today_actual_cost, 0)
|
|
const diffTotal = Math.max(0, props.total - sumTotal)
|
|
const diffToday = Math.max(0, props.today - sumToday)
|
|
if (diffTotal > OTHER_THRESHOLD || diffToday > OTHER_THRESHOLD) {
|
|
rows.push({
|
|
platform: '__other__',
|
|
today_actual_cost: diffToday,
|
|
total_actual_cost: diffTotal,
|
|
isOther: true
|
|
})
|
|
}
|
|
return rows
|
|
})
|
|
|
|
const hasBreakdown = computed(() => sortedBreakdown.value.length > 0)
|
|
|
|
const PLATFORM_LABELS: Record<string, string> = {
|
|
anthropic: 'Claude',
|
|
openai: 'OpenAI',
|
|
gemini: 'Gemini',
|
|
antigravity: 'Antigravity'
|
|
}
|
|
|
|
function platformLabel(platform: string): string {
|
|
return PLATFORM_LABELS[platform] ?? platform
|
|
}
|
|
</script>
|