diff --git a/README.md b/README.md index c1a0212..3c2d192 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,10 @@ docker compose ps # migrate 应为 exited(0),其余 running/healthy | [`docs/design/交互设计-总览与共性.md`](docs/design/交互设计-总览与共性.md) | 交互总纲:访问拓扑、导航、跨页组件、状态与数值呈现约定 | 定稿(2026-10-01) | | [`docs/design/交互设计-登录与会话.md`](docs/design/交互设计-登录与会话.md) | 登录页、引导页、改密、成员登录壳 | 同上 | | [`docs/design/交互设计-全员总览.md`](docs/design/交互设计-全员总览.md) | 管理面首页:健康带 + 按成员 / key 汇总 | 同上 | -| [`docs/design/交互设计-调用日志.md`](docs/design/交互设计-调用日志.md) | 调用日志:筛选、汇总条、明细表、与 key/成员/总览的跳转链 | 定稿(2026-10-08;模型筛选与汇总条修订) | +| [`docs/design/交互设计-调用日志.md`](docs/design/交互设计-调用日志.md) | 调用日志:筛选、汇总条、来源徽章支付弹窗、明细表与跳转链 | 定稿(2026-10-08;模型筛选与汇总条、徽章弹窗修订) | | [`docs/design/实现方案-日志模型筛选与汇总条.md`](docs/design/实现方案-日志模型筛选与汇总条.md) | 日志页模型筛选 + 条数/消费用量汇总条:API、数据层、前端改造与验收 | 已实现(2026-10-13) | | [`docs/design/交互设计-key管理.md`](docs/design/交互设计-key管理.md) | 登记(sk-key 唯一界面露面)、启停、归属、死亡显形 | 定稿(2026-10-01) | -| [`docs/design/交互设计-支付记录.md`](docs/design/交互设计-支付记录.md) | 支付记录(两 Tab):订阅权益 / 充值入账登记、核对提示 | 定稿(2026-10-12) | +| [`docs/design/交互设计-支付记录.md`](docs/design/交互设计-支付记录.md) | 支付记录(两 Tab):订阅权益 / 充值入账登记、核对提示、ID/交易号筛选 | 定稿(2026-10-12;补筛选) | | [`docs/design/交互设计-成员管理.md`](docs/design/交互设计-成员管理.md) | 开户、档案修改、令牌签发/重发、启停、空壳删除 | 定稿(2026-10-10) | | [`docs/design/交互设计-审计日志.md`](docs/design/交互设计-审计日志.md) | 管理操作留痕时间线 | 定稿(2026-10-10) | | [`docs/design/交互设计-成员视图.md`](docs/design/交互设计-成员视图.md) | 成员视图三页(总览 / 调用日志 / 支付参数);订阅徽章展开 + 调用详情 | 定稿(2026-10-09) | diff --git a/apps/web/src/views/admin/logs-view.vue b/apps/web/src/views/admin/logs-view.vue index 87df3ac..04f0ef7 100644 --- a/apps/web/src/views/admin/logs-view.vue +++ b/apps/web/src/views/admin/logs-view.vue @@ -3,7 +3,7 @@ * 管理面调用日志(交互设计-调用日志):筛选 + 明细表 + 与 key/成员跳转 */ import { computed, onMounted, onUnmounted, ref, watch } from "vue"; -import type { LogItem } from "@new-api-key-usage/core"; +import type { LogItem, SubscriptionItem } from "@new-api-key-usage/core"; import { useRoute, useRouter } from "vue-router"; import PageHeader from "@/components/common/page-header.vue"; import TableRefresh from "@/components/common/table-refresh.vue"; @@ -14,9 +14,9 @@ import { useLiveTimeRange, type RangeKind, } from "@/composables/use-live-time-range.ts"; -import { useAdminLogs, useKeys, useMembers } from "@/queries/admin.ts"; +import { useAdminLogs, useKeys, useMembers, useSubscriptions } from "@/queries/admin.ts"; import { formatDateTime } from "@/utils/time.ts"; -import { formatInt } from "@/utils/format.ts"; +import { formatInt, formatPaidAmount } from "@/utils/format.ts"; const route = useRoute(); const router = useRouter(); @@ -167,12 +167,54 @@ watch(page, () => pushQuery()); function sourceBadge(record: LogItem) { if (record.billing_source === "subscription" && record.subscription_id != null) { - return { text: `订阅 #${record.subscription_id}`, color: "blue" as const }; + return { + text: `订阅 #${record.subscription_id}`, + color: "blue" as const, + kind: "sub" as const, + id: record.subscription_id, + }; } if (record.billing_source === "wallet") { - return { text: "充值", color: "gold" as const }; + return { text: "充值", color: "gold" as const, kind: "wallet" as const, id: null }; } - return { text: "未关联", color: undefined }; + return { text: "未关联", color: undefined, kind: "none" as const, id: null }; +} + +const payModalOpen = ref(false); +const payModalSubId = ref(null); +const subsQuery = useSubscriptions(); +const paymentBySub = computed(() => { + const m = new Map(); + for (const p of subsQuery.data.value ?? []) { + m.set(p.subscription_id, p); + } + return m; +}); +const payModalPayment = computed(() => + payModalSubId.value == null + ? undefined + : paymentBySub.value.get(payModalSubId.value), +); + +function onBadge(record: LogItem, e: MouseEvent) { + e.stopPropagation(); + if (sourceBadge(record).kind !== "sub") return; + payModalSubId.value = sourceBadge(record).id; + payModalOpen.value = true; +} + +function periodText(p: SubscriptionItem): string { + const end = p.period_end == null ? "进行中" : formatDateTime(p.period_end); + return `${formatDateTime(p.period_start)} ~ ${end}`; +} + +function goPayments() { + if (payModalSubId.value == null) return; + payModalOpen.value = false; + void router.push({ + path: "/admin/payments", + query: { tab: "subs", subscription_id: String(payModalSubId.value) }, + }); } function goKey(keyId: number) { @@ -354,7 +396,19 @@ onUnmounted(() => { @@ -432,6 +486,41 @@ onUnmounted(() => { + + +
+
+ 期间 + {{ periodText(payModalPayment) }} +
+
+ 额度 + +
+
+ 金额 + {{ formatPaidAmount(payModalPayment.paid_amount) }} +
+
+ 备注 + {{ payModalPayment.remark }} +
+
+
该订阅无登记信息
+ +
@@ -508,4 +597,21 @@ onUnmounted(() => { line-height: 20px; font-family: ui-monospace, "Cascadia Code", Consolas, monospace; } + +.call-logs__badge { + cursor: pointer; +} + +.call-logs__pay { + display: flex; + flex-direction: column; + gap: 12px; +} + +.call-logs__foot { + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid #f0f0f0; + text-align: right; +} diff --git a/apps/web/src/views/admin/payments-view.vue b/apps/web/src/views/admin/payments-view.vue index 24ea51e..a889fc4 100644 --- a/apps/web/src/views/admin/payments-view.vue +++ b/apps/web/src/views/admin/payments-view.vue @@ -27,6 +27,8 @@ const subsQuery = useSubscriptions(); const topupsQuery = useTopups(); const filterDomain = ref(null); +const filterSubId = ref(null); +const filterTradeNo = ref(null); const domainOptions = computed(() => { const set = new Set(); for (const s of subsQuery.data.value ?? []) { @@ -39,7 +41,10 @@ const domainOptions = computed(() => { }); const filteredSubs = computed(() => { - const list = subsQuery.data.value ?? []; + let list = subsQuery.data.value ?? []; + if (filterSubId.value != null) { + list = list.filter((r) => r.subscription_id === filterSubId.value); + } if (!filterDomain.value) return list; if (filterDomain.value === "none") { return list.filter((r) => r.billing_domain == null); @@ -48,7 +53,15 @@ const filteredSubs = computed(() => { }); const filteredTops = computed(() => { - const list = topupsQuery.data.value ?? []; + let list = topupsQuery.data.value ?? []; + if (filterTradeNo.value) { + const q = filterTradeNo.value.trim().toLowerCase(); + list = list.filter( + (r) => + (r.trade_no ?? "").toLowerCase().includes(q) || + (r.upstream_id != null && String(r.upstream_id).includes(q)), + ); + } if (!filterDomain.value) return list; if (filterDomain.value === "none") { return list.filter((r) => r.billing_domain == null); @@ -67,18 +80,23 @@ const checking = ref(false); function syncFromRoute() { tab.value = route.query.tab === "topups" ? "topups" : "subs"; + const sid = route.query.subscription_id; + filterSubId.value = typeof sid === "string" && sid ? Number(sid) || null : null; + const tn = route.query.trade_no; + filterTradeNo.value = typeof tn === "string" && tn ? tn : null; } -function pushTab() { - void router.replace({ - path: "/admin/payments", - query: tab.value === "topups" ? { tab: "topups" } : {}, - }); +function pushQuery() { + const query: Record = {}; + if (tab.value === "topups") query.tab = "topups"; + if (filterSubId.value != null) query.subscription_id = String(filterSubId.value); + if (filterTradeNo.value) query.trade_no = filterTradeNo.value; + void router.replace({ path: "/admin/payments", query }); } syncFromRoute(); watch(() => route.query, () => syncFromRoute()); -watch(tab, () => pushTab()); +watch([tab, filterSubId, filterTradeNo], () => pushQuery()); const subRows = computed(() => [...filteredSubs.value]); const topRows = computed(() => { @@ -231,6 +249,24 @@ watch(
+ +