继续完善购物逻辑闭环,consumer模块完成度75%
This commit is contained in:
@@ -55,10 +55,78 @@
|
||||
</scroll-view>
|
||||
|
||||
<!-- 添加地址按钮 -->
|
||||
<view class="add-address-btn" @click="addNewAddress">
|
||||
<view class="add-address-btn" @click="showNewAddressForm = true">
|
||||
<text class="btn-icon">+</text>
|
||||
<text class="btn-text">添加新地址</text>
|
||||
</view>
|
||||
|
||||
<!-- 新建地址表单弹窗 -->
|
||||
<view v-if="showNewAddressForm" class="address-form-mask" @click="cancelNewAddress">
|
||||
<view class="address-form-popup" @click.stop>
|
||||
<view class="form-header">
|
||||
<text class="form-title">新建收货地址</text>
|
||||
<text class="form-close" @click="cancelNewAddress">×</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="form-content" scroll-y>
|
||||
<view class="form-item">
|
||||
<text class="form-label">收货人</text>
|
||||
<input class="form-input" v-model="newAddress.recipient_name"
|
||||
placeholder="请输入收货人姓名" />
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">手机号</text>
|
||||
<input class="form-input" v-model="newAddress.phone"
|
||||
placeholder="请输入手机号码" type="number" />
|
||||
</view>
|
||||
|
||||
<!-- 地址智能填写 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">智能填写地址</text>
|
||||
<textarea class="form-textarea smart-address-input"
|
||||
v-model="smartAddressInput"
|
||||
placeholder="请输入完整地址,系统将自动识别省市区和详细地址"
|
||||
@blur="parseSmartAddress"
|
||||
maxlength="200" />
|
||||
<text class="smart-tip">例如:北京市朝阳区三里屯SOHO A座</text>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">所在地区</text>
|
||||
<view class="region-inputs">
|
||||
<input class="form-input region-input" v-model="newAddress.province"
|
||||
placeholder="省" readonly />
|
||||
<input class="form-input region-input" v-model="newAddress.city"
|
||||
placeholder="市" readonly />
|
||||
<input class="form-input region-input" v-model="newAddress.district"
|
||||
placeholder="区/县" readonly />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">详细地址</text>
|
||||
<textarea class="form-textarea" v-model="newAddress.detail"
|
||||
placeholder="街道、小区、楼栋、门牌号等"
|
||||
maxlength="100" />
|
||||
</view>
|
||||
|
||||
<view class="form-item checkbox-item">
|
||||
<view class="checkbox-wrapper" @click="newAddress.is_default = !newAddress.is_default">
|
||||
<view :class="['checkbox', { checked: newAddress.is_default }]">
|
||||
<text v-if="newAddress.is_default" class="checkbox-check">✓</text>
|
||||
</view>
|
||||
<text class="checkbox-label">设为默认地址</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="form-buttons">
|
||||
<button class="form-cancel-btn" @click="cancelNewAddress">取消</button>
|
||||
<button class="form-submit-btn" @click="saveNewAddress">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -83,6 +151,17 @@ type AddressType = {
|
||||
const addressList = ref<Array<AddressType>>([])
|
||||
const fromSelect = ref<boolean>(false)
|
||||
const selectCallback = ref<any>(null)
|
||||
const showNewAddressForm = ref<boolean>(false)
|
||||
const newAddress = ref<any>({
|
||||
recipient_name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail: '',
|
||||
is_default: false
|
||||
})
|
||||
const smartAddressInput = ref<string>('')
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
@@ -95,6 +174,16 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
loadAddresses()
|
||||
|
||||
// 监听地址更新事件(从checkout页面或其他页面)
|
||||
uni.$on('addressUpdated', (updatedAddressList: any) => {
|
||||
addressList.value = updatedAddressList
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时移除事件监听
|
||||
onUnmounted(() => {
|
||||
uni.$off('addressUpdated')
|
||||
})
|
||||
|
||||
// 加载地址列表
|
||||
@@ -287,11 +376,193 @@ const setAsDefault = async (address: AddressType) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新地址
|
||||
const addNewAddress = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/address-edit'
|
||||
})
|
||||
// 解析智能地址
|
||||
const parseSmartAddress = () => {
|
||||
const input = smartAddressInput.value.trim()
|
||||
if (!input) return
|
||||
|
||||
// 重置表单
|
||||
newAddress.value.recipient_name = ''
|
||||
newAddress.value.phone = ''
|
||||
newAddress.value.province = ''
|
||||
newAddress.value.city = ''
|
||||
newAddress.value.district = ''
|
||||
newAddress.value.detail = ''
|
||||
|
||||
// 尝试匹配手机号码(11位数字)
|
||||
const phoneRegex = /(1[3-9]\d{9})/g
|
||||
const phoneMatches = input.match(phoneRegex)
|
||||
if (phoneMatches && phoneMatches.length > 0) {
|
||||
newAddress.value.phone = phoneMatches[0]
|
||||
}
|
||||
|
||||
// 尝试匹配收件人姓名(中文姓名,2-4个汉字)
|
||||
const nameRegex = /([\u4e00-\u9fa5]{2,4})/g
|
||||
const nameMatches = input.match(nameRegex)
|
||||
if (nameMatches && nameMatches.length > 0) {
|
||||
// 取第一个匹配的中文姓名作为收件人
|
||||
newAddress.value.recipient_name = nameMatches[0]
|
||||
}
|
||||
|
||||
// 提取地址部分(移除姓名和手机号)
|
||||
let addressText = input
|
||||
if (newAddress.value.recipient_name) {
|
||||
addressText = addressText.replace(newAddress.value.recipient_name, '')
|
||||
}
|
||||
if (newAddress.value.phone) {
|
||||
addressText = addressText.replace(newAddress.value.phone, '')
|
||||
}
|
||||
|
||||
// 清理地址文本(移除多余的空格和标点)
|
||||
addressText = addressText.replace(/[,,;;\s]+/g, ' ').trim()
|
||||
|
||||
// 地址解析逻辑
|
||||
const patterns = [
|
||||
// 匹配格式:省市区详细地址
|
||||
/^(.*?省)?(.*?市)?(.*?[区县])?(.*)$/,
|
||||
// 匹配格式:省市详细地址
|
||||
/^(.*?省)?(.*?市)?(.*)$/
|
||||
]
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = addressText.match(pattern)
|
||||
if (match) {
|
||||
const [, province, city, district, detail] = match
|
||||
|
||||
if (province) newAddress.value.province = province.replace('省', '').trim()
|
||||
if (city) newAddress.value.city = city.replace('市', '').trim()
|
||||
if (district) newAddress.value.district = district.trim()
|
||||
if (detail) newAddress.value.detail = detail.trim()
|
||||
|
||||
// 如果详细地址为空,但还有剩余内容,则作为详细地址
|
||||
if (!newAddress.value.detail && district && detail) {
|
||||
newAddress.value.detail = detail.trim()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配到模式,尝试简单分割
|
||||
if (!newAddress.value.province && !newAddress.value.city && !newAddress.value.district) {
|
||||
// 尝试按常见分隔符分割
|
||||
const parts = addressText.split(/[省市县区]/)
|
||||
if (parts.length >= 2) {
|
||||
newAddress.value.province = parts[0] || ''
|
||||
newAddress.value.city = parts[1] || ''
|
||||
newAddress.value.detail = parts.slice(2).join('').trim() || addressText
|
||||
} else {
|
||||
newAddress.value.detail = addressText
|
||||
}
|
||||
}
|
||||
|
||||
// 如果地址部分为空,但还有剩余文本,则作为详细地址
|
||||
if (!newAddress.value.detail && addressText.trim()) {
|
||||
newAddress.value.detail = addressText.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// 保存新地址
|
||||
const saveNewAddress = async () => {
|
||||
// 验证表单
|
||||
if (!newAddress.value.recipient_name || !newAddress.value.phone || !newAddress.value.detail) {
|
||||
uni.showToast({
|
||||
title: '请填写完整信息',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const userId = getCurrentUserId()
|
||||
if (!userId) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supa
|
||||
.from('user_addresses')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
recipient_name: newAddress.value.recipient_name,
|
||||
phone: newAddress.value.phone,
|
||||
province: newAddress.value.province,
|
||||
city: newAddress.value.city,
|
||||
district: newAddress.value.district,
|
||||
detail: newAddress.value.detail,
|
||||
is_default: newAddress.value.is_default,
|
||||
created_at: new Date().toISOString()
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error !== null) {
|
||||
console.error('保存地址失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是默认地址,取消其他默认地址
|
||||
if (newAddress.value.is_default) {
|
||||
addressList.value.forEach(addr => {
|
||||
addr.is_default = false
|
||||
})
|
||||
|
||||
// 更新数据库中的其他地址
|
||||
await supa
|
||||
.from('user_addresses')
|
||||
.update({ is_default: false })
|
||||
.eq('user_id', userId)
|
||||
.neq('id', data.id)
|
||||
}
|
||||
|
||||
// 添加到列表
|
||||
addressList.value.unshift(data)
|
||||
|
||||
// 发布地址更新事件,让checkout页面也能获取到
|
||||
uni.$emit('addressUpdated', addressList.value)
|
||||
|
||||
// 重置表单
|
||||
resetNewAddressForm()
|
||||
|
||||
uni.showToast({
|
||||
title: '地址保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
console.error('保存地址异常:', err)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 重置新建地址表单
|
||||
const resetNewAddressForm = () => {
|
||||
showNewAddressForm.value = false
|
||||
newAddress.value = {
|
||||
recipient_name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail: '',
|
||||
is_default: false
|
||||
}
|
||||
smartAddressInput.value = ''
|
||||
}
|
||||
|
||||
// 取消新建地址
|
||||
const cancelNewAddress = () => {
|
||||
resetNewAddressForm()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -471,4 +742,177 @@ const addNewAddress = () => {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 新建地址表单弹窗样式 */
|
||||
.address-form-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.address-form-popup {
|
||||
background-color: #ffffff;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
padding: 20px 15px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.form-close {
|
||||
font-size: 24px;
|
||||
color: #999999;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input[readonly] {
|
||||
background-color: #f9f9f9;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.region-inputs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.region-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.smart-address-input {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.smart-tip {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
background-color: #007aff;
|
||||
border-color: #007aff;
|
||||
}
|
||||
|
||||
.checkbox-check {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-cancel-btn {
|
||||
flex: 1;
|
||||
background-color: #f5f5f5;
|
||||
color: #333333;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.form-submit-btn {
|
||||
flex: 1;
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user