/** * 七牛云上传工具封装 * 提供图片和文件上传到七牛云的功能 */ import { getQiniuUploadToken } from '@/api' /** * 上传文件到七牛云 * @param {string} filePath - 文件路径(临时路径) * @param {string} token - 七牛云上传token * @param {string} key - 文件key(可选,不传则自动生成) * @returns {Promise} 返回上传结果,包含key等信息 */ export function uploadToQiniu(filePath, token, key = '') { return new Promise((resolve, reject) => { uni.uploadFile({ url: 'https://up-z2.qiniup.com', filePath: filePath, name: 'file', formData: { token: token, key: key, }, success: (res) => { try { // 七牛云上传成功返回格式: { hash: "...", key: "uploads/xxx.jpg" } const data = JSON.parse(res.data) // 验证返回数据格式 if (!data || typeof data !== 'object') { reject(new Error('上传响应数据格式错误')) return } // 检查是否有错误信息(七牛云可能返回错误) if (data.error) { reject(new Error(data.error || '上传失败')) return } resolve(data) } catch (error) { console.error('解析上传响应失败:', res.data, error) reject(new Error('响应数据解析失败: ' + error.message)) } }, fail: (error) => { reject(error) }, }) }) } /** * 七牛云图片上传服务 * 将临时文件路径转换为七牛云URL * @param {string} tempFilePath - 临时文件路径 * @param {object} [options] - 配置选项 * @param {number} [options.maxSize=10] - 最大文件大小(MB) * @param {string} [options.prefix='uploads/'] - 文件前缀路径 * @param {boolean} [options.showToast=true] - 是否显示提示 * @param {string} [options.domain='https://api.ccttiot.com'] - 七牛云域名 * @returns {Promise} 七牛云文件URL */ export const tempUrlToRealUrl = async (tempFilePath, options = {}) => { const { maxSize = 10, prefix = 'uploads/', showToast = true, domain = 'https://api.ccttiot.com', } = options try { // 1. 检查文件大小 const fileInfo = await getFileInfo(tempFilePath) const sizeInMB = fileInfo.size / (1024 * 1024) if (sizeInMB > maxSize) { throw new Error(`文件大小不能超过 ${maxSize}MB`) } // 2. 获取上传凭证 const token = await getQiniuToken() if (!token) { throw new Error('获取上传凭证失败') } // 3. 生成唯一文件名 const fileExt = getFileExtension(tempFilePath) const key = generateUniqueKey(prefix, fileExt) // 4. 上传到七牛云 const uploadResult = await uploadToQiniu(tempFilePath, token, key) // 验证上传结果(七牛云返回格式: { hash: "...", key: "uploads/xxx.jpg" }) if (!uploadResult || typeof uploadResult !== 'object') { throw new Error('上传失败:返回数据格式错误') } if (!uploadResult.key) { console.error('上传响应缺少key字段:', uploadResult) throw new Error('上传失败:未返回文件key') } // 5. 构建完整URL const qiniuUrl = `${domain}/${uploadResult.key}` console.log('上传成功:', { key: uploadResult.key, hash: uploadResult.hash, url: qiniuUrl }) if (showToast) { // 上传成功不显示提示,避免打扰用户 // uni.showToast({ title: '上传成功', icon: 'success' }) } return qiniuUrl } catch (error) { console.error('七牛云上传失败:', error) if (showToast) { uni.showToast({ title: error.message || '上传失败', icon: 'none', }) } throw error } } /** * 批量上传多张图片 * @param {string[]} tempFilePaths - 临时文件路径数组 * @param {object} [options] - 配置选项 * @returns {Promise} 七牛云URL数组 */ export const batchUploadToQiniu = async (tempFilePaths, options = {}) => { return Promise.all( tempFilePaths.map(filePath => tempUrlToRealUrl(filePath, { ...options, showToast: false })) ) } /** * 选择图片并自动上传到七牛云 * @param {object} [options] - 配置选项 * @param {number} [options.count=9] - 最多选择图片数量 * @param {string[]} [options.sizeType=['original', 'compressed']] - 图片尺寸类型 * @param {string[]} [options.sourceType=['album', 'camera']] - 图片来源 * @param {object} [options.uploadOptions] - 上传配置选项(传递给tempUrlToRealUrl) * @returns {Promise} 七牛云URL数组 */ export const chooseAndUploadImages = (options = {}) => { const { count = 9, sizeType = ['original', 'compressed'], sourceType = ['album', 'camera'], uploadOptions = {} } = options return new Promise((resolve, reject) => { uni.chooseImage({ count: count, sizeType: sizeType, sourceType: sourceType, success: async (res) => { try { // 显示上传中提示 uni.showLoading({ title: '上传中...', mask: true }) // 批量上传图片 const urls = await batchUploadToQiniu(res.tempFilePaths, uploadOptions) uni.hideLoading() resolve(urls) } catch (error) { uni.hideLoading() reject(error) } }, fail: (err) => { reject(err) } }) }) } /** * 上传单个文件到七牛云(支持图片和文档) * @param {string} filePath - 文件临时路径或URI * @param {string} fileName - 文件名(可选,用于文档文件) * @param {object} [options] - 配置选项 * @param {number} [options.maxSize=50] - 最大文件大小(MB),文档文件默认50MB * @param {string} [options.prefix='uploads/'] - 文件前缀路径 * @param {boolean} [options.showToast=true] - 是否显示提示 * @param {string} [options.domain='https://api.ccttiot.com'] - 七牛云域名 * @returns {Promise} 返回 { url: string, name: string, size: number } */ export const uploadFileToQiniu = async (filePath, fileName = '', options = {}) => { const { maxSize = 50, prefix = 'uploads/', showToast = true, domain = 'https://api.ccttiot.com', } = options try { // 1. 如果是URI格式(content://),需要先转换为临时路径 let tempFilePath = filePath if (filePath.startsWith('content://')) { // 在uni-app中,content:// URI需要特殊处理 // 对于文档文件,uni.uploadFile可以直接使用URI tempFilePath = filePath } // 2. 获取文件信息(如果不是URI格式) let fileInfo = { size: 0 } if (!filePath.startsWith('content://')) { try { fileInfo = await getFileInfo(tempFilePath) } catch (error) { console.warn('获取文件信息失败:', error) } } const sizeInMB = fileInfo.size / (1024 * 1024) if (sizeInMB > maxSize) { throw new Error(`文件大小不能超过 ${maxSize}MB`) } // 3. 获取上传凭证 const token = await getQiniuToken() if (!token) { throw new Error('获取上传凭证失败') } // 4. 生成唯一文件名 let fileExt = 'file' if (fileName) { const parts = fileName.split('.') if (parts.length > 1) { fileExt = parts.pop().toLowerCase() } } else { fileExt = getFileExtension(tempFilePath) } const key = generateUniqueKey(prefix, fileExt) // 5. 上传到七牛云 const uploadResult = await uploadToQiniu(tempFilePath, token, key) // 验证上传结果 if (!uploadResult || typeof uploadResult !== 'object') { throw new Error('上传失败:返回数据格式错误') } if (!uploadResult.key) { console.error('上传响应缺少key字段:', uploadResult) throw new Error('上传失败:未返回文件key') } // 6. 构建完整URL const qiniuUrl = `${domain}/${uploadResult.key}` console.log('文件上传成功:', { key: uploadResult.key, url: qiniuUrl }) // 7. 生成文件名(如果没有提供) const finalFileName = fileName || `文件.${fileExt}` if (showToast) { // 上传成功不显示提示,避免打扰用户 } return { url: qiniuUrl, name: finalFileName, size: fileInfo.size || 0 } } catch (error) { console.error('文件上传失败:', error) if (showToast) { uni.showToast({ title: error.message || '上传失败', icon: 'none', }) } throw error } } /** * 批量上传文件到七牛云(支持图片和文档) * @param {Array<{path: string, name?: string}>} files - 文件数组,每个文件包含path和可选的name * @param {object} [options] - 配置选项 * @returns {Promise>} 上传结果数组 */ export const batchUploadFilesToQiniu = async (files, options = {}) => { return Promise.all( files.map(file => uploadFileToQiniu(file.path, file.name || '', { ...options, showToast: false })) ) } /** * 获取文件信息 * @private */ const getFileInfo = (filePath) => { return new Promise((resolve, reject) => { uni.getFileInfo({ filePath, success: resolve, fail: reject, }) }) } /** * 获取七牛云上传token * @private */ const getQiniuToken = async () => { try { const res = await getQiniuUploadToken() // 根据实际API返回格式处理 // 如果API返回的是 { code: 200, data: { token: 'xxx' } } 格式 if (res && typeof res === 'object') { // 如果响应拦截器已经处理过,直接返回token if (res.token) { return res.token } // 如果返回的是 { data: { token: 'xxx' } } 格式 if (res.data && res.data.token) { return res.data.token } // 如果返回的是 { token: 'xxx' } 格式 if (res.token) { return res.token } } // 如果返回的是字符串token if (typeof res === 'string') { return res } throw new Error('获取token失败:返回格式不正确') } catch (error) { console.error('获取七牛云token失败:', error) throw error } } /** * 获取文件扩展名 * @private */ const getFileExtension = (filePath) => { const parts = filePath.split('.') return parts.length > 1 ? parts.pop().toLowerCase() : 'jpg' } /** * 生成唯一文件名 * @private */ const generateUniqueKey = (prefix, extension) => { const timestamp = Date.now() const randomStr = Math.random().toString(36).slice(2, 10) return `${prefix}${timestamp}_${randomStr}.${extension}` } /** * 工具函数:检查是否为临时路径 * @param {string} url - 要检查的URL * @returns {boolean} 是否为临时路径 */ export const isTempFilePath = (url) => { return ( url && (url.startsWith('http://temp/') || url.startsWith('wxfile://') || url.includes('tmp/') || !url.startsWith('http')) ) } /** * 工具函数:从七牛云URL提取文件key * @param {string} qiniuUrl - 七牛云完整URL * @returns {string|null} 文件key */ export const extractQiniuKey = (qiniuUrl) => { if (!qiniuUrl) return null try { const urlObj = new URL(qiniuUrl) return urlObj.pathname.substring(1) // 去除开头的/ } catch (error) { // 如果不是标准URL,尝试直接提取路径 const match = qiniuUrl.match(/https?:\/\/[^\/]+\/(.+)/) return match ? match[1] : null } }