import { useEffect, useRef, useState } from 'react'
function CombinedHighlighter() {
const AudioLightedRefs = useRef(null)
const [currentSyncTime, setCurrentSyncTime] = useState(0)
const [currentEditingId, setCurrentEditingId] = useState(null)
const [inputKeyword, setInputKeyword] = useState('')
const [keyword, setKeyword] = useState('')
const [highlightCount, setHighlightCount] = useState(0)
const [currentIndex, setCurrentIndex] = useState(0)
const highlightedRefs = useRef([])
const initialTextSegments = [
{
begin: 1360,
end: 5000,
text: '多测几个,到时候我们测试的时候到时候我们测试的时候可能也测一下这种就是到时候我们测试的时候可能也测一下这种就是到时候我们测试的时候可能也测一下这种就是到时候我们测试的时候可能也测一下这种就是到时候我们测试的时候可能也测一下这种就是到时候我们测试的时候可能也测一下这种就是可能也测一下这种就是',
uuid: '794b6a811c41449496efd4adc1691008'
},
{
begin: 5020,
end: 7100,
text: '碎的片段,比如说好几个人碎的片段,比如说好几个人碎的片段,比如说好几个人碎的片段,比如说好几个人碎的片段,比如说好几个人碎的片段,比如说好几个人碎的片段,比如说好几个人碎的片段,比如说好几个人,',
uuid: '759be4d4071e412e9dc4cb283015fdd8'
},
{
begin: 7100,
end: 12410,
text: '同时在讨论每个人只有几个字的情况下,可能会出现什么效果,其实我的效果还是比较担心的同时在讨论每个人只有几个字的情况下同时在讨论每个人只有几个字的情况下同时在讨论每个人只有几个字的情况下同时在讨论每个人只有几个字的情况下。',
uuid: 'c8a6db458b7a4ef8a44e3ef7aa1b6cff'
},
{
begin: 13610,
end: 17580,
text: '好,那看这个其前端跟内容规整那看这个其前端跟内容规整这块还有没有什么问题?那看这个其前端跟内容规整这块还有没有什么问题?那看这个其前端跟内容规整这块还有没有什么问题?那看这个其前端跟内容规整这块还有没有什么问题?这块还有没有什么问题?',
uuid: '5273e88ca9f843e59be461b41a6a7b35'
},
{
begin: 20150,
end: 21470,
text: '没有的话我们这块。',
uuid: '71422483c25d42f2aed0224ec6888e8d'
}
]
const [textSegments, setTextSegments] = useState(initialTextSegments)
const [isEdit, setIsEdit] = useState(false)
useEffect(() => {
const audioElement = AudioLightedRefs.current
const handleTimeUpdate = () => {
setCurrentSyncTime(audioElement.currentTime * 1000)
}
audioElement.addEventListener('timeupdate', handleTimeUpdate)
return () => {
audioElement.removeEventListener('timeupdate', handleTimeUpdate)
}
}, [])
useEffect(() => {
if (keyword && highlightedRefs.current.length > 0) {
const currentElement = highlightedRefs.current[currentIndex];
const previousElement = highlightedRefs.current[currentIndex - 1];
const nextElement = highlightedRefs.current[currentIndex + 1];
if (previousElement) {
previousElement.style.backgroundColor = 'yellow'
}
if (currentElement) {
currentElement.style.backgroundColor = 'orange'
currentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
if (nextElement) {
nextElement.style.backgroundColor = 'yellow'
}
}
}, [keyword, currentIndex]);
const handleInputChange = (e) => {
setInputKeyword(e.target.value)
}
const handleHighlightClick = () => {
if (inputKeyword === keyword) return
setKeyword(inputKeyword)
highlightedRefs.current = []
setCurrentIndex(0)
const count = textSegments.reduce((acc, item) => {
const regex = new RegExp(`(${inputKeyword})`, 'gi')
return acc + (item.text.match(regex)?.length || 0)
}, 0)
setHighlightCount(count)
}
const handleNext = () => {
if (highlightCount > 0) {
setCurrentIndex((prevIndex) => (prevIndex + 1) % highlightCount)
}
}
const handlePrevious = () => {
if (highlightCount > 0) {
setCurrentIndex(
(prevIndex) => (prevIndex - 1 + highlightCount) % highlightCount
)
}
}
const getHighlightedText = () => {
console.log(11111);
return textSegments.map((segment) => {
const isActive =
currentSyncTime >= segment.begin && currentSyncTime < segment.end
const isCurrentEditing = currentEditingId === segment.uuid
const parts = segment.text.split(new RegExp(`(${keyword})`, 'gi'))
return (
<span
key={segment.uuid}
contentEditable={isEdit} // 当处于编辑模式时启用编辑功能
suppressContentEditableWarning={true} // 禁止警告
onFocus={() => setCurrentEditingId(segment.uuid)} // 聚焦时设置当前编辑句子
onBlur={(e) => handleTextChange(segment.uuid, e.target.textContent)} // 失去焦点时保存修改内容
style={{
backgroundColor: isActive && !isEdit ? '#654cff' : 'transparent',
outline: 'none',
padding: '2px', // 添加一些内边距使其更易点击
cursor: isEdit ? 'text' : 'default', // 编辑模式下显示文本光标
borderBottom: isCurrentEditing ? '1px solid black' : 'none' // 在编辑模式下添加下划线
}}
>
{parts.map((part, index) =>
part.toLowerCase() === keyword.toLowerCase() ? (
<span
key={index}
ref={(el) => el && highlightedRefs.current.push(el)} // 将关键词的引用添加到 highlightedRefs 中
style={{
backgroundColor:
currentIndex === highlightedRefs.current.length
? 'orange'
: 'yellow'
}}
>
{part}
</span>
) : (
part
)
)}
</span>
)
})
}
const handleTextChange = (uuid, newContent) => {
setTextSegments((prevSegments) =>
prevSegments.map((segment) =>
segment.uuid === uuid ? { ...segment, content: newContent } : segment
)
)
}
const editClick = () => {
if (!isEdit) {
clearHightLighter()
} else {
setCurrentEditingId(null)
}
setIsEdit(!isEdit)
}
const clearHightLighter = () => {
highlightedRefs.current.forEach((item) => {
console.log('🚀 ~ highlightedRefs.current.forEach ~ item:', item)
if (item) {
item.style.backgroundColor = 'transparent'
}
})
setInputKeyword('')
setKeyword('')
setHighlightCount(0)
setCurrentIndex(0)
highlightedRefs.current = []
}
const debounce = (func, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, delay);
};
};
const debounceScrollIntoView = debounce((element) => {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
}, 300);
const lastScrolledElement = useRef(null);
const BOTTOM_OFFSET = 400;
useEffect(() => {
const activeElement = AudioLightedRefs.current[0];
if (activeElement && lastScrolledElement.current !== activeElement) {
const rect = activeElement.getBoundingClientRect();
const isInViewport =
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) - BOTTOM_OFFSET &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
if (!isInViewport) {
debounceScrollIntoView(activeElement);
lastScrolledElement.current = activeElement;
}
}
}, [currentSyncTime]);
return (
<div>
<audio ref={AudioLightedRefs} controls>
<source
src="https://www.xzmp3.com/down/941e80129a35.mp3"
type="audio/mpeg"
/>
Your browser does not support the audio element.
</audio>
<button onClick={editClick}>{isEdit ? 'Save' : 'Edit'}</button>
<div style={{ marginTop: '20px' }}>
<input
type="text"
placeholder="Enter keyword"
value={inputKeyword}
onChange={handleInputChange}
style={{ marginBottom: '10px', padding: '10px', width: '300px' }}
/>
<button onClick={handleHighlightClick} style={{ marginLeft: '10px' }}>
Highlight
</button>
<div style={{ marginTop: '20px' }}>
<button onClick={handlePrevious} disabled={highlightCount === 0}>
Previous
</button>
<button
onClick={handleNext}
disabled={highlightCount === 0}
style={{ marginLeft: '10px' }}
>
Next
</button>
<span style={{ marginLeft: '10px' }}>
{highlightCount > 0
? `${currentIndex + 1}/${highlightCount}`
: '0/0'}
</span>
</div>
</div>
<div
style={{
maxHeight: '400px',
overflowY: 'auto',
border: '1px solid #ccc',
padding: '10px',
marginTop: '10px'
}}
>
{getHighlightedText()}
</div>
</div>
)
}
export default CombinedHighlighter