Files
medical-mall/pages/mall/consumer/payment.uvue

869 lines
19 KiB
Plaintext
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-page">
<view class="payment-content">
<!-- 价格明细 -->
<view class="price-detail-section">
<text class="section-title">价格明细</text>
<view class="price-detail">
<view class="price-row">
<text class="price-label">商品总价</text>
<text class="price-value">¥{{ productAmount.toFixed(2) }}</text>
</view>
<view class="price-row">
<text class="price-label">运费</text>
<text class="price-value">+¥{{ deliveryFee.toFixed(2) }}</text>
</view>
<view v-if="discountAmount > 0" class="price-row">
<text class="price-label">优惠减免</text>
<text class="price-value discount">-¥{{ discountAmount.toFixed(2) }}</text>
</view>
<view class="price-row total">
<text class="price-label">应付金额</text>
<text class="price-value total-price">¥{{ amount.toFixed(2) }}</text>
</view>
</view>
<text class="order-no">订单号: {{ orderNo }}</text>
</view>
<!-- 支付方式 -->
<view class="methods-section">
<text class="section-title">选择支付方式</text>
<view class="method-list">
<view v-for="method in paymentMethods"
:key="method.id"
:class="['method-item', { selected: selectedMethod === method.id }]"
@click="selectMethod(method)">
<view class="method-left">
<text class="method-icon">{{ getMethodIcon(method.id) }}</text>
<view class="method-info">
<text class="method-name">{{ method.name }}</text>
<text class="method-desc">{{ method.description }}</text>
</view>
</view>
<view v-if="selectedMethod === method.id" class="method-selected">
<text class="selected-icon">✓</text>
</view>
</view>
</view>
</view>
<!-- 余额支付 -->
<view v-if="selectedMethod === 'balance' && userBalance > 0" class="balance-section">
<view class="balance-info">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ userBalance.toFixed(2) }}</text>
</view>
<view v-if="userBalance < amount" class="balance-tip">
<text class="tip-text">余额不足,请选择其他支付方式</text>
</view>
</view>
<!-- 密码输入 -->
<view v-if="showPassword" class="password-section">
<text class="password-title">请输入支付密码</text>
<view class="password-input">
<view v-for="(_, index) in 6"
:key="index"
class="password-dot">
<text v-if="password.length > index">●</text>
</view>
</view>
<text class="forgot-password" @click="forgotPassword">忘记密码?</text>
</view>
</view>
<!-- 底部支付按钮 -->
<view class="payment-bottom">
<view class="price-summary">
<text class="summary-label">需支付:</text>
<text class="summary-price">¥{{ amount.toFixed(2) }}</text>
</view>
<button class="pay-btn"
:class="{ disabled: isPaying || (selectedMethod === 'balance' && userBalance < amount) }"
@click="confirmPayment">
<text v-if="!isPaying" class="pay-text">{{ getPayButtonText() }}</text>
<text v-else class="pay-text">支付中...</text>
</button>
</view>
<!-- 密码键盘 -->
<view v-if="showPassword" class="password-keyboard">
<view class="keyboard-grid">
<view v-for="num in 9"
:key="num"
class="keyboard-key"
@click="inputPassword(num.toString())">
<text class="key-text">{{ num }}</text>
</view>
<view class="keyboard-key"></view>
<view class="keyboard-key" @click="inputPassword('0')">
<text class="key-text">0</text>
</view>
<view class="keyboard-key" @click="deletePassword">
<text class="key-text">⌫</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, watch, computed, onUnmounted } from 'vue'
import { onLoad, onBackPress } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
type PaymentMethodType = {
id: string
name: string
description: string
icon: string
enabled: boolean
}
const orderId = ref<string>('')
const orderNo = ref<string>('')
const amount = ref<number>(0)
const paymentMethods = ref<Array<PaymentMethodType>>([])
const selectedMethod = ref<string>('wechat')
const userBalance = ref<number>(0)
const isPaying = ref<boolean>(false)
const showPassword = ref<boolean>(false)
const password = ref<string>('')
// 价格相关变量
const productAmount = ref<number>(0) // 商品总价
const deliveryFee = ref<number>(0) // 运费
const discountAmount = ref<number>(0) // 优惠减免
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as any
if (options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
}
if (options.amount) {
amount.value = parseFloat(options.amount)
}
// 获取传递的价格详情
if (options.productAmount) {
productAmount.value = parseFloat(options.productAmount)
}
if (options.deliveryFee) {
deliveryFee.value = parseFloat(options.deliveryFee)
}
if (options.discountAmount) {
discountAmount.value = parseFloat(options.discountAmount)
}
// 如果没有传详情,尝试根据总价估算(兼容旧逻辑,但优先使用传参)
if (!options.productAmount && amount.value > 0) {
calculatePriceDetails(amount.value)
}
loadPaymentMethods()
loadUserBalance()
})
// 监听返回操作(包含系统返回键和导航栏返回按钮)
onBackPress((options) => {
// 如果是通过代码主动调用 navigateBack 返回,则允许
if (options.from === 'navigateBack') {
return false
}
// 否则拦截返回,显示确认弹窗
goBack()
return true
})
// 更新本地存储中的订单状态
const updateOrderInStorage = (status: number) => {
try {
// 尝试从 'orders' 读取 (checkout页面写入的key)
const ordersStr = uni.getStorageSync('orders')
let orders: any[] = []
if (ordersStr) {
orders = JSON.parse(ordersStr as string) as any[]
}
const index = orders.findIndex((o: any) => o.id === orderId.value)
if (index !== -1) {
orders[index].status = status
orders[index].payment_status = status === 2 ? 1 : 0 // 2=待发货(已支付), 1=待支付(未支付)
orders[index].updated_at = new Date().toISOString()
// 确保更新的是 'orders' key
uni.setStorageSync('orders', JSON.stringify(orders))
console.log('订单状态已更新到Storage (orders):', orderId.value, status)
} else {
console.warn('在Storage (orders)中未找到订单:', orderId.value)
}
} catch (e) {
console.error('更新订单状态失败', e)
}
}
// 加载订单信息
const loadOrderInfo = async () => {
try {
if (!orderId.value) return
const order = await supabaseService.getOrderDetail(orderId.value)
if (order) {
orderNo.value = order.order_no
// Only update amount if not passed via options (options is priority for UI flow usually, but DB is source of truth)
// But checking consistency is good
const dbAmount = Number(order.total_amount)
if (dbAmount > 0) {
amount.value = dbAmount
}
if (order.items && order.items.length > 0) {
// Could update product name etc if displayed
}
} else {
// Fallback or error
console.warn('Order not found in DB', orderId.value)
if (!orderNo.value) orderNo.value = 'ORD_PENDING_' + Date.now()
}
} catch (err) {
console.error('加载订单信息异常:', err)
}
}
// 加载支付方式
const loadPaymentMethods = () => {
paymentMethods.value = [
{
id: 'wechat',
name: '微信支付',
description: '推荐安装微信5.0及以上版本使用',
icon: '💳',
enabled: true
},
{
id: 'alipay',
name: '支付宝',
description: '推荐安装支付宝10.0及以上版本使用',
icon: '💳',
enabled: true
},
{
id: 'balance',
name: '余额支付',
description: '使用账户余额支付',
icon: '💰',
enabled: true
},
{
id: 'bankcard',
name: '银行卡支付',
description: '支持储蓄卡、信用卡',
icon: '💳',
enabled: true
}
]
}
// 加载用户余额
const loadUserBalance = async () => {
try {
const balance = await supabaseService.getUserBalance()
userBalance.value = balance
} catch (err) {
console.error('加载用户余额异常:', err)
userBalance.value = 0
}
}
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 获取支付方式图标
const getMethodIcon = (methodId: string): string => {
const icons: Record<string, string> = {
wechat: '💳',
alipay: '💳',
balance: '💰',
bankcard: '💳'
}
return icons[methodId] || '💳'
}
// 选择支付方式
const selectMethod = (method: PaymentMethodType) => {
if (!method.enabled) {
uni.showToast({
title: '该支付方式暂不可用',
icon: 'none'
})
return
}
selectedMethod.value = method.id
showPassword.value = method.id === 'balance' || method.id === 'bankcard'
password.value = '' // 清空密码
}
// 获取支付按钮文本
const getPayButtonText = (): string => {
if (selectedMethod.value === 'balance' && userBalance.value < amount.value) {
return '余额不足'
}
const texts: Record<string, string> = {
wechat: '微信支付',
alipay: '支付宝支付',
balance: '余额支付',
bankcard: '银行卡支付'
}
return texts[selectedMethod.value] || '确认支付'
}
// 减少商品库存
// const reduceStock = (orderId: string) => {
// Update should happen on server side during payment processing
// }
// 确认支付
const confirmPayment = async () => {
if (isPaying.value) return
// 余额支付检查
if (selectedMethod.value === 'balance') {
if (userBalance.value < amount.value) {
uni.showToast({
title: '余额不足',
icon: 'none'
})
return
}
if (!showPassword.value) {
showPassword.value = true
return
}
if (password.value.length !== 6) {
uni.showToast({
title: '请输入6位支付密码',
icon: 'none'
})
return
}
}
isPaying.value = true
try {
// Call Supabase Service to handle payment
const success = await supabaseService.payOrder(orderId.value, selectedMethod.value, amount.value)
if (!success) {
throw new Error('Payment processing failed')
}
// 更新订单状态
updateOrderInStorage(2) // 2: 待发货(已支付)
// 支付成功
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 2 }) // 2: 待发货
// 跳转到支付成功页面
setTimeout(() => {
uni.redirectTo({
url: `/pages/mall/consumer/payment-success?orderId=${orderId.value}`
})
}, 1500)
} catch (err) {
console.error('支付失败:', err)
uni.showToast({
title: '支付失败',
icon: 'none'
})
} finally {
isPaying.value = false
}
}
// 获取支付方式代码
const getPaymentMethodCode = (methodId: string): number => {
const codes: Record<string, number> = {
wechat: 1,
alipay: 2,
balance: 3,
bankcard: 4
}
return codes[methodId] || 0
}
// 输入密码
const inputPassword = (num: string) => {
if (password.value.length >= 6) return
password.value += num
}
// 删除密码
const deletePassword = () => {
if (password.value.length > 0) {
password.value = password.value.slice(0, -1)
}
}
// 监听密码输入
watch(password, (newPassword) => {
if (newPassword.length === 6) {
// 自动验证密码
verifyPassword()
}
})
// 验证密码
const verifyPassword = async () => {
// 这里应该验证支付密码,这里简单模拟
const userId = getCurrentUserId()
try {
// 模拟验证
await new Promise(resolve => setTimeout(resolve, 500))
// 假设密码正确
const isCorrect = true
if (isCorrect) {
// 密码正确,继续支付
confirmPayment()
} else {
password.value = ''
uni.showToast({
title: '密码错误',
icon: 'none'
})
}
} catch (err) {
console.error('验证密码异常:', err)
}
}
// 忘记密码
const forgotPassword = () => {
uni.navigateTo({
url: '/pages/user/forgot-password'
})
}
// 计算价格明细
const calculatePriceDetails = (totalAmount: number) => {
// 模拟计算各项费用
// 假设商品总价占总金额的80%运费占10%优惠减免占10%
productAmount.value = totalAmount * 0.8
deliveryFee.value = totalAmount * 0.1
discountAmount.value = totalAmount * 0.1
// 确保总和等于应付金额
const calculatedTotal = productAmount.value + deliveryFee.value - discountAmount.value
if (Math.abs(calculatedTotal - totalAmount) > 0.01) {
// 调整商品总价以匹配应付金额
productAmount.value = totalAmount + discountAmount.value - deliveryFee.value
}
}
// 在组件卸载时移除返回键监听
onUnmounted(() => {
// uni.offBackPress() 在uni-app中不需要手动移除
})
// 返回
const goBack = () => {
uni.showModal({
title: '取消支付',
content: '确定要取消支付吗?取消后订单将保存到待支付订单中',
confirmText: '取消支付',
cancelText: '继续支付',
success: async (res) => {
if (res.confirm) {
// 用户确认取消支付,更新订单状态为待支付
await cancelPayment()
} else {
// 用户选择继续支付,留在当前页面
return
}
}
})
}
// 取消支付,更新订单状态
const cancelPayment = async () => {
try {
// 这里应该调用API更新订单状态为待支付status: 1
// 模拟更新订单状态
/* const { error } = await supa
.from('orders')
.update({
status: 1, // 待支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
console.error('更新订单状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'none'
})
return
} */
// 更新本地存储
updateOrderInStorage(orderId.value, 1) // 1: 待支付
// 发布订单更新事件让profile页面可以刷新数据
uni.$emit('orderUpdated', { orderId: orderId.value, status: 1 })
uni.showToast({
title: '已保存到待支付订单',
icon: 'success'
})
// 延迟返回,让用户看到提示
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (err) {
console.error('取消支付异常:', err)
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
</script>
<style scoped>
.payment-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.payment-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
margin-right: 15px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.payment-content {
flex: 1;
overflow-y: auto;
}
/* 价格明细部分 */
.price-detail-section {
background-color: #ffffff;
padding: 20px 15px;
margin-bottom: 10px;
}
.price-detail {
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 15px;
}
.price-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.price-row.total {
border-top: 1px solid #e5e5e5;
margin-top: 8px;
padding-top: 15px;
}
.price-label {
font-size: 14px;
color: #666666;
}
.price-value {
font-size: 14px;
color: #333333;
}
.price-value.discount {
color: #4caf50;
}
.price-value.total-price {
font-size: 18px;
color: #ff4757;
font-weight: bold;
}
.order-no {
display: block;
font-size: 12px;
color: #999999;
text-align: center;
}
.methods-section {
background-color: #ffffff;
padding: 20px 15px;
margin-bottom: 10px;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.method-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.method-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.method-item.selected {
border-color: #007aff;
background-color: #f0f8ff;
}
.method-left {
display: flex;
align-items: center;
}
.method-icon {
font-size: 24px;
margin-right: 15px;
}
.method-info {
display: flex;
flex-direction: column;
}
.method-name {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 5px;
}
.method-desc {
font-size: 12px;
color: #999999;
}
.method-selected {
width: 24px;
height: 24px;
border-radius: 12px;
background-color: #007aff;
display: flex;
align-items: center;
justify-content: center;
}
.selected-icon {
color: #ffffff;
font-size: 14px;
}
.balance-section {
background-color: #ffffff;
padding: 15px;
margin-bottom: 10px;
}
.balance-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.balance-label {
font-size: 14px;
color: #333333;
}
.balance-value {
font-size: 18px;
color: #ff4757;
font-weight: bold;
}
.balance-tip {
padding: 10px;
background-color: #fff0f0;
border-radius: 5px;
}
.tip-text {
font-size: 12px;
color: #ff4757;
}
.password-section {
background-color: #ffffff;
padding: 30px 15px;
text-align: center;
margin-bottom: 10px;
}
.password-title {
display: block;
font-size: 16px;
color: #333333;
margin-bottom: 30px;
}
.password-input {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.password-dot {
width: 12px;
height: 12px;
border-radius: 6px;
background-color: #333333;
display: flex;
align-items: center;
justify-content: center;
}
.password-dot text {
color: #ffffff;
font-size: 8px;
}
.forgot-password {
color: #007aff;
font-size: 14px;
}
.payment-bottom {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
.price-summary {
display: flex;
align-items: baseline;
}
.summary-label {
font-size: 14px;
color: #333333;
margin-right: 5px;
}
.summary-price {
font-size: 20px;
color: #ff4757;
font-weight: bold;
}
.pay-btn {
background-color: #007aff;
color: #ffffff;
padding: 0 40px;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
border: none;
}
.pay-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
.password-keyboard {
background-color: #ffffff;
border-top: 1px solid #e5e5e5;
padding: 10px;
}
.keyboard-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1px;
background-color: #e5e5e5;
}
.keyboard-key {
background-color: #ffffff;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.key-text {
font-size: 24px;
color: #333333;
}
</style>