mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
feat: unify user analytics and optimize overview aggregation
Merge user accounts and usage reporting into one page with a combined ranking and account table, shared precise time ranges, and simpler range labels. Parse overview metadata once through a schema-only view migration and disable JIT locally for bucket rebuilds. Preserve automatic backfills. Add redacted OAuth refresh diagnostics, bucket failure context, and regression coverage. Resolve strict Clippy warnings.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
:preset="relativePreset"
|
||||
:refresh-active="autoRefresh"
|
||||
:refresh-title="refreshTitle"
|
||||
:show-range="false"
|
||||
presets-only
|
||||
@update:range="setRange"
|
||||
@refresh="toggleAutoRefresh"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:preset="relativePreset"
|
||||
:refresh-active="autoRefresh"
|
||||
:refresh-title="refreshTitle"
|
||||
:show-range="false"
|
||||
presets-only
|
||||
:loading="loading"
|
||||
@update:range="setRange"
|
||||
|
||||
@@ -1,235 +1,253 @@
|
||||
<template>
|
||||
<main class="space-y-5 px-4 pb-8 sm:px-6 lg:px-0">
|
||||
<Tabs v-model="activeTab">
|
||||
<TabsList class="mb-4">
|
||||
<TabsTrigger value="accounts">
|
||||
{{ t('用户账目', 'User accounts') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="usage">
|
||||
{{ t('使用统计', 'Usage statistics') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
value="accounts"
|
||||
class="space-y-5"
|
||||
<OverviewToolbar
|
||||
:title="t('用户分析', 'User analysis')"
|
||||
:range="range"
|
||||
:show-range="false"
|
||||
:refresh-active="autoRefresh"
|
||||
:refresh-title="refreshTitle"
|
||||
@update:range="setRange"
|
||||
@refresh="toggleAutoRefresh"
|
||||
>
|
||||
<template #range-picker>
|
||||
<TimeRangePicker
|
||||
:model-value="{ ...range, preset: relativePreset || undefined }"
|
||||
:preset-options="['last1hour', 'today', 'yesterday', 'last24hours', 'last7days', 'last30days', 'last90days', 'custom']"
|
||||
:show-granularity="false"
|
||||
@update:model-value="handleRangePicker"
|
||||
/>
|
||||
</template>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="exporting"
|
||||
@click="exportCsv('users', requestQuery)"
|
||||
>
|
||||
<OverviewToolbar
|
||||
:title="t('用户分析', 'User analysis')"
|
||||
<Download class="mr-2 h-4 w-4" />{{ t('导出用户报表', 'Export user report') }}
|
||||
</Button>
|
||||
</OverviewToolbar>
|
||||
<OverviewStatus
|
||||
:error="error || exportError"
|
||||
@retry="refreshAll"
|
||||
/>
|
||||
<UserFinanceSummary
|
||||
:summary="data?.data.summary"
|
||||
:finance="data?.data.finance_summary"
|
||||
:user-count="data?.data.summary?.user_count"
|
||||
:active-user-count="data?.data.summary?.active_user_count"
|
||||
/>
|
||||
<UserReports :revision="revision">
|
||||
<template #additional>
|
||||
<UserUsageStats
|
||||
:range="range"
|
||||
:preset="relativePreset"
|
||||
:show-range="false"
|
||||
:refresh-active="autoRefresh"
|
||||
:refresh-title="refreshTitle"
|
||||
presets-only
|
||||
@update:range="setRange"
|
||||
@refresh="toggleAutoRefresh"
|
||||
:revision="revision"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="exporting"
|
||||
@click="exportCsv('users', requestQuery)"
|
||||
>
|
||||
<Download class="mr-2 h-4 w-4" />{{ t('导出用户报表', 'Export user report') }}
|
||||
</Button>
|
||||
</OverviewToolbar>
|
||||
<OverviewStatus
|
||||
:error="error || exportError"
|
||||
@retry="refreshAll"
|
||||
/>
|
||||
<UserFinanceSummary
|
||||
:summary="data?.data.summary"
|
||||
:finance="data?.data.finance_summary"
|
||||
:user-count="data?.data.summary?.user_count"
|
||||
:active-user-count="data?.data.summary?.active_user_count"
|
||||
/>
|
||||
<UserReports :revision="revision" />
|
||||
<TableCard
|
||||
:title="t('用户账目', 'User accounts')"
|
||||
:description="t('消费与到账按所选时间统计,余额为当前值', 'Consumption and credits follow the selected period; balances are current')"
|
||||
class="relative min-w-0"
|
||||
data-user-accounts
|
||||
>
|
||||
<template #actions>
|
||||
<form
|
||||
class="flex w-full items-center gap-2 md:w-auto"
|
||||
@submit.prevent="patch({ search: search.trim() || undefined, offset: undefined })"
|
||||
<template #user-leaderboard="{ selectUser }">
|
||||
<TableCard
|
||||
:title="t('用户排行与账目', 'User rankings and accounts')"
|
||||
:description="t('点击消费、请求数或 Tokens 排序;消费与到账按所选时间统计,余额为当前值', 'Sort by consumption, requests or Tokens; consumption and credits follow the selected period, balances are current')"
|
||||
class="relative min-w-0"
|
||||
data-user-accounts
|
||||
>
|
||||
<div class="relative min-w-0 flex-1 md:w-48 md:flex-none">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||
<Input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="t('搜索用户', 'Search users')"
|
||||
:aria-label="t('搜索用户', 'Search users')"
|
||||
class="h-8 w-full text-xs border-border/60 pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-8 text-xs"
|
||||
>
|
||||
{{ t('查询', 'Search') }}
|
||||
</Button>
|
||||
</form>
|
||||
</template>
|
||||
<p
|
||||
v-if="query.search"
|
||||
class="border-b border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-6"
|
||||
>
|
||||
{{ t('上方汇总与账目仅包含匹配的用户', 'The summary and accounts include matching users only') }}
|
||||
</p>
|
||||
<div
|
||||
class="min-w-0"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<Table class="min-w-[1040px]">
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="h-12 font-semibold">
|
||||
{{ t('用户', 'User') }}
|
||||
</TableHead>
|
||||
<SortableTableHead
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
class="h-12 font-semibold text-right"
|
||||
:column-key="column.key"
|
||||
:active-key="requestQuery.sort"
|
||||
:direction="requestQuery.order"
|
||||
default-direction="desc"
|
||||
align="right"
|
||||
@sort="sort"
|
||||
>
|
||||
{{ column.label }}
|
||||
</SortableTableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('充值', 'Recharges') }}
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('余额', 'Balance') }}
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('最近使用', 'Last used') }}
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('记录', 'Records') }}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="user in data?.data.items || []"
|
||||
:key="user.user_id"
|
||||
class="border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]"
|
||||
<template #actions>
|
||||
<form
|
||||
class="flex w-full items-center gap-2 md:w-auto"
|
||||
@submit.prevent="patch({ search: search.trim() || undefined, offset: undefined })"
|
||||
>
|
||||
<TableCell class="max-w-64 py-4 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="break-words hover:text-primary hover:underline"
|
||||
@click="accountUser = user"
|
||||
>
|
||||
{{ user.username }}
|
||||
</button>
|
||||
<span
|
||||
v-if="!user.is_active"
|
||||
class="ml-2 text-xs text-muted-foreground"
|
||||
>{{ t('停用', 'Disabled') }}</span>
|
||||
<p class="mt-0.5 break-words text-xs text-muted-foreground">
|
||||
{{ user.email }}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class="whitespace-nowrap py-4 text-right text-xs font-medium tabular-nums"
|
||||
:title="user.billable_amount?.value ?? ''"
|
||||
<div class="relative min-w-0 flex-1 md:w-48 md:flex-none">
|
||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground z-10 pointer-events-none" />
|
||||
<Input
|
||||
v-model="search"
|
||||
type="search"
|
||||
:placeholder="t('搜索用户', 'Search users')"
|
||||
:aria-label="t('搜索用户', 'Search users')"
|
||||
class="h-8 w-full text-xs border-border/60 pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-8 text-xs"
|
||||
>
|
||||
{{ money(user.billable_amount) }}
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-right text-xs tabular-nums">
|
||||
{{ count(user.request_count) }}
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-right text-xs tabular-nums">
|
||||
{{ count(user.total_tokens) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs tabular-nums">
|
||||
{{ money(user.finance?.recharge_amount) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs tabular-nums">
|
||||
{{ money(user.finance?.wallet_balance) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs text-muted-foreground">
|
||||
{{ timestamp(user.last_used_at, range.timezone) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:title="t('查看', 'View') + ' ' + user.username + ' ' + t('账目', 'account')"
|
||||
:aria-label="t('查看', 'View') + ' ' + user.username + ' ' + t('账目', 'account')"
|
||||
@click="accountUser = user"
|
||||
{{ t('查询', 'Search') }}
|
||||
</Button>
|
||||
</form>
|
||||
</template>
|
||||
<p
|
||||
v-if="query.search"
|
||||
class="border-b border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-6"
|
||||
>
|
||||
{{ t('上方汇总与账目仅包含匹配的用户', 'The summary and accounts include matching users only') }}
|
||||
</p>
|
||||
<div
|
||||
class="min-w-0"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<Table class="min-w-[1120px]">
|
||||
<TableHeader>
|
||||
<TableRow class="border-b border-border/60 hover:bg-transparent">
|
||||
<TableHead class="h-12 w-16 font-semibold">
|
||||
{{ t('序号', 'No.') }}
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold">
|
||||
{{ t('用户', 'User') }}
|
||||
</TableHead>
|
||||
<SortableTableHead
|
||||
v-for="column in columns"
|
||||
:key="column.key"
|
||||
class="h-12 font-semibold text-right"
|
||||
:column-key="column.key"
|
||||
:active-key="requestQuery.sort"
|
||||
:direction="requestQuery.order"
|
||||
default-direction="desc"
|
||||
align="right"
|
||||
@sort="sort"
|
||||
>
|
||||
{{ column.label }}
|
||||
</SortableTableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('充值', 'Recharges') }}
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('余额', 'Balance') }}
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('最近使用', 'Last used') }}
|
||||
</TableHead>
|
||||
<TableHead class="h-12 font-semibold text-right">
|
||||
{{ t('操作', 'Actions') }}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="(user, index) in data?.data.items || []"
|
||||
:key="user.user_id"
|
||||
class="border-b border-border/40 hover:bg-muted/30 transition-colors h-[72px]"
|
||||
>
|
||||
<ReceiptText class="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="!data?.data.items.length">
|
||||
<TableCell
|
||||
colspan="8"
|
||||
class="py-12 text-center text-muted-foreground"
|
||||
>
|
||||
{{ loading ? t('加载中', 'Loading') : error ? t('用户账目暂不可用', 'User accounts unavailable') : t('没有符合条件的用户', 'No matching users') }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<template #pagination>
|
||||
<Pagination
|
||||
v-if="data"
|
||||
:total="data.data.total"
|
||||
:current="Math.floor(requestQuery.offset / requestQuery.limit) + 1"
|
||||
:page-size="requestQuery.limit"
|
||||
:page-size-options="[25, 50, 100]"
|
||||
@update:current="changePage"
|
||||
@update:page-size="changePageSize"
|
||||
/>
|
||||
<TableCell
|
||||
class="py-4 text-xs font-medium tabular-nums"
|
||||
data-user-rank
|
||||
>
|
||||
{{ (data?.data.offset ?? requestQuery.offset) + index + 1 }}
|
||||
</TableCell>
|
||||
<TableCell class="max-w-64 py-4 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
class="break-words hover:text-primary hover:underline"
|
||||
@click="accountUser = user"
|
||||
>
|
||||
{{ user.username }}
|
||||
</button>
|
||||
<span
|
||||
v-if="!user.is_active"
|
||||
class="ml-2 text-xs text-muted-foreground"
|
||||
>{{ t('停用', 'Disabled') }}</span>
|
||||
<p class="mt-0.5 break-words text-xs text-muted-foreground">
|
||||
{{ user.email }}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class="whitespace-nowrap py-4 text-right text-xs font-medium tabular-nums"
|
||||
:title="user.billable_amount?.value ?? ''"
|
||||
>
|
||||
{{ money(user.billable_amount) }}
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-right text-xs tabular-nums">
|
||||
{{ count(user.request_count) }}
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-right text-xs tabular-nums">
|
||||
{{ count(user.total_tokens) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs tabular-nums">
|
||||
{{ money(user.finance?.recharge_amount) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs tabular-nums">
|
||||
{{ money(user.finance?.wallet_balance) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs text-muted-foreground">
|
||||
{{ timestamp(user.last_used_at, range.timezone) }}
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap py-4 text-right text-xs">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:title="t('查看', 'View') + ' ' + user.username + ' ' + t('使用趋势', 'usage trend')"
|
||||
:aria-label="t('查看', 'View') + ' ' + user.username + ' ' + t('使用趋势', 'usage trend')"
|
||||
@click="selectUser(user)"
|
||||
>
|
||||
<ChartNoAxesCombined class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:title="t('查看', 'View') + ' ' + user.username + ' ' + t('账目', 'account')"
|
||||
:aria-label="t('查看', 'View') + ' ' + user.username + ' ' + t('账目', 'account')"
|
||||
@click="accountUser = user"
|
||||
>
|
||||
<ReceiptText class="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="!data?.data.items.length">
|
||||
<TableCell
|
||||
colspan="9"
|
||||
class="py-12 text-center text-muted-foreground"
|
||||
>
|
||||
{{ loading ? t('加载中', 'Loading') : error ? t('用户账目暂不可用', 'User accounts unavailable') : t('没有符合条件的用户', 'No matching users') }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<template #pagination>
|
||||
<Pagination
|
||||
v-if="data"
|
||||
:total="data.data.total"
|
||||
:current="Math.floor(requestQuery.offset / requestQuery.limit) + 1"
|
||||
:page-size="requestQuery.limit"
|
||||
:page-size-options="[25, 50, 100]"
|
||||
@update:current="changePage"
|
||||
@update:page-size="changePageSize"
|
||||
/>
|
||||
</template>
|
||||
</TableCard>
|
||||
</template>
|
||||
</TableCard>
|
||||
<UserAccountDrawer
|
||||
v-if="accountUser"
|
||||
:key="accountUser.user_id"
|
||||
:user="accountUser"
|
||||
:timezone="range.timezone"
|
||||
@close="accountUser = null"
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="usage">
|
||||
<UserUsageStats v-if="activeTab === 'usage'" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</UserUsageStats>
|
||||
</template>
|
||||
</UserReports>
|
||||
<UserAccountDrawer
|
||||
v-if="accountUser"
|
||||
:key="accountUser.user_id"
|
||||
:user="accountUser"
|
||||
:timezone="range.timezone"
|
||||
@close="accountUser = null"
|
||||
/>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue'
|
||||
import { Download, Search, ReceiptText } from 'lucide-vue-next'
|
||||
import { Button, Input, Pagination, SortableTableHead, Table, TableBody, TableCard, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Download, Search, ReceiptText, ChartNoAxesCombined } from 'lucide-vue-next'
|
||||
import { Button, Input, Pagination, SortableTableHead, Table, TableBody, TableCard, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui'
|
||||
import TimeRangePicker from '@/components/common/TimeRangePicker.vue'
|
||||
import type { DateRangeParams } from '@/features/usage/types'
|
||||
import { overviewApi } from '@/api/overview'
|
||||
import OverviewToolbar from '@/features/overview/components/OverviewToolbar.vue'
|
||||
import OverviewStatus from '@/features/overview/components/OverviewStatus.vue'
|
||||
import UserFinanceSummary from '@/features/overview/users/UserFinanceSummary.vue'
|
||||
import UserReports from '@/features/overview/users/UserReports.vue'
|
||||
import UserAccountDrawer from '@/features/overview/users/UserAccountDrawer.vue'
|
||||
import UserUsageStats from '@/features/overview/users/UserUsageStats.vue'
|
||||
import { useUserAnalysisRefresh } from '@/features/overview/users/useUserAnalysisRefresh'
|
||||
import { useOverviewQuery } from '@/features/overview/query'
|
||||
import { presetRange, rangeFromQuery, useOverviewQuery } from '@/features/overview/query'
|
||||
import { useOverviewRequest } from '@/features/overview/useOverviewRequest'
|
||||
import { useOverviewExport } from '@/features/overview/useOverviewExport'
|
||||
import { useOverviewI18n } from '@/features/overview/i18n'
|
||||
import { count, money, timestamp } from '@/features/overview/format'
|
||||
const UserUsageStats = defineAsyncComponent(() => import('@/features/overview/users/UserUsageStats.vue'))
|
||||
const activeTab = ref('accounts')
|
||||
const { t } = useOverviewI18n()
|
||||
const { query, range, relativePreset, refreshRange, setRange, patch } = useOverviewQuery('today', { rolling: true })
|
||||
const { revision, autoRefresh, refreshTitle, refreshAll, toggleAutoRefresh } = useUserAnalysisRefresh(refreshRange)
|
||||
@@ -249,6 +267,29 @@ const scope = computed(() => JSON.stringify({ ...requestQuery.value, ...(relativ
|
||||
const { data, loading, error } = useOverviewRequest(() => JSON.stringify([requestQuery.value, revision.value]), signal => overviewApi.users(requestQuery.value, signal), { scopeKey: scope })
|
||||
const { exporting, exportError, exportCsv } = useOverviewExport()
|
||||
const columns = computed(() => [{ key: 'billable_amount', label: t('消费', 'Consumption') }, { key: 'request_count', label: t('请求数', 'Requests') }, { key: 'total_tokens', label: 'Tokens' }])
|
||||
function sameRange(left: DateRangeParams, right: typeof range.value): boolean {
|
||||
return left.from === right.from && left.to === right.to && (left.timezone || right.timezone) === right.timezone
|
||||
}
|
||||
function handleRangePicker(value: DateRangeParams) {
|
||||
const timezone = value.timezone || range.value.timezone
|
||||
if (value.preset) {
|
||||
if (value.preset === relativePreset.value) return
|
||||
const next = value.preset === 'yesterday'
|
||||
? rangeFromQuery({ preset: value.preset, timezone }, range.value)
|
||||
: presetRange(value.preset, timezone)
|
||||
void setRange(next, value.preset)
|
||||
return
|
||||
}
|
||||
if (value.from && value.to) {
|
||||
if (sameRange(value, range.value)) return
|
||||
void setRange({ from: value.from, to: value.to, timezone }, undefined)
|
||||
return
|
||||
}
|
||||
if (value.start_date && value.end_date) {
|
||||
const next = rangeFromQuery({ start_date: value.start_date, end_date: value.end_date, timezone }, range.value)
|
||||
if (!sameRange(next, range.value)) void setRange(next)
|
||||
}
|
||||
}
|
||||
function sort({ key, direction }: { key: string; direction: 'asc' | 'desc' }) {
|
||||
void patch({ sort: key, order: direction, offset: undefined })
|
||||
}
|
||||
|
||||
@@ -46,15 +46,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分析统计 -->
|
||||
<div
|
||||
v-if="isAdminPage"
|
||||
class="flex flex-wrap items-baseline gap-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<h2 class="text-sm font-medium text-foreground">
|
||||
{{ overviewT('全站时段统计', 'Site-wide period statistics') }}
|
||||
</h2><span>{{ analyticsRangeLabel }}</span>
|
||||
</div>
|
||||
<!-- 管理员:模型 + 提供商 + API格式(3列) -->
|
||||
<div
|
||||
v-if="isAdminPage"
|
||||
@@ -229,7 +220,7 @@ import type { ActivityHeatmap } from '@/types/activity'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { queryString, rangeFromQuery, presetRange } from '@/features/overview/query'
|
||||
import { useOverviewI18n } from '@/features/overview/i18n'
|
||||
import { attributionLabel, timestamp } from '@/features/overview/format'
|
||||
import { attributionLabel } from '@/features/overview/format'
|
||||
import { Button } from '@/components/ui'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -314,13 +305,6 @@ const hasScopedRecordFilters = computed(() => isAdminPage.value && (
|
||||
filterSearch.value.trim() !== '' || [filterUser.value, filterModel.value, filterProvider.value, filterApiFormat.value, filterStatus.value, filterClientFamily.value].some(value => value !== '__all__') ||
|
||||
!!filterProviderId.value || !!filterApiKeyId.value || !!filterRequestId.value || !!filterAttribution.value || !!filterEndpointKind.value || !!filterRequestType.value || filterIsStream.value !== undefined || filterFormatConversion.value !== undefined || filterSlowThreshold.value !== undefined || hideUnknownRecords.value
|
||||
))
|
||||
const analyticsRangeLabel = computed(() => {
|
||||
const range = timeRange.value
|
||||
if (range.from && range.to) return `${timestamp(range.from, range.timezone)} - ${timestamp(range.to, range.timezone)} · ${range.timezone || 'UTC'}`
|
||||
if (range.start_date && range.end_date) return `${range.start_date} - ${range.end_date} · ${range.timezone || 'UTC'}`
|
||||
const presets: Record<string, string> = { today: overviewT('今天', 'Today'), yesterday: overviewT('昨天', 'Yesterday'), last7days: overviewT('最近 7 天', 'Last 7 days'), last30days: overviewT('最近 30 天', 'Last 30 days'), last90days: overviewT('最近 90 天', 'Last 90 days') }
|
||||
return `${presets[range.preset || ''] || overviewT('全部时间', 'All time')} · ${range.timezone || 'UTC'}`
|
||||
})
|
||||
function clearRecordFilters() {
|
||||
const fields = ['search', 'user_id', 'model', 'provider', 'api_format', 'status', 'client_family', 'provider_id', 'api_key_id', 'request_id', 'attribution_kind', 'endpoint_kind', 'request_type', 'is_stream', 'has_format_conversion', 'slow_threshold_ms', 'detail_id', 'page']
|
||||
hideUnknownRecords.value = false
|
||||
|
||||
Reference in New Issue
Block a user