2026-03-08 00:05:48 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="space-y-6 pb-8">
|
|
|
|
|
|
<Card class="overflow-hidden">
|
|
|
|
|
|
<div class="px-5 py-4 border-b border-border/60 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 class="text-base font-semibold">
|
|
|
|
|
|
钱包管理
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
统一管理资金流水、退款审批、充值订单与支付回调
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="px-5 py-5">
|
|
|
|
|
|
<Tabs v-model="activeTab">
|
2026-04-17 10:07:52 +08:00
|
|
|
|
<TabsList class="tabs-button-list grid w-full max-w-[960px] grid-cols-5">
|
2026-03-08 00:05:48 +08:00
|
|
|
|
<TabsTrigger value="ledger">
|
|
|
|
|
|
资金流水
|
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="orders">
|
|
|
|
|
|
充值订单
|
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="refunds">
|
|
|
|
|
|
退款审批
|
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="callbacks">
|
|
|
|
|
|
回调日志
|
|
|
|
|
|
</TabsTrigger>
|
2026-04-17 10:07:52 +08:00
|
|
|
|
<TabsTrigger value="redeem_codes">
|
|
|
|
|
|
兑换码
|
|
|
|
|
|
</TabsTrigger>
|
2026-03-08 00:05:48 +08:00
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent
|
|
|
|
|
|
value="ledger"
|
|
|
|
|
|
class="mt-5 space-y-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<Select v-model="ledgerCategoryFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[170px]">
|
|
|
|
|
|
<SelectValue placeholder="一级分类" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部分类
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="recharge">
|
|
|
|
|
|
充值
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="gift">
|
|
|
|
|
|
赠款
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="adjust">
|
|
|
|
|
|
调账
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="refund">
|
|
|
|
|
|
退款
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
<Select v-model="ledgerReasonFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[180px]">
|
|
|
|
|
|
<SelectValue placeholder="二级分类" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部二级
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem
|
|
|
|
|
|
v-for="option in ledgerReasonOptions"
|
|
|
|
|
|
:key="option.value"
|
|
|
|
|
|
:value="option.value"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
<Select v-model="ledgerOwnerFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[170px]">
|
|
|
|
|
|
<SelectValue placeholder="归属类型" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部归属
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="user">
|
|
|
|
|
|
用户钱包
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="api_key">
|
|
|
|
|
|
独立密钥钱包
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<div class="text-sm text-muted-foreground">
|
|
|
|
|
|
共 {{ ledgerTotal }} 条
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<RefreshButton
|
|
|
|
|
|
:loading="loadingLedger"
|
|
|
|
|
|
@click="loadLedger"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 overflow-hidden bg-background">
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead>时间</TableHead>
|
|
|
|
|
|
<TableHead>归属</TableHead>
|
|
|
|
|
|
<TableHead>类型</TableHead>
|
|
|
|
|
|
<TableHead>金额</TableHead>
|
|
|
|
|
|
<TableHead>余额变化</TableHead>
|
|
|
|
|
|
<TableHead>说明</TableHead>
|
|
|
|
|
|
<TableHead class="text-right">
|
|
|
|
|
|
操作
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
v-for="tx in ledgerItems"
|
|
|
|
|
|
:key="tx.id"
|
|
|
|
|
|
class="hover:bg-muted/20"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground whitespace-nowrap">
|
|
|
|
|
|
{{ formatDateTime(tx.created_at) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="min-w-[180px]">
|
|
|
|
|
|
<div class="font-medium text-sm">
|
|
|
|
|
|
{{ ownerDisplayName(tx.owner_name, tx.owner_type) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-2">
|
|
|
|
|
|
<span>{{ ownerTypeLabel(tx.owner_type) }}</span>
|
|
|
|
|
|
<Badge
|
|
|
|
|
|
v-if="tx.wallet_status"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
class="text-[10px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ walletStatusLabel(tx.wallet_status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
<Badge
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
class="font-mono"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ walletTransactionCategoryLabel(tx.category) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<div class="text-[11px] text-muted-foreground">
|
|
|
|
|
|
{{ walletTransactionReasonLabel(tx.reason_code) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell :class="tx.amount >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-400'">
|
|
|
|
|
|
{{ tx.amount >= 0 ? '+' : '' }}{{ tx.amount.toFixed(4) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs tabular-nums whitespace-nowrap">
|
|
|
|
|
|
<div>{{ tx.balance_before.toFixed(4) }} → {{ tx.balance_after.toFixed(4) }}</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="tx.recharge_balance_before !== null && tx.recharge_balance_before !== undefined && tx.gift_balance_before !== null && tx.gift_balance_before !== undefined"
|
|
|
|
|
|
class="text-[11px] text-muted-foreground mt-0.5"
|
|
|
|
|
|
>
|
|
|
|
|
|
充 {{ Number(tx.recharge_balance_before).toFixed(4) }}→{{ Number(tx.recharge_balance_after ?? 0).toFixed(4) }}
|
|
|
|
|
|
· 赠 {{ Number(tx.gift_balance_before).toFixed(4) }}→{{ Number(tx.gift_balance_after ?? 0).toFixed(4) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground max-w-[280px] truncate">
|
|
|
|
|
|
{{ tx.description || '-' }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-right">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="openLedgerDrawer(tx)"
|
|
|
|
|
|
>
|
|
|
|
|
|
详情
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
<TableRow v-if="!loadingLedger && ledgerItems.length === 0">
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colspan="7"
|
|
|
|
|
|
class="py-12"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无资金流水"
|
|
|
|
|
|
description="当前筛选条件下没有资金动作记录"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
:current="ledgerPage"
|
|
|
|
|
|
:total="ledgerTotal"
|
|
|
|
|
|
:page-size="ledgerPageSize"
|
|
|
|
|
|
@update:current="handleLedgerPageChange"
|
|
|
|
|
|
@update:page-size="handleLedgerPageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent
|
|
|
|
|
|
value="refunds"
|
|
|
|
|
|
class="mt-5 space-y-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<Select v-model="refundStatusFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[170px]">
|
|
|
|
|
|
<SelectValue placeholder="退款状态" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部状态
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="pending_approval">
|
|
|
|
|
|
待审批
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="approved">
|
|
|
|
|
|
已审批
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="processing">
|
|
|
|
|
|
处理中
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="succeeded">
|
|
|
|
|
|
已完成
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="failed">
|
|
|
|
|
|
已失败
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
<Select v-model="refundOwnerFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[170px]">
|
|
|
|
|
|
<SelectValue placeholder="归属类型" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部归属
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="user">
|
|
|
|
|
|
用户钱包
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<div class="text-sm text-muted-foreground">
|
|
|
|
|
|
共 {{ refundTotal }} 条
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<RefreshButton
|
|
|
|
|
|
:loading="loadingRefunds"
|
|
|
|
|
|
@click="loadRefunds"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 overflow-hidden bg-background">
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead>归属</TableHead>
|
|
|
|
|
|
<TableHead>退款单号</TableHead>
|
|
|
|
|
|
<TableHead>金额</TableHead>
|
|
|
|
|
|
<TableHead>模式</TableHead>
|
|
|
|
|
|
<TableHead>状态</TableHead>
|
|
|
|
|
|
<TableHead>原因</TableHead>
|
|
|
|
|
|
<TableHead>申请时间</TableHead>
|
|
|
|
|
|
<TableHead class="text-right">
|
|
|
|
|
|
操作
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
v-for="refund in refundItems"
|
|
|
|
|
|
:key="refund.id"
|
|
|
|
|
|
class="hover:bg-muted/20"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableCell class="min-w-[180px]">
|
|
|
|
|
|
<div class="font-medium text-sm">
|
|
|
|
|
|
{{ ownerDisplayName(refund.owner_name, refund.owner_type) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-2">
|
|
|
|
|
|
<span>{{ ownerTypeLabel(refund.owner_type) }}</span>
|
|
|
|
|
|
<Badge
|
|
|
|
|
|
v-if="refund.wallet_status"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
class="text-[10px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ walletStatusLabel(refund.wallet_status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="font-mono text-xs whitespace-nowrap">
|
|
|
|
|
|
{{ refund.refund_no }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="tabular-nums whitespace-nowrap">
|
|
|
|
|
|
{{ formatCurrency(refund.amount_usd) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
{{ refundModeLabel(refund.refund_mode) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge :variant="refundStatusBadge(refund.status)">
|
|
|
|
|
|
{{ refundStatusLabel(refund.status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground max-w-[240px] truncate">
|
|
|
|
|
|
{{ refund.reason || refund.failure_reason || '-' }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground whitespace-nowrap">
|
|
|
|
|
|
{{ formatDateTime(refund.created_at) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-right">
|
|
|
|
|
|
<div class="flex justify-end gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="openRefundDrawer(refund)"
|
|
|
|
|
|
>
|
|
|
|
|
|
审批
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
<TableRow v-if="!loadingRefunds && refundItems.length === 0">
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colspan="8"
|
|
|
|
|
|
class="py-12"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无退款申请"
|
|
|
|
|
|
description="当前筛选条件下没有退款单"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
:current="refundPage"
|
|
|
|
|
|
:total="refundTotal"
|
|
|
|
|
|
:page-size="refundPageSize"
|
|
|
|
|
|
@update:current="handleRefundPageChange"
|
|
|
|
|
|
@update:page-size="handleRefundPageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent
|
|
|
|
|
|
value="orders"
|
|
|
|
|
|
class="mt-5 space-y-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<Select v-model="orderStatusFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[180px]">
|
|
|
|
|
|
<SelectValue placeholder="订单状态" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部状态
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="pending">
|
|
|
|
|
|
待支付
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="paid">
|
|
|
|
|
|
已支付
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="credited">
|
|
|
|
|
|
已到账
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="failed">
|
|
|
|
|
|
支付失败
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="expired">
|
|
|
|
|
|
已过期
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
<Select v-model="orderMethodFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[180px]">
|
|
|
|
|
|
<SelectValue placeholder="支付方式" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部方式
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="alipay">
|
|
|
|
|
|
支付宝
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="wechat">
|
|
|
|
|
|
微信支付
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="admin_manual">
|
|
|
|
|
|
人工充值
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="card_code">
|
|
|
|
|
|
充值卡
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="gift_code">
|
|
|
|
|
|
礼品卡
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="card_recharge">
|
|
|
|
|
|
卡密充值
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
<RefreshButton
|
|
|
|
|
|
:loading="loadingOrders"
|
|
|
|
|
|
@click="loadOrders"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 overflow-hidden bg-background">
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead>订单号</TableHead>
|
|
|
|
|
|
<TableHead>钱包名称</TableHead>
|
|
|
|
|
|
<TableHead>金额</TableHead>
|
|
|
|
|
|
<TableHead>支付方式</TableHead>
|
|
|
|
|
|
<TableHead>状态</TableHead>
|
|
|
|
|
|
<TableHead>创建时间</TableHead>
|
|
|
|
|
|
<TableHead class="text-right">
|
|
|
|
|
|
操作
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
v-for="order in orders"
|
|
|
|
|
|
:key="order.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableCell class="font-mono text-xs whitespace-nowrap">
|
|
|
|
|
|
{{ order.order_no }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="min-w-[180px]">
|
|
|
|
|
|
<div class="text-sm font-medium">
|
|
|
|
|
|
{{ orderWalletName(order.wallet_id) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
{{ orderWalletTypeLabel(order.wallet_id) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="tabular-nums">
|
|
|
|
|
|
{{ formatCurrency(order.amount_usd) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>{{ paymentMethodLabel(order.payment_method) }}</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge :variant="paymentStatusBadge(order.status)">
|
|
|
|
|
|
{{ paymentStatusLabel(order.status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground whitespace-nowrap">
|
|
|
|
|
|
{{ formatDateTime(order.created_at) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-right">
|
|
|
|
|
|
<div class="flex justify-end gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="canCreditOrder(order.status)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
@click="openCreditDialog(order)"
|
|
|
|
|
|
>
|
|
|
|
|
|
到账
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="canExpireOrder(order.status)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
:disabled="submittingOrderAction"
|
|
|
|
|
|
@click="expireOrder(order.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
过期
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="canFailOrder(order.status)"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="destructive"
|
|
|
|
|
|
:disabled="submittingOrderAction"
|
|
|
|
|
|
@click="failOrder(order.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
失败
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
<TableRow v-if="!loadingOrders && orders.length === 0">
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colspan="7"
|
|
|
|
|
|
class="py-10"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无支付订单"
|
|
|
|
|
|
description="当前筛选条件下没有数据"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
:current="orderPage"
|
|
|
|
|
|
:total="orderTotal"
|
|
|
|
|
|
:page-size="orderPageSize"
|
|
|
|
|
|
@update:current="handleOrderPageChange"
|
|
|
|
|
|
@update:page-size="handleOrderPageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent
|
|
|
|
|
|
value="callbacks"
|
|
|
|
|
|
class="mt-5 space-y-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<Select v-model="callbackMethodFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[180px]">
|
|
|
|
|
|
<SelectValue placeholder="支付方式" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部方式
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="alipay">
|
|
|
|
|
|
支付宝
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="wechat">
|
|
|
|
|
|
微信支付
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<RefreshButton
|
|
|
|
|
|
:loading="loadingCallbacks"
|
|
|
|
|
|
@click="loadCallbacks"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 overflow-hidden bg-background">
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead>回调键</TableHead>
|
|
|
|
|
|
<TableHead>订单号</TableHead>
|
|
|
|
|
|
<TableHead>方式</TableHead>
|
|
|
|
|
|
<TableHead>验签</TableHead>
|
|
|
|
|
|
<TableHead>状态</TableHead>
|
|
|
|
|
|
<TableHead>时间</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
v-for="callback in callbacks"
|
|
|
|
|
|
:key="callback.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableCell class="font-mono text-xs">
|
|
|
|
|
|
{{ callback.callback_key }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="font-mono text-xs whitespace-nowrap">
|
|
|
|
|
|
{{ callback.order_no || '-' }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>{{ paymentMethodLabel(callback.payment_method) }}</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge :variant="callback.signature_valid ? 'success' : 'destructive'">
|
|
|
|
|
|
{{ callback.signature_valid ? '通过' : '失败' }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge :variant="callbackStatusBadge(callback.status)">
|
|
|
|
|
|
{{ callbackStatusLabel(callback.status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground whitespace-nowrap">
|
|
|
|
|
|
{{ formatDateTime(callback.created_at) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
<TableRow v-if="!loadingCallbacks && callbacks.length === 0">
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colspan="6"
|
|
|
|
|
|
class="py-10"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无回调日志"
|
|
|
|
|
|
description="当前筛选条件下没有数据"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
:current="callbackPage"
|
|
|
|
|
|
:total="callbackTotal"
|
|
|
|
|
|
:page-size="callbackPageSize"
|
|
|
|
|
|
@update:current="handleCallbackPageChange"
|
|
|
|
|
|
@update:page-size="handleCallbackPageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TabsContent>
|
2026-04-17 10:07:52 +08:00
|
|
|
|
|
|
|
|
|
|
<TabsContent
|
|
|
|
|
|
value="redeem_codes"
|
|
|
|
|
|
class="mt-5 space-y-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 bg-background p-4 space-y-4">
|
|
|
|
|
|
<div class="flex items-center justify-between gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 class="text-sm font-semibold">
|
|
|
|
|
|
批量生成兑换码
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
生成后本会话可切换显示明文;页面刷新后仅保留脱敏码。
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<RefreshButton
|
|
|
|
|
|
:loading="loadingRedeemBatches || loadingRedeemCodes"
|
|
|
|
|
|
@click="loadRedeemCodeBatches"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-3 lg:grid-cols-4">
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>批次名称</Label>
|
|
|
|
|
|
<Input v-model="redeemBatchForm.name" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>面额 (USD)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model.number="redeemBatchForm.amount_usd"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0.01"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>生成数量</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model.number="redeemBatchForm.total_count"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
step="1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>过期时间(可选)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="redeemBatchForm.expires_at"
|
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>备注(可选)</Label>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
v-model="redeemBatchForm.description"
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
placeholder="例如:五一活动 / 线下渠道 / KOC 发放"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex flex-wrap justify-end gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
:disabled="!canExportLatestGeneratedRedeemCodes"
|
|
|
|
|
|
@click="exportLatestGeneratedRedeemCodes"
|
|
|
|
|
|
>
|
|
|
|
|
|
导出最近生成
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
:disabled="submittingRedeemBatch"
|
|
|
|
|
|
@click="submitRedeemCodeBatch"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ submittingRedeemBatch ? '生成中...' : '生成兑换码' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="latestGeneratedRedeemBatch"
|
|
|
|
|
|
class="rounded-xl border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
最近生成批次:
|
|
|
|
|
|
<span class="font-medium text-foreground">{{ latestGeneratedRedeemBatch.name }}</span>
|
|
|
|
|
|
· {{ latestGeneratedRedeemCodes.length }} 个兑换码
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-5 xl:grid-cols-[1.1fr_1fr]">
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<Select v-model="redeemBatchStatusFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[180px]">
|
|
|
|
|
|
<SelectValue placeholder="批次状态" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部状态
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="active">
|
|
|
|
|
|
可用
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="disabled">
|
|
|
|
|
|
已停用
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<div class="text-sm text-muted-foreground">
|
|
|
|
|
|
共 {{ redeemBatchTotal }} 个批次
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 overflow-hidden bg-background">
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead>批次</TableHead>
|
|
|
|
|
|
<TableHead>面额</TableHead>
|
|
|
|
|
|
<TableHead>数量</TableHead>
|
|
|
|
|
|
<TableHead>状态</TableHead>
|
|
|
|
|
|
<TableHead class="text-right">
|
|
|
|
|
|
操作
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
v-for="batch in redeemBatches"
|
|
|
|
|
|
:key="batch.id"
|
|
|
|
|
|
class="hover:bg-muted/20"
|
|
|
|
|
|
:class="batch.id === selectedRedeemBatchId ? 'bg-muted/30 ring-1 ring-border/60' : ''"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableCell class="min-w-[220px]">
|
|
|
|
|
|
<div class="text-sm font-medium">
|
|
|
|
|
|
{{ batch.name }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
过期: {{ formatDateTime(batch.expires_at) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="tabular-nums">
|
|
|
|
|
|
{{ formatCurrency(batch.amount_usd) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{ batch.redeemed_count }} / {{ batch.total_count }} 已使用
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge :variant="batch.status === 'active' ? 'success' : 'secondary'">
|
|
|
|
|
|
{{ batch.status === 'active' ? '可用' : '已停用' }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-right">
|
|
|
|
|
|
<div class="flex justify-end gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
:variant="batch.id === selectedRedeemBatchId ? 'default' : 'outline'"
|
|
|
|
|
|
@click="selectRedeemBatch(batch)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ batch.id === selectedRedeemBatchId ? '当前查看' : '查看码' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="batch.status === 'active'"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="destructive"
|
|
|
|
|
|
@click="disableRedeemBatch(batch.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
停用批次
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="batch.status === 'disabled'"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="destructive"
|
|
|
|
|
|
:disabled="batch.redeemed_count > 0"
|
|
|
|
|
|
@click="deleteRedeemBatch(batch)"
|
|
|
|
|
|
>
|
|
|
|
|
|
删除批次
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
<TableRow v-if="!loadingRedeemBatches && redeemBatches.length === 0">
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colspan="5"
|
|
|
|
|
|
class="py-10"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无兑换码批次"
|
|
|
|
|
|
description="创建批次后会在这里显示"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
:current="redeemBatchPage"
|
|
|
|
|
|
:total="redeemBatchTotal"
|
|
|
|
|
|
:page-size="redeemBatchPageSize"
|
|
|
|
|
|
@update:current="handleRedeemBatchPageChange"
|
|
|
|
|
|
@update:page-size="handleRedeemBatchPageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref="redeemCodesPanelRef"
|
|
|
|
|
|
class="space-y-4"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h4 class="text-sm font-semibold">
|
|
|
|
|
|
{{ currentRedeemBatch?.name || '兑换码列表' }}
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
{{ currentRedeemBatch ? `面额 ${formatCurrency(currentRedeemBatch.amount_usd)} · 剩余 ${currentRedeemBatch.active_count}` : '先从左侧选择一个批次' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<span class="text-xs text-muted-foreground">显示明文</span>
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
:model-value="showPlainRedeemCodes"
|
|
|
|
|
|
:disabled="!canRevealPlainRedeemCodes"
|
|
|
|
|
|
@update:model-value="showPlainRedeemCodes = Boolean($event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Select v-model="redeemCodeStatusFilter">
|
|
|
|
|
|
<SelectTrigger class="w-[180px]">
|
|
|
|
|
|
<SelectValue placeholder="码状态" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="all">
|
|
|
|
|
|
全部状态
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="active">
|
|
|
|
|
|
可用
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="disabled">
|
|
|
|
|
|
已停用
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
<SelectItem value="redeemed">
|
|
|
|
|
|
已兑换
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{
|
|
|
|
|
|
canRevealPlainRedeemCodes
|
|
|
|
|
|
? '当前批次属于本次生成,已支持明文显示开关。'
|
|
|
|
|
|
: '仅当前会话内最近生成的一批兑换码支持明文显示;其余批次仅显示脱敏码。'
|
|
|
|
|
|
}}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 overflow-hidden bg-background">
|
|
|
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
|
<Table>
|
|
|
|
|
|
<TableHeader>
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableHead>兑换码</TableHead>
|
|
|
|
|
|
<TableHead>状态</TableHead>
|
|
|
|
|
|
<TableHead>兑换用户</TableHead>
|
|
|
|
|
|
<TableHead>关联订单</TableHead>
|
|
|
|
|
|
<TableHead class="text-right">
|
|
|
|
|
|
操作
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
<TableRow
|
|
|
|
|
|
v-for="code in redeemCodes"
|
|
|
|
|
|
:key="code.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<TableCell class="font-mono text-xs">
|
|
|
|
|
|
{{ displayRedeemCode(code) }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell>
|
|
|
|
|
|
<Badge :variant="redeemCodeStatusBadge(code.status)">
|
|
|
|
|
|
{{ redeemCodeStatusLabel(code.status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{ code.redeemed_by_user_name || code.redeemed_by_user_id || '-' }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="font-mono text-xs">
|
|
|
|
|
|
{{ code.redeemed_order_no || code.redeemed_payment_order_id || '-' }}
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
<TableCell class="text-right">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="code.status === 'active'"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="disableRedeemCode(code.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
停用
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
<TableRow v-if="!loadingRedeemCodes && redeemCodes.length === 0">
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colspan="5"
|
|
|
|
|
|
class="py-10"
|
|
|
|
|
|
>
|
|
|
|
|
|
<EmptyState
|
|
|
|
|
|
title="暂无兑换码"
|
|
|
|
|
|
description="选择左侧批次后会显示兑换码明细"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableBody>
|
|
|
|
|
|
</Table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
:current="redeemCodePage"
|
|
|
|
|
|
:total="redeemCodeTotal"
|
|
|
|
|
|
:page-size="redeemCodePageSize"
|
|
|
|
|
|
@update:current="handleRedeemCodePageChange"
|
|
|
|
|
|
@update:page-size="handleRedeemCodePageSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</TabsContent>
|
2026-03-08 00:05:48 +08:00
|
|
|
|
</Tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
|
<Transition name="drawer">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="showLedgerDrawer && currentLedger"
|
|
|
|
|
|
class="fixed inset-0 z-[80] flex justify-end"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="absolute inset-0 bg-black/35 backdrop-blur-sm"
|
|
|
|
|
|
@click="closeLedgerDrawer"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="drawer-panel relative h-full w-full sm:w-[760px] lg:w-[860px] sm:max-w-[95vw] border-l border-border bg-background shadow-2xl overflow-y-auto">
|
|
|
|
|
|
<div class="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur px-4 py-3 sm:px-6 sm:py-4">
|
|
|
|
|
|
<div class="flex items-start justify-between gap-3">
|
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
|
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
|
|
|
|
|
流水详情
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
|
资金动作审计信息
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-9 w-9 shrink-0"
|
|
|
|
|
|
title="关闭"
|
|
|
|
|
|
@click="closeLedgerDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X class="h-4 w-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="p-4 sm:p-6 space-y-5">
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 bg-muted/30 p-4 space-y-3">
|
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<Badge variant="outline">
|
|
|
|
|
|
{{ walletTransactionCategoryLabel(currentLedger.category) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Badge variant="secondary">
|
|
|
|
|
|
{{ walletTransactionReasonLabel(currentLedger.reason_code) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="text-sm font-semibold tabular-nums"
|
|
|
|
|
|
:class="currentLedger.amount >= 0 ? 'text-emerald-600' : 'text-rose-600'"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ currentLedger.amount >= 0 ? '+' : '' }}{{ currentLedger.amount.toFixed(4) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-muted-foreground">
|
|
|
|
|
|
{{ formatDateTime(currentLedger.created_at) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
归属
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-medium">
|
|
|
|
|
|
{{ ownerDisplayName(currentLedger.owner_name, currentLedger.owner_type) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-xs text-muted-foreground flex items-center gap-2">
|
|
|
|
|
|
<span>{{ ownerTypeLabel(currentLedger.owner_type) }}</span>
|
|
|
|
|
|
<Badge
|
|
|
|
|
|
v-if="currentLedger.wallet_status"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
class="text-[10px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ walletStatusLabel(currentLedger.wallet_status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
余额变化
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-medium tabular-nums">
|
|
|
|
|
|
{{ currentLedger.balance_before.toFixed(4) }} → {{ currentLedger.balance_after.toFixed(4) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentLedger.recharge_balance_before !== null && currentLedger.recharge_balance_before !== undefined && currentLedger.gift_balance_before !== null && currentLedger.gift_balance_before !== undefined"
|
|
|
|
|
|
class="mt-1 text-xs text-muted-foreground tabular-nums"
|
|
|
|
|
|
>
|
|
|
|
|
|
充 {{ Number(currentLedger.recharge_balance_before).toFixed(4) }}→{{ Number(currentLedger.recharge_balance_after ?? 0).toFixed(4) }}
|
|
|
|
|
|
· 赠 {{ Number(currentLedger.gift_balance_before).toFixed(4) }}→{{ Number(currentLedger.gift_balance_after ?? 0).toFixed(4) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
关联类型
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-medium break-all">
|
|
|
|
|
|
{{ walletLinkTypeLabel(currentLedger.link_type) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
交易ID
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-mono break-all">
|
|
|
|
|
|
{{ currentLedger.id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentLedger.link_type === 'payment_order'"
|
|
|
|
|
|
class="grid gap-3 sm:grid-cols-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
支付方式
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-medium">
|
|
|
|
|
|
<span v-if="loadingLedgerOrderNo">加载中...</span>
|
|
|
|
|
|
<span v-else>{{ ledgerPaymentMethod ? paymentMethodLabel(ledgerPaymentMethod) : '-' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
充值订单号
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-mono break-all">
|
|
|
|
|
|
<span v-if="loadingLedgerOrderNo">加载中...</span>
|
|
|
|
|
|
<span v-else>{{ ledgerPaymentOrderNo || '-' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
操作用户
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-medium">
|
|
|
|
|
|
{{ currentLedger.operator_name || (currentLedger.operator_id ? '已删除用户' : '系统自动') }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
|
ID: {{ currentLedger.operator_id || '-' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentLedger.operator_email"
|
|
|
|
|
|
class="mt-1 text-xs text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
邮箱: {{ currentLedger.operator_email }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-xl border border-border/60 p-3">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
说明
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm text-foreground whitespace-pre-wrap break-words">
|
|
|
|
|
|
{{ currentLedger.description || '-' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
|
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
|
<Transition name="drawer">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="showRefundDrawer && currentRefund"
|
|
|
|
|
|
class="fixed inset-0 z-[80] flex justify-end"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="absolute inset-0 bg-black/35 backdrop-blur-sm"
|
|
|
|
|
|
@click="closeRefundDrawer"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="drawer-panel relative h-full w-full sm:w-[760px] lg:w-[860px] sm:max-w-[95vw] border-l border-border bg-background shadow-2xl overflow-y-auto">
|
|
|
|
|
|
<div class="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur px-4 py-3 sm:px-6 sm:py-4">
|
|
|
|
|
|
<div class="flex items-start justify-between gap-3">
|
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
|
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
|
|
|
|
|
退款审批
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
|
退款单: {{ currentRefund.refund_no }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
class="h-9 w-9 shrink-0"
|
|
|
|
|
|
title="关闭"
|
|
|
|
|
|
@click="closeRefundDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X class="h-4 w-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="p-4 sm:p-6 space-y-5">
|
|
|
|
|
|
<div class="rounded-2xl border border-border/60 bg-muted/30 p-4">
|
|
|
|
|
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
归属
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-medium">
|
|
|
|
|
|
{{ ownerDisplayName(currentRefund.owner_name, currentRefund.owner_type) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
|
{{ ownerTypeLabel(currentRefund.owner_type) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
金额
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm font-semibold tabular-nums">
|
|
|
|
|
|
{{ formatCurrency(currentRefund.amount_usd) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
退款模式
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm">
|
|
|
|
|
|
{{ refundModeLabel(currentRefund.refund_mode) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
状态
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1">
|
|
|
|
|
|
<Badge :variant="refundStatusBadge(currentRefund.status)">
|
|
|
|
|
|
{{ refundStatusLabel(currentRefund.status) }}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sm:col-span-2">
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
申请原因
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm text-foreground whitespace-pre-wrap break-words">
|
|
|
|
|
|
{{ currentRefund.reason || '-' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="currentRefund.failure_reason"
|
|
|
|
|
|
class="sm:col-span-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">
|
|
|
|
|
|
失败原因
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-1 text-sm text-rose-600 whitespace-pre-wrap break-words">
|
|
|
|
|
|
{{ currentRefund.failure_reason }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="canFailRefund(currentRefund.status)"
|
|
|
|
|
|
class="rounded-xl border border-border/60 p-4 space-y-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Label>驳回原因</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model="failRefundForm.reason"
|
|
|
|
|
|
placeholder="请填写驳回原因"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="canCompleteRefund(currentRefund.status)"
|
|
|
|
|
|
class="rounded-xl border border-border/60 p-4 space-y-3"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>网关退款号(可选)</Label>
|
|
|
|
|
|
<Input v-model="completeRefundForm.gateway_refund_id" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>打款凭证 / 参考号(可选)</Label>
|
|
|
|
|
|
<Input v-model="completeRefundForm.payout_reference" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="sticky bottom-0 border-t border-border bg-background/95 backdrop-blur px-4 py-3 sm:px-6 sm:py-4">
|
|
|
|
|
|
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="closeRefundDrawer"
|
|
|
|
|
|
>
|
|
|
|
|
|
关闭
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="canProcessRefund(currentRefund.status)"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
:disabled="submittingRefundAction"
|
|
|
|
|
|
@click="processRefund(currentRefund)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ submittingRefundAction ? '处理中...' : '处理退款' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="canCompleteRefund(currentRefund.status)"
|
|
|
|
|
|
:disabled="submittingRefundAction"
|
|
|
|
|
|
@click="submitCompleteRefund"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ submittingRefundAction ? '提交中...' : '确认完成' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
v-if="canFailRefund(currentRefund.status)"
|
|
|
|
|
|
variant="destructive"
|
|
|
|
|
|
:disabled="submittingRefundAction"
|
|
|
|
|
|
@click="submitFailRefund"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ submittingRefundAction ? '提交中...' : '驳回退款' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
|
|
|
|
|
|
<Dialog v-model="showCreditDialog">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="px-6 py-4 border-b border-border">
|
|
|
|
|
|
<h3 class="text-lg font-semibold">
|
|
|
|
|
|
人工到账
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-xs text-muted-foreground mt-1">
|
|
|
|
|
|
订单: {{ currentOrder?.order_no || '-' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>网关订单号(可选)</Label>
|
|
|
|
|
|
<Input v-model="creditForm.gateway_order_id" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>实付金额(可选)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model.number="creditForm.pay_amount"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0.01"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>币种(可选)</Label>
|
|
|
|
|
|
<Input v-model="creditForm.pay_currency" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
|
<Label>汇率(可选)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
v-model.number="creditForm.exchange_rate"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min="0.000001"
|
|
|
|
|
|
step="0.000001"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
@click="showCreditDialog = false"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
:disabled="submittingOrderAction"
|
|
|
|
|
|
@click="submitCreditOrder"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ submittingOrderAction ? '提交中...' : '确认到账' }}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-17 10:07:52 +08:00
|
|
|
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
|
import {
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Label,
|
|
|
|
|
|
Pagination,
|
|
|
|
|
|
RefreshButton,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectContent,
|
|
|
|
|
|
SelectItem,
|
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
|
SelectValue,
|
2026-04-17 10:07:52 +08:00
|
|
|
|
Switch,
|
2026-03-08 00:05:48 +08:00
|
|
|
|
Table,
|
|
|
|
|
|
TableBody,
|
|
|
|
|
|
TableCell,
|
|
|
|
|
|
TableHead,
|
|
|
|
|
|
TableHeader,
|
|
|
|
|
|
TableRow,
|
|
|
|
|
|
Tabs,
|
|
|
|
|
|
TabsContent,
|
|
|
|
|
|
TabsList,
|
|
|
|
|
|
TabsTrigger,
|
2026-04-17 10:07:52 +08:00
|
|
|
|
Textarea,
|
2026-03-08 00:05:48 +08:00
|
|
|
|
} from '@/components/ui'
|
|
|
|
|
|
import { EmptyState } from '@/components/common'
|
|
|
|
|
|
import { X } from 'lucide-vue-next'
|
|
|
|
|
|
import {
|
|
|
|
|
|
adminWalletApi,
|
|
|
|
|
|
type AdminGlobalRefund,
|
|
|
|
|
|
type AdminLedgerTransaction,
|
|
|
|
|
|
} from '@/api/admin-wallets'
|
2026-04-17 10:07:52 +08:00
|
|
|
|
import {
|
|
|
|
|
|
adminPaymentsApi,
|
|
|
|
|
|
type PaymentCallbackRecord,
|
|
|
|
|
|
type RedeemCodeBatch,
|
|
|
|
|
|
type RedeemCodeRecord,
|
|
|
|
|
|
} from '@/api/admin-payments'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
import type { PaymentOrder } from '@/api/wallet'
|
|
|
|
|
|
import { parseApiError } from '@/utils/errorParser'
|
|
|
|
|
|
import { useToast } from '@/composables/useToast'
|
2026-06-30 14:14:12 +08:00
|
|
|
|
import { useI18n } from '@/i18n'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
import { log } from '@/utils/logger'
|
|
|
|
|
|
import {
|
|
|
|
|
|
callbackStatusBadge,
|
|
|
|
|
|
callbackStatusLabel,
|
|
|
|
|
|
formatWalletCurrency as formatCurrency,
|
|
|
|
|
|
paymentMethodLabel,
|
|
|
|
|
|
paymentStatusBadge,
|
|
|
|
|
|
paymentStatusLabel,
|
|
|
|
|
|
refundModeLabel,
|
|
|
|
|
|
refundStatusBadge,
|
|
|
|
|
|
refundStatusLabel,
|
|
|
|
|
|
walletLinkTypeLabel,
|
|
|
|
|
|
walletStatusLabel,
|
|
|
|
|
|
walletTransactionCategoryLabel,
|
|
|
|
|
|
walletTransactionReasonLabel,
|
|
|
|
|
|
} from '@/utils/walletDisplay'
|
|
|
|
|
|
|
2026-04-17 10:07:52 +08:00
|
|
|
|
type WalletManagementTab = 'ledger' | 'refunds' | 'orders' | 'callbacks' | 'redeem_codes'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
type LedgerCategory = 'recharge' | 'gift' | 'adjust' | 'refund'
|
|
|
|
|
|
type LedgerReasonOption = {
|
|
|
|
|
|
value: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
category: LedgerCategory
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const LEDGER_REASON_OPTIONS: LedgerReasonOption[] = [
|
|
|
|
|
|
{ value: 'topup_admin_manual', label: '人工充值', category: 'recharge' },
|
|
|
|
|
|
{ value: 'topup_gateway', label: '支付充值', category: 'recharge' },
|
|
|
|
|
|
{ value: 'topup_card_code', label: '卡密充值', category: 'recharge' },
|
|
|
|
|
|
{ value: 'gift_initial', label: '初始赠款', category: 'gift' },
|
|
|
|
|
|
{ value: 'gift_campaign', label: '活动赠款', category: 'gift' },
|
|
|
|
|
|
{ value: 'gift_expire_reclaim', label: '赠款回收', category: 'gift' },
|
|
|
|
|
|
{ value: 'adjust_admin', label: '人工调账', category: 'adjust' },
|
|
|
|
|
|
{ value: 'adjust_system', label: '系统调账', category: 'adjust' },
|
|
|
|
|
|
{ value: 'refund_out', label: '退款扣减', category: 'refund' },
|
|
|
|
|
|
{ value: 'refund_revert', label: '退款回补', category: 'refund' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const { success, error: showError } = useToast()
|
2026-06-30 14:14:12 +08:00
|
|
|
|
const { legacyT } = useI18n()
|
2026-03-08 00:05:48 +08:00
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
|
|
|
|
|
|
const activeTab = ref<WalletManagementTab>('ledger')
|
|
|
|
|
|
|
|
|
|
|
|
const loadingLedger = ref(false)
|
|
|
|
|
|
const loadingRefunds = ref(false)
|
|
|
|
|
|
const loadingOrders = ref(false)
|
|
|
|
|
|
const loadingCallbacks = ref(false)
|
2026-04-17 10:07:52 +08:00
|
|
|
|
const loadingRedeemBatches = ref(false)
|
|
|
|
|
|
const loadingRedeemCodes = ref(false)
|
2026-03-08 00:05:48 +08:00
|
|
|
|
const submittingRefundAction = ref(false)
|
|
|
|
|
|
const submittingOrderAction = ref(false)
|
2026-04-17 10:07:52 +08:00
|
|
|
|
const submittingRedeemBatch = ref(false)
|
2026-03-08 00:05:48 +08:00
|
|
|
|
|
|
|
|
|
|
const ledgerItems = ref<AdminLedgerTransaction[]>([])
|
|
|
|
|
|
const ledgerTotal = ref(0)
|
|
|
|
|
|
const ledgerPage = ref(1)
|
|
|
|
|
|
const ledgerPageSize = ref(20)
|
|
|
|
|
|
const ledgerCategoryFilter = ref('all')
|
|
|
|
|
|
const ledgerReasonFilter = ref('all')
|
|
|
|
|
|
const ledgerOwnerFilter = ref('all')
|
|
|
|
|
|
const ledgerReasonOptions = computed(() => {
|
|
|
|
|
|
if (ledgerCategoryFilter.value === 'all') {
|
|
|
|
|
|
return LEDGER_REASON_OPTIONS
|
|
|
|
|
|
}
|
|
|
|
|
|
return LEDGER_REASON_OPTIONS.filter((option) => option.category === ledgerCategoryFilter.value)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const refundItems = ref<AdminGlobalRefund[]>([])
|
|
|
|
|
|
const refundTotal = ref(0)
|
|
|
|
|
|
const refundPage = ref(1)
|
|
|
|
|
|
const refundPageSize = ref(20)
|
|
|
|
|
|
const refundStatusFilter = ref('all')
|
|
|
|
|
|
const refundOwnerFilter = ref('all')
|
|
|
|
|
|
|
|
|
|
|
|
const orders = ref<PaymentOrder[]>([])
|
|
|
|
|
|
const orderTotal = ref(0)
|
|
|
|
|
|
const orderPage = ref(1)
|
|
|
|
|
|
const orderPageSize = ref(20)
|
|
|
|
|
|
const orderStatusFilter = ref('all')
|
|
|
|
|
|
const orderMethodFilter = ref('all')
|
|
|
|
|
|
|
|
|
|
|
|
const callbacks = ref<PaymentCallbackRecord[]>([])
|
|
|
|
|
|
const callbackTotal = ref(0)
|
|
|
|
|
|
const callbackPage = ref(1)
|
|
|
|
|
|
const callbackPageSize = ref(20)
|
|
|
|
|
|
const callbackMethodFilter = ref('all')
|
|
|
|
|
|
|
2026-04-17 10:07:52 +08:00
|
|
|
|
const redeemBatches = ref<RedeemCodeBatch[]>([])
|
|
|
|
|
|
const redeemBatchTotal = ref(0)
|
|
|
|
|
|
const redeemBatchPage = ref(1)
|
|
|
|
|
|
const redeemBatchPageSize = ref(20)
|
|
|
|
|
|
const redeemBatchStatusFilter = ref('all')
|
|
|
|
|
|
|
|
|
|
|
|
const redeemCodes = ref<RedeemCodeRecord[]>([])
|
|
|
|
|
|
const redeemCodeTotal = ref(0)
|
|
|
|
|
|
const redeemCodePage = ref(1)
|
|
|
|
|
|
const redeemCodePageSize = ref(20)
|
|
|
|
|
|
const redeemCodeStatusFilter = ref('all')
|
|
|
|
|
|
const selectedRedeemBatchId = ref<string | null>(null)
|
|
|
|
|
|
const currentRedeemBatch = ref<RedeemCodeBatch | null>(null)
|
|
|
|
|
|
const latestGeneratedRedeemBatch = ref<RedeemCodeBatch | null>(null)
|
|
|
|
|
|
const latestGeneratedRedeemCodes = ref<Array<{ id: string; code: string; masked_code: string }>>([])
|
|
|
|
|
|
const showPlainRedeemCodes = ref(false)
|
|
|
|
|
|
const redeemCodesPanelRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const redeemBatchForm = reactive({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
amount_usd: 10,
|
|
|
|
|
|
total_count: 20,
|
|
|
|
|
|
expires_at: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const canRevealPlainRedeemCodes = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
!!currentRedeemBatch.value &&
|
|
|
|
|
|
currentRedeemBatch.value.id === latestGeneratedRedeemBatch.value?.id &&
|
|
|
|
|
|
latestGeneratedRedeemCodes.value.length > 0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const canExportLatestGeneratedRedeemCodes = computed(
|
|
|
|
|
|
() => !!latestGeneratedRedeemBatch.value && latestGeneratedRedeemCodes.value.length > 0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
const walletMetaMap = ref<Record<string, { ownerName: string; ownerType: 'user' | 'api_key' }>>({})
|
|
|
|
|
|
|
|
|
|
|
|
const showLedgerDrawer = ref(false)
|
|
|
|
|
|
const showRefundDrawer = ref(false)
|
|
|
|
|
|
const currentLedger = ref<AdminLedgerTransaction | null>(null)
|
|
|
|
|
|
const currentRefund = ref<AdminGlobalRefund | null>(null)
|
|
|
|
|
|
const loadingLedgerOrderNo = ref(false)
|
|
|
|
|
|
const ledgerPaymentOrderNo = ref<string | null>(null)
|
|
|
|
|
|
const ledgerPaymentMethod = ref<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const showCreditDialog = ref(false)
|
|
|
|
|
|
const currentOrder = ref<PaymentOrder | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const failRefundForm = reactive({
|
|
|
|
|
|
reason: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const completeRefundForm = reactive({
|
|
|
|
|
|
gateway_refund_id: '',
|
|
|
|
|
|
payout_reference: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const creditForm = reactive({
|
|
|
|
|
|
gateway_order_id: '',
|
|
|
|
|
|
pay_amount: undefined as number | undefined,
|
|
|
|
|
|
pay_currency: '',
|
|
|
|
|
|
exchange_rate: undefined as number | undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch([ledgerCategoryFilter, ledgerReasonFilter, ledgerOwnerFilter], () => {
|
|
|
|
|
|
ledgerPage.value = 1
|
|
|
|
|
|
void loadLedger()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(ledgerCategoryFilter, () => {
|
|
|
|
|
|
if (ledgerReasonFilter.value === 'all') {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const valid = ledgerReasonOptions.value.some((option) => option.value === ledgerReasonFilter.value)
|
|
|
|
|
|
if (!valid) {
|
|
|
|
|
|
ledgerReasonFilter.value = 'all'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch([refundStatusFilter, refundOwnerFilter], () => {
|
|
|
|
|
|
refundPage.value = 1
|
|
|
|
|
|
void loadRefunds()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch([orderStatusFilter, orderMethodFilter], () => {
|
|
|
|
|
|
orderPage.value = 1
|
|
|
|
|
|
void loadOrders()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(callbackMethodFilter, () => {
|
|
|
|
|
|
callbackPage.value = 1
|
|
|
|
|
|
void loadCallbacks()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-17 10:07:52 +08:00
|
|
|
|
watch(redeemBatchStatusFilter, () => {
|
|
|
|
|
|
redeemBatchPage.value = 1
|
|
|
|
|
|
void loadRedeemCodeBatches()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(redeemCodeStatusFilter, () => {
|
|
|
|
|
|
redeemCodePage.value = 1
|
|
|
|
|
|
void loadRedeemCodes()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(canRevealPlainRedeemCodes, (enabled) => {
|
|
|
|
|
|
if (!enabled) {
|
|
|
|
|
|
showPlainRedeemCodes.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => route.query.tab,
|
|
|
|
|
|
(tab) => {
|
|
|
|
|
|
const tabValue = Array.isArray(tab) ? tab[0] : tab
|
|
|
|
|
|
if (isValidTab(tabValue)) {
|
|
|
|
|
|
activeTab.value = tabValue
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
loadWalletMetaMap(),
|
|
|
|
|
|
loadLedger(),
|
|
|
|
|
|
loadRefunds(),
|
|
|
|
|
|
loadOrders(),
|
|
|
|
|
|
loadCallbacks(),
|
2026-04-17 10:07:52 +08:00
|
|
|
|
loadRedeemCodeBatches(),
|
2026-03-08 00:05:48 +08:00
|
|
|
|
])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function isValidTab(tab: unknown): tab is WalletManagementTab {
|
2026-04-17 10:07:52 +08:00
|
|
|
|
return tab === 'ledger' || tab === 'refunds' || tab === 'orders' || tab === 'callbacks' || tab === 'redeem_codes'
|
2026-03-08 00:05:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadWalletMetaMap() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const wallets = await adminWalletApi.listAllWallets()
|
|
|
|
|
|
walletMetaMap.value = wallets.reduce<Record<string, { ownerName: string; ownerType: 'user' | 'api_key' }>>(
|
|
|
|
|
|
(acc, wallet) => {
|
|
|
|
|
|
const ownerName =
|
|
|
|
|
|
wallet.owner_name || (wallet.owner_type === 'user' ? '未命名用户' : '未命名密钥')
|
|
|
|
|
|
acc[wallet.id] = {
|
|
|
|
|
|
ownerName,
|
|
|
|
|
|
ownerType: wallet.owner_type,
|
|
|
|
|
|
}
|
|
|
|
|
|
return acc
|
|
|
|
|
|
},
|
|
|
|
|
|
{}
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载钱包名称映射失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadLedger() {
|
|
|
|
|
|
loadingLedger.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const offset = (ledgerPage.value - 1) * ledgerPageSize.value
|
|
|
|
|
|
const resp = await adminWalletApi.listLedger({
|
|
|
|
|
|
category: ledgerCategoryFilter.value !== 'all' ? ledgerCategoryFilter.value : undefined,
|
|
|
|
|
|
reason_code: ledgerReasonFilter.value !== 'all' ? ledgerReasonFilter.value : undefined,
|
|
|
|
|
|
owner_type: ledgerOwnerFilter.value !== 'all' ? ledgerOwnerFilter.value : undefined,
|
|
|
|
|
|
limit: ledgerPageSize.value,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
})
|
|
|
|
|
|
ledgerItems.value = resp.items
|
|
|
|
|
|
ledgerTotal.value = resp.total
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载全局资金流水失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '加载全局资金流水失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingLedger.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadRefunds() {
|
|
|
|
|
|
loadingRefunds.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const offset = (refundPage.value - 1) * refundPageSize.value
|
|
|
|
|
|
const resp = await adminWalletApi.listGlobalRefunds({
|
|
|
|
|
|
status: refundStatusFilter.value !== 'all' ? refundStatusFilter.value : undefined,
|
|
|
|
|
|
owner_type: refundOwnerFilter.value === 'user' ? 'user' : undefined,
|
|
|
|
|
|
limit: refundPageSize.value,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
})
|
|
|
|
|
|
refundItems.value = resp.items
|
|
|
|
|
|
refundTotal.value = resp.total
|
|
|
|
|
|
if (currentRefund.value) {
|
|
|
|
|
|
syncCurrentRefund(currentRefund.value.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载全局退款列表失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '加载全局退款列表失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingRefunds.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadOrders() {
|
|
|
|
|
|
loadingOrders.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const offset = (orderPage.value - 1) * orderPageSize.value
|
|
|
|
|
|
const resp = await adminPaymentsApi.listOrders({
|
|
|
|
|
|
status: orderStatusFilter.value !== 'all' ? orderStatusFilter.value : undefined,
|
|
|
|
|
|
payment_method: orderMethodFilter.value !== 'all' ? orderMethodFilter.value : undefined,
|
|
|
|
|
|
limit: orderPageSize.value,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
})
|
|
|
|
|
|
orders.value = resp.items
|
|
|
|
|
|
orderTotal.value = resp.total
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载支付订单失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '加载支付订单失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingOrders.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadCallbacks() {
|
|
|
|
|
|
loadingCallbacks.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const offset = (callbackPage.value - 1) * callbackPageSize.value
|
|
|
|
|
|
const resp = await adminPaymentsApi.listCallbacks({
|
|
|
|
|
|
payment_method: callbackMethodFilter.value !== 'all' ? callbackMethodFilter.value : undefined,
|
|
|
|
|
|
limit: callbackPageSize.value,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
})
|
|
|
|
|
|
callbacks.value = resp.items
|
|
|
|
|
|
callbackTotal.value = resp.total
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载支付回调失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '加载支付回调失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingCallbacks.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 10:07:52 +08:00
|
|
|
|
async function loadRedeemCodeBatches() {
|
|
|
|
|
|
loadingRedeemBatches.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const offset = (redeemBatchPage.value - 1) * redeemBatchPageSize.value
|
|
|
|
|
|
const resp = await adminPaymentsApi.listRedeemCodeBatches({
|
|
|
|
|
|
status: redeemBatchStatusFilter.value !== 'all' ? redeemBatchStatusFilter.value : undefined,
|
|
|
|
|
|
limit: redeemBatchPageSize.value,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
})
|
|
|
|
|
|
redeemBatches.value = resp.items
|
|
|
|
|
|
redeemBatchTotal.value = resp.total
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedRedeemBatchId.value) {
|
|
|
|
|
|
const latest = resp.items.find(item => item.id === selectedRedeemBatchId.value)
|
|
|
|
|
|
if (latest) {
|
|
|
|
|
|
currentRedeemBatch.value = latest
|
|
|
|
|
|
await loadRedeemCodes(latest.id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedRedeemBatchId.value = null
|
|
|
|
|
|
currentRedeemBatch.value = null
|
|
|
|
|
|
redeemCodes.value = []
|
|
|
|
|
|
redeemCodeTotal.value = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载兑换码批次失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '加载兑换码批次失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingRedeemBatches.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadRedeemCodes(batchId = selectedRedeemBatchId.value || undefined) {
|
|
|
|
|
|
if (!batchId) {
|
|
|
|
|
|
redeemCodes.value = []
|
|
|
|
|
|
redeemCodeTotal.value = 0
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
loadingRedeemCodes.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const offset = (redeemCodePage.value - 1) * redeemCodePageSize.value
|
|
|
|
|
|
const resp = await adminPaymentsApi.listRedeemCodes(batchId, {
|
|
|
|
|
|
status: redeemCodeStatusFilter.value !== 'all' ? redeemCodeStatusFilter.value : undefined,
|
|
|
|
|
|
limit: redeemCodePageSize.value,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
})
|
|
|
|
|
|
currentRedeemBatch.value = resp.batch
|
|
|
|
|
|
selectedRedeemBatchId.value = resp.batch.id
|
|
|
|
|
|
redeemCodes.value = resp.items
|
|
|
|
|
|
redeemCodeTotal.value = resp.total
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载兑换码列表失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '加载兑换码列表失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingRedeemCodes.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function selectRedeemBatch(batch: RedeemCodeBatch) {
|
|
|
|
|
|
currentRedeemBatch.value = batch
|
|
|
|
|
|
selectedRedeemBatchId.value = batch.id
|
|
|
|
|
|
redeemCodePage.value = 1
|
|
|
|
|
|
await loadRedeemCodes(batch.id)
|
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
redeemCodesPanelRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function exportRedeemCodesCsv(batch: RedeemCodeBatch, codes: Array<{ id: string; code: string; masked_code: string }>) {
|
|
|
|
|
|
const header = ['id', 'batch_name', 'code', 'masked_code']
|
|
|
|
|
|
const rows = codes.map(code => [code.id, batch.name, code.code, code.masked_code])
|
|
|
|
|
|
const csv = [header, ...rows]
|
|
|
|
|
|
.map(row => row.map(cell => `"${String(cell).replaceAll('"', '""')}"`).join(','))
|
|
|
|
|
|
.join('\n')
|
|
|
|
|
|
const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' })
|
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
link.href = url
|
|
|
|
|
|
link.download = `redeem-codes-${batch.name}-${batch.id}.csv`
|
|
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitRedeemCodeBatch() {
|
|
|
|
|
|
if (!redeemBatchForm.name.trim()) {
|
|
|
|
|
|
showError('请填写批次名称')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!redeemBatchForm.amount_usd || redeemBatchForm.amount_usd <= 0) {
|
|
|
|
|
|
showError('请填写有效面额')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!redeemBatchForm.total_count || redeemBatchForm.total_count <= 0) {
|
|
|
|
|
|
showError('请填写有效数量')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
submittingRedeemBatch.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
name: redeemBatchForm.name.trim(),
|
|
|
|
|
|
amount_usd: redeemBatchForm.amount_usd,
|
|
|
|
|
|
total_count: redeemBatchForm.total_count,
|
|
|
|
|
|
expires_at: redeemBatchForm.expires_at ? new Date(redeemBatchForm.expires_at).toISOString() : undefined,
|
|
|
|
|
|
description: redeemBatchForm.description.trim() || undefined,
|
|
|
|
|
|
}
|
|
|
|
|
|
const resp = await adminPaymentsApi.createRedeemCodeBatch(payload)
|
|
|
|
|
|
latestGeneratedRedeemBatch.value = resp.batch
|
|
|
|
|
|
latestGeneratedRedeemCodes.value = resp.codes
|
|
|
|
|
|
showPlainRedeemCodes.value = true
|
|
|
|
|
|
success('兑换码批次已创建')
|
|
|
|
|
|
redeemBatchForm.name = ''
|
|
|
|
|
|
redeemBatchForm.description = ''
|
|
|
|
|
|
redeemBatchForm.expires_at = ''
|
|
|
|
|
|
currentRedeemBatch.value = resp.batch
|
|
|
|
|
|
selectedRedeemBatchId.value = resp.batch.id
|
|
|
|
|
|
await loadRedeemCodeBatches()
|
|
|
|
|
|
await loadRedeemCodes(resp.batch.id)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('创建兑换码批次失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '创建兑换码批次失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingRedeemBatch.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function exportLatestGeneratedRedeemCodes() {
|
|
|
|
|
|
if (!latestGeneratedRedeemBatch.value || latestGeneratedRedeemCodes.value.length === 0) {
|
|
|
|
|
|
showError('当前没有可导出的新生成兑换码')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
exportRedeemCodesCsv(latestGeneratedRedeemBatch.value, latestGeneratedRedeemCodes.value)
|
|
|
|
|
|
success('CSV 已导出')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function displayRedeemCode(code: RedeemCodeRecord) {
|
|
|
|
|
|
if (!showPlainRedeemCodes.value || !canRevealPlainRedeemCodes.value) {
|
|
|
|
|
|
return code.masked_code
|
|
|
|
|
|
}
|
|
|
|
|
|
return latestGeneratedRedeemCodes.value.find(item => item.id === code.id)?.code || code.masked_code
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function disableRedeemBatch(batchId: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminPaymentsApi.disableRedeemCodeBatch(batchId)
|
|
|
|
|
|
success('批次已停用')
|
|
|
|
|
|
await loadRedeemCodeBatches()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('停用兑换码批次失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '停用兑换码批次失败'))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function deleteRedeemBatch(batch: RedeemCodeBatch) {
|
|
|
|
|
|
if (batch.redeemed_count > 0) {
|
|
|
|
|
|
showError('已有兑换记录的批次不能删除')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-30 14:14:12 +08:00
|
|
|
|
if (!window.confirm(legacyT(`确认删除批次「${batch.name}」吗?删除后无法恢复。`))) {
|
2026-04-17 10:07:52 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminPaymentsApi.deleteRedeemCodeBatch(batch.id)
|
|
|
|
|
|
success('批次已删除')
|
|
|
|
|
|
if (selectedRedeemBatchId.value === batch.id) {
|
|
|
|
|
|
selectedRedeemBatchId.value = null
|
|
|
|
|
|
currentRedeemBatch.value = null
|
|
|
|
|
|
redeemCodes.value = []
|
|
|
|
|
|
redeemCodeTotal.value = 0
|
|
|
|
|
|
showPlainRedeemCodes.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (latestGeneratedRedeemBatch.value?.id === batch.id) {
|
|
|
|
|
|
latestGeneratedRedeemBatch.value = null
|
|
|
|
|
|
latestGeneratedRedeemCodes.value = []
|
|
|
|
|
|
showPlainRedeemCodes.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
await loadRedeemCodeBatches()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('删除兑换码批次失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '删除兑换码批次失败'))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function disableRedeemCode(codeId: string) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminPaymentsApi.disableRedeemCode(codeId)
|
|
|
|
|
|
success('兑换码已停用')
|
|
|
|
|
|
await Promise.all([loadRedeemCodes(), loadRedeemCodeBatches()])
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('停用兑换码失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '停用兑换码失败'))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
function orderWalletName(walletId: string) {
|
|
|
|
|
|
return walletMetaMap.value[walletId]?.ownerName || '未知钱包'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function orderWalletTypeLabel(walletId: string) {
|
|
|
|
|
|
const ownerType = walletMetaMap.value[walletId]?.ownerType
|
|
|
|
|
|
if (!ownerType) return '未知归属'
|
|
|
|
|
|
return ownerType === 'user' ? '用户钱包' : '独立密钥钱包'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openLedgerDrawer(tx: AdminLedgerTransaction) {
|
|
|
|
|
|
currentLedger.value = tx
|
|
|
|
|
|
ledgerPaymentOrderNo.value = null
|
|
|
|
|
|
ledgerPaymentMethod.value = null
|
|
|
|
|
|
showLedgerDrawer.value = true
|
|
|
|
|
|
void resolveLedgerRechargeOrderNo(tx)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function resolveLedgerRechargeOrderNo(tx: AdminLedgerTransaction) {
|
|
|
|
|
|
if (tx.link_type !== 'payment_order' || !tx.link_id) {
|
|
|
|
|
|
ledgerPaymentOrderNo.value = null
|
|
|
|
|
|
ledgerPaymentMethod.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tx.link_id.startsWith('po_')) {
|
|
|
|
|
|
ledgerPaymentOrderNo.value = tx.link_id
|
|
|
|
|
|
ledgerPaymentMethod.value = null
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loadingLedgerOrderNo.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const resp = await adminPaymentsApi.getOrder(tx.link_id)
|
|
|
|
|
|
ledgerPaymentOrderNo.value = resp.order.order_no || null
|
|
|
|
|
|
ledgerPaymentMethod.value = resp.order.payment_method || null
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('加载关联充值订单失败:', error)
|
|
|
|
|
|
ledgerPaymentOrderNo.value = null
|
|
|
|
|
|
ledgerPaymentMethod.value = null
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingLedgerOrderNo.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeLedgerDrawer() {
|
|
|
|
|
|
showLedgerDrawer.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openRefundDrawer(refund: AdminGlobalRefund) {
|
|
|
|
|
|
currentRefund.value = refund
|
|
|
|
|
|
failRefundForm.reason = ''
|
|
|
|
|
|
completeRefundForm.gateway_refund_id = ''
|
|
|
|
|
|
completeRefundForm.payout_reference = ''
|
|
|
|
|
|
showRefundDrawer.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeRefundDrawer() {
|
|
|
|
|
|
showRefundDrawer.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncCurrentRefund(refundId: string) {
|
|
|
|
|
|
const latest = refundItems.value.find((item) => item.id === refundId)
|
|
|
|
|
|
if (latest) {
|
|
|
|
|
|
currentRefund.value = latest
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function processRefund(refund: AdminGlobalRefund) {
|
|
|
|
|
|
submittingRefundAction.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminWalletApi.processRefund(refund.wallet_id, refund.id)
|
|
|
|
|
|
success('退款已进入 processing')
|
|
|
|
|
|
await Promise.all([loadRefunds(), loadLedger()])
|
|
|
|
|
|
syncCurrentRefund(refund.id)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('处理退款失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '处理退款失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingRefundAction.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitFailRefund() {
|
|
|
|
|
|
if (!currentRefund.value) return
|
|
|
|
|
|
if (!failRefundForm.reason.trim()) {
|
|
|
|
|
|
showError('请填写驳回原因')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
submittingRefundAction.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminWalletApi.failRefund(currentRefund.value.wallet_id, currentRefund.value.id, {
|
|
|
|
|
|
reason: failRefundForm.reason.trim(),
|
|
|
|
|
|
})
|
|
|
|
|
|
success('退款已驳回')
|
|
|
|
|
|
await Promise.all([loadRefunds(), loadLedger()])
|
|
|
|
|
|
syncCurrentRefund(currentRefund.value.id)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('驳回退款失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '驳回退款失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingRefundAction.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitCompleteRefund() {
|
|
|
|
|
|
if (!currentRefund.value) return
|
|
|
|
|
|
|
|
|
|
|
|
submittingRefundAction.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminWalletApi.completeRefund(currentRefund.value.wallet_id, currentRefund.value.id, {
|
|
|
|
|
|
gateway_refund_id: completeRefundForm.gateway_refund_id || undefined,
|
|
|
|
|
|
payout_reference: completeRefundForm.payout_reference || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
success('退款已完成')
|
|
|
|
|
|
await Promise.all([loadRefunds(), loadLedger()])
|
|
|
|
|
|
syncCurrentRefund(currentRefund.value.id)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('完成退款失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '完成退款失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingRefundAction.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openCreditDialog(order: PaymentOrder) {
|
|
|
|
|
|
currentOrder.value = order
|
|
|
|
|
|
creditForm.gateway_order_id = order.gateway_order_id || ''
|
|
|
|
|
|
creditForm.pay_amount = order.pay_amount || undefined
|
|
|
|
|
|
creditForm.pay_currency = order.pay_currency || ''
|
|
|
|
|
|
creditForm.exchange_rate = order.exchange_rate || undefined
|
|
|
|
|
|
showCreditDialog.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitCreditOrder() {
|
|
|
|
|
|
if (!currentOrder.value) return
|
|
|
|
|
|
submittingOrderAction.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminPaymentsApi.creditOrder(currentOrder.value.id, {
|
|
|
|
|
|
gateway_order_id: creditForm.gateway_order_id || undefined,
|
|
|
|
|
|
pay_amount: creditForm.pay_amount,
|
|
|
|
|
|
pay_currency: creditForm.pay_currency || undefined,
|
|
|
|
|
|
exchange_rate: creditForm.exchange_rate,
|
|
|
|
|
|
})
|
|
|
|
|
|
success('订单已手动到账')
|
|
|
|
|
|
showCreditDialog.value = false
|
|
|
|
|
|
await Promise.all([loadOrders(), loadLedger(), loadWalletMetaMap()])
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('手动到账失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '手动到账失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingOrderAction.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function expireOrder(orderId: string) {
|
|
|
|
|
|
submittingOrderAction.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminPaymentsApi.expireOrder(orderId)
|
|
|
|
|
|
success('订单已标记过期')
|
|
|
|
|
|
await loadOrders()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('标记过期失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '标记过期失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingOrderAction.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function failOrder(orderId: string) {
|
|
|
|
|
|
submittingOrderAction.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await adminPaymentsApi.failOrder(orderId)
|
|
|
|
|
|
success('订单已标记失败')
|
|
|
|
|
|
await loadOrders()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log.error('标记失败失败:', error)
|
|
|
|
|
|
showError(parseApiError(error, '标记失败失败'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submittingOrderAction.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canProcessRefund(status: string) {
|
|
|
|
|
|
return status === 'pending_approval' || status === 'approved'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canFailRefund(status: string) {
|
|
|
|
|
|
return status === 'processing' || status === 'pending_approval' || status === 'approved'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canCompleteRefund(status: string) {
|
|
|
|
|
|
return status === 'processing'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canCreditOrder(status: string) {
|
|
|
|
|
|
return status === 'pending' || status === 'paid'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canExpireOrder(status: string) {
|
|
|
|
|
|
return status === 'pending'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canFailOrder(status: string) {
|
|
|
|
|
|
return status !== 'credited' && status !== 'refunded'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleLedgerPageChange(page: number) {
|
|
|
|
|
|
ledgerPage.value = page
|
|
|
|
|
|
void loadLedger()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleLedgerPageSizeChange(size: number) {
|
|
|
|
|
|
ledgerPageSize.value = size
|
|
|
|
|
|
ledgerPage.value = 1
|
|
|
|
|
|
void loadLedger()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleRefundPageChange(page: number) {
|
|
|
|
|
|
refundPage.value = page
|
|
|
|
|
|
void loadRefunds()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleRefundPageSizeChange(size: number) {
|
|
|
|
|
|
refundPageSize.value = size
|
|
|
|
|
|
refundPage.value = 1
|
|
|
|
|
|
void loadRefunds()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleOrderPageChange(page: number) {
|
|
|
|
|
|
orderPage.value = page
|
|
|
|
|
|
void loadOrders()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleOrderPageSizeChange(size: number) {
|
|
|
|
|
|
orderPageSize.value = size
|
|
|
|
|
|
orderPage.value = 1
|
|
|
|
|
|
void loadOrders()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleCallbackPageChange(page: number) {
|
|
|
|
|
|
callbackPage.value = page
|
|
|
|
|
|
void loadCallbacks()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleCallbackPageSizeChange(size: number) {
|
|
|
|
|
|
callbackPageSize.value = size
|
|
|
|
|
|
callbackPage.value = 1
|
|
|
|
|
|
void loadCallbacks()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 10:07:52 +08:00
|
|
|
|
function handleRedeemBatchPageChange(page: number) {
|
|
|
|
|
|
redeemBatchPage.value = page
|
|
|
|
|
|
void loadRedeemCodeBatches()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleRedeemBatchPageSizeChange(size: number) {
|
|
|
|
|
|
redeemBatchPageSize.value = size
|
|
|
|
|
|
redeemBatchPage.value = 1
|
|
|
|
|
|
void loadRedeemCodeBatches()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleRedeemCodePageChange(page: number) {
|
|
|
|
|
|
redeemCodePage.value = page
|
|
|
|
|
|
void loadRedeemCodes()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleRedeemCodePageSizeChange(size: number) {
|
|
|
|
|
|
redeemCodePageSize.value = size
|
|
|
|
|
|
redeemCodePage.value = 1
|
|
|
|
|
|
void loadRedeemCodes()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 00:05:48 +08:00
|
|
|
|
function ownerTypeLabel(ownerType: 'user' | 'api_key') {
|
|
|
|
|
|
return ownerType === 'user' ? '用户钱包' : '独立密钥'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ownerDisplayName(name: string | null | undefined, ownerType: 'user' | 'api_key') {
|
|
|
|
|
|
if (name) return name
|
|
|
|
|
|
return ownerType === 'user' ? '未命名用户' : '未命名密钥'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDateTime(value: string | null | undefined) {
|
|
|
|
|
|
if (!value) return '-'
|
|
|
|
|
|
return new Date(value).toLocaleString('zh-CN', {
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-04-17 10:07:52 +08:00
|
|
|
|
|
|
|
|
|
|
function redeemCodeStatusLabel(status: string) {
|
|
|
|
|
|
if (status === 'active') return '可用'
|
|
|
|
|
|
if (status === 'disabled') return '已停用'
|
|
|
|
|
|
if (status === 'redeemed') return '已兑换'
|
|
|
|
|
|
return status
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function redeemCodeStatusBadge(status: string) {
|
|
|
|
|
|
if (status === 'active') return 'success'
|
|
|
|
|
|
if (status === 'disabled') return 'secondary'
|
|
|
|
|
|
if (status === 'redeemed') return 'outline'
|
|
|
|
|
|
return 'secondary'
|
|
|
|
|
|
}
|
2026-03-08 00:05:48 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.drawer-enter-active,
|
|
|
|
|
|
.drawer-leave-active {
|
|
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drawer-enter-active .drawer-panel,
|
|
|
|
|
|
.drawer-leave-active .drawer-panel {
|
|
|
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drawer-enter-from,
|
|
|
|
|
|
.drawer-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.drawer-enter-from .drawer-panel,
|
|
|
|
|
|
.drawer-leave-to .drawer-panel {
|
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|