Files
CosScene/clients/pages/spot/pick-location.vue
T
shiran 3f3acf834d feat(map): 添加地图定位功能的条件渲染控制
为了解决H5平台地图定位显示的问题,在index页面和pick-location页面中添加了
mapShowLocation响应式变量,并通过条件编译确保仅在非H5平台启用地图定位功能,
避免H5环境下出现定位相关的兼容性问题。
2026-05-09 18:49:42 +08:00

371 lines
7.6 KiB
Vue

<script setup>
import { ref, onMounted } from "vue";
import { get } from "@/utils/request";
const latitude = ref(39.908823);
const longitude = ref(116.39747);
const address = ref("移动地图选择位置");
const currentCity = ref("");
const mapCtx = ref(null);
const mapShowLocation = ref(false);
// #ifndef H5
mapShowLocation.value = true;
// #endif
const keyword = ref("");
const searchResults = ref([]);
const showResults = ref(false);
let searchTimer = null;
onMounted(() => {
mapCtx.value = uni.createMapContext("pickMap");
uni.getLocation({
type: "gcj02",
success: (res) => {
latitude.value = res.latitude;
longitude.value = res.longitude;
reverseGeocode(res.latitude, res.longitude);
},
fail: () => {},
});
});
const onRegionChange = (e) => {
if (e.type === "end" || e.detail?.type === "end") {
mapCtx.value.getCenterLocation({
success: (res) => {
latitude.value = res.latitude;
longitude.value = res.longitude;
reverseGeocode(res.latitude, res.longitude);
},
});
}
};
const reverseGeocode = async (lat, lng) => {
try {
const data = await get("/map/geocoder/reverse", {
location: `${lat},${lng}`,
});
if (data && data.status === 0) {
const r = data.result;
const poi = r.pois && r.pois.length > 0 ? r.pois[0].title : "";
currentCity.value =
r.address_component?.city || r.ad_info?.city || "";
address.value =
poi || r.address || r.formatted_addresses?.recommend || "已定位";
}
} catch {
currentCity.value = "";
address.value = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
}
};
const onSearchInput = () => {
clearTimeout(searchTimer);
if (!keyword.value.trim()) {
searchResults.value = [];
showResults.value = false;
return;
}
searchTimer = setTimeout(() => {
searchPlace(keyword.value.trim());
}, 400);
};
const searchPlace = async (kw) => {
try {
const boundary = `nearby(${latitude.value},${longitude.value},50000)`;
const data = await get("/map/place/search", { keyword: kw, boundary });
if (data && data.status === 0 && data.data) {
searchResults.value = data.data.map((item) => ({
title: item.title,
address: item.address,
lat: item.location.lat,
lng: item.location.lng,
}));
showResults.value = true;
} else {
searchResults.value = [];
showResults.value = false;
}
} catch {
searchResults.value = [];
showResults.value = false;
}
};
const selectResult = (item) => {
latitude.value = item.lat;
longitude.value = item.lng;
address.value = item.title;
keyword.value = item.title;
showResults.value = false;
searchResults.value = [];
mapCtx.value.moveToLocation({
latitude: item.lat,
longitude: item.lng,
});
};
const clearSearch = () => {
keyword.value = "";
searchResults.value = [];
showResults.value = false;
};
const confirmLocation = () => {
uni.$emit("locationPicked", {
latitude: latitude.value,
longitude: longitude.value,
name: address.value,
city: currentCity.value,
});
uni.navigateBack();
};
</script>
<template>
<view class="pick-page">
<map
id="pickMap"
class="pick-map"
:latitude="latitude"
:longitude="longitude"
:scale="16"
:show-location="mapShowLocation"
@regionchange="onRegionChange"
/>
<view class="center-pin">
<image class="pin-icon" src="/static/marker.svg" mode="aspectFit" />
</view>
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-inner">
<uni-icons type="search" size="18" color="#94a3b8" />
<input
v-model="keyword"
class="search-input"
placeholder="搜索地点"
placeholder-class="search-placeholder"
confirm-type="search"
@input="onSearchInput"
@confirm="onSearchInput"
/>
<view v-if="keyword" class="clear-btn" @tap="clearSearch">
<uni-icons type="clear" size="18" color="#94a3b8" />
</view>
</view>
</view>
<!-- 搜索结果列表 -->
<scroll-view
v-if="showResults && searchResults.length > 0"
class="search-results"
scroll-y
>
<view
v-for="(item, idx) in searchResults"
:key="idx"
class="result-item"
@tap="selectResult(item)"
>
<uni-icons type="location" size="18" color="#6366f1" />
<view class="result-info">
<text class="result-title">{{ item.title }}</text>
<text class="result-addr">{{ item.address }}</text>
</view>
</view>
</scroll-view>
<!-- 底部信息 -->
<view class="bottom-bar">
<view class="address-info">
<uni-icons type="location-filled" size="20" color="#6366f1" />
<text class="address-text">{{ address }}</text>
</view>
<view class="coord-row">
<text class="coord-text"
>经度: {{ longitude.toFixed(6) }} 纬度:
{{ latitude.toFixed(6) }}</text
>
</view>
<button class="confirm-btn" @tap="confirmLocation">确认选点</button>
</view>
</view>
</template>
<style scoped>
.pick-page {
position: relative;
width: 100vw;
height: 100vh;
}
.pick-map {
width: 100%;
height: 100%;
}
.center-pin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -100%);
pointer-events: none;
z-index: 10;
}
.pin-icon {
width: 40px;
height: 52px;
}
.search-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 24rpx 32rpx;
padding-top: calc(24rpx + env(safe-area-inset-top));
z-index: 30;
}
.search-inner {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 40rpx;
padding: 0 24rpx;
height: 80rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
gap: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #1e293b;
height: 80rpx;
}
.search-placeholder {
color: #94a3b8;
}
.clear-btn {
padding: 8rpx;
}
.search-results {
position: absolute;
top: calc(120rpx + env(safe-area-inset-top));
left: 32rpx;
right: 32rpx;
max-height: 500rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.12);
z-index: 30;
overflow: hidden;
}
.result-item {
display: flex;
align-items: flex-start;
padding: 24rpx;
gap: 16rpx;
border-bottom: 1rpx solid #f1f5f9;
}
.result-item:last-child {
border-bottom: none;
}
.result-item:active {
background: #f8fafc;
}
.result-info {
flex: 1;
overflow: hidden;
}
.result-title {
font-size: 28rpx;
color: #1e293b;
font-weight: 500;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.result-addr {
font-size: 24rpx;
color: #94a3b8;
display: block;
margin-top: 4rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
border-radius: 24rpx 24rpx 0 0;
padding: 32rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
z-index: 20;
}
.address-info {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.address-text {
font-size: 30rpx;
font-weight: 600;
color: #1e293b;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.coord-row {
margin-bottom: 24rpx;
}
.coord-text {
font-size: 24rpx;
color: #94a3b8;
}
.confirm-btn {
width: 100%;
height: 84rpx;
line-height: 84rpx;
background: #6366f1;
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
border-radius: 16rpx;
border: none;
}
.confirm-btn::after {
border: none;
}
</style>