|
|
@@ -3,6 +3,16 @@
|
|
|
<div ref="mapContainer" class="map-container" />
|
|
|
<!-- 定位按钮 -->
|
|
|
<div class="map-controls">
|
|
|
+ <van-button
|
|
|
+ v-if="userStore.isAdmin && pendingCreateLngLat"
|
|
|
+ icon="plus"
|
|
|
+ round
|
|
|
+ type="success"
|
|
|
+ size="small"
|
|
|
+ @click="goCreateTree"
|
|
|
+ >
|
|
|
+ 在此创建许愿树
|
|
|
+ </van-button>
|
|
|
<van-button
|
|
|
icon="aim"
|
|
|
round
|
|
|
@@ -67,6 +77,32 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</van-popup>
|
|
|
+
|
|
|
+ <!-- 管理员创建许愿树弹窗 -->
|
|
|
+ <van-popup
|
|
|
+ v-model:show="showCreatePopup"
|
|
|
+ position="bottom"
|
|
|
+ round
|
|
|
+ :style="{ maxHeight: '70%' }"
|
|
|
+ safe-area-inset-bottom
|
|
|
+ >
|
|
|
+ <div class="create-popup">
|
|
|
+ <h3>创建许愿树</h3>
|
|
|
+ <van-field v-model="createForm.name" label="名称" placeholder="输入许愿树名称" required />
|
|
|
+ <div class="create-cover">
|
|
|
+ <span class="create-cover-label">封面图</span>
|
|
|
+ <ImageUploader v-model="createForm.coverImages" :max="1" />
|
|
|
+ </div>
|
|
|
+ <van-field v-model.number="createForm.radius" label="可许愿范围" placeholder="100" type="number">
|
|
|
+ <template #suffix><span style="color:#999;font-size:14px">米</span></template>
|
|
|
+ </van-field>
|
|
|
+ <van-field :model-value="createForm.lng + ', ' + createForm.lat" label="坐标" readonly />
|
|
|
+ <div class="create-actions">
|
|
|
+ <van-button round plain @click="showCreatePopup = false">取消</van-button>
|
|
|
+ <van-button round type="primary" :loading="creating" @click="handleCreateTree">创建</van-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </van-popup>
|
|
|
</div>
|
|
|
</template>
|
|
|
<script setup lang="ts">
|
|
|
@@ -76,17 +112,25 @@ import { showToast } from "vant";
|
|
|
import { useLocationStore } from "@/stores/location";
|
|
|
import { getUserLocation } from "@/utils/geo";
|
|
|
import { getTreeGradient, getTreeEmoji } from "@/utils/theme";
|
|
|
-import { fetchNearbyTrees } from "@/api/tree";
|
|
|
+import { fetchNearbyTrees, createTree } from "@/api/tree";
|
|
|
+import { useUserStore } from "@/stores/user";
|
|
|
import { gcj02ToWgs84 } from "@/utils/mapTool";
|
|
|
import markerImg from "@/assets/location-marker.png";
|
|
|
import L from "leaflet";
|
|
|
import "leaflet/dist/leaflet.css";
|
|
|
+import ImageUploader from "@/components/ImageUploader.vue";
|
|
|
const router = useRouter();
|
|
|
const locationStore = useLocationStore();
|
|
|
+const userStore = useUserStore();
|
|
|
const mapContainer = ref<HTMLDivElement>();
|
|
|
const showPopup = ref(false);
|
|
|
const selectedTree = ref<any>(null);
|
|
|
const locating = ref(false);
|
|
|
+
|
|
|
+// 管理员创建许愿树
|
|
|
+const showCreatePopup = ref(false);
|
|
|
+const creating = ref(false);
|
|
|
+const createForm = ref({ name: '', radius: 100, lng: 0, lat: 0, coverImages: [] as string[] });
|
|
|
const popupGradient = computed(() =>
|
|
|
getTreeGradient(selectedTree.value?.id || 1),
|
|
|
);
|
|
|
@@ -94,6 +138,7 @@ const popupEmoji = computed(() => getTreeEmoji(selectedTree.value?.id || 1));
|
|
|
let map: any = null;
|
|
|
let userMarker: any = null;
|
|
|
let treeMarkers: L.Marker[] = [];
|
|
|
+const pendingCreateLngLat = ref<[number, number] | null>(null);
|
|
|
function fmtDist(m: number) {
|
|
|
return m >= 1000 ? (m / 1000).toFixed(1) + "km" : m + "m";
|
|
|
}
|
|
|
@@ -105,6 +150,37 @@ function goMakeWish() {
|
|
|
showPopup.value = false;
|
|
|
router.push({ path: "/make-wish", query: { treeId: selectedTree.value.id } });
|
|
|
}
|
|
|
+function goCreateTree() {
|
|
|
+ if (!pendingCreateLngLat.value) return;
|
|
|
+ const [lng, lat] = pendingCreateLngLat.value;
|
|
|
+ createForm.value = { name: '', radius: 100, lng: Number(lng.toFixed(6)), lat: Number(lat.toFixed(6)), coverImages: [] };
|
|
|
+ showCreatePopup.value = true;
|
|
|
+}
|
|
|
+async function handleCreateTree() {
|
|
|
+ if (!createForm.value.name.trim()) {
|
|
|
+ showToast("请输入许愿树名称");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ creating.value = true;
|
|
|
+ try {
|
|
|
+ await createTree({
|
|
|
+ name: createForm.value.name,
|
|
|
+ description: '',
|
|
|
+ longitude: createForm.value.lng,
|
|
|
+ latitude: createForm.value.lat,
|
|
|
+ address: '',
|
|
|
+ radius: createForm.value.radius,
|
|
|
+ coverImage: createForm.value.coverImages[0] || '',
|
|
|
+ });
|
|
|
+ showToast("许愿树创建成功");
|
|
|
+ showCreatePopup.value = false;
|
|
|
+ await loadTrees();
|
|
|
+ } catch {
|
|
|
+ showToast("创建失败");
|
|
|
+ } finally {
|
|
|
+ creating.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
// 高德瓦片(GCJ-02 坐标系)
|
|
|
async function initMap() {
|
|
|
try {
|
|
|
@@ -154,6 +230,20 @@ async function initMap() {
|
|
|
L.layerGroup().addTo(map);
|
|
|
L.control.scale({ metric: true, imperial: false }).addTo(map);
|
|
|
L.control.zoom({ position: "topright" }).addTo(map);
|
|
|
+ // 管理员点击地图:移动定位标记到点击位置
|
|
|
+ map.on("click", (e: any) => {
|
|
|
+ if (!userStore.isAdmin) return;
|
|
|
+ showPopup.value = false;
|
|
|
+ const { lat, lng } = e.latlng;
|
|
|
+ pendingCreateLngLat.value = [lng, lat];
|
|
|
+ if (userMarker) map.removeLayer(userMarker);
|
|
|
+ const icon = L.icon({
|
|
|
+ iconUrl: markerImg,
|
|
|
+ iconSize: [25, 45],
|
|
|
+ iconAnchor: [12, 45],
|
|
|
+ });
|
|
|
+ userMarker = L.marker([lat, lng], { icon, zIndexOffset: 200 }).addTo(map);
|
|
|
+ });
|
|
|
locateUser();
|
|
|
} catch (err) {
|
|
|
showToast("地图加载失败");
|
|
|
@@ -315,4 +405,27 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
+.create-popup {
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+.create-popup h3 {
|
|
|
+ font-size: 18px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.create-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 16px 0;
|
|
|
+}
|
|
|
+.create-cover {
|
|
|
+ padding: 12px 16px;
|
|
|
+}
|
|
|
+.create-cover-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
</style>
|