useFileOperation.js
13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
/**
* 统一的文件操作 Composable
*
* @description 提供文件下载、打开、预览等统一操作逻辑
* 处理 PDF、Office 文档、视频等多种文件格式的预览和下载
*
* @author Claude Code
* @date 2026-01-31
*/
import Taro from '@tarojs/taro'
import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile, previewImage } from '@tarojs/taro'
import { isVideoFile } from '@/utils/tools'
import { extractExtensionFromFile } from '@/utils/documentIcons'
/**
* 文件操作 Hook
*
* @returns {Object} 文件操作方法集合
*/
export function useFileOperation() {
/**
* 判断是否为图片文件
*
* @description 支持传入文件名或包含 extension 字段的对象,优先使用 extension 字段
* @param {string|Object} fileNameOrItem - 文件名或文件对象
* @param {string} [fileNameOrItem.fileName] - 文件名
* @param {string} [fileNameOrItem.extension] - 文件扩展名(优先使用)
* @returns {boolean} 是否为图片文件
*/
const isImageFile = (fileNameOrItem) => {
const extension = extractExtensionFromFile(fileNameOrItem)
if (!extension) return false
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']
return imageExtensions.includes(extension)
}
/**
* 打开文件的通用函数
*
* @description 使用 Taro.openDocument 打开文件,支持菜单转发和保存
* @async
* @param {string} filePath - 本地文件路径
* @param {Object} item - 文件信息对象
* @param {string} [item.fileName] - 文件名(用于判断文件类型)
* @param {string} [item.extension] - 文件扩展名(优先使用)
* @param {string} [item.downloadUrl] - 原始下载地址(用于失败时复制链接)
* @returns {Promise<void>}
*
* @example
* const { openFile } = useFileOperation()
* await openFile(tempFilePath, { fileName: 'document.pdf' })
* await openFile(tempFilePath, { extension: 'pdf' }) // 优先使用 extension
*/
const openFile = async (filePath, item) => {
// 记录文件打开开始的日志
console.log('[文件操作] 开始打开文件:', {
filePath,
fileName: item.fileName,
extension: item.extension,
downloadUrl: item.downloadUrl
})
try {
let openResult = false
await openDocument({
filePath: filePath,
showMenu: true, // 显示右上角菜单,用户可以转发、保存等
success: () => {
// 记录成功回调
console.log('[文件操作] ✅ openDocument success 回调触发 - 文件已打开')
openResult = true
// 文件打开后,延迟提示用户如果看不到内容该如何操作
// 使用统一的扩展名提取函数
const fileExt = extractExtensionFromFile(item)
const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']
if (unsupportedFormats.includes(fileExt)) {
console.log('[文件操作] ⚠️ 检测到不支持的格式:', fileExt, '- 将在 1.5s 后显示提示')
setTimeout(() => {
showToast({
title: '如无法预览,请使用右上角菜单分享',
icon: 'none',
duration: 3000
})
}, 1500)
}
},
fail: (err) => {
// 记录失败回调(这个通常不会在"内容无法显示"时触发)
console.log('[文件操作] ❌ openDocument fail 回调触发:', err)
console.log('[文件操作] 错误详情:', {
errMsg: err.errMsg,
errorCode: err.errCode
})
openResult = false
// 获取文件扩展名(使用统一的扩展名提取函数)
const fileExt = extractExtensionFromFile(item)
// 根据文件类型给出提示
let message = '文件打开失败'
let suggestion = ''
let showCopyButton = false
if (['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(fileExt)) {
message = '暂不支持预览 Office 文档'
suggestion = '\n\n您可以复制链接,在电脑或其他应用中打开'
showCopyButton = true
} else if (['pdf'].includes(fileExt)) {
message = 'PDF 文件打开失败'
suggestion = '\n\n您可以复制链接在其他应用中打开,或前往"意见反馈"告诉我们'
showCopyButton = !!item.downloadUrl
} else {
message = `暂不支持预览 ${fileExt.toUpperCase()} 格式文件`
suggestion = '\n\n请在电脑或其他应用中打开'
showCopyButton = !!item.downloadUrl
}
// 构建 showModal 参数
const modalParams = {
title: '提示',
content: message + suggestion,
confirmText: showCopyButton ? '复制链接' : '我知道了',
cancelText: '去反馈',
showCancel: true
}
showModal({
...modalParams,
success: (modalRes) => {
console.log('[文件操作] 用户选择:', modalRes.confirm ? '复制链接' : '去反馈')
if (modalRes.confirm) {
// 点击主按钮:复制链接(如果有 downloadUrl)
if (showCopyButton && item.downloadUrl) {
// 复制下载链接到剪贴板
console.log('[文件操作] 开始复制链接:', item.downloadUrl)
Taro.setClipboardData({
data: item.downloadUrl,
success: () => {
console.log('[文件操作] ✅ 链接复制成功')
showToast({
title: '链接已复制',
icon: 'success',
duration: 2000
})
},
fail: (clipboardErr) => {
console.log('[文件操作] ❌ 链接复制失败:', clipboardErr)
showToast({
title: '复制失败',
icon: 'none',
duration: 2000
})
}
})
}
// 如果没有 downloadUrl,点击"我知道了"不做任何事
} else {
// 点击取消按钮:跳转到意见反馈页面
console.log('[文件操作] 跳转到意见反馈页面')
Taro.navigateTo({
url: '/pages/feedback/index'
})
}
}
})
}
})
return openResult
} catch (error) {
// 记录异常(Promise rejection)
console.log('[文件操作] ❌ openDocument 异常捕获:', error)
showToast({
title: '打开文件失败',
icon: 'none',
duration: 2000
})
return false
}
}
/**
* 下载并打开文件的内部函数
*
* @description 先下载文件到本地临时路径,再调用 openFile 打开
* @async
* @param {Object} item - 文件信息对象
* @param {string} item.downloadUrl - 文件下载地址
* @param {string} [item.fileName] - 文件名
* @param {string} [item.extension] - 文件扩展名(优先使用)
* @returns {Promise<void>}
*
* @example
* const { downloadAndOpenFile } = useFileOperation()
* await downloadAndOpenFile({
* downloadUrl: 'https://example.com/file.pdf',
* fileName: 'document.pdf'
* })
* await downloadAndOpenFile({
* downloadUrl: 'https://example.com/file.pdf',
* extension: 'pdf' // 优先使用 extension
* })
*/
const downloadAndOpenFile = async (item) => {
try {
console.log('[文件操作] 开始下载文件:', {
downloadUrl: item.downloadUrl,
fileName: item.fileName
})
// 下载文件
const downloadResult = await downloadFile({
url: item.downloadUrl
})
console.log('[文件操作] 下载结果:', {
statusCode: downloadResult.statusCode,
tempFilePath: downloadResult.tempFilePath
})
// 检查下载结果
if (downloadResult.statusCode !== 200) {
throw new Error(`打开失败: HTTP ${downloadResult.statusCode}`)
}
if (!downloadResult.tempFilePath) {
throw new Error('打开失败: 未获取到文件')
}
// 隐藏加载提示
hideLoading()
// 获取文件扩展名(使用统一的扩展名提取函数)
const fileExt = extractExtensionFromFile(item)
// 微信小程序对 Office 文档支持有限,提前提示用户
const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']
const dontShowAgainKey = 'office_doc_tip_dont_show_again'
// 检查用户是否已选择"不再提醒"
const dontShowAgain = Taro.getStorageSync(dontShowAgainKey)
console.log('[文件操作] "不再提醒"设置状态:', dontShowAgain)
if (unsupportedFormats.includes(fileExt) && !dontShowAgain) {
// 首次打开 Office 文档,显示提示
console.log('[文件操作] 首次打开 Office 文档,显示提示')
return await new Promise((resolve) => {
showModal({
title: '预览提示',
content: `小程序对 ${fileExt.toUpperCase()} 文档的预览支持有限,如果显示为空白,请点击右上角"..."菜单,选择"发送给朋友"后在电脑或其他应用中打开。\n\n是否继续预览?`,
confirmText: '继续预览',
cancelText: '取消',
success: async (modalRes) => {
console.log('[文件操作] 用户选择:', modalRes.confirm ? '继续预览' : '取消')
if (modalRes.confirm) {
// 记录用户选择,下次不再提示
try {
Taro.setStorageSync(dontShowAgainKey, true)
console.log('[文件操作] ✅ 已保存"不再提醒"设置')
} catch (storageErr) {
console.log('[文件操作] ❌ 保存设置失败:', storageErr)
}
const openResult = await openFile(downloadResult.tempFilePath, item)
resolve(openResult)
return
}
resolve(false)
}
})
})
} else {
// 用户已选择"不再提醒"或非 Office 文档,直接打开
console.log('[文件操作] 直接打开文件(已选择不再提醒 或 非 Office 文档)')
const openResult = await openFile(downloadResult.tempFilePath, item)
return openResult
}
} catch (error) {
// 确保隐藏加载提示
hideLoading()
console.error('打开文件出错:', error)
// 根据错误类型显示不同的提示
let errorMessage = '打开失败,请重试'
if (error.errMsg && error.errMsg.includes('network')) {
errorMessage = '网络连接失败,请检查网络'
} else if (error.errMsg && error.errMsg.includes('TLS')) {
errorMessage = '安全连接失败,请检查网络'
}
showToast({
title: errorMessage,
icon: 'none',
duration: 2000
})
return false
}
}
/**
* 查看文件(入口函数)
*
* @description 检查文件是否有下载地址,判断文件类型:
* - 图片文件:使用 Taro.previewImage 预览
* - 视频文件:跳转播放页面
* - 其他文件:下载并使用 Taro.openDocument 打开
* @async
* @param {Object} item - 文件信息对象
* @param {string} [item.downloadUrl] - 文件下载地址
* @param {string} [item.fileName] - 文件名
* @param {string} [item.extension] - 文件扩展名(优先使用)
* @returns {Promise<void>}
*
* @example
* const { viewFile } = useFileOperation()
* await viewFile({
* downloadUrl: 'https://example.com/file.pdf',
* fileName: 'document.pdf'
* })
* await viewFile({
* downloadUrl: 'https://example.com/file.pdf',
* extension: 'pdf' // 优先使用 extension
* })
*/
const viewFile = async (item) => {
// 检查是否有下载地址
if (!item.downloadUrl) {
showToast({
title: '该文件暂无查看地址',
icon: 'none',
duration: 2000
})
return false
}
// 判断是否为图片文件(优先使用 extension 字段)
if (isImageFile(item)) {
// 图片文件:使用图片预览
console.log('[文件操作] 检测到图片文件,使用图片预览')
try {
await previewImage({
urls: [item.downloadUrl],
current: item.downloadUrl
})
return true
} catch (error) {
console.error('[文件操作] 图片预览失败:', error)
showToast({
title: '图片预览失败',
icon: 'none',
duration: 2000
})
return false
}
}
// 判断是否为视频文件(优先使用 extension 字段)
if (isVideoFile(item)) {
// 视频文件:跳转到视频播放页面
try {
await Taro.navigateTo({
url: `/pages/video-player/index?url=${encodeURIComponent(item.downloadUrl)}&title=${encodeURIComponent(item.title || item.fileName)}`
})
return true
} catch (error) {
console.error('[文件操作] 视频打开失败:', error)
showToast({
title: '视频打开失败',
icon: 'none',
duration: 2000
})
return false
}
}
// 非视频文件:下载并打开
// 显示加载提示
showLoading({
title: '打开中...',
mask: true
})
// 下载并打开文件
const openResult = await downloadAndOpenFile(item)
return openResult
}
return {
openFile,
downloadAndOpenFile,
viewFile
}
}