diff --git a/apps/web/src/views/admin/logs-view.vue b/apps/web/src/views/admin/logs-view.vue index 88fb125..3d6df6e 100644 --- a/apps/web/src/views/admin/logs-view.vue +++ b/apps/web/src/views/admin/logs-view.vue @@ -73,7 +73,7 @@ const columns = [ { title: "时间", key: "created_at", width: 150 }, { title: "Key", key: "token_name", width: 120 }, { title: "归属", key: "member_name", width: 110 }, - { title: "模型", key: "model_name", width: 110 }, + { title: "模型", key: "model_name", dataIndex: "model_name", width: 110 }, { title: "类型", key: "type", width: 80 }, { title: "来源", key: "source", width: 110 }, { title: "用量", key: "usage", width: 140 }, @@ -127,10 +127,13 @@ watch( () => route.query, () => syncFromRoute(), ); -watch([memberId, apiKeyId, billingSource, subscriptionId, typeFilter, range], () => { - page.value = 1; - pushQuery(); -}); +watch( + [memberId, apiKeyId, billingSource, subscriptionId, typeFilter], + () => { + page.value = 1; + pushQuery(); + }, +); watch(page, () => pushQuery()); function sourceBadge(record: LogItem) { @@ -185,7 +188,16 @@ onUnmounted(() => { diff --git a/apps/web/src/views/member/home-view.vue b/apps/web/src/views/member/home-view.vue index 1c43fd9..2f41519 100644 --- a/apps/web/src/views/member/home-view.vue +++ b/apps/web/src/views/member/home-view.vue @@ -2,8 +2,11 @@ /** * 成员视图单页(交互设计-成员视图):聚合 · 支付参数 · 我的 key · 日志徽章展开 */ -import { computed, ref, watch } from "vue"; +import { computed, h, onMounted, onUnmounted, ref, watch } from "vue"; import type { LogItem, PaymentItem } from "@new-api-key-usage/core"; +import { Tooltip } from "antdv-next"; +import { QuestionCircleOutlined } from "@antdv-next/icons"; +import { useRoute, useRouter } from "vue-router"; import PageHeader from "@/components/common/page-header.vue"; import TimeRangePicker from "@/components/common/time-range-picker.vue"; import QuotaText from "@/components/common/quota-text.vue"; @@ -13,6 +16,8 @@ import { useMemberLogs, useMemberOverview, useMemberMe } from "@/queries/member. import { formatDateTime, formatMinute, hoursSince } from "@/utils/time.ts"; import { formatInt } from "@/utils/format.ts"; +const route = useRoute(); +const router = useRouter(); const { range, setRange } = useLiveTimeRange("recent-30"); const meQuery = useMemberMe(); const overviewQuery = useMemberOverview(() => range.value); @@ -55,22 +60,71 @@ const paymentBySub = computed(() => { const logs = computed(() => logsQuery.data.value?.items ?? []); const logTotal = computed(() => logsQuery.data.value?.total ?? 0); - const expandedRowKeys = ref([]); +function tipTitle(label: string, tip: string) { + return h("span", { class: "mv-col-title" }, [ + label, + h( + Tooltip, + { title: tip }, + () => + h(QuestionCircleOutlined, { + class: "mv-col-title__icon", + "aria-hidden": "true", + }), + ), + ]); +} + const logColumns = [ { title: "时间", key: "created_at", width: 150 }, - { title: "Key", key: "token_name", width: 120 }, - { title: "模型", key: "model_name", width: 120 }, + { title: "Key", key: "token_name", dataIndex: "token_name", width: 120 }, + { + title: "模型", + key: "model_name", + dataIndex: "model_name", + width: 120, + }, { title: "来源", key: "source", width: 120 }, { title: "用量", key: "usage", width: 150 }, - { title: "Tokens", key: "tokens", width: 110 }, + { + title: tipTitle("Tokens", "提示 tokens / 补全 tokens"), + key: "tokens", + width: 110, + }, ]; -watch(range, () => { +function syncFromRoute() { + const from = route.query.from; + const to = route.query.to; + if (typeof from === "string" && from && typeof to === "string" && to) { + setRange({ from, to }, "fixed"); + } +} + +function pushRangeQuery() { + void router.replace({ + path: "/member", + query: { from: range.value.from, to: range.value.to }, + }); +} + +syncFromRoute(); +watch( + () => route.query, + () => syncFromRoute(), +); + +function onRangeChange( + v: { from: string; to: string }, + kind: "recent-7" | "recent-30" | "fixed", +) { + setRange(v, kind); page.value = 1; expandedRowKeys.value = []; -}); + pushRangeQuery(); +} function isStalled(at: number | null | undefined): boolean { const h = hoursSince(at); @@ -79,7 +133,11 @@ function isStalled(at: number | null | undefined): boolean { function sourceBadge(record: LogItem) { if (record.billing_source === "subscription" && record.subscription_id != null) { - return { text: `订阅 #${record.subscription_id}`, kind: "sub" as const, id: record.subscription_id }; + return { + text: `订阅 #${record.subscription_id}`, + kind: "sub" as const, + id: record.subscription_id, + }; } if (record.billing_source === "wallet") { return { text: "充值", kind: "wallet" as const, id: null }; @@ -95,30 +153,49 @@ function onBadge(record: LogItem) { else expandedRowKeys.value.push(record.id); } -function paymentText(p: PaymentItem | undefined): string { - if (!p) return "该订阅无登记信息"; - const period = - p.kind === "subscription" - ? `${formatDateTime(p.period_start)} ~ ${p.period_end == null ? "进行中" : formatDateTime(p.period_end)}` - : formatDateTime(p.paid_at); - return `${period} · 订阅 #${p.subscription_id ?? p.id} · 额度 ${formatInt(p.quota_granted)} ($${p.quota_usd}) · 金额 ${p.paid_amount}${p.remark ? ` · ${p.remark}` : ""}`; -} - -function onRangeChange(v: { from: string; to: string }, kind: "recent-7" | "recent-30" | "fixed") { - setRange(v, kind); -} - function onExpand(expanded: boolean, record: LogItem) { const i = expandedRowKeys.value.indexOf(record.id); if (expanded && i < 0) expandedRowKeys.value.push(record.id); if (!expanded && i >= 0) expandedRowKeys.value.splice(i, 1); } + +/** 表体滚动,避免整页被明细撑高 */ +const boxRef = ref(null); +const bodyY = ref(420); +const CHROME = 120; +function measure() { + const h = boxRef.value?.clientHeight ?? 0; + if (h > 0) bodyY.value = Math.max(220, h - CHROME); +} +let ro: ResizeObserver | null = null; +onMounted(() => { + measure(); + if (typeof ResizeObserver !== "undefined" && boxRef.value) { + ro = new ResizeObserver(() => measure()); + ro.observe(boxRef.value); + } + window.addEventListener("resize", measure); +}); +onUnmounted(() => { + ro?.disconnect(); + window.removeEventListener("resize", measure); +}); + +function periodText(p: PaymentItem): string { + if (p.kind !== "subscription") return formatDateTime(p.paid_at); + const end = p.period_end == null ? "进行中" : formatDateTime(p.period_end); + return `${formatDateTime(p.period_start)} ~ ${end}`; +} + +function expandPayment(record: LogItem): PaymentItem | undefined { + return paymentBySub.value.get(sourceBadge(record).id ?? -1); +}