feat: 添加AI播放器相关功能和设置界面

- 新增批量语种设置和开启AI处理按钮
- 实现AI翻译、AI处理和AI设置的弹框功能
- 增加AI权限检查和相关状态管理
- 更新表格选择功能以支持AI相关操作
This commit is contained in:
huweihang
2025-12-22 23:39:05 +08:00
parent 79111b9e6b
commit bb453a0200

View File

@@ -162,6 +162,25 @@
<div class="table-wrapper"> <div class="table-wrapper">
<div class="filter-extra-actions"> <div class="filter-extra-actions">
<div class="create-course-btn" @click="addNewCourse()" aria-label="新建课程">新建课程</div> <div class="create-course-btn" @click="addNewCourse()" aria-label="新建课程">新建课程</div>
<!-- AI 播放器相关批量语种设置 / 开启AI处理 -->
<el-button
v-if="aiPermission"
type="primary"
@click="setLanguage()"
icon="el-icon-connection"
:disabled="selectedCourses.length === 0"
>
设置语种
</el-button>
<el-button
v-if="aiPermission"
type="primary"
@click="enableAI()"
icon="el-icon-switch-button"
:disabled="selectedCourses.length === 0"
>
开启AI处理
</el-button>
<el-tooltip v-if="showSetTopFeature" content="置顶排序" placement="top" effect="dark" popper-class="icon-btn-tooltip"> <el-tooltip v-if="showSetTopFeature" content="置顶排序" placement="top" effect="dark" popper-class="icon-btn-tooltip">
<div class="icon-btn icon-btn--top" @click="handleTopSort" aria-label="置顶排序"></div> <div class="icon-btn icon-btn--top" @click="handleTopSort" aria-label="置顶排序"></div>
</el-tooltip> </el-tooltip>
@@ -170,7 +189,9 @@
@click="!exportLoading && handleExport()" aria-label="导出"></div> @click="!exportLoading && handleExport()" aria-label="导出"></div>
</el-tooltip> </el-tooltip>
</div> </div>
<el-table :data="pageData" @sort-change="handleSortChange"> <el-table :data="pageData" @sort-change="handleSortChange" @selection-change="handleSelectionChange">
<!-- AI 播放器相关多选勾选列 -->
<el-table-column v-if="aiPermission" type="selection" width="55"></el-table-column>
<el-table-column v-if="forChoose" label="选择" width="80" align="center"> <el-table-column v-if="forChoose" label="选择" width="80" align="center">
<template slot-scope="scope" v-if="scope.row.published"> <template slot-scope="scope" v-if="scope.row.published">
<el-button type="default" size="mini" @click="handleChoose(scope.row)">选择</el-button> <el-button type="default" size="mini" @click="handleChoose(scope.row)">选择</el-button>
@@ -444,6 +465,234 @@
<course-form ref="courseForm" @submitSuccess="searchData" @close="searchData"></course-form> <course-form ref="courseForm" @submitSuccess="searchData" @close="searchData"></course-form>
</div> </div>
<top-course-sorter v-if="showSetTopFeature" ref="topSorter" @sorted="onTopSorted"></top-course-sorter> <top-course-sorter v-if="showSetTopFeature" ref="topSorter" @sorted="onTopSorted"></top-course-sorter>
<!-- AI 播放器相关AI翻译 / 开启AI处理 / 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.languageCode"
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="aiSetting.aiAbstract === 1 ? 'custom-putaway' : 'custom-takeout'">
{{ aiSetting.aiAbstract === 1 ? '已上架' : '已下架' }}
</span>
<div class="action-buttons">
<el-button type="text" @click="changeAIKey('aiAbstract')">
{{ aiSetting.aiAbstract === 1 ? '下架' : '上架' }}
</el-button>
<el-button v-show="false" type="text">编辑</el-button>
</div>
</div>
<!-- AI文稿状态 -->
<div class="form-item">
<span class="form-label">AI文稿状态</span>
<span :class="aiSetting.aiDraft === 1 ? 'custom-putaway' : 'custom-takeout'">
{{ aiSetting.aiDraft === 1 ? '已上架' : '已下架' }}
</span>
<div class="action-buttons">
<el-button type="text" @click="changeAIKey('aiDraft')">
{{ aiSetting.aiDraft === 1 ? '下架' : '上架' }}
</el-button>
</div>
</div>
<!-- AI翻译状态 -->
<div class="form-item">
<span class="form-label">AI翻译状态</span>
<span :class="aiSetting.aiTranslate === 1 ? 'custom-putaway' : 'custom-takeout'">
{{ aiSetting.aiTranslate === 1 ? '已上架' : '已下架' }}
</span>
<div class="action-buttons">
<el-button type="text" @click="changeAIKey('aiTranslate')">
{{ aiSetting.aiTranslate === 1 ? '下架' : '上架' }}
</el-button>
<el-button v-show="false" type="text">编辑</el-button>
</div>
</div>
<!-- 支持语种 -->
<div class="form-item" style="align-items: flex-start;">
<span class="form-label" style="white-space: nowrap;">支持语种</span>
<div class="languages-list" v-show="false">
<div v-for="lang in aiSetting.languageCode" :key="lang" class="language-tag">
{{ getLanguageName(lang) }}
<span class="custom-takeout">已下架</span>
</div>
</div>
<el-select
v-model="aiSetting.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>
<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> </div>
</template> </template>
@@ -457,7 +706,7 @@ import adminPage from '@/components/Administration/adminPage.vue';
import TopCourseSorter from '@/components/Course/TopCourseSorter.vue'; import TopCourseSorter from '@/components/Course/TopCourseSorter.vue';
import apiResowner from '../../api/modules/resowner.js'; import apiResowner from '../../api/modules/resowner.js';
import apiType from '../../api/modules/type.js' import apiType from '../../api/modules/type.js'
import { courseType } from '../../utils/tools.js'; import { courseType, deepCopy } from '../../utils/tools.js';
import apiCourse from '@/api/modules/course.js'; import apiCourse from '@/api/modules/course.js';
// import {resOwnerIndexName,sysTypeIndexName} from '@/utils/type.js'; // import {resOwnerIndexName,sysTypeIndexName} from '@/utils/type.js';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
@@ -467,7 +716,7 @@ export default {
name: 'manageCourse', name: 'manageCourse',
components: { courseForm, manager, auditCourse1, auditCourse2, adminPage, TopCourseSorter }, components: { courseForm, manager, auditCourse1, auditCourse2, adminPage, TopCourseSorter },
computed: { computed: {
...mapGetters(['resOwnerMap', 'sysTypeMap', 'userInfo', 'identity']), ...mapGetters(['resOwnerMap', 'sysTypeMap', 'userInfo', 'identity', 'selectAllLang']),
resOwnerCascaderProps() { resOwnerCascaderProps() {
// 搜索模式:关闭懒加载,直接使用全量 options // 搜索模式:关闭懒加载,直接使用全量 options
if (this.resOwnerSearchMode) { if (this.resOwnerSearchMode) {
@@ -611,12 +860,44 @@ export default {
extendRefId: '', extendRefId: '',
extendRefType: '', extendRefType: '',
scrollbarStyleApplied: false, scrollbarStyleApplied: false,
// AI 播放器相关
aiPermission: false,
selectedCourses: [],
languageSetting: {
dlgShow: false,
languageCode: ['zh-CN', 'en-US'],
aiSetNum: 0,
aiSetNoNum: 0
},
aiProcessSetting: {
dlgShow: false,
aiSet: 1,
aiAbstract: 1,
aiDraft: 1,
languageCode: ['zh-CN', 'en-US'],
aiSetNum: 0,
aiSetNoNum: 0
},
aiSetting: {
dlgShow: false,
courseId: '',
aiSet: 1,
aiAbstract: 1,
aiDraft: 1,
aiTranslate: 1,
languageCode: ['zh-CN', 'en-US', 'vi-VN']
},
aiSetTip: '是否将课程进行AI处理',
aiAbstractTip: '一键提炼课程视频核心要点,助力学员课前高效掌握重点,快速筛选学习资源',
aiDraftTip: '分段展示视频内容并精准同步时间轴,实现视频进度与文稿双向定位,学习内容触手可及',
aiTranslateTip: '智能转换视频字幕与语音为多语种,支持全球学员按需切换语言,打破学习边界',
}; };
}, },
created() { created() {
this.pickerOptions = this.buildPickerOptions(); this.pickerOptions = this.buildPickerOptions();
}, },
mounted() { mounted() {
this.getAiPermission();
this.getAudiences() this.getAudiences()
let chooseFlag = this.$route.query.f; let chooseFlag = this.$route.query.f;
this.extendRefId = this.$route.query.refId; this.extendRefId = this.$route.query.refId;
@@ -1769,6 +2050,8 @@ export default {
return this.isDisable(row); return this.isDisable(row);
case 'toggleTop': case 'toggleTop':
return this.setTop(row); return this.setTop(row);
case 'aiSetting':
return this.setAI(row);
default: default:
return; return;
} }
@@ -1779,6 +2062,9 @@ export default {
if (row.isPermission && !this.forChoose && row.status == 2) { if (row.isPermission && !this.forChoose && row.status == 2) {
actions.push({ key: 'withdraw', label: '撤回', className: 'action-link--primary' }); actions.push({ key: 'withdraw', label: '撤回', className: 'action-link--primary' });
} }
if (this.aiPermission) {
actions.push({ key: 'aiSetting', label: 'AI设置', className: 'action-link--primary' });
}
if (row.isPermission && row.status != 2) { if (row.isPermission && row.status != 2) {
actions.push({ key: 'edit', label: '编辑', className: 'action-link--primary' }); actions.push({ key: 'edit', label: '编辑', className: 'action-link--primary' });
} }
@@ -1837,6 +2123,151 @@ export default {
if (inputInner) { if (inputInner) {
inputInner.style.width = `${target}px`; inputInner.style.width = `${target}px`;
} }
},
// AI 播放器相关方法
getLanguageName(lang) {
const item = (this.selectAllLang || []).find(it => it.srclang === lang);
return (item && item.label) || '';
},
handleSelectionChange(val) {
this.selectedCourses = val || [];
},
getAIInfoByList(list = []) {
const total = list.length;
let aiSetNum = 0;
let aiSetNoNum = 0;
list.forEach(item => {
if (item.aiSet === 1) {
aiSetNum++;
} else {
aiSetNoNum++;
}
});
return {
selectNum: total,
aiSetNum,
aiSetNoNum
};
},
setAI(row) {
this.aiSetting = {
dlgShow: true,
...row
};
},
changeAIKey(key) {
this.aiSetting[key] = this.aiSetting[key] === 1 ? 0 : 1;
},
confirmAISetting() {
const item = deepCopy(this.aiSetting);
item.languageStatus = item.aiSet;
item.languageCode = item.languageCode || [];
if (!item.languageCode.includes('zh-CN')) {
item.languageCode.unshift('zh-CN');
}
this._benchAiSet(
[item],
() => {
this.$showManageMessage('AI设置保存成功', 'success');
this.aiSetting.dlgShow = false;
this.searchData();
},
() => {
this.$showManageMessage('AI设置保存失败', 'error');
}
);
},
setLanguage() {
if (!this.selectedCourses.length) return;
const info = this.getAIInfoByList(this.selectedCourses);
this.languageSetting = {
dlgShow: true,
languageCode: ['zh-CN', 'en-US'],
...info
};
},
enableAI() {
if (!this.selectedCourses.length) return;
const info = this.getAIInfoByList(this.selectedCourses);
this.aiProcessSetting = {
dlgShow: true,
aiSet: 1,
aiAbstract: 1,
aiDraft: 1,
languageCode: ['zh-CN', 'en-US'],
...info
};
},
confirmLanguageSetting() {
const courseList = deepCopy(this.selectedCourses || []);
let languageCode = deepCopy(this.languageSetting.languageCode || []);
if (!languageCode.includes('zh-CN')) {
languageCode.unshift('zh-CN');
}
courseList.forEach(item => {
item.languageCode = languageCode;
item.aiTranslate = item.aiSet;
item.languageStatus = item.aiSet;
});
this._benchAiSet(
courseList,
() => {
this.$showManageMessage('设置语种成功!', 'success');
this.languageSetting.dlgShow = false;
this.searchData();
},
() => {
this.$showManageMessage('设置语种失败!', 'error');
}
);
},
confirmAiProcess() {
const courseList = deepCopy(this.selectedCourses || []);
let { aiSet, aiAbstract, aiDraft, languageCode } = this.aiProcessSetting;
languageCode = languageCode || [];
if (!languageCode.includes('zh-CN')) {
languageCode.unshift('zh-CN');
}
courseList.forEach(item => {
item.aiSet = aiSet;
item.aiAbstract = aiAbstract;
item.aiDraft = aiDraft;
item.aiTranslate = aiSet;
item.languageStatus = aiSet;
item.languageCode = languageCode;
});
this._benchAiSet(
courseList,
() => {
this.$showManageMessage('开启AI处理成功', 'success');
this.aiProcessSetting.dlgShow = false;
this.searchData();
},
() => {
this.$showManageMessage('开启AI处理失败', 'error');
}
);
},
_benchAiSet(courseList, successCB, failCB) {
apiCourse.benchAiSet({ courseList }).then(res => {
if (res && res.status === 200) {
if (successCB) successCB(res);
} else if (failCB) {
failCB(res);
}
});
},
getAiPermission() {
apiCourse.listByUser({}).then(res => {
if (res && res.code === 200 && Array.isArray(res.data)) {
const index = res.data.findIndex(item => item.permissionCode === 'KjbAiSetCode');
this.aiPermission = index !== -1;
} else {
this.aiPermission = false;
}
}).catch(() => {
this.aiPermission = false;
});
} }
} }
}; };
@@ -2219,6 +2650,55 @@ export default {
overflow: hidden; overflow: hidden;
} }
// AI 播放器相关样式
.form-item {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.form-label {
white-space: nowrap;
font-size: 14px;
color: #000000;
}
.status-text {
font-size: 14px;
color: #000000;
}
.action-buttons {
display: flex;
gap: 10px;
margin-left: auto;
}
.tips {
color: #f56c6c;
font-size: 12px;
margin: 10px 0;
line-height: 1.5;
}
.languages-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.language-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
background-color: #f5f5f5;
font-size: 12px;
}
// 注意custom-putaway 和 custom-takeout 已在全局样式 src/assets/styles/btn.scss 中定义
// 无需在此重复定义
::v-deep .el-table .el-table__body-wrapper::-webkit-scrollbar-thumb { ::v-deep .el-table .el-table__body-wrapper::-webkit-scrollbar-thumb {
border-radius: 4px; border-radius: 4px;
background-color: #4284F7; background-color: #4284F7;