功能背景:項目部署在外網,然后其中有一個功能需要上傳下載附件,附件是上傳到華為云對象存儲服務OBS中(私有云),所以采用iframe嵌套頁面的方式解決跨域問題。
實現思路:
1、父窗口封裝一個組件專門用于處理iframe相關功能
2、子窗口是另外一個項目,封裝了附件上傳、下載、刪除等功能
3、父窗口通過監聽組件綁定的value值,不斷輪詢去問子窗口是否已經掛載完畢了,如果已經掛載了,就將接口返回的附件列表發送給子窗口進行展示
4、子窗口掛載完畢后主動告知父窗口已經掛載好了,可以接收消息了。子窗口接收到了附件列表后進行展示,上傳成功、刪除成功后子窗口都需要發送消息給父窗口,告知附件列表發生了改變
5、父窗口是vue2語法寫的,子窗口是vue3寫的,可自行更改。
父窗口(vue2語法)
<template><div><iframe id="modle_iframe" ref="modle_iframe" frameborder="no" border="0"style="width:100%;height:calc(100vh - 450px);" :src="`https://xxxxx:8443/rcxjsp`"></iframe></div>
</template><script>
/*** @Date 2025-05-14* @Description 視頻上傳組件*/
export default {props: {value: { //附件列表type: Array,default: () => { return [] }},disabled: { //是否只讀type: Boolean,default: false}},data() {return {fileList: [],isIframeLoaded: false, //標記iframe是否加載完成timer: null}},watch: {value: {immediate: true,handler(newValue, oldValue) {if (newValue && newValue.length > 0) {this.fileList = newValue;} else {this.fileList = [];}this.loop();}},},mounted() {window.addEventListener('message', this.getMessage);},beforeDestroy() {window.removeEventListener('message', this.getMessage);if (this.timer) {clearInterval(this.timer);}},methods: {//不斷詢問子窗口是否已經掛載完畢,只有掛載完畢后發送消息,子窗口才能接收到loop() {if (this.timer) {clearInterval(this.timer);}this.timer = setInterval(() => {if (this.isIframeLoaded) {this.sendMessage();}}, 100)},sendMessage() {if (this.timer) {clearInterval(this.timer);}const iframe = document.getElementById('modle_iframe');const data = {disabled: this.disabled,fileList: this.fileList,}const msg = JSON.stringify(data);//父窗口發送列表和是否只讀給子窗口iframe.contentWindow.postMessage(msg, '*');},getMessage(event) { //父窗口接收來自子窗口的消息const data = JSON.parse(event.data);if (data.type === 'onMounted') {this.isIframeLoaded = true;} else if (data.type == 'updateFileList') {this.fileList = data.fileList;this.$emit('input', this.fileList);}},}
}
</script><style lang="scss" scoped></style>
子窗口(vue3+element-plus)
這里的上傳和下載都是采用的分片上傳和分片下載實現的。這里只提供核心代碼,有用得上的自行修改。
<template><div style="overflow-y: auto;"><el-upload v-if="!disabled" action="#" :file-list="fileList" :data="{ path: 'rcxjsp' }" :show-file-list="false":http-request="uploadRequest"><el-button type="primary" :loading="loading" style="margin-bottom: 10px;">點擊上傳</el-button></el-upload><!-- 文件列表 --><div v-for="(item, index) in fileList" :key="index" class="fileList" style="display: flex;"><img src="./images/pdf.png" mode="" v-if="fileHz(item) == 'pdf'"> </img><img src="./images/zip.png" mode="" v-else-if="fileHz(item) == 'zip' || fileHz(item) == 'rar'"></img><img src="./images/txt.png" mode="" v-else-if="fileHz(item) == 'txt'"></img><img src="./images/ppt.png" mode="" v-else-if="fileHz(item) == 'ppt'"></img><img src="./images/xls.png" mode="" v-else-if="fileHz(item) == 'xls' || fileHz(item) == 'xlsx'"></img><img src="./images/doc.png" mode="" v-else-if="fileHz(item) == 'doc' || fileHz(item) == 'docx'"></img><img src="./images/mp3.png" mode=""v-else-if="fileHz(item) == 'mp3' || fileHz(item) == 'wav' || fileHz(item) == 'wma'"></img><img src="./images/mp4.png" mode=""v-else-if="fileHz(item) == 'mp4' || fileHz(item) == 'avi' || fileHz(item) == 'mov'"></img><img src="./images/fj.png" mode="" v-else></img><div class="filename">{{ item.fileName || item.name }}</div><el-button type="primary" @click="fpDown(item, index)" class="yulan":loading="downloading[index].loading">{{ downloading[index].loading ?`正在下載(${downloading[index].percent}%)` : '下載' }}</el-button><el-button type="danger" @click.stop="handleDelete(index)" v-show="!disabled">刪除</el-button></div><el-progress v-show="showProgress" :percentage="progressPercent"></el-progress></div>
</template><script setup>
/*** @Date 2025-05-13* @Description 視頻上傳下載*/
import { initializeUploadId, uploadChunk, completeUpload, rangeDownload } from '@/api/rcxjsp';//父組件發送過來的兩個參數
const fileList = ref([]);
const disabled = ref(false);
//本組件需要使用到的變量
let loading = ref(false);
let downloading = ref([]); // 下載進度
let currentFile = ref({}); // 當前文件
let showProgress = ref(false); // 進度條
let progressPercent = ref(0); // 進度百分比
const path = 'rcxjsp'; // 上傳路徑onMounted(() => {//頁面掛載后主動通知父窗口已經準備好了window.parent.postMessage(JSON.stringify({ type: 'onMounted' }), '*');window.addEventListener('message', getMessage);
});
onUnmounted(() => {window.removeEventListener('message', getMessage);
});
const getMessage = (event) => {if (event.source === window.parent) {const data = JSON.parse(event.data);disabled.value = data.disabled;fileList.value = data.fileList;downloading.value = new Array(fileList.value.length).fill({ loading: false, percent: 0 });}
}
//獲取文件后綴名
const fileHz = (file) => {const name = file.fileName || file.name;if (name) {var index = name.lastIndexOf(".");var ext = name.substr(index + 1);return ext}
}
//上傳附件
const uploadRequest = (params) => {currentFile.value.fileName = params.file.name;showProgress.value = true;loading.value = true;let formdata = new FormData();formdata.append('file', params.file);formdata.append('path', path);formdata.append('fileName', params.file.name);initializeUploadId({ fileName: params.file.name, path: path }).then(res => {currentFile.value = res.data;const chunkSize = 1024 * 1024 * 25; // 每個切片的大小(這里設置為25MB)const totalChunks = Math.ceil(params.file.size / chunkSize);let currentChunk = 0;const uploadNextChunk = () => {const start = currentChunk * chunkSize;const end = Math.min((currentChunk + 1) * chunkSize, params.file.size);let chunkData = params.file.slice(start, end);let formdata = new FormData();formdata.append('file', chunkData);formdata.append('uploadId', currentFile.value.uploadId);formdata.append('objectKey', currentFile.value.objectKey);formdata.append('chunkIndex', currentChunk + 1);uploadChunk(formdata).then(res => {currentChunk++;progressPercent.value = +Math.floor((currentChunk / totalChunks) * 100).toFixed(0);if (currentChunk < totalChunks) {uploadNextChunk();} else {// 所有切片上傳完成,將切片合并progressPercent.value = 100;complete();}}).catch((error) => {ElMessage({ type: 'error', message: '切片上傳失敗!' });loading.value = false;showProgress.value = false;progressPercent.value = 0;});};uploadNextChunk();})
}
//合并切片
const complete = () => {completeUpload({ ...currentFile.value }).then(res => {//定義需要傳輸給父窗口的附件列表數據結構let fileitem = {name: res.data.fileName,fileName: res.data.fileName,url: res.data.fileUrl,fileUrl: res.data.fileUrl,fileLength: res.data.fileLength}fileList.value.push(fileitem)downloading.value.push({ loading: false, percent: 0 });//通知父窗口附件列表變化了sendMsgToParent();}).catch(() => {ElMessage({ type: 'error', message: '切片合并失敗!' });}).finally(() => {loading.value = false;showProgress.value = false;progressPercent.value = 0;})
}
//刪除文件
const handleDelete = (index) => {fileList.value.splice(index, 1);//通知父窗口附件列表變化了sendMsgToParent();
}
//分片下載
const fpDown = (e, index) => { //分片下載同步下載(按順序一個一個下載)downloading.value.splice(index, 1, { loading: true, percent: 0 });const chunkSize = 1024 * 1024 * 25; // 每個切片的大小(這里設置為25MB)const totalChunks = Math.ceil(e.fileLength / chunkSize);let currentChunk = 0;const chunks = [];const download = () => {const start = currentChunk * chunkSize;const end = Math.min((currentChunk + 1) * chunkSize, e.fileLength);rangeDownload({ fileName: e.fileName, start: start, end: end, fileLength: e.fileLength }).then(bolb => {currentChunk++;let percent = +Math.floor((currentChunk / totalChunks) * 100).toFixed(0);downloading.value.splice(index, 1, { loading: true, percent: percent });chunks.push(bolb);if (currentChunk < totalChunks) {download();} else {downloading.value.splice(index, 1, { loading: false, percent: 100 });const mergedBlob = new Blob(chunks);const downloadUrl = window.URL.createObjectURL(mergedBlob);const link = document.createElement('a');link.href = downloadUrl;link.setAttribute('download', e.fileName);link.click();window.URL.revokeObjectURL(downloadUrl);}}).catch((error) => {ElMessage({ type: 'error', message: '切片下載失敗!' });downloading.value.splice(index, 1, { loading: false, percent: 0 });});}download();
}//通知父窗口文件列表改變
const sendMsgToParent = () => {const msg = {type: 'updateFileList',fileList: fileList.value}window.parent.postMessage(JSON.stringify(msg), '*');
}</script><style lang="scss" scoped>
.fileList {display: flex;align-items: center;margin-bottom: 10px;img,.el-image {width: 30px;height: 30px;margin-right: 10px;}div.filename {flex: 1;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;}el-button.yulan {margin-left: 10px;}el-button.delete {margin-left: 10px;}
}
</style>
接口rcxjsp.js大致如下:
import request from '@/utils/request'//獲取uploadid
export function initializeUploadId(params) {return request({url: '/xxxxx',method: 'get',params,})
}//上傳分片
export function uploadChunk(data) {return request({headers: {'Content-Type': 'multipart/form-data',},url: '/gtfrgftft',method: 'post',data,})
}//合并分片
export function completeUpload(data) {return request({url: '/thgthtgh',method: 'post',data,})
}//下載附件
export function download(params) {return request({url: '/wrderfefre',method: 'get',params,responseType: 'blob',})
}//分片下載
export function rangeDownload(params) {return request({url: 'kuju',method: 'get',params,responseType: 'blob',})
}