一、问题描述
在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>