mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-07 18:06:44 +08:00
Compare commits
15 Commits
20151115-z
...
player-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa9b23766 | ||
|
|
4b4c0943d8 | ||
|
|
b31f02bf23 | ||
|
|
003fece291 | ||
|
|
986a47f22a | ||
|
|
782bcc31e5 | ||
|
|
1a95852912 | ||
|
|
f5d865ccc3 | ||
|
|
65673561d8 | ||
|
|
2cbb379fa6 | ||
|
|
6d4af3aa2d | ||
|
|
5ebee96ce4 | ||
| 408d6a1612 | |||
| b1508ad226 | |||
| a9764bf2f8 |
@@ -47,7 +47,9 @@ const findList = function(data) {
|
||||
}
|
||||
*/
|
||||
const saveUpload = function(data) {
|
||||
return ajax.post('/xboe/m/course/file/upload/save', data);
|
||||
return ajax.post('/xboe/m/course/file/upload/save', data, {
|
||||
timeout: 60000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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/courseNew.png
Normal file
BIN
src/assets/images/course/courseNew.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 46 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 |
BIN
src/assets/images/course/wengaoTip.png
Normal file
BIN
src/assets/images/course/wengaoTip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 B |
403
src/components/Course/aiScript.vue
Normal file
403
src/components/Course/aiScript.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<template>
|
||||
<div class="ai-script">
|
||||
<!-- 搜索和语言选择区域 -->
|
||||
<div 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 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
export default {
|
||||
name: 'ai-script',
|
||||
data() {
|
||||
return {
|
||||
searchKeyword: '',
|
||||
selectedLanguage: 'zh-CN',
|
||||
originalContentList: [],
|
||||
contentList: [], // 用于显示的内容列表
|
||||
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.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('跳转到时间点:', 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;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
border-radius: 20px;
|
||||
border-color: #2688FF;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner:focus) {
|
||||
border-color: #1a6fe0;
|
||||
box-shadow: 0 0 0 2px rgba(38, 136, 255, 0.2);
|
||||
}
|
||||
|
||||
:deep(.el-input__prefix) {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
:deep(.el-select__inner) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon-time) {
|
||||
color: #2688FF;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.search-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -50,7 +50,7 @@
|
||||
<el-input-number v-model="duration" size="mini" :min="1" :max="100"></el-input-number>
|
||||
</span>
|
||||
</div>
|
||||
<el-upload class="upload-demo" :headers="headers" :data="data" drag :action="uploadFileUrl" :on-success="handleUploadSuccess" :before-upload="handleBeforeUpload">
|
||||
<el-upload ref="uploadRef" class="upload-demo" :headers="headers" :data="data" drag :action="uploadFileUrl" :on-success="handleUploadSuccess" :before-upload="handleBeforeUpload">
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div class="el-upload__tip" slot="tip">文件大小限制:{{curComType.maxSizeName}},支持的文件类型:{{curComType.fileTypes.join(',')}}</div>
|
||||
@@ -195,6 +195,7 @@
|
||||
// this.cware.content.content=result.filePath;
|
||||
}else{
|
||||
this.$message.error(rs.message);
|
||||
this.$refs.uploadRef.clearFiles();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
|
||||
@@ -158,6 +158,43 @@
|
||||
placeholder="请尽量填写课程简介,用于列表中显示,可以让用户更容易了解课程信息">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="AI设置">
|
||||
<div style="margin-top: 7px;">
|
||||
<div style="display: flex; align-items: center;gap: 5px;">
|
||||
<el-switch v-model="courseInfo.aiSet" :active-value="1" :inactive-value="0"></el-switch>
|
||||
<el-tooltip class="item" effect="dark" :content="aiSetTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div v-show="courseInfo.aiSet==1" style="margin-left: -20px;">
|
||||
<div style="display: flex; justify-content: space-between;;align-items: center;gap: 5px;margin: 10px 0;">
|
||||
<div style="display: flex; align-items: center;gap: 5px;">
|
||||
<span>AI摘要</span>
|
||||
<el-switch v-model="courseInfo.aiAbstract" :active-value="1" :inactive-value="0"></el-switch>
|
||||
<el-tooltip class="item" effect="dark" :content="aiAbstractTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;gap: 5px;">
|
||||
<span>AI文稿</span>
|
||||
<el-switch v-model="courseInfo.aiDraft" :active-value="1" :inactive-value="0"></el-switch>
|
||||
<el-tooltip class="item" effect="dark" :content="aiDraftTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;gap: 5px;margin: 10px 0;margin-left: -30px;">
|
||||
<span>AI翻译语种</span>
|
||||
<el-select v-model="courseInfo.languageCode" placeholder="请选择" multiple filterable style="width: 240px;">
|
||||
<el-option v-for="item in selectAllLang" :key="item.key" :label="item.label" :value="item.srclang"> </el-option>
|
||||
</el-select>
|
||||
<el-tooltip class="item" effect="dark" :content="aiTranslateTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div @click="checkCourse"><weikeContent ref="weikeContent" :reset="weikeReset" :contents="contentInfo.list" :course="courseInfo" min-height="644px"></weikeContent></div>
|
||||
@@ -329,6 +366,44 @@
|
||||
placeholder="请尽量填写课程简介,用于列表中显示,可以让用户更容易了解课程信息">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<!-- ai播放器相关 -->
|
||||
<el-form-item label="AI设置">
|
||||
<div style="margin-top: 7px;">
|
||||
<div style="display: flex; align-items: center;gap: 5px;">
|
||||
<el-switch v-model="courseInfo.aiSet" :active-value="1" :inactive-value="0"></el-switch>
|
||||
<el-tooltip class="item" effect="dark" :content="aiSetTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div v-show="courseInfo.aiSet==1" style="margin-left: -20px;">
|
||||
<div style="display: flex;align-items: center;gap: 80px;margin: 20px 0;">
|
||||
<div style="display: flex; align-items: center;gap: 5px;">
|
||||
<span>AI摘要</span>
|
||||
<el-switch v-model="courseInfo.aiAbstract" :active-value="1" :inactive-value="0"></el-switch>
|
||||
<el-tooltip class="item" effect="dark" :content="aiAbstractTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;gap: 5px;">
|
||||
<span>AI文稿</span>
|
||||
<el-switch v-model="courseInfo.aiDraft" :active-value="1" :inactive-value="0"></el-switch>
|
||||
<el-tooltip class="item" effect="dark" :content="aiDraftTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;gap: 5px;margin: 20px 0;margin-left: -30px;">
|
||||
<span>AI翻译语种</span>
|
||||
<el-select v-model="courseInfo.languageCode" placeholder="请选择" multiple filterable style="flex:1">
|
||||
<el-option v-for="item in selectAllLang" :key="item.key" :label="item.label" :value="item.srclang"> </el-option>
|
||||
</el-select>
|
||||
<el-tooltip class="item" effect="dark" :content="aiTranslateTip" placement="top">
|
||||
<i class="el-icon-question" style="margin-left: 5px; color: #909399; cursor: pointer;"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- v-if="!weike.onlyRequired" -->
|
||||
<!-- <el-form-item label="课程描述">
|
||||
<WxEditor v-model="courseInfo.overview" :minHeight="50"></WxEditor>
|
||||
@@ -535,14 +610,19 @@ export default {
|
||||
selectedOrg: {
|
||||
orgId: null,
|
||||
name: ''
|
||||
}
|
||||
},
|
||||
aiSetTip: '是否将课程进行AI处理', //提示信息
|
||||
aiAbstractTip: '一键提炼课程视频核心要点,助力学员课前高效掌握重点,快速筛选学习资源', // 提示信息
|
||||
aiDraftTip: '分段展示视频内容并精准同步时间轴,实现视频进度与文稿双向定位,学习内容触手可及', //提示信息
|
||||
aiTranslateTip: '智能转换视频字幕与语音为多语种,支持全球学员按需切换语言,打破学习边界', // 提示信息
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getSceneData();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['resOwnerMap', 'sysTypeMap','userInfo','identity']),
|
||||
// ai播放器相关
|
||||
...mapGetters(['resOwnerMap', 'sysTypeMap','userInfo','identity', 'selectAllLang']),
|
||||
catalogTree() {
|
||||
let treeList = [];
|
||||
let $this = this;
|
||||
@@ -951,6 +1031,19 @@ export default {
|
||||
this.dicts = result.dicts; //课程的老师信息
|
||||
console.log("--- 编辑查看 this.isPermission = ",this.isPermission)
|
||||
console.log("--- 编辑查看 this.dicts = ",this.dicts)
|
||||
// ai播放器相关
|
||||
// 如果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.languageCode = ['zh-CN', 'en-US'];
|
||||
} else {
|
||||
// 获取ai设置信息
|
||||
this.courseInfo.isAddAI = 0;
|
||||
}
|
||||
if(!this.courseInfo.orgId){
|
||||
//根据课程创建者获取机构id
|
||||
apiUser.getOrgSimpleByUserId(result.course.sysCreateAid).then(ors=>{
|
||||
|
||||
@@ -119,7 +119,35 @@
|
||||
</div>
|
||||
<div class="player-time">{{ currentTimeFormat }} / {{ fullTimeFormat }}</div>
|
||||
</div>
|
||||
<!-- ai播放器相关 -->
|
||||
<div class="player-controls-bottom-right">
|
||||
<div class="player-controls-btn box-aiTranslate">
|
||||
<div v-show="isSubtitle" class="player-controls-btn cursor-pointer btn-speed">
|
||||
<span>{{!currentLang ? 'AI翻译' : currentLangLabel}}</span>
|
||||
<div class="speed-control">
|
||||
<ul class="speed-control-list">
|
||||
<li
|
||||
v-for="item in selectableLang"
|
||||
:key="item.srclang"
|
||||
@click="changeLang(item)"
|
||||
:data-value="item.srclang"
|
||||
class="one-line-ellipsis"
|
||||
:title="item.label"
|
||||
:class="{'current': currentLang === item.srclang}"
|
||||
>{{ item.label }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="isSubtitle" style="margin-top: -3px;">|</div>
|
||||
<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>
|
||||
<div style="margin-top: -3px;">|</div>
|
||||
</div>
|
||||
<div class="player-controls-btn cursor-pointer btn-speed">
|
||||
<span>{{currentSpeed === 1 ? '倍速' : `${currentSpeed}x`}}</span>
|
||||
<div class="speed-control">
|
||||
@@ -224,6 +252,7 @@
|
||||
import volumeBar from "@/components/VideoPlayer/volume-bar.vue";
|
||||
import progressBar from "@/components/VideoPlayer/progress-bar.vue";
|
||||
import playerBarrageScreen from "@/components/VideoPlayer/player-barrage-screen.vue";
|
||||
import { mapGetters, mapMutations } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "barrage-videoplayer",
|
||||
@@ -301,10 +330,18 @@ export default {
|
||||
fullTimeFormat: "00:00:00", // 视频总长度的文字
|
||||
barrageTimelineStart: 0, // 弹幕时间轴的起始时间点(手动调整进度条触发更新)
|
||||
isInit:false, // 是否初始化过
|
||||
// ai播放器相关
|
||||
isSubtitle: true, // 是否开启字幕
|
||||
currentLangLabel:'', // 当前字幕语言
|
||||
};
|
||||
},
|
||||
// ai播放器相关
|
||||
computed: {
|
||||
...mapGetters(['selectableLang','currentLang'])
|
||||
},
|
||||
created() {
|
||||
|
||||
// ai播放器相关
|
||||
this.SET_currentLang('');
|
||||
},
|
||||
mounted() {
|
||||
this.videoDom = this.$refs.video;
|
||||
@@ -411,6 +448,11 @@ export default {
|
||||
// });
|
||||
},
|
||||
methods: {
|
||||
// ai播放器相关
|
||||
...mapMutations({
|
||||
SET_currentLang: 'video/SET_currentLang',
|
||||
SET_currentTime: 'video/SET_currentTime',
|
||||
}),
|
||||
//当视频由于需要缓冲下一帧而停止,解决一直计时的问题
|
||||
onWaiting(){
|
||||
console.log('触发了onWairing');
|
||||
@@ -624,6 +666,8 @@ export default {
|
||||
},
|
||||
onAudioTimeUpdate() {
|
||||
const currentTime = this.$refs.video.currentTime;
|
||||
// ai播放器相关
|
||||
this.SET_currentTime(currentTime)
|
||||
this.$emit('onTimeUpdate', currentTime);
|
||||
},
|
||||
/**
|
||||
@@ -641,9 +685,77 @@ export default {
|
||||
this.$emit('onFullscreen',false);//全屏
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
/** ai播放器相关
|
||||
* 切换字幕
|
||||
*/
|
||||
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';
|
||||
}
|
||||
}
|
||||
},
|
||||
/** ai播放器相关
|
||||
* 切换字幕语言
|
||||
*/
|
||||
changeLang(item) {
|
||||
this.SET_currentLang(item.srclang);
|
||||
this.currentLangLabel = item.label;
|
||||
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 () {
|
||||
@@ -907,6 +1019,12 @@ export default {
|
||||
color: #fff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.box-aiTranslate{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
@media (device-width: 100vw) {
|
||||
.player-controls-btn .player-controls-icon {
|
||||
/* height: 26px; */
|
||||
|
||||
@@ -28,5 +28,10 @@ const getters = {
|
||||
studyTaskCount:state => state.user.studyTaskCount,
|
||||
praisesUnicom:state =>state.pdf.praisesUnicom,
|
||||
favoritesUnicom:state =>state.pdf.favoritesUnicom,
|
||||
// ai播放器相关
|
||||
selectAllLang:state => state.video.selectAllLang,
|
||||
selectableLang:state => state.video.selectableLang,
|
||||
currentLang:state => state.video.currentLang,
|
||||
currentTime:state => state.video.currentTime,
|
||||
}
|
||||
export default getters
|
||||
|
||||
@@ -12,6 +12,7 @@ import resOwner from './modules/resOwner'
|
||||
import majorType from './modules/majorType'
|
||||
import orgDomain from './modules/orgDomain'
|
||||
import pdf from './modules/pdf'
|
||||
import video from './modules/video' // ai播放器相关
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@@ -27,7 +28,8 @@ const store = new Vuex.Store({
|
||||
resOwner,
|
||||
majorType,
|
||||
orgDomain,
|
||||
pdf
|
||||
pdf,
|
||||
video
|
||||
},
|
||||
getters
|
||||
})
|
||||
|
||||
137
src/store/modules/video.js
Normal file
137
src/store/modules/video.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// ai播放器相关
|
||||
const state = {
|
||||
selectAllLang: [
|
||||
{
|
||||
key: 'ZH_CN',
|
||||
srclang: 'zh-CN',
|
||||
label: '中文',
|
||||
name: '中文',
|
||||
},
|
||||
{
|
||||
key: 'EN_US',
|
||||
srclang: 'en-US',
|
||||
label: '英语',
|
||||
name: 'English',
|
||||
},
|
||||
{
|
||||
key: 'JA_JP',
|
||||
srclang: 'ja-JP',
|
||||
label: '日语',
|
||||
name: '日本語',
|
||||
},
|
||||
{
|
||||
key: 'KO_KR',
|
||||
srclang: 'ko-KR',
|
||||
label: '韩语',
|
||||
name: '한국어',
|
||||
},
|
||||
{
|
||||
key: 'FR_FR',
|
||||
srclang: 'fr-FR',
|
||||
label: '法语',
|
||||
name: 'français',
|
||||
},
|
||||
{
|
||||
key: 'DE_DE',
|
||||
srclang: 'de-DE',
|
||||
label: '德语',
|
||||
name: 'Deutsch',
|
||||
},
|
||||
{
|
||||
key: 'ES_ES',
|
||||
srclang: 'es-ES',
|
||||
label: '西班牙语',
|
||||
name: 'español',
|
||||
},
|
||||
{
|
||||
key: 'RU_RU',
|
||||
srclang: 'ru-RU',
|
||||
label: '俄语',
|
||||
name: 'русский',
|
||||
},
|
||||
{
|
||||
key: 'PT_BR',
|
||||
srclang: 'pt-BR',
|
||||
label: '葡萄牙语',
|
||||
name: 'português',
|
||||
},
|
||||
{
|
||||
key: 'IT_IT',
|
||||
srclang: 'it-IT',
|
||||
label: '意大利语',
|
||||
name: 'italiano',
|
||||
},
|
||||
{
|
||||
key: 'AR_SA',
|
||||
srclang: 'ar-SA',
|
||||
label: '阿拉伯语',
|
||||
name: 'العربية',
|
||||
},
|
||||
{
|
||||
key: 'TH_TH',
|
||||
srclang: 'th-TH',
|
||||
label: '泰语',
|
||||
name: 'ไทย',
|
||||
},
|
||||
{
|
||||
key: 'VI_VN',
|
||||
srclang: 'vi-VN',
|
||||
label: '越南语',
|
||||
name: 'tiếng Việt',
|
||||
},
|
||||
{
|
||||
key: 'ID_ID',
|
||||
srclang: 'id-ID',
|
||||
label: '印度尼西亚语',
|
||||
name: 'Bahasa Indonesia',
|
||||
},
|
||||
{
|
||||
key: 'HI_IN',
|
||||
srclang: 'hi-IN',
|
||||
label: '印地语',
|
||||
name: 'हिन्दी',
|
||||
}
|
||||
], // 全部语言列表
|
||||
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 = {
|
||||
unicomPraises({ commit }, data) {
|
||||
commit('SET_praisesUnicom', data)
|
||||
},
|
||||
unicomFavorites({ commit }, data) {
|
||||
commit('SET_favoritesUnicom', data)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
mutations,
|
||||
actions
|
||||
}
|
||||
|
||||
@@ -483,8 +483,11 @@ export default {
|
||||
} else if (this.form.device2 === true) {
|
||||
this.form.device = 2;
|
||||
}
|
||||
//时长,秒与分钟的转化
|
||||
//if(this.form.)
|
||||
// 时长,秒与分钟的转化
|
||||
if (this.form.minute) {
|
||||
this.form.duration = this.form.minute * 60;
|
||||
}
|
||||
|
||||
try {
|
||||
const { status,message} = await coueseFile.batchUpdate([this.form]);
|
||||
if (status === 200) {
|
||||
|
||||
@@ -64,14 +64,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-row :gutter="20" style="margin-top:10px">
|
||||
<el-col :span="4">
|
||||
<!-- ai播放器相关 -->
|
||||
<el-col :span="24">
|
||||
<!-- <el-button icon="el-icon-folder" type="primary" size="small">导出</el-button> -->
|
||||
<el-button class="Create-coures" type="primary" @click="addNewCourse()" icon="el-icon-plus">新建课程</el-button>
|
||||
<el-button type="primary" @click="setLanguage()" icon="el-icon-connection" :disabled="selectedCourses.length === 0">设置语种</el-button>
|
||||
<el-button type="primary" @click="enableAI()" icon="el-icon-switch-button" :disabled="selectedCourses.length === 0">开启AI处理</el-button>
|
||||
</el-col >
|
||||
</el-row>
|
||||
</div>
|
||||
<div style="margin-right:30px;">
|
||||
<el-table style="margin:10px 32px 10px 22px;" :data="pageData" border stripe>
|
||||
<!-- ai播放器相关 -->
|
||||
<el-table style="margin:10px 32px 10px 22px;" :data="pageData" border stripe @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column label="序号" type="index" width="50"></el-table-column>
|
||||
<el-table-column v-if="forChoose" label="选择" width="80">
|
||||
<template slot-scope="scope" v-if="scope.row.published">
|
||||
@@ -136,6 +141,8 @@
|
||||
<el-table-column label="操作" width="180px" fixed="right">
|
||||
<template slot-scope="scope" class="btn-gl">
|
||||
<!-- 20240621 修改scope.row.isPermission = fasle 时不展示操作按钮-->
|
||||
<!-- ai播放器相关 -->
|
||||
<el-button v-if="scope.row.isPermission && scope.row.status != 2" type="text" size="mini" @click="setAI(scope.row)">AI设置</el-button>
|
||||
<el-button type="text" size="mini" v-if="scope.row.isPermission && scope.row.status == 5 && !scope.row.published" @click="releaseData(scope.row)">发布</el-button>
|
||||
<el-button v-if="scope.row.isPermission && pageManage && scope.row.published" @click="showStudent(scope.row)" type="text" size="mini">学员</el-button>
|
||||
<el-button v-if="scope.row.isPermission && !forChoose && scope.row.published" @click="showManageStudy(scope.row)" type="text" size="mini">管理</el-button>
|
||||
@@ -287,6 +294,231 @@
|
||||
<div>
|
||||
<course-form ref="courseForm" @submitSuccess="searchData" @close="searchData"></course-form>
|
||||
</div>
|
||||
|
||||
<!-- ai播放器相关 -->
|
||||
<!-- 设置语种弹框 -->
|
||||
<el-dialog
|
||||
title="AI翻译"
|
||||
:visible.sync="languageSetting.dlgShow"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div style="margin-bottom: 15px;">请选择课程所支持语种</div>
|
||||
<el-select
|
||||
v-model="languageSetting.selectedLanguages"
|
||||
multiple
|
||||
placeholder="请选择语种"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option
|
||||
v-for="lang in selectAllLang"
|
||||
:key="lang.srclang"
|
||||
:label="lang.label"
|
||||
:value="lang.srclang"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div style="color: #ff4d4f; font-size: 12px;">
|
||||
注:仅支持对已开启AI处理的课程进行批量语种设置;所选的课程中有{{languageSetting.aiSetNoNum}}个未开启AI处理的课程,以上配置仅对{{languageSetting.aiSetNum}}个已开启AI处理的课程生效。
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="languageSetting.dlgShow = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmLanguageSetting">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 开启AI处理弹框 -->
|
||||
<el-dialog
|
||||
title="开启AI处理"
|
||||
:visible.sync="aiProcessSetting.dlgShow"
|
||||
width="400px"
|
||||
>
|
||||
<div class="ai-process-dialog">
|
||||
<!-- AI处理状态 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">
|
||||
<el-tooltip class="item" effect="dark" :content="aiSetTip" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
</el-tooltip>
|
||||
AI处理:
|
||||
</span>
|
||||
<span class="status-text">
|
||||
{{ aiProcessSetting.aiSet === 1 ? '开启' : '关闭' }}
|
||||
</span>
|
||||
<el-switch
|
||||
v-model="aiProcessSetting.aiSet"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
></el-switch>
|
||||
</div>
|
||||
|
||||
<div v-show="aiProcessSetting.aiSet === 1">
|
||||
<!-- 是否生成AI摘要 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">
|
||||
<el-tooltip class="item" effect="dark" :content="aiAbstractTip" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
</el-tooltip>
|
||||
AI摘要:
|
||||
</span>
|
||||
<span class="status-text">
|
||||
{{ aiProcessSetting.aiAbstract === 1 ? '开启' : '关闭' }}
|
||||
</span>
|
||||
<el-switch
|
||||
v-model="aiProcessSetting.aiAbstract"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
>
|
||||
</el-switch>
|
||||
</div>
|
||||
|
||||
<!-- 是否生成AI文稿 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">
|
||||
<el-tooltip class="item" effect="dark" :content="aiDraftTip" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
</el-tooltip>
|
||||
AI文稿:
|
||||
</span>
|
||||
<span class="status-text">
|
||||
{{ aiProcessSetting.aiDraft === 1 ? '开启' : '关闭' }}
|
||||
</span>
|
||||
<el-switch
|
||||
v-model="aiProcessSetting.aiDraft"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
>
|
||||
</el-switch>
|
||||
</div>
|
||||
|
||||
<!-- 课程支持语种选择 -->
|
||||
<div class="form-item" style="flex-flow: column;align-items: baseline;">
|
||||
<span class="form-label">
|
||||
<el-tooltip class="item" effect="dark" :content="aiTranslateTip" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
</el-tooltip>
|
||||
AI翻译语种:
|
||||
</span>
|
||||
<el-select
|
||||
v-model="aiProcessSetting.languageCode"
|
||||
multiple
|
||||
style="width: 100%;"
|
||||
placeholder="请选择语种"
|
||||
>
|
||||
<el-option
|
||||
v-for="lang in selectAllLang"
|
||||
:key="lang.srclang"
|
||||
:label="lang.label"
|
||||
:value="lang.srclang"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="tips">
|
||||
<span>注:已跳过{{aiProcessSetting.aiSetNum}}个已开启AI处理的课程,仅更新剩余{{aiProcessSetting.aiSetNoNum}}个</span>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="aiProcessSetting.dlgShow = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAiProcess">确认</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<!-- AI设置弹框 -->
|
||||
<el-dialog
|
||||
title="AI设置"
|
||||
:visible.sync="aiSetting.dlgShow"
|
||||
width="500px"
|
||||
>
|
||||
<div class="ai-setting-dialog">
|
||||
|
||||
<!-- AI功能状态 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">
|
||||
<el-tooltip class="item" effect="dark" content="是否将课程进行AI处理" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
</el-tooltip>
|
||||
AI功能状态:
|
||||
</span>
|
||||
<span class="status-text">
|
||||
{{ aiSetting.aiSet === 1 ? '已开放' : '未开放' }}
|
||||
</span>
|
||||
<el-switch
|
||||
v-model="aiSetting.aiSet"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
></el-switch>
|
||||
</div>
|
||||
<div v-show="aiSetting.aiSet === 1">
|
||||
<!-- 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>
|
||||
<div class="action-buttons">
|
||||
<el-button type="text" @click="aiSetting.aiAbstract = 0">
|
||||
{{ aiSetting.aiAbstract === 0 ? '上架' : '下架' }}
|
||||
</el-button>
|
||||
<el-button type="text" >编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<div class="action-buttons">
|
||||
<el-button type="text" @click="aiSetting.aiDraft = 0">
|
||||
{{ aiSetting.aiDraft === 0 ? '上架' : '下架' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<div class="action-buttons">
|
||||
<el-button type="text" @click="aiSetting.aiTranslate = 0">
|
||||
{{ aiSetting.aiTranslate === 0 ? '上架' : '下架' }}
|
||||
</el-button>
|
||||
<el-button type="text" >编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支持语种 -->
|
||||
<div class="form-item">
|
||||
<span class="form-label">支持语种:</span>
|
||||
<div class="languages-list">
|
||||
<div class="language-tag">
|
||||
中文
|
||||
<span class="status-badge">已下架</span>
|
||||
</div>
|
||||
<div class="language-tag">
|
||||
英语
|
||||
<span class="status-badge">生成中</span>
|
||||
</div>
|
||||
<div class="language-tag">
|
||||
越南语
|
||||
<span class="status-badge status-badge--active">已上架</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="aiSetting.dlgShow = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAISetting">确认</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -307,8 +539,9 @@ import apiUserbasic from "@/api/boe/userbasic.js"
|
||||
export default {
|
||||
name: 'manageCourse',
|
||||
components: {courseForm, manager, auditCourse1, auditCourse2,adminPage},
|
||||
// ai播放器相关
|
||||
computed: {
|
||||
...mapGetters(['resOwnerMap','sysTypeMap','userInfo']),
|
||||
...mapGetters(['resOwnerMap','sysTypeMap','userInfo', 'selectAllLang']),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -392,6 +625,32 @@ export default {
|
||||
},
|
||||
extendRefId:'',
|
||||
extendRefType:'',
|
||||
// ai播放器相关
|
||||
selectedCourses: [], //已选课程
|
||||
languageSetting: { // 设置语种弹框
|
||||
dlgShow: false,
|
||||
languageCode: ['zh-CN', 'en-US'] // 默认选中的语种
|
||||
},
|
||||
aiProcessSetting: { // 开启AI处理弹框
|
||||
dlgShow: true,
|
||||
aiSet: 1,
|
||||
aiAbstract: 1,
|
||||
aiDraft: 1,
|
||||
languageCode: ['zh-CN', 'en-US'] // 默认选中的语种
|
||||
},
|
||||
aiSetting: { // AI设置弹框
|
||||
dlgShow: false,
|
||||
courseId: '',
|
||||
aiSet: 1,
|
||||
aiAbstract: 1, // 1:上架 0:下架
|
||||
aiDraft: 1, // 1:上架 0:下架
|
||||
aiTranslate: 1, // 1:上架 0:下架
|
||||
languageCode: ['zh-CN', 'en-US', 'vi-VN'] // 支持的语种
|
||||
},
|
||||
aiSetTip: '是否将课程进行AI处理', //提示信息
|
||||
aiAbstractTip: '一键提炼课程视频核心要点,助力学员课前高效掌握重点,快速筛选学习资源', // 提示信息
|
||||
aiDraftTip: '分段展示视频内容并精准同步时间轴,实现视频进度与文稿双向定位,学习内容触手可及', //提示信息
|
||||
aiTranslateTip: '智能转换视频字幕与语音为多语种,支持全球学员按需切换语言,打破学习边界', // 提示信息
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -449,6 +708,7 @@ export default {
|
||||
inputOn() {
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
// 置顶
|
||||
setTop(row) {
|
||||
let params = {
|
||||
@@ -885,6 +1145,143 @@ export default {
|
||||
saveNewCatalogZhang() {
|
||||
this.catalogs.addNewZhang = false;
|
||||
},
|
||||
|
||||
|
||||
// ai播放器相关
|
||||
handleSelectionChange(val){
|
||||
this.selectedCourses = val;
|
||||
console.log(val);
|
||||
},
|
||||
// 获取选中课程的AI信息
|
||||
getAIInfoByList(list = []) {
|
||||
let selectNum = 0; // 选中的课程数量
|
||||
let aiSetNum = 0; // 已设置AI的课程数量
|
||||
let aiSetNoNum = 0; // 未设置AI的课程数量
|
||||
list.forEach(item => {
|
||||
if(item.aiSet === 1){
|
||||
aiSetNum++;
|
||||
}else{
|
||||
aiSetNoNum++;
|
||||
}
|
||||
});
|
||||
return {
|
||||
selectNum,
|
||||
aiSetNum,
|
||||
aiSetNoNum
|
||||
}
|
||||
},
|
||||
// AI设置
|
||||
setAI(row) {
|
||||
this.aiSetting.courseId = row.id;
|
||||
// 这里可以添加获取当前课程AI设置的逻辑
|
||||
this.aiSetting.dlgShow = true;
|
||||
},
|
||||
|
||||
// 确认AI设置
|
||||
confirmAISetting() {
|
||||
const { courseId, aiSet, aiAbstract, aiDraft, aiTranslate } = this.aiSetting;
|
||||
|
||||
// 这里可以添加保存AI设置的API调用
|
||||
console.log('保存AI设置', {
|
||||
courseId,
|
||||
aiSet,
|
||||
aiAbstract,
|
||||
aiDraft,
|
||||
aiTranslate
|
||||
});
|
||||
|
||||
// 模拟成功保存
|
||||
this.$message.success('AI设置保存成功');
|
||||
this.aiSetting.dlgShow = false;
|
||||
// 可以选择是否刷新列表数据
|
||||
},
|
||||
setLanguage() {
|
||||
if (this.selectedCourses.length > 0) {
|
||||
this.languageSetting = {...this.languageSetting, ...this.getAIInfoByList(this.selectedCourses)}
|
||||
this.languageSetting.dlgShow = true;
|
||||
}
|
||||
},
|
||||
enableAI() {
|
||||
// 开启AI处理按钮点击事件
|
||||
if (this.selectedCourses.length > 0) {
|
||||
this.aiProcessSetting = {...this.aiProcessSetting, ...this.getAIInfoByList(this.selectedCourses)}
|
||||
this.aiProcessSetting.dlgShow = true;
|
||||
}
|
||||
},
|
||||
|
||||
async confirmLanguageSetting() {
|
||||
try {
|
||||
// 收集选中的课程ID
|
||||
const courseIds = this.selectedCourses.map(course => course.id).join(',');
|
||||
// 获取选择的语种
|
||||
const languages = this.languageSetting.selectedLanguages.join(',');
|
||||
|
||||
// 这里可以根据实际需求调用API进行语种设置
|
||||
console.log('批量设置课程语种', { courseIds, languages });
|
||||
|
||||
// 显示成功提示
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: `已成功为${this.selectedCourses.length}个课程设置语种`,
|
||||
offset: 50
|
||||
});
|
||||
|
||||
// 关闭弹框
|
||||
this.languageSetting.dlgShow = false;
|
||||
|
||||
// 刷新数据(如果需要)
|
||||
// this.searchData();
|
||||
} catch (error) {
|
||||
console.error('设置语种失败', error);
|
||||
this.$message({
|
||||
type: 'error',
|
||||
message: '设置语种失败,请稍后重试',
|
||||
offset: 50
|
||||
});
|
||||
}
|
||||
},
|
||||
async confirmAiProcess() {
|
||||
try {
|
||||
// 收集选中的课程ID
|
||||
const courseIds = this.selectedCourses.map(course => course.id).join(',');
|
||||
// 获取AI处理配置
|
||||
const { aiSet, aiAbstract, aiDraft, selectedLanguages } = this.aiProcessSetting;
|
||||
|
||||
// 这里可以根据实际需求调用API进行AI处理设置
|
||||
console.log('批量开启AI处理', {
|
||||
courseIds,
|
||||
aiSet,
|
||||
aiAbstract,
|
||||
aiDraft,
|
||||
languages: selectedLanguages.join(',')
|
||||
});
|
||||
|
||||
// 模拟计算跳过和更新的课程数量
|
||||
const totalCourses = this.selectedCourses.length;
|
||||
const skippedCount = Math.floor(totalCourses * 0.2); // 假设20%的课程已开启AI处理
|
||||
const updatedCount = totalCourses - skippedCount;
|
||||
|
||||
// 显示成功提示
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: `已跳过${skippedCount}个已开启AI处理的课程,成功为${updatedCount}个课程更新AI处理设置`,
|
||||
offset: 50
|
||||
});
|
||||
|
||||
// 关闭弹框
|
||||
this.aiProcessSetting.dlgShow = false;
|
||||
|
||||
// 刷新数据
|
||||
this.searchData();
|
||||
} catch (error) {
|
||||
console.error('开启AI处理失败', error);
|
||||
this.$message({
|
||||
type: 'error',
|
||||
message: '开启AI处理失败,请稍后重试',
|
||||
offset: 50
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -953,4 +1350,38 @@ export default {
|
||||
.el-dialog__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-item{
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.tips {
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
margin: 10px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status-badge{
|
||||
display: inline-block;
|
||||
padding: 3px 13px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
background: rgba(254, 249, 195, 1);
|
||||
color: rgba(133, 77, 14, 1);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 17px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
.status-badge--active{
|
||||
background: rgba(220, 252, 231, 1);
|
||||
color: rgba(22, 101, 52, 1);
|
||||
}
|
||||
.languages-list{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -376,8 +376,25 @@
|
||||
<div class="couresstartTime">
|
||||
<span v-if="cinfo.type == 30 && cinfo.startTime">开课时间:{{ cinfo.startTime }}</span>
|
||||
</div>
|
||||
|
||||
<div class="course-info">
|
||||
<!-- ai播放器相关 -->
|
||||
<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>
|
||||
@@ -387,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>
|
||||
@@ -2422,3 +2439,43 @@ console.log(res.result.list,'data')
|
||||
.option-active {
|
||||
color: #387DF7;
|
||||
}</style>
|
||||
<!-- ai播放器相关 -->
|
||||
<style lang="scss">
|
||||
.course-popover{
|
||||
border-radius: 12px;
|
||||
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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user