| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- <template>
- <!-- 模板部分保持不变 -->
- <view v-if="custom">
- <up-upload @after-read="afterRead" :deletable="!disabled" @delete="deletePic"
- :max-count="disabled ? fileList.length : maxCount" :accept="accept" multiple @clickPreview="clickPreview">
- <slot />
- </up-upload>
- </view>
- <up-upload v-else :fileList="fileList" @after-read="afterRead" :deletable="!disabled" @delete="deletePic"
- :max-count="disabled ? fileList.length : maxCount" :accept="accept" multiple @clickPreview="clickPreview" />
- </template>
- <script setup>
- import { ref, reactive, defineProps, defineEmits, getCurrentInstance, onUnmounted } from 'vue'
- const emit = defineEmits(['uploadCallback', 'startUploading'])
- const props = defineProps({
- accept: {
- type: String,
- default: "image"
- },
- maxCount: {
- type: [String, Number],
- default: 99
- },
- uploadCallback: {
- type: Function
- },
- startUploading: {
- type: Function
- },
- fileList: {
- type: Array,
- default: reactive([])
- },
- usetype: {
- type: String,
- default: 'default'
- },
- ownertable: {
- type: String,
- default: 'temporary'
- },
- ownerid: {
- type: [String, Number],
- default: 1
- },
- disabled: {
- type: Boolean,
- default: false
- },
- custom: {
- type: Boolean,
- default: false
- },
- // 新增压缩相关配置
- compressConfig: {
- type: Object,
- default: () => ({
- enable: true, // 是否启用压缩
- maxSize: 1024 * 1024, // 1MB,超过此大小才压缩(单位:字节)
- maxWidth: 1920, // 最大宽度
- maxHeight: 1080, // 最大高度
- quality: 0.8, // 图片质量 0-1
- videoBitrate: 1000000, // 视频比特率 1Mbps
- videoMaxWidth: 1280, // 视频最大宽度
- videoMaxHeight: 720 // 视频最大高度
- })
- }
- })
- const { $Http } = getCurrentInstance().proxy;
- const deleteList = reactive([]); // 用于存储待删除的文件列表
- const clickPreview = (e) => {
- uni.previewImage({
- urls: props.fileList.map(v => v.url),
- current: e.url,
- loop: true,
- })
- }
- // 判断是否为图片
- const isImage = (file) => {
- return file.type?.startsWith('image/') ||
- /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name) ||
- /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.url);
- }
- // 判断是否为视频
- const isVideo = (file) => {
- return file.type?.startsWith('video/') ||
- /\.(mp4|mov|avi|wmv|flv|mkv|webm)$/i.test(file.name) ||
- /\.(mp4|mov|avi|wmv|flv|mkv|webm)$/i.test(file.url);
- }
- // H5平台压缩图片
- const compressImageH5 = (file) => {
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.crossOrigin = 'Anonymous';
- img.onload = () => {
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
-
- // 计算压缩后的尺寸
- let width = img.width;
- let height = img.height;
-
- if (width > props.compressConfig.maxWidth || height > props.compressConfig.maxHeight) {
- const ratio = Math.min(
- props.compressConfig.maxWidth / width,
- props.compressConfig.maxHeight / height
- );
- width = Math.floor(width * ratio);
- height = Math.floor(height * ratio);
- }
-
- canvas.width = width;
- canvas.height = height;
-
- // 填充白色背景(对于透明图片)
- ctx.fillStyle = '#FFFFFF';
- ctx.fillRect(0, 0, width, height);
-
- // 绘制图片
- ctx.drawImage(img, 0, 0, width, height);
-
- // 转换为Blob
- canvas.toBlob((blob) => {
- resolve(blob);
- }, file.type || 'image/jpeg', props.compressConfig.quality);
- };
-
- img.onerror = reject;
-
- // 创建Object URL
- if (file.url.startsWith('blob:')) {
- img.src = file.url;
- } else if (file.originFileObj) {
- img.src = URL.createObjectURL(file.originFileObj);
- } else {
- img.src = file.url;
- }
- });
- }
- // H5平台压缩视频(使用MediaRecorder API)
- const compressVideoH5 = (file) => {
- return new Promise((resolve, reject) => {
- const video = document.createElement('video');
- video.preload = 'metadata';
-
- video.onloadedmetadata = () => {
- // 计算压缩后的尺寸
- let width = video.videoWidth;
- let height = video.videoHeight;
-
- if (width > props.compressConfig.videoMaxWidth || height > props.compressConfig.videoMaxHeight) {
- const ratio = Math.min(
- props.compressConfig.videoMaxWidth / width,
- props.compressConfig.videoMaxHeight / height
- );
- width = Math.floor(width * ratio);
- height = Math.floor(height * ratio);
- }
-
- // 创建Canvas来捕获视频帧
- const canvas = document.createElement('canvas');
- canvas.width = width;
- canvas.height = height;
- const ctx = canvas.getContext('2d');
-
- // 设置视频尺寸
- video.width = width;
- video.height = height;
-
- // 开始捕获视频(这里简化处理,实际应用中可能需要使用MediaRecorder)
- // 注意:完整视频压缩需要更复杂的实现,这里只做演示
- video.onseeked = () => {
- ctx.drawImage(video, 0, 0, width, height);
- canvas.toBlob((blob) => {
- // 这里应该压缩整个视频,而不是单帧
- // 实际项目中建议使用第三方库或服务端压缩
- resolve(blob);
- }, 'video/mp4');
- };
-
- video.currentTime = 0;
- };
-
- video.onerror = reject;
-
- if (file.url.startsWith('blob:')) {
- video.src = file.url;
- } else if (file.originFileObj) {
- video.src = URL.createObjectURL(file.originFileObj);
- } else {
- video.src = file.url;
- }
- });
- }
- // 小程序/App平台压缩图片
- const compressImageNative = (file) => {
- return new Promise((resolve, reject) => {
- uni.compressImage({
- src: file.url,
- quality: props.compressConfig.quality * 100, // 转换为百分比
- success: (res) => {
- console.log('图片压缩成功', res);
- resolve(res.tempFilePath);
- },
- fail: (err) => {
- console.error('图片压缩失败', err);
- // 压缩失败时使用原文件
- resolve(file.url);
- }
- });
- });
- }
- // 小程序/App平台压缩视频
- const compressVideoNative = (file) => {
- return new Promise((resolve, reject) => {
- uni.compressVideo({
- src: file.url,
- quality: 'medium', // low, medium, high
- bitrate: props.compressConfig.videoBitrate,
- fps: 30,
- resolution: props.compressConfig.videoMaxHeight,
- success: (res) => {
- console.log('视频压缩成功', res);
- resolve(res.tempFilePath);
- },
- fail: (err) => {
- console.error('视频压缩失败', err);
- // 压缩失败时使用原文件
- resolve(file.url);
- }
- });
- });
- }
- // 压缩文件处理
- const compressFile = async (file) => {
- try {
- // 检查是否启用压缩
- if (!props.compressConfig.enable) {
- return file;
- }
-
- // 检查文件大小,小于阈值不压缩
- let fileSize = file.size;
-
- // 如果在H5环境,获取文件大小
- if (!fileSize && file.originFileObj) {
- fileSize = file.originFileObj.size;
- }
-
- // 如果无法获取大小,默认压缩
- if (!fileSize || fileSize > props.compressConfig.maxSize) {
- let compressedUrl;
-
- // #ifdef H5
- if (isImage(file)) {
- const blob = await compressImageH5(file);
- compressedUrl = URL.createObjectURL(blob);
- file.compressedSize = blob.size;
- } else if (isVideo(file)) {
- // H5视频压缩需要专门的库,这里简化处理
- console.warn('H5视频压缩需要专门的库,暂使用原文件');
- compressedUrl = file.url;
- }
- // #endif
-
- // #ifndef H5
- if (isImage(file)) {
- compressedUrl = await compressImageNative(file);
- } else if (isVideo(file)) {
- compressedUrl = await compressVideoNative(file);
- }
- // #endif
-
- if (compressedUrl && compressedUrl !== file.url) {
- return {
- ...file,
- url: compressedUrl,
- compressed: true
- };
- }
- }
-
- return file;
- } catch (error) {
- console.error('压缩文件失败:', error);
- // 压缩失败返回原文件
- return file;
- }
- }
- // 文件读取后处理(修改后)
- const afterRead = async ({ file }) => {
- emit('startUploading', file);
-
- for (const item of file) {
- try {
- // 压缩处理
- const compressedFile = await compressFile(item);
-
- // #ifdef H5
- const arrayBuffer = await getArrayBuffer(compressedFile);
- arrayBuffer.data.url = compressedFile.url;
- handleUploadFile(requestType(compressedFile), arrayBuffer.data);
-
- // 更新文件列表状态
- props.fileList.push({
- ...compressedFile,
- status: 'uploading',
- message: '上传中',
- });
- // #endif
-
- // #ifndef H5
- uni.getFileSystemManager().readFile({
- filePath: compressedFile.url,
- success: data => {
- data.data.url = compressedFile.url;
- handleUploadFile(requestType(compressedFile), data.data);
-
- // 更新文件列表状态
- props.fileList.push({
- ...compressedFile,
- status: 'uploading',
- message: '上传中',
- });
- },
- fail: console.error
- });
- // #endif
-
- } catch (error) {
- console.error('处理文件失败:', error);
- uni.showToast({
- title: '文件处理失败',
- icon: 'none'
- });
- }
- }
- }
- // 获取文件类型信息
- const requestType = (file) => {
- let ext = ''
- // #ifdef H5
- ext = file.name.substring(file.name.lastIndexOf(".") + 1)
- // #endif
- // #ifndef H5
- ext = file.type?.split("/")[1] ||
- file.url.substring(file.url.lastIndexOf(".") + 1) ||
- file.name.substring(file.name.lastIndexOf(".") + 1)
- // #endif
- return {
- id: '10019701',
- "content": {
- "filename": `${Date.now() + (file.size || 0)}.${ext}`,
- "filetype": ext,
- "parentid": uni.getStorageSync('siteP').appfolderid
- }
- }
- }
- // 获取ArrayBuffer (H5专用) - 修改以支持压缩后的文件
- const getArrayBuffer = (file) => {
- return new Promise((resolve, reject) => {
- // 如果是压缩后的文件且是Blob URL
- if (file.url.startsWith('blob:')) {
- fetch(file.url)
- .then(response => response.blob())
- .then(blob => {
- const reader = new FileReader();
- reader.readAsArrayBuffer(blob);
- reader.onload = () => resolve({
- data: reader.result,
- compressed: file.compressed
- });
- reader.onerror = error => reject(error);
- })
- .catch(reject);
- } else {
- // 原逻辑
- const xhr = new XMLHttpRequest()
- xhr.open('GET', file.url, true)
- xhr.responseType = 'blob'
- xhr.onload = function () {
- if (this.status === 200) {
- const myBlob = this.response
- const files = new File(
- [myBlob],
- file.name,
- { type: file.type },
- )
- const reader = new FileReader()
- reader.readAsArrayBuffer(files)
- reader.onload = () => resolve({
- data: reader.result,
- compressed: file.compressed
- })
- reader.onerror = error => reject(error)
- } else {
- reject(`文件加载失败: ${this.status}`)
- }
- }
- xhr.onerror = () => reject('网络请求失败')
- xhr.send()
- }
- })
- }
- // 处理文件上传
- const handleUploadFile = (file, data) => {
- $Http.basic(file).then(res => {
- console.log("上传文件成功", res)
- if (res.msg == "成功") {
- uploadFile(res.data, data)
- } else {
- uni.showToast({
- title: `${file.content.filename}上传失败`,
- icon: "none"
- })
- }
- })
- }
- // 上传文件到服务器
- const uploadFile = (res, data) => {
- uni.request({
- url: res.uploadurl,
- method: "PUT",
- data,
- header: { 'Content-Type': 'application/octet-stream' },
- success: () => {
- $Http.basic({
- id: 10019901,
- "content": { "serialfilename": res.serialfilename }
- }).then(s => {
- console.log("文件上传反馈", s)
- handleFileLink([{
- attachmentid: s.data.attachmentids[0],
- url: data.url
- }], "temporary", 1, props.usetype)
- }).catch(console.error)
- },
- fail: console.error
- })
- }
- function handleFileLink(list, ownertable = "temporary", ownerid = 1, usetype = 'default', resolve = () => { }) {
- if (list.length == 0) return resolve(true);
- let content = {
- ownertable,
- ownerid,
- usetype,
- attachmentids: list.map(v => v.attachmentid),
- siteid: uni.getStorageSync("userMsg").siteid
- }
- $Http.basic({
- "classname": "system.attachment.Attachment",
- "method": "createFileLink",
- content
- }).then(res => {
- console.log('跟进记录绑定附件', res)
- resolve(res.code == '1')
- if (res.code != '1') return uni.showToast({
- title: res.msg,
- icon: "none"
- })
- list.forEach(v => {
- const file = props.fileList.find(s => v.url === s.url || v.url === s.thumb);
- if (file) {
- delete file.status;
- delete file.message;
- Object.assign(file, res.data.find(s => s.attachmentid === v.attachmentid));
- }
- });
- emit('uploadCallback', { fileList: props.fileList, attachmentids: content.attachmentids })
- })
- }
- // 保存所有的附件绑定到表上,有在上传的文件不能保存
- const isUploading = (showToast = true) => {
- let res = props.fileList.some(file => file.status === 'uploading');
- if (res && showToast) uni.showToast({
- title: '文件正在上传中,请稍后再试',
- icon: 'none'
- });
- return res
- }
- // 保存接口 接受数据调用handleFileLink
- const saveFileLinks = (ownertable, ownerid, usetype = 'default') => {
- // 如果有待删除的文件,先删除
- deleteList.length && deleteFile(deleteList);
- return new Promise((resolve, reject) => {
- const list = props.fileList;
- console.log("list", list)
- if (list.length) {
- return handleFileLink(list, ownertable, ownerid, usetype, resolve);
- } else {
- resolve(true)
- }
- })
- }
- function deletePic({ file, index, name }) {
- uni.showModal({
- cancelText: '取消',
- confirmText: '删除',
- content: '是否确定删除该文件?',
- title: '提示',
- success: ({ confirm }) => {
- if (confirm) {
- console.log("删除文件", file);
- if (file.ownertable == 'temporary') {
- // 临时文件直接删除
- deleteFile([file]).then(res => {
- if (res) {
- props.fileList.splice(index, 1);
- emit('uploadCallback', { fileList: props.fileList });
- }
- })
- } else {
- deleteList.push(file)
- props.fileList.splice(index, 1);
- emit('uploadCallback', { fileList: props.fileList })
- }
- }
- },
- })
- }
- // 直接删除文件
- const deleteFile = (arr) => {
- return new Promise((resolve, reject) => {
- let list = arr.filter(file => file.linksid);
- if (list.length) {
- $Http.basic({
- "classname": "system.attachment.Attachment",
- "method": "deleteFileLink",
- "content": {
- linksids: list.map(v => v.linksid),
- }
- }).then(res => {
- console.log("删除文件", res);
- resolve(res.code == 1)
- if (res.code != 1) uni.showToast({
- title: res.msg,
- icon: "none"
- })
- })
- } else {
- resolve(true)
- }
- })
- }
- // 清空临时文件 ownertable == 'temporary'
- const clearTemporaryFiles = (arr = props.fileList) => {
- let list = arr.filter(file => file.ownertable == 'temporary' && file.linksid);
- if (list.length) $Http.basic({
- "classname": "system.attachment.Attachment",
- "method": "deleteFileLink",
- "content": {
- linksids: list.map(v => v.linksid),
- }
- }).then(res => {
- console.log("清空临时文件", res);
- })
- }
- // 在页面销毁的时候 自动清空所有的临时文件
- onUnmounted(() => {
- // 清理压缩产生的临时URL(H5)
- // #ifdef H5
- props.fileList.forEach(file => {
- if (file.compressed && file.url.startsWith('blob:')) {
- URL.revokeObjectURL(file.url);
- }
- });
- // #endif
-
- clearTemporaryFiles();
- })
- defineExpose({ isUploading, handleFileLink, saveFileLinks, deleteFile })
- </script>
|