feat: 初次提交我的项目代码

This commit is contained in:
2026-01-22 17:07:39 +08:00
parent 75fad97d5d
commit 73498128dd
39 changed files with 21439 additions and 835 deletions

View File

@@ -0,0 +1,755 @@
<!-- 支付页面 -->
<template>
<view class="payment-page">
<!-- 顶部栏 -->
<view class="payment-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">收银台</text>
</view>
<view class="payment-content">
<!-- 支付金额 -->
<view class="amount-section">
<text class="amount-label">支付金额</text>
<text class="amount-value">¥{{ amount.toFixed(2) }}</text>
<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 } from 'vue'
import supa from '@/components/supadb/aksupainstance.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>('')
// 生命周期
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)
}
loadPaymentMethods()
loadUserBalance()
})
// 加载订单信息
const loadOrderInfo = async () => {
try {
const { data, error } = await supa
.from('orders')
.select('order_no, actual_amount')
.eq('id', orderId.value)
.single()
if (error !== null) {
console.error('加载订单信息失败:', error)
return
}
if (data) {
orderNo.value = data.order_no
amount.value = data.actual_amount || amount.value
}
} 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 () => {
const userId = getCurrentUserId()
if (!userId) return
try {
// 这里假设有用户钱包表
const { data, error } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载用户余额失败:', error)
return
}
userBalance.value = data?.balance || 0
} catch (err) {
console.error('加载用户余额异常:', err)
}
}
// 获取当前用户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 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 {
// 模拟支付过程
await new Promise(resolve => setTimeout(resolve, 2000))
// 更新订单状态
const { error } = await supa
.from('orders')
.update({
status: 2, // 待发货
payment_method: getPaymentMethodCode(selectedMethod.value),
payment_status: 1, // 已支付
updated_at: new Date().toISOString()
})
.eq('id', orderId.value)
if (error !== null) {
throw error
}
// 余额支付需要扣减余额
if (selectedMethod.value === 'balance') {
await updateUserBalance(-amount.value)
}
// 支付成功
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
// 跳转到支付成功页面
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 updateUserBalance = async (change: number) => {
const userId = getCurrentUserId()
if (!userId) return
try {
const { data: wallet, error: walletError } = await supa
.from('user_wallets')
.select('balance')
.eq('user_id', userId)
.single()
if (walletError !== null) {
console.error('查询钱包失败:', walletError)
return
}
const newBalance = (wallet?.balance || 0) + change
const { error: updateError } = await supa
.from('user_wallets')
.update({ balance: newBalance })
.eq('user_id', userId)
if (updateError !== null) {
console.error('更新余额失败:', updateError)
return
}
// 记录余额变动
const { error: recordError } = await supa
.from('balance_records')
.insert({
user_id: userId,
change_amount: change,
current_balance: newBalance,
change_type: 'order_payment',
related_id: orderId.value,
remark: `订单支付: ${orderNo.value}`
})
if (recordError !== null) {
console.error('记录余额变动失败:', recordError)
}
userBalance.value = newBalance
} catch (err) {
console.error('更新余额异常:', err)
}
}
// 输入密码
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 goBack = () => {
uni.navigateBack()
}
</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;
}
.amount-section {
background-color: #ffffff;
padding: 40px 20px;
text-align: center;
margin-bottom: 10px;
}
.amount-label {
display: block;
font-size: 14px;
color: #666666;
margin-bottom: 15px;
}
.amount-value {
display: block;
font-size: 36px;
font-weight: bold;
color: #ff4757;
margin-bottom: 10px;
}
.order-no {
display: block;
font-size: 12px;
color: #999999;
}
.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>