info
This commit is contained in:
@@ -0,0 +1,608 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user