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)
# 打卡系统分析
**最后更新**: 2026-02-09
**相关文件**:
- `src/api/checkin.js` - 打卡 API
- `src/views/checkin/` - 打卡页面
- `src/views/bieyuan/scan.vue` - 别院扫码
- `src/views/by/scan.vue` - BY 扫码
## 系统架构
### 打卡流程
```
用户扫描二维码
打开打卡页面(checkin/index.vue)
获取打卡信息(detail_id)
检查是否已打卡(isCheckedAPI)
已打卡? → 显示打卡信息(info_w.vue)
未打卡? → 显示打卡按钮
用户点击打卡
提交打卡(checkinAPI)
显示打卡成功(info_w.vue)
```
### 打卡类型
项目支持多种打卡场景:
1. **别院打卡** (`bieyuan/`)
- 页面: `src/views/bieyuan/scan.vue`
- 信息页面: `src/views/bieyuan/info_w.vue`
2. **BY 打卡** (`by/`)
- 页面: `src/views/by/scan.vue`
- 信息页面: `src/views/by/info_w.vue`
3. **通用打卡** (`checkin/`)
- 页面: `src/views/checkin/scan.vue`
- 信息页面: `src/views/checkin/info_w.vue`
- 信息页面: `src/views/checkin/info.vue`
## API 接口
### 1. 检查是否已打卡
**端点**: `/srv/?f=walk&a=map&t=is_checked`
**方法**: GET
**请求参数**:
```javascript
{
detail_id: String, // 打卡点 ID(必需)
openid: String // 微信 OpenID(必需)
}
```
**响应数据**:
```javascript
{
code: 1,
msg: '',
data: {
is_checked: Number // 是否已打卡(0: 未打卡, 1: 已打卡)
}
}
```
**API 调用**:
```javascript
import { isCheckedAPI } from '@/api/checkin.js';
const { data } = await isCheckedAPI({
detail_id: '123',
openid: 'wx_openid_123'
});
if (data.is_checked === 1) {
console.log('已打卡');
} else {
console.log('未打卡');
}
```
### 2. 提交打卡
**端点**: `/srv/?f=walk&a=map&t=checkin`
**方法**: POST
**请求参数**:
```javascript
{
detail_id: String, // 打卡点 ID(必需)
openid: String // 微信 OpenID(必需)
}
```
**响应数据**:
```javascript
{
code: 1,
msg: '打卡成功',
data: {
// 打卡信息(可能包含)
checkin_time: String, // 打卡时间
checkin_date: String, // 打卡日期
user_info: Object, // 用户信息
location_info: Object // 位置信息
}
}
```
**API 调用**:
```javascript
import { checkinAPI } from '@/api/checkin.js';
const { data } = await checkinAPI({
detail_id: '123',
openid: 'wx_openid_123'
});
console.log('打卡成功:', data);
```
## 页面组件
### 1. 打卡首页 (checkin/index.vue)
**功能**:
- ✅ 显示打卡点信息
- ✅ 检查打卡状态
- ✅ 显示打卡按钮
- ✅ 跳转到扫码页面
**使用示例**:
```vue
<template>
<div class="checkin-page">
<h1>{{ locationName }}</h1>
<p>{{ locationDesc }}</p>
<van-button
v-if="!isChecked"
type="primary"
@click="handleCheckin"
>
立即打卡
</van-button>
<van-button
v-else
disabled
>
已打卡
</van-button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { isCheckedAPI, checkinAPI } from '@/api/checkin.js';
import { useUserStore } from '@/store/user';
const router = useRouter();
const userStore = useUserStore();
const locationName = ref('打卡点名称');
const locationDesc = ref('打卡点描述');
const isChecked = ref(false);
const checkCheckinStatus = async () => {
const { data } = await isCheckedAPI({
detail_id: router.currentRoute.value.query.id,
openid: userStore.openid,
});
isChecked.value = data.is_checked === 1;
};
const handleCheckin = async () => {
const { data } = await checkinAPI({
detail_id: router.currentRoute.value.query.id,
openid: userStore.openid,
});
// 跳转到打卡信息页面
router.push({
path: '/checkin/info_w',
query: { id: router.currentRoute.value.query.id }
});
};
onMounted(() => {
checkCheckinStatus();
});
</script>
```
### 2. 扫码页面 (scan.vue)
**功能**:
- ✅ 调用扫码功能
- ✅ 解析二维码
- ✅ 跳转到打卡页面
**使用示例**:
```vue
<template>
<div class="scan-page">
<button @click="handleScan">扫码打卡</button>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const handleScan = async () => {
// 调用微信扫码
wx.scanQRCode({
needResult: 1, // 1: 返回扫码结果
scanType: ['qrCode'], // 可以指定扫二维码还是一维码
success: (res) => {
// res.resultStr: 扫码结果
const detailId = extractDetailId(res.resultStr);
// 跳转到打卡页面
router.push({
path: '/checkin',
query: { id: detailId }
});
},
fail: (err) => {
console.error('扫码失败:', err);
}
});
};
const extractDetailId = (resultStr) => {
// 从扫码结果中提取 detail_id
// 例如: https://example.com/checkin?id=123
const url = new URL(resultStr);
return url.searchParams.get('id');
};
</script>
```
### 3. 打卡信息页面 (info.vue, info_w.vue)
**功能**:
- ✅ 显示打卡时间
- ✅ 显示用户信息
- ✅ 显示位置信息
- ✅ 显示打卡状态
**区别**:
- `info.vue`: 普通信息页面
- `info_w.vue`: 带警告样式的信息页面
## 路由配置
### 路由定义
```javascript
// src/router/routes/modules/checkin/index.js(如果存在)
{
path: '/checkin',
name: 'Checkin',
component: () => import('@/views/checkin/index.vue'),
meta: {
title: '打卡',
requireAuth: true, // 需要登录
},
children: [
{
path: 'info',
name: 'CheckinInfo',
component: () => import('@/views/checkin/info.vue'),
},
{
path: 'info_w',
name: 'CheckinInfoWarn',
component: () => import('@/views/checkin/info_w.vue'),
},
{
path: 'scan',
name: 'CheckinScan',
component: () => import('@/views/checkin/scan.vue'),
},
],
}
```
## 数据流程
### 1. 用户扫码
```
微信扫码
微信返回扫码结果
解析 detail_id
路由跳转(携带 detail_id)
```
### 2. 检查打卡状态
```
进入打卡页面
获取 detail_id(路由参数)
获取 openid(用户信息)
调用 isCheckedAPI
显示打卡状态
```
### 3. 提交打卡
```
用户点击打卡按钮
调用 checkinAPI
更新打卡状态
显示打卡成功信息
```
## 微信集成
### 1. 扫码功能
**微信 JS-SDK 配置**:
```javascript
// src/api/wx/jsApiList.js
export const jsApiList = [
'scanQRCode', // 扫码
// 其他 API...
];
```
**使用示例**:
```javascript
import { jsApiList } from '@/api/wx/jsApiList';
wx.config({
debug: false,
appId: 'YOUR_APPID',
timestamp: timestamp,
nonceStr: nonceStr,
signature: signature,
jsApiList: jsApiList,
});
wx.ready(() => {
// 扫码功能就绪
});
wx.error((res) => {
console.error('微信配置失败:', res);
});
```
### 2. 用户信息
**OpenID 获取**:
```javascript
// 从用户信息中获取 OpenID
import { useUserStore } from '@/store/user';
const userStore = useUserStore();
const openid = userStore.openid;
```
## 已知问题
### 1. 重复打卡
**问题**: 用户可能多次点击打卡按钮
**解决方案**:
```javascript
// 防止重复提交
const isSubmitting = ref(false);
const handleCheckin = async () => {
if (isSubmitting.value) {
return;
}
isSubmitting.value = true;
try {
const { data } = await checkinAPI({
detail_id: detailId,
openid: openid,
});
// 打卡成功
} catch (err) {
console.error('打卡失败:', err);
} finally {
isSubmitting.value = false;
}
};
```
### 2. 网络异常
**问题**: 网络请求失败
**解决方案**:
```javascript
const handleCheckin = async () => {
try {
const { data } = await checkinAPI({
detail_id: detailId,
openid: openid,
});
showToast('打卡成功');
} catch (err) {
showToast('打卡失败,请重试');
// 延迟重试
setTimeout(() => {
handleCheckin();
}, 1000);
}
};
```
### 3. OpenID 获取失败
**问题**: OpenID 未正确获取
**解决方案**:
```javascript
// 检查 OpenID 是否存在
const checkOpenId = () => {
const openid = userStore.openid;
if (!openid) {
// 跳转到登录页面
router.push('/login');
return false;
}
return true;
};
const handleCheckin = async () => {
if (!checkOpenId()) {
return;
}
// 继续打卡流程
};
```
## 最佳实践
### 1. 错误处理
```javascript
// ✅ 推荐:统一的错误处理
const checkin = async () => {
try {
const { data } = await checkinAPI(params);
if (data.code === 1) {
showToast('打卡成功');
return data;
} else {
showToast(data.msg || '打卡失败');
return null;
}
} catch (err) {
console.error('打卡异常:', err);
showToast('网络异常,请重试');
return null;
}
};
// ❌ 不推荐:不处理错误
const checkin = async () => {
const { data } = await checkinAPI(params);
return data;
};
```
### 2. 状态管理
```javascript
// ✅ 推荐:使用 Pinia 管理打卡状态
import { useCheckinStore } from '@/store/checkin';
const checkinStore = useCheckinStore();
const handleCheckin = async () => {
const success = await checkinStore.checkin(detailId, openid);
if (success) {
showToast('打卡成功');
}
};
// ❌ 不推荐:在组件中管理状态
const isChecked = ref(false);
const handleCheckin = async () => {
const { data } = await checkinAPI(params);
isChecked.value = true;
};
```
### 3. 路由跳转
```javascript
// ✅ 推荐:使用命名路由
router.push({
name: 'CheckinInfo',
query: { id: detailId }
});
// ❌ 不推荐:使用路径拼接
router.push(`/checkin/info?id=${detailId}`);
```
## 调试技巧
### 1. 模拟打卡
```javascript
// 在控制台模拟打卡
import { checkinAPI } from '@/api/checkin.js';
checkinAPI({
detail_id: '123',
openid: 'test_openid'
}).then(res => console.log(res));
```
### 2. 查看打卡状态
```javascript
// 查看当前打卡状态
console.log('打卡状态:', {
detailId: router.currentRoute.value.query.id,
openid: userStore.openid,
isChecked: isChecked.value
});
```
### 3. 清除打卡状态
```javascript
// 清除本地打卡状态(测试用)
localStorage.removeItem('checkin_status');
```
## 参考文档
- [微信 JS-SDK 文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK)
- [微信扫码文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK#55)
- [项目路由配置](../项目架构分析/目录结构分析.md)
# 音频系统分析
**最后更新**: 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)
# 常见开发任务
**最后更新**: 2026-02-09
**目的**: 记录项目中的常见开发任务和解决方案
## 1. 添加新页面
### 步骤
```bash
# 1. 创建页面目录
mkdir src/views/mypage
# 2. 创建页面文件
touch src/views/mypage/index.vue
touch src/views/mypage/index.config.js
```
### 页面文件
```vue
<!-- src/views/mypage/index.vue -->
<template>
<div class="mypage">
<h1>{{ title }}</h1>
</div>
</template>
<script setup>
/**
* 我的页面
*
* @description 这是一个示例页面
*/
import { ref } from 'vue';
const title = ref('我的页面');
</script>
<style lang="less" scoped>
.mypage {
padding: 20px;
h1 {
font-size: 24px;
}
}
</style>
```
### 页面配置
```javascript
// src/views/mypage/index.config.js
export default {
navigationBarTitleText: '我的页面',
};
```
### 添加路由
```javascript
// src/router/routes/modules/mypage/index.js
export default {
path: '/mypage',
name: 'MyPage',
component: () => import('@/views/mypage/index.vue'),
meta: {
title: '我的页面',
},
};
```
## 2. 添加新组件
### 步骤
```bash
# 1. 创建组件目录
mkdir src/components/MyComponent
# 2. 创建组件文件
touch src/components/MyComponent/index.vue
```
### 组件文件
```vue
<!-- src/components/MyComponent/index.vue -->
<template>
<div class="my-component">
<h2>{{ title }}</h2>
<slot></slot>
</div>
</template>
<script setup>
/**
* 我的组件
*
* @description 这是一个示例组件
* @component MyComponent
*/
const props = defineProps({
/** 标题 */
title: {
type: String,
default: '',
},
});
const emit = defineEmits({
/** 点击事件 */
click: (event) => true,
});
</script>
<style lang="less" scoped>
.my-component {
h2 {
font-size: 20px;
}
}
</style>
```
### 使用组件
```vue
<template>
<div>
<MyComponent
title="组件标题"
@click="handleClick"
>
组件内容
</MyComponent>
</div>
</template>
<script setup>
import MyComponent from '@components/MyComponent/index.vue';
const handleClick = (event) => {
console.log('组件被点击:', event);
};
</script>
```
## 3. 添加 API 接口
### 步骤
```bash
# 1. 创建 API 文件
touch src/api/myfeature.js
```
### API 文件
```javascript
/**
* 我的特性 API
*
* @description 我的特性相关接口
*/
import { fn, fetch } from '@/api/fn';
const Api = {
FEATURE: '/srv/?a=myfeature',
};
/**
* 获取特性数据
*
* @description 获取特性数据
* @param {Object} params - 请求参数
* @param {number} params.id - ID
* @returns {Promise<Object>} 响应数据
*/
export const featureAPI = (params) => fn(fetch.get(Api.FEATURE, params));
/**
* 提交特性数据
*
* @description 提交特性数据
* @param {Object} params - 请求参数
* @param {string} params.name - 名称
* @returns {Promise<Object>} 响应数据
*/
export const submitFeatureAPI = (params) => fn(fetch.post(Api.FEATURE, params));
```
### 使用 API
```vue
<script setup>
import { featureAPI, submitFeatureAPI } from '@/api/myfeature';
import { onMounted } from 'vue';
const fetchData = async () => {
try {
const { data } = await featureAPI({ id: 123 });
if (data) {
console.log('获取数据成功:', data);
}
} catch (err) {
console.error('获取数据失败:', err);
}
};
const submitData = async () => {
try {
const { data } = await submitFeatureAPI({ name: '名称' });
if (data) {
console.log('提交成功:', data);
}
} catch (err) {
console.error('提交失败:', err);
}
};
onMounted(() => {
fetchData();
});
</script>
```
## 4. 添加 Store
### 步骤
```bash
# 1. 创建 Store 文件
touch src/store/myfeature.js
```
### Store 文件
```javascript
/**
* 我的特性 Store
*
* @description 管理特性相关状态
*/
import { defineStore } from 'pinia';
export const useMyFeatureStore = defineStore('myfeature', {
state: () => ({
data: null,
loading: false,
error: null,
}),
getters: {
/**
* 是否有数据
*/
hasData: (state) => state.data !== null,
/**
* 数据数量
*/
dataCount: (state) => state.data?.length || 0,
},
actions: {
/**
* 获取数据
*
* @param {number} id - ID
*/
async fetchData(id) {
this.loading = true;
this.error = null;
try {
const { data } = await featureAPI({ id });
this.data = data;
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
},
/**
* 清空数据
*/
clearData() {
this.data = null;
this.error = null;
},
},
});
```
### 使用 Store
```vue
<script setup>
import { useMyFeatureStore } from '@/store/myfeature';
import { onMounted } from 'vue';
const featureStore = useMyFeatureStore();
onMounted(() => {
featureStore.fetchData(123);
});
</script>
<template>
<div v-if="featureStore.loading">加载中...</div>
<div v-else-if="featureStore.error">{{ featureStore.error }}</div>
<div v-else>
<p>数据数量: {{ featureStore.dataCount }}</p>
<p>有数据: {{ featureStore.hasData }}</p>
</div>
</template>
```
## 5. 路由跳转
### 方式 1: 路径跳转
```javascript
import { useRouter } from 'vue-router';
const router = useRouter();
// 跳转(Hash 模式需要包含前缀)
router.push('/index.html/mypage');
```
### 方式 2: 命名路由
```javascript
import { useRouter } from 'vue-router';
const router = useRouter();
// 跳转(推荐)
router.push({ name: 'MyPage' });
```
### 方式 3: 带参数跳转
```javascript
import { useRouter } from 'vue-router';
const router = useRouter();
// 带查询参数
router.push({
name: 'MyPage',
query: { id: 123, name: 'test' }
});
// 带路径参数
router.push({
name: 'MyPageDetail',
params: { id: 123 }
});
```
### 方式 4: 返回
```javascript
import { useRouter } from 'vue-router';
const router = useRouter();
// 返回上一页
router.back();
// 或
router.go(-1);
```
## 6. 获取路由参数
### 查询参数
```javascript
import { useRoute } from 'vue-router';
const route = useRoute();
// 获取查询参数
const id = route.query.id;
const name = route.query.name;
```
### 路径参数
```javascript
import { useRoute } from 'vue-router';
const route = useRoute();
// 获取路径参数
const id = route.params.id;
```
## 7. 使用 Vant 组件
### 按钮
```vue
<template>
<van-button type="primary">主要按钮</van-button>
<van-button type="success">成功按钮</van-button>
<van-button type="warning">警告按钮</van-button>
<van-button type="danger">危险按钮</van-button>
<van-button plain>朴素按钮</van-button>
<van-button round>圆形按钮</van-button>
</template>
```
### 表单
```vue
<template>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="username"
name="username"
label="用户名"
placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
</template>
<script setup>
import { ref } from 'vue';
const username = ref('');
const password = ref('');
const onSubmit = (values) => {
console.log('表单数据:', values);
};
</script>
```
### 弹窗
```vue
<template>
<van-button type="primary" @click="showDialog">显示弹窗</van-button>
</template>
<script setup>
import { showDialog } from 'vant';
const showDialog = () => {
showDialog({
title: '标题',
message: '这是弹窗内容',
}).then((action) => {
console.log('确认', action);
}).catch(() => {
console.log('取消');
});
};
</script>
```
## 8. 使用 Element Plus 组件
### 按钮
```vue
<template>
<el-button>默认按钮</el-button>
<el-button type="primary">主要按钮</el-button>
<el-button type="success">成功按钮</el-button>
<el-button type="warning">警告按钮</el-button>
<el-button type="danger">危险按钮</el-button>
<el-button round>圆形按钮</el-button>
</template>
```
### 表格
```vue
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="name" label="姓名" width="180" />
<el-table-column prop="age" label="年龄" width="180" />
<el-table-column prop="address" label="地址" />
</el-table>
</template>
<script setup>
import { ref } from 'vue';
const tableData = ref([
{
name: '张三',
age: 30,
address: '北京市',
},
{
name: '李四',
age: 25,
address: '上海市',
},
]);
</script>
```
## 9. 添加 Keep-Alive 缓存
### 步骤
```javascript
// 在 store 中添加页面
import { useMainStore } from '@/store';
const mainStore = useMainStore();
// 添加需要缓存的页面
const keepThisPage = () => {
const keepPages = [...mainStore.keepPages];
if (!keepPages.includes('PageName')) {
keepPages.push('PageName');
}
mainStore.keepPages = keepPages;
};
// 移除缓存
const removeKeepPage = () => {
const keepPages = mainStore.keepPages.filter(page => page !== 'PageName');
mainStore.keepPages = keepPages.length ? keepPages : ['default'];
};
```
### 使用
```vue
<script setup>
import { onMounted } from 'vue';
import { useMainStore } from '@/store';
const mainStore = useMainStore();
onMounted(() => {
// 添加到缓存
mainStore.keepThisPage();
});
</script>
```
## 10. 处理微信分享
### 步骤
```javascript
// 引入微信分享工具
import { share } from '@/utils/share';
// 设置分享
const setShare = () => {
share({
title: '分享标题',
desc: '分享描述',
link: window.location.href,
imgUrl: 'https://example.com/share.png',
});
};
// 在页面加载时设置
onMounted(() => {
setShare();
});
```
## 11. 添加音频播放
### 单模式
```vue
<template>
<audioBackground
:audio-url="audioUrl"
:autoplay="false"
@play="handlePlay"
@pause="handlePause"
/>
</template>
<script setup>
import audioBackground from '@components/audioBackground.vue';
import { ref } from 'vue';
const audioUrl = ref('/audio/guide.mp3');
const handlePlay = () => {
console.log('开始播放');
};
const handlePause = () => {
console.log('暂停播放');
};
</script>
```
### 播放列表模式
```vue
<template>
<audioList
v-model:playlist="playlist"
:current-index="currentIndex"
/>
</template>
<script setup>
import audioList from '@components/audioList.vue';
import { ref } from 'vue';
const playlist = ref([
{
id: 1,
title: '音频 1',
url: '/audio/1.mp3',
},
{
id: 2,
title: '音频 2',
url: '/audio/2.mp3',
},
]);
const currentIndex = ref(0);
</script>
```
## 12. 使用高德地图
### 初始化地图
```javascript
import AMapLoader from '@amap/amap-jsapi-loader';
const initMap = async () => {
try {
const AMap = await AMapLoader.load({
key: 'YOUR_AMAP_KEY',
version: '2.0',
plugins: ['AMap.Scale', 'AMap.ToolBar'],
});
const map = new AMap.Map('container', {
zoom: 11,
center: [116.397428, 39.90923],
});
return map;
} catch (err) {
console.error('地图加载失败:', err);
}
};
```
### 添加标记
```javascript
const addMarker = (map, position) => {
const marker = new AMap.Marker({
position: position,
map: map,
});
return marker;
};
```
## 13. 错误处理
### 统一错误处理
```javascript
const handleError = (err) => {
console.error('操作失败:', err);
// 显示错误提示
showToast(err.message || '操作失败,请重试');
// 上报错误
// reportError(err);
};
// 使用
try {
await someAPI();
} catch (err) {
handleError(err);
}
```
### Toast 提示
```javascript
import { showToast } from 'vant';
// 成功提示
showToast('操作成功');
// 错误提示
showToast({
type: 'fail',
message: '操作失败',
});
```
## 14. 加载状态
### 使用 Loading
```vue
<template>
<div v-if="loading" class="loading">加载中...</div>
<div v-else>
<!-- 内容 -->
</div>
</template>
<script setup>
import { ref } from 'vue';
const loading = ref(false);
const fetchData = async () => {
loading.value = true;
try {
const { data } = await someAPI();
// 处理数据
} catch (err) {
console.error(err);
} finally {
loading.value = false;
}
};
</script>
```
### 使用 Vant Loading
```vue
<template>
<van-button @click="showLoading">显示加载</van-button>
</template>
<script setup>
import { showLoadingToast, closeToast } from 'vant';
const showLoading = () => {
showLoadingToast({
message: '加载中...',
forbidClick: true,
duration: 0,
});
// 模拟异步操作
setTimeout(() => {
closeToast();
}, 2000);
};
</script>
```
## 15. 图片处理
### 图片懒加载
```vue
<template>
<van-image
:src="imageUrl"
lazy-load
:show-error="true"
:show-loading="true"
>
<template #error>
<div class="error">加载失败</div>
</template>
</van-image>
</template>
<script setup>
import { ref } from 'vue';
const imageUrl = ref('/images/example.jpg');
</script>
```
### 图片预览
```vue
<template>
<van-image
:src="imageUrl"
@click="previewImage"
/>
</template>
<script setup>
import { ref } from 'vue';
import { showImagePreview } from 'vant';
const imageUrl = ref('/images/example.jpg');
const images = ref([
'/images/1.jpg',
'/images/2.jpg',
'/images/3.jpg',
]);
const previewImage = () => {
showImagePreview({
images: images.value,
startPosition: 0,
});
};
</script>
```
## 参考文档
- [新手入门指南](./新手入门指南.md)
- [已知问题汇总](../注意事项与陷阱/已知问题汇总.md)
- [地图集成分析](../功能模块分析/地图集成分析.md)
- [音频系统分析](../功能模块分析/音频系统分析.md)
# 新手入门指南
**最后更新**: 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)
# 项目目录结构分析
**最后更新**: 2026-02-09
**项目根目录**: /Users/huyirui/program/itomix/git/map-demo
## 完整目录树
```
map-demo/
├── public/ # 静态资源目录
├── docs/ # 项目文档(新增)
│ ├── 项目架构分析/
│ ├── 功能模块分析/
│ ├── 注意事项与陷阱/
│ └── 开发指南/
├── src/ # 源代码目录
│ ├── api/ # API 接口定义
│ │ ├── B/ # B端(企业端)API
│ │ │ ├── audit.js # 审计相关
│ │ │ ├── kg.js # 卡片相关?
│ │ │ └── localism.js # 本土化相关
│ │ ├── C/ # C端(用户端)API
│ │ │ ├── book.js # 预订相关
│ │ │ ├── donate.js # 捐赠相关
│ │ │ ├── kg.js # 卡片相关?
│ │ │ ├── me.js # 个人中心
│ │ │ ├── perf.js # 性能相关?
│ │ │ └── prod.js # 产品相关
│ │ ├── wx/ # 微信相关 API
│ │ │ ├── config.js # 微信配置
│ │ │ ├── jsApiList.js # JS API 列表
│ │ │ └── pay.js # 微信支付
│ │ ├── checkin.js # 打卡 API
│ │ ├── common.js # 通用 API(短信、上传、Token)
│ │ ├── fn.js # API 包装器(错误处理)
│ │ └── map.js # 地图数据 API
│ ├── assets/ # 资源文件
│ │ ├── css/ # 全局样式
│ │ ├── images/ # 图片资源
│ │ ├── mock/ # Mock 数据
│ │ │ ├── routes.js # 路由 Mock 数据
│ │ │ ├── video_list.js # 视频列表 Mock
│ │ │ └── video_list1.js # 视频列表 Mock 1
│ │ └── styles/ # 样式文件
│ │ └── base.less # 基础样式(全局注入)
│ ├── common/ # 通用代码
│ │ ├── alert.js # 弹窗提示
│ │ ├── inner_router.js # 内部路由
│ │ ├── map_data.js # 地图数据
│ │ ├── max.js # 最大值处理?
│ │ ├── members.js # 成员管理
│ │ ├── mixin.js # Vue 混入
│ │ ├── my_router.js # 自定义路由
│ │ ├── tool.js # 工具函数
│ │ ├── yard.js # 院落相关
│ │ └── vueuse.js # VueUse 封装
│ ├── components/ # 公共组件
│ │ ├── Floor/ # 楼层平面图组件
│ │ │ ├── index.vue # 主组件
│ │ │ ├── pin.js # 标记点
│ │ │ └── svgIcon.vue # SVG 图标
│ │ ├── VRViewer/ # VR 全景查看器
│ │ │ └── index.vue
│ │ ├── audioBackground.vue # 背景音频(单模式)
│ │ ├── audioBackground1.vue # 背景音频(备用)
│ │ ├── audioList.vue # 音频列表(播放列表模式)
│ │ ├── InfoWindowLite.vue # 信息窗口(轻量版)
│ │ ├── InfoWindowWarn.vue # 信息窗口(警告版)
│ │ ├── InfoWindowYard.vue # 信息窗口(院落版)
│ │ ├── InfoPopupLite.vue # 信息弹窗(轻量版)
│ │ └── InfoPopupWarn.vue # 信息弹窗(警告版)
│ ├── hooks/ # Vue Hooks
│ │ ├── injectionSymbols.js # 注入符号
│ │ ├── useContext.js # 上下文 Hook
│ │ ├── useDebounce.js # 防抖 Hook
│ │ ├── useFlowFn.js # 流程函数 Hook
│ │ ├── useGo.js # 跳转 Hook
│ │ └── useKeepAlive.js # Keep-Alive Hook
│ ├── packages/ # 多页面应用包(已废弃)
│ │ ├── mono1/ # 子应用 1
│ │ │ ├── App.vue
│ │ │ ├── index.html
│ │ │ ├── main.js
│ │ │ ├── router.js
│ │ │ └── views/
│ │ │ └── index.vue
│ │ └── mono2/ # 子应用 2
│ │ ├── App.vue
│ │ ├── index.html
│ │ ├── main.js
│ │ ├── router.js
│ │ └── views/
│ │ └── index.vue
│ ├── router/ # 路由配置
│ │ ├── routes/ # 路由模块
│ │ │ └── modules/
│ │ │ └── auth/ # 认证路由
│ │ │ └── index.js
│ │ └── methods/ # 路由方法(可能不存在)
│ ├── settings/ # 设置
│ │ ├── componentSetting.js # 组件设置
│ │ └── designSetting.js # 设计设置
│ ├── store/ # Pinia 状态管理
│ │ ├── index.js # 主 Store
│ │ └── test.js # 测试 Store
│ ├── test/ # 测试
│ │ └── mocha/
│ │ └── test.js # Mocha 测试
│ ├── utils/ # 工具函数
│ │ ├── MonitorKeyboard.js # 键盘监控
│ │ ├── TileCutter.js # 瓦片切割
│ │ ├── axios.js # Axios 实例
│ │ ├── generateIcons.js # 生成图标
│ │ ├── generateModules.js # 生成模块
│ │ ├── generatePackage.js # 生成包
│ │ ├── generateRoute.js # 生成路由
│ │ ├── share.js # 分享工具
│ │ ├── tools.js # 通用工具
│ │ └── vconsole.js # VConsole 调试
│ ├── views/ # 页面组件
│ │ ├── bieyuan/ # 别院模块
│ │ │ ├── index.vue # 别院首页
│ │ │ ├── info_w.vue # 信息页面(带警告)
│ │ │ └── scan.vue # 扫码页面
│ │ ├── by/ # BY 模块
│ │ │ ├── index.vue # BY 首页
│ │ │ ├── info.vue # 信息页面
│ │ │ ├── info_w.vue # 信息页面(带警告)
│ │ │ └── scan.vue # 扫码页面
│ │ ├── checkin/ # 打卡模块
│ │ │ ├── index.vue # 打卡首页
│ │ │ ├── info.vue # 打卡信息
│ │ │ ├── info_w.vue # 打卡信息(带警告)
│ │ │ └── scan.vue # 扫码打卡
│ │ ├── xys/ # XYS 模块
│ │ ├── about.vue # 关于页面
│ │ ├── auth.vue # 认证页面
│ │ ├── info.vue # 信息页面
│ │ ├── noticeList.vue # 通知列表
│ │ └── test.vue # 测试页面
│ ├── App.vue # 根组件
│ ├── constant.js # 常量定义
│ ├── main.js # 入口文件
│ ├── route.js # 路由配置(辅助)
│ ├── router.js # 路由配置(主)
│ ├── theme-vars.js # 主题变量
│ └── auto-imports.d.ts # 自动导入类型定义
├── build/ # 构建配置
│ └── proxy.js # 代理配置
├── .history/ # 文件历史(Git 记录)
├── .env # 环境变量(基础)
├── .env.development # 环境变量(开发)
├── .env.production # 环境变量(生产)
├── .eslintrc.js # ESLint 配置
├── .gitignore # Git 忽略文件
├── index.html # 主入口 HTML
├── package.json # 项目依赖
├── vite.config.js # Vite 配置
├── cypress.json # Cypress 配置
├── CLAUDE.md # Claude Code 项目指南
├── README.md # 项目说明
└── tsconfig.json # TypeScript 配置
```
## 核心目录说明
### 1. src/api/ - API 接口层
**职责**: 定义所有与后端交互的 API 接口
**结构**:
- `B/`: B端(企业/管理端)接口
- `C/`: C端(用户/客户端)接口
- `wx/`: 微信相关接口(配置、支付)
- `common.js`: 通用接口(短信、上传、Token)
- `fn.js`: API 包装器,统一错误处理
**关键文件**:
- `fn.js`: 核心 API 包装器,处理响应验证和错误
- `map.js`: 地图数据接口
- `checkin.js`: 打卡功能接口
### 2. src/components/ - 公共组件
**职责**: 可复用的 Vue 组件
**核心组件**:
- `Floor/`: 楼层平面图组件(SVG 交互)
- `VRViewer/`: 360° 全景查看器
- `audioBackground.vue`: 背景音频播放(单模式)
- `audioList.vue`: 音频列表播放(列表模式)
- `InfoWindow*.vue`: 各种样式的信息窗口
### 3. src/views/ - 页面组件
**职责**: 页面级组件
**模块划分**:
- `bieyuan/`: 别院模块
- `by/`: BY 模块
- `checkin/`: 打卡模块
- `xys/`: XYS 模块
**共同页面**:
- `about.vue`: 关于页面
- `auth.vue`: 认证页面
- `info.vue`: 信息页面
- `noticeList.vue`: 通知列表
- `test.vue`: 测试页面
### 4. src/store/ - 状态管理
**职责**: Pinia 全局状态
**核心状态**:
- `keepPages`: Keep-Alive 缓存页面列表
- 音频播放状态(单模式和播放列表模式)
- 滚动位置状态
- 用户认证状态
### 5. src/router/ - 路由配置
**职责**: Vue Router 路由定义
**特点**:
- 使用 Hash 模式 (`createWebHashHistory('/index.html')`)
- 动态路由生成(从 Mock 数据加载)
- 模块化路由(`routes/modules/`
### 6. src/utils/ - 工具函数
**职责**: 通用工具函数和辅助方法
**核心工具**:
- `axios.js`: Axios 实例(拦截器配置)
- `tools.js`: 通用工具函数
- `share.js`: 微信分享工具
- `generateRoute.js`: 动态路由生成
### 7. src/common/ - 通用代码
**职责**: 跨层级的通用代码
**核心文件**:
- `map_data.js`: 地图数据处理
- `mixin.js`: Vue 混入
- `alert.js`: 弹窗提示
### 8. src/packages/ - 多页面应用(已废弃)
**状态**: 代码已存在,但未在构建配置中启用
**说明**:
- `mono1/`, `mono2/`: 两个独立的子应用
- `vite.config.js` 中相关配置已注释
### 9. build/ - 构建配置
**职责**: Vite 构建相关配置
**核心文件**:
- `proxy.js`: 代理配置生成器
## 文件命名规范
### Vue 组件
**页面组件**: `index.vue`(放在模块目录下)
**公共组件**: `PascalCase.vue`(多单词,大驼峰)
**示例**:
-`audioList.vue`
-`VRViewer/index.vue`
-`InfoWindowLite.vue`
### JavaScript 文件
**工具函数**: `camelCase.js`(小驼峰)
**常量文件**: `constant.js`
**配置文件**: `*.config.js`
**示例**:
-`tools.js`
-`useDebounce.js`
-`vite.config.js`
### 样式文件
**全局样式**: `base.less`
**组件样式**: 与组件同名的 `.less` 文件
## 目录组织原则
### 1. 按功能划分
- `api/`: 按业务端(B/C)和功能(wx/map)划分
- `views/`: 按业务模块(bieyuan/by/checkin/xys)划分
- `components/`: 按功能组件(Floor/VRViewer)划分
### 2. 按层级划分
- `src/`: 源代码根目录
- `public/`: 静态资源(不经过 Vite 处理)
- `build/`: 构建脚本和配置
- `docs/`: 项目文档
### 3. 特殊目录
- `.history/`: Git 文件历史(本地编辑器生成)
- `node_modules/`: 依赖包(.gitignore)
- `dist/`: 构建输出(.gitignore)
## 建议的目录优化
### 1. 清理废弃代码
```bash
# 建议移除或归档
src/packages/ # 多页面应用(已废弃)
.history/ # 本地编辑器历史(不应提交)
```
### 2. 统一 API 结构
```
src/api/
├── modules/ # 按功能模块划分
│ ├── auth.js
│ ├── map.js
│ ├── checkin.js
│ └── wechat/
│ ├── config.js
│ ├── pay.js
│ └── jsapi.js
├── common.js # 通用 API
└── fn.js # API 包装器
```
### 3. 组件分类
```
src/components/
├── ui/ # 基础 UI 组件
├── business/ # 业务组件
│ ├── map/
│ │ ├── Floor/
│ │ ├── VRViewer/
│ │ └── InfoWindow/
│ └── audio/
│ ├── audioBackground.vue
│ └── audioList.vue
└── layouts/ # 布局组件
```
### 4. Hooks 分类
```
src/hooks/
├── router/ # 路由相关
│ ├── useGo.js
│ └── useKeepAlive.js
├── data/ # 数据相关
│ └── useContext.js
└── utils/ # 工具相关
└── useDebounce.js
```
## 文件搜索指南
### 查找页面组件
```bash
# 查找所有页面组件
find src/views -name "index.vue"
# 查找特定模块的页面
ls src/views/bieyuan/
ls src/views/by/
ls src/views/checkin/
```
### 查找公共组件
```bash
# 查找所有公共组件
find src/components -name "*.vue"
# 查找特定组件
find src/components -name "*Window*.vue"
find src/components -name "*audio*.vue"
```
### 查找 API 定义
```bash
# 查找所有 API 文件
find src/api -name "*.js"
# 查找特定 API
grep -r "mapAPI" src/api/
grep -r "checkinAPI" src/api/
```
### 查找工具函数
```bash
# 查找所有工具文件
ls src/utils/
# 搜索特定函数
grep -r "function formatTime" src/
```
## Git 相关
### .gitignore 关键配置
```
node_modules/
dist/
.history/
*.log
.DS_Store
```
### 建议添加
```
# IDE
.vscode/
.idea/
# 本地环境
.env.local
.env.*.local
# 测试覆盖率
coverage/
# 临时文件
*.tmp
*.temp
```
## 参考文档
- [项目技术栈详解](./项目技术栈详解.md)
- [功能模块分析](../功能模块分析/)
- [注意事项与陷阱](../注意事项与陷阱/)
# 项目技术栈详解
**最后更新**: 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