2 Commits a66a92872c ... 75250a02c7

Autor SHA1 Mensaje Fecha
  wangxy 75250a02c7 Merge remote-tracking branch 'origin/2025鹅状元数学' into 2025鹅状元数学 hace 22 horas
  wangxy 668765bf4a 超级单词选择修改 hace 22 horas
Se han modificado 25 ficheros con 2962 adiciones y 14 borrados
  1. 14 14
      pages.json
  2. 59 0
      pages/chaojidanci/newEnglish/components/audioFour.vue
  3. 216 0
      pages/chaojidanci/newEnglish/components/audioManager.js
  4. 60 0
      pages/chaojidanci/newEnglish/components/audioOne.vue
  5. 64 0
      pages/chaojidanci/newEnglish/components/audioRightWrong.vue
  6. 58 0
      pages/chaojidanci/newEnglish/components/audioThree.vue
  7. 59 0
      pages/chaojidanci/newEnglish/components/audioTwo.vue
  8. 213 0
      pages/chaojidanci/newEnglish/components/beiPage.vue
  9. 36 0
      pages/chaojidanci/newEnglish/components/btnTxt.vue
  10. 106 0
      pages/chaojidanci/newEnglish/components/learnContent.vue
  11. 139 0
      pages/chaojidanci/newEnglish/components/mainCard.vue
  12. 176 0
      pages/chaojidanci/newEnglish/components/pinPage.vue
  13. 376 0
      pages/chaojidanci/newEnglish/components/readContent.vue
  14. 114 0
      pages/chaojidanci/newEnglish/components/selectPage.vue
  15. 38 0
      pages/chaojidanci/newEnglish/components/selectTypes.vue
  16. 16 0
      pages/chaojidanci/newEnglish/components/selectWords.vue
  17. 159 0
      pages/chaojidanci/newEnglish/components/useAudio.js
  18. 85 0
      pages/chaojidanci/newEnglish/components/useAudioRightWrong.js
  19. 82 0
      pages/chaojidanci/newEnglish/components/useYinbiao.js
  20. 120 0
      pages/chaojidanci/newEnglish/components/xiangjie.vue
  21. 169 0
      pages/chaojidanci/newEnglish/components/xuePage.vue
  22. 22 0
      pages/chaojidanci/newEnglish/components/yinbiaoTxt.vue
  23. 265 0
      pages/chaojidanci/newEnglish/index.vue
  24. 258 0
      pages/chaojidanci/wordList/wordList.vue
  25. 58 0
      utils/common.js

+ 14 - 14
pages.json

@@ -174,21 +174,21 @@
 			{
 				"navigationStyle": "custom"
 			}
+		},
+		{
+			"path" : "pages/chaojidanci/wordList/wordList",
+			"style" :
+			{
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path" : "pages/chaojidanci/newEnglish/index",
+			"style" :
+			{
+				"navigationStyle": "custom"
+			}
 		}
-//		{
-//			"path" : "pages/chaojidanci/wordList/wordList",
-//			"style" :
-//			{
-//				"navigationStyle": "custom"
-//			}
-//		},
-//		{
-//			"path" : "pages/chaojidanci/newEnglish/index",
-//			"style" :
-//			{
-//				"navigationStyle": "custom"
-//			}
-//		}
 	],
 	"tabBar": {
 		"custom": true,

+ 59 - 0
pages/chaojidanci/newEnglish/components/audioFour.vue

@@ -0,0 +1,59 @@
+<template>
+	<view @click="handlePlay" class="yj-block-item">
+		<view class="item-top">{{YItem.cigen}}</view>
+		<view class="item-bottom">{{YItem.yinbiao}}</view>
+	</view>
+</template>
+
+<script setup>
+	// yinjie
+	import {
+		reactive,
+		computed,
+		onUnmounted
+	} from 'vue';
+	import {
+		onLoad
+	} from "@dcloudio/uni-app"
+
+	const props = defineProps({
+		YItem: {
+			type: Object,
+		},
+	})
+
+	const emits = defineEmits(['play-audio'])
+
+	const data = reactive({
+		code: null
+	})
+
+	onLoad(() => {
+		uni.$on('danci-audio-ended', (code) => {
+			data.isPlaying = false;
+		})
+		uni.$on('danci-audio-play', (code) => {
+			if (data.code == code) {
+				data.isPlaying = true;
+			} else {
+				data.isPlaying = false;
+			}
+		})
+	})
+
+	onUnmounted(() => {
+		uni.$off('danci-audio-ended')
+		uni.$off('danci-audio-play')
+	})
+
+	function handlePlay() {
+		data.code = new Date().getTime()
+		emits('play-audio', {
+			url: props.YItem.yinpin,
+			code: data.code
+		})
+	}
+</script>
+
+<style>
+</style>

+ 216 - 0
pages/chaojidanci/newEnglish/components/audioManager.js

@@ -0,0 +1,216 @@
+// utils/audioManager.js
+export default {
+  player: null,
+    isApp: uni.getSystemInfoSync().platform === 'ios' || uni.getSystemInfoSync().platform === 'android',
+  initPlayer() {
+    if (!this.player) {
+      this.player = uni.createInnerAudioContext()
+    //  this.player.obeyMuteSwitch = false // iOS静音模式设置
+    }
+    return this.player
+  },
+
+  // 从单词数据中提取所有音频信息
+  extractAudioInfo(wordData) {
+    const audioInfo = {
+      main: null,
+      syllables: [],
+      frequencies: []
+    }
+    
+    // 主音频
+    if (wordData.yinpin) {
+      audioInfo.main = {
+        id: `word_${wordData.id}_main`,
+        url: wordData.yinpin,
+        type: 'main',
+        text: wordData.name
+      }
+    }
+    
+    // 音节音频
+    if (wordData.yinjie && Array.isArray(wordData.yinjie)) {
+      audioInfo.syllables = wordData.yinjie.map((item, index) => ({
+        id: `word_${wordData.id}_syllable_${index}`,
+        url: item.yinpin,
+        type: 'syllable',
+        text: item.cigen,
+        index
+      })).filter(item => item.url)
+    }
+    
+    // 频度音频
+    if (wordData.pindu && Array.isArray(wordData.pindu)) {
+      audioInfo.frequencies = wordData.pindu.map((item, index) => ({
+        id: `word_${wordData.id}_frequency_${index}`,
+        url: item.yinpin,
+        type: 'frequency',
+        text: item.cigen,
+        index
+      })).filter(item => item.url)
+    }
+    
+    return audioInfo
+  },
+
+  // 缓存单词的所有音频
+  async cacheWordAudios(wordData) {
+    try {
+      // 1. 提取音频信息
+      const { main, syllables, frequencies } = this.extractAudioInfo(wordData)
+      
+      // 2. 准备所有需要缓存的音频
+      const allAudios = []
+      if (main) allAudios.push(main)
+      allAudios.push(...syllables)
+      allAudios.push(...frequencies)
+      
+
+      
+	  // App环境才进行真实下载
+	  if (this.isApp) {
+	   await this.downloadAndSaveAudios(allAudios)
+	  }
+	  
+	  
+      // 4. 保存音频信息到统一存储
+      this.saveAudioInfoToStorage(wordData.id, {
+        main,
+        syllables,
+        frequencies
+      })
+      
+      return true
+    } catch (error) {
+      console.error('音频缓存失败:', error)
+      return false
+    }
+  },
+
+  // 保存音频信息到storage(结构化存储)
+  saveAudioInfoToStorage(wordId, audioInfo) {
+    const cacheKey = `word_audio_${wordId}`
+    const cachedData = {
+      timestamp: Date.now(),
+      ...audioInfo
+    }
+    uni.setStorageSync(cacheKey, cachedData)
+  },
+
+  // 从storage获取音频信息
+  getAudioInfoFromStorage(wordId) {
+    const cacheKey = `word_audio_${wordId}`
+    return uni.getStorageSync(cacheKey) || null
+  },
+
+  // 下载并保存音频(仅App环境)
+  async downloadAndSaveAudios(audioList) {
+	  
+    const downloadPromises = audioList.map(audio => {
+      return new Promise((resolve, reject) => {
+        // 先检查是否已缓存
+        const cachedPath = uni.getStorageSync(`audio_file_${audio.id}`)
+        if (cachedPath) return resolve()
+        console.log('audio.url',audio.url);
+        uni.downloadFile({
+          url: audio.url,
+          success: (res) => {
+            if (res.statusCode === 200) {
+              uni.setStorageSync(`audio_file_${audio.id}`, res.tempFilePath)
+              resolve()
+            } else {
+              reject(new Error(`下载失败,状态码: ${res.statusCode}`))
+            }
+          },
+          fail: reject
+        })
+      }).catch(e => {
+        console.warn(`音频 ${audio.id} 预加载失败:`, e)
+        // 即使下载失败也继续其他下载
+      })
+    })
+    
+    await Promise.all(downloadPromises)
+  },
+
+  // 缓存单个音频
+  async cacheSingleAudio(id, url) {
+    return new Promise((resolve, reject) => {
+      uni.downloadFile({
+        url,
+        success: (res) => {
+          if (res.statusCode === 200) {
+            // 存储音频文件路径
+            uni.setStorageSync(`audio_file_${id}`, res.tempFilePath)
+            resolve()
+          } else {
+            reject(new Error(`下载失败,状态码: ${res.statusCode}`))
+          }
+        },
+        fail: reject
+      })
+    })
+  },
+
+  // 播放音频(优先本地,失败后回退网络)
+  playAudio(audioInfo) {
+    if (!audioInfo?.url) return
+    
+    const player = this.initPlayer()
+    player.stop()
+    console.log('this.isApp',this.isApp);
+    // 在App环境尝试使用本地缓存
+    if (this.isApp) {
+      const cachedPath = uni.getStorageSync(`audio_file_${audioInfo.id}`)
+      if (cachedPath) {
+        player.src = cachedPath
+        player.play()
+        return player
+      }
+    }
+    
+    // 非App环境或缓存失败时使用网络
+    console.warn(`使用网络音频: ${audioInfo.id}`)
+	console.log('player',player);
+	console.log('audioInfo.url',audioInfo.url);
+    player.src = audioInfo.url
+    player.play()
+    
+    // App环境下后台继续尝试缓存
+    if (this.isApp) {
+      this.downloadAndSaveAudios([audioInfo]).catch(console.error)
+    }
+    
+    // player.onError((err) => {
+    //   console.error('播放失败:', err)
+    //   uni.showToast({ title: '播放失败', icon: 'none' })
+    // })
+    
+    return player
+  },
+
+  // 清理指定单词的缓存
+  clearWordCache(wordId) {
+    // 1. 清理音频信息
+    uni.removeStorageSync(`word_audio_${wordId}`)
+    
+    // 2. 清理所有相关音频文件
+    const prefix = `word_${wordId}_`
+    const keys = uni.getStorageInfoSync().keys
+    
+    keys.filter(key => key.startsWith('audio_file_') && key.includes(prefix))
+      .forEach(key => uni.removeStorageSync(key))
+  },
+
+  // 清理所有缓存
+  clearAllCache() {
+    // 1. 清理所有音频信息
+    const keys = uni.getStorageInfoSync().keys
+    keys.filter(key => key.startsWith('word_audio_'))
+      .forEach(key => uni.removeStorageSync(key))
+    
+    // 2. 清理所有音频文件
+    keys.filter(key => key.startsWith('audio_file_'))
+      .forEach(key => uni.removeStorageSync(key))
+  }
+}

+ 60 - 0
pages/chaojidanci/newEnglish/components/audioOne.vue

@@ -0,0 +1,60 @@
+<template>
+	<!-- 待播放 -->
+	<view class="audio-play-btn" v-if="!data.isPlaying" @click="handlePlay"></view>
+	<!-- 播放中 -->
+	<view class="audio-playing-btn" v-else></view>
+</template>
+
+<script setup>
+	import {
+		reactive,
+		computed,
+		onUnmounted
+	} from 'vue';
+	import {
+		onLoad
+	} from "@dcloudio/uni-app"
+
+	const props = defineProps({
+		activeWord: {
+			type: Object,
+		},
+	})
+
+	const emits = defineEmits(['play-audio'])
+
+	const data = reactive({
+		isPlaying: false,
+		code: null
+	})
+
+	onLoad(() => {
+		uni.$on('danci-audio-ended', (code) => {
+			data.isPlaying = false;
+		})
+		uni.$on('danci-audio-play', (code) => {
+			if (data.code == code) {
+				data.isPlaying = true;
+			} else {
+				data.isPlaying = false;
+			}
+		})
+	})
+
+	onUnmounted(() => {
+		uni.$off('danci-audio-ended')
+		uni.$off('danci-audio-play')
+	})
+
+	function handlePlay() {
+		console.log('播放')
+		data.code = new Date().getTime()
+		emits('play-audio', {
+			url: props.activeWord.yinpin,
+			code: data.code
+		})
+	}
+</script>
+
+<style>
+</style>

+ 64 - 0
pages/chaojidanci/newEnglish/components/audioRightWrong.vue

@@ -0,0 +1,64 @@
+<template>
+  <uni-popup ref="popupRef" :animation="false" :is-mask-click="false"
+             mask-background-color="rgba(51, 137, 217, 0.65);">
+    <view class="ezy-image-dialog" :class="data.result">
+    </view>
+  </uni-popup>
+</template>
+
+<script setup>
+	import {
+		onLoad
+	} from "@dcloudio/uni-app"
+	import {
+		ref,
+		reactive,
+    onUnmounted
+	} from "vue"
+	import {
+		resultAudioPlayer,
+		resultImageList
+	} from "./useAudioRightWrong.js"
+
+  const popupRef = ref(null)
+
+	const data = reactive({
+		result: '',
+		image: '',
+		showImage: false,
+	})
+
+	onLoad(() => {
+		pageInit()
+	})
+
+  onUnmounted(() =>{
+    uni.$off('result-audio-play')
+    uni.$off('result-audio-ended')
+  })
+
+	function pageInit() {
+		uni.$on('result-audio-play', (code) => {
+			const {
+				codeT
+			} = code;
+			data.result = codeT;
+			data.image = resultImageList[code]
+			data.showImage = true;
+			popupRef.value.open();
+		})
+		uni.$on('result-audio-ended', (code,) => {
+			const {
+				codeT
+			} = code;
+      data.result = codeT;
+			data.image = resultImageList[code];
+			data.showImage = false;
+			popupRef.value.close();
+    })
+	}
+</script>
+
+<style scoped>
+
+</style>

+ 58 - 0
pages/chaojidanci/newEnglish/components/audioThree.vue

@@ -0,0 +1,58 @@
+<template>
+	<text @click="handlePlay">
+		{{YItem.cigen}}
+	</text>
+</template>
+
+<script setup>
+	// pindu
+	import {
+		reactive,
+		computed,
+		onUnmounted
+	} from 'vue';
+	import {
+		onLoad
+	} from "@dcloudio/uni-app"
+
+	const props = defineProps({
+		YItem: {
+			type: Object,
+		},
+	})
+
+	const emits = defineEmits(['play-audio'])
+
+	const data = reactive({
+		code: null
+	})
+
+	onLoad(() => {
+		uni.$on('danci-audio-ended', (code) => {
+			data.isPlaying = false;
+		})
+		uni.$on('danci-audio-play', (code) => {
+			if (data.code == code) {
+				data.isPlaying = true;
+			} else {
+				data.isPlaying = false;
+			}
+		})
+	})
+
+	onUnmounted(() => {
+		uni.$off('danci-audio-ended')
+		uni.$off('danci-audio-play')
+	})
+
+	function handlePlay() {
+		data.code = new Date().getTime()
+		emits('play-audio', {
+			url: props.YItem.yinpin,
+			code: data.code
+		})
+	}
+</script>
+
+<style>
+</style>

+ 59 - 0
pages/chaojidanci/newEnglish/components/audioTwo.vue

@@ -0,0 +1,59 @@
+<template>
+	<!-- 待播放 -->
+	<icon class="yb-play-btn" v-if="!data.isPlaying" @click="handlePlay"></icon>
+	<!-- 播放中 -->
+	<icon class="yb-playing-btn" v-else></icon>
+</template>
+
+<script setup>
+	import {
+		reactive,
+		computed,
+		onUnmounted
+	} from 'vue';
+	import {
+		onLoad
+	} from "@dcloudio/uni-app"
+
+	const props = defineProps({
+		activeWord: {
+			type: Object,
+		},
+	})
+
+	const emits = defineEmits(['play-audio'])
+
+	const data = reactive({
+		isPlaying: false,
+		code: null
+	})
+
+	onLoad(() => {
+		uni.$on('danci-audio-ended', (code) => {
+			data.isPlaying = false;
+		})
+		uni.$on('danci-audio-play', (code) => {
+			if (data.code == code) {
+				data.isPlaying = true;
+			} else {
+				data.isPlaying = false;
+			}
+		})
+	})
+
+	onUnmounted(() => {
+		uni.$off('danci-audio-ended')
+		uni.$off('danci-audio-play')
+	})
+
+	function handlePlay() {
+		data.code = new Date().getTime()
+		emits('play-audio', {
+			url: props.activeWord.yinpin,
+			code: data.code
+		})
+	}
+</script>
+
+<style>
+</style>

+ 213 - 0
pages/chaojidanci/newEnglish/components/beiPage.vue

@@ -0,0 +1,213 @@
+<!-- 单词区 && 音标区:最多15位,超过隐藏-->
+<!-- 单音节最长:swimming 多音节最长:transportation -->
+<template>
+	<!-- 显示区 -->
+	<!--		<selectTypesVue activeSelect="5"></selectTypesVue>-->
+	<view class="ezy-border-body">
+		<view class="words-bei-box">
+			<!-- 输入区 -->
+			<input class="words-answer-box" placeholder="请输入答案" v-model.trim="data.answer" readonly
+				:class="{'words-answer-right-box': data.result&&data.result!=null, 'words-answer-error-box': !data.result&&data.result!=null}" />
+			<!-- 清空按钮 -->
+			<view class="clean-btn" @click="handleReset('all')" v-if="data.answer.length"></view>
+			<view class="bei-body-box">
+				<!-- 解释区 -->
+				<view class="pin-words-explain-box">
+					<view class="words-explain-item" v-for="item in activeWord.jianyi" :key="item">{{item}}</view>
+				</view>
+				<!-- 播放和待播 -->
+				<audioOneVue :active-word="activeWord" @play-audio="handlePlay"></audioOneVue>
+			</view>
+			<!-- 浮层输入区 -->
+			<view class="words-keyboard-box">
+				<view class="keyboard-row">
+					<template v-if="!isDaxie">
+						<btnTxtVue @text-select="handleSelect('a')">a</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('b')">b</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('c')">c</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('d')">d</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('e')">e</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('f')">f</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('g')">g</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('h')">h</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('i')">i</btnTxtVue>
+					</template>
+					<template v-else>
+						<btnTxtVue @text-select="handleSelect('A')">A</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('B')">B</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('C')">C</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('D')">D</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('E')">E</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('F')">F</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('G')">G</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('H')">H</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('I')">I</btnTxtVue>
+					</template>
+
+				</view>
+				<view class="keyboard-row">
+					<template v-if="!isDaxie">
+						<btnTxtVue @text-select="handleSelect('j')">j</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('k')">k</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('l')">l</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('m')">m</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('n')">n</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('o')">o</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('p')">p</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('q')">q</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('r')">r</btnTxtVue>
+					</template>
+					<template v-else>
+						<btnTxtVue @text-select="handleSelect('J')">J</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('K')">K</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('L')">L</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('M')">M</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('N')">N</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('O')">O</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('P')">P</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('Q')">Q</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('R')">R</btnTxtVue>
+					</template>
+				</view>
+				<view class="keyboard-row">
+					<template v-if="!isDaxie">
+						<btnTxtVue @text-select="handleSelect('s')">s</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('t')">t</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('u')">u</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('v')">v</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('w')">w</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('x')">x</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('y')">y</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('z')">z</btnTxtVue>
+						<btnTxtVue @text-select="handleReset" class="del-btn"></btnTxtVue>
+					</template>
+					<template v-else>
+						<btnTxtVue @text-select="handleSelect('S')">S</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('T')">T</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('U')">U</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('V')">V</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('W')">W</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('X')">X</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('Y')">Y</btnTxtVue>
+						<btnTxtVue @text-select="handleSelect('Z')">Z</btnTxtVue>
+						<btnTxtVue @text-select="handleReset" class="del-btn"></btnTxtVue>
+					</template>
+				</view>
+				<view class="bei-confirm-btn-box">
+					<!-- active -->
+					<view class="big-btn" :class="{active: isDaxie}" @click="handleChangeDaxie">大写</view>
+					<view class="bei-confirm-btn" @click="checkIsRight">确定</view>
+				</view>
+
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import selectWordsVue from './selectWords.vue';
+	import selectTypesVue from './selectTypes.vue';
+	import btnTxtVue from './btnTxt.vue';
+	import audioOneVue from './audioOne.vue';
+	import {
+		reactive,
+		nextTick,
+		ref
+	} from 'vue';
+	import * as httpApi from "@/api/chaojidanci.js"
+	import cacheManager from '@/utils/cacheManager';
+	import {
+		resultAudioPlayer
+	} from "./useAudioRightWrong"
+
+	const resultAudioPlayerD = new resultAudioPlayer();
+
+	const isDaxie = ref(false)
+
+	const emits = defineEmits(['play-audio'])
+
+	const props = defineProps({
+		activeWord: { // 单词数据
+			type: Object,
+		},
+		activeWords: {
+			type: Array
+		},
+		pageData: {
+			type: Object
+		}
+	})
+
+	const data = reactive({
+		answer: '',
+		result: null, // 正确性
+		isPlaying: false,
+		code: null
+	})
+
+	function handleChangeDaxie() {
+		isDaxie.value = !isDaxie.value;
+	}
+
+	function handlePlay(opt) {
+		emits('play-audio', opt)
+	}
+
+	// 选择单词
+
+	function checkIsRight() {
+		let ans1 = props.activeWord.name;
+		if (data.answer == ans1) {
+			data.result = true;
+			resultAudioPlayerD.play('right', 'bei')
+			// noticeBackDb()
+		} else {
+			data.result = false;
+			resultAudioPlayerD.play('wrong', 'bei')
+		}
+		noticeBackComplete()
+	}
+
+	function noticeBackComplete() {
+		if (props.pageData.danyuanId == 0) {
+			// 已掌握不需要调用接口通知完成
+			return;
+		}
+		httpApi.getWordWancheng({
+			danyuanId: props.pageData.danyuanId,
+			wordId: props.activeWord.id
+		}).then((res) => {
+			const {
+				wanchengFlag
+			} = res.data;
+			if (wanchengFlag == 1) {
+				// 更新当前岛小节完成状态
+				cacheManager.updateUnitStatus('zhangInfo', props.pageData.danyuanId)
+			}
+		})
+	}
+
+	function handleReset(code) {
+		if (code == 'all') {
+			// 全部清空
+			data.answer = '';
+		} else {
+			// 单个清空
+			data.answer = data.answer ? data.answer.slice(0, -1) : '';
+		}
+		// 重置错误状态
+		if (!data.answer.length) {
+			nextTick(() => {
+				data.result = null;
+			})
+		}
+	}
+
+	function handleSelect(word) {
+		data.answer += word;
+		isDaxie.value = false;
+	}
+</script>
+
+<style>
+</style>

+ 36 - 0
pages/chaojidanci/newEnglish/components/btnTxt.vue

@@ -0,0 +1,36 @@
+<template>
+	<text class="keyboard-button" 
+		:class="{active: btnStatus}" 
+		@touchstart="handleTouchStart"
+		@touchend="handleTouchEnd">
+		<slot></slot>
+	</text>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from "vue"
+
+	// 按下状态
+	const btnStatus = ref(false)
+	
+	const emits = defineEmits(['text-select', 'touch-start','touch-end'])
+
+	// 按下
+	function handleTouchStart() {
+		btnStatus.value = true // 按下变红色
+		emits('touch-start')
+	}
+	// 松开
+	function handleTouchEnd() {
+		btnStatus.value = false // 松开恢复蓝色
+		emits('text-select')
+	}
+</script>
+
+<style lang="scss" scoped>
+	.active {
+		color: red;
+	}
+</style>

+ 106 - 0
pages/chaojidanci/newEnglish/components/learnContent.vue

@@ -0,0 +1,106 @@
+<template>
+	<view>
+		<!-- 单词区 -->
+		<selectWordsVue :active-words="activeWords" :activeWord="activeWord"></selectWordsVue>
+		<selectTypesVue activeSelect="1"></selectTypesVue>
+		<view>{{activeWord.name}}</view>
+		<view>{{activeWord.yinbiao}}</view>
+		<view @click="playMainAudio">{{activeWord.yinpin}}</view>
+		<view>{{activeWord.jianyi}}</view>
+		<view @click="goXiangjie">详解 ></view>
+		<view v-for="(item,index) in activeWord.yinjie" :key="index">
+			<view>{{item.cigen}}</view>
+			<view>{{item.yinbiao}}</view>
+			<view>{{item.yinpin}}</view>
+		</view>
+		<view>自然拼读</view>
+		<view>音节拆分</view>
+		<view v-if="tabFlag ==1">
+			<view>词根助记</view>
+			<view>
+				{{activeWord.cigen}}
+			</view>
+		</view>
+
+		<view @click="clearFun">
+			asdfasdfasfasdfdasdfasdfasfadsfasdfadsfasdfasd
+		</view>
+
+	</view>
+</template>
+
+<script setup>
+	import selectWordsVue from './selectWords.vue';
+	import selectTypesVue from './selectTypes.vue';
+	import audioManager from './audioManager'
+	import {
+		reactive,
+		ref,
+		onMounted
+	} from 'vue';
+	const props = defineProps({
+		activeWord: {
+			type: Object,
+		},
+		pageData: {
+			type: Object,
+		},
+		activeWords: {
+			type: Array
+		},
+	})
+	let tabFlag = ref(1)
+	// setTimeout(() => {
+	// 	console.log('activeWord',props.activeWord)
+	// })
+	const goXiangjie = () => {
+		uni.redirectTo({
+			url: '/pages/chaojidanci/newEnglish/components/xiangjie?jieId='+props.pageData.jieId+'&wordId='+props.pageData.activeId
+		})
+	}
+	const audioInfo = ref(null)
+
+	// 初始化并缓存音频
+	const initAudio = async () => {
+		// 1. 从缓存获取已有信息
+		const cachedInfo = audioManager.getAudioInfoFromStorage(props.activeWord.id)
+
+		if (cachedInfo) {
+			audioInfo.value = cachedInfo
+		}
+
+		// 2. 预加载所有音频(无论是否已有缓存)
+		await audioManager.cacheWordAudios(props.activeWord)
+
+		// 3. 更新音频信息
+		audioInfo.value = audioManager.getAudioInfoFromStorage(props.activeWord.id)
+	}
+
+	// 播放主音频
+	const playMainAudio = () => {
+		if (audioInfo.value && audioInfo.value.main) {
+			audioManager.playAudio(audioInfo.value.main)
+		}
+	}
+
+	// 播放音节音频
+	const playSyllable = (syllable) => {
+		audioManager.playAudio(syllable)
+	}
+
+
+	// 播放频度音频
+	const playFrequency = (frequency) => {
+		audioManager.playAudio(frequency)
+	}
+	const clearFun = (frequency) => {
+		audioManager.clearAllCache()
+	}
+
+	onMounted(() => {
+		initAudio()
+	})
+</script>
+
+<style>
+</style>

+ 139 - 0
pages/chaojidanci/newEnglish/components/mainCard.vue

@@ -0,0 +1,139 @@
+<template>
+	<selectTypesVue :activeSelect="currentIndex" @change="onChange"></selectTypesVue>
+	<swiper class="word-view-swiper-box" :indicator-dots="false" :autoplay="false" :circular="false" :current="+currentIndex-1"
+		@change="handleSwiperChange" :disable-touch="isPlaying">
+		<swiper-item>
+			<view class="swiper-item uni-bg-red">
+				<xuePage :active-word="activeWord" :active-words="activeWords" @goXiangjie="goXiangjie"
+					@play-audio="handlePlayAudio" :pageData="pageData"></xuePage>
+			</view>
+		</swiper-item>
+		<swiper-item>
+			<view class="swiper-item uni-bg-red">
+				<pinPageVue :active-word="activeWord" :active-words="activeWords" @play-audio="handlePlayAudio">
+				</pinPageVue>
+			</view>
+		</swiper-item>
+		<swiper-item>
+			<view class="swiper-item uni-bg-blue">
+				<readContent :active-word="activeWord" :pageData="pageData" @play-audio="handlePlayAudio"
+					@luyinSuccess="luyinSuccess" :active-words="activeWords"></readContent>
+			</view>
+		</swiper-item>
+		<swiper-item>
+			<view class="swiper-item uni-bg-blue">
+				<selectPageVue :active-word="activeWord" :active-words="activeWords" @play-audio="handlePlayAudio">
+				</selectPageVue>
+			</view>
+		</swiper-item>
+		<swiper-item>
+			<view class="swiper-item uni-bg-blue">
+				<beiPageVue :active-word="activeWord" :pageData="pageData" :active-words="activeWords"
+					@play-audio="handlePlayAudio"></beiPageVue>
+			</view>
+		</swiper-item>
+	</swiper>
+
+	<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>
+
+	<audioRightWrongVue></audioRightWrongVue>
+</template>
+
+<script setup>
+	import pinPageVue from './pinPage.vue';
+	import selectPageVue from './selectPage.vue';
+	import beiPageVue from './beiPage.vue';
+	import readContent from './readContent.vue';
+	import xuePage from './xuePage.vue';
+	import audioRightWrongVue from "../components/audioRightWrong.vue";
+	import selectTypesVue from './selectTypes.vue';
+  import {
+    ref,
+    onMounted,
+    onUnmounted, nextTick
+  } from 'vue';
+	const props = defineProps({
+		activeWord: {
+			type: Object,
+		},
+		pageData: {
+			type: Object,
+		},
+		activeWords: {
+			type: Array
+		},
+		isPlaying: Boolean // 新增 isPlaying prop
+	})
+	const statusPopup = ref(null)
+	const popupImage = ref('')
+	const statusAudio = uni.createInnerAudioContext()
+	const emits = defineEmits(['play-audio', 'goXiangjie', 'swiper-change'])
+	const currentIndex = ref(1)
+  const mySwiperRef = ref(null);
+	const isXunHuanbofangYinbiao = ref(false)
+
+
+	const luyinSuccess = (imagePath, text, audioPath) => {
+		console.log('111111');
+		showStatusPopup('', '录音成功!', '/static/mp3/newYingyu/right-tip.mp3')
+	}
+
+	const showStatusPopup = (imagePath, text, audioPath) => {
+		console.log('audioPath', audioPath);
+		// 播放音效
+		statusAudio.stop()
+		statusAudio.src = audioPath
+		statusAudio.play()
+
+		// 显示弹窗
+		statusPopup.value.open()
+
+	}
+
+	function handleSwiperChange(e) {
+		emits('swiper-change', e.detail.current)
+		currentIndex.value = e.detail.current + 1;
+	}
+
+	function onChange(code) {
+		if (props.isPlaying || isXunHuanbofangYinbiao.value) return;
+		currentIndex.value = (code + 1)
+	}
+
+	function handlePlayAudio({
+		url,
+		code
+	}) {
+		emits('play-audio', {
+			url,
+			code
+		})
+	}
+
+	function goXiangjie() {
+		emits('goXiangjie')
+	}
+	
+	function updateYinbiaoSataus(da) {
+		isXunHuanbofangYinbiao.value = da
+	}
+	
+	onMounted(() => {
+		statusAudio.autoplay = false
+		statusAudio.onEnded(() => {
+			statusPopup.value.close()
+		})
+		uni.$on('xunhuanYinbiaoBofang',updateYinbiaoSataus)
+	})
+	onUnmounted(() => {
+		statusAudio.destroy();
+		uni.$on('xunhuanYinbiaoBofang',updateYinbiaoSataus)
+	})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 176 - 0
pages/chaojidanci/newEnglish/components/pinPage.vue

@@ -0,0 +1,176 @@
+<!-- 单词区 && 音标区:最多14位,超过换行-->
+<!-- 单音节最长:swimming 多音节最长:transportation -->
+<template>
+	<!-- 显示区 -->
+<!--	<selectTypesVue activeSelect="2"></selectTypesVue>-->
+	<view class="ezy-border-body">
+		<view class="words-pin-box">
+			
+			<!-- 拼读区 -->
+			<!-- 单词字母多余6个需要追加 class:pin-small-words-box  -->
+			<view class="pin-words-box"
+				:class="{'pin-small-words-box': wordLength > 6, 'isAll': data.isAll, 'pin-right-words-box':  data.isAll && data.result, 'pin-error-words-box':  data.isAll && !data.result}">
+				<view class="words-item" v-for="item in data.selectList">{{item}}</view>
+			</view>
+			<view class="pin-body-box">
+				<!-- 清空按钮 -->
+				<view class="clean-btn" v-if="isAlreadyAnswer" @click="handleReset"></view>
+				<!-- 提示 -->
+				<view class="pin-tip" v-else>提示:请点击页面下方字母,选择正确答案。</view>
+				<!-- 解释区-->
+				<view class="pin-words-explain-box">
+					<view class="words-explain-item" v-for="item in activeWord.jianyi" :key="item">{{item}}</view>
+				</view>
+				<audioOneVue @play-audio="handlePlay" :activeWord="activeWord"></audioOneVue>
+			</view>
+			<!-- 选择区 -->
+			<view class="pin-words-box pin-words-change-box" :class="{'pin-small-words-box': wordLength>6}">
+				<view class="words-item words-change-item" v-for="(item,index) in data.randomList" :key="index"
+					:class="{disabled:  isSelect(item,index)}" @click="handleSelect(item,index)">{{item}}</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import selectTypesVue from './selectTypes.vue';
+	import audioOneVue from './audioOne.vue';
+	import {
+		reactive,
+		computed,
+	} from 'vue';
+	import {
+		onLoad
+	} from "@dcloudio/uni-app"
+	import * as httpApi from "@/api/chaojidanci.js"
+  import {resultAudioPlayer} from "./useAudioRightWrong"
+
+  const resultAudioPlayerD = new resultAudioPlayer();
+
+	const emits = defineEmits(['play-audio'])
+
+	const props = defineProps({
+		activeWord: {
+			type: Object,
+		},
+		activeWords: {
+			type: Array
+		},
+	})
+
+	function handlePlay(opt) {
+		emits('play-audio', opt)
+	}
+
+	const data = reactive({
+		list: [],
+		randomList: [],
+		selectList: [],
+		result: false, // 正确性
+		isAll: false, // 是否全答
+		indexArr: [],
+	})
+
+	onLoad(() => {
+		initItem()
+	})
+
+	const wordLength = computed(() => props.activeWord.name.length)
+	const isAlreadyAnswer = computed(() => {
+		return data.selectList.some(item => item != '')
+	})
+
+	function isSelect(item,index) {
+		return data.indexArr.some(ii => ii == index)
+	}
+
+	function handleReset() {
+		data.list.forEach((item, index) => {
+			data.selectList[index] = ''
+		})
+
+		data.result = false;
+		data.isAll = false;
+		data.indexArr = [];
+	}
+
+	function shuffleArray(array) {
+		for (let i = array.length - 1; i > 0; i--) {
+			const j = Math.floor(Math.random() * (i + 1));
+			[array[i], array[j]] = [array[j], array[i]]; // ES6解构赋值交换元素
+		}
+		return array;
+	}
+
+	function randomClone(arr) {
+		const clone = [...arr];
+		return shuffleArray(clone); // 复用方法一的洗牌算法
+	}
+
+	// 初始化 单词列表
+	function initItem() {
+
+		data.list = props.activeWord.chaifenPin;
+		data.randomList = randomClone(data.list);
+		data.list.forEach((item, index) => {
+			data.selectList[index] = ''
+		})
+	}
+
+	function handleSelect(word,mIndex) {
+    if (data.indexArr.some(ii => ii == mIndex)) {
+      // 已选过 禁止再选
+      return;
+    }
+
+		// 覆盖状态
+		let status = false;
+		data.selectList.forEach((item, index) => {
+			// 无值 无修改
+			if (!item && !status) {
+				// 第一项空值覆盖
+				data.selectList[index] = word;
+				// 以有控制覆盖
+				status = true;
+				// 更新已选择的下标
+				data.indexArr.push(mIndex);
+			}
+		})
+		// 校验正确性
+		checkIsRight();
+
+		if (data.selectList.some(item => item == '')) {
+			data.isAll = false;
+		} else {
+			data.isAll = true;
+      if (data.isAll && !data.result) {
+        resultAudioPlayerD.play('wrong','pin')
+      }
+		}
+
+	}
+
+	function checkIsRight() {
+		if (data.list.join('') === data.selectList.join('')) {
+			// 正确
+			data.result = true;
+      resultAudioPlayerD.play('right','pin')
+			noticeBackDb();
+		} else {
+			data.result = false;
+		}
+	}
+
+	function noticeBackDb() {
+		httpApi.getWordZhangwo({
+			type: 1,
+			wordId: props.activeWord.id
+		})
+	}
+</script>
+
+<style lang="scss" scoped>
+
+
+
+</style>

+ 376 - 0
pages/chaojidanci/newEnglish/components/readContent.vue

@@ -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>

+ 114 - 0
pages/chaojidanci/newEnglish/components/selectPage.vue

@@ -0,0 +1,114 @@
+<!-- 单词区 && 音标区:最多16位,超过换行   选项最多两行超出省略-->
+<!-- 单音节最长:swimming 多音节最长:transportation -->
+<template>
+	<!-- 显示区 -->
+<!--	<selectTypesVue activeSelect="4"></selectTypesVue>-->
+	<view class="ezy-border-body">
+		<view class="words-xuan-box">
+			
+			<view class="xuan-body-box">
+				<!-- 单词区 -->
+				<view class="show-words-box"> {{data.name}} </view>
+				<!--  音标区  -->
+				<view class="yb-play-box">
+					<!-- <text>{{activeWord.yinbiao}}</text> -->
+					<yinbiaoTxtVue :yinbiao="activeWord.yinbiao"></yinbiaoTxtVue>
+					<!-- active -->
+					<audioTwoVue @play-audio="handlePlay" :active-word="activeWord"></audioTwoVue>
+				</view>
+			</view>
+			<!-- 选择区 -->
+			<view class="select-change-box">
+				<view class="select-item"
+					:class="{active: data.answer == 'A', 'select-error': data.answer =='A' && !data.result, 'select-right':data.answer =='A' && data.result}"
+					@click="handleSelect('A')"><text>{{data.opa}}</text></view>
+				<view class="select-item"
+					:class="{active: data.answer == 'B', 'select-error':  data.answer =='B' && !data.result, 'select-right':data.answer =='B' && data.result}"
+					@click="handleSelect('B')"><text>{{data.opb}}</text></view>
+				<view class="select-item"
+					:class="{active: data.answer == 'C', 'select-error':  data.answer =='C' && !data.result, 'select-right':data.answer =='C' && data.result}"
+					@click="handleSelect('C')"><text>{{data.opc}}</text></view>
+				<view class="select-item"
+					:class="{active: data.answer == 'D', 'select-error':  data.answer =='D' && !data.result, 'select-right':data.answer =='D' && data.result}"
+					@click="handleSelect('D')"><text>{{data.opd}}</text></view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import selectWordsVue from './selectWords.vue';
+	import selectTypesVue from './selectTypes.vue';
+	import audioTwoVue from './audioTwo.vue';
+	import * as httpApi from "@/api/chaojidanci.js"
+	import yinbiaoTxtVue from "./yinbiaoTxt.vue"
+	import {
+		onLoad
+	} from "@dcloudio/uni-app"
+	import {
+		reactive,
+	} from 'vue';
+  import {resultAudioPlayer} from "./useAudioRightWrong"
+
+  const resultAudioPlayerD = new resultAudioPlayer();
+
+	const emits = defineEmits(['play-audio'])
+
+	const props = defineProps({
+		activeWord: { // 单词数据
+			type: Object,
+		},
+		activeWords: {
+			type: Array
+		},
+	})
+	const data = reactive({
+		name: [],
+		opa: null,
+		opb: null,
+		opc: null,
+		opd: null,
+		answer: null, // 已选项
+		result: false, // 正确性
+	})
+
+	onLoad(() => {
+		initItem()
+	})
+
+	function handlePlay(opt) {
+		emits('play-audio', opt)
+	}
+
+	function initItem() {
+		data.name = props.activeWord.name;
+		data.opa = props.activeWord.opa;
+		data.opb = props.activeWord.opb;
+		data.opc = props.activeWord.opc;
+		data.opd = props.activeWord.opd;
+	}
+
+	function handleSelect(d1) {
+		data.answer = d1;
+		checkIsRight();
+	}
+
+	function checkIsRight() {
+		if (data.answer == props.activeWord.daan) {
+			// 正确
+			data.result = true;
+      resultAudioPlayerD.play('right','select')
+			noticeBackDb()
+		} else {
+			data.result = false;
+      resultAudioPlayerD.play('wrong','select')
+		}
+	}
+
+	function noticeBackDb() {
+		httpApi.getWordZhangwo({
+			type: 2,
+			wordId: props.activeWord.id
+		})
+	}
+</script>

+ 38 - 0
pages/chaojidanci/newEnglish/components/selectTypes.vue

@@ -0,0 +1,38 @@
+<template>
+	<view class="select-types-box">
+		<view class="types-item" :class="{active: activeSelect == 1}" @click="handleClick(0)">学</view>
+		<icon class="jt-item" :class="{active: activeSelect == 1}"></icon>
+		<view class="types-item" :class="{active: activeSelect == 2}" @click="handleClick(1)">拼</view>
+		<icon class="jt-item" :class="{active: activeSelect == 2}"></icon>
+		<view class="types-item" :class="{active: activeSelect == 3}" @click="handleClick(2)">读</view>
+		<icon class="jt-item" :class="{active: activeSelect == 3}"></icon>
+		<view class="types-item" :class="{active: activeSelect == 4}" @click="handleClick(3)">选</view>
+		<icon class="jt-item" :class="{active: activeSelect == 4}"></icon>
+		<view class="types-item" :class="{active: activeSelect == 5}" @click="handleClick(4)">背</view>
+	</view>
+</template>
+
+<script setup>
+	defineProps({
+		activeSelect: {
+			type: [String,Number],
+			required: true
+		}
+	})
+	
+	const emits = defineEmits(['change'])
+	
+	function handleClick(code) {
+		emits('change',code)
+	}
+</script>
+
+<style lang="scss" scoped>
+	.select-types {
+		display: flex;
+		justify-content: space-between;
+	}
+	.active {
+		color: red;
+	}
+</style>

+ 16 - 0
pages/chaojidanci/newEnglish/components/selectWords.vue

@@ -0,0 +1,16 @@
+<template>
+	<view class="select-words-box">
+		<view class="words-item" v-for="item in activeWords" :class="{active: activeWord.id == item.id}">{{item.name}}</view>
+	</view>
+</template>
+
+<script setup>
+	const props = defineProps({
+		activeWord: {
+			type: Object,
+		},
+		activeWords: {
+			type: Array
+		},
+	})
+</script>

+ 159 - 0
pages/chaojidanci/newEnglish/components/useAudio.js

@@ -0,0 +1,159 @@
+import {
+	reactive,
+	ref
+} from 'vue';
+import {
+	onHide,
+	onUnload
+} from "@dcloudio/uni-app"
+import {
+	nextTick
+} from 'vue';
+
+let audioContext = null;
+let code = null; // 身份标识
+audioContext = uni.createInnerAudioContext(); // 单例模式[3](@ref)
+audioContext.onEnded(() => {
+	// console.log('触发播放结束')
+	// 播放结束
+	uni.$emit('danci-audio-ended', code)
+	audioContext?.stop();
+
+})
+audioContext.onPlay(() => {
+	// 播放
+	// console.log('播放事件:', code)
+	uni.$emit('danci-audio-play', code);
+});
+audioContext.onError((err) => {
+	// 播放
+	uni.$emit('danci-audio-ended', code)
+	audioContext?.stop();
+	// uni.$emit('danci-audio-play', code);
+	uni.showToast({
+		title: '音频播放异常,已重置'
+	})
+});
+function createAudioNew() {
+	audioContext = uni.createInnerAudioContext(); // 单例模式[3](@ref)
+	audioContext.onEnded(() => {
+		// console.log('触发播放结束')
+		// 播放结束
+		uni.$emit('danci-audio-ended', code)
+		audioContext?.stop();
+	
+	})
+	audioContext.onPlay(() => {
+		// 播放
+		// console.log('播放事件:', code)
+		uni.$emit('danci-audio-play', code);
+	});
+	audioContext.onError((err) => {
+		// 播放
+		uni.$emit('danci-audio-ended', code)
+		audioContext?.stop();
+		// uni.$emit('danci-audio-play', code);
+		uni.showToast({
+			title: '音频播放异常,已重置'
+		})
+	});
+}
+
+
+
+export class audioPlayer {
+	// 播放音频
+	play(path, code1) {
+		// console.log('播放文件地址', path)
+		code = code1;
+		if (audioContext.src === path && !audioContext.paused) return;
+		audioContext.stop();
+		audioContext.destroy();
+		audioContext = null;
+		
+		nextTick(() => {
+			createAudioNew();
+			
+			audioContext.src = path;
+			audioContext.play();
+		})
+		
+
+	}
+
+	// 暂停播放
+	pause() {
+		audioContext?.pause();
+	}
+
+	// 停止播放(释放资源)
+	stop() {
+		audioContext?.stop();
+		audioContext = null;
+	}
+
+}
+
+export function useAudioCache() {
+	const cacheMapKey = 'audio_cache_map'; // Storage 中缓存映射的键名
+
+	// 下载并缓存音频
+	async function cacheAudio(url) {
+		try {
+			let cacheMap = uni.getStorageSync(cacheMapKey) || {};
+
+			if (cacheMap[url]) {
+				// console.log('已缓存音频地址', cacheMap[url])
+				return cacheMap[url]; // 返回有效缓存路径
+			}
+
+			// 3. 下载音频文件
+			const {
+				tempFilePath
+			} = await new Promise((resolve, reject) => {
+				uni.downloadFile({
+					url,
+					success: resolve,
+					fail: reject
+				});
+			});
+
+			if (!tempFilePath.includes('.mp3')) {
+				// console.log(`文件下载失败${url}`)
+				return null;
+			}
+
+			// 4. 持久化存储到本地
+			const savedFilePath = tempFilePath; // 跨平台安全路径[2](@ref)
+			await uni.saveFile({
+				tempFilePath,
+				toSavedFilePath: savedFilePath
+			});
+			cacheMap[url] = savedFilePath;
+			uni.setStorageSync(cacheMapKey, cacheMap);
+			// console.log('当前音频地址', savedFilePath)
+			return savedFilePath;
+		} catch (err) {
+			// console.error('音频缓存失败:', err);
+			return null;
+		}
+	}
+
+	function clearAudioCache() {
+		// uni.setStorageSync(cacheMapKey, {})
+		const cacheMap = uni.getStorageSync(cacheMapKey) || {};
+		Object.entries(cacheMap).forEach(([key, path]) => {
+			uni.removeSavedFile({
+				filePath: path
+			}); // 删除文件
+			delete cacheMap[key];
+		})
+		// console.log('已清理完成', cacheMap)
+		uni.setStorageSync(cacheMapKey, cacheMap)
+	}
+
+	return {
+		cacheAudio,
+		clearAudioCache
+	}
+}

+ 85 - 0
pages/chaojidanci/newEnglish/components/useAudioRightWrong.js

@@ -0,0 +1,85 @@
+import { nextTick } from "vue";
+
+let audioContext = null;
+let code = null; // 身份标识
+audioContext = uni.createInnerAudioContext(); // 单例模式[3](@ref)
+audioContext.onEnded(() => {
+	// 播放结束
+	uni.$emit('result-audio-ended', code)
+})
+audioContext.onPlay(() => {
+	// 播放
+	uni.$emit('result-audio-play', code);
+});
+audioContext.onError((err) => {
+	// 播放
+	uni.$emit('result-audio-ended', code)
+	uni.showToast({
+		title: '音频播放异常,已重置.'
+	})
+});
+
+const audioList = {
+	right: '/static/mp3/newYingyu/right-tip.mp3',
+	wrong: '/static/mp3/newYingyu/error-tip.mp3'
+}
+
+export const resultImageList = {
+	right: '/static/images/study/cjdc/right-tip-img-gif',
+	wrong: '/static/images/study/cjdc/error-tip-img-gif'
+}
+
+function createAudioNew() {
+	audioContext = uni.createInnerAudioContext(); // 单例模式[3](@ref)
+	audioContext.onEnded(() => {
+		// 播放结束
+		uni.$emit('result-audio-ended', code)
+	})
+	audioContext.onPlay(() => {
+		// 播放
+		uni.$emit('result-audio-play', code);
+	});
+	audioContext.onError((err) => {
+		// 播放
+		console.log('errr',err)
+		uni.$emit('result-audio-ended', code);
+		uni.showToast({
+			title: '音频播放异常,已重置.'
+		})
+	});
+}
+
+
+
+export class resultAudioPlayer {
+	// 播放音频
+	play(codeT, code1) {
+		code = {
+			code1,
+			codeT
+		};
+		if (audioContext.src === audioList[codeT] && !audioContext.paused) return;
+		audioContext.stop();
+		audioContext.destroy();
+		audioContext = null;
+		
+		nextTick(() => {
+			createAudioNew();
+			audioContext.src = audioList[codeT];
+			audioContext.play();
+		})
+	
+	}
+
+	// 暂停播放
+	pause() {
+		audioContext?.pause();
+	}
+
+	// 停止播放(释放资源)
+	stop() {
+		audioContext?.stop();
+		audioContext = null;
+	}
+
+}

+ 82 - 0
pages/chaojidanci/newEnglish/components/useYinbiao.js

@@ -0,0 +1,82 @@
+import {ref} from "vue"
+import {
+    audioPlayer,
+    useAudioCache,
+} from "./useAudio.js";
+
+export function useYinBiaoAutoPlay () {
+    const AudioP = new audioPlayer();
+    const {
+        cacheAudio,
+    } = useAudioCache();
+
+    const current = ref(0);
+    const list = ref([]);
+    let code = null;
+    const isAutoPlaying = ref(false);
+
+    async function handlePlay() {
+        isAutoPlaying.value = true;
+		uni.$emit('xunhuanYinbiaoBofang', isAutoPlaying.value)
+        code = new Date().getTime();
+        const activeYin = {
+            url: list.value[current.value].yinpin,
+            code
+        }
+        const cachedPath = await cacheAudio(activeYin.url);
+        if (cachedPath && cachedPath.includes('.mp3')) {
+            AudioP.play(cachedPath, code);
+        } else {
+            uni.showToast({
+                title: '音频加载失败',
+                icon: 'none'
+            });
+            isAutoPlaying.value = false;
+			uni.$emit('xunhuanYinbiaoBofang', isAutoPlaying.value)
+        }
+    }
+
+    function upd(mCode) {
+        if (code !== mCode) {
+            isAutoPlaying.value = false;
+            uni.$emit('xunhuanYinbiaoBofang', isAutoPlaying.value)
+            return;
+        }
+
+        if (current.value<list.value.length-1) {
+            current.value = current.value+1;
+            // 继续播放第二音频
+            handlePlay();
+        } else {
+            // 播放结束
+            isAutoPlaying.value = false;
+            uni.$emit('xunhuanYinbiaoBofang', isAutoPlaying.value)
+        }
+    }
+
+    function initListen() {
+        uni.$on('danci-audio-ended', upd)
+    }
+
+    function removeListen() {
+        uni.$off('danci-audio-ended',upd)
+    }
+
+
+    function playYinbiaoAuto(clist) {
+		isAutoPlaying.value = true;
+		uni.$emit('xunhuanYinbiaoBofang', isAutoPlaying.value)
+        current.value = 0
+        list.value = clist;
+        // 执行首次播放
+        handlePlay();
+    }
+
+    // 初始化首次监听
+    initListen();
+    return {
+        playYinbiaoAuto,
+        isAutoPlaying,
+        removeListen
+    }
+}

+ 120 - 0
pages/chaojidanci/newEnglish/components/xiangjie.vue

@@ -0,0 +1,120 @@
+<template>
+	<view class="word-view-page words-details-page">
+		<view class="icon-title-navBar-box">
+			<view @click="handleBack" class="nav-bar-icon"></view>
+			<text class="nav-bar-title">单词详解</text>
+		</view>
+		<view class="ezy-tab-border words-details-body">
+			<view class="ezy-border-body ">
+				<view class="details-word">{{pageInfo.name}}</view>
+				<view class="details-content-box">
+					<text class="details-title">通用释义</text>
+					<view class="tysy-content" v-for="(item,index) in pageInfo.xiangyi" :key="index">{{item}}</view>
+				</view>
+				<view class="details-content-box">
+					<text class="details-title">实用口语</text>
+					<view v-for="(item,index) in pageInfo.kouyu" :key="index" class="syky-content">
+						<view class="details-en-content">
+
+							<rich-text :nodes="highlightWord(item.en)"></rich-text>
+							<!-- 	<text>{{item.en}}</text> -->
+							<!-- 变色  word-color-->
+							<!-- <text class="word-color">{{item.en}}</text>
+							<text>{{item.en}}</text> -->
+						</view>
+						<view class="details-cn-content">{{item.zn}}</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		reactive,
+		ref,
+		onMounted
+	} from 'vue';
+	import {
+		getWordInfo,
+		getWordInfoYk
+	} from "@/api/word.js"
+	import {
+		onLoad
+	} from '@dcloudio/uni-app';
+
+	import {
+		getUserIdentity,
+	} from "@/utils/common.js"
+	const goXiangjie = () => {
+
+	}
+
+	const pageInfo = ref({
+		kouyu: [],
+		xiangyi: [],
+		name: ''
+	})
+	const data = ref(null)
+	const userCode = getUserIdentity();
+
+	onLoad((options) => {
+		if (userCode !== 'Visitor') {
+			getInfo(options)
+			data.value = options
+		} else {
+			getCommonInfo(options)
+			data.value = options
+		}
+	})
+	const highlightWord = (text) => {
+		if (!text || !pageInfo.value.name) {
+			return text
+		}
+		const word = pageInfo.value.name;
+		const regex = /<([^>]+)>/g;
+		console.log('pageInfo.value.name', pageInfo.value.name);
+		return text.replace(regex, (match) => {
+			return `<span style="color: #3a7fe9;">${match.replace(/[<>]/g, "")}</span>`;
+		});
+	}
+	const handleBack = () => {
+
+
+		if (userCode !== 'Visitor') {
+			uni.redirectTo({
+				url: `/pages/chaojidanci/newEnglish/index?jieId=${data.value.jieId}&wordId=${data.value.wordId}`
+			});
+		} else {
+			
+			console.log('data123',data);
+			uni.redirectTo({
+				url: `/pages/chaojidanci/newEnglish/index?jieId=${data.value.jieId}&wordId=${data.value.wordId}&levelId=${data.value.levelId}&typeId=${data.value.typeId}&subjectId=${data.value.subjectId}&tipFlag=${data.value.tipFlag}&youkeZhangId=${data.value.youkeZhangId}`
+			});
+		}
+	}
+	const getInfo = (data) => {
+		let req = {
+			jieId: data.jieId,
+			wordId: data.wordId
+		}
+		getWordInfo(req).then(res => {
+			console.log('res', res);
+			pageInfo.value = res.data
+		})
+	}
+	const getCommonInfo = (data) => {
+		let req = {
+			jieId: data.jieId,
+			wordId: data.wordId
+		}
+		getWordInfoYk(req).then(res => {
+			console.log('res', res);
+			pageInfo.value = res.data
+		})
+	}
+</script>
+
+<style>
+</style>

+ 169 - 0
pages/chaojidanci/newEnglish/components/xuePage.vue

@@ -0,0 +1,169 @@
+<!-- 单词区 && 音标区:最多14位,超过换行  词根助记区:宽度不限,可以滚动-->
+<!-- 单音节最长:swimming 多音节最长:transportation -->
+<template>
+<!--		<selectTypesVue activeSelect="1"></selectTypesVue>-->
+		<view class="ezy-border-body">
+			<view class="words-xue-box">
+				<view class="words-xue-body">
+					<!-- 单词区 -->
+					<view class="word-circle-box">
+						<view class="word-link-box">
+							<text v-for="(item,index) in activeWord.chaifen" :key="index">{{item}}</text>
+						</view>
+					</view>
+					<view class="word-block-box">
+						<text v-for="(item,index) in activeWord.chaifen" :key="index">{{item}}</text>
+					</view>
+					<!-- 音标区 -->
+					<view class="yb-play-box xue-yb-play-box">
+						<yinbiaoTxtVue :yinbiao="activeWord.yinbiao"></yinbiaoTxtVue>
+						<!-- 音频播放 -->
+						<audioTwoVue :active-word="activeWord" @play-audio="handlePlay"></audioTwoVue>
+					</view>
+					<!-- 注释区 -->
+					<view class="pin-words-explain-box xue-words-explain-box">
+						<view class="words-explain-item" v-for="item in activeWord.jianyi" :key="item">{{item}}</view>
+					</view>
+					<!-- 详解触发 -->
+					<view @click="goXiangjie" class="details-btn">详解 ></view>
+					<!-- 音标拆分区 -->
+					<view v-show="data.isPindu" class="word-block-box yb-block-box">
+						<!-- pindu -->
+						<audioThreeVue v-for="(item,index) in activeWord.pindu" :key="index" :YItem="item"
+							@play-audio="handlePlay"></audioThreeVue>
+					</view>
+					<view v-show="!data.isPindu" class="yj-block-box">
+						<!-- yinjie -->
+						<audioFourVue v-for="(item,index) in activeWord.yinjie" :key="index" :YItem="item"
+							@play-audio="handlePlay"></audioFourVue>
+					</view>
+
+					<!-- 音标按钮 -->
+					<view class="xue-change-btn-box">
+						<view class="change-btn" :class="{active: !data.isPindu}" @click="handlePindu"><text>自然拼读</text>
+						</view>
+						<view class="change-btn" :class="{active: data.isPindu}" @click="handleYinjie"><text>音节拆分</text>
+						</view>
+					</view>
+					<!-- 词根+实用口语 -->
+					<view v-if="activeWord.cigenzhuji.length" class="details-content-box xue-details-content-box">
+						<text class="details-title">词根助记</text>
+						<scroll-view class="cg-item-list" scroll-x @touchmove.stop>
+							<view class="cg-item-box" v-for="(item,index) in activeWord.cigenzhuji" :key="index">
+								<view class="cg-item">
+									<view
+										:class="{isEven: index% 2 !== 0 && index!==activeWord.cigenzhuji.length-1,isOdd: index% 2 === 0 && index!==activeWord.cigenzhuji.length-1}">
+										{{item.en}}
+									</view>
+									<view>{{item.zn}}</view>
+								</view>
+								<view class="cg-symbol" v-if="index<activeWord.cigenzhuji.length-2">+</view>
+								<view class="cg-symbol" v-if="index == activeWord.cigenzhuji.length - 2">=</view>
+							</view>
+						</scroll-view>
+					</view>
+
+					<!-- 实用语句 -->
+					<view class="details-content-box xue-details-content-box">
+						<text class="details-title">实用口语</text>
+						<view v-for="(item,index) in activeWord.kouyu" :key="index" class="syky-content">
+							<view class="details-en-content">
+								<rich-text :nodes="highlightWord(item.en)"></rich-text>
+							</view>
+							<view class="details-cn-content">{{item.zn}}</view>
+						</view>
+					</view>
+
+				</view>
+			</view>
+		</view>
+</template>
+
+<script setup>
+	import selectTypesVue from './selectTypes.vue';
+	import audioTwoVue from './audioTwo.vue';
+	import audioThreeVue from './audioThree.vue';
+	import audioFourVue from './audioFour.vue';
+	import yinbiaoTxtVue from "./yinbiaoTxt.vue"
+	import {
+		reactive,
+		computed,
+    onUnmounted
+	} from 'vue';
+	import {
+		audioPlayer,
+		useAudioCache
+	} from './useAudio.js';
+  import {useYinBiaoAutoPlay} from "./useYinbiao.js"
+
+  const { playYinbiaoAuto,isAutoPlaying,removeListen } = useYinBiaoAutoPlay();
+	const emits = defineEmits(['play-audio', 'goXiangjie'])
+	const {
+		cacheAudio,
+		clearAudioCache
+	} = useAudioCache();
+
+  onUnmounted(() => {
+    removeListen();
+  })
+
+	const data = reactive({
+		isPlaying: false,
+		isPindu: true,
+	})
+
+	const props = defineProps({
+		activeWord: { // 单词数据
+			type: Object,
+		},
+		activeWords: {
+			type: Array
+		},
+		pageData: {
+			type: Object,
+		},
+	})
+
+
+	const highlightWord = (text) => {
+		if (!text || !props.activeWord.name) {
+			return text
+		}
+		const word = props.activeWord.name;
+		const regex = /<([^>]+)>/g;
+		return text.replace(regex, (match) => {
+			console.log('match', match)
+			return `<span style="color: #3a7fe9;">${match.replace(/[<>]/g, "")}</span>`;
+		});
+	}
+
+	async function handlePlay(opt) {
+		emits('play-audio', opt)
+	}
+
+	function handlePindu() {
+    // 自动播放中不允许切换
+    if (isAutoPlaying.value) return
+    if (data.isPindu) return
+		data.isPindu = true
+    playYinbiaoAuto(props.activeWord.pindu)
+	}
+
+	function handleYinjie() {
+    // 自动播放中不允许切换
+    if (isAutoPlaying.value) return
+    if (!data.isPindu) return
+		data.isPindu = false
+    playYinbiaoAuto(props.activeWord.yinjie)
+	}
+
+	function goXiangjie() {
+    uni.redirectTo({
+      url: '/pages/chaojidanci/newEnglish/components/xiangjie?danyuanId=' + props.pageData.danyuanId + '&wordId=' + props
+          .pageData.activeId
+    })
+	}
+</script>
+
+<style>
+</style>

+ 22 - 0
pages/chaojidanci/newEnglish/components/yinbiaoTxt.vue

@@ -0,0 +1,22 @@
+<template>
+  <text v-for="item in yinbiaoArr">{{item}}</text>
+</template>
+
+<script setup>
+import {computed} from "vue"
+const props = defineProps({
+  yinbiao: {
+    type: String,
+    required: true
+  }
+})
+
+const yinbiaoArr = computed(() => {
+  const result = props.yinbiao.split(/(\/)/).filter(Boolean);
+  return result
+})
+</script>
+
+<style scoped>
+
+</style>

+ 265 - 0
pages/chaojidanci/newEnglish/index.vue

@@ -0,0 +1,265 @@
+<template>
+  <view class="word-view-page" :style="{backgroundImage: 'url(' + courseBjFun() + ')'}">
+    <view class="icon-title-navBar-box">
+      <view @click="handleBack" class="nav-bar-icon"></view>
+      <text class="nav-bar-title">学习</text>
+    </view>
+    <view class="ezy-tab-border">
+      <template v-for="item in data.wordList">
+        <mainCardVue :active-word="data.activeWord" :pageData="data" :active-words="activeWords"
+                     @play-audio="handlePlayAudio" @goXiangjie="goXiangjie" @swiper-change="handleSwiperChange"
+                     :is-playing="isAudioPlaying" v-if="item.id == data.activeId" :key="item.id">
+        </mainCardVue>
+      </template>
+    </view>
+    <view class="word-view-bottom">
+      <view class="bottom-btn-box">
+        <view class="word-view-btn" @click="prevWord" v-if="!isFirst&&isLearnRecord!=0">上一词</view>
+        <view class="word-view-btn" @click="nextWord" v-if="!isLast&&isLearnRecord!=0">下一词</view>
+        <view class="word-view-btn" v-if="isLast&&isLearnRecord!=0" @click="handleComplete">完成</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import mainCardVue from "./components/mainCard.vue";
+import {
+  ref,
+  reactive,
+  computed,
+  nextTick,
+  onUnmounted
+} from "vue";
+import {
+  onLoad
+} from "@dcloudio/uni-app";
+import * as httpApi from "@/api/chaojidanci.js"
+import {
+  useAudioCache,
+  audioPlayer
+} from "./components/useAudio.js"
+import cacheManager from "@/utils/cacheManager";
+
+const {
+  cacheAudio,
+  clearAudioCache
+} = useAudioCache();
+
+const AudioP = new audioPlayer();
+const isLearnRecord = ref(null)
+const isLearnStatus = ref(null)
+const isAudioPlaying = ref(false) // 音频播放状态
+// 监听音频播放事件
+uni.$on('danci-audio-play', updateAudioPlayingTrue)
+uni.$on('danci-audio-ended', updateAudioPlayingFalse)
+
+onUnmounted(() => {
+  uni.$off('danci-audio-play', updateAudioPlayingTrue)
+  uni.$off('danci-audio-ended', updateAudioPlayingFalse)
+})
+
+function updateAudioPlayingFalse() {
+  isAudioPlaying.value = false
+}
+
+function updateAudioPlayingTrue() {
+  isAudioPlaying.value = true
+}
+
+function handleSwiperChange(index) {
+  return false
+  // 客户暂时不要
+  if (isAudioPlaying.value) return; // 如果正在播放,不允许切换
+
+  // 无论切换到哪个页面,都播放单词的主发音(yinpin)
+  if (data.activeWord && data.activeWord.yinpin) {
+    handlePlayAudio({
+      url: data.activeWord.yinpin,
+      code: 'swiper-auto-play'
+    });
+  }
+}
+
+function courseBjFun() {
+  return 'static/images/course/course-cjdc-bj.png'
+}
+
+function chunkArray(arr, chunkSize) {
+  const result = [];
+  for (let i = 0; i < arr.length; i += chunkSize) {
+    result.push(arr.slice(i, i + chunkSize));
+  }
+  return result;
+}
+
+const data = reactive({
+  danyuanId: null, // 节/单元ID
+  activeId: null, // 当前单词
+  wordList: [], // 单词列表
+  arrayList: [], // 整合数据
+  activeWord: null, // 单词详情数据
+  collectFlag: 0, // 收藏状态
+  subjectId: null, // 学科ID
+  levelId: null, // 等级
+  typeId: null, // 类型
+  tipFlag: null, // 提示
+  youkeZhangId: null, // 章ID
+  isLearnStatus: null, // 学习记录状态
+})
+
+onLoad(({
+          danyuanId,
+          wordId,
+          subjectId,
+          levelId,
+          typeId,
+          tipFlag,
+          isLearnStatus,
+        }) => {
+  data.danyuanId = danyuanId;
+  isLearnRecord.value = danyuanId;
+  data.activeId = wordId;
+  data.isLearnStatus = isLearnStatus;
+  data.subjectId = subjectId;
+  data.levelId = levelId;
+  data.typeId = typeId;
+  data.tipFlag = tipFlag;
+  // 获取单词列表数据
+  initWordInfo();
+  // 清理过期文件
+  clearAudioCache();
+})
+
+// 是否是最后一题
+const isLast = computed(() => {
+  if (!data.wordList.length) {
+    return false
+  }
+  return data.activeId == data.wordList[data.wordList.length - 1].id;
+})
+const isFirst = computed(() => {
+  if (!data.wordList.length) {
+    return false
+  }
+  return data.activeId == data.wordList[0].id;
+})
+const activeWords = computed(() => {
+
+  if (!data.activeId) {
+    return null
+  }
+  return data.arrayList.find(item => item.some(cit => cit.id == data.activeId))
+})
+
+function nextWord() {
+  const index = data.wordList.findIndex(item => item.id == data.activeId);
+  if (index < data.wordList.length - 1) {
+    uni.redirectTo({
+      url: `/pages/chaojidanci/newEnglish/index?danyuanId=${data.danyuanId}&wordId=${data.wordList[index + 1].id}`
+    })
+  }
+}
+
+function prevWord() {
+  const index = data.wordList.findIndex(item => item.id == data.activeId);
+  if (index > 0) {
+    uni.redirectTo({
+      url: `/pages/chaojidanci/newEnglish/index?danyuanId=${data.danyuanId}&wordId=${data.wordList[index - 1].id}`
+    })
+  }
+}
+
+function handleBack() {
+  // 返回单词列表
+  uni.redirectTo({
+    url: `/pages/wordList/wordList?danyuanId=${data.danyuanId}`
+  })
+}
+
+function handleComplete() {
+  // 返回岛
+  uni.redirectTo({
+    url: `/pages/study/index`
+  })
+}
+
+function initWordInfo() {
+  httpApi.getWordInfo({
+    danyuanId: data.danyuanId,
+    wordId: data.activeId
+  }).then(res => {
+    data.activeWord = res.data;
+    data.wordList = res.data.danciList; // 单词组
+    data.collectFlag = res.data.collectFlag; // 收藏状态
+    if (data.wordList.length) {
+      data.arrayList = chunkArray(data.wordList, 5); // 将1维单词数组转换为2维数组
+    }
+    handleCacheAudio()
+  })
+}
+
+// 新增自动播放功能
+const autoPlayAudio = async () => {
+  // 等待数据加载完成
+  await nextTick();
+
+  // 检查是否有单词发音数据
+  if (data.activeWord && data.activeWord.yinpin) {
+    // 延迟500ms确保音频已缓存
+    setTimeout(() => {
+      handlePlayAudio({
+        url: data.activeWord.yinpin,
+        code: 'auto-play' // 特殊标记,区分自动播放
+      });
+    }, 500);
+  }
+};
+
+function goXiangjie() {
+
+  uni.redirectTo({
+    url: `/pages/chaojidanci/newEnglish/components/xiangjie?levelId=${data.levelId}&typeId=${data.typeId}&subjectId=${data.subjectId}&tipFlag=${data.tipFlag}&wordId=${data.activeId}&youkeZhangId=${data.youkeZhangId}&danyuanId=${data.danyuanId}`
+  })
+}
+
+function handleCacheAudio() {
+
+  const arr = []
+  // yinpin
+  arr.push(data.activeWord.yinpin);
+  // yinjie
+  data.activeWord.yinjie.forEach(item => {
+    arr.push(item.yinpin)
+  })
+  // pindu
+  data.activeWord.pindu.forEach(item => {
+    arr.push(item.yinpin)
+  })
+  // 缓存音频
+  arr.map(item => cacheAudio(item));
+  // console.log('arr',arr)
+}
+
+async function handlePlayAudio({
+                                 url,
+                                 code
+                               }) {
+  console.log('播放', url)
+  console.log('播放', code)
+  const cachedPath = await cacheAudio(url);
+  if (cachedPath && cachedPath.includes('.mp3')) {
+    // console.log('地址:', cachedPath)
+    AudioP.play(cachedPath, code);
+  } else {
+    uni.showToast({
+      title: '音频加载失败',
+      icon: 'none'
+    });
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 258 - 0
pages/chaojidanci/wordList/wordList.vue

@@ -0,0 +1,258 @@
+<template>
+  <view class="word-list-page">
+    <view class="icon-title-navBar-box">
+      <view @click="goBack" class="nav-bar-icon"></view>
+      <text class="nav-bar-title">{{ listData.title || "" }}</text>
+    </view>
+    <view class="ezy-tab-border">
+      <view
+        class="word-title-box"
+        v-if="listData.danyuanNumberList && listData.danyuanNumberList.length > 0"
+      >
+        <!-- @click="handleTitleClick(item)"-->
+        <view
+          :id="'item-' + item.danyuanId"
+          class="title-item"
+          v-for="(item, index) in listData.danyuanNumberList"
+          :key="item.danyuanId"
+          :class="{ active: listData.activeIndex == index }"
+          v-show="
+            isTargetInSameGroup(
+              listData.danyuanNumberList,
+              listData.danyuanNumberList[listData.activeIndex],
+              item.danyuanId,
+            )
+          "
+        >
+          Unit{{ item.number }}</view
+        >
+      </view>
+
+      <view class="ezy-border-body">
+        <swiper
+          class="word-list-swiper-box"
+          :indicator-dots="false"
+          :autoplay="false"
+          :circular="false"
+          :current="listData.activeIndex"
+          @change="handleSwiperChange"
+          :disable-touch="!swiperData.isAllowed"
+        >
+          <swiper-item
+            class="word-list-swiper-item"
+            v-for="citem in unitList"
+            :key="citem.danyuanId"
+            @touchstart="handleTouchStart"
+            @touchend="handleTouchEnd"
+          >
+            <view
+              class="word-list-body"
+              v-if="citem.wordList && citem.wordList.length > 0"
+            >
+              <!-- num -->
+              <view class="word-num-box">
+                <icon></icon
+                ><text
+                  >{{ citem.studyCount || 0 }}/{{ citem.count || 0 }}词</text
+                >
+              </view>
+
+              <!-- 单词 -->
+              <view
+                class="word-list-item"
+                v-for="(item, index) in citem.wordList"
+                :key="index"
+                @click="toWord(item)"
+                :class="{ active: item.wcFlag == 1 }"
+              >
+                <view class="item-word">
+                  <view class="word-text">
+                    <text
+                      v-for="(word, wordIndex) in item.chaifen"
+                      :key="wordIndex"
+                      class="word-color"
+                    >
+                      {{ word }}
+                    </text>
+                  </view>
+                  <view class="phonetic-alphabet">{{
+                    item.yinbiao || ""
+                  }}</view>
+                </view>
+                <view class="item-explain">
+                  <view class="item-explain-content">
+                    <view
+                      class="explain-text"
+                      v-for="(meaning, meaningIndex) in item.jianyi"
+                      :key="meaningIndex"
+                      >{{ meaning }}</view
+                    >
+                  </view>
+                </view>
+                <view class="item-arrow">
+                  <icon></icon>
+                </view>
+              </view>
+            </view>
+            <!-- 没有单词 -->
+            <view class="no-word-box" v-else> 暂无单词 </view>
+          </swiper-item>
+        </swiper>
+      </view>
+    </view>
+	<tip-small-dialog ref="goPayDialogRef" @confirm-btn="goPayPage" :content="tipContent"></tip-small-dialog>
+  </view>
+</template>
+
+<script setup>
+import { reactive, ref, nextTick, computed } from "vue";
+import {
+  toast,
+  getDataFromStr,
+  useActiveDomIntoView,
+  isTargetInSameGroup,
+} from "@/utils/common";
+import { onLoad } from "@dcloudio/uni-app";
+import { getWordZhangList } from "@/api/chaojidanci.js";
+import cacheManager from "@/utils/cacheManager.js";
+import { useSwiper } from "@/utils/useSwiper";
+import tipSmallDialog from "@/components/dialog/tipSmallDialog.vue";
+
+
+const goPayDialogRef = ref(null);
+const listData = reactive({
+  danyuanNumberList: [], //节名称
+  title: "", // 版本+年级+学期
+  wordList: [], // 单词列表,默认值设为空数组
+  activeIndex: 0,
+});
+let danyuanId = ref(null);
+let routerOpt = ref(false);
+let wordLeft = ref(0);
+const unitList = ref([]);
+const info = reactive({
+  levelId: null,
+  zhangId: null,
+});
+const tipContent = '是否前往开通付费?';
+
+
+const { swiperData, handleTouchStart, handleTouchEnd, userCode } = useSwiper(
+  listData,
+  (userCode) => { },
+);
+
+onLoad((options) => {
+  routerOpt = options;
+  // 非游客
+  danyuanId.value = routerOpt.danyuanId;
+  getWordListData();
+});
+
+// Swiper 切换
+function handleSwiperChange(e) {
+  listData.activeIndex = e.detail.current;
+  if (!swiperData.isAllowed) {
+    // 不满足条件时回退到原索引
+    nextTick(() => {
+      listData.activeIndex = 0; // 强制回退
+    });
+    swiperData.isAllowed = false; // 重置状态
+  }
+  findWordLeft(listData.activeIndex);
+  updataShuju(listData.danyuanNumberList[listData.activeIndex]);
+}
+
+
+// 返回
+function goBack() {
+  uni.redirectTo({
+    url: `/pages/study/index`,
+  });
+}
+
+// 修改缓存zid  用于定位岛
+function updataCache(data) {
+  cacheManager.updateObject("zhangInfo", {
+    curZid: data,
+  });
+}
+
+//tab click
+function handleTitleClick(item) {
+ updataShuju(item);
+}
+
+// 修改数据 &&定位岛
+function updataShuju(data) {
+  // 非游客
+  danyuanId.value = data.danyuanId;
+  // 修改缓存zid  用于定位岛
+  updataCache(data.zhangZid);
+  getWordListData();
+}
+
+// 返回节index
+function findIndexByDanyuanId(list, danyuanId) {
+  const findIndex = list.findIndex((item) => item.danyuanId == danyuanId);
+  // unit滚动到指定位置
+  findWordLeft(findIndex);
+  return findIndex;
+}
+
+// 获取滚动距离
+function findWordLeft(data) {
+  useActiveDomIntoView(".word-title-box", ".title-item.active");
+}
+
+function getWordListData() {
+  const opt = {
+    danyuanId: 244, // routerOpt.danyuanId
+	  banbenId: 39, // routerOpt.banbenId
+  };
+  getWordZhangList(opt)
+    .then((res) => {
+      if (res.code === 0) {
+        listData.title = res.data.title;
+        unitList.value = res.data.danyuanWordsList;
+        listData.danyuanNumberList = res.data.danyuanNumberList;
+        unitList.value.forEach((item) => {
+          if (item.danyuanId == danyuanId.value) {
+            listData.activeIndex = findIndexByDanyuanId(
+              listData.danyuanNumberList,
+              danyuanId.value
+            );
+          }
+        });
+      }
+    })
+    .catch((err) => {
+		console.log('err',err)
+      toast("获取单词列表数据失败");
+    });
+}
+
+// 单词
+function toWord(data) {
+ uni.redirectTo({
+   url: `/pages/chaojidanci/newEnglish/index?danyuanId=${danyuanId.value}&wordId=${data.id}`,
+ });
+}
+
+// 去支付
+function goPayPage() {
+  let zhangInfoLocal = cacheManager.get("zhangInfo");
+  if (!zhangInfoLocal.cardId) {
+    toast("cardId 丢失请重新选择学科LevelId");
+    return false;
+  }
+  uni.redirectTo({
+    url:
+      "/pages/mall/mallPage?cardId=" +
+      zhangInfoLocal.cardId +
+      "&from=daoPage" +
+      "&subjectId=" +
+      zhangInfoLocal.subjectId,
+  });
+}
+</script>

+ 58 - 0
utils/common.js

@@ -1,4 +1,5 @@
 import cacheManager from "./cacheManager.js"
+import {nextTick} from "vue";
 
 /**
 * 显示消息提示框
@@ -203,4 +204,61 @@ export function useSelectDateForUpdate(code) {
 		isNowDate,
 		resetDate
 	}
+}
+
+export function getDataFromStr(strdata) {
+	if (!strdata) {
+		return []
+	}
+	return strdata.toString().split(',')
+}
+
+export function isTargetInSameGroup(arr, currentElement, targetDanyuanId) {
+  // 1. 参数校验
+  if (!Array.isArray(arr) || arr.length === 0) return false;
+  if (!currentElement || !targetDanyuanId) return false;
+
+  // 2. 将数组拆分为4个一组的二维数组
+  const chunkSize = 4;
+  const groupedArray = [];
+  for (let i = 0; i < arr.length; i += chunkSize) {
+    groupedArray.push(arr.slice(i, i + chunkSize));
+  }
+
+  // 3. 查找包含当前元素的子数组
+  const currentGroup = groupedArray.find(group => 
+    group.some(item => item.danyuanId === currentElement.danyuanId)
+  );
+
+  // 4. 判断目标jieId是否在该子数组中
+  if (!currentGroup) return false;
+  return currentGroup.some(item => item.danyuanId === targetDanyuanId);
+}
+
+export function useActiveDomIntoView(classNameParent, classNameActive) {
+	nextTick(() => {
+		const container = document.querySelector(classNameParent);
+		const highlightItem = document.querySelector(classNameActive);
+
+		// 1. 检查元素是否已在可视区
+		const containerRect = container.getBoundingClientRect();
+		const activeRect = highlightItem.getBoundingClientRect();
+		const isVisible = activeRect.left >= containerRect.left &&
+			activeRect.right <= containerRect.right;
+		if (isVisible) return;
+
+		// 2. 优先使用 scrollIntoView
+		const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
+		if (supportsSmoothScroll) {
+			highlightItem.scrollIntoView({
+				behavior: 'smooth',
+				inline: 'center',
+				block: 'nearest'
+			});
+		} else {
+			// 3. 降级方案:瞬时滚动 + 手动动画
+			const targetPos = highlightItem.offsetLeft - container.offsetLeft;
+			container.scrollTo({ left: targetPos });
+		}
+	});
 }