hookehuyr

docs: 添加项目文档和分析报告

- 创建完整的 docs/ 目录结构
- 添加项目技术栈详解、目录结构分析
- 添加功能模块分析(地图、音频、VR、打卡)
- 添加已知问题汇总和开发指南
- 建立 CHANGELOG 机制

详细内容:
- 项目架构分析:技术栈详解、目录结构分析
- 功能模块分析:地图集成、音频系统、VR全景、打卡系统
- 注意事项与陷阱:已知问题汇总(版本冲突、Keep-Alive、路由等)
- 开发指南:新手入门指南、常见开发任务
- 文档索引:README.md、CHANGELOG.md

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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 +```
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 +**维护者**: 项目团队
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)
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)
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)
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)
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)
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 +祝开发顺利!🎉
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)
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 +- [注意事项与陷阱](../注意事项与陷阱/)
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/)
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