Browse Source

refactor(audio): 提取音频工具方法到独立模块并优化缓存验证

将音频相关工具方法从 AudioControls.vue 抽离到 audioUtils.js 模块
重构缓存验证逻辑,增加页面匹配检查
优化状态重置和高亮清除方法
统一缓存键生成和验证流程
main
前端-胡立永 16 hours ago
parent
commit
13ee23c060
4 changed files with 326 additions and 159 deletions
  1. +0
    -1
      api/modules/book.js
  2. +105
    -156
      subPages/home/AudioControls.vue
  3. +219
    -0
      utils/audioUtils.js
  4. +2
    -2
      utils/share.js

+ 0
- 1
api/modules/book.js View File

@ -40,7 +40,6 @@ export default {
url: "/books/list",
method: "GET",
data,
debounce: 200
})
}else {
return request({


+ 105
- 156
subPages/home/AudioControls.vue View File

@ -61,6 +61,16 @@
<script>
import config from '@/mixins/config.js'
import audioManager from '@/utils/audioManager.js'
import {
splitTextIntelligently,
formatTime,
generateCacheKey,
validateCacheData,
findFirstNonLeadAudio,
limitCacheSize,
clearAudioCache,
resetAudioState
} from '@/utils/audioUtils.js'
export default {
name: 'AudioControls',
@ -162,7 +172,7 @@ export default {
//
hasCurrentPageCache() {
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const cachedData = this.audioCache[cacheKey];
//
@ -170,20 +180,8 @@ export default {
return false;
}
// ID
if (cachedData.voiceId && cachedData.voiceId !== this.localVoiceId) {
// console.warn(':', cachedData.voiceId, '!=', this.localVoiceId);
return false;
}
// URL
const firstAudio = cachedData.audios[0];
if (!firstAudio || !firstAudio.url) {
// console.warn('');
return false;
}
return true;
// 使
return validateCacheData(cachedData, this.currentPage);
},
//
@ -200,7 +198,7 @@ export default {
// true
if (this.isPreloading) {
//
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const hasCache = this.audioCache[cacheKey] && this.audioCache[cacheKey].audios && this.audioCache[cacheKey].audios.length > 0;
//
@ -272,24 +270,57 @@ export default {
}
},
methods: {
// ==================== ====================
//
findFirstNonLeadAudio() {
if (!this.currentPageAudios || this.currentPageAudios.length === 0) {
return -1;
}
return findFirstNonLeadAudio(this.currentPageAudios);
},
//
for (let i = 0; i < this.currentPageAudios.length; i++) {
const audioData = this.currentPageAudios[i];
if (audioData && !audioData.isLead) {
console.log(`🎵 findFirstNonLeadAudio: 找到第一个非导语音频,索引=${i}, isLead=${audioData.isLead}`);
return i;
}
//
formatTime(seconds) {
return formatTime(seconds);
},
/**
* 重置音频播放状态
* @param {boolean} clearHighlight - 是否清除高亮索引默认true
* @param {boolean} emitEvent - 是否发送状态变化事件默认true
*/
resetPlaybackState(clearHighlight = true, emitEvent = true) {
this.isPlaying = false;
this.currentTime = 0;
this.sliderValue = 0;
if (clearHighlight) {
this.currentHighlightIndex = -1;
}
if (emitEvent) {
this.$emit('audio-state-change', {
hasAudioData: this.hasAudioData,
isLoading: this.isAudioLoading,
currentHighlightIndex: this.currentHighlightIndex
});
}
},
// -1
console.log('🎵 findFirstNonLeadAudio: 所有音频都是导语,返回 -1 不播放');
return -1;
/**
* 清除高亮索引并发送事件
*/
clearHighlight() {
this.currentHighlightIndex = -1;
this.emitHighlightChange(-1);
},
/**
* 初始化音频索引为第一个非导语音频
* @returns {number} 设置的音频索引
*/
initializeAudioIndex() {
const firstNonLeadIndex = this.findFirstNonLeadAudio();
this.currentAudioIndex = firstNonLeadIndex >= 0 ? firstNonLeadIndex : 0;
return this.currentAudioIndex;
},
//
@ -312,9 +343,11 @@ export default {
}
//
const pageKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const pageKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const cachedAudio = this.audioCache[pageKey];
console.log(`🎵 checkAndLoadPreloadedAudio: 检查缓存,页面=${this.currentPage}, 缓存键=${pageKey}, 有缓存=${!!cachedAudio}`);
if (cachedAudio && cachedAudio.audios && cachedAudio.audios.length > 0) {
//
this.currentPageAudios = cachedAudio.audios;
@ -323,12 +356,21 @@ export default {
this.isAudioLoading = false;
//
const firstNonLeadIndex = this.findFirstNonLeadAudio();
this.currentAudioIndex = firstNonLeadIndex;
this.initializeAudioIndex();
this.currentTime = 0;
this.currentHighlightIndex = -1;
this.clearHighlight();
console.log(`🎵 checkAndLoadPreloadedAudio: 从缓存加载音频,页面=${this.currentPage}, 音频数量=${this.currentPageAudios.length}`);
console.log(`🎵 checkAndLoadPreloadedAudio: 从缓存加载音频,页面=${this.currentPage}, 音频数量=${this.currentPageAudios.length}, 缓存页面=${cachedAudio.pageNumber || '未知'}`);
//
if (cachedAudio.pageNumber && cachedAudio.pageNumber !== this.currentPage) {
console.error(`🚨 缓存数据页面不匹配!当前页面=${this.currentPage}, 缓存页面=${cachedAudio.pageNumber}`);
//
delete this.audioCache[pageKey];
//
this.getCurrentPageAudio(true);
return;
}
//
this.$emit('audio-state-change', {
@ -372,91 +414,9 @@ export default {
}
},
//
splitTextIntelligently(text) {
if (!text || typeof text !== 'string') {
return [text];
}
//
const isChinese = /[\u4e00-\u9fa5]/.test(text);
const maxLength = isChinese ? 100 : 200;
//
if (text.length <= maxLength) {
return [{
text: text,
startIndex: 0,
endIndex: text.length - 1
}];
}
const segments = [];
let currentText = text;
let globalStartIndex = 0;
while (currentText.length > 0) {
if (currentText.length <= maxLength) {
//
segments.push({
text: currentText,
startIndex: globalStartIndex,
endIndex: globalStartIndex + currentText.length - 1
});
break;
}
//
let splitIndex = maxLength;
let bestSplitIndex = -1;
//
for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) {
const char = currentText[i];
if (char === '。' || char === '.') {
bestSplitIndex = i + 1; //
break;
}
}
//
if (bestSplitIndex === -1) {
for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) {
const char = currentText[i];
if (char === ',' || char === ',' || char === ';' || char === ';') {
bestSplitIndex = i + 1; //
break;
}
}
}
// 使
if (bestSplitIndex === -1) {
bestSplitIndex = maxLength;
}
//
const segment = currentText.substring(0, bestSplitIndex).trim();
if (segment.length > 0) {
segments.push({
text: segment,
startIndex: globalStartIndex,
endIndex: globalStartIndex + segment.length - 1
});
}
//
currentText = currentText.substring(bestSplitIndex).trim();
globalStartIndex += bestSplitIndex;
}
return segments;
},
//
async requestAudioInBatches(text, voiceType) {
const segments = this.splitTextIntelligently(text);
const segments = splitTextIntelligently(text);
const audioSegments = [];
let totalDuration = 0;
const requestId = this.currentRequestId; // ID
@ -585,7 +545,7 @@ export default {
this.currentAudioIndex = 0;
this.currentTime = 0;
this.totalTime = 0;
this.currentHighlightIndex = -1;
this.clearHighlight();
//
this.$emit('audio-state-change', {
@ -609,7 +569,7 @@ export default {
console.log('this.audioCache:', this.audioCache);
//
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
if (this.audioCache[cacheKey]) {
//
@ -617,10 +577,8 @@ export default {
this.totalTime = this.audioCache[cacheKey].totalDuration;
//
const firstNonLeadIndex = this.findFirstNonLeadAudio();
this.currentAudioIndex = firstNonLeadIndex;
this.isPlaying = false;
this.currentTime = 0;
this.initializeAudioIndex();
this.resetPlaybackState(false, false);
this.hasAudioData = true;
this.isAudioLoading = false;
@ -796,11 +754,12 @@ export default {
await this.calculateTotalDuration();
//
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
this.audioCache[cacheKey] = {
audios: [...this.currentPageAudios], //
totalDuration: this.totalTime,
voiceId: this.localVoiceId, // ID
pageNumber: this.currentPage, //
timestamp: Date.now() //
};
@ -871,7 +830,7 @@ export default {
//
this.audioLoadFailed = false;
//
const pageKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const pageKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
if (this.audioCache[pageKey]) {
delete this.audioCache[pageKey];
@ -889,15 +848,11 @@ export default {
audioManager.stopCurrentAudio();
this.currentAudio = null;
//
this.currentAudioIndex = 0;
this.isPlaying = false;
this.currentTime = 0;
// 使
this.resetPlaybackState(true, false);
this.totalTime = 0;
this.sliderValue = 0;
this.isAudioLoading = false;
this.audioLoadFailed = false;
this.currentHighlightIndex = -1;
this.playSpeed = 1.0;
//
@ -916,14 +871,14 @@ export default {
//
loadCachedAudioData() {
const cacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const cachedData = this.audioCache[cacheKey];
//
if (!cachedData || !cachedData.audios || cachedData.audios.length === 0) {
console.warn('缓存数据不存在或为空:', cacheKey);
// 使
if (!validateCacheData(cachedData, this.currentPage)) {
console.warn('缓存数据不存在或无效:', cacheKey);
uni.showToast({
title: '缓存音频数据不存在',
title: '缓存音频数据无效',
icon: 'none'
});
return;
@ -961,7 +916,7 @@ export default {
this.hasAudioData = true;
this.isAudioLoading = false;
this.audioLoadFailed = false;
this.currentHighlightIndex = -1;
this.clearHighlight();
//
this.$emit('audio-state-change', {
@ -1178,7 +1133,7 @@ export default {
}
//
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const audioCacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const currentPageCache = this.audioCache[audioCacheKey];
if (!currentPageCache || !currentPageCache.audios.includes(currentAudioData)) {
@ -1210,7 +1165,7 @@ export default {
audioManager.pause();
this.isPlaying = false;
//
this.currentHighlightIndex = -1;
this.clearHighlight();
//
this.emitHighlightChange(-1);
},
@ -1608,7 +1563,7 @@ export default {
}
//
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const audioCacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const currentPageCache = this.audioCache[audioCacheKey];
//
@ -1676,7 +1631,7 @@ export default {
}
//
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const audioCacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const currentPageCache = this.audioCache[audioCacheKey];
//
@ -2259,12 +2214,6 @@ export default {
}
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
},
//
clearAudioCache() {
this.audioCache = {};
@ -2679,7 +2628,7 @@ export default {
//
if (pageData && pageData.length > 0) {
const hasTextContent = pageData.some(item => item.type === 'text' && item.content);
const cacheKey = `${this.courseId}_${i + 1}_${this.voiceId}`;
const cacheKey = generateCacheKey(this.courseId, i + 1, this.localVoiceId);
const isAlreadyCached = this.audioCache[cacheKey];
if (hasTextContent && !isAlreadyCached) {
@ -2696,7 +2645,7 @@ export default {
//
async preloadPageAudio(pageIndex, pageData) {
const cacheKey = `${this.courseId}_${pageIndex + 1}_${this.voiceId}`;
const cacheKey = generateCacheKey(this.courseId, pageIndex + 1, this.localVoiceId);
//
if (this.audioCache[cacheKey]) {
@ -2773,6 +2722,7 @@ export default {
audios: audioArray,
totalDuration: totalDuration,
voiceId: this.localVoiceId, // ID
pageNumber: pageIndex + 1, //
timestamp: Date.now() //
};
@ -2785,13 +2735,13 @@ export default {
//
checkAudioCache(pageNumber) {
const cacheKey = `${this.courseId}_${pageNumber}_${this.localVoiceId}`;
const cacheKey = generateCacheKey(this.courseId, pageNumber, this.localVoiceId);
const cachedData = this.audioCache[cacheKey];
if (cachedData && cachedData.audios && cachedData.audios.length > 0) {
return true;
if (!validateCacheData(cachedData, pageNumber)) {
return false;
}
return true;
return false;
@ -2806,11 +2756,10 @@ export default {
return;
}
const cacheKey = `${this.courseId}_${this.currentPage}_${this.voiceId}`;
const cacheKey = generateCacheKey(this.courseId, this.currentPage, this.localVoiceId);
const cachedData = this.audioCache[cacheKey];
if (!cachedData || !cachedData.audios || cachedData.audios.length === 0) {
if (!validateCacheData(cachedData, this.currentPage)) {
return;
}


+ 219
- 0
utils/audioUtils.js View File

@ -0,0 +1,219 @@
/**
* 音频相关工具方法
* AudioControls.vue 抽离的通用工具方法
*/
/**
* 智能分割文本按句号和逗号分割中英文文本
* @param {string} text - 要分割的文本
* @returns {Array} 分割后的文本段落数组
*/
export function splitTextIntelligently(text) {
if (!text || typeof text !== 'string') {
return [text];
}
// 判断是否为中文文本(包含中文字符)
const isChinese = /[\u4e00-\u9fa5]/.test(text);
const maxLength = isChinese ? 100 : 200;
// 如果文本长度不超过限制,直接返回
if (text.length <= maxLength) {
return [{
text: text,
startIndex: 0,
endIndex: text.length - 1
}];
}
const segments = [];
let currentText = text;
let globalStartIndex = 0;
while (currentText.length > 0) {
if (currentText.length <= maxLength) {
// 剩余文本不超过限制,直接添加
segments.push({
text: currentText,
startIndex: globalStartIndex,
endIndex: globalStartIndex + currentText.length - 1
});
break;
}
// 在限制长度内寻找最佳分割点
let splitIndex = maxLength;
let bestSplitIndex = -1;
// 优先寻找句号
for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) {
const char = currentText[i];
if (char === '。' || char === '.') {
bestSplitIndex = i + 1; // 包含句号
break;
}
}
// 如果没找到句号,寻找逗号
if (bestSplitIndex === -1) {
for (let i = Math.min(maxLength, currentText.length - 1); i >= Math.max(0, maxLength - 50); i--) {
const char = currentText[i];
if (char === ',' || char === ',' || char === ';' || char === ';') {
bestSplitIndex = i + 1; // 包含标点符号
break;
}
}
}
// 如果还是没找到合适的分割点,使用默认长度
if (bestSplitIndex === -1) {
bestSplitIndex = maxLength;
}
// 提取当前段落
const segment = currentText.substring(0, bestSplitIndex).trim();
if (segment.length > 0) {
segments.push({
text: segment,
startIndex: globalStartIndex,
endIndex: globalStartIndex + segment.length - 1
});
}
// 更新剩余文本和全局索引
currentText = currentText.substring(bestSplitIndex).trim();
globalStartIndex += bestSplitIndex;
}
return segments;
}
/**
* 格式化时间显示
* @param {number} seconds - 秒数
* @returns {string} 格式化后的时间字符串 (mm:ss)
*/
export function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
/**
* 生成音频缓存键
* @param {string} courseId - 课程ID
* @param {number} pageNumber - 页面号码
* @param {string} voiceId - 音色ID
* @returns {string} 缓存键
*/
export function generateCacheKey(courseId, pageNumber, voiceId) {
return `${courseId}_${pageNumber}_${voiceId}`;
}
/**
* 验证缓存数据的有效性
* @param {Object} cachedData - 缓存的音频数据
* @param {number} expectedPage - 期望的页面号码
* @returns {boolean} 缓存数据是否有效
*/
export function validateCacheData(cachedData, expectedPage) {
if (!cachedData || !cachedData.audios || cachedData.audios.length === 0) {
return false;
}
// 检查页面号码匹配
if (expectedPage !== null && cachedData.pageNumber && cachedData.pageNumber !== expectedPage) {
return false;
}
// 检查音频URL有效性
const firstAudio = cachedData.audios[0];
if (!firstAudio || !firstAudio.url) {
return false;
}
return true;
}
/**
* 查找第一个非导语音频的索引
* @param {Array} audioArray - 音频数组
* @returns {number} 第一个非导语音频的索引如果没有找到返回-1
*/
export function findFirstNonLeadAudio(audioArray) {
if (!audioArray || audioArray.length === 0) {
return -1;
}
// 从第一个音频开始查找非导语音频
for (let i = 0; i < audioArray.length; i++) {
const audioData = audioArray[i];
if (audioData && !audioData.isLead) {
return i;
}
}
// 如果所有音频都是导语,返回 -1 表示不播放
return -1;
}
/**
* 限制缓存大小保留最近访问的页面
* @param {Object} audioCache - 音频缓存对象
* @param {number} maxSize - 最大缓存数量默认10
* @returns {Object} 清理后的缓存对象
*/
export function limitCacheSize(audioCache, maxSize = 10) {
const cacheKeys = Object.keys(audioCache);
if (cacheKeys.length > maxSize) {
// 删除最旧的缓存项
const keysToDelete = cacheKeys.slice(0, cacheKeys.length - maxSize);
keysToDelete.forEach(key => {
delete audioCache[key];
});
}
return audioCache;
}
/**
* 清理音频缓存
* @param {Object} audioCache - 音频缓存对象
* @returns {Object} 清空的缓存对象
*/
export function clearAudioCache(audioCache) {
return {};
}
/**
* 音频状态重置工具
* @param {Object} audioState - 音频状态对象
* @param {boolean} clearHighlight - 是否清除高亮索引默认true
* @returns {Object} 重置后的状态对象
*/
export function resetAudioState(audioState, clearHighlight = true) {
const resetState = {
...audioState,
isPlaying: false,
currentTime: 0,
sliderValue: 0
};
if (clearHighlight) {
resetState.currentHighlightIndex = -1;
}
return resetState;
}
/**
* 检查音频数据是否属于当前页面
* @param {Object} audioData - 音频数据
* @param {string} expectedCacheKey - 期望的缓存键
* @returns {boolean} 是否属于当前页面
*/
export function isAudioDataForCurrentPage(audioData, expectedCacheKey) {
if (!audioData || !audioData.cacheKey) {
return false;
}
return audioData.cacheKey === expectedCacheKey;
}

+ 2
- 2
utils/share.js View File

@ -94,5 +94,5 @@ function addQueryParams(url) {
}
}
export default function(){}
// export default share
// export default function(){}
export default share

Loading…
Cancel
Save