hookehuyr

新增地址选择组件给身份证地址使用

......@@ -7,6 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AddressSelector: typeof import('./src/components/AddressSelector.vue')['default']
BannerSwiper: typeof import('./src/components/BannerSwiper.vue')['default']
BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default']
FeaturedRecommendations: typeof import('./src/components/FeaturedRecommendations.vue')['default']
......@@ -15,6 +16,7 @@ declare module 'vue' {
NavBar: typeof import('./src/components/navBar.vue')['default']
NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutCascader: typeof import('@nutui/nutui-taro')['Cascader']
NutCheckbox: typeof import('@nutui/nutui-taro')['Checkbox']
NutCol: typeof import('@nutui/nutui-taro')['Col']
NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
......
......@@ -37,6 +37,7 @@
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.7.7",
"@nutui/icons-vue": "^0.1.1",
"@nutui/icons-vue-taro": "^0.0.9",
"@nutui/nutui-taro": "^4.3.13",
"@tarojs/components": "4.1.2",
......@@ -55,6 +56,7 @@
"@tarojs/shared": "4.1.2",
"@tarojs/taro": "4.1.2",
"axios-miniprogram": "^2.7.2",
"element-china-area-data": "^6.1.0",
"pinia": "^3.0.3",
"taro-plugin-pinia": "^1.0.0",
"vue": "^3.3.0"
......
This diff could not be displayed because it is too large.
......@@ -47,6 +47,12 @@ button::after {
color: #9ca3af;
}
/* 覆盖 NutUI Tabs 组件的 width: 0 样式 */
.nut-tabs.horizontal .nut-sticky__box > .nut-tabs__titles .nut-tabs__titles-item,
.nut-tabs.horizontal > .nut-tabs__titles .nut-tabs__titles-item {
width: auto !important;
}
.bg-orange-400 {
background-color: #fb923c;
}
......
<template>
<view>
<!-- 地址选择弹窗 -->
<nut-popup
:visible="visible"
position="bottom"
:style="{ height: '85%' }"
close-icon-position="top-right"
@close="closeModal"
:z-index="9999"
>
<view class="address-modal">
<view class="address-modal-header">
<text class="address-modal-title">选择地址</text>
</view>
<view class="address-modal-content">
<!-- 省市县选择 -->
<view class="address-section">
<text class="address-section-title">选择省市县</text>
<nut-cascader
v-model="selectedAreaCodes"
v-model:visible="showAreaPicker"
:options="areaData"
@change="onAreaChange"
@path-change="onAreaPathChange"
title="请选择省市县"
/>
<view
class="area-selector"
@click="showAreaPicker = true"
>
<text class="area-text" :class="{ 'area-selected': selectedAreaText }">
{{ selectedAreaText || '请选择省市县' }}
</text>
<ArrowRight color="#9ca3af" size="12" />
</view>
</view>
<!-- 详细地址输入 -->
<view class="address-section">
<text class="address-section-title">详细地址</text>
<nut-textarea
v-model="detailAddress"
placeholder="请输入详细地址(街道、门牌号等)"
rows="3"
class="detail-address-input"
/>
</view>
</view>
<view class="address-modal-footer">
<nut-button
type="primary"
color="orange"
block
@click="confirmAddress"
:disabled="!selectedAreaText || !detailAddress.trim()"
>
确定
</nut-button>
</view>
</view>
</nut-popup>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { RectRight, ArrowRight } from '@nutui/icons-vue-taro'
import { regionData } from 'element-china-area-data'
/**
* 转换地区数据格式以适配NutUI Cascader组件
* @param {Array} data - element-china-area-data的原始数据
* @returns {Array} - 转换后的数据格式
*/
const transformAreaData = (data) => {
return data.map(item => ({
value: item.value,
text: item.label,
children: item.children ? transformAreaData(item.children) : undefined
}))
}
/**
* 组件属性定义
*/
const props = defineProps({
// 当前选中的地址信息
modelValue: {
type: Object,
default: () => ({
province: '',
city: '',
county: '',
detail_address: '',
full_address: ''
})
},
// 占位符文本
placeholder: {
type: String,
default: '请选择省市县并填写详细地址'
},
// 控制弹窗显示状态
visible: {
type: Boolean,
default: false
}
})
/**
* 组件事件定义
*/
const emit = defineEmits(['update:modelValue', 'change', 'update:visible'])
/**
* 地址选择相关状态
*/
const showAreaPicker = ref(false)
const selectedAreaCodes = ref([])
const selectedAreaText = ref('')
const detailAddress = ref('')
const areaData = ref(transformAreaData(regionData))
/**
* 计算完整地址
*/
const fullAddress = computed(() => {
if (selectedAreaText.value && detailAddress.value.trim()) {
return `${selectedAreaText.value} ${detailAddress.value.trim()}`
}
return ''
})
/**
* 初始化地址数据
* @param {Object} addressData - 地址数据对象
*/
const initAddressData = (addressData) => {
if (addressData.province && addressData.city && addressData.county) {
selectedAreaText.value = `${addressData.province}${addressData.city}${addressData.county}`
detailAddress.value = addressData.detail_address || ''
} else {
selectedAreaText.value = ''
detailAddress.value = ''
}
}
/**
* 监听props变化,初始化组件数据
*/
watch(() => props.modelValue, (newValue) => {
if (newValue) {
initAddressData(newValue)
}
}, { immediate: true, deep: true })
/**
* 地区选择变化回调(选中值改变时触发)
* @param {Array} value - 选中的值数组
* @param {Array} pathNodes - 选中的路径节点数组
*/
const onAreaChange = (value, pathNodes) => {
if (pathNodes && Array.isArray(pathNodes) && pathNodes.length === 3) {
selectedAreaCodes.value = value
selectedAreaText.value = pathNodes
.filter(node => node && node.text)
.map(node => node.text)
.join('')
// 选择完三级地址后自动关闭选择器
showAreaPicker.value = false
}
}
/**
* 地区路径选择变化回调(选中项改变时触发)
* @param {Array} pathNodes - 选中的路径节点数组
*/
const onAreaPathChange = (pathNodes) => {
if (pathNodes && Array.isArray(pathNodes)) {
selectedAreaText.value = pathNodes
.filter(node => node && node.text)
.map(node => node.text)
.join('')
}
}
/**
* 关闭地址选择弹窗
*/
const closeModal = () => {
emit('update:visible', false)
}
/**
* 确认地址选择
*/
const confirmAddress = () => {
if (selectedAreaText.value && detailAddress.value.trim()) {
const codes = selectedAreaCodes.value
const addressData = {
province: getProvinceFromText(selectedAreaText.value),
city: getCityFromText(selectedAreaText.value),
county: getCountyFromText(selectedAreaText.value),
province_code: codes[0] || '',
city_code: codes[1] || '',
county_code: codes[2] || '',
detail_address: detailAddress.value.trim(),
full_address: fullAddress.value
}
// 触发更新事件
emit('update:modelValue', addressData)
emit('change', addressData)
// 关闭弹窗
emit('update:visible', false)
}
}
/**
* 从选中文本中提取省份
* @param {String} text - 选中的地区文本
* @returns {String} - 省份名称
*/
const getProvinceFromText = (text) => {
// 这里可以根据实际需要实现更精确的解析逻辑
// 暂时使用简单的方式
const codes = selectedAreaCodes.value
if (codes.length >= 1) {
const provinceNode = findNodeByCode(areaData.value, codes[0])
return provinceNode ? provinceNode.text : ''
}
return ''
}
/**
* 从选中文本中提取城市
* @param {String} text - 选中的地区文本
* @returns {String} - 城市名称
*/
const getCityFromText = (text) => {
const codes = selectedAreaCodes.value
if (codes.length >= 2) {
const provinceNode = findNodeByCode(areaData.value, codes[0])
if (provinceNode && provinceNode.children) {
const cityNode = findNodeByCode(provinceNode.children, codes[1])
return cityNode ? cityNode.text : ''
}
}
return ''
}
/**
* 从选中文本中提取县区
* @param {String} text - 选中的地区文本
* @returns {String} - 县区名称
*/
const getCountyFromText = (text) => {
const codes = selectedAreaCodes.value
if (codes.length >= 3) {
const provinceNode = findNodeByCode(areaData.value, codes[0])
if (provinceNode && provinceNode.children) {
const cityNode = findNodeByCode(provinceNode.children, codes[1])
if (cityNode && cityNode.children) {
const countyNode = findNodeByCode(cityNode.children, codes[2])
return countyNode ? countyNode.text : ''
}
}
}
return ''
}
/**
* 根据编码查找节点
* @param {Array} nodes - 节点数组
* @param {String} code - 编码
* @returns {Object|null} - 找到的节点或null
*/
const findNodeByCode = (nodes, code) => {
return nodes.find(node => node.value === code) || null
}
</script>
<style lang="less">
// 地址选择器样式
.address-selector {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
border: 1px solid #e5e7eb;
border-radius: 24rpx;
background-color: white;
min-height: 96rpx;
transition: all 0.2s ease;
&:active {
background-color: #f9fafb;
}
.address-text {
font-size: 28rpx;
color: #9ca3af;
flex: 1;
line-height: 1.6;
word-break: break-all;
&.address-selected {
color: #111827;
}
}
}
// 地址选择弹窗样式
.address-modal {
display: flex;
flex-direction: column;
height: 100%;
background-color: white;
.address-modal-header {
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
border-bottom: 1px solid #f3f4f6;
background-color: white;
position: sticky;
top: 0;
z-index: 10;
.address-modal-title {
font-size: 36rpx;
font-weight: 600;
color: #111827;
}
}
.address-modal-content {
flex: 1;
padding: 32rpx;
overflow-y: auto;
.address-section {
margin-bottom: 48rpx;
.address-section-title {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #374151;
margin-bottom: 24rpx;
}
.area-selector {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
border: 1px solid #e5e7eb;
border-radius: 24rpx;
background-color: white;
transition: all 0.2s ease;
&:active {
background-color: #f9fafb;
}
.area-text {
font-size: 28rpx;
color: #9ca3af;
flex: 1;
line-height: 1.6;
word-break: break-all;
&.area-selected {
color: #111827;
}
}
}
.detail-address-input {
width: 100%;
:deep(.nut-textarea) {
border: 1px solid #e5e7eb;
border-radius: 24rpx;
.nut-textarea__textarea {
padding: 24rpx 32rpx;
font-size: 28rpx;
line-height: 1.6;
min-height: 160rpx;
}
}
}
}
}
.address-modal-footer {
padding: 32rpx;
border-top: 1px solid #f3f4f6;
background-color: white;
position: sticky;
bottom: 0;
z-index: 10;
:deep(.nut-button) {
border-radius: 24rpx;
font-weight: 500;
height: 88rpx;
}
}
}
</style>
......@@ -474,3 +474,32 @@
font-size: 28rpx;
margin-right: 8rpx;
}
// 地址选择器触发器样式
.address-selector {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
border: 1px solid #e5e7eb;
border-radius: 24rpx;
background-color: white;
min-height: 96rpx;
transition: all 0.2s ease;
&:active {
background-color: #f9fafb;
}
.address-text {
font-size: 28rpx;
color: #9ca3af;
flex: 1;
line-height: 1.6;
word-break: break-all;
&.address-selected {
color: #111827;
}
}
}
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-08 13:58:13
* @LastEditTime: 2025-08-12 17:05:50
* @FilePath: /jgdl/src/pages/collectionSettings/index.vue
* @Description: 收款设置
-->
......@@ -32,9 +32,9 @@
</view>
<view class="setting-right">
<text class="setting-status"
:class="{ 'status-set': identityInfo.userName && identityInfo.idCard && identityInfo.idcard_1_img && identityInfo.idcard_2_img && identityInfo.idcard_effect_begin && identityInfo.idcard_effect_end && identityInfo.idcard_address }">
:class="{ 'status-set': identityInfo.userName && identityInfo.idCard && identityInfo.idcard_1_img && identityInfo.idcard_2_img && identityInfo.idcard_effect_begin && identityInfo.idcard_effect_end && (identityInfo.idcard_province && identityInfo.idcard_city && identityInfo.idcard_district && identityInfo.idcard_address) }">
{{ identityInfo.userName && identityInfo.idCard && identityInfo.idcard_1_img && identityInfo.idcard_2_img &&
identityInfo.idcard_effect_begin && identityInfo.idcard_effect_end && identityInfo.idcard_address ? '已设置' :
identityInfo.idcard_effect_begin && identityInfo.idcard_effect_end && (identityInfo.idcard_province && identityInfo.idcard_city && identityInfo.idcard_district && identityInfo.idcard_address) ? '已设置' :
'未设置' }}
</text>
<!-- <text class="arrow">></text> -->
......@@ -194,8 +194,16 @@
<!-- 身份证地址 -->
<view class="form-item">
<text class="form-label"><text class="required-mark">*</text>身份证地址</text>
<textarea v-model="tempIdentityInfo.idcard_address" placeholder="请输入身份证地址" class="form-input native-input"
:cursor-spacing="50" />
<!-- 地址选择器触发器 -->
<view
class="address-selector"
@click="showAddressSelector = true"
>
<text class="address-text" :class="{ 'address-selected': addressData.full_address }">
{{ addressData.full_address || '请选择省市县并填写详细地址' }}
</text>
<RectRight color="#999" size="10" />
</view>
</view>
</view>
......@@ -204,7 +212,7 @@
关闭
</nut-button>
<nut-button type="primary" @click="saveIdentityInfo" color="#ffa500"
:disabled="!tempIdentityInfo.userName || !tempIdentityInfo.idCard || !tempIdentityInfo.idcard_1_img || !tempIdentityInfo.idcard_2_img || !tempIdentityInfo.idcard_effect_begin || !tempIdentityInfo.idcard_effect_end || !tempIdentityInfo.idcard_address || !!idCardError"
:disabled="!tempIdentityInfo.userName || !tempIdentityInfo.idCard || !tempIdentityInfo.idcard_1_img || !tempIdentityInfo.idcard_2_img || !tempIdentityInfo.idcard_effect_begin || !tempIdentityInfo.idcard_effect_end || !tempIdentityInfo.idcard_province || !tempIdentityInfo.idcard_city || !tempIdentityInfo.idcard_district || !tempIdentityInfo.idcard_address || !!idCardError"
class="footer-btn footer-btn-save">
保存
</nut-button>
......@@ -234,6 +242,15 @@
<view v-if="showBackButton" class="fixed-back-btn" @click="goBack">
<text class="back-text">返回</text>
</view>
<!-- 地址选择器组件 -->
<AddressSelector
v-model:visible="showAddressSelector"
v-model="addressData"
@change="onAddressChange"
placeholder="请选择省市县并填写详细地址"
/>
</view>
</template>
......@@ -252,6 +269,9 @@ import BASE_URL from '@/utils/config'
// 导入用户状态管理
import { useUserStore } from '@/stores/user'
// 导入地址选择组件
import AddressSelector from '@/components/AddressSelector.vue'
// 获取页面参数
const instance = Taro.getCurrentInstance()
const { target } = instance.router?.params || {}
......@@ -308,6 +328,14 @@ const tempIdentityInfo = ref({
idcard_2_img: '',
idcard_effect_begin: '',
idcard_effect_end: '',
// 身份证地址相关字段(显示用中文名称)
province: '',
city: '',
county: '',
// 身份证地址相关字段(提交用真实字段名)
idcard_province: '',
idcard_city: '',
idcard_district: '',
idcard_address: ''
});
......@@ -320,6 +348,7 @@ const showBankModal = ref(false);
const showIdcardBeginDate = ref(false);
const showIdcardEndDate = ref(false);
const showEndDateTypeModal = ref(false);
const showAddressSelector = ref(false);
/**
* 身份证号码错误信息
......@@ -418,6 +447,14 @@ const getUserInfo = async () => {
idcard_2_img: userInfo.idcard_2_img || '',
idcard_effect_begin: userInfo.idcard_effect_begin || '',
idcard_effect_end: userInfo.idcard_effect_end || '',
// 地址字段(用于显示的中文名称)
province: userInfo.province_name || '',
city: userInfo.city_name || '',
county: userInfo.county_name || userInfo.district_name || '',
// 身份证地址字段(真实字段名)
idcard_province: userInfo.idcard_province || '',
idcard_city: userInfo.idcard_city || '',
idcard_district: userInfo.idcard_district || '',
idcard_address: userInfo.idcard_address || ''
};
}
......@@ -620,10 +657,28 @@ const saveAccountInfo = async () => {
const openIdentityModal = () => {
tempIdentityInfo.value = { ...identityInfo.value };
idCardError.value = '';
// 初始化地址选择器数据
initAddressData();
showIdentityModal.value = true;
};
/**
* 初始化地址选择器数据
*/
const initAddressData = () => {
addressData.value = {
province: tempIdentityInfo.value.province || '',
city: tempIdentityInfo.value.city || '',
county: tempIdentityInfo.value.county || '',
province_code: tempIdentityInfo.value.idcard_province || '',
city_code: tempIdentityInfo.value.idcard_city || '',
county_code: tempIdentityInfo.value.idcard_district || '',
detail_address: tempIdentityInfo.value.idcard_address || '',
full_address: (tempIdentityInfo.value.province + tempIdentityInfo.value.city + tempIdentityInfo.value.county + tempIdentityInfo.value.idcard_address) || ''
}
};
/**
* 关闭身份信息弹窗
*/
const closeIdentityModal = () => {
......@@ -635,6 +690,12 @@ const closeIdentityModal = () => {
idcard_2_img: '',
idcard_effect_begin: '',
idcard_effect_end: '',
province: '',
city: '',
county: '',
idcard_province: '',
idcard_city: '',
idcard_district: '',
idcard_address: ''
};
idCardError.value = '';
......@@ -995,6 +1056,10 @@ const saveIdentityInfo = async () => {
idcard_2_img: tempIdentityInfo.value.idcard_2_img,
idcard_effect_begin: tempIdentityInfo.value.idcard_effect_begin,
idcard_effect_end: tempIdentityInfo.value.idcard_effect_end,
// 使用真实的身份证地址字段名
idcard_province: tempIdentityInfo.value.idcard_province,
idcard_city: tempIdentityInfo.value.idcard_city,
idcard_district: tempIdentityInfo.value.idcard_district,
idcard_address: tempIdentityInfo.value.idcard_address
});
......@@ -1009,6 +1074,10 @@ const saveIdentityInfo = async () => {
idcard_2_img: tempIdentityInfo.value.idcard_2_img,
idcard_effect_begin: tempIdentityInfo.value.idcard_effect_begin,
idcard_effect_end: tempIdentityInfo.value.idcard_effect_end,
// 身份证地址字段
idcard_province: tempIdentityInfo.value.idcard_province,
idcard_city: tempIdentityInfo.value.idcard_city,
idcard_district: tempIdentityInfo.value.idcard_district,
idcard_address: tempIdentityInfo.value.idcard_address
});
......@@ -1041,6 +1110,32 @@ const goBack = () => {
url: '/pages/sell/index'
});
};
// 地址选择相关状态
const addressData = ref({
province: '',
city: '',
county: '',
detail_address: '',
full_address: ''
})
/**
* 地址变化处理回调
* @param {Object} address - 地址数据对象
*/
const onAddressChange = (address) => {
// 更新临时身份信息中的地址字段(中文名称,用于显示)
tempIdentityInfo.value.province = address.province
tempIdentityInfo.value.city = address.city
tempIdentityInfo.value.county = address.county
// 更新身份证地址字段(真实字段名,用于提交)
tempIdentityInfo.value.idcard_province = address.province_code || address.province
tempIdentityInfo.value.idcard_city = address.city_code || address.city
tempIdentityInfo.value.idcard_district = address.county_code || address.county
tempIdentityInfo.value.idcard_address = address.detail_address
}
</script>
<script>
......
/*
* @Date: 2025-01-08 18:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-07 21:50:44
* @LastEditTime: 2025-08-12 16:36:02
* @FilePath: /jgdl/src/stores/user.js
* @Description: 用户状态管理
*/
......@@ -34,6 +34,10 @@ export const useUserStore = defineStore('user', {
idcard_2_img: '',
idcard_effect_begin: '',
idcard_effect_end: '',
// 身份证地址字段
idcard_province: '',
idcard_city: '',
idcard_district: '',
idcard_address: '',
division_agree_pic: '',
},
......@@ -55,16 +59,19 @@ export const useUserStore = defineStore('user', {
*/
hasCompleteCollectionInfo: (state) => {
return !!(
state.userInfo.bank_id && state.userInfo.bank_id &&
state.userInfo.bank_no && state.userInfo.bank_no &&
state.userInfo.bank_img && state.userInfo.bank_img &&
state.userInfo.idcard_1_img && state.userInfo.idcard_1_img &&
state.userInfo.idcard_2_img && state.userInfo.idcard_2_img &&
state.userInfo.name && state.userInfo.name &&
state.userInfo.idcard && state.userInfo.idcard &&
state.userInfo.idcard_address && state.userInfo.idcard_address &&
state.userInfo.idcard_effect_begin && state.userInfo.idcard_effect_begin &&
state.userInfo.idcard_effect_end && state.userInfo.idcard_effect_end
state.userInfo.bank_id &&
state.userInfo.bank_no &&
state.userInfo.bank_img &&
state.userInfo.idcard_1_img &&
state.userInfo.idcard_2_img &&
state.userInfo.name &&
state.userInfo.idcard &&
state.userInfo.idcard_province &&
state.userInfo.idcard_city &&
state.userInfo.idcard_district &&
state.userInfo.idcard_address &&
state.userInfo.idcard_effect_begin &&
state.userInfo.idcard_effect_end
)
},
......@@ -142,6 +149,10 @@ export const useUserStore = defineStore('user', {
idcard_2_img: '',
idcard_effect_begin: '',
idcard_effect_end: '',
// 身份证地址字段
idcard_province: '',
idcard_city: '',
idcard_district: '',
idcard_address: '',
division_agree_pic: '',
}
......