diff --git a/pages/address/search.vue b/pages/address/search.vue index 164c05e..3cf7178 100644 --- a/pages/address/search.vue +++ b/pages/address/search.vue @@ -140,7 +140,7 @@ export default { data() { return { - useApiRawRecognize: false, // API直出:true + useApiRawRecognize: false, mapVisible: true, showMapSnapshot: false, mapSnapshotUrl: '', @@ -191,7 +191,7 @@ // 首次进入且上页没有详细地址时,禁止自动回填详细地址 suppressInitDetailFill: false, - ignoreRegionChangeOnce: false, // 忽略一次程序触发的地图变化 + ignoreRegionChangeOnce: false, // 地区选择弹窗数据 showRegionPicker: false, @@ -262,16 +262,13 @@ this.suppressInitDetailFill = true } - // 关键:锁住首次地图初始化阶段,避免首屏被旧地图中心点反写 this.startDependencyInitLock() - // 有经纬度:只用于地图定位,不再 reverseGeocode 覆盖所在地区 if (hasLocation) { this.applyMapLocation(options.longitude, options.latitude, true) return } - // 没经纬度:用地区中心定位地图 const dependencyCode = options.dependency_code ;(async () => { @@ -301,7 +298,6 @@ ) } } else if (!skipAutoLocate) { - // 走当前位置时,不再锁 this.clearDependencyInitLock() this.getCurrentLocation() } else { @@ -358,8 +354,7 @@ return } - // 6. 空白进入且明确要求不自动定位: - // 不仅不调用当前位置,还要锁住首屏地图默认中心点触发的regionchange反写 + // 6. 空白进入且明确要求不自动定位 if (skipAutoLocate && !hasDependency && !hasLocation) { this.blankPageInitLock = true this.currentProvince = '' @@ -382,7 +377,6 @@ }, onShow() { - // 只有城市选择模式下,才允许 searchCity 缓存更新地图 if (!this.isSelectCity) return const searchCity = uni.getStorageSync('searchCity') @@ -395,7 +389,6 @@ }, methods: { - // 直辖市处理 normalizeMunicipalityName(name) { const value = String(name || '').trim() @@ -490,8 +483,6 @@ if (!Number.isFinite(lng) || !Number.isFinite(lat)) return - // 程序主动改中心时,给一个更短的保护时间 - // 避免真机上用户刚进页面第一次拖动也被吞掉 this.ignoreRegionChangeOnce = true this.mapProgrammaticUntil = Date.now() + 280 @@ -520,7 +511,7 @@ }, 220) } }, - // 直辖市处理 + normalizeCityName(province, city) { let cityName = '' @@ -666,7 +657,6 @@ const locationRes = await this.getLocation(silent) if (lockInit) { - // 首次进入搜索地址页、且需要按当前定位初始化时,锁住地图初始化阶段 this.startDependencyInitLock(1200) this.userSelectedRegion = false this.isManualDetail = false @@ -675,7 +665,6 @@ this.updateMapCenter(locationRes.longitude, locationRes.latitude, false) } catch (error) { - // 静默定位失败时,保持页面为空,等用户自己操作 if (silent) { this.latitude = 0 this.longitude = 0 @@ -691,9 +680,6 @@ this.currentDetail = '' this.isManualDetail = false - // 关键补丁: - // 未授权/静默定位失败后,立刻进入空白锁定模式, - // 防止地图默认中心点再把“北京市西城区”反写回来 this.blankPageInitLock = true this.suppressInitDetailFill = true } @@ -740,7 +726,6 @@ res?.data?.regeocode?.addressComponent?.adcode || '' ) - // 取不到adcode时不拦,避免误杀正常流程 if (!adcode) { resolve(true) return @@ -749,7 +734,6 @@ resolve(adcode === targetCode) }, fail: () => { - // 校验失败时不拦,避免把正常功能带坏 resolve(true) } }) @@ -787,7 +771,6 @@ } }, - // 逆地理编码 reverseGeocode(lng, lat) { const requestId = ++this.reverseGeocodeRequestId const targetLng = parseFloat(lng) @@ -803,7 +786,6 @@ extensions: 'base' }, success: (res) => { - // 只认最后一次逆地理编码,避免旧请求把新位置覆盖掉 if (requestId !== this.reverseGeocodeRequestId) return if (res.data.status === '1' && res.data.regeocode) { @@ -843,7 +825,6 @@ detail = formatted.trim() } - // 首次进入且上页没传详细地址:保持空 if (this.suppressInitDetailFill) { this.suppressInitDetailFill = false return @@ -863,7 +844,6 @@ }) }, - // 地图点击:点击后立即更新地区和详细地址 onMapTap(e) { this.clearDependencyInitLock() this.clearBlankPageInitLock() @@ -932,7 +912,6 @@ }) }, - // 地图拖动结束:用getCenterLocation拿中心点,然后更新地区和详细地址 changeMap(e) { if (this.showRegionPicker || !this.mapVisible) return if (this.recognizeMapMoving) return @@ -949,7 +928,6 @@ } } - // 只跳过明确的 begin if (eventType === 'begin') return if (Date.now() < this.mapProgrammaticUntil) { @@ -980,14 +958,12 @@ }, delay) }, - // 城市选择 goSelectCity() { uni.navigateTo({ url: '/subPackages/address/selectCity' }) }, - // 底部编辑区方法 clearRawAddress() { this.rawAddress = '' uni.showToast({ title: '已清空', icon: 'none' }) @@ -1095,43 +1071,32 @@ 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 } @@ -1147,46 +1112,65 @@ '道路交叉口', '乡镇', '村庄', - '村级地名' + '村级地名', + 'roadno', + 'poi' ].includes(String(level || '').trim()) }, - validateRecognizedDetail(detailText, geo, requireKeyword = true) { + 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' } } - // 有些地址虽然没有“号栋室”,但高德返回到了较精确层级,也可以放行 - const geoLevel = String(geo?.level || '').trim() - const preciseLevel = this.isPreciseGeocodeLevel(geoLevel) - const hasAddressFeature = this.hasStrongAddressFeature(cleaned) - - // 没有地理编码结果,直接无效 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, town) { + cleanRecognizedDetail(detail, province, city, district) { let text = this.stripLeadingRegionParts(detail, [ province, city, - district, - town + district ]) text = text @@ -1202,35 +1186,703 @@ return text }, - buildRecognizedDetail(townName, detail) { - const town = (townName || '').trim() - const detailText = (detail || '').trim() + appendIfMissing(base, part) { + const src = String(base || '').trim() + const seg = String(part || '').trim() - if (town && detailText) { - return detailText.startsWith(town) ? detailText : `${town}${detailText}` + 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 detailText || town + 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) { - const directDetail = (hit?.detail || hit?.address || hit?.addr || hit?.full_address_detail || hit?.address_detail || '').trim(); - - // 计算从原始文本中扣除省市区后的详细地址 - const stripped = this.stripLeadingRegionParts( + return this.assembleDetailByInterfaceFields( + hit, sanitizedText, - [province, city, district].filter(Boolean) - ).trim(); + province, + city, + district + ) + }, - // 如果 directDetail 存在且长度合理,优先使用;否则使用 stripped - if (directDetail && directDetail.length >= 4) { - // 如果 stripped 更长,且 directDetail 是 stripped 的前缀,说明接口返回的不完整,使用 stripped - if (stripped.length > directDetail.length && stripped.startsWith(directDetail)) { - return stripped; - } - return directDetail; + 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' + }) } - return stripped; }, getRecognizeLevel(hit, detailText, rawText) { @@ -1246,15 +1898,16 @@ return 'low' } - if (!detailText || detailText.length < 4) { + if (!detailText || detailText.length < 2) { return 'medium' } return 'high' }, - // 垃圾文本拦截 - shouldOnlyToastForGarbageRecognize(rawText, hit, detailText, detailRejected = false) { + shouldOnlyToastForGarbageRecognize(rawText, hit, detailText, detailRejected = false, geocodeOk = false) { + if (geocodeOk) return false + const input = String(rawText || '').trim() if (!input) return true @@ -1270,7 +1923,6 @@ const hasLongLetterChunk = /[A-Za-z]{2,}/.test(input) const hasInvalidNoise = this.containsObviousInvalidNoise(input) - // 既不像正常地址,又夹杂明显乱码/字母串,同时还没有有效详细地址时,只提示不回填 if (!hasAddressFeature && !hasRegionKeyword && !detailText) { if (hasLongLetterChunk || hasInvalidNoise || detailRejected) { return true @@ -1363,424 +2015,15 @@ } }, - 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(), - 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() - } - }, - - normalizeRecognizeHit(res) { - if (!res || (res.code !== 200 && res.code !== 0)) return null - - // 旧格式1:data 是数组 - if (Array.isArray(res.data) && res.data.length) { - return res.data[0] - } - - // 旧格式2:data.data 是数组 - if (Array.isArray(res?.data?.data) && res.data.data.length) { - return res.data.data[0] - } - - // 旧格式3:result 是数组 - if (Array.isArray(res.result) && res.result.length) { - return res.result[0] - } - - // 新格式: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 res.data - } - - // 新格式:data.street.street_std - if (res?.data?.street && typeof res.data.street === 'object') { - const street = res.data.street - const stdParsed = this.parseStreetStd(street.street_std || '') - - return { - province_name: (street.province_name || street.province || stdParsed.province_name || '').trim(), - city_name: (street.city_name || street.city || stdParsed.city_name || '').trim(), - county_name: (street.county_name || street.district || street.county || stdParsed.county_name || '').trim(), - town_name: (street.town_name || street.town || street.street_name || stdParsed.town_name || '').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 || '').trim(), - city_code: (street.city_code || stdParsed.city_code || '').trim(), - county_code: (street.county_code || street.district_code || stdParsed.county_code || '').trim() - } - } - - 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 - }) - }, - - 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 - }, - - 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 town = (hit.town_name || '').trim() - - 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, - town - ) - - 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 - - // ========== 改进1:使用增强的 validateRecognizedDetail ========== - // 该方法内部会结合地理编码级别和强/弱关键词进行判断 - const detailDecision = this.validateRecognizedDetail(detailCandidate, geo, true) - 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 - detailText = '' // 详细地址留空 - } - - if (geo) { - const location = geo.location - const adcode = geo.adcode || '' - - if (adcode.length >= 6) { - this.selectedDistrictCode = adcode - this.selectedCityCode = adcode.substring(0, 4) + '00' - this.selectedProvinceCode = adcode.substring(0, 2) + '0000' - } - - 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 - ) - - - // 低可信度或明显无效 → 只提示,不回填 - 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' - }) - } - }, - 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.selectedCityCode = hit.city_code || '' - this.selectedDistrictCode = hit.county_code || '' + 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 @@ -1805,7 +2048,6 @@ } }, - // 更新地图中心 updateMapCenter(longitude, latitude, noReverse = false) { const lng = parseFloat(longitude) const lat = parseFloat(latitude) @@ -1815,7 +2057,6 @@ this.applyMapLocation(lng, lat, noReverse) }, - // 地区选择弹窗 selectRegion() { this.openRegionPicker() }, @@ -1865,7 +2106,6 @@ this.showMapSnapshot = false this.mapSnapshotUrl = '' - // 恢复地图时做一次保护,避免重建后的 regionchange 抢状态 this.ignoreRegionChangeOnce = true this.mapProgrammaticUntil = Date.now() + 500 @@ -1941,7 +2181,6 @@ this.selectedCityCode = cityItem.code this.selectedDistrictCode = districtItem.code - // 选地区后只定位到地区中心,详细地址保持空 this.isManualDetail = false this.suppressInitDetailFill = true this.detailAddress = '' @@ -1962,7 +2201,6 @@ this.closeRegionPicker() }, - // 获取地区中心点 getDistrictCenter(keyword, adcode = '', subdistrict = 0) { return new Promise((resolve) => { const params = { @@ -2031,7 +2269,6 @@ }) }, - // 确认返回 onConfirm() { const fullAddress = (this.currentRegion + (this.detailAddress ? ' ' + this.detailAddress : '')).trim()