mirror of
https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal.git
synced 2025-12-20 00:06:45 +08:00
Compare commits
4 Commits
fix_1155
...
player-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e1517c114 | ||
|
|
1e0a911e4e | ||
|
|
423275ed08 | ||
|
|
04e163b63e |
142
src/api/modules/courseAiVideo.js
Normal file
142
src/api/modules/courseAiVideo.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import ajax from '@/utils/xajax.js'
|
||||||
|
const BASE_URL = process.env.NODE_ENV=='development'?'/aiVideo':'';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI摘要-摘要课程详情
|
||||||
|
* @param {Object} data
|
||||||
|
* {
|
||||||
|
courseId: "COURSE_001"//课程id
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const getCourse = function(data) {
|
||||||
|
return ajax.postJson(BASE_URL+'/aiVideo/getCourse', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI摘要-摘要详情
|
||||||
|
* @param {Object} data
|
||||||
|
* {
|
||||||
|
courseId: "COURSE_001"//课程id
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const selectVideoSummary = function(data) {
|
||||||
|
return ajax.postJson(BASE_URL+'/videoSummary/selectVideoSummary', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI摘要-更新摘要详情
|
||||||
|
* @param {Object} data
|
||||||
|
* {
|
||||||
|
courseId:课程的id
|
||||||
|
summaryContent: 摘要内容
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const updateVideoSummary = function(data) {
|
||||||
|
return ajax.postJson(BASE_URL+'/videoSummary/updateVideoSummary', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI摘要-根据视频id查询详情
|
||||||
|
* @param {Object} id 视频id
|
||||||
|
*/
|
||||||
|
const selectById = function(id) {
|
||||||
|
return ajax.postJson(BASE_URL+'/videoSummary/selectById/'+id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI摘要-下载课程摘要文本
|
||||||
|
* @param {Object} courseId 课程 的id
|
||||||
|
*/
|
||||||
|
const downloadSummaryTxt = function(courseId) {
|
||||||
|
let requestParam ={
|
||||||
|
url: BASE_URL+'/videoSummary/downloadSummaryTxt?courseId='+courseId,
|
||||||
|
timeout: 0, // 下载不设置超时
|
||||||
|
responseType: 'blob', // 表明返回服务器返回的数据类型
|
||||||
|
}
|
||||||
|
return ajax.get(requestParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI摘要-重新生成课程摘要
|
||||||
|
* {
|
||||||
|
courseId: "COURSE_001"//课程id
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const retrySummaryTxt = function(data) {
|
||||||
|
return ajax.postJson(BASE_URL+'/videoSummary/retrySummaryTxt',data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI翻译-获取视频语种下拉框
|
||||||
|
* @param {Object} videoId 视频的id
|
||||||
|
*/
|
||||||
|
const getTextDropdown = function(videoId) {
|
||||||
|
return ajax.postJson(BASE_URL+'/subtitle/getTextDropdown',{videoId});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI翻译-获取视频指定语种字幕
|
||||||
|
* @param {object} data
|
||||||
|
{
|
||||||
|
"videoId": "VIDEO_111",
|
||||||
|
"language": "zh-CN"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const getTextDetail = function(data) {
|
||||||
|
return ajax.postJson(BASE_URL+'/subtitle/getTextDetail',data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI翻译-更新字幕
|
||||||
|
* @param {Object} data
|
||||||
|
* {
|
||||||
|
"videoId": "VIDEO_001",
|
||||||
|
"language": "zh-CN",
|
||||||
|
"subtitleData": "dddd",
|
||||||
|
"updateStatus": 1, // 0-编辑 1-重新生成 2-同步更新
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const updateText = function(data) {
|
||||||
|
return ajax.postJson(BASE_URL+'/subtitle/updateText',data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI翻译-下载字幕
|
||||||
|
* @param {Object} params
|
||||||
|
{
|
||||||
|
videoId: 视频的id,
|
||||||
|
language: 语种
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const downloadText = function(studyId,contentId) {
|
||||||
|
return ajax.get(BASE_URL+'/subtitle/downloadText', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI翻译-同步跟更新字幕
|
||||||
|
* @param {Object} data
|
||||||
|
{
|
||||||
|
"videoId": "VIDEO_010",
|
||||||
|
"language": "zh-CN"
|
||||||
|
}
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const regenerate = function(data) {
|
||||||
|
return ajax.postJson(BASE_URL+'/subtitle/regenerate',data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
selectVideoSummary,
|
||||||
|
updateVideoSummary,
|
||||||
|
selectById,
|
||||||
|
retrySummaryTxt,
|
||||||
|
updateText,
|
||||||
|
downloadText,
|
||||||
|
getTextDetail,
|
||||||
|
regenerate,
|
||||||
|
getTextDropdown,
|
||||||
|
downloadSummaryTxt,
|
||||||
|
getCourse,
|
||||||
|
}
|
||||||
@@ -463,7 +463,7 @@
|
|||||||
<el-button type="text" @click="changeAIKey('aiAbstract')">
|
<el-button type="text" @click="changeAIKey('aiAbstract')">
|
||||||
{{ aiSetting.aiAbstract === 1 ? '下架' : '上架' }}
|
{{ aiSetting.aiAbstract === 1 ? '下架' : '上架' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-show="false" type="text" >编辑</el-button>
|
<el-button v-show="isV2" type="text" @click="toAiAbstract">编辑</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -490,7 +490,7 @@
|
|||||||
<el-button type="text" @click="changeAIKey('aiTranslate')">
|
<el-button type="text" @click="changeAIKey('aiTranslate')">
|
||||||
{{ aiSetting.aiTranslate === 1 ? '下架' : '上架' }}
|
{{ aiSetting.aiTranslate === 1 ? '下架' : '上架' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-show="false" type="text" >编辑</el-button>
|
<el-button v-show="isV2" type="text" @click="toAiTranslate">编辑</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -657,6 +657,7 @@ export default {
|
|||||||
aiAbstractTip: '一键提炼课程视频核心要点,助力学员课前高效掌握重点,快速筛选学习资源', // 提示信息
|
aiAbstractTip: '一键提炼课程视频核心要点,助力学员课前高效掌握重点,快速筛选学习资源', // 提示信息
|
||||||
aiDraftTip: '分段展示视频内容并精准同步时间轴,实现视频进度与文稿双向定位,学习内容触手可及', //提示信息
|
aiDraftTip: '分段展示视频内容并精准同步时间轴,实现视频进度与文稿双向定位,学习内容触手可及', //提示信息
|
||||||
aiTranslateTip: '智能转换视频字幕与语音为多语种,支持全球学员按需切换语言,打破学习边界', // 提示信息
|
aiTranslateTip: '智能转换视频字幕与语音为多语种,支持全球学员按需切换语言,打破学习边界', // 提示信息
|
||||||
|
isV2: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -1297,7 +1298,23 @@ export default {
|
|||||||
console.log('index', index, this.aiPermission);
|
console.log('index', index, this.aiPermission);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
toAiAbstract() {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/iframe/course/aiAbstract',
|
||||||
|
query: {
|
||||||
|
id: this.aiSetting.id
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toAiTranslate() {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/iframe/course/aiTranslate',
|
||||||
|
query: {
|
||||||
|
id: this.aiSetting.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aiAbstract">
|
<div class="aiAbstract">
|
||||||
<div class="ai-left">
|
<div class="ai-left" v-if="sections.length > 0">
|
||||||
<div class="left-title">{{courseName}}</div>
|
<div class="left-title">{{courseInfo.name}}</div>
|
||||||
<ul class="ai-list">
|
<ul class="ai-list" >
|
||||||
<li class="ai-item" v-for="(item, index) in videoList" @click="currentVideo = item" :class="{'active': currentVideo.id === item.id}" :key="index">
|
<template v-for="c in chapterList">
|
||||||
<div class="ai-item-title">{{item.name}}</div>
|
<li class="ai-item" v-for="(item, index) in c.children" @click="handleSelectVideo(item, index+1)" :class="{'active': currentVideo.id === item.id}" :key="item.id">
|
||||||
|
<div class="ai-item-title">{{item.chapterName}}-{{'视频' + (index + 1)}}</div>
|
||||||
</li>
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-right">
|
<div class="ai-right">
|
||||||
<div class="right-title">
|
<div class="right-title">
|
||||||
<h3>{{currentVideo.name}}</h3>
|
<h3>{{videoName}}</h3>
|
||||||
<div>
|
<div>
|
||||||
<el-button type="primary" @click="status = 1">下架本课程AI摘要</el-button>
|
<el-button type="primary" @click="handleSummaryStatus">{{courseInfo.aiAbstract == 1 ? '下架' : '上架'}}本课程AI摘要</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-content">
|
<div class="ai-content">
|
||||||
<div class="videoBox">
|
<div class="videoBox">
|
||||||
<videoPlayer :src="testUrl" style="height: auto;"> </videoPlayer>
|
<videoPlayer
|
||||||
|
:src="blobUrl"
|
||||||
|
:blobId="blobId"
|
||||||
|
style="height: auto;"> </videoPlayer>
|
||||||
<div class="video-content">
|
<div class="video-content">
|
||||||
<h4>视频摘要</h4>
|
<h4>视频摘要</h4>
|
||||||
<p>
|
<p>
|
||||||
{{currentVideo.aiAbstract}}
|
{{videoSummary}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,17 +34,18 @@
|
|||||||
<div class="opera-title">
|
<div class="opera-title">
|
||||||
<h4>课程摘要</h4>
|
<h4>课程摘要</h4>
|
||||||
<div class="opera-btn">
|
<div class="opera-btn">
|
||||||
<el-button type="primary" plain round size="mini" @click="status = 2">重新生成</el-button>
|
<el-button v-if="type == 1 || type == 3" type="primary" plain round size="mini" @click="retrySummaryTxt">重新生成</el-button>
|
||||||
<el-button type="primary" plain round size="mini" @click="status = 4">编辑</el-button>
|
<el-button v-if="type == 1" type="primary" plain round size="mini" @click="type = 4">编辑</el-button>
|
||||||
<el-button plain round size="mini" @click="status = 3">取消</el-button>
|
<el-button v-if="type == 4" plain round size="mini" @click="type = 1">取消</el-button>
|
||||||
|
<el-button v-if="type == 4" type="primary" plain round size="mini" @click="updateSummary">确认</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="opera-content" v-show="status != 4">
|
<div class="opera-content" v-show="type != 4">
|
||||||
<span v-show="status == 1">{{ aiAbstract }}</span>
|
<span v-show="type == 1">{{ aiAbstract }}</span>
|
||||||
<p v-show="status == 2" style="color: rgba(207, 207, 207, 1);text-align: center;margin-top: 48%;">AI 摘要重新生成中,过程可能耗时较长,<br>无需在此等待哦~</p>
|
<p v-show="type == 2" style="color: rgba(207, 207, 207, 1);text-align: center;margin-top: 48%;">AI 摘要重新生成中,过程可能耗时较长,<br>无需在此等待哦~</p>
|
||||||
<img v-show="status == 3" src="@/assets/images/course/generationFailed.png" alt="" width="150" height="159" style="display: flex;margin: 35% auto 0 auto;">
|
<img v-show="type == 3" src="@/assets/images/course/generationFailed.png" alt="" width="150" height="159" style="display: flex;margin: 35% auto 0 auto;">
|
||||||
</div>
|
</div>
|
||||||
<el-input v-show="status == 4"
|
<el-input v-show="type == 4"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
placeholder="请输入内容"
|
placeholder="请输入内容"
|
||||||
autosize
|
autosize
|
||||||
@@ -52,7 +58,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters, mapMutations } from "vuex";
|
||||||
import videoPlayer from "@/components/VideoPlayer/index.vue";
|
import videoPlayer from "@/components/VideoPlayer/index.vue";
|
||||||
|
import apiStudy from "@/api/modules/courseStudy.js";
|
||||||
|
import cookies from "vue-cookies";
|
||||||
|
import apiAiVideo from "@/api/modules/courseAiVideo.js";
|
||||||
|
import apiCourse from '@/api/modules/course.js';
|
||||||
|
import { encrypt } from "@/utils/jsencrypt.js";
|
||||||
export default {
|
export default {
|
||||||
name: 'aiAbstract',
|
name: 'aiAbstract',
|
||||||
// ai播放器相关
|
// ai播放器相关
|
||||||
@@ -61,39 +73,238 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
courseName: '企业经营法则--课程单元',
|
chapterList: [],
|
||||||
videoList: [
|
videoName: '',
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: '1. 开源节流1',
|
|
||||||
aiAbstract: '人工智能(AI)是让计算机模拟人类智能的技术,核心包括机器学习、深度学习等。主要分为弱 AI(专注特定任务)和强 AI(通用智能)两类。应用涵盖医疗诊断、自动驾驶、语音助手等多个领域。它通过数据学习模式,实现预测和适应能力,正在改变生活方式和工作方式。未来发展需平衡创新与伦理考量,确保对人类社会有益。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '2. 企业经营法则总述',
|
|
||||||
aiAbstract: '本课程将介绍企业经营法则的总述,包括企业经营的基本原理、经营策略、经营模式等。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '3. 企业经营法则总述',
|
|
||||||
aiAbstract: '本课程将介绍企业经营法则的总述,包括企业经营的基本原理、经营策略、经营模式等。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '4. 企业经营法则总述',
|
|
||||||
aiAbstract: '本课程将介绍企业经营法则的总述,包括企业经营的基本原理、经营策略、经营模式等。'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
testUrl: 'https://vjs.zencdn.net/v/oceans.mp4',
|
|
||||||
currentVideo: {},
|
currentVideo: {},
|
||||||
aiAbstract: '人工智能(AI)是让计算机模拟人类智能的技术,核心包括机器学习、深度学习等。主要分为弱 AI(专注特定任务)和强 AI(通用智能)两类。应用涵盖医疗诊断、自动驾驶、语音助手等多个领域。它通过数据学习模式,实现预测和适应能力,正在改变生活方式和工作方式。未来发展需平衡创新与伦理考量,确保对人类社会有益。',
|
aiAbstract: '',
|
||||||
status: '1', // 1: 正常 2: 生成中 3: 错误 4: 编辑中
|
type: '1', // 1: 正常 2: 生成中 3: 错误 4: 编辑中
|
||||||
|
courseId: '',
|
||||||
|
courseInfo: {},
|
||||||
|
sections: [],
|
||||||
|
blobId: '',
|
||||||
|
blobUrl: '',
|
||||||
|
videoSummary: '',
|
||||||
|
summaryTimer: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
beforeDestroy() {
|
||||||
this.currentVideo = this.videoList[0];
|
// 组件销毁前清除定时器
|
||||||
|
if (this.summaryTimer) {
|
||||||
|
clearInterval(this.summaryTimer);
|
||||||
|
this.summaryTimer = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {}
|
created() {
|
||||||
|
this.courseId = this.$route.query.id;
|
||||||
|
console.log(this.courseId);
|
||||||
|
this.getCourseInfo();
|
||||||
|
this.getCourseSummary();
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
SET_selectableLang: 'video/SET_selectableLang',
|
||||||
|
SET_courseInfo: 'video/SET_courseInfo',
|
||||||
|
}),
|
||||||
|
// 上下架状态
|
||||||
|
handleSummaryStatus() {
|
||||||
|
apiCourse.benchAiSet({courseList:[{id: this.courseId, aiAbstract: this.courseInfo.aiAbstract == 1 ? 0 : 1}]}).then(res => {
|
||||||
|
if(res.status === 200){
|
||||||
|
this.$message.success(this.courseInfo.aiAbstract == 1 ? '下架成功!' : '上架成功!');
|
||||||
|
this.getCourseInfo();
|
||||||
|
}else{
|
||||||
|
this.$message.error(this.courseInfo.aiAbstract == 1 ? '下架失败!' : '上架失败!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 下载摘要 - 不需要
|
||||||
|
downloadSummary() {
|
||||||
|
apiAiVideo.downloadSummaryTxt(this.courseId)
|
||||||
|
.then((rs) => {
|
||||||
|
console.log('下载',rs);
|
||||||
|
// 1. 创建 Blob 的临时 URL
|
||||||
|
const url = URL.createObjectURL(rs);
|
||||||
|
|
||||||
|
// 2. 创建隐藏的 <a> 标签
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = '课程摘要.txt'; // 设置下载的文件名
|
||||||
|
a.style.display = 'none';
|
||||||
|
|
||||||
|
// 3. 添加到文档中并触发点击
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// 4. 清理
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url); // 释放 URL 对象
|
||||||
|
}, 100);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSummary() {
|
||||||
|
apiAiVideo.updateVideoSummary({
|
||||||
|
courseId: this.courseId,
|
||||||
|
summaryContent: this.aiAbstract,
|
||||||
|
})
|
||||||
|
.then((rs) => {
|
||||||
|
if (rs.rspCode == '0000') {
|
||||||
|
this.type = '1';
|
||||||
|
this.getCourseSummary();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
retrySummaryTxt() {
|
||||||
|
this.type = '2';
|
||||||
|
apiAiVideo.retrySummaryTxt({
|
||||||
|
courseId: this.courseId,
|
||||||
|
})
|
||||||
|
.then((rs) => {
|
||||||
|
if (rs.rspCode == '0000') {
|
||||||
|
this.type = '1';
|
||||||
|
this.getCourseSummary();
|
||||||
|
} else {
|
||||||
|
this.type = '3';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getVideoSummary(id) {
|
||||||
|
apiAiVideo.selectById(id)
|
||||||
|
.then((rs) => {
|
||||||
|
this.videoSummary = ''
|
||||||
|
if (rs.rspCode == '0000') {
|
||||||
|
this.videoSummary = rs.data.briefSummary || '';
|
||||||
|
} else {
|
||||||
|
this.$message.error('获取视频摘要失败!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.videoSummary = ''
|
||||||
|
this.$message.error('获取视频摘要失败!');
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getCourseSummary() {
|
||||||
|
apiAiVideo.getCourse({
|
||||||
|
courseId: this.courseId,
|
||||||
|
})
|
||||||
|
.then((rs) => {
|
||||||
|
// 任务状态 courseSummaryTaskStatus 0-未开始 1-执行中 2-执行完成 3-执行失败 4-重试中
|
||||||
|
// 页面状态 type 1: 正常 2: 生成中 3: 错误 4: 编辑中
|
||||||
|
let taskStatus = rs.data.courseSummaryTaskStatus;
|
||||||
|
|
||||||
|
// 统一清除可能存在的旧定时器
|
||||||
|
if (this.summaryTimer) {
|
||||||
|
clearInterval(this.summaryTimer);
|
||||||
|
this.summaryTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskStatus == 1 || taskStatus == 4) {
|
||||||
|
this.type = 2;
|
||||||
|
// 设置新定时器,每隔1秒调用一次
|
||||||
|
this.summaryTimer = setInterval(() => {
|
||||||
|
this.getCourseSummary();
|
||||||
|
}, 5000);
|
||||||
|
} else if (taskStatus == 3) {
|
||||||
|
this.type = 3;
|
||||||
|
} else {
|
||||||
|
this.type = 1;
|
||||||
|
}
|
||||||
|
this.aiAbstract = rs.data.summaryContent || '';
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 请求失败时也清除定时器
|
||||||
|
if (this.summaryTimer) {
|
||||||
|
clearInterval(this.summaryTimer);
|
||||||
|
this.summaryTimer = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createPlayUrl(fid, u) {
|
||||||
|
let nowDate = new Date();
|
||||||
|
let ctime = parseInt(nowDate.getTime() / 1000);
|
||||||
|
let beforeUrl = parseInt(nowDate.getTime() / 1000) + "/" + fid;
|
||||||
|
let urlSign = encodeURIComponent(encrypt(beforeUrl));
|
||||||
|
cookies.set("PLAYSIGN_TIME", ctime); //写客户端的cookie保存
|
||||||
|
//以下判断是为了区分本地环境和服务器环境
|
||||||
|
if (process.env.NODE_ENV == "development") {
|
||||||
|
this.blobUrl = process.env.VUE_APP_FILE_BASE_URL + u;
|
||||||
|
} else {
|
||||||
|
this.blobUrl =
|
||||||
|
process.env.VUE_APP_BASE_API +
|
||||||
|
"/xboe/m/course/cware/resource?sign=" +
|
||||||
|
urlSign;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSelectVideo(item, index) {
|
||||||
|
console.log('选择了该视频', item);
|
||||||
|
this.SET_selectableLang(item?.boeaiSubtitleRspList);
|
||||||
|
this.videoName = item.chapterName ? item.chapterName + "-视频" + index : this.courseInfo.name;
|
||||||
|
this.currentVideo = item;
|
||||||
|
let curriculumData = {};
|
||||||
|
if (item.content.startsWith("{")) {
|
||||||
|
curriculumData = JSON.parse(item.content);
|
||||||
|
} else {
|
||||||
|
curriculumData.url = item.content;
|
||||||
|
}
|
||||||
|
this.getVideoSummary(item.id);
|
||||||
|
this.blobId = item.id;
|
||||||
|
this.createPlayUrl(item.contentRefId, curriculumData.url);
|
||||||
|
},
|
||||||
|
getCourseInfo() {
|
||||||
|
apiStudy.studyIndexPost({
|
||||||
|
cid: this.courseId,
|
||||||
|
addView: false,
|
||||||
|
audiences: '',
|
||||||
|
})
|
||||||
|
.then((rs) => {
|
||||||
|
if (rs.status == 200) {
|
||||||
|
this.courseInfo = rs.result.course || {};
|
||||||
|
this.SET_courseInfo(this.courseInfo);
|
||||||
|
const contents = rs.result.contents?.filter(item => item.contentType == 10) || [];
|
||||||
|
this.sections = rs.result.sections || [];
|
||||||
|
if (this.sections.length > 0) {
|
||||||
|
let treeList = [];
|
||||||
|
let hasOne = false;
|
||||||
|
this.sections.forEach(section => {
|
||||||
|
let treeNode = {
|
||||||
|
...section,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
contents.forEach(item => {
|
||||||
|
if (section.id == item.csectionId) {
|
||||||
|
item.chapterName = section.name || '';
|
||||||
|
treeNode.children.push(item);
|
||||||
|
if (!hasOne) {
|
||||||
|
this.handleSelectVideo(item, 1);
|
||||||
|
hasOne = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
treeList.push(treeNode);
|
||||||
|
})
|
||||||
|
this.chapterList = treeList;
|
||||||
|
} else {
|
||||||
|
this.handleSelectVideo(contents[0], 1);
|
||||||
|
}
|
||||||
|
console.log('33333',this.courseInfo, this.courseInfo.name);
|
||||||
|
this.aiAbstract = this.courseInfo.summaryContent
|
||||||
|
if (contents.length == 0) {
|
||||||
|
$this.$message.error("课程内容已删除或课程已不再使用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!rs.result.course.enabled) {
|
||||||
|
$this.$message.error(
|
||||||
|
"十分抱歉,此课程已停用,如需使用,请联系管理员。"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -156,6 +367,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 28px;
|
padding: 0 28px;
|
||||||
height: 76px;
|
height: 76px;
|
||||||
|
min-height: 76px;
|
||||||
border-bottom: 1px solid rgba(229, 231, 235, 1);
|
border-bottom: 1px solid rgba(229, 231, 235, 1);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aiTranslate">
|
<div class="aiTranslate">
|
||||||
<div class="ai-left">
|
<div class="ai-left" v-if="sections.length > 0">
|
||||||
<div class="left-title">{{courseName}}</div>
|
<div class="left-title">{{courseInfo.name}}</div>
|
||||||
<ul class="ai-list">
|
<ul class="ai-list" >
|
||||||
<li class="ai-item" v-for="(item, index) in videoList" @click="currentVideo = item" :class="{'active': currentVideo.id === item.id}" :key="index">
|
<template v-for="c in chapterList">
|
||||||
<div class="ai-item-title">{{item.name}}</div>
|
<li class="ai-item" v-for="item in c.children" @click="handleSelectVideo(item)" :class="{'active': currentVideo.id === item.id}" :key="item.id">
|
||||||
|
<div class="ai-item-title">{{item.chapterName}}-{{'视频' + item.videoIndex}}</div>
|
||||||
</li>
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-right">
|
<div class="ai-right">
|
||||||
<div class="right-title">
|
<div class="right-title">
|
||||||
<h3>{{currentVideo.name}}</h3>
|
<h3>{{videoName}}</h3>
|
||||||
<div>
|
<div>
|
||||||
<el-button type="primary" @click="status = 1">下架本课程AI翻译</el-button>
|
<el-button type="primary" @click="handleSummaryStatus()">
|
||||||
|
{{ courseInfo.aiTranslate == 1 ? '下架本课程AI翻译' : '上架本课程AI翻译' }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-content">
|
<div class="ai-content">
|
||||||
<div class="videoBox">
|
<div class="videoBox">
|
||||||
<videoPlayer :src="testUrl" style="height: auto;"> </videoPlayer>
|
<videoPlayer
|
||||||
|
:src="blobUrl"
|
||||||
|
:blobId="blobId"
|
||||||
|
style="height: auto;"> </videoPlayer>
|
||||||
<div class="video-content">
|
<div class="video-content">
|
||||||
<div class="select-lang">
|
<div class="select-lang">
|
||||||
<img src="@/assets/images/course/languageIcon.png" alt="" width="10" height="10">
|
<img src="@/assets/images/course/languageIcon.png" alt="" width="10" height="10">
|
||||||
@@ -36,35 +43,37 @@
|
|||||||
<div class="videoOperation">
|
<div class="videoOperation">
|
||||||
<div class="opera-title">
|
<div class="opera-title">
|
||||||
<span>目标语种</span>
|
<span>目标语种</span>
|
||||||
<el-select v-model="value" placeholder="请选择目标语种" style="flex: 1;">
|
<el-select v-model="currentLang" placeholder="请选择目标语种" style="flex: 1;">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in selectAllLang"
|
v-for="item in currentLangObjList"
|
||||||
:key="item.srclang"
|
:key="item.srclang"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:value="item.srclang"
|
:value="item.srclang"
|
||||||
>
|
>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button type="primary" @click="status = 2">加载字幕</el-button>
|
<el-button type="primary" @click="handleLoadSubtitle()">加载字幕</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="opera-content">
|
<div class="opera-content">
|
||||||
<div class="bg-gray" v-show="status != 4">
|
<div class="bg-gray" v-show="type != 4">
|
||||||
<p v-show="status == 1" v-html="aiTranslate"></p>
|
<p v-show="type == 1" v-html="aiTranslate"></p>
|
||||||
<p v-show="status == 2" style="color: rgba(207, 207, 207, 1);text-align: center;margin-top: 48%;">AI 翻译重新生成中,过程可能耗时较长,<br>无需在此等待哦~</p>
|
<p v-show="type == 2" style="color: rgba(207, 207, 207, 1);text-align: center;margin-top: 48%;">AI 翻译重新生成中,过程可能耗时较长,<br>无需在此等待哦~</p>
|
||||||
<img v-show="status == 3" src="@/assets/images/course/generationFailed.png" alt="" width="150" height="159" style="display: flex;margin: 35% auto 0 auto;">
|
<img v-show="type == 3" src="@/assets/images/course/generationFailed.png" alt="" width="150" height="159" style="display: flex;margin: 35% auto 0 auto;">
|
||||||
<img v-show="status == 5" src="@/assets/images/course/selectLanguage.png" alt="" width="112" height="130" style="display: flex;margin: 35% auto 0 auto;">
|
<img v-show="type == 5" src="@/assets/images/course/selectLanguage.png" alt="" width="82" height="100" style="display: flex;margin: 35% auto 0 auto;">
|
||||||
</div>
|
</div>
|
||||||
<el-input v-show="status == 4"
|
<el-input v-show="type == 4"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
placeholder="请输入内容"
|
placeholder="请输入内容"
|
||||||
autosize
|
autosize
|
||||||
v-model="aiTranslate">
|
v-model="aiTranslate">
|
||||||
</el-input>
|
</el-input>
|
||||||
<div class="opera-btn">
|
<div class="opera-btn">
|
||||||
<el-button v-show="status == 1" type="primary" plain round size="mini" @click="updateDialogVisible = true">重新生成</el-button>
|
<!-- // 1: 正常 2: 生成中 3: 错误 4: 编辑中 5: 未选择语种 -->
|
||||||
<el-button v-show="status == 1" type="primary" plain round size="mini" @click="status = 4">编辑</el-button>
|
<el-button v-show="(type == 1 || type == 3) && currentLang != 'zh-CN'" type="primary" plain round size="mini" @click="handleGenerate()">重新生成</el-button>
|
||||||
<el-button v-show="status == 4" plain round size="mini" @click="status = 1">取消</el-button>
|
<el-button v-show="type == 1 && currentLang == 'zh-CN'" type="primary" plain round size="mini" @click="updateDialogVisible = true">同步更新</el-button>
|
||||||
<el-button v-show="status == 4" type="primary" plain round size="mini" @click="status = 1">确认</el-button>
|
<el-button v-show="type == 1" type="primary" plain round size="mini" @click="type = 4">编辑</el-button>
|
||||||
|
<el-button v-show="type == 4" plain round size="mini" @click="type = 1">取消</el-button>
|
||||||
|
<el-button v-show="type == 4" type="primary" plain round size="mini" @click="submitSubtitle">确认</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,17 +88,17 @@
|
|||||||
<p style="text-align: center;">系统将根据当前最新中文内容重新生成其他语种的翻译,您此前对翻译的修改将会丢失!</p>
|
<p style="text-align: center;">系统将根据当前最新中文内容重新生成其他语种的翻译,您此前对翻译的修改将会丢失!</p>
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
<el-button @click="updateDialogVisible = false">取 消</el-button>
|
<el-button @click="updateDialogVisible = false">取 消</el-button>
|
||||||
<el-button style="margin-left: 60px;" type="primary" @click="updateDialogVisible = false">确认同步更新</el-button>
|
<el-button style="margin-left: 60px;" type="primary" @click="handleSyncUpdate()">确认同步更新</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="AIf翻译"
|
title="AI翻译"
|
||||||
:visible.sync="selectDialogVisible"
|
:visible.sync="selectDialogVisible"
|
||||||
width="500px"
|
width="500px"
|
||||||
class="select-dialog">
|
class="select-dialog">
|
||||||
<div class="select-dialog-content">
|
<div class="select-dialog-content">
|
||||||
<p>请选择该课程所支持语种</p>
|
<p>请选择该课程所支持语种</p>
|
||||||
<el-select v-model="selectLang" placeholder="请选择目标语种" style="width: 100%;" multiple>
|
<el-select v-model="currentLangList" placeholder="请选择目标语种" style="width: 100%;" multiple>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in selectAllLang"
|
v-for="item in selectAllLang"
|
||||||
:key="item.srclang"
|
:key="item.srclang"
|
||||||
@@ -101,7 +110,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
<el-button @click="selectDialogVisible = false">取 消</el-button>
|
<el-button @click="selectDialogVisible = false">取 消</el-button>
|
||||||
<el-button style="" type="primary" @click="selectDialogVisible = false">确 认</el-button>
|
<el-button style="" type="primary" @click="handleSelectableLang">确 认</el-button>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +118,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import videoPlayer from "@/components/VideoPlayer/index.vue";
|
import videoPlayer from "@/components/VideoPlayer/index.vue";
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters, mapMutations } from 'vuex';
|
||||||
|
import apiCourse from '@/api/modules/course.js';
|
||||||
|
import apiAiVideo from "@/api/modules/courseAiVideo.js";
|
||||||
|
import apiStudy from "@/api/modules/courseStudy.js";
|
||||||
|
import cookies from "vue-cookies";
|
||||||
|
import { encrypt } from "@/utils/jsencrypt.js";
|
||||||
export default {
|
export default {
|
||||||
name: 'aiTranslate',
|
name: 'aiTranslate',
|
||||||
// ai播放器相关
|
// ai播放器相关
|
||||||
@@ -118,42 +132,8 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
courseName: '企业经营法则--课程单元',
|
aiTranslate: '',
|
||||||
videoList: [
|
type: 5, // 1: 正常 2: 生成中 3: 错误 4: 编辑中 5: 未选择语种
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: '1. 开源节流1',
|
|
||||||
aiTranslate: '人工智能(AI)是让计算机模拟人类智能的技术,核心包括机器学习、深度学习等。主要分为弱 AI(专注特定任务)和强 AI(通用智能)两类。应用涵盖医疗诊断、自动驾驶、语音助手等多个领域。它通过数据学习模式,实现预测和适应能力,正在改变生活方式和工作方式。未来发展需平衡创新与伦理考量,确保对人类社会有益。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '2. 企业经营法则总述',
|
|
||||||
aiTranslate: '本课程将介绍企业经营法则的总述,包括企业经营的基本原理、经营策略、经营模式等。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '3. 企业经营法则总述',
|
|
||||||
aiTranslate: '本课程将介绍企业经营法则的总述,包括企业经营的基本原理、经营策略、经营模式等。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '4. 企业经营法则总述',
|
|
||||||
aiTranslate: '本课程将介绍企业经营法则的总述,包括企业经营的基本原理、经营策略、经营模式等。'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
testUrl: 'https://vjs.zencdn.net/v/oceans.mp4',
|
|
||||||
currentVideo: {},
|
|
||||||
aiTranslate: `
|
|
||||||
00:00:01/00:00:03
|
|
||||||
Hello everyone in the audience
|
|
||||||
|
|
||||||
00:00:03/00:00:05
|
|
||||||
today I want to share with you the topic of
|
|
||||||
|
|
||||||
00:00:05/00:00:09
|
|
||||||
"The Development History and Future Prospects of Computer Technology -
|
|
||||||
`,
|
|
||||||
status: '1', // 1: 正常 2: 生成中 3: 错误 4: 编辑中 5: 未选择语种
|
|
||||||
selectedLang: [
|
selectedLang: [
|
||||||
{
|
{
|
||||||
label: '英文',
|
label: '英文',
|
||||||
@@ -180,21 +160,456 @@ today I want to share with you the topic of
|
|||||||
srclang: 'fr',
|
srclang: 'fr',
|
||||||
aiTranslate: 0,
|
aiTranslate: 0,
|
||||||
},
|
},
|
||||||
],
|
], // 当前课程支持的语种 - 待开发
|
||||||
|
currentLangList: [], // 当前课程支持的语种 - 回显下拉框
|
||||||
|
currentLangObjList: [], // 当前课程支持的语种对象 - 右侧下拉选项
|
||||||
|
currentLang: '', // 当前选中的语种 - 展示字幕
|
||||||
|
subtitleObj: {}, // 当前语种的字幕对象
|
||||||
updateDialogVisible: false,
|
updateDialogVisible: false,
|
||||||
selectDialogVisible: false,
|
selectDialogVisible: false,
|
||||||
selectLang: [],
|
chapterList: [],
|
||||||
|
videoName: '',
|
||||||
|
currentVideo: {},
|
||||||
|
aiAbstract: '',
|
||||||
|
courseId: '',
|
||||||
|
courseInfo: {},
|
||||||
|
sections: [],
|
||||||
|
blobId: '',
|
||||||
|
blobUrl: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['selectAllLang']),
|
...mapGetters(['selectableLang', 'selectAllLang']),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.currentVideo = this.videoList[0];
|
this.courseId = this.$route.query.id;
|
||||||
|
console.log(this.courseId);
|
||||||
|
this.getCourseInfo();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations({
|
||||||
|
SET_selectableLang: 'video/SET_selectableLang',
|
||||||
|
SET_courseInfo: 'video/SET_courseInfo',
|
||||||
|
}),
|
||||||
setLanguage(){
|
setLanguage(){
|
||||||
this.selectDialogVisible = true;
|
this.selectDialogVisible = true;
|
||||||
|
},
|
||||||
|
// 加载字幕
|
||||||
|
handleLoadSubtitle() {
|
||||||
|
//type 1: 正常 2: 生成中 3: 错误 4: 编辑中 5: 未选择语种
|
||||||
|
console.log('当前视频', this.currentVideo);
|
||||||
|
if (this.currentLang == '') {
|
||||||
|
this.$message.error('请选择目标语种!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const subtitleList = this.currentVideo?.boeaiSubtitleRspList || [];
|
||||||
|
this.subtitleObj = subtitleList.filter(item => item.language == this.currentLang)[0] || {};
|
||||||
|
if (!this.subtitleObj.subtitleData) {
|
||||||
|
this.type = 2
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 模拟加载test.json数据(实际项目中应从接口获取)
|
||||||
|
try {
|
||||||
|
const subtitleDataList = JSON.parse(this.subtitleObj.subtitleData);
|
||||||
|
console.log('格式化后的字幕', subtitleDataList);
|
||||||
|
// 将JSON转换为指定文本格式
|
||||||
|
this.aiTranslate = this.jsonToTextFormat(subtitleDataList);
|
||||||
|
this.type = 1;
|
||||||
|
} catch (error) {
|
||||||
|
this.type = 3;
|
||||||
|
this.$message.error('字幕数据错误,请重新生成!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 将JSON格式转换为文本格式
|
||||||
|
jsonToTextFormat(jsonData) {
|
||||||
|
return jsonData.map(item => {
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (seconds) => {
|
||||||
|
const h = Math.floor(seconds / 3600).toString().padStart(2, '0');
|
||||||
|
const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
|
||||||
|
const s = Math.floor(seconds % 60).toString().padStart(2, '0');
|
||||||
|
return `${h}:${m}:${s}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTime = formatTime(item.start);
|
||||||
|
const endTime = formatTime(item.end);
|
||||||
|
// 提取纯文本内容(去除时间戳)
|
||||||
|
const content = item.text.replace(/\[\d{2}:\d{2}:\d{2}\]\s*/g, '');
|
||||||
|
|
||||||
|
return `${startTime}/${endTime}\n${content}`;
|
||||||
|
}).join('\n\n');
|
||||||
|
},
|
||||||
|
// 将文本格式转换为JSON格式
|
||||||
|
textToJsonFormat(text) {
|
||||||
|
const lines = text.trim().split('\n');
|
||||||
|
const jsonData = [];
|
||||||
|
const timePattern = /^(\d{2}:\d{2}:\d{2})\/(\d{2}:\d{2}:\d{2})$/;
|
||||||
|
|
||||||
|
let currentTime = '';
|
||||||
|
let currentContent = '';
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// 检查是否是时间行
|
||||||
|
if (timePattern.test(trimmedLine)) {
|
||||||
|
// 如果已有内容,先保存上一个条目
|
||||||
|
if (currentTime && currentContent) {
|
||||||
|
const [startTime, endTime] = currentTime.split('/');
|
||||||
|
jsonData.push({
|
||||||
|
id: id++,
|
||||||
|
start: this.timeToSeconds(startTime),
|
||||||
|
end: this.timeToSeconds(endTime),
|
||||||
|
text: `${currentContent.trim()}`,
|
||||||
|
original_text: '',
|
||||||
|
confidence: 0.64
|
||||||
|
});
|
||||||
|
currentContent = '';
|
||||||
|
}
|
||||||
|
currentTime = trimmedLine;
|
||||||
|
} else if (trimmedLine) {
|
||||||
|
// 内容行(非空行)
|
||||||
|
if (currentContent) {
|
||||||
|
currentContent += '\n' + trimmedLine;
|
||||||
|
} else {
|
||||||
|
currentContent = trimmedLine;
|
||||||
|
}
|
||||||
|
} else if (currentContent) {
|
||||||
|
// 空行,添加换行符到内容中
|
||||||
|
currentContent += '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存最后一个条目
|
||||||
|
if (currentTime && currentContent) {
|
||||||
|
const [startTime, endTime] = currentTime.split('/');
|
||||||
|
jsonData.push({
|
||||||
|
id: id++,
|
||||||
|
start: this.timeToSeconds(startTime),
|
||||||
|
end: this.timeToSeconds(endTime),
|
||||||
|
text: `${currentContent.trim()}`,
|
||||||
|
original_text: '',
|
||||||
|
confidence: 0.64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonData;
|
||||||
|
},
|
||||||
|
// 时间字符串转秒数
|
||||||
|
timeToSeconds(timeStr) {
|
||||||
|
const [h, m, s] = timeStr.split(':').map(Number);
|
||||||
|
return h * 3600 + m * 60 + s;
|
||||||
|
},
|
||||||
|
// 校验文本格式
|
||||||
|
validateTextFormat(text) {
|
||||||
|
// console.log('实际文本内容:', JSON.stringify(text));
|
||||||
|
const lines = text.trim().split('\n');
|
||||||
|
// console.log('lines数组:', lines);
|
||||||
|
// console.log('lines数组长度:', lines.length);
|
||||||
|
const timePattern = /^(\d{2}:\d{2}:\d{2})\/(\d{2}:\d{2}:\d{2})$/;
|
||||||
|
|
||||||
|
// 如果文本为空,直接返回错误
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return { valid: false, message: '文本内容不能为空' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let expectingContent = false;
|
||||||
|
let previousEndTime = -1; // 上一个字幕的结束时间
|
||||||
|
let lineNumber = 0;
|
||||||
|
let currentContent = '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
lineNumber++;
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// 如果是空行,跳过
|
||||||
|
if (!trimmedLine) {
|
||||||
|
// 如果正在收集内容,继续收集
|
||||||
|
if (expectingContent) {
|
||||||
|
currentContent += '\n';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectingContent) {
|
||||||
|
// 检查是否是新的时间行
|
||||||
|
// console.log(`第${lineNumber}行,expectingContent=true,内容:"${trimmedLine}"`);
|
||||||
|
if (timePattern.test(trimmedLine)) {
|
||||||
|
// console.log(`第${lineNumber}行,内容:"${trimmedLine}"被识别为新的时间行`);
|
||||||
|
// 是新的时间行,检查当前内容是否为空
|
||||||
|
if (!currentContent.trim()) {
|
||||||
|
return { valid: false, message: `时间行后缺少字幕内容` };
|
||||||
|
}
|
||||||
|
// 解析时间并检查连贯性
|
||||||
|
const [startTimeStr, endTimeStr] = trimmedLine.split('/');
|
||||||
|
const startTime = this.timeToSeconds(startTimeStr);
|
||||||
|
const endTime = this.timeToSeconds(endTimeStr);
|
||||||
|
|
||||||
|
// 检查时间范围是否有效
|
||||||
|
if (endTime < startTime) {
|
||||||
|
return { valid: false, message: `时间范围错误,结束时间必须大于开始时间` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查时间顺序和重叠
|
||||||
|
if (startTime < previousEndTime) {
|
||||||
|
return { valid: false, message: `时间与上一行时间重叠或顺序错误` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新上一个结束时间
|
||||||
|
previousEndTime = endTime;
|
||||||
|
|
||||||
|
// 重置内容收集,准备处理下一个字幕的内容
|
||||||
|
currentContent = '';
|
||||||
|
expectingContent = true;
|
||||||
|
// console.log('处理完新的时间行后,expectingContent设置为:', expectingContent);
|
||||||
|
} else {
|
||||||
|
// console.log(`第${lineNumber}行,内容:"${trimmedLine}"被识别为内容行`);
|
||||||
|
// 是内容行,继续收集内容
|
||||||
|
currentContent += (currentContent ? '\n' : '') + trimmedLine;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 应该是时间行
|
||||||
|
// console.log(`第${lineNumber}行,expectingContent=false,内容:"${trimmedLine}"`);
|
||||||
|
if (!timePattern.test(trimmedLine)) {
|
||||||
|
// console.log('timePattern.test(trimmedLine)',trimmedLine, timePattern.test(trimmedLine));
|
||||||
|
return { valid: false, message: `时间格式错误,请使用HH:MM:SS/HH:MM:SS格式` };
|
||||||
|
}
|
||||||
|
// console.log(`第${lineNumber}行,内容:"${trimmedLine}"被识别为时间行`);
|
||||||
|
|
||||||
|
// 解析时间并检查连贯性
|
||||||
|
const [startTimeStr, endTimeStr] = trimmedLine.split('/');
|
||||||
|
const startTime = this.timeToSeconds(startTimeStr);
|
||||||
|
const endTime = this.timeToSeconds(endTimeStr);
|
||||||
|
|
||||||
|
// 检查时间范围是否有效
|
||||||
|
if (endTime <= startTime) {
|
||||||
|
return { valid: false, message: `时间范围错误,结束时间必须大于开始时间` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查时间顺序和重叠
|
||||||
|
if (startTime < previousEndTime) {
|
||||||
|
return { valid: false, message: `时间顺序错误,与上一个字幕存在时间重叠` };
|
||||||
|
}
|
||||||
|
|
||||||
|
previousEndTime = endTime;
|
||||||
|
expectingContent = true;
|
||||||
|
currentContent = '';
|
||||||
|
console.log('处理完正常时间行后,expectingContent设置为:', expectingContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查最后一个时间行是否有内容
|
||||||
|
if (expectingContent && !currentContent.trim()) {
|
||||||
|
return { valid: false, message: `时间行后缺少字幕内容` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, message: '格式正确' };
|
||||||
|
},
|
||||||
|
// 上下架ai翻译
|
||||||
|
handleSummaryStatus() {
|
||||||
|
apiCourse.benchAiSet({courseList:[{id: this.courseId, aiTranslate: this.courseInfo.aiTranslate == 1 ? 0 : 1}]}).then(res => {
|
||||||
|
if(res.status === 200){
|
||||||
|
this.$message.success(this.courseInfo.aiTranslate == 1 ? '下架成功!' : '上架成功!');
|
||||||
|
this.getCourseInfo();
|
||||||
|
}else{
|
||||||
|
this.$message.error(this.courseInfo.aiTranslate == 1 ? '下架失败!' : '上架失败!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 设置语种
|
||||||
|
handleSelectableLang() {
|
||||||
|
apiCourse.benchAiSet({courseList:[{id: this.courseId, languageCode: this.currentLangList}]}).then(res => {
|
||||||
|
this.selectDialogVisible = false;
|
||||||
|
if(res.status === 200){
|
||||||
|
this.$message.success('语种设置成功!');
|
||||||
|
this.getCourseInfo();
|
||||||
|
}else{
|
||||||
|
this.$message.error(res.message || '语种设置失败!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 提交编辑后的字幕
|
||||||
|
submitSubtitle() {
|
||||||
|
// 校验文本格式
|
||||||
|
const validateResult = this.validateTextFormat(this.aiTranslate);
|
||||||
|
if (!validateResult.valid) {
|
||||||
|
this.$message.error(validateResult.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为JSON格式
|
||||||
|
const jsonData = this.textToJsonFormat(this.aiTranslate);
|
||||||
|
console.log('转换后的JSON数据:', jsonData);
|
||||||
|
|
||||||
|
// 模拟发送给接口(实际项目中应调用真实接口)
|
||||||
|
apiAiVideo.updateText({
|
||||||
|
videoId: this.currentVideo.id,
|
||||||
|
language: this.currentLang,
|
||||||
|
subtitleData: JSON.stringify(jsonData),
|
||||||
|
updateStatus: 0, // 0-编辑 1-重新生成 2-同步更新
|
||||||
|
}).then(res => {
|
||||||
|
if(res.rspCode === '0000'){
|
||||||
|
this.$message.success('字幕编辑成功!');
|
||||||
|
this.getCourseInfo();
|
||||||
|
}else{
|
||||||
|
this.$message.error(res.rspDesc || '字幕编辑失败!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 确认同步更新
|
||||||
|
handleSyncUpdate() {
|
||||||
|
apiAiVideo.updateText({
|
||||||
|
videoId: this.currentVideo.id,
|
||||||
|
language: this.currentLang,
|
||||||
|
updateStatus: 2, // 0-编辑 1-重新生成 2-同步更新
|
||||||
|
}).then(res => {
|
||||||
|
this.updateDialogVisible = false;
|
||||||
|
if(res.rspCode === '0000'){
|
||||||
|
this.$message.success('同步更新中!');
|
||||||
|
this.getCourseInfo();
|
||||||
|
}else{
|
||||||
|
this.$message.error(res.rspDesc || '同步更新失败!');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 重新生成
|
||||||
|
handleGenerate() {
|
||||||
|
this.$confirm('此操作将重新生成译文, 是否继续?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
// 转换为JSON格式
|
||||||
|
apiAiVideo.updateText({
|
||||||
|
videoId: this.currentVideo.id,
|
||||||
|
language: this.currentLang,
|
||||||
|
updateStatus: 1, // 0-编辑 1-重新生成 2-同步更新
|
||||||
|
}).then(res => {
|
||||||
|
if(res.rspCode === '0000'){
|
||||||
|
this.$message.success('重新生成中!');
|
||||||
|
this.getCourseInfo();
|
||||||
|
}else{
|
||||||
|
this.$message.error(res.rspDesc || '重新生成失败!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(() => {});
|
||||||
|
},
|
||||||
|
// 创建播放地址
|
||||||
|
createPlayUrl(fid, u) {
|
||||||
|
let nowDate = new Date();
|
||||||
|
let ctime = parseInt(nowDate.getTime() / 1000);
|
||||||
|
let beforeUrl = parseInt(nowDate.getTime() / 1000) + "/" + fid;
|
||||||
|
let urlSign = encodeURIComponent(encrypt(beforeUrl));
|
||||||
|
cookies.set("PLAYSIGN_TIME", ctime); //写客户端的cookie保存
|
||||||
|
//以下判断是为了区分本地环境和服务器环境
|
||||||
|
if (process.env.NODE_ENV == "development") {
|
||||||
|
this.blobUrl = process.env.VUE_APP_FILE_BASE_URL + u;
|
||||||
|
} else {
|
||||||
|
this.blobUrl =
|
||||||
|
process.env.VUE_APP_BASE_API +
|
||||||
|
"/xboe/m/course/cware/resource?sign=" +
|
||||||
|
urlSign;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 选择视频
|
||||||
|
handleSelectVideo(item, isRefresh) {
|
||||||
|
this.currentVideo = item;
|
||||||
|
this.SET_selectableLang(item?.boeaiSubtitleRspList);
|
||||||
|
this.videoName = item.chapterName ? item.chapterName + "-视频" + item.videoIndex : this.courseInfo.name;
|
||||||
|
// 编辑时 刷新时保留选择
|
||||||
|
if (isRefresh && this.currentLang) {
|
||||||
|
this.handleLoadSubtitle();
|
||||||
|
} else {
|
||||||
|
this.currentLang = '';
|
||||||
|
this.aiTranslate = '';
|
||||||
|
this.type = 5;
|
||||||
|
}
|
||||||
|
let curriculumData = {};
|
||||||
|
if (item.content.startsWith("{")) {
|
||||||
|
curriculumData = JSON.parse(item.content);
|
||||||
|
} else {
|
||||||
|
curriculumData.url = item.content;
|
||||||
|
}
|
||||||
|
this.blobId = item.id;
|
||||||
|
this.createPlayUrl(item.contentRefId, curriculumData.url);
|
||||||
|
},
|
||||||
|
// 根据课程支持语种 设置 右侧下拉框选项currentLangObjList 和 中间下拉框值 currentLangList
|
||||||
|
setCurrentLang(langList) {
|
||||||
|
this.currentLangList = JSON.parse(JSON.stringify(langList));
|
||||||
|
let currentLangObjList = [];
|
||||||
|
this.selectAllLang.forEach(item => {
|
||||||
|
langList.forEach(item2 => {
|
||||||
|
if (item.srclang === item2) {
|
||||||
|
currentLangObjList.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.currentLangObjList = currentLangObjList;
|
||||||
|
},
|
||||||
|
// 获取课程信息 - 获取课程信息、章节信息、视频信息 选择视频后 将字幕信息存储到vuex中
|
||||||
|
getCourseInfo() {
|
||||||
|
apiStudy.studyIndexPost({
|
||||||
|
cid: this.courseId,
|
||||||
|
addView: false,
|
||||||
|
audiences: '',
|
||||||
|
})
|
||||||
|
.then((rs) => {
|
||||||
|
if (rs.status == 200) {
|
||||||
|
this.courseInfo = rs.result.course || {};
|
||||||
|
this.SET_courseInfo(this.courseInfo);
|
||||||
|
const contents = rs.result.contents?.filter(item => item.contentType == 10) || [];
|
||||||
|
this.sections = rs.result.sections || [];
|
||||||
|
this.setCurrentLang(this.courseInfo.languageCode || [])
|
||||||
|
if (this.sections.length > 0) {
|
||||||
|
let treeList = [];
|
||||||
|
let hasOne = false;
|
||||||
|
this.sections.forEach(section => {
|
||||||
|
let treeNode = {
|
||||||
|
...section,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
let videoIndex = 1;
|
||||||
|
contents.forEach(item => {
|
||||||
|
if (section.id == item.csectionId) {
|
||||||
|
item.chapterName = section.name || '';
|
||||||
|
item.videoIndex = videoIndex++;
|
||||||
|
treeNode.children.push(item);
|
||||||
|
// 刷新时保留选择
|
||||||
|
if(this.currentVideo.id) {
|
||||||
|
if(this.currentVideo.id == item.id) {
|
||||||
|
this.handleSelectVideo(item, true);
|
||||||
|
}
|
||||||
|
hasOne = true;
|
||||||
|
} else if (!hasOne) {
|
||||||
|
this.handleSelectVideo(item);
|
||||||
|
hasOne = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
treeList.push(treeNode);
|
||||||
|
})
|
||||||
|
this.chapterList = treeList;
|
||||||
|
} else {
|
||||||
|
this.handleSelectVideo(contents[0], this.currentVideo.id?true:false);
|
||||||
|
}
|
||||||
|
console.log('课程数据',this.courseInfo, this.courseInfo.name);
|
||||||
|
this.aiAbstract = this.courseInfo.summaryContent
|
||||||
|
if (contents.length == 0) {
|
||||||
|
$this.$message.error("课程内容已删除或课程已不再使用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!rs.result.course.enabled) {
|
||||||
|
$this.$message.error(
|
||||||
|
"十分抱歉,此课程已停用,如需使用,请联系管理员。"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -250,6 +665,7 @@ today I want to share with you the topic of
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ai-right{
|
.ai-right{
|
||||||
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -273,6 +689,7 @@ today I want to share with you the topic of
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ai-content{
|
.ai-content{
|
||||||
|
height: calc(100% - 76px);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 24px 30px;
|
padding: 24px 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -334,6 +751,7 @@ today I want to share with you the topic of
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.opera-content{
|
.opera-content{
|
||||||
|
height: calc(100% - 40px);
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: rgba(17, 24, 39, 1);
|
color: rgba(17, 24, 39, 1);
|
||||||
@@ -348,6 +766,7 @@ today I want to share with you the topic of
|
|||||||
background: rgba(249, 250, 251, 1);
|
background: rgba(249, 250, 251, 1);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.opera-btn{
|
.opera-btn{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -847,30 +847,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
// ai播放器相关
|
// ai播放器相关
|
||||||
import { mapGetters, mapMutations } from "vuex";
|
import { mapGetters, mapMutations } from "vuex";
|
||||||
import followButton from "@/components/Follow/button.vue";
|
import followButton from "@/components/Follow/button.vue";
|
||||||
import portalHeader from "@/components/PortalHeader.vue";
|
import portalHeader from "@/components/PortalHeader.vue";
|
||||||
import portalFooter from "@/components/PortalFooter.vue";
|
import portalFooter from "@/components/PortalFooter.vue";
|
||||||
import comments from "@/components/Portal/comments.vue";
|
import comments from "@/components/Portal/comments.vue";
|
||||||
import noteComments from "@/components/Portal/noteComment.vue";
|
import noteComments from "@/components/Portal/noteComment.vue";
|
||||||
import interactBar from "@/components/Portal/interactBar.vue";
|
import interactBar from "@/components/Portal/interactBar.vue";
|
||||||
import audioPlayer from "@/components/AudioPlayer/index.vue";
|
import audioPlayer from "@/components/AudioPlayer/index.vue";
|
||||||
import videoPlayer from "@/components/VideoPlayer/index.vue";
|
import videoPlayer from "@/components/VideoPlayer/index.vue";
|
||||||
import hyperLink from "@/components/Course/hyperLink.vue";
|
import hyperLink from "@/components/Course/hyperLink.vue";
|
||||||
import studyUtil from "@/utils/study.js";
|
import studyUtil from "@/utils/study.js";
|
||||||
import { encrypt } from "@/utils/jsencrypt.js";
|
import { encrypt } from "@/utils/jsencrypt.js";
|
||||||
import cookies from "vue-cookies";
|
import cookies from "vue-cookies";
|
||||||
import apiStat from "@/api/phase2/stat.js";
|
import apiStat from "@/api/phase2/stat.js";
|
||||||
import apiStudy from "@/api/modules/courseStudy.js";
|
import apiStudy from "@/api/modules/courseStudy.js";
|
||||||
import apiVideoStudy from "@/api/modules/videoStudy.js";
|
import apiVideoStudy from "@/api/modules/videoStudy.js";
|
||||||
import apiCourseGrade from "@/api/modules/courseGrade.js";
|
import apiCourseGrade from "@/api/modules/courseGrade.js";
|
||||||
import apiPraises from "@/api/modules/praises.js";
|
import apiPraises from "@/api/modules/praises.js";
|
||||||
import apiTrample from "@/api/modules/trample.js";
|
import apiTrample from "@/api/modules/trample.js";
|
||||||
import apiCoursePortal from "@/api/modules/coursePortal.js";
|
import apiCoursePortal from "@/api/modules/coursePortal.js";
|
||||||
import apiUser from "@/api/system/user.js";
|
import apiUser from "@/api/system/user.js";
|
||||||
import apiCourseFile from "@/api/modules/courseFile.js";
|
import apiCourseFile from "@/api/modules/courseFile.js";
|
||||||
import {
|
import {
|
||||||
resListMap,
|
resListMap,
|
||||||
resOwnerListMap,
|
resOwnerListMap,
|
||||||
courseType,
|
courseType,
|
||||||
@@ -878,20 +878,20 @@
|
|||||||
toScore,
|
toScore,
|
||||||
cutOrgNamePath,
|
cutOrgNamePath,
|
||||||
userAvatarText,
|
userAvatarText,
|
||||||
} from "@/utils/tools.js";
|
} from "@/utils/tools.js";
|
||||||
import pdfPreview from "@/components/PdfPreview/index.vue";
|
import pdfPreview from "@/components/PdfPreview/index.vue";
|
||||||
import courseImage from "@/components/Course/courseImage.vue";
|
import courseImage from "@/components/Course/courseImage.vue";
|
||||||
import exam from "@/components/Course/exam";
|
import exam from "@/components/Course/exam";
|
||||||
import homework from "@/components/Course/homework";
|
import homework from "@/components/Course/homework";
|
||||||
import assess from "@/components/Course/assess";
|
import assess from "@/components/Course/assess";
|
||||||
import myNote from "../../components/Course/myNote.vue";
|
import myNote from "../../components/Course/myNote.vue";
|
||||||
// ai播放器相关
|
// ai播放器相关
|
||||||
import aiScript from "../../components/Course/aiScript.vue";
|
import aiScript from "../../components/Course/aiScript.vue";
|
||||||
import apiFollow from "@/api/phase2/userfollow.js";
|
import apiFollow from "@/api/phase2/userfollow.js";
|
||||||
import apiMessage from "@/api/system/message.js";
|
import apiMessage from "@/api/system/message.js";
|
||||||
// import Vue from 'vue';
|
// import Vue from 'vue';
|
||||||
// Vue.forceUpdate();
|
// Vue.forceUpdate();
|
||||||
export default {
|
export default {
|
||||||
name: "atticle",
|
name: "atticle",
|
||||||
components: {
|
components: {
|
||||||
courseImage,
|
courseImage,
|
||||||
@@ -2397,37 +2397,37 @@
|
|||||||
this.cleanAppendTime();
|
this.cleanAppendTime();
|
||||||
this.stopStudyTime();
|
this.stopStudyTime();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
::v-deep .el-icon-arrow-down {
|
::v-deep .el-icon-arrow-down {
|
||||||
color: #000;
|
color: #000;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.el-menu,
|
.el-menu,
|
||||||
.el-menu-item,
|
.el-menu-item,
|
||||||
.el-submenu__title {
|
.el-submenu__title {
|
||||||
background-color: transparent !important; /* 设置背景色为透明,以移除默认的背景颜色变化 */
|
background-color: transparent !important; /* 设置背景色为透明,以移除默认的背景颜色变化 */
|
||||||
color: inherit !important; /* 确保文字颜色不变,如果需要的话 */
|
color: inherit !important; /* 确保文字颜色不变,如果需要的话 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 取消选中效果 */
|
/* 取消选中效果 */
|
||||||
.el-menu-item.is-active,
|
.el-menu-item.is-active,
|
||||||
.el-submenu__title.is-active {
|
.el-submenu__title.is-active {
|
||||||
background-color: transparent !important; /* 移除选中时的背景色 */
|
background-color: transparent !important; /* 移除选中时的背景色 */
|
||||||
color: inherit !important; /* 保持文字颜色与未选中时一致,如果需要的话 */
|
color: inherit !important; /* 保持文字颜色与未选中时一致,如果需要的话 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 如果有特定的 hover 样式,也可以取消 */
|
/* 如果有特定的 hover 样式,也可以取消 */
|
||||||
.el-menu-item:hover,
|
.el-menu-item:hover,
|
||||||
.el-submenu__title:hover {
|
.el-submenu__title:hover {
|
||||||
background-color: transparent !important; /* 移除鼠标悬停时的背景色变化 */
|
background-color: transparent !important; /* 移除鼠标悬停时的背景色变化 */
|
||||||
color: inherit !important; /* 保持文字颜色不变 */
|
color: inherit !important; /* 保持文字颜色不变 */
|
||||||
}
|
}
|
||||||
//内容块样式定义
|
//内容块样式定义
|
||||||
.course-content {
|
.course-content {
|
||||||
min-height: 745px;
|
min-height: 745px;
|
||||||
|
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
@@ -2491,9 +2491,9 @@
|
|||||||
width: 420px;
|
width: 420px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-tab {
|
.control-tab {
|
||||||
margin: 35px 30px 10px 30px;
|
margin: 35px 30px 10px 30px;
|
||||||
|
|
||||||
border-bottom: 1px solid #d8d8d8;
|
border-bottom: 1px solid #d8d8d8;
|
||||||
@@ -2508,9 +2508,9 @@
|
|||||||
color: #333333;
|
color: #333333;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-info-tab {
|
.course-info-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
margin-left: 47px;
|
margin-left: 47px;
|
||||||
@@ -2536,9 +2536,9 @@
|
|||||||
top: 128%;
|
top: 128%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-info-row {
|
.course-info-row {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #343434;
|
color: #343434;
|
||||||
@@ -2552,9 +2552,9 @@
|
|||||||
color: #666666;
|
color: #666666;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//右侧老师部分头部及标题
|
//右侧老师部分头部及标题
|
||||||
.cteacher-top {
|
.cteacher-top {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
.cteacher-top-bar {
|
.cteacher-top-bar {
|
||||||
@@ -2568,15 +2568,15 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//右侧老师部分 老师列表
|
//右侧老师部分 老师列表
|
||||||
.cteacher-list {
|
.cteacher-list {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 10px 30px 30px 30px;
|
padding: 10px 30px 30px 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
//以下是未整理的样式,上面的整理之后的样式
|
//以下是未整理的样式,上面的整理之后的样式
|
||||||
.coures-note {
|
.coures-note {
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
padding-left: 17px;
|
padding-left: 17px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
@@ -2589,9 +2589,9 @@
|
|||||||
|
|
||||||
.note-info {
|
.note-info {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-box {
|
.player-box {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
@@ -2616,64 +2616,64 @@
|
|||||||
.player-rate {
|
.player-rate {
|
||||||
margin-top: 35px;
|
margin-top: 35px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-text {
|
.score-text {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #ffb30f;
|
color: #ffb30f;
|
||||||
font-family: "Arial";
|
font-family: "Arial";
|
||||||
margin-left: 23px;
|
margin-left: 23px;
|
||||||
// font-weight: 600;
|
// font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .score {
|
// .score {
|
||||||
// display: flex;
|
// display: flex;
|
||||||
// justify-content: center;
|
// justify-content: center;
|
||||||
// align-items: center;
|
// align-items: center;
|
||||||
|
|
||||||
// .el-rate {
|
// .el-rate {
|
||||||
// display: inline-block;
|
// display: inline-block;
|
||||||
// }
|
// }
|
||||||
// // margin-bottom: 19px;
|
// // margin-bottom: 19px;
|
||||||
// .score-no {
|
// .score-no {
|
||||||
// color: #ffb30f;
|
// color: #ffb30f;
|
||||||
// padding: 5px 0;
|
// padding: 5px 0;
|
||||||
// background: #efefef;
|
// background: #efefef;
|
||||||
// border-radius: 20px;
|
// border-radius: 20px;
|
||||||
// width: 65px;
|
// width: 65px;
|
||||||
// font-size: 12px;
|
// font-size: 12px;
|
||||||
// font-weight: 600;
|
// font-weight: 600;
|
||||||
// text-align: center;
|
// text-align: center;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
.portal-content-title {
|
.portal-content-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
color: #333333;
|
color: #333333;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-kuang {
|
.type-kuang {
|
||||||
color: #666666;
|
color: #666666;
|
||||||
border: 1px solid #666666;
|
border: 1px solid #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-click {
|
.type-click {
|
||||||
color: #ffb30f !important;
|
color: #ffb30f !important;
|
||||||
border: 1px solid #ffb30f !important;
|
border: 1px solid #ffb30f !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-rate__icon {
|
::v-deep .el-rate__icon {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
color: #ff8e00;
|
color: #ff8e00;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ref-score {
|
.ref-score {
|
||||||
// background: #e4e4e4;
|
// background: #e4e4e4;
|
||||||
// width: 64px;
|
// width: 64px;
|
||||||
// height: 32px;
|
// height: 32px;
|
||||||
@@ -2683,16 +2683,16 @@
|
|||||||
// color: #000;
|
// color: #000;
|
||||||
// font-size: 14px;
|
// font-size: 14px;
|
||||||
// border: none;
|
// border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-small {
|
.icon-small {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.con-audio {
|
.con-audio {
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
@@ -2705,9 +2705,9 @@
|
|||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
padding: 60px 50px 80px 50px;
|
padding: 60px 50px 80px 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hyper-link {
|
.hyper-link {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 100px;
|
padding-top: 100px;
|
||||||
@@ -2715,9 +2715,9 @@
|
|||||||
.hyper-link-row {
|
.hyper-link-row {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-box {
|
.catalog-box {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
||||||
// padding: 15px 0px;
|
// padding: 15px 0px;
|
||||||
@@ -2728,19 +2728,19 @@
|
|||||||
color: #333333;
|
color: #333333;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-title {
|
.collapse-title {
|
||||||
flex: 1 0 90%;
|
flex: 1 0 90%;
|
||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-collapse-item__header {
|
.el-collapse-item__header {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
order: -1;
|
order: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-interact {
|
.course-interact {
|
||||||
flex: 1; // 20%高度
|
flex: 1; // 20%高度
|
||||||
height: 54px;
|
height: 54px;
|
||||||
// padding-top: 10px;
|
// padding-top: 10px;
|
||||||
@@ -2751,18 +2751,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.interact-btn {
|
.interact-btn {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uc-badge {
|
.uc-badge {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-right: 40px;
|
margin-right: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cres-list {
|
.cres-list {
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
@@ -2773,9 +2773,9 @@
|
|||||||
.current {
|
.current {
|
||||||
color: #1ea0fa;
|
color: #1ea0fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.uc-course {
|
.uc-course {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
@@ -2808,9 +2808,9 @@
|
|||||||
.uc-course-btns {
|
.uc-course-btns {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-cell-state2 {
|
.catalog-cell-state2 {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 58px;
|
width: 58px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
@@ -2821,9 +2821,9 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
background: #ffedd6;
|
background: #ffedd6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-cell-state9 {
|
.catalog-cell-state9 {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 58px;
|
width: 58px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
@@ -2836,9 +2836,9 @@
|
|||||||
background: #edf2fd;
|
background: #edf2fd;
|
||||||
// margin-left: 68%;
|
// margin-left: 68%;
|
||||||
// background-color: #BED2F8;
|
// background-color: #BED2F8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-cell-state1 {
|
.catalog-cell-state1 {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 58px;
|
width: 58px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
@@ -2849,9 +2849,9 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
background: #fff0f0;
|
background: #fff0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog {
|
.catalog {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -2887,9 +2887,9 @@
|
|||||||
color: #5c5c5c;
|
color: #5c5c5c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-div {
|
.video-div {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999999999;
|
z-index: 999999999;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -2921,9 +2921,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-tag {
|
::v-deep .el-tag {
|
||||||
background-color: #ecf5ff;
|
background-color: #ecf5ff;
|
||||||
border-color: #409eff;
|
border-color: #409eff;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -2937,14 +2937,14 @@
|
|||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .teacher .teacher-avator .teacher-text {
|
::v-deep .teacher .teacher-avator .teacher-text {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teacher {
|
.teacher {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -2988,14 +2988,14 @@
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadname {
|
.breadname {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
//目录的样式更改
|
//目录的样式更改
|
||||||
::v-deep .el-collapse-item {
|
::v-deep .el-collapse-item {
|
||||||
.el-collapse-item__header {
|
.el-collapse-item__header {
|
||||||
// position: relative;
|
// position: relative;
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
@@ -3013,9 +3013,9 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-units {
|
.course-units {
|
||||||
padding: 10px 30px;
|
padding: 10px 30px;
|
||||||
.units-info {
|
.units-info {
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
@@ -3041,13 +3041,13 @@
|
|||||||
// margin-right: 20px;
|
// margin-right: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.more {
|
.more {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coures-title {
|
.coures-title {
|
||||||
margin-left: 82px;
|
margin-left: 82px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -3077,32 +3077,32 @@
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep.el-breadcrumb__item:first-child .el-breadcrumb__inner a,
|
::v-deep.el-breadcrumb__item:first-child .el-breadcrumb__inner a,
|
||||||
.el-breadcrumb__inner .is-link {
|
.el-breadcrumb__inner .is-link {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep.el-breadcrumb__item:last-child .el-breadcrumb__inner {
|
::v-deep.el-breadcrumb__item:last-child .el-breadcrumb__inner {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep.hear-nav {
|
::v-deep.hear-nav {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-nav {
|
.breadcrumb-nav {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coures-bg {
|
.coures-bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
background: url("../../../public/images/couresdetail.png");
|
background: url("../../../public/images/couresdetail.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.protocol {
|
.protocol {
|
||||||
.protocol-title {
|
.protocol-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -3114,5 +3114,5 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -144,6 +144,17 @@ module.exports = {
|
|||||||
'^/manageApi': '/manageApi'
|
'^/manageApi': '/manageApi'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'/systemapi/aiVideo': {
|
||||||
|
// 目标代理服务器地址
|
||||||
|
// target: 'http://127.0.0.1:9090',
|
||||||
|
target: 'http://10.29.0.168:8088',
|
||||||
|
changeOrigin: true,
|
||||||
|
logLevel: 'debug',
|
||||||
|
secure: false,
|
||||||
|
pathRewrite: {
|
||||||
|
'^/systemapi/aiVideo': ''
|
||||||
|
}
|
||||||
|
},
|
||||||
'/systemapi': {
|
'/systemapi': {
|
||||||
// 目标代理服务器地址
|
// 目标代理服务器地址
|
||||||
// target: 'http://127.0.0.1:9090',
|
// target: 'http://127.0.0.1:9090',
|
||||||
|
|||||||
Reference in New Issue
Block a user