Initial project commit
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
<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 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
|
||||
@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>
|
||||
Reference in New Issue
Block a user