@@ -0,0 +1,351 @@
< 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.25 s ease ;
}
@ keyframes ppm - fade - in {
from { opacity : 0 ; }
to { opacity : 1 ; }
}
. ppm - dialog {
width : 600 rpx ;
background : # FFFFFF ;
border - radius : 32 rpx ;
padding : 48 rpx 40 rpx 36 rpx ;
box - shadow : 0 16 rpx 48 rpx rgba ( 0 , 0 , 0 , 0.15 ) ;
animation : ppm - scale - in 0.3 s 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 : 28 rpx ;
}
. ppm - header _ _icon {
font - size : 48 rpx ;
margin - bottom : 12 rpx ;
}
. ppm - header _ _title {
font - size : 34 rpx ;
font - weight : 700 ;
color : # 1 A202C ;
}
. ppm - header _ _subtitle {
font - size : 24 rpx ;
color : # 8 A99B4 ;
margin - top : 6 rpx ;
}
. ppm - amount {
display : flex ;
flex - direction : column ;
align - items : center ;
padding : 20 rpx 40 rpx ;
background : linear - gradient ( 135 deg , # FFF5F0 0 % , # FFF8F5 100 % ) ;
border - radius : 16 rpx ;
margin - bottom : 24 rpx ;
width : 100 % ;
box - sizing : border - box ;
}
. ppm - amount _ _label {
font - size : 22 rpx ;
color : # 8 A99B4 ;
margin - bottom : 4 rpx ;
}
. ppm - amount _ _value {
font - size : 48 rpx ;
font - weight : 800 ;
color : # FF6B35 ;
font - family : 'DIN' , 'Helvetica Neue' , sans - serif ;
}
. ppm - notice {
width : 100 % ;
padding : 12 rpx 24 rpx ;
margin - bottom : 20 rpx ;
background : # FFF7ED ;
border - radius : 12 rpx ;
border : 1 rpx solid # FFEDD5 ;
box - sizing : border - box ;
}
. ppm - notice text {
font - size : 22 rpx ;
color : # C2410C ;
line - height : 1.5 ;
}
. ppm - input - wrap {
width : 100 % ;
display : flex ;
justify - content : center ;
margin - bottom : 16 rpx ;
}
. ppm - code - box {
display : flex ;
justify - content : center ;
gap : 8 px ;
}
. ppm - code - item {
width : 36 px ;
height : 40 px ;
background : # FFFFFF ;
border : 1 px solid # E2E8F0 ;
border - radius : 6 px ;
display : flex ;
align - items : center ;
justify - content : center ;
transition : all 0.2 s ease ;
}
. ppm - code - item -- focus {
border - color : # FF6B35 ;
}
. ppm - code - item -- filled {
border - color : # 1 A202C ;
}
. ppm - code - dot {
width : 8 px ;
height : 8 px ;
border - radius : 50 % ;
background : # 1 A202C ;
}
. ppm - code - cursor {
width : 2 px ;
height : 20 px ;
background : # FF6B35 ;
animation : ppm - blink 1 s infinite ;
}
@ keyframes ppm - blink {
0 % , 100 % { opacity : 1 ; }
50 % { opacity : 0 ; }
}
. ppm - hidden - input {
position : fixed ;
left : - 9999 px ;
top : - 9999 px ;
width : 1 px ;
height : 1 px ;
opacity : 0 ;
z - index : - 1 ;
}
. ppm - tips {
margin - bottom : 32 rpx ;
}
. ppm - tips text {
font - size : 22 rpx ;
color : # 8 A99B4 ;
}
. ppm - buttons {
display : flex ;
gap : 20 rpx ;
width : 100 % ;
}
. ppm - btn {
flex : 1 ;
height : 80 rpx ;
border - radius : 40 rpx ;
display : flex ;
align - items : center ;
justify - content : center ;
font - size : 28 rpx ;
font - weight : 600 ;
transition : all 0.2 s ease ;
}
. ppm - btn : active {
transform : scale ( 0.96 ) ;
}
. ppm - btn -- cancel {
background : # F3F4F6 ;
color : # 6 B7280 ;
}
. ppm - btn -- confirm {
background : linear - gradient ( 135 deg , # FF6B35 0 % , # FF8C5A 100 % ) ;
color : # FFFFFF ;
box - shadow : 0 8 rpx 20 rpx rgba ( 255 , 107 , 53 , 0.25 ) ;
}
. ppm - btn -- disabled {
opacity : 0.5 ;
}
< / style >