mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-12 04:16:45 +08:00
439 lines
13 KiB
Vue
439 lines
13 KiB
Vue
<template>
|
||
<div class="ai-script">
|
||
<!-- 搜索和语言选择区域 -->
|
||
<div v-if="selectableLang && selectableLang.length > 0" class="search-container">
|
||
<el-input
|
||
v-model="searchKeyword"
|
||
placeholder="请输入关键词查找文稿内容"
|
||
class="search-input"
|
||
prefix-icon="el-icon-search"
|
||
@keyup.enter.native="searchContent"
|
||
@input="handleInputChange"
|
||
clearable
|
||
native-type="text"
|
||
/>
|
||
<div class="language-selector">
|
||
<span class="language-label">语言</span>
|
||
<el-select v-model="selectedLanguage" class="language-select" @change="changeLanguage" placeholder="请选择语言">
|
||
<el-option v-for="lang in selectableLang" :key="lang.srclang" :label="getSelectLabel(lang)" :value="lang.srclang"></el-option>
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容展示区域 -->
|
||
<div v-if="selectableLang && selectableLang.length > 0" class="content-container">
|
||
<!-- 动态渲染内容块 -->
|
||
<div v-for="(item, index) in contentList" :key="index" class="content-item" :class="{'active': currentTime >= item.start && currentTime <= item.end}">
|
||
<div class="timestamp">
|
||
<div class="timestamp-text">
|
||
<i class="el-icon-time"></i>
|
||
{{ formatTime(item.start) }}
|
||
</div>
|
||
</div>
|
||
<el-card class="content-text" @click.native="scrollToTime(item.start)">
|
||
<div v-html="item.highlightedContent || item.text"></div>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
<div v-else class="no-data">
|
||
<img src="../../assets/images/course/noData.png" alt="">
|
||
<span >生成中,过程可能耗时较长,<br>无需在此等待哦~</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { mapGetters, mapMutations } from 'vuex'
|
||
export default {
|
||
props: {
|
||
// 视频链接对应的content Id
|
||
blobId: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
isDrag:{
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
},
|
||
name: 'ai-script',
|
||
data() {
|
||
return {
|
||
searchKeyword: '',
|
||
selectedLanguage: 'zh-CN',
|
||
originalContentList: [],
|
||
contentList: [], // 用于显示的内容列表
|
||
isUserScrolling: false, // 用户是否正在滚动
|
||
userScrollTimeout: null // 滚动超时计时器
|
||
}
|
||
},
|
||
computed: {
|
||
...mapGetters([
|
||
'currentTime',
|
||
'selectableLang',
|
||
'duration'
|
||
]),
|
||
},
|
||
mounted: function() {
|
||
// 添加滚动事件监听,检测用户手动滚动
|
||
const container = document.querySelector('.content-container');
|
||
if (container) {
|
||
container.addEventListener('scroll', this.handleUserScroll);
|
||
}
|
||
},
|
||
|
||
beforeDestroy: function() {
|
||
// 清理事件监听和计时器
|
||
const container = document.querySelector('.content-container');
|
||
if (container) {
|
||
container.removeEventListener('scroll', this.handleUserScroll);
|
||
}
|
||
if (this.userScrollTimeout) {
|
||
clearTimeout(this.userScrollTimeout);
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
// 监听currentTime变化,自动滚动到当前激活项
|
||
currentTime: function(newTime) {
|
||
// 只有当用户没有手动滚动时才执行自动滚动
|
||
if (!this.isUserScrolling) {
|
||
this.$nextTick(function() {
|
||
const activeElement = document.querySelector('.content-item.active');
|
||
if (activeElement) {
|
||
// 获取内容容器
|
||
const container = document.querySelector('.content-container');
|
||
|
||
// 计算元素是否在可视区域内
|
||
const containerRect = container.getBoundingClientRect();
|
||
const elementRect = activeElement.getBoundingClientRect();
|
||
|
||
// 如果元素不在可视区域内,则滚动到可视区域
|
||
if (elementRect.top < containerRect.top || elementRect.bottom > containerRect.bottom) {
|
||
// 计算元素相对于容器的偏移量,而不是使用scrollIntoView
|
||
// 这样只会滚动content-container内部,不会影响页面滚动
|
||
|
||
// 计算元素相对于容器的位置
|
||
const elementOffsetTop = activeElement.offsetTop;
|
||
const containerScrollTop = container.scrollTop;
|
||
const containerHeight = container.clientHeight;
|
||
const elementHeight = activeElement.clientHeight;
|
||
|
||
// 计算目标滚动位置,使元素居中显示
|
||
// 考虑容器的内边距和元素本身的高度
|
||
let targetScrollTop = elementOffsetTop - (containerHeight / 2) + (elementHeight / 2);
|
||
|
||
// 确保目标滚动位置不会小于0
|
||
targetScrollTop = Math.max(0, targetScrollTop);
|
||
|
||
// 确保目标滚动位置不会导致元素超出容器底部
|
||
const maxScrollTop = container.scrollHeight - containerHeight;
|
||
targetScrollTop = Math.min(targetScrollTop, maxScrollTop);
|
||
|
||
// 使用requestAnimationFrame实现平滑滚动
|
||
const startScrollTop = containerScrollTop;
|
||
const distance = targetScrollTop - startScrollTop;
|
||
const duration = 300; // 滚动持续时间,毫秒
|
||
let startTime = null;
|
||
|
||
function animateScroll(currentTime) {
|
||
if (!startTime) startTime = currentTime;
|
||
const timeElapsed = currentTime - startTime;
|
||
container.scrollTo({
|
||
top: startScrollTop + distance - elementHeight - 120,
|
||
behavior: 'smooth'
|
||
});
|
||
|
||
if (timeElapsed < duration) {
|
||
requestAnimationFrame(animateScroll);
|
||
}
|
||
}
|
||
|
||
requestAnimationFrame(animateScroll);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
},
|
||
created() {
|
||
// 初始化时根据语言选择显示内容
|
||
this.changeLanguage(this.selectedLanguage)
|
||
|
||
},
|
||
methods: {
|
||
// 动态获取选择框的标签
|
||
getSelectLabel(lang) {
|
||
if (lang.srclang == 'zh-CN') {
|
||
return lang.label;
|
||
}
|
||
return `${lang.name} (${lang.label})`;
|
||
},
|
||
formatTime (time) {
|
||
// 格式化时间为HH:MM:SS,如01:00:00
|
||
const hours = Math.floor(time / 3600);
|
||
const minutes = Math.floor((time % 3600) / 60);
|
||
const seconds = Math.floor(time % 60);
|
||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
},
|
||
// 跳转到指定时间点
|
||
scrollToTime(time) {
|
||
// console.log('scrollToTime', time , this.blobId, localStorage.getItem('videoProgressData'), this.duration)
|
||
if(!this.isDrag && this.duration){
|
||
var t = localStorage.getItem('videoProgressData')
|
||
var arr = t&&JSON.parse(t) || {}
|
||
if(arr[this.blobId] < time/this.duration || !arr[this.blobId]){
|
||
return
|
||
}
|
||
}
|
||
console.log('跳转到时间点:', time);
|
||
this.$emit('changeCurrentTime', time);
|
||
// 设置用户滚动状态,避免自动滚动干扰
|
||
this.isUserScrolling = true;
|
||
if (this.userScrollTimeout) {
|
||
clearTimeout(this.userScrollTimeout);
|
||
}
|
||
this.userScrollTimeout = setTimeout(() => {
|
||
this.isUserScrolling = false;
|
||
}, 3000);
|
||
},
|
||
// 处理用户滚动事件
|
||
handleUserScroll: function() {
|
||
this.isUserScrolling = true;
|
||
|
||
// 清除之前的计时器
|
||
if (this.userScrollTimeout) {
|
||
clearTimeout(this.userScrollTimeout);
|
||
}
|
||
|
||
// 设置新的计时器,3秒后恢复自动滚动
|
||
this.userScrollTimeout = setTimeout(() => {
|
||
this.isUserScrolling = false;
|
||
}, 3000);
|
||
},
|
||
|
||
searchContent () {
|
||
// 搜索功能实现
|
||
if (!this.searchKeyword.trim()) {
|
||
// 如果搜索关键词为空,显示所有内容
|
||
this.contentList = this.originalContentList.map(item => ({ ...item }));
|
||
return;
|
||
}
|
||
|
||
const keyword = this.searchKeyword.trim();
|
||
// 过滤包含关键词的内容
|
||
const filteredList = this.originalContentList.filter(item =>
|
||
item.text.includes(keyword)
|
||
);
|
||
|
||
if (filteredList.length === 0) {
|
||
// 如果没有搜索到内容,显示提示
|
||
this.$message({
|
||
message: '未找到相关内容',
|
||
type: 'info'
|
||
});
|
||
this.contentList = this.originalContentList.map(item => ({ ...item }));
|
||
} else {
|
||
// 对搜索到的内容进行关键词高亮处理
|
||
this.contentList = filteredList.map(item => ({
|
||
...item,
|
||
highlightedContent: this.highlightKeyword(item.text, keyword)
|
||
}));
|
||
console.log(this.contentList)
|
||
}
|
||
},
|
||
highlightKeyword(content, keyword) {
|
||
// 对关键词进行转义,防止正则表达式特殊字符的影响
|
||
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
// 使用正则表达式全局匹配关键词并添加高亮标记
|
||
const regex = new RegExp(`(${escapedKeyword})`, 'gi');
|
||
return content.replace(regex, '<span style="color: rgba(6, 125, 255, 1); background: rgba(6, 125, 255, 0.1);">$1</span>');
|
||
},
|
||
changeLanguage (event) {
|
||
// this.selectedLanguage = event
|
||
this.selectableLang.forEach(item => {
|
||
if (item.srclang === event) {
|
||
console.log('当前语言:', item)
|
||
if (!item.originalContentList) {
|
||
try {
|
||
item.originalContentList = JSON.parse(item.subtitleData)
|
||
} catch (error) {
|
||
console.error('ai文稿格式有问题!')
|
||
}
|
||
}
|
||
this.originalContentList = item.originalContentList || []
|
||
// 初始化时显示所有内容
|
||
this.contentList = this.originalContentList.map(item => ({ ...item }));
|
||
console.log('ai文稿数据:', this.originalContentList)
|
||
}
|
||
})
|
||
console.log('切换语言:', event)
|
||
},
|
||
handleInputChange() {
|
||
// 当输入框内容变化时,如果为空则重置显示所有内容
|
||
if (!this.searchKeyword.trim()) {
|
||
this.contentList = this.originalContentList.map(item => ({ ...item }));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.ai-script {
|
||
padding: 15px 0;
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.search-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
margin: 0 20px 15px 20px;
|
||
}
|
||
|
||
.search-box {
|
||
position: relative;
|
||
flex: 1;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
}
|
||
|
||
::v-deep .el-input__inner {
|
||
border-color: #2688FF;
|
||
}
|
||
|
||
::v-deep .el-input__inner:focus {
|
||
border-color: #1a6fe0;
|
||
box-shadow: 0 0 0 2px rgba(38, 136, 255, 0.2);
|
||
}
|
||
|
||
::v-deep .el-input__prefix {
|
||
left: 5px;
|
||
}
|
||
|
||
::v-deep .el-input__icon {
|
||
color: #2688FF;
|
||
}
|
||
|
||
.language-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.language-label {
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.language-select {
|
||
width: 90px;
|
||
}
|
||
|
||
|
||
.content-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
max-height: 410px;
|
||
overflow-y: auto;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.content-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
}
|
||
|
||
.timestamp {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 14px;
|
||
color: #666;
|
||
padding: 5px 0;
|
||
.timestamp-text{
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
border-radius: 12px;
|
||
padding: 2px 12px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
line-height: 20px;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
}
|
||
|
||
.content-text {
|
||
cursor: pointer;
|
||
line-height: 1.6;
|
||
font-size: 14px;
|
||
color: rgba(102, 102, 102, 1);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
background: rgba(250, 250, 250, 1);
|
||
line-height: 22px;
|
||
letter-spacing: 0.28px;
|
||
}
|
||
|
||
.active {
|
||
.timestamp-text{
|
||
color: rgba(6, 125, 255, 1);
|
||
background: rgba(6, 125, 255, 0.1);
|
||
}
|
||
.content-text{
|
||
border: 1px solid rgba(116, 182, 255, 1);
|
||
box-shadow: 0px 0px 7px 0px rgba(6, 125, 255, 0.24);
|
||
background: rgba(250, 250, 250, 1);
|
||
}
|
||
}
|
||
:deep(.el-card__body) {
|
||
padding: 15px;
|
||
}
|
||
|
||
:deep(.el-card.is-hover-shadow:focus, .el-card.is-hover-shadow:hover) {
|
||
box-shadow: 0 2px 12px 0 rgba(38, 136, 255, 0.2);
|
||
}
|
||
:deep(.el-input__inner) {
|
||
border-radius: 4px!important;
|
||
}
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.search-container {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.search-box {
|
||
max-width: none;
|
||
}
|
||
|
||
.language-selector {
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
.no-data{
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding-top: 60px;
|
||
color: rgba(0, 0, 0, 0.34);
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
line-height: 21px;
|
||
letter-spacing: 0.26px;
|
||
text-align: center;
|
||
img{
|
||
width: 96px;
|
||
height: 50px;
|
||
}
|
||
}
|
||
|
||
</style> |