mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-09 02:46:44 +08:00
ai视频一期功能提交
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/images/course/courseAbstract.png
Normal file
BIN
src/assets/images/course/courseAbstract.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/images/course/noData.png
Normal file
BIN
src/assets/images/course/noData.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
@@ -14,9 +14,8 @@
|
||||
/>
|
||||
<div class="language-selector">
|
||||
<span class="language-label">语言</span>
|
||||
<el-select v-model="selectedLanguage" class="language-select" @change="changeLanguage">
|
||||
<el-option label="中文" value="zh"></el-option>
|
||||
<el-option label="英文" value="en"></el-option>
|
||||
<el-select v-model="selectedLanguage" class="language-select" @change="changeLanguage" placeholder="请选择语言">
|
||||
<el-option v-for="lang in selectableLang" :key="lang.srclang" :label="lang.label" :value="lang.srclang"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,15 +23,15 @@
|
||||
<!-- 内容展示区域 -->
|
||||
<div class="content-container">
|
||||
<!-- 动态渲染内容块 -->
|
||||
<div v-for="(item, index) in contentList" :key="index" class="content-item" :class="{'active': activeIndex === index}">
|
||||
<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>
|
||||
{{ item.timestamp }}
|
||||
{{ formatTime(item.start) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-card class="content-text">
|
||||
<div v-html="item.highlightedContent || item.content"></div>
|
||||
<el-card class="content-text" @click.native="scrollToTime(item.start)">
|
||||
<div v-html="item.highlightedContent || item.text"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,35 +39,148 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
export default {
|
||||
name: 'ai-script',
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
searchKeyword: '',
|
||||
selectedLanguage: 'zh',
|
||||
originalContentList: [
|
||||
{
|
||||
timestamp: '00:00:04',
|
||||
content: '从功能上来看,整个平台是涵盖了四个核心的环节,第一个是我们的智课,第一个环节,通过我们的大模型能力将我们企业现有的材料上传之后,能够智能生成培训课件那培训课件。可以智能的转化成我们的培训视频已有的培训课件可以实现快速上架,第二个是学习的阶段,微信形式微课形式的一个线上学习的一个模式,通过岗位地图引导在线上进行视频学习,在移动端的时候,配合我们的智能数字导师可以进行线上微课的一个授课以及智能的互动问答解答。'
|
||||
},
|
||||
{
|
||||
timestamp: '00:10:04',
|
||||
content: '第三个就是考核的模式,也就是学习后的一个考核,通过我们的智能出题来生成我们课件对应的试题,那在进行学习完成之后,完成岗位的考试进行实时判卷,同时也能看到每个考核它的整个错题集的情况。最后就是我们的发证认证,所有的任务都会配合他对应的一个认证资格的认证,一人一证一码,随时可以查询他的证书情况和他的成绩培训情况。'
|
||||
},
|
||||
{
|
||||
timestamp: '00:13:04',
|
||||
content: '第三个就是考核的模式,也就是学习后的一个考核,通过我们的智能出题来生成我们课件对应的试题,那在进行学习完成之后,完成岗位的考试进行实时判卷,同时也能看到每个考核它的整个错题集的情况。最后就是我们的发证认证,所有的任务都会配合他对应的一个认证资格的认证,一人一证一码,随时可以查询他的证书情况和他的成绩培训情况。'
|
||||
}
|
||||
],
|
||||
selectedLanguage: 'zh-CN',
|
||||
originalContentList: [],
|
||||
contentList: [], // 用于显示的内容列表
|
||||
activeIndex: 0,
|
||||
isUserScrolling: false, // 用户是否正在滚动
|
||||
userScrollTimeout: null // 滚动超时计时器
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'currentTime',
|
||||
'selectableLang'
|
||||
]),
|
||||
},
|
||||
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.contentList = this.originalContentList.map(item => ({ ...item }));
|
||||
// 初始化时根据语言选择显示内容
|
||||
this.changeLanguage(this.selectedLanguage)
|
||||
|
||||
},
|
||||
methods: {
|
||||
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('跳转到时间点:', 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()) {
|
||||
@@ -80,7 +192,7 @@ export default {
|
||||
const keyword = this.searchKeyword.trim();
|
||||
// 过滤包含关键词的内容
|
||||
const filteredList = this.originalContentList.filter(item =>
|
||||
item.content.includes(keyword)
|
||||
item.text.includes(keyword)
|
||||
);
|
||||
|
||||
if (filteredList.length === 0) {
|
||||
@@ -94,7 +206,7 @@ export default {
|
||||
// 对搜索到的内容进行关键词高亮处理
|
||||
this.contentList = filteredList.map(item => ({
|
||||
...item,
|
||||
highlightedContent: this.highlightKeyword(item.content, keyword)
|
||||
highlightedContent: this.highlightKeyword(item.text, keyword)
|
||||
}));
|
||||
console.log(this.contentList)
|
||||
}
|
||||
@@ -107,8 +219,24 @@ export default {
|
||||
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
|
||||
console.log('切换语言:', this.selectedLanguage)
|
||||
// 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() {
|
||||
// 当输入框内容变化时,如果为空则重置显示所有内容
|
||||
@@ -185,7 +313,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
max-height: 530px;
|
||||
max-height: 410px;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
@@ -220,6 +348,7 @@ export default {
|
||||
}
|
||||
|
||||
.content-text {
|
||||
cursor: pointer;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
color: rgba(102, 102, 102, 1);
|
||||
|
||||
@@ -338,22 +338,22 @@
|
||||
</el-tooltip>
|
||||
<span>AI处理状态:</span>
|
||||
</div>
|
||||
<el-switch v-model="courseInfo.aiSet" active-value="1" inactive-value="0"></el-switch>
|
||||
<el-switch v-model="courseInfo.aiSet" :active-value="1" :inactive-value="0"></el-switch>
|
||||
</div>
|
||||
<div v-show="courseInfo.aiSet==1">
|
||||
<el-row :gutter="20" style="margin: 10px 0;">
|
||||
<el-col :span="9" style="display: flex; align-items: center;gap: 10px;">
|
||||
<span>是否需要生成AI摘要:</span>
|
||||
<el-radio-group v-model="courseInfo.aiAbstract">
|
||||
<el-radio label="1">是</el-radio>
|
||||
<el-radio label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-col>
|
||||
<el-col :span="10" style="display: flex; align-items: center;gap: 10px;">
|
||||
<span>是否需要生成AI文稿:</span>
|
||||
<el-radio-group v-model="courseInfo.aiDraft">
|
||||
<el-radio label="1">是</el-radio>
|
||||
<el-radio label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -992,15 +992,15 @@ export default {
|
||||
console.log("--- 编辑查看 this.dicts = ",this.dicts)
|
||||
// 如果ai设置为空则给默认值 - 会看成新增状态
|
||||
if(this.courseInfo.aiSet === null || this.courseInfo.aiSet === '' || this.courseInfo.aiSet === undefined){
|
||||
this.courseInfo.isAddAI = '1'; //暂时是否是新增
|
||||
this.courseInfo.aiSet = '1';
|
||||
this.courseInfo.aiAbstract = '1';
|
||||
this.courseInfo.aiDraft = '1';
|
||||
this.courseInfo.aiTranslate = '1';
|
||||
this.courseInfo.isAddAI = 1; //暂时是否是新增
|
||||
this.courseInfo.aiSet = 1;
|
||||
this.courseInfo.aiAbstract = 1;
|
||||
this.courseInfo.aiDraft = 1;
|
||||
this.courseInfo.aiTranslate = 1;
|
||||
this.courseInfo.languageCode = ['zh-CN', 'en-US'];
|
||||
} else {
|
||||
// 获取ai设置信息
|
||||
this.courseInfo.isAddAI = '0';
|
||||
this.courseInfo.isAddAI = 0;
|
||||
}
|
||||
if(!this.courseInfo.orgId){
|
||||
//根据课程创建者获取机构id
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
<div class="player-controls-btn" style="display: flex;gap: 0.3rem;align-items: center;">
|
||||
<span>字幕</span>
|
||||
<el-switch
|
||||
@change="toggleSubtitle"
|
||||
v-model="isSubtitle">
|
||||
</el-switch>
|
||||
</div>
|
||||
@@ -328,7 +329,7 @@ export default {
|
||||
fullTimeFormat: "00:00:00", // 视频总长度的文字
|
||||
barrageTimelineStart: 0, // 弹幕时间轴的起始时间点(手动调整进度条触发更新)
|
||||
isInit:false, // 是否初始化过
|
||||
isSubtitle:false, // 是否开启字幕
|
||||
isSubtitle: true, // 是否开启字幕
|
||||
currentLangLabel:'', // 当前字幕语言
|
||||
};
|
||||
},
|
||||
@@ -349,7 +350,7 @@ export default {
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
console.log('当前状态:',this.currentProgress,this.isDrag,this.videoDom.currentTime , this.videoDom.duration)
|
||||
// console.log('当前状态:',this.currentProgress,this.isDrag,this.videoDom.currentTime , this.videoDom.duration)
|
||||
// 视频播放时本地记录视频实时播放时长,视频设置了禁止拖动时执行
|
||||
if(!this.isDrag){
|
||||
var time = localStorage.getItem('videoProgressData')
|
||||
@@ -396,7 +397,7 @@ export default {
|
||||
}
|
||||
// 根据视频的readyState判断下一帧是否已加载,并控制loading的显示
|
||||
this.isShowLoading = this.videoDom.readyState < 3;
|
||||
console.log("当前缓存:"+this.videoDom.readyState)
|
||||
// console.log("当前缓存:"+this.videoDom.readyState)
|
||||
if (this.videoDom.readyState < 3){
|
||||
console.log("详细信息",this.videoDom)
|
||||
console.log("卡了",this.videoDom.readyState)
|
||||
@@ -445,6 +446,7 @@ export default {
|
||||
methods: {
|
||||
...mapMutations({
|
||||
SET_currentLang: 'video/SET_currentLang',
|
||||
SET_currentTime: 'video/SET_currentTime',
|
||||
}),
|
||||
//当视频由于需要缓冲下一帧而停止,解决一直计时的问题
|
||||
onWaiting(){
|
||||
@@ -659,6 +661,7 @@ export default {
|
||||
},
|
||||
onAudioTimeUpdate() {
|
||||
const currentTime = this.$refs.video.currentTime;
|
||||
this.SET_currentTime(currentTime)
|
||||
this.$emit('onTimeUpdate', currentTime);
|
||||
},
|
||||
/**
|
||||
@@ -677,15 +680,76 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换字幕
|
||||
*/
|
||||
toggleSubtitle(value) {
|
||||
if (this.videoDom && this.videoDom.textTracks && this.videoDom.textTracks.length >0) {
|
||||
if (!value) {
|
||||
// 关闭字幕
|
||||
this.videoDom.textTracks[this.videoDom.textTracks.length - 1].mode = 'hidden';
|
||||
} else {
|
||||
// 打开字幕
|
||||
this.videoDom.textTracks[this.videoDom.textTracks.length - 1].mode = 'showing';
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 切换字幕语言
|
||||
*/
|
||||
changeLang(item) {
|
||||
this.SET_currentLang(item.srclang);
|
||||
this.currentLangLabel = item.label;
|
||||
this.$emit('onLangChange', item.srclang);
|
||||
console.log("changeLang",item);
|
||||
// 先移除所有字幕轨道
|
||||
Array.from(this.videoDom.querySelectorAll('track')).forEach(t => t.remove());
|
||||
if(!item.vttContent){
|
||||
console.log("字幕内容为空!")
|
||||
return;
|
||||
}
|
||||
if(!item.srcUrl){
|
||||
try{
|
||||
const blob = new Blob([item.vttContent], { type: 'text/vtt' });
|
||||
item.srcUrl = URL.createObjectURL(blob);
|
||||
}catch(e){
|
||||
console.log("字幕格式错误",e)
|
||||
}
|
||||
}
|
||||
const trackEl = document.createElement('track');
|
||||
trackEl.kind = 'subtitles';
|
||||
trackEl.srclang = item.srclang;
|
||||
trackEl.label = item.label;
|
||||
trackEl.src = item.srcUrl;
|
||||
trackEl.default = true; // 确保字幕默认启用
|
||||
|
||||
// 使用箭头函数保持this上下文
|
||||
trackEl.addEventListener('load', () => {
|
||||
console.log('字幕加载成功!');
|
||||
// console.log('#########Track cues:', trackEl.track.cues);
|
||||
});
|
||||
|
||||
trackEl.addEventListener('error', () => {
|
||||
console.error('字幕加载失败!');
|
||||
});
|
||||
|
||||
// 确保视频已加载到可添加轨道的状态
|
||||
if (this.videoDom.readyState >= 1) {
|
||||
this.videoDom.appendChild(trackEl);
|
||||
this.videoDom.textTracks[this.videoDom.textTracks.length - 1].mode = 'showing';
|
||||
} else {
|
||||
this.videoDom.addEventListener('loadedmetadata', () => {
|
||||
this.videoDom.appendChild(trackEl);
|
||||
this.videoDom.textTracks[this.videoDom.textTracks.length - 1].mode = 'showing';
|
||||
}, { once: true });
|
||||
}
|
||||
},
|
||||
seekToTime(time) {
|
||||
if (!this.videoDom) return;
|
||||
this.videoDom.currentTime = time + 0.01;
|
||||
this.isPlaying = true;
|
||||
this.videoDom.play();
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
currentVolume: function () {
|
||||
|
||||
@@ -31,5 +31,6 @@ const getters = {
|
||||
selectAllLang:state => state.video.selectAllLang,
|
||||
selectableLang:state => state.video.selectableLang,
|
||||
currentLang:state => state.video.currentLang,
|
||||
currentTime:state => state.video.currentTime,
|
||||
}
|
||||
export default getters
|
||||
|
||||
@@ -92,15 +92,31 @@ const state = {
|
||||
name: 'हिन्दी',
|
||||
}
|
||||
], // 全部语言列表
|
||||
selectableLang: [],
|
||||
currentLang: '',
|
||||
favoritesUnicom: false,
|
||||
selectableLang: [], // 可选语言列表+字幕信息
|
||||
currentLang: '', // 当前选中语言
|
||||
currentTime: -1, // 当前视频时间
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
SET_currentLang: (state, lang) => {
|
||||
state.currentLang = lang
|
||||
},
|
||||
SET_selectableLang: (state, list = []) => {
|
||||
let selectableLang = []
|
||||
list.forEach(item => {
|
||||
let selectItem = state.selectAllLang.find(selectItem => selectItem.srclang === item.language)
|
||||
if (selectItem) {
|
||||
selectableLang.push({
|
||||
...item,
|
||||
...selectItem,
|
||||
})
|
||||
}
|
||||
})
|
||||
state.selectableLang = selectableLang
|
||||
},
|
||||
SET_currentTime: (state, time) => {
|
||||
state.currentTime = time
|
||||
},
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
||||
@@ -339,12 +339,12 @@
|
||||
</el-tooltip>
|
||||
AI处理状态:</span>
|
||||
<span class="status-text">
|
||||
{{ aiProcessSetting.aiSet === '1' ? '已开启' : '已关闭' }}
|
||||
{{ aiProcessSetting.aiSet === 1 ? '已开启' : '已关闭' }}
|
||||
</span>
|
||||
<el-switch
|
||||
v-model="aiProcessSetting.aiSet"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
></el-switch>
|
||||
</div>
|
||||
|
||||
@@ -353,8 +353,8 @@
|
||||
<div class="form-item">
|
||||
<span class="form-label">是否需要生成AI摘要:</span>
|
||||
<el-radio-group v-model="aiProcessSetting.aiAbstract" class="radio-group">
|
||||
<el-radio label="1">是</el-radio>
|
||||
<el-radio label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
@@ -362,8 +362,8 @@
|
||||
<div class="form-item">
|
||||
<span class="form-label">是否需要生成AI文稿:</span>
|
||||
<el-radio-group v-model="aiProcessSetting.aiDraft" class="radio-group">
|
||||
<el-radio label="1">是</el-radio>
|
||||
<el-radio label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
@@ -414,24 +414,24 @@
|
||||
AI功能状态:
|
||||
</span>
|
||||
<span class="status-text">
|
||||
{{ aiSetting.aiSet === '1' ? '已开放' : '未开放' }}
|
||||
{{ aiSetting.aiSet === 1 ? '已开放' : '未开放' }}
|
||||
</span>
|
||||
<el-switch
|
||||
v-model="aiSetting.aiSet"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
></el-switch>
|
||||
</div>
|
||||
|
||||
<!-- AI摘要状态 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">AI摘要状态:</span>
|
||||
<span class="status-badge" :class="{'status-badge--active': aiSetting.aiAbstract === '1'}">
|
||||
{{ aiSetting.aiAbstract === '1' ? '已上架' : '已下架' }}
|
||||
<span class="status-badge" :class="{'status-badge--active': aiSetting.aiAbstract === 1}">
|
||||
{{ aiSetting.aiAbstract === 1 ? '已上架' : '已下架' }}
|
||||
</span>
|
||||
<div class="action-buttons">
|
||||
<el-button type="text" @click="aiSetting.aiAbstract = '0'">
|
||||
{{ aiSetting.aiAbstract === '0' ? '上架' : '下架' }}
|
||||
<el-button type="text" @click="aiSetting.aiAbstract = 0">
|
||||
{{ aiSetting.aiAbstract === 0 ? '上架' : '下架' }}
|
||||
</el-button>
|
||||
<el-button type="text" >编辑</el-button>
|
||||
</div>
|
||||
@@ -440,12 +440,12 @@
|
||||
<!-- AI文稿状态 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">AI文稿状态:</span>
|
||||
<span class="status-badge" :class="{'status-badge--active': aiSetting.aiDraft === '1'}">
|
||||
{{ aiSetting.aiDraft === '1' ? '已上架' : '已下架' }}
|
||||
<span class="status-badge" :class="{'status-badge--active': aiSetting.aiDraft === 1}">
|
||||
{{ aiSetting.aiDraft === 1 ? '已上架' : '已下架' }}
|
||||
</span>
|
||||
<div class="action-buttons">
|
||||
<el-button type="text" @click="aiSetting.aiDraft = '0'">
|
||||
{{ aiSetting.aiDraft === '0' ? '上架' : '下架' }}
|
||||
<el-button type="text" @click="aiSetting.aiDraft = 0">
|
||||
{{ aiSetting.aiDraft === 0 ? '上架' : '下架' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -453,12 +453,12 @@
|
||||
<!-- AI翻译状态 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">AI翻译状态:</span>
|
||||
<span class="status-badge" :class="{'status-badge--active': aiSetting.aiTranslate === '1'}">
|
||||
{{ aiSetting.aiTranslate === '1' ? '已上架' : '已下架' }}
|
||||
<span class="status-badge" :class="{'status-badge--active': aiSetting.aiTranslate === 1}">
|
||||
{{ aiSetting.aiTranslate === 1 ? '已上架' : '已下架' }}
|
||||
</span>
|
||||
<div class="action-buttons">
|
||||
<el-button type="text" @click="aiSetting.aiTranslate = '0'">
|
||||
{{ aiSetting.aiTranslate === '0' ? '上架' : '下架' }}
|
||||
<el-button type="text" @click="aiSetting.aiTranslate = 0">
|
||||
{{ aiSetting.aiTranslate === 0 ? '上架' : '下架' }}
|
||||
</el-button>
|
||||
<el-button type="text" >编辑</el-button>
|
||||
</div>
|
||||
@@ -601,18 +601,18 @@ export default {
|
||||
},
|
||||
aiProcessSetting: {
|
||||
dlgShow: false,
|
||||
aiSet: '1',
|
||||
aiAbstract: '1',
|
||||
aiDraft: '1',
|
||||
aiSet: 1,
|
||||
aiAbstract: 1,
|
||||
aiDraft: 1,
|
||||
languageCode: ['zh-CN', 'en-US'] // 默认选中的语种
|
||||
},
|
||||
aiSetting: {
|
||||
dlgShow: false,
|
||||
courseId: '',
|
||||
aiSet: '1',
|
||||
aiAbstract: '1', // 1:上架 0:下架
|
||||
aiDraft: '1', // 1:上架 0:下架
|
||||
aiTranslate: '1', // 1:上架 0:下架
|
||||
aiSet: 1,
|
||||
aiAbstract: 1, // 1:上架 0:下架
|
||||
aiDraft: 1, // 1:上架 0:下架
|
||||
aiTranslate: 1, // 1:上架 0:下架
|
||||
languageCode: ['zh-CN', 'en-US', 'vi-VN'] // 支持的语种
|
||||
}
|
||||
};
|
||||
|
||||
@@ -360,20 +360,7 @@
|
||||
<span v-if="cinfo.type == 30" class="course-type-left">线下课</span>
|
||||
<span v-if="cinfo.type == 40" class="course-type-left">学习项目</span>
|
||||
</div>
|
||||
<div class="course-title" style="display: flex;justify-content: space-between;align-items: center;">
|
||||
<div class="two-line-ellipsis" style="flex: 1;" :title="cinfo.title" v-html="cinfo.name"></div>
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
width="402"
|
||||
trigger="hover"
|
||||
popper-class="course-popover"
|
||||
>
|
||||
<div>本课程以 “基础理论 + 实战应用” 为核心,助力学习者搭建完整 AI 知识体系,掌握技术落地能力。课程目标明确:一是理解 AI、机器学习等核心概念与发展脉络,建立科学认知;二是熟练掌握 AI 所需数学基础、Python 编程及主流算法逻辑;三是能独立完成简单 AI 项目的全流程开发。适用人群广泛,涵盖零基础入门者(学生、职场转型者)、需提升实操能力的技术从业者,以及关注 AI 商业价值的产品、运营等职场人。</div>
|
||||
<img slot="reference" :src="`${webBaseUrl}/images/courseAbstract.png`" alt="摘要" style="width: 84px; height: 36px;">
|
||||
</el-popover>
|
||||
|
||||
</div>
|
||||
<!-- <div class="course-title two-line-ellipsis" :title="cinfo.title" v-html="cinfo.name"></div> -->
|
||||
<div class="course-title two-line-ellipsis" :title="cinfo.title" v-html="cinfo.name"></div>
|
||||
<!-- 关键字 -->
|
||||
<div class="keywordInfo-every">
|
||||
<div class="keywordInfo" v-for="(keyword, index) in cinfo.keywordsActive" :key="index">
|
||||
@@ -390,7 +377,24 @@
|
||||
<span v-if="cinfo.type == 30 && cinfo.startTime">开课时间:{{ cinfo.startTime }}</span>
|
||||
</div>
|
||||
|
||||
<div class="course-info">
|
||||
<div class="course-info" style="align-items: center;">
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
:width="cinfo.summaryContent && cinfo.summaryContent.length > 200 ? '402' : '253'"
|
||||
trigger="hover"
|
||||
popper-class="course-popover"
|
||||
>
|
||||
<div class="course-popover-content">
|
||||
<h4>课程摘要</h4>
|
||||
<div v-if="cinfo.summaryContent" class="course-popover-content-text" >{{ cinfo.summaryContent }}</div>
|
||||
<div v-else class="course-popover-noContent" >
|
||||
<img src="../../../assets/images/course/noData.png" alt="">
|
||||
<span>暂无数据</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <img slot="reference" src="../../../assets/images/course/courseAbstract.png" alt="摘要" style="width: 94px;height: 44px;margin-left: -10px;"> -->
|
||||
<img v-show="cinfo.aiAbstract == '1'" slot="reference" src="../../../assets/images/course/courseAbstract.png" alt="摘要" style="width: 94px;height: 44px;margin-left: -10px;">
|
||||
</el-popover>
|
||||
<div class="course-info-user" style="max-width: 100px;" v-if="cinfo.teacher">
|
||||
<el-tooltip :content="cinfo.teacher" placement="bottom" effect="light">
|
||||
<span class="course-info-author">{{ cinfo.teacher }}</span>
|
||||
@@ -400,13 +404,13 @@
|
||||
<span class="course-info-studys">{{ formatNum(cinfo.studies) }}人学习</span>
|
||||
</div>
|
||||
<div class="course-info-score">
|
||||
<div style="display: flex;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<interactBar :type="1" nodeWidth="20px" :data="cinfo" :courseExclusive="true" :comments="false"
|
||||
:praises="false" :shares="false" :views="false"></interactBar>
|
||||
<div v-if="cinfo.score">
|
||||
<span class="course-score-value">{{ toScore(cinfo.score) }}分</span>
|
||||
<span class="course-score-value" style="white-space: nowrap;">{{ toScore(cinfo.score) }}分</span>
|
||||
</div>
|
||||
<div v-else class="course-score-no">未评分</div>
|
||||
<div v-else class="course-score-no" style="white-space: nowrap;">未评分</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -422,10 +426,6 @@
|
||||
<div class="course-img-box">
|
||||
<course-image :text="true" :course="cinfo"></course-image>
|
||||
</div>
|
||||
<div class="course-title ">
|
||||
<div class="two-line-ellipsis" :title="cinfo.title" v-html="cinfo.name"></div>
|
||||
<img :src="`${webBaseUrl}/images/courseAbstract.png`" alt="摘要" style="width: 84px; height: 36px;">
|
||||
</div>
|
||||
<div class="course-title two-line-ellipsis" :title="cinfo.title" v-html="cinfo.name"></div>
|
||||
<div class="course-title two-line-ellipsis">
|
||||
{{ cinfo.remark }}
|
||||
@@ -2438,33 +2438,43 @@ console.log(res.result.list,'data')
|
||||
|
||||
.option-active {
|
||||
color: #387DF7;
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
}</style>
|
||||
<style lang="scss">
|
||||
.course-popover{
|
||||
box-sizing: border-box;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0px 0px 16px 0px rgba(231, 242, 255, 0.25);
|
||||
/* 实现边框渐变效果 */
|
||||
position: relative;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(to bottom, #2688FF, #FFFFFF, #E7F2FF);
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.3px;
|
||||
padding: 13px 25px;
|
||||
|
||||
}
|
||||
.course-popover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
margin: 1px;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.12);
|
||||
.course-popover-content{
|
||||
h4{
|
||||
margin: 0 0 7px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 17px;
|
||||
letter-spacing: 0.3px;
|
||||
color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
.course-popover-content-text{
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.26px;
|
||||
color: rgba(102, 102, 102, 1);
|
||||
}
|
||||
.course-popover-noContent{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
color: rgba(0, 0, 0, 0.34);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
letter-spacing: 0.26px;
|
||||
padding-bottom: 20px;
|
||||
img{
|
||||
width: 96px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -648,8 +648,8 @@
|
||||
></my-note>
|
||||
</div>
|
||||
<!-- ai文稿 -->
|
||||
<div class="ai-script" v-show="tab == 3">
|
||||
<ai-script ref="aiscript"></ai-script>
|
||||
<div class="ai-script" v-if="tab == 3">
|
||||
<ai-script ref="aiscript" @changeCurrentTime="changeCurrentTime"></ai-script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -833,7 +833,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import { mapGetters, mapMutations } from "vuex";
|
||||
import followButton from "@/components/Follow/button.vue";
|
||||
import portalHeader from "@/components/PortalHeader.vue";
|
||||
import portalFooter from "@/components/PortalFooter.vue";
|
||||
@@ -1004,7 +1004,7 @@ export default {
|
||||
this.loadData();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["userInfo"]),
|
||||
...mapGetters(["userInfo", 'selectableLang']),
|
||||
catalogTree() {
|
||||
let treeList = [];
|
||||
this.completed = [];
|
||||
@@ -1040,6 +1040,14 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 处理从AI文稿组件传递过来的时间跳转事件
|
||||
changeCurrentTime(time) {
|
||||
console.log(time,'time')
|
||||
this.$refs.myVideoPlayer && this.$refs.myVideoPlayer.seekToTime(time);
|
||||
},
|
||||
...mapMutations({
|
||||
SET_selectableLang: 'video/SET_selectableLang',
|
||||
}),
|
||||
handleOpen(key, path) {
|
||||
if (this.isFalse) {
|
||||
this.defaultOpeneds = [key];
|
||||
@@ -1203,6 +1211,10 @@ export default {
|
||||
this.curriculumData.url = r.content;
|
||||
}
|
||||
this.$refs.mynote.showVideoTimeBtn(true);
|
||||
// 视频类型加载ai相关功能
|
||||
if (r.contentType == 10) {
|
||||
this.handleAIVideo(r.boeaiSubtitleRspList, r);
|
||||
}
|
||||
this.createPlayUrl(r.contentRefId, this.curriculumData.url);
|
||||
} else if (r.contentType == 40) {
|
||||
// if (r.content != '' && r.content.indexOf('.pdf') == -1) {
|
||||
@@ -1365,6 +1377,11 @@ export default {
|
||||
localStorage.setItem("videoProgressData", JSON.stringify(arr));
|
||||
}
|
||||
},
|
||||
// 视频处理 - 处理ai相关功能
|
||||
handleAIVideo(list = [], r) {
|
||||
this.SET_selectableLang(list);
|
||||
console.log("ai处理", this.selectableLang);
|
||||
},
|
||||
isShowTime() {
|
||||
if (this.isContentTypeTwo != this.contentData.contentType) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user