因爲項目是vue+vant,這裏圖片預覽直接使用vant的組件,不做多餘的封裝
html 結構
主要選擇圖片按鈕,圖片預覽區, 選擇圖片的input
<div class="img-upload">
<ul class="preview-list" >
<li
class="img-item"
v-for="(img, key) in fileList"
:key="key"
@click="() => imagePreviewShow = true"
>
<img class="preview-img" :src="img.base64" alt="img">
<img
:src="defImg"
class="del-img"
@click="delImg(img)"
/>
</li>
<li class="img-item" v-show="imgTotal < defCon.total">
<label for="upload">
<div slot="button">
<div class="import-img-wrap">
<span class="icon-add"><van-icon name="plus" /></span>
<p class="text">添加圖片</p>
</div>
</div>
<input
ref="file"
type="file"
class="input-upload"
id="upload"
accept="image/*"
@change="upload"
:multiple="defCon.multiple"
>
</label>
</li>
</ul>
<van-image-preview
v-model="imagePreviewShow"
:images="imgList">
</van-image-preview>
</div>
css 樣式
樣式文件有些冗餘,並沒有做優化,需要的話自己改改
<style lang="less">
.img-upload{
&{
position: relative;
overflow: hidden;
}
.input-upload{
position: absolute;
left: -20rem /* 2000/100 */;
}
.preview-list{
overflow: hidden;
padding: .15rem /* 15/100 */ 0;
.img-item{
float: left;
width: .8rem /* 80/100 */;
height: .8rem /* 80/100 */;
border-radius: .06rem /* 6/100 */;
margin: 0 .15rem /* 10/100 */ .1rem /* 10/100 */ 0;
}
.preview-img{
width: .8rem /* 80/100 */;
height: .8rem /* 80/100 */;
}
.del-img{
position: absolute;
width: .24rem /* 24/100 */;
height: .24rem /* 24/100 */;
transform: translate(-.1rem, -.1rem);
}
}
.import-img-wrap{
margin: .32rem /* 32/100 */ 0;
width: .8rem /* 80/100 */;
height: .8rem /* 80/100 */;
text-align: center;
font-size:12px;
overflow: hidden;
font-weight:400;
color:rgba(23,7,7,0.45);
.import-img-input{
transform: translateX(20000px);
}
.icon-add{
display: inline-block;
width: .26rem /* 26/100 */;
height: .26rem /* 26/100 */;
background: #999;
font-size: .22rem /* 24/100 */;
font-weight: 700;
color: #fff;
border-radius: .13rem /* 13/100 */;
}
}
}
</style>
js部分
整個js部分大概如下
- 常用配置, config
- data,常用配置信息,以及依賴屬性
- 函數調用
a) uoload, 選擇文件後觸發,
b) 然後裏邊對文件進行處理:壓縮, this.compressData;
c) 轉換爲formdata, this.processFormData => 上傳圖片 this.uploadImg
d) 將文件添加到fileList
e) 上傳圖片 this.uploadImg: 圖片上傳後出發回調 =>this.handleUploadEnd
f) handleUploadEnd> 根據狀態進行處理,上傳等待上傳的文件
以上是代碼的大概執行過程和對應的函數
import defImg from '@/assets/images/del.png' // 刪除圖片,可以換成iconfont
import { getOssSign } from '../pages/api' // 後臺獲取阿里雲配置
import EXIF from 'exif-js' // 針對移動端圖片做處理, 橫豎的問題
import OSS from 'ali-oss' // ali-oss 插件,上傳使用
import md5 from '../utils/md5' // 文件命名
export default {
name: 'img-upload',
props: {
config: {
// 最大上傳限制
maxSize: {
type: Number,
default: 10
},
// 壓縮比例
compressionRatio: {
type: Number,
default: 50
},
// 是否壓縮
compress:{
type: Boolean,
default: true
},
// 是否壓縮
total:{
type: Number,
default: 5
},
// 是否立即上傳
isUpdated:{
type: Boolean,
default: true
},
name: { // 上傳需要的
type: String,
default: 'file'
},
// 最大同時上傳數量
maxUploadSize: {
type: Number,
default: 5
}
}
},
data() {
return {
fileList: [], // 保存文件列表,雙向綁定的時候會用到
imgTotal: 0, // 當前文件最大數量
defCon: {
maxSize: 10, // more大小 m
compressionRatio: 71, // 默認壓縮比
compress: true, // 是否需要壓縮
multiple: false, // 是否多選
total: 5, // 最大數量
orientation: false, // 是否有橫豎過程
drawHeight: 80, // 如果需要限制文件寬高,默認高度
drawWidth: 80,
isUpdated: true, // 是否立即上傳
name: 'file', //
maxUploadSize: 5 // 最大同時上傳
},
defImg,
uploadQueue: [], // 上傳列表
uploadWaiting: [], // 等待上傳列表
uploadingQueue: new Set(), // 正在上傳的隊列
upLoadingError: new Set(), // 上傳失敗列表
timmer: 0,
errorTip: false,
timer: 0,
headers: {},
sigin: null, // 雲存儲簽名
client : null, // 雲存儲實例
imagePreviewShow: false // 控制圖片預覽艦
}
},
mounted() {
this.getSign()
},
methods: {
async upload(e){
let list = e.target.files || e.dataTransfer.files
if (!list.length) return
list = Array.from(list)
if (this.imgTotal > this.defCon.total) {
return this.$toast(`最多支持上傳${this.defCon.total}張照片`)
}
// 如果用戶選擇的文件數大於最大數量 截取
if (list.length + this.imgTotal > this.defCon.total) {
list = list.slice(0, this.defCon.total - this.imgTotal)
}
// 處理回調
let proList = await list.map(file => this.transformFileToBase64(file))
// 處理結果
Promise.all(proList)
.then(arr => {
return arr.map((item, key) => {
let { base64, orientation } = item
let file = list[key]
return this.initItem(file, base64, orientation)
})
})
.then(fileList => { // 壓縮圖片
// 如果不需要壓縮,直接返回
if (!this.defCon.compress) {
return fileList
}
// 壓縮圖片
let list = fileList.map(file => this.compressData(file))
return Promise.all(list)
// console.log('fileList', list)
}).then(fileList => {
if (this.defCon.isUpdated) {
fileList.map(file => this.processFormData(file))
}
return fileList
}).then(fileList => {
this.fileList = this.fileList.concat(fileList)
this.imgTotal = this.fileList.length
})
},
/**
* 處理文件流
* @param file
* @returns {Promise<any>}
*/
transformFileToBase64(file) {
/**
* 圖片上傳流程的第一步
* @param data file文件
*/
return new Promise((resolve) => {
let orientation = null
const reader = new FileReader()
reader.onload = (e) => {
const base64 = e.target.result
if (file.type.indexOf('image') > -1) {
EXIF.getData(file, function () {
EXIF.getAllTags(this)
orientation = EXIF.getTag(this, 'Orientation')
resolve({ base64, orientation })
})
} else {
resolve({ base64, orientation })
}
}
reader.readAsDataURL(file)
})
},
/**
* @desc 初始化
*/
initItem(file, base64, orientation){
const item = {
uid: this.getUid(),
status: this.upload ? 1 : 0, // 0 未開始,1 準備上傳,2 上傳成功,3 上傳失敗, -1 正在上傳
imgKey: '', // 服務器返回的唯一key
name: file.name, // 文件名稱
size: file.size, // 文件大小
type: file.type, // 文件類型
file, // 文件流
base64, // 沒有壓縮前的base64
progress: 0, // 當前文件上傳進度
compressBase64: null, // 轉換之後的base64
response: {},
orientation // 圖片的方向
}
if (item.size > this.defCon.maxSize * 1024 * 1024 ) {
this.$emit('uploadError', 'overSize')
if (this.showErrorToast) {
this.showToast(`文件最大可上傳${this.defCon.maxSize}MB`)
}
return
}
return item
},
getUid() {
this.imgTotal = this.imgTotal + 1
return `upload-${new Date().getTime()}-${this.imgTotal}`
},
/**
* 處理圖片
* @param data
* @param callback
*/
compressData(data) {
/**
* 壓縮圖片
* @param data file文件 數據會一直向下傳遞
* @param callback 下一步回調
*/
const self = this
const orientation = data.orientation
return new Promise((resolve) => {
const img = new Image()
img.src = data.base64
img.onload = function() {
let drawWidth = 0
let drawHeight = 0
drawWidth = this.naturalWidth
drawHeight = this.naturalHeight
// 改變一下圖片大小
let maxSide = Math.max(drawWidth, drawHeight)
if (maxSide > self.maxImageSize) {
let minSide = Math.min(drawWidth, drawHeight)
minSide = parseInt((minSide / maxSide * self.maxImageSize), 10)
maxSide = self.maxImageSize
if (drawWidth > drawHeight) {
drawWidth = maxSide
drawHeight = minSide
} else {
drawWidth = minSide
drawHeight = maxSide
}
}
resolve({
img: this,
drawWidth,
drawHeight,
orientation,
data
})
}
}).then(this.compressCanvas)
},
/**
* @desc 壓縮圖片
* @param img 加載完成後的圖片
* @param info 寬高, 是否需要處理旋轉
*/
compressCanvas(info) {
let { img, data } = info
const compressionRatio = this.defCon.compressionRatio
const compress = this.defCon.compress
return new Promise((resolve) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const width = info.drawWidth
const height = info.drawHeight
canvas.width = info.drawWidth
canvas.height = info.drawHeight
// 判斷圖片方向,重置 canvas 大小,確定旋轉角度,iphone 默認的是 home 鍵在右方的橫屏拍攝方式
switch (info.orientation) {
// 1 不需要旋轉
case 1: {
ctx.drawImage(img, 0, 0, info.drawWidth, info.drawHeight)
ctx.clearRect(0, 0, width, height)
ctx.drawImage(img, 0, 0, width, height)
break
}
// iphone 橫屏拍攝,此時 home 鍵在左側 旋轉180度
case 3: {
ctx.clearRect(0, 0, width, height)
ctx.translate(0, 0)
ctx.rotate(Math.PI)
ctx.drawImage(img, -width, -height, width, height)
break
}
// iphone 豎屏拍攝,此時 home 鍵在下方(正常拿手機的方向) 旋轉90度
case 6: {
canvas.width = height
canvas.height = width
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.translate(0, 0)
ctx.rotate(90 * Math.PI / 180)
ctx.drawImage(img, 0, -height, width, height)
break
}
// iphone 豎屏拍攝,此時 home 鍵在上方 旋轉270度
case 8: {
canvas.width = height
canvas.height = width
ctx.clearRect(0, 0, width, height)
ctx.translate(0, 0)
ctx.rotate(-90 * Math.PI / 180)
ctx.drawImage(img, -width, 0, width, height)
break
}
default: {
ctx.clearRect(0, 0, width, height)
ctx.drawImage(img, 0, 0, width, height)
break
}
}
let base64Url = ''
if (compress) {
base64Url = canvas.toDataURL(data.type, (compressionRatio / 100))
} else {
base64Url = canvas.toDataURL(data.type, 1)
}
data.compressBase64 = base64Url
data.compressWidth = width
data.compressHeight = height
resolve(data)
})
},
/**
* 處理上傳數據
* @param data
*/
processFormData (data) {
// 準備上傳數據
let arr = data.compressBase64.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
data.blob = new Blob([u8arr], { type: mime })
// data.status 1 等待上傳 2 上傳成功 3上傳失敗
// 添加到 正在上傳列表 使
this.uploadingQueue.add(data)
this.uploadQueue.push(data)
this.$emit('onChange', data, this.uploadQueue)
// 當前上傳數量達到最大時,不在放入隊 等待上傳
if (this.uploadingQueue.size < this.defCon.maxUploadSize) {
this.uploadImg(data)
} else {
// 加入等待上傳
this.uploadWaiting.push(data)
}
},
/**
* 上傳圖片
* @param data
*/
async uploadImg(data) {
this.$emit('update:isUpdated', true)
// 處理文件名
let imgName = md5(`${data.uid}-${data.name}`)
let imgKey = `ipxmall/${imgName}`
this.client.put(imgKey, data.blob)
.then(response => {
// 上傳完畢回調
let res = response.res
if(res.status === 200) {
data.imgKey = response.url
}
// 圖片前綴
// console.log('response', response)
this.handleUploadEnd(data, res, res.status)
})
},
// 上傳進度條
handleProgress(data, e) {
data.progress = parseInt((e.loaded / e.total) * 100, 10)
},
/**
* @desc 提示處理上傳失敗的圖片
*/
handleErrorUploadList() {
let uploadErrs = this.uploadQueue.filter(item => item.status === 3)
let len = uploadErrs.length
if (len === 0 && this.uploadingQueue.size === 0) {
this.$emit('onUploadEnd', this.uploadQueue)
return false
}
this.$modal.confirm(`有${len}張圖片上傳失敗,是否重新上傳!`, '上傳失敗提示')
.then(() => {
uploadErrs.map((item, idx) => {
item.status = 1
if (idx < this.defCon.maxUploadSize) {
this.uploadImg(item)
}
})
}, () => {
// 如果用戶不選擇繼續上傳,則從列表刪除該文件
let uids = uploadErrs.map(item => item.uid)
this.uploadQueue = this.fileList = this.fileList.filter(item => !uids.includes(item.uid))
this.$emit('onUploadEnd', this.uploadQueue)
})
},
/**
* 處理成功結果
* @param data
* @param response
*/
handleUploadEnd(data, response, status) {
data.response = response
data.status = status === 200 ? 2 : 3
// eslint-disable-next-line no-debugger
// debugger
this.$emit('onChange', data, this.uploadQueue)
// 上傳成功後從隊列中刪除
if (status === 200) {
this.uploadingQueue.delete(data)
}
// 處理等待上傳的
if (this.uploadWaiting.length) {
let uploadItem = this.uploadItem.shift()
this.uploadingQueue.add(uploadItem)
this.uploadImg(uploadItem)
}
if (this.uploadingQueue.size === 0) {
// 處理未上傳成功的
this.handleErrorUploadList()
}
},
/**
* @desc 上傳圖片
*/
delImg(delImg) {
this.fileList = this.fileList.filter(item => delImg.uid !== item.uid)
this.imgTotal -= 1
},
/**
* @desc 獲取簽名
*/
async getSign() {
let { errorCode, data } = await getOssSign()
if (errorCode === 0) {
this.sign = data
let {
securityToken,
accessKeyId,
accessKeySecret,
bucket,
endpoint
} = data
// 創建實例
let client = new OSS({
endpoint,
accessKeyId,
accessKeySecret,
bucket,
stsToken: securityToken
})
this.client = client
}
}
},
computed: {
imgList() {
return this.fileList.map(item => item.base64)
}
}
}
md5.js 內容
/* eslint-disable */
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF),
msw = (x >> 16) + (y >> 16) + (lsw >> 16)
return (msw << 16) | (lsw & 0xFFFF)
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt))
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t) {
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b)
}
function md5_ff(a, b, c, d, x, s, t) {
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t)
}
function md5_gg(a, b, c, d, x, s, t) {
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t)
}
function md5_hh(a, b, c, d, x, s, t) {
return md5_cmn(b ^ c ^ d, a, b, x, s, t)
}
function md5_ii(a, b, c, d, x, s, t) {
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t)
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length.
*/
function binl_md5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << (len % 32)
x[(((len + 64) >>> 9) << 4) + 14] = len
var i, olda, oldb, oldc, oldd,
a = 1732584193,
b = -271733879,
c = -1732584194,
d = 271733878
for (i = 0; i < x.length; i += 16) {
olda = a
oldb = b
oldc = c
oldd = d
a = md5_ff(a, b, c, d, x[i], 7, -680876936)
d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586)
c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819)
b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330)
a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897)
d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426)
c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341)
b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983)
a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416)
d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417)
c = md5_ff(c, d, a, b, x[i + 10], 17, -42063)
b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162)
a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682)
d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101)
c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290)
b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329)
a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510)
d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632)
c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713)
b = md5_gg(b, c, d, a, x[i], 20, -373897302)
a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691)
d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083)
c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335)
b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848)
a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438)
d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690)
c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961)
b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501)
a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467)
d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784)
c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473)
b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734)
a = md5_hh(a, b, c, d, x[i + 5], 4, -378558)
d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463)
c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562)
b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556)
a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060)
d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353)
c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632)
b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640)
a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174)
d = md5_hh(d, a, b, c, x[i], 11, -358537222)
c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979)
b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189)
a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487)
d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835)
c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520)
b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651)
a = md5_ii(a, b, c, d, x[i], 6, -198630844)
d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415)
c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905)
b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055)
a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571)
d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606)
c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523)
b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799)
a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359)
d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744)
c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380)
b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649)
a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070)
d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379)
c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259)
b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551)
a = safe_add(a, olda)
b = safe_add(b, oldb)
c = safe_add(c, oldc)
d = safe_add(d, oldd)
}
return [a, b, c, d]
}
/*
* Convert an array of little-endian words to a string
*/
function binl2rstr(input) {
var i,
output = ''
for (i = 0; i < input.length * 32; i += 8) {
output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF)
}
return output
}
/*
* Convert a raw string to an array of little-endian words
* Characters >255 have their high-byte silently ignored.
*/
function rstr2binl(input) {
var i,
output = []
output[(input.length >> 2) - 1] = undefined
for (i = 0; i < output.length; i += 1) {
output[i] = 0
}
for (i = 0; i < input.length * 8; i += 8) {
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32)
}
return output
}
/*
* Calculate the MD5 of a raw string
*/
function rstr_md5(s) {
return binl2rstr(binl_md5(rstr2binl(s), s.length * 8))
}
/*
* Calculate the HMAC-MD5, of a key and some data (raw strings)
*/
function rstr_hmac_md5(key, data) {
var i,
bkey = rstr2binl(key),
ipad = [],
opad = [],
hash
ipad[15] = opad[15] = undefined
if (bkey.length > 16) {
bkey = binl_md5(bkey, key.length * 8)
}
for (i = 0; i < 16; i += 1) {
ipad[i] = bkey[i] ^ 0x36363636
opad[i] = bkey[i] ^ 0x5C5C5C5C
}
hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8)
return binl2rstr(binl_md5(opad.concat(hash), 512 + 128))
}
/*
* Convert a raw string to a hex string
*/
function rstr2hex(input) {
var hex_tab = '0123456789abcdef',
output = '',
x,
i
for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i)
output += hex_tab.charAt((x >>> 4) & 0x0F) +
hex_tab.charAt(x & 0x0F)
}
return output
}
/*
* Encode a string as utf-8
*/
function str2rstr_utf8(input) {
return unescape(encodeURIComponent(input))
}
/*
* Take string arguments and return either raw or hex encoded strings
*/
function raw_md5(s) {
return rstr_md5(str2rstr_utf8(s))
}
function hex_md5(s) {
return rstr2hex(raw_md5(s))
}
function raw_hmac_md5(k, d) {
return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))
}
function hex_hmac_md5(k, d) {
return rstr2hex(raw_hmac_md5(k, d))
}
function md5(string, key, raw) {
if (!key) {
if (!raw) {
return hex_md5(string)
}
return raw_md5(string)
}
if (!raw) {
return hex_hmac_md5(key, string)
}
return raw_hmac_md5(key, string)
}
export default md5
結束
代碼比較多,還差以後後續上傳的方法沒有寫。。。。做個筆記