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>
Showing
7 changed files
with
512 additions
and
5 deletions
| ... | @@ -18,6 +18,7 @@ declare module 'vue' { | ... | @@ -18,6 +18,7 @@ declare module 'vue' { |
| 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 18 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 19 | NutEmpty: typeof import('@nutui/nutui-taro')['Empty'] | 19 | NutEmpty: typeof import('@nutui/nutui-taro')['Empty'] |
| 20 | NutInput: typeof import('@nutui/nutui-taro')['Input'] | 20 | NutInput: typeof import('@nutui/nutui-taro')['Input'] |
| 21 | + NutLoading: typeof import('@nutui/nutui-taro')['Loading'] | ||
| 21 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] | 22 | NutPicker: typeof import('@nutui/nutui-taro')['Picker'] |
| 22 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] | 23 | NutPopup: typeof import('@nutui/nutui-taro')['Popup'] |
| 23 | NutRadio: typeof import('@nutui/nutui-taro')['Radio'] | 24 | NutRadio: typeof import('@nutui/nutui-taro')['Radio'] | ... | ... |
| ... | @@ -5,6 +5,66 @@ | ... | @@ -5,6 +5,66 @@ |
| 5 | 5 | ||
| 6 | --- | 6 | --- |
| 7 | 7 | ||
| 8 | +## [2026-02-05] - 产品详情页图片预览支持 | ||
| 9 | + | ||
| 10 | +### 新增 | ||
| 11 | +- **图片预览支持**: 更新 `useFileOperation` Composable | ||
| 12 | + - 添加 `isImageFile()` 辅助函数,判断文件是否为图片 | ||
| 13 | + - 更新 `viewFile()` 方法,图片文件使用 `Taro.previewImage` 预览 | ||
| 14 | + - 支持的图片格式:jpg, jpeg, png, gif, webp, bmp, svg | ||
| 15 | +- **产品详情页图片预览**: 产品详情页附件支持图片预览 | ||
| 16 | + - 集成 `useFileOperation` Composable | ||
| 17 | + - 点击附件时自动判断文件类型并使用对应的预览方式 | ||
| 18 | + | ||
| 19 | +### 优化 | ||
| 20 | +- 统一文件预览流程: | ||
| 21 | + - 图片文件 → `Taro.previewImage`(直接预览) | ||
| 22 | + - 视频文件 → 跳转视频播放页面 | ||
| 23 | + - 其他文件 → 下载后使用 `Taro.openDocument` 打开 | ||
| 24 | + | ||
| 25 | +--- | ||
| 26 | + | ||
| 27 | +**详细信息**: | ||
| 28 | +- **影响文件**: | ||
| 29 | + - src/composables/useFileOperation.js | ||
| 30 | + - src/pages/product-detail/index.vue | ||
| 31 | +- **技术栈**: Vue 3, Taro 4, Composition API | ||
| 32 | +- **测试状态**: 待测试 | ||
| 33 | +- **备注**: 图片预览功能参考 material-list 页面的实现 | ||
| 34 | + | ||
| 35 | +--- | ||
| 36 | + | ||
| 37 | +## [2026-02-05] - 埋点功能集成 | ||
| 38 | + | ||
| 39 | +### 新增 | ||
| 40 | +- 创建通用埋点 Composable:`src/composables/useEventTracking.js` | ||
| 41 | + - 支持多种埋点类型(通过 `EventType` 枚举扩展) | ||
| 42 | + - 当前实现 `READ_FILE`(阅读素材)事件追踪 | ||
| 43 | + - 自动处理错误,不影响用户体验 | ||
| 44 | + - 开发环境提供详细日志 | ||
| 45 | + - 提供便捷方法 `trackFileRead()` | ||
| 46 | +- 更新 `ListItemActions` 组件,集成自动埋点 | ||
| 47 | + - 新增 `itemId` prop,用于关联埋点对象 | ||
| 48 | + - 点击"查看"按钮时自动发送埋点数据 | ||
| 49 | + - 埋点异步执行,不阻塞用户操作 | ||
| 50 | + | ||
| 51 | +### 优化 | ||
| 52 | +- 埋点失败静默处理,不影响业务流程 | ||
| 53 | +- 使用 `try-catch` 捕获埋点异常 | ||
| 54 | +- 仅在开发环境输出日志,生产环境静默 | ||
| 55 | + | ||
| 56 | +--- | ||
| 57 | + | ||
| 58 | +**详细信息**: | ||
| 59 | +- **影响文件**: | ||
| 60 | + - src/composables/useEventTracking.js(新建) | ||
| 61 | + - src/components/ListItemActions/index.vue | ||
| 62 | +- **技术栈**: Vue 3, Composition API | ||
| 63 | +- **测试状态**: 待测试 | ||
| 64 | +- **备注**: API 接口已存在于 src/api/event.js | ||
| 65 | + | ||
| 66 | +--- | ||
| 67 | + | ||
| 8 | ## [2026-02-05] - 收藏页面联调完成 | 68 | ## [2026-02-05] - 收藏页面联调完成 |
| 9 | 69 | ||
| 10 | ### 新增 | 70 | ### 新增 | ... | ... |
docs/guides/event-tracking-guide.md
0 → 100644
| 1 | +# 埋点功能使用指南 | ||
| 2 | + | ||
| 3 | +## 概述 | ||
| 4 | + | ||
| 5 | +项目提供了统一的事件埋点功能,通过 `useEventTracking` Composable 实现各种用户行为的追踪。 | ||
| 6 | + | ||
| 7 | +## 核心文件 | ||
| 8 | + | ||
| 9 | +- **API 接口**: `src/api/event.js` - 埋点 API 接口定义 | ||
| 10 | +- **Composable**: `src/composables/useEventTracking.js` - 埋点逻辑封装 | ||
| 11 | +- **集成组件**: `src/components/ListItemActions/index.vue` - 已集成埋点功能 | ||
| 12 | + | ||
| 13 | +## 快速开始 | ||
| 14 | + | ||
| 15 | +### 1. 在列表页面使用(已集成) | ||
| 16 | + | ||
| 17 | +`ListItemActions` 组件已自动集成埋点功能,只需传递 `itemId` prop: | ||
| 18 | + | ||
| 19 | +```vue | ||
| 20 | +<template> | ||
| 21 | + <ListItemActions | ||
| 22 | + :item-id="item.id" | ||
| 23 | + @view="handleView" | ||
| 24 | + /> | ||
| 25 | +</template> | ||
| 26 | + | ||
| 27 | +<script setup> | ||
| 28 | +import ListItemActions from '@/components/ListItemActions/index.vue' | ||
| 29 | + | ||
| 30 | +const item = { | ||
| 31 | + id: 'file-123', | ||
| 32 | + name: '保险产品手册' | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +const handleView = () => { | ||
| 36 | + // 点击查看时,自动发送埋点数据 | ||
| 37 | + console.log('查看文件') | ||
| 38 | +} | ||
| 39 | +</script> | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +### 2. 自定义埋点 | ||
| 43 | + | ||
| 44 | +#### 基础用法 | ||
| 45 | + | ||
| 46 | +```vue | ||
| 47 | +<script setup> | ||
| 48 | +import { useEventTracking, EventType } from '@/composables/useEventTracking' | ||
| 49 | + | ||
| 50 | +const { trackEvent } = useEventTracking() | ||
| 51 | + | ||
| 52 | +// 追踪阅读素材事件 | ||
| 53 | +await trackEvent(EventType.READ_FILE, 'file-id-123') | ||
| 54 | +</script> | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +#### 带额外数据的埋点 | ||
| 58 | + | ||
| 59 | +```vue | ||
| 60 | +<script setup> | ||
| 61 | +import { useEventTracking, EventType } from '@/composables/useEventTracking' | ||
| 62 | + | ||
| 63 | +const { trackEvent } = useEventTracking() | ||
| 64 | + | ||
| 65 | +// 追踪阅读事件,并携带额外信息 | ||
| 66 | +await trackEvent(EventType.READ_FILE, 'file-id-123', { | ||
| 67 | + title: '保险产品手册', | ||
| 68 | + category: '产品资料', | ||
| 69 | + author: '张三' | ||
| 70 | +}) | ||
| 71 | +</script> | ||
| 72 | +``` | ||
| 73 | + | ||
| 74 | +#### 使用便捷方法 | ||
| 75 | + | ||
| 76 | +```vue | ||
| 77 | +<script setup> | ||
| 78 | +import { useEventTracking } from '@/composables/useEventTracking' | ||
| 79 | + | ||
| 80 | +const { trackFileRead } = useEventTracking() | ||
| 81 | + | ||
| 82 | +// 便捷方法:追踪文件阅读 | ||
| 83 | +await trackFileRead('file-id-123', { | ||
| 84 | + title: '保险产品手册' | ||
| 85 | +}) | ||
| 86 | +</script> | ||
| 87 | +``` | ||
| 88 | + | ||
| 89 | +## 事件类型 | ||
| 90 | + | ||
| 91 | +当前支持的事件类型: | ||
| 92 | + | ||
| 93 | +| 枚举值 | 说明 | 使用场景 | | ||
| 94 | +|--------|------|----------| | ||
| 95 | +| `EventType.READ_FILE` | 阅读素材 | 用户点击查看文件/素材时 | | ||
| 96 | + | ||
| 97 | +未来可扩展的事件类型(已预留): | ||
| 98 | + | ||
| 99 | +```javascript | ||
| 100 | +EventType.COLLECT_FILE // 收藏素材 | ||
| 101 | +EventType.SHARE_FILE // 分享素材 | ||
| 102 | +EventType.DOWNLOAD_FILE // 下载素材 | ||
| 103 | +EventType.VIEW_PRODUCT // 查看产品 | ||
| 104 | +``` | ||
| 105 | + | ||
| 106 | +## 添加新的事件类型 | ||
| 107 | + | ||
| 108 | +### 1. 更新 EventType 枚举 | ||
| 109 | + | ||
| 110 | +编辑 `src/composables/useEventTracking.js`: | ||
| 111 | + | ||
| 112 | +```javascript | ||
| 113 | +export const EventType = { | ||
| 114 | + READ_FILE: 'READ_FILE', | ||
| 115 | + // 添加新类型 | ||
| 116 | + COLLECT_FILE: 'COLLECT_FILE', | ||
| 117 | +} | ||
| 118 | +``` | ||
| 119 | + | ||
| 120 | +### 2. 添加便捷方法(可选) | ||
| 121 | + | ||
| 122 | +```javascript | ||
| 123 | +/** | ||
| 124 | + * 追踪收藏事件 | ||
| 125 | + */ | ||
| 126 | +const trackFileCollect = async (fileId, extraData = {}) => { | ||
| 127 | + await trackEvent(EventType.COLLECT_FILE, fileId, extraData) | ||
| 128 | +} | ||
| 129 | + | ||
| 130 | +return { | ||
| 131 | + trackEvent, | ||
| 132 | + trackFileRead, | ||
| 133 | + trackFileCollect // 导出新方法 | ||
| 134 | +} | ||
| 135 | +``` | ||
| 136 | + | ||
| 137 | +## 最佳实践 | ||
| 138 | + | ||
| 139 | +### ✅ 推荐做法 | ||
| 140 | + | ||
| 141 | +1. **使用枚举而非字符串** | ||
| 142 | + ```javascript | ||
| 143 | + // ✅ GOOD | ||
| 144 | + await trackEvent(EventType.READ_FILE, fileId) | ||
| 145 | + | ||
| 146 | + // ❌ BAD | ||
| 147 | + await trackEvent('READ_FILE', fileId) | ||
| 148 | + ``` | ||
| 149 | + | ||
| 150 | +2. **传递有意义的额外数据** | ||
| 151 | + ```javascript | ||
| 152 | + await trackEvent(EventType.READ_FILE, fileId, { | ||
| 153 | + title: '保险产品手册', | ||
| 154 | + category: '产品资料', | ||
| 155 | + format: 'PDF' | ||
| 156 | + }) | ||
| 157 | + ``` | ||
| 158 | + | ||
| 159 | +3. **埋点失败不影响用户体验** | ||
| 160 | + ```javascript | ||
| 161 | + // ✅ GOOD - 静默处理 | ||
| 162 | + try { | ||
| 163 | + await trackEvent(...) | ||
| 164 | + } catch (error) { | ||
| 165 | + // 仅在开发环境记录 | ||
| 166 | + if (process.env.NODE_ENV === 'development') { | ||
| 167 | + console.error('[埋点失败]', error) | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | + | ||
| 171 | + // ❌ BAD - 向用户显示错误 | ||
| 172 | + try { | ||
| 173 | + await trackEvent(...) | ||
| 174 | + } catch (error) { | ||
| 175 | + Taro.showToast({ title: '埋点失败' }) // 不要这样做 | ||
| 176 | + } | ||
| 177 | + ``` | ||
| 178 | + | ||
| 179 | +### ❌ 避免做法 | ||
| 180 | + | ||
| 181 | +1. **不要埋点阻塞用户操作** | ||
| 182 | + ```javascript | ||
| 183 | + // ❌ BAD - await 会阻塞 | ||
| 184 | + const handleView = async () => { | ||
| 185 | + await trackEvent(...) // 等待埋点完成 | ||
| 186 | + navigateToDetail() // 延迟跳转 | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + // ✅ GOOD - 异步执行 | ||
| 190 | + const handleView = () => { | ||
| 191 | + trackEvent(...) // 不等待 | ||
| 192 | + navigateToDetail() // 立即执行 | ||
| 193 | + } | ||
| 194 | + ``` | ||
| 195 | + | ||
| 196 | +2. **不要在循环中批量发送埋点** | ||
| 197 | + ```javascript | ||
| 198 | + // ❌ BAD | ||
| 199 | + items.forEach(item => { | ||
| 200 | + await trackEvent(...) // 串行执行 | ||
| 201 | + }) | ||
| 202 | + | ||
| 203 | + // ✅ GOOD | ||
| 204 | + await Promise.all(items.map(item => // 并行执行 | ||
| 205 | + trackEvent(...) | ||
| 206 | + )) | ||
| 207 | + ``` | ||
| 208 | + | ||
| 209 | +## 调试 | ||
| 210 | + | ||
| 211 | +### 开发环境日志 | ||
| 212 | + | ||
| 213 | +在开发环境中,埋点成功和失败都会输出日志: | ||
| 214 | + | ||
| 215 | +```javascript | ||
| 216 | +// 成功 | ||
| 217 | +[埋点成功] { type: 'READ_FILE', objectId: 'file-123', extraData: {...} } | ||
| 218 | + | ||
| 219 | +// 失败 | ||
| 220 | +[埋点失败] Error: Request failed | ||
| 221 | +``` | ||
| 222 | + | ||
| 223 | +### 验证埋点数据 | ||
| 224 | + | ||
| 225 | +1. 打开微信开发者工具 | ||
| 226 | +2. 切换到 "Network" 面板 | ||
| 227 | +3. 触发埋点操作 | ||
| 228 | +4. 查找 `/srv/?a=event&t=add` 请求 | ||
| 229 | +5. 检查请求参数是否正确 | ||
| 230 | + | ||
| 231 | +## 常见问题 | ||
| 232 | + | ||
| 233 | +### Q: 埋点失败会影响用户操作吗? | ||
| 234 | + | ||
| 235 | +**A**: 不会。埋点失败会被静默处理,不会影响用户的正常操作流程。 | ||
| 236 | + | ||
| 237 | +### Q: 如何查看埋点是否成功? | ||
| 238 | + | ||
| 239 | +**A**: | ||
| 240 | +1. 开发环境查看控制台日志 | ||
| 241 | +2. 使用微信开发者工具的 Network 面板查看请求 | ||
| 242 | +3. 联系后端查看埋点数据 | ||
| 243 | + | ||
| 244 | +### Q: 可以在组件卸载后继续埋点吗? | ||
| 245 | + | ||
| 246 | +**A**: 不建议。埋点应该在组件生命周期内完成,避免潜在的内存泄漏。 | ||
| 247 | + | ||
| 248 | +### Q: 埋点数据会持久化吗? | ||
| 249 | + | ||
| 250 | +**A**: 埋点失败的数据不会重试,也不会持久化。如果需要离线埋点,需要额外的实现。 | ||
| 251 | + | ||
| 252 | +## API 参考 | ||
| 253 | + | ||
| 254 | +### useEventTracking() | ||
| 255 | + | ||
| 256 | +返回对象: | ||
| 257 | + | ||
| 258 | +| 属性 | 类型 | 说明 | | ||
| 259 | +|------|------|------| | ||
| 260 | +| `trackEvent` | `(type, objectId, extraData?) => Promise<void>` | 通用追踪方法 | | ||
| 261 | +| `trackFileRead` | `(fileId, extraData?) => Promise<void>` | 文件阅读追踪 | | ||
| 262 | + | ||
| 263 | +### EventType | ||
| 264 | + | ||
| 265 | +枚举值: | ||
| 266 | + | ||
| 267 | +| 值 | 说明 | | ||
| 268 | +|----|------| | ||
| 269 | +| `READ_FILE` | 阅读素材 | | ||
| 270 | +| (未来扩展更多类型) | | | ||
| 271 | + | ||
| 272 | +## 相关文档 | ||
| 273 | + | ||
| 274 | +- [API 接口文档](../../api-specs/event/README.md) | ||
| 275 | +- [Composables 最佳实践](../../lessons-learned.md#composables-使用指南) |
| ... | @@ -8,6 +8,7 @@ | ... | @@ -8,6 +8,7 @@ |
| 8 | :collectable="true" | 8 | :collectable="true" |
| 9 | :deletable="true" | 9 | :deletable="true" |
| 10 | :collected="item.collected" | 10 | :collected="item.collected" |
| 11 | + :item-id="item.id" | ||
| 11 | @view="onView(item)" | 12 | @view="onView(item)" |
| 12 | @collect="onCollect(item)" | 13 | @collect="onCollect(item)" |
| 13 | @delete="onDelete(item)" | 14 | @delete="onDelete(item)" |
| ... | @@ -38,6 +39,7 @@ | ... | @@ -38,6 +39,7 @@ |
| 38 | <script setup> | 39 | <script setup> |
| 39 | import { computed } from 'vue' | 40 | import { computed } from 'vue' |
| 40 | import IconFont from '@/components/IconFont.vue' | 41 | import IconFont from '@/components/IconFont.vue' |
| 42 | +import { useEventTracking } from '@/composables/useEventTracking' | ||
| 41 | 43 | ||
| 42 | /** | 44 | /** |
| 43 | * 组件属性 | 45 | * 组件属性 |
| ... | @@ -78,6 +80,14 @@ const props = defineProps({ | ... | @@ -78,6 +80,14 @@ const props = defineProps({ |
| 78 | collected: { | 80 | collected: { |
| 79 | type: Boolean, | 81 | type: Boolean, |
| 80 | default: false | 82 | default: false |
| 83 | + }, | ||
| 84 | + /** | ||
| 85 | + * 关联对象 ID(用于埋点) | ||
| 86 | + * @type {string} | ||
| 87 | + */ | ||
| 88 | + itemId: { | ||
| 89 | + type: String, | ||
| 90 | + default: '' | ||
| 81 | } | 91 | } |
| 82 | }) | 92 | }) |
| 83 | 93 | ||
| ... | @@ -101,10 +111,20 @@ const emit = defineEmits({ | ... | @@ -101,10 +111,20 @@ const emit = defineEmits({ |
| 101 | 111 | ||
| 102 | const isCollected = computed(() => props.collected) | 112 | const isCollected = computed(() => props.collected) |
| 103 | 113 | ||
| 114 | +// 初始化埋点功能 | ||
| 115 | +const { trackFileRead } = useEventTracking() | ||
| 116 | + | ||
| 104 | /** | 117 | /** |
| 105 | * 处理查看点击 | 118 | * 处理查看点击 |
| 119 | + * | ||
| 120 | + * @description 触发查看事件,并自动发送埋点数据 | ||
| 106 | */ | 121 | */ |
| 107 | const handleView = () => { | 122 | const handleView = () => { |
| 123 | + // 如果提供了 itemId,自动发送埋点 | ||
| 124 | + if (props.itemId) { | ||
| 125 | + trackFileRead(props.itemId) | ||
| 126 | + } | ||
| 127 | + | ||
| 108 | emit('view') | 128 | emit('view') |
| 109 | } | 129 | } |
| 110 | 130 | ... | ... |
src/composables/useEventTracking.js
0 → 100644
| 1 | +/** | ||
| 2 | + * 事件埋点 Composable | ||
| 3 | + * | ||
| 4 | + * @description 提供统一的事件埋点功能,支持多种埋点类型 | ||
| 5 | + * @module composables/useEventTracking | ||
| 6 | + */ | ||
| 7 | + | ||
| 8 | +import { addAPI } from '@/api/event' | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 埋点事件类型枚举 | ||
| 12 | + * | ||
| 13 | + * @enum {string} | ||
| 14 | + */ | ||
| 15 | +export const EventType = { | ||
| 16 | + /** 阅读素材 */ | ||
| 17 | + READ_FILE: 'READ_FILE', | ||
| 18 | + // 未来可以添加更多事件类型 | ||
| 19 | + // COLLECT_FILE: 'COLLECT_FILE', // 收藏素材 | ||
| 20 | + // SHARE_FILE: 'SHARE_FILE', // 分享素材 | ||
| 21 | + // DOWNLOAD_FILE: 'DOWNLOAD_FILE', // 下载素材 | ||
| 22 | + // VIEW_PRODUCT: 'VIEW_PRODUCT', // 查看产品 | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * 使用事件埋点 | ||
| 27 | + * | ||
| 28 | + * @description 提供统一的事件埋点方法,自动处理错误 | ||
| 29 | + * @returns {Object} 埋点方法和状态 | ||
| 30 | + * | ||
| 31 | + * @example | ||
| 32 | + * // 基础用法 | ||
| 33 | + * const { trackEvent } = useEventTracking() | ||
| 34 | + * | ||
| 35 | + * // 追踪阅读素材事件 | ||
| 36 | + * trackEvent(EventType.READ_FILE, 'file-id-123') | ||
| 37 | + * | ||
| 38 | + * // 带额外数据的追踪 | ||
| 39 | + * trackEvent(EventType.READ_FILE, 'file-id-123', { | ||
| 40 | + * title: '文档标题', | ||
| 41 | + * category: '培训资料' | ||
| 42 | + * }) | ||
| 43 | + */ | ||
| 44 | +export function useEventTracking() { | ||
| 45 | + /** | ||
| 46 | + * 追踪事件 | ||
| 47 | + * | ||
| 48 | + * @description 发送埋点数据到后端 | ||
| 49 | + * @async | ||
| 50 | + * @param {string} type - 事件类型(使用 EventType 枚举) | ||
| 51 | + * @param {string} objectId - 关联的对象 ID(文件 ID、产品 ID 等) | ||
| 52 | + * @param {Object} extraData - 额外的埋点数据(可选) | ||
| 53 | + * @param {string} [extraData.title] - 对象标题 | ||
| 54 | + * @param {string} [extraData.category] - 分类 | ||
| 55 | + * @param {Record<string, any>} [extraData.*] - 其他自定义字段 | ||
| 56 | + * @returns {Promise<void>} | ||
| 57 | + * | ||
| 58 | + * @example | ||
| 59 | + * // 追踪阅读事件 | ||
| 60 | + * await trackEvent(EventType.READ_FILE, 'file-id-123') | ||
| 61 | + * | ||
| 62 | + * // 追踪带额外数据的阅读事件 | ||
| 63 | + * await trackEvent(EventType.READ_FILE, 'file-id-123', { | ||
| 64 | + * title: '保险产品手册', | ||
| 65 | + * category: '产品资料' | ||
| 66 | + * }) | ||
| 67 | + */ | ||
| 68 | + const trackEvent = async (type, objectId, extraData = {}) => { | ||
| 69 | + try { | ||
| 70 | + const params = { | ||
| 71 | + type, | ||
| 72 | + object_id: objectId, | ||
| 73 | + ...extraData | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + await addAPI(params) | ||
| 77 | + | ||
| 78 | + // 静默处理埋点失败,不影响用户操作 | ||
| 79 | + // 仅在开发环境打印日志 | ||
| 80 | + if (process.env.NODE_ENV === 'development') { | ||
| 81 | + console.log('[埋点成功]', { type, objectId, extraData }) | ||
| 82 | + } | ||
| 83 | + } catch (error) { | ||
| 84 | + // 埋点失败不应影响用户体验,静默处理 | ||
| 85 | + if (process.env.NODE_ENV === 'development') { | ||
| 86 | + console.error('[埋点失败]', error) | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + /** | ||
| 92 | + * 追踪文件阅读事件 | ||
| 93 | + * | ||
| 94 | + * @description 便捷方法:追踪文件/素材阅读 | ||
| 95 | + * @async | ||
| 96 | + * @param {string} fileId - 文件 ID | ||
| 97 | + * @param {Object} extraData - 额外的埋点数据 | ||
| 98 | + * @returns {Promise<void>} | ||
| 99 | + * | ||
| 100 | + * @example | ||
| 101 | + * await trackFileRead('file-id-123', { | ||
| 102 | + * title: '保险产品手册' | ||
| 103 | + * }) | ||
| 104 | + */ | ||
| 105 | + const trackFileRead = async (fileId, extraData = {}) => { | ||
| 106 | + await trackEvent(EventType.READ_FILE, fileId, extraData) | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + return { | ||
| 110 | + trackEvent, | ||
| 111 | + trackFileRead | ||
| 112 | + } | ||
| 113 | +} |
| ... | @@ -8,7 +8,7 @@ | ... | @@ -8,7 +8,7 @@ |
| 8 | * @date 2026-01-31 | 8 | * @date 2026-01-31 |
| 9 | */ | 9 | */ |
| 10 | 10 | ||
| 11 | -import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile } from '@tarojs/taro' | 11 | +import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile, previewImage } from '@tarojs/taro' |
| 12 | import { isVideoFile } from '@/utils/tools' | 12 | import { isVideoFile } from '@/utils/tools' |
| 13 | 13 | ||
| 14 | /** | 14 | /** |
| ... | @@ -18,6 +18,18 @@ import { isVideoFile } from '@/utils/tools' | ... | @@ -18,6 +18,18 @@ import { isVideoFile } from '@/utils/tools' |
| 18 | */ | 18 | */ |
| 19 | export function useFileOperation() { | 19 | export function useFileOperation() { |
| 20 | /** | 20 | /** |
| 21 | + * 判断是否为图片文件 | ||
| 22 | + * | ||
| 23 | + * @param {string} fileName - 文件名 | ||
| 24 | + * @returns {boolean} 是否为图片文件 | ||
| 25 | + */ | ||
| 26 | + const isImageFile = (fileName) => { | ||
| 27 | + if (!fileName) return false | ||
| 28 | + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'] | ||
| 29 | + const extension = fileName.split('.').pop()?.toLowerCase() || '' | ||
| 30 | + return imageExtensions.includes(extension) | ||
| 31 | + } | ||
| 32 | + /** | ||
| 21 | * 打开文件的通用函数 | 33 | * 打开文件的通用函数 |
| 22 | * | 34 | * |
| 23 | * @description 使用 Taro.openDocument 打开文件,支持菜单转发和保存 | 35 | * @description 使用 Taro.openDocument 打开文件,支持菜单转发和保存 |
| ... | @@ -175,7 +187,10 @@ export function useFileOperation() { | ... | @@ -175,7 +187,10 @@ export function useFileOperation() { |
| 175 | /** | 187 | /** |
| 176 | * 查看文件(入口函数) | 188 | * 查看文件(入口函数) |
| 177 | * | 189 | * |
| 178 | - * @description 检查文件是否有下载地址,判断文件类型,视频文件跳转播放页面,其他文件下载并打开 | 190 | + * @description 检查文件是否有下载地址,判断文件类型: |
| 191 | + * - 图片文件:使用 Taro.previewImage 预览 | ||
| 192 | + * - 视频文件:跳转播放页面 | ||
| 193 | + * - 其他文件:下载并使用 Taro.openDocument 打开 | ||
| 179 | * @async | 194 | * @async |
| 180 | * @param {Object} item - 文件信息对象 | 195 | * @param {Object} item - 文件信息对象 |
| 181 | * @param {string} [item.downloadUrl] - 文件下载地址 | 196 | * @param {string} [item.downloadUrl] - 文件下载地址 |
| ... | @@ -185,8 +200,8 @@ export function useFileOperation() { | ... | @@ -185,8 +200,8 @@ export function useFileOperation() { |
| 185 | * @example | 200 | * @example |
| 186 | * const { viewFile } = useFileOperation() | 201 | * const { viewFile } = useFileOperation() |
| 187 | * await viewFile({ | 202 | * await viewFile({ |
| 188 | - * downloadUrl: 'https://example.com/video.mp4', | 203 | + * downloadUrl: 'https://example.com/file.pdf', |
| 189 | - * fileName: 'tutorial.mp4' | 204 | + * fileName: 'document.pdf' |
| 190 | * }) | 205 | * }) |
| 191 | */ | 206 | */ |
| 192 | const viewFile = async (item) => { | 207 | const viewFile = async (item) => { |
| ... | @@ -200,6 +215,26 @@ export function useFileOperation() { | ... | @@ -200,6 +215,26 @@ export function useFileOperation() { |
| 200 | return | 215 | return |
| 201 | } | 216 | } |
| 202 | 217 | ||
| 218 | + // 判断是否为图片文件 | ||
| 219 | + if (isImageFile(item.fileName)) { | ||
| 220 | + // 图片文件:使用图片预览 | ||
| 221 | + console.log('[文件操作] 检测到图片文件,使用图片预览') | ||
| 222 | + try { | ||
| 223 | + await previewImage({ | ||
| 224 | + urls: [item.downloadUrl], | ||
| 225 | + current: item.downloadUrl | ||
| 226 | + }) | ||
| 227 | + } catch (error) { | ||
| 228 | + console.error('[文件操作] 图片预览失败:', error) | ||
| 229 | + showToast({ | ||
| 230 | + title: '图片预览失败', | ||
| 231 | + icon: 'none', | ||
| 232 | + duration: 2000 | ||
| 233 | + }) | ||
| 234 | + } | ||
| 235 | + return | ||
| 236 | + } | ||
| 237 | + | ||
| 203 | // 判断是否为视频文件 | 238 | // 判断是否为视频文件 |
| 204 | if (isVideoFile(item.fileName)) { | 239 | if (isVideoFile(item.fileName)) { |
| 205 | // 视频文件:跳转到视频播放页面 | 240 | // 视频文件:跳转到视频播放页面 | ... | ... |
| ... | @@ -168,10 +168,13 @@ const fetchProductDetail = async (id) => { | ... | @@ -168,10 +168,13 @@ const fetchProductDetail = async (id) => { |
| 168 | /** | 168 | /** |
| 169 | * 查看文档 | 169 | * 查看文档 |
| 170 | * | 170 | * |
| 171 | - * @description 打开文档预览 | 171 | + * @description 打开文档预览,支持图片、视频、PDF 等多种格式 |
| 172 | * @param {Object} doc - 文档对象 | 172 | * @param {Object} doc - 文档对象 |
| 173 | + * @param {string} doc.file_name - 文档名称 | ||
| 174 | + * @param {string} doc.file_url - 文档 URL | ||
| 173 | */ | 175 | */ |
| 174 | const viewDocument = (doc) => { | 176 | const viewDocument = (doc) => { |
| 177 | + // 打开文件预览(自动判断文件类型) | ||
| 175 | viewFile({ | 178 | viewFile({ |
| 176 | fileName: doc.file_name, | 179 | fileName: doc.file_name, |
| 177 | downloadUrl: doc.file_url | 180 | downloadUrl: doc.file_url | ... | ... |
-
Please register or login to post a comment