hookehuyr

feat(收款设置): 新增收款设置页面及相关功能

添加收款设置页面,包括银行账号和身份信息设置功能
扩展权限检查功能,支持返回未通过校验的字段信息
更新用户个人中心,添加收款设置入口
......@@ -44,6 +44,7 @@ declare module 'vue' {
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: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-15 15:42:47
* @LastEditTime: 2025-08-05 15:12:01
* @FilePath: /jgdl/src/app.config.js
* @Description: 配置文件
*/
......@@ -29,6 +29,7 @@ export default {
'pages/helpCenter/index',
'pages/search/index',
'pages/recommendCarList/index',
'pages/collectionSettings/index',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
{
......
/*
* @Date: 2025-08-05 15:10:51
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-05 15:12:21
* @FilePath: /jgdl/src/pages/collectionSettings/index.config.js
* @Description: 收款设置
*/
export default {
navigationBarTitleText: '收款设置',
usingComponents: {
},
}
.collection-settings {
min-height: 100vh;
background-color: #f5f5f5;
padding: 0;
.page-title {
background-color: #fff;
padding: 32rpx;
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
border-bottom: 1rpx solid #eee;
}
.settings-list {
margin-top: 24rpx;
background-color: #fff;
// border-radius: 16rpx;
// margin: 24rpx 32rpx;
overflow: hidden;
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.2s;
&:last-child {
border-bottom: none;
}
&:active {
background-color: #f8f8f8;
}
.setting-left {
.setting-label {
font-size: 32rpx;
color: #333;
// font-weight: 500;
}
}
.setting-right {
display: flex;
align-items: center;
gap: 16rpx;
.setting-status {
font-size: 28rpx;
color: #999;
&.status-set {
color: #52c41a;
}
}
.arrow {
font-size: 24rpx;
color: #ccc;
}
}
}
}
// 弹窗样式
.modal-content {
height: 100%;
display: flex;
flex-direction: column;
background-color: #fff;
.modal-header {
padding: 32rpx;
border-bottom: 1rpx solid #eee;
background-color: #fff;
position: sticky;
top: 0;
z-index: 10;
.modal-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
}
}
.form-content {
flex: 1;
padding: 32rpx;
overflow-y: auto;
.form-item {
margin-bottom: 48rpx;
.form-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.form-input {
width: 100%;
padding: 24rpx 0;
font-size: 32rpx;
border: none;
// border-bottom: 2rpx solid #eee;
background: transparent;
transition: border-color 0.3s;
&:focus {
border-bottom-color: #1890ff;
}
}
.error-text {
display: block;
font-size: 24rpx;
color: #ff4d4f;
margin-top: 8rpx;
}
}
}
.modal-footer {
padding: 32rpx;
border-top: 1rpx solid #eee;
background-color: #fff;
display: flex;
gap: 24rpx;
.footer-btn {
flex: 1;
height: 88rpx;
font-size: 32rpx;
border-radius: 44rpx;
}
.footer-btn-cancel {
// background-color: #f5f5f5;
color: #666;
// border: 1rpx solid #d9d9d9;
}
.footer-btn-save {
background-color: #ffa500;
color: #fff;
border: 1rpx solid #ffa500;
}
}
}
}
// NutUI 组件样式覆盖
:deep(.nut-popup) {
.nut-icon {
font-size: 32rpx;
color: #666;
}
}
:deep(.nut-input) {
.nut-input-value {
font-size: 32rpx;
color: #333;
}
.nut-input-placeholder {
color: #ccc;
}
}
:deep(.nut-button) {
&.nut-button--disabled {
background-color: #f5f5f5 !important;
color: #ccc !important;
border-color: #f5f5f5 !important;
}
}
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-05 15:29:14
* @FilePath: /jgdl/src/pages/collectionSettings/index.vue
* @Description: 收款设置
-->
<template>
<view class="collection-settings">
<!-- 设置列表 -->
<view class="settings-list">
<!-- 收款账号 -->
<view class="setting-item" @click="openAccountModal">
<view class="setting-left">
<text class="setting-label">收款账号</text>
</view>
<view class="setting-right">
<text class="setting-status" :class="{ 'status-set': accountInfo.bankName }">
{{ accountInfo.bankName ? '已设置' : '未设置' }}
</text>
<text class="arrow">></text>
</view>
</view>
<!-- 身份信息 -->
<view class="setting-item" @click="openIdentityModal">
<view class="setting-left">
<text class="setting-label">身份信息</text>
</view>
<view class="setting-right">
<text class="setting-status" :class="{ 'status-set': identityInfo.userName }">
{{ identityInfo.userName ? '已设置' : '未设置' }}
</text>
<text class="arrow">></text>
</view>
</view>
</view>
<!-- 收款账号弹窗 -->
<nut-popup
v-model:visible="showAccountModal"
position="bottom"
:style="{ width: '100%', height: '80%' }"
closeable
close-icon-position="top-right"
@close="closeAccountModal"
>
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">收款账号设置</text>
</view>
<view class="form-content">
<view class="form-item">
<text class="form-label">银行名称</text>
<nut-input
v-model="tempAccountInfo.bankName"
placeholder="请输入银行名称"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">银行账号</text>
<nut-input
v-model="tempAccountInfo.bankAccount"
placeholder="请输入银行账号"
class="form-input"
type="number"
/>
</view>
</view>
<view class="modal-footer">
<nut-button
type="default"
@click="closeAccountModal"
class="footer-btn footer-btn-cancel"
>
关闭
</nut-button>
<nut-button
type="primary"
@click="saveAccountInfo"
color="#ffa500"
:disabled="!tempAccountInfo.bankName || !tempAccountInfo.bankAccount"
class="footer-btn footer-btn-save"
>
保存
</nut-button>
</view>
</view>
</nut-popup>
<!-- 身份信息弹窗 -->
<nut-popup
v-model:visible="showIdentityModal"
position="bottom"
:style="{ width: '100%', height: '80%' }"
closeable
close-icon-position="top-right"
@close="closeIdentityModal"
>
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">身份信息设置</text>
</view>
<view class="form-content">
<view class="form-item">
<text class="form-label">用户名称</text>
<nut-input
v-model="tempIdentityInfo.userName"
placeholder="请输入真实姓名"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">身份证号码</text>
<nut-input
v-model="tempIdentityInfo.idCard"
placeholder="请输入身份证号码"
class="form-input"
maxlength="18"
@blur="handleIdCardBlur"
/>
<text v-if="idCardError" class="error-text">{{ idCardError }}</text>
</view>
</view>
<view class="modal-footer">
<nut-button
type="default"
@click="closeIdentityModal"
class="footer-btn footer-btn-cancel"
>
关闭
</nut-button>
<nut-button
type="primary"
@click="saveIdentityInfo"
color="#ffa500"
:disabled="!tempIdentityInfo.userName || !tempIdentityInfo.idCard || !!idCardError"
class="footer-btn footer-btn-save"
>
保存
</nut-button>
</view>
</view>
</nut-popup>
</view>
</template>
<script setup>
import { ref } from "vue";
import Taro from "@tarojs/taro";
import "./index.less";
/**
* 收款账号信息
*/
const accountInfo = ref({
bankName: '',
bankAccount: ''
});
/**
* 身份信息
*/
const identityInfo = ref({
userName: '',
idCard: ''
});
/**
* 临时收款账号信息(用于弹窗编辑)
*/
const tempAccountInfo = ref({
bankName: '',
bankAccount: ''
});
/**
* 临时身份信息(用于弹窗编辑)
*/
const tempIdentityInfo = ref({
userName: '',
idCard: ''
});
/**
* 弹窗显示状态
*/
const showAccountModal = ref(false);
const showIdentityModal = ref(false);
/**
* 身份证号码错误信息
*/
const idCardError = ref('');
/**
* 身份证号码校验
* @param {string} idCard - 身份证号码
* @returns {boolean} - 是否有效
*/
const validateIdCard = (idCard) => {
if (!idCard) {
return { valid: false, error: '请输入身份证号码' };
}
// 身份证号码正则表达式
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
if (!idCardRegex.test(idCard)) {
return { valid: false, error: '身份证号码格式不正确' };
}
// 校验码验证
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(idCard[i]) * weights[i];
}
const checkCode = checkCodes[sum % 11];
const lastChar = idCard[17].toUpperCase();
if (checkCode !== lastChar) {
return { valid: false, error: '身份证号码校验失败' };
}
return { valid: true, error: '' };
};
/**
* 处理身份证号码失焦事件
*/
const handleIdCardBlur = () => {
const idCard = tempIdentityInfo.value.idCard;
if (idCard) {
const result = validateIdCard(idCard);
idCardError.value = result.error;
} else {
idCardError.value = '';
}
};
/**
* 打开收款账号弹窗
*/
const openAccountModal = () => {
tempAccountInfo.value = { ...accountInfo.value };
showAccountModal.value = true;
};
/**
* 关闭收款账号弹窗
*/
const closeAccountModal = () => {
showAccountModal.value = false;
tempAccountInfo.value = { bankName: '', bankAccount: '' };
};
/**
* 保存收款账号信息
*/
const saveAccountInfo = () => {
if (!tempAccountInfo.value.bankName || !tempAccountInfo.value.bankAccount) {
Taro.showToast({
title: '请填写完整信息',
icon: 'none'
});
return;
}
accountInfo.value = { ...tempAccountInfo.value };
closeAccountModal();
Taro.showToast({
title: '保存成功',
icon: 'success'
});
};
/**
* 打开身份信息弹窗
*/
const openIdentityModal = () => {
tempIdentityInfo.value = { ...identityInfo.value };
idCardError.value = '';
showIdentityModal.value = true;
};
/**
* 关闭身份信息弹窗
*/
const closeIdentityModal = () => {
showIdentityModal.value = false;
tempIdentityInfo.value = { userName: '', idCard: '' };
idCardError.value = '';
};
/**
* 保存身份信息
*/
const saveIdentityInfo = () => {
if (!tempIdentityInfo.value.userName || !tempIdentityInfo.value.idCard) {
Taro.showToast({
title: '请填写完整信息',
icon: 'none'
});
return;
}
const validation = validateIdCard(tempIdentityInfo.value.idCard);
if (!validation.valid) {
Taro.showToast({
title: validation.error,
icon: 'none'
});
return;
}
identityInfo.value = { ...tempIdentityInfo.value };
closeIdentityModal();
Taro.showToast({
title: '保存成功',
icon: 'success'
});
};
</script>
<script>
export default {
name: "collectionSettings",
};
</script>
......@@ -357,13 +357,15 @@ const copyWechat = () => {
*/
const conversation_id = ref('')
const handleContactSeller = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.CONTACT_SELLER, {
const permissionResult = await checkPermission(PERMISSION_TYPES.CONTACT_SELLER, {
showToast: false,
autoRedirect: false
})
// 如果没有权限,显示确认弹窗
if (!hasPermission) {
if (!permissionResult.hasPermission) {
// 可以通过 permissionResult.missingFields 获取未通过校验的字段
// 可以通过 permissionResult.validFields 获取已通过校验的字段
Taro.showModal({
title: '提示',
content: '联系卖家需要先完善个人信息',
......@@ -441,13 +443,13 @@ const sendMessageToSeller = async () => {
* 购买商品
*/
const handlePurchase = async () => {
const hasPermission = await checkPermission(PERMISSION_TYPES.BUY_CAR, {
const permissionResult = await checkPermission(PERMISSION_TYPES.BUY_CAR, {
showToast: false,
autoRedirect: false
})
// 如果没有权限,显示确认弹窗
if (!hasPermission) {
if (!permissionResult.hasPermission) {
Taro.showModal({
title: '提示',
content: '购买车辆需要先完善个人信息',
......
......@@ -58,6 +58,12 @@
<Right size="18" color="#9ca3af" />
</view>
<view class="menu-item" @click="onCollectionSettings">
<Scan size="20" color="#6b7280" />
<text class="menu-text">收款设置</text>
<Right size="18" color="#9ca3af" />
</view>
<view class="menu-item" @click="onFeedback">
<Message size="20" color="#6b7280" />
<text class="menu-text">意见反馈</text>
......@@ -74,10 +80,10 @@
<!-- 自定义TabBar -->
<TabBar />
<!-- 隐私政策同意弹框 -->
<PrivacyAgreementModal
v-model:visible="showPrivacyModal"
<PrivacyAgreementModal
v-model:visible="showPrivacyModal"
@confirm="onPrivacyConfirm"
@cancel="onPrivacyCancel"
/>
......@@ -86,7 +92,7 @@
<script setup>
import { computed, ref } from 'vue'
import { Heart, Clock, Notice, Cart, Message, Tips, Right, StarN } from '@nutui/icons-vue-taro'
import { Heart, Clock, Notice, Cart, Message, Tips, Right, StarN, Scan } from '@nutui/icons-vue-taro'
import Taro, { useDidShow } from '@tarojs/taro'
import TabBar from '@/components/TabBar.vue'
import PrivacyAgreementModal from '@/components/PrivacyAgreementModal.vue'
......@@ -114,7 +120,7 @@ useDidShow(async () => {
const onEditProfile = async () => {
// 检查用户是否已完善资料
const hasCompleteProfile = userStore.hasCompleteProfile
if (hasCompleteProfile) {
// 已完善资料,直接进入编辑页面
Taro.navigateTo({
......@@ -207,6 +213,15 @@ const onFeedback = () => {
}
/**
* 收款设置
*/
const onCollectionSettings = () => {
Taro.navigateTo({
url: '/pages/collectionSettings/index'
})
}
/**
* 我的认证车
*/
const onMyAuthCar = () => {
......
......@@ -1130,24 +1130,33 @@ const loadBrandsModels = async () => {
// 页面加载时执行
onMounted(async () => {
// 检查卖车权限
const hasPermission = await checkPermission(PERMISSION_TYPES.SELL_CAR, {
const permissionResult = await checkPermission(PERMISSION_TYPES.SELL_CAR, {
showToast: false,
autoRedirect: false
})
// 如果没有权限,显示确认弹窗
if (!hasPermission) {
if (!permissionResult.hasPermission) {
// 可以通过 permissionResult.missingFields 获取未通过校验的字段
// 可以通过 permissionResult.validFields 获取已通过校验的字段
Taro.showModal({
title: '提示',
content: '发布车源需要先完善个人信息',
cancelText: '关闭',
confirmText: '前往完善',
title: '温馨提示',
content: `您还未填写${permissionResult.missingFields.includes('phone') ? '个人资料' : '收款信息'},展示无法发布`,
cancelText: '稍后设置',
confirmText: '立即设置',
success: (res) => {
if (res.confirm) {
// 用户点击前往完善,跳转到注册页面
Taro.navigateTo({
url: '/pages/register/index'
})
if (permissionResult.missingFields.includes('phone')) {
// 用户信息未填写
Taro.navigateTo({
url: '/pages/register/index'
})
} else if (permissionResult.missingFields.includes('paymentInfo')) {
// 收款信息未填写
Taro.navigateTo({
url: '/pages/collectionSettings/index'
})
}
} else {
// 用户点击关闭,返回上一页
// Taro.navigateBack()
......
......@@ -76,10 +76,25 @@ const PERMISSION_CONFIG = {
* @returns {boolean} 是否满足要求
*/
function checkUserFields(userInfo, checkFields) {
return checkFields.every(field => {
const result = {
isValid: true,
missingFields: [],
validFields: []
}
checkFields.forEach(field => {
const value = userInfo[field]
return value && value.toString().trim() !== ''
const isFieldValid = value && value.toString().trim() !== ''
if (isFieldValid) {
result.validFields.push(field)
} else {
result.missingFields.push(field)
result.isValid = false
}
})
return result
}
/**
......@@ -93,7 +108,11 @@ function checkUserFields(userInfo, checkFields) {
* @param {Function} options.onFail - 验证失败回调
* @param {boolean} options.showToast - 是否显示提示(默认true)
* @param {boolean} options.autoRedirect - 是否自动跳转(默认true)
* @returns {Promise<boolean>} 是否通过权限验证
* @returns {Promise<Object>} 权限检查结果
* @returns {boolean} returns.hasPermission - 是否通过权限验证
* @returns {Array} returns.validFields - 已通过校验的字段
* @returns {Array} returns.missingFields - 未通过校验的字段
* @returns {string} [returns.error] - 错误信息(如果有)
*/
export async function checkPermission(permissionType, options = {}) {
const userStore = useUserStore()
......@@ -119,12 +138,16 @@ export async function checkPermission(permissionType, options = {}) {
}
// 检查用户字段
const hasPermission = checkUserFields(userStore.userInfo, finalConfig.checkFields)
const fieldCheckResult = checkUserFields(userStore.userInfo, finalConfig.checkFields)
if (hasPermission) {
if (fieldCheckResult.isValid) {
// 权限验证成功
finalConfig.onSuccess && finalConfig.onSuccess()
return true
finalConfig.onSuccess && finalConfig.onSuccess(fieldCheckResult)
return {
hasPermission: true,
validFields: fieldCheckResult.validFields,
missingFields: []
}
} else {
// 权限验证失败
if (finalConfig.showToast) {
......@@ -148,13 +171,22 @@ export async function checkPermission(permissionType, options = {}) {
})
}
finalConfig.onFail && finalConfig.onFail()
return false
finalConfig.onFail && finalConfig.onFail(fieldCheckResult)
return {
hasPermission: false,
validFields: fieldCheckResult.validFields,
missingFields: fieldCheckResult.missingFields
}
}
} catch (error) {
console.error('权限检查失败:', error)
finalConfig.onFail && finalConfig.onFail(error)
return false
finalConfig.onFail && finalConfig.onFail({ error, missingFields: finalConfig.checkFields, validFields: [] })
return {
hasPermission: false,
validFields: [],
missingFields: finalConfig.checkFields,
error: error.message
}
}
}
......