| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- <template>
- <view class="upload-video">
- <view class="video-list">
- <view v-for="(video, index) in videoList" :key="index" class="video-item">
- <video class="video" :src="video.url" :poster="video.thumbnail" controls></video>
- <view class="video-info">
- <text class="video-duration">{{ formatDuration(video.duration) }}</text>
- <text class="video-size">{{ formatSize(video.fileSize) }}</text>
- </view>
- <view class="video-delete" @click="handleDelete(index)">
- <image class="delete-icon" src="/static/images/common/3.png" mode="aspectFit"></image>
- </view>
- </view>
- <!-- 上传按钮 -->
- <view v-if="videoList.length < maxCount && !uploading" class="upload-box" @click="handleChooseVideo">
- <text class="upload-icon">+</text>
- <text class="upload-text">上传视频</text>
- </view>
- <!-- 上传中 -->
- <view v-if="uploading" class="upload-box uploading">
- <text class="upload-text">上传中...</text>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="uts">
- import { ref, computed } from 'vue'
- import { uploadVideo } from '../../api/upload/video'
- import type { VideoItem } from '../../types/workbench'
- // Props
- type Props = {
- modelValue?: VideoItem[]
- maxCount?: number
- maxSize?: number // MB
- maxDuration?: number // 秒
- businessType?: string
- }
- const props = withDefaults(defineProps<Props>(), {
- modelValue: () => [],
- maxCount: 3,
- maxSize: 50,
- maxDuration: 60,
- businessType: 'video'
- })
- // Emits
- const emit = defineEmits<{
- (e: 'update:modelValue', videos: VideoItem[]): void
- (e: 'change', videos: VideoItem[]): void
- }>()
- // 视频列表
- const videoList = ref<VideoItem[]>(props.modelValue)
- const uploading = ref<boolean>(false)
- // 上传视频(必须在 handleChooseVideo 之前定义)
- const handleUpload = async (
- filePath: string,
- duration: number,
- size: number,
- thumbnail: string
- ): Promise<void> => {
- try {
- // 检查文件大小
- if (size > props.maxSize * 1024 * 1024) {
- uni.showToast({
- title: `视频大小不能超过${props.maxSize}MB`,
- icon: 'none'
- })
- return
- }
- uploading.value = true
- // 上传视频,获取完整的文件信息
- const result = await uploadVideo(filePath, props.businessType)
- // 构建视频项,包含完整的上传响应信息和视频元数据
- const videoItem: VideoItem = {
- url: result.url,
- fileId: result.fileId,
- fileName: result.fileName,
- filePath: result.filePath,
- fileSize: result.fileSize,
- fileExt: result.fileExt,
- businessType: result.businessType,
- thumbnail: thumbnail,
- duration: duration
- }
- videoList.value.push(videoItem)
- emit('update:modelValue', videoList.value)
- emit('change', videoList.value)
- uni.showToast({
- title: '上传成功',
- icon: 'success'
- })
- } catch (e: any) {
- uni.showToast({
- title: e.message ?? '上传失败',
- icon: 'none'
- })
- } finally {
- uploading.value = false
- }
- }
- // 选择视频
- const handleChooseVideo = (): void => {
- if (videoList.value.length >= props.maxCount) {
- uni.showToast({
- title: `最多上传${props.maxCount}个视频`,
- icon: 'none'
- })
- return
- }
- uni.chooseVideo({
- sourceType: ['album', 'camera'],
- maxDuration: props.maxDuration,
- success: (res) => {
- // 提取属性(any 类型不能直接访问属性)
- const tempFilePath = res.tempFilePath as string
- const duration = res.duration as number
- const size = res.size as number
- // uni.chooseVideo 在 uni-app x 中可能不返回缩略图,使用视频路径作为临时缩略图
- const thumbnail = tempFilePath
- handleUpload(tempFilePath, duration, size, thumbnail)
- },
- fail: (err: any) => {
- console.log('选择视频失败', err)
- uni.showToast({
- title: '选择视频失败',
- icon: 'none'
- })
- }
- })
- }
- // 删除视频
- const handleDelete = (index: number): void => {
- uni.showModal({
- title: '确认删除',
- content: '确定要删除这个视频吗?',
- success: (res) => {
- const confirm = res.confirm as boolean
- if (confirm) {
- videoList.value.splice(index, 1)
- emit('update:modelValue', videoList.value)
- emit('change', videoList.value)
- }
- }
- })
- }
- // 格式化时长
- const formatDuration = (duration: number): string => {
- const minutes = Math.floor(duration / 60)
- const seconds = Math.floor(duration % 60)
- return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
- }
- // 格式化大小
- const formatSize = (size: number): string => {
- if (size < 1024 * 1024) {
- return `${(size / 1024).toFixed(2)}KB`
- } else {
- return `${(size / 1024 / 1024).toFixed(2)}MB`
- }
- }
- </script>
- <style lang="scss">
- .video {
- &-list {
- flex-direction: row;
- flex-wrap: wrap;
- }
- &-item {
- background-color: #f5f7fa;
- border-radius: 8rpx;
- position: relative;
- width: 220rpx;
- height: 220rpx;
- margin-right: 20rpx;
- margin-bottom: 20rpx;
- }
- width: 220rpx;
- height: 220rpx;
- border-radius: 8rpx;
- &-info {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- padding: 10rpx;
- background-color: rgba(0, 0, 0, 0.6);
- border-bottom-left-radius: 8rpx;
- border-bottom-right-radius: 8rpx;
- flex-direction: row;
- justify-content: space-between;
- }
- &-duration,
- &-size {
- font-size: 22rpx;
- color: #ffffff;
- }
- &-delete {
- position: absolute;
- top: 2rpx;
- right: 2rpx;
- width: 44rpx;
- height: 44rpx;
- justify-content: center;
- align-items: center;
- }
- }
- .delete-icon {
- width: 32rpx;
- height: 32rpx;
- }
- .upload {
- &-box {
- background-color: #f5f7fa;
- border-radius: 8rpx;
- width: 220rpx;
- height: 220rpx;
- margin-right: 20rpx;
- margin-bottom: 20rpx;
- border: 2rpx dashed #d0d0d0;
- justify-content: center;
- align-items: center;
- }
- &-icon {
- font-size: 60rpx;
- color: #999999;
- line-height: 60rpx;
- margin-bottom: 10rpx;
- }
- &-text {
- font-size: 24rpx;
- color: #999999;
- }
- }
- .uploading {
- border-style: solid;
- border-color: #d0d0d0;
- background-color: #f5f7fa;
- }
- </style>
|