|
|
@@ -4,7 +4,14 @@
|
|
|
|
|
|
<!-- 定位按钮 -->
|
|
|
<div class="map-controls">
|
|
|
- <van-button icon="aim" round type="primary" size="small" :loading="locating" @click="locateUser">
|
|
|
+ <van-button
|
|
|
+ icon="aim"
|
|
|
+ round
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ :loading="locating"
|
|
|
+ @click="locateUser"
|
|
|
+ >
|
|
|
定位
|
|
|
</van-button>
|
|
|
</div>
|
|
|
@@ -24,10 +31,20 @@
|
|
|
<h3 class="popup-name">{{ selectedTree.name }}</h3>
|
|
|
<p class="popup-addr">{{ selectedTree.address }}</p>
|
|
|
<div class="popup-meta">
|
|
|
- <van-tag :type="selectedTree.isInRange ? 'success' : 'warning'" round size="medium">
|
|
|
- {{ selectedTree.isInRange ? '可许愿' : '距离 ' + fmtDist(selectedTree.distance) }}
|
|
|
+ <van-tag
|
|
|
+ :type="selectedTree.isInRange ? 'success' : 'warning'"
|
|
|
+ round
|
|
|
+ size="medium"
|
|
|
+ >
|
|
|
+ {{
|
|
|
+ selectedTree.isInRange
|
|
|
+ ? "可许愿"
|
|
|
+ : "距离 " + fmtDist(selectedTree.distance)
|
|
|
+ }}
|
|
|
</van-tag>
|
|
|
- <span class="popup-count">💬 {{ selectedTree.totalWishes }} 个愿望</span>
|
|
|
+ <span class="popup-count"
|
|
|
+ >💬 {{ selectedTree.totalWishes }} 个愿望</span
|
|
|
+ >
|
|
|
</div>
|
|
|
<div class="popup-actions">
|
|
|
<van-button
|
|
|
@@ -47,7 +64,7 @@
|
|
|
:disabled="!selectedTree.isInRange"
|
|
|
@click="goMakeWish"
|
|
|
>
|
|
|
- {{ selectedTree.isInRange ? '🙏 去许愿' : '需要靠近许愿树' }}
|
|
|
+ {{ selectedTree.isInRange ? "🙏 去许愿" : "需要靠近许愿树" }}
|
|
|
</van-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -56,114 +73,117 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
-import { useRouter } from 'vue-router'
|
|
|
-import { showToast } from 'vant'
|
|
|
-import { useLocationStore } from '@/stores/location'
|
|
|
-import { getUserLocation } from '@/utils/geo'
|
|
|
-import { loadAMap } from '@/utils/amap'
|
|
|
-import { getTreeGradient, getTreeEmoji } from '@/utils/theme'
|
|
|
-import { fetchNearbyTrees } from '@/api/tree'
|
|
|
+import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
|
|
+import { useRouter } from "vue-router";
|
|
|
+import { showToast } from "vant";
|
|
|
+import { useLocationStore } from "@/stores/location";
|
|
|
+import { getUserLocation } from "@/utils/geo";
|
|
|
+import { loadAMap } from "@/utils/amap";
|
|
|
+import { getTreeGradient, getTreeEmoji } from "@/utils/theme";
|
|
|
+import { fetchNearbyTrees } from "@/api/tree";
|
|
|
+import markerImg from "@/assets/location-marker.png";
|
|
|
|
|
|
-const router = useRouter()
|
|
|
-const locationStore = useLocationStore()
|
|
|
+const router = useRouter();
|
|
|
+const locationStore = useLocationStore();
|
|
|
|
|
|
-const mapContainer = ref<HTMLDivElement>()
|
|
|
-const showPopup = ref(false)
|
|
|
-const selectedTree = ref<any>(null)
|
|
|
-const locating = ref(false)
|
|
|
+const mapContainer = ref<HTMLDivElement>();
|
|
|
+const showPopup = ref(false);
|
|
|
+const selectedTree = ref<any>(null);
|
|
|
+const locating = ref(false);
|
|
|
|
|
|
-const popupGradient = computed(() => getTreeGradient(selectedTree.value?.id || 1))
|
|
|
-const popupEmoji = computed(() => getTreeEmoji(selectedTree.value?.id || 1))
|
|
|
+const popupGradient = computed(() =>
|
|
|
+ getTreeGradient(selectedTree.value?.id || 1),
|
|
|
+);
|
|
|
+const popupEmoji = computed(() => getTreeEmoji(selectedTree.value?.id || 1));
|
|
|
|
|
|
-let map: any = null
|
|
|
-let userMarker: any = null
|
|
|
-let treeMarkers: any[] = []
|
|
|
-let AMap: any = null
|
|
|
+let map: any = null;
|
|
|
+let userMarker: any = null;
|
|
|
+let treeMarkers: any[] = [];
|
|
|
+let AMap: any = null;
|
|
|
|
|
|
function fmtDist(m: number) {
|
|
|
- return m >= 1000 ? (m / 1000).toFixed(1) + 'km' : m + 'm'
|
|
|
+ return m >= 1000 ? (m / 1000).toFixed(1) + "km" : m + "m";
|
|
|
}
|
|
|
|
|
|
function goTreeDetail() {
|
|
|
- showPopup.value = false
|
|
|
- router.push(`/tree/${selectedTree.value.id}`)
|
|
|
+ showPopup.value = false;
|
|
|
+ router.push(`/tree/${selectedTree.value.id}`);
|
|
|
}
|
|
|
|
|
|
function goMakeWish() {
|
|
|
- showPopup.value = false
|
|
|
- router.push({ path: '/make-wish', query: { treeId: selectedTree.value.id } })
|
|
|
+ showPopup.value = false;
|
|
|
+ router.push({ path: "/make-wish", query: { treeId: selectedTree.value.id } });
|
|
|
}
|
|
|
|
|
|
async function initMap() {
|
|
|
try {
|
|
|
- AMap = await loadAMap()
|
|
|
+ AMap = await loadAMap();
|
|
|
|
|
|
map = new AMap.Map(mapContainer.value!, {
|
|
|
zoom: 14,
|
|
|
center: [116.4074, 39.9042],
|
|
|
- mapStyle: 'amap://styles/light',
|
|
|
- })
|
|
|
+ mapStyle: "amap://styles/light",
|
|
|
+ });
|
|
|
|
|
|
- map.addControl(new AMap.Scale())
|
|
|
- map.addControl(new AMap.ToolBar({ position: 'RT' }))
|
|
|
+ map.addControl(new AMap.Scale());
|
|
|
+ map.addControl(new AMap.ToolBar({ position: "RT" }));
|
|
|
|
|
|
- await locateUser()
|
|
|
+ await locateUser();
|
|
|
} catch (err) {
|
|
|
- showToast('地图加载失败')
|
|
|
- console.error(err)
|
|
|
+ showToast("地图加载失败");
|
|
|
+ console.error(err);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function locateUser() {
|
|
|
- locating.value = true
|
|
|
+ locating.value = true;
|
|
|
try {
|
|
|
- const { lng, lat } = await getUserLocation()
|
|
|
- locationStore.setLocation(lng, lat)
|
|
|
+ const { lng, lat } = await getUserLocation();
|
|
|
+ locationStore.setLocation(lng, lat);
|
|
|
|
|
|
if (map) {
|
|
|
- map.setCenter([lng, lat])
|
|
|
+ map.setCenter([lng, lat]);
|
|
|
|
|
|
- if (userMarker) map.remove(userMarker)
|
|
|
+ if (userMarker) map.remove(userMarker);
|
|
|
userMarker = new AMap.Marker({
|
|
|
position: [lng, lat],
|
|
|
icon: new AMap.Icon({
|
|
|
- size: new (AMap as any).Size(28, 28),
|
|
|
- image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png',
|
|
|
- imageSize: new (AMap as any).Size(28, 28),
|
|
|
+ size: new (AMap as any).Size(25, 45),
|
|
|
+ image: markerImg,
|
|
|
+ imageSize: new (AMap as any).Size(25, 45),
|
|
|
}),
|
|
|
zIndex: 200,
|
|
|
- anchor: 'center',
|
|
|
- })
|
|
|
- map.add(userMarker)
|
|
|
+ anchor: "center",
|
|
|
+ });
|
|
|
+ map.add(userMarker);
|
|
|
}
|
|
|
|
|
|
- await loadTrees()
|
|
|
+ await loadTrees();
|
|
|
} catch {
|
|
|
- await loadTrees()
|
|
|
+ await loadTrees();
|
|
|
} finally {
|
|
|
- locating.value = false
|
|
|
+ locating.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadTrees() {
|
|
|
- if (!locationStore.lng) return
|
|
|
+ if (!locationStore.lng) return;
|
|
|
|
|
|
const trees = await fetchNearbyTrees(
|
|
|
locationStore.lng,
|
|
|
locationStore.lat!,
|
|
|
50000,
|
|
|
- 100
|
|
|
- )
|
|
|
+ 100,
|
|
|
+ );
|
|
|
|
|
|
// 清除旧标记
|
|
|
- treeMarkers.forEach((m) => map.remove(m))
|
|
|
- treeMarkers = []
|
|
|
+ treeMarkers.forEach((m) => map.remove(m));
|
|
|
+ treeMarkers = [];
|
|
|
|
|
|
trees.forEach((tree: any) => {
|
|
|
- const isIn = tree.isInRange
|
|
|
- const color = isIn ? '#07c160' : '#ff976a'
|
|
|
- const label = `${tree.name} ${fmtDist(tree.distance)}`
|
|
|
+ const isIn = tree.isInRange;
|
|
|
+ const color = isIn ? "#07c160" : "#ff976a";
|
|
|
+ const label = `${tree.name} ${fmtDist(tree.distance)}`;
|
|
|
|
|
|
const content = `
|
|
|
<div style="
|
|
|
@@ -192,40 +212,52 @@ async function loadTrees() {
|
|
|
border-right:6px solid transparent;
|
|
|
border-top:6px solid ${color};
|
|
|
"></div>
|
|
|
- </div>`
|
|
|
+ </div>`;
|
|
|
|
|
|
const marker = new AMap.Marker({
|
|
|
position: [tree.longitude, tree.latitude],
|
|
|
content: content,
|
|
|
- anchor: 'bottom-center',
|
|
|
+ anchor: "bottom-center",
|
|
|
offset: new (AMap as any).Pixel(0, -6),
|
|
|
- })
|
|
|
+ });
|
|
|
|
|
|
- marker.on('click', () => {
|
|
|
- selectedTree.value = tree
|
|
|
- showPopup.value = true
|
|
|
- })
|
|
|
+ marker.on("click", () => {
|
|
|
+ selectedTree.value = tree;
|
|
|
+ showPopup.value = true;
|
|
|
+ });
|
|
|
|
|
|
- marker.setMap(map)
|
|
|
- treeMarkers.push(marker)
|
|
|
- })
|
|
|
+ marker.setMap(map);
|
|
|
+ treeMarkers.push(marker);
|
|
|
+ });
|
|
|
|
|
|
// 适配视野
|
|
|
if (trees.length > 0 && locationStore.lng) {
|
|
|
- const points = trees.map((t: any) => [t.longitude, t.latitude])
|
|
|
- points.push([locationStore.lng, locationStore.lat])
|
|
|
- map.setFitView(points, true, [80, 80, 80, 200])
|
|
|
+ const points = trees.map((t: any) => [t.longitude, t.latitude]);
|
|
|
+ points.push([locationStore.lng, locationStore.lat]);
|
|
|
+ map.setFitView(points, true, [80, 80, 80, 200]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+watch(
|
|
|
+ () => [locationStore.lng, locationStore.lat] as const,
|
|
|
+ ([lng, lat]) => {
|
|
|
+ if (lng != null && lat != null) {
|
|
|
+ console.log(`当前位置:经度 ${lng},纬度 ${lat}`);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+);
|
|
|
+
|
|
|
onMounted(async () => {
|
|
|
- await nextTick()
|
|
|
- initMap()
|
|
|
-})
|
|
|
+ await nextTick();
|
|
|
+ initMap();
|
|
|
+ locationStore.startWatch(5000);
|
|
|
+});
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
- if (map) map.destroy()
|
|
|
-})
|
|
|
+ locationStore.stopWatch();
|
|
|
+ if (map) map.destroy();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -261,7 +293,7 @@ onUnmounted(() => {
|
|
|
}
|
|
|
.popup-emoji {
|
|
|
font-size: 44px;
|
|
|
- text-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
|
|
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
|
|
}
|
|
|
.popup-name {
|
|
|
font-size: 17px;
|