当前位置: 首页 > news >正文

微信小程序选择图片、视频、音频

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

<template><view><!-- 多媒体展示网格(图片/视频/音频) --><view class="cu-form-group padding-top-sm"><view class="grid col-4 grid-square flex-sub"><!-- 已选多媒体项 --><view class="bg-img bg-lightGray relative" v-for="(item, index) in imgList" :key="index" :data-index="index"><!-- 图片 --><image v-if="item.type === 'image'" :src="processedImageUrl(item.url)" mode="aspectFill"@tap="ViewImage(index)" class="media-item"></image><video v-if="item.type === 'video'" class="videoView" :show-center-play-btn="false":show-fullscreen-btn="false" :show-play-btn="false" :src="processedImageUrl(item.url)"@tap="ViewImage(index)"></video><!-- 音频控制区 --><view v-if="item.type === 'audio'" class="audio-control" @tap.stop="togglePlay(item, index)"><view class="audio-info"><text class="lg cuIcon-playfill playBtn" v-if="!item.isPlaying"></text><text class="lg cuIcon-stop playBtn" v-else></text><view class="time-display"><text>{{ formatTime(item.currentTime || 0) }}</text><text>/</text><text>{{ formatTime(item.duration || 0) }}</text></view></view></view><!-- 删除按钮 --><view class="cu-tag bg-red delete-btn" @tap.stop="DelImg(index)" :data-index="index"><text class='cuIcon-close'></text></view></view><!-- 添加按钮(限制最多4个) --><view class="solids line-olives text-red add-btn" @tap="showModal" v-if="imgList.length < 9"><text class='cuIcon-add ' style="color: #06cb7c"></text></view></view></view><!-- 1. 主选择弹框(底部弹出) --><view class="cu-modal bottom-modal" :class="modalName ? 'show' : ''"><view class="cu-dialog"><view class="cu-bar bg-white justify-end"><view class="content font-bold">请选择操作</view><view class="action" @tap="hideModal"><text class="cuIcon-close text-red text-lg"></text></view></view><view class="modal-list"><view class="modal-item" @click="handleLocalImage"><text class="cuIcon-image text-greenac70 mr-2"></text>1. 从本地选择图片</view><view class="modal-item" @click="handleLocalVideo"><text class="cuIcon-video text-greenac70 mr-2"></text>2. 从本地选择视频</view><view class="modal-item" @click="handleWechatSelect"><text class="cuIcon-wechat text-greenac70 mr-2"></text>3. 从微信选择(图片/视频/录音文件)</view><view class="modal-item" @click="openRecordPopup"><text class="cuIcon-mic text-greenac70 mr-2"></text>4. 录音</view></view></view></view><!-- 2. 录音弹框(居中弹出) --><view class="cu-modal" :class="recordModal ? 'show' : ''"><view class="cu-dialog"><view class="record-popup"><view class="popup-title font-bold">录音</view><!-- 录音时长显示 --><view class="record-info"><text class="current-time">{{ formatTime(currentPlayDuration) }}</text><text class="separator">/</text><text class="max-time">最大{{ formatTime(maxRecordTime) }}</text></view><!-- 录音状态提示 --><view class="record-status" v-if="isRecording"><text class="blink-dot"></text><text class="status-text">正在录音...</text></view><view class="record-status" v-else-if="hasRecord"><text class="status-text text-greenac70">录音已完成</text></view><!-- 录音控制按钮 --><view class="record-btn-group"><button class="record-btn start-btn" @tap="startRecord" :disabled="isRecording || hasRecord"><text class="cuIcon-mic"></text>开始录音</button><button class="record-btn stop-btn" @tap="stopRecord" :disabled="!isRecording"><text class="cuIcon-stop"></text>停止录音</button><button class="record-btn play-btn" @tap="toggleRecordPlay" :disabled="!hasRecord || isRecording"><text class="cuIcon-playfill" v-if="!isPlayingRecord"></text><text class="cuIcon-pause" v-else></text>{{ isPlayingRecord ? '暂停' : '播放' }}</button><button class="record-btn delete-btn" @tap="deleteRecord" :disabled="!hasRecord || isRecording"><text class="cuIcon-trash"></text>删除</button></view><!-- 底部确认/取消按钮 --><view class="record-bottom-btn-group"><button class="bottom-btn cancel-btn" @tap="closeRecordPopup">取消</button><button class="bottom-btn confirm-btn" @tap="confirmRecord" :disabled="!hasRecord">确认添加</button></view></view></view></view></view>
</template><script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
import RequestPageBase from "@/lib/request-page-base";// 初始化录音/音频播放管理器
const recorderManager = uni.getRecorderManager();
const innerAudioContext = uni.createInnerAudioContext();
innerAudioContext.obeyMuteSwitch = false; // 不受系统静音影响
// 1. 定义统一的媒体文件类型接口(避免any类型)
interface MediaFile {url: string;type: "image" | "video" | "audio";fileName: string;size: number;isPlaying: boolean;duration: number;currentTime: number;coverUrl?: string;
}
@Component({})
export default class ChooseMessageFile extends RequestPageBase {// 2. 修复父组件传参:类型改为 MediaFile 数组,且命名更语义化@Prop({ type: Array, default: () => [] }) parentMediaList!: MediaFile[];// 组件自身维护的媒体列表imgList: MediaFile[] = [];// 状态管理(其余状态不变)modalName = false;recordModal = false;currentPlayingIndex = -1;isRecording = false;hasRecord = false;isPlayingRecord = false;totalDuration = 0;currentPlayDuration = 0;maxRecordTime = 60;recordTempPath = "";recordTimer: any = null;playTimer: any = null;// // 3. 修复监听器:监听父组件的 parentMediaList,而非自身 imgList// @Watch("parentMediaList", { immediate: true, deep: true })// onParentMediaListChange (newVal: MediaFile[]) {//   // 深拷贝父组件数据,避免直接修改父组件数据,且防止重复添加//   const newMediaList = JSON.parse(JSON.stringify(newVal));//   // 只更新差异部分(避免全量替换导致的DOM重绘)//   if (JSON.stringify(newMediaList) !== JSON.stringify(this.imgList)) {//     this.imgList = newMediaList;//   }// }// 生命周期:初始化父组件数据mounted () {// 初始化音频事件(移到这里统一绑定,避免重复绑定)this.initAudioEvents();// 初始化父组件传的初始数据this.imgList = JSON.parse(JSON.stringify(this.parentMediaList));}// 4. 统一初始化音频事件,避免重复绑定private initAudioEvents () {// 录音相关事件recorderManager.onStop((res) => {this.isRecording = false;this.hasRecord = true;this.totalDuration = Math.floor(res.duration / 1000);this.recordTempPath = res.tempFilePath;if (this.recordTimer) clearInterval(this.recordTimer);});recorderManager.onError((err) => {uni.showToast({ title: `录音失败:${err.errMsg}`, icon: "none" });this.isRecording = false;if (this.recordTimer) clearInterval(this.recordTimer);});// 音频播放相关事件(统一绑定,用条件判断区分场景)innerAudioContext.onCanplay(() => {// 只在有当前播放索引时更新时长(避免无意义执行)if (this.currentPlayingIndex !== -1) {this.imgList[this.currentPlayingIndex].duration = Math.floor(innerAudioContext.duration);}});innerAudioContext.onTimeUpdate(() => {const currentTime = Math.floor(innerAudioContext.currentTime);this.currentPlayDuration = currentTime;// 更新当前播放项的进度if (this.currentPlayingIndex !== -1) {this.imgList[this.currentPlayingIndex].currentTime = currentTime;}});innerAudioContext.onEnded(() => {// 重置播放状态if (this.currentPlayingIndex !== -1) {this.imgList[this.currentPlayingIndex].isPlaying = false;this.imgList[this.currentPlayingIndex].currentTime = 0;}this.isPlayingRecord = false;this.currentPlayingIndex = -1;this.currentPlayDuration = 0;if (this.playTimer) clearInterval(this.playTimer);});innerAudioContext.onError((err) => {uni.showToast({ title: `播放失败:${err.errMsg}`, icon: "none" });this.isPlayingRecord = false;if (this.playTimer) clearInterval(this.playTimer);});}// 5. 清理资源方法(整合重复逻辑)private cleanUpResources () {// 停止录音if (this.isRecording) recorderManager.stop();// 停止音频播放if (this.isPlayingRecord || this.currentPlayingIndex !== -1) {innerAudioContext.stop();}// 清除定时器if (this.recordTimer) clearInterval(this.recordTimer);if (this.playTimer) clearInterval(this.playTimer);// 重置状态this.currentPlayingIndex = -1;this.isPlayingRecord = false;this.currentPlayDuration = 0;}// 生命周期:页面隐藏/销毁时清理资源onHide () {this.cleanUpResources();}onUnload () {this.cleanUpResources();innerAudioContext.destroy(); // 销毁音频上下文,释放资源}beforeDestroy () {this.cleanUpResources();innerAudioContext.destroy();}// 工具方法/** 格式化时间:秒 -> mm:ss */formatTime (seconds: number): string {const mins = Math.floor(seconds / 60);const secs = Math.floor(seconds % 60);return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;}/** 处理图片URL(补全协议/添加鉴权参数) */processedImageUrl (value: string): string {if (!value) return value;if (/^http:\/\/tmp/.test(value) || /^wxfile:\/\/tmp/.test(value)) {return value;}if (/^http:\/\//.test(value)) {value = value.replace("http://", "https://");}if (String(value).includes("auth=wkxiaolvsan")) {return value;}const separator = String(value).includes("?") ? "&" : "?";return `${value}${separator}auth=wkxiaolvsan`;}// 弹框控制/** 显示主选择弹框 */showModal () {this.modalName = true;}/** 隐藏主选择弹框 */hideModal () {this.modalName = false;}/** 打开录音弹框 */openRecordPopup () {this.hideModal(); // 先关闭主弹框this.recordModal = true;}/** 关闭录音弹框(重置状态) */closeRecordPopup () {this.recordModal = false;this.isPlayingRecord = false;this.currentPlayDuration = 0;if (this.playTimer) clearInterval(this.playTimer);innerAudioContext.stop();}// 多媒体选择逻辑/** 本地选择图片 */handleLocalImage () {uni.chooseImage({count: 9 - this.imgList.length, // 限制最多选4个sourceType: ["album"],success: (res: any) => {res.tempFiles.forEach((file: any) => {this.imgList.push({url: file.path,type: "image",fileName: file.name || `img_${Date.now()}`,size: file.size,isPlaying: false,duration: 0,currentTime: 0,});});this.changeImageList()this.hideModal();uni.showToast({ title: `已选择${res.tempFiles.length}张图片`, icon: "none" });},fail: (err) => uni.showToast({ title: `选择失败:${err.errMsg}`, icon: "none" }),});}/** 本地选择视频 */handleLocalVideo () {uni.chooseVideo({sourceType: ["album"],maxDuration: 60, // 限制视频时长success: (res: any) => {this.imgList.push({url: res.tempFilePath,type: "video",fileName: res.tempFilePath.split("/").pop() || `video_${Date.now()}`,size: res.size,isPlaying: false,duration: Math.floor(res.duration),currentTime: 0,coverUrl: '', // 视频封面图});this.changeImageList()this.hideModal();uni.showToast({ title: "视频已选择", icon: "none" });},fail: (err) => uni.showToast({ title: `选择失败:${err.errMsg}`, icon: "none" }),});}/** 从微信选择(仅允许:图片/视频/录音文件) */handleWechatSelect () {const that = this;// 1. 定义文件类型和后缀的联合类型(关键:约束允许的后缀)type FileType = "image" | "video" | "audio";type AllowedExt = "jpg" | "jpeg" | "png" | "gif" | "bmp"| "mp4" | "mov" | "avi" | "mkv" | "flv"| "mp3" | "wav" | "amr" | "aac";// 2. 定义「后缀名-文件类型」映射表(带类型约束)const extTypeMap: Record<AllowedExt, FileType> = {// 图片后缀'jpg': 'image','jpeg': 'image','png': 'image','gif': 'image','bmp': 'image',// 视频后缀'mp4': 'video','mov': 'video','avi': 'video','mkv': 'video','flv': 'video',// 音频(录音)后缀'mp3': 'audio','wav': 'audio','amr': 'audio','aac': 'audio'};// 允许的后缀列表(从映射表中提取,避免重复定义)const allowedExtensions: AllowedExt[] = Object.keys(extTypeMap) as AllowedExt[];uni.chooseMessageFile({count: 9 - this.imgList.length,type: 'file',extension: allowedExtensions,success: (res) => {// 3. 二次校验 + 类型判断(带类型断言)const validFiles = res.tempFiles.filter(file => {const fileExt = file.name?.split('.').pop()?.toLowerCase() as AllowedExt | undefined;return fileExt !== undefined && allowedExtensions.includes(fileExt);});if (validFiles.length === 0) {return uni.showToast({ title: '请选择图片、视频或录音文件', icon: 'none' });}// 4. 分类处理(类型安全)validFiles.forEach(file => {// 明确断言为允许的后缀类型const fileExt = file.name?.split('.').pop()?.toLowerCase() as AllowedExt;// 此时索引访问不会报错,因为 fileExt 被约束为 AllowedExtconst fileType: FileType = extTypeMap[fileExt];that.imgList.push({url: file.path,type: fileType,fileName: file.name || `${fileType}_${Date.now()}.${fileExt}`,size: file.size,isPlaying: false,duration: 0,currentTime: 0,coverUrl: fileType === "video" ? file.path : undefined,});});this.changeImageList()that.hideModal();uni.showToast({title: `已选择${validFiles.length}个文件(图片/视频/录音)`,icon: "none"});},fail: (err) => {if (err.errMsg !== 'chooseMessageFile:fail cancel') {uni.showToast({ title: `选择失败:${err.errMsg}`, icon: "none" });}},});}changeImageList () {this.$emit('changeImageList', this.imgList)}// 预览和删除/** 预览图片 */ViewImage (index: number) {const currentItem = this.imgList[index];wx.previewMedia({current: index,sources: [{url: this.processedImageUrl(currentItem.url),type: currentItem.type // 修复预览类型,支持图片/视频}]});}/** 删除媒体项 */DelImg (index: number) {this.cleanUpResources(); // 删除时清理播放状态this.imgList.splice(index, 1);this.changeImageList();uni.showToast({ title: '已删除', icon: 'none' });}/** 切换音频播放状态 */togglePlay (item: MediaFile, index: number) {// 清理之前的播放状态this.cleanUpResources();// 记录当前播放索引this.currentPlayingIndex = index;const currentItem = this.imgList[index];// 设置音频源并播放innerAudioContext.src = currentItem.url;innerAudioContext.play();currentItem.isPlaying = true;this.isPlayingRecord = true; // 复用录音的播放状态,避免重复定义}// 录音逻辑/** 开始录音 */startRecord () {if (this.totalDuration >= this.maxRecordTime) {return uni.showToast({ title: `已达最大时长${this.maxRecordTime}秒`, icon: "none" });}// 录音配置const recordOptions = {duration: this.maxRecordTime * 1000,sampleRate: 16000,numberOfChannels: 1,encodeBitRate: 96000,format: "mp3",};recorderManager.start(recordOptions);this.isRecording = true;this.currentPlayDuration = 0;// 录音计时(每秒更新)this.recordTimer = setInterval(() => {this.currentPlayDuration += 1;// 达最大时长自动停止if (this.currentPlayDuration >= this.maxRecordTime) {this.stopRecord();}}, 1000);}/** 停止录音 */stopRecord () {recorderManager.stop();this.isRecording = false;if (this.recordTimer) clearInterval(this.recordTimer);}/** 播放/暂停录音 */// 7. 修复录音播放逻辑:复用统一事件toggleRecordPlay () {if (!this.recordTempPath) return;// 如果正在播放,暂停if (this.isPlayingRecord) {innerAudioContext.pause();this.isPlayingRecord = false;if (this.playTimer) clearInterval(this.playTimer);} else {// 清理之前的状态this.cleanUpResources();// 设置录音源并播放innerAudioContext.src = this.recordTempPath;innerAudioContext.play();this.isPlayingRecord = true;// 播放计时(避免和 onTimeUpdate 冲突,用定时器补充)this.playTimer = setInterval(() => {if (this.currentPlayDuration >= this.totalDuration) {this.cleanUpResources(); // 播放结束后清理return;}this.currentPlayDuration += 1;}, 1000);}}/** 删除录音 */deleteRecord () {// this.audioContext.src = '';this.hasRecord = false;this.totalDuration = 0;this.currentPlayDuration = 0;this.recordTempPath = '';uni.showToast({ title: '录音已删除', icon: 'none' });}/** 确认添加录音到列表 */confirmRecord () {if (!this.recordTempPath) return;this.imgList.push({url: this.recordTempPath,type: 'audio',fileName: `audio_${Date.now()}.mp3`,size: 0,isPlaying: false,duration: this.totalDuration,currentTime: 0});this.changeImageList()this.closeRecordPopup();uni.showToast({ title: '录音已添加', icon: 'none' });}
}
</script><style lang="less" scoped>
// 媒体项通用样式
.media-item {width: 100%;height: 100%;display: block;
}// 视频容器样式
.video-container {position: relative;width: 100%;height: 100%;.video-play-icon {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);color: white;font-size: 60rpx;text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);z-index: 1;}.video-player {position: absolute;top: 0;left: 0;opacity: 0;}
}// 音频控制样式
.audio-control {.audio-info {width: 100%;position: absolute;bottom: 0;margin: 0 auto;display: flex;flex-direction: column;align-items: center;justify-content: center;.playBtn {font-size: 60rpx;color: #06cb7c;}.time-display {font-size: 24rpx;color: #666;display: flex;gap: 8rpx;}}
}// 删除按钮样式
.delete-btn {position: absolute;top: 0;right: 0;z-index: 2;opacity: 0.8;transition: opacity 0.3s;&:hover {opacity: 1;}
}.videoView {
position: absolute;width: 100%;height: 100%;display: block;
}// 添加按钮样式
.add-btn {display: flex;align-items: center;justify-content: center;font-size: 60rpx;// border: 2rpx dashed #06cb7c;`
}// 录音弹框样式
.record-popup {width: 80%;max-width: 400px;padding: 30rpx;background: #fff;border-radius: 16rpx;box-sizing: border-box;
}.popup-title {font-size: 36rpx;text-align: center;margin-bottom: 30rpx;
}.record-info {text-align: center;margin: 30rpx 0;font-size: 32rpx;color: #333;.current-time {color: #06cb7c;font-weight: bold;}.separator {margin: 0 10rpx;}
}.record-status {text-align: center;margin: 20rpx 0;font-size: 28rpx;.status-text {color: #666;}.blink-dot {display: inline-block;width: 16rpx;height: 16rpx;border-radius: 50%;background-color: #ff4d4f;margin-right: 10rpx;animation: blink 1s infinite;}
}@keyframes blink {0%,100% {opacity: 1;}50% {opacity: 0.4;}
}.record-btn-group {display: flex;flex-wrap: wrap;gap: 15rpx;margin: 20rpx 0;.record-btn {flex: 1;min-width: 120rpx;padding: 15rpx 0;border-radius: 8rpx;font-size: 26rpx;display: flex;align-items: center;justify-content: center;gap: 8rpx;&.start-btn {background-color: #06cb7c;color: white;}&.stop-btn {background-color: #ff4d4f;color: white;}&.play-btn {background-color: #1677ff;color: white;}&.delete-btn {background-color: #666;color: white;}&:disabled {opacity: 0.6;cursor: not-allowed;}}
}.record-bottom-btn-group {display: flex;gap: 20rpx;margin-top: 30rpx;.bottom-btn {flex: 1;padding: 20rpx 0;font-size: 28rpx;border-radius: 8rpx;&.cancel-btn {background-color: #f5f5f5;color: #666;}&.confirm-btn {background-color: #06cb7c;color: white;}&:disabled {opacity: 0.6;background-color: #ccc;color: #999;}}
}// 模态框列表样式
.modal-list {.modal-item {padding: 25rpx 30rpx;border-bottom: 1px solid #f5f5f5;font-size: 30rpx;display: flex;align-items: center;cursor: pointer;transition: background-color 0.2s;&:last-child {border-bottom: none;}&:hover {background-color: #f9f9f9;}}
}
</style>
http://www.dtcms.com/a/384625.html

相关文章:

  • 【C++上岸】C++常见面试题目--网络篇(第二十三期)
  • mapbox进阶,使用jsts实现平角缓冲区
  • A股大盘数据-20250915分析
  • MySQL服务启动全平台指南:从Windows服务、Linux systemctl到macOS的完整攻略
  • 八、vue3后台项目系列——封装layout页面下切换组件Appmain
  • 学习React-12-useEffect
  • MFC_Button
  • [K8S学习笔记]YAML相关
  • 贪心算法在物联网能耗优化中的应用
  • 使用paddlepaddle-Gpu库时的一个小bug!
  • 从 Linux 到 Kubernetes:操作系统的演变与云原生未来
  • Java网络编程:(socket API编程:TCP协议的 socket API -- 服务器端处理请求的三个步骤)
  • 新能源汽车总装车间案例:四台S7-1200通过无线网桥同步控制16组ET 200SP的秘诀
  • k8s事件驱动运维利器 shell operator
  • GitHub Actions 部署配置
  • java后端工程师进修ing(研一版‖day45)
  • k8s核心资料基本操作
  • Redis 在电商系统中的应用:高并发场景下的架构艺术
  • RK3588:MIPI底层驱动学习——芯外拾遗第一篇:从四个模块到整个“江湖”
  • K8S里的“豌豆荚”:Pod
  • OpenStack 管理与基础操作学习笔记(一):角色、用户及项目管理实践
  • 大数据毕业设计选题推荐-基于大数据的金融数据分析与可视化系统-Spark-Hadoop-Bigdata
  • Python爬虫实战:研究Pandas,构建期货数据采集和分析系统
  • 软考中级习题与解答——第六章_计算机硬件基础(3)
  • Nvidia显卡架构解析与cuda应用生态浅析
  • AppStore 如何上架?iOS 应用发布全流程、uni-app 打包上传 ipa、App Store 审核与多工具组合实战指南
  • 贪心算法应用:卫星链路调度问题详解
  • 基于https的数据加密技术
  • 自学嵌入式第四十一天:单片机-中断
  • 二分图 系列