Эх сурвалжийг харах

feat: 许愿树 H5 前端项目初始化

- Vue 3 + Vite + TypeScript + Vant UI 移动端 H5
- 高德地图 JS SDK 集成
- Mock 数据:8 棵许愿树 + 10 条愿望
- 核心页面:首页、地图、许愿树详情、许愿、我的愿望、愿望详情、登录、个人中心
- 支持拍照/选图/压缩、GPS 定位、标签选择、公开/私有设置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tanlie 1 сар өмнө
commit
732bc251b0
48 өөрчлөгдсөн 6352 нэмэгдсэн , 0 устгасан
  1. 7 0
      .gitignore
  2. 1329 0
      plan-v1.md
  3. 24 0
      wishing-tree-h5/.gitignore
  4. 3 0
      wishing-tree-h5/.vscode/extensions.json
  5. 5 0
      wishing-tree-h5/README.md
  6. 42 0
      wishing-tree-h5/components.d.ts
  7. 13 0
      wishing-tree-h5/index.html
  8. 2357 0
      wishing-tree-h5/package-lock.json
  9. 31 0
      wishing-tree-h5/package.json
  10. 0 0
      wishing-tree-h5/public/favicon.svg
  11. 24 0
      wishing-tree-h5/public/icons.svg
  12. 30 0
      wishing-tree-h5/src/App.vue
  13. 18 0
      wishing-tree-h5/src/api/auth.ts
  14. 38 0
      wishing-tree-h5/src/api/request.ts
  15. 9 0
      wishing-tree-h5/src/api/tree.ts
  16. 8 0
      wishing-tree-h5/src/api/upload.ts
  17. 33 0
      wishing-tree-h5/src/api/wish.ts
  18. 0 0
      wishing-tree-h5/src/assets/vite.svg
  19. 8 0
      wishing-tree-h5/src/components/BottomNav.vue
  20. 129 0
      wishing-tree-h5/src/components/ImageUploader.vue
  21. 38 0
      wishing-tree-h5/src/components/TagSelector.vue
  22. 104 0
      wishing-tree-h5/src/components/TreeCard.vue
  23. 109 0
      wishing-tree-h5/src/components/WishCard.vue
  24. 13 0
      wishing-tree-h5/src/main.ts
  25. 293 0
      wishing-tree-h5/src/mock/data.ts
  26. 38 0
      wishing-tree-h5/src/mock/tree.ts
  27. 62 0
      wishing-tree-h5/src/mock/wish.ts
  28. 49 0
      wishing-tree-h5/src/router/index.ts
  29. 16 0
      wishing-tree-h5/src/stores/location.ts
  30. 36 0
      wishing-tree-h5/src/stores/user.ts
  31. 50 0
      wishing-tree-h5/src/style.css
  32. 23 0
      wishing-tree-h5/src/utils/amap.ts
  33. 53 0
      wishing-tree-h5/src/utils/camera.ts
  34. 13 0
      wishing-tree-h5/src/utils/geo.ts
  35. 19 0
      wishing-tree-h5/src/utils/storage.ts
  36. 20 0
      wishing-tree-h5/src/utils/theme.ts
  37. 135 0
      wishing-tree-h5/src/views/HomeView.vue
  38. 127 0
      wishing-tree-h5/src/views/LoginView.vue
  39. 184 0
      wishing-tree-h5/src/views/MakeWishView.vue
  40. 289 0
      wishing-tree-h5/src/views/MapView.vue
  41. 68 0
      wishing-tree-h5/src/views/MyWishesView.vue
  42. 150 0
      wishing-tree-h5/src/views/TreeDetailView.vue
  43. 128 0
      wishing-tree-h5/src/views/UserView.vue
  44. 158 0
      wishing-tree-h5/src/views/WishDetailView.vue
  45. 15 0
      wishing-tree-h5/tsconfig.app.json
  46. 7 0
      wishing-tree-h5/tsconfig.json
  47. 24 0
      wishing-tree-h5/tsconfig.node.json
  48. 23 0
      wishing-tree-h5/vite.config.ts

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+node_modules/
+dist/
+.env
+*.log
+.DS_Store
+Thumbs.db
+.claude/

+ 1329 - 0
plan-v1.md

@@ -0,0 +1,1329 @@
+# 许愿树应用开发计划 — Plan B (H5)
+
+## 一、项目背景与需求分析
+
+### 核心痛点
+传统物理许愿树的问题:
+- 愿望挂在树上,风吹雨淋后损坏丢失
+- 无法长期保存和回顾
+- 无法记录许愿者的位置信息
+- 无法与他人分享互动
+
+### 解决方案
+开发线上许愿树 H5 应用,结合 LBS(基于位置的服务):
+- 用户在许愿树附近开启定位
+- 拍照上传 + 文字记录愿望
+- 永久保存许愿记录
+- 支持查看历史愿望和位置
+
+### 目标用户
+- 景区游客
+- 寺庙/公园访客
+- 任何有许愿树场景的场所
+
+---
+
+## 二、方案对比:Plan A vs Plan B
+
+| 维度 | Plan A:微信小程序 | Plan B:H5(推荐) |
+|------|-------------------|-------------------|
+| 访问方式 | 微信内扫码/搜索 | 扫码/链接/浏览器打开 |
+| 平台限制 | 仅微信内 | 微信、浏览器、其他App均可 |
+| 审核 | 需微信审核(1-7天) | 无审核,上线自由 |
+| 用户登录 | wx.login 静默登录 | 手机号/微信OAuth授权 |
+| 定位 | wx.getLocation | navigator.geolocation |
+| 拍照 | wx.chooseImage | input[type=file] capture |
+| 地图 | 腾讯地图小程序SDK | 高德地图 JS SDK |
+| 分享 | 微信转发 | Web Share / 复制链接 |
+| 离线 | 小程序缓存 | PWA Service Worker |
+| 开发成本 | 中(需学小程序) | 低(标准前端技能) |
+| 运营灵活性 | 低(受微信约束) | 高(完全自主) |
+| iOS/Android | 统一体验 | 统一体验 |
+| 年费 | 300元认证费 | 0元 |
+
+### 决策:推荐 Plan B(H5)
+
+理由:
+1. **跨平台**:微信内、浏览器、QQ、微博皆可打开,流量渠道更广
+2. **无审核**:迭代更新无需等待微信审核,bug 修复即时生效
+3. **低成本**:无微信认证年费,标准前端技术栈,团队更好招聘
+4. **灵活运营**:不受微信诱导分享等规则限制
+5. **景区场景适配**:景区可在票根印二维码、指示牌贴二维码,用户扫码即用
+
+---
+
+## 三、系统架构设计
+
+### 技术栈
+| 层级 | 技术 | 说明 |
+|------|------|------|
+| 前端 | Vue 3 + Vant UI + TypeScript | 移动端 H5 框架 |
+| 构建 | Vite | 极速开发体验 |
+| 路由 | Vue Router 4 | Hash 模式(兼容性好) |
+| 状态管理 | Pinia | Vue 3 官方推荐 |
+| HTTP | Axios | 请求封装 |
+| 地图 | 高德地图 JS SDK 2.0 | H5 地图展示 |
+| 后端 | Java 17 + Spring Boot 3.x | RESTful API |
+| 数据库 | MySQL 8.0 | 持久化存储 |
+| 缓存 | Redis 6.0+ | GEO 地理位置查询 |
+| ORM | MyBatis-Plus | 数据访问层 |
+| 文件存储 | 腾讯云 COS | 图片存储 |
+| 部署 | Nginx + Docker | H5 静态资源 + 后端服务 |
+
+### 系统架构图
+```
+┌─────────────────────────────────────────────────────────────┐
+│                     H5 前端 (Vue 3 + Vant)                    │
+│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
+│  │  首页    │ │ 地图页   │ │ 许愿页   │ │ 我的     │       │
+│  │ (许愿树) │ │ (LBS)   │ │ (拍照)   │ │ (历史)   │       │
+│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
+└─────────────────────────┬───────────────────────────────────┘
+                          │ HTTPS
+┌─────────────────────────▼───────────────────────────────────┐
+│                     Nginx (静态资源 + 反向代理)               │
+└─────────────────────────┬───────────────────────────────────┘
+                          │
+┌─────────────────────────▼───────────────────────────────────┐
+│                   Spring Boot 后端服务 (Docker)              │
+│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
+│  │ Controller   │ │   Service    │ │    Mapper    │        │
+│  └──────────────┘ └──────────────┘ └──────────────┘        │
+└─────────────────────────┬───────────────────────────────────┘
+                          │
+┌─────────────────────────▼───────────────────────────────────┐
+│                       数据层                                 │
+│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐        │
+│  │    MySQL     │ │     COS      │ │    Redis     │        │
+│  │ (业务数据)   │ │  (图片存储)  │ │ (GEO/缓存)   │        │
+│  └──────────────┘ └──────────────┘ └──────────────┘        │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 四、数据存储设计
+
+### 4.1 存储分层架构
+
+| 存储 | 角色 | 数据内容 |
+|------|------|----------|
+| MySQL | 持久化 | 用户、许愿树、愿望、点赞 |
+| Redis GEO | 地理位置计算 | 许愿树经纬度、附近查询 |
+| Redis Hash | 热点缓存 | 许愿树详情、愿望列表 |
+| COS | 图片存储 | 许愿树封面、用户上传图片 |
+
+### 4.2 Redis Key 设计
+
+```
+# GEO 数据结构 — 存储所有许愿树位置
+geo:wishing:trees       →  GEO { member:treeId, lng, lat }
+
+# Hash — 缓存许愿树详情(TTL 24h)
+wishing:tree:{treeId}   →  Hash { name, coverImage, radius, totalWishes, ... }
+
+# String — 热点计数(TTL 永久,定时同步到 MySQL)
+wish:count:{treeId}     →  许愿树愿望总数
+wish:count:daily:{treeId}:{date}  →  当日许愿数
+
+# String — 愿望详情缓存(TTL 1h)
+wish:detail:{wishId}    →  JSON
+```
+
+### 4.3 数据同步策略
+
+```
+启动初始化:
+  ApplicationRunner → 从 MySQL 全量加载许愿树 → Redis GEO + Hash
+
+增量写入:
+  创建/更新许愿树 → MySQL 写入 → 同步更新 Redis GEO + Hash
+  创建愿望 → MySQL 写入 → 更新 wish:count 计数
+
+定时补偿:
+  每 5 分钟 → 对比 MySQL 和 Redis 的许愿树数据 → 修复差异
+  每小时   → 将 wish:count 回写 MySQL total_wishes 字段
+```
+
+### 4.4 MySQL 表结构
+
+#### 许愿树表 (wishing_tree)
+```sql
+CREATE TABLE wishing_tree (
+    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
+    name         VARCHAR(100) NOT NULL COMMENT '许愿树名称',
+    description  TEXT COMMENT '描述',
+    longitude    DECIMAL(10, 7) NOT NULL COMMENT '经度',
+    latitude     DECIMAL(10, 7) NOT NULL COMMENT '纬度',
+    address      VARCHAR(255) COMMENT '详细地址',
+    radius       INT DEFAULT 100 COMMENT '有效范围(米)',
+    cover_image  VARCHAR(500) COMMENT '封面图URL',
+    total_wishes INT DEFAULT 0 COMMENT '总许愿数(Redis定时同步)',
+    is_active    TINYINT DEFAULT 1 COMMENT '是否启用',
+    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_active (is_active)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿树表';
+```
+
+#### 愿望表 (wish)
+```sql
+CREATE TABLE wish (
+    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
+    tree_id      BIGINT NOT NULL COMMENT '许愿树ID',
+    user_id      BIGINT NOT NULL COMMENT '用户ID',
+    content      TEXT NOT NULL COMMENT '愿望内容',
+    images       JSON COMMENT '图片URL数组',
+    longitude    DECIMAL(10, 7) COMMENT '许愿时经度',
+    latitude     DECIMAL(10, 7) COMMENT '许愿时纬度',
+    address      VARCHAR(255) COMMENT '地址描述',
+    is_public    TINYINT DEFAULT 1 COMMENT '是否公开',
+    tags         JSON COMMENT '标签JSON数组',
+    likes        INT DEFAULT 0 COMMENT '点赞数',
+    status       TINYINT DEFAULT 0 COMMENT '0生效 1已实现',
+    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_tree (tree_id),
+    INDEX idx_user (user_id),
+    INDEX idx_created (created_at),
+    FOREIGN KEY (tree_id) REFERENCES wishing_tree(id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='愿望表';
+```
+
+#### 用户表 (user)
+```sql
+CREATE TABLE user (
+    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
+    phone        VARCHAR(20) COMMENT '手机号',
+    open_id      VARCHAR(64) COMMENT '微信OpenID(可选)',
+    nickname     VARCHAR(100) COMMENT '昵称',
+    avatar_url   VARCHAR(500) COMMENT '头像URL',
+    total_wishes INT DEFAULT 0 COMMENT '总许愿数',
+    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_phone (phone),
+    UNIQUE KEY uk_open_id (open_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
+```
+
+---
+
+## 五、Redis GEO 核心服务
+
+### 5.1 Redis 配置
+```java
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(factory);
+
+        StringRedisSerializer stringSerializer = new StringRedisSerializer();
+        template.setKeySerializer(stringSerializer);
+        template.setHashKeySerializer(stringSerializer);
+
+        GenericJackson2JsonRedisSerializer jsonSerializer =
+            new GenericJackson2JsonRedisSerializer();
+        template.setValueSerializer(jsonSerializer);
+        template.setHashValueSerializer(jsonSerializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
+        return new StringRedisTemplate(factory);
+    }
+}
+```
+
+### 5.2 GEO 服务完整实现
+```java
+@Service
+@Slf4j
+public class WishingTreeGeoService {
+
+    private static final String GEO_KEY = "geo:wishing:trees";
+    private static final String TREE_CACHE_PREFIX = "wishing:tree:";
+    private static final String WISH_COUNT_PREFIX = "wish:count:";
+
+    @Autowired
+    private StringRedisTemplate redisTemplate;
+    @Autowired
+    private WishingTreeMapper treeMapper;
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    // ==================== 初始化加载 ====================
+
+    @EventListener(ApplicationReadyEvent.class)
+    public void initGeoData() {
+        log.info("开始加载许愿树到 Redis GEO...");
+        redisTemplate.delete(GEO_KEY);
+
+        List<WishingTree> trees = treeMapper.selectList(
+            new LambdaQueryWrapper<WishingTree>().eq(WishingTree::getIsActive, true)
+        );
+
+        if (!trees.isEmpty()) {
+            batchAdd(trees);
+            log.info("已加载 {} 棵许愿树到 Redis", trees.size());
+        }
+    }
+
+    // ==================== 许愿树管理 ====================
+
+    /** 批量添加许愿树到 GEO + Hash */
+    public void batchAdd(List<WishingTree> trees) {
+        // 写入 GEO
+        Map<String, Point> points = new HashMap<>();
+        for (WishingTree t : trees) {
+            points.put(t.getId().toString(),
+                new Point(t.getLongitude().doubleValue(), t.getLatitude().doubleValue()));
+        }
+        redisTemplate.opsForGeo().add(GEO_KEY, points);
+
+        // 写入 Hash 缓存
+        Map<String, Object> pipe = redisTemplate.opsForHash().getOperations()
+            .executePipelined((RedisCallback<Object>) connection -> {
+                for (WishingTree t : trees) {
+                    String key = TREE_CACHE_PREFIX + t.getId();
+                    Map<String, String> map = treeToHash(t);
+                    redisTemplate.opsForHash().putAll(key, map);
+                    redisTemplate.expire(key, Duration.ofHours(24));
+                }
+                return null;
+            });
+    }
+
+    /** 新增许愿树 */
+    public void addTree(WishingTree tree) {
+        // MySQL
+        treeMapper.insert(tree);
+
+        // Redis: GEO + Hash
+        redisTemplate.opsForGeo().add(GEO_KEY,
+            new Point(tree.getLongitude().doubleValue(), tree.getLatitude().doubleValue()),
+            tree.getId().toString());
+        cacheTree(tree);
+    }
+
+    /** 更新许愿树 */
+    public void updateTree(WishingTree tree) {
+        treeMapper.updateById(tree);
+
+        // 移除旧位置 + 添加新位置
+        redisTemplate.opsForGeo().remove(GEO_KEY, tree.getId().toString());
+        redisTemplate.opsForGeo().add(GEO_KEY,
+            new Point(tree.getLongitude().doubleValue(), tree.getLatitude().doubleValue()),
+            tree.getId().toString());
+        cacheTree(tree);
+    }
+
+    /** 删除许愿树 */
+    public void removeTree(Long treeId) {
+        treeMapper.update(new LambdaUpdateWrapper<WishingTree>()
+            .eq(WishingTree::getId, treeId)
+            .set(WishingTree::getIsActive, false));
+
+        redisTemplate.opsForGeo().remove(GEO_KEY, treeId.toString());
+        redisTemplate.delete(TREE_CACHE_PREFIX + treeId);
+    }
+
+    // ==================== 附近查询 ====================
+
+    /**
+     * 查询附近的许愿树 — 核心接口
+     *
+     * @param lng         用户经度
+     * @param lat         用户纬度
+     * @param maxDistance 最大距离(米) 默认5000
+     * @param limit       最大返回数
+     */
+    public List<NearbyTreeVO> findNearby(double lng, double lat,
+                                          double maxDistance, int limit) {
+        GeoResults<GeoLocation<String>> results = redisTemplate.opsForGeo()
+            .search(GEO_KEY,
+                new GeoReference.Point(lng, lat),
+                new Distance(maxDistance, Metrics.METERS),
+                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
+                    .includeDistance()
+                    .sortAscending()
+                    .limit(limit));
+
+        if (results == null) {
+            return Collections.emptyList();
+        }
+
+        List<NearbyTreeVO> list = new ArrayList<>();
+        for (GeoResult<GeoLocation<String>> r : results) {
+            Long treeId = Long.valueOf(r.getContent().getName());
+            int distance = (int) r.getDistance().getValue();
+
+            // 从 Hash 缓存读详情
+            WishingTree tree = getTreeCache(treeId);
+            if (tree == null || !tree.getIsActive()) continue;
+
+            NearbyTreeVO vo = new NearbyTreeVO();
+            BeanUtils.copyProperties(tree, vo);
+            vo.setId(treeId);
+            vo.setDistance(distance);
+            vo.setIsInRange(distance <= tree.getRadius());
+            list.add(vo);
+        }
+        return list;
+    }
+
+    /**
+     * 计算用户到指定许愿树的距离(用于许愿前置校验)
+     */
+    public int distanceToTree(Long treeId, double userLng, double userLat) {
+        // 方法1:Redis 内置(Haversine,误差 < 1%)
+        Distance distance = redisTemplate.opsForGeo().distance(GEO_KEY,
+            treeId.toString(),
+            userLng + ":" + userLat, // 其他 member 或转换
+            Metrics.METERS);
+
+        if (distance != null) {
+            return (int) distance.getValue();
+        }
+
+        // 方法2:Java 计算(兜底)
+        WishingTree tree = getTreeCache(treeId);
+        if (tree == null) return Integer.MAX_VALUE;
+
+        return (int) haversine(userLng, userLat,
+            tree.getLongitude().doubleValue(), tree.getLatitude().doubleValue());
+    }
+
+    // ==================== 缓存操作 ====================
+
+    public void cacheTree(WishingTree tree) {
+        String key = TREE_CACHE_PREFIX + tree.getId();
+        Map<String, String> map = treeToHash(tree);
+        redisTemplate.opsForHash().putAll(key, map);
+        redisTemplate.expire(key, Duration.ofHours(24));
+    }
+
+    public WishingTree getTreeCache(Long treeId) {
+        String key = TREE_CACHE_PREFIX + treeId;
+        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
+
+        if (entries.isEmpty()) {
+            // 缓存未命中,查 MySQL 后回填
+            WishingTree tree = treeMapper.selectById(treeId);
+            if (tree != null) cacheTree(tree);
+            return tree;
+        }
+        return hashToTree(entries, treeId);
+    }
+
+    // ==================== 愿望计数 ====================
+
+    /** 许愿成功后递增计数 */
+    public void incrWishCount(Long treeId) {
+        String key = WISH_COUNT_PREFIX + treeId;
+        redisTemplate.opsForValue().increment(key);
+    }
+
+    /** 定时任务:回写计数到 MySQL */
+    @Scheduled(cron = "0 0 * * * ?") // 每小时
+    public void syncWishCount() {
+        Set<String> keys = redisTemplate.keys(WISH_COUNT_PREFIX + "*");
+        if (keys == null) return;
+
+        for (String key : keys) {
+            Long treeId = Long.valueOf(key.replace(WISH_COUNT_PREFIX, ""));
+            String count = redisTemplate.opsForValue().get(key);
+            if (count != null) {
+                treeMapper.update(null, new LambdaUpdateWrapper<WishingTree>()
+                    .eq(WishingTree::getId, treeId)
+                    .set(WishingTree::getTotalWishes, Integer.valueOf(count)));
+            }
+        }
+    }
+
+    // ==================== 工具方法 ====================
+
+    private Map<String, String> treeToHash(WishingTree tree) {
+        Map<String, String> map = new HashMap<>();
+        map.put("id",          tree.getId().toString());
+        map.put("name",        tree.getName());
+        map.put("description", tree.getDescription() == null ? "" : tree.getDescription());
+        map.put("longitude",   tree.getLongitude().toString());
+        map.put("latitude",    tree.getLatitude().toString());
+        map.put("address",     tree.getAddress() == null ? "" : tree.getAddress());
+        map.put("radius",      tree.getRadius().toString());
+        map.put("coverImage",  tree.getCoverImage() == null ? "" : tree.getCoverImage());
+        map.put("totalWishes", tree.getTotalWishes().toString());
+        map.put("isActive",    tree.getIsActive().toString());
+        return map;
+    }
+
+    private WishingTree hashToTree(Map<Object, Object> map, Long treeId) {
+        WishingTree t = new WishingTree();
+        t.setId(treeId);
+        t.setName((String) map.get("name"));
+        t.setDescription((String) map.get("description"));
+        t.setLongitude(new BigDecimal((String) map.get("longitude")));
+        t.setLatitude(new BigDecimal((String) map.get("latitude")));
+        t.setAddress((String) map.get("address"));
+        t.setRadius(Integer.valueOf((String) map.get("radius")));
+        t.setCoverImage((String) map.get("coverImage"));
+        t.setTotalWishes(Integer.valueOf((String) map.get("totalWishes")));
+        t.setIsActive(Boolean.valueOf((String) map.get("isActive")));
+        return t;
+    }
+
+    /** Haversine 公式 — 计算球面距离 */
+    public static double haversine(double lng1, double lat1, double lng2, double lat2) {
+        final double R = 6371000; // 地球半径(米)
+        double dLat = Math.toRadians(lat2 - lat1);
+        double dLng = Math.toRadians(lng2 - lng1);
+        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+                 + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
+                 * Math.sin(dLng / 2) * Math.sin(dLng / 2);
+        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+    }
+}
+```
+
+---
+
+## 六、API 接口设计
+
+### 6.1 接口总览
+
+#### 用户模块
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /api/auth/send-sms | 发送短信验证码 |
+| POST | /api/auth/login | 手机号验证码登录 |
+| GET  | /api/user/profile | 获取用户信息 |
+| PUT  | /api/user/profile | 更新用户信息 |
+
+#### 许愿树模块
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET  | /api/trees/nearby | 附近许愿树(Redis GEO) |
+| GET  | /api/trees/{id} | 许愿树详情 |
+| GET  | /api/trees/{id}/wishes | 树下公开愿望(分页) |
+| POST | /api/trees | 新增许愿树(管理) |
+| PUT  | /api/trees/{id} | 更新许愿树(管理) |
+
+#### 愿望模块
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /api/wishes | 创建愿望 |
+| GET  | /api/wishes/my | 我的愿望列表(分页) |
+| GET  | /api/wishes/{id} | 愿望详情 |
+| DELETE | /api/wishes/{id} | 删除愿望 |
+| POST | /api/wishes/{id}/like | 点赞 |
+
+#### 文件模块
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | /api/upload/image | 上传图片(返回URL) |
+
+### 6.2 核心接口详解
+
+#### GET /api/trees/nearby — 附近许愿树
+```
+请求:
+  GET /api/trees/nearby?lng=116.4074&lat=39.9042&maxDistance=5000&limit=20
+
+响应:
+{
+  "code": 0,
+  "data": [
+    {
+      "id": 1,
+      "name": "千年银杏许愿树",
+      "distance": 150,                // 距离(米) — Redis GEOSEARCH 返回
+      "isInRange": true,              // 是否在可许愿范围内 — distance <= radius
+      "radius": 100,
+      "coverImage": "https://cos.xxx/tree/xxx.jpg",
+      "totalWishes": 9999,
+      "address": "北京市东城区XX公园"
+    }
+  ]
+}
+```
+
+#### POST /api/wishes — 许愿
+```
+请求:
+{
+  "treeId": 1,
+  "content": "希望家人身体健康,工作顺利",
+  "images": ["https://cos.xxx/wish/img1.jpg", "https://cos.xxx/wish/img2.jpg"],
+  "lng": 116.4074,
+  "lat": 39.9042,
+  "isPublic": true,
+  "tags": ["健康", "事业"]
+}
+
+响应:
+{
+  "code": 0,
+  "data": { "wishId": 123, "message": "许愿成功!" }
+}
+
+业务校验:
+  - treeId 对应的许愿树是否存在且启用
+  - 用户与许愿树的距离是否在 radius 范围内(Redis 计算)
+  - 用户当日许愿次数是否超限(可选)
+```
+
+---
+
+## 七、H5 前端设计
+
+### 7.1 工程化配置
+```
+wishing-tree-h5/
+├── package.json
+├── vite.config.ts
+├── tsconfig.json
+├── index.html                    # SPA 入口
+├── public/
+│   └── favicon.ico
+└── src/
+    ├── main.ts                   # 入口
+    ├── App.vue                   # 根组件
+    ├── router/
+    │   └── index.ts              # Vue Router (hash)
+    ├── stores/
+    │   ├── user.ts               # Pinia 用户状态
+    │   └── location.ts           # 定位状态
+    ├── api/
+    │   ├── request.ts            # Axios 封装 + 拦截器
+    │   ├── auth.ts               # 登录接口
+    │   ├── tree.ts               # 许愿树接口
+    │   └── wish.ts               # 愿望接口
+    ├── utils/
+    │   ├── geo.ts                # 定位工具
+    │   ├── camera.ts             # 拍照工具
+    │   └── storage.ts            # localStorage 封装
+    ├── components/
+    │   ├── TreeCard.vue          # 许愿树卡片
+    │   ├── WishCard.vue          # 愿望卡片
+    │   ├── ImageUploader.vue     # 图片上传组件
+    │   ├── TagSelector.vue       # 标签选择器
+    │   └── BottomNav.vue         # 底部导航
+    ├── views/
+    │   ├── HomeView.vue          # 首页 — 许愿树列表
+    │   ├── MapView.vue           # 地图页 — 许愿树分布
+    │   ├── TreeDetailView.vue    # 许愿树详情 + 公开愿望
+    │   ├── MakeWishView.vue      # 许愿页 — 拍照 + 输入
+    │   ├── MyWishesView.vue      # 我的愿望
+    │   ├── WishDetailView.vue    # 愿望详情
+    │   └── UserView.vue          # 个人中心
+    └── assets/
+        └── styles/
+            └── global.css
+```
+
+### 7.2 核心技术依赖
+```json
+{
+  "dependencies": {
+    "vue": "^3.4",
+    "vue-router": "^4.3",
+    "pinia": "^2.1",
+    "axios": "^1.6",
+    "vant": "^4.8",
+    "@vant/use": "^1.6",
+    "@amap/amap-jsapi-loader": "^1.0"
+  },
+  "devDependencies": {
+    "vite": "^5.2",
+    "typescript": "^5.4",
+    "@vitejs/plugin-vue": "^5.0",
+    "unplugin-vue-components": "^0.26",
+    "@vant/auto-import-resolver": "^1.2"
+  }
+}
+```
+
+### 7.3 页面设计与交互
+
+#### 首页 — HomeView.vue
+```
+┌──────────────────────────────────┐
+│        ✨ 许愿树                │
+│    🌸 找到你身边的许愿树        │
+├──────────────────────────────────┤
+│  ┌────────────────────────────┐  │
+│  │ 📍 正在获取位置...         │  │
+│  │ 或者                       │  │
+│  │ [手动选择城市 ▾]          │  │
+│  └────────────────────────────┘  │
+├──────────────────────────────────┤
+│  附近的许愿树 (3公里内)         │
+│                                  │
+│  ┌───────────┐                   │
+│  │           │ 千年银杏许愿树    │
+│  │  封面图   │ 📍 150m  ✅可许愿│
+│  │           │ 💬 9999个愿望    │
+│  └───────────┘                   │
+│                                  │
+│  ┌───────────┐                   │
+│  │           │ 祈福樱花树        │
+│  │  封面图   │ 📍 2.5km ❌太远  │
+│  │           │ 💬 888个愿望     │
+│  └───────────┘                   │
+│                                  │
+│  没有找到附近的许愿树?          │
+│  [ 查看全部许愿树 → ]            │
+├──────────────────────────────────┤
+│   🏠首页  🗺️地图  👤我的       │
+└──────────────────────────────────┘
+```
+
+#### 许愿页 — MakeWishView.vue
+```
+┌──────────────────────────────────┐
+│  ← 返回    许下心愿             │
+├──────────────────────────────────┤
+│  🌳 千年银杏许愿树               │
+│  📍 您距许愿树 50 米 ✅ 在范围内 │
+├──────────────────────────────────┤
+│  上传心愿照片(可选)            │
+│                                  │
+│  ┌────┐ ┌────┐ ┌────┐          │
+│  │ +  │ │ 📷 │ │ 📷 │          │
+│  │拍照│ │图片│ │图片│          │
+│  └────┘ └────┘ └────┘          │
+│  最多上传 9 张                  │
+├──────────────────────────────────┤
+│  ✏️ 写下你的愿望                │
+│  ┌────────────────────────────┐  │
+│  │                            │  │
+│  │ 希望家人身体健康...        │  │
+│  │                            │  │
+│  └────────────────────────────┘  │
+│  0/200                           │
+├──────────────────────────────────┤
+│  🏷️ 选择标签(可选)             │
+│  ┌────┐ ┌────┐ ┌────┐ ┌────┐   │
+│  │爱情│ │事业│ │健康│ │学业│   │
+│  └────┘ └────┘ └────┘ └────┘   │
+│  ┌────┐ ┌────┐ ┌────┐          │
+│  │财富│ │平安│ │祈福│ ...      │
+│  └────┘ └────┘ └────┘          │
+├──────────────────────────────────┤
+│  👁️ 公开愿望   [切换开关 ON]    │
+│     公开后其他游客也能看到       │
+├──────────────────────────────────┤
+│  ┌────────────────────────────┐  │
+│  │        🙏 许愿            │  │
+│  └────────────────────────────┘  │
+└──────────────────────────────────┘
+```
+
+### 7.4 核心业务流
+
+```
+用户打开 H5 页面 (扫码或链接)
+         │
+         ▼
+   请求定位权限
+    navigator.geolocation
+    ┌──────┴──────┐
+    ▼             ▼
+  允许          拒绝
+    │             │
+    ▼             ▼
+  获取GPS     手动选择城市/默认位置
+    │             │
+    └────┬────────┘
+         ▼
+   POST /api/trees/nearby
+   携带 lng, lat, maxDistance
+         │
+         ▼
+  后端调用 Redis GEOSEARCH
+  计算距离 + 判断是否在范围
+         │
+         ▼
+  返回许愿树列表
+  标记 distance / isInRange
+         │
+         ▼
+  用户点击许愿树
+    ┌────┴────┐
+    ▼         ▼
+ isInRange  !isInRange
+    │         │
+    ▼         ▼
+ 进入许愿页   提示"需靠近",仅可浏览
+    │
+    ▼
+ 拍照/选图 → 写愿望 → 选标签 → 设公开
+    │
+    ▼
+ POST /api/wishes
+ 后端校验距离 + 保存 + Redis计数+1
+    │
+    ▼
+ 许愿成功 → 可分享/可查看
+```
+
+---
+
+## 八、H5 定位与拍照实现
+
+### 8.1 定位工具 (utils/geo.ts)
+```typescript
+// 获取用户位置
+export function getUserLocation(): Promise<{ lng: number; lat: number }> {
+  return new Promise((resolve, reject) => {
+    // 优先使用浏览器 Geolocation API
+    if (!navigator.geolocation) {
+      reject(new Error('浏览器不支持定位'));
+      return;
+    }
+
+    navigator.geolocation.getCurrentPosition(
+      (position) => {
+        resolve({
+          lng: position.coords.longitude,
+          lat: position.coords.latitude,
+        });
+      },
+      (error) => {
+        switch (error.code) {
+          case error.PERMISSION_DENIED:
+            reject(new Error('用户拒绝了定位请求'));
+            break;
+          case error.TIMEOUT:
+            reject(new Error('定位超时'));
+            break;
+          default:
+            reject(new Error('定位失败'));
+        }
+      },
+      {
+        enableHighAccuracy: true,  // 高精度
+        timeout: 10000,            // 10秒超时
+        maximumAge: 300000,        // 5分钟内缓存
+      }
+    );
+  });
+}
+```
+
+### 8.2 拍照工具 (utils/camera.ts)
+```typescript
+// H5 拍照 — 使用 input[type=file] capture="environment"
+export function takePhoto(): Promise<File> {
+  return new Promise((resolve, reject) => {
+    const input = document.createElement('input');
+    input.type = 'file';
+    input.accept = 'image/*';
+    input.capture = 'environment'; // 调用后置摄像头
+
+    input.onchange = (e: Event) => {
+      const files = (e.target as HTMLInputElement).files;
+      if (files && files.length > 0) {
+        const file = files[0];
+        // 压缩图片(前端压缩后再上传)
+        compressImage(file).then(resolve).catch(reject);
+      } else {
+        reject(new Error('未选择图片'));
+      }
+    };
+
+    input.click();
+  });
+}
+
+// 从相册选择
+export function pickFromAlbum(multiple = false): Promise<File[]> {
+  return new Promise((resolve, reject) => {
+    const input = document.createElement('input');
+    input.type = 'file';
+    input.accept = 'image/*';
+    input.multiple = multiple;
+
+    input.onchange = (e: Event) => {
+      const files = (e.target as HTMLInputElement).files;
+      if (files && files.length > 0) {
+        const compressTasks = Array.from(files).map(compressImage);
+        Promise.all(compressTasks).then(resolve).catch(reject);
+      } else {
+        reject(new Error('未选择图片'));
+      }
+    };
+
+    input.click();
+  });
+}
+
+// Canvas 压缩图片(限制 1080px + 质量 0.8)
+export function compressImage(file: File): Promise<File> {
+  return new Promise((resolve) => {
+    const reader = new FileReader();
+    reader.onload = (e) => {
+      const img = new Image();
+      img.onload = () => {
+        const canvas = document.createElement('canvas');
+        const maxSize = 1080;
+        let { width, height } = img;
+
+        if (width > maxSize || height > maxSize) {
+          if (width > height) {
+            height = (height / width) * maxSize;
+            width = maxSize;
+          } else {
+            width = (width / height) * maxSize;
+            height = maxSize;
+          }
+        }
+
+        canvas.width = width;
+        canvas.height = height;
+        const ctx = canvas.getContext('2d')!;
+        ctx.drawImage(img, 0, 0, width, height);
+
+        canvas.toBlob(
+          (blob) => {
+            resolve(new File([blob!], file.name, { type: 'image/jpeg' }));
+          },
+          'image/jpeg',
+          0.8
+        );
+      };
+      img.src = e.target!.result as string;
+    };
+    reader.readAsDataURL(file);
+  });
+}
+```
+
+### 8.3 高德地图 JS SDK 集成 (utils/amap.ts)
+
+#### 初始化
+
+```typescript
+// utils/amap.ts
+import AMapLoader from '@amap/amap-jsapi-loader';
+
+// 高德地图 Key(从环境变量获取)
+const AMAP_KEY = import.meta.env.VITE_AMAP_KEY;
+// 安全密钥(2.0 版本需要)
+const AMAP_SECRET = import.meta.env.VITE_AMAP_SECRET;
+
+let AMapInstance: any = null;
+
+/** 加载高德地图 JS SDK(单例) */
+export function loadAMap(): Promise<any> {
+  if (AMapInstance) return Promise.resolve(AMapInstance);
+
+  // 2.0 版本需要配置安全密钥
+  (window as any)._AMapSecurityConfig = {
+    securityJsCode: AMAP_SECRET,
+  };
+
+  return AMapLoader.load({
+    key: AMAP_KEY,
+    version: '2.0',
+    plugins: [
+      'AMap.Geolocation',    // 定位
+      'AMap.Scale',          // 比例尺
+      'AMap.ToolBar',        // 工具条
+      'AMap.Marker',         // 标记点
+      'AMap.InfoWindow',     // 信息窗体
+    ],
+  }).then((AMap: any) => {
+    AMapInstance = AMap;
+    return AMap;
+  });
+}
+```
+
+#### 地图组件 (MapView.vue)
+
+```vue
+<!-- views/MapView.vue -->
+<template>
+  <div class="map-page">
+    <!-- 地图容器 -->
+    <div ref="mapContainer" class="map-container"></div>
+
+    <!-- 定位按钮 -->
+    <van-button
+      class="locate-btn"
+      icon="aim"
+      round
+      type="primary"
+      @click="locateUser"
+    >
+      重新定位
+    </van-button>
+
+    <!-- 许愿树信息卡片(点击标记弹出) -->
+    <van-popup
+      v-model:show="showTreePopup"
+      position="bottom"
+      round
+      :style="{ maxHeight: '40%' }"
+    >
+      <div class="tree-popup" v-if="selectedTree">
+        <img :src="selectedTree.coverImage" class="tree-cover" />
+        <h3>{{ selectedTree.name }}</h3>
+        <p class="tree-address">{{ selectedTree.address }}</p>
+        <p class="tree-distance">
+          距离你 {{ selectedTree.distance }} 米
+          <van-tag :type="selectedTree.isInRange ? 'success' : 'warning'">
+            {{ selectedTree.isInRange ? '可许愿' : '太远了' }}
+          </van-tag>
+        </p>
+        <van-button
+          type="primary"
+          block
+          :disabled="!selectedTree.isInRange"
+          @click="goMakeWish"
+        >
+          {{ selectedTree.isInRange ? '去许愿' : '需要靠近许愿树' }}
+        </van-button>
+      </div>
+    </van-popup>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, nextTick } from 'vue';
+import { useRouter } from 'vue-router';
+import { showToast } from 'vant';
+import { loadAMap } from '@/utils/amap';
+import { getUserLocation } from '@/utils/geo';
+import { getNearbyTrees } from '@/api/tree';
+
+const router = useRouter();
+
+const mapContainer = ref<HTMLDivElement>();
+const showTreePopup = ref(false);
+const selectedTree = ref<any>(null);
+
+let map: any = null;
+let markers: any[] = [];
+
+onMounted(async () => {
+  await nextTick();
+  await initMap();
+});
+
+onUnmounted(() => {
+  if (map) map.destroy();
+});
+
+/** 初始化地图 + 加载许愿树 */
+async function initMap() {
+  const AMap = await loadAMap();
+
+  // 创建地图
+  map = new AMap.Map(mapContainer.value!, {
+    zoom: 14,
+    center: [116.397428, 39.90923], // 默认中心(北京)
+  });
+
+  // 添加控件
+  map.addControl(new AMap.Scale());
+  map.addControl(new AMap.ToolBar({ position: 'RT' }));
+
+  // 定位用户位置
+  await locateUser();
+}
+
+/** 定位用户并加载附近许愿树 */
+async function locateUser() {
+  try {
+    const { lng, lat } = await getUserLocation();
+    const AMap = await loadAMap();
+
+    // 移动地图中心
+    map.setCenter([lng, lat]);
+
+    // 添加用户位置标记
+    const userMarker = new AMap.Marker({
+      position: [lng, lat],
+      icon: new AMap.Icon({
+        size: new AMap.Size(24, 24),
+        image: '/marker-user.png',
+        imageSize: new AMap.Size(24, 24),
+      }),
+      zIndex: 100,
+    });
+    map.add(userMarker);
+
+    // 加载附近许愿树
+    const trees = await getNearbyTrees(lng, lat, 5000, 50);
+    renderTreeMarkers(AMap, trees);
+
+    // 自适应显示所有标记
+    map.setFitView(null, true, [100, 100, 100, 100]);
+  } catch {
+    showToast('定位失败,请开启位置权限');
+  }
+}
+
+/** 渲染许愿树标记 */
+function renderTreeMarkers(AMap: any, trees: any[]) {
+  // 清除旧标记
+  markers.forEach((m) => map.remove(m));
+  markers = [];
+
+  trees.forEach((tree) => {
+    const content = `
+      <div class="tree-marker" style="
+        text-align:center;
+        background:${tree.isInRange ? '#07c160' : '#ff976a'};
+        color:#fff;
+        padding:4px 8px;
+        border-radius:12px;
+        font-size:12px;
+        white-space:nowrap;
+        box-shadow:0 2px 6px rgba(0,0,0,0.3);
+      ">
+        ${tree.name}<br/>
+        <span style="font-size:10px">${tree.distance}m</span>
+      </div>`;
+
+    const marker = new AMap.Marker({
+      position: [tree.longitude, tree.latitude],
+      content: content,
+      anchor: 'bottom-center',
+    });
+
+    // 点击弹出详情
+    marker.on('click', () => {
+      selectedTree.value = tree;
+      showTreePopup.value = true;
+    });
+
+    marker.setMap(map);
+    markers.push(marker);
+  });
+}
+
+function goMakeWish() {
+  showTreePopup.value = false;
+  router.push({ path: '/make-wish', query: { treeId: selectedTree.value.id } });
+}
+</script>
+
+<style scoped>
+.map-page {
+  position: relative;
+  width: 100%;
+  height: 100vh;
+}
+.map-container {
+  width: 100%;
+  height: 100%;
+}
+.locate-btn {
+  position: absolute;
+  bottom: 80px;
+  right: 16px;
+  z-index: 10;
+}
+.tree-popup {
+  padding: 20px;
+}
+.tree-cover {
+  width: 100%;
+  height: 160px;
+  object-fit: cover;
+  border-radius: 8px;
+}
+.tree-address {
+  color: #999;
+  font-size: 14px;
+}
+.tree-distance {
+  margin: 12px 0;
+}
+</style>
+```
+
+#### 环境变量配置
+```env
+# .env
+VITE_AMAP_KEY=your_amap_js_api_key
+VITE_AMAP_SECRET=your_amap_security_code
+```
+
+> **高德地图 Key 申请**:前往 [高德开放平台](https://lbs.amap.com/) 创建应用 → 添加 Key → 选择「Web端(JS API)」→ 获取 Key 和安全密钥。
+
+### 8.4 微信环境下的 JS-SDK 增强(可选)
+
+在微信浏览器中打开时,可接入微信 JS-SDK 获得更好的体验:
+- `wx.getLocation()` — 更精确的微信定位
+- `wx.chooseImage()` — 直接调起相机
+- `wx.onMenuShareTimeline()` — 自定义分享到朋友圈
+
+```typescript
+// 判断是否在微信中
+export function isWechat(): boolean {
+  return /micromessenger/i.test(navigator.userAgent);
+}
+
+// 微信环境使用 JS-SDK
+if (isWechat()) {
+  // 注入 wx.config,获取微信特有 API
+}
+```
+
+---
+
+## 九、Spring Boot 后端目录结构
+
+```
+wishing-tree-server/
+├── pom.xml
+├── src/main/java/com/wishingtree/
+│   ├── WishingTreeApplication.java         # 启动类
+│   ├── config/
+│   │   ├── RedisConfig.java                # Redis 配置
+│   │   ├── CosConfig.java                  # COS 配置
+│   │   ├── WebConfig.java                  # CORS / 拦截器
+│   │   └── SwaggerConfig.java              # 接口文档
+│   ├── controller/
+│   │   ├── AuthController.java             # 登录
+│   │   ├── UserController.java             # 用户
+│   │   ├── TreeController.java             # 许愿树
+│   │   ├── WishController.java             # 愿望
+│   │   └── UploadController.java           # 上传
+│   ├── service/
+│   │   ├── WishingTreeGeoService.java      # Redis GEO 服务
+│   │   ├── WishingTreeService.java
+│   │   ├── WishService.java
+│   │   ├── UserService.java
+│   │   ├── AuthService.java
+│   │   └── CosService.java
+│   ├── mapper/
+│   │   ├── WishingTreeMapper.java
+│   │   ├── WishMapper.java
+│   │   └── UserMapper.java
+│   ├── entity/
+│   │   ├── WishingTree.java
+│   │   ├── Wish.java
+│   │   └── User.java
+│   ├── dto/                                 # 请求体
+│   │   ├── LoginDTO.java
+│   │   ├── CreateWishDTO.java
+│   │   └── NearbyQueryDTO.java
+│   ├── vo/                                  # 响应体
+│   │   ├── NearbyTreeVO.java
+│   │   ├── WishVO.java
+│   │   └── Result.java
+│   ├── handler/
+│   │   └── GlobalExceptionHandler.java
+│   └── util/
+│       ├── JwtUtil.java
+│       └── SmsUtil.java
+├── src/main/resources/
+│   ├── application.yml
+│   ├── application-dev.yml
+│   └── application-prod.yml
+└── Dockerfile
+```
+
+---
+
+## 十、开发阶段规划
+
+### 第一阶段:MVP(3-4周)
+
+**后端(第 1-2 周):**
+- [ ] Spring Boot 项目初始化 + pom.xml 依赖
+- [ ] MySQL 表结构创建(3 张核心表)
+- [ ] Redis GEO 服务实现(核心)
+- [ ] 许愿树 CRUD + 附近查询接口
+- [ ] 愿望 CRUD 接口
+- [ ] 手机号验证码登录接口
+- [ ] COS 图片上传接口
+- [ ] JWT 鉴权拦截器
+- [ ] 全局异常处理 + 统一返回格式
+
+**前端(第 2-4 周):**
+- [ ] Vue 3 + Vite + Vant 项目脚手架
+- [ ] Axios 封装 + Token 管理
+- [ ] 定位模块(getUserLocation)
+- [ ] 拍照/选图/压缩模块
+- [ ] 首页 — 附近许愿树列表
+- [ ] 许愿页 — 拍照 + 表单提交
+- [ ] 我的愿望列表
+- [ ] 登录页
+
+### 第二阶段:增强功能(1-2周)
+
+- [ ] 地图页 — 高德地图 JS SDK 集成
+- [ ] 许愿树详情页 — 展示公开愿望瀑布流
+- [ ] 愿望点赞
+- [ ] 标签筛选
+- [ ] 分享卡片生成(Canvas 合成)
+- [ ] 微信 JS-SDK 接入(微信环境增强)
+
+### 第三阶段:运营功能(1-2周)
+
+- [ ] 管理后台(Vue 3 + vite-admin)
+- [ ] 许愿树管理(新增/编辑/禁用)
+- [ ] 数据看板(用户数、许愿数、日活)
+- [ ] 内容审核(违规内容屏蔽)
+- [ ] PWA 配置(离线访问能力)
+
+---
+
+## 十一、成本预估
+
+| 项目 | 费用(月) |
+|------|-----------|
+| 云服务器(2核4G) | ¥100-200 |
+| 对象存储 COS | 按量计费(初期极低)|
+| MySQL 云数据库 | ¥50-150 |
+| Redis 云缓存 | ¥30-80 |
+| 短信服务 | ¥0.04/条(按量)|
+| 域名 + HTTPS | ¥50-100/年 |
+| **H5 无需认证费** | **¥0** |
+| **总计** | **约 ¥200-500/月** |
+
+> H5 方案比小程序方案每月节省约 ¥200(无认证费、部署更轻量)
+
+---
+
+## 十二、H5 方案优势总结
+
+| 优势 | 说明 |
+|------|------|
+| 跨平台 | 微信、QQ、浏览器、微博等所有 App 内均可打开 |
+| 免审核 | 更新即上线,bug 修复秒级生效 |
+| 低成本 | 无微信认证年费,标准前端技术栈 |
+| 灵活运营 | 不受微信规则约束,可自由设计分享/活动 |
+| SEO 友好 | 页面可被搜索引擎收录(非封闭生态) |
+| 多入口 | 景区二维码、公众号菜单、朋友圈链接均可引流 |
+| PWA | 可添加桌面图标、离线访问,体验接近原生 App |
+
+---
+
+## 十三、风险与应对
+
+| 风险 | 影响 | 应对 |
+|------|------|------|
+| HTTPS 定位限制 | iOS Safari 仅 HTTPS 下允许定位 | 全站 HTTPS + 证书自动续期 |
+| 浏览器兼容 | 不同浏览器相机/定位 API 差异 | 多套降级方案 + 微信环境走 JS-SDK |
+| 用户拒绝定位 | 无法获取精确位置 | 提供手动选择城市 + 查看全部许愿树 |
+| 图片过大 | 上传慢、流量消耗 | 前端 Canvas 压缩至 1080px + 质量 0.8 |
+| 高并发 | 节假日景区流量洪峰 | Redis 缓存扛读 + Nginx 限流 |
+
+---
+
+*计划创建时间:2026-05-17*
+*当前采用方案:Plan B(H5)*
+*版本:v2.0*

+ 24 - 0
wishing-tree-h5/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
wishing-tree-h5/.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 5 - 0
wishing-tree-h5/README.md

@@ -0,0 +1,5 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

+ 42 - 0
wishing-tree-h5/components.d.ts

@@ -0,0 +1,42 @@
+/* eslint-disable */
+// @ts-nocheck
+// biome-ignore lint: disable
+// oxlint-disable
+// ------
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    BottomNav: typeof import('./src/components/BottomNav.vue')['default']
+    ImageUploader: typeof import('./src/components/ImageUploader.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    TagSelector: typeof import('./src/components/TagSelector.vue')['default']
+    TreeCard: typeof import('./src/components/TreeCard.vue')['default']
+    VanActionSheet: typeof import('vant/es')['ActionSheet']
+    VanButton: typeof import('vant/es')['Button']
+    VanCell: typeof import('vant/es')['Cell']
+    VanCellGroup: typeof import('vant/es')['CellGroup']
+    VanEmpty: typeof import('vant/es')['Empty']
+    VanField: typeof import('vant/es')['Field']
+    VanForm: typeof import('vant/es')['Form']
+    VanIcon: typeof import('vant/es')['Icon']
+    VanImage: typeof import('vant/es')['Image']
+    VanList: typeof import('vant/es')['List']
+    VanLoading: typeof import('vant/es')['Loading']
+    VanNavBar: typeof import('vant/es')['NavBar']
+    VanPopup: typeof import('vant/es')['Popup']
+    VanPullRefresh: typeof import('vant/es')['PullRefresh']
+    VanSwitch: typeof import('vant/es')['Switch']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabbar: typeof import('vant/es')['Tabbar']
+    VanTabbarItem: typeof import('vant/es')['TabbarItem']
+    VanTabs: typeof import('vant/es')['Tabs']
+    VanTag: typeof import('vant/es')['Tag']
+    WishCard: typeof import('./src/components/WishCard.vue')['default']
+  }
+}

+ 13 - 0
wishing-tree-h5/index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <title>许愿树 - 记录你每一次许愿</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 2357 - 0
wishing-tree-h5/package-lock.json

@@ -0,0 +1,2357 @@
+{
+  "name": "wishing-tree-h5",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "wishing-tree-h5",
+      "version": "0.0.0",
+      "dependencies": {
+        "@amap/amap-jsapi-loader": "^1.0.1",
+        "@vant/use": "^1.6.0",
+        "axios": "^1.16.1",
+        "pinia": "^3.0.4",
+        "vant": "^4.9.24",
+        "vue": "^3.5.34",
+        "vue-router": "^4.6.4"
+      },
+      "devDependencies": {
+        "@types/node": "^24.12.3",
+        "@vant/auto-import-resolver": "^1.3.0",
+        "@vitejs/plugin-vue": "^6.0.6",
+        "@vue/tsconfig": "^0.9.1",
+        "typescript": "~6.0.2",
+        "unplugin-vue-components": "^32.0.0",
+        "vite": "^8.0.12",
+        "vite-plugin-style-import": "^2.0.0",
+        "vue-tsc": "^3.2.8"
+      }
+    },
+    "node_modules/@amap/amap-jsapi-loader": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz",
+      "integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==",
+      "license": "MIT"
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.3",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz",
+      "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz",
+      "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.2.1",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz",
+      "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+      "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+      "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@tybys/wasm-util": "^0.10.1"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.130.0",
+      "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.130.0.tgz",
+      "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
+      "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
+      "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
+      "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
+      "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
+      "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
+      "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
+      "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
+      "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
+      "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
+      "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
+      "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
+      "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-wasm32-wasi": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
+      "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "1.10.0",
+        "@emnapi/runtime": "1.10.0",
+        "@napi-rs/wasm-runtime": "^1.1.4"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
+      "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
+      "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/pluginutils": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
+      "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "estree-walker": "^2.0.1",
+        "picomatch": "^2.2.2"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      }
+    },
+    "node_modules/@rollup/pluginutils/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.2",
+      "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+      "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "24.12.4",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.4.tgz",
+      "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@vant/auto-import-resolver": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/@vant/auto-import-resolver/-/auto-import-resolver-1.3.0.tgz",
+      "integrity": "sha512-lJyWtCyFizR4bHZvMiNMF3w+WTFTUWAvka1eqTnPK9ticUcKTCOx6qEmHcm8JPb3g1t3GaD2W3MnHkBp/nHamw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vant/popperjs": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz",
+      "integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==",
+      "license": "MIT"
+    },
+    "node_modules/@vant/use": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.6.0.tgz",
+      "integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.7",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
+      "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz",
+      "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.28"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz",
+      "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.28",
+      "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz",
+      "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.34.tgz",
+      "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/shared": "3.5.34",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz",
+      "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz",
+      "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/compiler-core": "3.5.34",
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/compiler-ssr": "3.5.34",
+        "@vue/shared": "3.5.34",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.14",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz",
+      "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+      "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.9"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+      "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.9",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+      "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/language-core": {
+      "version": "3.2.9",
+      "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.2.9.tgz",
+      "integrity": "sha512-ie0ojt/0fU/GfIogh+zgHbaYRPlt9S+cLOxcWwF7nTSFh897BVgnFKL2byT4kpp1mlqYWZ2psGwSniyE2xsxYw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.28",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^3.2.0",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1",
+        "picomatch": "^4.0.4"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.34.tgz",
+      "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.34.tgz",
+      "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz",
+      "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.34",
+        "@vue/runtime-core": "3.5.34",
+        "@vue/shared": "3.5.34",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.34.tgz",
+      "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.34",
+        "@vue/shared": "3.5.34"
+      },
+      "peerDependencies": {
+        "vue": "3.5.34"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz",
+      "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/tsconfig": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.9.1.tgz",
+      "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "typescript": ">= 5.8",
+        "vue": "^3.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz",
+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz",
+      "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz",
+      "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/camel-case": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/camel-case/-/camel-case-4.1.2.tgz",
+      "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pascal-case": "^3.1.2",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/capital-case": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/capital-case/-/capital-case-1.0.4.tgz",
+      "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "no-case": "^3.0.4",
+        "tslib": "^2.0.3",
+        "upper-case-first": "^2.0.2"
+      }
+    },
+    "node_modules/change-case": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/change-case/-/change-case-4.1.2.tgz",
+      "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "camel-case": "^4.1.2",
+        "capital-case": "^1.0.4",
+        "constant-case": "^3.0.4",
+        "dot-case": "^3.0.4",
+        "header-case": "^2.0.4",
+        "no-case": "^3.0.4",
+        "param-case": "^3.0.4",
+        "pascal-case": "^3.1.2",
+        "path-case": "^3.0.4",
+        "sentence-case": "^3.0.4",
+        "snake-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/console": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmmirror.com/console/-/console-0.7.2.tgz",
+      "integrity": "sha512-+JSDwGunA4MTEgAV/4VBKwUHonP8CzJ/6GIuwPi6acKFqFfHUdSGCm89ZxZ5FfGWdZfkdgAroy5bJ5FSeN/t4g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/constant-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/constant-case/-/constant-case-3.0.4.tgz",
+      "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "no-case": "^3.0.4",
+        "tslib": "^2.0.3",
+        "upper-case": "^2.0.2"
+      }
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dot-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/dot-case/-/dot-case-3.0.4.tgz",
+      "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "no-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
+      "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/exsolve": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
+      "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
+      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/header-case": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/header-case/-/header-case-2.0.4.tgz",
+      "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "capital-case": "^1.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/jsonfile": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz",
+      "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz",
+      "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "dev": true,
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+      "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+      "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+      "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+      "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+      "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+      "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+      "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+      "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+      "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+      "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+      "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/local-pkg": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz",
+      "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/lower-case": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/lower-case/-/lower-case-2.0.2.tgz",
+      "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/mlly": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz",
+      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.3"
+      }
+    },
+    "node_modules/mlly/node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mlly/node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.12",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz",
+      "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/no-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/no-case/-/no-case-3.0.4.tgz",
+      "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lower-case": "^2.0.2",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/obug": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.1.tgz",
+      "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "license": "MIT"
+    },
+    "node_modules/param-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/param-case/-/param-case-3.0.4.tgz",
+      "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dot-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/pascal-case": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/pascal-case/-/pascal-case-3.1.2.tgz",
+      "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "no-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/path-case/-/path-case-3.0.4.tgz",
+      "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dot-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pkg-types": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.1.tgz",
+      "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.4",
+        "exsolve": "^1.0.8",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.14",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz",
+      "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/antfu"
+        },
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/sxzz"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rolldown": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.1.tgz",
+      "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.130.0",
+        "@rolldown/pluginutils": "^1.0.0"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.1",
+        "@rolldown/binding-darwin-arm64": "1.0.1",
+        "@rolldown/binding-darwin-x64": "1.0.1",
+        "@rolldown/binding-freebsd-x64": "1.0.1",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.1",
+        "@rolldown/binding-linux-arm64-musl": "1.0.1",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.1",
+        "@rolldown/binding-linux-x64-gnu": "1.0.1",
+        "@rolldown/binding-linux-x64-musl": "1.0.1",
+        "@rolldown/binding-openharmony-arm64": "1.0.1",
+        "@rolldown/binding-wasm32-wasi": "1.0.1",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.1",
+        "@rolldown/binding-win32-x64-msvc": "1.0.1"
+      }
+    },
+    "node_modules/sentence-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/sentence-case/-/sentence-case-3.0.4.tgz",
+      "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "no-case": "^3.0.4",
+        "tslib": "^2.0.3",
+        "upper-case-first": "^2.0.2"
+      }
+    },
+    "node_modules/snake-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/snake-case/-/snake-case-3.0.4.tgz",
+      "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dot-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/sourcemap-codec": {
+      "version": "1.4.8",
+      "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+      "deprecated": "Please use @jridgewell/sourcemap-codec instead",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.16",
+      "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz",
+      "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "dev": true,
+      "license": "0BSD"
+    },
+    "node_modules/typescript": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz",
+      "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/ufo": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz",
+      "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/unplugin": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz",
+      "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "picomatch": "^4.0.3",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/unplugin-utils": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
+      "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/unplugin-vue-components": {
+      "version": "32.0.0",
+      "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-32.0.0.tgz",
+      "integrity": "sha512-uLdccgS7mf3pv1bCCP20y/hm+u1eOjAmygVkh+Oa70MPkzgl1eQv1L0CwdHNM3gscO8/GDMGIET98Ja47CBbZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^5.0.0",
+        "local-pkg": "^1.1.2",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.2",
+        "obug": "^2.1.1",
+        "picomatch": "^4.0.3",
+        "tinyglobby": "^0.2.15",
+        "unplugin": "^3.0.0",
+        "unplugin-utils": "^0.3.1"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@nuxt/kit": "^3.2.2 || ^4.0.0",
+        "vue": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@nuxt/kit": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/upper-case": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/upper-case/-/upper-case-2.0.2.tgz",
+      "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/upper-case-first": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/upper-case-first/-/upper-case-first-2.0.2.tgz",
+      "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/vant": {
+      "version": "4.9.24",
+      "resolved": "https://registry.npmmirror.com/vant/-/vant-4.9.24.tgz",
+      "integrity": "sha512-tP1A7Vjzv1/B1ljb95Jhv9Q9w6acaaZDJvy6wcKrwGgY0gQZlg+FXLZH/AIKZBE3xvYGDUsv/M7AuGcr/Pqd6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@vant/popperjs": "^1.3.0",
+        "@vant/use": "^1.6.0",
+        "@vue/shared": "^3.5.31"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.0.13",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.13.tgz",
+      "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.4",
+        "postcss": "^8.5.14",
+        "rolldown": "1.0.1",
+        "tinyglobby": "^0.2.16"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.1.18",
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-plugin-style-import": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/vite-plugin-style-import/-/vite-plugin-style-import-2.0.0.tgz",
+      "integrity": "sha512-qtoHQae5dSUQPo/rYz/8p190VU5y19rtBaeV7ryLa/AYAU/e9CG89NrN/3+k7MR8mJy/GPIu91iJ3zk9foUOSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rollup/pluginutils": "^4.1.2",
+        "change-case": "^4.1.2",
+        "console": "^0.7.2",
+        "es-module-lexer": "^0.9.3",
+        "fs-extra": "^10.0.0",
+        "magic-string": "^0.25.7",
+        "pathe": "^0.2.0"
+      },
+      "peerDependencies": {
+        "vite": ">=2.0.0"
+      }
+    },
+    "node_modules/vite-plugin-style-import/node_modules/magic-string": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz",
+      "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "sourcemap-codec": "^1.4.8"
+      }
+    },
+    "node_modules/vite-plugin-style-import/node_modules/pathe": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/pathe/-/pathe-0.2.0.tgz",
+      "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
+      "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz",
+      "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/compiler-sfc": "3.5.34",
+        "@vue/runtime-dom": "3.5.34",
+        "@vue/server-renderer": "3.5.34",
+        "@vue/shared": "3.5.34"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-router/node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/vue-tsc": {
+      "version": "3.2.9",
+      "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.2.9.tgz",
+      "integrity": "sha512-qm8/nbo+9eZc1SCndm9wT+gq23pM+wRIdHY0wjm83B3lIginHTwcdrLUyTrKjDWXbMVNjKegNrnymhpdqnCL3A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.28",
+        "@vue/language-core": "3.2.9"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+      "dev": true,
+      "license": "MIT"
+    }
+  }
+}

+ 31 - 0
wishing-tree-h5/package.json

@@ -0,0 +1,31 @@
+{
+  "name": "wishing-tree-h5",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc -b && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
+    "@vant/use": "^1.6.0",
+    "axios": "^1.16.1",
+    "pinia": "^3.0.4",
+    "vant": "^4.9.24",
+    "vue": "^3.5.34",
+    "vue-router": "^4.6.4"
+  },
+  "devDependencies": {
+    "@types/node": "^24.12.3",
+    "@vant/auto-import-resolver": "^1.3.0",
+    "@vitejs/plugin-vue": "^6.0.6",
+    "@vue/tsconfig": "^0.9.1",
+    "typescript": "~6.0.2",
+    "unplugin-vue-components": "^32.0.0",
+    "vite": "^8.0.12",
+    "vite-plugin-style-import": "^2.0.0",
+    "vue-tsc": "^3.2.8"
+  }
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
wishing-tree-h5/public/favicon.svg


+ 24 - 0
wishing-tree-h5/public/icons.svg

@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <symbol id="bluesky-icon" viewBox="0 0 16 17">
+    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
+    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
+  </symbol>
+  <symbol id="discord-icon" viewBox="0 0 20 19">
+    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
+  </symbol>
+  <symbol id="documentation-icon" viewBox="0 0 21 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
+  </symbol>
+  <symbol id="github-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
+  </symbol>
+  <symbol id="social-icon" viewBox="0 0 20 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
+  </symbol>
+  <symbol id="x-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
+  </symbol>
+</svg>

+ 30 - 0
wishing-tree-h5/src/App.vue

@@ -0,0 +1,30 @@
+<template>
+  <div id="app-root">
+    <router-view v-slot="{ Component }">
+      <transition name="fade" mode="out-in">
+        <component :is="Component" />
+      </transition>
+    </router-view>
+    <bottom-nav v-if="showNav" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRoute } from 'vue-router'
+import BottomNav from './components/BottomNav.vue'
+
+const route = useRoute()
+const showNav = computed(() => !['login', 'make-wish'].includes(route.name as string))
+</script>
+
+<style>
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.2s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+}
+</style>

+ 18 - 0
wishing-tree-h5/src/api/auth.ts

@@ -0,0 +1,18 @@
+// Mock login
+export async function sendSms(phone: string) {
+  console.log(`[Mock] Sending SMS to ${phone}`)
+  return Promise.resolve({ code: 0, data: null })
+}
+
+export async function login(phone: string, code: string) {
+  console.log(`[Mock] Login with ${phone}, code: ${code}`)
+  return Promise.resolve({
+    code: 0,
+    data: {
+      token: 'mock_token_' + Date.now(),
+      nickname: '用户' + phone.slice(-4),
+      phone,
+      avatarUrl: '',
+    },
+  })
+}

+ 38 - 0
wishing-tree-h5/src/api/request.ts

@@ -0,0 +1,38 @@
+import { showToast } from 'vant'
+import { getStorage } from '@/utils/storage'
+
+// 当前使用 mock 模式
+const USE_MOCK = true
+
+export interface ApiResponse<T = any> {
+  code: number
+  data: T
+  message?: string
+}
+
+export async function request<T = any>(url: string, options?: RequestInit): Promise<ApiResponse<T>> {
+  if (USE_MOCK) {
+    throw new Error('Mock mode — use mock services directly')
+  }
+
+  const token = getStorage('token')
+  const headers: Record<string, string> = {
+    'Content-Type': 'application/json',
+    ...(token ? { Authorization: `Bearer ${token}` } : {}),
+  }
+
+  try {
+    const res = await fetch(`/api${url}`, {
+      ...options,
+      headers: { ...headers, ...(options?.headers as Record<string, string> || {}) },
+    })
+    const json = await res.json()
+    if (json.code !== 0) {
+      showToast(json.message || '请求失败')
+    }
+    return json
+  } catch (err: any) {
+    showToast(err.message || '网络错误')
+    throw err
+  }
+}

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

@@ -0,0 +1,9 @@
+import { getNearbyTrees, getTreeById } from '@/mock/tree'
+
+export function fetchNearbyTrees(lng: number, lat: number, maxDistance?: number, limit?: number) {
+  return getNearbyTrees(lng, lat, maxDistance, limit)
+}
+
+export function fetchTreeDetail(id: number) {
+  return getTreeById(id)
+}

+ 8 - 0
wishing-tree-h5/src/api/upload.ts

@@ -0,0 +1,8 @@
+// Mock upload — 将图片转为 DataURL 当作"已上传的URL"
+export async function uploadImage(file: File): Promise<string> {
+  return new Promise((resolve) => {
+    const reader = new FileReader()
+    reader.onload = () => resolve(reader.result as string)
+    reader.readAsDataURL(file)
+  })
+}

+ 33 - 0
wishing-tree-h5/src/api/wish.ts

@@ -0,0 +1,33 @@
+import {
+  getWishesByTree,
+  getMyWishes as getMyWishesMock,
+  getWishById,
+  createWish as createWishMock,
+  deleteWish as deleteWishMock,
+  likeWish as likeWishMock,
+} from '@/mock/wish'
+import type { Wish } from '@/mock/data'
+
+export function fetchTreeWishes(treeId: number, page?: number, pageSize?: number) {
+  return getWishesByTree(treeId, page, pageSize)
+}
+
+export function fetchMyWishes(userId: string, page?: number, pageSize?: number) {
+  return getMyWishesMock(userId, page, pageSize)
+}
+
+export function fetchWishDetail(id: number) {
+  return getWishById(id)
+}
+
+export function submitWish(data: Omit<Wish, 'id' | 'status' | 'likes' | 'createdAt'>) {
+  return createWishMock(data)
+}
+
+export function removeWish(id: number) {
+  return deleteWishMock(id)
+}
+
+export function toggleLikeWish(id: number) {
+  return likeWishMock(id)
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
wishing-tree-h5/src/assets/vite.svg


+ 8 - 0
wishing-tree-h5/src/components/BottomNav.vue

@@ -0,0 +1,8 @@
+<template>
+  <van-tabbar route :fixed="true" :placeholder="true" :safe-area-inset-bottom="true">
+    <van-tabbar-item to="/" icon="home-o" replace>首页</van-tabbar-item>
+    <van-tabbar-item to="/map" icon="map-marked" replace>地图</van-tabbar-item>
+    <van-tabbar-item to="/my-wishes" icon="label-o" replace>愿望</van-tabbar-item>
+    <van-tabbar-item to="/user" icon="user-o" replace>我的</van-tabbar-item>
+  </van-tabbar>
+</template>

+ 129 - 0
wishing-tree-h5/src/components/ImageUploader.vue

@@ -0,0 +1,129 @@
+<template>
+  <div class="image-uploader">
+    <div class="upload-grid">
+      <div
+        v-for="(img, i) in images"
+        :key="i"
+        class="upload-item"
+      >
+        <van-image :src="img" width="100%" height="100%" fit="cover" radius="6" />
+        <van-icon
+          name="cross"
+          class="remove-btn"
+          @click="removeImage(i)"
+        />
+      </div>
+      <div
+        v-if="images.length < max"
+        class="upload-item upload-add"
+        @click="showAction = true"
+      >
+        <van-icon name="photograph" size="24" color="#999" />
+        <span class="add-text">拍照</span>
+      </div>
+    </div>
+
+    <van-action-sheet
+      v-model:show="showAction"
+      :actions="actions"
+      cancel-text="取消"
+      @select="onSelect"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { showToast } from 'vant'
+import { takePhoto, pickFromAlbum } from '@/utils/camera'
+import { uploadImage } from '@/api/upload'
+
+const props = withDefaults(defineProps<{
+  modelValue: string[]
+  max?: number
+}>(), { max: 9 })
+
+const emit = defineEmits<{ 'update:modelValue': [v: string[]] }>()
+
+const showAction = ref(false)
+const uploading = ref(false)
+
+const actions = [
+  { name: 'camera', description: '拍摄照片' },
+  { name: 'album', description: '从相册选择' },
+]
+
+const images = computedWithEmit()
+
+function computedWithEmit() {
+  return new Proxy(props.modelValue, {
+    get: (_, key) => props.modelValue[key as any],
+  })
+}
+
+function removeImage(i: number) {
+  const arr = [...props.modelValue]
+  arr.splice(i, 1)
+  emit('update:modelValue', arr)
+}
+
+async function onSelect(action: { name: string }) {
+  showAction.value = false
+  if (uploading.value) return
+  uploading.value = true
+
+  try {
+    const files = action.name === 'camera'
+      ? [await takePhoto()]
+      : await pickFromAlbum(props.max - props.modelValue.length > 1)
+
+    showToast('上传中...')
+    const urls = await Promise.all(files.map(uploadImage))
+    emit('update:modelValue', [...props.modelValue, ...urls])
+  } catch (err: any) {
+    if (err.message !== '未选择图片') {
+      showToast(err.message || '操作失败')
+    }
+  } finally {
+    uploading.value = false
+  }
+}
+</script>
+
+<style scoped>
+.upload-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.upload-item {
+  position: relative;
+  width: calc((100% - 24px) / 4);
+  aspect-ratio: 1;
+}
+.upload-add {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  border: 1px dashed #ddd;
+  border-radius: 6px;
+  background: #fafafa;
+  cursor: pointer;
+  gap: 2px;
+}
+.add-text {
+  font-size: 11px;
+  color: #999;
+}
+.remove-btn {
+  position: absolute;
+  top: -6px;
+  right: -6px;
+  background: rgba(0,0,0,0.5);
+  color: #fff;
+  border-radius: 50%;
+  padding: 2px;
+  font-size: 12px;
+}
+</style>

+ 38 - 0
wishing-tree-h5/src/components/TagSelector.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="tag-selector">
+    <van-tag
+      v-for="t in allTags"
+      :key="t"
+      :type="selected.includes(t) ? 'primary' : 'default'"
+      :plain="!selected.includes(t)"
+      size="medium"
+      style="margin: 4px; padding: 4px 12px; cursor: pointer"
+      @click="toggle(t)"
+    >
+      {{ t }}
+    </van-tag>
+  </div>
+</template>
+
+<script setup lang="ts">
+const allTags = ['爱情', '事业', '健康', '学业', '财富', '平安', '祈福', '家庭', '友谊', '梦想']
+
+const props = defineProps<{ modelValue: string[] }>()
+const emit = defineEmits<{ 'update:modelValue': [v: string[]] }>()
+
+const selected = computedWithEmit()
+
+function computedWithEmit() {
+  return new Proxy(props.modelValue, {
+    get: (_, key) => props.modelValue[key as any],
+  })
+}
+
+function toggle(tag: string) {
+  const arr = [...props.modelValue]
+  const i = arr.indexOf(tag)
+  if (i > -1) arr.splice(i, 1)
+  else arr.push(tag)
+  emit('update:modelValue', arr)
+}
+</script>

+ 104 - 0
wishing-tree-h5/src/components/TreeCard.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="tree-card" @click="$emit('click')">
+    <div class="tree-cover" :style="{ background: gradient }">
+      <span class="tree-emoji">{{ emoji }}</span>
+      <span class="tree-label">{{ tree.name }}</span>
+    </div>
+    <div class="tree-card-info">
+      <h4 class="tree-name">{{ tree.name }}</h4>
+      <p class="tree-address ellipsis">{{ tree.address }}</p>
+      <div class="tree-meta">
+        <span class="tree-distance">
+          <van-tag :type="tree.isInRange ? 'success' : 'warning'" round size="medium">
+            {{ tree.isInRange ? '可许愿' : formatDistance(tree.distance) }}
+          </van-tag>
+        </span>
+        <span class="tree-wish-count">💬 {{ tree.totalWishes }} 个愿望</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+const props = defineProps<{
+  tree: {
+    id: number
+    name: string
+    coverImage: string
+    address: string
+    distance: number
+    isInRange: boolean
+    totalWishes: number
+  }
+}>()
+
+defineEmits<{ click: [] }>()
+
+import { getTreeGradient, getTreeEmoji } from '@/utils/theme'
+
+const gradient = computed(() => getTreeGradient(props.tree.id))
+const emoji = computed(() => getTreeEmoji(props.tree.id))
+
+function formatDistance(m: number) {
+  return m >= 1000 ? (m / 1000).toFixed(1) + 'km' : m + 'm'
+}
+</script>
+
+<style scoped>
+.tree-card {
+  background: #fff;
+  border-radius: 12px;
+  overflow: hidden;
+  margin: 12px 16px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+  cursor: pointer;
+  transition: transform 0.15s;
+}
+.tree-card:active {
+  transform: scale(0.98);
+}
+.tree-cover {
+  width: 100%;
+  height: 140px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  color: #fff;
+  overflow: hidden;
+}
+.tree-emoji {
+  font-size: 40px;
+  text-shadow: 0 1px 4px rgba(0,0,0,0.2);
+}
+.tree-label {
+  font-size: 12px;
+  opacity: 0.9;
+  text-shadow: 0 1px 2px rgba(0,0,0,0.2);
+}
+.tree-card-info {
+  padding: 12px;
+}
+.tree-name {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 4px;
+}
+.tree-address {
+  font-size: 12px;
+  color: #999;
+  margin-bottom: 8px;
+}
+.tree-meta {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.tree-wish-count {
+  font-size: 12px;
+  color: #666;
+}
+</style>

+ 109 - 0
wishing-tree-h5/src/components/WishCard.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="wish-card" @click="$emit('click')">
+    <p class="wish-content">{{ wish.content }}</p>
+    <div class="wish-images" v-if="wish.images?.length">
+      <van-image
+        v-for="(img, i) in wish.images.slice(0, 3)"
+        :key="i"
+        :src="img"
+        width="80"
+        height="80"
+        fit="cover"
+        radius="4"
+        lazy-load
+      />
+      <span v-if="wish.images.length > 3" class="more-images">
+        +{{ wish.images.length - 3 }}
+      </span>
+    </div>
+    <div class="wish-tags" v-if="wish.tags?.length">
+      <van-tag v-for="t in wish.tags" :key="t" plain type="primary" size="medium" style="margin-right: 4px">
+        {{ t }}
+      </van-tag>
+    </div>
+    <div class="wish-footer">
+      <span class="wish-tree">{{ wish.treeName }}</span>
+      <span class="wish-meta">
+        ❤ {{ wish.likes }}
+        <span class="wish-time">{{ formatTime(wish.createdAt) }}</span>
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  wish: {
+    id: number
+    content: string
+    images: string[]
+    tags: string[]
+    likes: number
+    treeName: string
+    createdAt: string
+  }
+}>()
+
+defineEmits<{ click: [] }>()
+
+function formatTime(s: string) {
+  const d = new Date(s)
+  const now = new Date()
+  const diff = now.getTime() - d.getTime()
+  if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
+  if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
+  return s.substring(5, 10)
+}
+</script>
+
+<style scoped>
+.wish-card {
+  background: #fff;
+  border-radius: 12px;
+  padding: 16px;
+  margin: 8px 16px;
+  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
+}
+.wish-content {
+  font-size: 15px;
+  line-height: 1.6;
+  margin-bottom: 10px;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+.wish-images {
+  display: flex;
+  gap: 6px;
+  margin-bottom: 10px;
+  flex-wrap: wrap;
+}
+.more-images {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 80px;
+  height: 80px;
+  background: #f0f0f0;
+  border-radius: 4px;
+  font-size: 14px;
+  color: #999;
+}
+.wish-tags {
+  margin-bottom: 10px;
+}
+.wish-footer {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  color: #999;
+}
+.wish-tree {
+  color: #07c160;
+}
+.wish-meta {
+  display: flex;
+  gap: 8px;
+}
+</style>

+ 13 - 0
wishing-tree-h5/src/main.ts

@@ -0,0 +1,13 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import router from './router'
+import App from './App.vue'
+
+// Vant 样式
+import 'vant/lib/index.css'
+import './style.css'
+
+const app = createApp(App)
+app.use(createPinia())
+app.use(router)
+app.mount('#app')

+ 293 - 0
wishing-tree-h5/src/mock/data.ts

@@ -0,0 +1,293 @@
+export interface WishingTree {
+  id: number
+  name: string
+  description: string
+  longitude: number
+  latitude: number
+  address: string
+  radius: number
+  coverImage: string
+  totalWishes: number
+  isActive: boolean
+}
+
+export interface Wish {
+  id: number
+  treeId: number
+  treeName: string
+  userId: string
+  content: string
+  images: string[]
+  lng: number
+  lat: number
+  address: string
+  isPublic: boolean
+  tags: string[]
+  likes: number
+  status: 0 | 1
+  createdAt: string
+}
+
+// 许愿树 mock 数据
+export const mockTrees: WishingTree[] = [
+  {
+    id: 1,
+    name: '千年银杏许愿树',
+    description: '位于公园中心的千年银杏树,每年秋天金黄的落叶铺满大地,是许下美好心愿的理想之地。据传这棵银杏树已有1200年历史,见证了无数人的悲欢离合。',
+    longitude: 116.4074,
+    latitude: 39.9042,
+    address: '北京市东城区景山公园中心广场',
+    radius: 150,
+    coverImage: 'https://picsum.photos/seed/tree1/400/300',
+    totalWishes: 9999,
+    isActive: true,
+  },
+  {
+    id: 2,
+    name: '祈福樱花树',
+    description: '春天的樱花树下,粉色花瓣随风飘落,许下的愿望将随着花香飘向天空。每年3-4月是最佳许愿时节。',
+    longitude: 116.3974,
+    latitude: 39.9142,
+    address: '北京市西城区玉渊潭公园樱花区',
+    radius: 100,
+    coverImage: 'https://picsum.photos/seed/tree2/400/300',
+    totalWishes: 8888,
+    isActive: true,
+  },
+  {
+    id: 3,
+    name: '菩提许愿树',
+    description: '寺庙内百年菩提树,佛陀曾在此树下悟道,许下心灵深处的愿望,让智慧之光点亮前路。',
+    longitude: 116.3874,
+    latitude: 39.8942,
+    address: '北京市海淀区大钟寺内',
+    radius: 80,
+    coverImage: 'https://picsum.photos/seed/tree3/400/300',
+    totalWishes: 6666,
+    isActive: true,
+  },
+  {
+    id: 4,
+    name: '牵缘许愿树',
+    description: '连理枝下许心愿,愿天下有情人终成眷属。许多情侣在这里留下了爱的见证。',
+    longitude: 116.4174,
+    latitude: 39.9242,
+    address: '北京市朝阳区朝阳公园湖边',
+    radius: 120,
+    coverImage: 'https://picsum.photos/seed/tree4/400/300',
+    totalWishes: 7777,
+    isActive: true,
+  },
+  {
+    id: 5,
+    name: '平安许愿松',
+    description: '山顶古松傲立,俯瞰整座城市。在此许下平安健康的心愿,让松树的坚韧守护你的祝福。',
+    longitude: 116.3674,
+    latitude: 39.9342,
+    address: '北京市海淀区香山公园山顶',
+    radius: 200,
+    coverImage: 'https://picsum.photos/seed/tree5/400/300',
+    totalWishes: 5555,
+    isActive: true,
+  },
+  {
+    id: 6,
+    name: '外滩许愿墙',
+    description: '黄浦江畔,万国建筑群前,对江许下心愿,让繁华都市见证你的梦想。夜晚华灯初上时最浪漫。',
+    longitude: 121.4903,
+    latitude: 31.2405,
+    address: '上海市黄浦区外滩观景平台',
+    radius: 120,
+    coverImage: 'https://picsum.photos/seed/tree6/400/300',
+    totalWishes: 7890,
+    isActive: true,
+  },
+  {
+    id: 7,
+    name: '白塔许愿台',
+    description: '广州白塔下,珠江夜景尽收眼底,对着江水许下最真诚的愿望,让江水带走烦恼。',
+    longitude: 113.2644,
+    latitude: 23.1291,
+    address: '广州市越秀区白塔公园山顶',
+    radius: 100,
+    coverImage: 'https://picsum.photos/seed/tree7/400/300',
+    totalWishes: 6543,
+    isActive: true,
+  },
+  {
+    id: 8,
+    name: '锦鲤许愿池',
+    description: '宽窄巷子里的百年许愿池,池中锦鲤游弋,投一枚硬币许下心愿,好运自然来。',
+    longitude: 104.0574,
+    latitude: 30.6727,
+    address: '成都市青羊区宽窄巷子中心广场',
+    radius: 80,
+    coverImage: 'https://picsum.photos/seed/tree8/400/300',
+    totalWishes: 10234,
+    isActive: true,
+  },
+]
+
+// 愿望 mock 数据
+export const mockWishes: Wish[] = [
+  {
+    id: 1,
+    treeId: 1,
+    treeName: '千年银杏许愿树',
+    userId: 'user1',
+    content: '希望家人身体健康,平安喜乐,新的一年一切顺利!',
+    images: ['https://picsum.photos/seed/wish1/400/400'],
+    lng: 116.4074,
+    lat: 39.9042,
+    address: '北京市东城区景山公园中心广场',
+    isPublic: true,
+    tags: ['健康', '平安'],
+    likes: 128,
+    status: 0,
+    createdAt: '2026-05-15 10:30:00',
+  },
+  {
+    id: 2,
+    treeId: 1,
+    treeName: '千年银杏许愿树',
+    userId: 'user2',
+    content: '希望今年考研顺利上岸,进入理想的学校!',
+    images: [],
+    lng: 116.4072,
+    lat: 39.9040,
+    address: '北京市东城区景山公园中心广场',
+    isPublic: true,
+    tags: ['学业'],
+    likes: 56,
+    status: 0,
+    createdAt: '2026-05-15 11:00:00',
+  },
+  {
+    id: 3,
+    treeId: 2,
+    treeName: '祈福樱花树',
+    userId: 'user3',
+    content: '愿得一人心,白首不相离。希望能遇到对的人。',
+    images: ['https://picsum.photos/seed/wish3/400/400'],
+    lng: 116.3974,
+    lat: 39.9142,
+    address: '北京市西城区玉渊潭公园樱花区',
+    isPublic: true,
+    tags: ['爱情'],
+    likes: 99,
+    status: 0,
+    createdAt: '2026-05-14 15:20:00',
+  },
+  {
+    id: 4,
+    treeId: 2,
+    treeName: '祈福樱花树',
+    userId: 'user1',
+    content: '希望事业蒸蒸日上,拿到满意的offer!',
+    images: [],
+    lng: 116.3975,
+    lat: 39.9140,
+    address: '北京市西城区玉渊潭公园樱花区',
+    isPublic: false,
+    tags: ['事业'],
+    likes: 0,
+    status: 0,
+    createdAt: '2026-05-14 16:00:00',
+  },
+  {
+    id: 5,
+    treeId: 3,
+    treeName: '菩提许愿树',
+    userId: 'user2',
+    content: '愿世间所有美好都如期而至,愿所有遗憾都随风而逝。',
+    images: ['https://picsum.photos/seed/wish5/400/400', 'https://picsum.photos/seed/wish5b/400/400'],
+    lng: 116.3874,
+    lat: 39.8942,
+    address: '北京市海淀区大钟寺内',
+    isPublic: true,
+    tags: ['平安', '祈福'],
+    likes: 233,
+    status: 0,
+    createdAt: '2026-05-13 09:15:00',
+  },
+  {
+    id: 6,
+    treeId: 4,
+    treeName: '牵缘许愿树',
+    userId: 'user3',
+    content: '希望和喜欢的人永远在一起,长长久久。',
+    images: [],
+    lng: 116.4174,
+    lat: 39.9242,
+    address: '北京市朝阳区朝阳公园湖边',
+    isPublic: true,
+    tags: ['爱情'],
+    likes: 77,
+    status: 0,
+    createdAt: '2026-05-12 14:00:00',
+  },
+  {
+    id: 7,
+    treeId: 1,
+    treeName: '千年银杏许愿树',
+    userId: 'user4',
+    content: '希望今年能存够钱,带爸妈去旅行一次!',
+    images: ['https://picsum.photos/seed/wish7/400/400'],
+    lng: 116.4073,
+    lat: 39.9041,
+    address: '北京市东城区景山公园中心广场',
+    isPublic: true,
+    tags: ['财富', '健康'],
+    likes: 45,
+    status: 0,
+    createdAt: '2026-05-12 10:00:00',
+  },
+  {
+    id: 8,
+    treeId: 5,
+    treeName: '平安许愿松',
+    userId: 'user1',
+    content: '登高望远,愿你三冬暖,愿你春不寒,愿你天黑有灯,下雨有伞。',
+    images: ['https://picsum.photos/seed/wish8/400/400', 'https://picsum.photos/seed/wish8b/400/400', 'https://picsum.photos/seed/wish8c/400/400'],
+    lng: 116.3674,
+    lat: 39.9342,
+    address: '北京市海淀区香山公园山顶',
+    isPublic: true,
+    tags: ['平安', '祈福'],
+    likes: 188,
+    status: 0,
+    createdAt: '2026-05-11 08:30:00',
+  },
+  {
+    id: 9,
+    treeId: 6,
+    treeName: '外滩许愿墙',
+    userId: 'user2',
+    content: '站在外滩看对岸陆家嘴的灯火,许愿有一天也能在这里拥有自己的一片天地。',
+    images: ['https://picsum.photos/seed/wish9/400/400'],
+    lng: 121.4903,
+    lat: 31.2405,
+    address: '上海市黄浦区外滩观景平台',
+    isPublic: true,
+    tags: ['事业', '梦想'],
+    likes: 302,
+    status: 0,
+    createdAt: '2026-05-10 19:30:00',
+  },
+  {
+    id: 10,
+    treeId: 8,
+    treeName: '锦鲤许愿池',
+    userId: 'user3',
+    content: '投了三次硬币才投进许愿池中央!锦鲤保佑,希望今年好运连连!',
+    images: ['https://picsum.photos/seed/wish10/400/400'],
+    lng: 104.0574,
+    lat: 30.6727,
+    address: '成都市青羊区宽窄巷子中心广场',
+    isPublic: true,
+    tags: ['好运', '财富'],
+    likes: 67,
+    status: 0,
+    createdAt: '2026-05-09 13:20:00',
+  },
+]

+ 38 - 0
wishing-tree-h5/src/mock/tree.ts

@@ -0,0 +1,38 @@
+import { mockTrees, type WishingTree } from './data'
+
+export function getNearbyTrees(
+  lng: number,
+  lat: number,
+  _maxDistance = 5000,
+  _limit = 50
+) {
+  // Mock 模式:返回所有许愿树,按距离排序(不过滤距离)
+  const results: (WishingTree & { distance: number; isInRange: boolean })[] = []
+
+  for (const tree of mockTrees) {
+    if (!tree.isActive) continue
+    const dist = haversine(lng, lat, tree.longitude, tree.latitude)
+    results.push({
+      ...tree,
+      distance: Math.round(dist),
+      isInRange: dist <= tree.radius,
+    })
+  }
+
+  results.sort((a, b) => a.distance - b.distance)
+  return Promise.resolve(results)
+}
+
+export function getTreeById(id: number) {
+  const tree = mockTrees.find((t) => t.id === id)
+  return Promise.resolve(tree || null)
+}
+
+function haversine(lng1: number, lat1: number, lng2: number, lat2: number): number {
+  const R = 6371000
+  const toRad = (d: number) => (d * Math.PI) / 180
+  const dLat = toRad(lat2 - lat1)
+  const dLng = toRad(lng2 - lng1)
+  const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2
+  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
+}

+ 62 - 0
wishing-tree-h5/src/mock/wish.ts

@@ -0,0 +1,62 @@
+import { mockWishes, type Wish } from './data'
+
+let nextId = 100
+
+export function getWishesByTree(treeId: number, page = 1, pageSize = 20) {
+  const list = mockWishes
+    .filter((w) => w.treeId === treeId && w.isPublic)
+    .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
+
+  const start = (page - 1) * pageSize
+  return Promise.resolve({
+    list: list.slice(start, start + pageSize),
+    total: list.length,
+  })
+}
+
+export function getMyWishes(userId: string, page = 1, pageSize = 20) {
+  const list = mockWishes
+    .filter((w) => w.userId === userId)
+    .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
+
+  const start = (page - 1) * pageSize
+  return Promise.resolve({
+    list: list.slice(start, start + pageSize),
+    total: list.length,
+  })
+}
+
+export function getWishById(id: number) {
+  const wish = mockWishes.find((w) => w.id === id)
+  return Promise.resolve(wish || null)
+}
+
+export function createWish(data: Omit<Wish, 'id' | 'status' | 'likes' | 'createdAt'>) {
+  const wish: Wish = {
+    ...data,
+    id: nextId++,
+    likes: 0,
+    status: 0,
+    createdAt: new Date().toISOString().replace('T', ' ').substring(0, 19),
+  }
+  mockWishes.unshift(wish)
+  return Promise.resolve(wish)
+}
+
+export function deleteWish(id: number) {
+  const idx = mockWishes.findIndex((w) => w.id === id)
+  if (idx > -1) {
+    mockWishes.splice(idx, 1)
+    return Promise.resolve(true)
+  }
+  return Promise.resolve(false)
+}
+
+export function likeWish(id: number) {
+  const wish = mockWishes.find((w) => w.id === id)
+  if (wish) {
+    wish.likes++
+    return Promise.resolve(wish.likes)
+  }
+  return Promise.resolve(0)
+}

+ 49 - 0
wishing-tree-h5/src/router/index.ts

@@ -0,0 +1,49 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: '/',
+      name: 'home',
+      component: () => import('@/views/HomeView.vue'),
+    },
+    {
+      path: '/map',
+      name: 'map',
+      component: () => import('@/views/MapView.vue'),
+    },
+    {
+      path: '/tree/:id',
+      name: 'tree-detail',
+      component: () => import('@/views/TreeDetailView.vue'),
+    },
+    {
+      path: '/make-wish',
+      name: 'make-wish',
+      component: () => import('@/views/MakeWishView.vue'),
+    },
+    {
+      path: '/my-wishes',
+      name: 'my-wishes',
+      component: () => import('@/views/MyWishesView.vue'),
+    },
+    {
+      path: '/wish/:id',
+      name: 'wish-detail',
+      component: () => import('@/views/WishDetailView.vue'),
+    },
+    {
+      path: '/user',
+      name: 'user',
+      component: () => import('@/views/UserView.vue'),
+    },
+    {
+      path: '/login',
+      name: 'login',
+      component: () => import('@/views/LoginView.vue'),
+    },
+  ],
+})
+
+export default router

+ 16 - 0
wishing-tree-h5/src/stores/location.ts

@@ -0,0 +1,16 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useLocationStore = defineStore('location', () => {
+  const lng = ref<number | null>(null)
+  const lat = ref<number | null>(null)
+  const located = ref(false)
+
+  function setLocation(longitude: number, latitude: number) {
+    lng.value = longitude
+    lat.value = latitude
+    located.value = true
+  }
+
+  return { lng, lat, located, setLocation }
+})

+ 36 - 0
wishing-tree-h5/src/stores/user.ts

@@ -0,0 +1,36 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { getStorage, setStorage, removeStorage } from '@/utils/storage'
+
+export const useUserStore = defineStore('user', () => {
+  const token = ref(getStorage('token') || '')
+  const nickname = ref(getStorage('nickname') || '游客')
+  const phone = ref(getStorage('phone') || '')
+  const avatarUrl = ref(getStorage('avatarUrl') || '')
+
+  const isLoggedIn = computed(() => !!token.value)
+
+  function login(userData: { token: string; nickname: string; phone: string; avatarUrl?: string }) {
+    token.value = userData.token
+    nickname.value = userData.nickname
+    phone.value = userData.phone
+    avatarUrl.value = userData.avatarUrl || ''
+    setStorage('token', userData.token)
+    setStorage('nickname', userData.nickname)
+    setStorage('phone', userData.phone)
+    setStorage('avatarUrl', userData.avatarUrl || '')
+  }
+
+  function logout() {
+    token.value = ''
+    nickname.value = '游客'
+    phone.value = ''
+    avatarUrl.value = ''
+    removeStorage('token')
+    removeStorage('nickname')
+    removeStorage('phone')
+    removeStorage('avatarUrl')
+  }
+
+  return { token, nickname, phone, avatarUrl, isLoggedIn, login, logout }
+})

+ 50 - 0
wishing-tree-h5/src/style.css

@@ -0,0 +1,50 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html, body {
+  height: 100%;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+  font-size: 14px;
+  color: #333;
+  background: #f7f8fa;
+  -webkit-font-smoothing: antialiased;
+  -webkit-tap-highlight-color: transparent;
+}
+
+/* 禁止所有元素出现文本光标和选中 */
+* {
+  -webkit-user-select: none;
+  user-select: none;
+}
+
+/* 输入框和文本域允许选中 */
+input, textarea, [contenteditable] {
+  -webkit-user-select: text;
+  user-select: text;
+}
+
+#app {
+  height: 100%;
+}
+
+#app-root {
+  height: 100%;
+  overflow-y: auto;
+}
+
+.with-bottom-nav {
+  padding-bottom: 50px;
+}
+
+.ellipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.safe-area-bottom {
+  padding-bottom: env(safe-area-inset-bottom);
+}

+ 23 - 0
wishing-tree-h5/src/utils/amap.ts

@@ -0,0 +1,23 @@
+import AMapLoader from '@amap/amap-jsapi-loader'
+
+let AMapInstance: any = null
+
+export async function loadAMap(): Promise<any> {
+  if (AMapInstance) return AMapInstance
+
+  return AMapLoader.load({
+    key: '88a0c82c3f60a748c6c638ef9db16bca',
+    version: '2.0',
+    plugins: [
+      'AMap.Geolocation',
+      'AMap.Scale',
+      'AMap.ToolBar',
+      'AMap.Marker',
+      'AMap.InfoWindow',
+      'AMap.Geocoder',
+    ],
+  }).then((AMap: any) => {
+    AMapInstance = AMap
+    return AMap
+  })
+}

+ 53 - 0
wishing-tree-h5/src/utils/camera.ts

@@ -0,0 +1,53 @@
+export function takePhoto(): Promise<File> {
+  return new Promise((resolve, reject) => {
+    const input = document.createElement('input')
+    input.type = 'file'
+    input.accept = 'image/*'
+    input.capture = 'environment'
+    input.onchange = (e) => {
+      const files = (e.target as HTMLInputElement).files
+      if (files?.length) compressImage(files[0]).then(resolve).catch(reject)
+      else reject(new Error('未选择图片'))
+    }
+    input.click()
+  })
+}
+
+export function pickFromAlbum(multiple = false): Promise<File[]> {
+  return new Promise((resolve, reject) => {
+    const input = document.createElement('input')
+    input.type = 'file'
+    input.accept = 'image/*'
+    input.multiple = multiple
+    input.onchange = (e) => {
+      const files = (e.target as HTMLInputElement).files
+      if (files?.length) {
+        Promise.all(Array.from(files).map(compressImage)).then(resolve).catch(reject)
+      } else reject(new Error('未选择图片'))
+    }
+    input.click()
+  })
+}
+
+function compressImage(file: File): Promise<File> {
+  return new Promise((resolve) => {
+    const reader = new FileReader()
+    reader.onload = (e) => {
+      const img = new Image()
+      img.onload = () => {
+        const max = 1080
+        let w = img.width, h = img.height
+        if (w > max || h > max) {
+          if (w > h) { h = (h / w) * max; w = max }
+          else { w = (w / h) * max; h = max }
+        }
+        const c = document.createElement('canvas')
+        c.width = w; c.height = h
+        c.getContext('2d')!.drawImage(img, 0, 0, w, h)
+        c.toBlob((b) => resolve(new File([b!], file.name, { type: 'image/jpeg' })), 'image/jpeg', 0.8)
+      }
+      img.src = e.target!.result as string
+    }
+    reader.readAsDataURL(file)
+  })
+}

+ 13 - 0
wishing-tree-h5/src/utils/geo.ts

@@ -0,0 +1,13 @@
+export function getUserLocation(): Promise<{ lng: number; lat: number }> {
+  return new Promise((resolve, reject) => {
+    if (!navigator.geolocation) {
+      reject(new Error('浏览器不支持定位'))
+      return
+    }
+    navigator.geolocation.getCurrentPosition(
+      (pos) => resolve({ lng: pos.coords.longitude, lat: pos.coords.latitude }),
+      () => reject(new Error('定位失败')),
+      { enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 }
+    )
+  })
+}

+ 19 - 0
wishing-tree-h5/src/utils/storage.ts

@@ -0,0 +1,19 @@
+export function getStorage(key: string): string | null {
+  try {
+    return localStorage.getItem(key)
+  } catch {
+    return null
+  }
+}
+
+export function setStorage(key: string, value: string) {
+  try {
+    localStorage.setItem(key, value)
+  } catch { /* noop */ }
+}
+
+export function removeStorage(key: string) {
+  try {
+    localStorage.removeItem(key)
+  } catch { /* noop */ }
+}

+ 20 - 0
wishing-tree-h5/src/utils/theme.ts

@@ -0,0 +1,20 @@
+export const gradients = [
+  'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+  'linear-gradient(135deg, #ff6b6b 0%, #e74c3c 100%)',
+  'linear-gradient(135deg, #6c5ce7 0%, #4834d4 100%)',
+  'linear-gradient(135deg, #fd79a8 0%, #e84393 100%)',
+  'linear-gradient(135deg, #fdcb6e 0%, #f39c12 100%)',
+  'linear-gradient(135deg, #00b894 0%, #00a381 100%)',
+  'linear-gradient(135deg, #74b9ff 0%, #0984e3 100%)',
+  'linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%)',
+]
+
+export const emojis = ['🌳', '🌸', '🪷', '💕', '🌲', '🏙️', '🗼', '🐟']
+
+export function getTreeGradient(treeId: number) {
+  return gradients[(treeId - 1) % gradients.length]
+}
+
+export function getTreeEmoji(treeId: number) {
+  return emojis[(treeId - 1) % emojis.length]
+}

+ 135 - 0
wishing-tree-h5/src/views/HomeView.vue

@@ -0,0 +1,135 @@
+<template>
+  <div class="home-page with-bottom-nav">
+    <div class="hero-section">
+      <h1 class="hero-title">许愿树 ✨</h1>
+      <p class="hero-sub">找到你身边的许愿树,记录美好心愿</p>
+      <div class="location-bar">
+        <van-icon name="location-o" />
+        <span v-if="locating">正在获取位置...</span>
+        <span v-else-if="locationStore.located">
+          已定位 · 附近 {{ nearby.length }} 棵许愿树
+        </span>
+        <span v-else>定位失败,显示全部许愿树</span>
+      </div>
+    </div>
+
+    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+      <van-list
+        v-model:loading="loading"
+        :finished="finished"
+        finished-text="已经找到所有许愿树啦 ✨"
+        @load="loadTrees"
+      >
+        <tree-card
+          v-for="tree in nearby"
+          :key="tree.id"
+          :tree="tree"
+          @click="goTree(tree.id)"
+        />
+      </van-list>
+    </van-pull-refresh>
+
+    <div class="empty-hint" v-if="!loading && nearby.length === 0">
+      <van-empty description="附近暂无许愿树" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { showDialog } from 'vant'
+import { useLocationStore } from '@/stores/location'
+import { getUserLocation } from '@/utils/geo'
+import { fetchNearbyTrees } from '@/api/tree'
+import TreeCard from '@/components/TreeCard.vue'
+
+const router = useRouter()
+const locationStore = useLocationStore()
+
+const nearby = ref<any[]>([])
+const loading = ref(false)
+const finished = ref(false)
+const refreshing = ref(false)
+const locating = ref(true)
+
+async function loadLocation() {
+  locating.value = true
+  try {
+    const { lng, lat } = await getUserLocation()
+    locationStore.setLocation(lng, lat)
+  } catch {
+    // 定位失败,使用默认坐标(北京)
+    locationStore.setLocation(116.4074, 39.9042)
+  } finally {
+    locating.value = false
+  }
+}
+
+async function loadTrees() {
+  loading.value = true
+  try {
+    const result = await fetchNearbyTrees(
+      locationStore.lng || 116.4074,
+      locationStore.lat || 39.9042,
+      50000
+    )
+    nearby.value = result
+    finished.value = true
+  } finally {
+    loading.value = false
+    refreshing.value = false
+  }
+}
+
+async function onRefresh() {
+  refreshing.value = true
+  await loadLocation()
+  finished.value = false
+  await loadTrees()
+}
+
+function goTree(id: number) {
+  router.push(`/tree/${id}`)
+}
+
+onMounted(async () => {
+  await loadLocation()
+  loadTrees()
+})
+</script>
+
+<style scoped>
+.home-page {
+  min-height: 100vh;
+  background: #f7f8fa;
+}
+.hero-section {
+  background: linear-gradient(135deg, #07c160, #05a650);
+  padding: 30px 20px 20px;
+  color: #fff;
+  text-align: center;
+}
+.hero-title {
+  font-size: 28px;
+  font-weight: 700;
+  margin-bottom: 6px;
+}
+.hero-sub {
+  font-size: 14px;
+  opacity: 0.85;
+  margin-bottom: 16px;
+}
+.location-bar {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  background: rgba(255,255,255,0.2);
+  border-radius: 20px;
+  padding: 6px 16px;
+  font-size: 12px;
+}
+.empty-hint {
+  padding-top: 60px;
+}
+</style>

+ 127 - 0
wishing-tree-h5/src/views/LoginView.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="login-page">
+    <van-nav-bar title="登录" left-text="返回" left-arrow @click-left="$router.back()" />
+
+    <div class="login-content">
+      <div class="login-hero">
+        <h2>欢迎来到许愿树 ✨</h2>
+        <p>登录后记录你的每一个心愿</p>
+      </div>
+
+      <van-form @submit="handleLogin">
+        <van-cell-group inset>
+          <van-field
+            v-model="phone"
+            name="phone"
+            label="手机号"
+            placeholder="请输入手机号"
+            type="tel"
+            maxlength="11"
+            :rules="[{ required: true, pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }]"
+          />
+          <van-field
+            v-model="code"
+            name="code"
+            label="验证码"
+            placeholder="请输入验证码"
+            maxlength="6"
+            :rules="[{ required: true, message: '请输入验证码' }]"
+          >
+            <template #button>
+              <van-button
+                size="small"
+                type="primary"
+                plain
+                :disabled="countdown > 0"
+                @click="sendCode"
+              >
+                {{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
+              </van-button>
+            </template>
+          </van-field>
+        </van-cell-group>
+
+        <div class="submit-wrap">
+          <van-button type="primary" block round native-type="submit" :loading="logging">
+            登录
+          </van-button>
+        </div>
+      </van-form>
+
+      <p class="login-tip">Mock 模式:输入任意手机号和验证码即可登录</p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { showSuccessToast, showToast } from 'vant'
+import { useUserStore } from '@/stores/user'
+import { sendSms, login } from '@/api/auth'
+
+const router = useRouter()
+const userStore = useUserStore()
+
+const phone = ref('')
+const code = ref('')
+const countdown = ref(0)
+const logging = ref(false)
+
+function sendCode() {
+  if (!/^1[3-9]\d{9}$/.test(phone.value)) {
+    showToast('请输入正确的手机号')
+    return
+  }
+  sendSms(phone.value)
+  showToast('验证码已发送(Mock模式任意6位数字)')
+  countdown.value = 60
+  const t = setInterval(() => {
+    countdown.value--
+    if (countdown.value <= 0) clearInterval(t)
+  }, 1000)
+}
+
+async function handleLogin() {
+  logging.value = true
+  try {
+    const res = await login(phone.value, code.value)
+    userStore.login(res.data)
+    showSuccessToast('登录成功')
+    router.replace('/')
+  } finally {
+    logging.value = false
+  }
+}
+</script>
+
+<style scoped>
+.login-page {
+  min-height: 100vh;
+  background: #f7f8fa;
+}
+.login-content {
+  padding: 16px;
+}
+.login-hero {
+  text-align: center;
+  padding: 30px 0;
+}
+.login-hero h2 {
+  font-size: 22px;
+  margin-bottom: 6px;
+}
+.login-hero p {
+  font-size: 14px;
+  color: #999;
+}
+.submit-wrap {
+  padding: 24px 16px;
+}
+.login-tip {
+  text-align: center;
+  font-size: 12px;
+  color: #bbb;
+  margin-top: 20px;
+}
+</style>

+ 184 - 0
wishing-tree-h5/src/views/MakeWishView.vue

@@ -0,0 +1,184 @@
+<template>
+  <div class="make-wish-page">
+    <van-nav-bar title="许下心愿" left-text="返回" left-arrow @click-left="$router.back()" />
+
+    <div class="page-body">
+      <div class="tree-info" v-if="tree">
+        <p class="for-tree">🌳 {{ tree.name }}</p>
+        <van-tag :type="isInRange ? 'success' : 'warning'" round>
+          {{ isInRange ? `距离 ${distance}m · 在范围内` : `距离 ${distance}m · 太远了` }}
+        </van-tag>
+      </div>
+
+      <!-- 图片上传 -->
+      <div class="section">
+        <h4 class="section-title">心愿照片(可选)</h4>
+        <image-uploader v-model="images" :max="9" />
+      </div>
+
+      <!-- 愿望内容 -->
+      <div class="section">
+        <h4 class="section-title">写下你的愿望</h4>
+        <van-field
+          v-model="content"
+          type="textarea"
+          rows="4"
+          :maxlength="200"
+          :show-word-limit="true"
+          placeholder="希望家人身体健康,工作顺利..."
+        />
+      </div>
+
+      <!-- 标签选择 -->
+      <div class="section">
+        <h4 class="section-title">选择标签(可选)</h4>
+        <tag-selector v-model="tags" />
+      </div>
+
+      <!-- 公开设置 -->
+      <div class="section">
+        <van-cell title="公开愿望" :value="isPublic ? '他人可见' : '仅自己可见'">
+          <template #right-icon>
+            <van-switch v-model="isPublic" size="22" />
+          </template>
+        </van-cell>
+      </div>
+
+      <!-- 提交按钮 -->
+      <div class="submit-section">
+        <van-button
+          type="primary"
+          block
+          round
+          :loading="submitting"
+          :disabled="!content.trim() || !isInRange"
+          @click="submitWish"
+        >
+          {{ isInRange ? '🙏 许愿' : '需要靠近许愿树才能许愿' }}
+        </van-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { showToast, showSuccessToast } from 'vant'
+import { useLocationStore } from '@/stores/location'
+import { useUserStore } from '@/stores/user'
+import { fetchTreeDetail } from '@/api/tree'
+import { submitWish as submitWishApi } from '@/api/wish'
+import ImageUploader from '@/components/ImageUploader.vue'
+import TagSelector from '@/components/TagSelector.vue'
+
+const route = useRoute()
+const router = useRouter()
+const locationStore = useLocationStore()
+const userStore = useUserStore()
+
+const tree = ref<any>(null)
+const images = ref<string[]>([])
+const content = ref('')
+const tags = ref<string[]>([])
+const isPublic = ref(true)
+const submitting = ref(false)
+
+const distance = computed(() => {
+  if (!tree.value || !locationStore.lng) return Infinity
+  return Math.round(
+    haversine(locationStore.lng, locationStore.lat!, tree.value.longitude, tree.value.latitude)
+  )
+})
+
+const isInRange = computed(() => {
+  if (!tree.value) return false
+  return distance.value <= (tree.value.radius || 100)
+})
+
+async function submitWish() {
+  if (!content.value.trim()) {
+    showToast('请写下你的愿望')
+    return
+  }
+  if (!userStore.isLoggedIn) {
+    showToast('请先登录')
+    router.push('/login')
+    return
+  }
+
+  submitting.value = true
+  try {
+    await submitWishApi({
+      treeId: tree.value.id,
+      treeName: tree.value.name,
+      userId: userStore.phone,
+      content: content.value,
+      images: images.value,
+      lng: locationStore.lng || 0,
+      lat: locationStore.lat || 0,
+      address: tree.value.address,
+      isPublic: isPublic.value,
+      tags: tags.value,
+    })
+    showSuccessToast('愿望已挂在树上!祝你心想事成 ✨')
+    setTimeout(() => router.replace('/my-wishes'), 800)
+  } finally {
+    submitting.value = false
+  }
+}
+
+function haversine(lng1: number, lat1: number, lng2: number, lat2: number) {
+  const R = 6371000
+  const toRad = (d: number) => (d * Math.PI) / 180
+  const dLat = toRad(lat2 - lat1)
+  const dLng = toRad(lng2 - lng1)
+  const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2
+  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
+}
+
+onMounted(async () => {
+  const id = Number(route.query.treeId)
+  if (id) {
+    tree.value = await fetchTreeDetail(id)
+  }
+})
+</script>
+
+<style scoped>
+.make-wish-page {
+  min-height: 100vh;
+  background: #f7f8fa;
+  padding-bottom: 30px;
+}
+.page-body {
+  padding: 12px;
+}
+.tree-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  background: #fff;
+  border-radius: 8px;
+  margin-bottom: 12px;
+}
+.for-tree {
+  font-size: 15px;
+  font-weight: 500;
+}
+.section {
+  background: #fff;
+  border-radius: 8px;
+  margin-bottom: 12px;
+  padding: 12px;
+}
+.section-title {
+  font-size: 14px;
+  color: #666;
+  margin-bottom: 8px;
+}
+.submit-section {
+  padding: 20px 16px;
+}
+</style>

+ 289 - 0
wishing-tree-h5/src/views/MapView.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="map-page with-bottom-nav">
+    <div ref="mapContainer" class="map-container" />
+
+    <!-- 定位按钮 -->
+    <div class="map-controls">
+      <van-button icon="aim" round type="primary" size="small" :loading="locating" @click="locateUser">
+        定位
+      </van-button>
+    </div>
+
+    <!-- 许愿树信息弹出层 -->
+    <van-popup
+      v-model:show="showPopup"
+      position="bottom"
+      round
+      :style="{ maxHeight: '45%' }"
+      safe-area-inset-bottom
+    >
+      <div class="tree-popup" v-if="selectedTree">
+        <div class="popup-cover" :style="{ background: popupGradient }">
+          <span class="popup-emoji">{{ popupEmoji }}</span>
+        </div>
+        <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>
+          <span class="popup-count">💬 {{ selectedTree.totalWishes }} 个愿望</span>
+        </div>
+        <div class="popup-actions">
+          <van-button
+            type="primary"
+            block
+            round
+            size="small"
+            @click="goTreeDetail"
+          >
+            查看详情
+          </van-button>
+          <van-button
+            type="success"
+            block
+            round
+            size="small"
+            :disabled="!selectedTree.isInRange"
+            @click="goMakeWish"
+          >
+            {{ selectedTree.isInRange ? '🙏 去许愿' : '需要靠近许愿树' }}
+          </van-button>
+        </div>
+      </div>
+    </van-popup>
+  </div>
+</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'
+
+const router = useRouter()
+const locationStore = useLocationStore()
+
+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))
+
+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'
+}
+
+function goTreeDetail() {
+  showPopup.value = false
+  router.push(`/tree/${selectedTree.value.id}`)
+}
+
+function goMakeWish() {
+  showPopup.value = false
+  router.push({ path: '/make-wish', query: { treeId: selectedTree.value.id } })
+}
+
+async function initMap() {
+  try {
+    AMap = await loadAMap()
+
+    map = new AMap.Map(mapContainer.value!, {
+      zoom: 14,
+      center: [116.4074, 39.9042],
+      mapStyle: 'amap://styles/light',
+    })
+
+    map.addControl(new AMap.Scale())
+    map.addControl(new AMap.ToolBar({ position: 'RT' }))
+
+    await locateUser()
+  } catch (err) {
+    showToast('地图加载失败')
+    console.error(err)
+  }
+}
+
+async function locateUser() {
+  locating.value = true
+  try {
+    const { lng, lat } = await getUserLocation()
+    locationStore.setLocation(lng, lat)
+
+    if (map) {
+      map.setCenter([lng, lat])
+
+      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),
+        }),
+        zIndex: 200,
+        anchor: 'center',
+      })
+      map.add(userMarker)
+    }
+
+    await loadTrees()
+  } catch {
+    await loadTrees()
+  } finally {
+    locating.value = false
+  }
+}
+
+async function loadTrees() {
+  if (!locationStore.lng) return
+
+  const trees = await fetchNearbyTrees(
+    locationStore.lng,
+    locationStore.lat!,
+    50000,
+    100
+  )
+
+  // 清除旧标记
+  treeMarkers.forEach((m) => map.remove(m))
+  treeMarkers = []
+
+  trees.forEach((tree: any) => {
+    const isIn = tree.isInRange
+    const color = isIn ? '#07c160' : '#ff976a'
+    const label = `${tree.name} ${tree.distance}m`
+
+    const content = `
+      <div style="
+        position:relative;
+        text-align:center;
+        background:${color};
+        color:#fff;
+        padding:4px 10px;
+        border-radius:16px;
+        font-size:11px;
+        white-space:nowrap;
+        box-shadow:0 2px 8px rgba(0,0,0,0.3);
+        max-width:160px;
+        overflow:hidden;
+        text-overflow:ellipsis;
+      ">
+        ${label}
+        <div style="
+          position:absolute;
+          bottom:-6px;
+          left:50%;
+          transform:translateX(-50%);
+          width:0;
+          height:0;
+          border-left:6px solid transparent;
+          border-right:6px solid transparent;
+          border-top:6px solid ${color};
+        "></div>
+      </div>`
+
+    const marker = new AMap.Marker({
+      position: [tree.longitude, tree.latitude],
+      content: content,
+      anchor: 'bottom-center',
+      offset: new (AMap as any).Pixel(0, -6),
+    })
+
+    marker.on('click', () => {
+      selectedTree.value = tree
+      showPopup.value = true
+    })
+
+    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])
+  }
+}
+
+onMounted(async () => {
+  await nextTick()
+  initMap()
+})
+
+onUnmounted(() => {
+  if (map) map.destroy()
+})
+</script>
+
+<style scoped>
+.map-page {
+  position: relative;
+  width: 100%;
+  height: calc(100vh - 50px);
+}
+.map-container {
+  width: 100%;
+  height: 100%;
+}
+.map-controls {
+  position: absolute;
+  bottom: 20px;
+  right: 16px;
+  z-index: 100;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.tree-popup {
+  padding: 16px;
+}
+.popup-cover {
+  width: 100%;
+  height: 120px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 12px;
+}
+.popup-emoji {
+  font-size: 44px;
+  text-shadow: 0 1px 4px rgba(0,0,0,0.2);
+}
+.popup-name {
+  font-size: 17px;
+  margin: 10px 0 4px;
+}
+.popup-addr {
+  font-size: 12px;
+  color: #999;
+  margin-bottom: 10px;
+}
+.popup-meta {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 14px;
+}
+.popup-count {
+  font-size: 12px;
+  color: #666;
+}
+.popup-actions {
+  display: flex;
+  gap: 8px;
+}
+</style>

+ 68 - 0
wishing-tree-h5/src/views/MyWishesView.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="my-wishes-page with-bottom-nav">
+    <van-nav-bar title="我的愿望" />
+
+    <div v-if="!userStore.isLoggedIn" class="no-login">
+      <van-empty description="请先登录查看你的愿望">
+        <van-button type="primary" round to="/login">去登录</van-button>
+      </van-empty>
+    </div>
+
+    <template v-else>
+      <van-tabs v-model:active="activeTab" sticky>
+        <van-tab title="全部愿望">
+          <van-pull-refresh v-model="refreshing" @refresh="loadWishes">
+            <wish-card
+              v-for="wish in wishes"
+              :key="wish.id"
+              :wish="wish"
+              @click="goWish(wish.id)"
+            />
+            <van-empty v-if="wishes.length === 0" description="你还没有许过愿望哦">
+              <van-button type="primary" round to="/">去许愿</van-button>
+            </van-empty>
+          </van-pull-refresh>
+        </van-tab>
+      </van-tabs>
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { useUserStore } from '@/stores/user'
+import { fetchMyWishes } from '@/api/wish'
+import WishCard from '@/components/WishCard.vue'
+import type { Wish } from '@/mock/data'
+
+const router = useRouter()
+const userStore = useUserStore()
+
+const wishes = ref<Wish[]>([])
+const activeTab = ref(0)
+const refreshing = ref(false)
+
+async function loadWishes() {
+  if (!userStore.phone) return
+  const result = await fetchMyWishes(userStore.phone)
+  wishes.value = result.list
+  refreshing.value = false
+}
+
+function goWish(id: number) {
+  router.push(`/wish/${id}`)
+}
+
+onMounted(loadWishes)
+</script>
+
+<style scoped>
+.my-wishes-page {
+  min-height: 100vh;
+  background: #f7f8fa;
+}
+.no-login {
+  padding-top: 60px;
+}
+</style>

+ 150 - 0
wishing-tree-h5/src/views/TreeDetailView.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="tree-detail-page">
+    <van-nav-bar title="许愿树详情" left-text="返回" left-arrow @click-left="$router.back()" />
+
+    <div v-if="tree">
+      <div class="tree-banner" :style="{ background: treeGradient }">
+        <span class="banner-emoji">{{ treeEmoji }}</span>
+        <h2 class="banner-name">{{ tree.name }}</h2>
+      </div>
+
+      <div class="tree-header">
+        <h2>{{ tree.name }}</h2>
+        <p class="tree-addr"><van-icon name="location-o" /> {{ tree.address }}</p>
+        <p class="tree-desc">{{ tree.description }}</p>
+        <div class="tree-stats">
+          <span>💬 {{ tree.totalWishes }} 个愿望</span>
+        </div>
+        <van-button
+          type="primary"
+          block
+          round
+          :disabled="!locationStore.located"
+          @click="goMakeWish"
+        >
+          去许愿 🙏
+        </van-button>
+      </div>
+
+      <van-tabs v-model:active="activeTab" sticky>
+        <van-tab title="公开愿望">
+          <van-pull-refresh v-model="refreshing" @refresh="loadWishes">
+            <wish-card
+              v-for="wish in wishes"
+              :key="wish.id"
+              :wish="wish"
+              @click="goWish(wish.id)"
+            />
+            <van-empty v-if="wishes.length === 0" description="还没有人在这里许愿,快来第一个!" />
+          </van-pull-refresh>
+        </van-tab>
+      </van-tabs>
+    </div>
+
+    <van-loading v-else class="loading" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { useLocationStore } from '@/stores/location'
+import { fetchTreeDetail } from '@/api/tree'
+import { fetchTreeWishes } from '@/api/wish'
+import WishCard from '@/components/WishCard.vue'
+import { getTreeGradient, getTreeEmoji } from '@/utils/theme'
+import type { WishingTree } from '@/mock/data'
+import type { Wish } from '@/mock/data'
+
+const route = useRoute()
+const router = useRouter()
+const locationStore = useLocationStore()
+
+const tree = ref<WishingTree | null>(null)
+const wishes = ref<Wish[]>([])
+const activeTab = ref(0)
+const refreshing = ref(false)
+
+const treeGradient = computed(() => getTreeGradient(tree.value?.id || 1))
+const treeEmoji = computed(() => getTreeEmoji(tree.value?.id || 1))
+
+async function loadWishes() {
+  const id = Number(route.params.id)
+  const result = await fetchTreeWishes(id)
+  wishes.value = result.list
+  refreshing.value = false
+}
+
+function goMakeWish() {
+  router.push({ path: '/make-wish', query: { treeId: route.params.id } })
+}
+
+function goWish(id: number) {
+  router.push(`/wish/${id}`)
+}
+
+onMounted(async () => {
+  const id = Number(route.params.id)
+  tree.value = await fetchTreeDetail(id)
+  loadWishes()
+})
+</script>
+
+<style scoped>
+.tree-detail-page {
+  min-height: 100vh;
+  background: #f7f8fa;
+}
+.tree-banner {
+  width: 100%;
+  height: 200px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  color: #fff;
+}
+.banner-emoji {
+  font-size: 48px;
+  text-shadow: 0 1px 4px rgba(0,0,0,0.2);
+}
+.banner-name {
+  font-size: 20px;
+  font-weight: 600;
+  color: #fff;
+  margin: 0;
+  text-shadow: 0 1px 3px rgba(0,0,0,0.2);
+}
+.tree-header {
+  padding: 16px;
+  background: #fff;
+  margin-bottom: 8px;
+}
+.tree-header h2 {
+  font-size: 20px;
+  margin-bottom: 8px;
+}
+.tree-addr {
+  font-size: 13px;
+  color: #666;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.tree-desc {
+  font-size: 14px;
+  color: #555;
+  line-height: 1.6;
+  margin-bottom: 12px;
+}
+.tree-stats {
+  margin-bottom: 16px;
+  font-size: 13px;
+  color: #07c160;
+}
+.loading {
+  margin: 100px auto;
+}
+</style>

+ 128 - 0
wishing-tree-h5/src/views/UserView.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="user-page with-bottom-nav">
+    <div class="user-header">
+      <van-image
+        :src="userStore.avatarUrl || 'https://picsum.photos/seed/avatar/200/200'"
+        width="60"
+        height="60"
+        round
+        fit="cover"
+      />
+      <h3>{{ userStore.isLoggedIn ? userStore.nickname : '游客' }}</h3>
+      <p class="user-phone" v-if="userStore.phone">{{ userStore.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}</p>
+    </div>
+
+    <div class="user-stats">
+      <div class="stat-item">
+        <span class="stat-num">{{ wishCount }}</span>
+        <span class="stat-label">许愿数</span>
+      </div>
+      <div class="stat-item">
+        <span class="stat-num">{{ totalLikes }}</span>
+        <span class="stat-label">收到的祝福</span>
+      </div>
+    </div>
+
+    <van-cell-group inset>
+      <van-cell title="我的愿望" icon="label-o" to="/my-wishes" is-link />
+      <van-cell title="最近许愿" icon="clock-o" is-link @click="$router.push('/my-wishes')" />
+    </van-cell-group>
+
+    <div class="logout-section">
+      <van-button
+        v-if="userStore.isLoggedIn"
+        type="danger"
+        round
+        plain
+        block
+        @click="handleLogout"
+      >
+        退出登录
+      </van-button>
+      <van-button
+        v-else
+        type="primary"
+        round
+        block
+        to="/login"
+      >
+        登录 / 注册
+      </van-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { showConfirmDialog } from 'vant'
+import { useUserStore } from '@/stores/user'
+import { useRouter } from 'vue-router'
+import { fetchMyWishes } from '@/api/wish'
+
+const userStore = useUserStore()
+const router = useRouter()
+
+const wishCount = ref(0)
+const totalLikes = ref(0)
+
+async function handleLogout() {
+  await showConfirmDialog({ title: '确定退出登录?' })
+  userStore.logout()
+  router.push('/')
+}
+
+onMounted(async () => {
+  if (userStore.phone) {
+    const result = await fetchMyWishes(userStore.phone)
+    wishCount.value = result.total
+    totalLikes.value = result.list.reduce((s, w) => s + w.likes, 0)
+  }
+})
+</script>
+
+<style scoped>
+.user-page {
+  min-height: 100vh;
+  background: #f7f8fa;
+}
+.user-header {
+  background: linear-gradient(135deg, #07c160, #05a650);
+  padding: 40px 20px 30px;
+  text-align: center;
+  color: #fff;
+}
+.user-header h3 {
+  margin-top: 12px;
+  font-size: 18px;
+}
+.user-phone {
+  font-size: 13px;
+  opacity: 0.8;
+  margin-top: 4px;
+}
+.user-stats {
+  display: flex;
+  background: #fff;
+  margin: -12px 16px 12px;
+  border-radius: 12px;
+  padding: 16px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+}
+.stat-item {
+  flex: 1;
+  text-align: center;
+}
+.stat-num {
+  display: block;
+  font-size: 22px;
+  font-weight: 700;
+  color: #07c160;
+}
+.stat-label {
+  font-size: 12px;
+  color: #999;
+}
+.logout-section {
+  padding: 30px 16px;
+}
+</style>

+ 158 - 0
wishing-tree-h5/src/views/WishDetailView.vue

@@ -0,0 +1,158 @@
+<template>
+  <div class="wish-detail-page">
+    <van-nav-bar title="愿望详情" left-text="返回" left-arrow @click-left="$router.back()" />
+
+    <div v-if="wish" class="wish-detail">
+      <div class="wish-content-box">
+        <p class="wish-text">{{ wish.content }}</p>
+      </div>
+
+      <div class="wish-images-box" v-if="wish.images?.length">
+        <van-image
+          v-for="(img, i) in wish.images"
+          :key="i"
+          :src="img"
+          width="100%"
+          fit="contain"
+          radius="8"
+          lazy-load
+          style="margin-bottom: 8px"
+        />
+      </div>
+
+      <div class="wish-tags-box" v-if="wish.tags?.length">
+        <van-tag v-for="t in wish.tags" :key="t" plain type="primary" size="medium" style="margin-right: 6px">
+          {{ t }}
+        </van-tag>
+      </div>
+
+      <div class="wish-meta-box">
+        <div class="meta-item">
+          <span class="meta-label">许愿树</span>
+          <span class="meta-value">{{ wish.treeName }}</span>
+        </div>
+        <div class="meta-item">
+          <span class="meta-label">地点</span>
+          <span class="meta-value">{{ wish.address }}</span>
+        </div>
+        <div class="meta-item">
+          <span class="meta-label">时间</span>
+          <span class="meta-value">{{ wish.createdAt }}</span>
+        </div>
+        <div class="meta-item">
+          <span class="meta-label">可见性</span>
+          <span class="meta-value">{{ wish.isPublic ? '公开' : '仅自己可见' }}</span>
+        </div>
+      </div>
+
+      <div class="wish-actions">
+        <van-button icon="good-job-o" type="default" round @click="handleLike">
+          祝福 {{ wish.likes }}
+        </van-button>
+        <van-button
+          v-if="isOwner"
+          icon="delete-o"
+          type="danger"
+          round
+          plain
+          @click="handleDelete"
+        >
+          删除
+        </van-button>
+      </div>
+    </div>
+
+    <van-loading v-else class="loading" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { showConfirmDialog, showSuccessToast } from 'vant'
+import { useUserStore } from '@/stores/user'
+import { fetchWishDetail, removeWish, toggleLikeWish } from '@/api/wish'
+import type { Wish } from '@/mock/data'
+
+const route = useRoute()
+const router = useRouter()
+const userStore = useUserStore()
+
+const wish = ref<Wish | null>(null)
+
+const isOwner = computed(() => wish.value?.userId === userStore.phone)
+
+async function handleLike() {
+  if (!wish.value) return
+  const likes = await toggleLikeWish(wish.value.id)
+  wish.value.likes = likes
+  showSuccessToast('已送上祝福 ❤')
+}
+
+async function handleDelete() {
+  if (!wish.value) return
+  await showConfirmDialog({ title: '确定删除这个愿望吗?', message: '删除后不可恢复' })
+  await removeWish(wish.value.id)
+  showSuccessToast('愿望已删除')
+  router.replace('/my-wishes')
+}
+
+onMounted(async () => {
+  const id = Number(route.params.id)
+  wish.value = await fetchWishDetail(id)
+})
+</script>
+
+<style scoped>
+.wish-detail-page {
+  min-height: 100vh;
+  background: #f7f8fa;
+}
+.wish-detail {
+  padding: 12px;
+}
+.wish-content-box {
+  background: #fff;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 12px;
+}
+.wish-text {
+  font-size: 17px;
+  line-height: 1.8;
+  white-space: pre-wrap;
+}
+.wish-images-box {
+  margin-bottom: 12px;
+}
+.wish-tags-box {
+  margin-bottom: 12px;
+}
+.wish-meta-box {
+  background: #fff;
+  border-radius: 12px;
+  padding: 16px;
+  margin-bottom: 12px;
+}
+.meta-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 6px 0;
+  font-size: 14px;
+}
+.meta-label {
+  color: #999;
+}
+.meta-value {
+  color: #333;
+}
+.wish-actions {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+  padding: 20px 0;
+}
+.loading {
+  margin: 100px auto;
+}
+</style>

+ 15 - 0
wishing-tree-h5/tsconfig.app.json

@@ -0,0 +1,15 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "types": ["vite/client"],
+    "paths": {
+      "@/*": ["./src/*"]
+    },
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}

+ 7 - 0
wishing-tree-h5/tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 24 - 0
wishing-tree-h5/tsconfig.node.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "es2023",
+    "lib": ["ES2023"],
+    "module": "esnext",
+    "types": ["node"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 23 - 0
wishing-tree-h5/vite.config.ts

@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import Components from 'unplugin-vue-components/vite'
+import { VantResolver } from '@vant/auto-import-resolver'
+import { fileURLToPath, URL } from 'node:url'
+
+export default defineConfig({
+  plugins: [
+    vue(),
+    Components({
+      resolvers: [VantResolver()],
+    }),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+  server: {
+    host: '0.0.0.0',
+    port: 3000,
+  },
+})

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно