一 、需求: 基于 vue cli 可以上传视频(可以观看)或者图片(可以预览) -进行自由拖拽-或者删除功能
二 、技术: vuedraggable + el-upload + videoPlayer
三、步骤:
1 安装
npm i -S vuedraggable
2 页面引入:
import {
API_pos_screen_update
} from '@/api/settings/pos-screen'
import vuedraggable from 'vuedraggable'
import store from '@/store'
import tools from '../../utils/tools'
import { videoPlayer } from 'vue-video-player'
import 'video.js/dist/video-js.css'
import 'vue-video-player/src/custom-theme.css'
import 'videojs-flash' // 引入才能播放rtmp视频
import 'videojs-contrib-hls' // 引入才能播放m3u8文件
3 定义
export default {
name: 'ImgUpload',
components: {
vuedraggable,
// eslint-disable-next-line vue/no-unused-components
videoPlayer
},
inject: ['reload'],
props: {
// 数据(url组成的数组) 通过v-model传递
value: {
type: Array,
default() {
return []
}
},
// 限制上传的数量
limit: {
type: Number,
default: 99
},
// 限制上传的文件大小(kb)
size: {
type: Number,
default: 5000
},
// 是否是单个上传(单图上传就是已传图片和上传按钮重叠)
isSingle: {
type: Boolean,
default: false
},
// 是否使用压缩
useCompress: {
type: Boolean,
default: false
},
// 显示的宽度(px)
width: {
type: Number,
default: 320
},
// 显示的高度(px)
height: {
type: Number,
default: 160
}
},
data() {
return {
headers: {
operatorToken: store.getters.token,
shopId: store.getters.shopId
},
videoUploadUrl: process.env.VUE_APP_BASE_API + '/shop/upload',
isUploading: false, // 正在上传状态
isFirstMount: true, // 控制防止重复回显
playerOptions: [],
formData: {
displayList: []
},
fileType: {
PICTURE: 'PICTURE',
VIDEO: 'VIDEO'
},
loading: false
}
},
computed: {
// 图片数组数据
imgList: {
get() {
return this.value
},
set(val) {
if (val.length < this.imgList.length) {
// 判断是删除图片时同步el-upload数据
this.syncElUpload(val)
}
// 同步v-model
this.$emit('input', val)
}
},
// 控制达到最大限制时隐藏上传按钮
isMaxHidden() {
return this.imgList.length >= this.limit
}
},
watch: {
value: {
handler(val) {
if (this.isFirstMount && this.value.length > 0) {
this.syncElUpload()
this.value.forEach(item => {
if (!this.isType(item)) {
const arrs = {
playbackRates: [0.5, 1.0, 1.5, 2.0], // 可选的播放速度
autoplay: false, // 如果为true,浏览器准备好时开始回放。
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 是否视频一结束就重新开始。
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '2:1', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{
src: item
}],
poster: '', // 封面地址
notSupportedMessage: '此视频暂无法播放,请稍后再试', // 允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true, // 当前时间和持续时间的分隔符
durationDisplay: true, // 显示持续时间
remainingTimeDisplay: false, // 是否显示剩余时间功能
fullscreenToggle: true // 是否显示全屏按钮
}
}
this.playerOptions.push(arrs)
} else {
const arr = {
playbackRates: [0.5, 1.0, 1.5, 2.0], // 可选的播放速度
autoplay: false, // 如果为true,浏览器准备好时开始回放。
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 是否视频一结束就重新开始。
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '2:1', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [],
poster: '', // 封面地址
notSupportedMessage: '此视频暂无法播放,请稍后再试', // 允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true, // 当前时间和持续时间的分隔符
durationDisplay: true, // 显示持续时间
remainingTimeDisplay: false, // 是否显示剩余时间功能
fullscreenToggle: true // 是否显示全屏按钮
}
}
this.playerOptions.push(arr)
}
})
}
},
deep: true
}
},
mounted() {
if (this.value.length > 0) {
this.syncElUpload()
}
},
methods: {
// 同步el-upload数据
syncElUpload(val) {
const imgList = val || this.imgList
this.$refs.uploadRef.uploadFiles = imgList.map((v, i) => {
return {
name: 'pic' + i,
url: v,
status: 'success',
uid: tools.createUniqueString()
}
})
this.isFirstMount = false
},
// 上传之前
beforeUpload(file) {
this.isFirstMount = false
this.isUploading = true
const isIMG =
file.type === 'image/jpg' ||
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'video/mp4' ||
file.type === 'video/flv' ||
file.type === 'video/avi' ||
file.type === 'video/wmv' ||
file.type === 'video/rmvb'
const isLt = file.size / 1024 / 1024 <= 80
if (!isIMG) {
this.$message.error('上传图片只支持jpg、jpeg、png格式! 上传视频只支持mp4,flv,avi,wmv,rmvb格式')
}
if (!isLt) {
this.$message.error('上传大小不能超过80MB!')
}
return isIMG && isLt
},
// 上传成功
onSuccessUpload(res, file, fileList) {
// 这里需要根据你自己的接口返回数据格式和层级来自行修改
if (res.status.code === 0) {
// 判断接口上传成功
if (this.imgList.length < this.limit) {
// 未超限时,把接口返回的图片url地址添加到imgList
this.imgList.push(res.result.url)
this.saveSetting()
}
} else {
// 判断接口上传失败
this.syncElUpload()
this.$message({ type: 'error', message: res.msg })
}
this.isUploading = false
},
// 移除
onRemoveHandler(index) {
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
this.imgList = this.imgList.filter((v, i) => {
return i !== index
})
}).finally(() => {
this.saveSetting()
})
},
// 超限
onExceed() {
this.$refs.uploadRef.abort() // 取消剩余接口请求
this.syncElUpload()
this.$message({
type: 'warning',
message: `图片超限,最多可上传${this.limit}张图片`
})
},
onDragStart(e) {
e.target.classList.add('hideShadow')
},
onDragEnd(e) {
e.target.classList.remove('hideShadow')
this.saveSetting()
},
isType: function(item) {
const acceptType = ['jpg', 'jpeg', 'png', 'bmp']
const isType = acceptType.indexOf(item.slice(-3)) >= 0
if (isType) {
return isType
} else {
return isType
}
},
saveSetting() {
const loading = this.$loading({
lock: true,
fullscreen: true,
text: 'Loading...'
})
// 保存上传文件信息
this.imgList.forEach((item, itemIndex) => {
if (this.isType(item)) {
var displayPicture = {
type: this.fileType.PICTURE,
url: item,
sort: itemIndex
}
this.formData.displayList.push(displayPicture)
} else {
var displayVideo = {
type: this.fileType.VIDEO,
url: item,
sort: itemIndex
}
this.formData.displayList.push(displayVideo)
}
})
API_pos_screen_update(this.formData).then(res => {
this.$message.success('Update Success')
loading.close()
}).finally(() => {
this.reload()
})
}
}
}
4 钩子
mounted() {
if (this.value.length > 0) {
this.syncElUpload()
}
},
methods: {
// 同步el-upload数据
syncElUpload(val) {
const imgList = val || this.imgList
this.$refs.uploadRef.uploadFiles = imgList.map((v, i) => {
return {
name: 'pic' + i,
url: v,
status: 'success',
uid: tools.createUniqueString()
}
})
this.isFirstMount = false
},
// 上传之前
beforeUpload(file) {
this.isFirstMount = false
this.isUploading = true
const isIMG =
file.type === 'image/jpg' ||
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'video/mp4' ||
file.type === 'video/flv' ||
file.type === 'video/avi' ||
file.type === 'video/wmv' ||
file.type === 'video/rmvb'
const isLt = file.size / 1024 / 1024 <= 80
if (!isIMG) {
this.$message.error('上传图片只支持jpg、jpeg、png格式! 上传视频只支持mp4,flv,avi,wmv,rmvb格式')
}
if (!isLt) {
this.$message.error('上传大小不能超过80MB!')
}
return isIMG && isLt
},
// 上传成功
onSuccessUpload(res, file, fileList) {
// 这里需要根据你自己的接口返回数据格式和层级来自行修改
if (res.status.code === 0) {
// 判断接口上传成功
if (this.imgList.length < this.limit) {
// 未超限时,把接口返回的图片url地址添加到imgList
this.imgList.push(res.result.url)
this.saveSetting()
}
} else {
// 判断接口上传失败
this.syncElUpload()
this.$message({ type: 'error', message: res.msg })
}
this.isUploading = false
},
// 移除
onRemoveHandler(index) {
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
this.imgList = this.imgList.filter((v, i) => {
return i !== index
})
}).finally(() => {
this.saveSetting()
})
},
// 超限
onExceed() {
this.$refs.uploadRef.abort() // 取消剩余接口请求
this.syncElUpload()
this.$message({
type: 'warning',
message: `图片超限,最多可上传${this.limit}张图片`
})
},
onDragStart(e) {
e.target.classList.add('hideShadow')
},
onDragEnd(e) {
e.target.classList.remove('hideShadow')
this.saveSetting()
},
isType: function(item) {
const acceptType = ['jpg', 'jpeg', 'png', 'bmp']
const isType = acceptType.indexOf(item.slice(-3)) >= 0
if (isType) {
return isType
} else {
return isType
}
},
saveSetting() {
const loading = this.$loading({
lock: true,
fullscreen: true,
text: 'Loading...'
})
// 保存上传文件信息
this.imgList.forEach((item, itemIndex) => {
if (this.isType(item)) {
var displayPicture = {
type: this.fileType.PICTURE,
url: item,
sort: itemIndex
}
this.formData.displayList.push(displayPicture)
} else {
var displayVideo = {
type: this.fileType.VIDEO,
url: item,
sort: itemIndex
}
this.formData.displayList.push(displayVideo)
}
})
API_pos_screen_update(this.formData).then(res => {
this.$message.success('Update Success')
loading.close()
}).finally(() => {
this.reload()
})
}
}
</script>
<style lang="less" scoped>
/deep/ .el-upload {
width: 100%;
height: 100%;
}
// 上传按钮
.uploadIcon {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #c0ccda;
background-color: #fbfdff;
border-radius: 6px;
font-size: 20px;
color: #999;
.limitTxt,
.uploading {
position: absolute;
bottom: 10%;
left: 0;
width: 100%;
font-size: 14px;
text-align: center;
}
}
// 拖拽
.vue-draggable {
display: flex;
flex-wrap: wrap;
.draggable-item {
width: 70%;
margin-right: 5px;
margin-bottom: 5px;
border: 1px solid #ddd;
border-radius: 6px;
position: relative;
overflow: hidden;
.el-image {
width: 100%;
height: 100%;
}
.video-player {
width: 100%;
height: 100%;
}
.shadow {
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, .5);
opacity: 0;
transition: opacity .3s;
color: #fff;
font-size: 20px;
line-height: 20px;
padding: 2px;
cursor: pointer;
}
&:hover {
.shadow {
opacity: 1;
}
}
}
&.hideShadow {
.shadow {
display: none;
}
}
&.single {
overflow: hidden;
position: relative;
.draggable-item {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
}
&.maxHidden {
.uploadBox {
display: none;
}
}
}
// el-image
.el-image-viewer__wrapper {
.el-image-viewer__mask {
opacity: .8;
}
.el-icon-circle-close {
color: #fff;
}
}
.video-js .vjs-big-play-button {
width: 20px;
height: 20px;
border-radius: 50%;
z-index: 50;
border: solid 1px #979797;
}
.avatar-uploader-icons {
font-size: 25px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 150px;
text-align: center;
}
</style>
5父组件引入
<imgUpload v-if="isRouterAlive" v-model="formData.imgList" />