add:新增保存功能

This commit is contained in:
2026-06-11 10:26:53 +08:00
parent 1f1581321c
commit d03cf59335
4 changed files with 1054 additions and 250 deletions
+286
View File
@@ -0,0 +1,286 @@
import { showMsg } from "@/utils/showMsg";
/**
* 全环境通用文件保存工具
* 支持:PC浏览器、手机浏览器、uniapp webview
* 支持文件类型:图片、视频、纯文字
*/
// 文件类型枚举
export const FileType = {
IMAGE: 'image',
VIDEO: 'video',
TEXT: 'text'
}
/**
* 检测是否在uniapp webview环境中
*/
const isUniAppWebview = () => {
return typeof uni !== 'undefined'
&& typeof uni.downloadFile === 'function'
&& typeof uni.saveImageToPhotosAlbum === 'function'
}
/**
* 检测是否是iOS设备
*/
const isIOS = () => {
return /iPhone|iPad|iPod/i.test(navigator.userAgent)
}
/**
* 通用保存文件入口(所有环境通用)
* @param {Object} options 保存选项
* @param {string} options.type 文件类型:FileType.IMAGE / FileType.VIDEO / FileType.TEXT
* @param {string} options.url 文件URL(图片/视频必填)
* @param {string} options.content 文字内容(文字类型必填)
* @param {string} options.fileName 保存的文件名(可选,自动生成)
* @returns {Promise<void>}
*/
export const saveFile = async (options) => {
const { type, url, content, fileName } = options
// 参数校验
if (!type) {
throw new Error('请指定文件类型')
}
if ((type === FileType.IMAGE || type === FileType.VIDEO) && !url) {
throw new Error('请提供文件URL')
}
if (type === FileType.TEXT && !content) {
throw new Error('请提供文字内容')
}
// 生成默认文件名
const timestamp = Date.now()
const defaultFileName = `file_${timestamp}`
const finalFileName = fileName || defaultFileName
// 自动选择保存方式
if (isUniAppWebview()) {
await saveInUniApp(type, url, content, finalFileName)
} else {
await saveInBrowser(type, url, content, finalFileName)
}
}
/**
* uniapp webview环境保存
*/
const saveInUniApp = async (type, url, content, fileName) => {
switch (type) {
case FileType.IMAGE:
await uniSaveImage(url)
break
case FileType.VIDEO:
await uniSaveVideo(url)
break
case FileType.TEXT:
// webview无法直接保存文件,降级为复制到剪贴板
await uniCopyText(content)
break
default:
throw new Error('不支持的文件类型')
}
}
/**
* 浏览器环境保存
*/
const saveInBrowser = async (type, url, content, fileName) => {
switch (type) {
case FileType.IMAGE:
await browserSaveImage(url, fileName)
break
case FileType.VIDEO:
await browserSaveVideo(url, fileName)
break
case FileType.TEXT:
browserSaveText(content, fileName)
break
default:
throw new Error('不支持的文件类型')
}
}
// ==================== uniapp保存方法 ====================
const uniSaveImage = (url) => {
return new Promise((resolve, reject) => {
uni.showLoading({ title: '保存中...' })
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error('图片下载失败'))
return
}
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => resolve(),
fail: (err) => {
if (err.errMsg.includes('auth')) {
reject(new Error('请在设置中开启相册权限后重试'))
} else if (err.errMsg.includes('cancel')) {
reject(new Error('已取消保存'))
} else {
reject(new Error('保存图片失败'))
}
}
})
},
fail: () => reject(new Error('网络请求失败,请检查网络连接')),
complete: () => uni.hideLoading()
})
})
}
const uniSaveVideo = (url) => {
return new Promise((resolve, reject) => {
uni.showLoading({ title: '保存中...' })
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode !== 200) {
reject(new Error('视频下载失败'))
return
}
uni.saveVideoToPhotosAlbum({
filePath: res.tempFilePath,
success: () => resolve(),
fail: (err) => {
if (err.errMsg.includes('auth')) {
reject(new Error('请在设置中开启相册权限后重试'))
} else if (err.errMsg.includes('cancel')) {
reject(new Error('已取消保存'))
} else {
reject(new Error('保存视频失败'))
}
}
})
},
fail: () => reject(new Error('网络请求失败,请检查网络连接')),
complete: () => uni.hideLoading()
})
})
}
const uniCopyText = (text) => {
return new Promise((resolve, reject) => {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({ title: '文案已复制到剪贴板', icon: 'success' })
resolve()
},
fail: () => reject(new Error('复制失败'))
})
})
}
// ==================== 浏览器保存方法 ====================
const browserSaveImage = (url, fileName) => {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
try {
const canvas = document.createElement('canvas')
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) {
// Canvas不支持,降级为打开新标签页
openInNewTab(url)
resolve()
return
}
ctx.drawImage(img, 0, 0)
canvas.toBlob((blob) => {
if (!blob) {
openInNewTab(url)
resolve()
return
}
downloadBlob(blob, `${fileName}.png`)
resolve()
}, 'image/png')
} catch (e) {
// 发生错误,降级为打开新标签页
openInNewTab(url)
resolve()
}
}
img.onerror = () => {
openInNewTab(url)
resolve()
}
img.src = url
})
}
const browserSaveVideo = async (url, fileName) => {
try {
// iOS Safari不支持Blob下载视频,直接打开新标签页
if (isIOS()) {
openInNewTab(url)
return
}
const response = await fetch(url)
if (!response.ok) {
throw new Error(`视频加载失败:${response.status}`)
}
const blob = await response.blob()
downloadBlob(blob, `${fileName}.mp4`)
} catch (error) {
// 发生错误,降级为打开新标签页
openInNewTab(url)
}
}
const browserSaveText = (text, fileName) => {
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
downloadBlob(blob, `${fileName}.txt`)
}
// ==================== 通用工具方法 ====================
const downloadBlob = (blob, fileName) => {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
a.style.display = 'none'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
showMsg("保存成功");
}
const openInNewTab = (url) => {
const a = document.createElement('a')
a.href = url
a.target = '_blank'
a.rel = 'noopener noreferrer'
a.style.display = 'none'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
// 导出默认对象,方便使用
export default {
saveFile,
FileType
}