AI技能大赛——郑州文旅地图
title: Vue 3 + Vite + Leaflet 实战:开发一个完整的文旅地图应用(含 Electron 桌面封装)
date: 2026-05-15
tags: [Vue3, Vite, Pinia, Leaflet, TypeScript, Electron, 前端实战, 地图应用]
categories: 前端开发

一、项目简介

郑州文旅地图是一个基于 Vue 3 全家桶开发的交互式文旅地图应用。它覆盖了郑州 60+ 个景点(从 5A 级少林寺到现代地标二七塔),提供了打卡签到、游戏化收集、路线规划、多主题切换等一系列完整的用户体验。

核心亮点: 纯前端 + localStorage 持久化,无需后端服务器,构建后即可通过浏览器或 Electron 桌面应用使用。

在这里插入图片描述

功能清单

功能 说明
🗺️ 交互式地图 Leaflet + 高德地图瓦片,支持缩放和平移
📍 景点标记 60+ 景点按 6 个类别分类展示
✅ 打卡签到 签到获得随机物品和成就徽章
🎮 游戏化系统 物品收集、作物种植、隐藏宝藏探索
🏆 成就徽章 14 个徽章,涵盖各种挑战目标
🎨 多主题切换 3 种地图风格 × 5 种季节主题
📖 电子护照 查看打卡记录、已获徽章
🤖 AI 路线规划 基于天气和评分的智能推荐
🖥️ 桌面应用 Electron 封装,双击即可运行

二、核心技术栈

技术 版本 用途
Vue 3 ^3.5.34 组合式 API + <script setup>
Vite ^8.0.12 构建工具
Pinia ^3.0.4 状态管理
TypeScript ~6.0.2 类型安全
Leaflet ^1.9.4 地图渲染引擎
Electron ^42.0.1 桌面应用封装
Vue Router ^4.6.4 SPA 路由(Hash 模式)

三、项目架构

src/
├── main.ts                    # 应用入口
├── App.vue                    # 根组件
├── router/index.ts            # 路由(Hash 模式)
├── stores/                    # 8 个 Pinia Store
│   ├── mapStore.ts            # 地图状态
│   ├── poiStore.ts            # POI 数据与搜索
│   ├── themeStore.ts          # 主题管理
│   ├── checkinStore.ts        # 打卡系统
│   ├── favoritesStore.ts      # 收藏管理
│   ├── farmStore.ts           # 农场系统
│   ├── routeStore.ts          # 路线规划
│   └── uiStore.ts             # UI 状态
├── components/
│   ├── map/MapViewport.vue    # 核心地图组件
│   ├── panels/                # 侧边面板(POI详情/规划器/天气等)
│   ├── passport/              # 电子护照
│   ├── farm/                  # 农场系统
│   └── ui/                    # UI 组件(季节横幅/Toast等)
├── composables/               # 可组合函数
├── data/                      # 数据层
│   ├── types/                 # TypeScript 类型定义
│   ├── cities/                # 城市模块(目前只有郑州)
│   └── constants/             # 常量(地图样式/徽章/物品等)
├── logic/                     # 业务逻辑
└── assets/styles/             # CSS 样式(含主题变量)

四、核心功能实现

4.1 地图引擎集成

地图使用 Leaflet 结合高德地图瓦片服务。初始化时锁定到郑州区域,限制缩放级别在地标可见范围内:

// src/components/map/MapViewport.vue (简化)
function initMap() {
  map = L.map('map', {
    center: [34.75, 113.68],  // 郑州中心坐标
    zoom: 11,
    minZoom: 10,
    maxBounds: [
      [34.30, 112.80],  // 西南角
      [34.95, 114.20]   // 东北角
    ]
  })
  initTileLayer(themeStore.mapStyle)
}

function initTileLayer(style: MapStyleId) {
  const config = MAP_TILE_CONFIG[style]
  return L.tileLayer(config.url, {
    subdomains: config.subdomains.split(''),
    attribution: config.attribution
  })
}

通过 CSS filter 实现主题风格的动态切换。以赛博朋克主题为例:

/* 赛博朋克:色调旋转 180° + 高饱和 + 低亮度 */
.map-viewport[data-style="cyberpunk"] .leaflet-tile-pane {
  filter: hue-rotate(180deg) saturate(2.5) brightness(0.65) contrast(1.2);
}

赛博朋克主题效果:
在这里插入图片描述

4.2 打卡签到 + 游戏化系统

打卡系统是应用的核心交互之一。每次打卡都会触发一套完整的奖励链:

// src/components/panels/PoiDetailCard.vue
function doCheckin() {
  if (!selectedPoi.value) return
  
  // 1. 检查现有徽章
  const prevBadges = checkBadges(checkinStore.badgeCheckData)
  
  // 2. 执行打卡
  checkinStore.checkin(selectedPoi.value.id)
  
  // 3. 检查是否解锁新徽章
  const newBadges = checkBadges(checkinStore.badgeCheckData)
  for (const badgeId of newBadges) {
    if (!prevBadges.has(badgeId)) {
      const badge = BADGES.find(b => b.id === badgeId)
      showToast('success', '🏅 解锁新成就!', `${badge.name}${badge.desc}`)
    }
  }
  
  // 4. 播放音效
  audio.playCheckinSound()
  
  // 5. 随机掉落物品
  const item = rollRandomItem()
  farmStore.addItem(item)
  
  // 6. 探索隐藏宝藏(8% 概率)
  if (Math.random() < 0.08) {
    farmStore.markTreasureFound(poi.id)
    showToast('info', '发现宝藏!', `隐藏宝藏已找到!`)
  }
}

打卡效果:
在这里插入图片描述

4.3 14 个成就徽章

徽章系统覆盖了从新手到资深玩家的全阶段成就:

徽章 条件 难度
🚀 初出茅庐 首次签到
🏕️ 开路先锋 签到 10 次 ⭐⭐
🏙️ 城市猎人 签到 30 次 ⭐⭐⭐
🏆 制霸全城 签到全部 63 个 POI ⭐⭐⭐⭐⭐
🌟 精英五档 签到全部 5A 景区 ⭐⭐
🦉 夜猫子 晚上 8 点后签到 3 次 ⭐⭐
🔥 持之以恒 连续签到 7 天 ⭐⭐⭐⭐
🎭 三连击 同一天签到 3 个 POI ⭐⭐⭐

徽章检查函数使用纯函数式设计,便于测试:

export function checkBadges(data: BadgeCheckData): Set<string> {
  const unlocked = new Set<string>()
  const { checkedCount, uniqueDays, categoryCounts, favCount, ... } = data
  
  if (checkedCount >= 1)    unlocked.add('first-step')
  if (checkedCount >= 10)   unlocked.add('trailblazer')
  if (checkedCount >= 30)   unlocked.add('city-hunter')
  if (checkedCount >= 63)   unlocked.add('full-house')
  if (uniqueDays >= 7)      unlocked.add('streak-7')
  // ...
  return unlocked
}

4.4 多主题切换系统

地图主题和季节主题是独立的两个维度,可以自由组合(3 × 5 = 15 种组合)。

地图主题通过切换高德瓦片的 style 参数 + CSS 滤镜实现:

// src/data/constants/mapStyles.ts
export const MAP_TILE_CONFIG = {
  pixel:     { url: '...&style=7', ... },  // 像素风
  doodle:    { url: '...&style=8', ... },  // 涂鸦风
  cyberpunk: { url: '...&style=7', ... },  // 赛博朋克(靠 CSS 滤镜实现)
}

季节主题通过 CSS 变量覆盖实现,5 个季节各有一套独立的配色方案:

/* 春季 - 粉绿清新 */
[data-season="spring"] {
  --sv-green: #6BAF8D;
  --sv-gold: #D4A373;
  --bg-card: #FAFAF5;
}

/* 秋季 - 金黄暖调 */
[data-season="autumn"] {
  --sv-green: #8B7D3C;
  --sv-gold: #C97D3C;
  --bg-card: #FDF6EC;
}

/* 冬季 - 霜白冷色 */
[data-season="winter"] {
  --sv-green: #5B8C7A;
  --sv-gold: #B8A88A;
  --bg-card: #F5F5F8;
}

4.5 农场种植系统

农场系统包含三个标签页:背包、种植、宝藏。

背包存放打卡获得的物品,按稀有度分为普通(绿色)、稀有(蓝色)、史诗(紫色):

// 物品掉落概率
const COMMON_RATE = 0.60   // 普通
const RARE_RATE   = 0.30   // 稀有
const EPIC_RATE   = 0.10   // 史诗

种植系统有 6 种作物,每种有各自的生长周期:

作物 生长时间 种子来源
🌾 小麦 1 天 打卡掉落
🥕 胡萝卜 1 天 打卡掉落
🌻 向日葵 2 天 打卡掉落
🍇 葡萄 3 天 打卡掉落
🌳 古树 5 天 稀有掉落

3 块田地支持同时种植,收获时根据作物类型获得奖励。

农场界面:
在这里插入图片描述

4.6 Electron 桌面封装

为了让用户像使用普通软件一样双击运行,使用 Electron 进行了桌面封装。

主进程配置:

// electron/main.cjs
const { app, BrowserWindow } = require('electron')
const path = require('path')

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 375,
    minHeight: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.cjs'),
      contextIsolation: true,   // 安全:隔离渲染进程
      nodeIntegration: false    // 安全:禁用 Node 集成
    }
  })
  win.loadFile(path.join(__dirname, '../dist/index.html'))
}

关键坑点: Vite 构建出的 index.html 默认使用绝对路径引用资源(如 /assets/index.js),但 Electron 通过 file:// 协议加载文件时,绝对路径会指向磁盘根目录。解决方案是在 vite.config.ts 中设置 base: './'

// vite.config.ts
export default defineConfig({
  base: './',  // 使用相对路径,兼容 Electron
  plugins: [vue()],
  resolve: {
    alias: { '@': resolve(__dirname, 'src') }
  }
})

桌面版效果:
在这里插入图片描述


五、本地运行教程

环境要求

  • Node.js >= 18
  • npm >= 9

步骤

# 1. 克隆项目
git clone https://github.com/yangyi785/zhengzhou-travel-map.git
cd zhengzhou-travel-map

# 2. 安装依赖
npm install

# 3. 启动开发服务器(浏览器)
npm run dev

# 4. 构建生产版本
npm run build

# 5. 启动 Electron 桌面版
npm run electron

或者直接双击项目根目录的 启动桌面版.bat(Windows),一键构建 + 启动桌面应用。


六、部署方式

本项目是纯静态 SPA,所有数据存储在代码和 localStorage 中,无需后端服务器,因此部署方式非常灵活:

  • 静态托管: 构建后的 dist/ 目录可直接部署到 Vercel、Netlify、GitHub Pages 等
  • 桌面应用: 构建后通过 Electron 直接在本地运行
  • 本地预览: npm run preview 预览生产构建

七、项目亮点总结

  1. 零服务器成本 — 纯前端 + localStorage 持久化,无需搭建后端
  2. 完整离线可用 — 所有数据在本地,无网络也能正常使用
  3. 双维度主题系统 — 3 种地图风格 × 5 种季节主题 = 15 种视觉组合
  4. 游戏化闭环设计 — 打卡 → 掉物品 → 种作物 → 找宝藏 → 解锁成就,形成持续激励
  5. TypeScript 全覆盖 — 类型安全,代码可维护性强
  6. 模块化城市架构 — 城市数据独立模块化,易于扩展到其他城市
  7. 双端适配 — 浏览器端 + Electron 桌面端,一套代码全覆盖

八、源码地址

项目完整源码已开源至 GitHub:

https://github.com/yangyi785/zhengzhou-travel-map

欢迎 Star、Fork、下载学习或二次开发!


九、总结

本项目完整覆盖了从地图展示、数据管理、用户交互到桌面封装的全流程开发。通过这个项目,你可以学到:

  • Vue 3 Composition API + <script setup> 的最佳实践
  • Pinia 状态管理的模块化设计
  • Leaflet 地图的集成与定制
  • CSS 变量驱动的主题系统设计
  • 纯前端游戏化系统设计
  • Electron 桌面应用的封装技巧

后续可在此基础上拓展更多功能:多城市支持、用户登录同步、更多游戏化玩法等。

如果文章对你有帮助,欢迎点赞、收藏、关注,持续更新更多前端实战教程~


标签: Vue3 Vite Pinia Leaflet TypeScript Electron 前端实战 地图应用

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐