Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
AI_BOOK_TOOL
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
template admin
AI_BOOK_TOOL
Commits
214b4d19
Commit
214b4d19
authored
Aug 02, 2025
by
liujiangnan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: AI 点选翻译
parent
f1c15699
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
954 additions
and
134 deletions
+954
-134
index.css
play/index.css
+307
-0
index.html
play/index.html
+110
-0
index.js
play/index.js
+537
-134
No files found.
play/index.css
View file @
214b4d19
...
...
@@ -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
;
...
...
play/index.html
View file @
214b4d19
...
...
@@ -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>
<!-- 控制按钮 -->
...
...
play/index.js
View file @
214b4d19
//对应模板的名称,这个可以不写,但是最好是能写上,同时开发多个模板的情况下如果不写这个,本地缓存可能会存在混乱
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
;
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment