|
|
|
@ -11,22 +11,29 @@ |
|
|
|
<input class="form-input" type="number" maxlength="11" v-model="addressForm.receiverPhone" |
|
|
|
@input="onReceiverPhoneInput" placeholder="手机号" placeholder-class="form-placeholder" /> |
|
|
|
</view> |
|
|
|
<view class="form-row" @tap="openAreaPopup"> |
|
|
|
<view v-if="!isSocialArea" class="form-row" @tap="openAreaPopup"> |
|
|
|
<text class="form-label">楼座区域</text> |
|
|
|
<text class="form-value" :class="{'form-placeholder-text': !areaTitleInput}"> |
|
|
|
{{areaTitleInput || (areaListLoading ? '加载中...' : '选择收货地址')}} |
|
|
|
</text> |
|
|
|
<uni-icons type="right" size="18" color="#999"></uni-icons> |
|
|
|
</view> |
|
|
|
<view class="form-row"> |
|
|
|
<view v-else class="form-row" @tap="openLocationPicker"> |
|
|
|
<text class="form-label">收货地址</text> |
|
|
|
<text class="form-value" :class="{'form-placeholder-text': !addressForm.areaName}"> |
|
|
|
{{addressForm.areaName || '搜索/定位选择地址'}} |
|
|
|
</text> |
|
|
|
<uni-icons type="right" size="18" color="#999"></uni-icons> |
|
|
|
</view> |
|
|
|
<view v-if="!isSocialArea" class="form-row"> |
|
|
|
<text class="form-label">楼层</text> |
|
|
|
<input class="form-input" type="number" v-model="addressForm.floor" @input="onFloorInput" |
|
|
|
placeholder="所在楼层,仅限整数" placeholder-class="form-placeholder" /> |
|
|
|
</view> |
|
|
|
<view class="form-row"> |
|
|
|
<text class="form-label">门牌号</text> |
|
|
|
<text class="form-label">{{isSocialArea ? '门牌号' : '门牌号'}}</text> |
|
|
|
<input class="form-input" type="text" maxlength="30" v-model="addressForm.roomNum" |
|
|
|
@input="onRoomNumInput" placeholder="例:5号楼203室" placeholder-class="form-placeholder" /> |
|
|
|
@input="onRoomNumInput" :placeholder="isSocialArea ? '例:3号楼2单元301室' : '例:5号楼203室'" placeholder-class="form-placeholder" /> |
|
|
|
</view> |
|
|
|
<view class="form-row form-row--switch"> |
|
|
|
<text class="form-label">设为默认地址</text> |
|
|
|
@ -57,6 +64,21 @@ |
|
|
|
</scroll-view> |
|
|
|
</view> |
|
|
|
</uni-popup> |
|
|
|
<view v-if="showPrivacyModal" class="privacy-mask"> |
|
|
|
<view class="privacy-dialog"> |
|
|
|
<view class="privacy-title">隐私授权提示</view> |
|
|
|
<view class="privacy-content"> |
|
|
|
选择收货地址需要使用定位能力。请先阅读并同意 |
|
|
|
<text class="privacy-link" @tap.stop="openPrivacyContract">{{privacyContractName}}</text> |
|
|
|
后继续。 |
|
|
|
</view> |
|
|
|
<view class="privacy-actions"> |
|
|
|
<button class="privacy-btn privacy-btn--cancel" @tap="rejectPrivacyAuthorization">不同意</button> |
|
|
|
<button id="agree-privacy-btn" class="privacy-btn privacy-btn--agree" open-type="agreePrivacyAuthorization" |
|
|
|
@agreeprivacyauthorization="handleAgreePrivacyAuthorization">同意并继续</button> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</template> |
|
|
|
|
|
|
|
@ -70,11 +92,17 @@ |
|
|
|
areaTitleInput: '', |
|
|
|
areaSearchInput: '', |
|
|
|
addressSubmitting: false, |
|
|
|
showPrivacyModal: false, |
|
|
|
privacyContractName: '《用户隐私保护指引》', |
|
|
|
pendingPrivacyResolve: null, |
|
|
|
pendingPrivacyReject: null, |
|
|
|
addressForm: { |
|
|
|
id: '', |
|
|
|
regionId: '', |
|
|
|
areaId: '', |
|
|
|
areaName: '', |
|
|
|
addressLng: '', |
|
|
|
addressLat: '', |
|
|
|
floor: '', |
|
|
|
roomNum: '', |
|
|
|
receiverName: '', |
|
|
|
@ -84,6 +112,10 @@ |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
isSocialArea() { |
|
|
|
const area = this.getCurrentArea(); |
|
|
|
return area && Number(area.areaKind) === 2; |
|
|
|
}, |
|
|
|
filteredAreaList() { |
|
|
|
const keyword = (this.areaSearchInput || '').toString(); |
|
|
|
if (!keyword) return this.areaList; |
|
|
|
@ -100,17 +132,25 @@ |
|
|
|
this.addressForm.id = id; |
|
|
|
this.initEditAddress(id); |
|
|
|
} |
|
|
|
this.getAreaList().then(() => { |
|
|
|
this.syncAreaTitle(); |
|
|
|
}); |
|
|
|
if (!this.isSocialArea) { |
|
|
|
this.getAreaList().then(() => { |
|
|
|
this.syncAreaTitle(); |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
getCurrentRegionId() { |
|
|
|
getCurrentArea() { |
|
|
|
try { |
|
|
|
const area = uni.getStorageSync('area'); |
|
|
|
if (!area) return ''; |
|
|
|
if (typeof area === 'object') return area.id ? String(area.id) : ''; |
|
|
|
const areaInfo = JSON.parse(area); |
|
|
|
if (!area) return null; |
|
|
|
return typeof area === 'object' ? area : JSON.parse(area); |
|
|
|
} catch (e) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
}, |
|
|
|
getCurrentRegionId() { |
|
|
|
try { |
|
|
|
const areaInfo = this.getCurrentArea(); |
|
|
|
return areaInfo.id ? String(areaInfo.id) : ''; |
|
|
|
} catch (e) { |
|
|
|
return ''; |
|
|
|
@ -136,6 +176,8 @@ |
|
|
|
regionId: item.regionId || this.getCurrentRegionId(), |
|
|
|
areaId: item.areaId || '', |
|
|
|
areaName: item.areaName || '', |
|
|
|
addressLng: item.addressLng || '', |
|
|
|
addressLat: item.addressLat || '', |
|
|
|
floor: item.floor || '', |
|
|
|
roomNum: item.roomNum || '', |
|
|
|
receiverName: item.receiverName || '', |
|
|
|
@ -143,7 +185,7 @@ |
|
|
|
isDefault: item.isDefault || 0 |
|
|
|
}; |
|
|
|
this.areaTitleInput = item.areaName || ''; |
|
|
|
this.syncAreaTitle(); |
|
|
|
if (!this.isSocialArea) this.syncAreaTitle(); |
|
|
|
}, |
|
|
|
getAreaList() { |
|
|
|
if (this.areaListLoading) return Promise.resolve(this.areaList); |
|
|
|
@ -166,6 +208,7 @@ |
|
|
|
if (match) this.areaTitleInput = match.title; |
|
|
|
}, |
|
|
|
openAreaPopup() { |
|
|
|
if (this.isSocialArea) return; |
|
|
|
this.areaSearchInput = ''; |
|
|
|
this.getAreaList(); |
|
|
|
this.$refs.areaPopup.open(); |
|
|
|
@ -180,6 +223,220 @@ |
|
|
|
this.areaTitleInput = item.title; |
|
|
|
this.closeAreaPopup(); |
|
|
|
}, |
|
|
|
openLocationPicker() { |
|
|
|
const openPicker = (option) => { |
|
|
|
this.ensurePrivacyAuthorization().then(() => { |
|
|
|
console.log('chooseLocation 入参:', option || {}); |
|
|
|
uni.chooseLocation({ |
|
|
|
...(option || {}), |
|
|
|
success: (res) => { |
|
|
|
console.log('chooseLocation 成功:', res); |
|
|
|
const name = (res.name || '').trim(); |
|
|
|
const address = (res.address || '').trim(); |
|
|
|
if (!name && !address) { |
|
|
|
this.tui.toast('请选择有效地址'); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.applySelectedLocation({ |
|
|
|
name: address && name && address.indexOf(name) === -1 ? address + name : (name || address), |
|
|
|
latitude: res.latitude, |
|
|
|
longitude: res.longitude |
|
|
|
}); |
|
|
|
}, |
|
|
|
fail: (err) => { |
|
|
|
console.log('chooseLocation 失败:', err); |
|
|
|
const errMsg = err && err.errMsg ? String(err.errMsg) : ''; |
|
|
|
if (errMsg.indexOf('cancel') > -1) { |
|
|
|
return; |
|
|
|
} |
|
|
|
this.showLocationFailTip(err, '打开位置选择失败,可重新进入页面后再试'); |
|
|
|
} |
|
|
|
}); |
|
|
|
}).catch(() => {}); |
|
|
|
}; |
|
|
|
const lat = this.toLocationNumber(this.addressForm.addressLat); |
|
|
|
const lng = this.toLocationNumber(this.addressForm.addressLng); |
|
|
|
if (this.isValidLocation(lat, lng)) { |
|
|
|
openPicker({ |
|
|
|
latitude: lat, |
|
|
|
longitude: lng |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.ensurePrivacyAuthorization().then(() => this.getPickerCurrentLocation()).then((location) => { |
|
|
|
openPicker(location); |
|
|
|
}).catch(() => {}); |
|
|
|
}, |
|
|
|
applySelectedLocation(location) { |
|
|
|
if (!location) return; |
|
|
|
this.validateSocialAddressLocation(location.latitude, location.longitude).then(() => { |
|
|
|
this.addressForm.areaId = ''; |
|
|
|
this.addressForm.areaName = location.name || '当前位置'; |
|
|
|
this.addressForm.addressLng = location.longitude; |
|
|
|
this.addressForm.addressLat = location.latitude; |
|
|
|
this.areaTitleInput = this.addressForm.areaName; |
|
|
|
}).catch(() => {}); |
|
|
|
}, |
|
|
|
toLocationNumber(value) { |
|
|
|
if (value === '' || value === null || value === undefined) return null; |
|
|
|
const numberValue = Number(value); |
|
|
|
return Number.isFinite(numberValue) ? numberValue : null; |
|
|
|
}, |
|
|
|
isValidLocation(latitude, longitude) { |
|
|
|
const lat = this.toLocationNumber(latitude); |
|
|
|
const lng = this.toLocationNumber(longitude); |
|
|
|
return lat !== null && lng !== null && |
|
|
|
lat >= -90 && lat <= 90 && |
|
|
|
lng >= -180 && lng <= 180 && |
|
|
|
!(lat === 0 && lng === 0); |
|
|
|
}, |
|
|
|
ensurePrivacyAuthorization() { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
// #ifdef MP-WEIXIN |
|
|
|
if (typeof wx !== 'undefined' && wx.getPrivacySetting) { |
|
|
|
wx.getPrivacySetting({ |
|
|
|
success: (res) => { |
|
|
|
console.log('getPrivacySetting:', res); |
|
|
|
if (!res || !res.needAuthorization) { |
|
|
|
resolve(); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.privacyContractName = res.privacyContractName || '《用户隐私保护指引》'; |
|
|
|
this.pendingPrivacyResolve = resolve; |
|
|
|
this.pendingPrivacyReject = reject; |
|
|
|
this.showPrivacyModal = true; |
|
|
|
}, |
|
|
|
fail: (err) => { |
|
|
|
console.log('getPrivacySetting 失败:', err); |
|
|
|
resolve(); |
|
|
|
} |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
// #endif |
|
|
|
resolve(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
openPrivacyContract() { |
|
|
|
// #ifdef MP-WEIXIN |
|
|
|
if (typeof wx !== 'undefined' && wx.openPrivacyContract) { |
|
|
|
wx.openPrivacyContract({ |
|
|
|
fail: () => { |
|
|
|
this.tui.toast('打开隐私协议失败'); |
|
|
|
} |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
// #endif |
|
|
|
this.tui.toast('当前微信版本暂不支持查看'); |
|
|
|
}, |
|
|
|
handleAgreePrivacyAuthorization() { |
|
|
|
this.showPrivacyModal = false; |
|
|
|
const resolve = this.pendingPrivacyResolve; |
|
|
|
this.pendingPrivacyResolve = null; |
|
|
|
this.pendingPrivacyReject = null; |
|
|
|
if (typeof resolve === 'function') resolve(); |
|
|
|
}, |
|
|
|
rejectPrivacyAuthorization() { |
|
|
|
this.showPrivacyModal = false; |
|
|
|
const reject = this.pendingPrivacyReject; |
|
|
|
this.pendingPrivacyResolve = null; |
|
|
|
this.pendingPrivacyReject = null; |
|
|
|
this.tui.toast('请先同意隐私协议后再定位'); |
|
|
|
if (typeof reject === 'function') reject(); |
|
|
|
}, |
|
|
|
getPickerCurrentLocation() { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
uni.getLocation({ |
|
|
|
type: 'gcj02', |
|
|
|
isHighAccuracy: true, |
|
|
|
highAccuracyExpireTime: 3000, |
|
|
|
success: (res) => { |
|
|
|
console.log('getLocation 成功:', res); |
|
|
|
if (this.isValidLocation(res.latitude, res.longitude)) { |
|
|
|
resolve({ |
|
|
|
latitude: res.latitude, |
|
|
|
longitude: res.longitude |
|
|
|
}); |
|
|
|
} else { |
|
|
|
console.log('getLocation 返回无效坐标:', res); |
|
|
|
resolve({}); |
|
|
|
} |
|
|
|
}, |
|
|
|
fail: (err) => { |
|
|
|
console.log('getLocation 失败:', err); |
|
|
|
const errMsg = (err && err.errMsg) || ''; |
|
|
|
const denied = errMsg.indexOf('auth deny') > -1 || |
|
|
|
errMsg.indexOf('authorize') > -1 || |
|
|
|
errMsg.indexOf('denied') > -1 || |
|
|
|
errMsg.indexOf('permission') > -1; |
|
|
|
if (!denied) { |
|
|
|
this.showLocationFailTip(err, '定位失败,可手动搜索地址'); |
|
|
|
resolve({}); |
|
|
|
return; |
|
|
|
} |
|
|
|
uni.showModal({ |
|
|
|
title: '需要定位权限', |
|
|
|
content: '请选择允许定位后再选择收货地址', |
|
|
|
confirmText: '去设置', |
|
|
|
success: (modalRes) => { |
|
|
|
if (modalRes.confirm) { |
|
|
|
uni.openSetting({ |
|
|
|
complete: () => reject(err) |
|
|
|
}); |
|
|
|
} else { |
|
|
|
reject(err); |
|
|
|
} |
|
|
|
}, |
|
|
|
fail: () => reject(err) |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, |
|
|
|
showLocationFailTip(err, fallbackText) { |
|
|
|
const errMsg = err && err.errMsg ? String(err.errMsg) : ''; |
|
|
|
if (errMsg) { |
|
|
|
console.log('location fail:', errMsg); |
|
|
|
} |
|
|
|
if (errMsg.indexOf('privacy') > -1) { |
|
|
|
this.tui.toast('请先同意隐私协议后再定位'); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.tui.toast(fallbackText || '定位失败,请重试'); |
|
|
|
}, |
|
|
|
validateSocialAddressLocation(lat, lng) { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
const currentRegionId = this.getCurrentRegionId(); |
|
|
|
const latitude = this.toLocationNumber(lat); |
|
|
|
const longitude = this.toLocationNumber(lng); |
|
|
|
if (!currentRegionId) { |
|
|
|
this.tui.toast('请先选择区域'); |
|
|
|
reject(); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!this.isValidLocation(latitude, longitude)) { |
|
|
|
this.tui.toast('地址坐标不合法,请重新选择'); |
|
|
|
reject(); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.tui.request('/app/shopArea/matchByLocation', 'GET', { |
|
|
|
lat: latitude, |
|
|
|
lng: longitude |
|
|
|
}, false, true).then((res) => { |
|
|
|
const matched = res && res.code == 200 && res.result && res.result.id; |
|
|
|
if (matched && String(res.result.id) === String(currentRegionId)) { |
|
|
|
resolve(); |
|
|
|
} else { |
|
|
|
this.tui.toast('该地址不在当前区域配送范围内'); |
|
|
|
reject(); |
|
|
|
} |
|
|
|
}).catch(() => { |
|
|
|
this.tui.toast('地址范围校验失败,请稍后重试'); |
|
|
|
reject(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, |
|
|
|
onFloorInput(e) { |
|
|
|
const value = (e.detail.value || '').toString(); |
|
|
|
const hasMinus = value.charAt(0) === '-'; |
|
|
|
@ -205,13 +462,20 @@ |
|
|
|
}, |
|
|
|
submitAddress() { |
|
|
|
if (this.addressSubmitting) return; |
|
|
|
if (!this.addressForm.areaId) return this.tui.toast('请选择楼座区域'); |
|
|
|
const isSocial = this.isSocialArea; |
|
|
|
if (!isSocial && !this.addressForm.areaId) return this.tui.toast('请选择楼座区域'); |
|
|
|
if (isSocial && !this.addressForm.areaName) return this.tui.toast('请选择收货地址'); |
|
|
|
if (isSocial && (!this.addressForm.addressLng || !this.addressForm.addressLat)) return this.tui.toast('请选择带定位的收货地址'); |
|
|
|
this.addressForm.floor = (this.addressForm.floor || '').toString(); |
|
|
|
this.addressForm.roomNum = (this.addressForm.roomNum || '').toString().trim(); |
|
|
|
this.addressForm.receiverName = (this.addressForm.receiverName || '').toString().trim(); |
|
|
|
this.addressForm.receiverPhone = (this.addressForm.receiverPhone || '').toString().replace(/\D/g, ''); |
|
|
|
if (!this.addressForm.floor) return this.tui.toast('请填写所在楼层,楼层必须为整数数字'); |
|
|
|
if (!/^-?\d+$/.test(this.addressForm.floor)) return this.tui.toast('楼层必须为整数'); |
|
|
|
if (!isSocial && !this.addressForm.floor) return this.tui.toast('请填写所在楼层,楼层必须为整数数字'); |
|
|
|
if (!isSocial && !/^-?\d+$/.test(this.addressForm.floor)) return this.tui.toast('楼层必须为整数'); |
|
|
|
if (isSocial) { |
|
|
|
this.addressForm.floor = ''; |
|
|
|
this.addressForm.areaId = ''; |
|
|
|
} |
|
|
|
if (!this.addressForm.roomNum) return this.tui.toast('请填写详细地址门牌号最多30字'); |
|
|
|
if (this.addressForm.roomNum.length > 30) return this.tui.toast('详细地址最多30字'); |
|
|
|
if (!this.addressForm.receiverName) return this.tui.toast('请填写收件人最多20字'); |
|
|
|
@ -221,6 +485,15 @@ |
|
|
|
this.addressForm.userId = uni.getStorageSync('id'); |
|
|
|
this.addressForm.regionId = this.getCurrentRegionId(); |
|
|
|
if (!this.addressForm.regionId) return this.tui.toast('请先选择区域'); |
|
|
|
if (isSocial) { |
|
|
|
this.validateSocialAddressLocation(this.addressForm.addressLat, this.addressForm.addressLng) |
|
|
|
.then(() => this.doSubmitAddress()) |
|
|
|
.catch(() => {}); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.doSubmitAddress(); |
|
|
|
}, |
|
|
|
doSubmitAddress() { |
|
|
|
const url = this.addressForm.id ? "/app/userAddress/edit" : "/app/userAddress/save"; |
|
|
|
this.addressSubmitting = true; |
|
|
|
this.tui.request(url, "POST", this.addressForm, false, true).then((res) => { |
|
|
|
@ -377,4 +650,74 @@ |
|
|
|
color: #999; |
|
|
|
text-align: center; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-mask { |
|
|
|
position: fixed; |
|
|
|
left: 0; |
|
|
|
right: 0; |
|
|
|
top: 0; |
|
|
|
bottom: 0; |
|
|
|
z-index: 9999; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
padding: 0 48rpx; |
|
|
|
background: rgba(0, 0, 0, 0.45); |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-dialog { |
|
|
|
width: 100%; |
|
|
|
padding: 42rpx 34rpx 30rpx; |
|
|
|
border-radius: 24rpx; |
|
|
|
background: #fff; |
|
|
|
box-sizing: border-box; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-title { |
|
|
|
font-size: 34rpx; |
|
|
|
font-weight: 700; |
|
|
|
color: #222; |
|
|
|
text-align: center; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-content { |
|
|
|
margin-top: 28rpx; |
|
|
|
font-size: 28rpx; |
|
|
|
line-height: 1.7; |
|
|
|
color: #555; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-link { |
|
|
|
color: #10b981; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-actions { |
|
|
|
display: flex; |
|
|
|
gap: 20rpx; |
|
|
|
margin-top: 36rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-btn { |
|
|
|
flex: 1; |
|
|
|
height: 80rpx; |
|
|
|
line-height: 80rpx; |
|
|
|
margin: 0; |
|
|
|
padding: 0; |
|
|
|
border-radius: 40rpx; |
|
|
|
font-size: 28rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-btn::after { |
|
|
|
border: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-btn--cancel { |
|
|
|
color: #666; |
|
|
|
background: #f5f5f5; |
|
|
|
} |
|
|
|
|
|
|
|
.privacy-btn--agree { |
|
|
|
color: #fff; |
|
|
|
background: #10b981; |
|
|
|
} |
|
|
|
</style> |