571 lines
18 KiB
Vue
571 lines
18 KiB
Vue
<template>
|
||
<a-modal
|
||
:destroy-on-close="true"
|
||
:footer="footerContent"
|
||
:mask-closable="false"
|
||
:open="visible"
|
||
:title="title"
|
||
:width="payMethod === 'native' ? 480 : 420"
|
||
@cancel="handleClose"
|
||
>
|
||
<!-- 订单信息 -->
|
||
<div class="mb-5">
|
||
<a-descriptions :column="1" bordered size="small">
|
||
<a-descriptions-item label="订单号">
|
||
<span class="font-mono text-sm">{{ orderNo }}</span>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="订单金额">
|
||
<span class="text-xl font-bold text-orange-500">¥{{ payPrice }}</span>
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
</div>
|
||
|
||
<!-- 支付方式选择 -->
|
||
<div v-if="!started" class="space-y-3">
|
||
<div class="text-sm text-gray-500 mb-2">请选择支付方式</div>
|
||
|
||
<!-- 微信支付 -->
|
||
<div
|
||
v-if="availableMethods.wechat"
|
||
:class="{ active: selectedMethod === 'wechat' }"
|
||
class="pay-method-item"
|
||
@click="selectedMethod = 'wechat'"
|
||
>
|
||
<svg class="h-8 w-8" fill="#07C160" viewBox="0 0 24 24">
|
||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.11.24-.245 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.87a5.755 5.755 0 0 0-.407-.012zm-1.155 2.26c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.857 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/>
|
||
</svg>
|
||
<div class="flex-1 ml-3">
|
||
<div class="font-medium">微信支付</div>
|
||
<div class="text-xs text-gray-400">推荐</div>
|
||
</div>
|
||
<a-radio :checked="selectedMethod === 'wechat'" />
|
||
</div>
|
||
|
||
<!-- 支付宝 -->
|
||
<div
|
||
v-if="availableMethods.alipay"
|
||
:class="{ active: selectedMethod === 'alipay' }"
|
||
class="pay-method-item"
|
||
@click="selectedMethod = 'alipay'"
|
||
>
|
||
<svg class="h-8 w-8" fill="#1677FF" viewBox="0 0 24 24">
|
||
<path d="M21.97 11.1c-1.23-.79-2.75-1.04-4.33-.86-1.09-1.65-2.78-2.59-4.72-2.59-2.91 0-5.27 2.27-5.27 5.06 0 2.07 1.25 3.89 3.1 4.78V13.2c-.42-.11-.86-.17-1.32-.17-2.56 0-4.64 2.03-4.64 4.53 0 2.51 2.08 4.53 4.64 4.53.32 0 .63-.03.93-.08v2.58c-.31.04-.63.06-.95.06-3.45 0-6.25-2.74-6.25-6.1 0-3.36 2.8-6.1 6.25-6.1 1.01 0 1.95.24 2.78.67-.05-.32-.08-.65-.08-.99 0-2.59 2.15-4.69 4.79-4.69 1.52 0 2.88.68 3.78 1.75.19-.03.38-.05.58-.05 1.94 0 3.51 1.57 3.51 3.51 0 1.06-.48 2-1.24 2.64 1.1.4 1.83 1.47 1.83 2.67 0 1.73-1.49 3.13-3.32 3.13-.65 0-1.26-.18-1.78-.5.06.22.1.45.1.69 0 2.07-1.7 3.75-3.79 3.75-.45 0-.89-.07-1.3-.22v2.49c.45.13.92.2 1.4.2 2.97 0 5.38-2.36 5.38-5.26 0-2.22-1.32-4.15-3.22-4.93zm-8.16 2.09c-.46 0-.84-.37-.84-.83s.38-.84.84-.84.84.38.84.84-.38.83-.84.83zm5.14-.83c0-.46.38-.84.84-.84s.84.38.84.84-.38.83-.84.83-.84-.37-.84-.83zm-.87 2.49l-.69-.55c-.34-.27-.55-.67-.55-1.1 0-.74.61-1.34 1.36-1.34.46 0 .88.24 1.12.62l.77.62c.01-.35-.28-.67-.64-.8v-.06h-.09c-.54 0-.98-.44-.98-.98s.44-.98.98-.98.98.44.98.98v.03h.05l.15.12c.57.45.91 1.14.91 1.87 0 1.21-.89 2.23-2.05 2.48l-.33-.91h-.01zm-2.24-1.38h.08c.34 0 .61-.27.61-.61s-.27-.61-.61-.61h-.08v1.22zm.61 2.16c-.14.04-.29.06-.45.06h-.16v-1.16h.25c.26 0 .47.1.47.36 0 .38-.06.67-.11.74zm-.11-3.52c.18 0 .32-.14.32-.32s-.14-.32-.32-.32-.32.14-.32.32.14.32.32.32zm0 1.86c-.18 0-.32.14-.32.32s.14.32.32.32.32-.14.32-.32-.14-.32-.32-.32z"/>
|
||
</svg>
|
||
<div class="flex-1 ml-3">
|
||
<div class="font-medium">支付宝</div>
|
||
<div class="text-xs text-gray-400">安全便捷</div>
|
||
</div>
|
||
<a-radio :checked="selectedMethod === 'alipay'" />
|
||
</div>
|
||
|
||
<!-- 余额支付 -->
|
||
<div
|
||
v-if="availableMethods.balance && userBalance !== undefined"
|
||
:class="{ active: selectedMethod === 'balance' }"
|
||
class="pay-method-item"
|
||
@click="selectedMethod = 'balance'"
|
||
>
|
||
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-yellow-100 text-yellow-600">
|
||
<WalletOutlined />
|
||
</div>
|
||
<div class="flex-1 ml-3">
|
||
<div class="font-medium">余额支付</div>
|
||
<div class="text-xs text-gray-400">可用余额:¥{{ userBalance }}</div>
|
||
</div>
|
||
<a-radio :checked="selectedMethod === 'balance'" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Native 二维码支付 -->
|
||
<div v-else-if="payMethod === 'native' && !paid" class="text-center">
|
||
<div v-if="loading" class="py-10">
|
||
<a-spin size="large" />
|
||
<div class="mt-3 text-gray-500">正在获取支付二维码...</div>
|
||
</div>
|
||
|
||
<div v-else-if="codeUrl" class="py-4">
|
||
<div class="text-sm text-gray-500 mb-4">请使用微信/支付宝扫描下方二维码完成支付</div>
|
||
<div class="flex justify-center bg-white p-4 rounded-lg">
|
||
<a-qrcode :size="200" :value="codeUrl" />
|
||
</div>
|
||
<div class="mt-4 text-sm text-gray-500">
|
||
<template v-if="countdown > 0">
|
||
二维码 {{ countdown }} 秒后过期,请尽快支付
|
||
</template>
|
||
<a-button v-else size="small" type="link" @click="rebuildQrcode">
|
||
二维码已过期,点击重新获取
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="errorMsg" class="py-10 text-center">
|
||
<a-result :sub-title="errorMsg" status="error" title="获取二维码失败">
|
||
<template #extra>
|
||
<a-button type="primary" @click="rebuildQrcode">重试</a-button>
|
||
</template>
|
||
</a-result>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 等待支付状态 -->
|
||
<div v-else-if="!paid" class="text-center py-6">
|
||
<a-spin size="large" />
|
||
<div class="mt-4">
|
||
<div class="text-base font-medium">等待支付中...</div>
|
||
<div class="text-sm text-gray-500 mt-1">
|
||
{{ selectedMethod === 'balance' ? '正在处理余额支付...' : '请在支付页面完成付款' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支付成功 -->
|
||
<div v-else class="text-center py-6">
|
||
<a-result status="success" sub-title="您的订单已支付成功,页面即将跳转..." title="支付成功">
|
||
<template #extra>
|
||
<a-button type="primary" @click="handleSuccess">查看订单</a-button>
|
||
<a-button @click="handleClose">继续逛逛</a-button>
|
||
</template>
|
||
</a-result>
|
||
</div>
|
||
|
||
<!-- 错误提示 -->
|
||
<div v-if="errorMsg && !paid" class="mt-4">
|
||
<a-alert :message="errorMsg" show-icon type="error" />
|
||
</div>
|
||
</a-modal>
|
||
|
||
<!-- 支付宝跳转提示 -->
|
||
<a-modal
|
||
v-model:open="alipayRedirectVisible"
|
||
:footer="null"
|
||
:mask-closable="false"
|
||
centered
|
||
title="正在跳转支付宝"
|
||
>
|
||
<div class="text-center py-6">
|
||
<a-spin size="large" />
|
||
<div class="mt-4 text-base">正在跳转到支付宝支付页面</div>
|
||
<div class="text-sm text-gray-500 mt-2">
|
||
如果支付页面没有弹出,<a :href="alipayUrl" target="_blank">点击这里</a>继续支付
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, computed, watch, h } from 'vue'
|
||
import { message } from 'ant-design-vue'
|
||
import { WalletOutlined } from '@ant-design/icons-vue'
|
||
import {
|
||
detectPayEnvironment,
|
||
isWechatBrowser,
|
||
isAlipayBrowser,
|
||
createWechatJsapiPay,
|
||
createWechatH5Pay,
|
||
createWechatNativePay,
|
||
createAlipayPay,
|
||
queryPayStatus
|
||
} from '@/api/payment'
|
||
import type { PayResult } from '@/api/payment'
|
||
|
||
/** Props */
|
||
const props = defineProps<{
|
||
visible: boolean
|
||
orderNo: string
|
||
orderId?: number
|
||
payPrice: string | number
|
||
/** 可用支付方式,默认全部 */
|
||
availableMethods?: {
|
||
wechat?: boolean
|
||
alipay?: boolean
|
||
balance?: boolean
|
||
native?: boolean
|
||
}
|
||
/** 用户余额(余额支付时) */
|
||
userBalance?: number
|
||
/** 标题 */
|
||
title?: string
|
||
}>()
|
||
|
||
/** Emits */
|
||
const emit = defineEmits<{
|
||
(e: 'update:visible', val: boolean): void
|
||
(e: 'success', result: PayResult): void
|
||
(e: 'close'): void
|
||
}>()
|
||
|
||
/** 状态 */
|
||
const selectedMethod = ref<'wechat' | 'alipay' | 'balance'>('wechat')
|
||
const started = ref(false)
|
||
const loading = ref(false)
|
||
const paid = ref(false)
|
||
const errorMsg = ref('')
|
||
const codeUrl = ref('')
|
||
const countdown = ref(0)
|
||
const alipayRedirectVisible = ref(false)
|
||
const alipayUrl = ref('')
|
||
|
||
// 轮询定时器
|
||
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
/** 默认可用支付方式 */
|
||
const defaultMethods = { wechat: true, alipay: true, balance: true, native: true }
|
||
const methods = computed(() => ({ ...defaultMethods, ...props.availableMethods }))
|
||
|
||
/** 实际使用的支付方式 */
|
||
const payMethod = computed(() => {
|
||
if (!started.value) return 'select'
|
||
|
||
const env = detectPayEnvironment()
|
||
|
||
// 微信浏览器内 → JSAPI
|
||
if (selectedMethod.value === 'wechat' && isWechatBrowser()) {
|
||
return 'wechat_jsapi'
|
||
}
|
||
|
||
// 支付宝浏览器内 → 支付宝 WAP
|
||
if (selectedMethod.value === 'alipay' && isAlipayBrowser()) {
|
||
return 'alipay_wap'
|
||
}
|
||
|
||
// 桌面端 / 其他情况 → Native 或 H5
|
||
if (env === 'desktop') {
|
||
return 'native'
|
||
}
|
||
|
||
return 'h5'
|
||
})
|
||
|
||
/** 底部按钮 */
|
||
const footerContent = computed(() => {
|
||
if (paid.value || started.value) return null
|
||
|
||
return [
|
||
h('a-button', { onClick: handleClose }, '取消'),
|
||
h('a-button', {
|
||
type: 'primary',
|
||
loading: loading.value,
|
||
onClick: startPay
|
||
}, `确认支付 ¥${props.payPrice}`)
|
||
]
|
||
})
|
||
|
||
/** 关闭弹窗 */
|
||
function handleClose() {
|
||
stopPoll()
|
||
emit('update:visible', false)
|
||
emit('close')
|
||
}
|
||
|
||
/** 支付成功 */
|
||
function handleSuccess() {
|
||
stopPoll()
|
||
emit('success', { orderNo: props.orderNo })
|
||
handleClose()
|
||
}
|
||
|
||
/** 开始支付 */
|
||
async function startPay() {
|
||
errorMsg.value = ''
|
||
loading.value = true
|
||
|
||
try {
|
||
const params = {
|
||
orderNo: props.orderNo,
|
||
subject: `订单支付-${props.orderNo}`,
|
||
body: 'WebSoft 产品订单',
|
||
totalAmount: Math.round(Number(props.payPrice) * 100) // 转为分
|
||
}
|
||
|
||
let result: PayResult
|
||
|
||
switch (payMethod.value) {
|
||
case 'wechat_jsapi':
|
||
result = await createWechatJsapiPay({
|
||
...params,
|
||
openId: await getWxOpenId()
|
||
})
|
||
await handleWechatJsapiResult(result)
|
||
break
|
||
|
||
case 'native':
|
||
started.value = true
|
||
result = await createWechatNativePay(params)
|
||
handleNativeResult(result)
|
||
break
|
||
|
||
case 'h5':
|
||
if (selectedMethod.value === 'wechat') {
|
||
result = await createWechatH5Pay({ ...params, returnUrl: window.location.href })
|
||
handleH5Result(result, 'wechat')
|
||
} else {
|
||
result = await createAlipayPay({ ...params, returnUrl: window.location.href })
|
||
handleH5Result(result, 'alipay')
|
||
}
|
||
break
|
||
|
||
case 'alipay_wap':
|
||
result = await createAlipayPay({ ...params, returnUrl: window.location.href })
|
||
handleH5Result(result, 'alipay')
|
||
break
|
||
|
||
case 'balance':
|
||
await handleBalancePay()
|
||
break
|
||
|
||
default:
|
||
throw new Error('不支持的支付方式')
|
||
}
|
||
} catch (err: unknown) {
|
||
const e = err as Error
|
||
errorMsg.value = e.message || '发起支付失败'
|
||
message.error(errorMsg.value)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 处理微信 JSAPI 支付结果 */
|
||
async function handleWechatJsapiResult(result: PayResult) {
|
||
// 如果返回的是跳转链接(H5支付链接),直接跳转
|
||
if (result.mwebUrl) {
|
||
window.location.href = result.mwebUrl
|
||
return
|
||
}
|
||
|
||
// JSAPI 调起支付
|
||
if (result.prepayId) {
|
||
const appId = result.codeUrl || '' // 实际应该从后端获取
|
||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||
const nonceStr = Math.random().toString(36).slice(2)
|
||
const pkg = `prepay_id=${result.prepayId}`
|
||
const signType = 'MD5'
|
||
const paySign = '' // 后端需要返回签名
|
||
|
||
try {
|
||
// 动态加载微信 JSSDK
|
||
await loadWechatJSSDK()
|
||
|
||
// @ts-ignore
|
||
window.WeixinJSBridge?.invoke('getBrandWCPayRequest', {
|
||
appId,
|
||
timeStamp: timestamp,
|
||
nonceStr,
|
||
package: pkg,
|
||
signType,
|
||
paySign
|
||
}, (res: { err_msg: string }) => {
|
||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||
paid.value = true
|
||
startPoll()
|
||
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||
errorMsg.value = '用户取消支付'
|
||
} else {
|
||
errorMsg.value = res.err_msg
|
||
}
|
||
})
|
||
} catch {
|
||
// 如果 JSSDK 不可用,降级到 H5 支付
|
||
const h5Result = await createWechatH5Pay({
|
||
orderNo: props.orderNo,
|
||
subject: `订单支付-${props.orderNo}`,
|
||
totalAmount: Math.round(Number(props.payPrice) * 100)
|
||
})
|
||
if (h5Result.mwebUrl) {
|
||
window.location.href = h5Result.mwebUrl
|
||
} else {
|
||
throw new Error('JSAPI 支付失败')
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 处理 Native 支付结果 */
|
||
function handleNativeResult(result: PayResult) {
|
||
loading.value = false
|
||
|
||
if (result.codeUrl) {
|
||
codeUrl.value = result.codeUrl
|
||
// 二维码有效期 2 分钟
|
||
countdown.value = 120
|
||
startCountdown()
|
||
startPoll()
|
||
} else if (result.qrcode) {
|
||
codeUrl.value = result.qrcode
|
||
countdown.value = 120
|
||
startCountdown()
|
||
startPoll()
|
||
} else {
|
||
errorMsg.value = '获取支付二维码失败'
|
||
}
|
||
}
|
||
|
||
/** 处理 H5 支付结果 */
|
||
function handleH5Result(result: PayResult, type: 'wechat' | 'alipay') {
|
||
if (type === 'alipay' && (result.paymentUrl || result.payUrl)) {
|
||
const url = result.paymentUrl || result.payUrl
|
||
if (result.paymentUrl?.includes('https://openapi.alipay.com')) {
|
||
// PC 场景:显示二维码
|
||
alipayRedirectVisible.value = true
|
||
alipayUrl.value = url
|
||
} else if (result.paymentUrl?.includes('https://m.alipay.com')) {
|
||
// H5 场景:直接跳转
|
||
window.location.href = url
|
||
} else {
|
||
// 未知格式,直接跳转
|
||
window.location.href = url
|
||
}
|
||
started.value = true
|
||
startPoll()
|
||
} else if (type === 'wechat' && result.mwebUrl) {
|
||
window.location.href = result.mwebUrl
|
||
}
|
||
}
|
||
|
||
/** 余额支付 */
|
||
async function handleBalancePay() {
|
||
started.value = true
|
||
loading.value = false
|
||
|
||
// 余额支付直接查询状态
|
||
try {
|
||
const status = await queryPayStatus(props.orderNo)
|
||
if (status.paid || status.payStatus === 1) {
|
||
paid.value = true
|
||
} else {
|
||
startPoll()
|
||
}
|
||
} catch {
|
||
// 轮询查询
|
||
startPoll()
|
||
}
|
||
}
|
||
|
||
/** 轮询支付状态 */
|
||
async function startPoll() {
|
||
stopPoll()
|
||
pollTimer = setTimeout(async () => {
|
||
try {
|
||
const status = await queryPayStatus(props.orderNo)
|
||
if (status.paid || status.payStatus === 1) {
|
||
paid.value = true
|
||
stopPoll()
|
||
setTimeout(() => handleSuccess(), 1500)
|
||
} else {
|
||
startPoll()
|
||
}
|
||
} catch {
|
||
startPoll()
|
||
}
|
||
}, 2000)
|
||
}
|
||
|
||
/** 停止轮询 */
|
||
function stopPoll() {
|
||
if (pollTimer) {
|
||
clearTimeout(pollTimer)
|
||
pollTimer = null
|
||
}
|
||
if (countdownTimer) {
|
||
clearInterval(countdownTimer)
|
||
countdownTimer = null
|
||
}
|
||
}
|
||
|
||
/** 二维码倒计时 */
|
||
function startCountdown() {
|
||
countdownTimer = setInterval(() => {
|
||
if (countdown.value > 0) {
|
||
countdown.value--
|
||
} else {
|
||
stopPoll()
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
/** 重新获取二维码 */
|
||
async function rebuildQrcode() {
|
||
loading.value = true
|
||
errorMsg.value = ''
|
||
codeUrl.value = ''
|
||
|
||
try {
|
||
const params = {
|
||
orderNo: props.orderNo,
|
||
subject: `订单支付-${props.orderNo}`,
|
||
totalAmount: Math.round(Number(props.payPrice) * 100)
|
||
}
|
||
|
||
const result = await createWechatNativePay(params)
|
||
handleNativeResult(result)
|
||
} catch (err: unknown) {
|
||
const e = err as Error
|
||
errorMsg.value = e.message || '获取二维码失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 获取微信 OpenId */
|
||
async function getWxOpenId(): Promise<string> {
|
||
// 从 localStorage 或 Vuex 获取已存储的 openId
|
||
const stored = localStorage.getItem('wx_openid')
|
||
if (stored) return stored
|
||
|
||
// 如果没有,需要先通过 OAuth 获取
|
||
// 这里可以引导用户授权,或从后端获取
|
||
throw new Error('请先在微信中打开,或联系客服获取支付方式')
|
||
}
|
||
|
||
/** 动态加载微信 JSSDK */
|
||
async function loadWechatJSSDK(): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
// 如果已加载
|
||
if (window.WeixinJSBridge || window.jWeixin) {
|
||
resolve()
|
||
return
|
||
}
|
||
|
||
// 加载 JSSDK
|
||
const script = document.createElement('script')
|
||
script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js'
|
||
script.onload = () => resolve()
|
||
script.onerror = () => reject(new Error('微信 JSSDK 加载失败'))
|
||
document.head.appendChild(script)
|
||
})
|
||
}
|
||
|
||
// 监听 visible 变化
|
||
watch(() => props.visible, (val) => {
|
||
if (!val) {
|
||
stopPoll()
|
||
started.value = false
|
||
paid.value = false
|
||
errorMsg.value = ''
|
||
codeUrl.value = ''
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.pay-method-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.pay-method-item:hover {
|
||
border-color: #d1d5db;
|
||
}
|
||
|
||
.pay-method-item.active {
|
||
border-color: #1890ff;
|
||
background: #f0f9ff;
|
||
}
|
||
</style>
|