Files
gym-manage/gym-manage-uniapp/components/memberInfo/PaymentPanel.vue
T

1025 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="payment-panel">
<!-- 支付方式 -->
<view class="pp-section">
<text class="pp-section__title">支付方式</text>
<view class="pp-payment">
<view
class="pp-payment__item"
:class="{ 'pp-payment__item--selected': selectedPayment === 'alipay' }"
@tap="selectPayment('alipay')"
>
<view class="pp-payment__icon pp-payment__icon--alipay">
<image src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/alipay.png" mode="aspectFit" />
</view>
<view class="pp-payment__info">
<text class="pp-payment__name">支付宝支付</text>
<text class="pp-payment__desc">推荐使用安全快捷</text>
</view>
<view class="pp-payment__check">
<view class="pp-payment__check-inner" :class="{ 'pp-payment__check-inner--checked': selectedPayment === 'alipay' }"></view>
</view>
</view>
<view
v-if="showStoredCard"
class="pp-payment__item"
:class="{ 'pp-payment__item--selected': selectedPayment === 'stored' }"
@tap="selectPayment('stored')"
>
<view class="pp-payment__icon pp-payment__icon--stored">💰</view>
<view class="pp-payment__info">
<text class="pp-payment__name">储值卡支付</text>
<text class="pp-payment__desc">余额: ¥{{ storedBalance.toFixed(2) }}</text>
</view>
<view class="pp-payment__check">
<view class="pp-payment__check-inner" :class="{ 'pp-payment__check-inner--checked': selectedPayment === 'stored' }"></view>
</view>
<view class="pp-payment__disabled" v-if="storedBalance < totalAmount">
<text>余额不足</text>
</view>
</view>
</view>
</view>
<!-- 金额明细 -->
<view class="pp-section">
<text class="pp-section__title">金额明细</text>
<view class="pp-amount">
<view class="pp-amount__item">
<text class="pp-amount__label">{{ amountLabel }}</text>
<text class="pp-amount__value">¥{{ totalAmount.toFixed(2) }}</text>
</view>
<view class="pp-amount__item" v-if="showStoredCard && selectedPayment === 'stored' && storedBalance > 0">
<text class="pp-amount__label">储值抵扣</text>
<text class="pp-amount__value pp-amount__value--discount">-¥{{ Math.min(storedBalance, totalAmount).toFixed(2) }}</text>
</view>
<view class="pp-amount__item pp-amount__item--total">
<text class="pp-amount__label">实付金额</text>
<text class="pp-amount__value pp-amount__value--total">¥{{ actualPay.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 底部支付栏 -->
<view class="pp-bottom">
<view class="pp-bottom__left">
<text class="pp-bottom__label">实付金额</text>
<text class="pp-bottom__price">¥{{ actualPay.toFixed(2) }}</text>
</view>
<view
class="pp-bottom__btn"
:class="{ 'pp-bottom__btn--disabled': !canPay || loading }"
@tap="handlePay"
>
<text class="pp-bottom__btn-text">{{ loading ? '处理中...' : '确认支付' }}</text>
</view>
</view>
<!-- 支付成功弹窗 -->
<view class="success-modal-overlay" v-if="showSuccessModal" @tap="closeSuccessModal">
<view class="success-modal-dialog" @tap.stop>
<view class="success-modal__icon">🎉</view>
<text class="success-modal__title">{{ successTitle }}</text>
<text class="success-modal__desc">{{ successDesc }}</text>
<view class="success-modal__buttons">
<view class="success-modal__btn success-modal__btn--secondary" @tap="handleBack">
<text>返回上一页</text>
</view>
<view class="success-modal__btn success-modal__btn--primary" @tap="handleGoToResult">
<text>{{ successBtnText }}</text>
</view>
</view>
</view>
</view>
<!-- 支付密码验证弹窗 -->
<view class="pay-password-modal-overlay" v-if="showPayPasswordModal" @tap="closePayPasswordModal">
<view class="pay-password-modal-dialog" @tap.stop>
<view class="pay-password-modal__header">
<text class="pay-password-modal__title">验证支付密码</text>
<view class="pay-password-modal__close" @tap="closePayPasswordModal">
<text></text>
</view>
</view>
<view class="pay-password-modal__body">
<text class="pay-password-modal__amount">¥{{ totalAmount.toFixed(2) }}</text>
<VerifyCodeInput
ref="payPasswordInputRef"
v-model="payPassword"
:length="6"
:mask="true"
type="box"
:errorMsg="payPasswordError"
@complete="handlePayPasswordComplete"
/>
<view class="pay-password-modal__forgot" @tap="goForgotPassword">
<text>忘记密码</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { checkPayPasswordSet, verifyPayPassword, payWithStoredCard as storedCardPay, createPayment, createQrCodePayment, getPaymentStatus, getPendingOrder, closeOrder } from '@/api/main.js'
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
const props = defineProps({
orderType: {
type: String,
required: true
},
goodsDesc: {
type: String,
required: true
},
remark: {
type: String,
default: ''
},
totalAmount: {
type: Number,
required: true
},
showStoredCard: {
type: Boolean,
default: false
},
storedBalance: {
type: Number,
default: 0
},
memberId: {
type: [Number, String],
required: true
},
amountLabel: {
type: String,
default: '商品金额'
},
successTitle: {
type: String,
default: '支付成功'
},
successDesc: {
type: String,
default: '恭喜您,支付成功!'
},
successBtnText: {
type: String,
default: '查看详情'
},
showSuccessModalOnSuccess: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['success', 'fail', 'cancel', 'storedBalanceUpdate', 'goToResult'])
const selectedPayment = ref('alipay')
const loading = ref(false)
const paymentResolved = ref(false)
const currentOrderId = ref(null)
const currentPayUrl = ref('')
const showSuccessModal = ref(false)
let pollTimer = null
let pollCount = 0
const MAX_POLL_COUNT = 10
const showPayPasswordModal = ref(false)
const payPassword = ref('')
const payPasswordError = ref('')
const payPasswordInputRef = ref(null)
const actualPay = computed(() => {
if (selectedPayment.value === 'stored') {
return Math.max(0, props.totalAmount - props.storedBalance)
}
return props.totalAmount
})
const canPay = computed(() => {
if (props.totalAmount <= 0) return false
if (selectedPayment.value === 'alipay') {
return true
}
if (selectedPayment.value === 'stored') {
return props.storedBalance >= props.totalAmount
}
return false
})
function selectPayment(type) {
if (type === 'stored') {
if (props.storedBalance < props.totalAmount) {
uni.showToast({ title: '储值余额不足', icon: 'none' })
return
}
uni.showLoading({ title: '检查支付密码...' })
checkPayPasswordSet(props.memberId)
.then(res => {
uni.hideLoading()
if (!res.data?.isSet) {
uni.showModal({
title: '提示',
content: '您尚未设置支付密码,请先设置支付密码',
showCancel: false,
success: () => {
uni.navigateTo({ url: '/pages/memberInfo/setPayPassword' })
}
})
return
}
selectedPayment.value = type
})
.catch(e => {
uni.hideLoading()
console.error('检查支付密码失败:', e)
uni.showToast({ title: '检查失败', icon: 'none' })
})
return
}
selectedPayment.value = type
}
function handlePay() {
if (!canPay.value || loading.value) return
loading.value = true
if (selectedPayment.value === 'alipay') {
payWithAlipay()
} else if (selectedPayment.value === 'stored') {
payWithStoredCard()
}
}
function payWithStoredCard() {
if (props.storedBalance < props.totalAmount) {
uni.showToast({
title: `余额不足,当前余额:¥${props.storedBalance.toFixed(2)}`,
icon: 'none',
duration: 3000
})
loading.value = false
return
}
payPassword.value = ''
payPasswordError.value = ''
showPayPasswordModal.value = true
nextTick(() => {
payPasswordInputRef.value?.focusInput()
})
}
function closePayPasswordModal() {
showPayPasswordModal.value = false
payPassword.value = ''
payPasswordError.value = ''
if (loading.value) {
loading.value = false
}
}
function handlePayPasswordComplete(password) {
payPasswordError.value = ''
doPayWithStoredCard(password)
}
function goForgotPassword() {
uni.showToast({ title: '请联系客服重置密码', icon: 'none' })
}
async function doPayWithStoredCard(password) {
uni.showLoading({ title: '处理中...' })
try {
const payRes = await storedCardPay(props.memberId, password, props.totalAmount)
if (payRes.code !== 200) {
uni.hideLoading()
const msg = payRes.message || '支付失败'
if (msg.includes('密码错误') || msg.includes('密码')) {
payPasswordError.value = msg
payPassword.value = ''
nextTick(() => {
payPasswordInputRef.value?.focusInput()
})
} else {
uni.showToast({ title: msg, icon: 'none' })
loading.value = false
closePayPasswordModal()
emit('fail', payRes)
}
return
}
const newBalance = payRes.data?.balance || 0
emit('storedBalanceUpdate', newBalance)
closePayPasswordModal()
paymentResolved.value = true
uni.hideLoading()
uni.showToast({ title: '支付成功', icon: 'success' })
if (props.showSuccessModalOnSuccess) {
showSuccessModal.value = true
}
emit('success', {
orderId: null,
payType: 'stored',
balance: newBalance
})
} catch (e) {
uni.hideLoading()
console.error('储值卡支付失败:', e)
uni.showToast({ title: '支付失败,请稍后重试', icon: 'none' })
loading.value = false
closePayPasswordModal()
emit('fail', e)
}
}
function payWithAlipay() {
const transAmt = String(Math.round(props.totalAmount * 100))
const confirmContent = `确认支付 ¥${props.totalAmount.toFixed(2)} ${props.goodsDesc}`
uni.showModal({
title: '确认支付',
content: confirmContent,
confirmText: '确认支付',
confirmColor: '#1677FF',
success: async (modalRes) => {
if (!modalRes.confirm) {
loading.value = false
emit('cancel')
return
}
try {
const pendingRes = await getPendingOrder(props.memberId, props.orderType)
if (pendingRes.code === 200 && pendingRes.data) {
const pendingOrder = pendingRes.data
const pendingAmount = pendingOrder.transAmt || (pendingOrder.amount ? (pendingOrder.amount / 100).toFixed(2) : '0.00')
uni.showModal({
title: '存在未支付订单',
content: `您有一笔¥${pendingAmount}${props.goodsDesc}订单尚未支付,是否继续支付该订单?\n\n点击"支付旧订单"继续支付\n点击"创建新订单"关闭旧订单并重新创建`,
confirmText: '支付旧订单',
cancelText: '创建新订单',
confirmColor: '#1677FF',
success: async (chooseRes) => {
if (chooseRes.confirm) {
useExistingOrder(pendingOrder)
} else {
try {
const orderId = pendingOrder.orderId || pendingOrder.merchantOrderId || pendingOrder.orderNo
await closeOrder(orderId)
createNewPayment(transAmt)
} catch (e) {
console.error('[Payment] 关闭旧订单失败:', e)
uni.showToast({ title: '关闭旧订单失败,请重试', icon: 'none' })
loading.value = false
}
}
},
fail: () => {
loading.value = false
}
})
} else {
createNewPayment(transAmt)
}
} catch (e) {
console.error('[Payment] 查询未支付订单失败:', e)
createNewPayment(transAmt)
}
},
fail: () => {
loading.value = false
emit('cancel')
}
})
}
function useExistingOrder(order) {
currentOrderId.value = order.orderId || order.merchantOrderId || order.orderNo
currentPayUrl.value = order.payUrl || order.payInfo || order.h5PayUrl
paymentResolved.value = false
const payUrl = currentPayUrl.value
const isWebEnv = typeof navigator !== 'undefined' && typeof plus === 'undefined'
if (isWebEnv && !payUrl) {
uni.showToast({ title: '请扫码支付', icon: 'none' })
startPolling()
return
}
if (payUrl && typeof plus !== 'undefined') {
const encodedUrl = encodeURIComponent(payUrl)
const alipayScheme = `alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`
plus.runtime.openURL(alipayScheme, function(err) {
console.log('[Alipay] 唤起支付宝App失败,尝试H5支付:', err)
plus.runtime.openURL(payUrl, function(h5Err) {
console.error('[Alipay] H5支付也失败:', h5Err)
uni.showModal({ title: '提示', content: '无法唤起支付宝,请确认已安装', showCancel: false })
loading.value = false
})
})
startPolling()
} else if (payUrl) {
window.location.href = payUrl
startPolling()
}
}
async function createNewPayment(transAmt) {
const isWebEnv = typeof navigator !== 'undefined' && typeof plus === 'undefined'
try {
let payRes
if (isWebEnv) {
payRes = await createQrCodePayment({
memberId: props.memberId,
orderType: props.orderType,
goodsDesc: props.goodsDesc,
transAmt: transAmt,
tradeType: 'QRCODE',
remark: props.remark
})
} else {
payRes = await createPayment({
memberId: props.memberId,
orderType: props.orderType,
goodsDesc: props.goodsDesc,
transAmt: transAmt,
tradeType: 'ALIPAY',
remark: props.remark
})
}
if (payRes.code !== 200) {
uni.showToast({ title: payRes.message || '创建订单失败', icon: 'none' })
loading.value = false
emit('fail', payRes)
return
}
currentOrderId.value = payRes.data.orderId || payRes.data.merchantOrderId || payRes.data.tradeNo
currentPayUrl.value = payRes.data.payUrl || payRes.data.payInfo || payRes.data.h5PayUrl
paymentResolved.value = false
if (isWebEnv) {
uni.showToast({ title: '请扫码支付', icon: 'none' })
} else {
const payUrl = currentPayUrl.value
if (payUrl && typeof plus !== 'undefined') {
const encodedUrl = encodeURIComponent(payUrl)
const alipayScheme = `alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`
plus.runtime.openURL(alipayScheme, function(err) {
console.log('[Alipay] 唤起支付宝App失败,尝试H5支付:', err)
plus.runtime.openURL(payUrl, function(h5Err) {
console.error('[Alipay] H5支付也失败:', h5Err)
uni.showModal({ title: '提示', content: '无法唤起支付宝,请确认已安装', showCancel: false })
loading.value = false
})
})
} else if (payUrl) {
window.location.href = payUrl
}
}
startPolling()
} catch (e) {
console.error('[Payment] 创建订单失败:', e)
uni.showToast({ title: '创建订单失败', icon: 'none' })
loading.value = false
emit('fail', e)
}
}
function handleAppResume() {
if (paymentResolved.value || !currentOrderId.value) {
return
}
console.log('[PaymentPanel] APP从后台返回,查询支付结果')
startPolling()
}
function startPolling() {
if (paymentResolved.value || !currentOrderId.value) {
return
}
stopPolling()
pollCount = 0
console.log('[PaymentPanel] 开始轮询支付状态')
checkPaymentStatus()
pollTimer = setInterval(() => {
pollCount++
console.log(`[PaymentPanel] 轮询第 ${pollCount}`)
if (pollCount >= MAX_POLL_COUNT) {
console.log('[PaymentPanel] 轮询次数达到上限,停止轮询')
stopPolling()
return
}
checkPaymentStatus(true)
}, 2000)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
function checkPaymentStatus(silent = false) {
if (paymentResolved.value || !currentOrderId.value) {
stopPolling()
return
}
if (!silent) {
uni.showLoading({ title: '查询支付结果...' })
}
const queryOrderId = currentOrderId.value
getPaymentStatus(queryOrderId)
.then(res => {
if (!silent) {
uni.hideLoading()
}
if (res.code === 200 && res.data) {
const status = res.data.status || res.data.payStatus
console.log('[PaymentPanel] 支付状态:', status)
if (status === 'SUCCESS') {
paymentResolved.value = true
stopPolling()
uni.showToast({ title: '支付成功', icon: 'success', duration: 2000 })
if (props.showSuccessModalOnSuccess) {
showSuccessModal.value = true
}
emit('success', {
orderId: queryOrderId,
payType: 'alipay',
orderInfo: res.data
})
} else if (status === 'FAIL') {
paymentResolved.value = true
stopPolling()
uni.showToast({ title: '支付失败', icon: 'error' })
loading.value = false
emit('fail', res.data)
} else if (status === 'CLOSED') {
paymentResolved.value = true
stopPolling()
uni.showToast({ title: '订单已关闭', icon: 'none' })
loading.value = false
emit('fail', res.data)
}
} else {
if (!silent) {
uni.hideLoading()
}
}
})
.catch(() => {
if (!silent) {
uni.hideLoading()
}
console.error('[PaymentPanel] 查询支付状态失败')
})
}
function closeSuccessModal() {
showSuccessModal.value = false
}
function handleBack() {
showSuccessModal.value = false
uni.navigateBack()
}
function handleGoToResult() {
showSuccessModal.value = false
emit('goToResult')
}
onMounted(() => {
if (typeof plus !== 'undefined') {
plus.globalEvent.addEventListener('resume', handleAppResume)
}
})
onShow(() => {
if (currentOrderId.value && !paymentResolved.value) {
console.log('[PaymentPanel] 页面显示,检查支付状态')
startPolling()
}
})
onUnmounted(() => {
paymentResolved.value = true
stopPolling()
if (typeof plus !== 'undefined') {
plus.globalEvent.removeEventListener('resume', handleAppResume)
}
})
defineExpose({
getCurrentOrderId: () => currentOrderId.value,
isPaymentResolved: () => paymentResolved.value,
resetPayment: () => {
paymentResolved.value = false
currentOrderId.value = null
currentPayUrl.value = ''
stopPolling()
}
})
</script>
<style lang="scss" scoped>
.payment-panel {
display: flex;
flex-direction: column;
flex: 1;
}
.pp-section {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.pp-section__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.pp-payment {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.pp-payment__item {
display: flex;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
border: 2rpx solid #eee;
position: relative;
transition: all 0.2s;
}
.pp-payment__item--selected {
border-color: #1677FF;
background: #F0F7FF;
}
.pp-payment__icon {
width: 60rpx;
height: 60rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.pp-payment__icon--alipay {
background: #1677FF;
image {
width: 40rpx;
height: 40rpx;
}
}
.pp-payment__icon--stored {
background: #FFD700;
}
.pp-payment__info {
flex: 1;
}
.pp-payment__name {
font-size: 28rpx;
font-weight: 500;
color: #333;
display: block;
}
.pp-payment__desc {
font-size: 24rpx;
color: #999;
margin-top: 6rpx;
display: block;
}
.pp-payment__check {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #ddd;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.pp-payment__check-inner {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
background: transparent;
transition: all 0.2s;
}
.pp-payment__check-inner--checked {
background: #1677FF;
}
.pp-payment__disabled {
position: absolute;
right: 80rpx;
top: 50%;
transform: translateY(-50%);
font-size: 24rpx;
color: #FF4D4F;
background: rgba(255, 77, 79, 0.1);
padding: 6rpx 12rpx;
border-radius: 8rpx;
}
.pp-amount {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.pp-amount__item {
display: flex;
justify-content: space-between;
align-items: center;
}
.pp-amount__label {
font-size: 28rpx;
color: #666;
}
.pp-amount__value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.pp-amount__value--discount {
color: #1677FF;
}
.pp-amount__item--total {
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
}
.pp-amount__item--total .pp-amount__label {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.pp-amount__item--total .pp-amount__value--total {
font-size: 36rpx;
font-weight: 700;
color: #FF4D4F;
}
.pp-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
z-index: 100;
}
.pp-bottom__left {
flex: 1;
}
.pp-bottom__label {
font-size: 24rpx;
color: #999;
display: block;
}
.pp-bottom__price {
font-size: 40rpx;
font-weight: 700;
color: #FF4D4F;
margin-top: 4rpx;
display: block;
}
.pp-bottom__btn {
background: #1677FF;
border-radius: 48rpx;
padding: 20rpx 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.pp-bottom__btn--disabled {
background: #B8D2FF;
}
.pp-bottom__btn-text {
color: #fff;
font-size: 30rpx;
font-weight: 500;
}
.success-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.success-modal-dialog {
width: 600rpx;
background: #fff;
border-radius: 24rpx;
padding: 60rpx 40rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.success-modal__icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.success-modal__title {
font-size: 36rpx;
font-weight: 700;
color: #333;
margin-bottom: 12rpx;
}
.success-modal__desc {
font-size: 28rpx;
color: #999;
text-align: center;
margin-bottom: 40rpx;
}
.success-modal__buttons {
display: flex;
gap: 20rpx;
width: 100%;
}
.success-modal__btn {
flex: 1;
padding: 24rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 28rpx;
font-weight: 500;
}
}
.success-modal__btn--secondary {
background: #f5f5f5;
text {
color: #666;
}
}
.success-modal__btn--primary {
background: #1677FF;
text {
color: #fff;
}
}
.pay-password-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
animation: fadeIn 0.3s ease;
}
.pay-password-modal-dialog {
width: 85%;
max-width: 360px;
background: #FFFFFF;
border-radius: 20px;
overflow: hidden;
animation: modalPop 0.3s ease;
}
.pay-password-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 12px;
}
.pay-password-modal__title {
font-size: 18px;
font-weight: 600;
color: #1A202C;
}
.pay-password-modal__close {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #A0AEC0;
font-size: 16px;
}
.pay-password-modal__body {
padding: 0 24px 32px;
display: flex;
flex-direction: column;
align-items: center;
}
.pay-password-modal__amount {
font-size: 32px;
font-weight: 700;
color: #E53E3E;
margin-bottom: 24px;
}
.pay-password-modal__forgot {
margin-top: 16px;
text-align: right;
width: 100%;
text {
font-size: 13px;
color: #1677FF;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalPop {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>