feat(web): 管理端日志来源徽章支付弹窗与支付记录筛选——订阅 ID / 交易号可筛,弹窗可跳转核对补录
This commit is contained in:
@@ -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<number | null>(null);
|
||||
const subsQuery = useSubscriptions();
|
||||
const paymentBySub = computed(() => {
|
||||
const m = new Map<number, SubscriptionItem>();
|
||||
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(() => {
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'source'">
|
||||
<a-tag :color="sourceBadge(record).color">
|
||||
<a-tooltip
|
||||
v-if="sourceBadge(record).kind === 'sub'"
|
||||
title="点击查看该订阅支付参数"
|
||||
>
|
||||
<a-tag
|
||||
color="blue"
|
||||
class="call-logs__badge"
|
||||
@click="(e: MouseEvent) => onBadge(record, e)"
|
||||
>
|
||||
{{ sourceBadge(record).text }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<a-tag v-else :color="sourceBadge(record).color">
|
||||
{{ sourceBadge(record).text }}
|
||||
</a-tag>
|
||||
</template>
|
||||
@@ -432,6 +486,41 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="payModalOpen"
|
||||
:title="payModalSubId != null ? `订阅 #${payModalSubId} 支付参数` : '支付参数'"
|
||||
:footer="null"
|
||||
width="480"
|
||||
centered
|
||||
:styles="{ body: { maxHeight: '56vh', overflowY: 'auto' } }"
|
||||
>
|
||||
<div v-if="payModalPayment" class="call-logs__pay">
|
||||
<div class="call-logs__kv">
|
||||
<span class="call-logs__note">期间</span>
|
||||
<span>{{ periodText(payModalPayment) }}</span>
|
||||
</div>
|
||||
<div class="call-logs__kv">
|
||||
<span class="call-logs__note">额度</span>
|
||||
<QuotaText
|
||||
:quota="payModalPayment.quota_granted"
|
||||
:quota-usd="payModalPayment.quota_usd"
|
||||
/>
|
||||
</div>
|
||||
<div class="call-logs__kv">
|
||||
<span class="call-logs__note">金额</span>
|
||||
<span class="call-logs__num">{{ formatPaidAmount(payModalPayment.paid_amount) }}</span>
|
||||
</div>
|
||||
<div v-if="payModalPayment.remark" class="call-logs__kv">
|
||||
<span class="call-logs__note">备注</span>
|
||||
<span>{{ payModalPayment.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="call-logs__note">该订阅无登记信息</div>
|
||||
<div class="call-logs__foot">
|
||||
<a @click="goPayments">在支付记录中查看</a>
|
||||
</div>
|
||||
</a-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,6 +27,8 @@ const subsQuery = useSubscriptions();
|
||||
const topupsQuery = useTopups();
|
||||
|
||||
const filterDomain = ref<string | "none" | null>(null);
|
||||
const filterSubId = ref<number | null>(null);
|
||||
const filterTradeNo = ref<string | null>(null);
|
||||
const domainOptions = computed(() => {
|
||||
const set = new Set<string>();
|
||||
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<string, string> = {};
|
||||
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(
|
||||
</a-tabs>
|
||||
|
||||
<div class="payments-toolbar">
|
||||
<template v-if="tab === 'subs'">
|
||||
<a-input-number
|
||||
v-model:value="filterSubId"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
style="width: 140px"
|
||||
placeholder="订阅 ID"
|
||||
allow-clear
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-input
|
||||
v-model:value="filterTradeNo"
|
||||
style="width: 180px"
|
||||
placeholder="交易号 / 上游 ID"
|
||||
allow-clear
|
||||
/>
|
||||
</template>
|
||||
<a-select
|
||||
v-model:value="filterDomain"
|
||||
:options="[
|
||||
|
||||
Reference in New Issue
Block a user