Commit 214b4d19 authored by liujiangnan's avatar liujiangnan

feat: AI 点选翻译

parent f1c15699
......@@ -193,6 +193,38 @@ body {
background-color: rgba(76, 175, 80, 1);
}
/* AI翻译按钮样式 */
.translate-btn {
position: absolute;
top: 20px;
right: 120px;
z-index: 100;
padding: 10px 20px;
border: none;
border-radius: 25px;
background-color: rgba(255, 255, 255, 0.9);
color: #333;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.translate-btn:hover {
background-color: rgba(255, 255, 255, 1);
transform: scale(1.05);
}
.translate-btn.active {
background-color: rgba(156, 39, 176, 0.9);
color: white;
}
.translate-btn.active:hover {
background-color: rgba(156, 39, 176, 1);
}
/* 点读模式下的可点击矩形框 */
.highlight-rect.clickable {
pointer-events: auto;
......@@ -217,6 +249,281 @@ body {
box-shadow: 0 0 10px rgba(255, 193, 7, 0.6);
}
/* 翻译模式下的单词框 */
.translate-word-box {
position: absolute;
border: 2px dashed rgba(156, 39, 176, 0.6);
background-color: rgba(156, 39, 176, 0.1);
pointer-events: none;
z-index: 15;
transition: all 0.2s ease;
}
.translate-word-box.clickable {
pointer-events: auto;
cursor: pointer;
border: 2px dashed rgba(156, 39, 176, 0.8);
background-color: rgba(156, 39, 176, 0.15);
transition: all 0.2s ease;
}
.translate-word-box.clickable:hover {
background-color: rgba(156, 39, 176, 0.3);
border-color: rgba(156, 39, 176, 1);
transform: scale(1.02);
}
/* 翻译气泡框样式 */
.translate-bubble {
position: absolute;
width: 600px;
max-height: 400px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 1px solid #e0e0e0;
z-index: 1000;
overflow: hidden;
}
.bubble-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.bubble-title {
font-weight: bold;
color: #333;
font-size: 14px;
}
.ai-provider {
font-weight: normal;
color: #666;
font-size: 12px;
}
.bubble-close {
background: none;
border: none;
font-size: 18px;
color: #666;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.bubble-close:hover {
background-color: #e0e0e0;
color: #333;
}
.bubble-content {
padding: 16px;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
display: flex;
flex-direction: column;
}
.translate-word {
font-weight: bold;
color: #333;
font-size: 16px;
margin-bottom: 8px;
padding: 4px 8px;
background-color: #f0f0f0;
border-radius: 4px;
display: inline-block;
}
.translate-result {
color: #666;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
padding-right: 8px; /* 为滚动条留出空间 */
margin: 0;
min-height: 20px;
flex: 1;
}
/* 可视化翻译结果样式 */
.translate-visual {
flex: 1;
overflow-y: auto;
padding-right: 8px;
}
.word-section {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.word-display {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.phonetic-section {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.phonetic-item {
display: flex;
align-items: center;
gap: 4px;
}
.phonetic-label {
font-size: 12px;
color: #666;
font-weight: 500;
}
.phonetic-text {
font-size: 12px;
color: #007bff;
font-family: 'Courier New', monospace;
}
.translation-section {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.part-of-speech,
.contextual-translation,
.general-translations {
margin-bottom: 8px;
display: flex;
align-items: flex-start;
gap: 8px;
}
.pos-label,
.translation-label {
font-size: 12px;
color: #666;
font-weight: 500;
min-width: 60px;
}
.pos-text {
font-size: 14px;
color: #007bff;
font-weight: 500;
}
.translation-text {
font-size: 14px;
color: #333;
flex: 1;
}
.section-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 2px solid #007bff;
}
.syntax-section,
.phrases-section,
.examples-section,
.usage-section {
margin-bottom: 16px;
}
.syntax-content {
font-size: 13px;
line-height: 1.6;
color: #555;
text-align: justify;
}
.phrases-list,
.examples-list,
.usage-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.phrase-item,
.example-item {
padding: 8px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #007bff;
}
.phrase-english,
.example-english {
font-size: 13px;
color: #333;
font-weight: 500;
margin-bottom: 4px;
}
.phrase-translation,
.example-translation {
font-size: 12px;
color: #666;
}
.usage-item {
font-size: 12px;
color: #555;
line-height: 1.5;
padding: 4px 0;
border-bottom: 1px solid #f0f0f0;
}
.usage-item:last-child {
border-bottom: none;
}
/* 自定义滚动条样式 */
.bubble-content::-webkit-scrollbar {
width: 8px;
}
.bubble-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.bubble-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.bubble-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 调试信息样式 */
.debug-info {
position: absolute;
......
......@@ -36,6 +36,16 @@
{{ isPointReadMode ? '退出点读' : '点读' }}
</button>
<!-- AI翻译按钮 -->
<button
class="translate-btn"
:class="{ active: isTranslateMode }"
@click="toggleTranslateMode"
title="AI翻译模式"
>
{{ isTranslateMode ? '退出翻译' : 'AI翻译' }}
</button>
<!-- 图片容器 -->
<div class="image-container">
<img
......@@ -69,6 +79,106 @@
class="word-highlight"
:style="getWordStyle(word)"
></div>
<!-- 翻译模式下的单词框 -->
<div
v-for="(word, index) in translateWords"
:key="'translate-word-' + index"
class="translate-word-box"
:class="{ clickable: isTranslateMode }"
:style="getWordStyle(word)"
@click="isTranslateMode ? showTranslateBubble(word, index) : null"
></div>
<!-- 翻译气泡框 -->
<div
v-if="translateBubble.show"
class="translate-bubble"
:style="translateBubble.style"
>
<div class="bubble-header">
<span class="bubble-title">AI翻译 <span class="ai-provider">(阿里云通义千问 qwen-plus)</span></span>
<button class="bubble-close" @click="closeTranslateBubble">×</button>
</div>
<div class="bubble-content">
<!-- 原始JSON显示(调试用) -->
<div v-if="translateBubble.result && !translateBubble.parsedData" class="translate-result" ref="translateResult">
{{ translateBubble.result }}
</div>
<!-- 可视化翻译结果 -->
<div v-if="translateBubble.parsedData" class="translate-visual">
<!-- 单词和音标 -->
<div class="word-section">
<div class="word-display">{{ translateBubble.parsedData.word }}</div>
<div class="phonetic-section">
<span class="phonetic-item">
<span class="phonetic-label">美音:</span>
<span class="phonetic-text">{{ translateBubble.parsedData.us_phonetic }}</span>
</span>
<span class="phonetic-item">
<span class="phonetic-label">英音:</span>
<span class="phonetic-text">{{ translateBubble.parsedData.uk_phonetic }}</span>
</span>
</div>
</div>
<!-- 词性和翻译 -->
<div class="translation-section">
<div class="part-of-speech">
<span class="pos-label">词性:</span>
<span class="pos-text">{{ translateBubble.parsedData.part_of_speech }}</span>
</div>
<div class="contextual-translation">
<span class="translation-label">语境翻译:</span>
<span class="translation-text">{{ translateBubble.parsedData.contextual_translation }}</span>
</div>
<div class="general-translations">
<span class="translation-label">一般翻译:</span>
<span class="translation-text">{{ translateBubble.parsedData.general_translations.join(', ') }}</span>
</div>
</div>
<!-- 语法分析 -->
<div class="syntax-section">
<div class="section-title">语法分析</div>
<div class="syntax-content">{{ translateBubble.parsedData.syntax_analysis }}</div>
</div>
<!-- 常用短语 -->
<div v-if="translateBubble.parsedData.common_phrases && translateBubble.parsedData.common_phrases.length > 0" class="phrases-section">
<div class="section-title">常用短语</div>
<div class="phrases-list">
<div v-for="(phrase, index) in translateBubble.parsedData.common_phrases" :key="index" class="phrase-item">
<div class="phrase-english">{{ phrase.phrase }}</div>
<div class="phrase-translation">{{ phrase.translation }}</div>
</div>
</div>
</div>
<!-- 例句 -->
<div v-if="translateBubble.parsedData.example_usage && translateBubble.parsedData.example_usage.length > 0" class="examples-section">
<div class="section-title">例句</div>
<div class="examples-list">
<div v-for="(example, index) in translateBubble.parsedData.example_usage" :key="index" class="example-item">
<div class="example-english">{{ example.example }}</div>
<div class="example-translation">{{ example.translation }}</div>
</div>
</div>
</div>
<!-- 用法说明 -->
<div v-if="translateBubble.parsedData.usage_notes && translateBubble.parsedData.usage_notes.length > 0" class="usage-section">
<div class="section-title">用法说明</div>
<div class="usage-list">
<div v-for="(note, index) in translateBubble.parsedData.usage_notes" :key="index" class="usage-item">
{{ note }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 控制按钮 -->
......
//对应模板的名称,这个可以不写,但是最好是能写上,同时开发多个模板的情况下如果不写这个,本地缓存可能会存在混乱
var t_name = 'AI_BOOK_TOOL';
$(function() {
window.courseware.getData(function(data) {
$(function () {
window.courseware.getData(function (data) {
if (!data) {
// 修正默认数据路径为根目录
$.getJSON('./defaultData_1.json', function(defaultData) {
$.getJSON('./defaultData_1.json', function (defaultData) {
console.log('加载默认数据:', defaultData);
initializeApp(defaultData);
}).fail(function() {
}).fail(function () {
console.error('默认数据加载失败!');
});
} else {
......@@ -26,65 +26,91 @@ function initializeApp(data) {
bookData: data,
pages: data.pages || [],
audioUrl: data.audio_url,
// 状态相关
loading: false,
error: null,
isPlaying: false,
currentPageIndex: 0,
isPointReadMode: false,
isTranslateMode: false,
currentPlayingRect: null,
// 音频相关
audio: null,
currentTime: 0,
// 图片相关
imageWidth: 0,
imageHeight: 0,
imageLoaded: false,
// 高亮相关
activeRects: [],
activeWords: [],
// 翻译相关
translateWords: [],
translateBubble: {
show: false,
word: '',
result: '',
parsedData: null,
style: {}
},
// 定时器
audioTimer: null,
highlightTimer: null
highlightTimer: null,
// AI_SERVER_URL: 'http://localhost:1339',
AI_SERVER_URL: 'https://stagingopensource.iteachabc.com',
},
computed: {
totalPages() {
return this.pages.length;
},
currentPage() {
return this.pages[this.currentPageIndex] || null;
},
currentPageStartTime() {
return this.currentPage ? this.currentPage.start : 0;
},
currentPageEndTime() {
return this.currentPage ? this.currentPage.end : 0;
}
},
mounted() {
// 根据 staging 字符串,判断是否为 staging 环境
const isStaging = window.location.href.includes('staging');
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
if (isLocalhost) {
this.AI_SERVER_URL = 'http://localhost:1339';
} else if (isStaging) {
this.AI_SERVER_URL = 'https://stagingopensource.iteachabc.com';
} else {
this.AI_SERVER_URL = 'https://opensource.iteachabc.com';
}
console.log('Vue组件已挂载,数据:', this.bookData);
this.initAudio();
this.loadCurrentPage();
// 监听窗口大小变化,重新计算高亮位置
window.addEventListener('resize', this.onWindowResize);
},
beforeDestroy() {
this.cleanup();
window.removeEventListener('resize', this.onWindowResize);
},
methods: {
// 初始化音频
initAudio() {
......@@ -92,60 +118,60 @@ function initializeApp(data) {
this.error = '音频文件地址无效';
return;
}
console.log('初始化音频:', this.audioUrl);
this.audio = new Audio(this.audioUrl);
this.audio.preload = 'auto';
this.audio.addEventListener('loadeddata', () => {
console.log('音频加载完成');
});
this.audio.addEventListener('error', (e) => {
console.error('音频加载失败:', e);
this.error = '音频加载失败';
});
this.audio.addEventListener('ended', () => {
this.onAudioEnded();
});
this.audio.addEventListener('timeupdate', () => {
this.onAudioTimeUpdate();
});
},
// 音频时间更新
onAudioTimeUpdate() {
if (!this.audio) return;
this.currentTime = this.audio.currentTime;
this.updateHighlights();
// 在点读模式下不更新页面
if (!this.isPointReadMode) {
this.updatePageByTime();
}
// 每5秒输出一次调试信息
if (Math.floor(this.currentTime) % 5 === 0 && this.currentTime > 0) {
console.log('当前时间:', this.currentTime, '当前页面:', this.currentPageIndex);
}
},
// 音频结束
onAudioEnded() {
this.isPlaying = false;
this.currentPlayingRect = null;
// 清除定时器
if (this.audioTimer) {
clearTimeout(this.audioTimer);
this.audioTimer = null;
}
// 在点读模式下不重置页面
if (!this.isPointReadMode) {
// 在点读模式或翻译模式下不重置页面
if (!this.isPointReadMode && !this.isTranslateMode) {
this.currentPageIndex = 0;
this.currentTime = 0;
this.audio.currentTime = 0;
......@@ -153,16 +179,16 @@ function initializeApp(data) {
this.loadCurrentPage();
}
},
// 根据时间更新页面
updatePageByTime() {
if (!this.audio || !this.pages.length) return;
// 如果处于点读模式,不自动跳页
if (this.isPointReadMode) return;
// 如果处于点读模式或翻译模式,不自动跳页
if (this.isPointReadMode || this.isTranslateMode) return;
const currentTime = this.audio.currentTime;
// 找到当前时间对应的页面
for (let i = 0; i < this.pages.length; i++) {
const page = this.pages[i];
......@@ -176,28 +202,28 @@ function initializeApp(data) {
}
}
},
// 更新高亮
updateHighlights() {
if (!this.currentPage || !this.imageLoaded) return;
// 如果处于点读模式,不更新高亮
if (this.isPointReadMode) return;
// 如果处于点读模式或翻译模式,不更新高亮
if (this.isPointReadMode || this.isTranslateMode) return;
const currentTime = this.audio.currentTime;
const newRects = [];
const newWords = [];
// 检查是否在当前页的时间范围内
if (currentTime >= this.currentPage.start && currentTime <= this.currentPage.end) {
// 遍历当前页面的所有组
this.currentPage.groups.forEach(group => {
// 检查句子是否有有效的时间信息
if (group.start === null || group.start === undefined ||
group.end === null || group.end === undefined) {
if (group.start === null || group.start === undefined ||
group.end === null || group.end === undefined) {
return; // 跳过没有时间信息的句子
}
// 检查句子时间
if (currentTime >= group.start && currentTime <= group.end) {
// 遍历句子中的文本组
......@@ -206,7 +232,7 @@ function initializeApp(data) {
if (textGroup.rect) {
newRects.push(textGroup.rect);
}
// 检查单词时间 - 单词高亮应该保持到切换页面
if (textGroup.words) {
textGroup.words.forEach(word => {
......@@ -222,36 +248,43 @@ function initializeApp(data) {
}
});
}
// 只在有变化时更新,避免不必要的DOM操作
if (JSON.stringify(this.activeRects) !== JSON.stringify(newRects) ||
JSON.stringify(this.activeWords) !== JSON.stringify(newWords)) {
JSON.stringify(this.activeWords) !== JSON.stringify(newWords)) {
this.activeRects = newRects;
this.activeWords = newWords;
console.log('更新高亮:', newRects.length, '个矩形框,', newWords.length, '个单词');
}
},
// 清除高亮
clearHighlights() {
this.activeRects = [];
this.activeWords = [];
},
// 加载当前页面
loadCurrentPage() {
console.log('加载页面:', this.currentPageIndex, this.currentPage);
this.clearHighlights();
this.imageLoaded = false;
// 如果处于点读模式,重新显示所有矩形框
if (this.isPointReadMode) {
this.$nextTick(() => {
this.showAllRects();
});
}
// 如果处于翻译模式,重新显示所有单词框
if (this.isTranslateMode) {
this.$nextTick(() => {
this.showAllTranslateWords();
});
}
},
// 图片加载完成
onImageLoad(event) {
const img = event.target;
......@@ -259,45 +292,49 @@ function initializeApp(data) {
this.imageHeight = img.naturalHeight;
this.imageLoaded = true;
console.log('图片加载完成:', this.imageWidth, this.imageHeight);
// 如果处于点读模式,显示所有矩形框
if (this.isPointReadMode) {
this.showAllRects();
}
// 如果处于翻译模式,显示所有单词框
else if (this.isTranslateMode) {
this.showAllTranslateWords();
}
// 如果音频正在播放,立即更新高亮
else if (this.isPlaying) {
this.updateHighlights();
}
},
// 图片加载失败
onImageError() {
this.error = '图片加载失败';
},
// 获取矩形框样式
getRectStyle(rect) {
if (!this.imageLoaded || !rect) return {};
try {
const imgElement = document.querySelector('.page-image');
if (!imgElement) return {};
const imgRect = imgElement.getBoundingClientRect();
const containerRect = imgElement.parentElement.getBoundingClientRect();
// 计算图片在容器中的实际位置和尺寸
const imgLeft = imgRect.left - containerRect.left;
const imgTop = imgRect.top - containerRect.top;
const imgWidth = imgRect.width;
const imgHeight = imgRect.height;
// 根据相对坐标计算绝对位置
const left = imgLeft + parseFloat(rect.x) * imgWidth;
const top = imgTop + parseFloat(rect.y) * imgHeight;
const width = parseFloat(rect.width) * imgWidth;
const height = parseFloat(rect.height) * imgHeight;
return {
position: 'absolute',
left: left + 'px',
......@@ -310,30 +347,30 @@ function initializeApp(data) {
return {};
}
},
// 获取单词高亮样式
getWordStyle(word) {
if (!this.imageLoaded || !word || !word.rect) return {};
try {
const imgElement = document.querySelector('.page-image');
if (!imgElement) return {};
const imgRect = imgElement.getBoundingClientRect();
const containerRect = imgElement.parentElement.getBoundingClientRect();
// 计算图片在容器中的实际位置和尺寸
const imgLeft = imgRect.left - containerRect.left;
const imgTop = imgRect.top - containerRect.top;
const imgWidth = imgRect.width;
const imgHeight = imgRect.height;
// 根据相对坐标计算绝对位置
const left = imgLeft + parseFloat(word.rect.x) * imgWidth;
const top = imgTop + parseFloat(word.rect.y) * imgHeight;
const width = parseFloat(word.rect.width) * imgWidth;
const height = parseFloat(word.rect.height) * imgHeight;
return {
position: 'absolute',
left: left + 'px',
......@@ -346,14 +383,14 @@ function initializeApp(data) {
return {};
}
},
// 播放音频
playAudio() {
if (!this.audio) return;
// 如果音频暂停在当前页面,继续播放
if (this.isPlaying) return;
// 在点读模式下不自动跳转时间
if (!this.isPointReadMode) {
// 如果音频不在当前页面时间范围内,跳转到当前页面开始时间
......@@ -362,7 +399,7 @@ function initializeApp(data) {
this.audio.currentTime = this.currentPageStartTime;
}
}
this.audio.play().then(() => {
this.isPlaying = true;
console.log('开始播放音频');
......@@ -371,62 +408,68 @@ function initializeApp(data) {
this.error = '播放失败';
});
},
// 暂停音频
pauseAudio() {
if (!this.audio) return;
this.audio.pause();
this.isPlaying = false;
this.currentPlayingRect = null;
// 清除定时器
if (this.audioTimer) {
clearTimeout(this.audioTimer);
this.audioTimer = null;
}
},
// 上一页
previousPage() {
if (this.currentPageIndex > 0) {
this.currentPageIndex--;
this.loadCurrentPage();
// 在点读模式下不跳转音频时间
if (this.isPlaying && !this.isPointReadMode) {
this.audio.currentTime = this.currentPageStartTime;
}
}
},
// 下一页
nextPage() {
if (this.currentPageIndex < this.totalPages - 1) {
this.currentPageIndex++;
this.loadCurrentPage();
// 在点读模式下不跳转音频时间
if (this.isPlaying && !this.isPointReadMode) {
this.audio.currentTime = this.currentPageStartTime;
}
}
},
// 窗口大小变化处理
onWindowResize() {
// 强制重新计算高亮位置
this.$forceUpdate();
},
// 切换点读模式
togglePointReadMode() {
// 如果正在翻译模式,先退出翻译模式
if (this.isTranslateMode) {
this.isTranslateMode = false;
this.clearTranslateMode();
}
this.isPointReadMode = !this.isPointReadMode;
console.log('切换点读模式:', this.isPointReadMode ? '开启' : '关闭');
console.log('当前页面:', this.currentPage);
console.log('图片是否加载:', this.imageLoaded);
if (this.isPointReadMode) {
// 进入点读模式,显示所有矩形框
this.showAllRects();
......@@ -438,38 +481,65 @@ function initializeApp(data) {
// 退出点读模式,清除所有矩形框
this.clearHighlights();
}
console.log('点读模式:', this.isPointReadMode ? '开启' : '关闭');
},
// 切换翻译模式
toggleTranslateMode() {
// 如果正在点读模式,先退出点读模式
if (this.isPointReadMode) {
this.isPointReadMode = false;
this.clearHighlights();
}
this.isTranslateMode = !this.isTranslateMode;
console.log('切换翻译模式:', this.isTranslateMode ? '开启' : '关闭');
if (this.isTranslateMode) {
// 进入翻译模式,显示所有单词框
this.showAllTranslateWords();
// 暂停当前播放
if (this.isPlaying) {
this.pauseAudio();
}
} else {
// 退出翻译模式,清除所有单词框
this.clearTranslateMode();
}
console.log('翻译模式:', this.isTranslateMode ? '开启' : '关闭');
},
// 显示当前页面所有矩形框
showAllRects() {
console.log('showAllRects 被调用');
console.log('当前页面:', this.currentPage);
console.log('图片是否加载:', this.imageLoaded);
if (!this.currentPage || !this.imageLoaded) {
console.log('条件不满足,退出 showAllRects');
return;
}
const allRects = [];
// 遍历当前页面的所有组
this.currentPage.groups.forEach((group, groupIndex) => {
console.log(`处理组 ${groupIndex}:`, group);
// 检查句子是否有有效的时间信息
if (group.start === null || group.start === undefined ||
group.end === null || group.end === undefined) {
if (group.start === null || group.start === undefined ||
group.end === null || group.end === undefined) {
console.log(`组 ${groupIndex} 缺少时间信息,跳过:`, group);
return;
}
// 遍历句子中的文本组
group.group.forEach((textGroup, textIndex) => {
console.log(`处理文本组 ${textIndex}:`, textGroup);
// 添加文本组的矩形框
if (textGroup.rect) {
// 为每个矩形框添加额外的数据属性,包括句子信息
......@@ -488,24 +558,24 @@ function initializeApp(data) {
}
});
});
this.activeRects = allRects;
console.log('显示所有矩形框:', allRects.length, '个');
console.log('activeRects 已更新:', this.activeRects);
// 强制更新视图
this.$forceUpdate();
},
// 播放矩形框对应的音频
playRectAudio(rectData, index) {
if (!this.audio || !rectData) return;
// 获取句子信息(开始时间和结束时间)
const sentenceStart = rectData._start;
const sentenceEnd = rectData._end;
const groupIndex = rectData._groupIndex;
console.log('播放句子音频:', {
index,
sentenceStart,
......@@ -514,28 +584,28 @@ function initializeApp(data) {
sentence: rectData._sentence,
groupIndex: groupIndex
});
// 如果正在播放同一个句子,则忽略点击
if (this.isPlaying && this.currentPlayingRect &&
this.currentPlayingRect._groupIndex === groupIndex) {
if (this.isPlaying && this.currentPlayingRect &&
this.currentPlayingRect._groupIndex === groupIndex) {
console.log('正在播放同一个句子,忽略点击');
return;
}
// 清除之前的定时器
if (this.audioTimer) {
clearTimeout(this.audioTimer);
this.audioTimer = null;
}
// 设置音频时间并播放整个句子
this.audio.currentTime = sentenceStart;
this.audio.play().then(() => {
this.isPlaying = true;
this.currentPlayingRect = rectData;
console.log('开始播放句子音频,当前时间:', this.audio.currentTime);
// 设置定时器在句子结束时间停止播放
this.audioTimer = setTimeout(() => {
if (this.audio && this.isPlaying) {
......@@ -545,102 +615,102 @@ function initializeApp(data) {
this.currentPlayingRect = null;
}
}, (sentenceEnd - sentenceStart) * 1000);
}).catch(error => {
console.error('播放失败:', error);
this.error = '播放失败';
});
},
// 计算开始时间(健壮性处理)
calculateStartTime(rectData, index) {
const currentStart = rectData._start;
const currentEnd = rectData._end;
// 获取当前页面的所有矩形框数据
const allRects = this.getAllRectsData();
// 找到当前矩形框在数组中的位置
const currentIndex = allRects.findIndex(item =>
const currentIndex = allRects.findIndex(item =>
item._start === currentStart && item._end === currentEnd
);
if (currentIndex === -1) {
// 如果找不到,使用原始开始时间
return currentStart;
}
// 获取上一个矩形框的结束时间
const prevRect = currentIndex > 0 ? allRects[currentIndex - 1] : null;
const prevEnd = prevRect ? prevRect._end : currentStart;
// 开始时间往前提100毫秒
let startTime = currentStart - 0.1;
// 如果开始时间小于上一句的结束时间,则改为往前提50毫秒
if (startTime < prevEnd) {
startTime = currentStart - 0.05;
// 如果还小于上一句的结束时间,则直接取上一句的结束时间
if (startTime < prevEnd) {
startTime = prevEnd;
}
}
return startTime;
},
// 计算结束时间(直接使用句子结束时间)
calculateEndTime(rectData, index) {
// 直接返回句子的原始结束时间
return rectData._end;
},
// 高亮整个句子
highlightSentence(rectData) {
if (!rectData || !this.isPointReadMode) return;
const groupIndex = rectData._groupIndex;
console.log('高亮句子,组索引:', groupIndex);
// 找到同一句子的所有矩形框并高亮
this.activeRects.forEach(rect => {
if (rect._groupIndex === groupIndex) {
rect._highlighted = true;
}
});
// 强制更新视图
this.$forceUpdate();
},
// 清除句子高亮
clearSentenceHighlight() {
if (!this.isPointReadMode) return;
// 清除所有高亮状态
this.activeRects.forEach(rect => {
rect._highlighted = false;
});
// 强制更新视图
this.$forceUpdate();
},
// 获取当前页面所有矩形框的数据
getAllRectsData() {
if (!this.currentPage) return [];
const allRects = [];
// 遍历当前页面的所有组
this.currentPage.groups.forEach(group => {
// 检查句子是否有有效的时间信息
if (group.start === null || group.start === undefined ||
group.end === null || group.end === undefined) {
if (group.start === null || group.start === undefined ||
group.end === null || group.end === undefined) {
return; // 跳过没有时间信息的句子
}
// 遍历句子中的文本组
group.group.forEach(textGroup => {
// 添加文本组的矩形框数据
......@@ -654,27 +724,360 @@ function initializeApp(data) {
}
});
});
return allRects;
},
// 获取当前页面所有单词的扁平化数组
getAllWordsFlat() {
if (!this.currentPage) return [];
const allWords = [];
// 遍历当前页面的所有组
this.currentPage.groups.forEach((group, groupIndex) => {
// 遍历句子中的文本组
group.group.forEach((textGroup, textIndex) => {
// 检查单词
if (textGroup.words) {
textGroup.words.forEach(word => {
if (word.rect) {
// 为每个单词添加额外的数据属性
const wordWithData = {
...word,
_text: word.text,
_sentence: group.sentence,
_groupIndex: groupIndex,
_textGroupIndex: textIndex
};
allWords.push(wordWithData);
}
});
}
});
});
return allWords;
},
// 显示当前页面所有翻译单词
showAllTranslateWords() {
console.log('showAllTranslateWords 被调用');
console.log('当前页面:', this.currentPage);
console.log('图片是否加载:', this.imageLoaded);
if (!this.currentPage || !this.imageLoaded) {
console.log('条件不满足,退出 showAllTranslateWords');
return;
}
const allWords = [];
// 遍历当前页面的所有组
this.currentPage.groups.forEach((group, groupIndex) => {
console.log(`处理组 ${groupIndex}:`, group);
let wordIndex = 0; // 每个groups的单词索引从0开始
// 遍历句子中的文本组
group.group.forEach((textGroup, textIndex) => {
console.log(`处理文本组 ${textIndex}:`, textGroup);
// 检查单词
if (textGroup.words) {
textGroup.words.forEach(word => {
if (word.rect) {
// 为每个单词添加额外的数据属性
const wordWithData = {
...word,
_text: word.text, // 根据schema,单词文本在text字段
_sentence: group.sentence,
_groupIndex: groupIndex,
_groupsIndex: wordIndex // 添加基于当前groups的索引
};
allWords.push(wordWithData);
console.log('添加翻译单词:', wordWithData);
wordIndex++; // 递增单词索引
}
});
}
});
});
this.translateWords = allWords;
console.log('显示所有翻译单词:', allWords.length, '个');
console.log('translateWords 已更新:', this.translateWords);
// 强制更新视图
this.$forceUpdate();
},
// 清除翻译模式
clearTranslateMode() {
this.translateWords = [];
this.closeTranslateBubble();
},
// 显示翻译气泡框
showTranslateBubble(word, index) {
if (!this.isTranslateMode || !word) return;
console.log('显示翻译气泡框:', word);
// 计算气泡框位置
const wordStyle = this.getWordStyle(word);
const bubbleStyle = this.calculateBubblePosition(wordStyle);
this.translateBubble = {
show: true,
word: word._text || word.text,
result: '翻译中...',
style: bubbleStyle
};
// 请求翻译
this.requestTranslation(word);
},
// 计算气泡框位置
calculateBubblePosition(wordStyle) {
const imgElement = document.querySelector('.page-image');
if (!imgElement) return {};
const imgRect = imgElement.getBoundingClientRect();
const containerRect = imgElement.parentElement.getBoundingClientRect();
// 计算单词在容器中的位置
const wordLeft = parseFloat(wordStyle.left);
const wordTop = parseFloat(wordStyle.top);
const wordWidth = parseFloat(wordStyle.width);
const wordHeight = parseFloat(wordStyle.height);
// 气泡框位置(在单词右侧)
const bubbleLeft = wordLeft + wordWidth + 10;
const bubbleTop = wordTop - 20;
// 确保气泡框不超出容器边界
const maxLeft = containerRect.width - 600; // 气泡框宽度约600px
const maxTop = containerRect.height - 400; // 气泡框高度约400px
const finalLeft = Math.min(bubbleLeft, maxLeft);
const finalTop = Math.max(bubbleTop, 10);
return {
position: 'absolute',
left: finalLeft + 'px',
top: finalTop + 'px',
zIndex: 1000
};
},
// 关闭翻译气泡框
closeTranslateBubble() {
this.translateBubble = {
show: false,
word: '',
result: '',
parsedData: null,
style: {}
};
},
// 请求翻译
requestTranslation(word) {
const wordText = word._text || word.text;
const sentence = word._sentence || '';
const groupsIndex = word._groupsIndex || 0;
// 真实流式请求
this.requestStreamingTranslation(wordText, sentence, groupsIndex);
},
// 真实流式翻译API调用
async requestStreamingTranslation(word, sentence, pos) {
try {
// 重置翻译结果
this.translateBubble.result = '正在翻译...';
this.translateBubble.parsedData = null;
// 滚动到底部
this.scrollToBottom();
const requestUrl = this.AI_SERVER_URL + '/open/api/ai/dpe/v1/super/translatetoc';
const requestData = {
word: word,
sentence: sentence,
pos: pos
};
await new Promise((resolve, reject) => {
// 使用 XMLHttpRequest 来处理流式响应
const xhr = new XMLHttpRequest();
xhr.open('POST', requestUrl, true);
xhr.setRequestHeader('Content-Type', 'application/json');
let lastResponseLength = 0;
let responseText = '';
xhr.onprogress = (event) => {
if (xhr.readyState === 3 || xhr.readyState === 4) {
const newData = xhr.responseText.substring(lastResponseLength);
lastResponseLength = xhr.responseText.length;
if (newData) {
responseText += newData;
// 尝试解析JSON数据
try {
const parsedData = JSON.parse(responseText);
this.translateBubble.parsedData = parsedData;
this.translateBubble.result = ''; // 隐藏原始JSON
} catch (e) {
// 如果JSON不完整,继续显示原始文本
this.translateBubble.result = responseText;
}
// 每次更新后滚动到底部
this.$nextTick(() => {
setTimeout(() => {
this.scrollToBottom();
}, 10);
});
}
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
// 翻译完成,尝试最终解析
try {
const parsedData = JSON.parse(responseText);
this.translateBubble.parsedData = parsedData;
this.translateBubble.result = ''; // 隐藏原始JSON
} catch (e) {
console.error('JSON解析失败:', e);
this.translateBubble.result = responseText;
}
this.scrollToBottom();
console.log('翻译完成:', responseText);
resolve();
} else {
const error = new Error(`HTTP error! status: ${xhr.status}`);
console.error('翻译请求失败:', error);
this.translateBubble.result = `翻译失败: HTTP ${xhr.status}`;
reject(error);
}
};
xhr.onerror = () => {
const error = new Error('Network error');
console.error('翻译网络错误:', error);
this.translateBubble.result = '翻译失败: 网络错误';
reject(error);
};
xhr.send(JSON.stringify(requestData));
});
} catch (error) {
console.error('翻译失败:', error);
this.translateBubble.result = `翻译失败: ${error.message}`;
}
},
// 滚动到底部的辅助方法
scrollToBottom() {
console.log('scrollToBottom 被调用');
this.$nextTick(() => {
// 获取滚动容器(bubble-content)
const bubbleContent = document.querySelector('.bubble-content');
if (bubbleContent) {
// 强制滚动到底部
const scrollToBottom = () => {
// 确保内容已经渲染完成
if (bubbleContent.scrollHeight > bubbleContent.clientHeight) {
bubbleContent.scrollTop = bubbleContent.scrollHeight;
console.log('强制滚动:', {
scrollTop: bubbleContent.scrollTop,
scrollHeight: bubbleContent.scrollHeight,
clientHeight: bubbleContent.clientHeight,
canScroll: bubbleContent.scrollHeight > bubbleContent.clientHeight
});
} else {
console.log('内容高度不足,无法滚动:', {
scrollHeight: bubbleContent.scrollHeight,
clientHeight: bubbleContent.clientHeight
});
}
};
// 立即滚动
scrollToBottom();
// 使用 requestAnimationFrame
requestAnimationFrame(scrollToBottom);
// 使用 setTimeout
setTimeout(scrollToBottom, 0);
// 再次确保滚动
setTimeout(scrollToBottom, 10);
setTimeout(scrollToBottom, 50);
} else {
console.log('未找到滚动容器 .bubble-content');
}
});
},
// 测试滚动功能
testScroll() {
console.log('测试滚动功能');
const bubbleContent = document.querySelector('.bubble-content');
if (bubbleContent) {
console.log('找到滚动容器:', bubbleContent);
console.log('容器样式:', window.getComputedStyle(bubbleContent));
console.log('滚动属性:', {
scrollTop: bubbleContent.scrollTop,
scrollHeight: bubbleContent.scrollHeight,
clientHeight: bubbleContent.clientHeight,
offsetHeight: bubbleContent.offsetHeight
});
// 手动设置一些测试内容
this.translateBubble.result = '测试内容\n'.repeat(50);
// 等待内容更新后再滚动
this.$nextTick(() => {
setTimeout(() => {
this.scrollToBottom();
}, 100);
});
} else {
console.log('未找到滚动容器');
}
},
// 清理资源
cleanup() {
if (this.audio) {
this.audio.pause();
this.audio = null;
}
if (this.audioTimer) {
clearTimeout(this.audioTimer);
this.audioTimer = null;
}
if (this.highlightTimer) {
clearInterval(this.highlightTimer);
this.highlightTimer = null;
}
this.currentPlayingRect = null;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment