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)
This diff is collapsed. Click to expand it.
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)
This diff is collapsed. Click to expand it.
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)
This diff is collapsed. Click to expand it.
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