Files
t-admin/uni_modules/uni-media-library/components/media-list/index.vue
T
2025-06-13 21:16:12 +08:00

609 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>