mrr.sj.front/pages/address/search.vue

2745 lines
72 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 ? '#E8101E' : '#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
// 旧格式1data是数组
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: {}
}
}
// 旧格式2data.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: {}
}
}
// 旧格式3result是数组
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: #E8101E;
color: #E8101E;
}
.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: #E8101E;
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: #e8101e;
}
.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>