uniapp在vue3项目中使用<audio>报错问题

一、问题描述

在uniapp的vue3项目中使用 <audio> 标签报错,提示找不到audio的css文件:[plugin:vite:import-analysis] Cannot find module '@dcloudio/uni-components/style/audio.css

二、问题原因

通过查阅uniapp官方文档<audio>在uniapp的vue3项目中已经弃用,故而找不到对应的css文件

三、如果确实需要,如何在uniapp的vue3项目中使用audio

可以使用Vue 自带的"动态组件"标签:is="'audio'" 会让 Vue 在运行时把这一行渲染成 原生的 <audio> 元素

vue 复制代码
<component v-if="currentFile.type =='audio" :is="'audio'" controls>
    <source:src="currentFile.url" type="audio/mpeg"/>
</component>

四、实际应用

图片、视频、音频上传、预览组件(这里是对 uview-plus组件库 进行了二次封装)

vue 复制代码
<template>
  <view class="u-page" id="uploadFile">
    <up-upload
      :fileList="fileList"
      @afterRead="afterRead"
      @delete="deletePic"
      name="1"
      :capture="capture"
      multiple
      width="196rpx"
      height="196rpx"
      :deletable="!readOnly && deletable"
      :class="{ hiddenSuccess: !deletable || readOnly }"
      :maxCount="maxCount"
      :previewFullImage="true"
      :preview="true"
      :maxSize="maxSize"
      @oversize="oversize"
      accept="file"
      :getVideoThumb="true"
      @clickPreview="clickPreview"
      :disabled="readOnly"
    >
      <image
        v-if="!readOnly"
        src="@/static/delear/empty_file.png"
        mode="widthFix"
        style="width: 196rpx; height: 196rpx; border-radius: 8rpx"
      ></image>
    </up-upload>
    <BaseModal :show="showFileModal" closeOnClickOverlay @close="showFileModal = false">
      <template #default>
        <view class="close-btn" @click="showFileModal = false"></view>
        <view class="video-content" v-if="currentFile.type == 'video'">
          <video class="video-content-content" :src="currentFile.url" controls autoplay></video>
        </view>
        <view class="audio-content" v-if="currentFile.type == 'audio'">
        
          //  使用动态组件渲染原生audio标签
          <component class="audio-content-content" :is="'audio'" controls>
            <source :src="currentFile.url" />
          </component>
          
        </view>
      </template>
      <template #confirmButton>
        <view class="file-modal-btn"></view>
      </template>
    </BaseModal>
  </view>
</template>

<script setup>
import { beforeUpload } from '@/util/uploadHuaWeiCloud/up'
import BaseModal from '@/components/BaseModal.vue'
// import { useUser } from '@/stores/useUser'
// import { getUploadConfig } from '@/server/baseApi'

const STORE_NAME = 'AUTH_INFO'
const userInfo = uni.getStorageSync(STORE_NAME)
const fileList = ref([])
const emit = defineEmits()
const StoreName = 'hwyResult' // 存储fileList内华为云结果字段名
const ReturnEmitName = 'getImgResult' // 获取图片上传结果emit事件名
const getPictureList = 'getPictureList'
const showFileModal = ref(false)
const currentFile = ref({ url: '', type: '' })

// 定义组件接受的属性
const props = defineProps({
  // 图片最多上传数量
  inputFileList: {
    type: Array,
    default: () => []
  },
  // 是否可删除,true=可删除,false=不可删除
  deletable: {
    type: Boolean,
    default: true
  },
  maxCount: {
    type: Number,
    default: 10
  },
  // 标识返回结果
  id: {
    type: [String, Number],
    required: false
  },
  point: {
    type: Object,
    default: {}
  },
  dir: {
    type: String,
    required: false
  },
  waterMark: {
    type: String,
    default: ''
  },
  //经销商编码
  joinCode: {
    type: [String, Number],
    default: ''
  },
  // 图片最大尺寸
  maxSize: {
    type: [Number, String],
    default: 500 * 1024 * 1024 // 500MB
  },
  // 是否只读
  readOnly: {
    type: Boolean,
    default: false
  }
})

// 监听inputFileList的变化,同步更新fileList的值
watch(
  () => props.inputFileList,
  (newVal) => {
    // fileList不为空才执行
    if (newVal) {
      fileList.value = newVal.map((item) => {
        return {
          status: 'success',
          message: '',
          url: item.fileFullUrl || item.stashUrl,
          [StoreName]: item
        }
      })
    }

    // 图片最多上传数量
  },
  { deep: true, immediate: true }
)

// 过滤出存储在fileList中华为云数据
const filterResult = (list) => {
  const result = []

  list.forEach((item) => {
    result.push(item[StoreName])
  })

  let res = result

  if (props.id || props.id === 0) {
    res = {
      id: props.id,
      result
    }
  }
  return res
}

// 删除图片
const deletePic = (event) => {
  fileList.value.splice(event.index, 1)

  emit(ReturnEmitName, filterResult(fileList.value))
  emit(getPictureList, filterResult(fileList.value), props.point)
}

// 按固定长度切割数组
const splitByLength = (str, len, fillChar = '') => {
  const result = []
  for (let i = 0; i < str.length; i += len) {
    let chunk = str.slice(i, i + len)
    // 如果是最后一段且长度不足,用指定字符填充
    if (chunk.length < len) {
      chunk = chunk.padEnd(len, fillChar)
    }
    result.push(chunk)
  }
  return result
}

// 添加水印
const addWaterMark = (file, text, type) => {
  return new Promise((resolve, reject) => {
    const img = new Image()
    const url = file.url
    img.onload = function () {
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')

      const width = img.width
      const height = img.height
      canvas.width = width
      canvas.height = height

      // 绘制图片
      ctx.drawImage(img, 0, 0)

      let texts = text.split(',')

      // 计算每行文字的位置
      const imgBase = Math.min(width, height)
      const fontSize = Math.ceil(imgBase / 500) * 15
      const lineHeight = fontSize + fontSize / 2 // 每行文字的高度

      const textLeft = 20
      const textBottom = 0
      const textMaxWidth = width - textLeft
      const textMaxCount = Math.floor(textMaxWidth / fontSize)

      let newTexts = texts.map((text) => {
        // 如果文字长度大于最大宽度,则进行分割
        return splitByLength(text, textMaxCount)
      })
      newTexts = newTexts.flat(Infinity)

      let y = height - lineHeight * newTexts.length - textBottom

      ctx.font = `${fontSize}px Arial`
      ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'

      // 绘制每行文字
      newTexts.forEach((text) => {
        ctx.fillText(text, textLeft, y)
        y += lineHeight
      })

      // 添加水印canvas转blob
      canvas.toBlob((blob) => {
        const blobUrl = URL.createObjectURL(blob)
        const newFile = new File([blob], file.name, {
          type: type,
          lastModified: new Date().getTime()
        })

        resolve({ blob, url: blobUrl, file: newFile })
      }, type)
    }
    img.onerror = function () {
      reject(new Error('Failed to load image'))
    }
    img.src = url
  })
}

// 1. 通过文件扩展名校验
const checkFileType = (file) => {
  const allowTypes = {
    image: ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'],
    video: ['.mp4', '.mov', '.avi', '.mkv', '.wmv'],
    audio: ['.mp3', '.wav', '.ogg', '.flac','m4a']
  }
  const fileName = file.toLowerCase()
  return {
    isImage: allowTypes.image.some((ext) => fileName.endsWith(ext)),
    isVideo: allowTypes.video.some((ext) => fileName.endsWith(ext)),
    isAudio: allowTypes.audio.some((ext) => fileName.endsWith(ext))
  }
}

// 新增图片或视频
const afterRead = async (event) => {
  const fileType = event.file[0]?.name
  const fileCheck = checkFileType(fileType)

  if (!fileCheck.isImage && !fileCheck.isVideo && !fileCheck.isAudio) {
    uni.showToast({
      title: '暂不支持此格式!',
      icon: 'none'
    })
    return
  }

  // 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
  let lists = [].concat(event.file)
  let fileListLen = fileList.value.length

  lists.map((item) => {
    fileList.value.push({
      ...item,
      status: 'uploading',
      message: '上传中'
    })
  })

  const userName = JSON.parse(userInfo)?.username || 'anonymity'
  let routes = getCurrentPages()
  let curRoute = routes.at(-1)?.route || ''
  let dir = curRoute.split('/').at(-1) || 'default'

  for (let i = 0; i < lists.length; i++) {
    let waterMark = {}

    // 只对图片添加水印
    if (props.waterMark && !fileCheck.isVideo && !fileCheck.isAudio) {
      waterMark = await addWaterMark(lists[i], props.waterMark, lists[i].file.type)
    }

    const file = waterMark.file || lists[i]
    try {
      const result = await beforeUpload(file, userName, props.dir || dir)
      result.stashUrl = waterMark.url || lists[i].url

      let item = fileList.value[fileListLen]
      fileList.value.splice(fileListLen, 1, {
        ...item,
        status: 'success',
        message: '',
        url: lists[i].url,
        [StoreName]: result
      })
    } catch (error) {
      console.error('上传失败', error)
      fileList.value.splice(fileListLen, 1)
      uni.showToast({
        title: '上传失败!',
        icon: 'none',
        duration: 2000,
        mask: true
      })
    }
    fileListLen++
  }

  emit(ReturnEmitName, filterResult(fileList.value))
  emit(getPictureList, filterResult(fileList.value), props.point)
}

// 图片大小超过限制,可自定义最大上传文件大小
const oversize = () => {
  uni.showToast({
    title: `图片大小不能超过${props.maxSize / 1024 / 1024}MB!`,
    icon: 'none'
  })
}

// 特殊处理开往前过渡模块,此模块支持拍照和上传
if (props.stage == 'beforeNet') {
  capture.value = ['camera', 'album']
}

// 如果joinCode和stage都有值,才调用isCanUpload判断后台配置是否支持上传
if (props.joinCode && props.stage) {
  isCanUpload(props.joinCode, props.stage)
}

const clickPreview = (item) => {
  const file = item
  // 提取 MIME 类型中的主类型(如 "audio"、"video"、"image")
  let type
  if (props.readOnly) {
    if (file.isImage) {
      type = 'image'
    } else if (file.isVideo) {
      type = 'video'
    } else {
      type = 'audio'
    }
  } else {
    type = file.type.split('/')[0]
  }
  currentFile.value = {
    url: file.url,
    type: type
  }
  if (type == 'video') {
    showFileModal.value = true
  } else if (type == 'audio') {
    showFileModal.value = true
  } else if (type == 'image') {
    uni.previewImage({
      urls: fileList.value.map((item) => item.url),
      current: item.index
    })
  } else {
    // 提示不支持的文件类型
    uni.showModal({
      title: '文件类型不支持',
      content: '当前文件类型暂不支持预览',
      showCancel: false
    })
  }
}
</script>

<style lang="scss" scoped>
.u-page#uploadFile {
  background: unset;
  :deep(.u-upload__wrap) {
    gap: 12rpx;

    & > uni-view {
      height: 196rpx;
    }
  }

  :deep(.u-upload__wrap__preview) {
    height: 196rpx;
    border-radius: 8rpx;
    margin: 0;
  }

  .hiddenSuccess :deep(.u-upload__success) {
    display: none;
  }
  :deep(.u-modal) {
    border-radius: 0;
  }
  :deep(.u-modal__content) {
    // padding: 68rpx 0 0 !important;
    padding: 0 0 0 !important;
    position: relative;
    .video-content {
      width: 650rpx;
      height: 450rpx;
      .video-content-content {
        width: 650rpx;
      }
    }
    .audio-content {
      width: 650rpx;
      height: 108rpx;
      .audio-content-content {
        width: 650rpx;
      }
    }
  }
  :deep(.u-modal__button-group--confirm-button) {
    padding: 0;
  }
  :deep(.u-upload__wrap__preview__other__text) {
    word-break: break-all;
    padding: 0 16rpx;
    overflow-y: hidden;
    max-height: 96rpx;
    text-align: justify;
  }
}
</style>
相关推荐
Amodoro2 分钟前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin10 分钟前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说1 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang4531 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2431 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
苹果醋31 小时前
iview中实现点击表格单元格完成编辑和查看(span和input切换)
运维·vue.js·spring boot·nginx·课程设计
武昌库里写JAVA1 小时前
iView Table组件二次封装
vue.js·spring boot·毕业设计·layui·课程设计
三口吃掉你1 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat
Trust yourself2431 小时前
在easyui中如何设置自带的弹窗,有输入框
前端·javascript·easyui
烛阴1 小时前
Tile Pattern
前端·webgl