docs: 添加项目文档和分析报告
- 创建完整的 docs/ 目录结构 - 添加项目技术栈详解、目录结构分析 - 添加功能模块分析(地图、音频、VR、打卡) - 添加已知问题汇总和开发指南 - 建立 CHANGELOG 机制 详细内容: - 项目架构分析:技术栈详解、目录结构分析 - 功能模块分析:地图集成、音频系统、VR全景、打卡系统 - 注意事项与陷阱:已知问题汇总(版本冲突、Keep-Alive、路由等) - 开发指南:新手入门指南、常见开发任务 - 文档索引:README.md、CHANGELOG.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
12 changed files
with
4861 additions
and
0 deletions
docs/CHANGELOG.md
0 → 100644
| 1 | +# CHANGELOG | ||
| 2 | + | ||
| 3 | +项目变更日志,记录所有重要的更新和修改。 | ||
| 4 | + | ||
| 5 | +## [2026-02-09] - 项目文档化 | ||
| 6 | + | ||
| 7 | +### 新增 | ||
| 8 | +- 创建完整的 docs/ 目录结构 | ||
| 9 | +- 添加项目技术栈详解 | ||
| 10 | +- 添加目录结构分析 | ||
| 11 | +- 添加地图集成分析 | ||
| 12 | +- 添加音频系统分析 | ||
| 13 | +- 添加 VR 全景分析 | ||
| 14 | +- 添加打卡系统分析 | ||
| 15 | +- 添加已知问题汇总 | ||
| 16 | +- 添加新手入门指南 | ||
| 17 | +- 添加常见开发任务 | ||
| 18 | + | ||
| 19 | +--- | ||
| 20 | + | ||
| 21 | +**详细信息**: | ||
| 22 | +- **影响文件**: docs/ 目录下所有新增文档 | ||
| 23 | +- **技术栈**: 文档 | ||
| 24 | +- **测试状态**: N/A | ||
| 25 | +- **备注**: 全面分析项目,创建文档以便后续开发和维护 | ||
| 26 | + | ||
| 27 | +--- | ||
| 28 | + | ||
| 29 | +**历史记录**: | ||
| 30 | + | ||
| 31 | +本文档从 2026-02-09 开始记录。 | ||
| 32 | + | ||
| 33 | +之前的变更历史请参考 Git 提交记录: | ||
| 34 | +```bash | ||
| 35 | +git log --oneline --since="2026-01-01" | ||
| 36 | +``` |
docs/README.md
0 → 100644
| 1 | +# 项目文档索引 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**目的**: 提供项目文档的快速导航 | ||
| 5 | + | ||
| 6 | +## 📚 文档结构 | ||
| 7 | + | ||
| 8 | +``` | ||
| 9 | +docs/ | ||
| 10 | +├── CHANGELOG.md # 变更日志 | ||
| 11 | +├── README.md # 本文档(索引) | ||
| 12 | +├── 项目架构分析/ | ||
| 13 | +│ ├── 项目技术栈详解.md # 技术栈和版本信息 | ||
| 14 | +│ └── 目录结构分析.md # 目录结构和文件组织 | ||
| 15 | +├── 功能模块分析/ | ||
| 16 | +│ ├── 地图集成分析.md # 高德地图集成 | ||
| 17 | +│ ├── 音频系统分析.md # 音频播放系统 | ||
| 18 | +│ ├── VR全景分析.md # 360° 全景查看器 | ||
| 19 | +│ └── 打卡系统分析.md # 打卡功能实现 | ||
| 20 | +├── 注意事项与陷阱/ | ||
| 21 | +│ └── 已知问题汇总.md # 已知问题和解决方案 | ||
| 22 | +└── 开发指南/ | ||
| 23 | + ├── 新手入门指南.md # 快速上手 | ||
| 24 | + └── 常见开发任务.md # 常见任务实现 | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +## 🚀 快速开始 | ||
| 28 | + | ||
| 29 | +### 新手必读 | ||
| 30 | + | ||
| 31 | +1. **[新手入门指南](开发指南/新手入门指南.md)** - 快速上手项目 | ||
| 32 | +2. **[项目技术栈详解](项目架构分析/项目技术栈详解.md)** - 了解技术栈 | ||
| 33 | +3. **[已知问题汇总](注意事项与陷阱/已知问题汇总.md)** - 避免常见问题 | ||
| 34 | + | ||
| 35 | +### 核心功能 | ||
| 36 | + | ||
| 37 | +1. **[地图集成分析](功能模块分析/地图集成分析.md)** - 地图功能 | ||
| 38 | +2. **[音频系统分析](功能模块分析/音频系统分析.md)** - 音频播放 | ||
| 39 | +3. **[VR全景分析](功能模块分析/VR全景分析.md)** - 全景查看器 | ||
| 40 | +4. **[打卡系统分析](功能模块分析/打卡系统分析.md)** - 打卡功能 | ||
| 41 | + | ||
| 42 | +### 开发参考 | ||
| 43 | + | ||
| 44 | +1. **[常见开发任务](开发指南/常见开发任务.md)** - 任务实现指南 | ||
| 45 | +2. **[目录结构分析](项目架构分析/目录结构分析.md)** - 文件组织 | ||
| 46 | + | ||
| 47 | +### 问题排查 | ||
| 48 | + | ||
| 49 | +1. **[已知问题汇总](注意事项与陷阱/已知问题汇总.md)** - 问题解决方案 | ||
| 50 | +2. **[CHANGELOG](CHANGELOG.md)** - 变更历史 | ||
| 51 | + | ||
| 52 | +## 📖 文档阅读顺序 | ||
| 53 | + | ||
| 54 | +### 第一次接触项目 | ||
| 55 | + | ||
| 56 | +``` | ||
| 57 | +1. 新手入门指南 | ||
| 58 | +2. 项目技术栈详解 | ||
| 59 | +3. 目录结构分析 | ||
| 60 | +4. 已知问题汇总 | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +### 开发新功能 | ||
| 64 | + | ||
| 65 | +``` | ||
| 66 | +1. 常见开发任务 | ||
| 67 | +2. 对应的功能模块分析 | ||
| 68 | +3. 已知问题汇总(避免重复踩坑) | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +### 问题排查 | ||
| 72 | + | ||
| 73 | +``` | ||
| 74 | +1. 已知问题汇总 | ||
| 75 | +2. 对应的功能模块分析 | ||
| 76 | +3. 技术栈详解(版本兼容性) | ||
| 77 | +``` | ||
| 78 | + | ||
| 79 | +## 🔍 文档搜索 | ||
| 80 | + | ||
| 81 | +### 查找功能文档 | ||
| 82 | + | ||
| 83 | +```bash | ||
| 84 | +# 搜索功能关键词 | ||
| 85 | +grep -r "地图" docs/ | ||
| 86 | +grep -r "音频" docs/ | ||
| 87 | +grep -r "打卡" docs/ | ||
| 88 | +grep -r "VR" docs/ | ||
| 89 | +``` | ||
| 90 | + | ||
| 91 | +### 查找问题解决方案 | ||
| 92 | + | ||
| 93 | +```bash | ||
| 94 | +# 搜索问题关键词 | ||
| 95 | +grep -r "问题" docs/ | ||
| 96 | +grep -r "错误" docs/ | ||
| 97 | +grep -r "注意事项" docs/ | ||
| 98 | +``` | ||
| 99 | + | ||
| 100 | +## 📝 文档更新日志 | ||
| 101 | + | ||
| 102 | +### 2026-02-09 | ||
| 103 | + | ||
| 104 | +**新增**: | ||
| 105 | +- 创建完整的 docs/ 目录结构 | ||
| 106 | +- 添加 10 份核心文档 | ||
| 107 | +- 涵盖技术栈、架构、功能、问题、开发指南 | ||
| 108 | + | ||
| 109 | +**详细内容**: | ||
| 110 | +- [CHANGELOG](CHANGELOG.md#2026-02-09---项目文档化) | ||
| 111 | + | ||
| 112 | +## 🤝 贡献指南 | ||
| 113 | + | ||
| 114 | +### 更新文档 | ||
| 115 | + | ||
| 116 | +当你修改了项目或发现了新问题时,请更新相关文档: | ||
| 117 | + | ||
| 118 | +1. **新增功能**: 更新对应的功能模块分析文档 | ||
| 119 | +2. **修复 Bug**: 更新已知问题汇总文档 | ||
| 120 | +3. **新增依赖**: 更新技术栈详解文档 | ||
| 121 | +4. **结构变更**: 更新目录结构分析文档 | ||
| 122 | +5. **任务完成**: 更新 CHANGELOG.md | ||
| 123 | + | ||
| 124 | +### 文档规范 | ||
| 125 | + | ||
| 126 | +- 使用中文编写文档标题和内容 | ||
| 127 | +- 使用清晰的章节和子章节结构 | ||
| 128 | +- 添加代码示例和使用说明 | ||
| 129 | +- 标注更新日期和作者 | ||
| 130 | + | ||
| 131 | +## 🔗 相关资源 | ||
| 132 | + | ||
| 133 | +### 项目文档 | ||
| 134 | + | ||
| 135 | +- [CLAUDE.md](../CLAUDE.md) - Claude Code 项目指南 | ||
| 136 | +- [README.md](../README.md) - 项目说明 | ||
| 137 | + | ||
| 138 | +### 外部资源 | ||
| 139 | + | ||
| 140 | +- [Vue 3 文档](https://cn.vuejs.org/) | ||
| 141 | +- [Vant 文档](https://vant-ui.github.io/vant/#/zh-CN) | ||
| 142 | +- [Element Plus 文档](https://element-plus.org/zh-CN/) | ||
| 143 | +- [高德地图文档](https://lbs.amap.com/api/jsapi-v2/summary) | ||
| 144 | +- [Pinia 文档](https://pinia.vuejs.org/zh/) | ||
| 145 | +- [Vue Router 文档](https://router.vuejs.org/zh/) | ||
| 146 | + | ||
| 147 | +## 📮 反馈 | ||
| 148 | + | ||
| 149 | +如果你发现文档有错误或需要补充的内容,请: | ||
| 150 | + | ||
| 151 | +1. 提交 Issue | ||
| 152 | +2. 提交 Pull Request | ||
| 153 | +3. 联系维护者 | ||
| 154 | + | ||
| 155 | +--- | ||
| 156 | + | ||
| 157 | +**最后更新**: 2026-02-09 | ||
| 158 | +**维护者**: 项目团队 |
docs/功能模块分析/VR全景分析.md
0 → 100644
| 1 | +# VR 全景分析 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**相关文件**: | ||
| 5 | +- `src/components/VRViewer/index.vue` - VR 全景查看器组件 | ||
| 6 | +- `src/api/map.js` - 地图数据 API(包含全景图 URL) | ||
| 7 | + | ||
| 8 | +## 技术栈 | ||
| 9 | + | ||
| 10 | +### 全景查看器库 | ||
| 11 | + | ||
| 12 | +**Photo Sphere Viewer** - 360° 全景查看器 | ||
| 13 | + | ||
| 14 | +| 库 | 版本 | 用途 | | ||
| 15 | +|------|------|------| | ||
| 16 | +| `photo-sphere-viewer` | 4.8.1 | 全景查看器(旧版本,已弃用) | | ||
| 17 | +| `@photo-sphere-viewer/core` | 5.7.3 | 全景查看器核心(新版本) | | ||
| 18 | +| `@photo-sphere-viewer/markers-plugin` | 5.7.3 | 标记点插件 | | ||
| 19 | +| `@photo-sphere-viewer/virtual-tour-plugin` | 5.7.3 | 虚拟漫游插件 | | ||
| 20 | +| `@photo-sphere-viewer/gallery-plugin` | 5.7.3 | 图库插件 | | ||
| 21 | +| `@photo-sphere-viewer/gyroscope-plugin` | 5.7.3 | 陀螺仪插件 | | ||
| 22 | +| `@photo-sphere-viewer/stereo-plugin` | 5.7.3 | 立体插件(VR 眼镜) | | ||
| 23 | + | ||
| 24 | +⚠️ **注意**: 项目中同时存在 4.8.1 和 5.7.3 两个版本,建议统一使用 5.x 版本。 | ||
| 25 | + | ||
| 26 | +## 核心功能 | ||
| 27 | + | ||
| 28 | +### 1. 全景图查看 | ||
| 29 | + | ||
| 30 | +**功能**: | ||
| 31 | +- ✅ 360° 全景浏览 | ||
| 32 | +- ✅ 缩放控制 | ||
| 33 | +- ✅ 自动旋转 | ||
| 34 | +- ✅ 陀螺仪控制(移动设备) | ||
| 35 | + | ||
| 36 | +**初始化**: | ||
| 37 | +```javascript | ||
| 38 | +import { Viewer } from '@photo-sphere-viewer/core'; | ||
| 39 | + | ||
| 40 | +const viewer = new Viewer({ | ||
| 41 | + container: document.querySelector('#viewer'), | ||
| 42 | + panorama: '/path/to/panorama.jpg', | ||
| 43 | + defaultZoomLvl: 0, | ||
| 44 | + fisheye: true, | ||
| 45 | + loadingTxt: '加载中...', | ||
| 46 | +}); | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +### 2. 标记点 (Markers) | ||
| 50 | + | ||
| 51 | +**插件**: `@photo-sphere-viewer/markers-plugin` | ||
| 52 | + | ||
| 53 | +**功能**: | ||
| 54 | +- ✅ 添加标记点 | ||
| 55 | +- ✅ 标记点点击事件 | ||
| 56 | +- ✅ 自定义标记点样式 | ||
| 57 | +- ✅ 多边形标记点 | ||
| 58 | + | ||
| 59 | +**使用示例**: | ||
| 60 | +```javascript | ||
| 61 | +import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; | ||
| 62 | + | ||
| 63 | +const markersPlugin = new MarkersPlugin(); | ||
| 64 | + | ||
| 65 | +viewer.setPlugin(markersPlugin); | ||
| 66 | + | ||
| 67 | +// 添加标记点 | ||
| 68 | +viewer.addMarker({ | ||
| 69 | + id: 'marker1', | ||
| 70 | + position: { yaw: 0, pitch: 0 }, | ||
| 71 | + html: '<div class="marker">标记点</div>', | ||
| 72 | + scale: [1, 1], | ||
| 73 | +}); | ||
| 74 | + | ||
| 75 | +// 监听标记点点击 | ||
| 76 | +markersPlugin.addEventListener('select-marker', ({ marker }) => { | ||
| 77 | + console.log('点击标记点:', marker.id); | ||
| 78 | +}); | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +### 3. 虚拟漫游 (Virtual Tour) | ||
| 82 | + | ||
| 83 | +**插件**: `@photo-sphere-viewer/virtual-tour-plugin` | ||
| 84 | + | ||
| 85 | +**功能**: | ||
| 86 | +- ✅ 多场景切换 | ||
| 87 | +- ✅ 场景之间的链接 | ||
| 88 | +- ✅ 箭头指示器 | ||
| 89 | +- ✅ 自动路径播放 | ||
| 90 | + | ||
| 91 | +**使用示例**: | ||
| 92 | +```javascript | ||
| 93 | +import { VirtualTourPlugin } from '@photo-sphere-viewer/virtual-tour-plugin'; | ||
| 94 | + | ||
| 95 | +const virtualTour = new VirtualTourPlugin(); | ||
| 96 | + | ||
| 97 | +viewer.setPlugin(virtualTour); | ||
| 98 | + | ||
| 99 | +// 设置节点 | ||
| 100 | +virtualTour.setNodes([ | ||
| 101 | + { | ||
| 102 | + id: '1', | ||
| 103 | + panorama: '/path/to/panorama1.jpg', | ||
| 104 | + links: [ | ||
| 105 | + { | ||
| 106 | + name: '场景 2', | ||
| 107 | + nodeId: '2', | ||
| 108 | + position: { yaw: -300, pitch: 0 }, | ||
| 109 | + arrowStyle: { | ||
| 110 | + color: '#AEEEEE', | ||
| 111 | + hoverColor: 0xaa5500, | ||
| 112 | + outlineColor: 0x000000, | ||
| 113 | + scale: [1, 1], | ||
| 114 | + } | ||
| 115 | + }, | ||
| 116 | + ], | ||
| 117 | + markers: [...], | ||
| 118 | + }, | ||
| 119 | + { | ||
| 120 | + id: '2', | ||
| 121 | + panorama: '/path/to/panorama2.jpg', | ||
| 122 | + links: [...], | ||
| 123 | + markers: [...], | ||
| 124 | + }, | ||
| 125 | +]); | ||
| 126 | + | ||
| 127 | +// 切换场景 | ||
| 128 | +virtualTour.setCurrentNode('2'); | ||
| 129 | +``` | ||
| 130 | + | ||
| 131 | +### 4. 图库 (Gallery) | ||
| 132 | + | ||
| 133 | +**插件**: `@photo-sphere-viewer/gallery-plugin` | ||
| 134 | + | ||
| 135 | +**功能**: | ||
| 136 | +- ✅ 全景图列表 | ||
| 137 | +- ✅ 缩略图导航 | ||
| 138 | +- ✅ 自动切换 | ||
| 139 | + | ||
| 140 | +**使用示例**: | ||
| 141 | +```javascript | ||
| 142 | +import { GalleryPlugin } from '@photo-sphere-viewer/gallery-plugin'; | ||
| 143 | + | ||
| 144 | +const gallery = new GalleryPlugin({ | ||
| 145 | + thumbnailSize: { | ||
| 146 | + width: 100, | ||
| 147 | + height: 100, | ||
| 148 | + }, | ||
| 149 | +}); | ||
| 150 | + | ||
| 151 | +viewer.setPlugin(gallery); | ||
| 152 | + | ||
| 153 | +// 设置图库 | ||
| 154 | +gallery.setItems([ | ||
| 155 | + { | ||
| 156 | + id: '1', | ||
| 157 | + panorama: '/path/to/panorama1.jpg', | ||
| 158 | + name: '场景 1', | ||
| 159 | + thumbnail: '/path/to/thumb1.jpg', | ||
| 160 | + }, | ||
| 161 | + { | ||
| 162 | + id: '2', | ||
| 163 | + panorama: '/path/to/panorama2.jpg', | ||
| 164 | + name: '场景 2', | ||
| 165 | + thumbnail: '/path/to/thumb2.jpg', | ||
| 166 | + }, | ||
| 167 | +]); | ||
| 168 | +``` | ||
| 169 | + | ||
| 170 | +### 5. 陀螺仪 (Gyroscope) | ||
| 171 | + | ||
| 172 | +**插件**: `@photo-sphere-viewer/gyroscope-plugin` | ||
| 173 | + | ||
| 174 | +**功能**: | ||
| 175 | +- ✅ 设备方向控制 | ||
| 176 | +- ✅ 移动设备支持 | ||
| 177 | +- ✅ VR 眼镜模式 | ||
| 178 | + | ||
| 179 | +**使用示例**: | ||
| 180 | +```javascript | ||
| 181 | +import { GyroscopePlugin } from '@photo-sphere-viewer/gyroscope-plugin'; | ||
| 182 | + | ||
| 183 | +const gyroscope = new GyroscopePlugin(); | ||
| 184 | + | ||
| 185 | +viewer.setPlugin(gyroscope); | ||
| 186 | + | ||
| 187 | +// 启用陀螺仪 | ||
| 188 | +gyroscope.toggle(); | ||
| 189 | +``` | ||
| 190 | + | ||
| 191 | +### 6. 立体模式 (Stereo) | ||
| 192 | + | ||
| 193 | +**插件**: `@photo-sphere-viewer/stereo-plugin` | ||
| 194 | + | ||
| 195 | +**功能**: | ||
| 196 | +- ✅ VR 眼镜模式 | ||
| 197 | +- ✅ 立体视图 | ||
| 198 | + | ||
| 199 | +**使用示例**: | ||
| 200 | +```javascript | ||
| 201 | +import { StereoPlugin } from '@photo-sphere-viewer/stereo-plugin'; | ||
| 202 | + | ||
| 203 | +const stereo = new StereoPlugin(); | ||
| 204 | + | ||
| 205 | +viewer.setPlugin(stereo); | ||
| 206 | + | ||
| 207 | +// 启用立体模式 | ||
| 208 | +stereo.toggle(); | ||
| 209 | +``` | ||
| 210 | + | ||
| 211 | +## 组件使用 | ||
| 212 | + | ||
| 213 | +### VRViewer 组件 | ||
| 214 | + | ||
| 215 | +**路径**: `src/components/VRViewer/index.vue` | ||
| 216 | + | ||
| 217 | +**Props**: | ||
| 218 | +```javascript | ||
| 219 | +{ | ||
| 220 | + show: Boolean // 控制显示/隐藏 | ||
| 221 | +} | ||
| 222 | +``` | ||
| 223 | + | ||
| 224 | +**使用示例**: | ||
| 225 | +```vue | ||
| 226 | +<template> | ||
| 227 | + <VRViewer | ||
| 228 | + v-model:show="showVR" | ||
| 229 | + :panorama="panoramaUrl" | ||
| 230 | + :markers="markers" | ||
| 231 | + @marker-click="handleMarkerClick" | ||
| 232 | + /> | ||
| 233 | +</template> | ||
| 234 | + | ||
| 235 | +<script setup> | ||
| 236 | +import VRViewer from '@components/VRViewer/index.vue'; | ||
| 237 | +import { ref } from 'vue'; | ||
| 238 | + | ||
| 239 | +const showVR = ref(false); | ||
| 240 | +const panoramaUrl = ref('/vr/panorama.jpg'); | ||
| 241 | +const markers = ref([ | ||
| 242 | + { | ||
| 243 | + id: 'marker1', | ||
| 244 | + position: { yaw: 0, pitch: 0 }, | ||
| 245 | + html: '<div class="marker">标记点</div>', | ||
| 246 | + } | ||
| 247 | +]); | ||
| 248 | + | ||
| 249 | +const handleMarkerClick = (marker) => { | ||
| 250 | + console.log('点击标记点:', marker); | ||
| 251 | +}; | ||
| 252 | + | ||
| 253 | +// 打开 VR | ||
| 254 | +const openVR = () => { | ||
| 255 | + showVR.value = true; | ||
| 256 | +}; | ||
| 257 | +</script> | ||
| 258 | +``` | ||
| 259 | + | ||
| 260 | +## 全景图格式 | ||
| 261 | + | ||
| 262 | +### 支持的格式 | ||
| 263 | + | ||
| 264 | +- ✅ JPEG (推荐) | ||
| 265 | +- ✅ PNG | ||
| 266 | +- ✅ WebP (推荐,体积更小) | ||
| 267 | + | ||
| 268 | +### 拍摄要求 | ||
| 269 | + | ||
| 270 | +**等距柱状投影 (Equirectangular Projection)**: | ||
| 271 | +- 宽高比: 2:1 | ||
| 272 | +- 分辨率: 至少 4096 x 2048 | ||
| 273 | +- 格式: 等距柱状投影 | ||
| 274 | + | ||
| 275 | +**文件大小建议**: | ||
| 276 | +| 分辨率 | 文件大小 | | ||
| 277 | +|--------|---------| | ||
| 278 | +| 2048 x 1024 | < 2 MB | | ||
| 279 | +| 4096 x 2048 | 2-5 MB | | ||
| 280 | +| 8192 x 4096 | 5-15 MB | | ||
| 281 | + | ||
| 282 | +### 图片优化 | ||
| 283 | + | ||
| 284 | +**压缩建议**: | ||
| 285 | +- JPEG 质量: 80-90% | ||
| 286 | +- WebP 质量: 80-90% | ||
| 287 | +- 使用渐进式 JPEG | ||
| 288 | + | ||
| 289 | +**工具推荐**: | ||
| 290 | +- Adobe Lightroom | ||
| 291 | +- Photoshop | ||
| 292 | +- 在线压缩工具 | ||
| 293 | + | ||
| 294 | +## 性能优化 | ||
| 295 | + | ||
| 296 | +### 1. 懒加载 | ||
| 297 | + | ||
| 298 | +```javascript | ||
| 299 | +// 仅在需要时加载全景图 | ||
| 300 | +const loadPanorama = async (url) => { | ||
| 301 | + const viewer = new Viewer({ | ||
| 302 | + container: document.querySelector('#viewer'), | ||
| 303 | + panorama: url, | ||
| 304 | + loadingTxt: '加载中...', | ||
| 305 | + }); | ||
| 306 | + | ||
| 307 | + await viewer.ready(); | ||
| 308 | + | ||
| 309 | + return viewer; | ||
| 310 | +}; | ||
| 311 | +``` | ||
| 312 | + | ||
| 313 | +### 2. 缩略图 | ||
| 314 | + | ||
| 315 | +```javascript | ||
| 316 | +// 先加载缩略图,再加载高清图 | ||
| 317 | +const viewer = new Viewer({ | ||
| 318 | + container: document.querySelector('#viewer'), | ||
| 319 | + panorama: thumbnailUrl, // 缩略图 | ||
| 320 | + requestFullscreen: true, | ||
| 321 | +}); | ||
| 322 | + | ||
| 323 | +// 后台加载高清图 | ||
| 324 | +loadFullPanorama(fullUrl).then((fullUrl) => { | ||
| 325 | + viewer.setPanorama(fullUrl); | ||
| 326 | +}); | ||
| 327 | +``` | ||
| 328 | + | ||
| 329 | +### 3. 缓存策略 | ||
| 330 | + | ||
| 331 | +```javascript | ||
| 332 | +// 缓存已加载的全景图 | ||
| 333 | +const panoramaCache = new Map(); | ||
| 334 | + | ||
| 335 | +export const getCachedPanorama = (url) => { | ||
| 336 | + if (panoramaCache.has(url)) { | ||
| 337 | + return Promise.resolve(panoramaCache.get(url)); | ||
| 338 | + } | ||
| 339 | + | ||
| 340 | + return new Promise((resolve) => { | ||
| 341 | + const img = new Image(); | ||
| 342 | + img.onload = () => { | ||
| 343 | + panoramaCache.set(url, url); | ||
| 344 | + resolve(url); | ||
| 345 | + }; | ||
| 346 | + img.src = url; | ||
| 347 | + }); | ||
| 348 | +}; | ||
| 349 | +``` | ||
| 350 | + | ||
| 351 | +### 4. 资源释放 | ||
| 352 | + | ||
| 353 | +```javascript | ||
| 354 | +// 组件卸载时释放资源 | ||
| 355 | +onUnmounted(() => { | ||
| 356 | + if (viewer.value) { | ||
| 357 | + viewer.value.destroy(); | ||
| 358 | + viewer.value = null; | ||
| 359 | + } | ||
| 360 | +}); | ||
| 361 | +``` | ||
| 362 | + | ||
| 363 | +## 已知问题 | ||
| 364 | + | ||
| 365 | +### 1. 版本冲突 | ||
| 366 | + | ||
| 367 | +**问题**: 同时使用 4.x 和 5.x 版本 | ||
| 368 | + | ||
| 369 | +**解决方案**: 统一使用 5.x 版本 | ||
| 370 | + | ||
| 371 | +```javascript | ||
| 372 | +// ❌ 错误 | ||
| 373 | +import { Viewer } from 'photo-sphere-viewer'; // 4.x | ||
| 374 | +import { Viewer } from '@photo-sphere-viewer/core'; // 5.x | ||
| 375 | + | ||
| 376 | +// ✅ 正确 | ||
| 377 | +import { Viewer } from '@photo-sphere-viewer/core'; // 仅使用 5.x | ||
| 378 | +``` | ||
| 379 | + | ||
| 380 | +### 2. jQuery 依赖 | ||
| 381 | + | ||
| 382 | +**问题**: 组件中使用 jQuery | ||
| 383 | + | ||
| 384 | +**解决方案**: 逐步迁移到 Vue 原生 API | ||
| 385 | + | ||
| 386 | +```javascript | ||
| 387 | +// ❌ 错误 | ||
| 388 | +$('.psv-zoom-button').css('display', ''); | ||
| 389 | + | ||
| 390 | +// ✅ 正确 | ||
| 391 | +document.querySelector('.psv-zoom-button').style.display = ''; | ||
| 392 | +``` | ||
| 393 | + | ||
| 394 | +### 3. 内存泄漏 | ||
| 395 | + | ||
| 396 | +**问题**: 未正确销毁查看器实例 | ||
| 397 | + | ||
| 398 | +**解决方案**: 组件卸载时销毁 | ||
| 399 | + | ||
| 400 | +```javascript | ||
| 401 | +onUnmounted(() => { | ||
| 402 | + if (viewer) { | ||
| 403 | + viewer.destroy(); | ||
| 404 | + } | ||
| 405 | +}); | ||
| 406 | +``` | ||
| 407 | + | ||
| 408 | +### 4. 移动端性能 | ||
| 409 | + | ||
| 410 | +**问题**: 高分辨率全景图加载慢 | ||
| 411 | + | ||
| 412 | +**解决方案**: | ||
| 413 | +- 使用 WebP 格式 | ||
| 414 | +- 降低分辨率 | ||
| 415 | +- 使用缩略图预加载 | ||
| 416 | + | ||
| 417 | +## 最佳实践 | ||
| 418 | + | ||
| 419 | +### 1. 组件使用 | ||
| 420 | + | ||
| 421 | +```vue | ||
| 422 | +<!-- ✅ 推荐:使用 v-model --> | ||
| 423 | +<VRViewer v-model:show="showVR" /> | ||
| 424 | + | ||
| 425 | +<!-- ❌ 不推荐:手动控制 --> | ||
| 426 | +<VRViewer :show="showVR" @close="showVR = false" /> | ||
| 427 | +``` | ||
| 428 | + | ||
| 429 | +### 2. 全景图路径 | ||
| 430 | + | ||
| 431 | +```javascript | ||
| 432 | +// ✅ 推荐:使用别名 | ||
| 433 | +const panoramaUrl = ref('@images/vr/panorama.jpg'); | ||
| 434 | + | ||
| 435 | +// ❌ 不推荐:使用相对路径 | ||
| 436 | +const panoramaUrl = ref('../../images/vr/panorama.jpg'); | ||
| 437 | +``` | ||
| 438 | + | ||
| 439 | +### 3. 标记点定义 | ||
| 440 | + | ||
| 441 | +```javascript | ||
| 442 | +// ✅ 推荐:使用配置对象 | ||
| 443 | +const markerConfig = { | ||
| 444 | + id: 'marker1', | ||
| 445 | + position: { yaw: 0, pitch: 0 }, | ||
| 446 | + html: '<div class="marker">标记点</div>', | ||
| 447 | +}; | ||
| 448 | + | ||
| 449 | +// ❌ 不推荐:内联定义 | ||
| 450 | +viewer.addMarker({ | ||
| 451 | + id: 'marker1', | ||
| 452 | + position: { yaw: 0, pitch: 0 }, | ||
| 453 | + html: '<div class="marker">标记点</div>', | ||
| 454 | +}); | ||
| 455 | +``` | ||
| 456 | + | ||
| 457 | +## 调试技巧 | ||
| 458 | + | ||
| 459 | +### 1. 查看查看器状态 | ||
| 460 | + | ||
| 461 | +```javascript | ||
| 462 | +// 在控制台查看查看器状态 | ||
| 463 | +console.log('查看器状态:', { | ||
| 464 | + position: viewer.getPosition(), | ||
| 465 | + zoom: viewer.getZoomLevel(), | ||
| 466 | + size: viewer.getSize(), | ||
| 467 | +}); | ||
| 468 | +``` | ||
| 469 | + | ||
| 470 | +### 2. 监听事件 | ||
| 471 | + | ||
| 472 | +```javascript | ||
| 473 | +// 监听所有事件 | ||
| 474 | +viewer.addEventListener('ready', () => console.log('就绪')); | ||
| 475 | +viewer.addEventListener('position-updated', (e) => console.log('位置更新:', e.position)); | ||
| 476 | +viewer.addEventListener('zoom-updated', (e) => console.log('缩放更新:', e.zoomLevel)); | ||
| 477 | +``` | ||
| 478 | + | ||
| 479 | +### 3. 手动控制 | ||
| 480 | + | ||
| 481 | +```javascript | ||
| 482 | +// 手动旋转 | ||
| 483 | +viewer.animate({ | ||
| 484 | + yaw: 180, | ||
| 485 | + pitch: 0, | ||
| 486 | + zoom: 0, | ||
| 487 | + speed: 1000, | ||
| 488 | +}); | ||
| 489 | + | ||
| 490 | +// 手动缩放 | ||
| 491 | +viewer.zoom(1); | ||
| 492 | +``` | ||
| 493 | + | ||
| 494 | +## 参考文档 | ||
| 495 | + | ||
| 496 | +- [Photo Sphere Viewer 文档](https://photo-sphere-viewer.js.org/) | ||
| 497 | +- [全景摄影指南](https://en.wikipedia.org/wiki/ panoramic_photography) | ||
| 498 | +- [等距柱状投影](https://en.wikipedia.org/wiki/Equirectangular_projection) |
docs/功能模块分析/地图集成分析.md
0 → 100644
| 1 | +# 地图集成分析 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**相关文件**: | ||
| 5 | +- `src/api/map.js` - 地图 API | ||
| 6 | +- `src/components/Floor/` - 楼层平面图组件 | ||
| 7 | +- `src/components/InfoWindow*.vue` - 信息窗口组件 | ||
| 8 | +- `src/common/map_data.js` - 地图数据处理 | ||
| 9 | + | ||
| 10 | +## 技术栈 | ||
| 11 | + | ||
| 12 | +### 地图服务提供商 | ||
| 13 | + | ||
| 14 | +**高德地图 (AMap)** | ||
| 15 | + | ||
| 16 | +```javascript | ||
| 17 | +import AMapLoader from '@amap/amap-jsapi-loader'; | ||
| 18 | +``` | ||
| 19 | + | ||
| 20 | +**版本**: 1.0.1 | ||
| 21 | +**文档**: https://lbs.amap.com/api/jsapi-v2/summary | ||
| 22 | + | ||
| 23 | +## 核心功能 | ||
| 24 | + | ||
| 25 | +### 1. 地图数据获取 | ||
| 26 | + | ||
| 27 | +**API 端点**: `/srv/?a=map` | ||
| 28 | + | ||
| 29 | +```javascript | ||
| 30 | +// src/api/map.js | ||
| 31 | +export const mapAPI = (params) => fn(fetch.get(Api.MAP, params)); | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +**请求参数**: | ||
| 35 | +- `id`: 地图/位置 ID | ||
| 36 | +- 其他业务参数 | ||
| 37 | + | ||
| 38 | +**响应数据结构**(推测): | ||
| 39 | +```javascript | ||
| 40 | +{ | ||
| 41 | + code: 1, | ||
| 42 | + data: { | ||
| 43 | + coordinates: [...], // 坐标数据 | ||
| 44 | + markers: [...], // 标记点数据 | ||
| 45 | + polygons: [...], // 多边形数据 | ||
| 46 | + // 其他地图数据 | ||
| 47 | + }, | ||
| 48 | + msg: '' | ||
| 49 | +} | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +### 2. 楼层平面图组件 (Floor) | ||
| 53 | + | ||
| 54 | +**组件路径**: `src/components/Floor/index.vue` | ||
| 55 | + | ||
| 56 | +**功能**: | ||
| 57 | +- ✅ 多楼层切换(1-4 层) | ||
| 58 | +- ✅ SVG 交互式平面图 | ||
| 59 | +- ✅ 标记点(Pin)显示与点击 | ||
| 60 | +- ✅ 区域动画(安全区、厕所、入口) | ||
| 61 | +- ✅ VR 全景入口 | ||
| 62 | +- ✅ 搜索功能 | ||
| 63 | + | ||
| 64 | +**楼层切换**: | ||
| 65 | +```javascript | ||
| 66 | +// 左右箭头切换楼层 | ||
| 67 | +switchFloor('left') // 上一层 | ||
| 68 | +switchFloor('right') // 下一层 | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +**标记点交互**: | ||
| 72 | +```vue | ||
| 73 | +<a @click="clickPin(item, $event)" | ||
| 74 | + class="pin" | ||
| 75 | + :data-category="item.category" | ||
| 76 | + :data-space="item.space"> | ||
| 77 | + <!-- 标记点图标 --> | ||
| 78 | +</a> | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +**区域动画**: | ||
| 82 | +```javascript | ||
| 83 | +// 触发区域动画 | ||
| 84 | +createAnimation(true, levelIndex - 1, 'safe') // 安全区动画 | ||
| 85 | +createAnimation(true, levelIndex - 1, 'toilet') // 厕所动画 | ||
| 86 | +createAnimation(true, levelIndex - 1, 'door') // 入口动画 | ||
| 87 | +``` | ||
| 88 | + | ||
| 89 | +**工具栏功能**: | ||
| 90 | +- 🔍 搜索按钮 | ||
| 91 | +- ❌ 关闭按钮 | ||
| 92 | +- ⬆️⬇️ 楼层切换 | ||
| 93 | +- 📺 VR 全景入口 | ||
| 94 | +- ⭐ 区域动画开关 | ||
| 95 | + | ||
| 96 | +### 3. 信息窗口组件 | ||
| 97 | + | ||
| 98 | +**多种样式**: | ||
| 99 | +- `InfoWindowLite.vue` - 轻量版 | ||
| 100 | +- `InfoWindowWarn.vue` - 警告版 | ||
| 101 | +- `InfoWindowYard.vue` - 院落版 | ||
| 102 | + | ||
| 103 | +**功能**: | ||
| 104 | +- 显示位置信息 | ||
| 105 | +- 显示音频播放按钮 | ||
| 106 | +- 显示图片 | ||
| 107 | +- 显示操作按钮 | ||
| 108 | + | ||
| 109 | +### 4. 坐标系统 | ||
| 110 | + | ||
| 111 | +**数据来源**: `src/common/map_data.js` | ||
| 112 | + | ||
| 113 | +**坐标类型**: | ||
| 114 | +- **网格坐标**: 用于平面图定位 | ||
| 115 | +- **GPS 坐标**: 用于地图定位 | ||
| 116 | +- **像素坐标**: 用于 SVG 渲染 | ||
| 117 | + | ||
| 118 | +**坐标转换**(推测): | ||
| 119 | +```javascript | ||
| 120 | +// 网格坐标 → 像素坐标 | ||
| 121 | +function gridToPixel(gridX, gridY, level) { | ||
| 122 | + // 转换逻辑 | ||
| 123 | + return { x: pixelX, y: pixelY }; | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +// GPS 坐标 → 网格坐标 | ||
| 127 | +function gpsToGrid(lat, lng) { | ||
| 128 | + // 转换逻辑 | ||
| 129 | + return { x: gridX, y: gridY }; | ||
| 130 | +} | ||
| 131 | +``` | ||
| 132 | + | ||
| 133 | +## 使用示例 | ||
| 134 | + | ||
| 135 | +### 1. 加载地图数据 | ||
| 136 | + | ||
| 137 | +```javascript | ||
| 138 | +import { mapAPI } from '@/api/map.js'; | ||
| 139 | + | ||
| 140 | +// 获取地图数据 | ||
| 141 | +const { data } = await mapAPI({ id: locationId }); | ||
| 142 | + | ||
| 143 | +// 处理坐标数据 | ||
| 144 | +if (data && data.coordinates) { | ||
| 145 | + // 渲染地图 | ||
| 146 | +} | ||
| 147 | +``` | ||
| 148 | + | ||
| 149 | +### 2. 使用 Floor 组件 | ||
| 150 | + | ||
| 151 | +```vue | ||
| 152 | +<template> | ||
| 153 | + <Floor | ||
| 154 | + :level-list="floorData" | ||
| 155 | + :current-level="currentFloor" | ||
| 156 | + @pin-click="handlePinClick" | ||
| 157 | + @floor-change="handleFloorChange" | ||
| 158 | + @vr-click="handleVRClick" | ||
| 159 | + /> | ||
| 160 | +</template> | ||
| 161 | + | ||
| 162 | +<script setup> | ||
| 163 | +import Floor from '@components/Floor/index.vue'; | ||
| 164 | +import { ref } from 'vue'; | ||
| 165 | + | ||
| 166 | +const floorData = ref([ | ||
| 167 | + { | ||
| 168 | + svg: '<svg>...</svg>', | ||
| 169 | + pin: [ | ||
| 170 | + { | ||
| 171 | + category: 'entrance', | ||
| 172 | + space: 'main', | ||
| 173 | + icon: 'door', | ||
| 174 | + style: { left: '100px', top: '200px' } | ||
| 175 | + } | ||
| 176 | + ] | ||
| 177 | + } | ||
| 178 | +]); | ||
| 179 | + | ||
| 180 | +const currentFloor = ref(1); | ||
| 181 | + | ||
| 182 | +const handlePinClick = (pin, event) => { | ||
| 183 | + console.log('点击标记点:', pin); | ||
| 184 | + // 显示信息窗口 | ||
| 185 | + // 播放音频 | ||
| 186 | +}; | ||
| 187 | + | ||
| 188 | +const handleFloorChange = (floor) => { | ||
| 189 | + currentFloor.value = floor; | ||
| 190 | +}; | ||
| 191 | + | ||
| 192 | +const handleVRClick = () => { | ||
| 193 | + // 打开 VR 全景 | ||
| 194 | +}; | ||
| 195 | +</script> | ||
| 196 | +``` | ||
| 197 | + | ||
| 198 | +### 3. 显示信息窗口 | ||
| 199 | + | ||
| 200 | +```vue | ||
| 201 | +<template> | ||
| 202 | + <InfoWindowLite | ||
| 203 | + v-model:show="showInfoWindow" | ||
| 204 | + :title="locationTitle" | ||
| 205 | + :description="locationDesc" | ||
| 206 | + :audio-url="audioUrl" | ||
| 207 | + :images="locationImages" | ||
| 208 | + /> | ||
| 209 | +</template> | ||
| 210 | + | ||
| 211 | +<script setup> | ||
| 212 | +import InfoWindowLite from '@components/InfoWindowLite.vue'; | ||
| 213 | +import { ref } from 'vue'; | ||
| 214 | + | ||
| 215 | +const showInfoWindow = ref(false); | ||
| 216 | +const locationTitle = ref('标题'); | ||
| 217 | +const locationDesc = ref('描述'); | ||
| 218 | +const audioUrl = ref('/audio/guide.mp3'); | ||
| 219 | +const locationImages = ref(['/img/1.jpg', '/img/2.jpg']); | ||
| 220 | +</script> | ||
| 221 | +``` | ||
| 222 | + | ||
| 223 | +## 地图集成流程 | ||
| 224 | + | ||
| 225 | +### 初始化流程 | ||
| 226 | + | ||
| 227 | +``` | ||
| 228 | +1. 加载高德地图 JS API | ||
| 229 | + ↓ | ||
| 230 | +2. 创建地图实例 | ||
| 231 | + ↓ | ||
| 232 | +3. 获取地图数据 (mapAPI) | ||
| 233 | + ↓ | ||
| 234 | +4. 渲染平面图 (Floor 组件) | ||
| 235 | + ↓ | ||
| 236 | +5. 添加标记点 (Pin) | ||
| 237 | + ↓ | ||
| 238 | +6. 绑定交互事件 | ||
| 239 | +``` | ||
| 240 | + | ||
| 241 | +### 交互流程 | ||
| 242 | + | ||
| 243 | +``` | ||
| 244 | +用户点击标记点 | ||
| 245 | + ↓ | ||
| 246 | +触发 clickPin 事件 | ||
| 247 | + ↓ | ||
| 248 | +显示信息窗口 (InfoWindow) | ||
| 249 | + ↓ | ||
| 250 | +播放音频 (audioBackground/audioList) | ||
| 251 | + ↓ | ||
| 252 | +可选:打开 VR 全景 (VRViewer) | ||
| 253 | +``` | ||
| 254 | + | ||
| 255 | +## 地图数据结构 | ||
| 256 | + | ||
| 257 | +### 响应数据示例 | ||
| 258 | + | ||
| 259 | +```javascript | ||
| 260 | +{ | ||
| 261 | + "code": 1, | ||
| 262 | + "data": { | ||
| 263 | + "id": 1, | ||
| 264 | + "name": "别院", | ||
| 265 | + "floors": [ | ||
| 266 | + { | ||
| 267 | + "level": 1, | ||
| 268 | + "svg": "<svg>...</svg>", | ||
| 269 | + "pins": [ | ||
| 270 | + { | ||
| 271 | + "id": 1, | ||
| 272 | + "category": "entrance", | ||
| 273 | + "space": "main", | ||
| 274 | + "icon": "door", | ||
| 275 | + "style": { | ||
| 276 | + "left": "100px", | ||
| 277 | + "top": "200px" | ||
| 278 | + }, | ||
| 279 | + "info": { | ||
| 280 | + "title": "正门入口", | ||
| 281 | + "description": "...", | ||
| 282 | + "audioUrl": "/audio/entrance.mp3", | ||
| 283 | + "images": ["/img/1.jpg"] | ||
| 284 | + } | ||
| 285 | + } | ||
| 286 | + ] | ||
| 287 | + } | ||
| 288 | + ], | ||
| 289 | + "coordinates": { | ||
| 290 | + "center": { "lat": 31.230416, "lng": 121.473701 }, | ||
| 291 | + "bounds": { | ||
| 292 | + "northeast": { "lat": 31.231516, "lng": 121.474801 }, | ||
| 293 | + "southwest": { "lat": 31.229316, "lng": 121.472601 } | ||
| 294 | + } | ||
| 295 | + } | ||
| 296 | + }, | ||
| 297 | + "msg": "" | ||
| 298 | +} | ||
| 299 | +``` | ||
| 300 | + | ||
| 301 | +## 性能优化 | ||
| 302 | + | ||
| 303 | +### 1. 懒加载 | ||
| 304 | + | ||
| 305 | +```javascript | ||
| 306 | +// 楼层 SVG 懒加载 | ||
| 307 | +const loadFloorSVG = async (level) => { | ||
| 308 | + const { data } = await mapAPI({ id: locationId, level }); | ||
| 309 | + return data.floors[level - 1].svg; | ||
| 310 | +}; | ||
| 311 | +``` | ||
| 312 | + | ||
| 313 | +### 2. 缓存策略 | ||
| 314 | + | ||
| 315 | +```javascript | ||
| 316 | +// 缓存地图数据 | ||
| 317 | +const mapDataCache = new Map(); | ||
| 318 | + | ||
| 319 | +export const mapAPI = async (params) => { | ||
| 320 | + const cacheKey = JSON.stringify(params); | ||
| 321 | + | ||
| 322 | + if (mapDataCache.has(cacheKey)) { | ||
| 323 | + return { code: 1, data: mapDataCache.get(cacheKey) }; | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + const result = await fn(fetch.get(Api.MAP, params)); | ||
| 327 | + | ||
| 328 | + if (result.code === 1) { | ||
| 329 | + mapDataCache.set(cacheKey, result.data); | ||
| 330 | + } | ||
| 331 | + | ||
| 332 | + return result; | ||
| 333 | +}; | ||
| 334 | +``` | ||
| 335 | + | ||
| 336 | +### 3. SVG 优化 | ||
| 337 | + | ||
| 338 | +- 使用简洁的 SVG 路径 | ||
| 339 | +- 压缩 SVG 文件大小 | ||
| 340 | +- 使用 `v-html` 动态渲染(注意 XSS 风险) | ||
| 341 | + | ||
| 342 | +## 已知问题 | ||
| 343 | + | ||
| 344 | +### 1. SVG 渲染性能 | ||
| 345 | + | ||
| 346 | +**问题**: 复杂 SVG 可能导致渲染卡顿 | ||
| 347 | + | ||
| 348 | +**解决方案**: | ||
| 349 | +- 简化 SVG 路径 | ||
| 350 | +- 使用 `will-change` 属性 | ||
| 351 | +- 虚拟滚动(如果标记点很多) | ||
| 352 | + | ||
| 353 | +### 2. 坐标系混乱 | ||
| 354 | + | ||
| 355 | +**问题**: 多种坐标系容易混淆 | ||
| 356 | + | ||
| 357 | +**解决方案**: | ||
| 358 | +- 统一使用网格坐标 | ||
| 359 | +- 提供统一的坐标转换工具函数 | ||
| 360 | +- 文档化每种坐标系的用途 | ||
| 361 | + | ||
| 362 | +### 3. XSS 风险 | ||
| 363 | + | ||
| 364 | +**问题**: `v-html` 渲染 SVG 可能存在 XSS 风险 | ||
| 365 | + | ||
| 366 | +**解决方案**: | ||
| 367 | +```javascript | ||
| 368 | +// 清理 SVG 字符串 | ||
| 369 | +import DOMPurify from 'dompurify'; | ||
| 370 | + | ||
| 371 | +const cleanSVG = DOMPurify.sanitize(svgString); | ||
| 372 | +``` | ||
| 373 | + | ||
| 374 | +## 最佳实践 | ||
| 375 | + | ||
| 376 | +### 1. 组件使用 | ||
| 377 | + | ||
| 378 | +```vue | ||
| 379 | +<!-- ✅ 推荐:使用 v-model 绑定显示状态 --> | ||
| 380 | +<Floor v-model:show="showFloor" /> | ||
| 381 | + | ||
| 382 | +<!-- ❌ 不推荐:手动控制显示隐藏 --> | ||
| 383 | +<Floor :show="showFloor" @close="showFloor = false" /> | ||
| 384 | +``` | ||
| 385 | + | ||
| 386 | +### 2. 事件处理 | ||
| 387 | + | ||
| 388 | +```javascript | ||
| 389 | +// ✅ 推荐:使用事件修饰符 | ||
| 390 | +<div @click.stop="handlePinClick"> | ||
| 391 | + | ||
| 392 | +// ❌ 不推荐:在事件处理函数中阻止冒泡 | ||
| 393 | +<div @click="handlePinClick"> | ||
| 394 | +``` | ||
| 395 | + | ||
| 396 | +### 3. 数据缓存 | ||
| 397 | + | ||
| 398 | +```javascript | ||
| 399 | +// ✅ 推荐:使用 Pinia 缓存地图数据 | ||
| 400 | +import { useMapStore } from '@/store/map'; | ||
| 401 | + | ||
| 402 | +const mapStore = useMapStore(); | ||
| 403 | +const mapData = await mapStore.fetchMapData(locationId); | ||
| 404 | + | ||
| 405 | +// ❌ 不推荐:每次都重新获取数据 | ||
| 406 | +const { data } = await mapAPI({ id: locationId }); | ||
| 407 | +``` | ||
| 408 | + | ||
| 409 | +## 调试技巧 | ||
| 410 | + | ||
| 411 | +### 1. 查看地图数据 | ||
| 412 | + | ||
| 413 | +```javascript | ||
| 414 | +// 在控制台查看地图数据 | ||
| 415 | +console.log('地图数据:', JSON.stringify(mapData, null, 2)); | ||
| 416 | +``` | ||
| 417 | + | ||
| 418 | +### 2. 查看标记点 | ||
| 419 | + | ||
| 420 | +```javascript | ||
| 421 | +// 高亮所有标记点 | ||
| 422 | +document.querySelectorAll('.pin').forEach(pin => { | ||
| 423 | + pin.style.border = '2px solid red'; | ||
| 424 | +}); | ||
| 425 | +``` | ||
| 426 | + | ||
| 427 | +### 3. 模拟点击 | ||
| 428 | + | ||
| 429 | +```javascript | ||
| 430 | +// 模拟点击标记点 | ||
| 431 | +document.querySelector('.pin').click(); | ||
| 432 | +``` | ||
| 433 | + | ||
| 434 | +## 参考文档 | ||
| 435 | + | ||
| 436 | +- [高德地图 JS API 文档](https://lbs.amap.com/api/jsapi-v2/summary) | ||
| 437 | +- [高德地图坐标转换](https://lbs.amap.com/api/jsapi-v2/guide/transform/convert) | ||
| 438 | +- [SVG 优化指南](https://web.dev/svg-optimization/) | ||
| 439 | +- [项目目录结构分析](../项目架构分析/目录结构分析.md) |
docs/功能模块分析/打卡系统分析.md
0 → 100644
| 1 | +# 打卡系统分析 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**相关文件**: | ||
| 5 | +- `src/api/checkin.js` - 打卡 API | ||
| 6 | +- `src/views/checkin/` - 打卡页面 | ||
| 7 | +- `src/views/bieyuan/scan.vue` - 别院扫码 | ||
| 8 | +- `src/views/by/scan.vue` - BY 扫码 | ||
| 9 | + | ||
| 10 | +## 系统架构 | ||
| 11 | + | ||
| 12 | +### 打卡流程 | ||
| 13 | + | ||
| 14 | +``` | ||
| 15 | +用户扫描二维码 | ||
| 16 | + ↓ | ||
| 17 | +打开打卡页面(checkin/index.vue) | ||
| 18 | + ↓ | ||
| 19 | +获取打卡信息(detail_id) | ||
| 20 | + ↓ | ||
| 21 | +检查是否已打卡(isCheckedAPI) | ||
| 22 | + ↓ | ||
| 23 | +已打卡? → 显示打卡信息(info_w.vue) | ||
| 24 | +未打卡? → 显示打卡按钮 | ||
| 25 | + ↓ | ||
| 26 | +用户点击打卡 | ||
| 27 | + ↓ | ||
| 28 | +提交打卡(checkinAPI) | ||
| 29 | + ↓ | ||
| 30 | +显示打卡成功(info_w.vue) | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +### 打卡类型 | ||
| 34 | + | ||
| 35 | +项目支持多种打卡场景: | ||
| 36 | + | ||
| 37 | +1. **别院打卡** (`bieyuan/`) | ||
| 38 | + - 页面: `src/views/bieyuan/scan.vue` | ||
| 39 | + - 信息页面: `src/views/bieyuan/info_w.vue` | ||
| 40 | + | ||
| 41 | +2. **BY 打卡** (`by/`) | ||
| 42 | + - 页面: `src/views/by/scan.vue` | ||
| 43 | + - 信息页面: `src/views/by/info_w.vue` | ||
| 44 | + | ||
| 45 | +3. **通用打卡** (`checkin/`) | ||
| 46 | + - 页面: `src/views/checkin/scan.vue` | ||
| 47 | + - 信息页面: `src/views/checkin/info_w.vue` | ||
| 48 | + - 信息页面: `src/views/checkin/info.vue` | ||
| 49 | + | ||
| 50 | +## API 接口 | ||
| 51 | + | ||
| 52 | +### 1. 检查是否已打卡 | ||
| 53 | + | ||
| 54 | +**端点**: `/srv/?f=walk&a=map&t=is_checked` | ||
| 55 | + | ||
| 56 | +**方法**: GET | ||
| 57 | + | ||
| 58 | +**请求参数**: | ||
| 59 | +```javascript | ||
| 60 | +{ | ||
| 61 | + detail_id: String, // 打卡点 ID(必需) | ||
| 62 | + openid: String // 微信 OpenID(必需) | ||
| 63 | +} | ||
| 64 | +``` | ||
| 65 | + | ||
| 66 | +**响应数据**: | ||
| 67 | +```javascript | ||
| 68 | +{ | ||
| 69 | + code: 1, | ||
| 70 | + msg: '', | ||
| 71 | + data: { | ||
| 72 | + is_checked: Number // 是否已打卡(0: 未打卡, 1: 已打卡) | ||
| 73 | + } | ||
| 74 | +} | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +**API 调用**: | ||
| 78 | +```javascript | ||
| 79 | +import { isCheckedAPI } from '@/api/checkin.js'; | ||
| 80 | + | ||
| 81 | +const { data } = await isCheckedAPI({ | ||
| 82 | + detail_id: '123', | ||
| 83 | + openid: 'wx_openid_123' | ||
| 84 | +}); | ||
| 85 | + | ||
| 86 | +if (data.is_checked === 1) { | ||
| 87 | + console.log('已打卡'); | ||
| 88 | +} else { | ||
| 89 | + console.log('未打卡'); | ||
| 90 | +} | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +### 2. 提交打卡 | ||
| 94 | + | ||
| 95 | +**端点**: `/srv/?f=walk&a=map&t=checkin` | ||
| 96 | + | ||
| 97 | +**方法**: POST | ||
| 98 | + | ||
| 99 | +**请求参数**: | ||
| 100 | +```javascript | ||
| 101 | +{ | ||
| 102 | + detail_id: String, // 打卡点 ID(必需) | ||
| 103 | + openid: String // 微信 OpenID(必需) | ||
| 104 | +} | ||
| 105 | +``` | ||
| 106 | + | ||
| 107 | +**响应数据**: | ||
| 108 | +```javascript | ||
| 109 | +{ | ||
| 110 | + code: 1, | ||
| 111 | + msg: '打卡成功', | ||
| 112 | + data: { | ||
| 113 | + // 打卡信息(可能包含) | ||
| 114 | + checkin_time: String, // 打卡时间 | ||
| 115 | + checkin_date: String, // 打卡日期 | ||
| 116 | + user_info: Object, // 用户信息 | ||
| 117 | + location_info: Object // 位置信息 | ||
| 118 | + } | ||
| 119 | +} | ||
| 120 | +``` | ||
| 121 | + | ||
| 122 | +**API 调用**: | ||
| 123 | +```javascript | ||
| 124 | +import { checkinAPI } from '@/api/checkin.js'; | ||
| 125 | + | ||
| 126 | +const { data } = await checkinAPI({ | ||
| 127 | + detail_id: '123', | ||
| 128 | + openid: 'wx_openid_123' | ||
| 129 | +}); | ||
| 130 | + | ||
| 131 | +console.log('打卡成功:', data); | ||
| 132 | +``` | ||
| 133 | + | ||
| 134 | +## 页面组件 | ||
| 135 | + | ||
| 136 | +### 1. 打卡首页 (checkin/index.vue) | ||
| 137 | + | ||
| 138 | +**功能**: | ||
| 139 | +- ✅ 显示打卡点信息 | ||
| 140 | +- ✅ 检查打卡状态 | ||
| 141 | +- ✅ 显示打卡按钮 | ||
| 142 | +- ✅ 跳转到扫码页面 | ||
| 143 | + | ||
| 144 | +**使用示例**: | ||
| 145 | +```vue | ||
| 146 | +<template> | ||
| 147 | + <div class="checkin-page"> | ||
| 148 | + <h1>{{ locationName }}</h1> | ||
| 149 | + <p>{{ locationDesc }}</p> | ||
| 150 | + | ||
| 151 | + <van-button | ||
| 152 | + v-if="!isChecked" | ||
| 153 | + type="primary" | ||
| 154 | + @click="handleCheckin" | ||
| 155 | + > | ||
| 156 | + 立即打卡 | ||
| 157 | + </van-button> | ||
| 158 | + | ||
| 159 | + <van-button | ||
| 160 | + v-else | ||
| 161 | + disabled | ||
| 162 | + > | ||
| 163 | + 已打卡 | ||
| 164 | + </van-button> | ||
| 165 | + </div> | ||
| 166 | +</template> | ||
| 167 | + | ||
| 168 | +<script setup> | ||
| 169 | +import { ref, onMounted } from 'vue'; | ||
| 170 | +import { useRouter } from 'vue-router'; | ||
| 171 | +import { isCheckedAPI, checkinAPI } from '@/api/checkin.js'; | ||
| 172 | +import { useUserStore } from '@/store/user'; | ||
| 173 | + | ||
| 174 | +const router = useRouter(); | ||
| 175 | +const userStore = useUserStore(); | ||
| 176 | + | ||
| 177 | +const locationName = ref('打卡点名称'); | ||
| 178 | +const locationDesc = ref('打卡点描述'); | ||
| 179 | +const isChecked = ref(false); | ||
| 180 | + | ||
| 181 | +const checkCheckinStatus = async () => { | ||
| 182 | + const { data } = await isCheckedAPI({ | ||
| 183 | + detail_id: router.currentRoute.value.query.id, | ||
| 184 | + openid: userStore.openid, | ||
| 185 | + }); | ||
| 186 | + | ||
| 187 | + isChecked.value = data.is_checked === 1; | ||
| 188 | +}; | ||
| 189 | + | ||
| 190 | +const handleCheckin = async () => { | ||
| 191 | + const { data } = await checkinAPI({ | ||
| 192 | + detail_id: router.currentRoute.value.query.id, | ||
| 193 | + openid: userStore.openid, | ||
| 194 | + }); | ||
| 195 | + | ||
| 196 | + // 跳转到打卡信息页面 | ||
| 197 | + router.push({ | ||
| 198 | + path: '/checkin/info_w', | ||
| 199 | + query: { id: router.currentRoute.value.query.id } | ||
| 200 | + }); | ||
| 201 | +}; | ||
| 202 | + | ||
| 203 | +onMounted(() => { | ||
| 204 | + checkCheckinStatus(); | ||
| 205 | +}); | ||
| 206 | +</script> | ||
| 207 | +``` | ||
| 208 | + | ||
| 209 | +### 2. 扫码页面 (scan.vue) | ||
| 210 | + | ||
| 211 | +**功能**: | ||
| 212 | +- ✅ 调用扫码功能 | ||
| 213 | +- ✅ 解析二维码 | ||
| 214 | +- ✅ 跳转到打卡页面 | ||
| 215 | + | ||
| 216 | +**使用示例**: | ||
| 217 | +```vue | ||
| 218 | +<template> | ||
| 219 | + <div class="scan-page"> | ||
| 220 | + <button @click="handleScan">扫码打卡</button> | ||
| 221 | + </div> | ||
| 222 | +</template> | ||
| 223 | + | ||
| 224 | +<script setup> | ||
| 225 | +import { useRouter } from 'vue-router'; | ||
| 226 | + | ||
| 227 | +const router = useRouter(); | ||
| 228 | + | ||
| 229 | +const handleScan = async () => { | ||
| 230 | + // 调用微信扫码 | ||
| 231 | + wx.scanQRCode({ | ||
| 232 | + needResult: 1, // 1: 返回扫码结果 | ||
| 233 | + scanType: ['qrCode'], // 可以指定扫二维码还是一维码 | ||
| 234 | + success: (res) => { | ||
| 235 | + // res.resultStr: 扫码结果 | ||
| 236 | + const detailId = extractDetailId(res.resultStr); | ||
| 237 | + | ||
| 238 | + // 跳转到打卡页面 | ||
| 239 | + router.push({ | ||
| 240 | + path: '/checkin', | ||
| 241 | + query: { id: detailId } | ||
| 242 | + }); | ||
| 243 | + }, | ||
| 244 | + fail: (err) => { | ||
| 245 | + console.error('扫码失败:', err); | ||
| 246 | + } | ||
| 247 | + }); | ||
| 248 | +}; | ||
| 249 | + | ||
| 250 | +const extractDetailId = (resultStr) => { | ||
| 251 | + // 从扫码结果中提取 detail_id | ||
| 252 | + // 例如: https://example.com/checkin?id=123 | ||
| 253 | + const url = new URL(resultStr); | ||
| 254 | + return url.searchParams.get('id'); | ||
| 255 | +}; | ||
| 256 | +</script> | ||
| 257 | +``` | ||
| 258 | + | ||
| 259 | +### 3. 打卡信息页面 (info.vue, info_w.vue) | ||
| 260 | + | ||
| 261 | +**功能**: | ||
| 262 | +- ✅ 显示打卡时间 | ||
| 263 | +- ✅ 显示用户信息 | ||
| 264 | +- ✅ 显示位置信息 | ||
| 265 | +- ✅ 显示打卡状态 | ||
| 266 | + | ||
| 267 | +**区别**: | ||
| 268 | +- `info.vue`: 普通信息页面 | ||
| 269 | +- `info_w.vue`: 带警告样式的信息页面 | ||
| 270 | + | ||
| 271 | +## 路由配置 | ||
| 272 | + | ||
| 273 | +### 路由定义 | ||
| 274 | + | ||
| 275 | +```javascript | ||
| 276 | +// src/router/routes/modules/checkin/index.js(如果存在) | ||
| 277 | +{ | ||
| 278 | + path: '/checkin', | ||
| 279 | + name: 'Checkin', | ||
| 280 | + component: () => import('@/views/checkin/index.vue'), | ||
| 281 | + meta: { | ||
| 282 | + title: '打卡', | ||
| 283 | + requireAuth: true, // 需要登录 | ||
| 284 | + }, | ||
| 285 | + children: [ | ||
| 286 | + { | ||
| 287 | + path: 'info', | ||
| 288 | + name: 'CheckinInfo', | ||
| 289 | + component: () => import('@/views/checkin/info.vue'), | ||
| 290 | + }, | ||
| 291 | + { | ||
| 292 | + path: 'info_w', | ||
| 293 | + name: 'CheckinInfoWarn', | ||
| 294 | + component: () => import('@/views/checkin/info_w.vue'), | ||
| 295 | + }, | ||
| 296 | + { | ||
| 297 | + path: 'scan', | ||
| 298 | + name: 'CheckinScan', | ||
| 299 | + component: () => import('@/views/checkin/scan.vue'), | ||
| 300 | + }, | ||
| 301 | + ], | ||
| 302 | +} | ||
| 303 | +``` | ||
| 304 | + | ||
| 305 | +## 数据流程 | ||
| 306 | + | ||
| 307 | +### 1. 用户扫码 | ||
| 308 | + | ||
| 309 | +``` | ||
| 310 | +微信扫码 | ||
| 311 | + ↓ | ||
| 312 | +微信返回扫码结果 | ||
| 313 | + ↓ | ||
| 314 | +解析 detail_id | ||
| 315 | + ↓ | ||
| 316 | +路由跳转(携带 detail_id) | ||
| 317 | +``` | ||
| 318 | + | ||
| 319 | +### 2. 检查打卡状态 | ||
| 320 | + | ||
| 321 | +``` | ||
| 322 | +进入打卡页面 | ||
| 323 | + ↓ | ||
| 324 | +获取 detail_id(路由参数) | ||
| 325 | + ↓ | ||
| 326 | +获取 openid(用户信息) | ||
| 327 | + ↓ | ||
| 328 | +调用 isCheckedAPI | ||
| 329 | + ↓ | ||
| 330 | +显示打卡状态 | ||
| 331 | +``` | ||
| 332 | + | ||
| 333 | +### 3. 提交打卡 | ||
| 334 | + | ||
| 335 | +``` | ||
| 336 | +用户点击打卡按钮 | ||
| 337 | + ↓ | ||
| 338 | +调用 checkinAPI | ||
| 339 | + ↓ | ||
| 340 | +更新打卡状态 | ||
| 341 | + ↓ | ||
| 342 | +显示打卡成功信息 | ||
| 343 | +``` | ||
| 344 | + | ||
| 345 | +## 微信集成 | ||
| 346 | + | ||
| 347 | +### 1. 扫码功能 | ||
| 348 | + | ||
| 349 | +**微信 JS-SDK 配置**: | ||
| 350 | + | ||
| 351 | +```javascript | ||
| 352 | +// src/api/wx/jsApiList.js | ||
| 353 | +export const jsApiList = [ | ||
| 354 | + 'scanQRCode', // 扫码 | ||
| 355 | + // 其他 API... | ||
| 356 | +]; | ||
| 357 | +``` | ||
| 358 | + | ||
| 359 | +**使用示例**: | ||
| 360 | + | ||
| 361 | +```javascript | ||
| 362 | +import { jsApiList } from '@/api/wx/jsApiList'; | ||
| 363 | + | ||
| 364 | +wx.config({ | ||
| 365 | + debug: false, | ||
| 366 | + appId: 'YOUR_APPID', | ||
| 367 | + timestamp: timestamp, | ||
| 368 | + nonceStr: nonceStr, | ||
| 369 | + signature: signature, | ||
| 370 | + jsApiList: jsApiList, | ||
| 371 | +}); | ||
| 372 | + | ||
| 373 | +wx.ready(() => { | ||
| 374 | + // 扫码功能就绪 | ||
| 375 | +}); | ||
| 376 | + | ||
| 377 | +wx.error((res) => { | ||
| 378 | + console.error('微信配置失败:', res); | ||
| 379 | +}); | ||
| 380 | +``` | ||
| 381 | + | ||
| 382 | +### 2. 用户信息 | ||
| 383 | + | ||
| 384 | +**OpenID 获取**: | ||
| 385 | + | ||
| 386 | +```javascript | ||
| 387 | +// 从用户信息中获取 OpenID | ||
| 388 | +import { useUserStore } from '@/store/user'; | ||
| 389 | + | ||
| 390 | +const userStore = useUserStore(); | ||
| 391 | +const openid = userStore.openid; | ||
| 392 | +``` | ||
| 393 | + | ||
| 394 | +## 已知问题 | ||
| 395 | + | ||
| 396 | +### 1. 重复打卡 | ||
| 397 | + | ||
| 398 | +**问题**: 用户可能多次点击打卡按钮 | ||
| 399 | + | ||
| 400 | +**解决方案**: | ||
| 401 | +```javascript | ||
| 402 | +// 防止重复提交 | ||
| 403 | +const isSubmitting = ref(false); | ||
| 404 | + | ||
| 405 | +const handleCheckin = async () => { | ||
| 406 | + if (isSubmitting.value) { | ||
| 407 | + return; | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + isSubmitting.value = true; | ||
| 411 | + | ||
| 412 | + try { | ||
| 413 | + const { data } = await checkinAPI({ | ||
| 414 | + detail_id: detailId, | ||
| 415 | + openid: openid, | ||
| 416 | + }); | ||
| 417 | + | ||
| 418 | + // 打卡成功 | ||
| 419 | + } catch (err) { | ||
| 420 | + console.error('打卡失败:', err); | ||
| 421 | + } finally { | ||
| 422 | + isSubmitting.value = false; | ||
| 423 | + } | ||
| 424 | +}; | ||
| 425 | +``` | ||
| 426 | + | ||
| 427 | +### 2. 网络异常 | ||
| 428 | + | ||
| 429 | +**问题**: 网络请求失败 | ||
| 430 | + | ||
| 431 | +**解决方案**: | ||
| 432 | +```javascript | ||
| 433 | +const handleCheckin = async () => { | ||
| 434 | + try { | ||
| 435 | + const { data } = await checkinAPI({ | ||
| 436 | + detail_id: detailId, | ||
| 437 | + openid: openid, | ||
| 438 | + }); | ||
| 439 | + | ||
| 440 | + showToast('打卡成功'); | ||
| 441 | + } catch (err) { | ||
| 442 | + showToast('打卡失败,请重试'); | ||
| 443 | + | ||
| 444 | + // 延迟重试 | ||
| 445 | + setTimeout(() => { | ||
| 446 | + handleCheckin(); | ||
| 447 | + }, 1000); | ||
| 448 | + } | ||
| 449 | +}; | ||
| 450 | +``` | ||
| 451 | + | ||
| 452 | +### 3. OpenID 获取失败 | ||
| 453 | + | ||
| 454 | +**问题**: OpenID 未正确获取 | ||
| 455 | + | ||
| 456 | +**解决方案**: | ||
| 457 | +```javascript | ||
| 458 | +// 检查 OpenID 是否存在 | ||
| 459 | +const checkOpenId = () => { | ||
| 460 | + const openid = userStore.openid; | ||
| 461 | + | ||
| 462 | + if (!openid) { | ||
| 463 | + // 跳转到登录页面 | ||
| 464 | + router.push('/login'); | ||
| 465 | + return false; | ||
| 466 | + } | ||
| 467 | + | ||
| 468 | + return true; | ||
| 469 | +}; | ||
| 470 | + | ||
| 471 | +const handleCheckin = async () => { | ||
| 472 | + if (!checkOpenId()) { | ||
| 473 | + return; | ||
| 474 | + } | ||
| 475 | + | ||
| 476 | + // 继续打卡流程 | ||
| 477 | +}; | ||
| 478 | +``` | ||
| 479 | + | ||
| 480 | +## 最佳实践 | ||
| 481 | + | ||
| 482 | +### 1. 错误处理 | ||
| 483 | + | ||
| 484 | +```javascript | ||
| 485 | +// ✅ 推荐:统一的错误处理 | ||
| 486 | +const checkin = async () => { | ||
| 487 | + try { | ||
| 488 | + const { data } = await checkinAPI(params); | ||
| 489 | + | ||
| 490 | + if (data.code === 1) { | ||
| 491 | + showToast('打卡成功'); | ||
| 492 | + return data; | ||
| 493 | + } else { | ||
| 494 | + showToast(data.msg || '打卡失败'); | ||
| 495 | + return null; | ||
| 496 | + } | ||
| 497 | + } catch (err) { | ||
| 498 | + console.error('打卡异常:', err); | ||
| 499 | + showToast('网络异常,请重试'); | ||
| 500 | + return null; | ||
| 501 | + } | ||
| 502 | +}; | ||
| 503 | + | ||
| 504 | +// ❌ 不推荐:不处理错误 | ||
| 505 | +const checkin = async () => { | ||
| 506 | + const { data } = await checkinAPI(params); | ||
| 507 | + return data; | ||
| 508 | +}; | ||
| 509 | +``` | ||
| 510 | + | ||
| 511 | +### 2. 状态管理 | ||
| 512 | + | ||
| 513 | +```javascript | ||
| 514 | +// ✅ 推荐:使用 Pinia 管理打卡状态 | ||
| 515 | +import { useCheckinStore } from '@/store/checkin'; | ||
| 516 | + | ||
| 517 | +const checkinStore = useCheckinStore(); | ||
| 518 | + | ||
| 519 | +const handleCheckin = async () => { | ||
| 520 | + const success = await checkinStore.checkin(detailId, openid); | ||
| 521 | + | ||
| 522 | + if (success) { | ||
| 523 | + showToast('打卡成功'); | ||
| 524 | + } | ||
| 525 | +}; | ||
| 526 | + | ||
| 527 | +// ❌ 不推荐:在组件中管理状态 | ||
| 528 | +const isChecked = ref(false); | ||
| 529 | + | ||
| 530 | +const handleCheckin = async () => { | ||
| 531 | + const { data } = await checkinAPI(params); | ||
| 532 | + isChecked.value = true; | ||
| 533 | +}; | ||
| 534 | +``` | ||
| 535 | + | ||
| 536 | +### 3. 路由跳转 | ||
| 537 | + | ||
| 538 | +```javascript | ||
| 539 | +// ✅ 推荐:使用命名路由 | ||
| 540 | +router.push({ | ||
| 541 | + name: 'CheckinInfo', | ||
| 542 | + query: { id: detailId } | ||
| 543 | +}); | ||
| 544 | + | ||
| 545 | +// ❌ 不推荐:使用路径拼接 | ||
| 546 | +router.push(`/checkin/info?id=${detailId}`); | ||
| 547 | +``` | ||
| 548 | + | ||
| 549 | +## 调试技巧 | ||
| 550 | + | ||
| 551 | +### 1. 模拟打卡 | ||
| 552 | + | ||
| 553 | +```javascript | ||
| 554 | +// 在控制台模拟打卡 | ||
| 555 | +import { checkinAPI } from '@/api/checkin.js'; | ||
| 556 | + | ||
| 557 | +checkinAPI({ | ||
| 558 | + detail_id: '123', | ||
| 559 | + openid: 'test_openid' | ||
| 560 | +}).then(res => console.log(res)); | ||
| 561 | +``` | ||
| 562 | + | ||
| 563 | +### 2. 查看打卡状态 | ||
| 564 | + | ||
| 565 | +```javascript | ||
| 566 | +// 查看当前打卡状态 | ||
| 567 | +console.log('打卡状态:', { | ||
| 568 | + detailId: router.currentRoute.value.query.id, | ||
| 569 | + openid: userStore.openid, | ||
| 570 | + isChecked: isChecked.value | ||
| 571 | +}); | ||
| 572 | +``` | ||
| 573 | + | ||
| 574 | +### 3. 清除打卡状态 | ||
| 575 | + | ||
| 576 | +```javascript | ||
| 577 | +// 清除本地打卡状态(测试用) | ||
| 578 | +localStorage.removeItem('checkin_status'); | ||
| 579 | +``` | ||
| 580 | + | ||
| 581 | +## 参考文档 | ||
| 582 | + | ||
| 583 | +- [微信 JS-SDK 文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK) | ||
| 584 | +- [微信扫码文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK#55) | ||
| 585 | +- [项目路由配置](../项目架构分析/目录结构分析.md) |
docs/功能模块分析/音频系统分析.md
0 → 100644
| 1 | +# 音频系统分析 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**相关文件**: | ||
| 5 | +- `src/components/audioBackground.vue` - 背景音频(单模式) | ||
| 6 | +- `src/components/audioBackground1.vue` - 背景音频(备用) | ||
| 7 | +- `src/components/audioList.vue` - 音频列表(播放列表模式) | ||
| 8 | +- `src/api/map.js` - 音频 API (`mapAudioAPI`) | ||
| 9 | + | ||
| 10 | +## 系统架构 | ||
| 11 | + | ||
| 12 | +### 双模式设计 | ||
| 13 | + | ||
| 14 | +音频系统支持两种播放模式: | ||
| 15 | + | ||
| 16 | +1. **单模式 (Single Mode)** | ||
| 17 | + - 组件: `audioBackground.vue`, `audioBackground1.vue` | ||
| 18 | + - 用途: 单个音频的背景播放 | ||
| 19 | + - 特点: 简单、轻量 | ||
| 20 | + | ||
| 21 | +2. **播放列表模式 (Playlist Mode)** | ||
| 22 | + - 组件: `audioList.vue` | ||
| 23 | + - 用途: 多个音频的顺序播放 | ||
| 24 | + - 特点: 支持播放列表控制 | ||
| 25 | + | ||
| 26 | +### 状态管理 | ||
| 27 | + | ||
| 28 | +**Pinia Store** (`src/store/index.js`): | ||
| 29 | + | ||
| 30 | +```javascript | ||
| 31 | +// 单模式状态 | ||
| 32 | +audio_entity: null, // 当前音频实体 | ||
| 33 | +audio_status: false, // 播放状态 | ||
| 34 | + | ||
| 35 | +// 播放列表模式状态 | ||
| 36 | +audio_list_entity: [], // 播放列表 | ||
| 37 | +audio_list_status: false, // 播放状态 | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +## 核心功能 | ||
| 41 | + | ||
| 42 | +### 1. 音频加载 | ||
| 43 | + | ||
| 44 | +**API 端点**: `/srv/?a=map_audio` | ||
| 45 | + | ||
| 46 | +```javascript | ||
| 47 | +// src/api/map.js | ||
| 48 | +export const mapAudioAPI = (params) => fn(fetch.get(Api.MAP_AUDIO, params)); | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +**请求参数**: | ||
| 52 | +- `id`: 位置/景点 ID | ||
| 53 | +- `type`: 音频类型(可选) | ||
| 54 | + | ||
| 55 | +**响应数据**: | ||
| 56 | +```javascript | ||
| 57 | +{ | ||
| 58 | + code: 1, | ||
| 59 | + data: { | ||
| 60 | + audioUrl: '/audio/guide.mp3', | ||
| 61 | + title: '景点介绍', | ||
| 62 | + duration: 120, | ||
| 63 | + // 其他音频信息 | ||
| 64 | + }, | ||
| 65 | + msg: '' | ||
| 66 | +} | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +### 2. 单模式播放 | ||
| 70 | + | ||
| 71 | +**组件**: `audioBackground.vue` | ||
| 72 | + | ||
| 73 | +**功能**: | ||
| 74 | +- ✅ 播放/暂停控制 | ||
| 75 | +- ✅ 进度条显示 | ||
| 76 | +- ✅ 音量控制 | ||
| 77 | +- ✅ 自动播放(可选) | ||
| 78 | +- ✅ 循环播放(可选) | ||
| 79 | + | ||
| 80 | +**使用示例**: | ||
| 81 | +```vue | ||
| 82 | +<template> | ||
| 83 | + <audioBackground | ||
| 84 | + :audio-url="currentAudio" | ||
| 85 | + :autoplay="true" | ||
| 86 | + @play="handlePlay" | ||
| 87 | + @pause="handlePause" | ||
| 88 | + @ended="handleEnded" | ||
| 89 | + /> | ||
| 90 | +</template> | ||
| 91 | + | ||
| 92 | +<script setup> | ||
| 93 | +import audioBackground from '@components/audioBackground.vue'; | ||
| 94 | +import { ref } from 'vue'; | ||
| 95 | + | ||
| 96 | +const currentAudio = ref('/audio/guide.mp3'); | ||
| 97 | + | ||
| 98 | +const handlePlay = () => { | ||
| 99 | + console.log('开始播放'); | ||
| 100 | +}; | ||
| 101 | + | ||
| 102 | +const handlePause = () => { | ||
| 103 | + console.log('暂停播放'); | ||
| 104 | +}; | ||
| 105 | + | ||
| 106 | +const handleEnded = () => { | ||
| 107 | + console.log('播放结束'); | ||
| 108 | +}; | ||
| 109 | +</script> | ||
| 110 | +``` | ||
| 111 | + | ||
| 112 | +### 3. 播放列表模式 | ||
| 113 | + | ||
| 114 | +**组件**: `audioList.vue` | ||
| 115 | + | ||
| 116 | +**功能**: | ||
| 117 | +- ✅ 播放列表管理 | ||
| 118 | +- ✅ 上一首/下一首 | ||
| 119 | +- ✅ 播放进度 | ||
| 120 | +- ✅ 当前播放高亮 | ||
| 121 | +- ✅ 自动播放下一首 | ||
| 122 | +- ✅ 列表循环 | ||
| 123 | + | ||
| 124 | +**使用示例**: | ||
| 125 | +```vue | ||
| 126 | +<template> | ||
| 127 | + <audioList | ||
| 128 | + v-model:playlist="audioPlaylist" | ||
| 129 | + :current-index="currentIndex" | ||
| 130 | + @play="handlePlay" | ||
| 131 | + @pause="handlePause" | ||
| 132 | + @next="handleNext" | ||
| 133 | + @prev="handlePrev" | ||
| 134 | + /> | ||
| 135 | +</template> | ||
| 136 | + | ||
| 137 | +<script setup> | ||
| 138 | +import audioList from '@components/audioList.vue'; | ||
| 139 | +import { ref } from 'vue'; | ||
| 140 | + | ||
| 141 | +const audioPlaylist = ref([ | ||
| 142 | + { | ||
| 143 | + id: 1, | ||
| 144 | + title: '景点介绍 1', | ||
| 145 | + url: '/audio/guide1.mp3', | ||
| 146 | + duration: 120 | ||
| 147 | + }, | ||
| 148 | + { | ||
| 149 | + id: 2, | ||
| 150 | + title: '景点介绍 2', | ||
| 151 | + url: '/audio/guide2.mp3', | ||
| 152 | + duration: 90 | ||
| 153 | + } | ||
| 154 | +]); | ||
| 155 | + | ||
| 156 | +const currentIndex = ref(0); | ||
| 157 | + | ||
| 158 | +const handlePlay = (index) => { | ||
| 159 | + console.log('播放索引:', index); | ||
| 160 | +}; | ||
| 161 | + | ||
| 162 | +const handleNext = () => { | ||
| 163 | + if (currentIndex.value < audioPlaylist.value.length - 1) { | ||
| 164 | + currentIndex.value++; | ||
| 165 | + } | ||
| 166 | +}; | ||
| 167 | + | ||
| 168 | +const handlePrev = () => { | ||
| 169 | + if (currentIndex.value > 0) { | ||
| 170 | + currentIndex.value--; | ||
| 171 | + } | ||
| 172 | +}; | ||
| 173 | +</script> | ||
| 174 | +``` | ||
| 175 | + | ||
| 176 | +### 4. 音频状态同步 | ||
| 177 | + | ||
| 178 | +**Pinia Store**: | ||
| 179 | + | ||
| 180 | +```javascript | ||
| 181 | +import { defineStore } from 'pinia'; | ||
| 182 | + | ||
| 183 | +export const useAudioStore = defineStore('audio', { | ||
| 184 | + state: () => ({ | ||
| 185 | + // 单模式 | ||
| 186 | + audio_entity: null, | ||
| 187 | + audio_status: false, | ||
| 188 | + | ||
| 189 | + // 播放列表模式 | ||
| 190 | + audio_list_entity: [], | ||
| 191 | + audio_list_status: false, | ||
| 192 | + audio_list_index: 0, | ||
| 193 | + }), | ||
| 194 | + | ||
| 195 | + actions: { | ||
| 196 | + // 设置单模式音频 | ||
| 197 | + setAudio(entity) { | ||
| 198 | + this.audio_entity = entity; | ||
| 199 | + }, | ||
| 200 | + | ||
| 201 | + // 播放/暂停单模式 | ||
| 202 | + toggleAudio() { | ||
| 203 | + this.audio_status = !this.audio_status; | ||
| 204 | + }, | ||
| 205 | + | ||
| 206 | + // 设置播放列表 | ||
| 207 | + setPlaylist(list) { | ||
| 208 | + this.audio_list_entity = list; | ||
| 209 | + }, | ||
| 210 | + | ||
| 211 | + // 播放/暂停播放列表 | ||
| 212 | + togglePlaylist() { | ||
| 213 | + this.audio_list_status = !this.audio_list_status; | ||
| 214 | + }, | ||
| 215 | + | ||
| 216 | + // 下一首 | ||
| 217 | + nextTrack() { | ||
| 218 | + if (this.audio_list_index < this.audio_list_entity.length - 1) { | ||
| 219 | + this.audio_list_index++; | ||
| 220 | + } | ||
| 221 | + }, | ||
| 222 | + | ||
| 223 | + // 上一首 | ||
| 224 | + prevTrack() { | ||
| 225 | + if (this.audio_list_index > 0) { | ||
| 226 | + this.audio_list_index--; | ||
| 227 | + } | ||
| 228 | + }, | ||
| 229 | + }, | ||
| 230 | +}); | ||
| 231 | +``` | ||
| 232 | + | ||
| 233 | +## 音频格式 | ||
| 234 | + | ||
| 235 | +### 支持的格式 | ||
| 236 | + | ||
| 237 | +- ✅ MP3(推荐) | ||
| 238 | +- ✅ WAV | ||
| 239 | +- ✅ OGG | ||
| 240 | +- ✅ AAC | ||
| 241 | +- ✅ M4A | ||
| 242 | + | ||
| 243 | +### 推荐配置 | ||
| 244 | + | ||
| 245 | +**编码格式**: MP3 | ||
| 246 | +**比特率**: 128 kbps | ||
| 247 | +**采样率**: 44.1 kHz | ||
| 248 | +**声道**: 立体声 | ||
| 249 | + | ||
| 250 | +### 文件大小建议 | ||
| 251 | + | ||
| 252 | +| 音频时长 | 推荐大小 | | ||
| 253 | +|---------|---------| | ||
| 254 | +| < 1 分钟 | < 1 MB | | ||
| 255 | +| 1-3 分钟 | 1-3 MB | | ||
| 256 | +| 3-5 分钟 | 3-5 MB | | ||
| 257 | +| > 5 分钟 | 建议分割 | | ||
| 258 | + | ||
| 259 | +## 性能优化 | ||
| 260 | + | ||
| 261 | +### 1. 懒加载 | ||
| 262 | + | ||
| 263 | +```javascript | ||
| 264 | +// 仅在需要时加载音频 | ||
| 265 | +const loadAudio = async (url) => { | ||
| 266 | + const audio = new Audio(); | ||
| 267 | + audio.src = url; | ||
| 268 | + await audio.load(); // 预加载 | ||
| 269 | + return audio; | ||
| 270 | +}; | ||
| 271 | +``` | ||
| 272 | + | ||
| 273 | +### 2. 缓存策略 | ||
| 274 | + | ||
| 275 | +```javascript | ||
| 276 | +// 缓存已加载的音频 | ||
| 277 | +const audioCache = new Map(); | ||
| 278 | + | ||
| 279 | +export const getCachedAudio = (url) => { | ||
| 280 | + if (audioCache.has(url)) { | ||
| 281 | + return audioCache.get(url); | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + const audio = new Audio(url); | ||
| 285 | + audioCache.set(url, audio); | ||
| 286 | + return audio; | ||
| 287 | +}; | ||
| 288 | +``` | ||
| 289 | + | ||
| 290 | +### 3. 预加载 | ||
| 291 | + | ||
| 292 | +```vue | ||
| 293 | +<audio :src="audioUrl" preload="auto" /> | ||
| 294 | +``` | ||
| 295 | + | ||
| 296 | +**preload 选项**: | ||
| 297 | +- `none`: 不预加载 | ||
| 298 | +- `metadata`: 仅预加载元数据(时长、尺寸等) | ||
| 299 | +- `auto`: 完全预加载 | ||
| 300 | + | ||
| 301 | +### 4. 资源释放 | ||
| 302 | + | ||
| 303 | +```javascript | ||
| 304 | +// 组件卸载时释放音频资源 | ||
| 305 | +onUnmounted(() => { | ||
| 306 | + if (audio.value) { | ||
| 307 | + audio.value.pause(); | ||
| 308 | + audio.value.src = ''; | ||
| 309 | + audio.value.load(); | ||
| 310 | + } | ||
| 311 | +}); | ||
| 312 | +``` | ||
| 313 | + | ||
| 314 | +## 已知问题 | ||
| 315 | + | ||
| 316 | +### 1. iOS 自动播放限制 | ||
| 317 | + | ||
| 318 | +**问题**: iOS 不允许自动播放音频 | ||
| 319 | + | ||
| 320 | +**解决方案**: | ||
| 321 | +```javascript | ||
| 322 | +// 用户交互后播放 | ||
| 323 | +const playAudio = () => { | ||
| 324 | + document.addEventListener('touchstart', function onTouchStart() { | ||
| 325 | + // 播放音频 | ||
| 326 | + audio.play(); | ||
| 327 | + | ||
| 328 | + // 移除监听器 | ||
| 329 | + document.removeEventListener('touchstart', onTouchStart); | ||
| 330 | + }, { once: true }); | ||
| 331 | +}; | ||
| 332 | +``` | ||
| 333 | + | ||
| 334 | +### 2. 多个音频冲突 | ||
| 335 | + | ||
| 336 | +**问题**: 同时播放多个音频 | ||
| 337 | + | ||
| 338 | +**解决方案**: | ||
| 339 | +```javascript | ||
| 340 | +// 播放新音频前停止其他音频 | ||
| 341 | +const playAudio = (newAudio) => { | ||
| 342 | + if (currentAudio.value) { | ||
| 343 | + currentAudio.value.pause(); | ||
| 344 | + } | ||
| 345 | + currentAudio.value = newAudio; | ||
| 346 | + currentAudio.value.play(); | ||
| 347 | +}; | ||
| 348 | +``` | ||
| 349 | + | ||
| 350 | +### 3. 音频加载失败 | ||
| 351 | + | ||
| 352 | +**问题**: 网络错误或文件不存在 | ||
| 353 | + | ||
| 354 | +**解决方案**: | ||
| 355 | +```javascript | ||
| 356 | +audio.addEventListener('error', (e) => { | ||
| 357 | + console.error('音频加载失败:', e); | ||
| 358 | + | ||
| 359 | + // 显示错误提示 | ||
| 360 | + showToast('音频加载失败,请检查网络'); | ||
| 361 | + | ||
| 362 | + // 尝试重新加载 | ||
| 363 | + setTimeout(() => { | ||
| 364 | + audio.load(); | ||
| 365 | + }, 1000); | ||
| 366 | +}); | ||
| 367 | +``` | ||
| 368 | + | ||
| 369 | +## 最佳实践 | ||
| 370 | + | ||
| 371 | +### 1. 音频路径管理 | ||
| 372 | + | ||
| 373 | +```javascript | ||
| 374 | +// ✅ 推荐:使用别名 | ||
| 375 | +const audioUrl = ref('@images/audio/guide.mp3'); | ||
| 376 | + | ||
| 377 | +// ❌ 不推荐:使用相对路径 | ||
| 378 | +const audioUrl = ref('../../images/audio/guide.mp3'); | ||
| 379 | +``` | ||
| 380 | + | ||
| 381 | +### 2. 音频加载状态 | ||
| 382 | + | ||
| 383 | +```vue | ||
| 384 | +<template> | ||
| 385 | + <div v-if="loading" class="loading">加载中...</div> | ||
| 386 | + <div v-else-if="error" class="error">加载失败</div> | ||
| 387 | + <audio v-else :src="audioUrl" @loadeddata="onLoaded" /> | ||
| 388 | +</template> | ||
| 389 | + | ||
| 390 | +<script setup> | ||
| 391 | +import { ref } from 'vue'; | ||
| 392 | + | ||
| 393 | +const loading = ref(true); | ||
| 394 | +const error = ref(false); | ||
| 395 | + | ||
| 396 | +const onLoaded = () => { | ||
| 397 | + loading.value = false; | ||
| 398 | +}; | ||
| 399 | +</script> | ||
| 400 | +``` | ||
| 401 | + | ||
| 402 | +### 3. 音频控制 | ||
| 403 | + | ||
| 404 | +```javascript | ||
| 405 | +// ✅ 推荐:使用 Vue 组件 | ||
| 406 | +<audioBackground v-model:playing="isPlaying" :audio-url="audioUrl" /> | ||
| 407 | + | ||
| 408 | +// ❌ 不推荐:直接操作 DOM | ||
| 409 | +document.querySelector('audio').play(); | ||
| 410 | +``` | ||
| 411 | + | ||
| 412 | +## 调试技巧 | ||
| 413 | + | ||
| 414 | +### 1. 查看音频状态 | ||
| 415 | + | ||
| 416 | +```javascript | ||
| 417 | +// 在控制台查看音频状态 | ||
| 418 | +console.log('音频状态:', { | ||
| 419 | + src: audio.src, | ||
| 420 | + duration: audio.duration, | ||
| 421 | + currentTime: audio.currentTime, | ||
| 422 | + paused: audio.paused, | ||
| 423 | + ended: audio.ended, | ||
| 424 | +}); | ||
| 425 | +``` | ||
| 426 | + | ||
| 427 | +### 2. 监听音频事件 | ||
| 428 | + | ||
| 429 | +```javascript | ||
| 430 | +audio.addEventListener('loadstart', () => console.log('开始加载')); | ||
| 431 | +audio.addEventListener('canplay', () => console.log('可以播放')); | ||
| 432 | +audio.addEventListener('play', () => console.log('播放')); | ||
| 433 | +audio.addEventListener('pause', () => console.log('暂停')); | ||
| 434 | +audio.addEventListener('ended', () => console.log('结束')); | ||
| 435 | +audio.addEventListener('error', (e) => console.error('错误:', e)); | ||
| 436 | +``` | ||
| 437 | + | ||
| 438 | +### 3. 模拟音频播放 | ||
| 439 | + | ||
| 440 | +```javascript | ||
| 441 | +// 在控制台手动播放音频 | ||
| 442 | +document.querySelector('audio').play(); | ||
| 443 | +``` | ||
| 444 | + | ||
| 445 | +## 参考文档 | ||
| 446 | + | ||
| 447 | +- [HTML5 Audio API](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio) | ||
| 448 | +- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) | ||
| 449 | +- [音频格式对比](https://en.wikipedia.org/wiki/Audio_file_format) |
docs/开发指南/常见开发任务.md
0 → 100644
| 1 | +# 常见开发任务 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**目的**: 记录项目中的常见开发任务和解决方案 | ||
| 5 | + | ||
| 6 | +## 1. 添加新页面 | ||
| 7 | + | ||
| 8 | +### 步骤 | ||
| 9 | + | ||
| 10 | +```bash | ||
| 11 | +# 1. 创建页面目录 | ||
| 12 | +mkdir src/views/mypage | ||
| 13 | + | ||
| 14 | +# 2. 创建页面文件 | ||
| 15 | +touch src/views/mypage/index.vue | ||
| 16 | +touch src/views/mypage/index.config.js | ||
| 17 | +``` | ||
| 18 | + | ||
| 19 | +### 页面文件 | ||
| 20 | + | ||
| 21 | +```vue | ||
| 22 | +<!-- src/views/mypage/index.vue --> | ||
| 23 | +<template> | ||
| 24 | + <div class="mypage"> | ||
| 25 | + <h1>{{ title }}</h1> | ||
| 26 | + </div> | ||
| 27 | +</template> | ||
| 28 | + | ||
| 29 | +<script setup> | ||
| 30 | +/** | ||
| 31 | + * 我的页面 | ||
| 32 | + * | ||
| 33 | + * @description 这是一个示例页面 | ||
| 34 | + */ | ||
| 35 | +import { ref } from 'vue'; | ||
| 36 | + | ||
| 37 | +const title = ref('我的页面'); | ||
| 38 | +</script> | ||
| 39 | + | ||
| 40 | +<style lang="less" scoped> | ||
| 41 | +.mypage { | ||
| 42 | + padding: 20px; | ||
| 43 | + | ||
| 44 | + h1 { | ||
| 45 | + font-size: 24px; | ||
| 46 | + } | ||
| 47 | +} | ||
| 48 | +</style> | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +### 页面配置 | ||
| 52 | + | ||
| 53 | +```javascript | ||
| 54 | +// src/views/mypage/index.config.js | ||
| 55 | +export default { | ||
| 56 | + navigationBarTitleText: '我的页面', | ||
| 57 | +}; | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +### 添加路由 | ||
| 61 | + | ||
| 62 | +```javascript | ||
| 63 | +// src/router/routes/modules/mypage/index.js | ||
| 64 | +export default { | ||
| 65 | + path: '/mypage', | ||
| 66 | + name: 'MyPage', | ||
| 67 | + component: () => import('@/views/mypage/index.vue'), | ||
| 68 | + meta: { | ||
| 69 | + title: '我的页面', | ||
| 70 | + }, | ||
| 71 | +}; | ||
| 72 | +``` | ||
| 73 | + | ||
| 74 | +## 2. 添加新组件 | ||
| 75 | + | ||
| 76 | +### 步骤 | ||
| 77 | + | ||
| 78 | +```bash | ||
| 79 | +# 1. 创建组件目录 | ||
| 80 | +mkdir src/components/MyComponent | ||
| 81 | + | ||
| 82 | +# 2. 创建组件文件 | ||
| 83 | +touch src/components/MyComponent/index.vue | ||
| 84 | +``` | ||
| 85 | + | ||
| 86 | +### 组件文件 | ||
| 87 | + | ||
| 88 | +```vue | ||
| 89 | +<!-- src/components/MyComponent/index.vue --> | ||
| 90 | +<template> | ||
| 91 | + <div class="my-component"> | ||
| 92 | + <h2>{{ title }}</h2> | ||
| 93 | + <slot></slot> | ||
| 94 | + </div> | ||
| 95 | +</template> | ||
| 96 | + | ||
| 97 | +<script setup> | ||
| 98 | +/** | ||
| 99 | + * 我的组件 | ||
| 100 | + * | ||
| 101 | + * @description 这是一个示例组件 | ||
| 102 | + * @component MyComponent | ||
| 103 | + */ | ||
| 104 | +const props = defineProps({ | ||
| 105 | + /** 标题 */ | ||
| 106 | + title: { | ||
| 107 | + type: String, | ||
| 108 | + default: '', | ||
| 109 | + }, | ||
| 110 | +}); | ||
| 111 | + | ||
| 112 | +const emit = defineEmits({ | ||
| 113 | + /** 点击事件 */ | ||
| 114 | + click: (event) => true, | ||
| 115 | +}); | ||
| 116 | +</script> | ||
| 117 | + | ||
| 118 | +<style lang="less" scoped> | ||
| 119 | +.my-component { | ||
| 120 | + h2 { | ||
| 121 | + font-size: 20px; | ||
| 122 | + } | ||
| 123 | +} | ||
| 124 | +</style> | ||
| 125 | +``` | ||
| 126 | + | ||
| 127 | +### 使用组件 | ||
| 128 | + | ||
| 129 | +```vue | ||
| 130 | +<template> | ||
| 131 | + <div> | ||
| 132 | + <MyComponent | ||
| 133 | + title="组件标题" | ||
| 134 | + @click="handleClick" | ||
| 135 | + > | ||
| 136 | + 组件内容 | ||
| 137 | + </MyComponent> | ||
| 138 | + </div> | ||
| 139 | +</template> | ||
| 140 | + | ||
| 141 | +<script setup> | ||
| 142 | +import MyComponent from '@components/MyComponent/index.vue'; | ||
| 143 | + | ||
| 144 | +const handleClick = (event) => { | ||
| 145 | + console.log('组件被点击:', event); | ||
| 146 | +}; | ||
| 147 | +</script> | ||
| 148 | +``` | ||
| 149 | + | ||
| 150 | +## 3. 添加 API 接口 | ||
| 151 | + | ||
| 152 | +### 步骤 | ||
| 153 | + | ||
| 154 | +```bash | ||
| 155 | +# 1. 创建 API 文件 | ||
| 156 | +touch src/api/myfeature.js | ||
| 157 | +``` | ||
| 158 | + | ||
| 159 | +### API 文件 | ||
| 160 | + | ||
| 161 | +```javascript | ||
| 162 | +/** | ||
| 163 | + * 我的特性 API | ||
| 164 | + * | ||
| 165 | + * @description 我的特性相关接口 | ||
| 166 | + */ | ||
| 167 | +import { fn, fetch } from '@/api/fn'; | ||
| 168 | + | ||
| 169 | +const Api = { | ||
| 170 | + FEATURE: '/srv/?a=myfeature', | ||
| 171 | +}; | ||
| 172 | + | ||
| 173 | +/** | ||
| 174 | + * 获取特性数据 | ||
| 175 | + * | ||
| 176 | + * @description 获取特性数据 | ||
| 177 | + * @param {Object} params - 请求参数 | ||
| 178 | + * @param {number} params.id - ID | ||
| 179 | + * @returns {Promise<Object>} 响应数据 | ||
| 180 | + */ | ||
| 181 | +export const featureAPI = (params) => fn(fetch.get(Api.FEATURE, params)); | ||
| 182 | + | ||
| 183 | +/** | ||
| 184 | + * 提交特性数据 | ||
| 185 | + * | ||
| 186 | + * @description 提交特性数据 | ||
| 187 | + * @param {Object} params - 请求参数 | ||
| 188 | + * @param {string} params.name - 名称 | ||
| 189 | + * @returns {Promise<Object>} 响应数据 | ||
| 190 | + */ | ||
| 191 | +export const submitFeatureAPI = (params) => fn(fetch.post(Api.FEATURE, params)); | ||
| 192 | +``` | ||
| 193 | + | ||
| 194 | +### 使用 API | ||
| 195 | + | ||
| 196 | +```vue | ||
| 197 | +<script setup> | ||
| 198 | +import { featureAPI, submitFeatureAPI } from '@/api/myfeature'; | ||
| 199 | +import { onMounted } from 'vue'; | ||
| 200 | + | ||
| 201 | +const fetchData = async () => { | ||
| 202 | + try { | ||
| 203 | + const { data } = await featureAPI({ id: 123 }); | ||
| 204 | + | ||
| 205 | + if (data) { | ||
| 206 | + console.log('获取数据成功:', data); | ||
| 207 | + } | ||
| 208 | + } catch (err) { | ||
| 209 | + console.error('获取数据失败:', err); | ||
| 210 | + } | ||
| 211 | +}; | ||
| 212 | + | ||
| 213 | +const submitData = async () => { | ||
| 214 | + try { | ||
| 215 | + const { data } = await submitFeatureAPI({ name: '名称' }); | ||
| 216 | + | ||
| 217 | + if (data) { | ||
| 218 | + console.log('提交成功:', data); | ||
| 219 | + } | ||
| 220 | + } catch (err) { | ||
| 221 | + console.error('提交失败:', err); | ||
| 222 | + } | ||
| 223 | +}; | ||
| 224 | + | ||
| 225 | +onMounted(() => { | ||
| 226 | + fetchData(); | ||
| 227 | +}); | ||
| 228 | +</script> | ||
| 229 | +``` | ||
| 230 | + | ||
| 231 | +## 4. 添加 Store | ||
| 232 | + | ||
| 233 | +### 步骤 | ||
| 234 | + | ||
| 235 | +```bash | ||
| 236 | +# 1. 创建 Store 文件 | ||
| 237 | +touch src/store/myfeature.js | ||
| 238 | +``` | ||
| 239 | + | ||
| 240 | +### Store 文件 | ||
| 241 | + | ||
| 242 | +```javascript | ||
| 243 | +/** | ||
| 244 | + * 我的特性 Store | ||
| 245 | + * | ||
| 246 | + * @description 管理特性相关状态 | ||
| 247 | + */ | ||
| 248 | +import { defineStore } from 'pinia'; | ||
| 249 | + | ||
| 250 | +export const useMyFeatureStore = defineStore('myfeature', { | ||
| 251 | + state: () => ({ | ||
| 252 | + data: null, | ||
| 253 | + loading: false, | ||
| 254 | + error: null, | ||
| 255 | + }), | ||
| 256 | + | ||
| 257 | + getters: { | ||
| 258 | + /** | ||
| 259 | + * 是否有数据 | ||
| 260 | + */ | ||
| 261 | + hasData: (state) => state.data !== null, | ||
| 262 | + | ||
| 263 | + /** | ||
| 264 | + * 数据数量 | ||
| 265 | + */ | ||
| 266 | + dataCount: (state) => state.data?.length || 0, | ||
| 267 | + }, | ||
| 268 | + | ||
| 269 | + actions: { | ||
| 270 | + /** | ||
| 271 | + * 获取数据 | ||
| 272 | + * | ||
| 273 | + * @param {number} id - ID | ||
| 274 | + */ | ||
| 275 | + async fetchData(id) { | ||
| 276 | + this.loading = true; | ||
| 277 | + this.error = null; | ||
| 278 | + | ||
| 279 | + try { | ||
| 280 | + const { data } = await featureAPI({ id }); | ||
| 281 | + this.data = data; | ||
| 282 | + } catch (err) { | ||
| 283 | + this.error = err.message; | ||
| 284 | + } finally { | ||
| 285 | + this.loading = false; | ||
| 286 | + } | ||
| 287 | + }, | ||
| 288 | + | ||
| 289 | + /** | ||
| 290 | + * 清空数据 | ||
| 291 | + */ | ||
| 292 | + clearData() { | ||
| 293 | + this.data = null; | ||
| 294 | + this.error = null; | ||
| 295 | + }, | ||
| 296 | + }, | ||
| 297 | +}); | ||
| 298 | +``` | ||
| 299 | + | ||
| 300 | +### 使用 Store | ||
| 301 | + | ||
| 302 | +```vue | ||
| 303 | +<script setup> | ||
| 304 | +import { useMyFeatureStore } from '@/store/myfeature'; | ||
| 305 | +import { onMounted } from 'vue'; | ||
| 306 | + | ||
| 307 | +const featureStore = useMyFeatureStore(); | ||
| 308 | + | ||
| 309 | +onMounted(() => { | ||
| 310 | + featureStore.fetchData(123); | ||
| 311 | +}); | ||
| 312 | +</script> | ||
| 313 | + | ||
| 314 | +<template> | ||
| 315 | + <div v-if="featureStore.loading">加载中...</div> | ||
| 316 | + <div v-else-if="featureStore.error">{{ featureStore.error }}</div> | ||
| 317 | + <div v-else> | ||
| 318 | + <p>数据数量: {{ featureStore.dataCount }}</p> | ||
| 319 | + <p>有数据: {{ featureStore.hasData }}</p> | ||
| 320 | + </div> | ||
| 321 | +</template> | ||
| 322 | +``` | ||
| 323 | + | ||
| 324 | +## 5. 路由跳转 | ||
| 325 | + | ||
| 326 | +### 方式 1: 路径跳转 | ||
| 327 | + | ||
| 328 | +```javascript | ||
| 329 | +import { useRouter } from 'vue-router'; | ||
| 330 | + | ||
| 331 | +const router = useRouter(); | ||
| 332 | + | ||
| 333 | +// 跳转(Hash 模式需要包含前缀) | ||
| 334 | +router.push('/index.html/mypage'); | ||
| 335 | +``` | ||
| 336 | + | ||
| 337 | +### 方式 2: 命名路由 | ||
| 338 | + | ||
| 339 | +```javascript | ||
| 340 | +import { useRouter } from 'vue-router'; | ||
| 341 | + | ||
| 342 | +const router = useRouter(); | ||
| 343 | + | ||
| 344 | +// 跳转(推荐) | ||
| 345 | +router.push({ name: 'MyPage' }); | ||
| 346 | +``` | ||
| 347 | + | ||
| 348 | +### 方式 3: 带参数跳转 | ||
| 349 | + | ||
| 350 | +```javascript | ||
| 351 | +import { useRouter } from 'vue-router'; | ||
| 352 | + | ||
| 353 | +const router = useRouter(); | ||
| 354 | + | ||
| 355 | +// 带查询参数 | ||
| 356 | +router.push({ | ||
| 357 | + name: 'MyPage', | ||
| 358 | + query: { id: 123, name: 'test' } | ||
| 359 | +}); | ||
| 360 | + | ||
| 361 | +// 带路径参数 | ||
| 362 | +router.push({ | ||
| 363 | + name: 'MyPageDetail', | ||
| 364 | + params: { id: 123 } | ||
| 365 | +}); | ||
| 366 | +``` | ||
| 367 | + | ||
| 368 | +### 方式 4: 返回 | ||
| 369 | + | ||
| 370 | +```javascript | ||
| 371 | +import { useRouter } from 'vue-router'; | ||
| 372 | + | ||
| 373 | +const router = useRouter(); | ||
| 374 | + | ||
| 375 | +// 返回上一页 | ||
| 376 | +router.back(); | ||
| 377 | + | ||
| 378 | +// 或 | ||
| 379 | +router.go(-1); | ||
| 380 | +``` | ||
| 381 | + | ||
| 382 | +## 6. 获取路由参数 | ||
| 383 | + | ||
| 384 | +### 查询参数 | ||
| 385 | + | ||
| 386 | +```javascript | ||
| 387 | +import { useRoute } from 'vue-router'; | ||
| 388 | + | ||
| 389 | +const route = useRoute(); | ||
| 390 | + | ||
| 391 | +// 获取查询参数 | ||
| 392 | +const id = route.query.id; | ||
| 393 | +const name = route.query.name; | ||
| 394 | +``` | ||
| 395 | + | ||
| 396 | +### 路径参数 | ||
| 397 | + | ||
| 398 | +```javascript | ||
| 399 | +import { useRoute } from 'vue-router'; | ||
| 400 | + | ||
| 401 | +const route = useRoute(); | ||
| 402 | + | ||
| 403 | +// 获取路径参数 | ||
| 404 | +const id = route.params.id; | ||
| 405 | +``` | ||
| 406 | + | ||
| 407 | +## 7. 使用 Vant 组件 | ||
| 408 | + | ||
| 409 | +### 按钮 | ||
| 410 | + | ||
| 411 | +```vue | ||
| 412 | +<template> | ||
| 413 | + <van-button type="primary">主要按钮</van-button> | ||
| 414 | + <van-button type="success">成功按钮</van-button> | ||
| 415 | + <van-button type="warning">警告按钮</van-button> | ||
| 416 | + <van-button type="danger">危险按钮</van-button> | ||
| 417 | + <van-button plain>朴素按钮</van-button> | ||
| 418 | + <van-button round>圆形按钮</van-button> | ||
| 419 | +</template> | ||
| 420 | +``` | ||
| 421 | + | ||
| 422 | +### 表单 | ||
| 423 | + | ||
| 424 | +```vue | ||
| 425 | +<template> | ||
| 426 | + <van-form @submit="onSubmit"> | ||
| 427 | + <van-cell-group inset> | ||
| 428 | + <van-field | ||
| 429 | + v-model="username" | ||
| 430 | + name="username" | ||
| 431 | + label="用户名" | ||
| 432 | + placeholder="用户名" | ||
| 433 | + :rules="[{ required: true, message: '请填写用户名' }]" | ||
| 434 | + /> | ||
| 435 | + <van-field | ||
| 436 | + v-model="password" | ||
| 437 | + type="password" | ||
| 438 | + name="password" | ||
| 439 | + label="密码" | ||
| 440 | + placeholder="密码" | ||
| 441 | + :rules="[{ required: true, message: '请填写密码' }]" | ||
| 442 | + /> | ||
| 443 | + </van-cell-group> | ||
| 444 | + <div style="margin: 16px;"> | ||
| 445 | + <van-button round block type="primary" native-type="submit"> | ||
| 446 | + 提交 | ||
| 447 | + </van-button> | ||
| 448 | + </div> | ||
| 449 | + </van-form> | ||
| 450 | +</template> | ||
| 451 | + | ||
| 452 | +<script setup> | ||
| 453 | +import { ref } from 'vue'; | ||
| 454 | + | ||
| 455 | +const username = ref(''); | ||
| 456 | +const password = ref(''); | ||
| 457 | + | ||
| 458 | +const onSubmit = (values) => { | ||
| 459 | + console.log('表单数据:', values); | ||
| 460 | +}; | ||
| 461 | +</script> | ||
| 462 | +``` | ||
| 463 | + | ||
| 464 | +### 弹窗 | ||
| 465 | + | ||
| 466 | +```vue | ||
| 467 | +<template> | ||
| 468 | + <van-button type="primary" @click="showDialog">显示弹窗</van-button> | ||
| 469 | +</template> | ||
| 470 | + | ||
| 471 | +<script setup> | ||
| 472 | +import { showDialog } from 'vant'; | ||
| 473 | + | ||
| 474 | +const showDialog = () => { | ||
| 475 | + showDialog({ | ||
| 476 | + title: '标题', | ||
| 477 | + message: '这是弹窗内容', | ||
| 478 | + }).then((action) => { | ||
| 479 | + console.log('确认', action); | ||
| 480 | + }).catch(() => { | ||
| 481 | + console.log('取消'); | ||
| 482 | + }); | ||
| 483 | +}; | ||
| 484 | +</script> | ||
| 485 | +``` | ||
| 486 | + | ||
| 487 | +## 8. 使用 Element Plus 组件 | ||
| 488 | + | ||
| 489 | +### 按钮 | ||
| 490 | + | ||
| 491 | +```vue | ||
| 492 | +<template> | ||
| 493 | + <el-button>默认按钮</el-button> | ||
| 494 | + <el-button type="primary">主要按钮</el-button> | ||
| 495 | + <el-button type="success">成功按钮</el-button> | ||
| 496 | + <el-button type="warning">警告按钮</el-button> | ||
| 497 | + <el-button type="danger">危险按钮</el-button> | ||
| 498 | + <el-button round>圆形按钮</el-button> | ||
| 499 | +</template> | ||
| 500 | +``` | ||
| 501 | + | ||
| 502 | +### 表格 | ||
| 503 | + | ||
| 504 | +```vue | ||
| 505 | +<template> | ||
| 506 | + <el-table :data="tableData" style="width: 100%"> | ||
| 507 | + <el-table-column prop="name" label="姓名" width="180" /> | ||
| 508 | + <el-table-column prop="age" label="年龄" width="180" /> | ||
| 509 | + <el-table-column prop="address" label="地址" /> | ||
| 510 | + </el-table> | ||
| 511 | +</template> | ||
| 512 | + | ||
| 513 | +<script setup> | ||
| 514 | +import { ref } from 'vue'; | ||
| 515 | + | ||
| 516 | +const tableData = ref([ | ||
| 517 | + { | ||
| 518 | + name: '张三', | ||
| 519 | + age: 30, | ||
| 520 | + address: '北京市', | ||
| 521 | + }, | ||
| 522 | + { | ||
| 523 | + name: '李四', | ||
| 524 | + age: 25, | ||
| 525 | + address: '上海市', | ||
| 526 | + }, | ||
| 527 | +]); | ||
| 528 | +</script> | ||
| 529 | +``` | ||
| 530 | + | ||
| 531 | +## 9. 添加 Keep-Alive 缓存 | ||
| 532 | + | ||
| 533 | +### 步骤 | ||
| 534 | + | ||
| 535 | +```javascript | ||
| 536 | +// 在 store 中添加页面 | ||
| 537 | +import { useMainStore } from '@/store'; | ||
| 538 | + | ||
| 539 | +const mainStore = useMainStore(); | ||
| 540 | + | ||
| 541 | +// 添加需要缓存的页面 | ||
| 542 | +const keepThisPage = () => { | ||
| 543 | + const keepPages = [...mainStore.keepPages]; | ||
| 544 | + | ||
| 545 | + if (!keepPages.includes('PageName')) { | ||
| 546 | + keepPages.push('PageName'); | ||
| 547 | + } | ||
| 548 | + | ||
| 549 | + mainStore.keepPages = keepPages; | ||
| 550 | +}; | ||
| 551 | + | ||
| 552 | +// 移除缓存 | ||
| 553 | +const removeKeepPage = () => { | ||
| 554 | + const keepPages = mainStore.keepPages.filter(page => page !== 'PageName'); | ||
| 555 | + | ||
| 556 | + mainStore.keepPages = keepPages.length ? keepPages : ['default']; | ||
| 557 | +}; | ||
| 558 | +``` | ||
| 559 | + | ||
| 560 | +### 使用 | ||
| 561 | + | ||
| 562 | +```vue | ||
| 563 | +<script setup> | ||
| 564 | +import { onMounted } from 'vue'; | ||
| 565 | +import { useMainStore } from '@/store'; | ||
| 566 | + | ||
| 567 | +const mainStore = useMainStore(); | ||
| 568 | + | ||
| 569 | +onMounted(() => { | ||
| 570 | + // 添加到缓存 | ||
| 571 | + mainStore.keepThisPage(); | ||
| 572 | +}); | ||
| 573 | +</script> | ||
| 574 | +``` | ||
| 575 | + | ||
| 576 | +## 10. 处理微信分享 | ||
| 577 | + | ||
| 578 | +### 步骤 | ||
| 579 | + | ||
| 580 | +```javascript | ||
| 581 | +// 引入微信分享工具 | ||
| 582 | +import { share } from '@/utils/share'; | ||
| 583 | + | ||
| 584 | +// 设置分享 | ||
| 585 | +const setShare = () => { | ||
| 586 | + share({ | ||
| 587 | + title: '分享标题', | ||
| 588 | + desc: '分享描述', | ||
| 589 | + link: window.location.href, | ||
| 590 | + imgUrl: 'https://example.com/share.png', | ||
| 591 | + }); | ||
| 592 | +}; | ||
| 593 | + | ||
| 594 | +// 在页面加载时设置 | ||
| 595 | +onMounted(() => { | ||
| 596 | + setShare(); | ||
| 597 | +}); | ||
| 598 | +``` | ||
| 599 | + | ||
| 600 | +## 11. 添加音频播放 | ||
| 601 | + | ||
| 602 | +### 单模式 | ||
| 603 | + | ||
| 604 | +```vue | ||
| 605 | +<template> | ||
| 606 | + <audioBackground | ||
| 607 | + :audio-url="audioUrl" | ||
| 608 | + :autoplay="false" | ||
| 609 | + @play="handlePlay" | ||
| 610 | + @pause="handlePause" | ||
| 611 | + /> | ||
| 612 | +</template> | ||
| 613 | + | ||
| 614 | +<script setup> | ||
| 615 | +import audioBackground from '@components/audioBackground.vue'; | ||
| 616 | +import { ref } from 'vue'; | ||
| 617 | + | ||
| 618 | +const audioUrl = ref('/audio/guide.mp3'); | ||
| 619 | + | ||
| 620 | +const handlePlay = () => { | ||
| 621 | + console.log('开始播放'); | ||
| 622 | +}; | ||
| 623 | + | ||
| 624 | +const handlePause = () => { | ||
| 625 | + console.log('暂停播放'); | ||
| 626 | +}; | ||
| 627 | +</script> | ||
| 628 | +``` | ||
| 629 | + | ||
| 630 | +### 播放列表模式 | ||
| 631 | + | ||
| 632 | +```vue | ||
| 633 | +<template> | ||
| 634 | + <audioList | ||
| 635 | + v-model:playlist="playlist" | ||
| 636 | + :current-index="currentIndex" | ||
| 637 | + /> | ||
| 638 | +</template> | ||
| 639 | + | ||
| 640 | +<script setup> | ||
| 641 | +import audioList from '@components/audioList.vue'; | ||
| 642 | +import { ref } from 'vue'; | ||
| 643 | + | ||
| 644 | +const playlist = ref([ | ||
| 645 | + { | ||
| 646 | + id: 1, | ||
| 647 | + title: '音频 1', | ||
| 648 | + url: '/audio/1.mp3', | ||
| 649 | + }, | ||
| 650 | + { | ||
| 651 | + id: 2, | ||
| 652 | + title: '音频 2', | ||
| 653 | + url: '/audio/2.mp3', | ||
| 654 | + }, | ||
| 655 | +]); | ||
| 656 | + | ||
| 657 | +const currentIndex = ref(0); | ||
| 658 | +</script> | ||
| 659 | +``` | ||
| 660 | + | ||
| 661 | +## 12. 使用高德地图 | ||
| 662 | + | ||
| 663 | +### 初始化地图 | ||
| 664 | + | ||
| 665 | +```javascript | ||
| 666 | +import AMapLoader from '@amap/amap-jsapi-loader'; | ||
| 667 | + | ||
| 668 | +const initMap = async () => { | ||
| 669 | + try { | ||
| 670 | + const AMap = await AMapLoader.load({ | ||
| 671 | + key: 'YOUR_AMAP_KEY', | ||
| 672 | + version: '2.0', | ||
| 673 | + plugins: ['AMap.Scale', 'AMap.ToolBar'], | ||
| 674 | + }); | ||
| 675 | + | ||
| 676 | + const map = new AMap.Map('container', { | ||
| 677 | + zoom: 11, | ||
| 678 | + center: [116.397428, 39.90923], | ||
| 679 | + }); | ||
| 680 | + | ||
| 681 | + return map; | ||
| 682 | + } catch (err) { | ||
| 683 | + console.error('地图加载失败:', err); | ||
| 684 | + } | ||
| 685 | +}; | ||
| 686 | +``` | ||
| 687 | + | ||
| 688 | +### 添加标记 | ||
| 689 | + | ||
| 690 | +```javascript | ||
| 691 | +const addMarker = (map, position) => { | ||
| 692 | + const marker = new AMap.Marker({ | ||
| 693 | + position: position, | ||
| 694 | + map: map, | ||
| 695 | + }); | ||
| 696 | + | ||
| 697 | + return marker; | ||
| 698 | +}; | ||
| 699 | +``` | ||
| 700 | + | ||
| 701 | +## 13. 错误处理 | ||
| 702 | + | ||
| 703 | +### 统一错误处理 | ||
| 704 | + | ||
| 705 | +```javascript | ||
| 706 | +const handleError = (err) => { | ||
| 707 | + console.error('操作失败:', err); | ||
| 708 | + | ||
| 709 | + // 显示错误提示 | ||
| 710 | + showToast(err.message || '操作失败,请重试'); | ||
| 711 | + | ||
| 712 | + // 上报错误 | ||
| 713 | + // reportError(err); | ||
| 714 | +}; | ||
| 715 | + | ||
| 716 | +// 使用 | ||
| 717 | +try { | ||
| 718 | + await someAPI(); | ||
| 719 | +} catch (err) { | ||
| 720 | + handleError(err); | ||
| 721 | +} | ||
| 722 | +``` | ||
| 723 | + | ||
| 724 | +### Toast 提示 | ||
| 725 | + | ||
| 726 | +```javascript | ||
| 727 | +import { showToast } from 'vant'; | ||
| 728 | + | ||
| 729 | +// 成功提示 | ||
| 730 | +showToast('操作成功'); | ||
| 731 | + | ||
| 732 | +// 错误提示 | ||
| 733 | +showToast({ | ||
| 734 | + type: 'fail', | ||
| 735 | + message: '操作失败', | ||
| 736 | +}); | ||
| 737 | +``` | ||
| 738 | + | ||
| 739 | +## 14. 加载状态 | ||
| 740 | + | ||
| 741 | +### 使用 Loading | ||
| 742 | + | ||
| 743 | +```vue | ||
| 744 | +<template> | ||
| 745 | + <div v-if="loading" class="loading">加载中...</div> | ||
| 746 | + <div v-else> | ||
| 747 | + <!-- 内容 --> | ||
| 748 | + </div> | ||
| 749 | +</template> | ||
| 750 | + | ||
| 751 | +<script setup> | ||
| 752 | +import { ref } from 'vue'; | ||
| 753 | + | ||
| 754 | +const loading = ref(false); | ||
| 755 | + | ||
| 756 | +const fetchData = async () => { | ||
| 757 | + loading.value = true; | ||
| 758 | + | ||
| 759 | + try { | ||
| 760 | + const { data } = await someAPI(); | ||
| 761 | + // 处理数据 | ||
| 762 | + } catch (err) { | ||
| 763 | + console.error(err); | ||
| 764 | + } finally { | ||
| 765 | + loading.value = false; | ||
| 766 | + } | ||
| 767 | +}; | ||
| 768 | +</script> | ||
| 769 | +``` | ||
| 770 | + | ||
| 771 | +### 使用 Vant Loading | ||
| 772 | + | ||
| 773 | +```vue | ||
| 774 | +<template> | ||
| 775 | + <van-button @click="showLoading">显示加载</van-button> | ||
| 776 | +</template> | ||
| 777 | + | ||
| 778 | +<script setup> | ||
| 779 | +import { showLoadingToast, closeToast } from 'vant'; | ||
| 780 | + | ||
| 781 | +const showLoading = () => { | ||
| 782 | + showLoadingToast({ | ||
| 783 | + message: '加载中...', | ||
| 784 | + forbidClick: true, | ||
| 785 | + duration: 0, | ||
| 786 | + }); | ||
| 787 | + | ||
| 788 | + // 模拟异步操作 | ||
| 789 | + setTimeout(() => { | ||
| 790 | + closeToast(); | ||
| 791 | + }, 2000); | ||
| 792 | +}; | ||
| 793 | +</script> | ||
| 794 | +``` | ||
| 795 | + | ||
| 796 | +## 15. 图片处理 | ||
| 797 | + | ||
| 798 | +### 图片懒加载 | ||
| 799 | + | ||
| 800 | +```vue | ||
| 801 | +<template> | ||
| 802 | + <van-image | ||
| 803 | + :src="imageUrl" | ||
| 804 | + lazy-load | ||
| 805 | + :show-error="true" | ||
| 806 | + :show-loading="true" | ||
| 807 | + > | ||
| 808 | + <template #error> | ||
| 809 | + <div class="error">加载失败</div> | ||
| 810 | + </template> | ||
| 811 | + </van-image> | ||
| 812 | +</template> | ||
| 813 | + | ||
| 814 | +<script setup> | ||
| 815 | +import { ref } from 'vue'; | ||
| 816 | + | ||
| 817 | +const imageUrl = ref('/images/example.jpg'); | ||
| 818 | +</script> | ||
| 819 | +``` | ||
| 820 | + | ||
| 821 | +### 图片预览 | ||
| 822 | + | ||
| 823 | +```vue | ||
| 824 | +<template> | ||
| 825 | + <van-image | ||
| 826 | + :src="imageUrl" | ||
| 827 | + @click="previewImage" | ||
| 828 | + /> | ||
| 829 | +</template> | ||
| 830 | + | ||
| 831 | +<script setup> | ||
| 832 | +import { ref } from 'vue'; | ||
| 833 | +import { showImagePreview } from 'vant'; | ||
| 834 | + | ||
| 835 | +const imageUrl = ref('/images/example.jpg'); | ||
| 836 | +const images = ref([ | ||
| 837 | + '/images/1.jpg', | ||
| 838 | + '/images/2.jpg', | ||
| 839 | + '/images/3.jpg', | ||
| 840 | +]); | ||
| 841 | + | ||
| 842 | +const previewImage = () => { | ||
| 843 | + showImagePreview({ | ||
| 844 | + images: images.value, | ||
| 845 | + startPosition: 0, | ||
| 846 | + }); | ||
| 847 | +}; | ||
| 848 | +</script> | ||
| 849 | +``` | ||
| 850 | + | ||
| 851 | +## 参考文档 | ||
| 852 | + | ||
| 853 | +- [新手入门指南](./新手入门指南.md) | ||
| 854 | +- [已知问题汇总](../注意事项与陷阱/已知问题汇总.md) | ||
| 855 | +- [地图集成分析](../功能模块分析/地图集成分析.md) | ||
| 856 | +- [音频系统分析](../功能模块分析/音频系统分析.md) |
docs/开发指南/新手入门指南.md
0 → 100644
| 1 | +# 新手入门指南 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**目的**: 帮助新开发者快速上手项目 | ||
| 5 | + | ||
| 6 | +## 环境准备 | ||
| 7 | + | ||
| 8 | +### 1. 安装 Node.js | ||
| 9 | + | ||
| 10 | +**要求**: Node.js 18.13.x | ||
| 11 | + | ||
| 12 | +```bash | ||
| 13 | +# 检查 Node.js 版本 | ||
| 14 | +node -v | ||
| 15 | + | ||
| 16 | +# 如果版本不符合,安装 nvm | ||
| 17 | +# macOS/Linux | ||
| 18 | +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash | ||
| 19 | + | ||
| 20 | +# Windows | ||
| 21 | +# 下载安装程序: https://github.com/coreybutler/nvm-windows/releases | ||
| 22 | + | ||
| 23 | +# 安装 Node.js 18.13.x | ||
| 24 | +nvm install 18.13.0 | ||
| 25 | +nvm use 18.13.0 | ||
| 26 | +``` | ||
| 27 | + | ||
| 28 | +### 2. 克隆项目 | ||
| 29 | + | ||
| 30 | +```bash | ||
| 31 | +# 克隆项目 | ||
| 32 | +git clone <repository-url> | ||
| 33 | +cd map-demo | ||
| 34 | + | ||
| 35 | +# 安装依赖 | ||
| 36 | +npm install | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +### 3. 启动开发服务器 | ||
| 40 | + | ||
| 41 | +```bash | ||
| 42 | +# 启动开发服务器(localhost) | ||
| 43 | +npm run dev | ||
| 44 | + | ||
| 45 | +# 启动开发服务器(网络访问) | ||
| 46 | +npm run start | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +访问: `http://localhost:8006` | ||
| 50 | + | ||
| 51 | +## 项目结构快速了解 | ||
| 52 | + | ||
| 53 | +### 核心目录 | ||
| 54 | + | ||
| 55 | +``` | ||
| 56 | +src/ | ||
| 57 | +├── api/ # API 接口 | ||
| 58 | +├── components/ # 公共组件 | ||
| 59 | +├── views/ # 页面组件 | ||
| 60 | +├── store/ # 状态管理 | ||
| 61 | +├── router/ # 路由配置 | ||
| 62 | +├── utils/ # 工具函数 | ||
| 63 | +└── common/ # 通用代码 | ||
| 64 | +``` | ||
| 65 | + | ||
| 66 | +### 关键文件 | ||
| 67 | + | ||
| 68 | +- `src/main.js` - 入口文件 | ||
| 69 | +- `src/App.vue` - 根组件 | ||
| 70 | +- `vite.config.js` - Vite 配置 | ||
| 71 | +- `package.json` - 项目依赖 | ||
| 72 | + | ||
| 73 | +## 开发工作流 | ||
| 74 | + | ||
| 75 | +### 1. 创建新页面 | ||
| 76 | + | ||
| 77 | +#### 步骤 1: 创建页面文件 | ||
| 78 | + | ||
| 79 | +```bash | ||
| 80 | +# 在 src/views/ 下创建页面目录 | ||
| 81 | +mkdir src/views/mypage | ||
| 82 | + | ||
| 83 | +# 创建页面文件 | ||
| 84 | +touch src/views/mypage/index.vue | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +#### 步骤 2: 编写页面组件 | ||
| 88 | + | ||
| 89 | +```vue | ||
| 90 | +<!-- src/views/mypage/index.vue --> | ||
| 91 | +<template> | ||
| 92 | + <div class="mypage"> | ||
| 93 | + <h1>{{ title }}</h1> | ||
| 94 | + <p>{{ content }}</p> | ||
| 95 | + </div> | ||
| 96 | +</template> | ||
| 97 | + | ||
| 98 | +<script setup> | ||
| 99 | +import { ref } from 'vue'; | ||
| 100 | + | ||
| 101 | +const title = ref('我的页面'); | ||
| 102 | +const content = ref('页面内容'); | ||
| 103 | +</script> | ||
| 104 | + | ||
| 105 | +<style lang="less" scoped> | ||
| 106 | +.mypage { | ||
| 107 | + padding: 20px; | ||
| 108 | + | ||
| 109 | + h1 { | ||
| 110 | + font-size: 24px; | ||
| 111 | + color: #333; | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + p { | ||
| 115 | + font-size: 16px; | ||
| 116 | + color: #666; | ||
| 117 | + } | ||
| 118 | +} | ||
| 119 | +</style> | ||
| 120 | +``` | ||
| 121 | + | ||
| 122 | +#### 步骤 3: 添加路由 | ||
| 123 | + | ||
| 124 | +```javascript | ||
| 125 | +// src/router/routes/modules/mypage/index.js(创建新文件) | ||
| 126 | +export default { | ||
| 127 | + path: '/mypage', | ||
| 128 | + name: 'MyPage', | ||
| 129 | + component: () => import('@/views/mypage/index.vue'), | ||
| 130 | + meta: { | ||
| 131 | + title: '我的页面', | ||
| 132 | + }, | ||
| 133 | +}; | ||
| 134 | +``` | ||
| 135 | + | ||
| 136 | +#### 步骤 4: 访问页面 | ||
| 137 | + | ||
| 138 | +``` | ||
| 139 | +URL: http://localhost:8006/index.html#/index.html/mypage | ||
| 140 | +``` | ||
| 141 | + | ||
| 142 | +### 2. 创建新组件 | ||
| 143 | + | ||
| 144 | +#### 步骤 1: 创建组件文件 | ||
| 145 | + | ||
| 146 | +```bash | ||
| 147 | +# 创建组件目录 | ||
| 148 | +mkdir src/components/MyComponent | ||
| 149 | + | ||
| 150 | +# 创建组件文件 | ||
| 151 | +touch src/components/MyComponent/index.vue | ||
| 152 | +``` | ||
| 153 | + | ||
| 154 | +#### 步骤 2: 编写组件 | ||
| 155 | + | ||
| 156 | +```vue | ||
| 157 | +<!-- src/components/MyComponent/index.vue --> | ||
| 158 | +<template> | ||
| 159 | + <div class="my-component"> | ||
| 160 | + <h2>{{ title }}</h2> | ||
| 161 | + <slot></slot> | ||
| 162 | + </div> | ||
| 163 | +</template> | ||
| 164 | + | ||
| 165 | +<script setup> | ||
| 166 | +/** | ||
| 167 | + * 我的组件 | ||
| 168 | + * | ||
| 169 | + * @description 这是一个示例组件 | ||
| 170 | + * @component MyComponent | ||
| 171 | + * @example | ||
| 172 | + * <MyComponent title="标题"> | ||
| 173 | + * 内容 | ||
| 174 | + * </MyComponent> | ||
| 175 | + */ | ||
| 176 | +const props = defineProps({ | ||
| 177 | + /** 标题 */ | ||
| 178 | + title: { | ||
| 179 | + type: String, | ||
| 180 | + default: '', | ||
| 181 | + }, | ||
| 182 | +}); | ||
| 183 | + | ||
| 184 | +const emit = defineEmits({ | ||
| 185 | + /** 点击事件 */ | ||
| 186 | + click: (payload) => true, | ||
| 187 | +}); | ||
| 188 | +</script> | ||
| 189 | + | ||
| 190 | +<style lang="less" scoped> | ||
| 191 | +.my-component { | ||
| 192 | + h2 { | ||
| 193 | + font-size: 20px; | ||
| 194 | + } | ||
| 195 | +} | ||
| 196 | +</style> | ||
| 197 | +``` | ||
| 198 | + | ||
| 199 | +#### 步骤 3: 使用组件 | ||
| 200 | + | ||
| 201 | +```vue | ||
| 202 | +<template> | ||
| 203 | + <div> | ||
| 204 | + <MyComponent title="组件标题"> | ||
| 205 | + 组件内容 | ||
| 206 | + </MyComponent> | ||
| 207 | + </div> | ||
| 208 | +</template> | ||
| 209 | + | ||
| 210 | +<script setup> | ||
| 211 | +import MyComponent from '@components/MyComponent/index.vue'; | ||
| 212 | +</script> | ||
| 213 | +``` | ||
| 214 | + | ||
| 215 | +### 3. 添加 API 接口 | ||
| 216 | + | ||
| 217 | +#### 步骤 1: 创建 API 文件 | ||
| 218 | + | ||
| 219 | +```bash | ||
| 220 | +touch src/api/mypage.js | ||
| 221 | +``` | ||
| 222 | + | ||
| 223 | +#### 步骤 2: 定义 API | ||
| 224 | + | ||
| 225 | +```javascript | ||
| 226 | +/** | ||
| 227 | + * 我的页面 API | ||
| 228 | + * | ||
| 229 | + * @description 我的页面相关接口 | ||
| 230 | + */ | ||
| 231 | +import { fn, fetch } from '@/api/fn'; | ||
| 232 | + | ||
| 233 | +const Api = { | ||
| 234 | + MYPAGE: '/srv/?a=mypage', | ||
| 235 | +}; | ||
| 236 | + | ||
| 237 | +/** | ||
| 238 | + * 获取我的页面数据 | ||
| 239 | + * | ||
| 240 | + * @param {Object} params - 请求参数 | ||
| 241 | + * @param {number} params.id - ID | ||
| 242 | + * @returns {Promise<Object>} 响应数据 | ||
| 243 | + */ | ||
| 244 | +export const myPageAPI = (params) => fn(fetch.get(Api.MYPAGE, params)); | ||
| 245 | +``` | ||
| 246 | + | ||
| 247 | +#### 步骤 3: 使用 API | ||
| 248 | + | ||
| 249 | +```javascript | ||
| 250 | +import { myPageAPI } from '@/api/mypage.js'; | ||
| 251 | + | ||
| 252 | +const { data } = await myPageAPI({ id: 123 }); | ||
| 253 | + | ||
| 254 | +if (data) { | ||
| 255 | + console.log('获取数据成功:', data); | ||
| 256 | +} | ||
| 257 | +``` | ||
| 258 | + | ||
| 259 | +### 4. 添加状态管理 | ||
| 260 | + | ||
| 261 | +#### 步骤 1: 创建 Store | ||
| 262 | + | ||
| 263 | +```javascript | ||
| 264 | +// src/store/mypage.js | ||
| 265 | +import { defineStore } from 'pinia'; | ||
| 266 | + | ||
| 267 | +export const useMyPageStore = defineStore('mypage', { | ||
| 268 | + state: () => ({ | ||
| 269 | + data: null, | ||
| 270 | + loading: false, | ||
| 271 | + error: null, | ||
| 272 | + }), | ||
| 273 | + | ||
| 274 | + getters: { | ||
| 275 | + hasData: (state) => state.data !== null, | ||
| 276 | + }, | ||
| 277 | + | ||
| 278 | + actions: { | ||
| 279 | + async fetchData(id) { | ||
| 280 | + this.loading = true; | ||
| 281 | + this.error = null; | ||
| 282 | + | ||
| 283 | + try { | ||
| 284 | + const { data } = await myPageAPI({ id }); | ||
| 285 | + this.data = data; | ||
| 286 | + } catch (err) { | ||
| 287 | + this.error = err.message; | ||
| 288 | + } finally { | ||
| 289 | + this.loading = false; | ||
| 290 | + } | ||
| 291 | + }, | ||
| 292 | + }, | ||
| 293 | +}); | ||
| 294 | +``` | ||
| 295 | + | ||
| 296 | +#### 步骤 2: 使用 Store | ||
| 297 | + | ||
| 298 | +```vue | ||
| 299 | +<script setup> | ||
| 300 | +import { useMyPageStore } from '@/store/mypage'; | ||
| 301 | +import { onMounted } from 'vue'; | ||
| 302 | + | ||
| 303 | +const myPageStore = useMyPageStore(); | ||
| 304 | + | ||
| 305 | +onMounted(() => { | ||
| 306 | + myPageStore.fetchData(123); | ||
| 307 | +}); | ||
| 308 | +</script> | ||
| 309 | + | ||
| 310 | +<template> | ||
| 311 | + <div v-if="myPageStore.loading">加载中...</div> | ||
| 312 | + <div v-else-if="myPageStore.error">{{ myPageStore.error }}</div> | ||
| 313 | + <div v-else>{{ myPageStore.data }}</div> | ||
| 314 | +</template> | ||
| 315 | +``` | ||
| 316 | + | ||
| 317 | +## 常见开发任务 | ||
| 318 | + | ||
| 319 | +### 修改样式 | ||
| 320 | + | ||
| 321 | +```vue | ||
| 322 | +<template> | ||
| 323 | + <div class="page">内容</div> | ||
| 324 | +</template> | ||
| 325 | + | ||
| 326 | +<style lang="less" scoped> | ||
| 327 | +.page { | ||
| 328 | + /* 使用 Less */ | ||
| 329 | + padding: 20px; | ||
| 330 | + | ||
| 331 | + /* 嵌套选择器 */ | ||
| 332 | + h1 { | ||
| 333 | + font-size: 24px; | ||
| 334 | + | ||
| 335 | + /* 伪元素 */ | ||
| 336 | + &:hover { | ||
| 337 | + color: blue; | ||
| 338 | + } | ||
| 339 | + } | ||
| 340 | +} | ||
| 341 | +</style> | ||
| 342 | +``` | ||
| 343 | + | ||
| 344 | +### 添加路由跳转 | ||
| 345 | + | ||
| 346 | +```javascript | ||
| 347 | +import { useRouter } from 'vue-router'; | ||
| 348 | + | ||
| 349 | +const router = useRouter(); | ||
| 350 | + | ||
| 351 | +// 方式 1: 路径跳转 | ||
| 352 | +router.push('/mypage'); | ||
| 353 | + | ||
| 354 | +// 方式 2: 命名路由 | ||
| 355 | +router.push({ name: 'MyPage' }); | ||
| 356 | + | ||
| 357 | +// 方式 3: 带参数跳转 | ||
| 358 | +router.push({ | ||
| 359 | + name: 'MyPage', | ||
| 360 | + query: { id: 123 } | ||
| 361 | +}); | ||
| 362 | + | ||
| 363 | +// 方式 4: 带参数跳转(Hash 模式) | ||
| 364 | +router.push('/index.html/mypage?id=123'); | ||
| 365 | +``` | ||
| 366 | + | ||
| 367 | +### 使用 Vant 组件 | ||
| 368 | + | ||
| 369 | +```vue | ||
| 370 | +<template> | ||
| 371 | + <!-- Vant 组件自动导入,无需手动 import --> | ||
| 372 | + <van-button type="primary">按钮</van-button> | ||
| 373 | + <van-cell-group> | ||
| 374 | + <van-cell title="单元格" value="内容" /> | ||
| 375 | + </van-cell-group> | ||
| 376 | + <van-image :src="image_url" /> | ||
| 377 | +</template> | ||
| 378 | +``` | ||
| 379 | + | ||
| 380 | +### 使用 Element Plus 组件 | ||
| 381 | + | ||
| 382 | +```vue | ||
| 383 | +<template> | ||
| 384 | + <!-- Element Plus 组件自动导入,无需手动 import --> | ||
| 385 | + <el-button type="primary">按钮</el-button> | ||
| 386 | + <el-table :data="tableData"> | ||
| 387 | + <el-table-column prop="name" label="姓名" /> | ||
| 388 | + </el-table> | ||
| 389 | +</template> | ||
| 390 | +``` | ||
| 391 | + | ||
| 392 | +### 处理响应式数据 | ||
| 393 | + | ||
| 394 | +```javascript | ||
| 395 | +import { ref, reactive, computed } from 'vue'; | ||
| 396 | + | ||
| 397 | +// ref:基本类型 | ||
| 398 | +const count = ref(0); | ||
| 399 | +const message = ref('Hello'); | ||
| 400 | + | ||
| 401 | +// reactive:对象类型 | ||
| 402 | +const user = reactive({ | ||
| 403 | + name: '', | ||
| 404 | + age: 0, | ||
| 405 | +}); | ||
| 406 | + | ||
| 407 | +// computed:计算属性 | ||
| 408 | +const fullName = computed(() => { | ||
| 409 | + return `${user.name} (${user.age}岁)`; | ||
| 410 | +}); | ||
| 411 | + | ||
| 412 | +// 修改数据 | ||
| 413 | +count.value++; // ref 需要 .value | ||
| 414 | +user.name = '张三'; // reactive 不需要 .value | ||
| 415 | +``` | ||
| 416 | + | ||
| 417 | +## 调试技巧 | ||
| 418 | + | ||
| 419 | +### 1. 使用 VConsole | ||
| 420 | + | ||
| 421 | +```javascript | ||
| 422 | +// 引入 VConsole | ||
| 423 | +import { initVConsole } from '@/utils/vconsole'; | ||
| 424 | + | ||
| 425 | +// 在开发环境启用 | ||
| 426 | +if (import.meta.env.DEV) { | ||
| 427 | + initVConsole(); | ||
| 428 | +} | ||
| 429 | +``` | ||
| 430 | + | ||
| 431 | +### 2. 查看状态 | ||
| 432 | + | ||
| 433 | +```javascript | ||
| 434 | +// 在控制台查看 Pinia 状态 | ||
| 435 | +import { useMainStore } from '@/store'; | ||
| 436 | + | ||
| 437 | +const mainStore = useMainStore(); | ||
| 438 | +console.log('Main Store:', mainStore.$state); | ||
| 439 | +``` | ||
| 440 | + | ||
| 441 | +### 3. 查看路由 | ||
| 442 | + | ||
| 443 | +```javascript | ||
| 444 | +// 查看当前路由 | ||
| 445 | +import { useRoute } from 'vue-router'; | ||
| 446 | + | ||
| 447 | +const route = useRoute(); | ||
| 448 | +console.log('当前路由:', route.path, route.query, route.params); | ||
| 449 | +``` | ||
| 450 | + | ||
| 451 | +## 部署 | ||
| 452 | + | ||
| 453 | +### 本地构建 | ||
| 454 | + | ||
| 455 | +```bash | ||
| 456 | +# 构建 | ||
| 457 | +npm run build | ||
| 458 | + | ||
| 459 | +# 预览 | ||
| 460 | +npm run serve | ||
| 461 | +``` | ||
| 462 | + | ||
| 463 | +### 部署到服务器 | ||
| 464 | + | ||
| 465 | +```bash | ||
| 466 | +# 开发环境 | ||
| 467 | +npm run dev_upload | ||
| 468 | + | ||
| 469 | +# OA 环境 | ||
| 470 | +npm run oa_upload | ||
| 471 | + | ||
| 472 | +# Walk 环境 | ||
| 473 | +npm run walk_upload | ||
| 474 | + | ||
| 475 | +# XYS 环境 | ||
| 476 | +npm run xys_upload | ||
| 477 | +``` | ||
| 478 | + | ||
| 479 | +## 常见问题 | ||
| 480 | + | ||
| 481 | +### Q1: 组件自动导入不生效? | ||
| 482 | + | ||
| 483 | +**A**: 检查 `vite.config.js` 中的组件解析器配置。 | ||
| 484 | + | ||
| 485 | +```javascript | ||
| 486 | +Components({ | ||
| 487 | + resolvers: [VantResolver(), ElementPlusResolver()], | ||
| 488 | +}), | ||
| 489 | +``` | ||
| 490 | + | ||
| 491 | +### Q2: API 自动导入不生效? | ||
| 492 | + | ||
| 493 | +**A**: 检查 `vite.config.js` 中的自动导入配置。 | ||
| 494 | + | ||
| 495 | +```javascript | ||
| 496 | +AutoImport({ | ||
| 497 | + imports: ['vue', 'vue-router'], | ||
| 498 | + dts: 'src/auto-imports.d.ts', | ||
| 499 | +}), | ||
| 500 | +``` | ||
| 501 | + | ||
| 502 | +### Q3: 路由跳转 404? | ||
| 503 | + | ||
| 504 | +**A**: 检查路由是否包含 `#/index.html` 前缀。 | ||
| 505 | + | ||
| 506 | +```javascript | ||
| 507 | +// ✅ 正确 | ||
| 508 | +router.push('/index.html/mypage') | ||
| 509 | + | ||
| 510 | +// ❌ 错误 | ||
| 511 | +router.push('/mypage') | ||
| 512 | +``` | ||
| 513 | + | ||
| 514 | +### Q4: 样式不生效? | ||
| 515 | + | ||
| 516 | +**A**: 检查是否添加了 `scoped`。 | ||
| 517 | + | ||
| 518 | +```vue | ||
| 519 | +<style lang="less" scoped> | ||
| 520 | +/* 添加 scoped */ | ||
| 521 | +</style> | ||
| 522 | +``` | ||
| 523 | + | ||
| 524 | +### Q5: 图片加载失败? | ||
| 525 | + | ||
| 526 | +**A**: 检查图片路径是否正确。 | ||
| 527 | + | ||
| 528 | +```javascript | ||
| 529 | +// ✅ 正确:使用别名 | ||
| 530 | +const imageUrl = ref('@images/logo.png'); | ||
| 531 | + | ||
| 532 | +// ✅ 正确:使用绝对路径 | ||
| 533 | +const imageUrl = ref('/images/logo.png'); | ||
| 534 | + | ||
| 535 | +// ❌ 错误:相对路径 | ||
| 536 | +const imageUrl = ref('../../images/logo.png'); | ||
| 537 | +``` | ||
| 538 | + | ||
| 539 | +## 学习资源 | ||
| 540 | + | ||
| 541 | +### Vue 3 | ||
| 542 | + | ||
| 543 | +- [Vue 3 官方文档](https://cn.vuejs.org/) | ||
| 544 | +- [Vue 3 Composition API](https://cn.vuejs.org/guide/extras/composition-api-faq.html) | ||
| 545 | + | ||
| 546 | +### Vant | ||
| 547 | + | ||
| 548 | +- [Vant 官方文档](https://vant-ui.github.io/vant/#/zh-CN) | ||
| 549 | +- [Vant 移动端组件](https://vant-ui.github.io/vant/#/zh-CN/home) | ||
| 550 | + | ||
| 551 | +### Element Plus | ||
| 552 | + | ||
| 553 | +- [Element Plus 官方文档](https://element-plus.org/zh-CN/) | ||
| 554 | +- [Element Plus 组件](https://element-plus.org/zh-CN/component/button.html) | ||
| 555 | + | ||
| 556 | +### Pinia | ||
| 557 | + | ||
| 558 | +- [Pinia 官方文档](https://pinia.vuejs.org/zh/) | ||
| 559 | +- [Pinia 核心概念](https://pinia.vuejs.org/zh/core-concepts/) | ||
| 560 | + | ||
| 561 | +### Vue Router | ||
| 562 | + | ||
| 563 | +- [Vue Router 官方文档](https://router.vuejs.org/zh/) | ||
| 564 | +- [Vue Router Hash 模式](https://router.vuejs.org/zh/guide/essentials/history-mode.html) | ||
| 565 | + | ||
| 566 | +## 下一步 | ||
| 567 | + | ||
| 568 | +1. 阅读 [项目技术栈详解](../项目架构分析/项目技术栈详解.md) | ||
| 569 | +2. 阅读 [目录结构分析](../项目架构分析/目录结构分析.md) | ||
| 570 | +3. 阅读 [已知问题汇总](../注意事项与陷阱/已知问题汇总.md) | ||
| 571 | +4. 开始开发! | ||
| 572 | + | ||
| 573 | +祝开发顺利!🎉 |
docs/注意事项与陷阱/已知问题汇总.md
0 → 100644
| 1 | +# 已知问题汇总 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**目的**: 记录项目中的已知问题和解决方案,避免后续开发中重复踩坑 | ||
| 5 | + | ||
| 6 | +## 🔴 高优先级问题 | ||
| 7 | + | ||
| 8 | +### 1. 版本冲突 | ||
| 9 | + | ||
| 10 | +#### 问题描述 | ||
| 11 | + | ||
| 12 | +项目中同时存在同一库的不同版本,可能导致兼容性问题。 | ||
| 13 | + | ||
| 14 | +#### 具体案例 | ||
| 15 | + | ||
| 16 | +**Photo Sphere Viewer**: | ||
| 17 | +```javascript | ||
| 18 | +// package.json | ||
| 19 | +"photo-sphere-viewer": "^4.8.1", | ||
| 20 | +"@photo-sphere-viewer/core": "^5.7.3", | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +**影响**: | ||
| 24 | +- VR 全景功能可能不稳定 | ||
| 25 | +- 包体积增大 | ||
| 26 | +- API 不一致 | ||
| 27 | + | ||
| 28 | +**解决方案**: | ||
| 29 | +```javascript | ||
| 30 | +// 统一使用 5.x 版本 | ||
| 31 | +// 1. 移除旧版本 | ||
| 32 | +npm uninstall photo-sphere-viewer | ||
| 33 | + | ||
| 34 | +// 2. 更新导入语句 | ||
| 35 | +// 之前: import { Viewer } from 'photo-sphere-viewer'; | ||
| 36 | +// 之后: import { Viewer } from '@photo-sphere-viewer/core'; | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +**日期管理库**: | ||
| 40 | +```javascript | ||
| 41 | +// package.json | ||
| 42 | +"dayjs": "^1.11.3", | ||
| 43 | +"moment": "^2.29.3", | ||
| 44 | +``` | ||
| 45 | + | ||
| 46 | +**影响**: | ||
| 47 | +- 包体积增大 | ||
| 48 | +- API 不一致 | ||
| 49 | + | ||
| 50 | +**解决方案**: | ||
| 51 | +```javascript | ||
| 52 | +// 统一使用 dayjs(更轻量) | ||
| 53 | +// 1. 移除 moment | ||
| 54 | +npm uninstall moment | ||
| 55 | + | ||
| 56 | +// 2. 替换所有 moment 调用为 dayjs | ||
| 57 | +// moment(date).format('YYYY-MM-DD') | ||
| 58 | +// → dayjs(date).format('YYYY-MM-DD') | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +### 2. Keep-Alive 缓存问题 | ||
| 62 | + | ||
| 63 | +#### 问题描述 | ||
| 64 | + | ||
| 65 | +`keepPages` 空数组会导致所有页面都被缓存。 | ||
| 66 | + | ||
| 67 | +#### 具体案例 | ||
| 68 | + | ||
| 69 | +```javascript | ||
| 70 | +// src/store/index.js:25 | ||
| 71 | +keepPages: ['default'], // 很坑爹,空值全部都缓存 | ||
| 72 | +``` | ||
| 73 | + | ||
| 74 | +**影响**: | ||
| 75 | +- 如果 `keepPages` 为 `[]`,所有页面都会被缓存 | ||
| 76 | +- 页面状态不会重置 | ||
| 77 | +- 可能导致内存泄漏 | ||
| 78 | + | ||
| 79 | +**解决方案**: | ||
| 80 | +```javascript | ||
| 81 | +// ✅ 正确:至少包含 'default' 作为占位符 | ||
| 82 | +keepPages: ['default'] | ||
| 83 | + | ||
| 84 | +// ❌ 错误:空数组 | ||
| 85 | +keepPages: [] // 会导致所有页面都被缓存 | ||
| 86 | +``` | ||
| 87 | + | ||
| 88 | +**使用方法**: | ||
| 89 | +```javascript | ||
| 90 | +// 添加需要缓存的页面 | ||
| 91 | +const keepThisPage = () => { | ||
| 92 | + const keepPages = [...mainStore.keepPages]; | ||
| 93 | + if (!keepPages.includes('PageName')) { | ||
| 94 | + keepPages.push('PageName'); | ||
| 95 | + } | ||
| 96 | + mainStore.keepPages = keepPages; | ||
| 97 | +}; | ||
| 98 | +``` | ||
| 99 | + | ||
| 100 | +### 3. 路由 Hash 模式 | ||
| 101 | + | ||
| 102 | +#### 问题描述 | ||
| 103 | + | ||
| 104 | +项目使用 Hash 模式,所有路由必须包含 `#/index.html` 前缀。 | ||
| 105 | + | ||
| 106 | +#### 具体案例 | ||
| 107 | + | ||
| 108 | +```javascript | ||
| 109 | +// src/router/index.js | ||
| 110 | +history: createWebHashHistory('/index.html') | ||
| 111 | +``` | ||
| 112 | + | ||
| 113 | +**影响**: | ||
| 114 | +- URL 格式: `http://localhost:8006/index.html#/index.html/views/page` | ||
| 115 | +- 如果忘记前缀,路由无法匹配 | ||
| 116 | + | ||
| 117 | +**解决方案**: | ||
| 118 | +```javascript | ||
| 119 | +// ✅ 正确:使用完整路径 | ||
| 120 | +router.push('/index.html/views/page') | ||
| 121 | + | ||
| 122 | +// ❌ 错误:缺少前缀 | ||
| 123 | +router.push('/views/page') | ||
| 124 | + | ||
| 125 | +// ✅ 推荐:使用路由名称 | ||
| 126 | +router.push({ name: 'PageName' }) | ||
| 127 | +``` | ||
| 128 | + | ||
| 129 | +## 🟡 中优先级问题 | ||
| 130 | + | ||
| 131 | +### 4. jQuery 依赖 | ||
| 132 | + | ||
| 133 | +#### 问题描述 | ||
| 134 | + | ||
| 135 | +项目仍使用 jQuery,与 Vue 3 冲突。 | ||
| 136 | + | ||
| 137 | +#### 具体案例 | ||
| 138 | + | ||
| 139 | +```javascript | ||
| 140 | +// src/components/VRViewer/index.vue:23 | ||
| 141 | +import $ from 'jquery'; | ||
| 142 | + | ||
| 143 | +// 使用 jQuery 操作 DOM | ||
| 144 | +$('.psv-zoom-button').css('display', ''); | ||
| 145 | +``` | ||
| 146 | + | ||
| 147 | +**影响**: | ||
| 148 | +- 不符合 Vue 3 理念 | ||
| 149 | +- 性能较差 | ||
| 150 | +- 不利于维护 | ||
| 151 | + | ||
| 152 | +**解决方案**: | ||
| 153 | +```javascript | ||
| 154 | +// ❌ 错误:使用 jQuery | ||
| 155 | +import $ from 'jquery'; | ||
| 156 | +$('.psv-zoom-button').css('display', ''); | ||
| 157 | + | ||
| 158 | +// ✅ 正确:使用 Vue 原生 API | ||
| 159 | +import { ref, onMounted } from 'vue'; | ||
| 160 | + | ||
| 161 | +const zoomButton = ref(null); | ||
| 162 | + | ||
| 163 | +onMounted(() => { | ||
| 164 | + if (zoomButton.value) { | ||
| 165 | + zoomButton.value.style.display = ''; | ||
| 166 | + } | ||
| 167 | +}); | ||
| 168 | +``` | ||
| 169 | + | ||
| 170 | +**迁移优先级**: | ||
| 171 | +1. 新代码避免使用 jQuery | ||
| 172 | +2. 逐步重构现有 jQuery 代码 | ||
| 173 | +3. 最终完全移除 jQuery 依赖 | ||
| 174 | + | ||
| 175 | +### 5. 全局样式注入 | ||
| 176 | + | ||
| 177 | +#### 问题描述 | ||
| 178 | + | ||
| 179 | +Less 配置中全局注入 `base.less`,所有组件都会包含全局样式。 | ||
| 180 | + | ||
| 181 | +#### 具体案例 | ||
| 182 | + | ||
| 183 | +```javascript | ||
| 184 | +// vite.config.js:107 | ||
| 185 | +additionalData: `@import "${path.resolve(__dirname, 'src/assets/styles/base.less')}";` | ||
| 186 | +``` | ||
| 187 | + | ||
| 188 | +**影响**: | ||
| 189 | +- 所有 `.vue` 文件中的 `<style lang="less">` 都会包含全局样式 | ||
| 190 | +- 可能导致样式冲突 | ||
| 191 | +- 编译时间增加 | ||
| 192 | + | ||
| 193 | +**解决方案**: | ||
| 194 | +```less | ||
| 195 | +/* ✅ 推荐:仅在需要时导入 */ | ||
| 196 | +@import './base.less'; | ||
| 197 | + | ||
| 198 | +/* ❌ 不推荐:重复导入 */ | ||
| 199 | +/* base.less 已经全局注入,无需重复导入 */ | ||
| 200 | +``` | ||
| 201 | + | ||
| 202 | +### 6. SVG 渲染性能 | ||
| 203 | + | ||
| 204 | +#### 问题描述 | ||
| 205 | + | ||
| 206 | +复杂 SVG 可能导致渲染卡顿。 | ||
| 207 | + | ||
| 208 | +#### 具体案例 | ||
| 209 | + | ||
| 210 | +```vue | ||
| 211 | +<!-- src/components/Floor/index.vue --> | ||
| 212 | +<div v-html="level.svg"></div> | ||
| 213 | +``` | ||
| 214 | + | ||
| 215 | +**影响**: | ||
| 216 | +- 楼层平面图 SVG 可能很复杂 | ||
| 217 | +- 渲染时间长 | ||
| 218 | +- 可能导致页面卡顿 | ||
| 219 | + | ||
| 220 | +**解决方案**: | ||
| 221 | +```javascript | ||
| 222 | +// 1. 简化 SVG 路径 | ||
| 223 | +// 2. 使用懒加载 | ||
| 224 | +const loadFloorSVG = async (level) => { | ||
| 225 | + const { data } = await mapAPI({ id: locationId, level }); | ||
| 226 | + return data.svg; | ||
| 227 | +}; | ||
| 228 | + | ||
| 229 | +// 3. 使用 will-change 优化 | ||
| 230 | +.floor-svg { | ||
| 231 | + will-change: transform; | ||
| 232 | +} | ||
| 233 | +``` | ||
| 234 | + | ||
| 235 | +### 7. XSS 风险 | ||
| 236 | + | ||
| 237 | +#### 问题描述 | ||
| 238 | + | ||
| 239 | +使用 `v-html` 渲染 SVG 可能存在 XSS 风险。 | ||
| 240 | + | ||
| 241 | +#### 具体案例 | ||
| 242 | + | ||
| 243 | +```vue | ||
| 244 | +<div v-html="level.svg"></div> | ||
| 245 | +``` | ||
| 246 | + | ||
| 247 | +**风险**: | ||
| 248 | +- 如果 SVG 来自用户输入或不可信来源 | ||
| 249 | +- 可能包含恶意脚本 | ||
| 250 | + | ||
| 251 | +**解决方案**: | ||
| 252 | +```javascript | ||
| 253 | +// 安装 DOMPurify | ||
| 254 | +npm install dompurify | ||
| 255 | + | ||
| 256 | +// 清理 SVG | ||
| 257 | +import DOMPurify from 'dompurify'; | ||
| 258 | + | ||
| 259 | +const cleanSVG = DOMPurify.sanitize(svgString); | ||
| 260 | +``` | ||
| 261 | + | ||
| 262 | +## 🟢 低优先级问题 | ||
| 263 | + | ||
| 264 | +### 8. 代码重复 | ||
| 265 | + | ||
| 266 | +#### 问题描述 | ||
| 267 | + | ||
| 268 | +多个页面中存在重复的代码模式。 | ||
| 269 | + | ||
| 270 | +#### 具体案例 | ||
| 271 | + | ||
| 272 | +**信息窗口组件**: | ||
| 273 | +- `InfoWindowLite.vue` | ||
| 274 | +- `InfoWindowWarn.vue` | ||
| 275 | +- `InfoWindowYard.vue` | ||
| 276 | +- `InfoPopupLite.vue` | ||
| 277 | +- `InfoPopupWarn.vue` | ||
| 278 | + | ||
| 279 | +**影响**: | ||
| 280 | +- 维护成本高 | ||
| 281 | +- 代码重复 | ||
| 282 | + | ||
| 283 | +**解决方案**: | ||
| 284 | +```vue | ||
| 285 | +// ✅ 推荐:合并为一个组件,使用 props 控制样式 | ||
| 286 | +<InfoWindow | ||
| 287 | + :variant="'lite' | 'warn' | 'yard'" | ||
| 288 | + :title="title" | ||
| 289 | + :description="description" | ||
| 290 | +/> | ||
| 291 | + | ||
| 292 | +// ❌ 不推荐:维护多个相似组件 | ||
| 293 | +<InfoWindowLite :title="title" /> | ||
| 294 | +<InfoWindowWarn :title="title" /> | ||
| 295 | +<InfoWindowYard :title="title" /> | ||
| 296 | +``` | ||
| 297 | + | ||
| 298 | +### 9. 缺少类型定义 | ||
| 299 | + | ||
| 300 | +#### 问题描述 | ||
| 301 | + | ||
| 302 | +项目使用 TypeScript,但缺少完整的类型定义。 | ||
| 303 | + | ||
| 304 | +#### 具体案例 | ||
| 305 | + | ||
| 306 | +```javascript | ||
| 307 | +// src/auto-imports.d.ts(自动生成) | ||
| 308 | +// 但很多组件和函数缺少类型定义 | ||
| 309 | +``` | ||
| 310 | + | ||
| 311 | +**影响**: | ||
| 312 | +- IDE 提示不完整 | ||
| 313 | +- 容易出现类型错误 | ||
| 314 | +- 不利于重构 | ||
| 315 | + | ||
| 316 | +**解决方案**: | ||
| 317 | +```typescript | ||
| 318 | +// 为组件添加类型定义 | ||
| 319 | +// src/components/Floor/index.vue | ||
| 320 | + | ||
| 321 | +interface FloorData { | ||
| 322 | + svg: string; | ||
| 323 | + pin: PinData[]; | ||
| 324 | +} | ||
| 325 | + | ||
| 326 | +interface PinData { | ||
| 327 | + category: string; | ||
| 328 | + space: string; | ||
| 329 | + icon: string; | ||
| 330 | + style: Record<string, string>; | ||
| 331 | + info?: InfoData; | ||
| 332 | +} | ||
| 333 | +``` | ||
| 334 | + | ||
| 335 | +### 10. 测试覆盖不足 | ||
| 336 | + | ||
| 337 | +#### 问题描述 | ||
| 338 | + | ||
| 339 | +项目缺少完整的测试覆盖。 | ||
| 340 | + | ||
| 341 | +#### 具体案例 | ||
| 342 | + | ||
| 343 | +```javascript | ||
| 344 | +// src/test/mocha/test.js(仅有一个测试文件) | ||
| 345 | +``` | ||
| 346 | + | ||
| 347 | +**影响**: | ||
| 348 | +- 重构时容易引入 Bug | ||
| 349 | +- 难以保证代码质量 | ||
| 350 | + | ||
| 351 | +**解决方案**: | ||
| 352 | +```javascript | ||
| 353 | +// 补充单元测试 | ||
| 354 | +// tests/components/Floor.spec.js | ||
| 355 | +import { mount } from '@vue/test-utils'; | ||
| 356 | +import { describe, it, expect } from 'vitest'; | ||
| 357 | +import Floor from '@/components/Floor/index.vue'; | ||
| 358 | + | ||
| 359 | +describe('Floor', () => { | ||
| 360 | + it('should render floor list', () => { | ||
| 361 | + const wrapper = mount(Floor, { | ||
| 362 | + props: { | ||
| 363 | + levelList: [...], | ||
| 364 | + }, | ||
| 365 | + }); | ||
| 366 | + | ||
| 367 | + expect(wrapper.findAll('.level').length).toBe(4); | ||
| 368 | + }); | ||
| 369 | +}); | ||
| 370 | +``` | ||
| 371 | + | ||
| 372 | +## ⚠️ 注意事项 | ||
| 373 | + | ||
| 374 | +### 1. 环境变量 | ||
| 375 | + | ||
| 376 | +**问题**: 环境变量必须以 `VITE_` 开头 | ||
| 377 | + | ||
| 378 | +```javascript | ||
| 379 | +// ✅ 正确 | ||
| 380 | +VITE_PORT=8006 | ||
| 381 | +VITE_PROXY_TARGET=http://api.example.com | ||
| 382 | + | ||
| 383 | +// ❌ 错误 | ||
| 384 | +PORT=8006 | ||
| 385 | +PROXY_TARGET=http://api.example.com | ||
| 386 | +``` | ||
| 387 | + | ||
| 388 | +### 2. 路径别名 | ||
| 389 | + | ||
| 390 | +**问题**: 使用路径别名时必须使用绝对路径 | ||
| 391 | + | ||
| 392 | +```javascript | ||
| 393 | +// ✅ 正确 | ||
| 394 | +import Floor from '@components/Floor/index.vue'; | ||
| 395 | + | ||
| 396 | +// ❌ 错误 | ||
| 397 | +import Floor from '../../components/Floor/index.vue'; | ||
| 398 | +``` | ||
| 399 | + | ||
| 400 | +### 3. 组件命名 | ||
| 401 | + | ||
| 402 | +**问题**: 组件文件名必须使用 PascalCase | ||
| 403 | + | ||
| 404 | +```javascript | ||
| 405 | +// ✅ 正确 | ||
| 406 | +audioList.vue | ||
| 407 | +InfoWindowLite.vue | ||
| 408 | +VRViewer/index.vue | ||
| 409 | + | ||
| 410 | +// ❌ 错误 | ||
| 411 | +audio-list.vue | ||
| 412 | +infoWindowLite.vue | ||
| 413 | +vr-viewer/index.vue | ||
| 414 | +``` | ||
| 415 | + | ||
| 416 | +### 4. API 响应检查 | ||
| 417 | + | ||
| 418 | +**问题**: 所有 API 调用必须检查 `res.code === 1` | ||
| 419 | + | ||
| 420 | +```javascript | ||
| 421 | +// ✅ 正确 | ||
| 422 | +const { data } = await mapAPI(params); | ||
| 423 | +if (res.code === 1 && res.data) { | ||
| 424 | + // 处理数据 | ||
| 425 | +} | ||
| 426 | + | ||
| 427 | +// ❌ 错误 | ||
| 428 | +const { data } = await mapAPI(params); | ||
| 429 | +if (data) { | ||
| 430 | + // res.code 未检查,可能处理错误数据 | ||
| 431 | +} | ||
| 432 | +``` | ||
| 433 | + | ||
| 434 | +### 5. 异步错误处理 | ||
| 435 | + | ||
| 436 | +**问题**: 所有 async 函数必须有 try-catch | ||
| 437 | + | ||
| 438 | +```javascript | ||
| 439 | +// ✅ 正确 | ||
| 440 | +const fetchData = async () => { | ||
| 441 | + try { | ||
| 442 | + const { data } = await mapAPI(params); | ||
| 443 | + return data; | ||
| 444 | + } catch (err) { | ||
| 445 | + console.error('请求失败:', err); | ||
| 446 | + return null; | ||
| 447 | + } | ||
| 448 | +}; | ||
| 449 | + | ||
| 450 | +// ❌ 错误 | ||
| 451 | +const fetchData = async () => { | ||
| 452 | + const { data } = await mapAPI(params); | ||
| 453 | + return data; | ||
| 454 | + // 错误未处理,可能导致应用崩溃 | ||
| 455 | +}; | ||
| 456 | +``` | ||
| 457 | + | ||
| 458 | +## 📝 待解决问题 | ||
| 459 | + | ||
| 460 | +### 1. 多页面应用配置未清理 | ||
| 461 | + | ||
| 462 | +**问题**: `src/packages/` 目录存在但未使用 | ||
| 463 | + | ||
| 464 | +**影响**: | ||
| 465 | +- 代码混乱 | ||
| 466 | +- 构建时间增加 | ||
| 467 | + | ||
| 468 | +**建议**: 归档或删除未使用的多页面应用代码 | ||
| 469 | + | ||
| 470 | +### 2. 文件历史目录 | ||
| 471 | + | ||
| 472 | +**问题**: `.history/` 目录存在 | ||
| 473 | + | ||
| 474 | +**影响**: | ||
| 475 | +- Git 仓库混乱 | ||
| 476 | +- 磁盘空间浪费 | ||
| 477 | + | ||
| 478 | +**建议**: 添加到 `.gitignore` | ||
| 479 | + | ||
| 480 | +### 3. 控制台调试代码 | ||
| 481 | + | ||
| 482 | +**问题**: 项目中可能存在 `console.log` 和 `debugger` | ||
| 483 | + | ||
| 484 | +**影响**: | ||
| 485 | +- 生产环境性能 | ||
| 486 | +- 可能泄露敏感信息 | ||
| 487 | + | ||
| 488 | +**建议**: 使用 ESLint 检测并移除调试代码 | ||
| 489 | + | ||
| 490 | +### 4. 图片优化 | ||
| 491 | + | ||
| 492 | +**问题**: 图片未压缩和优化 | ||
| 493 | + | ||
| 494 | +**影响**: | ||
| 495 | +- 加载速度慢 | ||
| 496 | +- 流量消耗大 | ||
| 497 | + | ||
| 498 | +**建议**: | ||
| 499 | +- 使用 WebP 格式 | ||
| 500 | +- 压缩图片 | ||
| 501 | +- 使用 CDN 优化 | ||
| 502 | + | ||
| 503 | +## 参考文档 | ||
| 504 | + | ||
| 505 | +- [项目技术栈详解](../项目架构分析/项目技术栈详解.md) | ||
| 506 | +- [目录结构分析](../项目架构分析/目录结构分析.md) | ||
| 507 | +- [地图集成分析](../功能模块分析/地图集成分析.md) | ||
| 508 | +- [音频系统分析](../功能模块分析/音频系统分析.md) | ||
| 509 | +- [VR全景分析](../功能模块分析/VR全景分析.md) | ||
| 510 | +- [打卡系统分析](../功能模块分析/打卡系统分析.md) |
docs/项目架构分析/目录结构分析.md
0 → 100644
| 1 | +# 项目目录结构分析 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**项目根目录**: /Users/huyirui/program/itomix/git/map-demo | ||
| 5 | + | ||
| 6 | +## 完整目录树 | ||
| 7 | + | ||
| 8 | +``` | ||
| 9 | +map-demo/ | ||
| 10 | +├── public/ # 静态资源目录 | ||
| 11 | +├── docs/ # 项目文档(新增) | ||
| 12 | +│ ├── 项目架构分析/ | ||
| 13 | +│ ├── 功能模块分析/ | ||
| 14 | +│ ├── 注意事项与陷阱/ | ||
| 15 | +│ └── 开发指南/ | ||
| 16 | +├── src/ # 源代码目录 | ||
| 17 | +│ ├── api/ # API 接口定义 | ||
| 18 | +│ │ ├── B/ # B端(企业端)API | ||
| 19 | +│ │ │ ├── audit.js # 审计相关 | ||
| 20 | +│ │ │ ├── kg.js # 卡片相关? | ||
| 21 | +│ │ │ └── localism.js # 本土化相关 | ||
| 22 | +│ │ ├── C/ # C端(用户端)API | ||
| 23 | +│ │ │ ├── book.js # 预订相关 | ||
| 24 | +│ │ │ ├── donate.js # 捐赠相关 | ||
| 25 | +│ │ │ ├── kg.js # 卡片相关? | ||
| 26 | +│ │ │ ├── me.js # 个人中心 | ||
| 27 | +│ │ │ ├── perf.js # 性能相关? | ||
| 28 | +│ │ │ └── prod.js # 产品相关 | ||
| 29 | +│ │ ├── wx/ # 微信相关 API | ||
| 30 | +│ │ │ ├── config.js # 微信配置 | ||
| 31 | +│ │ │ ├── jsApiList.js # JS API 列表 | ||
| 32 | +│ │ │ └── pay.js # 微信支付 | ||
| 33 | +│ │ ├── checkin.js # 打卡 API | ||
| 34 | +│ │ ├── common.js # 通用 API(短信、上传、Token) | ||
| 35 | +│ │ ├── fn.js # API 包装器(错误处理) | ||
| 36 | +│ │ └── map.js # 地图数据 API | ||
| 37 | +│ ├── assets/ # 资源文件 | ||
| 38 | +│ │ ├── css/ # 全局样式 | ||
| 39 | +│ │ ├── images/ # 图片资源 | ||
| 40 | +│ │ ├── mock/ # Mock 数据 | ||
| 41 | +│ │ │ ├── routes.js # 路由 Mock 数据 | ||
| 42 | +│ │ │ ├── video_list.js # 视频列表 Mock | ||
| 43 | +│ │ │ └── video_list1.js # 视频列表 Mock 1 | ||
| 44 | +│ │ └── styles/ # 样式文件 | ||
| 45 | +│ │ └── base.less # 基础样式(全局注入) | ||
| 46 | +│ ├── common/ # 通用代码 | ||
| 47 | +│ │ ├── alert.js # 弹窗提示 | ||
| 48 | +│ │ ├── inner_router.js # 内部路由 | ||
| 49 | +│ │ ├── map_data.js # 地图数据 | ||
| 50 | +│ │ ├── max.js # 最大值处理? | ||
| 51 | +│ │ ├── members.js # 成员管理 | ||
| 52 | +│ │ ├── mixin.js # Vue 混入 | ||
| 53 | +│ │ ├── my_router.js # 自定义路由 | ||
| 54 | +│ │ ├── tool.js # 工具函数 | ||
| 55 | +│ │ ├── yard.js # 院落相关 | ||
| 56 | +│ │ └── vueuse.js # VueUse 封装 | ||
| 57 | +│ ├── components/ # 公共组件 | ||
| 58 | +│ │ ├── Floor/ # 楼层平面图组件 | ||
| 59 | +│ │ │ ├── index.vue # 主组件 | ||
| 60 | +│ │ │ ├── pin.js # 标记点 | ||
| 61 | +│ │ │ └── svgIcon.vue # SVG 图标 | ||
| 62 | +│ │ ├── VRViewer/ # VR 全景查看器 | ||
| 63 | +│ │ │ └── index.vue | ||
| 64 | +│ │ ├── audioBackground.vue # 背景音频(单模式) | ||
| 65 | +│ │ ├── audioBackground1.vue # 背景音频(备用) | ||
| 66 | +│ │ ├── audioList.vue # 音频列表(播放列表模式) | ||
| 67 | +│ │ ├── InfoWindowLite.vue # 信息窗口(轻量版) | ||
| 68 | +│ │ ├── InfoWindowWarn.vue # 信息窗口(警告版) | ||
| 69 | +│ │ ├── InfoWindowYard.vue # 信息窗口(院落版) | ||
| 70 | +│ │ ├── InfoPopupLite.vue # 信息弹窗(轻量版) | ||
| 71 | +│ │ └── InfoPopupWarn.vue # 信息弹窗(警告版) | ||
| 72 | +│ ├── hooks/ # Vue Hooks | ||
| 73 | +│ │ ├── injectionSymbols.js # 注入符号 | ||
| 74 | +│ │ ├── useContext.js # 上下文 Hook | ||
| 75 | +│ │ ├── useDebounce.js # 防抖 Hook | ||
| 76 | +│ │ ├── useFlowFn.js # 流程函数 Hook | ||
| 77 | +│ │ ├── useGo.js # 跳转 Hook | ||
| 78 | +│ │ └── useKeepAlive.js # Keep-Alive Hook | ||
| 79 | +│ ├── packages/ # 多页面应用包(已废弃) | ||
| 80 | +│ │ ├── mono1/ # 子应用 1 | ||
| 81 | +│ │ │ ├── App.vue | ||
| 82 | +│ │ │ ├── index.html | ||
| 83 | +│ │ │ ├── main.js | ||
| 84 | +│ │ │ ├── router.js | ||
| 85 | +│ │ │ └── views/ | ||
| 86 | +│ │ │ └── index.vue | ||
| 87 | +│ │ └── mono2/ # 子应用 2 | ||
| 88 | +│ │ ├── App.vue | ||
| 89 | +│ │ ├── index.html | ||
| 90 | +│ │ ├── main.js | ||
| 91 | +│ │ ├── router.js | ||
| 92 | +│ │ └── views/ | ||
| 93 | +│ │ └── index.vue | ||
| 94 | +│ ├── router/ # 路由配置 | ||
| 95 | +│ │ ├── routes/ # 路由模块 | ||
| 96 | +│ │ │ └── modules/ | ||
| 97 | +│ │ │ └── auth/ # 认证路由 | ||
| 98 | +│ │ │ └── index.js | ||
| 99 | +│ │ └── methods/ # 路由方法(可能不存在) | ||
| 100 | +│ ├── settings/ # 设置 | ||
| 101 | +│ │ ├── componentSetting.js # 组件设置 | ||
| 102 | +│ │ └── designSetting.js # 设计设置 | ||
| 103 | +│ ├── store/ # Pinia 状态管理 | ||
| 104 | +│ │ ├── index.js # 主 Store | ||
| 105 | +│ │ └── test.js # 测试 Store | ||
| 106 | +│ ├── test/ # 测试 | ||
| 107 | +│ │ └── mocha/ | ||
| 108 | +│ │ └── test.js # Mocha 测试 | ||
| 109 | +│ ├── utils/ # 工具函数 | ||
| 110 | +│ │ ├── MonitorKeyboard.js # 键盘监控 | ||
| 111 | +│ │ ├── TileCutter.js # 瓦片切割 | ||
| 112 | +│ │ ├── axios.js # Axios 实例 | ||
| 113 | +│ │ ├── generateIcons.js # 生成图标 | ||
| 114 | +│ │ ├── generateModules.js # 生成模块 | ||
| 115 | +│ │ ├── generatePackage.js # 生成包 | ||
| 116 | +│ │ ├── generateRoute.js # 生成路由 | ||
| 117 | +│ │ ├── share.js # 分享工具 | ||
| 118 | +│ │ ├── tools.js # 通用工具 | ||
| 119 | +│ │ └── vconsole.js # VConsole 调试 | ||
| 120 | +│ ├── views/ # 页面组件 | ||
| 121 | +│ │ ├── bieyuan/ # 别院模块 | ||
| 122 | +│ │ │ ├── index.vue # 别院首页 | ||
| 123 | +│ │ │ ├── info_w.vue # 信息页面(带警告) | ||
| 124 | +│ │ │ └── scan.vue # 扫码页面 | ||
| 125 | +│ │ ├── by/ # BY 模块 | ||
| 126 | +│ │ │ ├── index.vue # BY 首页 | ||
| 127 | +│ │ │ ├── info.vue # 信息页面 | ||
| 128 | +│ │ │ ├── info_w.vue # 信息页面(带警告) | ||
| 129 | +│ │ │ └── scan.vue # 扫码页面 | ||
| 130 | +│ │ ├── checkin/ # 打卡模块 | ||
| 131 | +│ │ │ ├── index.vue # 打卡首页 | ||
| 132 | +│ │ │ ├── info.vue # 打卡信息 | ||
| 133 | +│ │ │ ├── info_w.vue # 打卡信息(带警告) | ||
| 134 | +│ │ │ └── scan.vue # 扫码打卡 | ||
| 135 | +│ │ ├── xys/ # XYS 模块 | ||
| 136 | +│ │ ├── about.vue # 关于页面 | ||
| 137 | +│ │ ├── auth.vue # 认证页面 | ||
| 138 | +│ │ ├── info.vue # 信息页面 | ||
| 139 | +│ │ ├── noticeList.vue # 通知列表 | ||
| 140 | +│ │ └── test.vue # 测试页面 | ||
| 141 | +│ ├── App.vue # 根组件 | ||
| 142 | +│ ├── constant.js # 常量定义 | ||
| 143 | +│ ├── main.js # 入口文件 | ||
| 144 | +│ ├── route.js # 路由配置(辅助) | ||
| 145 | +│ ├── router.js # 路由配置(主) | ||
| 146 | +│ ├── theme-vars.js # 主题变量 | ||
| 147 | +│ └── auto-imports.d.ts # 自动导入类型定义 | ||
| 148 | +├── build/ # 构建配置 | ||
| 149 | +│ └── proxy.js # 代理配置 | ||
| 150 | +├── .history/ # 文件历史(Git 记录) | ||
| 151 | +├── .env # 环境变量(基础) | ||
| 152 | +├── .env.development # 环境变量(开发) | ||
| 153 | +├── .env.production # 环境变量(生产) | ||
| 154 | +├── .eslintrc.js # ESLint 配置 | ||
| 155 | +├── .gitignore # Git 忽略文件 | ||
| 156 | +├── index.html # 主入口 HTML | ||
| 157 | +├── package.json # 项目依赖 | ||
| 158 | +├── vite.config.js # Vite 配置 | ||
| 159 | +├── cypress.json # Cypress 配置 | ||
| 160 | +├── CLAUDE.md # Claude Code 项目指南 | ||
| 161 | +├── README.md # 项目说明 | ||
| 162 | +└── tsconfig.json # TypeScript 配置 | ||
| 163 | +``` | ||
| 164 | + | ||
| 165 | +## 核心目录说明 | ||
| 166 | + | ||
| 167 | +### 1. src/api/ - API 接口层 | ||
| 168 | + | ||
| 169 | +**职责**: 定义所有与后端交互的 API 接口 | ||
| 170 | + | ||
| 171 | +**结构**: | ||
| 172 | +- `B/`: B端(企业/管理端)接口 | ||
| 173 | +- `C/`: C端(用户/客户端)接口 | ||
| 174 | +- `wx/`: 微信相关接口(配置、支付) | ||
| 175 | +- `common.js`: 通用接口(短信、上传、Token) | ||
| 176 | +- `fn.js`: API 包装器,统一错误处理 | ||
| 177 | + | ||
| 178 | +**关键文件**: | ||
| 179 | +- `fn.js`: 核心 API 包装器,处理响应验证和错误 | ||
| 180 | +- `map.js`: 地图数据接口 | ||
| 181 | +- `checkin.js`: 打卡功能接口 | ||
| 182 | + | ||
| 183 | +### 2. src/components/ - 公共组件 | ||
| 184 | + | ||
| 185 | +**职责**: 可复用的 Vue 组件 | ||
| 186 | + | ||
| 187 | +**核心组件**: | ||
| 188 | +- `Floor/`: 楼层平面图组件(SVG 交互) | ||
| 189 | +- `VRViewer/`: 360° 全景查看器 | ||
| 190 | +- `audioBackground.vue`: 背景音频播放(单模式) | ||
| 191 | +- `audioList.vue`: 音频列表播放(列表模式) | ||
| 192 | +- `InfoWindow*.vue`: 各种样式的信息窗口 | ||
| 193 | + | ||
| 194 | +### 3. src/views/ - 页面组件 | ||
| 195 | + | ||
| 196 | +**职责**: 页面级组件 | ||
| 197 | + | ||
| 198 | +**模块划分**: | ||
| 199 | +- `bieyuan/`: 别院模块 | ||
| 200 | +- `by/`: BY 模块 | ||
| 201 | +- `checkin/`: 打卡模块 | ||
| 202 | +- `xys/`: XYS 模块 | ||
| 203 | + | ||
| 204 | +**共同页面**: | ||
| 205 | +- `about.vue`: 关于页面 | ||
| 206 | +- `auth.vue`: 认证页面 | ||
| 207 | +- `info.vue`: 信息页面 | ||
| 208 | +- `noticeList.vue`: 通知列表 | ||
| 209 | +- `test.vue`: 测试页面 | ||
| 210 | + | ||
| 211 | +### 4. src/store/ - 状态管理 | ||
| 212 | + | ||
| 213 | +**职责**: Pinia 全局状态 | ||
| 214 | + | ||
| 215 | +**核心状态**: | ||
| 216 | +- `keepPages`: Keep-Alive 缓存页面列表 | ||
| 217 | +- 音频播放状态(单模式和播放列表模式) | ||
| 218 | +- 滚动位置状态 | ||
| 219 | +- 用户认证状态 | ||
| 220 | + | ||
| 221 | +### 5. src/router/ - 路由配置 | ||
| 222 | + | ||
| 223 | +**职责**: Vue Router 路由定义 | ||
| 224 | + | ||
| 225 | +**特点**: | ||
| 226 | +- 使用 Hash 模式 (`createWebHashHistory('/index.html')`) | ||
| 227 | +- 动态路由生成(从 Mock 数据加载) | ||
| 228 | +- 模块化路由(`routes/modules/`) | ||
| 229 | + | ||
| 230 | +### 6. src/utils/ - 工具函数 | ||
| 231 | + | ||
| 232 | +**职责**: 通用工具函数和辅助方法 | ||
| 233 | + | ||
| 234 | +**核心工具**: | ||
| 235 | +- `axios.js`: Axios 实例(拦截器配置) | ||
| 236 | +- `tools.js`: 通用工具函数 | ||
| 237 | +- `share.js`: 微信分享工具 | ||
| 238 | +- `generateRoute.js`: 动态路由生成 | ||
| 239 | + | ||
| 240 | +### 7. src/common/ - 通用代码 | ||
| 241 | + | ||
| 242 | +**职责**: 跨层级的通用代码 | ||
| 243 | + | ||
| 244 | +**核心文件**: | ||
| 245 | +- `map_data.js`: 地图数据处理 | ||
| 246 | +- `mixin.js`: Vue 混入 | ||
| 247 | +- `alert.js`: 弹窗提示 | ||
| 248 | + | ||
| 249 | +### 8. src/packages/ - 多页面应用(已废弃) | ||
| 250 | + | ||
| 251 | +**状态**: 代码已存在,但未在构建配置中启用 | ||
| 252 | + | ||
| 253 | +**说明**: | ||
| 254 | +- `mono1/`, `mono2/`: 两个独立的子应用 | ||
| 255 | +- `vite.config.js` 中相关配置已注释 | ||
| 256 | + | ||
| 257 | +### 9. build/ - 构建配置 | ||
| 258 | + | ||
| 259 | +**职责**: Vite 构建相关配置 | ||
| 260 | + | ||
| 261 | +**核心文件**: | ||
| 262 | +- `proxy.js`: 代理配置生成器 | ||
| 263 | + | ||
| 264 | +## 文件命名规范 | ||
| 265 | + | ||
| 266 | +### Vue 组件 | ||
| 267 | + | ||
| 268 | +**页面组件**: `index.vue`(放在模块目录下) | ||
| 269 | +**公共组件**: `PascalCase.vue`(多单词,大驼峰) | ||
| 270 | + | ||
| 271 | +**示例**: | ||
| 272 | +- ✅ `audioList.vue` | ||
| 273 | +- ✅ `VRViewer/index.vue` | ||
| 274 | +- ✅ `InfoWindowLite.vue` | ||
| 275 | + | ||
| 276 | +### JavaScript 文件 | ||
| 277 | + | ||
| 278 | +**工具函数**: `camelCase.js`(小驼峰) | ||
| 279 | +**常量文件**: `constant.js` | ||
| 280 | +**配置文件**: `*.config.js` | ||
| 281 | + | ||
| 282 | +**示例**: | ||
| 283 | +- ✅ `tools.js` | ||
| 284 | +- ✅ `useDebounce.js` | ||
| 285 | +- ✅ `vite.config.js` | ||
| 286 | + | ||
| 287 | +### 样式文件 | ||
| 288 | + | ||
| 289 | +**全局样式**: `base.less` | ||
| 290 | +**组件样式**: 与组件同名的 `.less` 文件 | ||
| 291 | + | ||
| 292 | +## 目录组织原则 | ||
| 293 | + | ||
| 294 | +### 1. 按功能划分 | ||
| 295 | + | ||
| 296 | +- `api/`: 按业务端(B/C)和功能(wx/map)划分 | ||
| 297 | +- `views/`: 按业务模块(bieyuan/by/checkin/xys)划分 | ||
| 298 | +- `components/`: 按功能组件(Floor/VRViewer)划分 | ||
| 299 | + | ||
| 300 | +### 2. 按层级划分 | ||
| 301 | + | ||
| 302 | +- `src/`: 源代码根目录 | ||
| 303 | +- `public/`: 静态资源(不经过 Vite 处理) | ||
| 304 | +- `build/`: 构建脚本和配置 | ||
| 305 | +- `docs/`: 项目文档 | ||
| 306 | + | ||
| 307 | +### 3. 特殊目录 | ||
| 308 | + | ||
| 309 | +- `.history/`: Git 文件历史(本地编辑器生成) | ||
| 310 | +- `node_modules/`: 依赖包(.gitignore) | ||
| 311 | +- `dist/`: 构建输出(.gitignore) | ||
| 312 | + | ||
| 313 | +## 建议的目录优化 | ||
| 314 | + | ||
| 315 | +### 1. 清理废弃代码 | ||
| 316 | + | ||
| 317 | +```bash | ||
| 318 | +# 建议移除或归档 | ||
| 319 | +src/packages/ # 多页面应用(已废弃) | ||
| 320 | +.history/ # 本地编辑器历史(不应提交) | ||
| 321 | +``` | ||
| 322 | + | ||
| 323 | +### 2. 统一 API 结构 | ||
| 324 | + | ||
| 325 | +``` | ||
| 326 | +src/api/ | ||
| 327 | +├── modules/ # 按功能模块划分 | ||
| 328 | +│ ├── auth.js | ||
| 329 | +│ ├── map.js | ||
| 330 | +│ ├── checkin.js | ||
| 331 | +│ └── wechat/ | ||
| 332 | +│ ├── config.js | ||
| 333 | +│ ├── pay.js | ||
| 334 | +│ └── jsapi.js | ||
| 335 | +├── common.js # 通用 API | ||
| 336 | +└── fn.js # API 包装器 | ||
| 337 | +``` | ||
| 338 | + | ||
| 339 | +### 3. 组件分类 | ||
| 340 | + | ||
| 341 | +``` | ||
| 342 | +src/components/ | ||
| 343 | +├── ui/ # 基础 UI 组件 | ||
| 344 | +├── business/ # 业务组件 | ||
| 345 | +│ ├── map/ | ||
| 346 | +│ │ ├── Floor/ | ||
| 347 | +│ │ ├── VRViewer/ | ||
| 348 | +│ │ └── InfoWindow/ | ||
| 349 | +│ └── audio/ | ||
| 350 | +│ ├── audioBackground.vue | ||
| 351 | +│ └── audioList.vue | ||
| 352 | +└── layouts/ # 布局组件 | ||
| 353 | +``` | ||
| 354 | + | ||
| 355 | +### 4. Hooks 分类 | ||
| 356 | + | ||
| 357 | +``` | ||
| 358 | +src/hooks/ | ||
| 359 | +├── router/ # 路由相关 | ||
| 360 | +│ ├── useGo.js | ||
| 361 | +│ └── useKeepAlive.js | ||
| 362 | +├── data/ # 数据相关 | ||
| 363 | +│ └── useContext.js | ||
| 364 | +└── utils/ # 工具相关 | ||
| 365 | + └── useDebounce.js | ||
| 366 | +``` | ||
| 367 | + | ||
| 368 | +## 文件搜索指南 | ||
| 369 | + | ||
| 370 | +### 查找页面组件 | ||
| 371 | + | ||
| 372 | +```bash | ||
| 373 | +# 查找所有页面组件 | ||
| 374 | +find src/views -name "index.vue" | ||
| 375 | + | ||
| 376 | +# 查找特定模块的页面 | ||
| 377 | +ls src/views/bieyuan/ | ||
| 378 | +ls src/views/by/ | ||
| 379 | +ls src/views/checkin/ | ||
| 380 | +``` | ||
| 381 | + | ||
| 382 | +### 查找公共组件 | ||
| 383 | + | ||
| 384 | +```bash | ||
| 385 | +# 查找所有公共组件 | ||
| 386 | +find src/components -name "*.vue" | ||
| 387 | + | ||
| 388 | +# 查找特定组件 | ||
| 389 | +find src/components -name "*Window*.vue" | ||
| 390 | +find src/components -name "*audio*.vue" | ||
| 391 | +``` | ||
| 392 | + | ||
| 393 | +### 查找 API 定义 | ||
| 394 | + | ||
| 395 | +```bash | ||
| 396 | +# 查找所有 API 文件 | ||
| 397 | +find src/api -name "*.js" | ||
| 398 | + | ||
| 399 | +# 查找特定 API | ||
| 400 | +grep -r "mapAPI" src/api/ | ||
| 401 | +grep -r "checkinAPI" src/api/ | ||
| 402 | +``` | ||
| 403 | + | ||
| 404 | +### 查找工具函数 | ||
| 405 | + | ||
| 406 | +```bash | ||
| 407 | +# 查找所有工具文件 | ||
| 408 | +ls src/utils/ | ||
| 409 | + | ||
| 410 | +# 搜索特定函数 | ||
| 411 | +grep -r "function formatTime" src/ | ||
| 412 | +``` | ||
| 413 | + | ||
| 414 | +## Git 相关 | ||
| 415 | + | ||
| 416 | +### .gitignore 关键配置 | ||
| 417 | + | ||
| 418 | +``` | ||
| 419 | +node_modules/ | ||
| 420 | +dist/ | ||
| 421 | +.history/ | ||
| 422 | +*.log | ||
| 423 | +.DS_Store | ||
| 424 | +``` | ||
| 425 | + | ||
| 426 | +### 建议添加 | ||
| 427 | + | ||
| 428 | +``` | ||
| 429 | +# IDE | ||
| 430 | +.vscode/ | ||
| 431 | +.idea/ | ||
| 432 | + | ||
| 433 | +# 本地环境 | ||
| 434 | +.env.local | ||
| 435 | +.env.*.local | ||
| 436 | + | ||
| 437 | +# 测试覆盖率 | ||
| 438 | +coverage/ | ||
| 439 | + | ||
| 440 | +# 临时文件 | ||
| 441 | +*.tmp | ||
| 442 | +*.temp | ||
| 443 | +``` | ||
| 444 | + | ||
| 445 | +## 参考文档 | ||
| 446 | + | ||
| 447 | +- [项目技术栈详解](./项目技术栈详解.md) | ||
| 448 | +- [功能模块分析](../功能模块分析/) | ||
| 449 | +- [注意事项与陷阱](../注意事项与陷阱/) |
docs/项目架构分析/项目技术栈详解.md
0 → 100644
| 1 | +# 项目技术栈详解 | ||
| 2 | + | ||
| 3 | +**最后更新**: 2026-02-09 | ||
| 4 | +**项目名称**: map-demo (地图演示项目) | ||
| 5 | +**项目类型**: Vue 3 + Vite 单页应用 | ||
| 6 | + | ||
| 7 | +## 核心技术栈 | ||
| 8 | + | ||
| 9 | +### 1. 前端框架与构建工具 | ||
| 10 | + | ||
| 11 | +| 技术 | 版本 | 用途 | | ||
| 12 | +|------|------|------| | ||
| 13 | +| **Vue** | 3.2.36 | 核心框架,使用 Composition API | | ||
| 14 | +| **Vite** | 2.9.9 | 构建工具和开发服务器 | | ||
| 15 | +| **Vue Router** | 4.0.15 | 路由管理(Hash 模式) | | ||
| 16 | +| **Pinia** | 2.0.14 | 状态管理 | | ||
| 17 | + | ||
| 18 | +### 2. UI 组件库 | ||
| 19 | + | ||
| 20 | +| 库名 | 版本 | 用途 | | ||
| 21 | +|------|------|------| | ||
| 22 | +| **Vant** | 4.9.6 | 移动端 UI 组件库(主 UI 库) | | ||
| 23 | +| **Element Plus** | 2.9.3 | PC 端 UI 组件库(辅助) | | ||
| 24 | +| **@element-plus/icons-vue** | 2.3.1 | Element Plus 图标库 | | ||
| 25 | +| **font-awesome** | 4.7.0 | 图标字体库 | | ||
| 26 | + | ||
| 27 | +### 3. 地图与全景 | ||
| 28 | + | ||
| 29 | +| 技术 | 版本 | 用途 | | ||
| 30 | +|------|------|------| | ||
| 31 | +| **@amap/amap-jsapi-loader** | 1.0.1 | 高德地图加载器 | | ||
| 32 | +| **photo-sphere-viewer** | 4.8.1 | 360° 全景查看器(旧版本) | | ||
| 33 | +| **@photo-sphere-viewer/core** | 5.7.3 | 全景查看器核心(新版本) | | ||
| 34 | +| **@photo-sphere-viewer/gallery-plugin** | 5.7.3 | 全景图库插件 | | ||
| 35 | +| **@photo-sphere-viewer/gyroscope-plugin** | 5.7.3 | 全景陀螺仪插件 | | ||
| 36 | +| **@photo-sphere-viewer/markers-plugin** | 5.7.3 | 全景标记插件 | | ||
| 37 | +| **@photo-sphere-viewer/stereo-plugin** | 5.7.3 | 全景立体插件 | | ||
| 38 | +| **@photo-sphere-viewer/virtual-tour-plugin** | 5.7.3 | 全景虚拟漫游插件 | | ||
| 39 | + | ||
| 40 | +⚠️ **注意**: 项目中同时存在新旧两个版本的全景查看器库,可能存在兼容性问题。 | ||
| 41 | + | ||
| 42 | +### 4. 音频与视频 | ||
| 43 | + | ||
| 44 | +| 技术 | 版本 | 用途 | | ||
| 45 | +|------|------|------| | ||
| 46 | +| **video.js** | 8.3.0 | 视频播放器 | | ||
| 47 | +| **@videojs-player/vue** | 1.0.0 | Vue 3 视频播放器组件 | | ||
| 48 | +| **mui-player** | 1.6.0 | 另一个视频播放器 | | ||
| 49 | + | ||
| 50 | +### 5. 二维码与扫描 | ||
| 51 | + | ||
| 52 | +| 技术 | 版本 | 用途 | | ||
| 53 | +|------|------|------| | ||
| 54 | +| **@zxing/library** | 0.21.3 | 二维码扫描库 | | ||
| 55 | + | ||
| 56 | +### 6. 工具库 | ||
| 57 | + | ||
| 58 | +| 技术 | 版本 | 用途 | | ||
| 59 | +|------|------|------| | ||
| 60 | +| **axios** | 0.27.2 | HTTP 请求库 | | ||
| 61 | +| **lodash** | 4.17.21 | JavaScript 工具库 | | ||
| 62 | +| **dayjs** | 1.11.3 | 日期处理库 | | ||
| 63 | +| **moment** | 2.29.3 | 日期处理库(旧库) | | ||
| 64 | +| **js-cookie** | 3.0.1 | Cookie 管理 | | ||
| 65 | +| **qs** | 6.10.3 | 查询字符串解析 | | ||
| 66 | +| **uuid** | 8.3.2 | UUID 生成 | | ||
| 67 | +| **file-saver** | 2.0.5 | 文件下载 | | ||
| 68 | +| **jszip** | 3.10.1 | ZIP 文件处理 | | ||
| 69 | +| **html2canvas** | 1.4.1 | 截图功能 | | ||
| 70 | +| **jquery** | 3.6.0 | DOM 操作(遗留代码) | | ||
| 71 | + | ||
| 72 | +### 7. 样式与动画 | ||
| 73 | + | ||
| 74 | +| 技术 | 版本 | 用途 | | ||
| 75 | +|------|------|------| | ||
| 76 | +| **less** | 4.1.2 | CSS 预处理器 | | ||
| 77 | +| **animate.css** | 4.1.1 | CSS 动画库 | | ||
| 78 | + | ||
| 79 | +### 8. 微信相关 | ||
| 80 | + | ||
| 81 | +| 技术 | 版本 | 用途 | | ||
| 82 | +|------|------|------| | ||
| 83 | +| **weixin-js-sdk** | 1.6.0 | 微信 JS-SDK | | ||
| 84 | + | ||
| 85 | +### 9. 开发工具 | ||
| 86 | + | ||
| 87 | +| 技术 | 版本 | 用途 | | ||
| 88 | +|------|------|------| | ||
| 89 | +| **@vitejs/plugin-vue** | 2.3.3 | Vite Vue 插件 | | ||
| 90 | +| **unplugin-vue-components** | 0.24.1 | 组件自动导入 | | ||
| 91 | +| **unplugin-auto-import** | 0.8.8 | API 自动导入 | | ||
| 92 | +| **unplugin-vue-define-options** | 0.6.1 | 支持 setup 语法中定义组件名 | | ||
| 93 | +| **vite-plugin-dynamic-import** | 0.9.6 | 动态导入增强 | | ||
| 94 | +| **@vitejs/plugin-legacy** | 1.8.2 | 旧版浏览器支持(已注释) | | ||
| 95 | +| **postcss-px-to-viewport** | 1.1.1 | px 转 vw(已注释) | | ||
| 96 | + | ||
| 97 | +### 10. 测试工具 | ||
| 98 | + | ||
| 99 | +| 技术 | 版本 | 用途 | | ||
| 100 | +|------|------|------| | ||
| 101 | +| **cypress** | 9.7.0 | E2E 测试框架 | | ||
| 102 | +| **mocha** | 10.0.0 | 单元测试框架 | | ||
| 103 | +| **chai** | 4.3.6 | 断言库 | | ||
| 104 | +| **vconsole** | 3.14.6 | 移动端调试工具 | | ||
| 105 | + | ||
| 106 | +### 11. TypeScript | ||
| 107 | + | ||
| 108 | +| 技术 | 版本 | 用途 | | ||
| 109 | +|------|------|------| | ||
| 110 | +| **typescript** | 4.7.3 | TypeScript 支持(主要用于类型检查) | | ||
| 111 | + | ||
| 112 | +## Node.js 版本要求 | ||
| 113 | + | ||
| 114 | +```json | ||
| 115 | +"engines": { | ||
| 116 | + "node": "18.13.x" | ||
| 117 | +} | ||
| 118 | +``` | ||
| 119 | + | ||
| 120 | +⚠️ **重要**: 项目要求使用 Node.js 18.13.x 版本。 | ||
| 121 | + | ||
| 122 | +## Vite 插件配置 | ||
| 123 | + | ||
| 124 | +### 1. 组件自动导入 | ||
| 125 | + | ||
| 126 | +```javascript | ||
| 127 | +Components({ | ||
| 128 | + resolvers: [VantResolver(), ElementPlusResolver()], | ||
| 129 | +}) | ||
| 130 | +``` | ||
| 131 | + | ||
| 132 | +**自动导入的组件**: | ||
| 133 | +- Vant 组件(移动端) | ||
| 134 | +- Element Plus 组件(PC 端) | ||
| 135 | + | ||
| 136 | +**效果**: 无需手动 import,直接在模板中使用组件 | ||
| 137 | + | ||
| 138 | +### 2. API 自动导入 | ||
| 139 | + | ||
| 140 | +```javascript | ||
| 141 | +AutoImport({ | ||
| 142 | + dts: 'src/auto-imports.d.ts', | ||
| 143 | + imports: ['vue', 'vue-router'], | ||
| 144 | + eslintrc: { enabled: true }, | ||
| 145 | + resolvers: [ElementPlusResolver()], | ||
| 146 | +}) | ||
| 147 | +``` | ||
| 148 | + | ||
| 149 | +**自动导入的 API**: | ||
| 150 | +- Vue: `ref`, `reactive`, `computed`, `watch`, `onMounted` 等 | ||
| 151 | +- Vue Router: `useRouter`, `useRoute` 等 | ||
| 152 | + | ||
| 153 | +### 3. Setup 语法支持 | ||
| 154 | + | ||
| 155 | +```javascript | ||
| 156 | +DefineOptions() // 允许在 <script setup> 中定义组件名 | ||
| 157 | +``` | ||
| 158 | + | ||
| 159 | +### 4. 动态导入增强 | ||
| 160 | + | ||
| 161 | +```javascript | ||
| 162 | +dynamicImport() // 支持在 import() 中使用路径别名 | ||
| 163 | +``` | ||
| 164 | + | ||
| 165 | +## 路径别名配置 | ||
| 166 | + | ||
| 167 | +```javascript | ||
| 168 | +{ | ||
| 169 | + '@': 'src', | ||
| 170 | + '@components': 'src/components', | ||
| 171 | + '@composables': 'src/composables', | ||
| 172 | + '@utils': 'src/utils', | ||
| 173 | + '@images': 'images', | ||
| 174 | + '@css': 'src/assets/css', | ||
| 175 | + '@mock': 'src/assets/mock', | ||
| 176 | + 'common': 'src/common', | ||
| 177 | +} | ||
| 178 | +``` | ||
| 179 | + | ||
| 180 | +## 环境变量 | ||
| 181 | + | ||
| 182 | +### 开发环境 (.env.development) | ||
| 183 | + | ||
| 184 | +```bash | ||
| 185 | +VITE_PORT=8006 # 开发服务器端口 | ||
| 186 | +VITE_BASE=/ # 基础路径 | ||
| 187 | +VITE_PROXY_PREFIX=/srv/ # API 代理前缀 | ||
| 188 | +VITE_PROXY_TARGET=<后端地址> # 后端代理目标 | ||
| 189 | +VITE_OUTDIR=map # 构建输出目录 | ||
| 190 | +VITE_APPID=<微信 AppID> # 微信 AppID | ||
| 191 | +VITE_OPENID=<测试 OpenID> # 测试用 OpenID | ||
| 192 | +``` | ||
| 193 | + | ||
| 194 | +## 构建配置 | ||
| 195 | + | ||
| 196 | +### 输出目录结构 | ||
| 197 | + | ||
| 198 | +``` | ||
| 199 | +dist (map) | ||
| 200 | +├── index.html | ||
| 201 | +├── static/ | ||
| 202 | +│ ├── js/ | ||
| 203 | +│ │ ├── [name]-[hash].js | ||
| 204 | +│ │ └── vendor-[hash].js | ||
| 205 | +│ ├── css/ | ||
| 206 | +│ │ └── [name]-[hash].css | ||
| 207 | +│ └── [ext]/ | ||
| 208 | +│ └── [name]-[hash].[ext] | ||
| 209 | +``` | ||
| 210 | + | ||
| 211 | +### 代码分割策略 | ||
| 212 | + | ||
| 213 | +```javascript | ||
| 214 | +manualChunks (id) { | ||
| 215 | + if (id.includes('node_modules')) { | ||
| 216 | + // 每个 node_modules 包单独打包 | ||
| 217 | + return id.toString().split('node_modules/')[1].split('/')[0].toString(); | ||
| 218 | + } | ||
| 219 | +} | ||
| 220 | +``` | ||
| 221 | + | ||
| 222 | +## 部署脚本 | ||
| 223 | + | ||
| 224 | +项目包含 4 个自动化部署脚本: | ||
| 225 | + | ||
| 226 | +| 命令 | 目标服务器 | 说明 | | ||
| 227 | +|------|-----------|------| | ||
| 228 | +| `npm run dev_upload` | ipadbiz-inner:/opt/space-dev/f | 开发环境 | | ||
| 229 | +| `npm run oa_upload` | ipadbiz-inner:/opt/oa/f | OA 环境 | | ||
| 230 | +| `npm run walk_upload` | ipadbiz-inner:/opt/walk/f | Walk 环境 | | ||
| 231 | +| `npm run xys_upload` | zhsy@oa.jcedu.org:/home/www/f:12101 | XYS 环境(SSH 端口 12101) | | ||
| 232 | + | ||
| 233 | +每个脚本执行: | ||
| 234 | +1. 构建项目 (`npm run build`) | ||
| 235 | +2. 打包 (`npm run tar`) | ||
| 236 | +3. SCP 传输 (`npm run scp-*`) | ||
| 237 | +4. 远程解压 (`npm run dec-*`) | ||
| 238 | +5. 清理本地文件 (`npm run remove_tar`, `npm run remove_dist`) | ||
| 239 | + | ||
| 240 | +## 已知问题与注意事项 | ||
| 241 | + | ||
| 242 | +### 1. 版本冲突 | ||
| 243 | + | ||
| 244 | +- ⚠️ **photo-sphere-viewer**: 同时使用 4.8.1 和 5.7.3 两个版本 | ||
| 245 | +- ⚠️ **日期库**: 同时使用 dayjs 和 moment | ||
| 246 | +- ⚠️ **视频播放器**: 同时使用 video.js 和 mui-player | ||
| 247 | + | ||
| 248 | +### 2. 已注释的功能 | ||
| 249 | + | ||
| 250 | +- 🚫 **旧版浏览器支持**: `@vitejs/plugin-legacy` 已注释 | ||
| 251 | +- 🚫 **px 转 vw**: `postcss-px-to-viewport` 已注释 | ||
| 252 | +- 🚫 **多页面应用**: `mono1`, `mono2` 入口已注释 | ||
| 253 | + | ||
| 254 | +### 3. jQuery 依赖 | ||
| 255 | + | ||
| 256 | +项目仍使用 jQuery (3.6.0),建议: | ||
| 257 | +- 新代码避免使用 jQuery | ||
| 258 | +- 逐步迁移到 Vue 原生 API 或 VueUse | ||
| 259 | + | ||
| 260 | +### 4. 全局样式注入 | ||
| 261 | + | ||
| 262 | +Less 配置中全局注入 `base.less`: | ||
| 263 | + | ||
| 264 | +```javascript | ||
| 265 | +additionalData: `@import "${path.resolve(__dirname, 'src/assets/styles/base.less')}";` | ||
| 266 | +``` | ||
| 267 | + | ||
| 268 | +**影响**: 所有 `.vue` 文件中的 `<style lang="less">` 都会自动包含全局样式。 | ||
| 269 | + | ||
| 270 | +## 技术债务 | ||
| 271 | + | ||
| 272 | +### 高优先级 | ||
| 273 | + | ||
| 274 | +1. **全景查看器版本统一**: 统一使用 5.x 版本,移除 4.x | ||
| 275 | +2. **日期库统一**: 统一使用 dayjs,移除 moment | ||
| 276 | +3. **jQuery 移除**: 逐步移除 jQuery 依赖 | ||
| 277 | + | ||
| 278 | +### 中优先级 | ||
| 279 | + | ||
| 280 | +4. **视频播放器统一**: 选择一个播放器,移除另一个 | ||
| 281 | +5. **TypeScript 类型完善**: 添加完整的类型定义 | ||
| 282 | +6. **测试覆盖**: 补充单元测试和 E2E 测试 | ||
| 283 | + | ||
| 284 | +### 低优先级 | ||
| 285 | + | ||
| 286 | +7. **代码格式化**: 添加 ESLint + Prettier | ||
| 287 | +8. **Git Hooks**: 添加 Husky + lint-staged | ||
| 288 | +9. **文档完善**: API 文档、组件文档 | ||
| 289 | + | ||
| 290 | +## 参考资源 | ||
| 291 | + | ||
| 292 | +- [Vue 3 文档](https://cn.vuejs.org/) | ||
| 293 | +- [Vite 文档](https://cn.vitejs.dev/) | ||
| 294 | +- [Vant 文档](https://vant-ui.github.io/vant/#/zh-CN) | ||
| 295 | +- [Element Plus 文档](https://element-plus.org/zh-CN/) | ||
| 296 | +- [高德地图文档](https://lbs.amap.com/api/jsapi-v2/summary) | ||
| 297 | +- [Photo Sphere Viewer 文档](https://photo-sphere-viewer.js.org/) |
src/views/checkin/CLAUDE.md
0 → 100644
| 1 | +<claude-mem-context> | ||
| 2 | +# Recent Activity | ||
| 3 | + | ||
| 4 | +<!-- This section is auto-generated by claude-mem. Edit content outside the tags. --> | ||
| 5 | + | ||
| 6 | +### Feb 9, 2026 | ||
| 7 | + | ||
| 8 | +| ID | Time | T | Title | Read | | ||
| 9 | +|----|------|---|-------|------| | ||
| 10 | +| #3978 | 11:52 AM | 🔵 | Code duplication identified in src/views directory structure | ~320 | | ||
| 11 | +</claude-mem-context> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or login to post a comment