hookehuyr

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
# CHANGELOG
项目变更日志,记录所有重要的更新和修改。
## [2026-02-09] - 项目文档化
### 新增
- 创建完整的 docs/ 目录结构
- 添加项目技术栈详解
- 添加目录结构分析
- 添加地图集成分析
- 添加音频系统分析
- 添加 VR 全景分析
- 添加打卡系统分析
- 添加已知问题汇总
- 添加新手入门指南
- 添加常见开发任务
---
**详细信息**
- **影响文件**: docs/ 目录下所有新增文档
- **技术栈**: 文档
- **测试状态**: N/A
- **备注**: 全面分析项目,创建文档以便后续开发和维护
---
**历史记录**
本文档从 2026-02-09 开始记录。
之前的变更历史请参考 Git 提交记录:
```bash
git log --oneline --since="2026-01-01"
```
# 项目文档索引
**最后更新**: 2026-02-09
**目的**: 提供项目文档的快速导航
## 📚 文档结构
```
docs/
├── CHANGELOG.md # 变更日志
├── README.md # 本文档(索引)
├── 项目架构分析/
│ ├── 项目技术栈详解.md # 技术栈和版本信息
│ └── 目录结构分析.md # 目录结构和文件组织
├── 功能模块分析/
│ ├── 地图集成分析.md # 高德地图集成
│ ├── 音频系统分析.md # 音频播放系统
│ ├── VR全景分析.md # 360° 全景查看器
│ └── 打卡系统分析.md # 打卡功能实现
├── 注意事项与陷阱/
│ └── 已知问题汇总.md # 已知问题和解决方案
└── 开发指南/
├── 新手入门指南.md # 快速上手
└── 常见开发任务.md # 常见任务实现
```
## 🚀 快速开始
### 新手必读
1. **[新手入门指南](开发指南/新手入门指南.md)** - 快速上手项目
2. **[项目技术栈详解](项目架构分析/项目技术栈详解.md)** - 了解技术栈
3. **[已知问题汇总](注意事项与陷阱/已知问题汇总.md)** - 避免常见问题
### 核心功能
1. **[地图集成分析](功能模块分析/地图集成分析.md)** - 地图功能
2. **[音频系统分析](功能模块分析/音频系统分析.md)** - 音频播放
3. **[VR全景分析](功能模块分析/VR全景分析.md)** - 全景查看器
4. **[打卡系统分析](功能模块分析/打卡系统分析.md)** - 打卡功能
### 开发参考
1. **[常见开发任务](开发指南/常见开发任务.md)** - 任务实现指南
2. **[目录结构分析](项目架构分析/目录结构分析.md)** - 文件组织
### 问题排查
1. **[已知问题汇总](注意事项与陷阱/已知问题汇总.md)** - 问题解决方案
2. **[CHANGELOG](CHANGELOG.md)** - 变更历史
## 📖 文档阅读顺序
### 第一次接触项目
```
1. 新手入门指南
2. 项目技术栈详解
3. 目录结构分析
4. 已知问题汇总
```
### 开发新功能
```
1. 常见开发任务
2. 对应的功能模块分析
3. 已知问题汇总(避免重复踩坑)
```
### 问题排查
```
1. 已知问题汇总
2. 对应的功能模块分析
3. 技术栈详解(版本兼容性)
```
## 🔍 文档搜索
### 查找功能文档
```bash
# 搜索功能关键词
grep -r "地图" docs/
grep -r "音频" docs/
grep -r "打卡" docs/
grep -r "VR" docs/
```
### 查找问题解决方案
```bash
# 搜索问题关键词
grep -r "问题" docs/
grep -r "错误" docs/
grep -r "注意事项" docs/
```
## 📝 文档更新日志
### 2026-02-09
**新增**:
- 创建完整的 docs/ 目录结构
- 添加 10 份核心文档
- 涵盖技术栈、架构、功能、问题、开发指南
**详细内容**:
- [CHANGELOG](CHANGELOG.md#2026-02-09---项目文档化)
## 🤝 贡献指南
### 更新文档
当你修改了项目或发现了新问题时,请更新相关文档:
1. **新增功能**: 更新对应的功能模块分析文档
2. **修复 Bug**: 更新已知问题汇总文档
3. **新增依赖**: 更新技术栈详解文档
4. **结构变更**: 更新目录结构分析文档
5. **任务完成**: 更新 CHANGELOG.md
### 文档规范
- 使用中文编写文档标题和内容
- 使用清晰的章节和子章节结构
- 添加代码示例和使用说明
- 标注更新日期和作者
## 🔗 相关资源
### 项目文档
- [CLAUDE.md](../CLAUDE.md) - Claude Code 项目指南
- [README.md](../README.md) - 项目说明
### 外部资源
- [Vue 3 文档](https://cn.vuejs.org/)
- [Vant 文档](https://vant-ui.github.io/vant/#/zh-CN)
- [Element Plus 文档](https://element-plus.org/zh-CN/)
- [高德地图文档](https://lbs.amap.com/api/jsapi-v2/summary)
- [Pinia 文档](https://pinia.vuejs.org/zh/)
- [Vue Router 文档](https://router.vuejs.org/zh/)
## 📮 反馈
如果你发现文档有错误或需要补充的内容,请:
1. 提交 Issue
2. 提交 Pull Request
3. 联系维护者
---
**最后更新**: 2026-02-09
**维护者**: 项目团队
# VR 全景分析
**最后更新**: 2026-02-09
**相关文件**:
- `src/components/VRViewer/index.vue` - VR 全景查看器组件
- `src/api/map.js` - 地图数据 API(包含全景图 URL)
## 技术栈
### 全景查看器库
**Photo Sphere Viewer** - 360° 全景查看器
| 库 | 版本 | 用途 |
|------|------|------|
| `photo-sphere-viewer` | 4.8.1 | 全景查看器(旧版本,已弃用) |
| `@photo-sphere-viewer/core` | 5.7.3 | 全景查看器核心(新版本) |
| `@photo-sphere-viewer/markers-plugin` | 5.7.3 | 标记点插件 |
| `@photo-sphere-viewer/virtual-tour-plugin` | 5.7.3 | 虚拟漫游插件 |
| `@photo-sphere-viewer/gallery-plugin` | 5.7.3 | 图库插件 |
| `@photo-sphere-viewer/gyroscope-plugin` | 5.7.3 | 陀螺仪插件 |
| `@photo-sphere-viewer/stereo-plugin` | 5.7.3 | 立体插件(VR 眼镜) |
⚠️ **注意**: 项目中同时存在 4.8.1 和 5.7.3 两个版本,建议统一使用 5.x 版本。
## 核心功能
### 1. 全景图查看
**功能**:
- ✅ 360° 全景浏览
- ✅ 缩放控制
- ✅ 自动旋转
- ✅ 陀螺仪控制(移动设备)
**初始化**:
```javascript
import { Viewer } from '@photo-sphere-viewer/core';
const viewer = new Viewer({
container: document.querySelector('#viewer'),
panorama: '/path/to/panorama.jpg',
defaultZoomLvl: 0,
fisheye: true,
loadingTxt: '加载中...',
});
```
### 2. 标记点 (Markers)
**插件**: `@photo-sphere-viewer/markers-plugin`
**功能**:
- ✅ 添加标记点
- ✅ 标记点点击事件
- ✅ 自定义标记点样式
- ✅ 多边形标记点
**使用示例**:
```javascript
import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin';
const markersPlugin = new MarkersPlugin();
viewer.setPlugin(markersPlugin);
// 添加标记点
viewer.addMarker({
id: 'marker1',
position: { yaw: 0, pitch: 0 },
html: '<div class="marker">标记点</div>',
scale: [1, 1],
});
// 监听标记点点击
markersPlugin.addEventListener('select-marker', ({ marker }) => {
console.log('点击标记点:', marker.id);
});
```
### 3. 虚拟漫游 (Virtual Tour)
**插件**: `@photo-sphere-viewer/virtual-tour-plugin`
**功能**:
- ✅ 多场景切换
- ✅ 场景之间的链接
- ✅ 箭头指示器
- ✅ 自动路径播放
**使用示例**:
```javascript
import { VirtualTourPlugin } from '@photo-sphere-viewer/virtual-tour-plugin';
const virtualTour = new VirtualTourPlugin();
viewer.setPlugin(virtualTour);
// 设置节点
virtualTour.setNodes([
{
id: '1',
panorama: '/path/to/panorama1.jpg',
links: [
{
name: '场景 2',
nodeId: '2',
position: { yaw: -300, pitch: 0 },
arrowStyle: {
color: '#AEEEEE',
hoverColor: 0xaa5500,
outlineColor: 0x000000,
scale: [1, 1],
}
},
],
markers: [...],
},
{
id: '2',
panorama: '/path/to/panorama2.jpg',
links: [...],
markers: [...],
},
]);
// 切换场景
virtualTour.setCurrentNode('2');
```
### 4. 图库 (Gallery)
**插件**: `@photo-sphere-viewer/gallery-plugin`
**功能**:
- ✅ 全景图列表
- ✅ 缩略图导航
- ✅ 自动切换
**使用示例**:
```javascript
import { GalleryPlugin } from '@photo-sphere-viewer/gallery-plugin';
const gallery = new GalleryPlugin({
thumbnailSize: {
width: 100,
height: 100,
},
});
viewer.setPlugin(gallery);
// 设置图库
gallery.setItems([
{
id: '1',
panorama: '/path/to/panorama1.jpg',
name: '场景 1',
thumbnail: '/path/to/thumb1.jpg',
},
{
id: '2',
panorama: '/path/to/panorama2.jpg',
name: '场景 2',
thumbnail: '/path/to/thumb2.jpg',
},
]);
```
### 5. 陀螺仪 (Gyroscope)
**插件**: `@photo-sphere-viewer/gyroscope-plugin`
**功能**:
- ✅ 设备方向控制
- ✅ 移动设备支持
- ✅ VR 眼镜模式
**使用示例**:
```javascript
import { GyroscopePlugin } from '@photo-sphere-viewer/gyroscope-plugin';
const gyroscope = new GyroscopePlugin();
viewer.setPlugin(gyroscope);
// 启用陀螺仪
gyroscope.toggle();
```
### 6. 立体模式 (Stereo)
**插件**: `@photo-sphere-viewer/stereo-plugin`
**功能**:
- ✅ VR 眼镜模式
- ✅ 立体视图
**使用示例**:
```javascript
import { StereoPlugin } from '@photo-sphere-viewer/stereo-plugin';
const stereo = new StereoPlugin();
viewer.setPlugin(stereo);
// 启用立体模式
stereo.toggle();
```
## 组件使用
### VRViewer 组件
**路径**: `src/components/VRViewer/index.vue`
**Props**:
```javascript
{
show: Boolean // 控制显示/隐藏
}
```
**使用示例**:
```vue
<template>
<VRViewer
v-model:show="showVR"
:panorama="panoramaUrl"
:markers="markers"
@marker-click="handleMarkerClick"
/>
</template>
<script setup>
import VRViewer from '@components/VRViewer/index.vue';
import { ref } from 'vue';
const showVR = ref(false);
const panoramaUrl = ref('/vr/panorama.jpg');
const markers = ref([
{
id: 'marker1',
position: { yaw: 0, pitch: 0 },
html: '<div class="marker">标记点</div>',
}
]);
const handleMarkerClick = (marker) => {
console.log('点击标记点:', marker);
};
// 打开 VR
const openVR = () => {
showVR.value = true;
};
</script>
```
## 全景图格式
### 支持的格式
- ✅ JPEG (推荐)
- ✅ PNG
- ✅ WebP (推荐,体积更小)
### 拍摄要求
**等距柱状投影 (Equirectangular Projection)**:
- 宽高比: 2:1
- 分辨率: 至少 4096 x 2048
- 格式: 等距柱状投影
**文件大小建议**:
| 分辨率 | 文件大小 |
|--------|---------|
| 2048 x 1024 | < 2 MB |
| 4096 x 2048 | 2-5 MB |
| 8192 x 4096 | 5-15 MB |
### 图片优化
**压缩建议**:
- JPEG 质量: 80-90%
- WebP 质量: 80-90%
- 使用渐进式 JPEG
**工具推荐**:
- Adobe Lightroom
- Photoshop
- 在线压缩工具
## 性能优化
### 1. 懒加载
```javascript
// 仅在需要时加载全景图
const loadPanorama = async (url) => {
const viewer = new Viewer({
container: document.querySelector('#viewer'),
panorama: url,
loadingTxt: '加载中...',
});
await viewer.ready();
return viewer;
};
```
### 2. 缩略图
```javascript
// 先加载缩略图,再加载高清图
const viewer = new Viewer({
container: document.querySelector('#viewer'),
panorama: thumbnailUrl, // 缩略图
requestFullscreen: true,
});
// 后台加载高清图
loadFullPanorama(fullUrl).then((fullUrl) => {
viewer.setPanorama(fullUrl);
});
```
### 3. 缓存策略
```javascript
// 缓存已加载的全景图
const panoramaCache = new Map();
export const getCachedPanorama = (url) => {
if (panoramaCache.has(url)) {
return Promise.resolve(panoramaCache.get(url));
}
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
panoramaCache.set(url, url);
resolve(url);
};
img.src = url;
});
};
```
### 4. 资源释放
```javascript
// 组件卸载时释放资源
onUnmounted(() => {
if (viewer.value) {
viewer.value.destroy();
viewer.value = null;
}
});
```
## 已知问题
### 1. 版本冲突
**问题**: 同时使用 4.x 和 5.x 版本
**解决方案**: 统一使用 5.x 版本
```javascript
// ❌ 错误
import { Viewer } from 'photo-sphere-viewer'; // 4.x
import { Viewer } from '@photo-sphere-viewer/core'; // 5.x
// ✅ 正确
import { Viewer } from '@photo-sphere-viewer/core'; // 仅使用 5.x
```
### 2. jQuery 依赖
**问题**: 组件中使用 jQuery
**解决方案**: 逐步迁移到 Vue 原生 API
```javascript
// ❌ 错误
$('.psv-zoom-button').css('display', '');
// ✅ 正确
document.querySelector('.psv-zoom-button').style.display = '';
```
### 3. 内存泄漏
**问题**: 未正确销毁查看器实例
**解决方案**: 组件卸载时销毁
```javascript
onUnmounted(() => {
if (viewer) {
viewer.destroy();
}
});
```
### 4. 移动端性能
**问题**: 高分辨率全景图加载慢
**解决方案**:
- 使用 WebP 格式
- 降低分辨率
- 使用缩略图预加载
## 最佳实践
### 1. 组件使用
```vue
<!-- ✅ 推荐:使用 v-model -->
<VRViewer v-model:show="showVR" />
<!-- ❌ 不推荐:手动控制 -->
<VRViewer :show="showVR" @close="showVR = false" />
```
### 2. 全景图路径
```javascript
// ✅ 推荐:使用别名
const panoramaUrl = ref('@images/vr/panorama.jpg');
// ❌ 不推荐:使用相对路径
const panoramaUrl = ref('../../images/vr/panorama.jpg');
```
### 3. 标记点定义
```javascript
// ✅ 推荐:使用配置对象
const markerConfig = {
id: 'marker1',
position: { yaw: 0, pitch: 0 },
html: '<div class="marker">标记点</div>',
};
// ❌ 不推荐:内联定义
viewer.addMarker({
id: 'marker1',
position: { yaw: 0, pitch: 0 },
html: '<div class="marker">标记点</div>',
});
```
## 调试技巧
### 1. 查看查看器状态
```javascript
// 在控制台查看查看器状态
console.log('查看器状态:', {
position: viewer.getPosition(),
zoom: viewer.getZoomLevel(),
size: viewer.getSize(),
});
```
### 2. 监听事件
```javascript
// 监听所有事件
viewer.addEventListener('ready', () => console.log('就绪'));
viewer.addEventListener('position-updated', (e) => console.log('位置更新:', e.position));
viewer.addEventListener('zoom-updated', (e) => console.log('缩放更新:', e.zoomLevel));
```
### 3. 手动控制
```javascript
// 手动旋转
viewer.animate({
yaw: 180,
pitch: 0,
zoom: 0,
speed: 1000,
});
// 手动缩放
viewer.zoom(1);
```
## 参考文档
- [Photo Sphere Viewer 文档](https://photo-sphere-viewer.js.org/)
- [全景摄影指南](https://en.wikipedia.org/wiki/ panoramic_photography)
- [等距柱状投影](https://en.wikipedia.org/wiki/Equirectangular_projection)
# 地图集成分析
**最后更新**: 2026-02-09
**相关文件**:
- `src/api/map.js` - 地图 API
- `src/components/Floor/` - 楼层平面图组件
- `src/components/InfoWindow*.vue` - 信息窗口组件
- `src/common/map_data.js` - 地图数据处理
## 技术栈
### 地图服务提供商
**高德地图 (AMap)**
```javascript
import AMapLoader from '@amap/amap-jsapi-loader';
```
**版本**: 1.0.1
**文档**: https://lbs.amap.com/api/jsapi-v2/summary
## 核心功能
### 1. 地图数据获取
**API 端点**: `/srv/?a=map`
```javascript
// src/api/map.js
export const mapAPI = (params) => fn(fetch.get(Api.MAP, params));
```
**请求参数**:
- `id`: 地图/位置 ID
- 其他业务参数
**响应数据结构**(推测):
```javascript
{
code: 1,
data: {
coordinates: [...], // 坐标数据
markers: [...], // 标记点数据
polygons: [...], // 多边形数据
// 其他地图数据
},
msg: ''
}
```
### 2. 楼层平面图组件 (Floor)
**组件路径**: `src/components/Floor/index.vue`
**功能**:
- ✅ 多楼层切换(1-4 层)
- ✅ SVG 交互式平面图
- ✅ 标记点(Pin)显示与点击
- ✅ 区域动画(安全区、厕所、入口)
- ✅ VR 全景入口
- ✅ 搜索功能
**楼层切换**:
```javascript
// 左右箭头切换楼层
switchFloor('left') // 上一层
switchFloor('right') // 下一层
```
**标记点交互**:
```vue
<a @click="clickPin(item, $event)"
class="pin"
:data-category="item.category"
:data-space="item.space">
<!-- 标记点图标 -->
</a>
```
**区域动画**:
```javascript
// 触发区域动画
createAnimation(true, levelIndex - 1, 'safe') // 安全区动画
createAnimation(true, levelIndex - 1, 'toilet') // 厕所动画
createAnimation(true, levelIndex - 1, 'door') // 入口动画
```
**工具栏功能**:
- 🔍 搜索按钮
- ❌ 关闭按钮
- ⬆️⬇️ 楼层切换
- 📺 VR 全景入口
- ⭐ 区域动画开关
### 3. 信息窗口组件
**多种样式**:
- `InfoWindowLite.vue` - 轻量版
- `InfoWindowWarn.vue` - 警告版
- `InfoWindowYard.vue` - 院落版
**功能**:
- 显示位置信息
- 显示音频播放按钮
- 显示图片
- 显示操作按钮
### 4. 坐标系统
**数据来源**: `src/common/map_data.js`
**坐标类型**:
- **网格坐标**: 用于平面图定位
- **GPS 坐标**: 用于地图定位
- **像素坐标**: 用于 SVG 渲染
**坐标转换**(推测):
```javascript
// 网格坐标 → 像素坐标
function gridToPixel(gridX, gridY, level) {
// 转换逻辑
return { x: pixelX, y: pixelY };
}
// GPS 坐标 → 网格坐标
function gpsToGrid(lat, lng) {
// 转换逻辑
return { x: gridX, y: gridY };
}
```
## 使用示例
### 1. 加载地图数据
```javascript
import { mapAPI } from '@/api/map.js';
// 获取地图数据
const { data } = await mapAPI({ id: locationId });
// 处理坐标数据
if (data && data.coordinates) {
// 渲染地图
}
```
### 2. 使用 Floor 组件
```vue
<template>
<Floor
:level-list="floorData"
:current-level="currentFloor"
@pin-click="handlePinClick"
@floor-change="handleFloorChange"
@vr-click="handleVRClick"
/>
</template>
<script setup>
import Floor from '@components/Floor/index.vue';
import { ref } from 'vue';
const floorData = ref([
{
svg: '<svg>...</svg>',
pin: [
{
category: 'entrance',
space: 'main',
icon: 'door',
style: { left: '100px', top: '200px' }
}
]
}
]);
const currentFloor = ref(1);
const handlePinClick = (pin, event) => {
console.log('点击标记点:', pin);
// 显示信息窗口
// 播放音频
};
const handleFloorChange = (floor) => {
currentFloor.value = floor;
};
const handleVRClick = () => {
// 打开 VR 全景
};
</script>
```
### 3. 显示信息窗口
```vue
<template>
<InfoWindowLite
v-model:show="showInfoWindow"
:title="locationTitle"
:description="locationDesc"
:audio-url="audioUrl"
:images="locationImages"
/>
</template>
<script setup>
import InfoWindowLite from '@components/InfoWindowLite.vue';
import { ref } from 'vue';
const showInfoWindow = ref(false);
const locationTitle = ref('标题');
const locationDesc = ref('描述');
const audioUrl = ref('/audio/guide.mp3');
const locationImages = ref(['/img/1.jpg', '/img/2.jpg']);
</script>
```
## 地图集成流程
### 初始化流程
```
1. 加载高德地图 JS API
2. 创建地图实例
3. 获取地图数据 (mapAPI)
4. 渲染平面图 (Floor 组件)
5. 添加标记点 (Pin)
6. 绑定交互事件
```
### 交互流程
```
用户点击标记点
触发 clickPin 事件
显示信息窗口 (InfoWindow)
播放音频 (audioBackground/audioList)
可选:打开 VR 全景 (VRViewer)
```
## 地图数据结构
### 响应数据示例
```javascript
{
"code": 1,
"data": {
"id": 1,
"name": "别院",
"floors": [
{
"level": 1,
"svg": "<svg>...</svg>",
"pins": [
{
"id": 1,
"category": "entrance",
"space": "main",
"icon": "door",
"style": {
"left": "100px",
"top": "200px"
},
"info": {
"title": "正门入口",
"description": "...",
"audioUrl": "/audio/entrance.mp3",
"images": ["/img/1.jpg"]
}
}
]
}
],
"coordinates": {
"center": { "lat": 31.230416, "lng": 121.473701 },
"bounds": {
"northeast": { "lat": 31.231516, "lng": 121.474801 },
"southwest": { "lat": 31.229316, "lng": 121.472601 }
}
}
},
"msg": ""
}
```
## 性能优化
### 1. 懒加载
```javascript
// 楼层 SVG 懒加载
const loadFloorSVG = async (level) => {
const { data } = await mapAPI({ id: locationId, level });
return data.floors[level - 1].svg;
};
```
### 2. 缓存策略
```javascript
// 缓存地图数据
const mapDataCache = new Map();
export const mapAPI = async (params) => {
const cacheKey = JSON.stringify(params);
if (mapDataCache.has(cacheKey)) {
return { code: 1, data: mapDataCache.get(cacheKey) };
}
const result = await fn(fetch.get(Api.MAP, params));
if (result.code === 1) {
mapDataCache.set(cacheKey, result.data);
}
return result;
};
```
### 3. SVG 优化
- 使用简洁的 SVG 路径
- 压缩 SVG 文件大小
- 使用 `v-html` 动态渲染(注意 XSS 风险)
## 已知问题
### 1. SVG 渲染性能
**问题**: 复杂 SVG 可能导致渲染卡顿
**解决方案**:
- 简化 SVG 路径
- 使用 `will-change` 属性
- 虚拟滚动(如果标记点很多)
### 2. 坐标系混乱
**问题**: 多种坐标系容易混淆
**解决方案**:
- 统一使用网格坐标
- 提供统一的坐标转换工具函数
- 文档化每种坐标系的用途
### 3. XSS 风险
**问题**: `v-html` 渲染 SVG 可能存在 XSS 风险
**解决方案**:
```javascript
// 清理 SVG 字符串
import DOMPurify from 'dompurify';
const cleanSVG = DOMPurify.sanitize(svgString);
```
## 最佳实践
### 1. 组件使用
```vue
<!-- ✅ 推荐:使用 v-model 绑定显示状态 -->
<Floor v-model:show="showFloor" />
<!-- ❌ 不推荐:手动控制显示隐藏 -->
<Floor :show="showFloor" @close="showFloor = false" />
```
### 2. 事件处理
```javascript
// ✅ 推荐:使用事件修饰符
<div @click.stop="handlePinClick">
// ❌ 不推荐:在事件处理函数中阻止冒泡
<div @click="handlePinClick">
```
### 3. 数据缓存
```javascript
// ✅ 推荐:使用 Pinia 缓存地图数据
import { useMapStore } from '@/store/map';
const mapStore = useMapStore();
const mapData = await mapStore.fetchMapData(locationId);
// ❌ 不推荐:每次都重新获取数据
const { data } = await mapAPI({ id: locationId });
```
## 调试技巧
### 1. 查看地图数据
```javascript
// 在控制台查看地图数据
console.log('地图数据:', JSON.stringify(mapData, null, 2));
```
### 2. 查看标记点
```javascript
// 高亮所有标记点
document.querySelectorAll('.pin').forEach(pin => {
pin.style.border = '2px solid red';
});
```
### 3. 模拟点击
```javascript
// 模拟点击标记点
document.querySelector('.pin').click();
```
## 参考文档
- [高德地图 JS API 文档](https://lbs.amap.com/api/jsapi-v2/summary)
- [高德地图坐标转换](https://lbs.amap.com/api/jsapi-v2/guide/transform/convert)
- [SVG 优化指南](https://web.dev/svg-optimization/)
- [项目目录结构分析](../项目架构分析/目录结构分析.md)
This diff is collapsed. Click to expand it.
# 音频系统分析
**最后更新**: 2026-02-09
**相关文件**:
- `src/components/audioBackground.vue` - 背景音频(单模式)
- `src/components/audioBackground1.vue` - 背景音频(备用)
- `src/components/audioList.vue` - 音频列表(播放列表模式)
- `src/api/map.js` - 音频 API (`mapAudioAPI`)
## 系统架构
### 双模式设计
音频系统支持两种播放模式:
1. **单模式 (Single Mode)**
- 组件: `audioBackground.vue`, `audioBackground1.vue`
- 用途: 单个音频的背景播放
- 特点: 简单、轻量
2. **播放列表模式 (Playlist Mode)**
- 组件: `audioList.vue`
- 用途: 多个音频的顺序播放
- 特点: 支持播放列表控制
### 状态管理
**Pinia Store** (`src/store/index.js`):
```javascript
// 单模式状态
audio_entity: null, // 当前音频实体
audio_status: false, // 播放状态
// 播放列表模式状态
audio_list_entity: [], // 播放列表
audio_list_status: false, // 播放状态
```
## 核心功能
### 1. 音频加载
**API 端点**: `/srv/?a=map_audio`
```javascript
// src/api/map.js
export const mapAudioAPI = (params) => fn(fetch.get(Api.MAP_AUDIO, params));
```
**请求参数**:
- `id`: 位置/景点 ID
- `type`: 音频类型(可选)
**响应数据**:
```javascript
{
code: 1,
data: {
audioUrl: '/audio/guide.mp3',
title: '景点介绍',
duration: 120,
// 其他音频信息
},
msg: ''
}
```
### 2. 单模式播放
**组件**: `audioBackground.vue`
**功能**:
- ✅ 播放/暂停控制
- ✅ 进度条显示
- ✅ 音量控制
- ✅ 自动播放(可选)
- ✅ 循环播放(可选)
**使用示例**:
```vue
<template>
<audioBackground
:audio-url="currentAudio"
:autoplay="true"
@play="handlePlay"
@pause="handlePause"
@ended="handleEnded"
/>
</template>
<script setup>
import audioBackground from '@components/audioBackground.vue';
import { ref } from 'vue';
const currentAudio = ref('/audio/guide.mp3');
const handlePlay = () => {
console.log('开始播放');
};
const handlePause = () => {
console.log('暂停播放');
};
const handleEnded = () => {
console.log('播放结束');
};
</script>
```
### 3. 播放列表模式
**组件**: `audioList.vue`
**功能**:
- ✅ 播放列表管理
- ✅ 上一首/下一首
- ✅ 播放进度
- ✅ 当前播放高亮
- ✅ 自动播放下一首
- ✅ 列表循环
**使用示例**:
```vue
<template>
<audioList
v-model:playlist="audioPlaylist"
:current-index="currentIndex"
@play="handlePlay"
@pause="handlePause"
@next="handleNext"
@prev="handlePrev"
/>
</template>
<script setup>
import audioList from '@components/audioList.vue';
import { ref } from 'vue';
const audioPlaylist = ref([
{
id: 1,
title: '景点介绍 1',
url: '/audio/guide1.mp3',
duration: 120
},
{
id: 2,
title: '景点介绍 2',
url: '/audio/guide2.mp3',
duration: 90
}
]);
const currentIndex = ref(0);
const handlePlay = (index) => {
console.log('播放索引:', index);
};
const handleNext = () => {
if (currentIndex.value < audioPlaylist.value.length - 1) {
currentIndex.value++;
}
};
const handlePrev = () => {
if (currentIndex.value > 0) {
currentIndex.value--;
}
};
</script>
```
### 4. 音频状态同步
**Pinia Store**:
```javascript
import { defineStore } from 'pinia';
export const useAudioStore = defineStore('audio', {
state: () => ({
// 单模式
audio_entity: null,
audio_status: false,
// 播放列表模式
audio_list_entity: [],
audio_list_status: false,
audio_list_index: 0,
}),
actions: {
// 设置单模式音频
setAudio(entity) {
this.audio_entity = entity;
},
// 播放/暂停单模式
toggleAudio() {
this.audio_status = !this.audio_status;
},
// 设置播放列表
setPlaylist(list) {
this.audio_list_entity = list;
},
// 播放/暂停播放列表
togglePlaylist() {
this.audio_list_status = !this.audio_list_status;
},
// 下一首
nextTrack() {
if (this.audio_list_index < this.audio_list_entity.length - 1) {
this.audio_list_index++;
}
},
// 上一首
prevTrack() {
if (this.audio_list_index > 0) {
this.audio_list_index--;
}
},
},
});
```
## 音频格式
### 支持的格式
- ✅ MP3(推荐)
- ✅ WAV
- ✅ OGG
- ✅ AAC
- ✅ M4A
### 推荐配置
**编码格式**: MP3
**比特率**: 128 kbps
**采样率**: 44.1 kHz
**声道**: 立体声
### 文件大小建议
| 音频时长 | 推荐大小 |
|---------|---------|
| < 1 分钟 | < 1 MB |
| 1-3 分钟 | 1-3 MB |
| 3-5 分钟 | 3-5 MB |
| > 5 分钟 | 建议分割 |
## 性能优化
### 1. 懒加载
```javascript
// 仅在需要时加载音频
const loadAudio = async (url) => {
const audio = new Audio();
audio.src = url;
await audio.load(); // 预加载
return audio;
};
```
### 2. 缓存策略
```javascript
// 缓存已加载的音频
const audioCache = new Map();
export const getCachedAudio = (url) => {
if (audioCache.has(url)) {
return audioCache.get(url);
}
const audio = new Audio(url);
audioCache.set(url, audio);
return audio;
};
```
### 3. 预加载
```vue
<audio :src="audioUrl" preload="auto" />
```
**preload 选项**:
- `none`: 不预加载
- `metadata`: 仅预加载元数据(时长、尺寸等)
- `auto`: 完全预加载
### 4. 资源释放
```javascript
// 组件卸载时释放音频资源
onUnmounted(() => {
if (audio.value) {
audio.value.pause();
audio.value.src = '';
audio.value.load();
}
});
```
## 已知问题
### 1. iOS 自动播放限制
**问题**: iOS 不允许自动播放音频
**解决方案**:
```javascript
// 用户交互后播放
const playAudio = () => {
document.addEventListener('touchstart', function onTouchStart() {
// 播放音频
audio.play();
// 移除监听器
document.removeEventListener('touchstart', onTouchStart);
}, { once: true });
};
```
### 2. 多个音频冲突
**问题**: 同时播放多个音频
**解决方案**:
```javascript
// 播放新音频前停止其他音频
const playAudio = (newAudio) => {
if (currentAudio.value) {
currentAudio.value.pause();
}
currentAudio.value = newAudio;
currentAudio.value.play();
};
```
### 3. 音频加载失败
**问题**: 网络错误或文件不存在
**解决方案**:
```javascript
audio.addEventListener('error', (e) => {
console.error('音频加载失败:', e);
// 显示错误提示
showToast('音频加载失败,请检查网络');
// 尝试重新加载
setTimeout(() => {
audio.load();
}, 1000);
});
```
## 最佳实践
### 1. 音频路径管理
```javascript
// ✅ 推荐:使用别名
const audioUrl = ref('@images/audio/guide.mp3');
// ❌ 不推荐:使用相对路径
const audioUrl = ref('../../images/audio/guide.mp3');
```
### 2. 音频加载状态
```vue
<template>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="error" class="error">加载失败</div>
<audio v-else :src="audioUrl" @loadeddata="onLoaded" />
</template>
<script setup>
import { ref } from 'vue';
const loading = ref(true);
const error = ref(false);
const onLoaded = () => {
loading.value = false;
};
</script>
```
### 3. 音频控制
```javascript
// ✅ 推荐:使用 Vue 组件
<audioBackground v-model:playing="isPlaying" :audio-url="audioUrl" />
// ❌ 不推荐:直接操作 DOM
document.querySelector('audio').play();
```
## 调试技巧
### 1. 查看音频状态
```javascript
// 在控制台查看音频状态
console.log('音频状态:', {
src: audio.src,
duration: audio.duration,
currentTime: audio.currentTime,
paused: audio.paused,
ended: audio.ended,
});
```
### 2. 监听音频事件
```javascript
audio.addEventListener('loadstart', () => console.log('开始加载'));
audio.addEventListener('canplay', () => console.log('可以播放'));
audio.addEventListener('play', () => console.log('播放'));
audio.addEventListener('pause', () => console.log('暂停'));
audio.addEventListener('ended', () => console.log('结束'));
audio.addEventListener('error', (e) => console.error('错误:', e));
```
### 3. 模拟音频播放
```javascript
// 在控制台手动播放音频
document.querySelector('audio').play();
```
## 参考文档
- [HTML5 Audio API](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio)
- [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
- [音频格式对比](https://en.wikipedia.org/wiki/Audio_file_format)
This diff is collapsed. Click to expand it.
# 新手入门指南
**最后更新**: 2026-02-09
**目的**: 帮助新开发者快速上手项目
## 环境准备
### 1. 安装 Node.js
**要求**: Node.js 18.13.x
```bash
# 检查 Node.js 版本
node -v
# 如果版本不符合,安装 nvm
# macOS/Linux
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Windows
# 下载安装程序: https://github.com/coreybutler/nvm-windows/releases
# 安装 Node.js 18.13.x
nvm install 18.13.0
nvm use 18.13.0
```
### 2. 克隆项目
```bash
# 克隆项目
git clone <repository-url>
cd map-demo
# 安装依赖
npm install
```
### 3. 启动开发服务器
```bash
# 启动开发服务器(localhost)
npm run dev
# 启动开发服务器(网络访问)
npm run start
```
访问: `http://localhost:8006`
## 项目结构快速了解
### 核心目录
```
src/
├── api/ # API 接口
├── components/ # 公共组件
├── views/ # 页面组件
├── store/ # 状态管理
├── router/ # 路由配置
├── utils/ # 工具函数
└── common/ # 通用代码
```
### 关键文件
- `src/main.js` - 入口文件
- `src/App.vue` - 根组件
- `vite.config.js` - Vite 配置
- `package.json` - 项目依赖
## 开发工作流
### 1. 创建新页面
#### 步骤 1: 创建页面文件
```bash
# 在 src/views/ 下创建页面目录
mkdir src/views/mypage
# 创建页面文件
touch src/views/mypage/index.vue
```
#### 步骤 2: 编写页面组件
```vue
<!-- src/views/mypage/index.vue -->
<template>
<div class="mypage">
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const title = ref('我的页面');
const content = ref('页面内容');
</script>
<style lang="less" scoped>
.mypage {
padding: 20px;
h1 {
font-size: 24px;
color: #333;
}
p {
font-size: 16px;
color: #666;
}
}
</style>
```
#### 步骤 3: 添加路由
```javascript
// src/router/routes/modules/mypage/index.js(创建新文件)
export default {
path: '/mypage',
name: 'MyPage',
component: () => import('@/views/mypage/index.vue'),
meta: {
title: '我的页面',
},
};
```
#### 步骤 4: 访问页面
```
URL: http://localhost:8006/index.html#/index.html/mypage
```
### 2. 创建新组件
#### 步骤 1: 创建组件文件
```bash
# 创建组件目录
mkdir src/components/MyComponent
# 创建组件文件
touch src/components/MyComponent/index.vue
```
#### 步骤 2: 编写组件
```vue
<!-- src/components/MyComponent/index.vue -->
<template>
<div class="my-component">
<h2>{{ title }}</h2>
<slot></slot>
</div>
</template>
<script setup>
/**
* 我的组件
*
* @description 这是一个示例组件
* @component MyComponent
* @example
* <MyComponent title="标题">
* 内容
* </MyComponent>
*/
const props = defineProps({
/** 标题 */
title: {
type: String,
default: '',
},
});
const emit = defineEmits({
/** 点击事件 */
click: (payload) => true,
});
</script>
<style lang="less" scoped>
.my-component {
h2 {
font-size: 20px;
}
}
</style>
```
#### 步骤 3: 使用组件
```vue
<template>
<div>
<MyComponent title="组件标题">
组件内容
</MyComponent>
</div>
</template>
<script setup>
import MyComponent from '@components/MyComponent/index.vue';
</script>
```
### 3. 添加 API 接口
#### 步骤 1: 创建 API 文件
```bash
touch src/api/mypage.js
```
#### 步骤 2: 定义 API
```javascript
/**
* 我的页面 API
*
* @description 我的页面相关接口
*/
import { fn, fetch } from '@/api/fn';
const Api = {
MYPAGE: '/srv/?a=mypage',
};
/**
* 获取我的页面数据
*
* @param {Object} params - 请求参数
* @param {number} params.id - ID
* @returns {Promise<Object>} 响应数据
*/
export const myPageAPI = (params) => fn(fetch.get(Api.MYPAGE, params));
```
#### 步骤 3: 使用 API
```javascript
import { myPageAPI } from '@/api/mypage.js';
const { data } = await myPageAPI({ id: 123 });
if (data) {
console.log('获取数据成功:', data);
}
```
### 4. 添加状态管理
#### 步骤 1: 创建 Store
```javascript
// src/store/mypage.js
import { defineStore } from 'pinia';
export const useMyPageStore = defineStore('mypage', {
state: () => ({
data: null,
loading: false,
error: null,
}),
getters: {
hasData: (state) => state.data !== null,
},
actions: {
async fetchData(id) {
this.loading = true;
this.error = null;
try {
const { data } = await myPageAPI({ id });
this.data = data;
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
},
},
});
```
#### 步骤 2: 使用 Store
```vue
<script setup>
import { useMyPageStore } from '@/store/mypage';
import { onMounted } from 'vue';
const myPageStore = useMyPageStore();
onMounted(() => {
myPageStore.fetchData(123);
});
</script>
<template>
<div v-if="myPageStore.loading">加载中...</div>
<div v-else-if="myPageStore.error">{{ myPageStore.error }}</div>
<div v-else>{{ myPageStore.data }}</div>
</template>
```
## 常见开发任务
### 修改样式
```vue
<template>
<div class="page">内容</div>
</template>
<style lang="less" scoped>
.page {
/* 使用 Less */
padding: 20px;
/* 嵌套选择器 */
h1 {
font-size: 24px;
/* 伪元素 */
&:hover {
color: blue;
}
}
}
</style>
```
### 添加路由跳转
```javascript
import { useRouter } from 'vue-router';
const router = useRouter();
// 方式 1: 路径跳转
router.push('/mypage');
// 方式 2: 命名路由
router.push({ name: 'MyPage' });
// 方式 3: 带参数跳转
router.push({
name: 'MyPage',
query: { id: 123 }
});
// 方式 4: 带参数跳转(Hash 模式)
router.push('/index.html/mypage?id=123');
```
### 使用 Vant 组件
```vue
<template>
<!-- Vant 组件自动导入,无需手动 import -->
<van-button type="primary">按钮</van-button>
<van-cell-group>
<van-cell title="单元格" value="内容" />
</van-cell-group>
<van-image :src="image_url" />
</template>
```
### 使用 Element Plus 组件
```vue
<template>
<!-- Element Plus 组件自动导入,无需手动 import -->
<el-button type="primary">按钮</el-button>
<el-table :data="tableData">
<el-table-column prop="name" label="姓名" />
</el-table>
</template>
```
### 处理响应式数据
```javascript
import { ref, reactive, computed } from 'vue';
// ref:基本类型
const count = ref(0);
const message = ref('Hello');
// reactive:对象类型
const user = reactive({
name: '',
age: 0,
});
// computed:计算属性
const fullName = computed(() => {
return `${user.name} (${user.age}岁)`;
});
// 修改数据
count.value++; // ref 需要 .value
user.name = '张三'; // reactive 不需要 .value
```
## 调试技巧
### 1. 使用 VConsole
```javascript
// 引入 VConsole
import { initVConsole } from '@/utils/vconsole';
// 在开发环境启用
if (import.meta.env.DEV) {
initVConsole();
}
```
### 2. 查看状态
```javascript
// 在控制台查看 Pinia 状态
import { useMainStore } from '@/store';
const mainStore = useMainStore();
console.log('Main Store:', mainStore.$state);
```
### 3. 查看路由
```javascript
// 查看当前路由
import { useRoute } from 'vue-router';
const route = useRoute();
console.log('当前路由:', route.path, route.query, route.params);
```
## 部署
### 本地构建
```bash
# 构建
npm run build
# 预览
npm run serve
```
### 部署到服务器
```bash
# 开发环境
npm run dev_upload
# OA 环境
npm run oa_upload
# Walk 环境
npm run walk_upload
# XYS 环境
npm run xys_upload
```
## 常见问题
### Q1: 组件自动导入不生效?
**A**: 检查 `vite.config.js` 中的组件解析器配置。
```javascript
Components({
resolvers: [VantResolver(), ElementPlusResolver()],
}),
```
### Q2: API 自动导入不生效?
**A**: 检查 `vite.config.js` 中的自动导入配置。
```javascript
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts',
}),
```
### Q3: 路由跳转 404?
**A**: 检查路由是否包含 `#/index.html` 前缀。
```javascript
// ✅ 正确
router.push('/index.html/mypage')
// ❌ 错误
router.push('/mypage')
```
### Q4: 样式不生效?
**A**: 检查是否添加了 `scoped`
```vue
<style lang="less" scoped>
/* 添加 scoped */
</style>
```
### Q5: 图片加载失败?
**A**: 检查图片路径是否正确。
```javascript
// ✅ 正确:使用别名
const imageUrl = ref('@images/logo.png');
// ✅ 正确:使用绝对路径
const imageUrl = ref('/images/logo.png');
// ❌ 错误:相对路径
const imageUrl = ref('../../images/logo.png');
```
## 学习资源
### Vue 3
- [Vue 3 官方文档](https://cn.vuejs.org/)
- [Vue 3 Composition API](https://cn.vuejs.org/guide/extras/composition-api-faq.html)
### Vant
- [Vant 官方文档](https://vant-ui.github.io/vant/#/zh-CN)
- [Vant 移动端组件](https://vant-ui.github.io/vant/#/zh-CN/home)
### Element Plus
- [Element Plus 官方文档](https://element-plus.org/zh-CN/)
- [Element Plus 组件](https://element-plus.org/zh-CN/component/button.html)
### Pinia
- [Pinia 官方文档](https://pinia.vuejs.org/zh/)
- [Pinia 核心概念](https://pinia.vuejs.org/zh/core-concepts/)
### Vue Router
- [Vue Router 官方文档](https://router.vuejs.org/zh/)
- [Vue Router Hash 模式](https://router.vuejs.org/zh/guide/essentials/history-mode.html)
## 下一步
1. 阅读 [项目技术栈详解](../项目架构分析/项目技术栈详解.md)
2. 阅读 [目录结构分析](../项目架构分析/目录结构分析.md)
3. 阅读 [已知问题汇总](../注意事项与陷阱/已知问题汇总.md)
4. 开始开发!
祝开发顺利!🎉
# 已知问题汇总
**最后更新**: 2026-02-09
**目的**: 记录项目中的已知问题和解决方案,避免后续开发中重复踩坑
## 🔴 高优先级问题
### 1. 版本冲突
#### 问题描述
项目中同时存在同一库的不同版本,可能导致兼容性问题。
#### 具体案例
**Photo Sphere Viewer**:
```javascript
// package.json
"photo-sphere-viewer": "^4.8.1",
"@photo-sphere-viewer/core": "^5.7.3",
```
**影响**:
- VR 全景功能可能不稳定
- 包体积增大
- API 不一致
**解决方案**:
```javascript
// 统一使用 5.x 版本
// 1. 移除旧版本
npm uninstall photo-sphere-viewer
// 2. 更新导入语句
// 之前: import { Viewer } from 'photo-sphere-viewer';
// 之后: import { Viewer } from '@photo-sphere-viewer/core';
```
**日期管理库**:
```javascript
// package.json
"dayjs": "^1.11.3",
"moment": "^2.29.3",
```
**影响**:
- 包体积增大
- API 不一致
**解决方案**:
```javascript
// 统一使用 dayjs(更轻量)
// 1. 移除 moment
npm uninstall moment
// 2. 替换所有 moment 调用为 dayjs
// moment(date).format('YYYY-MM-DD')
// → dayjs(date).format('YYYY-MM-DD')
```
### 2. Keep-Alive 缓存问题
#### 问题描述
`keepPages` 空数组会导致所有页面都被缓存。
#### 具体案例
```javascript
// src/store/index.js:25
keepPages: ['default'], // 很坑爹,空值全部都缓存
```
**影响**:
- 如果 `keepPages``[]`,所有页面都会被缓存
- 页面状态不会重置
- 可能导致内存泄漏
**解决方案**:
```javascript
// ✅ 正确:至少包含 'default' 作为占位符
keepPages: ['default']
// ❌ 错误:空数组
keepPages: [] // 会导致所有页面都被缓存
```
**使用方法**:
```javascript
// 添加需要缓存的页面
const keepThisPage = () => {
const keepPages = [...mainStore.keepPages];
if (!keepPages.includes('PageName')) {
keepPages.push('PageName');
}
mainStore.keepPages = keepPages;
};
```
### 3. 路由 Hash 模式
#### 问题描述
项目使用 Hash 模式,所有路由必须包含 `#/index.html` 前缀。
#### 具体案例
```javascript
// src/router/index.js
history: createWebHashHistory('/index.html')
```
**影响**:
- URL 格式: `http://localhost:8006/index.html#/index.html/views/page`
- 如果忘记前缀,路由无法匹配
**解决方案**:
```javascript
// ✅ 正确:使用完整路径
router.push('/index.html/views/page')
// ❌ 错误:缺少前缀
router.push('/views/page')
// ✅ 推荐:使用路由名称
router.push({ name: 'PageName' })
```
## 🟡 中优先级问题
### 4. jQuery 依赖
#### 问题描述
项目仍使用 jQuery,与 Vue 3 冲突。
#### 具体案例
```javascript
// src/components/VRViewer/index.vue:23
import $ from 'jquery';
// 使用 jQuery 操作 DOM
$('.psv-zoom-button').css('display', '');
```
**影响**:
- 不符合 Vue 3 理念
- 性能较差
- 不利于维护
**解决方案**:
```javascript
// ❌ 错误:使用 jQuery
import $ from 'jquery';
$('.psv-zoom-button').css('display', '');
// ✅ 正确:使用 Vue 原生 API
import { ref, onMounted } from 'vue';
const zoomButton = ref(null);
onMounted(() => {
if (zoomButton.value) {
zoomButton.value.style.display = '';
}
});
```
**迁移优先级**:
1. 新代码避免使用 jQuery
2. 逐步重构现有 jQuery 代码
3. 最终完全移除 jQuery 依赖
### 5. 全局样式注入
#### 问题描述
Less 配置中全局注入 `base.less`,所有组件都会包含全局样式。
#### 具体案例
```javascript
// vite.config.js:107
additionalData: `@import "${path.resolve(__dirname, 'src/assets/styles/base.less')}";`
```
**影响**:
- 所有 `.vue` 文件中的 `<style lang="less">` 都会包含全局样式
- 可能导致样式冲突
- 编译时间增加
**解决方案**:
```less
/* ✅ 推荐:仅在需要时导入 */
@import './base.less';
/* ❌ 不推荐:重复导入 */
/* base.less 已经全局注入,无需重复导入 */
```
### 6. SVG 渲染性能
#### 问题描述
复杂 SVG 可能导致渲染卡顿。
#### 具体案例
```vue
<!-- src/components/Floor/index.vue -->
<div v-html="level.svg"></div>
```
**影响**:
- 楼层平面图 SVG 可能很复杂
- 渲染时间长
- 可能导致页面卡顿
**解决方案**:
```javascript
// 1. 简化 SVG 路径
// 2. 使用懒加载
const loadFloorSVG = async (level) => {
const { data } = await mapAPI({ id: locationId, level });
return data.svg;
};
// 3. 使用 will-change 优化
.floor-svg {
will-change: transform;
}
```
### 7. XSS 风险
#### 问题描述
使用 `v-html` 渲染 SVG 可能存在 XSS 风险。
#### 具体案例
```vue
<div v-html="level.svg"></div>
```
**风险**:
- 如果 SVG 来自用户输入或不可信来源
- 可能包含恶意脚本
**解决方案**:
```javascript
// 安装 DOMPurify
npm install dompurify
// 清理 SVG
import DOMPurify from 'dompurify';
const cleanSVG = DOMPurify.sanitize(svgString);
```
## 🟢 低优先级问题
### 8. 代码重复
#### 问题描述
多个页面中存在重复的代码模式。
#### 具体案例
**信息窗口组件**:
- `InfoWindowLite.vue`
- `InfoWindowWarn.vue`
- `InfoWindowYard.vue`
- `InfoPopupLite.vue`
- `InfoPopupWarn.vue`
**影响**:
- 维护成本高
- 代码重复
**解决方案**:
```vue
// ✅ 推荐:合并为一个组件,使用 props 控制样式
<InfoWindow
:variant="'lite' | 'warn' | 'yard'"
:title="title"
:description="description"
/>
// ❌ 不推荐:维护多个相似组件
<InfoWindowLite :title="title" />
<InfoWindowWarn :title="title" />
<InfoWindowYard :title="title" />
```
### 9. 缺少类型定义
#### 问题描述
项目使用 TypeScript,但缺少完整的类型定义。
#### 具体案例
```javascript
// src/auto-imports.d.ts(自动生成)
// 但很多组件和函数缺少类型定义
```
**影响**:
- IDE 提示不完整
- 容易出现类型错误
- 不利于重构
**解决方案**:
```typescript
// 为组件添加类型定义
// src/components/Floor/index.vue
interface FloorData {
svg: string;
pin: PinData[];
}
interface PinData {
category: string;
space: string;
icon: string;
style: Record<string, string>;
info?: InfoData;
}
```
### 10. 测试覆盖不足
#### 问题描述
项目缺少完整的测试覆盖。
#### 具体案例
```javascript
// src/test/mocha/test.js(仅有一个测试文件)
```
**影响**:
- 重构时容易引入 Bug
- 难以保证代码质量
**解决方案**:
```javascript
// 补充单元测试
// tests/components/Floor.spec.js
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Floor from '@/components/Floor/index.vue';
describe('Floor', () => {
it('should render floor list', () => {
const wrapper = mount(Floor, {
props: {
levelList: [...],
},
});
expect(wrapper.findAll('.level').length).toBe(4);
});
});
```
## ⚠️ 注意事项
### 1. 环境变量
**问题**: 环境变量必须以 `VITE_` 开头
```javascript
// ✅ 正确
VITE_PORT=8006
VITE_PROXY_TARGET=http://api.example.com
// ❌ 错误
PORT=8006
PROXY_TARGET=http://api.example.com
```
### 2. 路径别名
**问题**: 使用路径别名时必须使用绝对路径
```javascript
// ✅ 正确
import Floor from '@components/Floor/index.vue';
// ❌ 错误
import Floor from '../../components/Floor/index.vue';
```
### 3. 组件命名
**问题**: 组件文件名必须使用 PascalCase
```javascript
// ✅ 正确
audioList.vue
InfoWindowLite.vue
VRViewer/index.vue
// ❌ 错误
audio-list.vue
infoWindowLite.vue
vr-viewer/index.vue
```
### 4. API 响应检查
**问题**: 所有 API 调用必须检查 `res.code === 1`
```javascript
// ✅ 正确
const { data } = await mapAPI(params);
if (res.code === 1 && res.data) {
// 处理数据
}
// ❌ 错误
const { data } = await mapAPI(params);
if (data) {
// res.code 未检查,可能处理错误数据
}
```
### 5. 异步错误处理
**问题**: 所有 async 函数必须有 try-catch
```javascript
// ✅ 正确
const fetchData = async () => {
try {
const { data } = await mapAPI(params);
return data;
} catch (err) {
console.error('请求失败:', err);
return null;
}
};
// ❌ 错误
const fetchData = async () => {
const { data } = await mapAPI(params);
return data;
// 错误未处理,可能导致应用崩溃
};
```
## 📝 待解决问题
### 1. 多页面应用配置未清理
**问题**: `src/packages/` 目录存在但未使用
**影响**:
- 代码混乱
- 构建时间增加
**建议**: 归档或删除未使用的多页面应用代码
### 2. 文件历史目录
**问题**: `.history/` 目录存在
**影响**:
- Git 仓库混乱
- 磁盘空间浪费
**建议**: 添加到 `.gitignore`
### 3. 控制台调试代码
**问题**: 项目中可能存在 `console.log``debugger`
**影响**:
- 生产环境性能
- 可能泄露敏感信息
**建议**: 使用 ESLint 检测并移除调试代码
### 4. 图片优化
**问题**: 图片未压缩和优化
**影响**:
- 加载速度慢
- 流量消耗大
**建议**:
- 使用 WebP 格式
- 压缩图片
- 使用 CDN 优化
## 参考文档
- [项目技术栈详解](../项目架构分析/项目技术栈详解.md)
- [目录结构分析](../项目架构分析/目录结构分析.md)
- [地图集成分析](../功能模块分析/地图集成分析.md)
- [音频系统分析](../功能模块分析/音频系统分析.md)
- [VR全景分析](../功能模块分析/VR全景分析.md)
- [打卡系统分析](../功能模块分析/打卡系统分析.md)
This diff is collapsed. Click to expand it.
# 项目技术栈详解
**最后更新**: 2026-02-09
**项目名称**: map-demo (地图演示项目)
**项目类型**: Vue 3 + Vite 单页应用
## 核心技术栈
### 1. 前端框架与构建工具
| 技术 | 版本 | 用途 |
|------|------|------|
| **Vue** | 3.2.36 | 核心框架,使用 Composition API |
| **Vite** | 2.9.9 | 构建工具和开发服务器 |
| **Vue Router** | 4.0.15 | 路由管理(Hash 模式) |
| **Pinia** | 2.0.14 | 状态管理 |
### 2. UI 组件库
| 库名 | 版本 | 用途 |
|------|------|------|
| **Vant** | 4.9.6 | 移动端 UI 组件库(主 UI 库) |
| **Element Plus** | 2.9.3 | PC 端 UI 组件库(辅助) |
| **@element-plus/icons-vue** | 2.3.1 | Element Plus 图标库 |
| **font-awesome** | 4.7.0 | 图标字体库 |
### 3. 地图与全景
| 技术 | 版本 | 用途 |
|------|------|------|
| **@amap/amap-jsapi-loader** | 1.0.1 | 高德地图加载器 |
| **photo-sphere-viewer** | 4.8.1 | 360° 全景查看器(旧版本) |
| **@photo-sphere-viewer/core** | 5.7.3 | 全景查看器核心(新版本) |
| **@photo-sphere-viewer/gallery-plugin** | 5.7.3 | 全景图库插件 |
| **@photo-sphere-viewer/gyroscope-plugin** | 5.7.3 | 全景陀螺仪插件 |
| **@photo-sphere-viewer/markers-plugin** | 5.7.3 | 全景标记插件 |
| **@photo-sphere-viewer/stereo-plugin** | 5.7.3 | 全景立体插件 |
| **@photo-sphere-viewer/virtual-tour-plugin** | 5.7.3 | 全景虚拟漫游插件 |
⚠️ **注意**: 项目中同时存在新旧两个版本的全景查看器库,可能存在兼容性问题。
### 4. 音频与视频
| 技术 | 版本 | 用途 |
|------|------|------|
| **video.js** | 8.3.0 | 视频播放器 |
| **@videojs-player/vue** | 1.0.0 | Vue 3 视频播放器组件 |
| **mui-player** | 1.6.0 | 另一个视频播放器 |
### 5. 二维码与扫描
| 技术 | 版本 | 用途 |
|------|------|------|
| **@zxing/library** | 0.21.3 | 二维码扫描库 |
### 6. 工具库
| 技术 | 版本 | 用途 |
|------|------|------|
| **axios** | 0.27.2 | HTTP 请求库 |
| **lodash** | 4.17.21 | JavaScript 工具库 |
| **dayjs** | 1.11.3 | 日期处理库 |
| **moment** | 2.29.3 | 日期处理库(旧库) |
| **js-cookie** | 3.0.1 | Cookie 管理 |
| **qs** | 6.10.3 | 查询字符串解析 |
| **uuid** | 8.3.2 | UUID 生成 |
| **file-saver** | 2.0.5 | 文件下载 |
| **jszip** | 3.10.1 | ZIP 文件处理 |
| **html2canvas** | 1.4.1 | 截图功能 |
| **jquery** | 3.6.0 | DOM 操作(遗留代码) |
### 7. 样式与动画
| 技术 | 版本 | 用途 |
|------|------|------|
| **less** | 4.1.2 | CSS 预处理器 |
| **animate.css** | 4.1.1 | CSS 动画库 |
### 8. 微信相关
| 技术 | 版本 | 用途 |
|------|------|------|
| **weixin-js-sdk** | 1.6.0 | 微信 JS-SDK |
### 9. 开发工具
| 技术 | 版本 | 用途 |
|------|------|------|
| **@vitejs/plugin-vue** | 2.3.3 | Vite Vue 插件 |
| **unplugin-vue-components** | 0.24.1 | 组件自动导入 |
| **unplugin-auto-import** | 0.8.8 | API 自动导入 |
| **unplugin-vue-define-options** | 0.6.1 | 支持 setup 语法中定义组件名 |
| **vite-plugin-dynamic-import** | 0.9.6 | 动态导入增强 |
| **@vitejs/plugin-legacy** | 1.8.2 | 旧版浏览器支持(已注释) |
| **postcss-px-to-viewport** | 1.1.1 | px 转 vw(已注释) |
### 10. 测试工具
| 技术 | 版本 | 用途 |
|------|------|------|
| **cypress** | 9.7.0 | E2E 测试框架 |
| **mocha** | 10.0.0 | 单元测试框架 |
| **chai** | 4.3.6 | 断言库 |
| **vconsole** | 3.14.6 | 移动端调试工具 |
### 11. TypeScript
| 技术 | 版本 | 用途 |
|------|------|------|
| **typescript** | 4.7.3 | TypeScript 支持(主要用于类型检查) |
## Node.js 版本要求
```json
"engines": {
"node": "18.13.x"
}
```
⚠️ **重要**: 项目要求使用 Node.js 18.13.x 版本。
## Vite 插件配置
### 1. 组件自动导入
```javascript
Components({
resolvers: [VantResolver(), ElementPlusResolver()],
})
```
**自动导入的组件**:
- Vant 组件(移动端)
- Element Plus 组件(PC 端)
**效果**: 无需手动 import,直接在模板中使用组件
### 2. API 自动导入
```javascript
AutoImport({
dts: 'src/auto-imports.d.ts',
imports: ['vue', 'vue-router'],
eslintrc: { enabled: true },
resolvers: [ElementPlusResolver()],
})
```
**自动导入的 API**:
- Vue: `ref`, `reactive`, `computed`, `watch`, `onMounted`
- Vue Router: `useRouter`, `useRoute`
### 3. Setup 语法支持
```javascript
DefineOptions() // 允许在 <script setup> 中定义组件名
```
### 4. 动态导入增强
```javascript
dynamicImport() // 支持在 import() 中使用路径别名
```
## 路径别名配置
```javascript
{
'@': 'src',
'@components': 'src/components',
'@composables': 'src/composables',
'@utils': 'src/utils',
'@images': 'images',
'@css': 'src/assets/css',
'@mock': 'src/assets/mock',
'common': 'src/common',
}
```
## 环境变量
### 开发环境 (.env.development)
```bash
VITE_PORT=8006 # 开发服务器端口
VITE_BASE=/ # 基础路径
VITE_PROXY_PREFIX=/srv/ # API 代理前缀
VITE_PROXY_TARGET=<后端地址> # 后端代理目标
VITE_OUTDIR=map # 构建输出目录
VITE_APPID=<微信 AppID> # 微信 AppID
VITE_OPENID=<测试 OpenID> # 测试用 OpenID
```
## 构建配置
### 输出目录结构
```
dist (map)
├── index.html
├── static/
│ ├── js/
│ │ ├── [name]-[hash].js
│ │ └── vendor-[hash].js
│ ├── css/
│ │ └── [name]-[hash].css
│ └── [ext]/
│ └── [name]-[hash].[ext]
```
### 代码分割策略
```javascript
manualChunks (id) {
if (id.includes('node_modules')) {
// 每个 node_modules 包单独打包
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
}
```
## 部署脚本
项目包含 4 个自动化部署脚本:
| 命令 | 目标服务器 | 说明 |
|------|-----------|------|
| `npm run dev_upload` | ipadbiz-inner:/opt/space-dev/f | 开发环境 |
| `npm run oa_upload` | ipadbiz-inner:/opt/oa/f | OA 环境 |
| `npm run walk_upload` | ipadbiz-inner:/opt/walk/f | Walk 环境 |
| `npm run xys_upload` | zhsy@oa.jcedu.org:/home/www/f:12101 | XYS 环境(SSH 端口 12101) |
每个脚本执行:
1. 构建项目 (`npm run build`)
2. 打包 (`npm run tar`)
3. SCP 传输 (`npm run scp-*`)
4. 远程解压 (`npm run dec-*`)
5. 清理本地文件 (`npm run remove_tar`, `npm run remove_dist`)
## 已知问题与注意事项
### 1. 版本冲突
- ⚠️ **photo-sphere-viewer**: 同时使用 4.8.1 和 5.7.3 两个版本
- ⚠️ **日期库**: 同时使用 dayjs 和 moment
- ⚠️ **视频播放器**: 同时使用 video.js 和 mui-player
### 2. 已注释的功能
- 🚫 **旧版浏览器支持**: `@vitejs/plugin-legacy` 已注释
- 🚫 **px 转 vw**: `postcss-px-to-viewport` 已注释
- 🚫 **多页面应用**: `mono1`, `mono2` 入口已注释
### 3. jQuery 依赖
项目仍使用 jQuery (3.6.0),建议:
- 新代码避免使用 jQuery
- 逐步迁移到 Vue 原生 API 或 VueUse
### 4. 全局样式注入
Less 配置中全局注入 `base.less`:
```javascript
additionalData: `@import "${path.resolve(__dirname, 'src/assets/styles/base.less')}";`
```
**影响**: 所有 `.vue` 文件中的 `<style lang="less">` 都会自动包含全局样式。
## 技术债务
### 高优先级
1. **全景查看器版本统一**: 统一使用 5.x 版本,移除 4.x
2. **日期库统一**: 统一使用 dayjs,移除 moment
3. **jQuery 移除**: 逐步移除 jQuery 依赖
### 中优先级
4. **视频播放器统一**: 选择一个播放器,移除另一个
5. **TypeScript 类型完善**: 添加完整的类型定义
6. **测试覆盖**: 补充单元测试和 E2E 测试
### 低优先级
7. **代码格式化**: 添加 ESLint + Prettier
8. **Git Hooks**: 添加 Husky + lint-staged
9. **文档完善**: API 文档、组件文档
## 参考资源
- [Vue 3 文档](https://cn.vuejs.org/)
- [Vite 文档](https://cn.vitejs.dev/)
- [Vant 文档](https://vant-ui.github.io/vant/#/zh-CN)
- [Element Plus 文档](https://element-plus.org/zh-CN/)
- [高德地图文档](https://lbs.amap.com/api/jsapi-v2/summary)
- [Photo Sphere Viewer 文档](https://photo-sphere-viewer.js.org/)
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Feb 9, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #3978 | 11:52 AM | 🔵 | Code duplication identified in src/views directory structure | ~320 |
</claude-mem-context>
\ No newline at end of file