feat: 调用日志补模型筛选与汇总条——可按模型收窄查询,并在当前筛选下直接读出请求次数与消费用量

This commit is contained in:
2026-10-14 08:01:58 +08:00
parent b0e9ea750b
commit 199972a2dc
17 changed files with 705 additions and 84 deletions
+4 -5
View File
@@ -1,4 +1,4 @@
import type { LogItem, Paginated } from "@new-api-key-usage/core";
import type { LogPage } from "@new-api-key-usage/core";
import { adminHttp } from "../http.ts";
export type AdminLogFilters = {
@@ -10,13 +10,12 @@ export type AdminLogFilters = {
member_id?: number | "unassigned";
billing_source?: string;
subscription_id?: number;
model_name?: string;
type?: number | "error";
};
export async function fetchLogs(
params: AdminLogFilters,
): Promise<Paginated<LogItem>> {
const { data } = await adminHttp.get<Paginated<LogItem>>("/logs", {
export async function fetchLogs(params: AdminLogFilters): Promise<LogPage> {
const { data } = await adminHttp.get<LogPage>("/logs", {
params,
});
return data;
+4 -4
View File
@@ -1,6 +1,5 @@
import type {
LogItem,
Paginated,
LogPage,
SubscriptionItem,
TopupItem,
UnixSec,
@@ -55,11 +54,12 @@ export type MemberLogQuery = {
api_key_id?: number;
billing_source?: string;
subscription_id?: number;
model_name?: string;
type?: number | "error";
};
export async function fetchLogs(query: MemberLogQuery): Promise<Paginated<LogItem>> {
const { data } = await memberHttp.get<Paginated<LogItem>>("/logs", {
export async function fetchLogs(query: MemberLogQuery): Promise<LogPage> {
const { data } = await memberHttp.get<LogPage>("/logs", {
params: query,
});
return data;
+2
View File
@@ -36,6 +36,7 @@ export const qk = {
range.page_size,
range.api_key_id ?? "",
range.member_id ?? "",
range.model_name ?? "",
range.billing_source ?? "",
range.subscription_id ?? "",
range.type ?? "",
@@ -60,6 +61,7 @@ export const qk = {
range.page,
range.page_size,
range.api_key_id ?? "",
range.model_name ?? "",
range.billing_source ?? "",
range.subscription_id ?? "",
range.type ?? "",
@@ -0,0 +1,74 @@
<script setup lang="ts">
/**
* 调用日志汇总条(交互设计-调用日志 §2.5):
* 单行低高度——请求次数 + 消费用量;用量恒 type=2 口径由上层保证。
*/
import QuotaText from "./quota-text.vue";
defineProps<{
hitCount: number;
usageCount: number;
errorCount: number;
quota: number;
quotaUsd: string;
loading?: boolean;
}>();
</script>
<template>
<div class="logs-summary-bar" :class="{ 'is-loading': loading }">
<div class="logs-summary-bar__seg">
<span class="logs-summary-bar__label">请求次数</span>
<span class="logs-summary-bar__value">
<template v-if="loading">—</template>
<template v-else>
{{ hitCount }} 条<template v-if="errorCount > 0">
(消费 {{ usageCount }} / 错误 {{ errorCount }})</template>
</template>
</span>
</div>
<div class="logs-summary-bar__seg">
<span class="logs-summary-bar__label">消费用量</span>
<span class="logs-summary-bar__value">
<QuotaText v-if="!loading" :quota="quota" :quota-usd="quotaUsd" />
<template v-else>—</template>
</span>
</div>
</div>
</template>
<style scoped>
.logs-summary-bar {
display: flex;
align-items: center;
gap: 24px;
min-height: 36px;
padding: 0 12px;
margin-bottom: 12px;
border: 1px solid #e8ecf4;
border-radius: 6px;
background: #f7f9fc;
}
.logs-summary-bar.is-loading {
opacity: 0.55;
}
.logs-summary-bar__seg {
display: inline-flex;
align-items: baseline;
gap: 8px;
white-space: nowrap;
}
.logs-summary-bar__label {
font-size: 12px;
color: #8a8f99;
}
.logs-summary-bar__value {
font-size: 13px;
font-weight: 500;
color: #1f2329;
}
</style>
+30 -1
View File
@@ -9,6 +9,7 @@ import PageHeader from "@/components/common/page-header.vue";
import TableRefresh from "@/components/common/table-refresh.vue";
import TimeRangePicker from "@/components/common/time-range-picker.vue";
import QuotaText from "@/components/common/quota-text.vue";
import LogsSummaryBar from "@/components/common/logs-summary-bar.vue";
import {
useLiveTimeRange,
type RangeKind,
@@ -23,6 +24,7 @@ const { range, setRange } = useLiveTimeRange("recent-30");
const memberId = ref<number | "unassigned" | null>(null);
const apiKeyId = ref<number | null>(null);
const modelFilter = ref<string | null>(null);
const billingSource = ref<string | null>(null);
const subscriptionId = ref<number | null>(null);
const typeFilter = ref<number | "error" | null>(null);
@@ -50,6 +52,7 @@ const params = computed(() => ({
page_size: pageSize.value,
api_key_id: apiKeyId.value ?? undefined,
member_id: memberId.value ?? undefined,
model_name: modelFilter.value ?? undefined,
billing_source: billingSource.value ?? undefined,
subscription_id: subscriptionId.value ?? undefined,
type: typeFilter.value ?? undefined,
@@ -58,6 +61,11 @@ const params = computed(() => ({
const logsQuery = useAdminLogs(() => params.value);
const rows = computed(() => logsQuery.data.value?.items ?? []);
const total = computed(() => logsQuery.data.value?.total ?? 0);
const summary = computed(() => logsQuery.data.value?.summary);
const modelOptions = computed(() => [
{ value: null, label: "全部模型" },
...(logsQuery.data.value?.models ?? []).map((m) => ({ value: m, label: m })),
]);
const memberOptions = computed(() => [
{ value: null, label: "全部成员" },
@@ -105,6 +113,8 @@ function syncFromRoute() {
else if (typeof m === "string" && m) memberId.value = Number(m);
const k = q.api_key_id ?? q.key_id;
if (typeof k === "string" && k) apiKeyId.value = Number(k);
modelFilter.value =
typeof q.model_name === "string" && q.model_name ? q.model_name : null;
if (typeof q.billing_source === "string" && q.billing_source) {
billingSource.value = q.billing_source;
}
@@ -131,6 +141,7 @@ function pushQuery() {
if (memberId.value === "unassigned") query.member_id = "unassigned";
else if (memberId.value != null) query.member_id = String(memberId.value);
if (apiKeyId.value != null) query.api_key_id = String(apiKeyId.value);
if (modelFilter.value) query.model_name = modelFilter.value;
if (billingSource.value) query.billing_source = billingSource.value;
if (subscriptionId.value != null) {
query.subscription_id = String(subscriptionId.value);
@@ -146,7 +157,7 @@ watch(
() => syncFromRoute(),
);
watch(
[memberId, apiKeyId, billingSource, subscriptionId, typeFilter],
[memberId, apiKeyId, modelFilter, billingSource, subscriptionId, typeFilter],
() => {
page.value = 1;
pushQuery();
@@ -245,6 +256,15 @@ onUnmounted(() => {
show-search
option-filter-prop="label"
/>
<a-select
v-model:value="modelFilter"
:options="modelOptions"
style="width: 140px"
allow-clear
placeholder="模型"
show-search
option-filter-prop="label"
/>
<a-select
v-model:value="billingSource"
:options="[
@@ -276,6 +296,15 @@ onUnmounted(() => {
/>
</div>
<LogsSummaryBar
:hit-count="summary?.hit_count ?? 0"
:usage-count="summary?.usage_count ?? 0"
:error-count="summary?.error_count ?? 0"
:quota="summary?.quota ?? 0"
:quota-usd="summary?.quota_usd ?? '0.00'"
:loading="logsQuery.isPending.value || logsQuery.isFetching.value"
/>
<TableRefresh
:loading="logsQuery.isFetching.value"
@refresh="() => logsQuery.refetch()"
+30 -1
View File
@@ -12,6 +12,7 @@ import PageHeader from "@/components/common/page-header.vue";
import TableRefresh from "@/components/common/table-refresh.vue";
import TimeRangePicker from "@/components/common/time-range-picker.vue";
import QuotaText from "@/components/common/quota-text.vue";
import LogsSummaryBar from "@/components/common/logs-summary-bar.vue";
import { useMemberRange } from "@/composables/use-member-range.ts";
import type { RangeKind } from "@/stores/member-range.ts";
import { useMemberLogs, useMemberOverview } from "@/queries/member.ts";
@@ -23,6 +24,7 @@ const router = useRouter();
const { range, setRange } = useMemberRange();
const apiKeyId = ref<number | null>(null);
const modelFilter = ref<string | null>(null);
const billingSource = ref<string | null>(null);
const subscriptionId = ref<number | null>(null);
const typeFilter = ref<number | "error" | null>(null);
@@ -40,6 +42,7 @@ const params = computed(() => ({
page: page.value,
page_size: pageSize.value,
api_key_id: apiKeyId.value ?? undefined,
model_name: modelFilter.value ?? undefined,
billing_source: billingSource.value ?? undefined,
subscription_id: subscriptionId.value ?? undefined,
type: typeFilter.value ?? undefined,
@@ -48,6 +51,11 @@ const params = computed(() => ({
const logsQuery = useMemberLogs(() => params.value);
const rows = computed(() => logsQuery.data.value?.items ?? []);
const total = computed(() => logsQuery.data.value?.total ?? 0);
const summary = computed(() => logsQuery.data.value?.summary);
const modelOptions = computed(() => [
{ value: null, label: "全部模型" },
...(logsQuery.data.value?.models ?? []).map((m) => ({ value: m, label: m })),
]);
const keyOptions = computed(() => [
{ value: null, label: "全部 Key" },
@@ -137,6 +145,8 @@ function syncFromRoute() {
const q = route.query;
const k = q.api_key_id ?? q.key_id;
apiKeyId.value = typeof k === "string" && k ? Number(k) || null : null;
modelFilter.value =
typeof q.model_name === "string" && q.model_name ? q.model_name : null;
billingSource.value =
typeof q.billing_source === "string" && q.billing_source
? q.billing_source
@@ -160,6 +170,7 @@ function pushQuery() {
to: range.value.to,
};
if (apiKeyId.value != null) query.api_key_id = String(apiKeyId.value);
if (modelFilter.value) query.model_name = modelFilter.value;
if (billingSource.value) query.billing_source = billingSource.value;
if (subscriptionId.value != null) {
query.subscription_id = String(subscriptionId.value);
@@ -171,7 +182,7 @@ function pushQuery() {
syncFromRoute();
watch(() => route.query, () => syncFromRoute());
watch([apiKeyId, billingSource, subscriptionId, typeFilter], () => {
watch([apiKeyId, modelFilter, billingSource, subscriptionId, typeFilter], () => {
if (applyingRoute.value) return;
page.value = 1;
pushQuery();
@@ -246,6 +257,15 @@ watch(
show-search
option-filter-prop="label"
/>
<a-select
v-model:value="modelFilter"
:options="modelOptions"
style="width: 140px"
allow-clear
placeholder="模型"
show-search
option-filter-prop="label"
/>
<a-select
v-model:value="billingSource"
:options="[
@@ -277,6 +297,15 @@ watch(
/>
</div>
<LogsSummaryBar
:hit-count="summary?.hit_count ?? 0"
:usage-count="summary?.usage_count ?? 0"
:error-count="summary?.error_count ?? 0"
:quota="summary?.quota ?? 0"
:quota-usd="summary?.quota_usd ?? '0.00'"
:loading="logsQuery.isPending.value || logsQuery.isFetching.value"
/>
<TableRefresh
:loading="logsQuery.isFetching.value"
@refresh="() => logsQuery.refetch()"