mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
支持官方直连支付、退款配置与套餐联动
This commit is contained in:
@@ -114,14 +114,14 @@
|
||||
<Select v-model="selectedChannel">
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
:placeholder="epayOptions.length ? '选择支付通道' : '暂无可用支付通道'"
|
||||
:placeholder="checkoutOptions.length ? '选择支付通道' : '暂无可用支付通道'"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="option in epayOptions"
|
||||
:key="`${option.payment_channel}-${option.display_name}`"
|
||||
:value="option.payment_channel || ''"
|
||||
v-for="option in checkoutOptions"
|
||||
:key="option.key"
|
||||
:value="option.key"
|
||||
>
|
||||
{{ option.display_name }}
|
||||
</SelectItem>
|
||||
@@ -131,7 +131,7 @@
|
||||
class="w-full"
|
||||
:disabled="
|
||||
checkoutLoadingPlanId === plan.id
|
||||
|| epayOptions.length === 0
|
||||
|| checkoutOptions.length === 0
|
||||
|| !selectedChannel
|
||||
"
|
||||
@click="checkoutPlan(plan)"
|
||||
@@ -168,16 +168,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="latestPaymentUrl"
|
||||
v-if="latestCheckoutActionLabel"
|
||||
variant="outline"
|
||||
@click="openPaymentUrl(latestPaymentUrl)"
|
||||
@click="openLatestPayment"
|
||||
>
|
||||
打开支付链接
|
||||
{{ latestCheckoutActionLabel }}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<StripePaymentDialog
|
||||
v-model:open="stripeDialogOpen"
|
||||
:instructions="stripePaymentInstructions"
|
||||
title="套餐 Stripe 支付"
|
||||
description="完成支付后,套餐权益会由 Stripe Webhook 自动发放。"
|
||||
confirm-text="支付套餐"
|
||||
@success="handleStripePaymentSuccess"
|
||||
/>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
@@ -203,11 +212,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui'
|
||||
import { EmptyState, LoadingState } from '@/components/common'
|
||||
import { EmptyState, LoadingState, StripePaymentDialog } from '@/components/common'
|
||||
import { CardSection, PageContainer, PageHeader } from '@/components/layout'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
import {
|
||||
getPaymentInstructionString,
|
||||
getStripePaymentInstructions,
|
||||
type PaymentInstructionMap,
|
||||
} from '@/utils/paymentInstructions'
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
|
||||
@@ -218,14 +232,29 @@ const rechargeOptions = ref<WalletRechargeOption[]>([])
|
||||
const selectedChannel = ref('')
|
||||
const checkoutLoadingPlanId = ref<string | null>(null)
|
||||
const latestCheckout = ref<BillingCheckoutResponse | null>(null)
|
||||
const stripeDialogOpen = ref(false)
|
||||
const stripePaymentInstructions = ref<PaymentInstructionMap | null>(null)
|
||||
|
||||
const epayOptions = computed(() =>
|
||||
rechargeOptions.value.filter((option) =>
|
||||
(option.payment_provider === 'epay' || option.payment_method === 'epay')
|
||||
&& Boolean(option.payment_channel)
|
||||
)
|
||||
const checkoutOptions = computed(() =>
|
||||
rechargeOptions.value
|
||||
.filter((option) => Boolean(paymentOptionProvider(option)) && Boolean(option.payment_channel))
|
||||
.map((option, index) => ({
|
||||
...option,
|
||||
key: [
|
||||
paymentOptionProvider(option),
|
||||
option.payment_method,
|
||||
option.payment_channel || '',
|
||||
index,
|
||||
].join(':'),
|
||||
}))
|
||||
)
|
||||
|
||||
const selectedCheckoutOption = computed(() => {
|
||||
if (checkoutOptions.value.length === 0) return null
|
||||
return checkoutOptions.value.find(option => option.key === selectedChannel.value)
|
||||
|| checkoutOptions.value[0]
|
||||
})
|
||||
|
||||
const activeEntitlements = computed(() =>
|
||||
entitlements.value.filter((item) =>
|
||||
item.active !== false
|
||||
@@ -243,12 +272,17 @@ const latestPaymentUrl = computed(() => {
|
||||
return typeof value === 'string' && value ? value : ''
|
||||
})
|
||||
|
||||
watch(epayOptions, (options) => {
|
||||
const channels = options
|
||||
.map(option => option.payment_channel || '')
|
||||
.filter(Boolean)
|
||||
if (!channels.includes(selectedChannel.value)) {
|
||||
selectedChannel.value = channels[0] || ''
|
||||
const latestCheckoutActionLabel = computed(() => {
|
||||
const instructions = latestCheckout.value?.payment_instructions
|
||||
if (getStripePaymentInstructions(instructions)) return '打开 Stripe 支付'
|
||||
if (latestPaymentUrl.value) return '打开支付链接'
|
||||
return ''
|
||||
})
|
||||
|
||||
watch(checkoutOptions, (options) => {
|
||||
const keys = options.map(option => option.key)
|
||||
if (!keys.includes(selectedChannel.value)) {
|
||||
selectedChannel.value = keys[0] || ''
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
@@ -285,8 +319,8 @@ async function loadRechargeOptions() {
|
||||
try {
|
||||
const response = await walletApi.listRechargeOptions()
|
||||
rechargeOptions.value = response.items
|
||||
if (!selectedChannel.value && epayOptions.value.length > 0) {
|
||||
selectedChannel.value = epayOptions.value[0].payment_channel || ''
|
||||
if (!selectedChannel.value && checkoutOptions.value.length > 0) {
|
||||
selectedChannel.value = checkoutOptions.value[0].key
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('加载支付通道失败:', err)
|
||||
@@ -299,12 +333,22 @@ async function checkoutPlan(plan: BillingPlan) {
|
||||
const confirmed = window.confirm('购买成功后,同类旧套餐会自动失效。确定继续购买吗?')
|
||||
if (!confirmed) return
|
||||
}
|
||||
const option = selectedCheckoutOption.value
|
||||
if (!option) {
|
||||
showError('请选择支付方式')
|
||||
return
|
||||
}
|
||||
const provider = paymentOptionProvider(option)
|
||||
if (!provider) {
|
||||
showError('支付方式配置无效')
|
||||
return
|
||||
}
|
||||
checkoutLoadingPlanId.value = plan.id
|
||||
try {
|
||||
const response = await billingApi.checkout(plan.id, {
|
||||
payment_method: 'epay',
|
||||
payment_provider: 'epay',
|
||||
payment_channel: selectedChannel.value,
|
||||
payment_method: option.payment_method || provider,
|
||||
payment_provider: provider,
|
||||
payment_channel: option.payment_channel,
|
||||
})
|
||||
latestCheckout.value = response
|
||||
success('套餐订单已创建')
|
||||
@@ -317,14 +361,20 @@ async function checkoutPlan(plan: BillingPlan) {
|
||||
}
|
||||
}
|
||||
|
||||
function openPaymentUrl(url: string) {
|
||||
submitPaymentInstructions(latestCheckout.value?.payment_instructions || { payment_url: url })
|
||||
function openLatestPayment() {
|
||||
submitPaymentInstructions(latestCheckout.value?.payment_instructions || { payment_url: latestPaymentUrl.value })
|
||||
}
|
||||
|
||||
function submitPaymentInstructions(instructions: Record<string, unknown> | null | undefined) {
|
||||
if (!instructions) return
|
||||
const paymentUrl = instructions.payment_url
|
||||
if (typeof paymentUrl !== 'string' || !paymentUrl) return
|
||||
const stripeInstructions = getStripePaymentInstructions(instructions)
|
||||
if (stripeInstructions) {
|
||||
stripePaymentInstructions.value = instructions
|
||||
stripeDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
const paymentUrl = getPaymentInstructionString(instructions, 'payment_url')
|
||||
if (!paymentUrl) return
|
||||
const paymentParams = instructions.payment_params
|
||||
if (paymentParams && typeof paymentParams === 'object' && !Array.isArray(paymentParams)) {
|
||||
submitPaymentForm(paymentUrl, paymentParams as Record<string, unknown>)
|
||||
@@ -360,6 +410,15 @@ function isSafariBrowser(): boolean {
|
||||
return navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')
|
||||
}
|
||||
|
||||
async function handleStripePaymentSuccess() {
|
||||
success('支付已完成,正在刷新套餐状态')
|
||||
await Promise.all([loadEntitlements(), loadPlans()])
|
||||
}
|
||||
|
||||
function paymentOptionProvider(option: WalletRechargeOption): string {
|
||||
return (option.payment_provider || option.provider || option.payment_method || '').trim()
|
||||
}
|
||||
|
||||
function planTitle(planId: string): string {
|
||||
return plans.value.find((plan) => plan.id === planId)?.title || planId
|
||||
}
|
||||
|
||||
@@ -195,6 +195,12 @@
|
||||
>
|
||||
· {{ option.pay_currency }}
|
||||
</span>
|
||||
<span
|
||||
v-if="Number(option.fee_rate || 0) > 0"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
· 手续费 {{ Number(option.fee_rate || 0).toFixed(2) }}%
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -208,10 +214,14 @@
|
||||
预计支付:
|
||||
<span class="font-medium text-foreground">
|
||||
{{ estimatedRechargePayAmount }}
|
||||
{{ selectedRechargeOption.pay_currency || 'CNY' }}
|
||||
{{ rechargePayCurrency }}
|
||||
</span>
|
||||
· 1 USD = {{ Number(selectedRechargeOption.usd_exchange_rate).toFixed(4) }}
|
||||
{{ selectedRechargeOption.pay_currency || 'CNY' }}
|
||||
{{ rechargePayCurrency }}
|
||||
<template v-if="estimatedRechargeFeeAmount > 0">
|
||||
· 手续费 {{ estimatedRechargeFeeAmount.toFixed(2) }} {{ rechargePayCurrency }}
|
||||
({{ estimatedRechargeFeeRate.toFixed(2) }}%)
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -239,15 +249,23 @@
|
||||
</Badge>
|
||||
</div>
|
||||
<a
|
||||
v-if="latestRecharge.payment_instructions?.payment_url"
|
||||
v-if="latestRechargePaymentUrl"
|
||||
class="inline-flex text-xs text-primary hover:underline"
|
||||
:href="String(latestRecharge.payment_instructions.payment_url)"
|
||||
:href="latestRechargePaymentUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.prevent="submitPaymentInstructions(latestRecharge.payment_instructions)"
|
||||
>
|
||||
打开支付链接
|
||||
</a>
|
||||
<button
|
||||
v-if="latestRechargeStripeInstructions"
|
||||
type="button"
|
||||
class="inline-flex text-xs text-primary hover:underline"
|
||||
@click="submitPaymentInstructions(latestRecharge?.payment_instructions)"
|
||||
>
|
||||
打开 Stripe 支付
|
||||
</button>
|
||||
<div
|
||||
v-if="latestRecharge.payment_instructions?.qr_code"
|
||||
class="text-xs text-muted-foreground break-all"
|
||||
@@ -263,11 +281,18 @@
|
||||
申请退款
|
||||
</h3>
|
||||
<RefreshButton
|
||||
:loading="loadingRefunds"
|
||||
@click="loadRefunds"
|
||||
:loading="loadingRefunds || loadingRefundEligibility"
|
||||
@click="refreshRefundPanel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!loadingRefundEligibility && refundableOrders.length === 0"
|
||||
class="rounded-xl border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground"
|
||||
>
|
||||
当前没有开启用户自助退款的可退充值订单。
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label>退款金额 (USD)</Label>
|
||||
@@ -299,15 +324,12 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label>关联充值订单(可选)</Label>
|
||||
<Label>关联充值订单</Label>
|
||||
<Select v-model="refundForm.payment_order_id">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="不指定订单,直接从钱包余额退款" />
|
||||
<SelectValue placeholder="选择允许用户退款的订单" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">
|
||||
不指定
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="order in refundableOrders"
|
||||
:key="order.id"
|
||||
@@ -329,13 +351,13 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
|
||||
仅充值余额可退款,赠款余额不可退款。
|
||||
仅开启“允许用户退款”的支付方式可由用户自助提交退款申请。
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:disabled="submittingRefund"
|
||||
:disabled="submittingRefund || refundableOrders.length === 0"
|
||||
@click="submitRefund"
|
||||
>
|
||||
{{ submittingRefund ? '提交中...' : '提交退款申请' }}
|
||||
@@ -647,6 +669,15 @@
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<StripePaymentDialog
|
||||
v-model:open="stripeDialogOpen"
|
||||
:instructions="stripePaymentInstructions"
|
||||
title="钱包 Stripe 支付"
|
||||
description="完成支付后,钱包余额会由 Stripe Webhook 自动入账。"
|
||||
confirm-text="支付充值"
|
||||
@success="handleStripePaymentSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -677,7 +708,7 @@ import {
|
||||
TabsTrigger,
|
||||
Textarea,
|
||||
} from '@/components/ui'
|
||||
import { EmptyState, LoadingState } from '@/components/common'
|
||||
import { EmptyState, LoadingState, StripePaymentDialog } from '@/components/common'
|
||||
import {
|
||||
walletApi,
|
||||
type DailyUsageRecord,
|
||||
@@ -691,6 +722,11 @@ import {
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { log } from '@/utils/logger'
|
||||
import {
|
||||
getPaymentInstructionString,
|
||||
getStripePaymentInstructions,
|
||||
type PaymentInstructionMap,
|
||||
} from '@/utils/paymentInstructions'
|
||||
import {
|
||||
dailyUsageCategoryLabel,
|
||||
formatTokenCount,
|
||||
@@ -715,6 +751,7 @@ const loadingInitial = ref(true)
|
||||
const loadingTransactions = ref(false)
|
||||
const loadingOrders = ref(false)
|
||||
const loadingRefunds = ref(false)
|
||||
const loadingRefundEligibility = ref(false)
|
||||
const submittingRedeem = ref(false)
|
||||
const submittingRecharge = ref(false)
|
||||
const submittingRefund = ref(false)
|
||||
@@ -723,6 +760,8 @@ const walletBalance = ref<WalletBalanceResponse | null>(null)
|
||||
const latestRecharge = ref<{ order: PaymentOrder; payment_instructions: Record<string, unknown> } | null>(null)
|
||||
const latestRedeem = ref<WalletRedeemResponse | null>(null)
|
||||
const rechargeOptions = ref<WalletRechargeOption[]>([])
|
||||
const stripeDialogOpen = ref(false)
|
||||
const stripePaymentInstructions = ref<PaymentInstructionMap | null>(null)
|
||||
|
||||
const flowItems = ref<FlowItem[]>([])
|
||||
const todayUsage = ref<DailyUsageRecord | null>(null)
|
||||
@@ -734,6 +773,7 @@ const rechargeOrders = ref<PaymentOrder[]>([])
|
||||
const orderTotal = ref(0)
|
||||
const orderPage = ref(1)
|
||||
const orderPageSize = ref(20)
|
||||
const refundEligiblePaymentMethods = ref<Set<string>>(new Set())
|
||||
|
||||
const refunds = ref<RefundRequest[]>([])
|
||||
const refundTotal = ref(0)
|
||||
@@ -750,7 +790,7 @@ const rechargeForm = reactive({
|
||||
|
||||
const refundForm = reactive({
|
||||
amount_usd: 0,
|
||||
payment_order_id: '__none__',
|
||||
payment_order_id: '',
|
||||
refund_mode: 'offline_payout',
|
||||
reason: '',
|
||||
})
|
||||
@@ -760,7 +800,10 @@ const redeemForm = reactive({
|
||||
})
|
||||
|
||||
const refundableOrders = computed(() =>
|
||||
rechargeOrders.value.filter(o => (o.refundable_amount_usd || 0) > 0)
|
||||
rechargeOrders.value.filter(order =>
|
||||
(order.refundable_amount_usd || 0) > 0
|
||||
&& refundEligiblePaymentMethods.value.has(refundPaymentMethod(order))
|
||||
)
|
||||
)
|
||||
|
||||
const rechargeOptionsWithKey = computed(() =>
|
||||
@@ -781,12 +824,45 @@ const selectedRechargeOption = computed(() => {
|
||||
|| rechargeOptionsWithKey.value[0]
|
||||
})
|
||||
|
||||
const estimatedRechargePayAmount = computed(() => {
|
||||
function roundPayAmount(value: number): number {
|
||||
return Math.round(value * 100) / 100
|
||||
}
|
||||
|
||||
const rechargePaymentBreakdown = computed(() => {
|
||||
const rate = Number(selectedRechargeOption.value?.usd_exchange_rate || 0)
|
||||
if (!Number.isFinite(rate) || rate <= 0) return '-'
|
||||
return (Number(rechargeForm.amount_usd || 0) * rate).toFixed(2)
|
||||
const amount = Number(rechargeForm.amount_usd || 0)
|
||||
if (!Number.isFinite(rate) || rate <= 0 || !Number.isFinite(amount) || amount <= 0) return null
|
||||
const rawFeeRate = Number(selectedRechargeOption.value?.fee_rate || 0)
|
||||
const feeRate = Number.isFinite(rawFeeRate) && rawFeeRate > 0 ? rawFeeRate : 0
|
||||
const basePayAmount = roundPayAmount(amount * rate)
|
||||
const feeAmount = roundPayAmount(basePayAmount * feeRate / 100)
|
||||
return {
|
||||
basePayAmount,
|
||||
feeAmount,
|
||||
feeRate,
|
||||
totalPayAmount: roundPayAmount(basePayAmount + feeAmount),
|
||||
}
|
||||
})
|
||||
|
||||
const rechargePayCurrency = computed(() => selectedRechargeOption.value?.pay_currency || 'CNY')
|
||||
|
||||
const estimatedRechargePayAmount = computed(() => {
|
||||
if (!rechargePaymentBreakdown.value) return '-'
|
||||
return rechargePaymentBreakdown.value.totalPayAmount.toFixed(2)
|
||||
})
|
||||
const estimatedRechargeFeeAmount = computed(() =>
|
||||
rechargePaymentBreakdown.value?.feeAmount || 0
|
||||
)
|
||||
const estimatedRechargeFeeRate = computed(() =>
|
||||
rechargePaymentBreakdown.value?.feeRate || 0
|
||||
)
|
||||
const latestRechargePaymentUrl = computed(() =>
|
||||
getPaymentInstructionString(latestRecharge.value?.payment_instructions, 'payment_url')
|
||||
)
|
||||
const latestRechargeStripeInstructions = computed(() =>
|
||||
getStripePaymentInstructions(latestRecharge.value?.payment_instructions)
|
||||
)
|
||||
|
||||
const dailyQuota = computed(() => walletBalance.value?.daily_quota ?? null)
|
||||
const hasActiveDailyQuota = computed(() => Boolean(dailyQuota.value?.has_active))
|
||||
const walletOnlyBalance = computed(() => {
|
||||
@@ -836,6 +912,7 @@ onMounted(async () => {
|
||||
loadTodayCost(),
|
||||
loadOrders(),
|
||||
loadRefunds(),
|
||||
loadRefundEligibility(),
|
||||
loadRechargeOptions(),
|
||||
])
|
||||
syncTodayCostPolling()
|
||||
@@ -853,6 +930,10 @@ watch(activeTab, () => {
|
||||
syncTodayCostPolling()
|
||||
})
|
||||
|
||||
watch(refundableOrders, () => {
|
||||
syncRefundOrderSelection()
|
||||
})
|
||||
|
||||
async function loadBalance() {
|
||||
walletBalance.value = await walletApi.getBalance()
|
||||
}
|
||||
@@ -928,6 +1009,7 @@ async function loadOrders() {
|
||||
const resp = await walletApi.listRechargeOrders({ limit: orderPageSize.value, offset })
|
||||
rechargeOrders.value = resp.items
|
||||
orderTotal.value = resp.total
|
||||
syncRefundOrderSelection()
|
||||
} catch (error) {
|
||||
log.error('加载充值订单失败:', error)
|
||||
showError(parseApiError(error, '加载充值订单失败'))
|
||||
@@ -936,6 +1018,24 @@ async function loadOrders() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRefundEligibility() {
|
||||
loadingRefundEligibility.value = true
|
||||
try {
|
||||
const resp = await walletApi.listRefundEligibleProviders()
|
||||
refundEligiblePaymentMethods.value = new Set(
|
||||
(resp.payment_methods || [])
|
||||
.map(item => item.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
)
|
||||
syncRefundOrderSelection()
|
||||
} catch (error) {
|
||||
refundEligiblePaymentMethods.value = new Set()
|
||||
log.error('加载退款资格失败:', error)
|
||||
} finally {
|
||||
loadingRefundEligibility.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRefunds() {
|
||||
loadingRefunds.value = true
|
||||
try {
|
||||
@@ -951,6 +1051,10 @@ async function loadRefunds() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRefundPanel() {
|
||||
await Promise.all([loadRefunds(), loadRefundEligibility(), loadOrders()])
|
||||
}
|
||||
|
||||
async function submitRedeem() {
|
||||
if (!redeemForm.code.trim()) {
|
||||
showError('请输入兑换码')
|
||||
@@ -1011,14 +1115,23 @@ async function submitRecharge() {
|
||||
|
||||
function submitPaymentInstructions(instructions: Record<string, unknown> | null | undefined) {
|
||||
if (!instructions) return
|
||||
const paymentUrl = instructions.payment_url
|
||||
if (typeof paymentUrl !== 'string' || !paymentUrl) return
|
||||
const stripeInstructions = getStripePaymentInstructions(instructions)
|
||||
if (stripeInstructions) {
|
||||
stripePaymentInstructions.value = instructions
|
||||
stripeDialogOpen.value = true
|
||||
return
|
||||
}
|
||||
const paymentUrl = getPaymentInstructionString(instructions, 'payment_url')
|
||||
if (!paymentUrl) return
|
||||
const paymentParams = instructions.payment_params
|
||||
if (paymentParams && typeof paymentParams === 'object' && !Array.isArray(paymentParams)) {
|
||||
submitPaymentForm(paymentUrl, paymentParams as Record<string, unknown>)
|
||||
return
|
||||
}
|
||||
window.open(paymentUrl, '_blank', 'noopener,noreferrer')
|
||||
const opened = window.open(paymentUrl, '_blank', 'noopener,noreferrer')
|
||||
if (!opened) {
|
||||
window.location.href = paymentUrl
|
||||
}
|
||||
}
|
||||
|
||||
function submitPaymentForm(url: string, params: Record<string, unknown>) {
|
||||
@@ -1045,11 +1158,26 @@ function isSafariBrowser(): boolean {
|
||||
return navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome')
|
||||
}
|
||||
|
||||
async function handleStripePaymentSuccess() {
|
||||
success('支付已完成,正在刷新钱包余额')
|
||||
await Promise.all([loadBalance(), loadOrders(), loadTransactions(), loadTodayCost()])
|
||||
activeTab.value = 'orders'
|
||||
}
|
||||
|
||||
async function submitRefund() {
|
||||
if (!refundForm.amount_usd || refundForm.amount_usd <= 0) {
|
||||
showError('请输入有效的退款金额')
|
||||
return
|
||||
}
|
||||
const selectedOrder = refundableOrders.value.find(order => order.id === refundForm.payment_order_id)
|
||||
if (!selectedOrder) {
|
||||
showError('请选择允许用户退款的充值订单')
|
||||
return
|
||||
}
|
||||
if (refundForm.amount_usd > (selectedOrder.refundable_amount_usd || 0)) {
|
||||
showError(`退款金额超过该订单可退金额(当前可退 ${formatCurrency(selectedOrder.refundable_amount_usd || 0)})`)
|
||||
return
|
||||
}
|
||||
const refundableBalance =
|
||||
walletBalance.value?.wallet?.refundable_balance ?? walletBalance.value?.refundable_balance ?? null
|
||||
if (refundableBalance !== null && refundForm.amount_usd > refundableBalance) {
|
||||
@@ -1061,19 +1189,15 @@ async function submitRefund() {
|
||||
try {
|
||||
await walletApi.createRefund({
|
||||
amount_usd: refundForm.amount_usd,
|
||||
payment_order_id:
|
||||
refundForm.payment_order_id && refundForm.payment_order_id !== '__none__'
|
||||
? refundForm.payment_order_id
|
||||
: undefined,
|
||||
payment_order_id: selectedOrder.id,
|
||||
refund_mode: refundForm.refund_mode || undefined,
|
||||
reason: refundForm.reason || undefined,
|
||||
idempotency_key: `web_refund_${buildRefundIdempotencyKey()}`,
|
||||
})
|
||||
success('退款申请已提交')
|
||||
refundForm.amount_usd = 0
|
||||
refundForm.payment_order_id = '__none__'
|
||||
refundForm.reason = ''
|
||||
await Promise.all([loadRefunds(), loadBalance(), loadOrders(), loadTransactions(), loadTodayCost()])
|
||||
await Promise.all([loadRefunds(), loadBalance(), loadOrders(), loadRefundEligibility(), loadTransactions(), loadTodayCost()])
|
||||
activeTab.value = 'refunds'
|
||||
} catch (error) {
|
||||
log.error('提交退款申请失败:', error)
|
||||
@@ -1083,6 +1207,15 @@ async function submitRefund() {
|
||||
}
|
||||
}
|
||||
|
||||
function refundPaymentMethod(order: PaymentOrder): string {
|
||||
return String(order.payment_provider || order.payment_method || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function syncRefundOrderSelection() {
|
||||
if (refundableOrders.value.some(order => order.id === refundForm.payment_order_id)) return
|
||||
refundForm.payment_order_id = refundableOrders.value[0]?.id || ''
|
||||
}
|
||||
|
||||
function buildRefundIdempotencyKey(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID().replaceAll('-', '')
|
||||
|
||||
Reference in New Issue
Block a user