Files
gym-manage/gym-manage-uniapp/components/global/PayPasswordModal.vue
T

352 lines
7.0 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="ppm-overlay" v-if="visible" @tap="handleCancel">
<view class="ppm-dialog" @tap.stop>
<!-- 头部 -->
<view class="ppm-header">
<text class="ppm-header__icon">&#128274;</text>
<text class="ppm-header__title">{{ title }}</text>
<text v-if="subtitle" class="ppm-header__subtitle">{{ subtitle }}</text>
</view>
<!-- 金额展示 -->
<view v-if="amount != null" class="ppm-amount">
<text class="ppm-amount__label">{{ amountLabel }}</text>
<text class="ppm-amount__value">¥{{ formatAmount(amount) }}</text>
</view>
<!-- 退款提示免费次数 -->
<view v-if="cancelNotice && cancelNotice.length" class="ppm-notice">
<text>{{ cancelNotice }}</text>
</view>
<!-- 密码输入 -->
<view class="ppm-input-wrap">
<view class="ppm-code-box">
<view
v-for="i in 6"
:key="i"
class="ppm-code-item"
:class="{
'ppm-code-item--focus': focusIndex === i - 1 && password.length < 6,
'ppm-code-item--filled': password.length >= i
}"
>
<text v-if="password.length >= i" class="ppm-code-dot"></text>
<view v-if="focusIndex === i - 1 && password.length === i - 1" class="ppm-code-cursor"></view>
</view>
</view>
</view>
<!-- 隐藏输入框 -->
<input
class="ppm-hidden-input"
type="number"
:maxlength="6"
:focus="inputFocus"
:value="password"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
/>
<!-- 提示 -->
<view class="ppm-tips">
<text>请输入6位支付密码</text>
</view>
<!-- 底部按钮 -->
<view class="ppm-buttons">
<view class="ppm-btn ppm-btn--cancel" @tap="handleCancel">
<text>取消</text>
</view>
<view
class="ppm-btn ppm-btn--confirm"
:class="{ 'ppm-btn--disabled': password.length < 6 || loading }"
@tap="handleConfirm"
>
<text>{{ loading ? '验证中...' : '确认' }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
const props = defineProps({
visible: { type: Boolean, default: false },
title: { type: String, default: '支付验证' },
subtitle: { type: String, default: '' },
amount: { type: [Number, String], default: null },
amountLabel: { type: String, default: '支付金额' },
cancelNotice: { type: String, default: '' },
errorMsg: { type: String, default: '' },
loading: { type: Boolean, default: false }
})
const emit = defineEmits(['confirm', 'cancel'])
const password = ref('')
const inputFocus = ref(false)
const focusIndex = ref(0)
watch(() => props.visible, (val) => {
if (val) {
password.value = ''
focusIndex.value = 0
nextTick(() => {
setTimeout(() => {
inputFocus.value = true
}, 300)
})
}
})
function formatAmount(val) {
return Number(val).toFixed(2)
}
function onInput(e) {
let val = String(e.detail.value || '').replace(/\D/g, '').slice(0, 6)
password.value = val
focusIndex.value = val.length
}
function onFocus() {
inputFocus.value = true
focusIndex.value = password.value.length
}
function onBlur() {
// Keep focusIndex, just mark input as not focused
}
function handleConfirm() {
if (password.value.length < 6 || props.loading) return
emit('confirm', password.value)
}
function handleCancel() {
emit('cancel')
}
function clear() {
password.value = ''
focusIndex.value = 0
}
defineExpose({ clear })
</script>
<style lang="scss" scoped>
.ppm-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: 9999;
animation: ppm-fade-in 0.25s ease;
}
@keyframes ppm-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.ppm-dialog {
width: 600rpx;
background: #FFFFFF;
border-radius: 32rpx;
padding: 48rpx 40rpx 36rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.15);
animation: ppm-scale-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
display: flex;
flex-direction: column;
align-items: center;
}
@keyframes ppm-scale-in {
from { opacity: 0; transform: scale(0.85); }
to { opacity: 1; transform: scale(1); }
}
.ppm-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 28rpx;
}
.ppm-header__icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.ppm-header__title {
font-size: 34rpx;
font-weight: 700;
color: #1A202C;
}
.ppm-header__subtitle {
font-size: 24rpx;
color: #8A99B4;
margin-top: 6rpx;
}
.ppm-amount {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 40rpx;
background: linear-gradient(135deg, #FFF5F0 0%, #FFF8F5 100%);
border-radius: 16rpx;
margin-bottom: 24rpx;
width: 100%;
box-sizing: border-box;
}
.ppm-amount__label {
font-size: 22rpx;
color: #8A99B4;
margin-bottom: 4rpx;
}
.ppm-amount__value {
font-size: 48rpx;
font-weight: 800;
color: #FF6B35;
font-family: 'DIN', 'Helvetica Neue', sans-serif;
}
.ppm-notice {
width: 100%;
padding: 12rpx 24rpx;
margin-bottom: 20rpx;
background: #FFF7ED;
border-radius: 12rpx;
border: 1rpx solid #FFEDD5;
box-sizing: border-box;
}
.ppm-notice text {
font-size: 22rpx;
color: #C2410C;
line-height: 1.5;
}
.ppm-input-wrap {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 16rpx;
}
.ppm-code-box {
display: flex;
justify-content: center;
gap: 8px;
}
.ppm-code-item {
width: 36px;
height: 40px;
background: #FFFFFF;
border: 1px solid #E2E8F0;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.ppm-code-item--focus {
border-color: #FF6B35;
}
.ppm-code-item--filled {
border-color: #1A202C;
}
.ppm-code-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #1A202C;
}
.ppm-code-cursor {
width: 2px;
height: 20px;
background: #FF6B35;
animation: ppm-blink 1s infinite;
}
@keyframes ppm-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.ppm-hidden-input {
position: fixed;
left: -9999px;
top: -9999px;
width: 1px;
height: 1px;
opacity: 0;
z-index: -1;
}
.ppm-tips {
margin-bottom: 32rpx;
}
.ppm-tips text {
font-size: 22rpx;
color: #8A99B4;
}
.ppm-buttons {
display: flex;
gap: 20rpx;
width: 100%;
}
.ppm-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
transition: all 0.2s ease;
}
.ppm-btn:active {
transform: scale(0.96);
}
.ppm-btn--cancel {
background: #F3F4F6;
color: #6B7280;
}
.ppm-btn--confirm {
background: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%);
color: #FFFFFF;
box-shadow: 0 8rpx 20rpx rgba(255, 107, 53, 0.25);
}
.ppm-btn--disabled {
opacity: 0.5;
}
</style>