Переглянути джерело

feat: 管理员地图点击创建许愿树功能

isAdmin 持久化、地图点击移动标记、创建表单含封面图和坐标
tanlie 1 тиждень тому
батько
коміт
a77345a7e3
16 змінених файлів з 421 додано та 6 видалено
  1. 8 0
      wishing-platform/platform-service/platform-service-mobile/pom.xml
  2. 2 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/MobileApplication.java
  3. 60 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/FeignConfiguration.java
  4. 46 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/AdminController.java
  5. 1 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/TreeController.java
  6. 19 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeAddressReq.java
  7. 29 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeCreateReq.java
  8. 19 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/TreeAddressResp.java
  9. 10 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/TreeService.java
  10. 57 2
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/TreeServiceImpl.java
  11. 31 0
      wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/openfeign/UpmsFeignClient.java
  12. 2 1
      wishing-tree-h5/src/api/auth.ts
  13. 16 0
      wishing-tree-h5/src/api/tree.ts
  14. 7 2
      wishing-tree-h5/src/stores/user.ts
  15. 114 1
      wishing-tree-h5/src/views/MapView.vue
  16. BIN
      wishing-tree-h5/wish.zip

+ 8 - 0
wishing-platform/platform-service/platform-service-mobile/pom.xml

@@ -73,6 +73,14 @@
             <version>2.22.22</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <finalName>wishing-mobile</finalName>

+ 2 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/MobileApplication.java

@@ -2,12 +2,14 @@ package cn.qinys.platform;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
 
 /**
  * @author lie tan
  * @description
  * @date 2026-05-17 21:40
  **/
+@EnableFeignClients
 @SpringBootApplication
 public class MobileApplication {
 

+ 60 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/config/FeignConfiguration.java

@@ -0,0 +1,60 @@
+package cn.qinys.platform.config;
+
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import feign.Retryer;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Feign配置类
+ *
+ * @author v-lishiquan.gx@chinatelecom.cn
+ * @date 2020-08-26
+ */
+@Configuration
+@Slf4j
+public class FeignConfiguration implements RequestInterceptor {
+
+    @Bean
+    Retryer feignRetryer() {
+        return Retryer.NEVER_RETRY;
+    }
+
+    @Override
+    public void apply(RequestTemplate requestTemplate) {
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        if (Objects.nonNull(requestAttributes)) {
+            RequestContextHolder.setRequestAttributes(requestAttributes, true);
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
+            HttpServletRequest request = attributes.getRequest();
+            // 添加请求头
+            Enumeration<String> headerNames = request.getHeaderNames();
+            if (headerNames != null) {
+                while (headerNames.hasMoreElements()) {
+                    String name = headerNames.nextElement();
+                    if(this.removeHead().contains(name)){
+                        continue;
+                    }
+                    String values = request.getHeader(name);
+                    requestTemplate.header(name, values);
+                }
+            }
+        }
+    }
+
+    private List<String> removeHead() {
+        return List.of("content-length");
+    }
+}

+ 46 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/AdminController.java

@@ -0,0 +1,46 @@
+package cn.qinys.platform.mobile.controller;
+
+import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
+import cn.qinys.platform.mobile.resp.TreeAddressResp;
+import cn.qinys.platform.mobile.service.TreeService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-15 19:26
+ **/
+
+@RestController
+@RequestMapping("/dgapi/mobile/admin")
+public class AdminController {
+
+    @Resource
+    private TreeService treeService;
+
+    /**
+     * 创建许愿树(管理员功能)
+     */
+    @PostMapping("/create/tree")
+    public Result<Boolean> create(@RequestBody @Valid TreeCreateReq req) {
+        treeService.create(req);
+        return new Result<>(true);
+    }
+
+    /**
+     * 创建许愿树(管理员功能)
+     */
+    @PostMapping("/get/address")
+    public Result<TreeAddressResp> getAddress(@RequestBody @Valid TreeAddressReq req) {
+        TreeAddressResp address = treeService.getAddress(req);
+        return new Result<>(address);
+    }
+
+}

+ 1 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/controller/TreeController.java

@@ -1,6 +1,7 @@
 package cn.qinys.platform.mobile.controller;
 
 import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
 import cn.qinys.platform.mobile.req.TreeDetailReq;
 import cn.qinys.platform.mobile.req.WishingTreeListReq;
 import cn.qinys.platform.mobile.resp.TreeDetailResp;

+ 19 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeAddressReq.java

@@ -0,0 +1,19 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class TreeAddressReq implements Serializable {
+
+
+    @NotNull(message = "经度不能为空")
+    private BigDecimal longitude;
+
+    @NotNull(message = "纬度不能为空")
+    private BigDecimal latitude;
+
+}

+ 29 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/req/TreeCreateReq.java

@@ -0,0 +1,29 @@
+package cn.qinys.platform.mobile.req;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+public class TreeCreateReq implements Serializable {
+
+    @NotBlank(message = "名称不能为空")
+    private String name;
+
+    private String description;
+
+    @NotNull(message = "经度不能为空")
+    private BigDecimal longitude;
+
+    @NotNull(message = "纬度不能为空")
+    private BigDecimal latitude;
+
+    private String address;
+
+    private Integer radius = 100;
+
+    private String coverImage;
+}

+ 19 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/resp/TreeAddressResp.java

@@ -0,0 +1,19 @@
+package cn.qinys.platform.mobile.resp;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-06-15 19:29
+ **/
+@Data
+public class TreeAddressResp implements Serializable {
+
+    private String address;
+
+    private String description;
+
+}

+ 10 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/TreeService.java

@@ -1,7 +1,10 @@
 package cn.qinys.platform.mobile.service;
 
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
 import cn.qinys.platform.mobile.req.TreeDetailReq;
 import cn.qinys.platform.mobile.req.WishingTreeListReq;
+import cn.qinys.platform.mobile.resp.TreeAddressResp;
 import cn.qinys.platform.mobile.resp.TreeDetailResp;
 import cn.qinys.platform.mobile.resp.TreeListResp;
 
@@ -25,4 +28,11 @@ public interface TreeService {
      * 查询许愿树详情
      */
     TreeDetailResp getTreeDetail(TreeDetailReq req);
+
+    /**
+     * 创建许愿树(管理员功能)
+     */
+    void create(TreeCreateReq req);
+
+    TreeAddressResp getAddress(TreeAddressReq req);
 }

+ 57 - 2
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/mobile/service/impl/TreeServiceImpl.java

@@ -1,21 +1,30 @@
 package cn.qinys.platform.mobile.service.impl;
 
+import cn.qinys.platform.base.response.Result;
 import cn.qinys.platform.base.security.utils.CurrentUtils;
+import cn.qinys.platform.entity.wishing.WishingTree;
 import cn.qinys.platform.mobile.mapper.UserWishMapper;
 import cn.qinys.platform.mobile.mapper.WishingTreeMapper;
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import cn.qinys.platform.mobile.req.TreeCreateReq;
 import cn.qinys.platform.mobile.req.TreeDetailReq;
 import cn.qinys.platform.mobile.req.WishingTreeListReq;
+import cn.qinys.platform.mobile.resp.TreeAddressResp;
 import cn.qinys.platform.mobile.resp.TreeDetailResp;
 import cn.qinys.platform.mobile.resp.TreeListResp;
 import cn.qinys.platform.mobile.service.TreeService;
 import cn.qinys.platform.mobile.util.CoordUtil;
+import cn.qinys.platform.openfeign.UpmsFeignClient;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 /**
@@ -31,6 +40,10 @@ public class TreeServiceImpl implements TreeService {
     private WishingTreeMapper treeMapper;
     @Resource
     UserWishMapper wishMapper;
+    @Resource
+    UpmsFeignClient upmsFeignClient;
+    @Resource
+    ChatClient chatClient;
 
     private static final int EARTH_RADIUS = 6371000; // 地球半径(米)
 
@@ -79,14 +92,56 @@ public class TreeServiceImpl implements TreeService {
         return (int) Math.round(EARTH_RADIUS * c);
     }
 
-    private List<Integer> userTrees(){
+    @Async("mobileTaskExecutor")
+    @Override
+    public void create(TreeCreateReq req) {
+        TreeAddressReq addressReq = new TreeAddressReq();
+        addressReq.setLatitude(req.getLatitude());
+        addressReq.setLongitude(req.getLongitude());
+        String address = upmsFeignClient.getAddressByAltitudeAndLongitude(addressReq).getData();
+        String description  = this.getDescription(address);
+        WishingTree tree = new WishingTree();
+        tree.setName(req.getName());
+        tree.setAddress(address);
+        tree.setDescription(description);
+        tree.setSummary(description);
+        tree.setLongitude(req.getLongitude());
+        tree.setLatitude(req.getLatitude());
+        tree.setRadius(req.getRadius() != null ? req.getRadius() : 100);
+        tree.setCoverImage(req.getCoverImage());
+        tree.setIsActive(1);
+        treeMapper.insert(tree);
+    }
+
+    @Override
+    public TreeAddressResp getAddress(TreeAddressReq req) {
+        String address = upmsFeignClient.getAddressByAltitudeAndLongitude(req).getData();
+        TreeAddressResp addressResp = new TreeAddressResp();
+        addressResp.setAddress(address);
+        addressResp.setDescription(this.getDescription(address));
+        return addressResp;
+    }
+
+    private List<Integer> userTrees() {
         Integer userId = Integer.valueOf(CurrentUtils.getCurrentUserId());
         List<Integer> trees = wishMapper.selectUserTrees(userId);
-        if(trees == null || trees.isEmpty()){
+        if (trees == null || trees.isEmpty()) {
             trees = new ArrayList<>();
             trees.add(0);
         }
         return trees;
 
     }
+
+    private String getDescription(String address) {
+        return chatClient.prompt("""
+                        你是一个人文地理知识库助手,用户给你一个地址,你根据地址信息给出200 - 300字左右的描述。
+                        如果地址是一座城市,给出城市的描述信息如历史,人文景观等;
+                        如果是一个小区,简单介绍小区的信息,如开发商,小区建成时间,小区周边等;
+                        如果是一个公司,介绍公司的情况,如果是一个旅游景点,也请给出一些描述信息
+                        """).user(address)
+                .advisors(a -> a.param("chat_memory_conversation_id", UUID.randomUUID().toString()))
+                .call().content();
+    }
+
 }

+ 31 - 0
wishing-platform/platform-service/platform-service-mobile/src/main/java/cn/qinys/platform/openfeign/UpmsFeignClient.java

@@ -0,0 +1,31 @@
+package cn.qinys.platform.openfeign;
+
+
+import cn.qinys.platform.base.constants.ServiceConstants;
+import cn.qinys.platform.base.response.Result;
+import cn.qinys.platform.config.FeignConfiguration;
+import cn.qinys.platform.mobile.req.TreeAddressReq;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-05-24 14:12
+ **/
+@Component
+@FeignClient(name = ServiceConstants.UPMS_SERVICE,
+        configuration = FeignConfiguration.class)
+public interface UpmsFeignClient {
+
+
+    /**
+     * 根据经纬度获取地址信息
+     *
+     * @return 无
+     */
+    @PostMapping("/upms/gaodekey/address")
+    Result<String> getAddressByAltitudeAndLongitude(@RequestBody TreeAddressReq req);
+}

+ 2 - 1
wishing-tree-h5/src/api/auth.ts

@@ -24,7 +24,7 @@ export async function sendSms(phone: string) {
 }
 
 export async function login(phone: string, code: string, captcha: string, captchaKey: string) {
-  const res = await request<{ token: string; mobile: string; nickname: string; avatar: string }>(
+  const res = await request<{ token: string; mobile: string; nickname: string; avatar: string; isAdmin: number }>(
     '/mobile/user/login',
     {
       method: 'POST',
@@ -36,5 +36,6 @@ export async function login(phone: string, code: string, captcha: string, captch
     phone: res.data.mobile,
     nickname: res.data.nickname,
     avatarUrl: res.data.avatar || '',
+    isAdmin: res.data.isAdmin || 0,
   }
 }

+ 16 - 0
wishing-tree-h5/src/api/tree.ts

@@ -25,3 +25,19 @@ export async function fetchTreeDetail(id: number, lng?: number, lat?: number) {
   })
   return res.data
 }
+
+export async function createTree(data: {
+  name: string
+  description: string
+  longitude: number
+  latitude: number
+  address: string
+  radius: number
+  coverImage: string
+}) {
+  const res = await request<any>('/mobile/admin/create/tree', {
+    method: 'POST',
+    body: JSON.stringify(data),
+  })
+  return res.data
+}

+ 7 - 2
wishing-tree-h5/src/stores/user.ts

@@ -7,18 +7,21 @@ export const useUserStore = defineStore('user', () => {
   const nickname = ref(getStorage('nickname') || '游客')
   const phone = ref(getStorage('phone') || '')
   const avatarUrl = ref(getStorage('avatarUrl') || '')
+  const isAdmin = ref(getStorage('isAdmin') === '1')
 
   const isLoggedIn = computed(() => !!token.value)
 
-  function login(userData: { token: string; nickname: string; phone: string; avatarUrl?: string }) {
+  function login(userData: { token: string; nickname: string; phone: string; avatarUrl?: string; isAdmin?: number }) {
     token.value = userData.token
     nickname.value = userData.nickname
     phone.value = userData.phone
     avatarUrl.value = userData.avatarUrl || ''
+    isAdmin.value = userData.isAdmin === 1
     setStorage('token', userData.token)
     setStorage('nickname', userData.nickname)
     setStorage('phone', userData.phone)
     setStorage('avatarUrl', userData.avatarUrl || '')
+    setStorage('isAdmin', String(userData.isAdmin || 0))
   }
 
   function logout() {
@@ -26,11 +29,13 @@ export const useUserStore = defineStore('user', () => {
     nickname.value = '游客'
     phone.value = ''
     avatarUrl.value = ''
+    isAdmin.value = false
     removeStorage('token')
     removeStorage('nickname')
     removeStorage('phone')
     removeStorage('avatarUrl')
+    removeStorage('isAdmin')
   }
 
-  return { token, nickname, phone, avatarUrl, isLoggedIn, login, logout }
+  return { token, nickname, phone, avatarUrl, isAdmin, isLoggedIn, login, logout }
 })

+ 114 - 1
wishing-tree-h5/src/views/MapView.vue

@@ -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>

BIN
wishing-tree-h5/wish.zip