feat(web): 支付记录双 Tab 与平台选用弹层

订阅/充值分形态表单(秒级时间、金额非负、额度换算预览);
选用弹层已登记灰显、每次打开重拉、关弹窗清密码。
This commit is contained in:
2026-10-09 22:02:36 +08:00
parent 10d8f4c915
commit c03fdc3d67
16 changed files with 1212 additions and 483 deletions
+7 -6
View File
@@ -4,7 +4,7 @@
* 行展开 = 模型调用详情;来源徽章 = 订阅支付参数弹窗(R12)
*/
import { computed, h, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import type { LogItem, PaymentItem } from "@new-api-key-usage/core";
import type { LogItem, SubscriptionItem } from "@new-api-key-usage/core";
import { Tooltip } from "antdv-next";
import { QuestionCircleOutlined } from "@antdv-next/icons";
import { useRoute, useRouter } from "vue-router";
@@ -28,7 +28,9 @@ const page = ref(1);
const pageSize = ref(50);
const overviewQuery = useMemberOverview(() => range.value);
const payments = computed(() => overviewQuery.data.value?.payments ?? []);
const payments = computed(
() => overviewQuery.data.value?.subscriptions ?? [],
);
const keys = computed(() => overviewQuery.data.value?.keys ?? []);
const params = computed(() => ({
@@ -51,9 +53,9 @@ const keyOptions = computed(() => [
]);
const paymentBySub = computed(() => {
const m = new Map<number, PaymentItem>();
const m = new Map<number, SubscriptionItem>();
for (const p of payments.value) {
if (p.subscription_id != null) m.set(p.subscription_id, p);
m.set(p.subscription_id, p);
}
return m;
});
@@ -115,8 +117,7 @@ function onBadge(record: LogItem, e: MouseEvent) {
payModalOpen.value = true;
}
function periodText(p: PaymentItem): string {
if (p.kind !== "subscription") return formatDateTime(p.paid_at);
function periodText(p: SubscriptionItem): string {
const end = p.period_end == null ? "进行中" : formatDateTime(p.period_end);
return `${formatDateTime(p.period_start)} ~ ${end}`;
}
+7 -4
View File
@@ -20,15 +20,18 @@ const overviewQuery = useMemberOverview(() => range.value);
const usage = computed(() => overviewQuery.data.value?.usage);
const bySource = computed(() => overviewQuery.data.value?.by_source ?? []);
const keys = computed(() => overviewQuery.data.value?.keys ?? []);
const payments = computed(() => overviewQuery.data.value?.payments ?? []);
const payments = computed(
() => overviewQuery.data.value?.subscriptions ?? [],
);
const extraPresets = computed(() =>
payments.value
.filter((p) => p.kind === "subscription" && p.period_start != null)
.map((p) => ({
label: `订阅 #${p.subscription_id ?? p.id} 期间`,
label: `订阅 #${p.subscription_id} 期间`,
from: new Date((p.period_start ?? 0) * 1000).toISOString(),
to: new Date((p.period_end ?? Math.floor(Date.now() / 1000)) * 1000).toISOString(),
to: new Date(
(p.period_end ?? Math.floor(Date.now() / 1000)) * 1000,
).toISOString(),
}))
.slice(0, 4),
);
+85 -41
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
/**
* 成员支付参数(交互设计-成员视图 §5):完全公开的登记参数表
* 成员支付参数(交互设计-成员视图 §5):订阅 + 充值清单
*/
import { computed } from "vue";
import PageHeader from "@/components/common/page-header.vue";
@@ -13,31 +13,42 @@ import { formatPaidAmount } from "@/utils/format.ts";
const { range, onRangeChange } = useMemberRange();
const overviewQuery = useMemberOverview(() => range.value);
const payments = computed(() => overviewQuery.data.value?.payments ?? []);
const subs = computed(() => overviewQuery.data.value?.subscriptions ?? []);
const tops = computed(() => overviewQuery.data.value?.topups ?? []);
const extraPresets = computed(() =>
payments.value
.filter((p) => p.kind === "subscription" && p.period_start != null)
subs.value
.map((p) => ({
label: `订阅 #${p.subscription_id ?? p.id} 期间`,
label: `订阅 #${p.subscription_id} 期间`,
from: new Date((p.period_start ?? 0) * 1000).toISOString(),
to: new Date((p.period_end ?? Math.floor(Date.now() / 1000)) * 1000).toISOString(),
to: new Date(
(p.period_end ?? Math.floor(Date.now() / 1000)) * 1000,
).toISOString(),
}))
.slice(0, 4),
);
function periodText(p: { kind: string; paid_at: number; period_start: number | null; period_end: number | null }): string {
if (p.kind !== "subscription") return "—";
function periodText(p: {
period_start: number;
period_end: number | null;
}): string {
const end = p.period_end == null ? "进行中" : formatDateTime(p.period_end);
return `${formatDateTime(p.period_start)} ~ ${end}`;
}
const columns = [
{ title: "类型", key: "kind", width: 80 },
{ title: "编号 / 时间", key: "id_or_time", width: 160 },
{ title: "期间", key: "period", width: 220 },
const subColumns = [
{ title: "订阅 ID", key: "subscription_id", width: 100 },
{ title: "期间", key: "period", width: 200 },
{ title: "额度", key: "quota", width: 160 },
{ title: "金额", key: "amount", width: 100 },
{ title: "付款", key: "paid", width: 140 },
{ title: "备注", key: "remark" },
];
const topColumns = [
{ title: "时间", key: "paid_at", width: 150 },
{ title: "计费单位 / 实付", key: "money", width: 140 },
{ title: "交易号 / 渠道", key: "trade", width: 180 },
{ title: "派生额度", key: "quota", width: 150 },
{ title: "备注", key: "remark" },
];
</script>
@@ -77,52 +88,74 @@ const columns = [
:paragraph="{ rows: 4 }"
/>
<a-empty
v-else-if="!overviewQuery.isError.value && payments.length === 0"
v-else-if="!overviewQuery.isError.value && subs.length === 0 && tops.length === 0"
description="暂无支付记录信息"
/>
<template v-else-if="payments.length > 0">
<template v-else>
<h3 class="mv-sec">订阅</h3>
<a-empty v-if="subs.length === 0" description="暂无订阅登记" />
<a-table
:columns="columns"
:data-source="payments"
v-else
:columns="subColumns"
:data-source="subs"
:row-key="(r: { id: number }) => r.id"
size="middle"
:pagination="false"
:scroll="{ x: 860 }"
:loading="overviewQuery.isFetching.value"
:scroll="{ x: 800 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'kind'">
<a-tag :color="record.kind === 'subscription' ? 'blue' : 'gold'">
{{ record.kind === "subscription" ? "订阅" : "充值" }}
</a-tag>
</template>
<template v-else-if="column.key === 'id_or_time'">
<template v-if="record.kind === 'subscription'">
订阅 #{{ record.subscription_id ?? record.id }}
</template>
<template v-else>
{{ formatDateTime(record.paid_at) }}
</template>
</template>
<template v-else-if="column.key === 'period'">
<template v-if="column.key === 'period'">
{{ periodText(record) }}
</template>
<template v-else-if="column.key === 'quota'">
<QuotaText
v-if="record.quota_granted != null"
:quota="record.quota_granted"
:quota-usd="record.quota_usd"
/>
<span v-else>—</span>
<QuotaText :quota="record.quota_granted" :quota-usd="record.quota_usd" />
</template>
<template v-else-if="column.key === 'amount'">
{{ formatPaidAmount(record.paid_amount) }}
<template v-else-if="column.key === 'paid'">
{{
record.paid_amount != null
? `${formatPaidAmount(record.paid_amount)} · ${formatDateTime(record.paid_at)}`
: "—"
}}
</template>
<template v-else-if="column.key === 'remark'">
{{ record.remark || "—" }}
</template>
</template>
</a-table>
<h3 class="mv-sec">充值</h3>
<a-empty v-if="tops.length === 0" description="暂无充值登记" />
<a-table
v-else
:columns="topColumns"
:data-source="tops"
:row-key="(r: { id: number }) => r.id"
size="middle"
:pagination="false"
:scroll="{ x: 760 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'paid_at'">
{{ formatDateTime(record.paid_at) }}
</template>
<template v-else-if="column.key === 'money'">
{{ record.amount }} · {{ formatPaidAmount(record.money) }}
</template>
<template v-else-if="column.key === 'trade'">
{{ record.trade_no || "—" }}
<span v-if="record.payment_method" class="mv-note">
· {{ record.payment_method }}
</span>
</template>
<template v-else-if="column.key === 'quota'">
<QuotaText :quota="record.quota_derived" :quota-usd="record.quota_usd" />
</template>
<template v-else-if="column.key === 'remark'">
{{ record.remark || "—" }}
</template>
</template>
</a-table>
<div class="mv-foot">
支付参数对成员完全公开;币种由成员与作者约定
</div>
@@ -136,6 +169,17 @@ const columns = [
color: rgba(0, 0, 0, 0.65);
}
.mv-sec {
margin: 20px 0 12px;
font-size: 14px;
font-weight: 600;
}
.mv-note {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.mv-foot {
margin-top: 16px;
font-size: 12px;