ElementUI修改實(shí)現(xiàn)更好用圖片上傳預(yù)覽組件
前言
嗯,,,跟之前封裝“全局 Loading”的出發(fā)點(diǎn)基本一樣,因?yàn)楫a(chǎn)品覺(jué)得 ElementUI 提供的默認(rèn)上傳組件,使用“照片墻”或者“縮略圖”模式都需要去改動(dòng)原本的組件樣式,并且縮略圖尺寸也不能調(diào)整,預(yù)覽模式也會(huì)對(duì)原始圖片進(jìn)行縮放和處理,不符合系統(tǒng)本身的樣式規(guī)范。
最離譜的是,預(yù)覽模式居然有背景色,但是背景色又沒(méi)有填滿整個(gè) model 的背景區(qū)域,,,甚至還出現(xiàn)了滾動(dòng)條?。。?/p>
所以,為了更好的配合產(chǎn)品和UI,特地重新編寫了一個(gè)圖片上傳組件。
1. 功能設(shè)計(jì)
嗯,既然是圖片上傳,那么肯定只支持圖片文件了。因?yàn)槭莾?nèi)部項(xiàng)目,所以也保留了 http 上傳部分,大家可以參照 ElementUI 適當(dāng)修改。
修改后的上傳組件支持以下功能:
- 上傳(基礎(chǔ)中的基礎(chǔ))
- 實(shí)現(xiàn) v-model 語(yǔ)法糖綁定上傳數(shù)據(jù)列表(嗯,,,也很基礎(chǔ))
- 需要支持大圖預(yù)覽
- 不能換行,超出寬度顯示滾動(dòng)條且支持鼠標(biāo)控制(不用 shift 的那種)
功能設(shè)計(jì)完成之后,大致的頁(yè)面樣式就是這樣的:

2. 實(shí)現(xiàn)
為了能夠適應(yīng)更多的場(chǎng)景,我決定把預(yù)覽部分也直接提取出來(lái)。
2.1 圖片預(yù)覽 PicturePreviewer
這里的圖片預(yù)覽是基于 ElDialog 開(kāi)發(fā)的,支持翻頁(yè)、循環(huán)等。
本身不依賴外部的圖片元素,可以和 ElDialog 一樣直接使用 visible 屬性來(lái)控制顯示和隱藏。
<template>
<el-dialog
:title="title"
:visible="visible"
:close-on-click-modal="false"
width="1000px"
destroy-on-close
append-to-body
@close="closeDialog"
>
<div class="q-picture__img-box">
<div
class="q-picture__prev-btn"
@mouseover="leftBtnStatus = true"
@mouseleave="leftBtnStatus = false"
>
<transition name="btn-fade">
<el-button
v-show="leftBtnStatus && isNeeding"
circle
icon="el-icon-arrow-left"
@click="lastImage()"
/>
</transition>
</div>
<img v-show="visible" :src="currentSrc" alt="" v-loading="loading" @load="loading = false" />
<div
class="q-picture__next-btn"
@mouseover="rightBtnStatus = true"
@mouseleave="rightBtnStatus = false"
>
<transition name="btn-fade">
<el-button
v-show="rightBtnStatus && isNeeding"
circle
icon="el-icon-arrow-right"
@click="nextImage()"
/>
</transition>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: "PicturePreviewer",
props: {
visible: { type: Boolean, default: false },
pageable: { type: Boolean, default: true },
recyclable: { type: Boolean, default: true },
src: { type: [String, Array], required: true },
title: { type: String, default: "圖片預(yù)覽" },
current: { type: Number, default: 0 }
},
data() {
return {
currentKey: -1,
leftBtnStatus: false,
rightBtnStatus: false,
loading: false
};
},
computed: {
isNeeding: function () {
return typeof this.src === "object" && this.pageable && this.src && this.src.length > 1;
},
currentSrc: function () {
if (typeof this.src === "string") return this.src;
if (this.src && this.src.length) {
return this.src[this.currentKey] || "";
}
return "";
}
},
methods: {
closeDialog() {
this.$emit("update:visible", false);
},
lastImage() {
if (this.currentKey - 1 === -1) {
if (this.recyclable) this.currentKey = this.src.length - 1;
else this.$message.info("當(dāng)前已經(jīng)是第一張圖片");
} else {
this.currentKey = this.currentKey - 1;
}
},
nextImage() {
if (this.currentKey + 1 === this.src.length) {
if (this.recyclable) this.currentKey = 0;
else this.$message.info("當(dāng)前已經(jīng)是最后一張圖片");
} else {
this.currentKey = this.currentKey + 1;
}
}
},
watch: {
current: {
handler: function (val) {
if (val) this.currentKey = val;
else this.currentKey = 0;
},
immediate: true
}
}
};
</script>
2.2 圖片上傳 ImageUpload
圖片預(yù)覽處理完成夠,就可以處理圖片上傳了。
<template>
<div
class="q-upload__preview"
ref="pictures"
:title="messageInfo"
@mouseenter="horizontalRolling"
>
<slot name="preSlot"></slot>
<input
class="q-upload__file-input"
type="file"
ref="fileInput"
name="fileInput"
@change="fileChange"
:accept="accept"
/>
<div
class="q-upload__file-label"
v-loading="fileLoading"
@click="selectFile"
v-show="fileLists.length < limitNum && !disabled"
>
<i class="el-icon-plus"></i>
</div>
<slot name="middle"></slot>
<div class="q-upload__pre-img" v-for="(i, k) in fileLists" :key="i.smallUrl">
<img class="q-upload__img" :src="i.smallUrl ? i.smallUrl : i.url" />
<div class="q-upload__mask">
<i v-if="prev" class="el-icon-zoom-in" @click="imgPreview(i, k)"></i>
<i class="el-icon-delete" v-if="!disabled" @click="imgRemove(k)"></i>
</div>
</div>
<slot name="endSlot"></slot>
<picture-previewer
:visible.sync="dialogImageVisible"
:src="imageUrls"
:current="currentImage"
/>
</div>
</template>
<script>
import Utils from "../../src/utils/commonUtils";
export default {
name: "ImageUpload",
props: {
active: { type: String, default: "/api/file/upload" },
accept: { type: String, default: "" },
limitNum: { type: Number, default: 9 },
size: { type: Number, default: 10 },
prev: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
value: { type: Array, default: () => [] }
},
data() {
return {
fileLoading: false,
dialogImageVisible: false,
dialogImageUrl: "",
currentImage: 0,
fileLists: [],
messageInfo: ""
};
},
computed: {
imageUrls: function () {
return this.fileLists.map(o => o.url);
}
},
methods: {
async validateImage(file) {
const isJPEG = file.type === "image/jpeg";
const isJPG = file.type === "image/jpg";
const isPNG = file.type === "image/png";
const isBMP = file.type === "image/bmp";
const isLtSize = file.size / 1024 / 1024 < this.size;
if (!(isJPEG || isJPG || isPNG || isBMP)) {
return {
status: false,
message: `上傳圖片必須是${this.accept}格式!`
};
}
if (!isLtSize) {
return {
status: false,
message: "上傳圖片大小不能超過(guò) " + this.size + " MB!"
};
}
return { status: true, message: "" };
},
// 選擇文件
selectFile() {
this.$refs.fileInput.value = null; // 置空,防止刪除后無(wú)法再次選擇
this.$refs.fileInput.click();
return true;
},
// 文件選取之后·
async fileChange(el) {
const file = [...el.target.files][0];
let { status, message } = await this.validateImage(file);
if (status) {
this.fileLoading = true;
await this.customHttpRequest(file);
} else {
this.$message.error(message);
return false;
}
},
// 上傳
async customHttpRequest(file) {
try {
let fData = Utils.createUploadForm(file);
let {
data: { data, status, message }
} = await this.$http.post(this.active, fData.formData, fData.config);
if (status) {
this.fileLists.unshift(data);
this.$emit("success", data);
} else {
this.$message.error(message);
this.$emit("error");
}
} finally {
this.fileLoading = false;
}
},
imgPreview(file, k) {
this.dialogImageUrl = file.url;
this.dialogImageVisible = true;
this.currentImage = k;
},
imgRemove(index) {
this.fileLists.splice(index, 1);
this.$emit("input", this.fileLists);
this.$emit("change", this.fileLists);
this.$emit("blur", this.fileLists);
},
horizontalRolling() {
if (this.$refs["pictures"].clientWidth < this.$refs["pictures"].scrollWidth) {
this.messageInfo = "滾動(dòng)滾輪查看所有信息";
} else {
this.messageInfo = "";
}
this.$refs["pictures"].addEventListener("mousewheel", this.$_scrollEvent);
this.$once("hook:beforeDestroy", () => {
this.$refs["pictures"].removeEventListener("mousewheel", this.$_scrollEvent);
});
},
$_scrollEvent(e) {
let left = this.$refs["pictures"].scrollLeft;
this.$refs["pictures"].scrollLeft = e.deltaY > 0 ? left + 40 : left - 40;
}
},
watch: {
value: {
deep: true,
immediate: true,
handler: function () {
this.fileLists = typeof this.value === "string" ? JSON.parse(this.value) : this.value;
if (!this.fileLists) this.fileLists = [];
}
},
fileLists: {
deep: true,
immediate: false,
handler: function () {
if (this.value && this.value.length > this.limitNum) {
this.$message.warning(`最多可以上傳【 ${this.limitNum} 】張圖片??!`);
}
this.$emit("input", this.fileLists);
this.$emit("change", this.fileLists);
}
}
}
};
</script>
因?yàn)槭莾?nèi)部項(xiàng)目,所以上傳方法還是使用的實(shí)例上的 axios 方法來(lái)發(fā)送上傳請(qǐng)求的;在獨(dú)立組件庫(kù)中依然應(yīng)該通過(guò) props 的方式傳遞項(xiàng)目中定義的 http 請(qǐng)求方法。
組件接收一個(gè)最大張數(shù)限制 limitNum 和文件大小限制 size,以及預(yù)覽控制 prev 和禁用狀態(tài) disabled。
在選擇文件之后會(huì)立即上傳、點(diǎn)擊已上傳文件則是預(yù)覽當(dāng)前文件;當(dāng)前內(nèi)部也依賴了 ElementUI 的 Message 組件,用來(lái)顯示提示信息。
在預(yù)覽區(qū)域前后也增加了一個(gè)插槽,用來(lái)插入開(kāi)發(fā)者需要的其他信息。
在整個(gè)組件的 Dom 節(jié)點(diǎn)上,會(huì)添加一個(gè)鼠標(biāo)的 mouseenter 事件,當(dāng)鼠標(biāo)在組件內(nèi)部的時(shí)候,則計(jì)算內(nèi)部的縮略圖區(qū)域與外層節(jié)點(diǎn)的大小進(jìn)行比較,如果大于外層父節(jié)點(diǎn)的寬度的話,則提示用戶通過(guò)鼠標(biāo)滾輪來(lái)控制縮略圖區(qū)域的滾動(dòng)。
3. 后記
整個(gè)組件雖然可以滿足當(dāng)時(shí)的系統(tǒng)的一個(gè)需求,但是仔細(xì)研究代碼的話會(huì)發(fā)現(xiàn)依然有很多細(xì)節(jié)的地方需要修復(fù)。例如:
- 組件的 mouseenter 事件,每次被觸發(fā)時(shí)都會(huì)給 dom 添加一個(gè)鼠標(biāo)監(jiān)聽(tīng)事件,而沒(méi)有在鼠標(biāo)移出時(shí)及時(shí)銷毀監(jiān)聽(tīng)
- 沒(méi)有增加自定義 http 配置
- 沒(méi)有控制預(yù)覽組件的配置項(xiàng)
- 縮略圖區(qū)域沒(méi)有尺寸控制
等等一系列的問(wèn)題,所以我們?cè)诔殡x組件、公共邏輯的時(shí)候,還是需要盡可能的保留以后擴(kuò)展的可能性,減少與外界邏輯或者業(yè)務(wù)的關(guān)聯(lián)。
以上就是ElementUI修改實(shí)現(xiàn)更好用圖片上傳預(yù)覽組件的詳細(xì)內(nèi)容,更多關(guān)于ElementUI圖片上傳預(yù)覽的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在Vue組件化中利用axios處理ajax請(qǐng)求的使用方法
這篇文章主要給大家介紹了在Vue組件化中利用axios處理ajax請(qǐng)求的使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面跟著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08
vue中數(shù)據(jù)請(qǐng)求axios的封裝和使用
這篇文章主要介紹了vue中數(shù)據(jù)請(qǐng)求axios的封裝和使用,Axios?是一個(gè)基于?promise?的?HTTP?庫(kù),下面文章圍繞主題的相關(guān)資料展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-04-04
VUE 直接通過(guò)JS 修改html對(duì)象的值導(dǎo)致沒(méi)有更新到數(shù)據(jù)中解決方法分析
這篇文章主要介紹了VUE 直接通過(guò)JS 修改html對(duì)象的值導(dǎo)致沒(méi)有更新到數(shù)據(jù)中解決方法,結(jié)合實(shí)例形式詳細(xì)分析了VUE使用JS修改html對(duì)象的值導(dǎo)致沒(méi)有更新到數(shù)據(jù)的原因與解決方法,需要的朋友可以參考下2019-12-12
vue3+echarts+折線投影(陰影)效果的實(shí)現(xiàn)
這篇文章主要介紹了vue3+echarts+折線投影(陰影)效果的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
vue項(xiàng)目webpack中Npm傳遞參數(shù)配置不同域名接口
這篇文章主要介紹了vue項(xiàng)目webpack中Npm傳遞參數(shù)配置不同域名接口,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06
Vue使用echarts散點(diǎn)圖在區(qū)域內(nèi)標(biāo)點(diǎn)
這篇文章主要為大家詳細(xì)介紹了Vue使用echarts散點(diǎn)圖在區(qū)域內(nèi)標(biāo)點(diǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
vue中defineProperty和Proxy的區(qū)別詳解
這篇文章主要介紹了vue中defineProperty和Proxy的區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11

