hookehuyr

feat(支付): 添加收款信息验证和支付协议弹框功能

新增支付协议弹框组件,完善用户信息验证逻辑
在用户发布车辆前验证收款信息是否完整
添加支付协议弹框组件及相关交互逻辑
更新权限检查以包含收款信息字段
......@@ -41,10 +41,10 @@ declare module 'vue' {
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
PayCard: typeof import('./src/components/payCard.vue')['default']
PaymentAgreementModal: typeof import('./src/components/PaymentAgreementModal.vue')['default']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
PrivacyAgreementModal: typeof import('./src/components/PrivacyAgreementModal.vue')['default']
QrcodePay: typeof import('./src/components/qrcodePay.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchPopup: typeof import('./src/components/SearchPopup.vue')['default']
......
/*
* @Date: 2023-12-22 10:29:37
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-09 15:55:52
* @LastEditTime: 2025-08-05 17:44:27
* @FilePath: /jgdl/src/api/index.js
* @Description: 用户相关API接口
*/
......@@ -36,6 +36,10 @@ export const payCheckAPI = (params) => fn(fetch.post(Api.PAY_CHECK, params));
* @param phone 手机号
* @param sms_code 短信验证码
* @param school_id 学校id
* @param name 真实姓名
* @param bank 开户行
* @param bank_no 银行卡号
* @param idcard 身份证号
* @returns
*/
export const updateProfileAPI = (params) => fn(fetch.post(Api.UPDATE_PROFILE, params));
......
<template>
<nut-popup
v-model:visible="visible"
position="bottom"
:style="{ width: '100%', height: '100%' }"
>
<div class="payment-agreement-modal">
<!-- 标题 -->
<div class="modal-header">
<h2 class="title">在捡个电驴收款</h2>
</div>
<!-- 内容区域 -->
<div class="modal-content">
<!-- 收款说明 -->
<div class="info-row">
<div class="label">收款说明</div>
<div class="content">{{ paymentDescription }}</div>
</div>
<!-- 扣费说明 -->
<div class="info-row">
<div class="label">扣费说明</div>
<div class="content">{{ feeDescription }}</div>
</div>
<!-- 协议勾选区域 -->
<div v-if="!hasAgreed" class="agreement-section">
<nut-checkbox v-model="isChecked" class="agreement-checkbox">
<view class="checkbox-text">
<text>我已阅读并同意</text>
<text class="agreement-link" @tap.stop="showProtocol">
《支付协议》
</text>
</view>
</nut-checkbox>
</div>
</div>
<!-- 按钮区域 -->
<div class="modal-footer">
<div class="button-row">
<nut-button
type="default"
class="close-button"
@click="handleClose"
>
关闭
</nut-button>
<nut-button
v-if="!hasAgreed"
:disabled="!isChecked"
type="primary"
class="main-button"
color="orange"
@click="handleAgree"
>
同意
</nut-button>
<nut-button
v-else
type="primary"
class="main-button"
color="orange"
@click="handleConfirm"
>
确认
</nut-button>
</div>
</div>
</div>
<!-- 支付协议弹框 -->
<nut-popup
v-model:visible="protocolVisible"
position="bottom"
:style="{ width: '100%', height: '70%' }"
round
closeable
>
<div class="protocol-modal">
<div class="protocol-header">
<h3>支付协议</h3>
</div>
<div class="protocol-content">
<p>{{ protocolContent }}</p>
</div>
</div>
</nut-popup>
</nut-popup>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
/**
* 用户收款说明组件
* @param {Boolean} modelValue - 控制弹框显示隐藏
* @param {Function} onAgree - 同意按钮回调
* @param {Function} onConfirm - 确认按钮回调
* @param {Function} onClose - 关闭弹框回调
*/
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'agree', 'confirm', 'close'])
const userStore = useUserStore()
// 弹框显示状态
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 支付协议弹框显示状态
const protocolVisible = ref(false)
// 勾选状态
const isChecked = ref(false)
// 是否已同意过协议(mock数据)
const hasAgreed = ref(false)
// 收款说明内容
const paymentDescription = ref('通过捡个电驴平台进行收款,资金安全有保障,支持多种收款方式。')
// 扣费说明内容
const feeDescription = ref('平台将收取交易金额的3%作为服务费,费用将在交易完成后自动扣除。')
// 支付协议内容
const protocolContent = ref(`
1. 用户在使用捡个电驴收款服务时,需遵守相关法律法规。
2. 平台有权对异常交易进行风险控制。
3. 用户应确保收款信息的真实性和准确性。
4. 平台将按照约定收取相应的服务费用。
5. 如有争议,双方应友好协商解决。
`)
/**
* 检查用户是否已同意过协议
*/
const checkAgreementStatus = () => {
// TODO: 实际项目中应该从userStore.userInfo中获取相关字段
// 这里使用mock数据
hasAgreed.value = userStore.userInfo?.paymentAgreementAccepted || false
}
/**
* 显示支付协议
*/
const showProtocol = () => {
protocolVisible.value = true
}
/**
* 处理同意按钮点击
*/
const handleAgree = () => {
if (!isChecked.value) return
// TODO: 实际项目中应该调用API更新用户协议状态
hasAgreed.value = true
emit('agree')
}
/**
* 处理确认按钮点击
*/
const handleConfirm = () => {
emit('confirm')
}
/**
* 处理弹框关闭
*/
const handleClose = () => {
emit('close')
}
// 组件挂载时检查协议状态
onMounted(() => {
checkAgreementStatus()
})
</script>
<style lang="less">
.payment-agreement-modal {
display: flex;
flex-direction: column;
height: 100%;
padding: 32rpx;
padding-top: 80rpx;
.modal-header {
text-align: center;
margin-bottom: 40rpx;
.title {
font-size: 36rpx;
font-weight: 600;
color: #333;
margin: 0;
}
}
.modal-content {
flex: 1;
display: flex;
flex-direction: column;
.info-row {
display: flex;
margin-bottom: 32rpx;
.label {
width: 160rpx;
font-size: 28rpx;
font-weight: 500;
color: #333;
flex-shrink: 0;
}
.content {
flex: 1;
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.agreement-section {
margin-top: 40rpx;
text-align: center;
.agreement-checkbox {
font-size: 28rpx;
.checkbox-text {
white-space: nowrap;
display: inline-block;
}
.agreement-link {
color: #ffa500;
text-decoration: underline;
cursor: pointer;
}
}
}
}
.modal-footer {
padding-top: 32rpx;
border-top: 1rpx solid #eee;
.button-row {
display: flex;
gap: 24rpx;
align-items: center;
.close-button {
flex: 1;
border: 1rpx solid #ddd;
color: #666;
}
.main-button {
flex: 1;
}
}
}
}
.protocol-modal {
padding: 32rpx;
height: 100%;
display: flex;
flex-direction: column;
.protocol-header {
text-align: center;
margin-bottom: 32rpx;
h3 {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin: 0;
}
}
.protocol-content {
flex: 1;
overflow-y: auto;
p {
font-size: 28rpx;
color: #666;
line-height: 1.8;
white-space: pre-line;
margin: 0;
}
}
}
</style>
......@@ -318,6 +318,14 @@
<nut-picker v-model="tireWearValue" :columns="wearLevelOptions" title="选择轮胎磨损度" @confirm="onTireWearConfirm"
@cancel="tireWearPickerVisible = false" />
</nut-popup>
<!-- 收款说明弹框 -->
<PaymentAgreementModal
v-model="paymentAgreementVisible"
@agree="handlePaymentAgreementAgree"
@confirm="handlePaymentAgreementConfirm"
@close="handlePaymentAgreementClose"
/>
</view>
</template>
......@@ -327,6 +335,7 @@ import { Plus, Right, Location, Close } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
import BASE_URL from '@/utils/config';
import BrandModelPicker from '@/components/BrandModelPicker.vue'
import PaymentAgreementModal from '@/components/PaymentAgreementModal.vue'
import { checkPermission, PERMISSION_TYPES } from '@/utils/permission'
import './index.less'
......@@ -410,6 +419,9 @@ const conditionPickerVisible = ref(false)
const brakeWearPickerVisible = ref(false)
const tireWearPickerVisible = ref(false)
// 收款说明弹框显示状态
const paymentAgreementVisible = ref(false)
// 新的品牌型号选择器状态
// 品牌型号选择器组件引用
const brandModelPickerRef = ref(null)
......@@ -1141,8 +1153,8 @@ onMounted(async () => {
// 可以通过 permissionResult.validFields 获取已通过校验的字段
Taro.showModal({
title: '温馨提示',
content: `您还未填写${permissionResult.missingFields.includes('phone') ? '个人资料' : '收款信息'},展示无法发布`,
cancelText: '稍后设置',
content: `您未填写${permissionResult.missingFields.includes('phone') ? '个人资料' : '收款信息'}, 将无法发布车辆`,
cancelText: '关闭',
confirmText: '立即设置',
success: (res) => {
if (res.confirm) {
......@@ -1151,7 +1163,7 @@ onMounted(async () => {
Taro.navigateTo({
url: '/pages/register/index'
})
} else if (permissionResult.missingFields.includes('paymentInfo')) {
} else if (permissionResult.missingFields.includes('name') || permissionResult.missingFields.includes('bank') || permissionResult.missingFields.includes('bank_no') || permissionResult.missingFields.includes('idcard')) {
// 收款信息未填写
Taro.navigateTo({
url: '/pages/collectionSettings/index'
......@@ -1166,6 +1178,9 @@ onMounted(async () => {
return
}
// 权限验证通过,显示收款说明弹框
paymentAgreementVisible.value = true
// 加载基础数据
await Promise.all([
loadSchools(),
......@@ -1194,4 +1209,50 @@ onMounted(async () => {
});
}
})
/**
* 处理收款说明弹框的同意操作
*/
const handlePaymentAgreementAgree = () => {
// 用户同意收款协议,组件内部会处理关闭逻辑
paymentAgreementVisible.value = false
// 这里可以添加同意后的逻辑,比如更新用户状态等
Taro.showToast({
title: '已同意收款协议',
icon: 'success'
})
}
/**
* 处理收款说明弹框的确认操作
*/
const handlePaymentAgreementConfirm = () => {
// 用户确认收款说明,组件内部会处理关闭逻辑
paymentAgreementVisible.value = false
// 这里可以添加确认后的逻辑
Taro.showToast({
title: '已确认收款说明',
icon: 'success'
})
}
/**
* 处理收款说明弹框的关闭操作
*/
const handlePaymentAgreementClose = () => {
// 用户关闭收款说明弹框
paymentAgreementVisible.value = false
// 判断是否有上一页,如果有就返回上一页,没有则返回首页
const pages = Taro.getCurrentPages()
if (pages.length > 1) {
// 有上一页,返回上一页
Taro.navigateBack()
} else {
// 没有上一页,跳转到首页
Taro.redirectTo({
url: '/pages/index/index'
})
}
}
</script>
......
/*
* @Date: 2025-01-08 18:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-24 10:52:42
* @LastEditTime: 2025-08-05 17:45:17
* @FilePath: /jgdl/src/stores/user.js
* @Description: 用户状态管理
*/
......@@ -24,6 +24,10 @@ export const useUserStore = defineStore('user', {
order_count: 0,
favorite_count: 0,
message_count: 0,
name: '',
bank: '',
bank_no: '',
idcard: '',
},
isAuthenticated: false,
isLoading: false
......@@ -102,6 +106,10 @@ export const useUserStore = defineStore('user', {
order_count: 0,
favorite_count: 0,
message_count: 0,
name: '',
bank: '',
bank_no: '',
idcard: '',
}
this.isAuthenticated = false
},
......
/*
* @Date: 2025-01-08 18:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-09 11:16:49
* @LastEditTime: 2025-08-05 17:46:11
* @FilePath: /jgdl/src/utils/permission.js
* @Description: 权限控制工具函数
*/
......@@ -40,7 +40,7 @@ const PERMISSION_CONFIG = {
[PERMISSION_TYPES.SELL_CAR]: {
message: '发布车源需要先完善个人信息',
redirectUrl: '/pages/register/index',
checkFields: ['phone']
checkFields: ['phone', 'name', 'bank', 'bank_no', 'idcard']
},
[PERMISSION_TYPES.BUY_CAR]: {
message: '购买车辆需要先完善个人信息',
......@@ -81,11 +81,11 @@ function checkUserFields(userInfo, checkFields) {
missingFields: [],
validFields: []
}
checkFields.forEach(field => {
const value = userInfo[field]
const isFieldValid = value && value.toString().trim() !== ''
if (isFieldValid) {
result.validFields.push(field)
} else {
......@@ -93,7 +93,7 @@ function checkUserFields(userInfo, checkFields) {
result.isValid = false
}
})
return result
}
......