useFileOperation.js 6.57 KB
/**
 * 统一的文件操作 Composable
 *
 * @description 提供文件下载、打开、预览等统一操作逻辑
 * 处理 PDF、Office 文档等多种文件格式的预览和下载
 *
 * @author Claude Code
 * @date 2026-01-31
 */

import { showToast, showLoading, hideLoading, showModal, openDocument, downloadFile } from '@tarojs/taro'

/**
 * 文件操作 Hook
 *
 * @returns {Object} 文件操作方法集合
 */
export function useFileOperation() {
  /**
   * 打开文件的通用函数
   *
   * @description 使用 Taro.openDocument 打开文件,支持菜单转发和保存
   * @async
   * @param {string} filePath - 本地文件路径
   * @param {Object} item - 文件信息对象
   * @param {string} item.fileName - 文件名(用于判断文件类型)
   * @returns {Promise<void>}
   *
   * @example
   * const { openFile } = useFileOperation()
   * await openFile(tempFilePath, { fileName: 'document.pdf' })
   */
  const openFile = async (filePath, item) => {
    try {
      await openDocument({
        filePath: filePath,
        showMenu: true, // 显示右上角菜单,用户可以转发、保存等
        success: () => {
          console.log('文件打开成功')
          // 文件打开后,延迟提示用户如果看不到内容该如何操作
          const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''
          const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']

          if (unsupportedFormats.includes(fileExt)) {
            setTimeout(() => {
              showToast({
                title: '如无法预览,请使用右上角菜单分享',
                icon: 'none',
                duration: 3000
              })
            }, 1500)
          }
        },
        fail: (err) => {
          console.error('打开文件失败:', err)

          // 获取文件扩展名
          const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''

          // 根据文件类型给出提示
          let message = '文件打开失败'
          let suggestion = ''

          if (['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(fileExt)) {
            message = '暂不支持预览 Office 文档'
            suggestion = '\n\n建议:\n1. 点击右上角"..."菜单\n2. 选择"发送给朋友"\n3. 在电脑或支持的应用中打开'
          } else if (['pdf'].includes(fileExt)) {
            message = 'PDF 文件打开失败'
            suggestion = '\n\n文件可能已损坏,请联系管理员'
          } else {
            message = `暂不支持预览 ${fileExt.toUpperCase()} 格式文件`
            suggestion = '\n\n请在电脑或其他应用中打开'
          }

          showModal({
            title: '提示',
            content: message + suggestion,
            showCancel: false,
            confirmText: '我知道了'
          })
        }
      })
    } catch (error) {
      console.error('打开文件异常:', error)
      showToast({
        title: '打开文件失败',
        icon: 'none',
        duration: 2000
      })
    }
  }

  /**
   * 下载并打开文件的内部函数
   *
   * @description 先下载文件到本地临时路径,再调用 openFile 打开
   * @async
   * @param {Object} item - 文件信息对象
   * @param {string} item.downloadUrl - 文件下载地址
   * @param {string} item.fileName - 文件名
   * @returns {Promise<void>}
   *
   * @example
   * const { downloadAndOpenFile } = useFileOperation()
   * await downloadAndOpenFile({
   *   downloadUrl: 'https://example.com/file.pdf',
   *   fileName: 'document.pdf'
   * })
   */
  const downloadAndOpenFile = async (item) => {
    try {
      // 下载文件
      const downloadResult = await downloadFile({
        url: item.downloadUrl
      })

      // 检查下载结果
      if (downloadResult.statusCode !== 200) {
        throw new Error(`打开失败: HTTP ${downloadResult.statusCode}`)
      }

      if (!downloadResult.tempFilePath) {
        throw new Error('打开失败: 未获取到文件')
      }

      // 隐藏加载提示
      hideLoading()

      // 获取文件扩展名
      const fileExt = item.fileName.split('.').pop()?.toLowerCase() || ''

      // 微信小程序对 Office 文档支持有限,提前提示用户
      const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']

      if (unsupportedFormats.includes(fileExt)) {
        // 对于 Office 文档,先提示用户,但仍尝试打开
        showModal({
          title: '预览提示',
          content: `小程序对 ${fileExt.toUpperCase()} 文档的预览支持有限,如果显示为空白,请点击右上角"..."菜单,选择"发送给朋友"后在电脑或其他应用中打开。\n\n是否继续尝试预览?`,
          confirmText: '继续',
          cancelText: '取消',
          success: (modalRes) => {
            if (modalRes.confirm) {
              openFile(downloadResult.tempFilePath, item)
            }
          }
        })
      } else {
        // 其他格式直接打开
        await openFile(downloadResult.tempFilePath, item)
      }
    } 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
      })
    }
  }

  /**
   * 查看文件(入口函数)
   *
   * @description 检查文件是否有下载地址,然后下载并打开文件
   * @async
   * @param {Object} item - 文件信息对象
   * @param {string} [item.downloadUrl] - 文件下载地址
   * @param {string} item.fileName - 文件名
   * @returns {Promise<void>}
   *
   * @example
   * const { viewFile } = useFileOperation()
   * await viewFile({
   *   downloadUrl: 'https://example.com/file.pdf',
   *   fileName: 'document.pdf'
   * })
   */
  const viewFile = async (item) => {
    // 检查是否有下载地址
    if (!item.downloadUrl) {
      showToast({
        title: '该文件暂无查看地址',
        icon: 'none',
        duration: 2000
      })
      return
    }

    // 显示加载提示
    showLoading({
      title: '打开中...',
      mask: true
    })

    // 下载并打开文件
    await downloadAndOpenFile(item)
  }

  return {
    openFile,
    downloadAndOpenFile,
    viewFile
  }
}