Files
akmon/pages/mall/nfc/student/nfc-pay.uvue
2026-01-20 08:04:15 +08:00

453 lines
9.4 KiB
Plaintext

<template>
<view class="nfc-pay">
<!-- NFC状态指示 -->
<view class="nfc-status" :class="{ 'active': nfcStatus === 'ready' }">
<image class="nfc-icon" src="/static/icons/nfc-large.png" />
<text class="status-text">{{ statusText }}</text>
<text class="status-desc">{{ statusDesc }}</text>
</view>
<!-- 支付信息 -->
<view class="payment-info" v-if="paymentAmount > 0">
<view class="amount-display">
<text class="currency">¥</text>
<text class="amount">{{ paymentAmount.toFixed(2) }}</text>
</view>
<text class="merchant-name">{{ merchantName }}</text>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="btn-primary" @click="enableNFC" v-if="nfcStatus === 'disabled'">
启用NFC
</button>
<button class="btn-secondary" @click="switchToQR" v-if="nfcStatus === 'ready'">
切换到扫码支付
</button>
</view>
<!-- 余额显示 -->
<view class="balance-display">
<text class="balance-label">当前余额</text>
<text class="balance-amount">¥{{ userBalance.toFixed(2) }}</text>
</view>
<!-- 最近交易 -->
<view class="recent-transactions" v-if="recentTransactions.length > 0">
<view class="section-title">最近交易</view>
<view class="transaction-item" v-for="item in recentTransactions" :key="item.id">
<view class="transaction-info">
<text class="merchant">{{ item.merchant }}</text>
<text class="time">{{ formatTime(item.time) }}</text>
</view>
<text class="amount">-¥{{ item.amount.toFixed(2) }}</text>
</view>
</view>
<!-- NFC支付动画 -->
<view class="nfc-animation" v-if="isPaymentProcessing">
<view class="ripple"></view>
<view class="ripple ripple-2"></view>
<view class="ripple ripple-3"></view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
nfcStatus: 'checking', // checking, disabled, ready, processing
paymentAmount: 0,
merchantName: '',
userBalance: 156.80,
isPaymentProcessing: false,
recentTransactions: [
{
id: 1,
merchant: '第一饭堂',
amount: 12.50,
time: new Date(Date.now() - 3600000)
},
{
id: 2,
merchant: '校园便利店',
amount: 8.80,
time: new Date(Date.now() - 7200000)
}
]
}
},
computed: {
statusText() {
const statusMap = {
checking: '检查NFC状态...',
disabled: 'NFC未启用',
ready: '请将手机靠近读卡器',
processing: '支付处理中...'
}
return statusMap[this.nfcStatus] || ''
},
statusDesc() {
const descMap = {
checking: '正在检测设备NFC功能',
disabled: '请在设置中启用NFC功能',
ready: '保持手机靠近直到支付完成',
processing: '请勿移动手机'
}
return descMap[this.nfcStatus] || ''
}
},
onLoad(options) {
this.paymentAmount = parseFloat(options.amount || 0)
this.merchantName = options.merchant || ''
this.checkNFCStatus()
},
onShow() {
this.initNFCPayment()
},
onHide() {
this.stopNFCPayment()
},
methods: {
checkNFCStatus() {
// 检查NFC状态
// #ifdef APP-PLUS
plus.nfc.isAvailable((available) => {
if (available) {
plus.nfc.isEnabled((enabled) => {
this.nfcStatus = enabled ? 'ready' : 'disabled'
})
} else {
this.nfcStatus = 'disabled'
uni.showToast({
title: '设备不支持NFC',
icon: 'none'
})
}
})
// #endif
// #ifndef APP-PLUS
// Web环境模拟
setTimeout(() => {
this.nfcStatus = 'ready'
}, 1000)
// #endif
},
enableNFC() {
// #ifdef APP-PLUS
plus.nfc.enable(() => {
this.nfcStatus = 'ready'
this.initNFCPayment()
}, (error) => {
uni.showToast({
title: '启用NFC失败',
icon: 'none'
})
})
// #endif
},
initNFCPayment() {
if (this.nfcStatus === 'ready') {
// 初始化NFC支付监听
this.startNFCListening()
}
},
startNFCListening() {
// #ifdef APP-PLUS
plus.nfc.addEventListener('tag', this.handleNFCTag)
// #endif
},
stopNFCPayment() {
// #ifdef APP-PLUS
plus.nfc.removeEventListener('tag', this.handleNFCTag)
// #endif
},
handleNFCTag(tag) {
if (this.paymentAmount <= 0) {
uni.showToast({
title: '无效的支付金额',
icon: 'none'
})
return
}
this.processPayment(tag)
},
processPayment(tag) {
this.isPaymentProcessing = true
this.nfcStatus = 'processing'
// 模拟支付处理
setTimeout(() => {
const success = Math.random() > 0.1 // 90% 成功率
if (success) {
this.userBalance -= this.paymentAmount
this.addRecentTransaction()
uni.showToast({
title: '支付成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
})
this.nfcStatus = 'ready'
}
this.isPaymentProcessing = false
}, 2000)
},
addRecentTransaction() {
this.recentTransactions.unshift({
id: Date.now(),
merchant: this.merchantName || '校园商户',
amount: this.paymentAmount,
time: new Date()
})
if (this.recentTransactions.length > 5) {
this.recentTransactions.pop()
}
},
switchToQR() {
uni.navigateTo({
url: `/pages/mall/nfc/student/qr-code?amount=${this.paymentAmount}&merchant=${this.merchantName}`
})
},
formatTime(time) {
const now = new Date()
const diff = now - time
const minutes = Math.floor(diff / 60000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}小时前`
return time.toLocaleDateString()
}
}
}
</script>
<style>
.nfc-pay {
padding: 40rpx;
background-color: #f8f9fa;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.nfc-status {
background: white;
border-radius: 20rpx;
padding: 60rpx 40rpx;
margin-bottom: 40rpx;
text-align: center;
width: 100%;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.nfc-status.active {
background: linear-gradient(135deg, #007AFF 0%, #5AC8FA 100%);
color: white;
}
.nfc-icon {
width: 120rpx;
height: 120rpx;
margin-bottom: 30rpx;
}
.status-text {
display: block;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 16rpx;
}
.status-desc {
display: block;
font-size: 24rpx;
opacity: 0.8;
}
.payment-info {
background: white;
border-radius: 16rpx;
padding: 40rpx;
margin-bottom: 30rpx;
text-align: center;
width: 100%;
}
.amount-display {
display: flex;
justify-content: center;
align-items: baseline;
margin-bottom: 20rpx;
}
.currency {
font-size: 36rpx;
color: #333;
margin-right: 8rpx;
}
.amount {
font-size: 72rpx;
font-weight: bold;
color: #007AFF;
}
.merchant-name {
font-size: 28rpx;
color: #666;
}
.action-buttons {
width: 100%;
margin-bottom: 30rpx;
}
.btn-primary, .btn-secondary {
width: 100%;
padding: 24rpx;
border-radius: 12rpx;
font-size: 32rpx;
border: none;
margin-bottom: 20rpx;
}
.btn-primary {
background: #007AFF;
color: white;
}
.btn-secondary {
background: white;
color: #007AFF;
border: 2rpx solid #007AFF;
}
.balance-display {
background: white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.balance-label {
font-size: 28rpx;
color: #666;
}
.balance-amount {
font-size: 32rpx;
font-weight: bold;
color: #28a745;
}
.recent-transactions {
background: white;
border-radius: 16rpx;
padding: 30rpx;
width: 100%;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-info {
display: flex;
flex-direction: column;
}
.merchant {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.time {
font-size: 24rpx;
color: #999;
}
.transaction-item .amount {
font-size: 28rpx;
color: #ff3b30;
font-weight: bold;
}
.nfc-animation {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200rpx;
height: 200rpx;
}
.ripple {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 4rpx solid #007AFF;
animation: ripple 2s infinite;
}
.ripple-2 {
animation-delay: 0.5s;
}
.ripple-3 {
animation-delay: 1s;
}
@keyframes ripple {
0% {
transform: scale(0.3);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
</style>