386 lines
11 KiB
Vue
386 lines
11 KiB
Vue
<template>
|
|
<z-paging>
|
|
<template #top>
|
|
<navbar/>
|
|
<view class="bg-white px-30rpx py-26rpx">
|
|
<view class="flex items-center h-86rpx px-28rpx bg-#F6F7F9 rounded-20rpx">
|
|
<wd-input v-model="mapSearchKeyword" :no-border="true" :placeholder="t('common.placeholder.pleaseEnter')"
|
|
custom-class="!w-full !bg-transparent" placeholder-class="text-#9B9CA0 text-28rpx">
|
|
<template #prefix>
|
|
<view class="w-12rpx h-12rpx rounded-50% bg-#F97C34"></view>
|
|
</template>
|
|
</wd-input>
|
|
</view>
|
|
|
|
<view class="" @click="handleUseLocation">
|
|
<view class="text-32rpx text-#333 font-500 my-30rpx">{{ t('common.useMyCurrentLocation') }}</view>
|
|
<view class="text-28rpx text-#333 font-500 flex items-center pb-20rpx pl-24rpx">
|
|
<view class="i-carbon:location-current text-32rpx mr-14rpx mt-4rpx"></view>
|
|
{{ t('common.useCurrentLocation') }}
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
<view class="bg-#f5f5f5">
|
|
<view class="px-22rpx py-20rpx">
|
|
<view v-if="logicStore.searchLoading" class="center py-100rpx text-28rpx text-#9B9CA0">
|
|
Loading...
|
|
</view>
|
|
|
|
<template v-else-if="placesLength > 0">
|
|
<view class="rounded-36rpx bg-white">
|
|
<view
|
|
v-for="(item, index) in logicStore.placesList"
|
|
:key="(item as any).id || index"
|
|
:class="[index === logicStore.placesList.length - 1 ? '' : 'border-bottom']"
|
|
class="px-22rpx pb-30rpx pt-34rpx"
|
|
@click="handleClickLocation(item as AddressPlaceItem)"
|
|
>
|
|
<view class="text-#000 text-26rpx font-bold">{{ (item as any).displayName }}</view>
|
|
<view class="text-#9B9CA0 text-24rpx flex-center-sb">
|
|
<view>{{ (item as any).formattedAddress }}</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<view v-else-if="hasSearched" class="center py-100rpx">
|
|
<image class="w-250rpx h-250rpx" src="@img/chef/100.png"></image>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</z-paging>
|
|
</template>
|
|
<script lang="ts" setup>
|
|
import Config from "@/config";
|
|
import {useLogicStore} from "@/pages-user/store/logic";
|
|
import {useUserStore} from "@/store";
|
|
import {debounce} from "throttle-debounce";
|
|
|
|
const {t, locale} = useI18n();
|
|
const userStore = useUserStore()
|
|
const logicStore = useLogicStore()
|
|
|
|
export interface AddressPlaceItem {
|
|
id: string
|
|
displayName: string
|
|
formattedAddress: string
|
|
location: { lat: number; lng: number } | null
|
|
}
|
|
|
|
let openerEventChannel: any = null
|
|
onLoad(() => {
|
|
const pages = getCurrentPages() as any[]
|
|
const page = pages[pages.length - 1]
|
|
openerEventChannel = page?.getOpenerEventChannel?.() || null
|
|
})
|
|
|
|
onMounted(() => {
|
|
logicStore.clearPlacesList()
|
|
})
|
|
|
|
const placesLength = computed(() => logicStore.placesList.length)
|
|
const mapSearchKeyword = ref<string>('')
|
|
const hasSearched = ref(false)
|
|
|
|
watch(
|
|
() => logicStore.searchLoading,
|
|
(loading) => {
|
|
if (loading) {
|
|
uni.showLoading({ title: 'Loading...', mask: true })
|
|
} else {
|
|
uni.hideLoading()
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
function resolveLanguageCode() {
|
|
if (locale.value === 'zh-Hans') return 'zh-CN'
|
|
if (locale.value === 'en') return 'en'
|
|
return String(locale.value || 'en')
|
|
}
|
|
|
|
function resolveRegionCode() {
|
|
return locale.value === 'zh-Hans' ? 'CN' : 'US'
|
|
}
|
|
|
|
function resolveBiasCenter() {
|
|
const lat = Number(userStore.location.latitude)
|
|
const lng = Number(userStore.location.longitude)
|
|
if (Number.isFinite(lat) && Number.isFinite(lng) && (lat !== 0 || lng !== 0)) {
|
|
return { latitude: lat, longitude: lng }
|
|
}
|
|
if (locale.value === 'zh-Hans') {
|
|
return { latitude: 39.9042, longitude: 116.4074 }
|
|
}
|
|
return { latitude: 38.8977, longitude: -77.0365 }
|
|
}
|
|
|
|
function parsePlacesApiPlace(place: any): AddressPlaceItem | null {
|
|
if (!place) return null
|
|
|
|
const lat = place.location?.latitude
|
|
const lng = place.location?.longitude
|
|
const formattedAddress = String(place.formattedAddress || '').trim()
|
|
const displayName = String(
|
|
typeof place.displayName === 'string'
|
|
? place.displayName
|
|
: place.displayName?.text || formattedAddress,
|
|
).trim()
|
|
|
|
if (!displayName && !formattedAddress) return null
|
|
|
|
return {
|
|
id: String(place.id || ''),
|
|
displayName: displayName || formattedAddress,
|
|
formattedAddress,
|
|
location:
|
|
Number.isFinite(Number(lat)) && Number.isFinite(Number(lng))
|
|
? { lat: Number(lat), lng: Number(lng) }
|
|
: null,
|
|
}
|
|
}
|
|
|
|
function parseGeocodeResult(result: any): AddressPlaceItem | null {
|
|
if (!result) return null
|
|
|
|
const formattedAddress = String(result.formatted_address || '').trim()
|
|
const lat = result.geometry?.location?.lat
|
|
const lng = result.geometry?.location?.lng
|
|
const displayName =
|
|
formattedAddress.split(',')[0]?.trim() || formattedAddress
|
|
|
|
if (!formattedAddress) return null
|
|
|
|
return {
|
|
id: String(result.place_id || ''),
|
|
displayName,
|
|
formattedAddress,
|
|
location:
|
|
Number.isFinite(Number(lat)) && Number.isFinite(Number(lng))
|
|
? { lat: Number(lat), lng: Number(lng) }
|
|
: null,
|
|
}
|
|
}
|
|
|
|
function requestJson<T = any>(options: UniApp.RequestOptions) {
|
|
return new Promise<{ statusCode: number; data: T }>((resolve, reject) => {
|
|
uni.request({
|
|
timeout: 10000,
|
|
...options,
|
|
success: (res) => resolve(res as any),
|
|
fail: reject,
|
|
})
|
|
})
|
|
}
|
|
|
|
async function searchByPlacesTextSearch(keyword: string) {
|
|
const languageCode = resolveLanguageCode()
|
|
const regionCode = resolveRegionCode()
|
|
const center = resolveBiasCenter()
|
|
|
|
const res = await requestJson<{ places?: any[]; error?: any }>({
|
|
url: 'https://places.googleapis.com/v1/places:searchText',
|
|
method: 'POST',
|
|
header: {
|
|
'Content-Type': 'application/json',
|
|
'X-Goog-Api-Key': Config.googleMapKey,
|
|
'X-Goog-FieldMask': 'places.id,places.displayName,places.formattedAddress,places.location',
|
|
},
|
|
data: {
|
|
textQuery: keyword,
|
|
languageCode,
|
|
regionCode,
|
|
pageSize: 20,
|
|
locationBias: {
|
|
circle: {
|
|
center,
|
|
radius: 50000,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (res.statusCode !== 200) {
|
|
const message = (res.data as any)?.error?.message || `HTTP ${res.statusCode}`
|
|
throw new Error(message)
|
|
}
|
|
|
|
const places = Array.isArray(res.data?.places) ? res.data.places : []
|
|
return places.map(parsePlacesApiPlace).filter(Boolean) as AddressPlaceItem[]
|
|
}
|
|
|
|
async function searchByGeocodeFallback(keyword: string) {
|
|
const languageCode = resolveLanguageCode()
|
|
const regionCode = resolveRegionCode()
|
|
const query = encodeURIComponent(keyword)
|
|
const url =
|
|
`https://maps.googleapis.com/maps/api/geocode/json?address=${query}` +
|
|
`&key=${Config.googleMapKey}&language=${languageCode}®ion=${regionCode}`
|
|
|
|
const res = await requestJson<{ results?: any[]; status?: string }>({
|
|
url,
|
|
method: 'GET',
|
|
})
|
|
|
|
if (res.statusCode !== 200 || res.data?.status !== 'OK') {
|
|
return []
|
|
}
|
|
|
|
const results = Array.isArray(res.data.results) ? res.data.results : []
|
|
return results.map(parseGeocodeResult).filter(Boolean) as AddressPlaceItem[]
|
|
}
|
|
|
|
async function searchPlaces(keyword: string) {
|
|
logicStore.searchLoading = true
|
|
hasSearched.value = true
|
|
|
|
try {
|
|
let list = await searchByPlacesTextSearch(keyword)
|
|
|
|
if (list.length === 0) {
|
|
list = await searchByGeocodeFallback(keyword)
|
|
}
|
|
|
|
logicStore.setPlacesList(list)
|
|
} catch (error) {
|
|
console.error('地址搜索失败', error)
|
|
try {
|
|
const fallback = await searchByGeocodeFallback(keyword)
|
|
logicStore.setPlacesList(fallback)
|
|
} catch (fallbackError) {
|
|
console.error('Geocode 回退失败', fallbackError)
|
|
logicStore.setPlacesList([])
|
|
}
|
|
} finally {
|
|
logicStore.searchLoading = false
|
|
}
|
|
}
|
|
|
|
const debouncedSearch = debounce(300, (keyword: string) => {
|
|
void searchPlaces(keyword)
|
|
})
|
|
|
|
watch(mapSearchKeyword, (keyword) => {
|
|
const text = String(keyword || '').trim()
|
|
if (!text) {
|
|
hasSearched.value = false
|
|
logicStore.clearPlacesList()
|
|
return
|
|
}
|
|
debouncedSearch(text)
|
|
})
|
|
|
|
function handleClickLocation(item: AddressPlaceItem) {
|
|
openerEventChannel?.emit?.('chooseAddress', item)
|
|
uni.navigateBack()
|
|
}
|
|
|
|
function handleUseLocation() {
|
|
if (!userStore.location.longitude || !userStore.location.latitude) {
|
|
uni.getLocation({
|
|
isHighAccuracy: true,
|
|
type: 'gcj02',
|
|
geocode: true,
|
|
success: (res) => {
|
|
getCityName(res.latitude, res.longitude)
|
|
},
|
|
fail: () => {
|
|
const settings = uni.getAppAuthorizeSetting()
|
|
if (settings.locationAuthorized === 'denied') {
|
|
uni.showToast({
|
|
title: t('common.prompt.please-authorize-location-information'),
|
|
icon: 'none',
|
|
})
|
|
setTimeout(() => {
|
|
uni.openAppAuthorizeSetting()
|
|
})
|
|
}
|
|
},
|
|
})
|
|
} else {
|
|
openerEventChannel?.emit?.('chooseAddress', {
|
|
displayName: userStore.location.location,
|
|
formattedAddress: userStore.location.formattedAddress || '',
|
|
location: {
|
|
lng: userStore.location.longitude,
|
|
lat: userStore.location.latitude,
|
|
},
|
|
})
|
|
uni.navigateBack()
|
|
}
|
|
}
|
|
|
|
function getCityName(latitude: number, longitude: number) {
|
|
const languageCode = resolveLanguageCode()
|
|
const url =
|
|
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}` +
|
|
`&key=${Config.googleMapKey}&language=${languageCode}`
|
|
|
|
uni.request({
|
|
url,
|
|
method: 'GET',
|
|
timeout: 10000,
|
|
success: (res: any) => {
|
|
const results = res.data?.results || []
|
|
|
|
if (!results.length) {
|
|
return handleFail()
|
|
}
|
|
|
|
const addr = results[0]
|
|
const components = addr.address_components || []
|
|
|
|
const pickAddress = (types: string[]) => {
|
|
return components.find((item: any) => item.types.some((type: string) => types.includes(type)))
|
|
}
|
|
|
|
const cityObj =
|
|
pickAddress(['locality']) ||
|
|
pickAddress(['administrative_area_level_2']) ||
|
|
pickAddress(['administrative_area_level_1']) ||
|
|
pickAddress(['political'])
|
|
|
|
const cityName = cityObj?.long_name || null
|
|
const formattedAddress = addr.formatted_address || cityName || ''
|
|
|
|
if (!cityName) {
|
|
return handleFail()
|
|
}
|
|
|
|
userStore.location = {
|
|
location: cityName,
|
|
formattedAddress,
|
|
longitude,
|
|
latitude,
|
|
}
|
|
|
|
openerEventChannel?.emit?.('chooseAddress', {
|
|
displayName: cityName,
|
|
formattedAddress,
|
|
location: { lng: longitude, lat: latitude },
|
|
})
|
|
|
|
uni.navigateBack()
|
|
},
|
|
fail: () => handleFail(),
|
|
})
|
|
|
|
function handleFail() {
|
|
uni.showToast({
|
|
title: t('common.prompt.getLocationFailed'),
|
|
icon: 'none',
|
|
})
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
page {
|
|
background-color: #f5f5f5;
|
|
}
|
|
</style>
|
|
<style lang="scss" scoped>
|
|
|
|
</style>
|