|
|
@@ -0,0 +1,376 @@
|
|
|
+<!-- 单词区 && 音标区:最多16位,超过换行 -->
|
|
|
+<!-- 单音节最长:swimming 多音节最长:transportation -->
|
|
|
+<template>
|
|
|
+<!-- <selectTypesVue activeSelect="3"></selectTypesVue>-->
|
|
|
+ <view class="ezy-border-body">
|
|
|
+ <view class="words-du-box">
|
|
|
+
|
|
|
+ <view class="du-body-box">
|
|
|
+ <!-- 单词区 -->
|
|
|
+ <view class="word-circle-box">{{data.name}}</view>
|
|
|
+ <!-- 音标区 -->
|
|
|
+ <view class="yb-play-box du-yb-play-box">
|
|
|
+ <yinbiaoTxtVue :yinbiao="activeWord.yinbiao"></yinbiaoTxtVue>
|
|
|
+ <!-- 音频播放 -->
|
|
|
+ <audioTwoVue :active-word="activeWord" @play-audio="handlePlay"></audioTwoVue>
|
|
|
+ </view>
|
|
|
+ <view class="pin-words-explain-box du-words-explain-box">
|
|
|
+ <view class="words-explain-item" v-if="data.jianyi&&data.jianyi.length>0"
|
|
|
+ v-for="(item,index) in data.jianyi" :key="index">
|
|
|
+ {{item}}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="mike-play-box">
|
|
|
+ <view class="mike-play-tip" v-if="isRecording">录音中...</view>
|
|
|
+ <view class="mike-play-tip" v-else>长按 读一读</view>
|
|
|
+ <!-- <view class="status">{{ recordingStatus }}</view> -->
|
|
|
+ <!-- <view class="duration" v-if="isRecording">录音时长: {{ Math.floor(duration) }}秒</view> -->
|
|
|
+ <view class="du-btn-box">
|
|
|
+ <button class="play-btn" :class="{ 'playing-btn': isPlaying}" @click="playVoice"
|
|
|
+ v-if="voicePath"></button>
|
|
|
+ <button class="mike-btn" :class="{ 'mike-az-btn': isRecording}" @touchstart="handleTouchStart"
|
|
|
+ @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" :disabled="isPlaying">
|
|
|
+ <!--{{ isRecording ? '松开结束' : '按住录音' }}
|
|
|
+ <view v-if="isPlaying" class="disabled-mask">播放中不可录音</view> -->
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- <button class="play-btn" :class="{ disabled: isRecording || !voicePath }" @click="playVoice"
|
|
|
+ :disabled="isRecording || !voicePath">
|
|
|
+ {{ isPlaying ? '播放中...' : '播放录音' }}
|
|
|
+ <view v-if="isRecording" class="disabled-mask">录音中不可播放</view>
|
|
|
+ </button> -->
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- <uni-popup ref="statusPopup" :animation="false" :is-mask-click="false"
|
|
|
+ mask-background-color="rgba(51, 137, 217, 0.65);">
|
|
|
+ <view class="ezy-image-dialog luyin">
|
|
|
+ </view>
|
|
|
+ </uni-popup> -->
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+ import selectTypesVue from './selectTypes.vue';
|
|
|
+ import audioTwoVue from './audioTwo.vue';
|
|
|
+ import yinbiaoTxtVue from "./yinbiaoTxt.vue"
|
|
|
+ import {
|
|
|
+ onLoad
|
|
|
+ } from "@dcloudio/uni-app"
|
|
|
+ import {
|
|
|
+ reactive,
|
|
|
+ ref,
|
|
|
+ onMounted,
|
|
|
+ onUnmounted
|
|
|
+ } from 'vue';
|
|
|
+
|
|
|
+ const props = defineProps({
|
|
|
+ activeWord: {
|
|
|
+ type: Object,
|
|
|
+ },
|
|
|
+ pageData: {
|
|
|
+ type: Object,
|
|
|
+ },
|
|
|
+ activeWords: {
|
|
|
+ type: Array
|
|
|
+ },
|
|
|
+ })
|
|
|
+ const emits = defineEmits(['play-audio', 'luyinSuccess'])
|
|
|
+ let tabFlag = ref(1)
|
|
|
+ const audioInfo = ref(null)
|
|
|
+ const data = reactive({
|
|
|
+ name: '',
|
|
|
+ yinbiao: '',
|
|
|
+ jianyi: []
|
|
|
+ })
|
|
|
+
|
|
|
+ // 录音相关状态
|
|
|
+ const isRecording = ref(false)
|
|
|
+ const isPlaying = ref(false)
|
|
|
+ const voicePath = ref('')
|
|
|
+ const duration = ref(0)
|
|
|
+ const timer = ref(null)
|
|
|
+ const recordingStatus = ref('准备就绪')
|
|
|
+ const startTime = ref(0)
|
|
|
+ const longPressTimer = ref(null) // 长按计时器
|
|
|
+ const isRecorderReady = ref(true) // 新增:录音器就绪状态
|
|
|
+
|
|
|
+ // 获取录音和音频播放管理器
|
|
|
+ const recorderManager = uni.getRecorderManager()
|
|
|
+ const innerAudioContext = uni.createInnerAudioContext()
|
|
|
+
|
|
|
+ // 初始化录音器
|
|
|
+ const initRecorder = () => {
|
|
|
+ recorderManager.onStart(() => {
|
|
|
+ console.log('recorder start')
|
|
|
+ isRecording.value = true
|
|
|
+ isRecorderReady.value = true // 录音结束后标记为就绪
|
|
|
+ // recordingStatus.value = '录音中...'
|
|
|
+ // startTime.value = Date.now()
|
|
|
+ // startTimer()
|
|
|
+ })
|
|
|
+ recorderManager.onStop((res) => {
|
|
|
+ // const recordTime = (Date.now() - startTime.value) / 1000 // 计算实际录音时间
|
|
|
+ isRecording.value = false
|
|
|
+ isRecorderReady.value = true // 录音结束后标记为就绪
|
|
|
+ // if (recordTime < 3) {
|
|
|
+ // uni.showToast({
|
|
|
+ // title: '录音时间太短,需3秒以上',
|
|
|
+ // icon: 'none'
|
|
|
+ // })
|
|
|
+ // return
|
|
|
+ // }
|
|
|
+
|
|
|
+ if (res.tempFilePath) {
|
|
|
+ voicePath.value = res.tempFilePath
|
|
|
+ uni.showToast({
|
|
|
+ title: '录音成功',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+
|
|
|
+ emits('luyinSuccess')
|
|
|
+ //showStatusPopup('', '录音成功!', '/static/mp3/newYingyu/right-tip.mp3')
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '录音失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ recorderManager.onError((res) => {
|
|
|
+ console.error('recorder error', res)
|
|
|
+ //stopTimer()
|
|
|
+ isRecording.value = false
|
|
|
+ isRecorderReady.value = true // 出错时也标记为就绪
|
|
|
+ setTimeout(() => {
|
|
|
+ initRecorder()
|
|
|
+ }, 300)
|
|
|
+ //recordingStatus.value = `录音错误: ${res.errMsg}`
|
|
|
+ // uni.showToast({
|
|
|
+ // title: `录音出错: ${res.errMsg}`,
|
|
|
+ // icon: 'none'
|
|
|
+ // })
|
|
|
+ })
|
|
|
+
|
|
|
+ innerAudioContext.onPlay(() => {
|
|
|
+ isPlaying.value = true
|
|
|
+ //recordingStatus.value = '播放中...'
|
|
|
+ })
|
|
|
+
|
|
|
+ innerAudioContext.onEnded(() => {
|
|
|
+ isPlaying.value = false
|
|
|
+ //recordingStatus.value = '播放完成'
|
|
|
+ })
|
|
|
+
|
|
|
+ innerAudioContext.onError((res) => {
|
|
|
+ isPlaying.value = false
|
|
|
+ console.error('play error', res)
|
|
|
+ uni.showToast({
|
|
|
+ title: '播放失败',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ const checkMicrophonePermission = async () => {
|
|
|
+ try {
|
|
|
+ // 1. 检查平台
|
|
|
+ const {
|
|
|
+ platform
|
|
|
+ } = uni.getSystemInfoSync()
|
|
|
+
|
|
|
+ // 2. Android权限处理
|
|
|
+ if (platform === 'android') {
|
|
|
+ console.log('1111');
|
|
|
+ const status = await new Promise(resolve => {
|
|
|
+ plus.android.requestPermissions(
|
|
|
+ ['android.permission.RECORD_AUDIO'],
|
|
|
+ (result) => {
|
|
|
+ console.log('22222', result);
|
|
|
+ if (result.deniedAlways?.length) {
|
|
|
+ resolve(false) // 永久拒绝
|
|
|
+ } else if (result.denied?.length) {
|
|
|
+ resolve(false) // 拒绝
|
|
|
+ } else {
|
|
|
+ resolve(true) // 已授权
|
|
|
+ }
|
|
|
+ },
|
|
|
+ () => resolve(false) // 出错
|
|
|
+ )
|
|
|
+ })
|
|
|
+ console.log('status', status);
|
|
|
+ if (!status) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '需要麦克风权限',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 3. iOS权限处理
|
|
|
+ else if (platform === 'ios') {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ } catch (error) {
|
|
|
+ console.error('权限检查错误:', error)
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function handlePlay(opt) {
|
|
|
+ emits('play-audio', opt)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理触摸开始(移动端)
|
|
|
+ const handleTouchStart = async (e) => {
|
|
|
+ e.preventDefault()
|
|
|
+ if (isPlaying.value) return
|
|
|
+
|
|
|
+ // 先检查权限
|
|
|
+ const hasPermission = await checkMicrophonePermission()
|
|
|
+ if (!hasPermission) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 设置长按计时器,500ms后才开始录音
|
|
|
+ longPressTimer.value = setTimeout(() => {
|
|
|
+ startRecording()
|
|
|
+ }, 400)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理触摸结束(移动端)
|
|
|
+ const handleTouchEnd = (e) => {
|
|
|
+ e.preventDefault()
|
|
|
+ clearTimeout(longPressTimer.value)
|
|
|
+ if (isRecording.value) {
|
|
|
+ endRecording()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理触摸取消(移动端,如被系统中断)
|
|
|
+ const handleTouchCancel = (e) => {
|
|
|
+ handleTouchEnd(e) // 与touchend同样处理
|
|
|
+ }
|
|
|
+ // 开始录音
|
|
|
+ const startRecording = async () => {
|
|
|
+ if (isRecording.value || !isRecorderReady.value) return
|
|
|
+
|
|
|
+ const hasPermission = await checkMicrophonePermission()
|
|
|
+ if (!hasPermission) {
|
|
|
+ // 权限被拒绝后重置状态
|
|
|
+ isRecording.value = false
|
|
|
+ isRecorderReady.value = true
|
|
|
+ return
|
|
|
+ }
|
|
|
+ console.log('开始录音')
|
|
|
+ //recordingStatus.value = '准备录音...'
|
|
|
+ isRecording.value = true // 提前设置状态,避免延迟
|
|
|
+ isRecorderReady.value = false
|
|
|
+ // startTime.value = Date.now()
|
|
|
+ // startTimer()
|
|
|
+
|
|
|
+ const options = {
|
|
|
+ duration: 60000,
|
|
|
+ sampleRate: 44100,
|
|
|
+ numberOfChannels: 1,
|
|
|
+ encodeBitRate: 192000,
|
|
|
+ format: 'mp3',
|
|
|
+ frameSize: 50
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await recorderManager.start(options)
|
|
|
+ } catch (e) {
|
|
|
+ console.error('启动录音失败:', e)
|
|
|
+ isRecording.value = false
|
|
|
+ isRecorderReady.value = true
|
|
|
+ uni.showToast({
|
|
|
+ title: '启动录音失败,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 结束录音
|
|
|
+ const endRecording = () => {
|
|
|
+ if (!isRecording.value) return
|
|
|
+
|
|
|
+ console.log('停止录音')
|
|
|
+ try {
|
|
|
+ recorderManager.stop()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('停止录音失败:', e)
|
|
|
+ isRecording.value = false
|
|
|
+ isRecorderReady.value = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const getRecordingDuration = () => {
|
|
|
+ if (!isRecording.value) return 0
|
|
|
+ return Math.floor((Date.now() - startTime.value) / 1000)
|
|
|
+ }
|
|
|
+ // 播放录音
|
|
|
+ const playVoice = () => {
|
|
|
+ if (isRecording.value || !voicePath.value) return
|
|
|
+
|
|
|
+ console.log('播放录音:', voicePath.value)
|
|
|
+ innerAudioContext.src = voicePath.value
|
|
|
+ innerAudioContext.play()
|
|
|
+ }
|
|
|
+
|
|
|
+ // // 计时器相关
|
|
|
+ // const startTimer = () => {
|
|
|
+ // duration.value = 0
|
|
|
+ // const start = Date.now()
|
|
|
+ // timer.value = setInterval(() => {
|
|
|
+ // // 计算实际经过的时间(毫秒),然后转换为秒
|
|
|
+ // const elapsed = Math.floor((Date.now() - start) / 1000)
|
|
|
+ // duration.value = elapsed
|
|
|
+ // }, 200) // 缩短检查间隔,但计算基于实际时间差
|
|
|
+ // }
|
|
|
+
|
|
|
+ // const stopTimer = () => {
|
|
|
+ // if (timer.value) {
|
|
|
+ // clearInterval(timer.value)
|
|
|
+ // timer.value = null
|
|
|
+ // }
|
|
|
+ // duration.value = 0
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 初始化单词数据
|
|
|
+ const initItem = () => {
|
|
|
+ data.name = props.activeWord.name;
|
|
|
+ data.yinbiao = props.activeWord.yinbiao;
|
|
|
+ data.jianyi = props.activeWord.jianyi;
|
|
|
+ console.log('data', data);
|
|
|
+ }
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ initItem()
|
|
|
+ initRecorder()
|
|
|
+
|
|
|
+ })
|
|
|
+
|
|
|
+ onUnmounted(() => {
|
|
|
+ //stopTimer()
|
|
|
+ clearTimeout(longPressTimer.value)
|
|
|
+
|
|
|
+ try {
|
|
|
+ recorderManager.stop()
|
|
|
+ innerAudioContext.stop()
|
|
|
+ innerAudioContext.destroy()
|
|
|
+ statusAudio.destroy()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('清理资源时出错:', e)
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ })
|
|
|
+</script>
|