hookehuyr

feat: 添加埋点功能和图片预览支持

新增功能:
- 创建通用埋点 Composable (useEventTracking)
  - 支持 READ_FILE 等事件类型追踪
  - 静默处理错误,不影响用户体验
  - 提供便捷方法 trackFileRead()

- ListItemActions 组件集成埋点
  - 新增 itemId prop,自动发送埋点数据
  - 向后兼容,不破坏现有代码

- 图片预览支持
  - useFileOperation 添加 isImageFile() 判断
  - 图片文件使用 Taro.previewImage 直接预览
  - 支持 jpg, jpeg, png, gif, webp, bmp, svg 格式

- 产品详情页文件预览
  - 集成 useFileOperation
  - 自动判断文件类型并选择最优预览方式

文档:
- 更新 CHANGELOG 记录变更
- 新增埋点功能使用指南

技术栈:Vue 3, Taro 4, Composition API

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -18,6 +18,7 @@ declare module 'vue' {
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutEmpty: typeof import('@nutui/nutui-taro')['Empty']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutLoading: typeof import('@nutui/nutui-taro')['Loading']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
NutRadio: typeof import('@nutui/nutui-taro')['Radio']
......
......@@ -5,6 +5,66 @@
---
## [2026-02-05] - 产品详情页图片预览支持
### 新增
- **图片预览支持**: 更新 `useFileOperation` Composable
- 添加 `isImageFile()` 辅助函数,判断文件是否为图片
- 更新 `viewFile()` 方法,图片文件使用 `Taro.previewImage` 预览
- 支持的图片格式:jpg, jpeg, png, gif, webp, bmp, svg
- **产品详情页图片预览**: 产品详情页附件支持图片预览
- 集成 `useFileOperation` Composable
- 点击附件时自动判断文件类型并使用对应的预览方式
### 优化
- 统一文件预览流程:
- 图片文件 → `Taro.previewImage`(直接预览)
- 视频文件 → 跳转视频播放页面
- 其他文件 → 下载后使用 `Taro.openDocument` 打开
---
**详细信息**
- **影响文件**:
- src/composables/useFileOperation.js
- src/pages/product-detail/index.vue
- **技术栈**: Vue 3, Taro 4, Composition API
- **测试状态**: 待测试
- **备注**: 图片预览功能参考 material-list 页面的实现
---
## [2026-02-05] - 埋点功能集成
### 新增
- 创建通用埋点 Composable:`src/composables/useEventTracking.js`
- 支持多种埋点类型(通过 `EventType` 枚举扩展)
- 当前实现 `READ_FILE`(阅读素材)事件追踪
- 自动处理错误,不影响用户体验
- 开发环境提供详细日志
- 提供便捷方法 `trackFileRead()`
- 更新 `ListItemActions` 组件,集成自动埋点
- 新增 `itemId` prop,用于关联埋点对象
- 点击"查看"按钮时自动发送埋点数据
- 埋点异步执行,不阻塞用户操作
### 优化
- 埋点失败静默处理,不影响业务流程
- 使用 `try-catch` 捕获埋点异常
- 仅在开发环境输出日志,生产环境静默
---
**详细信息**
- **影响文件**:
- src/composables/useEventTracking.js(新建)
- src/components/ListItemActions/index.vue
- **技术栈**: Vue 3, Composition API
- **测试状态**: 待测试
- **备注**: API 接口已存在于 src/api/event.js
---
## [2026-02-05] - 收藏页面联调完成
### 新增
......
# 埋点功能使用指南
## 概述
项目提供了统一的事件埋点功能,通过 `useEventTracking` Composable 实现各种用户行为的追踪。
## 核心文件
- **API 接口**: `src/api/event.js` - 埋点 API 接口定义
- **Composable**: `src/composables/useEventTracking.js` - 埋点逻辑封装
- **集成组件**: `src/components/ListItemActions/index.vue` - 已集成埋点功能
## 快速开始
### 1. 在列表页面使用(已集成)
`ListItemActions` 组件已自动集成埋点功能,只需传递 `itemId` prop:
```vue
<template>
<ListItemActions
:item-id="item.id"
@view="handleView"
/>
</template>
<script setup>
import ListItemActions from '@/components/ListItemActions/index.vue'
const item = {
id: 'file-123',
name: '保险产品手册'
}
const handleView = () => {
// 点击查看时,自动发送埋点数据
console.log('查看文件')
}
</script>
```
### 2. 自定义埋点
#### 基础用法
```vue
<script setup>
import { useEventTracking, EventType } from '@/composables/useEventTracking'
const { trackEvent } = useEventTracking()
// 追踪阅读素材事件
await trackEvent(EventType.READ_FILE, 'file-id-123')
</script>
```
#### 带额外数据的埋点
```vue
<script setup>
import { useEventTracking, EventType } from '@/composables/useEventTracking'
const { trackEvent } = useEventTracking()
// 追踪阅读事件,并携带额外信息
await trackEvent(EventType.READ_FILE, 'file-id-123', {
title: '保险产品手册',
category: '产品资料',
author: '张三'
})
</script>
```
#### 使用便捷方法
```vue
<script setup>
import { useEventTracking } from '@/composables/useEventTracking'
const { trackFileRead } = useEventTracking()
// 便捷方法:追踪文件阅读
await trackFileRead('file-id-123', {
title: '保险产品手册'
})
</script>
```
## 事件类型
当前支持的事件类型:
| 枚举值 | 说明 | 使用场景 |
|--------|------|----------|
| `EventType.READ_FILE` | 阅读素材 | 用户点击查看文件/素材时 |
未来可扩展的事件类型(已预留):
```javascript
EventType.COLLECT_FILE // 收藏素材
EventType.SHARE_FILE // 分享素材
EventType.DOWNLOAD_FILE // 下载素材
EventType.VIEW_PRODUCT // 查看产品
```
## 添加新的事件类型
### 1. 更新 EventType 枚举
编辑 `src/composables/useEventTracking.js`
```javascript
export const EventType = {
READ_FILE: 'READ_FILE',
// 添加新类型
COLLECT_FILE: 'COLLECT_FILE',
}
```
### 2. 添加便捷方法(可选)
```javascript
/**
* 追踪收藏事件
*/
const trackFileCollect = async (fileId, extraData = {}) => {
await trackEvent(EventType.COLLECT_FILE, fileId, extraData)
}
return {
trackEvent,
trackFileRead,
trackFileCollect // 导出新方法
}
```
## 最佳实践
### ✅ 推荐做法
1. **使用枚举而非字符串**
```javascript
// ✅ GOOD
await trackEvent(EventType.READ_FILE, fileId)
// ❌ BAD
await trackEvent('READ_FILE', fileId)
```
2. **传递有意义的额外数据**
```javascript
await trackEvent(EventType.READ_FILE, fileId, {
title: '保险产品手册',
category: '产品资料',
format: 'PDF'
})
```
3. **埋点失败不影响用户体验**
```javascript
// ✅ GOOD - 静默处理
try {
await trackEvent(...)
} catch (error) {
// 仅在开发环境记录
if (process.env.NODE_ENV === 'development') {
console.error('[埋点失败]', error)
}
}
// ❌ BAD - 向用户显示错误
try {
await trackEvent(...)
} catch (error) {
Taro.showToast({ title: '埋点失败' }) // 不要这样做
}
```
### ❌ 避免做法
1. **不要埋点阻塞用户操作**
```javascript
// ❌ BAD - await 会阻塞
const handleView = async () => {
await trackEvent(...) // 等待埋点完成
navigateToDetail() // 延迟跳转
}
// ✅ GOOD - 异步执行
const handleView = () => {
trackEvent(...) // 不等待
navigateToDetail() // 立即执行
}
```
2. **不要在循环中批量发送埋点**
```javascript
// ❌ BAD
items.forEach(item => {
await trackEvent(...) // 串行执行
})
// ✅ GOOD
await Promise.all(items.map(item => // 并行执行
trackEvent(...)
))
```
## 调试
### 开发环境日志
在开发环境中,埋点成功和失败都会输出日志:
```javascript
// 成功
[埋点成功] { type: 'READ_FILE', objectId: 'file-123', extraData: {...} }
// 失败
[埋点失败] Error: Request failed
```
### 验证埋点数据
1. 打开微信开发者工具
2. 切换到 "Network" 面板
3. 触发埋点操作
4. 查找 `/srv/?a=event&t=add` 请求
5. 检查请求参数是否正确
## 常见问题
### Q: 埋点失败会影响用户操作吗?
**A**: 不会。埋点失败会被静默处理,不会影响用户的正常操作流程。
### Q: 如何查看埋点是否成功?
**A**:
1. 开发环境查看控制台日志
2. 使用微信开发者工具的 Network 面板查看请求
3. 联系后端查看埋点数据
### Q: 可以在组件卸载后继续埋点吗?
**A**: 不建议。埋点应该在组件生命周期内完成,避免潜在的内存泄漏。
### Q: 埋点数据会持久化吗?
**A**: 埋点失败的数据不会重试,也不会持久化。如果需要离线埋点,需要额外的实现。
## API 参考
### useEventTracking()
返回对象:
| 属性 | 类型 | 说明 |
|------|------|------|
| `trackEvent` | `(type, objectId, extraData?) => Promise<void>` | 通用追踪方法 |
| `trackFileRead` | `(fileId, extraData?) => Promise<void>` | 文件阅读追踪 |
### EventType
枚举值:
| 值 | 说明 |
|----|------|
| `READ_FILE` | 阅读素材 |
| (未来扩展更多类型) | |
## 相关文档
- [API 接口文档](../../api-specs/event/README.md)
- [Composables 最佳实践](../../lessons-learned.md#composables-使用指南)
......@@ -8,6 +8,7 @@
:collectable="true"
:deletable="true"
:collected="item.collected"
:item-id="item.id"
@view="onView(item)"
@collect="onCollect(item)"
@delete="onDelete(item)"
......@@ -38,6 +39,7 @@
<script setup>
import { computed } from 'vue'
import IconFont from '@/components/IconFont.vue'
import { useEventTracking } from '@/composables/useEventTracking'
/**
* 组件属性
......@@ -78,6 +80,14 @@ const props = defineProps({
collected: {
type: Boolean,
default: false
},
/**
* 关联对象 ID(用于埋点)
* @type {string}
*/
itemId: {
type: String,
default: ''
}
})
......@@ -101,10 +111,20 @@ const emit = defineEmits({
const isCollected = computed(() => props.collected)
// 初始化埋点功能
const { trackFileRead } = useEventTracking()
/**
* 处理查看点击
*
* @description 触发查看事件,并自动发送埋点数据
*/
const handleView = () => {
// 如果提供了 itemId,自动发送埋点
if (props.itemId) {
trackFileRead(props.itemId)
}
emit('view')
}
......
/**
* 事件埋点 Composable
*
* @description 提供统一的事件埋点功能,支持多种埋点类型
* @module composables/useEventTracking
*/
import { addAPI } from '@/api/event'
/**
* 埋点事件类型枚举
*
* @enum {string}
*/
export const EventType = {
/** 阅读素材 */
READ_FILE: 'READ_FILE',
// 未来可以添加更多事件类型
// COLLECT_FILE: 'COLLECT_FILE', // 收藏素材
// SHARE_FILE: 'SHARE_FILE', // 分享素材
// DOWNLOAD_FILE: 'DOWNLOAD_FILE', // 下载素材
// VIEW_PRODUCT: 'VIEW_PRODUCT', // 查看产品
}
/**
* 使用事件埋点
*
* @description 提供统一的事件埋点方法,自动处理错误
* @returns {Object} 埋点方法和状态
*
* @example
* // 基础用法
* const { trackEvent } = useEventTracking()
*
* // 追踪阅读素材事件
* trackEvent(EventType.READ_FILE, 'file-id-123')
*
* // 带额外数据的追踪
* trackEvent(EventType.READ_FILE, 'file-id-123', {
* title: '文档标题',
* category: '培训资料'
* })
*/
export function useEventTracking() {
/**
* 追踪事件
*
* @description 发送埋点数据到后端
* @async
* @param {string} type - 事件类型(使用 EventType 枚举)
* @param {string} objectId - 关联的对象 ID(文件 ID、产品 ID 等)
* @param {Object} extraData - 额外的埋点数据(可选)
* @param {string} [extraData.title] - 对象标题
* @param {string} [extraData.category] - 分类
* @param {Record<string, any>} [extraData.*] - 其他自定义字段
* @returns {Promise<void>}
*
* @example
* // 追踪阅读事件
* await trackEvent(EventType.READ_FILE, 'file-id-123')
*
* // 追踪带额外数据的阅读事件
* await trackEvent(EventType.READ_FILE, 'file-id-123', {
* title: '保险产品手册',
* category: '产品资料'
* })
*/
const trackEvent = async (type, objectId, extraData = {}) => {
try {
const params = {
type,
object_id: objectId,
...extraData
}
await addAPI(params)
// 静默处理埋点失败,不影响用户操作
// 仅在开发环境打印日志
if (process.env.NODE_ENV === 'development') {
console.log('[埋点成功]', { type, objectId, extraData })
}
} catch (error) {
// 埋点失败不应影响用户体验,静默处理
if (process.env.NODE_ENV === 'development') {
console.error('[埋点失败]', error)
}
}
}
/**
* 追踪文件阅读事件
*
* @description 便捷方法:追踪文件/素材阅读
* @async
* @param {string} fileId - 文件 ID
* @param {Object} extraData - 额外的埋点数据
* @returns {Promise<void>}
*
* @example
* await trackFileRead('file-id-123', {
* title: '保险产品手册'
* })
*/
const trackFileRead = async (fileId, extraData = {}) => {
await trackEvent(EventType.READ_FILE, fileId, extraData)
}
return {
trackEvent,
trackFileRead
}
}
......@@ -8,7 +8,7 @@
* @date 2026-01-31
*/
import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile } from '@tarojs/taro'
import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile, previewImage } from '@tarojs/taro'
import { isVideoFile } from '@/utils/tools'
/**
......@@ -18,6 +18,18 @@ import { isVideoFile } from '@/utils/tools'
*/
export function useFileOperation() {
/**
* 判断是否为图片文件
*
* @param {string} fileName - 文件名
* @returns {boolean} 是否为图片文件
*/
const isImageFile = (fileName) => {
if (!fileName) return false
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']
const extension = fileName.split('.').pop()?.toLowerCase() || ''
return imageExtensions.includes(extension)
}
/**
* 打开文件的通用函数
*
* @description 使用 Taro.openDocument 打开文件,支持菜单转发和保存
......@@ -175,7 +187,10 @@ export function useFileOperation() {
/**
* 查看文件(入口函数)
*
* @description 检查文件是否有下载地址,判断文件类型,视频文件跳转播放页面,其他文件下载并打开
* @description 检查文件是否有下载地址,判断文件类型:
* - 图片文件:使用 Taro.previewImage 预览
* - 视频文件:跳转播放页面
* - 其他文件:下载并使用 Taro.openDocument 打开
* @async
* @param {Object} item - 文件信息对象
* @param {string} [item.downloadUrl] - 文件下载地址
......@@ -185,8 +200,8 @@ export function useFileOperation() {
* @example
* const { viewFile } = useFileOperation()
* await viewFile({
* downloadUrl: 'https://example.com/video.mp4',
* fileName: 'tutorial.mp4'
* downloadUrl: 'https://example.com/file.pdf',
* fileName: 'document.pdf'
* })
*/
const viewFile = async (item) => {
......@@ -200,6 +215,26 @@ export function useFileOperation() {
return
}
// 判断是否为图片文件
if (isImageFile(item.fileName)) {
// 图片文件:使用图片预览
console.log('[文件操作] 检测到图片文件,使用图片预览')
try {
await previewImage({
urls: [item.downloadUrl],
current: item.downloadUrl
})
} catch (error) {
console.error('[文件操作] 图片预览失败:', error)
showToast({
title: '图片预览失败',
icon: 'none',
duration: 2000
})
}
return
}
// 判断是否为视频文件
if (isVideoFile(item.fileName)) {
// 视频文件:跳转到视频播放页面
......
......@@ -168,10 +168,13 @@ const fetchProductDetail = async (id) => {
/**
* 查看文档
*
* @description 打开文档预览
* @description 打开文档预览,支持图片、视频、PDF 等多种格式
* @param {Object} doc - 文档对象
* @param {string} doc.file_name - 文档名称
* @param {string} doc.file_url - 文档 URL
*/
const viewDocument = (doc) => {
// 打开文件预览(自动判断文件类型)
viewFile({
fileName: doc.file_name,
downloadUrl: doc.file_url
......