diff --git a/web/src/types.ts b/web/src/types.ts index 656c3d1..0f4a1ab 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -94,6 +94,7 @@ export interface UsageLog { raw_response?: string created_at: string user?: string + user_id?: number } export interface Paged { diff --git a/web/src/views/admin/UsageView.vue b/web/src/views/admin/UsageView.vue index bcb5aa2..d42e940 100644 --- a/web/src/views/admin/UsageView.vue +++ b/web/src/views/admin/UsageView.vue @@ -7,13 +7,16 @@ import { fmtCost, fmtTime } from '@/lib/format' import Badge from '@/components/ui/Badge.vue' import Button from '@/components/ui/Button.vue' import Modal from '@/components/ui/Modal.vue' -import type { UsageLog } from '@/types' +import type { UsageLog, User } from '@/types' const toast = useToastStore() const logs = ref([]) const total = ref(0) const page = ref(1) const modelFilter = ref('') +const userFilter = ref('') +const userCandidates = ref([]) +let userSearchTimer: ReturnType | null = null const pageSize = 15 const viewing = ref(null) @@ -24,11 +27,47 @@ function openRaw(l: UsageLog) { viewTab.value = 'request' } +// 用户自动补全候选(防抖 300ms) +function onUserInput() { + if (userSearchTimer) clearTimeout(userSearchTimer) + const kw = userFilter.value.trim() + if (!kw) { + userCandidates.value = [] + return + } + userSearchTimer = setTimeout(async () => { + try { + const { data } = await http.get(`/admin/users?q=${encodeURIComponent(kw)}&page_size=10`) + userCandidates.value = data.data.items ?? [] + } catch { + userCandidates.value = [] + } + }, 300) +} + +// 用户名 → user_id(精确匹配),查不到返回 null +async function resolveUserId(username: string): Promise { + const { data } = await http.get(`/admin/users?q=${encodeURIComponent(username)}&page_size=10`) + const items: User[] = data.data.items ?? [] + const hit = items.find((u) => u.username === username) + return hit ? hit.id : null +} + async function load() { try { - const { data } = await http.get( - `/admin/usage?page=${page.value}&page_size=${pageSize}${modelFilter.value ? '&model=' + modelFilter.value : ''}`, - ) + const params = new URLSearchParams() + params.set('page', String(page.value)) + params.set('page_size', String(pageSize)) + if (modelFilter.value.trim()) params.set('model', modelFilter.value.trim()) + if (userFilter.value.trim()) { + const uid = await resolveUserId(userFilter.value.trim()) + if (uid === null) { + toast.err(`未找到用户:${userFilter.value.trim()}`) + return + } + params.set('user_id', String(uid)) + } + const { data } = await http.get(`/admin/usage?${params.toString()}`) logs.value = data.data.items total.value = data.data.total } catch (e) { @@ -41,6 +80,12 @@ function search() { load() } +function clearUser() { + userFilter.value = '' + userCandidates.value = [] + search() +} + function goPage(p: number) { page.value = p load() @@ -56,7 +101,28 @@ onMounted(load)

用量统计

全局请求明细与成本

-
+
+
+ + +
+ +