609 lines
14 KiB
Vue
609 lines
14 KiB
Vue
<template>
|
||
<scroll-view
|
||
class="media-view"
|
||
scroll-y
|
||
@scrolltolower="loadMore"
|
||
>
|
||
<unicloud-db
|
||
ref="mediaUdb"
|
||
v-slot:default="{data, loading, error, pagination}"
|
||
:collection="collection"
|
||
orderby="createDate desc"
|
||
loadtime="manual"
|
||
:page-size="50"
|
||
@load="onMediaListLoad"
|
||
>
|
||
<view v-if="(loading || processing) && pagination.current === 1" class="loading">
|
||
<uni-icons class="icon" type="spinner-cycle" size="30" color="#000"></uni-icons>
|
||
</view>
|
||
<view class="items" v-else-if="mediaList.length">
|
||
<view
|
||
class="media-item"
|
||
:class="{active: mediaItem.active, selected: mediaItem.selected}"
|
||
v-for="(mediaItem, index) in mediaList"
|
||
@click="onSelect(index)"
|
||
:key="mediaItem._id"
|
||
>
|
||
<view class="image" v-if="mediaItem.type === 'image'">
|
||
<image
|
||
:src="mediaItem.isUploading ? mediaItem.src: coverImage(mediaItem.cover, mediaItem.thumbRules && mediaItem.thumbRules.listCover)"
|
||
mode="aspectFill" class="img"></image>
|
||
</view>
|
||
<view class="image" v-if="mediaItem.type === 'video'">
|
||
<video
|
||
class="v"
|
||
:src="mediaItem.src"
|
||
:controls="false"
|
||
:show-center-play-btn="false"
|
||
:poster="coverImage(mediaItem.cover, mediaItem.thumbRules && mediaItem.thumbRules.listCover)"
|
||
v-if="mediaItem.isUploading || /^cloud:\/\//.test(mediaItem._src)"
|
||
></video>
|
||
<image v-else :src="coverImage(mediaItem.cover, mediaItem.thumbRules && mediaItem.thumbRules.listCover)" mode="aspectFill"
|
||
class="img"></image>
|
||
</view>
|
||
<view class="mask" v-if="mediaItem.isUploading">
|
||
<view class="progress">
|
||
<view class="inner" :style="{width: mediaItem.percent + '%'}"></view>
|
||
</view>
|
||
<view :class="{tip: true, error: mediaItem.status === 'failed'}">{{ mediaItem.tip }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="media-library-isnull" v-else>
|
||
<uni-icons type="images" size="60" color="#ccc"></uni-icons>
|
||
<view class="text">媒体库资源为空,是否上传资源?</view>
|
||
<button
|
||
type="primary"
|
||
size="mini"
|
||
@click="$emit('onUploadMedia');"
|
||
>上传媒体资源
|
||
</button>
|
||
</view>
|
||
</unicloud-db>
|
||
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script>
|
||
import {generateCoverURL} from "../../common/crop";
|
||
|
||
const db = uniCloud.database()
|
||
|
||
export default {
|
||
name: "media-list",
|
||
emits: ['onSelect', 'onUploadMedia'],
|
||
expose: ['cancelAllSelected', 'deleteSelectedMedia', 'chooseFiles', 'mediaUploadProgress', 'mediaUploadSuccess', 'mediaUploadFailed'],
|
||
props: {
|
||
mediaType: {
|
||
type: String,
|
||
default: () => "all"
|
||
},
|
||
mediaGroup: String,
|
||
keyword: String,
|
||
selectedCount: Number
|
||
},
|
||
data() {
|
||
return {
|
||
lastId: null,
|
||
lastSelectedIndex: 0,
|
||
remoteMediaList: [],
|
||
localMediaList: [],
|
||
shiftKey: false,
|
||
isUploading: false,
|
||
processing: true
|
||
}
|
||
},
|
||
watch: {
|
||
mediaGroup() {
|
||
this.loadMediaList(true)
|
||
},
|
||
keyword() {
|
||
this.loadMediaList(true)
|
||
}
|
||
},
|
||
computed: {
|
||
collection() {
|
||
return [
|
||
db.collection('uni-media-library').where(this.mediaListWhere || {}).getTemp(),
|
||
db.collection('uni-id-users').field('_id, nickname').getTemp()
|
||
]
|
||
},
|
||
mediaList() {
|
||
return this.localMediaList.concat(this.remoteMediaList)
|
||
},
|
||
mediaListWhere() {
|
||
const cond = []
|
||
|
||
if (this.mediaType !== 'all') {
|
||
cond.push(`type=="${this.mediaType}"`)
|
||
}
|
||
if (this.mediaGroup) {
|
||
cond.push(`group=="${this.mediaGroup}"`)
|
||
}
|
||
|
||
if (this.keyword) {
|
||
cond.push(`${new RegExp(this.keyword, 'i')}.test(description)`)
|
||
}
|
||
|
||
return cond.join('&')
|
||
}
|
||
},
|
||
mounted() {
|
||
this.loadMediaList()
|
||
// #ifdef H5
|
||
document.addEventListener('keydown', this.onKeyDown)
|
||
document.addEventListener('keyup', this.onKeyUp)
|
||
// #endif
|
||
},
|
||
destroyed() {
|
||
// #ifdef H5
|
||
document.removeEventListener('keydown', this.onKeyDown)
|
||
document.removeEventListener('keyup', this.onKeyUp)
|
||
// #endif
|
||
},
|
||
methods: {
|
||
coverImage(url, rule) {
|
||
return generateCoverURL(url, rule)
|
||
},
|
||
onKeyDown(e) {
|
||
if (e.keyCode === 16) {
|
||
this.shiftKey = true
|
||
}
|
||
},
|
||
onKeyUp(e) {
|
||
if (e.keyCode === 16) {
|
||
this.shiftKey = false
|
||
}
|
||
},
|
||
async onMediaListLoad(data) {
|
||
const cloudFiles = []
|
||
|
||
// 保存原始图片
|
||
data.forEach(item => {
|
||
item._src = item.src
|
||
item._cover = item.cover
|
||
})
|
||
|
||
for (let i = 0; i < data.length; i++) {
|
||
const media = data[i]
|
||
if (media.src?.startsWith('cloud://')) {
|
||
cloudFiles.push({
|
||
index: i,
|
||
type: media.type,
|
||
src: media.src,
|
||
cover: media.cover,
|
||
})
|
||
}
|
||
}
|
||
|
||
if (cloudFiles.length) {
|
||
const res = await this.getMediaAssetTempURLs(
|
||
new Set(
|
||
[].concat.call([], ...cloudFiles.map(item => [item.src, item.cover])).filter(item => item)
|
||
)
|
||
)
|
||
|
||
res.fileList.forEach(item => {
|
||
const srcTempFile = cloudFiles.find(file => file.src === item.fileID)
|
||
const coverTempFile = cloudFiles.find(file => file.cover === item.fileID)
|
||
|
||
if (srcTempFile) {
|
||
data[srcTempFile.index].src = item.tempFileURL
|
||
}
|
||
if (coverTempFile) {
|
||
data[coverTempFile.index].cover = item.tempFileURL
|
||
}
|
||
})
|
||
}
|
||
|
||
this.remoteMediaList = this.remoteMediaList.concat(data)
|
||
this.processing = false
|
||
},
|
||
async getMediaAssetTempURLs(listSet) {
|
||
const limit = 30
|
||
|
||
const fileList = Array.from(listSet)
|
||
|
||
const res = {
|
||
fileList: []
|
||
}
|
||
|
||
for (let i = 0; i < fileList.length; i += limit) {
|
||
const resTemp = await uniCloud.getTempFileURL({
|
||
fileList: fileList.slice(i, i + limit)
|
||
})
|
||
res.fileList = res.fileList.concat(resTemp.fileList)
|
||
}
|
||
|
||
return res
|
||
},
|
||
loadMediaList(reset = false) {
|
||
// 重新加载媒体库列表清空上传列表
|
||
if (reset) {
|
||
this.localMediaList = []
|
||
this.cancelAllSelected()
|
||
}
|
||
|
||
this.processing = true
|
||
this.remoteMediaList = []
|
||
this.$nextTick(() => this.$refs.mediaUdb.loadData({
|
||
clear: reset
|
||
}))
|
||
},
|
||
loadMore() {
|
||
this.$refs.mediaUdb.loadMore()
|
||
},
|
||
onSelect(index) {
|
||
if (this.isUploading) return
|
||
|
||
let minIndex = index
|
||
let maxIndex = index
|
||
// shift 多选
|
||
if (this.shiftKey) {
|
||
minIndex = Math.min(this.lastSelectedIndex, index)
|
||
maxIndex = Math.max(this.lastSelectedIndex, index)
|
||
}
|
||
|
||
const mediaItem = this.mediaList[index]
|
||
|
||
// 如果设置了最大选择数量,判断是否超过最大数量
|
||
if (this.selectedCount > 0 && !mediaItem.selected && !mediaItem.active) {
|
||
const selectedMediaCount = this.mediaList.reduce((count, item) => item.selected || item.active ? count + 1 : count, 0)
|
||
if (selectedMediaCount + (maxIndex - minIndex + 1) > this.selectedCount) {
|
||
uni.showToast({
|
||
title: `最多只能选择${this.selectedCount}个媒体文件`,
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 选择
|
||
if (!mediaItem.selected) {
|
||
// 选中当前
|
||
for (let i = minIndex; i <= maxIndex; i++) {
|
||
this.$set(this.mediaList[i], 'active', !mediaItem.active)
|
||
}
|
||
|
||
// 取消其他当前选中并设置为已选择
|
||
for (const otherMediaItem of this.mediaList) {
|
||
if (otherMediaItem.active && otherMediaItem._id !== mediaItem._id) {
|
||
this.$set(otherMediaItem, 'active', false)
|
||
this.$set(otherMediaItem, 'selected', true)
|
||
}
|
||
}
|
||
} else {
|
||
// 取消选择
|
||
this.$set(this.mediaList[index], 'selected', false)
|
||
}
|
||
|
||
this.lastSelectedIndex = index
|
||
|
||
this.$nextTick(() => {
|
||
this.$emit('onSelect', this.mediaList.filter(item => item.selected || item.active))
|
||
})
|
||
},
|
||
cancelAllSelected() {
|
||
if (this.isUploading) return
|
||
|
||
for (const mediaItem of this.mediaList) {
|
||
if (mediaItem.active || mediaItem.selected) {
|
||
this.$set(mediaItem, 'active', false)
|
||
this.$set(mediaItem, 'selected', false)
|
||
}
|
||
}
|
||
this.$nextTick(() => {
|
||
this.$emit('onSelect', [])
|
||
})
|
||
},
|
||
deleteSelectedMedia(mediaIds) {
|
||
if (this.isUploading) return
|
||
|
||
for (const id of mediaIds) {
|
||
const index = this.remoteMediaList.findIndex(item => item._id === id)
|
||
this.remoteMediaList.splice(index, 1)
|
||
}
|
||
|
||
this.$nextTick(() => {
|
||
this.$emit('onSelect', [])
|
||
})
|
||
},
|
||
chooseFiles(files = []) {
|
||
if (this.isUploading) return
|
||
|
||
this.isUploading = true
|
||
this.localMediaList = files.map(file => {
|
||
return {
|
||
...file,
|
||
src: file.thumbUrl,
|
||
type: file.mediaType,
|
||
isUploading: true,
|
||
tip: '等待上传'
|
||
}
|
||
})
|
||
},
|
||
mediaUploadProgress(file) {
|
||
const mediaItemIndex = this.localMediaList.findIndex(item => item.originUrl === file.originUrl)
|
||
|
||
if (mediaItemIndex < 0) return
|
||
|
||
this.localMediaList[mediaItemIndex].tip = '正在上传'
|
||
this.localMediaList[mediaItemIndex].status = file.status
|
||
this.localMediaList[mediaItemIndex].percent = file.percent
|
||
},
|
||
async mediaUploadSuccess(files) {
|
||
const failedFiles = files.filter(file => file.status !== 'uploaded')
|
||
|
||
if (failedFiles.length) return
|
||
|
||
for (const file of files) {
|
||
const mediaItemIndex = this.localMediaList.findIndex(item => item.originUrl === file.originUrl)
|
||
const mediaItem = this.localMediaList[mediaItemIndex]
|
||
|
||
if (mediaItemIndex < 0) continue
|
||
|
||
const attrs = {
|
||
cover: file.url, // 媒体资源封面
|
||
...(file.attributes || {})
|
||
}
|
||
|
||
if (mediaItem.type === 'video') {
|
||
// 截取视频封面
|
||
await this.cropVideoCover(mediaItemIndex, file.url, attrs)
|
||
}
|
||
|
||
// 上报媒体库
|
||
await this.reportFunction(mediaItemIndex, file.url, attrs)
|
||
|
||
this.localMediaList[mediaItemIndex].thumbUrl = file.url
|
||
this.localMediaList[mediaItemIndex].tip = '上传成功'
|
||
this.localMediaList[mediaItemIndex].status = file.status
|
||
}
|
||
|
||
this.isUploading = false
|
||
|
||
// 上传完成重新加载媒体库
|
||
this.loadMediaList(true)
|
||
},
|
||
async mediaUploadFailed (file) {
|
||
const mediaItemIndex = this.localMediaList.findIndex(item => item.originUrl === file.originUrl)
|
||
|
||
if (mediaItemIndex < 0) return
|
||
|
||
this.localMediaList[mediaItemIndex].tip = `上传失败: ${file.error?.message ?? '未知错误'}`
|
||
this.localMediaList[mediaItemIndex].status = file.status
|
||
},
|
||
async reportFunction(mediaIndex, url, attrs = {}) {
|
||
const mediaItem = this.localMediaList[mediaIndex]
|
||
if (!mediaItem) return
|
||
|
||
this.localMediaList[mediaIndex].tip = '正在添加至媒体库'
|
||
|
||
const uniMediaLibraryCo = uniCloud.importObject('uni-media-library-co', {
|
||
customUI: true
|
||
})
|
||
|
||
await uniMediaLibraryCo.report({
|
||
src: url,
|
||
type: mediaItem.type,
|
||
originalName: mediaItem.name,
|
||
fileType: mediaItem.fileType,
|
||
size: mediaItem.size,
|
||
resolution: {
|
||
width: attrs.width || 0,
|
||
height: attrs.height || 0
|
||
},
|
||
duration: attrs.duration || 0,
|
||
uploadUser: uniCloud.getCurrentUserInfo().uid,
|
||
...attrs
|
||
})
|
||
},
|
||
async cropVideoCover(mediaIndex, url, attrs) {
|
||
const mediaItem = this.localMediaList[mediaIndex]
|
||
const isTcbCloud = url.startsWith('cloud://')
|
||
const isAlipayCloud = url.startsWith('cloud://env-')
|
||
|
||
if (!mediaItem || (isTcbCloud && !isAlipayCloud)) return undefined
|
||
|
||
this.localMediaList[mediaIndex].tip = '正在截取视频封面'
|
||
const ossProcessRule = 'video/snapshot,t_0,f_jpg,w_0,h_0,m_fast'
|
||
let cropUrl = `${url}?x-oss-process=${ossProcessRule}`
|
||
|
||
if (isAlipayCloud) {
|
||
const res = await uniCloud.getTempFileURL({
|
||
fileList: [url]
|
||
})
|
||
|
||
if (res.fileList && res.fileList.length) {
|
||
url = res.fileList[0].tempFileURL
|
||
cropUrl = `${url}&x-oss-process=${ossProcessRule}`
|
||
}
|
||
}
|
||
|
||
const image = await fetch(cropUrl)
|
||
const imageBlob = await image.blob()
|
||
const imageFile = new File([imageBlob], 'image.jpg', {type: 'image/jpeg'})
|
||
const imageBlobUrl = await this.file2blob(imageFile)
|
||
|
||
const uploadRes = await uniCloud.uploadFile({
|
||
filePath: imageBlobUrl,
|
||
cloudPath: `uni-media-library/video-cover-${Date.now() + Math.round(Math.random() * 10000)}.jpg`,
|
||
fileType: 'image'
|
||
})
|
||
|
||
attrs.cover = uploadRes.fileID
|
||
},
|
||
file2blob(file) {
|
||
let url
|
||
if (window.createObjectURL !== undefined) {
|
||
url = window.createObjectURL(file);
|
||
} else if (window.URL !== undefined) {
|
||
url = window.URL.createObjectURL(file);
|
||
} else if (window.webkitURL !== undefined) {
|
||
url = window.webkitURL.createObjectURL(file);
|
||
}
|
||
return url
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.media-view {
|
||
flex: 1;
|
||
margin-top: 10px;
|
||
overflow-y: auto;
|
||
|
||
.items {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
.loading {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
|
||
.icon {
|
||
animation: rotate 1s linear infinite;
|
||
}
|
||
}
|
||
|
||
.media-item {
|
||
--row-media-item: 7;
|
||
width: calc(100% / var(--row-media-item) - (((var(--row-media-item) - 1) * 10px) / (var(--row-media-item)) + 0.01px));
|
||
border: #f5f5f5 solid 5px;
|
||
border-radius: 6px;
|
||
box-sizing: border-box;
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: border-color .25s;
|
||
|
||
&.selected {
|
||
border-color: rgba(41, 121, 255, .5);
|
||
}
|
||
|
||
&.active {
|
||
border-color: #2979ff;
|
||
}
|
||
|
||
.image {
|
||
height: 0;
|
||
padding-bottom: 100%;
|
||
position: relative;
|
||
|
||
.img, .v {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
}
|
||
}
|
||
|
||
.mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1;
|
||
background: rgba(0, 0, 0, .5);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.tip {
|
||
font-size: 12px;
|
||
color: #fff;
|
||
margin-top: 5px;
|
||
&.error {
|
||
color: red;
|
||
text-align: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
.progress {
|
||
width: 80%;
|
||
height: 4px;
|
||
border-radius: 2px;
|
||
background: #fff;
|
||
position: relative;
|
||
overflow: hidden;
|
||
|
||
.inner {
|
||
width: 0;
|
||
height: 100%;
|
||
border-radius: 2px;
|
||
transition: width .3s;
|
||
background: #2979ff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.media-library-isnull {
|
||
padding-top: 50px;
|
||
text-align: center;
|
||
|
||
.text {
|
||
font-size: 18px;
|
||
color: #ccc;
|
||
margin-top: 5px;
|
||
margin-bottom: 30px;
|
||
}
|
||
}
|
||
|
||
@keyframes rotate {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg)
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 768px) {
|
||
.media-item {
|
||
--row-media-item: 3;
|
||
}
|
||
}
|
||
|
||
@media screen and (min-width: 768px) {
|
||
.media-item {
|
||
--row-media-item: 5;
|
||
}
|
||
}
|
||
|
||
@media screen and (min-width: 1360px) {
|
||
.media-item {
|
||
--row-media-item: 7;
|
||
}
|
||
}
|
||
|
||
@media screen and (min-width: 1920px) {
|
||
.media-item {
|
||
--row-media-item: 10;
|
||
}
|
||
}
|
||
|
||
@media screen and (min-width: 2560px) {
|
||
.media-item {
|
||
--row-media-item: 15;
|
||
}
|
||
}
|
||
</style>
|