2745 lines
72 KiB
Vue
2745 lines
72 KiB
Vue
<template>
|
||
<view class="search-address-page">
|
||
<!-- 自定义导航栏 -->
|
||
<custom-navbar title="搜索地址" :showBack="true" backgroundColor="#FFFFFF"></custom-navbar>
|
||
|
||
<!-- 城市选择-->
|
||
<view class="search-container" v-if="isSelectCity">
|
||
<text class="search-container-text" @click="goSelectCity">{{ city.name }}</text>
|
||
<image src="https://mrrplus.oss-cn-beijing.aliyuncs.com/wxstaic/images/Fill.jpg" mode="aspectFit"
|
||
class="text-img"></image>
|
||
</view>
|
||
|
||
<!-- 地图容器 -->
|
||
<view class="map-container" :style="{ height: mapHeight + 'rpx' }">
|
||
<map v-if="mapVisible" :key="mapRenderKey" id="map" class="map" :latitude="latitude" :longitude="longitude"
|
||
scale="19" @tap="onMapTap" @regionchange="changeMap"></map>
|
||
|
||
<image v-if="showMapSnapshot" class="map-snapshot" :src="mapSnapshotUrl" mode="aspectFill"
|
||
@load="onSnapshotLoad" @error="onSnapshotError"></image>
|
||
|
||
<cover-view v-if="mapVisible" class="center-marker">
|
||
<cover-image
|
||
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/04007cab-3047-42b0-baab-44df344a92d2"
|
||
class="center-marker-img"></cover-image>
|
||
</cover-view>
|
||
</view>
|
||
|
||
<!-- 底部编辑区域 -->
|
||
<view class="bottom-edit-section">
|
||
<!-- 地址识别输入行 -->
|
||
<view class="address-input-row">
|
||
<textarea class="raw-address-input" v-model="rawAddress" placeholder="粘贴文本到此处,将自动识别地址信息。"
|
||
placeholder-style="color: #C6C9CC; font-size: 14px;" :maxlength="200" :adjust-position="true" />
|
||
<view class="action-buttons">
|
||
<view class="action-btn paste-btn" @click="hasRawContent ? clearRawAddress() : onPaste()">
|
||
<text class="btn-text" :style="{ color: '#333333' }">{{ hasRawContent ? '一键清空' : '粘贴' }}</text>
|
||
</view>
|
||
<view class="action-btn smart-btn" :class="{ 'smart-btn-active': hasRawContent }"
|
||
@click="onSmartRecognize">
|
||
<text class="btn-text"
|
||
:style="{ color: hasRawContent ? '#FF4767' : '#999999' }">{{ smartBtnText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 地址信息栏 -->
|
||
<view class="address-info-panel">
|
||
<view class="info-row" @click="selectRegion">
|
||
<text class="row-label">所在地区</text>
|
||
<view class="row-content">
|
||
<text :class="currentRegion ? 'content-text' : 'placeholder'">{{ currentRegion || "请选择"}}</text>
|
||
<image
|
||
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/71c63001-a104-49ad-a90d-30d2ade73f4d.png"
|
||
mode="aspectFit" class="arrow-icon"></image>
|
||
</view>
|
||
</view>
|
||
<view class="line"></view>
|
||
<view class="info-row">
|
||
<text class="row-label">详细地址</text>
|
||
<input type="text" class="detail-input" v-model="detailAddress" @input="onDetailInput"
|
||
placeholder="请填写详细地址" placeholder-style="color: #999999; font-size: 14px;"
|
||
:adjust-position="true" />
|
||
</view>
|
||
<view class="line"></view>
|
||
</view>
|
||
|
||
<!-- 确认按钮 -->
|
||
<view class="fixed-confirm-bar">
|
||
<view class="confirm-btn" @click="onConfirm">
|
||
<text class="con-btn-text">确认</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 地区选择弹窗(用户端) -->
|
||
<view class="popup" :class="{ show: showRegionPicker }">
|
||
<view class="popup-mask" @click="closeRegionPicker"></view>
|
||
<view class="popup-content">
|
||
<view class="popup-header">
|
||
<text class="popup-title">选择所在地区</text>
|
||
<text class="popup-close" @click="closeRegionPicker">×</text>
|
||
</view>
|
||
<view class="region-body">
|
||
<picker-view @change="onRegionChange" class="picker-scroll" :indicator-style="indicatorStyle"
|
||
:value="regionPickerValue">
|
||
<picker-view-column>
|
||
<view class="picker-item" v-for="(item, index) in regionProvinceList" :key="index">
|
||
{{ item.province }}</view>
|
||
</picker-view-column>
|
||
<picker-view-column>
|
||
<view class="picker-item" v-for="(item, index) in regionCityList" :key="index">
|
||
{{ item.city }}</view>
|
||
</picker-view-column>
|
||
<picker-view-column>
|
||
<view class="picker-item" v-for="(item, index) in regionDistrictList" :key="index">
|
||
{{ item.area }}</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
</view>
|
||
<view class="popup-footer">
|
||
<view class="popup-btn cancel" @click="closeRegionPicker">
|
||
<text class="btn-text">取消</text>
|
||
</view>
|
||
<view class="popup-btn confirm" @click="confirmRegion">
|
||
<text class="btn-text">确定</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import request from '@/utils/request'
|
||
import ChinaCitys from './data/ChinaCitys.json'
|
||
import store from '@/store'
|
||
import {
|
||
setVersion
|
||
} from '@/utils/version.js'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
useApiRawRecognize: false,
|
||
mapVisible: true,
|
||
showMapSnapshot: false,
|
||
mapSnapshotUrl: '',
|
||
mapRenderKey: 0,
|
||
mapMoveTimer: null,
|
||
mapProgrammaticUntil: 0,
|
||
mapGuardTimer: null,
|
||
mapReverseTimer: null,
|
||
popupRestoreTimer: null,
|
||
reverseGeocodeRequestId: 0,
|
||
dependencyInitLock: false,
|
||
dependencyInitTimer: null,
|
||
blankPageInitLock: false,
|
||
|
||
// 地图与定位相关
|
||
systemInfo: {},
|
||
latitude: 0,
|
||
longitude: 0,
|
||
newLatitude: 0,
|
||
newLongitude: 0,
|
||
mapHeight: 670,
|
||
city: {
|
||
center: '',
|
||
name: ''
|
||
},
|
||
isSelectCity: false,
|
||
searchCity: uni.getStorageSync('searchCity'),
|
||
source: '',
|
||
recognizeMapMoving: false,
|
||
recognizeMapMoveTimer: null,
|
||
|
||
// 编辑区数据
|
||
manualDetail: '',
|
||
isManualDetail: false,
|
||
currentProvince: '',
|
||
currentCity: '',
|
||
currentDistrict: '',
|
||
currentDetail: '',
|
||
selectedProvinceCode: '',
|
||
selectedCityCode: '',
|
||
selectedDistrictCode: '',
|
||
rawAddress: '',
|
||
hasRawContent: false,
|
||
smartBtnText: '智能识别',
|
||
hasDetailContent: false,
|
||
currentRegion: '',
|
||
detailAddress: '',
|
||
|
||
// 首次进入且上页没有详细地址时,禁止自动回填详细地址
|
||
suppressInitDetailFill: false,
|
||
ignoreRegionChangeOnce: false,
|
||
|
||
// 地区选择弹窗数据
|
||
showRegionPicker: false,
|
||
regionProvinceList: [],
|
||
regionCityList: [],
|
||
regionDistrictList: [],
|
||
regionPickerValue: [0, 0, 0],
|
||
|
||
indicatorStyle: 'height: 68rpx;',
|
||
userSelectedRegion: false,
|
||
|
||
// 高德地图 key
|
||
amapKey: '30b7eb1a1b2f88edc085b9b3ee9a2188'
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
rawAddress(newVal) {
|
||
this.hasRawContent = (newVal || '').trim().length > 0
|
||
this.smartBtnText = '智能识别'
|
||
},
|
||
detailAddress(newVal) {
|
||
this.hasDetailContent = (newVal || '').trim().length > 0
|
||
}
|
||
},
|
||
|
||
onLoad(options) {
|
||
console.log('ChinaCitys 数据:', ChinaCitys)
|
||
console.log('ChinaCitys 长度:', ChinaCitys?.length)
|
||
|
||
this.systemInfo = uni.getSystemInfoSync()
|
||
this.source = options.source || ''
|
||
this.mapHeight = 670
|
||
|
||
const incomingAddress = options.address ? decodeURIComponent(options.address) : ''
|
||
const hasDependency = !!options.dependency
|
||
const hasLocation = !!(options.longitude && options.latitude)
|
||
const skipAutoLocate = String(options.skip_auto_locate || '') === '1'
|
||
const initCurrentLocation = String(options.init_current_location || '') === '1'
|
||
const silentLocate = String(options.silent_locate || '') === '1'
|
||
|
||
// 1. 有传入地区:优先按传入地区初始化,禁止首屏地图自动反写
|
||
if (hasDependency) {
|
||
const regions = decodeURIComponent(options.dependency).split('-')
|
||
console.log('传入地区信息:', regions)
|
||
|
||
this.currentProvince = regions[0] || ''
|
||
this.currentCity = regions[1] || ''
|
||
this.currentDistrict = regions[2] || ''
|
||
|
||
let regionText = ''
|
||
if (this.currentProvince) regionText += this.currentProvince
|
||
if (this.currentCity && this.currentCity !== this.currentProvince) regionText += this.currentCity
|
||
if (this.currentDistrict) regionText += this.currentDistrict
|
||
this.currentRegion = regionText
|
||
|
||
if (incomingAddress) {
|
||
this.detailAddress = incomingAddress
|
||
this.manualDetail = incomingAddress
|
||
this.currentDetail = incomingAddress
|
||
this.isManualDetail = true
|
||
this.suppressInitDetailFill = false
|
||
} else {
|
||
this.detailAddress = ''
|
||
this.manualDetail = ''
|
||
this.currentDetail = ''
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = true
|
||
}
|
||
|
||
this.startDependencyInitLock()
|
||
if (hasLocation) {
|
||
|
||
this.applyMapLocation(options.longitude, options.latitude, false)
|
||
return
|
||
}
|
||
|
||
const dependencyCode = options.dependency_code
|
||
|
||
;
|
||
(async () => {
|
||
let center = null
|
||
|
||
if (dependencyCode && regions[2]) {
|
||
center = await this.getDistrictCenter(regions[2], dependencyCode)
|
||
}
|
||
|
||
if (!center && regions[2]) {
|
||
center = await this.getDistrictCenter(regions[2])
|
||
}
|
||
if (!center && regions[1] && regions[1] !== regions[0]) {
|
||
center = await this.getDistrictCenter(regions[1])
|
||
}
|
||
if (!center && regions[0]) {
|
||
center = await this.getDistrictCenter(regions[0])
|
||
}
|
||
|
||
if (center) {
|
||
const locationRes = center.split(',')
|
||
if (locationRes.length >= 2) {
|
||
this.updateMapCenter(
|
||
parseFloat(locationRes[0]),
|
||
parseFloat(locationRes[1]),
|
||
false
|
||
)
|
||
}
|
||
} else if (!skipAutoLocate) {
|
||
this.clearDependencyInitLock()
|
||
this.getCurrentLocation()
|
||
} else {
|
||
this.clearDependencyInitLock()
|
||
}
|
||
})()
|
||
|
||
return
|
||
}
|
||
|
||
// 2. 只有经纬度,没有地区
|
||
if (hasLocation) {
|
||
this.applyMapLocation(options.longitude, options.latitude, true)
|
||
|
||
if (incomingAddress) {
|
||
this.detailAddress = incomingAddress
|
||
this.manualDetail = incomingAddress
|
||
this.currentDetail = incomingAddress
|
||
this.isManualDetail = true
|
||
this.suppressInitDetailFill = false
|
||
} else {
|
||
this.detailAddress = ''
|
||
this.manualDetail = ''
|
||
this.currentDetail = ''
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = true
|
||
}
|
||
|
||
|
||
this.reverseGeocode(parseFloat(options.longitude), parseFloat(options.latitude))
|
||
return
|
||
}
|
||
|
||
// 3. 从城市选择页进入
|
||
if (options.city) {
|
||
const city = JSON.parse(decodeURIComponent(options.city))
|
||
this.city = city
|
||
this.ignoreRegionChangeOnce = true
|
||
this.getNowLocation(city.center)
|
||
this.isSelectCity = true
|
||
return
|
||
}
|
||
|
||
// 4. 仅标记城市选择模式
|
||
if (options.isSelectCity) {
|
||
this.isSelectCity = true
|
||
this.getCurrentLocation()
|
||
return
|
||
}
|
||
|
||
// 5. 手艺人信息页首次进入且详细地址为空:按当前定位初始化
|
||
if (initCurrentLocation) {
|
||
this.blankPageInitLock = false
|
||
this.getCurrentLocation(silentLocate, true)
|
||
return
|
||
}
|
||
|
||
// 6. 空白进入且明确要求不自动定位
|
||
if (skipAutoLocate && !hasDependency && !hasLocation) {
|
||
this.blankPageInitLock = true
|
||
this.currentProvince = ''
|
||
this.currentCity = ''
|
||
this.currentDistrict = ''
|
||
this.currentRegion = ''
|
||
this.detailAddress = ''
|
||
this.manualDetail = ''
|
||
this.currentDetail = ''
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = true
|
||
return
|
||
}
|
||
|
||
// 7. 默认当前位置
|
||
if (!skipAutoLocate) {
|
||
this.blankPageInitLock = false
|
||
this.getCurrentLocation(silentLocate, false)
|
||
}
|
||
},
|
||
|
||
onShow() {
|
||
if (!this.isSelectCity) return
|
||
|
||
const searchCity = uni.getStorageSync('searchCity')
|
||
if (searchCity && this.city.name !== searchCity.name) {
|
||
this.city = searchCity
|
||
this.searchCity = searchCity
|
||
this.ignoreRegionChangeOnce = true
|
||
this.getNowLocation(searchCity.center)
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
normalizeMunicipalityName(name) {
|
||
const value = String(name || '').trim()
|
||
|
||
const map = {
|
||
'北京': '北京市',
|
||
'北京市': '北京市',
|
||
'上海': '上海市',
|
||
'上海市': '上海市',
|
||
'天津': '天津市',
|
||
'天津市': '天津市',
|
||
'重庆': '重庆市',
|
||
'重庆市': '重庆市',
|
||
'香港': '香港特别行政区',
|
||
'香港特别行政区': '香港特别行政区',
|
||
'澳门': '澳门特别行政区',
|
||
'澳门特别行政区': '澳门特别行政区'
|
||
}
|
||
|
||
return map[value] || value
|
||
},
|
||
|
||
normalizeRecognizedRegionParts(province, city, district) {
|
||
let p = this.normalizeMunicipalityName(province)
|
||
let c = this.normalizeMunicipalityName(city)
|
||
const d = String(district || '').trim()
|
||
|
||
const municipalities = [
|
||
'北京市',
|
||
'上海市',
|
||
'天津市',
|
||
'重庆市',
|
||
'香港特别行政区',
|
||
'澳门特别行政区'
|
||
]
|
||
|
||
if (!p && municipalities.includes(c)) {
|
||
p = c
|
||
}
|
||
|
||
if (!c && municipalities.includes(p)) {
|
||
c = p
|
||
}
|
||
|
||
return {
|
||
province: p,
|
||
city: c,
|
||
district: d
|
||
}
|
||
},
|
||
|
||
buildRegionText(province, city, district) {
|
||
let text = ''
|
||
if (province) text += province
|
||
if (city && city !== province) text += city
|
||
if (district) text += district
|
||
return text
|
||
},
|
||
|
||
startDependencyInitLock(duration = 800) {
|
||
this.dependencyInitLock = true
|
||
|
||
if (this.dependencyInitTimer) {
|
||
clearTimeout(this.dependencyInitTimer)
|
||
}
|
||
|
||
this.dependencyInitTimer = setTimeout(() => {
|
||
this.dependencyInitLock = false
|
||
this.dependencyInitTimer = null
|
||
}, duration)
|
||
},
|
||
|
||
clearDependencyInitLock() {
|
||
this.dependencyInitLock = false
|
||
|
||
if (this.dependencyInitTimer) {
|
||
clearTimeout(this.dependencyInitTimer)
|
||
this.dependencyInitTimer = null
|
||
}
|
||
},
|
||
|
||
clearBlankPageInitLock() {
|
||
this.blankPageInitLock = false
|
||
},
|
||
|
||
isUserGestureMapEvent(causedBy) {
|
||
return ['gesture', 'drag', 'scale'].includes(String(causedBy || '').toLowerCase())
|
||
},
|
||
|
||
applyMapLocation(longitude, latitude, noReverse = true) {
|
||
const lng = Number(longitude)
|
||
const lat = Number(latitude)
|
||
|
||
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
|
||
|
||
this.ignoreRegionChangeOnce = true
|
||
this.mapProgrammaticUntil = Date.now() + 280
|
||
|
||
this.longitude = lng
|
||
this.latitude = lat
|
||
this.newLongitude = lng
|
||
this.newLatitude = lat
|
||
|
||
if (this.mapGuardTimer) {
|
||
clearTimeout(this.mapGuardTimer)
|
||
}
|
||
this.mapGuardTimer = setTimeout(() => {
|
||
this.ignoreRegionChangeOnce = false
|
||
this.mapGuardTimer = null
|
||
}, 320)
|
||
|
||
if (this.mapReverseTimer) {
|
||
clearTimeout(this.mapReverseTimer)
|
||
this.mapReverseTimer = null
|
||
}
|
||
|
||
|
||
if (!noReverse) {
|
||
this.mapReverseTimer = setTimeout(() => {
|
||
this.reverseGeocode(lng, lat)
|
||
this.mapReverseTimer = null
|
||
}, 220)
|
||
}
|
||
},
|
||
|
||
normalizeCityName(province, city) {
|
||
let cityName = ''
|
||
|
||
if (Array.isArray(city)) {
|
||
cityName = city[0] || ''
|
||
} else {
|
||
cityName = city || ''
|
||
}
|
||
|
||
const fallbackCities = [
|
||
'北京市',
|
||
'上海市',
|
||
'天津市',
|
||
'重庆市',
|
||
'香港特别行政区',
|
||
'澳门特别行政区'
|
||
]
|
||
|
||
if (!cityName && fallbackCities.includes(province)) {
|
||
cityName = province
|
||
}
|
||
|
||
return cityName
|
||
},
|
||
|
||
buildMapSnapshotUrl() {
|
||
const lng = Number(this.longitude || this.newLongitude || 0).toFixed(6)
|
||
const lat = Number(this.latitude || this.newLatitude || 0).toFixed(6)
|
||
return `https://restapi.amap.com/v3/staticmap?location=${lng},${lat}&zoom=17&size=750*500&scale=2&markers=mid,0xE8101E,A:${lng},${lat}&key=${this.amapKey}`
|
||
},
|
||
|
||
onSnapshotLoad() {
|
||
console.log('地图加载成功:', this.mapSnapshotUrl)
|
||
},
|
||
|
||
onSnapshotError(e) {
|
||
console.log('地图加载失败:', e, this.mapSnapshotUrl)
|
||
uni.showToast({
|
||
title: '地图加载失败',
|
||
icon: 'none'
|
||
})
|
||
},
|
||
|
||
findRegionIndexByCode(baseData, provinceCode, cityCode, areaCode) {
|
||
const result = {
|
||
provinceIndex: -1,
|
||
cityIndex: -1,
|
||
areaIndex: -1
|
||
}
|
||
const targetProvinceCode = (provinceCode || '').slice(0, 6)
|
||
const targetCityCode = (cityCode || '').slice(0, 6)
|
||
const targetAreaCode = (areaCode || '').slice(0, 6)
|
||
if (!targetProvinceCode || !targetCityCode || !targetAreaCode) return result
|
||
|
||
for (let pIdx = 0; pIdx < baseData.length; pIdx++) {
|
||
const provinceItem = baseData[pIdx]
|
||
const currentProvinceCode = (provinceItem.code || '').slice(0, 6)
|
||
if (currentProvinceCode === targetProvinceCode) {
|
||
result.provinceIndex = pIdx
|
||
const citys = provinceItem.citys || []
|
||
for (let cIdx = 0; cIdx < citys.length; cIdx++) {
|
||
const cityItem = citys[cIdx]
|
||
const currentCityCode = (cityItem.code || '').slice(0, 6)
|
||
if (currentCityCode === targetCityCode) {
|
||
result.cityIndex = cIdx
|
||
const areas = cityItem?.areas || []
|
||
for (let aIdx = 0; aIdx < areas.length; aIdx++) {
|
||
const areaItem = areas[aIdx]
|
||
const currentAreaCode = (areaItem.code || '').slice(0, 6)
|
||
if (currentAreaCode === targetAreaCode) {
|
||
result.areaIndex = aIdx
|
||
return result
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
}
|
||
return result
|
||
},
|
||
|
||
convert(item) {
|
||
const data = {
|
||
key: this.amapKey,
|
||
locations: `${item.longitude},${item.latitude}`,
|
||
coordsys: 'gps'
|
||
}
|
||
return new Promise((resolve) => {
|
||
uni.request({
|
||
url: 'https://restapi.amap.com/v3/assistant/coordinate/convert',
|
||
method: 'GET',
|
||
data,
|
||
success(res) {
|
||
resolve(res.data)
|
||
}
|
||
})
|
||
})
|
||
},
|
||
|
||
getLocation(silent = false) {
|
||
return new Promise((resolve, reject) => {
|
||
const handleFail = (err) => {
|
||
if (!silent) {
|
||
uni.showToast({
|
||
title: '获取位置失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
reject(err)
|
||
}
|
||
|
||
if (this.$store?.state?.version?.DeviceBrand === 'ios') {
|
||
uni.getLocation({
|
||
type: 'wgs84',
|
||
isHighAccuracy: true,
|
||
success: async (res) => {
|
||
try {
|
||
const convert = await this.convert(res)
|
||
const locations = String(convert?.locations || '').split(',')
|
||
|
||
if (locations.length >= 2) {
|
||
res.longitude = Number(locations[0])
|
||
res.latitude = Number(locations[1])
|
||
}
|
||
|
||
resolve(res)
|
||
} catch (error) {
|
||
handleFail(error)
|
||
}
|
||
},
|
||
fail: handleFail
|
||
})
|
||
} else {
|
||
uni.getLocation({
|
||
type: 'gcj02',
|
||
geocode: true,
|
||
success: (res) => resolve(res),
|
||
fail: handleFail
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
async getCurrentLocation(silent = false, lockInit = false) {
|
||
this.blankPageInitLock = false
|
||
|
||
try {
|
||
const locationRes = await this.getLocation(silent)
|
||
|
||
if (lockInit) {
|
||
this.startDependencyInitLock(1200)
|
||
this.userSelectedRegion = false
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = false
|
||
}
|
||
|
||
this.updateMapCenter(locationRes.longitude, locationRes.latitude, false)
|
||
} catch (error) {
|
||
if (silent) {
|
||
this.latitude = 0
|
||
this.longitude = 0
|
||
this.newLatitude = 0
|
||
this.newLongitude = 0
|
||
|
||
this.currentProvince = ''
|
||
this.currentCity = ''
|
||
this.currentDistrict = ''
|
||
this.currentRegion = ''
|
||
this.detailAddress = ''
|
||
this.manualDetail = ''
|
||
this.currentDetail = ''
|
||
this.isManualDetail = false
|
||
|
||
this.blankPageInitLock = true
|
||
this.suppressInitDetailFill = true
|
||
}
|
||
|
||
console.log('定位失败', error)
|
||
}
|
||
},
|
||
|
||
getNowLocation(center) {
|
||
if (!center) return
|
||
const locationRes = center.split(',')
|
||
if (locationRes.length < 2) return
|
||
|
||
this.updateMapCenter(
|
||
parseFloat(locationRes[0]),
|
||
parseFloat(locationRes[1]),
|
||
false
|
||
)
|
||
},
|
||
|
||
normalizeAdcode6(code) {
|
||
return String(code || '').slice(0, 6)
|
||
},
|
||
|
||
validateLocationWithDependency(longitude, latitude, dependencyCode) {
|
||
return new Promise((resolve) => {
|
||
const targetCode = this.normalizeAdcode6(dependencyCode)
|
||
|
||
if (!targetCode || !longitude || !latitude) {
|
||
resolve(true)
|
||
return
|
||
}
|
||
|
||
uni.request({
|
||
url: 'https://restapi.amap.com/v3/geocode/regeo',
|
||
method: 'GET',
|
||
data: {
|
||
key: this.amapKey,
|
||
location: `${longitude},${latitude}`,
|
||
extensions: 'base'
|
||
},
|
||
success: (res) => {
|
||
const adcode = this.normalizeAdcode6(
|
||
res?.data?.regeocode?.addressComponent?.adcode || ''
|
||
)
|
||
|
||
if (!adcode) {
|
||
resolve(true)
|
||
return
|
||
}
|
||
|
||
resolve(adcode === targetCode)
|
||
},
|
||
fail: () => {
|
||
resolve(true)
|
||
}
|
||
})
|
||
})
|
||
},
|
||
|
||
async locateByDependencyCenter(regions, dependencyCode, skipAutoLocate) {
|
||
let center = null
|
||
|
||
if (dependencyCode && regions[2]) {
|
||
center = await this.getDistrictCenter(regions[2], dependencyCode)
|
||
}
|
||
|
||
if (!center && regions[2]) {
|
||
center = await this.getDistrictCenter(regions[2])
|
||
}
|
||
if (!center && regions[1] && regions[1] !== regions[0]) {
|
||
center = await this.getDistrictCenter(regions[1])
|
||
}
|
||
if (!center && regions[0]) {
|
||
center = await this.getDistrictCenter(regions[0])
|
||
}
|
||
|
||
if (center) {
|
||
const locationRes = center.split(',')
|
||
if (locationRes.length >= 2) {
|
||
this.updateMapCenter(
|
||
parseFloat(locationRes[0]),
|
||
parseFloat(locationRes[1]),
|
||
true
|
||
)
|
||
}
|
||
} else if (!skipAutoLocate) {
|
||
this.getCurrentLocation()
|
||
}
|
||
},
|
||
|
||
reverseGeocode(lng, lat) {
|
||
const requestId = ++this.reverseGeocodeRequestId
|
||
const targetLng = parseFloat(lng)
|
||
const targetLat = parseFloat(lat)
|
||
|
||
if (!Number.isFinite(targetLng) || !Number.isFinite(targetLat)) return
|
||
|
||
uni.request({
|
||
url: 'https://restapi.amap.com/v3/geocode/regeo',
|
||
data: {
|
||
key: this.amapKey,
|
||
location: `${targetLng},${targetLat}`,
|
||
extensions: 'base'
|
||
},
|
||
success: (res) => {
|
||
if (requestId !== this.reverseGeocodeRequestId) return
|
||
|
||
if (res.data.status === '1' && res.data.regeocode) {
|
||
if (this.recognizeMapMoving) return
|
||
|
||
const regeo = res.data.regeocode
|
||
const formatted = regeo.formatted_address || ''
|
||
const addrComp = regeo.addressComponent || {}
|
||
|
||
const provinceName = addrComp.province || ''
|
||
const cityName = this.normalizeCityName(provinceName, addrComp.city)
|
||
const districtName = addrComp.district || ''
|
||
|
||
let region = ''
|
||
if (provinceName) region += provinceName
|
||
if (cityName && cityName !== provinceName) region += cityName
|
||
if (districtName) region += districtName
|
||
|
||
if (!this.userSelectedRegion) {
|
||
this.currentRegion = region || formatted
|
||
this.currentProvince = provinceName
|
||
this.currentCity = cityName
|
||
this.currentDistrict = districtName
|
||
|
||
const adcode = addrComp.adcode || ''
|
||
if (adcode.length >= 6) {
|
||
this.selectedDistrictCode = adcode
|
||
this.selectedCityCode = adcode.substring(0, 4) + '00'
|
||
this.selectedProvinceCode = adcode.substring(0, 2) + '0000'
|
||
}
|
||
}
|
||
|
||
let detail = ''
|
||
if (region && formatted.startsWith(region)) {
|
||
detail = formatted.substring(region.length).trim()
|
||
} else {
|
||
detail = formatted.trim()
|
||
}
|
||
|
||
if (this.suppressInitDetailFill) {
|
||
this.suppressInitDetailFill = false
|
||
return
|
||
}
|
||
|
||
if (!this.isManualDetail) {
|
||
this.detailAddress = detail || formatted
|
||
this.manualDetail = this.detailAddress
|
||
this.currentDetail = this.detailAddress
|
||
}
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
if (requestId !== this.reverseGeocodeRequestId) return
|
||
console.error('逆地理编码失败:', err)
|
||
}
|
||
})
|
||
},
|
||
|
||
onMapTap(e) {
|
||
this.clearDependencyInitLock()
|
||
this.clearBlankPageInitLock()
|
||
|
||
this.userSelectedRegion = false
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = false
|
||
|
||
const tapLat = Number(e?.detail?.latitude)
|
||
const tapLng = Number(e?.detail?.longitude)
|
||
|
||
if (Number.isFinite(tapLat) && Number.isFinite(tapLng)) {
|
||
this.latitude = tapLat
|
||
this.longitude = tapLng
|
||
this.newLatitude = tapLat
|
||
this.newLongitude = tapLng
|
||
this.reverseGeocode(tapLng, tapLat)
|
||
return
|
||
}
|
||
|
||
const mapCtx = uni.createMapContext('map', this)
|
||
mapCtx.getCenterLocation({
|
||
success: (res) => {
|
||
this.latitude = res.latitude
|
||
this.longitude = res.longitude
|
||
this.newLatitude = res.latitude
|
||
this.newLongitude = res.longitude
|
||
this.reverseGeocode(res.longitude, res.latitude)
|
||
},
|
||
fail: (err) => {
|
||
console.error('点击地图获取中心点失败', err)
|
||
}
|
||
})
|
||
},
|
||
|
||
syncCenterFromMap() {
|
||
if (this.dependencyInitLock) return
|
||
if (this.blankPageInitLock) return
|
||
|
||
const mapCtx = uni.createMapContext('map', this)
|
||
|
||
mapCtx.getCenterLocation({
|
||
success: (res) => {
|
||
if (
|
||
!res ||
|
||
!Number.isFinite(Number(res.longitude)) ||
|
||
!Number.isFinite(Number(res.latitude))
|
||
) {
|
||
return
|
||
}
|
||
|
||
this.userSelectedRegion = false
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = false
|
||
|
||
this.latitude = Number(res.latitude)
|
||
this.longitude = Number(res.longitude)
|
||
this.newLatitude = Number(res.latitude)
|
||
this.newLongitude = Number(res.longitude)
|
||
|
||
this.reverseGeocode(this.longitude, this.latitude)
|
||
},
|
||
fail: (err) => {
|
||
console.error('syncCenterFromMap失败', err)
|
||
}
|
||
})
|
||
},
|
||
|
||
changeMap(e) {
|
||
if (this.showRegionPicker || !this.mapVisible) return
|
||
if (this.recognizeMapMoving) return
|
||
if (this.dependencyInitLock) return
|
||
|
||
const eventType = e?.detail?.type || e?.type || ''
|
||
const causedBy = e?.detail?.causedBy || e?.causedBy || ''
|
||
|
||
if (this.blankPageInitLock) {
|
||
if (this.isUserGestureMapEvent(causedBy)) {
|
||
this.clearBlankPageInitLock()
|
||
} else {
|
||
return
|
||
}
|
||
}
|
||
|
||
if (eventType === 'begin') return
|
||
|
||
if (Date.now() < this.mapProgrammaticUntil) {
|
||
return
|
||
}
|
||
|
||
const isProgrammaticEvent = causedBy && causedBy !== 'gesture'
|
||
|
||
if (isProgrammaticEvent && this.ignoreRegionChangeOnce) {
|
||
this.ignoreRegionChangeOnce = false
|
||
return
|
||
}
|
||
|
||
if (this.mapMoveTimer) {
|
||
clearTimeout(this.mapMoveTimer)
|
||
}
|
||
|
||
const delay = eventType === 'end' ? 80 : 260
|
||
|
||
this.mapMoveTimer = setTimeout(() => {
|
||
this.syncCenterFromMap()
|
||
|
||
setTimeout(() => {
|
||
this.syncCenterFromMap()
|
||
}, 180)
|
||
|
||
this.mapMoveTimer = null
|
||
}, delay)
|
||
},
|
||
|
||
goSelectCity() {
|
||
uni.navigateTo({
|
||
url: '/subPackages/address/selectCity'
|
||
})
|
||
},
|
||
|
||
clearRawAddress() {
|
||
this.rawAddress = ''
|
||
uni.showToast({
|
||
title: '已清空',
|
||
icon: 'none'
|
||
})
|
||
},
|
||
|
||
onDetailInput(e) {
|
||
const val = e.detail.value || ''
|
||
this.detailAddress = val
|
||
this.manualDetail = val
|
||
this.currentDetail = val
|
||
this.isManualDetail = val.trim().length > 0
|
||
},
|
||
|
||
onPaste() {
|
||
uni.getClipboardData({
|
||
success: (res) => {
|
||
if (res.data) {
|
||
this.rawAddress = res.data
|
||
uni.showToast({
|
||
title: '已粘贴',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
sanitizeRawAddress(raw) {
|
||
let text = (raw || '')
|
||
.replace(/[\r\n\t]+/g, ' ')
|
||
.replace(/ /g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
|
||
const addressMatch = text.match(/地址[::]\s*(.*)/)
|
||
if (addressMatch && addressMatch[1]) {
|
||
text = addressMatch[1].trim()
|
||
}
|
||
|
||
text = text
|
||
.replace(/(收货人|联系人|姓名)[::]\s*[\u4e00-\u9fa5A-Za-z·]{1,20}/g, '')
|
||
.replace(/(电话|手机)[::]?\s*1[3-9]\d{9}/g, '')
|
||
.replace(/1[3-9]\d{9}/g, '')
|
||
.replace(/[,,;;]+/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
|
||
return text
|
||
},
|
||
|
||
escapeRegExp(str) {
|
||
return (str || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||
},
|
||
|
||
getNetworkTypeSafe() {
|
||
return new Promise((resolve) => {
|
||
uni.getNetworkType({
|
||
success: (res) => resolve(res.networkType || 'unknown'),
|
||
fail: () => resolve('unknown')
|
||
})
|
||
})
|
||
},
|
||
|
||
async ensureRecognizeNetworkAvailable() {
|
||
const networkType = await this.getNetworkTypeSafe()
|
||
|
||
if (networkType === 'none') {
|
||
this.showRecognizeNetworkError()
|
||
return false
|
||
}
|
||
|
||
return true
|
||
},
|
||
|
||
showRecognizeNetworkError() {
|
||
uni.showToast({
|
||
title: '网络异常,无法进行地址识别与地图定位,请检查网络后重试',
|
||
icon: 'none',
|
||
duration: 3500
|
||
})
|
||
},
|
||
|
||
isLikelyNetworkError(error) {
|
||
const msg = String(
|
||
error?.errMsg ||
|
||
error?.message ||
|
||
error ||
|
||
''
|
||
).toLowerCase()
|
||
|
||
return (
|
||
msg.includes('network') ||
|
||
msg.includes('timeout') ||
|
||
msg.includes('request:fail') ||
|
||
msg.includes('fail')
|
||
)
|
||
},
|
||
|
||
hasStrongAddressFeature(text) {
|
||
return /(\d|号|栋|幢|单元|室|楼|层|座|路|街|巷|道|村|镇|乡|里|弄|园|苑|小区|大厦|广场|市场|学校|医院|公寓|酒店|中心|胡同)/.test(text || '')
|
||
},
|
||
|
||
containsObviousInvalidNoise(text) {
|
||
return /(价格|价钱|多少钱|举报|投诉|建议|咨询|测试|示例|比如|比方|假设|无效|乱填)/i.test(text || '')
|
||
},
|
||
|
||
isLikelyInvalidRecognizedDetail(text) {
|
||
const value = String(text || '').trim()
|
||
if (!value) return true
|
||
|
||
if (/(价格|价钱|多少钱|举报|投诉|建议|咨询|测试|示例|比如|比方|假设|无效|乱填)/i.test(value)) {
|
||
return true
|
||
}
|
||
|
||
if (/^\d{6,}$/.test(value) || /^[A-Za-z]{6,}$/.test(value)) {
|
||
return true
|
||
}
|
||
|
||
const specialChars = (value.match(/[^\u4e00-\u9fa5a-zA-Z0-9]/g) || []).length
|
||
if (specialChars / value.length > 0.3) {
|
||
return true
|
||
}
|
||
|
||
if (value.length < 2) {
|
||
return true
|
||
}
|
||
|
||
const hasLongLetterChunk = /[A-Za-z]{6,}/.test(value)
|
||
const letterCount = (value.match(/[A-Za-z]/g) || []).length
|
||
const letterRatio = value.length ? (letterCount / value.length) : 0
|
||
const hasAddressFeature = this.hasStrongAddressFeature(value)
|
||
|
||
if (hasLongLetterChunk && !hasAddressFeature) {
|
||
return true
|
||
}
|
||
|
||
if (letterRatio > 0.35 && !hasAddressFeature) {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
},
|
||
|
||
isPreciseGeocodeLevel(level) {
|
||
return [
|
||
'门牌号',
|
||
'兴趣点',
|
||
'道路',
|
||
'道路交叉口',
|
||
'乡镇',
|
||
'村庄',
|
||
'村级地名',
|
||
'roadno',
|
||
'poi'
|
||
].includes(String(level || '').trim())
|
||
},
|
||
|
||
normalizeDistrictName(name) {
|
||
return String(name || '').trim()
|
||
},
|
||
|
||
validateRecognizedDetail(detailText, geo, hit = {}) {
|
||
const cleaned = String(detailText || '').trim()
|
||
if (!cleaned) {
|
||
return {
|
||
valid: false,
|
||
cleaned: '',
|
||
reason: 'empty'
|
||
}
|
||
}
|
||
|
||
if (this.isLikelyInvalidRecognizedDetail(cleaned)) {
|
||
return {
|
||
valid: false,
|
||
cleaned: '',
|
||
reason: 'invalid_detail'
|
||
}
|
||
}
|
||
|
||
if (!geo) {
|
||
return {
|
||
valid: false,
|
||
cleaned: '',
|
||
reason: 'geocode_failed'
|
||
}
|
||
}
|
||
|
||
const geoLevel = String(geo?.level || '').trim()
|
||
const preciseLevel = this.isPreciseGeocodeLevel(geoLevel)
|
||
const hasAddressFeature = this.hasStrongAddressFeature(cleaned)
|
||
|
||
const hitCountyCode = String(hit?.county_code || '').slice(0, 6)
|
||
const geoAdcode = String(geo?.adcode || '').slice(0, 6)
|
||
|
||
const hitDistrictName = this.normalizeDistrictName(hit?.county_name)
|
||
const geoDistrictName = this.normalizeDistrictName(geo?.district)
|
||
|
||
const sameDistrictByCode = !!hitCountyCode && !!geoAdcode && hitCountyCode === geoAdcode
|
||
const sameDistrictByName = !!hitDistrictName && !!geoDistrictName && hitDistrictName === geoDistrictName
|
||
const sameDistrict = sameDistrictByCode || sameDistrictByName
|
||
|
||
const purePoiLike =
|
||
cleaned.length >= 2 &&
|
||
/^[\u4e00-\u9fa5A-Za-z0-9]+$/.test(cleaned) &&
|
||
!/[A-Za-z]{6,}/.test(cleaned)
|
||
|
||
if (hasAddressFeature || preciseLevel) {
|
||
return {
|
||
valid: true,
|
||
cleaned,
|
||
reason: ''
|
||
}
|
||
}
|
||
|
||
if (sameDistrict && purePoiLike) {
|
||
return {
|
||
valid: true,
|
||
cleaned,
|
||
reason: 'district_geo_verified'
|
||
}
|
||
}
|
||
|
||
return {
|
||
valid: false,
|
||
cleaned: '',
|
||
reason: 'detail_not_precise'
|
||
}
|
||
},
|
||
|
||
cleanRecognizedDetail(detail, province, city, district) {
|
||
let text = this.stripLeadingRegionParts(detail, [
|
||
province,
|
||
city,
|
||
district
|
||
])
|
||
|
||
text = text
|
||
.replace(/(收货人|联系人|姓名)[::].*$/g, '')
|
||
.replace(/(电话|手机)[::]?\s*1[3-9]\d{9}.*$/g, '')
|
||
.replace(/备注[::].*$/g, '')
|
||
.replace(/1[3-9]\d{9}/g, '')
|
||
.replace(/[((][^))]*[))]/g, '')
|
||
.replace(/^[,,;;、\s]+/, '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
|
||
return text
|
||
},
|
||
|
||
appendIfMissing(base, part) {
|
||
const src = String(base || '').trim()
|
||
const seg = String(part || '').trim()
|
||
|
||
if (!seg) return src
|
||
if (!src) return seg
|
||
if (src.includes(seg)) return src
|
||
|
||
return `${src}${seg}`
|
||
},
|
||
|
||
deriveCodesFromDivision(divcode = '') {
|
||
const code = String(divcode || '').trim()
|
||
if (!/^\d{12}$/.test(code)) {
|
||
return {
|
||
province_code: '',
|
||
city_code: '',
|
||
county_code: ''
|
||
}
|
||
}
|
||
|
||
return {
|
||
province_code: `${code.slice(0, 2)}0000`,
|
||
city_code: `${code.slice(0, 4)}00`,
|
||
county_code: code.slice(0, 6)
|
||
}
|
||
},
|
||
|
||
parseDivisionInfo(area = {}) {
|
||
const divisionInfo = area?.division_info || {}
|
||
const divisionName = String(divisionInfo.division_name || '').trim()
|
||
const divcode = String(divisionInfo.divcode || '').trim()
|
||
|
||
let parts = []
|
||
if (divisionName.includes(';')) {
|
||
parts = divisionName.split(';')
|
||
} else if (divisionName) {
|
||
parts = divisionName.split(/\s+/)
|
||
}
|
||
|
||
const derivedCodes = this.deriveCodesFromDivision(divcode)
|
||
|
||
return {
|
||
province_name: String(parts[0] || '').trim(),
|
||
city_name: String(parts[1] || '').trim(),
|
||
county_name: String(parts[2] || '').trim(),
|
||
town_name: String(parts[3] || '').trim(),
|
||
province_code: derivedCodes.province_code,
|
||
city_code: derivedCodes.city_code,
|
||
county_code: derivedCodes.county_code
|
||
}
|
||
},
|
||
|
||
parseLonlatInfo(lonlat = {}) {
|
||
const offline = lonlat?.offline_geocode || {}
|
||
|
||
return {
|
||
gcj02: String(offline.gcj02 || '').trim(),
|
||
wgs84: String(offline.wgs84 || '').trim(),
|
||
level: String(offline.level || '').trim()
|
||
}
|
||
},
|
||
|
||
parseStreetStd(streetStd = '') {
|
||
const parsed = {}
|
||
|
||
String(streetStd)
|
||
.split(/\t+/)
|
||
.forEach(item => {
|
||
const index = item.indexOf('=')
|
||
if (index === -1) return
|
||
|
||
const key = item.slice(0, index).trim().toLowerCase()
|
||
const value = item.slice(index + 1).trim()
|
||
|
||
if (key) {
|
||
parsed[key] = value
|
||
}
|
||
})
|
||
|
||
return {
|
||
province_name: (parsed.prov || parsed.province || '').trim(),
|
||
city_name: (parsed.city || '').trim(),
|
||
county_name: (parsed.district || parsed.county || parsed.area || '').trim(),
|
||
town_name: (parsed.town || parsed.street || '').trim(),
|
||
road_name: (parsed.road || '').trim(),
|
||
road_no: (parsed.roadno || parsed.road_no || '').trim(),
|
||
poi_name: (parsed.poi || parsed.poi_name || '').trim(),
|
||
house_no: (parsed.houseno || parsed.house_no || '').trim(),
|
||
room_no: (parsed.roomno || parsed.room_no || '').trim(),
|
||
floor_no: (parsed.floor || parsed.floor_no || '').trim(),
|
||
unit_no: (parsed.unit || parsed.unit_no || '').trim(),
|
||
building_no: (parsed.building || parsed.building_no || '').trim(),
|
||
block_no: (parsed.block || parsed.block_no || '').trim(),
|
||
detail: (parsed.detail || parsed.address || parsed.addr || '').trim(),
|
||
address: (parsed.address || parsed.detail || parsed.addr || '').trim(),
|
||
province_code: (parsed.province_code || parsed.prov_code || '').trim(),
|
||
city_code: (parsed.city_code || '').trim(),
|
||
county_code: (parsed.county_code || parsed.district_code || parsed.area_code || '').trim(),
|
||
_street_std_map: parsed
|
||
}
|
||
},
|
||
|
||
normalizeRecognizeHit(res) {
|
||
if (!res || (res.code !== 200 && res.code !== 0)) return null
|
||
|
||
// 旧格式1:data是数组
|
||
if (Array.isArray(res.data) && res.data.length) {
|
||
const item = res.data[0]
|
||
return {
|
||
province_name: (item.province_name || '').trim(),
|
||
city_name: (item.city_name || '').trim(),
|
||
county_name: (item.county_name || '').trim(),
|
||
town_name: (item.town_name || '').trim(),
|
||
road_name: '',
|
||
road_no: '',
|
||
poi_name: '',
|
||
house_no: '',
|
||
room_no: '',
|
||
floor_no: '',
|
||
unit_no: '',
|
||
building_no: '',
|
||
block_no: '',
|
||
detail: (item.detail || '').trim(),
|
||
address: (item.address || item.detail || '').trim(),
|
||
province_code: (item.province_code || '').trim(),
|
||
city_code: (item.city_code || '').trim(),
|
||
county_code: (item.county_code || '').trim(),
|
||
backend_gcj02: '',
|
||
backend_wgs84: '',
|
||
backend_level: '',
|
||
_street_std_map: {}
|
||
}
|
||
}
|
||
|
||
// 旧格式2:data.data是数组
|
||
if (Array.isArray(res?.data?.data) && res.data.data.length) {
|
||
const item = res.data.data[0]
|
||
return {
|
||
province_name: (item.province_name || '').trim(),
|
||
city_name: (item.city_name || '').trim(),
|
||
county_name: (item.county_name || '').trim(),
|
||
town_name: (item.town_name || '').trim(),
|
||
road_name: '',
|
||
road_no: '',
|
||
poi_name: '',
|
||
house_no: '',
|
||
room_no: '',
|
||
floor_no: '',
|
||
unit_no: '',
|
||
building_no: '',
|
||
block_no: '',
|
||
detail: (item.detail || '').trim(),
|
||
address: (item.address || item.detail || '').trim(),
|
||
province_code: (item.province_code || '').trim(),
|
||
city_code: (item.city_code || '').trim(),
|
||
county_code: (item.county_code || '').trim(),
|
||
backend_gcj02: '',
|
||
backend_wgs84: '',
|
||
backend_level: '',
|
||
_street_std_map: {}
|
||
}
|
||
}
|
||
|
||
// 旧格式3:result是数组
|
||
if (Array.isArray(res.result) && res.result.length) {
|
||
const item = res.result[0]
|
||
return {
|
||
province_name: (item.province_name || '').trim(),
|
||
city_name: (item.city_name || '').trim(),
|
||
county_name: (item.county_name || '').trim(),
|
||
town_name: (item.town_name || '').trim(),
|
||
road_name: '',
|
||
road_no: '',
|
||
poi_name: '',
|
||
house_no: '',
|
||
room_no: '',
|
||
floor_no: '',
|
||
unit_no: '',
|
||
building_no: '',
|
||
block_no: '',
|
||
detail: (item.detail || '').trim(),
|
||
address: (item.address || item.detail || '').trim(),
|
||
province_code: (item.province_code || '').trim(),
|
||
city_code: (item.city_code || '').trim(),
|
||
county_code: (item.county_code || '').trim(),
|
||
backend_gcj02: '',
|
||
backend_wgs84: '',
|
||
backend_level: '',
|
||
_street_std_map: {}
|
||
}
|
||
}
|
||
|
||
// 新格式:data本身就是对象
|
||
if (
|
||
res.data &&
|
||
typeof res.data === 'object' &&
|
||
(
|
||
res.data.province_name ||
|
||
res.data.city_name ||
|
||
res.data.county_name ||
|
||
res.data.detail ||
|
||
res.data.address
|
||
)
|
||
) {
|
||
return {
|
||
province_name: (res.data.province_name || '').trim(),
|
||
city_name: (res.data.city_name || '').trim(),
|
||
county_name: (res.data.county_name || '').trim(),
|
||
town_name: (res.data.town_name || '').trim(),
|
||
road_name: (res.data.road_name || res.data.road || '').trim(),
|
||
road_no: (res.data.road_no || res.data.roadno || '').trim(),
|
||
poi_name: (res.data.poi_name || res.data.poi || '').trim(),
|
||
house_no: (res.data.house_no || res.data.houseno || '').trim(),
|
||
room_no: (res.data.room_no || res.data.roomno || '').trim(),
|
||
floor_no: (res.data.floor_no || res.data.floor || '').trim(),
|
||
unit_no: (res.data.unit_no || res.data.unit || '').trim(),
|
||
building_no: (res.data.building_no || res.data.building || '').trim(),
|
||
block_no: (res.data.block_no || res.data.block || '').trim(),
|
||
detail: (res.data.detail || '').trim(),
|
||
address: (res.data.address || res.data.detail || '').trim(),
|
||
province_code: (res.data.province_code || '').trim(),
|
||
city_code: (res.data.city_code || '').trim(),
|
||
county_code: (res.data.county_code || '').trim(),
|
||
backend_gcj02: '',
|
||
backend_wgs84: '',
|
||
backend_level: '',
|
||
_street_std_map: {}
|
||
}
|
||
}
|
||
|
||
// 新格式:data.street + data.area + data.lonlat
|
||
if (res?.data?.street && typeof res.data.street === 'object') {
|
||
const street = res.data.street
|
||
const stdParsed = this.parseStreetStd(street.street_std || '')
|
||
const divisionParsed = this.parseDivisionInfo(res?.data?.area || {})
|
||
const lonlatParsed = this.parseLonlatInfo(res?.data?.lonlat || {})
|
||
|
||
return {
|
||
province_name: (street.province_name || street.province || stdParsed.province_name ||
|
||
divisionParsed.province_name || '').trim(),
|
||
city_name: (street.city_name || street.city || stdParsed.city_name || divisionParsed.city_name ||
|
||
'').trim(),
|
||
county_name: (street.county_name || street.district || street.county || stdParsed.county_name ||
|
||
divisionParsed.county_name || '').trim(),
|
||
town_name: (street.town_name || street.town || street.street_name || stdParsed.town_name ||
|
||
divisionParsed.town_name || '').trim(),
|
||
road_name: (street.road_name || street.road || stdParsed.road_name || '').trim(),
|
||
road_no: (street.road_no || street.roadno || stdParsed.road_no || '').trim(),
|
||
poi_name: (street.poi_name || street.poi || stdParsed.poi_name || '').trim(),
|
||
house_no: (street.house_no || street.houseno || stdParsed.house_no || '').trim(),
|
||
room_no: (street.room_no || street.roomno || stdParsed.room_no || '').trim(),
|
||
floor_no: (street.floor_no || street.floor || stdParsed.floor_no || '').trim(),
|
||
unit_no: (street.unit_no || street.unit || stdParsed.unit_no || '').trim(),
|
||
building_no: (street.building_no || street.building || stdParsed.building_no || '').trim(),
|
||
block_no: (street.block_no || street.block || stdParsed.block_no || '').trim(),
|
||
detail: (street.detail || street.address || stdParsed.detail || stdParsed.address || '').trim(),
|
||
address: (street.address || street.detail || stdParsed.address || stdParsed.detail || '').trim(),
|
||
province_code: (street.province_code || stdParsed.province_code || divisionParsed.province_code ||
|
||
'').trim(),
|
||
city_code: (street.city_code || stdParsed.city_code || divisionParsed.city_code || '').trim(),
|
||
county_code: (street.county_code || street.district_code || stdParsed.county_code || divisionParsed
|
||
.county_code || '').trim(),
|
||
backend_gcj02: lonlatParsed.gcj02,
|
||
backend_wgs84: lonlatParsed.wgs84,
|
||
backend_level: lonlatParsed.level,
|
||
_street_std_map: stdParsed._street_std_map || {}
|
||
}
|
||
}
|
||
|
||
return null
|
||
},
|
||
|
||
getRecognizeApiUrl() {
|
||
const app = typeof getApp === 'function' ? getApp() : null
|
||
|
||
const baseUrl =
|
||
app?.globalData?.url ||
|
||
app?.globalData?.baseUrl ||
|
||
app?.globalData?.apiBaseUrl ||
|
||
'https://api.mrrwlkj.top'
|
||
|
||
return `${String(baseUrl).replace(/\/+$/, '')}/api/recognize/address`
|
||
},
|
||
|
||
buildMultipartFormData(fields = {}) {
|
||
const boundary = '----UniFormBoundary' + Date.now()
|
||
let body = ''
|
||
|
||
Object.keys(fields).forEach((key) => {
|
||
const value = fields[key] == null ? '' : String(fields[key])
|
||
body += `--${boundary}\r\n`
|
||
body += `Content-Disposition: form-data; name="${key}"\r\n\r\n`
|
||
body += `${value}\r\n`
|
||
})
|
||
|
||
body += `--${boundary}--\r\n`
|
||
|
||
return {
|
||
boundary,
|
||
body
|
||
}
|
||
},
|
||
|
||
requestRecognizeAddress(rawText) {
|
||
return request.post('/api/recognize/address', {
|
||
text: rawText
|
||
})
|
||
},
|
||
|
||
buildGeoFromBackend(hit) {
|
||
const location = String(hit?.backend_gcj02 || '').trim()
|
||
if (!location) return null
|
||
|
||
return {
|
||
location,
|
||
adcode: String(hit?.county_code || '').trim(),
|
||
level: String(hit?.backend_level || '').trim(),
|
||
district: String(hit?.county_name || '').trim()
|
||
}
|
||
},
|
||
|
||
geocodeRecognizedAddress(address) {
|
||
return new Promise((resolve) => {
|
||
if (!address) {
|
||
resolve({
|
||
ok: true,
|
||
data: null,
|
||
error: null
|
||
})
|
||
return
|
||
}
|
||
|
||
uni.request({
|
||
url: 'https://restapi.amap.com/v3/geocode/geo',
|
||
method: 'GET',
|
||
data: {
|
||
key: this.amapKey,
|
||
address,
|
||
output: 'JSON'
|
||
},
|
||
success: (res) => {
|
||
if (res.data.status === '1' && res.data.geocodes?.length > 0) {
|
||
resolve({
|
||
ok: true,
|
||
data: res.data.geocodes[0],
|
||
error: null
|
||
})
|
||
} else {
|
||
resolve({
|
||
ok: true,
|
||
data: null,
|
||
error: null
|
||
})
|
||
}
|
||
},
|
||
fail: (error) => resolve({
|
||
ok: false,
|
||
data: null,
|
||
error
|
||
})
|
||
})
|
||
})
|
||
},
|
||
|
||
stripLeadingRegionParts(text, parts = []) {
|
||
let result = String(text || '').trim()
|
||
|
||
const validParts = [...new Set(
|
||
parts
|
||
.map(item => String(item || '').trim())
|
||
.filter(Boolean)
|
||
)].sort((a, b) => b.length - a.length)
|
||
|
||
if (!result || !validParts.length) return result
|
||
|
||
let changed = true
|
||
|
||
while (changed && result) {
|
||
changed = false
|
||
result = result.replace(/^\s+/, '')
|
||
|
||
for (const part of validParts) {
|
||
const reg = new RegExp(`^${this.escapeRegExp(part)}(?:\\s+|\\b)?`)
|
||
if (reg.test(result)) {
|
||
result = result.replace(reg, '').trim()
|
||
changed = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
},
|
||
|
||
assembleDetailByInterfaceFields(hit, sanitizedText, province, city, district) {
|
||
const directDetail = String(
|
||
hit?.detail ||
|
||
hit?.address ||
|
||
hit?.addr ||
|
||
hit?.full_address_detail ||
|
||
hit?.address_detail ||
|
||
''
|
||
).trim()
|
||
|
||
const structuredValues = [
|
||
String(hit?.town_name || '').trim(),
|
||
String(hit?.road_name || '').trim(),
|
||
String(hit?.road_no || '').trim(),
|
||
String(hit?.poi_name || '').trim(),
|
||
String(hit?.house_no || '').trim(),
|
||
String(hit?.unit_no || '').trim(),
|
||
String(hit?.floor_no || '').trim(),
|
||
String(hit?.room_no || '').trim(),
|
||
String(hit?.building_no || '').trim(),
|
||
String(hit?.block_no || '').trim()
|
||
]
|
||
|
||
let result = ''
|
||
|
||
// 先放街道/镇
|
||
result = this.appendIfMissing(result, structuredValues[0])
|
||
|
||
// 再放接口直接给出的detail/address
|
||
if (directDetail) {
|
||
const cleanedDirect = this.stripLeadingRegionParts(
|
||
directDetail,
|
||
[province, city, district].filter(Boolean)
|
||
).trim()
|
||
result = this.appendIfMissing(result, cleanedDirect)
|
||
}
|
||
|
||
// 再按结构化字段补齐
|
||
for (let i = 1; i < structuredValues.length; i++) {
|
||
result = this.appendIfMissing(result, structuredValues[i])
|
||
}
|
||
|
||
// 再把street_std中其他未知字段补齐,做到“有什么就拼什么”
|
||
const rawMap = hit?._street_std_map || {}
|
||
const ignoredKeys = new Set([
|
||
'prov', 'province',
|
||
'city',
|
||
'district', 'county', 'area',
|
||
'town', 'street',
|
||
'road',
|
||
'roadno', 'road_no',
|
||
'poi', 'poi_name',
|
||
'houseno', 'house_no',
|
||
'roomno', 'room_no',
|
||
'floor', 'floor_no',
|
||
'unit', 'unit_no',
|
||
'building', 'building_no',
|
||
'block', 'block_no',
|
||
'detail', 'address', 'addr',
|
||
'province_code', 'prov_code',
|
||
'city_code',
|
||
'county_code', 'district_code', 'area_code'
|
||
])
|
||
|
||
Object.keys(rawMap).forEach((key) => {
|
||
if (ignoredKeys.has(key)) return
|
||
const value = String(rawMap[key] || '').trim()
|
||
if (!value) return
|
||
result = this.appendIfMissing(result, value)
|
||
})
|
||
|
||
// 最后兜底原始文本扣省市区
|
||
if (!result) {
|
||
result = this.stripLeadingRegionParts(
|
||
sanitizedText,
|
||
[province, city, district].filter(Boolean)
|
||
).trim()
|
||
}
|
||
|
||
// 最终再次确保详细地址不带省市区
|
||
result = this.stripLeadingRegionParts(
|
||
result,
|
||
[province, city, district].filter(Boolean)
|
||
).trim()
|
||
|
||
return result
|
||
},
|
||
|
||
buildExactRecognizedDetail(hit, sanitizedText, province, city, district) {
|
||
return this.assembleDetailByInterfaceFields(
|
||
hit,
|
||
sanitizedText,
|
||
province,
|
||
city,
|
||
district
|
||
)
|
||
},
|
||
|
||
async onSmartRecognize() {
|
||
this.clearBlankPageInitLock()
|
||
this.userSelectedRegion = false
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = false
|
||
|
||
if (!this.hasRawContent) {
|
||
uni.showToast({
|
||
title: '请先粘贴或输入地址',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
const originalRawText = (this.rawAddress || '').trim()
|
||
if (!originalRawText) {
|
||
uni.showToast({
|
||
title: '请先粘贴或输入地址',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
const hasNetwork = await this.ensureRecognizeNetworkAvailable()
|
||
if (!hasNetwork) return
|
||
|
||
const sanitizedText = this.sanitizeRawAddress(originalRawText)
|
||
const removedNoise = sanitizedText !== originalRawText
|
||
|
||
if (!sanitizedText) {
|
||
uni.showToast({
|
||
title: '未提取到有效地址',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
uni.showLoading({
|
||
title: '识别中...'
|
||
})
|
||
|
||
try {
|
||
const res = await this.requestRecognizeAddress(sanitizedText)
|
||
console.log('智能识别接口返回:', res)
|
||
|
||
const hit = this.normalizeRecognizeHit(res)
|
||
|
||
if (!res || (res.code !== 0 && res.code !== 200) || !hit) {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: res?.msg || '地址解析失败',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
console.log('智能识别标准化结果:', hit)
|
||
|
||
const normalizedRegion = this.normalizeRecognizedRegionParts(
|
||
hit.province_name,
|
||
hit.city_name,
|
||
hit.county_name
|
||
)
|
||
|
||
const province = normalizedRegion.province
|
||
const city = normalizedRegion.city
|
||
const district = normalizedRegion.district
|
||
|
||
const regionText = this.buildRegionText(province, city, district)
|
||
|
||
if (!regionText) {
|
||
uni.hideLoading()
|
||
const tip = this.getRecognizeToast('low', removedNoise, false, hit, '', false)
|
||
if (tip.type === 'modal') {
|
||
uni.showModal({
|
||
title: tip.title,
|
||
content: tip.content,
|
||
showCancel: false
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: tip.title,
|
||
icon: 'none'
|
||
})
|
||
}
|
||
return
|
||
}
|
||
|
||
const detailCandidate = this.cleanRecognizedDetail(
|
||
this.buildExactRecognizedDetail(
|
||
hit,
|
||
sanitizedText,
|
||
province,
|
||
city,
|
||
district
|
||
),
|
||
province,
|
||
city,
|
||
district
|
||
)
|
||
|
||
const fullAddress = `${regionText}${detailCandidate}`.trim()
|
||
console.log('用于定位的完整地址:', fullAddress)
|
||
|
||
let geocodeOk = false
|
||
let geo = null
|
||
let detailRejected = false
|
||
let detailText = ''
|
||
|
||
const fullGeoResult = await this.geocodeRecognizedAddress(fullAddress)
|
||
|
||
if (!fullGeoResult.ok) {
|
||
uni.hideLoading()
|
||
this.showRecognizeNetworkError()
|
||
return
|
||
}
|
||
|
||
geo = fullGeoResult.data || this.buildGeoFromBackend(hit)
|
||
|
||
const detailDecision = this.validateRecognizedDetail(detailCandidate, geo, hit)
|
||
detailRejected = !detailDecision.valid && !!detailCandidate
|
||
detailText = detailDecision.valid ? detailDecision.cleaned : ''
|
||
|
||
if (!detailText) {
|
||
const regionGeoResult = await this.geocodeRecognizedAddress(regionText)
|
||
if (!regionGeoResult.ok) {
|
||
uni.hideLoading()
|
||
this.showRecognizeNetworkError()
|
||
return
|
||
}
|
||
geo = regionGeoResult.data || this.buildGeoFromBackend(hit)
|
||
detailText = ''
|
||
}
|
||
|
||
if (geo) {
|
||
const location = geo.location
|
||
const adcode = geo.adcode || ''
|
||
|
||
if (adcode.length >= 6) {
|
||
this.selectedDistrictCode = adcode.slice(0, 6)
|
||
this.selectedCityCode = adcode.slice(0, 4) + '00'
|
||
this.selectedProvinceCode = adcode.slice(0, 2) + '0000'
|
||
} else {
|
||
this.selectedProvinceCode = hit.province_code || this.selectedProvinceCode || ''
|
||
this.selectedCityCode = hit.city_code || this.selectedCityCode || ''
|
||
this.selectedDistrictCode = hit.county_code || this.selectedDistrictCode || ''
|
||
}
|
||
|
||
if (location) {
|
||
const [lng, lat] = location.split(',')
|
||
this.longitude = parseFloat(lng)
|
||
this.latitude = parseFloat(lat)
|
||
this.newLongitude = this.longitude
|
||
this.newLatitude = this.latitude
|
||
|
||
this.recognizeMapMoving = true
|
||
if (this.recognizeMapMoveTimer) {
|
||
clearTimeout(this.recognizeMapMoveTimer)
|
||
}
|
||
|
||
this.updateMapCenter(this.longitude, this.latitude, true)
|
||
geocodeOk = true
|
||
|
||
this.recognizeMapMoveTimer = setTimeout(() => {
|
||
this.recognizeMapMoving = false
|
||
this.recognizeMapMoveTimer = null
|
||
}, 1500)
|
||
}
|
||
}
|
||
|
||
const level = this.getRecognizeLevel(hit, detailText, sanitizedText)
|
||
const shouldOnlyToast = this.shouldOnlyToastForGarbageRecognize(
|
||
sanitizedText,
|
||
hit,
|
||
detailText,
|
||
detailRejected,
|
||
geocodeOk
|
||
)
|
||
|
||
if (level === 'low' || shouldOnlyToast) {
|
||
uni.hideLoading()
|
||
|
||
if (shouldOnlyToast) {
|
||
uni.showModal({
|
||
title: '未识别到有效地址',
|
||
content: '当前输入内容无效,系统未自动填入所在地区和详细地址,请检查后重新输入,或手动选择地址。',
|
||
showCancel: false
|
||
})
|
||
} else {
|
||
const tip = this.getRecognizeToast(level, removedNoise, geocodeOk, hit, detailText,
|
||
detailRejected)
|
||
if (tip.type === 'modal') {
|
||
uni.showModal({
|
||
title: tip.title,
|
||
content: tip.content,
|
||
showCancel: false
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: tip.title,
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
this.commitRecognizedResult(province, city, district, regionText, detailText, geo, level,
|
||
removedNoise, geocodeOk, hit, detailRejected)
|
||
} catch (error) {
|
||
console.error('智能识别失败:', error)
|
||
uni.hideLoading()
|
||
|
||
if (this.isLikelyNetworkError(error)) {
|
||
this.showRecognizeNetworkError()
|
||
return
|
||
}
|
||
|
||
uni.showToast({
|
||
title: '识别失败,请稍后重试',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
|
||
getRecognizeLevel(hit, detailText, rawText) {
|
||
const province = (hit?.province_name || '').trim()
|
||
const city = (hit?.city_name || '').trim()
|
||
const district = (hit?.county_name || '').trim()
|
||
|
||
if (/火星市|月球|银河|测试地址/.test(rawText || '')) {
|
||
return 'low'
|
||
}
|
||
|
||
if (!province || (!city && !district)) {
|
||
return 'low'
|
||
}
|
||
|
||
if (!detailText || detailText.length < 2) {
|
||
return 'medium'
|
||
}
|
||
|
||
return 'high'
|
||
},
|
||
|
||
shouldOnlyToastForGarbageRecognize(rawText, hit, detailText, detailRejected = false, geocodeOk = false) {
|
||
if (geocodeOk) return false
|
||
|
||
const input = String(rawText || '').trim()
|
||
if (!input) return true
|
||
|
||
const province = (hit?.province_name || '').trim()
|
||
const city = (hit?.city_name || '').trim()
|
||
const district = (hit?.county_name || '').trim()
|
||
|
||
const hasFullRegion = !!(province && (city || district))
|
||
if (!hasFullRegion) return false
|
||
|
||
const hasAddressFeature = this.hasStrongAddressFeature(input)
|
||
const hasRegionKeyword = /(省|市|区|县|旗|镇|乡|村|街道)/.test(input)
|
||
const hasLongLetterChunk = /[A-Za-z]{2,}/.test(input)
|
||
const hasInvalidNoise = this.containsObviousInvalidNoise(input)
|
||
|
||
if (!hasAddressFeature && !hasRegionKeyword && !detailText) {
|
||
if (hasLongLetterChunk || hasInvalidNoise || detailRejected) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
},
|
||
|
||
getRecognizeToast(level, removedNoise, geocodeOk, hit, detailText, detailRejected = false) {
|
||
const province = (hit?.province_name || '').trim()
|
||
const city = (hit?.city_name || '').trim()
|
||
const district = (hit?.county_name || '').trim()
|
||
|
||
const hasRegion = !!(province || city || district)
|
||
const hasFullRegion = !!(province && (city || district))
|
||
const hasDetail = !!(detailText && detailText.trim())
|
||
|
||
if (detailRejected && hasFullRegion && !hasDetail) {
|
||
return {
|
||
type: 'modal',
|
||
title: '已识别到地区,详细地址未自动填入',
|
||
content: '已识别到省市区,但检测到详细地址中包含无效或不完整内容,系统未自动填入详细地址,请手动补充。'
|
||
}
|
||
}
|
||
|
||
if (level === 'low') {
|
||
if (!hasRegion) {
|
||
return {
|
||
type: 'modal',
|
||
title: '已识别,但未匹配到有效地区',
|
||
content: '未查找到明确的省市区信息,请手动选择所在地区,并检查输入内容是否包含错别字、备注或无效地址。'
|
||
}
|
||
}
|
||
|
||
if (hasRegion && !hasFullRegion) {
|
||
return {
|
||
type: 'modal',
|
||
title: '已识别到部分地区信息',
|
||
content: '当前仅识别到部分地区,未能完整匹配到市区县,请手动补全所在地区并核对详细地址。'
|
||
}
|
||
}
|
||
|
||
if (hasFullRegion && !hasDetail) {
|
||
return {
|
||
type: 'modal',
|
||
title: '已识别到地区,但详细地址不完整',
|
||
content: '已识别到省市区,但未能准确提取详细地址,请补充门牌号、楼栋号等信息。'
|
||
}
|
||
}
|
||
|
||
return {
|
||
type: 'modal',
|
||
title: '已识别,但未匹配到有效地区',
|
||
content: '当前地址未匹配到有效地区,请核对所在地区和详细地址,必要时请手动修正。'
|
||
}
|
||
}
|
||
|
||
if (level === 'medium') {
|
||
if (!hasDetail) {
|
||
return {
|
||
type: 'toast',
|
||
title: removedNoise ? '已识别到地区,请补充详细地址' : '已识别到地区,请补充详细地址'
|
||
}
|
||
}
|
||
|
||
if (!geocodeOk) {
|
||
return {
|
||
type: 'toast',
|
||
title: removedNoise ? '已识别,但未查到地图位置' : '已识别,但未查到对应地图位置'
|
||
}
|
||
}
|
||
|
||
return {
|
||
type: 'toast',
|
||
title: removedNoise ? '已识别到部分地址,请核对' : '已识别到部分地址,请核对'
|
||
}
|
||
}
|
||
|
||
if (!geocodeOk) {
|
||
return {
|
||
type: 'toast',
|
||
title: removedNoise ? '已识别,但未查到地图位置' : '已识别,请核对地址和地图位置'
|
||
}
|
||
}
|
||
|
||
return {
|
||
type: 'toast',
|
||
title: removedNoise ? '识别成功,请核对' : '识别成功,请核对'
|
||
}
|
||
},
|
||
|
||
commitRecognizedResult(province, city, district, regionText, detailText, geo, level, removedNoise, geocodeOk,
|
||
hit, detailRejected) {
|
||
this.currentProvince = province
|
||
this.currentCity = city
|
||
this.currentDistrict = district
|
||
this.currentRegion = regionText
|
||
|
||
this.selectedProvinceCode = hit.province_code || this.selectedProvinceCode || ''
|
||
this.selectedCityCode = hit.city_code || this.selectedCityCode || ''
|
||
this.selectedDistrictCode = hit.county_code || this.selectedDistrictCode || ''
|
||
|
||
this.detailAddress = detailText
|
||
this.manualDetail = detailText
|
||
this.currentDetail = detailText
|
||
this.isManualDetail = !!detailText
|
||
|
||
this.smartBtnText = '已识别'
|
||
uni.hideLoading()
|
||
|
||
const tip = this.getRecognizeToast(level, removedNoise, geocodeOk, hit, detailText, detailRejected)
|
||
if (tip.type === 'modal') {
|
||
uni.showModal({
|
||
title: tip.title,
|
||
content: tip.content,
|
||
showCancel: false
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: tip.title,
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
|
||
updateMapCenter(longitude, latitude, noReverse = false) {
|
||
const lng = parseFloat(longitude)
|
||
const lat = parseFloat(latitude)
|
||
|
||
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
|
||
|
||
this.applyMapLocation(lng, lat, noReverse)
|
||
},
|
||
|
||
selectRegion() {
|
||
this.openRegionPicker()
|
||
},
|
||
|
||
openRegionPicker() {
|
||
this.regionProvinceList = ChinaCitys || []
|
||
|
||
let provinceIndex = 0
|
||
let cityIndex = 0
|
||
let areaIndex = 0
|
||
|
||
if (this.selectedProvinceCode && this.selectedCityCode && this.selectedDistrictCode) {
|
||
const idx = this.findRegionIndexByCode(
|
||
ChinaCitys,
|
||
this.selectedProvinceCode,
|
||
this.selectedCityCode,
|
||
this.selectedDistrictCode
|
||
)
|
||
provinceIndex = idx.provinceIndex >= 0 ? idx.provinceIndex : 0
|
||
cityIndex = idx.cityIndex >= 0 ? idx.cityIndex : 0
|
||
areaIndex = idx.areaIndex >= 0 ? idx.areaIndex : 0
|
||
}
|
||
|
||
this.regionCityList = this.regionProvinceList[provinceIndex]?.citys || []
|
||
this.regionDistrictList = this.regionCityList[cityIndex]?.areas || []
|
||
this.regionPickerValue = [provinceIndex, cityIndex, areaIndex]
|
||
|
||
if (this.mapMoveTimer) {
|
||
clearTimeout(this.mapMoveTimer)
|
||
this.mapMoveTimer = null
|
||
}
|
||
|
||
this.mapSnapshotUrl = this.buildMapSnapshotUrl()
|
||
this.showMapSnapshot = true
|
||
this.mapVisible = false
|
||
this.showRegionPicker = true
|
||
},
|
||
|
||
closeRegionPicker() {
|
||
this.showRegionPicker = false
|
||
|
||
if (this.popupRestoreTimer) {
|
||
clearTimeout(this.popupRestoreTimer)
|
||
}
|
||
|
||
this.popupRestoreTimer = setTimeout(() => {
|
||
this.showMapSnapshot = false
|
||
this.mapSnapshotUrl = ''
|
||
|
||
this.ignoreRegionChangeOnce = true
|
||
this.mapProgrammaticUntil = Date.now() + 500
|
||
|
||
if (this.mapGuardTimer) {
|
||
clearTimeout(this.mapGuardTimer)
|
||
}
|
||
this.mapGuardTimer = setTimeout(() => {
|
||
this.ignoreRegionChangeOnce = false
|
||
this.mapGuardTimer = null
|
||
}, 550)
|
||
|
||
this.mapVisible = true
|
||
this.mapRenderKey += 1
|
||
this.popupRestoreTimer = null
|
||
}, 300)
|
||
},
|
||
|
||
onRegionChange(e) {
|
||
const val = e.detail.value
|
||
this.regionPickerValue = val
|
||
|
||
const provinceIdx = val[0]
|
||
const cityIdx = val[1] || 0
|
||
|
||
if (this.regionProvinceList.length > provinceIdx) {
|
||
this.regionCityList = this.regionProvinceList[provinceIdx].citys || []
|
||
const maxCityIdx = this.regionCityList.length - 1
|
||
const adjustedCityIdx = Math.min(cityIdx, maxCityIdx)
|
||
|
||
if (adjustedCityIdx !== cityIdx) {
|
||
this.regionPickerValue = [provinceIdx, adjustedCityIdx, 0]
|
||
}
|
||
|
||
if (this.regionCityList.length > adjustedCityIdx) {
|
||
this.regionDistrictList = this.regionCityList[adjustedCityIdx].areas || []
|
||
const areaIdx = val[2] || 0
|
||
const maxAreaIdx = this.regionDistrictList.length - 1
|
||
if (areaIdx > maxAreaIdx) {
|
||
this.regionPickerValue = [provinceIdx, adjustedCityIdx, 0]
|
||
}
|
||
} else {
|
||
this.regionDistrictList = []
|
||
this.regionPickerValue = [provinceIdx, 0, 0]
|
||
}
|
||
} else {
|
||
this.regionCityList = []
|
||
this.regionDistrictList = []
|
||
this.regionPickerValue = [0, 0, 0]
|
||
}
|
||
},
|
||
|
||
async confirmRegion() {
|
||
this.clearBlankPageInitLock()
|
||
const [provinceIdx, cityIdx, districtIdx] = this.regionPickerValue
|
||
const provinceItem = this.regionProvinceList[provinceIdx]
|
||
const cityItem = this.regionCityList[cityIdx]
|
||
const districtItem = this.regionDistrictList[districtIdx]
|
||
|
||
if (!provinceItem || !cityItem || !districtItem) {
|
||
uni.showToast({
|
||
title: '选择无效',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
this.userSelectedRegion = true
|
||
|
||
const regionStr = provinceItem.province + cityItem.city + districtItem.area
|
||
this.currentProvince = provinceItem.province
|
||
this.currentCity = cityItem.city
|
||
this.currentDistrict = districtItem.area
|
||
this.currentRegion = regionStr
|
||
|
||
this.selectedProvinceCode = provinceItem.code
|
||
this.selectedCityCode = cityItem.code
|
||
this.selectedDistrictCode = districtItem.code
|
||
|
||
this.isManualDetail = false
|
||
this.suppressInitDetailFill = true
|
||
this.detailAddress = ''
|
||
this.manualDetail = ''
|
||
this.currentDetail = ''
|
||
|
||
const shortCode = districtItem.code ? districtItem.code.toString().slice(0, 6) : ''
|
||
this.startDependencyInitLock()
|
||
const center = await this.getDistrictCenter(districtItem.area, shortCode)
|
||
|
||
if (center) {
|
||
const [lng, lat] = center.split(',')
|
||
this.updateMapCenter(parseFloat(lng), parseFloat(lat), true)
|
||
} else {
|
||
uni.showToast({
|
||
title: '无法定位该地区,请手动拖动地图',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
this.closeRegionPicker()
|
||
},
|
||
|
||
getDistrictCenter(keyword, adcode = '', subdistrict = 0) {
|
||
return new Promise((resolve) => {
|
||
const params = {
|
||
key: this.amapKey,
|
||
subdistrict,
|
||
extensions: 'all'
|
||
}
|
||
|
||
const queryAdcode = () => {
|
||
if (!adcode) return Promise.resolve(null)
|
||
|
||
return new Promise((resolveInner) => {
|
||
uni.request({
|
||
url: 'https://restapi.amap.com/v3/config/district',
|
||
method: 'GET',
|
||
data: {
|
||
...params,
|
||
keywords: adcode.slice(0, 6)
|
||
},
|
||
success: (res) => {
|
||
if (res.data.status === '1' && res.data.districts &&
|
||
res.data.districts.length > 0) {
|
||
const district = res.data.districts[0]
|
||
if (district.center) {
|
||
resolveInner(district.center)
|
||
return
|
||
}
|
||
}
|
||
resolveInner(null)
|
||
},
|
||
fail: () => resolveInner(null)
|
||
})
|
||
})
|
||
}
|
||
|
||
const queryKeyword = () => {
|
||
return new Promise((resolveInner) => {
|
||
uni.request({
|
||
url: 'https://restapi.amap.com/v3/config/district',
|
||
method: 'GET',
|
||
data: {
|
||
...params,
|
||
keywords: keyword
|
||
},
|
||
success: (res) => {
|
||
if (res.data.status === '1' && res.data.districts &&
|
||
res.data.districts.length > 0) {
|
||
const district = res.data.districts[0]
|
||
if (district.center) {
|
||
resolveInner(district.center)
|
||
return
|
||
}
|
||
}
|
||
resolveInner(null)
|
||
},
|
||
fail: () => resolveInner(null)
|
||
})
|
||
})
|
||
}
|
||
|
||
queryAdcode().then(center => {
|
||
if (center) {
|
||
resolve(center)
|
||
} else {
|
||
queryKeyword().then(resolve)
|
||
}
|
||
})
|
||
})
|
||
},
|
||
|
||
onConfirm() {
|
||
const fullAddress = (this.currentRegion + (this.detailAddress ? ' ' + this.detailAddress : '')).trim()
|
||
|
||
const addressItem = {
|
||
name: fullAddress,
|
||
address: this.detailAddress,
|
||
location: `${this.longitude},${this.latitude}`,
|
||
pname: this.currentProvince,
|
||
cityname: this.currentCity,
|
||
adname: this.currentDistrict,
|
||
province: this.currentProvince,
|
||
city: this.currentCity,
|
||
district: this.currentDistrict,
|
||
provinceCode: this.selectedProvinceCode,
|
||
cityCode: this.selectedCityCode,
|
||
districtCode: this.selectedDistrictCode,
|
||
source: this.source
|
||
}
|
||
|
||
const pages = getCurrentPages()
|
||
const prevPage = pages[pages.length - 2]
|
||
const currentPage = pages[pages.length - 1]
|
||
const source = this.source || currentPage.options.source
|
||
|
||
//特殊处理重庆
|
||
if (addressItem.cityCode == 500100) {
|
||
addressItem.cityname = "重庆城区"
|
||
}
|
||
if (addressItem.cityCode == 500200) {
|
||
addressItem.cityname = "重庆郊县"
|
||
}
|
||
|
||
|
||
if (source === 'syr_info') {
|
||
uni.$emit('address-selected-for-syr', addressItem)
|
||
uni.setStorageSync('syr_selected_address', addressItem)
|
||
} else if (source === 'sj_info') {
|
||
uni.$emit('address-selected-for-sj', addressItem)
|
||
uni.setStorageSync('sj_selected_address', addressItem)
|
||
if (getApp().globalData) getApp().globalData.selectedAddress = addressItem
|
||
} else if (prevPage) {
|
||
if (prevPage.$vm && typeof prevPage.$vm.editAddress === 'function') {
|
||
prevPage.$vm.editAddress(addressItem)
|
||
} else {
|
||
uni.$emit('address-selected', addressItem)
|
||
}
|
||
} else {
|
||
uni.$emit('address-selected', addressItem)
|
||
}
|
||
|
||
uni.navigateBack()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.search-address-page {
|
||
flex: 1;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #FFFFFF;
|
||
}
|
||
|
||
/* 城市选择 */
|
||
.search-container {
|
||
background-color: #FFFFFF;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 32rpx 30rpx 20rpx 30rpx;
|
||
|
||
.search-container-text {
|
||
font-family: PingFangSC, PingFang SC;
|
||
font-weight: 500;
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
line-height: 45rpx;
|
||
}
|
||
|
||
.text-img {
|
||
width: 20rpx;
|
||
height: 12rpx;
|
||
margin-left: 8rpx;
|
||
margin-right: 26rpx;
|
||
}
|
||
}
|
||
|
||
/* 地图区域 */
|
||
.map-container {
|
||
width: 750rpx;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.map {
|
||
width: 750rpx;
|
||
height: 100%;
|
||
}
|
||
|
||
.map-snapshot {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 750rpx;
|
||
height: 100%;
|
||
display: block;
|
||
z-index: 2;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.center-marker {
|
||
position: absolute;
|
||
left: 375rpx;
|
||
transform: translateX(-50%);
|
||
top: 250rpx;
|
||
width: 42rpx;
|
||
height: 69rpx;
|
||
z-index: 3;
|
||
|
||
.center-marker-img {
|
||
width: 42rpx;
|
||
}
|
||
}
|
||
|
||
.map-snapshot {
|
||
width: 750rpx;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
/* 底部编辑区域(用户端样式) */
|
||
.bottom-edit-section {
|
||
background-color: #FFFFFF;
|
||
padding: 20rpx 20rpx 120rpx 20rpx;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 固定按钮栏 */
|
||
.fixed-confirm-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background-color: #FFFFFF;
|
||
padding: 20rpx 20rpx env(safe-area-inset-bottom) 20rpx;
|
||
/* 适配刘海屏 */
|
||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||
z-index: 10;
|
||
}
|
||
|
||
.address-input-row {
|
||
// width: 702rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #F5F5F5;
|
||
border-radius: 20rpx;
|
||
padding: 16rpx 24rpx;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.raw-address-input {
|
||
width: 100%;
|
||
height: 120rpx;
|
||
font-size: 14px;
|
||
color: #333333;
|
||
background-color: transparent;
|
||
border-width: 0;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
margin-top: 0rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
margin-left: 16rpx;
|
||
padding: 7rpx 20rpx;
|
||
font-size: 24rpx;
|
||
border-radius: 11rpx;
|
||
border-width: 1rpx;
|
||
border-style: solid;
|
||
background-color: transparent;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.action-btn:first-child {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.btn-text {
|
||
font-size: 24rpx;
|
||
font-weight: 400;
|
||
line-height: 33rpx;
|
||
}
|
||
|
||
.paste-btn {
|
||
border-color: #C6C9CC;
|
||
color: #333333;
|
||
}
|
||
|
||
.smart-btn {
|
||
border-color: #C6C9CC;
|
||
color: #999999;
|
||
}
|
||
|
||
.smart-btn.smart-btn-active {
|
||
border-color: #FF4767;
|
||
color: #FF4767;
|
||
}
|
||
|
||
.address-info-panel {
|
||
background-color: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 30rpx 0;
|
||
}
|
||
|
||
.line {
|
||
border-bottom: 1rpx solid #F0F0F0;
|
||
width: 540rpx;
|
||
margin-left: 150rpx;
|
||
}
|
||
|
||
.row-label {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
font-weight: 500;
|
||
width: 160rpx;
|
||
}
|
||
|
||
.row-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.content-text {
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
margin-right: 12rpx;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.placeholder {
|
||
color: #999999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.arrow-icon {
|
||
width: 14rpx;
|
||
height: 26rpx;
|
||
position: absolute;
|
||
right: 60rpx;
|
||
}
|
||
|
||
.detail-input {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #333333;
|
||
padding: 0;
|
||
border-width: 0;
|
||
background-color: transparent;
|
||
height: 60rpx;
|
||
}
|
||
|
||
.confirm-btn {
|
||
background-color: #FF4767;
|
||
border-radius: 49rpx;
|
||
height: 98rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
color: #FFFFFF;
|
||
// margin-top: 40rpx;
|
||
}
|
||
|
||
.con-btn-text {
|
||
font-size: 32rpx;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
/* 弹窗基础 */
|
||
.popup {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 9999;
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.popup.show {
|
||
visibility: visible !important;
|
||
opacity: 1 !important;
|
||
}
|
||
|
||
.popup-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.popup-content {
|
||
position: relative;
|
||
background-color: #ffffff;
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
padding: 32rpx;
|
||
transform: translateY(100%);
|
||
transition: transform 0.3s ease;
|
||
max-height: 70vh;
|
||
}
|
||
|
||
.popup.show .popup-content {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.popup-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
padding: 0 0 20rpx 0;
|
||
border-bottom: 1rpx solid #F0F0F0;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.popup-close {
|
||
font-size: 40rpx;
|
||
color: #999;
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.region-body {
|
||
height: 400rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.picker-scroll {
|
||
height: 400rpx;
|
||
}
|
||
|
||
.picker-item {
|
||
height: 68rpx;
|
||
line-height: 68rpx;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.popup-footer {
|
||
display: flex;
|
||
margin-top: 30rpx;
|
||
padding-top: 20rpx;
|
||
border-top: 1rpx solid #F0F0F0;
|
||
}
|
||
|
||
.popup-btn {
|
||
flex: 1;
|
||
height: 88rpx;
|
||
border-radius: 44rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin: 0 16rpx;
|
||
}
|
||
|
||
.popup-btn.cancel {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.popup-btn.confirm {
|
||
background: #FF4767;
|
||
}
|
||
|
||
.popup-btn .btn-text {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.popup-btn.cancel .btn-text {
|
||
color: #666666;
|
||
}
|
||
|
||
.popup-btn.confirm .btn-text {
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* 指示器样式(沿用原有) */
|
||
.indicatorStyle {
|
||
height: 68rpx;
|
||
}
|
||
</style> |