Compare commits

..

51 Commits

Author SHA1 Message Date
huweihang
2b95b990d0 [FIX]文字重叠问题和光标拖动优化 2025-12-24 18:10:04 +08:00
赵依梦
018d9af1d8 Merge branch '251114-feature-course-online' of https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal into 251114-feature-course-online
# Conflicts:
#	src/views/course/ManageListRemote.vue
2025-12-24 18:04:16 +08:00
赵依梦
b26336604a ui调整 2025-12-24 18:00:38 +08:00
huweihang
d20f8c1724 [FIX]ai按钮高度修改 2025-12-24 17:44:18 +08:00
huweihang
c462eb00a2 feat: 下拉面板可勾选, 在线列表创建时间排序不可点击 2025-12-24 17:16:10 +08:00
huweihang
0a74f1a064 Merge branch '251114-feature-course-online' of https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal into 251114-feature-course-online 2025-12-24 15:44:01 +08:00
huweihang
afbcbcec97 [FIX]在线课程列表, 课程名称清除按钮位置修改, 资源归属下级面板处理 2025-12-24 15:42:55 +08:00
赵依梦
4eef13943d 姓名搜索框话术修改 2025-12-24 15:42:06 +08:00
赵依梦
c45548b629 ui调整 2025-12-24 14:53:11 +08:00
赵依梦
40edd09a87 ui调整 2025-12-24 14:29:31 +08:00
赵依梦
81962afb02 部门长度调整 2025-12-24 11:22:48 +08:00
huweihang
b9f69735cf style: 优化课程管理界面布局和交互
- 将课程信息部分分为左右两栏,增强视觉层次感。
- 添加返回按钮以改善用户导航体验。
- 调整CSS样式以实现更好的响应式布局。
2025-12-23 23:28:18 +08:00
huweihang
54f496f962 refactor: 使用NameRemoteSelect组件替换el-select
-更新了AudienceModal和ManageListRemote,以使用新的NameRemoteSelect组件进行成员和教师选择。
-从组件中删除了冗余加载和选项管理。
-增强了NameRemoteSelect组件的样式和功能,以改善用户体验。
-调整index.scss中的样式以获得更好的布局一致性。
2025-12-23 22:37:12 +08:00
赵依梦
0a11f6cff9 ui调整 2025-12-23 19:53:42 +08:00
赵依梦
e4b0591509 ui调整 2025-12-23 19:47:25 +08:00
huweihang
d368b45739 [FIX]调整操作列宽度 2025-12-23 18:01:46 +08:00
huweihang
ce00a46869 [FIX]修改AI权限获取逻辑 2025-12-23 18:01:18 +08:00
赵依梦
3dc0bf3949 ui调整 2025-12-23 14:54:56 +08:00
赵依梦
59588c1372 审核状态字段宽度,管理页面课程相关信息展示ui调整 2025-12-23 13:36:07 +08:00
huweihang
bb453a0200 feat: 添加AI播放器相关功能和设置界面
- 新增批量语种设置和开启AI处理按钮
- 实现AI翻译、AI处理和AI设置的弹框功能
- 增加AI权限检查和相关状态管理
- 更新表格选择功能以支持AI相关操作
2025-12-22 23:39:05 +08:00
huweihang
79111b9e6b style: 在线管理列表问题修改 2025-12-22 18:15:22 +08:00
huweihang
6922882111 Merge branch '251114-feature-course-online' 2025-12-22 14:55:10 +08:00
陈昱达
e217e107bc fix(App): 移除无效的内联样式和调试文本
- 删除了不必要的 `style="width: 100vw"` 内联样式
- 移除了模板中的调试文本 `12312`
2025-12-22 14:19:13 +08:00
赵依梦
a26dc0aa2b 管理tab切换闪烁问题修复 2025-12-20 16:51:44 +08:00
赵依梦
c1d6021ef8 教师端1期导出参数调整 2025-12-19 12:16:53 +08:00
赵依梦
c74213d63a 教师端1期导出参数调整 2025-12-19 11:56:01 +08:00
赵依梦
3be811cd8e 教师端1期导出参数调整 2025-12-19 10:32:38 +08:00
赵依梦
fa48bdf266 教师端1期导出参数调整 2025-12-19 09:58:07 +08:00
huweihang
1dbbb488d6 [FIX]受众列表总条数字段修改count 2025-12-19 09:20:47 +08:00
huweihang
bf44d562dd fix: 更新AUDIENCE_LIST API路径并调整AudienceModal中的成员ID参数 2025-12-18 21:42:00 +08:00
huweihang
d207d4eee6 fix: 更新SignupModal中的搜索功能以支持重置页码和添加文档注释 2025-12-18 17:26:58 +08:00
huweihang
2264a8a6a7 style: 更新审核状态的样式以增强可读性 2025-12-18 16:40:52 +08:00
huweihang
148ab93b07 fix: 更新SignupModal中的搜索功能以重置页码 2025-12-18 16:36:53 +08:00
赵依梦
7a9063ffae 1期问题修改 2025-12-18 16:32:59 +08:00
huweihang
caa9ee848e fix: 更新SignupModal中的studentList映射以包含realName 2025-12-18 15:25:45 +08:00
赵依梦
47036beb64 Merge branch '251114-feature-course-online' of https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal into 251114-feature-course-online 2025-12-18 14:11:55 +08:00
赵依梦
632aaf8e75 1期问题修改 2025-12-18 14:11:38 +08:00
huweihang
436bb99814 fix: 更新CourseManage组件中的受众id映射,以使用groupId而不是id 2025-12-18 14:10:08 +08:00
赵依梦
5d2d8fcac6 Merge branch '251114-feature-course-online' of https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal into 251114-feature-course-online 2025-12-18 11:58:54 +08:00
赵依梦
4277f985b0 1期问题修改 2025-12-18 11:58:36 +08:00
huweihang
0bbbbeb0ee feat: 新增根据关键字检索用户的API,增强用户选择功能
(cherry picked from commit f2e188c0e6)
2025-12-17 22:50:40 +08:00
huweihang
ea6ed74746 style: 调整受众弹窗表格列宽和数据映射,优化用户界面 2025-12-17 22:35:07 +08:00
赵依梦
2389b06f28 1期toast提示ui调整 2025-12-17 20:37:49 +08:00
赵依梦
523b5fd801 1期toast提示ui调整 2025-12-17 18:49:59 +08:00
huweihang
32874683e2 feat: 新增获取全量机构树的API,优化SignupModal和ManageListRemote组件的组织树交互逻辑
(cherry picked from commit a9b36884c3)
2025-12-17 18:49:09 +08:00
赵依梦
6c0d1de13d Merge branch '251114-feature-course-online' of https://codeup.aliyun.com/67762337eccfc218f6110e0e/vue/learning-system-portal into 251114-feature-course-online 2025-12-17 18:08:47 +08:00
赵依梦
c4fdebe811 1期ui优化 2025-12-17 18:08:17 +08:00
huweihang
d6132738d5 fix: 修改删除报名记录的确认提示文本,更新删除API调用以包含学生ID
(cherry picked from commit e176769174)
2025-12-17 17:45:40 +08:00
huweihang
b2654f5501 feat: 增强报名管理功能,新增删除课程学习记录API,优化受众选择弹窗的多选逻辑和搜索功能
(cherry picked from commit 5f518b713a)
2025-12-17 17:45:33 +08:00
huweihang
be730deef2 style: 更新对话框样式,调整按钮尺寸和间距,增强用户界面一致性
(cherry picked from commit b2395fd3a2)
2025-12-17 11:39:19 +08:00
huweihang
a6c389b80b style: 添加滚动条样式并调整选区区域的类名以提升用户体验 2025-12-17 02:07:14 +08:00
37 changed files with 10013 additions and 3027 deletions

View File

@@ -1,8 +1,7 @@
<template>
<div id="app" style="width: 100vw">
<div id="app">
<keep-alive :include="['case']">
<router-view />
12312
</keep-alive>
<!-- 添加AI Call组件 -->
<AICall

View File

@@ -4,6 +4,7 @@
*
**/
import ajax from '@/utils/xajax.js'
import ajax2 from '../unionAjax.js';
/**
* 保存课程基本信息,新增和更新都是此方式
@@ -42,8 +43,8 @@ import ajax from '@/utils/xajax.js'
]
}
*/
const saveBase = function(data) {
return ajax.postJson('/xboe/m/course/manage/save', data);
const saveBase = function (data) {
return ajax.postJson('/xboe/m/course/manage/save', data);
}
@@ -52,39 +53,39 @@ const saveBase = function(data) {
* 仅仅是保存课程信息,不包括教师信息
* @param {Object} data
*/
const saveOnlyCourse = function(data) {
return ajax.postJson('/xboe/m/course/manage/save-only-course', data);
const saveOnlyCourse = function (data) {
return ajax.postJson('/xboe/m/course/manage/save-only-course', data);
}
/**提交课程*/
const submitCourse = function(data) {
return ajax.postJson('/xboe/m/course/manage/submit', data);
const submitCourse = function (data) {
return ajax.postJson('/xboe/m/course/manage/submit', data);
}
/**撤销已提交审核的课程*/
const revokeSubmit = function(id) {
return ajax.post('/xboe/m/course/manage/revoke', {id});
const revokeSubmit = function (id) {
return ajax.post('/xboe/m/course/manage/revoke', { id });
}
/**
* 复制课程
* @param {Object}
*/
const copyCourse = function(data) {
return ajax.post('/xboe/m/course/manage/copy',data);
const copyCourse = function (data) {
return ajax.post('/xboe/m/course/manage/copy', data);
}
/*
查询课程是否有重复名称
*/
const isRedoName=function(){
return ajax.get('/xboe/m/course/manage/isRedoName');
const isRedoName = function () {
return ajax.get('/xboe/m/course/manage/isRedoName');
}
/*
查询当前添加课程是否已有
courseName 要添加的课程姓名
*/
const isCourseName=function(courseName,courseId){
return ajax.get(`/xboe/m/course/manage/isCourseName?courseName=${courseName}&courseId=${courseId}`);
const isCourseName = function (courseName, courseId) {
return ajax.get(`/xboe/m/course/manage/isCourseName?courseName=${courseName}&courseId=${courseId}`);
}
/**
* 查询修改日志,列表,不分页
@@ -95,16 +96,16 @@ const isCourseName=function(courseName,courseId){
name: 修改人
}
*/
const findUpdateLogs = function(params) {
return ajax.post('/xboe/m/course/manage/upldate-logs',params);
const findUpdateLogs = function (params) {
return ajax.post('/xboe/m/course/manage/upldate-logs', params);
}
/**
* 根据id获取修改的详细信息
* @param {Object} id
*/
const getUpdateLog = function(id) {
return ajax.get('/xboe/m/course/manage/upldate-log-detail?id='+id);
const getUpdateLog = function (id) {
return ajax.get('/xboe/m/course/manage/upldate-log-detail?id=' + id);
}
/**
@@ -139,8 +140,8 @@ const getUpdateLog = function(id) {
]
}
*/
const saveContent = function(data) {
return ajax.postJson('/xboe/m/course/content/save', data);
const saveContent = function (data) {
return ajax.postJson('/xboe/m/course/content/save', data);
}
@@ -159,18 +160,18 @@ const saveContent = function(data) {
* ]
* @returns
*/
const updateContentOrders = function(cid,items) {
return ajax.postJson('/xboe/m/course/content/update-orders/'+cid, items);
const updateContentOrders = function (cid, items) {
return ajax.postJson('/xboe/m/course/content/update-orders/' + cid, items);
}
/**
* 课程的详细信息
* @param {String} id
*/
const detail = function(id) {
return ajax.get('/xboe/m/course/manage/detail?id=' + id);
const detail = function (id) {
return ajax.get('/xboe/m/course/manage/detail?id=' + id);
}
const getDictIds = function(pid,type) {
const getDictIds = function (pid, type) {
return ajax.get(`/xboe/m/course/manage/getDictIds?pid=${pid}&type=${type}`);
}
/**
@@ -181,8 +182,8 @@ const getDictIds = function(pid,type) {
name:''
}
*/
const updateContentName = function(data) {
return ajax.post('/xboe/m/course/content/update-name', data);
const updateContentName = function (data) {
return ajax.post('/xboe/m/course/content/update-name', data);
}
/**
@@ -194,8 +195,8 @@ const updateContentName = function(data) {
erasable:是否物理删除,此值是课程信息中系统带过来的字段,直接使用它就可以了
}
*/
const delContent = function(data) {
return ajax.post('/xboe/m/course/content/delete', data);
const delContent = function (data) {
return ajax.post('/xboe/m/course/content/delete', data);
}
/**
@@ -207,40 +208,40 @@ const delContent = function(data) {
parentId 上级id。如果没有可以填“-1”字符串
orderIndex显示顺序顺序索引整数
*/
const saveSection = function(data) {
return ajax.post('/xboe/m/course/content/save-section', data);
const saveSection = function (data) {
return ajax.post('/xboe/m/course/content/save-section', data);
}
/**
* 删除章节目录,注意只有目录下没有学习内容时才允许删除
* @param {Object} data
*/
const delSection = function(id) {
return ajax.post('/xboe/m/course/content/delete-section?id=' + id);
const delSection = function (id) {
return ajax.post('/xboe/m/course/content/delete-section?id=' + id);
}
/**
* 根据课程学习内容的id。获取作业信息只有学习内容是作业时才会有信息
* @param {Object} ccid
*/
const getHomework = function(ccid) {
return ajax.post('/xboe/m/course/content/homework?ccid=' + ccid);
const getHomework = function (ccid) {
return ajax.post('/xboe/m/course/content/homework?ccid=' + ccid);
}
/**
* 根据课程学习内容的id。获取考试信息只有学习内容是考试时才会有信息
* @param {Object} ccid
*/
const getExam = function(ccid) {
return ajax.post('/xboe/m/course/content/exam?ccid=' + ccid);
const getExam = function (ccid) {
return ajax.post('/xboe/m/course/content/exam?ccid=' + ccid);
}
/**
* 根据课程学习内容的id。获取评估信息评估内容可以获取
* @param {Object} ccid
*/
const getAssess = function(ccid) {
return ajax.post('/xboe/m/course/content/assess?ccid=' + ccid);
const getAssess = function (ccid) {
return ajax.post('/xboe/m/course/content/assess?ccid=' + ccid);
}
/**
@@ -265,21 +266,21 @@ const getAssess = function(ccid) {
name 课程名称
*/
const pageList = function(query) {
return ajax.post('/xboe/m/course/manage/pagelist', query);
const pageList = function (query) {
return ajax.post('/xboe/m/course/manage/pagelist', query);
}
/**
* 课程管理分页查询(新)
* @param {Object} query
*/
const managePage = function(query) {
return ajax.postJson('/xboe/m/course/manage/page', query);
const managePage = function (query) {
return ajax.postJson('/xboe/m/course/manage/page', query);
}
/**计算待审核课程*/
const countWaitAudit = function() {
const countWaitAudit = function () {
return ajax.get('/xboe/m/course/manage/wait-audit-num');
}
@@ -288,8 +289,8 @@ const countWaitAudit = function() {
* 当前用户需要审核的课程列表
* @param {Object} query 同pageList
*/
const auditList = function(query) {
return ajax.post('/xboe/m/course/manage/audit-pagelist', query);
const auditList = function (query) {
return ajax.post('/xboe/m/course/manage/audit-pagelist', query);
}
@@ -297,8 +298,8 @@ const auditList = function(query) {
* 【已移到courseAudit中】
* 教师需要审核的课程列表
*/
const teacherAuditList = function(query) {
return ajax.post('/xboe/m/course/audit/teacher-course', query);
const teacherAuditList = function (query) {
return ajax.post('/xboe/m/course/audit/teacher-course', query);
}
/**
@@ -307,16 +308,16 @@ const teacherAuditList = function(query) {
* @param {Object} data
* {courseId:课程id,teacherId:指定的审核人教师的id,teacherName:教师名称,remark:备注}
*/
const auditAppoint = function(data) {
return ajax.post('/xboe/m/course/audit/appoint', data);
const auditAppoint = function (data) {
return ajax.post('/xboe/m/course/audit/appoint', data);
}
/**
* 获取审核信息,上面教师点击审核课程时,用于查询,上面“转审”时,用户填写的备注信息
* @param {courseId:'课程id',teacherId:'可以不填写,系统会查询当前人'} data
*/
const getAuditInfo = function(data) {
return ajax.post('/xboe/m/course/audit/infos', data);
const getAuditInfo = function (data) {
return ajax.post('/xboe/m/course/audit/infos', data);
}
@@ -324,56 +325,56 @@ const getAuditInfo = function(data) {
* 管理员的课程审核处理
* @param {Object} query {id:课程id,title:课程的名称, Boolean pass 是否通过,remark 备注}
*/
const audit = function(data) {
return ajax.post('/xboe/m/course/manage/audit', data);
const audit = function (data) {
return ajax.post('/xboe/m/course/manage/audit', data);
}
/**
* 审核记录列表,分页查询
*/
const auditPageRecords = function(data) {
return ajax.post('/xboe/m/course/audit/page-records', data);
const auditPageRecords = function (data) {
return ajax.post('/xboe/m/course/audit/page-records', data);
}
/**
* 审核记录列表要卖课程id查询出审核列记录信息
* { courseId:必须}
*/
const auditCourseRecords = function(data) {
return ajax.post('/xboe/m/course/audit/course-records',data);
const auditCourseRecords = function (data) {
return ajax.post('/xboe/m/course/audit/course-records', data);
}
/**
* 管理员的课程发布,当前已经不再使用了
* @param {Object} query {ids:课程id,多个使用逗号分隔,title:课程的名称, Boolean pass 是否发布}
*/
const publish = function(data) {
return ajax.post('/xboe/m/course/manage/publish', data);
const publish = function (data) {
return ajax.post('/xboe/m/course/manage/publish', data);
}
const auditAndPublish=function(data) {
return ajax.post('/xboe/m/course/manage/audit-publish', data);
const auditAndPublish = function (data) {
return ajax.post('/xboe/m/course/manage/audit-publish', data);
}
/**
* 设置top
* @param {Object} query {ids:课程id,多个使用逗号分隔,title:课程的名称,Boolean top 是否置顶}
*/
const setTop = function(data) {
return ajax.post('/xboe/m/course/manage/top', data);
const setTop = function (data) {
return ajax.post('/xboe/m/course/manage/top', data);
}
/**
* 是否展示置顶相关功能
*/
const showSetTop = function() {
const showSetTop = function () {
return ajax.get('/xboe/m/course/manage/show-settop');
}
/**
* 获取置顶课程列表
*/
const fetchTopCourseList = function() {
const fetchTopCourseList = function () {
return ajax.get('/xboe/m/course/manage/topList');
}
@@ -381,7 +382,7 @@ const fetchTopCourseList = function() {
* 更新置顶课程排序
* @param {Array<{id:string,sortWeight:number}>} data
*/
const updateTopCourseSort = function(data) {
const updateTopCourseSort = function (data) {
return ajax.postJson('/xboe/m/course/manage/top-sortchange', data);
}
@@ -389,8 +390,8 @@ const updateTopCourseSort = function(data) {
* 管理员的设置启用停用
* @param {Object} query {ids:课程id,多个使用逗号分隔,title:课程的名称, Boolean enabled 是否启用}
*/
const setEnabled = function(data) {
return ajax.post('/xboe/m/course/manage/enabled', data);
const setEnabled = function (data) {
return ajax.post('/xboe/m/course/manage/enabled', data);
}
/**
@@ -398,34 +399,34 @@ const setEnabled = function(data) {
* erasable 此值是课程信息带过来的,直接传就可以
* @param {Object} query {id:课程id,多个使用逗号分隔,Boolean erasable 是否物理删除,title:课程的名称, remark 备注}
*/
const del = function(data) {
return ajax.post('/xboe/m/course/manage/delete', data);
const del = function (data) {
return ajax.post('/xboe/m/course/manage/delete', data);
}
/*
详情
*/
const detailFew=function(id){
return ajax.get('/xboe/m/course/portal/detail-few?id=' + id);
const detailFew = function (id) {
return ajax.get('/xboe/m/course/portal/detail-few?id=' + id);
}
/*
直接审核,教师提交审核
*/
const sumbits=function(data){
return ajax.post('/xboe/m/course/manage/sumbits',data);
const sumbits = function (data) {
return ajax.post('/xboe/m/course/manage/sumbits', data);
}
/*
教师授课记录
*/
const teacherCourse=function(teacherId){
return ajax.get('/xboe/m/course/manage/teacher-course?teacherId='+teacherId);
const teacherCourse = function (teacherId) {
return ajax.get('/xboe/m/course/manage/teacher-course?teacherId=' + teacherId);
}
/*
教师授课记录导出
@param teacherId 教师id
*/
const exportTeacherCourse=function(teacherId){
return ajax.post('/xboe/m/course/manage/export-teacher-course?teacherId='+teacherId)
const exportTeacherCourse = function (teacherId) {
return ajax.post('/xboe/m/course/manage/export-teacher-course?teacherId=' + teacherId)
}
/*
*待审核课程记录导出
@@ -445,25 +446,25 @@ const exportTeacherCourse=function(teacherId){
type课程类型10微课21在线课(直播)20:在线课( 录播)30:面授课40:混合式,
name 课程名称
*/
const exportCourseAudit=function(query){
return ajax.post('/xboe/m/course/manage/exportCourseAudit',query);
const exportCourseAudit = function (query) {
return ajax.post('/xboe/m/course/manage/exportCourseAudit', query);
}
/*
参数同上待审核课程记录导出
课程的导出和已审核的课程导出
*/
const exportCourse = function(query) {
return ajax.get({
url: '/xboe/m/course/manage/export',
method: 'get',
params: query,
responseType: 'blob'
});
const exportCourse = function (query) {
return ajax.get({
url: '/xboe/m/course/manage/export',
method: 'get',
params: query,
responseType: 'blob'
});
}
//判断受众id是否有关联
const queryCrowd=function(query){
return ajax.postJson('/xboe/m/course/manage/queryCrowd',query);
const queryCrowd = function (query) {
return ajax.postJson('/xboe/m/course/manage/queryCrowd', query);
}
/**
@@ -472,69 +473,78 @@ const queryCrowd=function(query){
* ids
* }
* */
const ids=function (data){
return ajax.postJson('/xboe/m/course/manage/ids',data);
const ids = function (data) {
return ajax.postJson('/xboe/m/course/manage/ids', data);
}
const saveTip = function() {
const saveTip = function () {
return ajax.postJson('/xboe/m/course/manage/saveTip');
}
/**
* 获取我开发的课程列表
*/
const courseList = function(data) {
const courseList = function (data) {
return ajax.postJson('/xboe/m/course/manage/develop_page', data);
}
// ai播放器相关 - 批量AI设置
const benchAiSet = function (data) {
return ajax.postJson('/xboe/m/course/manage/benchAiSet', data);
}
const listByUser = function (data) {
return ajax2.get('/manageApi/admin/thirdApi/permission/listByUser?permissionType=PAGE');
}
export default {
saveBase,
submitCourse,
revokeSubmit,
copyCourse,
findUpdateLogs,
getUpdateLog,
detail,
getDictIds,
saveContent,
pageList,
managePage,
setEnabled,
del,
publish,
saveSection,
getHomework,
countWaitAudit,
auditList,
teacherAuditList,
auditAppoint,
getAuditInfo,
audit,
auditPageRecords,
auditCourseRecords,
auditAndPublish,
getAssess,
setTop,
showSetTop,
fetchTopCourseList,
updateTopCourseSort,
delSection,
getExam,
delContent,
updateContentName,
updateContentOrders,
saveOnlyCourse,
isRedoName,
isCourseName,
detailFew,
sumbits,
teacherCourse,
exportTeacherCourse,
exportCourseAudit,
exportCourse,
queryCrowd,
saveBase,
submitCourse,
revokeSubmit,
copyCourse,
findUpdateLogs,
getUpdateLog,
detail,
getDictIds,
saveContent,
pageList,
managePage,
setEnabled,
del,
publish,
saveSection,
getHomework,
countWaitAudit,
auditList,
teacherAuditList,
auditAppoint,
getAuditInfo,
audit,
auditPageRecords,
auditCourseRecords,
auditAndPublish,
getAssess,
setTop,
showSetTop,
fetchTopCourseList,
updateTopCourseSort,
delSection,
getExam,
delContent,
updateContentName,
updateContentOrders,
saveOnlyCourse,
isRedoName,
isCourseName,
detailFew,
sumbits,
teacherCourse,
exportTeacherCourse,
exportCourseAudit,
exportCourse,
queryCrowd,
ids,
saveTip,
courseList
courseList,
benchAiSet,
listByUser,
}

View File

@@ -5,7 +5,7 @@ export const USER_LIST_PAGE = "/manageApi/admin/thirdApi/user/list";
export const ORG_LIST = "/manageApi/admin/thirdApi/org/list";
export const ORG_CHILD_LIST = "/manageApi/admin/thirdApi/org/info";
// 查询受众中的用户列表
export const AUDIENCE_LIST = "/userbasic/audience/memberList";
export const AUDIENCE_LIST = "/systemapi/xboe/school/study/signup/audience/memberList";
export const USER_AUDIENCES = "/manageApi/admin/thirdApi/audience/userAudiences";
export const fetchUserAudiences = (params) =>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

View File

@@ -97,3 +97,29 @@
font-size: 14px;
border-radius: 4px;
}
// 已下架
.custom-takeout{
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;
}
// 已上架
.custom-putaway{
display: inline-block;
padding: 3px 13px;
border-radius: 20px;
font-size: 12px;
background: rgba(220, 252, 231, 1);
color: rgba(22, 101, 52, 1);
font-size: 12px;
font-weight: 500;
line-height: 17px;
letter-spacing: 0px;
}

View File

@@ -19,7 +19,7 @@ body {
font-family: "Menlo", "苹方-简" !important;
}
.el-message {
top: 15%!important
top: 15%;
}
.el-card.is-always-shadow, .el-card.is-hover-shadow:focus, .el-card.is-hover-shadow:hover{
box-shadow: none;
@@ -462,6 +462,7 @@ li{
justify-content: flex-end;
padding-right: 20px;
margin-top: 8px;
padding-bottom: 14px;
.el-button {
width: 78px;
@@ -651,7 +652,8 @@ li{
color: #000000;
margin-left: 4px;
.el-input__inner {
width: 28px;
width: 100%;
min-width: 28px;
height: 28px;
background: #F5F9FF;
border-radius: 4px;

View File

@@ -433,59 +433,73 @@
}
}
.el-message.new-message {
box-shadow: none !important;
background-color: #edf2fc !important;
min-width: 240px !important;
background-color: #fff;
box-shadow: 0px 8px 20px 0px rgba(0,35,114,0.1);
min-width: 180px !important;
height: 52px !important;
border-radius: 12px !important;
top: 236px !important;
font-size: 20px !important;
// top: 270px !important;
font-size: 16px !important;
font-weight: bold !important;
border: none !important;
.el-message__content {
font-size: 14px !important;
font-weight: bold !important;
// margin-top: 2px;
}
}
.el-message--success.new-message {
box-shadow: none !important;
background-color: rgba($color: #4CB967, $alpha: 0.1) !important;
min-width: 240px !important;
background-color: #fff;
box-shadow: 0px 8px 20px 0px rgba(0,35,114,0.1);
min-width: 180px !important;
height: 52px !important;
border-radius: 12px !important;
top: 236px !important;
font-size: 22px !important;
// top: 270px !important;
font-size: 16px !important;
border: none !important;
.el-message__content {
color: #189B39 !important;
font-size: 20px !important;
font-size: 14px !important;
font-weight: bold !important;
// margin-top: 2px;
}
}
.el-message--error.new-message {
box-shadow: none !important;
background-color: rgba($color: #FF3636 , $alpha: 0.1) !important;
min-width: 240px !important;
background-color: #fff;
box-shadow: 0px 8px 20px 0px rgba(0,35,114,0.1);
min-width: 180px !important;
height: 52px !important;
border-radius: 12px !important;
top: 236px !important;
font-size: 22px !important;
// top: 270px !important;
font-size: 16px !important;
font-weight: bold !important;
border: none !important;
.el-message__content {
color: #CF1717 !important;
font-size: 20px !important;
font-size: 14px !important;
font-weight: bold !important;
// margin-top: 2px;
}
}
.el-message--warning.new-message {
box-shadow: none !important;
background-color: #fdf6ec !important;
min-width: 240px !important;
background-color: #fff;
box-shadow: 0px 8px 20px 0px rgba(0,35,114,0.1);
min-width: 180px !important;
height: 52px !important;
border-radius: 12px !important;
top: 236px !important;
font-size: 20px !important;
// top: 270px !important;
font-size: 16px !important;
font-weight: bold !important;
border: none !important;
.el-message__content {
font-size: 14px !important;
font-weight: bold !important;
// margin-top: 2px;
}
}
@@ -1596,3 +1610,10 @@
}
}
}
.tooltip-multiline {
max-width: 400px; /* 设置最大宽度,超出则换行 */
white-space: normal; /* 允许换行 */
word-break: break-all; /* 断行规则 */
}

View File

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

View File

@@ -208,6 +208,44 @@
placeholder="请尽量填写课程简介,用于列表中显示,可以让用户更容易了解课程信息">
</el-input>
</el-form-item>
<!-- ai播放器相关 -->
<el-form-item v-show="aiPermission" 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>
@@ -390,6 +428,44 @@
placeholder="请尽量填写课程简介,用于列表中显示,可以让用户更容易了解课程信息">
</el-input>
</el-form-item>
<!-- ai播放器相关 -->
<el-form-item v-show="aiPermission" 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>
@@ -600,13 +676,23 @@ export default {
highlightStyle: {},
guidanceElements: [],
isFirstCreate: false, // 标记是否为首次创建
selectedOrg: {
orgId: null,
name: ''
},
aiPermission: false, // ai播放器权限
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;
@@ -632,6 +718,9 @@ export default {
}
},
mounted() {
this.getDictIds();
// ai播放器相关
this.getAiPermission();
let extendFlag=this.$route.query.f; //是否是管理端过来的
this.extendRefId=this.$route.query.refId;
this.extendRefType=this.$route.query.refType;
@@ -656,6 +745,16 @@ export default {
handleTagHelp(){
this.checkAndShowGuidance();
},
getAiPermission() {
apiCourse.listByUser({}).then(res => {
console.log('res', res);
if(res.code === 200){
let index = res.data.findIndex(item => item.permissionCode === 'KjbAiSetCode');
this.aiPermission = index !== -1;
console.log('index', index, this.aiPermission);
}
})
},
// 关键字的更改
changeKeywords(option){
if(option.target.value){
@@ -895,7 +994,7 @@ export default {
}
});
}
this.initAiData();
} else {
console.log(editData,'editData');
this.weikeReset = editData.id;
@@ -1082,6 +1181,45 @@ export default {
this.courseCoverurl = '';
this.courseInfo.coverImg = '';
},
//获取字典信息
async getDictIds() {
console.log("--- 获取字典信息 1 = ", this.dicts);
try {
const response = await apiCourse.getDictIds(637, 1); // 确保返回 Promise
console.log("--- 获取字典信息 2 result= ", response);
if (response.status === 200) {
this.dicts = response.result.dicts; // 正确提取 dicts
console.log("--- 获取字典信息 3 = ", this.dicts);
}
} catch (error) {
console.error("获取字典信息失败:", error);
}
},
// ai播放器相关
// 初始化ai数据
initAiData() {
if (!this.aiPermission) {
// 如果ai设置为空则给默认值 - 会看成新增状态
if(this.courseInfo.aiSet === null || this.courseInfo.aiSet === '' || this.courseInfo.aiSet === undefined){
this.courseInfo.languageCode = ['zh-CN', 'en-US'];
}
return;
}
// 如果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.languageStatus = 1;
this.courseInfo.languageCode = ['zh-CN', 'en-US'];
} else {
// 获取ai设置信息
this.courseInfo.isAddAI = 0;
}
},
//获取课程信息
async getDetail(id) {
this.curCourseId = id;
@@ -1106,6 +1244,12 @@ export default {
this.sectionInfo.list = result.sections;
this.courseTeachers = result.teachers; //课程的老师信息
this.isPermission = result.isPermission; //课程的老师信息
this.dicts = result.dicts; //课程的老师信息
console.log("--- 编辑查看 this.isPermission = ",this.isPermission)
console.log("--- 编辑查看 this.dicts = ",this.dicts)
// ai播放器相关
this.initAiData()
if(!this.courseInfo.orgId){
//根据课程创建者获取机构id
apiUser.getOrgSimpleByUserId(result.course.sysCreateAid).then(ors=>{

View File

@@ -1,142 +1,463 @@
<template>
<div id="NameFilterSelect">
<el-select @clear="handleClose" style="width:100%" @change="handleChange" clearable multiple v-model="aids"
filterable placeholder="姓名" v-limit-input="50" remote reserve-keyword :remote-method="initNameList"
:multiple-limit="5" :loading="nameListLoading">
<el-option v-for="item in nameList" :key="item.userId" :label="item.name" :value="item.userId">
<span>{{ item.name }}</span>
<div class="name-remote-select">
<el-select ref="selectRef" v-model="innerValue" multiple filterable remote clearable :remote-method="remoteSearch"
:loading="loading" :placeholder="placeholder" :multiple-limit="multipleLimit" :collapse-tags="innerCollapseTags"
:max-collapse-tag="maxCollapseTags" :no-data-text="noDataText" class="no-wrap-select" style="width: 100%;"
@visible-change="handleVisibleChange" @clear="handleClear" @change="emitChange">
<el-option v-for="item in options" :key="item.value" :label="formatLabel(item)" :value="item.value">
<span>{{ item.label }}</span>
<span v-if="item.code" class="option-code">{{ item.code }}</span>
</el-option>
</el-select>
<div v-if="panelVisible && selectedPanelItems.length"
class="selected-panel el-select-dropdown el-popper is-multiple" x-placement="bottom-start" :style="panelStyle"
ref="selectedPanel">
<div class="el-scrollbar">
<div class="el-select-dropdown__wrap el-scrollbar__wrap">
<ul class="el-scrollbar__view el-select-dropdown__list">
<li v-for="item in selectedPanelItems" :key="item.value"
:class="['el-select-dropdown__item', { selected: isSelected(item.value) }]"
@click.stop="togglePanelItem(item)">
<span>{{ item.label }}</span>
<span v-if="item.code" class="option-code">{{ item.code }}</span>
</li>
</ul>
</div>
</div>
<div class="popper__arrow"></div>
</div>
</div>
</template>
<script>
import apiUserbasic from "@/api/boe/userbasic.js";
export default {
name: "NameRemoteSelect",
props: {
value: {
type: Array,
default: () => [],
},
// 远程拉取方法,返回数组;可返回 Promise
fetcher: {
type: Function,
required: true,
},
// 将 fetcher 返回的原始项映射为 { value, label, code }
optionFormatter: {
type: Function,
default: (item) => item,
},
placeholder: {
type: String,
required: true,
},
multipleLimit: {
type: Number,
default: 5,
},
minQueryLen: {
type: Number,
default: 2,
},
collapseTags: {
type: Boolean,
default: true,
},
// 打开下拉时是否自动展开全部已选标签(取消+N收起
expandOnVisible: {
type: Boolean,
default: false,
},
maxCollapseTags: {
type: Number,
default: 2,
},
maxInputLen: {
type: Number,
default: 50,
},
// 是否在下拉时额外展示“已选列表”面板
showSelectedPanel: {
type: Boolean,
default: true,
},
// 额外增加的下拉最小宽度(在选择框宽度基础上追加,单位 px
panelWidthExtra: {
type: Number,
default: 60,
},
},
data() {
return {
aids: [],
nameListLoading: false,
nameList: [],
innerValue: [],
options: [],
loading: false,
keyword: "",
// 传递给 el-select 的 collapse 状态limitedMode 下需关闭折叠以便手动控制可见标签)
innerCollapseTags: true,
optionCache: {},
panelVisible: false,
countEl: null,
tagsContainerEl: null,
panelMinWidth: "",
outsideHandler: null,
selectedPanelItems: [],
// 标记:由点击 +N 主动关闭 el-select 下拉引起的 visible-change(false),需要忽略一次
ignoreNextVisibleChange: false,
};
},
props: {},
watch: {
value: {
immediate: true,
handler(val) {
this.innerValue = Array.isArray(val) ? val : [];
this.bindCountClick();
},
},
collapseTags: {
immediate: true,
handler(val) {
this.innerCollapseTags = val;
},
},
innerValue() {
this.bindCountClick();
},
panelVisible(visible) {
if (visible) {
this.bindOutsideClick();
} else {
this.unbindOutsideClick();
}
},
},
mounted() {
this.bindCountClick();
this.updatePanelWidth();
},
beforeDestroy() {
this.unbindCountClick();
this.unbindOutsideClick();
},
methods: {
handleChange() {
console.log("handleChange", this.aids);
this.$emit("handleNameChange", this.aids);
},
handleClose() {
this.$set(this, 'nameList', [])
console.log("handleClose", this.aids);
this.$emit("handleClose");
},
handleReset() {
this.aids = [];
this.nameList = [];
this.handleClear()
},
async initNameList(keyword) {
console.log("initNameList", keyword);
if (!keyword) {
formatLabel(item) {
if (!item || !item.label) return "";
return item.code ? `${item.label}` : item.label;
},
syncInputQuery(limited) {
const select = this.$refs.selectRef;
if (select && select.query !== limited) {
select.query = limited;
this.$nextTick(() => {
if (select.$refs && select.$refs.input) {
select.$refs.input.value = limited;
}
});
}
},
async remoteSearch(keyword) {
const limited = (keyword || "").slice(0, this.maxInputLen);
this.syncInputQuery(limited);
this.keyword = limited;
const query = limited.trim();
if (!query || query.length < this.minQueryLen) {
this.options = [];
return;
}
this.nameListLoading = true;
this.loading = true;
try {
const res = await apiUserbasic.selectUser(keyword);
this.nameListLoading = false;
if (res && res.status === 200) {
const resultList = res.result || [];
this.nameList = resultList
.map((item) => this.formatCreatorItem(item))
.filter((item) => item.userId);
} else {
this.creatorOptions = [];
}
const res = await this.fetcher(query);
const list = Array.isArray(res) ? res : [];
this.options = list
.map((item) => this.optionFormatter(item) || {})
.filter((item) => item.value);
// 缓存本次查询的选项,便于展示已选列表
this.options.forEach((item) => {
this.optionCache[item.value] = item;
});
} catch (error) {
this.nameList = [];
this.nameListLoading = false;
this.options = [];
} finally {
this.nameListLoading = false;
this.loading = false;
}
},
emitChange() {
const selectedOptions = this.selectedDisplay;
this.$emit("input", this.innerValue);
this.$emit("change", {
value: this.innerValue,
options: selectedOptions,
});
},
handleClear() {
this.innerValue = [];
this.options = [];
this.panelVisible = false;
this.selectedPanelItems = [];
this.emitChange();
},
handleVisibleChange(visible) {
// 如果是由点击 +N 主动触发的关闭,下拉的 visible-change(false) 需要被忽略一次,避免把自定义面板也关掉
if (!visible && this.ignoreNextVisibleChange) {
this.ignoreNextVisibleChange = false;
return;
}
// 聚焦/下拉打开时不自动展示面板
if (visible) {
// 打开时展开标签,避免点击 +N 看不到具体选项
if (this.expandOnVisible) {
this.innerCollapseTags = false;
}
// 无论关键字是否存在,都清空现有搜索结果,下次输入再重新远程拉取
this.options = [];
this.updatePanelWidth();
this.panelVisible = false;
} else {
// 关闭时恢复原始折叠配置
this.innerCollapseTags = this.collapseTags;
this.panelVisible = false;
this.selectedPanelItems = [];
}
},
// 用缓存兜底,保证已选项能在面板中展示
getOptionByValue(val) {
return (
this.options.find((item) => item.value === val) ||
this.optionCache[val] || { value: val, label: val }
);
},
handleCountClick(event) {
// 点击 +N 展开/收起已选列表
if (event && event.stopPropagation) {
event.stopPropagation();
}
if (!this.showSelectedPanel) return;
const hasSelected = this.selectedDisplay.length > 0;
if (!hasSelected) {
this.panelVisible = false;
return;
}
const nextVisible = !this.panelVisible;
if (nextVisible) {
// 准备已选面板,并忽略接下来由关闭 el-select 下拉触发的一次 visible-change(false)
this.ignoreNextVisibleChange = true;
this.prepareSelectedPanel();
this.closeSelectDropdown();
} else {
this.selectedPanelItems = [];
}
this.panelVisible = nextVisible;
},
bindCountClick() {
this.$nextTick(() => {
const selectEl = this.$refs.selectRef && this.$refs.selectRef.$el;
if (!selectEl) return;
let count = this.getCountElement(selectEl);
if (count && count !== this.countEl) {
this.unbindCountClick();
this.countEl = count;
this.countEl.style.cursor = "pointer";
this.countEl.addEventListener("click", this.handleCountClick);
}
formatCreatorItem(item = {}) {
// 同时绑定 tags 容器做事件委托,避免找不到 +N 时无法响应
const tagsContainer = selectEl.querySelector(".el-select__tags");
if (tagsContainer && tagsContainer !== this.tagsContainerEl) {
if (this.tagsContainerEl) {
this.tagsContainerEl.removeEventListener(
"click",
this.handleTagsContainerClick,
true
);
}
this.tagsContainerEl = tagsContainer;
this.tagsContainerEl.addEventListener(
"click",
this.handleTagsContainerClick,
true
);
}
});
},
unbindCountClick() {
if (this.countEl) {
this.countEl.removeEventListener("click", this.handleCountClick);
this.countEl = null;
}
if (this.tagsContainerEl) {
this.tagsContainerEl.removeEventListener(
"click",
this.handleTagsContainerClick,
true
);
this.tagsContainerEl = null;
}
},
handleTagsContainerClick(event) {
const target = event.target;
if (!target) return;
const isCountText =
target.classList && target.classList.contains("el-select__tags-text");
const isInfoTag =
target.classList && target.classList.contains("el-tag--info");
if (isCountText || isInfoTag) {
this.handleCountClick(event);
}
},
getCountElement(selectEl) {
let count = selectEl.querySelector(".el-select__tags-text");
if (!count) {
const tags = selectEl.querySelectorAll(
".el-select__tags .el-tag--info"
);
count = tags && tags.length ? tags[tags.length - 1] : null;
}
if (!count) {
count = selectEl.querySelector(".el-select__tags");
}
return count;
},
bindOutsideClick() {
if (this.outsideHandler) return;
this.outsideHandler = (e) => {
const panel = this.$refs.selectedPanel;
const selectEl = this.$refs.selectRef && this.$refs.selectRef.$el;
const target = e.target;
if (!target) return;
const insidePanel = panel && panel.contains(target);
const insideSelect = selectEl && selectEl.contains(target);
if (!insidePanel && !insideSelect) {
this.panelVisible = false;
this.selectedPanelItems = [];
}
};
document.addEventListener("mousedown", this.outsideHandler, true);
},
unbindOutsideClick() {
if (this.outsideHandler) {
document.removeEventListener("mousedown", this.outsideHandler, true);
this.outsideHandler = null;
}
},
updatePanelWidth() {
this.$nextTick(() => {
const selectEl = this.$refs.selectRef && this.$refs.selectRef.$el;
if (!selectEl) return;
const rect = selectEl.getBoundingClientRect();
const base = rect ? rect.width : 0;
const extra = Number(this.panelWidthExtra) || 0;
const minWidth = base + extra;
this.panelMinWidth = `${minWidth}px`;
});
},
prepareSelectedPanel() {
// 在展开已选列表时,冻结当前已选项,支持在面板内反复勾选/取消
this.selectedPanelItems = this.selectedDisplay.map((item) => ({
...item,
}));
},
closeSelectDropdown() {
// 关闭原生下拉,避免与已选列表面板同时展示;保留输入内容
const select = this.$refs.selectRef;
if (select && select.visible) {
if (typeof select.handleClose === "function") {
select.handleClose();
} else {
select.visible = false;
}
this.syncInputQuery(this.keyword || "");
}
},
isSelected(val) {
return (this.innerValue || []).includes(val);
},
togglePanelItem(item) {
if (!item || !item.value) return;
const current = this.innerValue || [];
const exists = this.isSelected(item.value);
this.innerValue = exists
? current.filter((v) => v !== item.value)
: [...current, item.value];
this.emitChange();
},
},
computed: {
noDataText() {
return this.keyword && this.keyword.length >= this.minQueryLen
? "无数据"
: `请至少输入${this.minQueryLen}个字`;
},
selectedDisplay() {
return (this.innerValue || [])
.map((val) => this.getOptionByValue(val))
.filter((item) => item && item.value);
},
panelStyle() {
return {
userId: item.id,
name: item.realName,
code: item.userNo,
minWidth: this.panelMinWidth || "100%",
};
},
},
watch: {},
};
</script>
<style scoped lang="scss">
::v-deep .el-upload-dragger {
width: 100% !important;
height: 100% !important;
}
.image-upload {
width: 410px;
height: 168px;
}
.image-card .el-upload--picture-card,
.image-card .el-upload-list--picture-card .el-upload-list__item {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
box-sizing: border-box;
vertical-align: top;
}
.el-upload--picture-card {
background-color: #fbfdff;
border: 1px dashed #c0ccda;
border-radius: 6px;
box-sizing: border-box;
vertical-align: top;
line-height: 100%;
}
.image-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
image-uploader .el-upload--picture-card {
.name-remote-select {
width: 100%;
}
image-uploader .el-upload:hover {
border-color: #409eff;
}
.image-uploader-icon {
font-size: 28px;
color: #8c939d;
text-align: center;
margin-top: 50px;
display: block;
}
.icon-text {
font-size: 14px;
display: block;
height: 30px;
line-height: 35px;
}
.image {
position: relative;
.mask {
opacity: 0;
position: absolute;
top: 0;
width: 100%;
background-color: rgba(0, 0, 0, 0.5);
transition: all 0.3s;
}
&:hover .mask {
opacity: 1;
}
}
.option-code {
margin-left: 4px;
color: #999;
}
.selected-panel {
position: absolute;
z-index: 2000;
width: 100%;
margin-top: 4px;
min-width: 100%;
}
/* 参照 ElementUI popper 箭头默认样式packages/theme-chalk/src/popper.scss */
.selected-panel .popper__arrow,
.selected-panel .popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.selected-panel .popper__arrow {
border-width: 6px;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
}
.selected-panel .popper__arrow::after {
content: " ";
border-width: 6px;
}
.selected-panel[x-placement^="bottom"] {
margin-top: 12px;
}
.selected-panel[x-placement^="bottom"] .popper__arrow {
top: -6px;
/* ElementUI 下拉箭头靠左偏移(非居中),与官方样式对齐 */
left: 35px;
border-top-width: 0;
border-bottom-color: #e4e7ed;
}
.selected-panel[x-placement^="bottom"] .popper__arrow::after {
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
</style>

View File

@@ -119,7 +119,35 @@
</div>
<div class="player-time">{{ currentTimeFormat }} / {{ fullTimeFormat }}</div>
</div>
<!-- ai播放器相关 -->
<div class="player-controls-bottom-right">
<div v-if="isAiTranslate" 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,12 +330,24 @@ export default {
fullTimeFormat: "00:00:00", // 视频总长度的文字
barrageTimelineStart: 0, // 弹幕时间轴的起始时间点(手动调整进度条触发更新)
isInit:false, // 是否初始化过
// ai播放器相关
isSubtitle: true, // 是否开启字幕
currentLangLabel:'', // 当前字幕语言
};
},
// ai播放器相关
computed: {
...mapGetters(['selectableLang','currentLang','courseInfo']),
isAiTranslate () {
return this.courseInfo?.aiSet == 1 && this.courseInfo?.aiTranslate == 1 && this.selectableLang && this.selectableLang.length > 0;
}
},
created() {
// ai播放器相关
this.SET_currentLang('');
},
mounted() {
console.log('---',this.isAiTranslate,this.courseInfo,'courseInfo');
this.videoDom = this.$refs.video;
this.videoDom.focus({preventScroll: true});
let speedValue=localStorage.getItem('boe_video_speed');
@@ -317,7 +358,8 @@ export default {
}
setInterval(() => {
console.log('当前状态:',this.currentProgress,this.isDrag,this.videoDom.currentTime , this.videoDom.duration)
this.SET_duration(this.videoDom.duration);
console.log('当前状态:',localStorage.getItem('videoProgressData'),this.currentProgress,this.isDrag,this.videoDom.currentTime , this.videoDom.duration)
// 视频播放时本地记录视频实时播放时长,视频设置了禁止拖动时执行
if(!this.isDrag){
var time = localStorage.getItem('videoProgressData')
@@ -372,6 +414,7 @@ export default {
//if()
//console.log(this.videoDom.readyState,'this.videoDom.readyState');
}, 1000);
// 视频dom监听器用于控制鼠标的显示
this.videoDom.addEventListener("mousemove", () => {
this.isCursorStatic = false;
@@ -411,6 +454,13 @@ export default {
// });
},
methods: {
// ai播放器相关
...mapMutations({
SET_currentLang: 'video/SET_currentLang',
SET_currentTime: 'video/SET_currentTime',
SET_selectableLang: 'video/SET_selectableLang',
SET_duration: 'video/SET_duration',
}),
//当视频由于需要缓冲下一帧而停止,解决一直计时的问题
onWaiting(){
console.log('触发了onWairing');
@@ -624,6 +674,8 @@ export default {
},
onAudioTimeUpdate() {
const currentTime = this.$refs.video.currentTime;
// ai播放器相关
this.SET_currentTime(currentTime)
this.$emit('onTimeUpdate', currentTime);
},
/**
@@ -641,9 +693,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 () {
@@ -668,9 +788,37 @@ export default {
// }
// },
src: function () {
// 当视频地址变更时,重载视频
// 当视频地址变更时,先重置字幕再重载视频
this.isPlaying = false;
// 重置字幕相关状态
this.SET_currentLang('');
this.currentLangLabel = '';
// 移除所有现有字幕轨道元素
Array.from(this.videoDom.querySelectorAll('track')).forEach(t => t.remove());
// 更彻底地清除字幕重置所有textTracks
Array.from(this.videoDom.textTracks).forEach(track => {
track.mode = 'hidden';
// 尝试移除所有cues浏览器支持的话
if (track.cues) {
while (track.cues.length > 0) {
track.cues.remove(0);
}
}
});
// 重载视频
this.videoDom.load();
this.isPlaying = false
// 如果有默认语言且支持AI翻译重新设置字幕
// if (this.isAiTranslate && this.selectableLang && this.selectableLang.length > 0) {
// // 找到默认语言或第一个可用语言
// const defaultLang = this.selectableLang.find(lang => lang.srclang === 'zh-CN') || this.selectableLang[0];
// if (defaultLang) {
// this.changeLang(defaultLang);
// }
// }
},
},
};
@@ -907,6 +1055,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; */
@@ -918,4 +1072,22 @@ export default {
height: 100px;
}
}
video::cue {
/* color: #fff; */
/* background-color: transparent; */
/* font-size: 0.85em; */
/* font-family: 'Arial', sans-serif;
-webkit-text-stroke: 4px #000;
text-stroke: 4px #000; */
/* text-shadow:
2px 2px 0 #000,
-2px 2px 0 #000,
2px -2px 0 #000,
-2px -2px 0 #000,
0 2px 0 #000,
2px 0 0 #000,
0 -2px 0 #000,
-2px 0 0 #000; */
}
</style>

View File

@@ -5,35 +5,20 @@
</el-tab-pane>
</el-tabs>
<div class="tab-search">
<el-select
<div class="member-select-wrapper">
<name-remote-select
class="member-select"
ref="memberSelect"
v-model="memberSelected"
multiple
filterable
remote
clearable
reserve-keyword
:fetcher="fetchMembers"
:option-formatter="formatMemberOption"
placeholder="姓名"
:multiple-limit="5"
:remote-method="remoteSearchMember"
:loading="memberLoading"
@input.native="limitMemberInput"
@visible-change="handleMemberVisibleChange"
@change="handleMemberChange"
@clear="handleMemberClear"
size="small"
>
<el-option
v-for="item in memberOptions"
:key="item.userId"
:label="item.name"
:value="item.userId"
>
<span>{{ item.name }}</span>
<span v-if="item.code" class="option-code">{{ item.code }}</span>
</el-option>
</el-select>
@clear="handleMemberClear"
size="small"
/>
</div>
<el-button type="primary" size="small" @click="onSearch" style="margin-left: 8px;">查询</el-button>
<el-button size="small" @click="onReset" style="margin-left: 4px;">重置</el-button>
</div>
@@ -44,12 +29,11 @@
:data="tableData"
@selection-change="onSelectionChange"
:row-key="row => row.id"
max-height="420"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="userName" label="姓名" min-width="220" show-overflow-tooltip />
<el-table-column prop="workNum" label="工号" width="120" />
<el-table-column prop="departName" label="部门" width="120" />
<el-table-column prop="userName" label="姓名" min-width="120" show-overflow-tooltip />
<el-table-column prop="workNum" label="工号" width="130" />
<el-table-column prop="departName" label="部门" width="140" />
<el-table-column prop="orgPath" label="组织路径" min-width="200" show-overflow-tooltip />
</el-table>
<div class="pagination">
@@ -73,9 +57,11 @@
<script>
import { fetchAudienceList, saveStu } from "@/api/signup/commonStudent";
import apiUserbasic from "@/api/boe/userbasic.js";
import NameRemoteSelect from '@/views/course/components/NameRemoteSelect.vue';
export default {
name: "AudienceModal",
components: { NameRemoteSelect },
props: {
visible: { type: Boolean, default: false },
pageSize: { type: Number, default: 10 },
@@ -98,8 +84,6 @@ export default {
selectedRows: [],
// 成员远程搜索多选
memberSelected: [],
memberOptions: [],
memberLoading: false,
};
},
watch: {
@@ -119,22 +103,22 @@ export default {
this.selectedRows = [];
this.keyword = "";
this.memberSelected = [];
this.memberOptions = [];
this.fetchList();
},
fetchList() {
this.loading = true;
fetchAudienceList({
// memberList 为多选成员 ID 列表
memberList: this.memberSelected || [],
memberIdList: this.memberSelected || [],
courseId: this.courseDetail?.id || '',
pageNo: this.pageNo,
pageSize: this.pageSize,
audienceIdList: this.audienceIds || [],
})
.then((res) => {
const data = res.data || res.result || res || {};
const data = res.result || res || {};
this.tableData = data.list || [];
this.total = data.total || 0;
this.total = data.count || 0;
})
.finally(() => {
this.loading = false;
@@ -147,7 +131,6 @@ export default {
onReset() {
this.keyword = "";
this.memberSelected = [];
this.memberOptions = [];
this.onSearch();
},
onPageChange(page) {
@@ -157,78 +140,28 @@ export default {
onSelectionChange(list) {
this.selectedRows = list;
},
// ===== 成员远程搜索,多选逻辑(参考 ManageListRemote.vue 的创建人筛选) =====
async remoteSearchMember(keyword) {
const limited = (keyword || "").slice(0, 50);
if (this.$refs.memberSelect && this.$refs.memberSelect.query !== limited) {
this.$refs.memberSelect.query = limited;
this.$nextTick(() => {
if (
this.$refs.memberSelect &&
this.$refs.memberSelect.$refs &&
this.$refs.memberSelect.$refs.input
) {
this.$refs.memberSelect.$refs.input.value = limited;
}
});
}
const query = limited.trim();
if (!query || query.length <= 1) {
this.memberOptions = [];
return;
}
this.memberLoading = true;
try {
const res = await apiUserbasic.selectUser(query);
if (res && res.status === 200) {
const resultList = res.result || [];
this.memberOptions = resultList
.map((item) => this.formatMemberItem(item))
.filter((item) => item.userId);
} else {
this.memberOptions = [];
}
} catch (error) {
this.memberOptions = [];
} finally {
this.memberLoading = false;
async fetchMembers(keyword) {
const query = (keyword || "").trim();
if (!query || query.length <= 1) return [];
const res = await apiUserbasic.selectUser(query);
if (res && res.status === 200) {
return res.result || [];
}
return [];
},
formatMemberItem(item = {}) {
formatMemberOption(item = {}) {
return {
userId: item.id,
name: item.realName,
value: item.id,
label: item.realName,
code: item.userNo,
};
},
handleMemberChange(value = []) {
handleMemberChange({ value = [] } = {}) {
// 限制最多 5 个,保持与 ManageListRemote 中创建人筛选一致
this.memberSelected = (value || []).slice(0, 5);
},
handleMemberClear() {
this.memberSelected = [];
this.memberOptions = [];
},
handleMemberVisibleChange(visible) {
if (!visible) return;
const select = this.$refs.memberSelect;
const query = (select && select.query) || "";
if (!query) {
this.memberOptions = [];
}
},
limitMemberInput(event) {
const limited =
(event && event.target && event.target.value
? event.target.value
: ""
).slice(0, 50);
if (event && event.target && event.target.value !== limited) {
event.target.value = limited;
}
if (this.$refs.memberSelect) {
this.$refs.memberSelect.query = limited;
}
},
handleClose() {
this.visibleSync = false;
@@ -236,7 +169,7 @@ export default {
handleConfirm() {
const targetId = this.courseDetail?.id;
if(!this.selectedRows.length) {
this.$showMessage('请添加学员', 'error');
this.$showManageMessage('请添加学员', 'error');
return
}
saveStu({
@@ -244,9 +177,9 @@ export default {
type: 13,
deptIds: [],
groupIds: [],
studentList: this.selectedRows.map((e) => ({ id: e.userId })),
studentList: this.selectedRows.map((e) => ({ id: e.userId, realName: e.userName })),
}).then(() => {
this.$showMessage("添加成功", 'success');
this.$showManageMessage("添加成功", 'success');
this.$emit("confirm", this.selectedRows);
this.handleClose();
});
@@ -271,6 +204,9 @@ export default {
margin-right: 12px;
}
}
.member-select-wrapper {
width: 240px;
}
::v-deep .el-dialog__body {
padding: 0 20px 30px 20px;
}

View File

@@ -32,7 +32,8 @@
<div class="tab-search">
<span class="label">姓名</span>
<el-input v-model="nameSearch.keyword" placeholder="请输入姓名" size="small" clearable class="input" />
<el-button type="primary" size="small" @click="onSearchStu">
<!-- 点击搜索时认为是一次新的搜索页码应重置为第 1 -->
<el-button type="primary" size="small" @click="onSearchStu(true)">
搜索
</el-button>
<el-button size="small" @click="resetStu">重置</el-button>
@@ -92,7 +93,8 @@
<div class="tab-search">
<span class="label">受众名称:</span>
<el-input v-model="audienceName.keyword" placeholder="请输入受众名称" size="small" clearable class="input" />
<el-button type="primary" size="small" @click="searchAudience">
<!-- 搜索受众时,视为新查询,页码需归 1 -->
<el-button type="primary" size="small" @click="searchAudience(true)">
搜索
</el-button>
<el-button size="small" @click="resetAudienceInfo">重置</el-button>
@@ -412,7 +414,15 @@ export default {
this.projectParams.studentName = "";
this.getProjectStu();
},
onSearchStu() {
/**
* 获取“快速选人”列表
* @param {Boolean} resetPage 是否重置到第 1 页(新的搜索条件或重置时需要)
*/
onSearchStu(resetPage = false) {
// 当关键字、组织等搜索条件变化或点击“搜索”按钮时,应将页码重置为 1
if (resetPage) {
this.stuTable.pageNo = 1;
}
fetchQuickStudents({
...this.nameSearch,
pageNo: this.stuTable.pageNo,
@@ -451,6 +461,8 @@ export default {
},
resetStu() {
this.nameSearch = { keyword: "", departId: "" };
// 重置时也应从第 1 页重新加载
this.stuTable.pageNo = 1;
this.onSearchStu();
},
fetchOrgTree() {
@@ -501,7 +513,14 @@ export default {
}
this.selectedOrgKeys = this.deptList.map((d) => d.id);
},
searchAudience() {
/**
* 搜索受众列表
* @param {Boolean} resetPage 是否重置到第 1 页(新搜索或重置时需要)
*/
searchAudience(resetPage = false) {
if (resetPage) {
this.audienceTable.pageNo = 1;
}
fetchUserAudiences({
...this.audienceName,
pageNo: this.audienceTable.pageNo,
@@ -568,6 +587,8 @@ export default {
},
resetAudienceInfo() {
this.audienceName.keyword = "";
// 重置后从第 1 页重新拉取
this.audienceTable.pageNo = 1;
this.searchAudience();
},
onOrgSelectChange() { },
@@ -615,17 +636,17 @@ export default {
this.groupMemberNumber &&
this.groupMemberCount < this.groupMemberNumber + this.projectSelectRows.length + this.stuSelectRows.length
) {
return this.$showMessage("添加小组学员超过最大值", 'error');
return this.$showManageMessage("添加小组学员超过最大值", 'error');
}
saveStu({
targetId: this.courseDetail?.id || this.id,
type: 13,
deptIds: this.deptList.map((e) => e.id),
groupIds: this.auditSelectRows.map((e) => e.id),
studentList: this.stuSelectRows.map((e) => ({id: e.id})),
studentList: this.stuSelectRows.map((e) => ({id: e.id, realName: e.realName})),
}).then((res) => {
console.log('res', res);
this.$showMessage("添加成功", 'success');
this.$showManageMessage("添加成功", 'success');
this.$emit("confirm");
this.handleClose();
});
@@ -688,9 +709,6 @@ export default {
overflow: auto;
}
.table-area {
}
.pager {
margin-top: 10px;
text-align: right;

View File

@@ -122,6 +122,8 @@ export const iframes=[
{title:'课程管理', path:'/iframe/course/manages',hidden:false,component:'course/ManageList'},
{title:'课程管理新版', path:'/iframe/course/manage-remote',hidden:false,component:'course/ManageListRemote'},
{title:'课程管理新版', path:'/iframe/course/coursemanage-remote',hidden:false,component:'course/CourseManageRemote'},
{title:'ai摘要', path:'/iframe/course/aiAbstract',hidden:false,component:'course/aiSet/aiAbstract'},
{title:'ai翻译', path:'/iframe/course/aiTranslate',hidden:false,component:'course/aiSet/aiTranslate'},
{title:'考试试题管理', path:'/iframe/exam/questions',hidden:false,component:'exam/Question'},
{title:'查看答卷', path:'/iframe/exam/viewanswer',hidden:false,component:'exam/viewAnswer'},
{title:'考试试卷管理', path:'/iframe/exam/papers',hidden:false,component:'exam/TestPaper'},

View File

@@ -59,7 +59,7 @@ import 'swiper/dist/css/swiper.css';
Vue.use(VueAwesomeSwiper)
import watermark from './utils/warterMark.js'
import Bus from './utils/bus.js'
import {showMessage} from './utils/index.js'
import {showMessage, showManageMessage} from './utils/index.js'
import MessageBoxService from './utils/simpleMessageBox.js'
@@ -68,6 +68,8 @@ Vue.use(MessageBoxService)
Vue.prototype.$showMessage = showMessage
Vue.prototype.$showManageMessage = showManageMessage
Vue.prototype.$bus = Bus
Vue.prototype.$watermark = watermark

View File

@@ -29,6 +29,19 @@ router.beforeEach((to, from, next) => {
// 在免登录白名单,直接进入
next()
}else{
// if (!store.getters.init) {
// store.commit('app/SET_INITDATA',true);
// let myRouters=routers();
// store.dispatch('GenerateRoutes',{routers:myRouters}).then(accessRoutes=>{
// console.log('accessRoutes::',accessRoutes)
// router.addRoutes(accessRoutes) // 动态添加可访问路由表
// next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
// });
// } else {
// to.meta.keepAlive = true
// next();
// }
// return;
if(getToken()){
if(to.path === '/login'){
// 如果是外部用户,把配置的路由跳转到个人中心

View File

@@ -28,5 +28,12 @@ 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,
courseInfo:state => state.video.courseInfo,
duration:state => state.video.duration,
}
export default getters

View File

@@ -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
})

171
src/store/modules/video.js Normal file
View File

@@ -0,0 +1,171 @@
// ai播放器相关
/**
*
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: 'हिन्दी',
}
], // 全部语言列表
*/
const state = {
selectAllLang: [
{
key: 'ZH_CN',
srclang: 'zh-CN',
label: '中文',
name: '中文',
},
{
key: 'EN_US',
srclang: 'en-US',
label: '英语',
name: 'English',
},
{
key: 'VI_VN',
srclang: 'vi-VN',
label: '越南语',
name: 'tiếng Việt',
},
{
key: 'ES_ES',
srclang: 'es-ES',
label: '西班牙语',
name: 'español',
},
], // 一期语言列表
selectableLang: [], // 可选语言列表+字幕信息
currentLang: '', // 当前选中语言
currentTime: -1, // 当前视频时间
courseInfo: {},
duration: 0, // 视频时长
}
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
},
SET_courseInfo: (state, info) => {
state.courseInfo = info
},
SET_duration: (state, duration) => {
state.duration = duration
},
}
const actions = {
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@@ -462,6 +462,17 @@ export function showMessage(message, status) {
message: message,
type: status,
duration: 5000,
customClass: 'new-message'
customClass: 'new-message',
offset: 270
});
}
export function showManageMessage(message, status) {
this.$message({
message: message,
type: status,
duration: 5000,
customClass: 'new-message',
offset: 80
});
}

View File

@@ -121,7 +121,7 @@ export function toContentType(fileType) {
// }
export function courseType(type) {
const maps = {
10: '录播课',
10: '课',
21: '在线课(直播)',
20: '录播课',
30: '线下课',

View File

@@ -2,24 +2,36 @@
<div id="courseManage">
<!--课程管理-->
<div class="course-info">
<div class="course-desc-item"><span class="title">课程名称:</span><span class="desc">{{courseDetail.name}}</span>
<div class="course-info-left">
<div class="course-top">
<p class="course-name">{{courseDetail.name}}</p>
<p class="course-type">{{courseType(courseDetail.type)}}</p>
</div>
<p class="course-sys-type">
{{sysTypeName(courseDetail.sysType1)}}{{courseDetail.sysType2 == ''? '': '/'}}{{sysTypeName(courseDetail.sysType2)}}{{courseDetail.sysType3 == ''? '': '/'}}{{sysTypeName(courseDetail.sysType3)}}
</p>
</div>
<div class="course-info-right" >
<el-button type="text" @click="handleBack">返回</el-button>
</div>
<!-- <div class="course-desc-item"><span class="title">课程名称:</span><span class="desc">{{courseDetail.name}}</span>
</div>
<div class="course-desc-item"><span class="title">类型:</span><span
class="desc">{{courseType(courseDetail.type)}}</span></div>
<div class="course-desc-item"><span class="title">课程分类:</span><span
class="desc">{{sysTypeName(courseDetail.sysType1)}}{{courseDetail.sysType2 == ''? '': '/'}}{{sysTypeName(courseDetail.sysType2)}}{{courseDetail.sysType3 == ''? '': '/'}}{{sysTypeName(courseDetail.sysType3)}}</span>
</div>
</div> -->
</div>
<!-- <div style="color: red;">下面的表格标题及内容需要调整完善</div> -->
<div>
<el-tabs style="width: 100%;" v-model="tabName" @tab-click="handleTabClick">
<el-tab-pane label="报名记录" name="second">
<el-tabs style="width: 100%;" type="border-card" v-model="tabName" >
<el-tab-pane label="报名记录" name="second">
<el-row style="margin: 20px 0;" :gutter="20">
<el-col :span="4">
<el-col :span="6">
<div class="grid-content bg-purple">
<!-- <el-input v-model="signup.name" clearable placeholder="姓名" maxlength="50" /> -->
<NameFilterSelect @handleNameChange="aids => signup.aid = aids" ref="signupNameFilter"
@handleClose="signup.aid = []" />
<NameFilterSelect @change="aids => signup.aid = aids.value" placeholder="姓名" ref="signupNameFilter" :fetcher="fetchNameList"
:option-formatter="formatOption" />
<!-- <el-select :key="2" style="width:100%" clearable multiple v-model="signup.aid" filterable
placeholder="姓名" v-limit-input="50" remote reserve-keyword :remote-method="initNameList"
:multiple-limit="5" :loading="nameListLoading">
@@ -40,20 +52,15 @@
</el-col>
<el-col :span="6">
<div class="grid-content bg-purple">
<el-button type="primary" @click="getSignupList()"> </el-button>
<el-button type="primary" @click="getSignupList(true)"> </el-button>
<el-button @click="resetSignupList()"> </el-button>
</div>
</el-col>
<el-col :span="6" :offset="4">
<el-col :span="6" :offset="2" style="padding-right:0 !important">
<div class="grid-content bg-purple" style="text-align: right;">
<el-button
v-if="showSignupActions"
type="primary"
icon="el-icon-plus"
style="margin-right: 10px;"
@click="handleAddSignupClick"
>
<el-button v-if="showSignupActions" type="primary" icon="el-icon-plus" style="margin-right: 10px;"
@click="handleAddSignupClick">
添加报名
</el-button>
<el-button type="primary" icon="el-icon-upload2" @click="handleExportSignup">
@@ -62,12 +69,13 @@
</div>
</el-col>
</el-row>
<div class="tab-content">
<div class="tab-content" v-if="tabName == 'second'">
<el-table border max-height="350" :data="study.list" :header-cell-style="{textAlign: 'center'}"
:cell-style="{ textAlign: 'center' }" style="width: 100%">
<!-- <el-table-column type="selection" width="55"></el-table-column> -->
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="code" label="工号">
<el-table-column prop="code" width="200" label="工号">
</el-table-column>
<el-table-column prop="orgInfo" label="部门">
<template slot-scope="scope">
@@ -83,40 +91,32 @@
</template>
</el-table-column>
<el-table-column prop="signTime" label="报名时间"></el-table-column>
<el-table-column
v-if="showSignupActions"
label="操作"
width="140"
>
<el-table-column v-if="showSignupActions" label="操作" width="140">
<template slot-scope="scope">
<el-button
type="text"
size="mini"
class="delete-action-link--danger"
@click="handleDeleteSignup(scope.row)"
>
<el-button type="text" size="mini" class="delete-action-link--danger"
@click="handleDeleteSignup(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div style="padding: 10px;">
<div style="text-align:center; padding: 10px;">
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="study.pageIndex" :page-sizes="[10, 20, 30, 40]" :page-size="study.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="study.count"></el-pagination>
</div>
<div class="pagination">
<!-- <div style="text-align:center; padding: 10px;"> -->
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange"
:current-page="study.pageIndex" :page-sizes="[10, 20, 30, 40]" :page-size="study.pageSize"
layout="total, sizes, prev, pager, next, jumper" :total="study.count"></el-pagination>
</div>
<!-- </div> -->
</div>
</el-tab-pane>
<el-tab-pane lazy label="学习记录" name="third">
<el-tab-pane label="学习记录" name="third">
<el-row style="margin: 20px 0;" :gutter="20">
<el-col :span="4">
<el-col :span="5">
<div class="grid-content bg-purple">
<!-- <el-input clearable v-model="learningRecords.name" maxlength="50"
placeholder="姓名"></el-input> -->
<NameFilterSelect @handleNameChange="aids => learningRecords.aid = aids" ref="learningRecordsNameFilter"
@handleClose="learningRecords.aid = []" />
<NameFilterSelect placeholder="姓名" @change="aids => learningRecords.aid = aids.value" ref="learningRecordsNameFilter"
:fetcher="fetchNameList" :option-formatter="formatOption" />
<!-- <el-select :key="1" style="width:100%" clearable multiple v-model="learningRecords.aid" filterable
placeholder="姓名" v-limit-input="50" reserve-keyword remote :remote-method="initNameList"
:multiple-limit="5" :loading="nameListLoading">
@@ -128,7 +128,7 @@
</div>
</el-col>
<el-col :span="4">
<el-col :span="3">
<div class="grid-content bg-purple">
<el-select style="width: 100%;" v-model="learningRecords.status" clearable placeholder="学习状态">
<el-option label="未开始" :value="1"></el-option>
@@ -149,22 +149,22 @@
</el-col>
<el-col :span="5">
<div class="grid-content bg-purple">
<el-button type="primary" @click="getStudyRecords()"> </el-button>
<el-button type="primary" @click="getStudyRecords(true)"> </el-button>
<el-button @click="resetStudyRecords()"> </el-button>
</div>
</el-col>
<el-col style="float: right; width:185px">
<div class="grid-content bg-purple" style="text-align: right;">
<el-button type="primary" icon="el-icon-upload2" @click="handleExportStudyDetail">导出学习课程记录</el-button>
<el-button type="primary" icon="el-icon-upload2" @click="handleExportStudyDetail">导出课程学习记录</el-button>
</div>
</el-col>
</el-row>
<div class="tab-content">
<div class="tab-content" v-if="tabName == 'third'">
<el-table max-height="350" border :data="learningRecords.list" :header-cell-style="{textAlign: 'center'}"
:cell-style="{ textAlign: 'center' }" style="width: 100%">
<el-table-column prop="aname" label="姓名"></el-table-column>
<el-table-column prop="code" label="工号"></el-table-column>
<el-table-column prop="orgInfo" label="部门">
<el-table-column prop="aname" width="100" label="姓名"></el-table-column>
<el-table-column prop="code" width="100" label="工号"></el-table-column>
<el-table-column prop="orgInfo" label="部门" width="200">
<template slot-scope="scope">
<el-tooltip class="item" effect="dark" :content="scope.row.orgInfo" placement="top-start">
<p class="no-wrap">
@@ -172,7 +172,7 @@
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="addTime" width="300" label="学习时间"></el-table-column>
<el-table-column prop="addTime" width="200" label="学习时间"></el-table-column>
<el-table-column prop="totalDuration" label="学习时长">
<template slot-scope="scope">
{{ scope.row.totalDuration == 0? '0': (scope.row.totalDuration/60).toFixed(2)}}
@@ -188,7 +188,7 @@
{{ scope.row.progress }}%
</template>
</el-table-column>
<el-table-column prop="orgInfo" label="操作" width="120">
<el-table-column label="操作" width="120">
<template slot-scope="scope">
<!--弹出每一项资源的学习情况列表-->
<el-link :underline="false" type="primary" @click.stop="showStudyDetails(scope.row)">查看</el-link>
@@ -196,7 +196,7 @@
</el-table-column>
</el-table>
<div style="text-align: center; padding: 10px;">
<div class="pagination">
<el-pagination @size-change="handleSizeChangeRecords" @current-change="handleCurrentChangeRecords"
:current-page="learningRecords.pageIndex" :page-sizes="[10, 20, 30, 40]"
:page-size="learningRecords.pageSize" layout="total, sizes, prev, pager, next, jumper"
@@ -205,28 +205,28 @@
</div>
</div>
</el-tab-pane>
<el-tab-pane lazy label="资源学习情况" name="first">
<el-tab-pane label="资源学习情况" name="first">
<el-row style="margin: 20px 0;" :gutter="20">
<el-col :span="4">
<el-col :span="6">
<div class="grid-content bg-purple"><el-input clearable v-model="recourseListQuery.contentName"
placeholder="资源名称"></el-input></div>
</el-col>
<el-col :span="5">
<div class="grid-content bg-purple">
<el-button type="primary" @click="getResourseList"> </el-button>
<el-button type="primary" @click="getResourseList(true)"> </el-button>
<el-button @click="resetResourseList"> </el-button>
</div>
</el-col>
<el-col :span="6" :offset="9">
<el-col :span="6" :offset="7" style="padding-right:0 !important">
<div class="grid-content bg-purple" style="text-align: right;">
<el-button type="primary" icon="el-icon-upload2" @click="handleExportStudyResource">导出资源学习记录</el-button>
</div>
</el-col>
</el-row>
<div class="tab-content">
<div class="tab-content" v-if="tabName == 'first'">
<el-table max-height="350" border :header-cell-style="{textAlign: 'center'}"
:cell-style="{ textAlign: 'center' }" :data="recourseList" style="width: 100%">
<el-table-column label="资源名称">
<el-table-column label="资源名称" prop="contentName">
<template slot-scope="scope">
{{scope.row.contentName}}
</template>
@@ -241,7 +241,7 @@
</el-table-column>
</el-table>
<div style="text-align: center; padding: 10px;">
<div class="pagination">
<el-pagination @size-change="handleSizeRecourseList" @current-change="handleCurrentChangeRecourseList"
:current-page="recourseListQuery.pageIndex" :page-sizes="[10, 20, 30, 40]"
:page-size="recourseListQuery.pageSize" layout="total, sizes, prev, pager, next, jumper"
@@ -253,19 +253,13 @@
</el-tab-pane>
</el-tabs>
</div>
<AudienceModal
v-if="showSignupActions && hasCourseCrowds"
:visible.sync="audienceDialogVisible"
@confirm="handleAudienceConfirm"
:audience-ids="courseCrowds.map(item => item.id)"
/>
<SignupModal
v-if="showSignupActions && !hasCourseCrowds"
:visible.sync="addSignupVisible"
@confirm="handleSignupCreate"
/>
<AudienceModal v-if="showSignupActions && hasCourseCrowds" :visible.sync="audienceDialogVisible"
@confirm="handleAudienceConfirm" :audience-ids="courseCrowds.map(item => item.groupId)" />
<SignupModal v-if="showSignupActions && !hasCourseCrowds" :visible.sync="addSignupVisible"
@confirm="handleSignupCreate" />
<!-- 学习详情 -->
<el-dialog title="学习详情" :visible.sync="study.detailShow" width="900px" :append-to-body="true">
<el-dialog title="学习详情" :visible.sync="study.detailShow" class="common-course-dialog" width="900px"
:append-to-body="true">
<div>
<!-- <div v-if="study.detailType == 10"><auditCourse1 :isDetails="true" :isShow="false" :id="study.examineId"></auditCourse1></div>
<div v-if="study.detailType == 20"><auditCourse2 :isDetails="true" :isShow="false" :id="study.examineId"></auditCourse2></div> -->
@@ -295,7 +289,7 @@
</el-table-column>
</el-table>
<div style="text-align: center;padding: 10px;" v-if="showStudyDetailPage">
<div class="pagination" v-if="showStudyDetailPage">
<el-pagination @size-change="handleSizeChangeStudyDetail" @current-change="handleCurrentStudyDetail"
:current-page="studyDetailQuery.pageIndex" :page-sizes="[10, 20, 30, 40]"
:page-size="studyDetailQuery.pageSize" layout="total, sizes, prev, pager, next, jumper"
@@ -308,14 +302,18 @@
</span>
</el-dialog>
<el-dialog title="学习详情" v-if="commonResourceStudyPeopleShow" :visible.sync="commonResourceStudyPeopleShow"
width="50%" :append-to-body="true">
<el-dialog class="common-course-dialog" title="学习详情" v-if="commonResourceStudyPeopleShow"
:visible.sync="commonResourceStudyPeopleShow" width="65%" :append-to-body="true">
<el-row style="margin: 20px 0 20px -10px;" :gutter="20">
<el-col :span="6">
<div class="grid-content bg-purple">
<NameFilterSelect ref="commonResourceStudyPeopleNameFilter"
@handleNameChange="aids => commonResourceStudyPeopleQuery.aid = aids"
@handleClose="commonResourceStudyPeopleQuery.aid = []" />
<NameFilterSelect ref="commonResourceStudyPeopleNameFilter" placeholder="姓名"
@change="aids => commonResourceStudyPeopleQuery.aid = aids.value" :option-formatter="formatOption"
:fetcher="fetchNameList"
/>
<!-- <el-select :key="3" style="width:100%" clearable multiple v-model="commonResourceStudyPeopleQuery.aid"
filterable placeholder="姓名" v-limit-input="50" remote reserve-keyword :remote-method="initNameList"
@@ -348,7 +346,7 @@
</el-col>
<el-col :span="8">
<div class="grid-content bg-purple">
<el-button type="primary" @click="queryResourceStudyPeopleList">查 询</el-button>
<el-button type="primary" @click="queryCommonResource(true)">查 询</el-button>
<el-button @click="resetCommonResourceQuery">重 置</el-button>
</div>
</el-col>
@@ -356,8 +354,8 @@
<el-table max-height="500" border :data="commonResourceStudyPeopleList" style="width: 100%"
:header-cell-style="{textAlign: 'center'}" :cell-style="{ textAlign: 'center' }">
<el-table-column prop="aname" label="姓名"></el-table-column>
<el-table-column prop="code" label="工号"></el-table-column>
<el-table-column prop="orgInfo" label="部门">
<el-table-column prop="code" label="工号" width="150"></el-table-column>
<el-table-column prop="orgInfo" label="部门" width="220">
<template slot-scope="scope">
<el-tooltip class="item" effect="dark" :content="scope.row.orgInfo" placement="top-start">
<p class="no-wrap">
@@ -381,7 +379,7 @@
label="满意度分数"></el-table-column>
<el-table-column prop="finishTime" width="200" label="完成时间"></el-table-column>
</el-table>
<div style="text-align: center;padding: 10px;">
<div class="pagination">
<el-pagination @size-change="handleSizeChangeStudyPeople" @current-change="handleCurrentChangeStudyPeople"
:current-page="commonResourceStudyPeopleQuery.pageIndex" :page-sizes="[10, 20, 30, 40]"
:page-size="commonResourceStudyPeopleQuery.pageSize" layout="total, sizes, prev, pager, next, jumper"
@@ -393,14 +391,14 @@
</span>
</el-dialog>
<el-dialog title="学习详情" v-if="examResourceStudyPeopleShow" :visible.sync="examResourceStudyPeopleShow" width="50%"
:append-to-body="true">
<el-dialog title="学习详情" class="common-course-dialog" v-if="examResourceStudyPeopleShow"
:visible.sync="examResourceStudyPeopleShow" width="65%" :append-to-body="true">
<el-row style="margin: 20px 0 20px -10px;" :gutter="20">
<el-col :span="6">
<div class="grid-content bg-purple">
<NameFilterSelect ref="examResourceStudyPeopleNameFilter"
@handleNameChange="aids => examResourceStudyPeopleQuery.aid = aids"
@handleClose="examResourceStudyPeopleQuery.aid = []" />
<NameFilterSelect ref="examResourceStudyPeopleNameFilter" placeholder="姓名"
@change="aids => examResourceStudyPeopleQuery.aid = aids.value"
:option-formatter="formatOption" :fetcher="fetchNameList" />
<!--
<el-select :key="4" style="width:100%" clearable multiple v-model="examResourceStudyPeopleQuery.aid"
filterable placeholder="姓名" v-limit-input="50" remote reserve-keyword :remote-method="initNameList"
@@ -424,7 +422,7 @@
</el-col>
<el-col :span="8">
<div class="grid-content bg-purple">
<el-button type="primary" @click="queryExamStudyPeopleList">查 询</el-button>
<el-button type="primary" @click="queryExamStudyPeopleList(true)">查 询</el-button>
<el-button @click="resetExamCommonResourceQuery">重 置</el-button>
</div>
</el-col>
@@ -446,8 +444,8 @@
</template>
</el-table-column>
<el-table-column prop="aname" label="姓名"></el-table-column>
<el-table-column prop="code" label="工号"></el-table-column>
<el-table-column prop="orgInfo" label="部门">
<el-table-column prop="code" label="工号" width="150"></el-table-column>
<el-table-column prop="orgInfo" label="部门" width="220">
<template slot-scope="scope">
<el-tooltip class="item" effect="dark" :content="scope.row.orgInfo" placement="top-start">
<p class="no-wrap">
@@ -463,7 +461,7 @@
<el-table-column prop="score" label="成绩"></el-table-column>
<el-table-column prop="finishTime" width="200" label="完成时间"></el-table-column>
</el-table>
<div style="text-align: center;padding: 10px;">
<div class="pagination">
<el-pagination @size-change="handleSizeChangeExamStudyPeople"
@current-change="handleCurrentChangeExamStudyPeople" :current-page="examResourceStudyPeopleQuery.pageIndex"
:page-sizes="[10, 20, 30, 40]" :page-size="examResourceStudyPeopleQuery.pageSize"
@@ -496,8 +494,6 @@ import axios from "axios";
import NameFilterSelect from "@/components/NameFilterSelect/index.vue";
import SignupModal from "@/components/signup/SignupModal.vue";
import AudienceModal from "@/components/signup/AudienceModal.vue";
NameFilterSelect;
export default {
components: { NameFilterSelect, SignupModal, AudienceModal },
props: {
@@ -680,6 +676,11 @@ export default {
mounted() {
this.getCourseDetailCrowds();
this.getSignupList();
this.getStudyRecords();
this.recourseListQuery.courseId = this.courseDetail.id;
this.getResourseList();
this.getResOwnerTree().then((rs) => {
this.resOwnerListMap = rs;
});
@@ -695,13 +696,36 @@ export default {
loadSysTypes: "sysType/loadSysTypes",
}),
async fetchNameList(query) {
console.log('in fetchNameList')
const res = await apiUserbasic.selectUser(query);
if (res && res.status === 200) {
return res.result || [];
}
return [];
},
formatOption(item = {}) {
return {
value: item.id,
label: item.realName,
code: item.userNo,
};
},
queryCommonResource(flag) {
if (this.rousourceRow.contentType == "62") {
this.queryAssessStudyPeopleList(flag);
} else {
this.queryResourceStudyPeopleList(flag);
}
},
// 查询课程详情,获取 crowds 信息
getCourseDetailCrowds() {
if (!this.courseDetail || !this.courseDetail.id) return;
apiCourse
.detail(this.courseDetail.id)
.then((res) => {
console.log('res1', res);
console.log("res1", res);
const result = res.result || {};
this.courseCrowds = Array.isArray(result.crowds) ? result.crowds : [];
})
@@ -722,30 +746,38 @@ export default {
this.getSignupList();
},
handleDeleteSignup(row) {
this.$confirm(`<i class="el-icon-warning-outline"></i>确定删除${row.name || ''}的报名记录吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true,
type: 'warning',
customClass: 'custom-confirm-dialog'
}).then(() => {
apicourseStudy.deleteNewSignUp({
id: row.id,
courseId: this.courseDetail.id,
studentId: row.aid
this.$confirm(
`<i class="el-icon-warning-outline"></i>确定删除${
row.name || ""
}的报名记录吗?`,
"删除确认",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
dangerouslyUseHTMLString: true,
type: "warning",
customClass: "custom-confirm-dialog",
}
)
.then(() => {
apicourseStudy
.deleteNewSignUp({
id: row.id,
courseId: this.courseDetail.id,
studentId: row.aid,
})
.then((res) => {
if (res && res.status === 200) {
this.$showManageMessage("删除成功", "success");
this.getSignupList();
} else if (res) {
this.$showManageMessage(res.message || "删除失败", "error");
}
})
.catch((err) => {
this.$showManageMessage("删除失败", "error");
});
})
.then((res) => {
if (res && res.status === 200) {
this.$showMessage("删除成功", 'success');
this.getSignupList();
} else if (res) {
this.$showMessage(res.message || "删除失败", 'error');
}
})
.catch((err) => {
this.$showMessage("删除失败", 'error');
});
})
.catch((err) => {
// this.$showMessage('已取消删除', 'info');
});
@@ -761,7 +793,11 @@ export default {
status: "",
aid: [],
};
this.queryResourceStudyPeopleList();
if (this.rousourceRow.contentType == "62") {
this.queryAssessStudyPeopleList();
} else {
this.queryResourceStudyPeopleList();
}
},
resetExamCommonResourceQuery() {
@@ -778,8 +814,6 @@ export default {
},
handleShowResourdeDetailList(row) {
console.log(row);
console.log(7777);
this.commonResourceStudyPeopleQuery = {
pageIndex: 1, //第几页
pageSize: 10, // 每页多少条
@@ -804,7 +838,10 @@ export default {
}
},
queryExamStudyPeopleList() {
queryExamStudyPeopleList(resetPage) {
if (resetPage) {
this.examResourceStudyPeopleQuery.pageIndex = 1;
}
apicourseStudy
.contentsExam({
courseId: this.courseDetail.id,
@@ -831,7 +868,11 @@ export default {
});
},
queryAssessStudyPeopleList() {
queryAssessStudyPeopleList(resetPage) {
if (resetPage) {
this.commonResourceStudyPeopleQuery.pageIndex = 1;
}
apicourseStudy
.contentsAssess({
courseId: this.courseDetail.id,
@@ -858,9 +899,10 @@ export default {
});
},
queryResourceStudyPeopleList() {
console.log(this.rousourceRow);
console.log(888);
queryResourceStudyPeopleList(resetPage) {
if (resetPage) {
this.commonResourceStudyPeopleQuery.pageIndex = 1;
}
apicourseStudy
.studyContentRecords({
courseId: this.courseDetail.id,
@@ -938,6 +980,7 @@ export default {
apicourseStudy
.studyExport({
courseId: this.courseDetail.id,
contentName: this.recourseListQuery.contentName,
})
.then((res) => {
this.handleExport(
@@ -950,10 +993,12 @@ export default {
apicourseStudy
.studyExport({
courseId: this.courseDetail.id,
aname: this.learningRecords.name,
aid: this.learningRecords.aid.join(","),
status: this.learningRecords.status,
queryStartTime: this.learningRecords.queryStartTime,
queryFinishTime: this.learningRecords.queryFinishTime,
queryStartTime:
this.studyDateTime.length > 0 ? this.studyDateTime[0] : "",
queryFinishTime:
this.studyDateTime.length > 1 ? this.studyDateTime[1] : "",
})
.then((res) => {
this.handleExport(res, this.courseDetail.name + "的学习记录.xlsx");
@@ -963,7 +1008,7 @@ export default {
apicourseStudy
.exportSignup({
courseId: this.courseDetail.id,
name: this.signup.name,
aid: this.signup.aid.join(","),
signType: this.signup.signType,
})
.then((res) => {
@@ -972,6 +1017,8 @@ export default {
},
resetResourseList() {
this.recourseListQuery.contentName = "";
this.recourseListQuery.pageIndex = 1;
this.recourseListQuery.pageSize = 10;
this.getResourseList();
},
resetStudyRecords() {
@@ -982,6 +1029,8 @@ export default {
this.studyDateTime = [];
this.learningRecords.queryStartTime = "";
this.learningRecords.queryFinishTime = "";
this.learningRecords.pageIndex = 1;
this.learningRecords.pageSize = 10;
this.getStudyRecords();
},
@@ -992,6 +1041,8 @@ export default {
signType: "",
aid: [],
};
this.study.pageIndex = 1;
this.study.pageSize = 10;
this.getSignupList();
},
resOwnerName(code) {
@@ -1006,7 +1057,10 @@ export default {
}
return this.sysTypeMap.get(code);
},
getResourseList() {
getResourseList(resetPage) {
if (resetPage) {
this.recourseListQuery.pageIndex = 1;
}
apiCoursePortal.pageListResource(this.recourseListQuery).then((rs) => {
if (rs.status == 200) {
this.recourseList = rs.result.list;
@@ -1101,12 +1155,21 @@ export default {
handleSizeChangeStudyPeople(val) {
this.commonResourceStudyPeopleQuery.pageSize = val;
this.commonResourceStudyPeopleQuery.pageIndex = 1;
this.queryResourceStudyPeopleList();
if (this.rousourceRow.contentType == "62") {
this.queryAssessStudyPeopleList();
} else {
this.queryResourceStudyPeopleList();
}
},
handleCurrentChangeStudyPeople(val) {
this.commonResourceStudyPeopleQuery.pageIndex = val;
//console.log('learningSituation.pageIndex',this.learningSituation.pageIndex);
this.queryResourceStudyPeopleList();
if (this.rousourceRow.contentType == "62") {
this.queryAssessStudyPeopleList();
} else {
this.queryResourceStudyPeopleList();
}
},
handleSizeChangeSituation(val) {
this.learningSituation.pageSize = val;
@@ -1125,12 +1188,15 @@ export default {
this.getStudyDetail();
},
handleCurrentStudyDetail(val) {
this.learningSituation.pageIndex = val;
this.studyDetailQuery.pageIndex = val;
//console.log('learningSituation.pageIndex',this.learningSituation.pageIndex);
this.getStudyDetail();
},
// 学习记录
getStudyRecords() {
getStudyRecords(resetPage) {
if (resetPage) {
this.learningRecords.pageIndex = 1;
}
let params = {
courseId: this.courseDetail.id, //课程的id
status: this.learningRecords.status, //状态
@@ -1162,10 +1228,10 @@ export default {
getStudyDetail() {
apiCoursePortal.detailStudyPage(this.studyDetailQuery).then((res) => {
if (res.status == 200) {
if (res.result && res.result.length > 0) {
if (res.result && res.result.list && res.result.list.length > 0) {
this.showStudyDetailPage = true;
this.courseStudyList = res.result.list;
this.getStudyDetail.count = res.result.count;
this.studyDetailQuery.count = res.result.count;
} else {
this.showStudyDetailPage = false;
this.courseStudyList = [
@@ -1196,7 +1262,10 @@ export default {
this.getSignupList();
},
// 报名列表
getSignupList() {
getSignupList(resetPage) {
if (resetPage) {
this.study.pageIndex = 1;
}
let params = {
courseId: this.courseDetail.id, //课程的id
signType: this.signup.signType, //报名方式
@@ -1237,7 +1306,6 @@ export default {
});
console.log(11111);
// this.study.list = list;
});
resolve();
} else {
@@ -1256,19 +1324,23 @@ export default {
this.study.pageIndex = val;
this.getSignupList();
},
handleTabClick(tab) {
if (tab.name === "second") {
this.getSignupList();
} else if (tab.name === "third") {
this.getStudyRecords();
} else {
// 资源
this.recourseListQuery.courseId = this.courseDetail.id;
this.getResourseList();
}
this.tabName = tab.name;
handleBack() {
window.history.back();
},
// handleTabClick(tab) {
// if (tab.name === "second") {
// this.getSignupList();
// } else if (tab.name === "third") {
// this.getStudyRecords();
// } else {
// // 资源
// this.getStudyRecords();
// this.recourseListQuery.courseId = this.courseDetail.id;
// this.getResourseList();
// }
// // this.tabName = tab.name;
// },
showStudyDetails(row) {
this.studyDetailQuery.courseId = row.courseId;
this.studyDetailQuery.aid = row.aid;
@@ -1282,7 +1354,7 @@ export default {
};
</script>
<style lang="scss">
<style lang="scss" scoped>
.no-wrap {
overflow: hidden;
text-overflow: ellipsis;
@@ -1292,30 +1364,109 @@ export default {
white-space: nowrap;
}
.delete-action-link--danger {
color: #E32E2E;
color: #e32e2e;
&:hover {
color: #E32E2E;
color: #e32e2e;
}
&:active {
color: #E32E2E;
color: #e32e2e;
}
&:focus {
color: #E32E2E;
color: #e32e2e;
}
}
#courseManage {
.pagination {
text-align: right;
padding-top: 20px;
::v-deep .el-pagination {
.el-pagination__total {
font-size: 14px;
color: #000000;
}
.el-pagination__sizes {
margin-right: 4px;
.el-input {
margin: 0;
width: 89px;
}
.el-input__inner {
width: 89px;
background: #f5f9ff;
border-radius: 4px;
border: 1px solid #dfdfdf;
height: 28px;
font-size: 14px;
color: #000000;
}
}
.btn-prev,
.btn-next {
width: 28px;
height: 28px;
background: #f5f9ff;
border-radius: 4px;
border: 1px solid #dfdfdf;
// &:hover {
// background: #4284F7;
// color: #FFFFFF;
// }
}
.btn-quicknext {
background: transparent;
border: none;
line-height: 44px;
&:before {
content: "......";
}
}
.el-pager {
.number {
min-width: 28px;
height: 28px;
background: #f5f9ff;
border-radius: 4px;
border: 1px solid #dfdfdf;
font-weight: normal;
color: #000000;
margin: 0 4px;
&.active {
background: #4284f7;
color: #ffffff;
border: none;
}
}
}
.el-pagination__jump {
font-size: 14px;
color: #000000;
margin-left: 4px;
.el-input__inner {
width: 100%;
min-width: 28px;
height: 28px;
background: #f5f9ff;
border-radius: 4px;
border: 1px solid #dfdfdf;
font-size: 14px;
color: #000000;
}
}
}
}
.option-code {
margin-left: 4px;
color: #999;
}
.noSplitDatePicker {
::v-deep .noSplitDatePicker {
/* 隐藏范围选择器的分隔符和占位符 */
.el-range-separator,
.el-range__close-icon {
display: none !important;
}
}
.resetDatePicker {
::v-deep .resetDatePicker {
.el-range-input {
text-align: left;
}
@@ -1342,14 +1493,43 @@ export default {
}
.course-info {
display: flex;
align-items: center;
margin-bottom: 40px;
.course-desc-item {
margin-right: 40px;
.title {
font-weight: bold;
justify-content: space-between;
.course-info-left {
flex: 1;
min-width: 0;
}
.course-info-right {
flex-shrink: 0;
text-align: right;
::v-deep .el-button {
font-size: 16px;
}
}
.course-top {
display: flex;
align-items: center;
.course-name {
font-size: 16px;
font-weight: bold;
margin-right: 8px;
}
.course-type {
// width: 48px;
padding: 0 5px;
height: 16px;
background: #FF8F20;
font-size: 12px;
line-height: 16px;
text-align: center;
border-radius: 4px;
color: #fff;
}
}
.course-sys-type {
margin: 8px 0;
font-size: 14px;
color: #999;
}
}
}
.ment-div {
@@ -1387,4 +1567,9 @@ export default {
.el-table .el-table__body-wrapper::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera*/
}
::v-deep.el-table .el-table__header-wrapper .el-table__header th {
background: rgba(66, 132, 247, 0.1);
color: #60769d;
}
</style>

View File

@@ -60,101 +60,108 @@
</div>
<div style="width:390px">
<el-button type="primary" @click="searchData(true)" icon="el-icon-search" >搜索</el-button>
<el-button icon="el-icon-refresh-right" type="primary" style="margin-left:5px" @click="reset">重置</el-button>
<el-button icon="el-icon-refresh-right" type="primary" style="margin-left:5px" @click="reset">重置</el-button>
</div>
</div>
<el-row :gutter="20" style="margin-top:10px">
<el-col :span="4">
<!-- <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-col >
<!-- 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 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-col >
</el-row>
</div>
<div style="margin-right:30px;">
<el-table style="margin:10px 32px 10px 22px;" :data="pageData" border stripe>
<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">
<el-button type="default" size="mini" @click="handleChoose(scope.row)">选择</el-button>
</template>
</el-table-column>
<el-table-column label="名称" prop="name" width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span class="previewStyle" @click="viewTopic(scope.row)">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="内容分类" prop="sysType" sortable width="240px">
<template slot-scope="scope">
<span>{{sysTypeName(scope.row.sysType1)}}</span>
<span v-if="scope.row.sysType2 !=''">/{{sysTypeName(scope.row.sysType2)}}</span>
<span v-if="scope.row.sysType3 !=''">/{{sysTypeName(scope.row.sysType3)}}</span>
</template>
</el-table-column>
<el-table-column label="关键字" :show-overflow-tooltip="true" prop="name" width="200px">
<template slot-scope="scope">
{{ scope.row.keywords }}
</template>
</el-table-column>
<!-- <el-table-column label="资源归属" sortable prop="author" width="240px">
<template slot-scope="scope">
<span>{{resOwnerName(scope.row.resOwner1)}}</span>
<span v-if="scope.row.resOwner2 != ''">/{{resOwnerName(scope.row.resOwner2)}}</span>
<span v-if="scope.row.resOwner3 != ''">/{{resOwnerName(scope.row.resOwner3)}}</span>
</template>
</el-table-column> -->
<!-- <el-table-column label="授课方式" prop="type" width="120px">
<template slot-scope="scope">
{{ courseType(scope.row.type)}}
</template>
</el-table-column> -->
<el-table-column label="状态" prop="status" width="120px">
<template slot-scope="scope">
<!-- 1未提交 2.已提交 = 未审核 5 已审核 -->
<span v-if="scope.row.status == 1">未提交</span>
<span v-if="scope.row.status == 2">待审核</span>
<span v-if="scope.row.status == 5">已审核</span>
<span v-if="scope.row.status == 3">审核未通过</span>
</template>
</el-table-column>
<el-table-column label="是否发布" width="130px">
<template slot-scope="scope">
{{ scope.row.published == true ? '已发布' : '未发布' }}
</template>
</el-table-column>
<el-table-column label="创建人" prop="sysCreateBy"></el-table-column>
<el-table-column label="创建时间" prop="sysCreateTime" width="230px" show-overflow-tooltip></el-table-column>
<el-table-column label="是否停用" width="130px">
<template slot-scope="scope">
{{ scope.row.enabled == true ? '启用' : '停用' }}
</template>
</el-table-column>
<el-table-column label="是否置顶" width="130px">
<template slot-scope="scope">
{{ scope.row.isTop == true ? '置顶' : '未置顶' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180px" fixed="right">
<template slot-scope="scope" class="btn-gl">
<!-- 20240621 修改scope.row.isPermission = fasle 时不展示操作按钮-->
<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>
<el-button v-if="scope.row.isPermission && !forChoose && scope.row.status == 2" @click="withdraw(scope.row)" type="text" size="mini">撤回</el-button>
<el-button v-if="scope.row.isPermission && scope.row.status != 2" type="text" size="mini" @click="editCurriculum(scope.row)">编辑</el-button>
<el-button v-if="scope.row.isPermission && (scope.row.status != 2 && !scope.row.published) || scope.row.isPermission &&!scope.row.enabled" type="text" size="mini" @click="delItem(scope.row)">删除</el-button>
<el-dropdown v-if="scope.row.isPermission && scope.row.published" type="text" size="mini" style="margin-left:10px">
<el-button type="text" size="mini">更多<i class="el-icon-arrow-down el-icon--right"></i></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="copyCourse(scope.row)">复制</el-dropdown-item>
<el-dropdown-item v-if="scope.row.published" @click.native="isDisable(scope.row)">{{scope.row.enabled? '停用':'启用'}}</el-dropdown-item>
<el-dropdown-item v-if="scope.row.published" @click.native="showQrimage(scope.row)">二维码</el-dropdown-item><!--发布之后才可以查看二维码-->
<el-dropdown-item v-if="scope.row.published" @click.native="setTop(scope.row)">{{scope.row.isTop? '取消置顶':'置顶'}}</el-dropdown-item>
<!-- ai播放器相关 -->
<el-table style="margin:10px 32px 10px 22px;" :data="pageData" border stripe @selection-change="handleSelectionChange">
<el-table-column v-if="aiPermission" 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">
<el-button type="default" size="mini" @click="handleChoose(scope.row)">选择</el-button>
</template>
</el-table-column>
<el-table-column label="名称" prop="name" width="200" show-overflow-tooltip>
<template slot-scope="scope">
<span class="previewStyle" @click="viewTopic(scope.row)">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="内容分类" prop="sysType" sortable width="240px">
<template slot-scope="scope">
<span>{{sysTypeName(scope.row.sysType1)}}</span>
<span v-if="scope.row.sysType2 !=''">/{{sysTypeName(scope.row.sysType2)}}</span>
<span v-if="scope.row.sysType3 !=''">/{{sysTypeName(scope.row.sysType3)}}</span>
</template>
</el-table-column>
<el-table-column label="关键字" :show-overflow-tooltip="true" prop="name" width="200px">
<template slot-scope="scope">
{{ scope.row.keywords }}
</template>
</el-table-column>
<!-- <el-table-column label="资源归属" sortable prop="author" width="240px">
<template slot-scope="scope">
<span>{{resOwnerName(scope.row.resOwner1)}}</span>
<span v-if="scope.row.resOwner2 != ''">/{{resOwnerName(scope.row.resOwner2)}}</span>
<span v-if="scope.row.resOwner3 != ''">/{{resOwnerName(scope.row.resOwner3)}}</span>
</template>
</el-table-column> -->
<!-- <el-table-column label="授课方式" prop="type" width="120px">
<template slot-scope="scope">
{{ courseType(scope.row.type)}}
</template>
</el-table-column> -->
<el-table-column label="状态" prop="status" width="120px">
<template slot-scope="scope">
<!-- 1未提交 2.已提交 = 未审核 5 已审核 -->
<span v-if="scope.row.status == 1">未提交</span>
<span v-if="scope.row.status == 2">审核</span>
<span v-if="scope.row.status == 5">已审核</span>
<span v-if="scope.row.status == 3">审核未通过</span>
</template>
</el-table-column>
<el-table-column label="是否发布" width="130px">
<template slot-scope="scope">
{{ scope.row.published == true ? '已发布' : '未发布' }}
</template>
</el-table-column>
<el-table-column label="创建人" prop="sysCreateBy"></el-table-column>
<el-table-column label="创建时间" prop="sysCreateTime" width="230px" show-overflow-tooltip></el-table-column>
<el-table-column label="是否停用" width="130px">
<template slot-scope="scope">
{{ scope.row.enabled == true ? '启用' : '停用' }}
</template>
</el-table-column>
<el-table-column label="是否置顶" width="130px">
<template slot-scope="scope">
{{ scope.row.isTop == true ? '置顶' : '未置顶' }}
</template>
</el-table-column>
<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="aiPermission && 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>
<el-button v-if="scope.row.isPermission && !forChoose && scope.row.status == 2" @click="withdraw(scope.row)" type="text" size="mini">撤回</el-button>
<el-button v-if="scope.row.isPermission && scope.row.status != 2" type="text" size="mini" @click="editCurriculum(scope.row)">编辑</el-button>
<el-button v-if="scope.row.isPermission && (scope.row.status != 2 && !scope.row.published) || scope.row.isPermission &&!scope.row.enabled" type="text" size="mini" @click="delItem(scope.row)">删除</el-button>
<el-dropdown v-if="scope.row.isPermission && scope.row.published" type="text" size="mini" style="margin-left:10px">
<el-button type="text" size="mini">更多<i class="el-icon-arrow-down el-icon--right"></i></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="copyCourse(scope.row)">复制</el-dropdown-item>
<el-dropdown-item v-if="scope.row.published" @click.native="isDisable(scope.row)">{{scope.row.enabled? '停用':'启用'}}</el-dropdown-item>
<el-dropdown-item v-if="scope.row.published" @click.native="showQrimage(scope.row)">二维码</el-dropdown-item><!--发布之后才可以查看二维码-->
<el-dropdown-item v-if="scope.row.published" @click.native="setTop(scope.row)">{{scope.row.isTop? '取消置顶':'置顶'}}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</div>
@@ -287,6 +294,236 @@
<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.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>
</template>
@@ -299,7 +536,7 @@ import auditCourse2 from '@/components/Course/auditCourse2.vue';
import adminPage from '@/components/Administration/adminPage.vue';
import apiResowner from '../../api/modules/resowner.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 {resOwnerIndexName,sysTypeIndexName} from '@/utils/type.js';
import { mapGetters,mapActions } from 'vuex';
@@ -307,8 +544,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,9 +630,38 @@ export default {
},
extendRefId:'',
extendRefType:'',
// ai播放器相关
aiPermission: false,
selectedCourses: [], //已选课程
languageSetting: { // 设置语种弹框
dlgShow: false,
languageCode: ['zh-CN', 'en-US'] // 默认选中的语种
},
aiProcessSetting: { // 开启AI处理弹框
dlgShow: false,
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() {
// ai播放器相关
this.getAiPermission();
this.getAudiences()
let chooseFlag=this.$route.query.f;
this.extendRefId=this.$route.query.refId;
@@ -426,6 +693,7 @@ export default {
},
methods: {
getAudiences(){
apiUserbasic.getInAudienceIds().then(res=>{
if (res.status == 200) {
@@ -449,6 +717,7 @@ export default {
inputOn() {
this.$forceUpdate();
},
// 置顶
setTop(row) {
let params = {
@@ -885,6 +1154,150 @@ export default {
saveNewCatalogZhang() {
this.catalogs.addNewZhang = false;
},
// ai播放器相关
getLanguageName(lang){
return this.selectAllLang.find(item => item.srclang === lang)?.label || '';
},
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) {
console.log('row', row);
this.aiSetting = {
dlgShow: true,
...row
};
},
changeAIKey(key) {
this.aiSetting[key] = this.aiSetting[key] === 1 ? 0 : 1;
},
// 确认AI设置
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], (res) => {
this.$message.success('AI设置保存成功');
this.aiSetting.dlgShow = false;
// 可以选择是否刷新列表数据
this.searchData();
}, (res) => {
this.$message.error('AI设置保存失败');
})
},
setLanguage() {
if (this.selectedCourses.length > 0) {
this.languageSetting = {...{
dlgShow: true,
languageCode: ['zh-CN', 'en-US'] // 默认选中的语种
}, ...this.getAIInfoByList(this.selectedCourses)}
}
},
enableAI() {
// 开启AI处理按钮点击事件
if (this.selectedCourses.length > 0) {
this.aiProcessSetting = {...{
dlgShow: true,
aiSet: 1,
aiAbstract: 1,
aiDraft: 1,
languageCode: ['zh-CN', 'en-US'] // 默认选中的语种
}, ...this.getAIInfoByList(this.selectedCourses)}
}
},
// 批量设置语种 - 确认
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, (res) => {
this.$message.success('设置语种成功!');
this.languageSetting.dlgShow = false;
// 可以选择是否刷新列表数据
this.searchData();
}, (res) => {
this.$message.error('设置语种失败!');
})
},
// 批量开启AI处理 - 确认
confirmAiProcess() {
// 获取AI处理配置
let { aiSet, aiAbstract, aiDraft, languageCode } = this.aiProcessSetting;
const courseList = deepCopy(this.selectedCourses);
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, (res) => {
this.$message.success('开启AI处理成功');
this.aiProcessSetting.dlgShow = false;
// 可以选择是否刷新列表数据
this.searchData();
}, (res) => {
this.$message.error('开启AI处理失败');
})
},
_benchAiSet(courseList, successCB, failCB) {
apiCourse.benchAiSet({courseList}).then(res => {
if(res.status === 200){
successCB && successCB(res);
}else{
failCB && failCB(res);
}
})
},
getAiPermission() {
apiCourse.listByUser({}).then(res => {
console.log('res', res);
if(res.code === 200){
let index = res.data.findIndex(item => item.permissionCode === 'KjbAiSetCode');
this.aiPermission = index !== -1;
console.log('index', index, this.aiPermission);
}
})
}
}
};
</script>
@@ -953,4 +1366,22 @@ 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;
}
.languages-list{
display: flex;
flex-wrap: wrap;
gap: 20px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -111,9 +111,9 @@
</div> -->
<div></div>
<el-dialog title="二维码" :visible.sync="qrCodedialogVisible" width="900px" @close="closeCode" custom-class="g-dialog">
<el-dialog title="二维码" :visible.sync="qrCodedialogVisible" width="700px" @close="closeCode" class="common-course-dialog">
<div>
<el-form size="medium" label-width="100px">
<el-form size="medium" label-width="70px">
<el-form-item label="二维码">
<div id="qrcode" ref="qrcode" class="qrcode-img" @mouseenter="showDownloadButton = true"
@mouseleave="showDownloadButton = false">
@@ -125,10 +125,12 @@
</div>
</el-form-item>
<el-form-item label="链接">
<el-form-item label="链接" >
<div style="width:500px">
<el-input v-model="copyUrl" readonly class="input-with-select" id="text">
<el-button slot="append" @click="handleCopyUrl">复制</el-button>
</el-input>
</div>
</el-form-item>
<el-form-item label="">上述内容兼容PC端与移动端您可按需分享</el-form-item>
</el-form>
@@ -137,23 +139,36 @@
</el-dialog>
<!-- TODO 修改展示字段 -->
<el-dialog title="审核记录" :visible.sync="dialogVisible" width="900px" custom-class="g-dialog">
<el-dialog title="审核记录" :visible.sync="dialogVisible" width="900px" class="common-course-dialog">
<div>
<el-table max-height="500" border :data="inviteTeacher" style="width: 100%;">
<el-table-column prop="type" label="审核类型">
<el-table :header-cell-style="{textAlign: 'center', background: 'rgba(66, 132, 247, 0.1)',
color: '#60769D'}"
:cell-style="{ textAlign: 'center' }" max-height="500" border :data="inviteTeacher" style="width: 100%;">
<el-table-column prop="auditType" width="80" label="审核类型">
<template slot-scope="scope">
{{ scope.row.auditState ? '通过' : '不通过' }}
{{ auditTypeEnum[scope.row.auditType] }}
</template>
</el-table-column>
<el-table-column prop="type" label="审核状态">
<el-table-column prop="status" width="100" label="审核状态">
<template slot-scope="scope">
{{ auditEnum[scope.row.status] }}
</template>
</el-table-column>
<el-table-column prop="auditUser" label="审核人"></el-table-column>
<el-table-column prop="auditTime" label="审核结果"></el-table-column>
<el-table-column prop="auditTime" label="审核时间"></el-table-column>
<el-table-column prop="auditRemark" label="审核意见"></el-table-column>
<el-table-column prop="auditUser" width="80" label="审核人"></el-table-column>
<el-table-column prop="auditResult" width="80" label="审核结果">
<template slot-scope="scope">
{{ auditResultEnum[scope.row.auditResult] }}
</template>
</el-table-column>
<el-table-column prop="auditTime" width="220" label="审核时间"></el-table-column>
<el-table-column prop="auditRemark" label="审核意见">
<template slot-scope="scope">
<el-tooltip class="item" popper-class="tooltip-multiline" effect="dark" :content="scope.row.auditRemark" placement="top-start">
<p class="no-wrap">
{{scope.row.auditRemark}}</p>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<span slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false"> </el-button></span>
@@ -274,9 +289,19 @@ export default {
loading: false,
noMore: false,
auditEnum: {
1: "审核",
1: "审核",
2: "审核不通过",
9: "审核通过",
},
auditTypeEnum: {
1: "新建",
2: "编辑",
3: "停用",
4: "启用",
},
auditResultEnum: {
0: "驳回",
1: "通过",
},
hoverStates: {},
};
@@ -617,6 +642,14 @@ export default {
</script>
<style scoped lang="scss">
.no-wrap {
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
word-break: break-all;
white-space: nowrap;
}
.page-tip {
margin: 20px auto;
text-align: center;

View File

@@ -0,0 +1,245 @@
<template>
<div class="aiAbstract">
<div class="ai-left">
<div class="left-title">{{courseName}}</div>
<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">
<div class="ai-item-title">{{item.name}}</div>
</li>
</ul>
</div>
<div class="ai-right">
<div class="right-title">
<h3>{{currentVideo.name}}</h3>
<div>
<el-button type="primary" @click="status = 1">下架本课程AI摘要</el-button>
</div>
</div>
<div class="ai-content">
<div class="videoBox">
<videoPlayer :src="testUrl" style="height: auto;"> </videoPlayer>
<div class="video-content">
<h4>视频摘要</h4>
<p>
{{currentVideo.aiAbstract}}
</p>
</div>
</div>
<div class="videoOperation">
<div class="opera-title">
<h4>课程摘要</h4>
<div class="opera-btn">
<el-button type="primary" plain round size="mini" @click="status = 2">重新生成</el-button>
<el-button type="primary" plain round size="mini" @click="status = 4">编辑</el-button>
<el-button plain round size="mini" @click="status = 3">取消</el-button>
</div>
</div>
<div class="opera-content" v-show="status != 4">
<span v-show="status == 1">{{ aiAbstract }}</span>
<p v-show="status == 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;">
</div>
<el-input v-show="status == 4"
type="textarea"
placeholder="请输入内容"
autosize
v-model="aiAbstract">
</el-input>
</div>
</div>
</div>
</div>
</template>
<script>
import videoPlayer from "@/components/VideoPlayer/index.vue";
export default {
name: 'aiAbstract',
// ai播放器相关
components: {
videoPlayer
},
data() {
return {
courseName: '企业经营法则--课程单元',
videoList: [
{
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: {},
aiAbstract: '人工智能AI是让计算机模拟人类智能的技术核心包括机器学习、深度学习等。主要分为弱 AI专注特定任务和强 AI通用智能两类。应用涵盖医疗诊断、自动驾驶、语音助手等多个领域。它通过数据学习模式实现预测和适应能力正在改变生活方式和工作方式。未来发展需平衡创新与伦理考量确保对人类社会有益。',
status: '1', // 1: 正常 2: 生成中 3: 错误 4: 编辑中
}
},
mounted() {
this.currentVideo = this.videoList[0];
},
methods: {}
};
</script>
<style lang="scss" scoped>
.aiAbstract{
height: 100%;
display: flex;
padding: 10px;
justify-content: space-between;
gap: 15px;
background: #f4f7fa;
.ai-left{
padding: 9px 10px;
width: 24%;
border-radius: 10px;
background: rgba(255, 255, 255, 1);
.left-title{
background: rgba(239, 244, 252, 1);
padding: 15px;
text-align: center;
color: rgba(75, 92, 118, 1);
font-family: Noto Sans SC;
font-size: 16px;
font-weight: 400;
line-height: 23px;
letter-spacing: 0px;
}
.ai-list{
margin: 0;
.ai-item{
cursor: pointer;
padding: 15px;
text-align: center;
color: rgba(75, 92, 118, 1);
font-family: Noto Sans SC;
font-size: 16px;
font-weight: 400;
line-height: 23px;
letter-spacing: 0px;
padding: 17px 31px;
border-bottom: 1px solid rgba(240, 240, 240, 1);
text-align: left;
&:hover{
background: rgba(240, 240, 240, 1);
}
&.active{
color: rgba(64, 158, 255, 1);
font-weight: 500;
}
}
}
}
.ai-right{
flex: 1;
display: flex;
flex-direction: column;
border-radius: 10px;
background: rgba(255, 255, 255, 1);
.right-title{
display: flex;
padding: 0 28px;
height: 76px;
border-bottom: 1px solid rgba(229, 231, 235, 1);
display: flex;
justify-content: space-between;
align-items: center;
h3{
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 18px;
font-weight: 600;
line-height: 26px;
margin: 0;
}
}
.ai-content{
flex: 1;
padding: 24px 30px;
display: flex;
gap: 30px;
.videoBox{
width: 55%;
display: flex;
flex-direction: column;
gap: 24px;
.video-content{
flex: 1;
h4{
margin: 0 0 10px 0;
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 16px;
font-weight: 500;
line-height: 23px;
}
p{
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
}
}
.videoOperation{
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
.opera-title{
display: flex;
justify-content: space-between;
align-items: center;
h4{
margin: 0;
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 16px;
font-weight: 500;
line-height: 23px;
}
.opera-btn{
display: flex;
}
}
.opera-content{
flex: 1;
border-radius: 7px;
background: rgba(249, 250, 251, 1);
padding: 15px;
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0px;
}
}
}
}
}
::v-deep .el-textarea{
flex: 1;
}
::v-deep .el-textarea__inner{
height: 100%!important;
}
</style>

View File

@@ -0,0 +1,388 @@
<template>
<div class="aiTranslate">
<div class="ai-left">
<div class="left-title">{{courseName}}</div>
<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">
<div class="ai-item-title">{{item.name}}</div>
</li>
</ul>
</div>
<div class="ai-right">
<div class="right-title">
<h3>{{currentVideo.name}}</h3>
<div>
<el-button type="primary" @click="status = 1">下架本课程AI翻译</el-button>
</div>
</div>
<div class="ai-content">
<div class="videoBox">
<videoPlayer :src="testUrl" style="height: auto;"> </videoPlayer>
<div class="video-content">
<div class="select-lang">
<img src="@/assets/images/course/languageIcon.png" alt="" width="10" height="10">
<span>本课程支持语种</span>
<el-button type="primary" @click="setLanguage()" icon="el-icon-connection">设置语种</el-button>
</div>
<div class="lang-content">
<div class="lang-item" v-for="(item, index) in selectedLang" :key="index">
<span>{{ item.label }}</span>
<span :class="item.aiTranslate == 1 ? 'custom-putaway' : 'custom-takeout'">{{ item.aiTranslate == 1 ? '已上架' : '已下架' }}</span>
<el-button type="text" @click="item.aiTranslate = item.aiTranslate == 1 ? 0 : 1">{{ item.aiTranslate == 1 ? '下架' : '上架' }}</el-button>
</div>
</div>
</div>
</div>
<div class="videoOperation">
<div class="opera-title">
<span>目标语种</span>
<el-select v-model="value" placeholder="请选择目标语种" style="flex: 1;">
<el-option
v-for="item in selectAllLang"
:key="item.srclang"
:label="item.label"
:value="item.srclang"
>
</el-option>
</el-select>
<el-button type="primary" @click="status = 2">加载字幕</el-button>
</div>
<div class="opera-content">
<div class="bg-gray" v-show="status != 4">
<p v-show="status == 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>
<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="status == 5" src="@/assets/images/course/selectLanguage.png" alt="" width="112" height="130" style="display: flex;margin: 35% auto 0 auto;">
</div>
<el-input v-show="status == 4"
type="textarea"
placeholder="请输入内容"
autosize
v-model="aiTranslate">
</el-input>
<div class="opera-btn">
<el-button v-show="status == 1" type="primary" plain round size="mini" @click="updateDialogVisible = true">重新生成</el-button>
<el-button v-show="status == 1" type="primary" plain round size="mini" @click="status = 4">编辑</el-button>
<el-button v-show="status == 4" plain round size="mini" @click="status = 1">取消</el-button>
<el-button v-show="status == 4" type="primary" plain round size="mini" @click="status = 1">确认</el-button>
</div>
</div>
</div>
</div>
</div>
<el-dialog
title="确认同步更新吗"
:visible.sync="updateDialogVisible"
width="420px"
style="border-radius: 10px;"
center>
<p style="text-align: center;">系统将根据当前最新中文内容重新生成其他语种的翻译您此前对翻译的修改将会丢失</p>
<span slot="footer" class="dialog-footer">
<el-button @click="updateDialogVisible = false"> </el-button>
<el-button style="margin-left: 60px;" type="primary" @click="updateDialogVisible = false">确认同步更新</el-button>
</span>
</el-dialog>
<el-dialog
title="AIf翻译"
:visible.sync="selectDialogVisible"
width="500px"
class="select-dialog">
<div class="select-dialog-content">
<p>请选择该课程所支持语种</p>
<el-select v-model="selectLang" placeholder="请选择目标语种" style="width: 100%;" multiple>
<el-option
v-for="item in selectAllLang"
:key="item.srclang"
:label="item.label"
:value="item.srclang"
>
</el-option>
</el-select>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="selectDialogVisible = false"> </el-button>
<el-button style="" type="primary" @click="selectDialogVisible = false"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import videoPlayer from "@/components/VideoPlayer/index.vue";
import { mapGetters } from 'vuex';
export default {
name: 'aiTranslate',
// ai播放器相关
components: {
videoPlayer
},
data() {
return {
courseName: '企业经营法则--课程单元',
videoList: [
{
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: [
{
label: '英文',
srclang: 'en',
aiTranslate: 1,
},
{
label: '中文',
srclang: 'zh',
aiTranslate: 1,
},
{
label: '日文',
srclang: 'ja',
aiTranslate: 1,
},
{
label: '韩文',
srclang: 'ko',
aiTranslate: 1,
},
{
label: '法文',
srclang: 'fr',
aiTranslate: 0,
},
],
updateDialogVisible: false,
selectDialogVisible: false,
selectLang: [],
}
},
computed: {
...mapGetters(['selectAllLang']),
},
mounted() {
this.currentVideo = this.videoList[0];
},
methods: {
setLanguage(){
this.selectDialogVisible = true;
}
}
};
</script>
<style lang="scss" scoped>
.aiTranslate{
height: 100%;
display: flex;
padding: 10px;
justify-content: space-between;
gap: 15px;
background: #f4f7fa;
.ai-left{
padding: 9px 10px;
width: 24%;
border-radius: 10px;
background: rgba(255, 255, 255, 1);
.left-title{
background: rgba(239, 244, 252, 1);
padding: 15px;
text-align: center;
color: rgba(75, 92, 118, 1);
font-family: Noto Sans SC;
font-size: 16px;
font-weight: 400;
line-height: 23px;
letter-spacing: 0px;
}
.ai-list{
margin: 0;
.ai-item{
cursor: pointer;
padding: 15px;
text-align: center;
color: rgba(75, 92, 118, 1);
font-family: Noto Sans SC;
font-size: 16px;
font-weight: 400;
line-height: 23px;
letter-spacing: 0px;
padding: 17px 31px;
border-bottom: 1px solid rgba(240, 240, 240, 1);
text-align: left;
&:hover{
background: rgba(240, 240, 240, 1);
}
&.active{
color: rgba(64, 158, 255, 1);
font-weight: 500;
}
}
}
}
.ai-right{
flex: 1;
display: flex;
flex-direction: column;
border-radius: 10px;
background: rgba(255, 255, 255, 1);
.right-title{
display: flex;
padding: 0 28px;
height: 76px;
border-bottom: 1px solid rgba(229, 231, 235, 1);
display: flex;
justify-content: space-between;
align-items: center;
h3{
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 18px;
font-weight: 600;
line-height: 26px;
margin: 0;
}
}
.ai-content{
flex: 1;
padding: 24px 30px;
display: flex;
gap: 30px;
.videoBox{
width: 55%;
display: flex;
flex-direction: column;
gap: 24px;
.video-content{
flex: 1;
.select-lang{
display: flex;
align-items: center;
gap: 7px;
color: rgba(107, 114, 128, 1);
font-family: Noto Sans SC;
font-size: 12px;
font-weight: 400;
line-height: 17px;
}
.lang-content{
display: flex;
margin-top: 25px;
align-items: center;
gap: 25px 60px;
flex-wrap: wrap;
.lang-item{
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
color: rgba(96, 98, 102, 1);
font-family: Noto Sans SC;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
}
}
}
.videoOperation{
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
.opera-title{
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
>span{
color: rgba(75, 92, 118, 1);
font-family: Noto Sans SC;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
}
.opera-content{
position: relative;
flex: 1;
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0px;
white-space: pre-wrap;
.bg-gray{
border-radius: 7px;
background: rgba(249, 250, 251, 1);
height: 100%;
padding: 15px;
}
.opera-btn{
position: absolute;
top: 16px;
right: 20px;
}
}
}
}
}
}
::v-deep .el-textarea{
height: 100%!important;
}
::v-deep .el-textarea__inner{
height: 100%!important;
}
::v-deep .el-dialog {
border-radius: 10px;
}
.select-dialog{
::v-deep .el-dialog__header{
border-bottom: 1px solid rgba(229, 231, 235, 1);
}
.select-dialog-content{
p{
color: rgba(17, 24, 39, 1);
font-family: Noto Sans SC;
font-size: 14px;
font-weight: 400;
line-height: 20px;
margin-bottom: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,457 @@
<template>
<div class="name-remote-select">
<el-select
ref="selectRef"
v-model="innerValue"
multiple
filterable
remote
clearable
:remote-method="remoteSearch"
:loading="loading"
:placeholder="placeholder"
:multiple-limit="multipleLimit"
:collapse-tags="innerCollapseTags"
:max-collapse-tag="maxCollapseTags"
:no-data-text="noDataText"
class="no-wrap-select"
style="width: 100%;"
@visible-change="handleVisibleChange"
@clear="handleClear"
@change="emitChange"
>
<el-option
v-for="item in options"
:key="item.value"
:label="formatLabel(item)"
:value="item.value"
>
<span>{{ item.label }}</span>
<span v-if="item.code" class="option-code">{{ item.code }}</span>
</el-option>
</el-select>
<div
v-if="panelVisible && selectedPanelItems.length"
class="selected-panel el-select-dropdown el-popper is-multiple"
x-placement="bottom-start"
:style="panelStyle"
ref="selectedPanel"
>
<div class="el-scrollbar">
<div class="el-select-dropdown__wrap el-scrollbar__wrap">
<ul class="el-scrollbar__view el-select-dropdown__list">
<li
v-for="item in selectedPanelItems"
:key="item.value"
:class="['el-select-dropdown__item', { selected: isSelected(item.value) }]"
@click.stop="togglePanelItem(item)"
>
<span>{{ item.label }}</span>
<span v-if="item.code" class="option-code">{{ item.code }}</span>
</li>
</ul>
</div>
</div>
<div class="popper__arrow"></div>
</div>
</div>
</template>
<script>
export default {
name: 'NameRemoteSelect',
props: {
value: {
type: Array,
default: () => [],
},
// 远程拉取方法,返回数组;可返回 Promise
fetcher: {
type: Function,
required: true,
},
// 将 fetcher 返回的原始项映射为 { value, label, code }
optionFormatter: {
type: Function,
default: item => item,
},
placeholder: {
type: String,
required: true,
},
multipleLimit: {
type: Number,
default: 5,
},
minQueryLen: {
type: Number,
default: 2,
},
collapseTags: {
type: Boolean,
default: true,
},
// 打开下拉时是否自动展开全部已选标签(取消+N收起
expandOnVisible: {
type: Boolean,
default: false,
},
maxCollapseTags: {
type: Number,
default: 2,
},
maxInputLen: {
type: Number,
default: 50,
},
// 是否在下拉时额外展示“已选列表”面板
showSelectedPanel: {
type: Boolean,
default: true,
},
// 额外增加的下拉最小宽度(在选择框宽度基础上追加,单位 px
panelWidthExtra: {
type: Number,
default: 60,
},
},
data() {
return {
innerValue: [],
options: [],
loading: false,
keyword: '',
// 传递给 el-select 的 collapse 状态limitedMode 下需关闭折叠以便手动控制可见标签)
innerCollapseTags: true,
optionCache: {},
panelVisible: false,
countEl: null,
tagsContainerEl: null,
panelMinWidth: '',
outsideHandler: null,
selectedPanelItems: [],
// 标记:由点击 +N 主动关闭 el-select 下拉引起的 visible-change(false),需要忽略一次
ignoreNextVisibleChange: false,
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.innerValue = Array.isArray(val) ? val : [];
this.bindCountClick();
},
},
collapseTags: {
immediate: true,
handler(val) {
this.innerCollapseTags = val;
},
},
innerValue() {
this.bindCountClick();
},
panelVisible(visible) {
if (visible) {
this.bindOutsideClick();
} else {
this.unbindOutsideClick();
}
},
},
mounted() {
this.bindCountClick();
this.updatePanelWidth();
},
beforeDestroy() {
this.unbindCountClick();
this.unbindOutsideClick();
},
methods: {
formatLabel(item) {
if (!item || !item.label) return '';
return item.code ? `${item.label}` : item.label;
},
syncInputQuery(limited) {
const select = this.$refs.selectRef;
if (select && select.query !== limited) {
select.query = limited;
this.$nextTick(() => {
if (select.$refs && select.$refs.input) {
select.$refs.input.value = limited;
}
});
}
},
async remoteSearch(keyword) {
const limited = (keyword || '').slice(0, this.maxInputLen);
this.syncInputQuery(limited);
this.keyword = limited;
const query = limited.trim();
if (!query || query.length < this.minQueryLen) {
this.options = [];
return;
}
this.loading = true;
try {
const res = await this.fetcher(query);
const list = Array.isArray(res) ? res : [];
this.options = list
.map(item => this.optionFormatter(item) || {})
.filter(item => item.value);
// 缓存本次查询的选项,便于展示已选列表
this.options.forEach(item => {
this.optionCache[item.value] = item;
});
} catch (error) {
this.options = [];
} finally {
this.loading = false;
}
},
emitChange() {
const selectedOptions = this.selectedDisplay;
this.$emit('input', this.innerValue);
this.$emit('change', { value: this.innerValue, options: selectedOptions });
},
handleClear() {
this.innerValue = [];
this.options = [];
this.panelVisible = false;
this.selectedPanelItems = [];
this.emitChange();
},
handleVisibleChange(visible) {
// 如果是由点击 +N 主动触发的关闭,下拉的 visible-change(false) 需要被忽略一次,避免把自定义面板也关掉
if (!visible && this.ignoreNextVisibleChange) {
this.ignoreNextVisibleChange = false;
return;
}
// 聚焦/下拉打开时不自动展示面板
if (visible) {
// 打开时展开标签,避免点击 +N 看不到具体选项
if (this.expandOnVisible) {
this.innerCollapseTags = false;
}
// 无论关键字是否存在,都清空现有搜索结果,下次输入再重新远程拉取
this.options = [];
this.updatePanelWidth();
this.panelVisible = false;
} else {
// 关闭时恢复原始折叠配置
this.innerCollapseTags = this.collapseTags;
this.panelVisible = false;
this.selectedPanelItems = [];
}
},
// 用缓存兜底,保证已选项能在面板中展示
getOptionByValue(val) {
return this.options.find(item => item.value === val) || this.optionCache[val] || { value: val, label: val };
},
handleCountClick(event) {
// 点击 +N 展开/收起已选列表
if (event && event.stopPropagation) {
event.stopPropagation();
}
if (!this.showSelectedPanel) return;
const hasSelected = this.selectedDisplay.length > 0;
if (!hasSelected) {
this.panelVisible = false;
return;
}
const nextVisible = !this.panelVisible;
if (nextVisible) {
// 准备已选面板,并忽略接下来由关闭 el-select 下拉触发的一次 visible-change(false)
this.ignoreNextVisibleChange = true;
this.prepareSelectedPanel();
this.closeSelectDropdown();
} else {
this.selectedPanelItems = [];
}
this.panelVisible = nextVisible;
},
bindCountClick() {
this.$nextTick(() => {
const selectEl = this.$refs.selectRef && this.$refs.selectRef.$el;
if (!selectEl) return;
let count = this.getCountElement(selectEl);
if (count && count !== this.countEl) {
this.unbindCountClick();
this.countEl = count;
this.countEl.style.cursor = 'pointer';
this.countEl.addEventListener('click', this.handleCountClick);
}
// 同时绑定 tags 容器做事件委托,避免找不到 +N 时无法响应
const tagsContainer = selectEl.querySelector('.el-select__tags');
if (tagsContainer && tagsContainer !== this.tagsContainerEl) {
if (this.tagsContainerEl) {
this.tagsContainerEl.removeEventListener('click', this.handleTagsContainerClick, true);
}
this.tagsContainerEl = tagsContainer;
this.tagsContainerEl.addEventListener('click', this.handleTagsContainerClick, true);
}
});
},
unbindCountClick() {
if (this.countEl) {
this.countEl.removeEventListener('click', this.handleCountClick);
this.countEl = null;
}
if (this.tagsContainerEl) {
this.tagsContainerEl.removeEventListener('click', this.handleTagsContainerClick, true);
this.tagsContainerEl = null;
}
},
handleTagsContainerClick(event) {
const target = event.target;
if (!target) return;
const isCountText = target.classList && target.classList.contains('el-select__tags-text');
const isInfoTag = target.classList && target.classList.contains('el-tag--info');
if (isCountText || isInfoTag) {
this.handleCountClick(event);
}
},
getCountElement(selectEl) {
let count = selectEl.querySelector('.el-select__tags-text');
if (!count) {
const tags = selectEl.querySelectorAll('.el-select__tags .el-tag--info');
count = tags && tags.length ? tags[tags.length - 1] : null;
}
if (!count) {
count = selectEl.querySelector('.el-select__tags');
}
return count;
},
bindOutsideClick() {
if (this.outsideHandler) return;
this.outsideHandler = (e) => {
const panel = this.$refs.selectedPanel;
const selectEl = this.$refs.selectRef && this.$refs.selectRef.$el;
const target = e.target;
if (!target) return;
const insidePanel = panel && panel.contains(target);
const insideSelect = selectEl && selectEl.contains(target);
if (!insidePanel && !insideSelect) {
this.panelVisible = false;
this.selectedPanelItems = [];
}
};
document.addEventListener('mousedown', this.outsideHandler, true);
},
unbindOutsideClick() {
if (this.outsideHandler) {
document.removeEventListener('mousedown', this.outsideHandler, true);
this.outsideHandler = null;
}
},
updatePanelWidth() {
this.$nextTick(() => {
const selectEl = this.$refs.selectRef && this.$refs.selectRef.$el;
if (!selectEl) return;
const rect = selectEl.getBoundingClientRect();
const base = rect ? rect.width : 0;
const extra = Number(this.panelWidthExtra) || 0;
const minWidth = base + extra;
this.panelMinWidth = `${minWidth}px`;
});
},
prepareSelectedPanel() {
// 在展开已选列表时,冻结当前已选项,支持在面板内反复勾选/取消
this.selectedPanelItems = this.selectedDisplay.map(item => ({ ...item }));
},
closeSelectDropdown() {
// 关闭原生下拉,避免与已选列表面板同时展示;保留输入内容
const select = this.$refs.selectRef;
if (select && select.visible) {
if (typeof select.handleClose === 'function') {
select.handleClose();
} else {
select.visible = false;
}
this.syncInputQuery(this.keyword || '');
}
},
isSelected(val) {
return (this.innerValue || []).includes(val);
},
togglePanelItem(item) {
if (!item || !item.value) return;
const current = this.innerValue || [];
const exists = this.isSelected(item.value);
this.innerValue = exists ? current.filter(v => v !== item.value) : [...current, item.value];
this.emitChange();
},
},
computed: {
noDataText() {
return this.keyword && this.keyword.length >= this.minQueryLen ? '无数据' : `请至少输入${this.minQueryLen}个字`;
},
selectedDisplay() {
return (this.innerValue || []).map(val => this.getOptionByValue(val)).filter(item => item && item.value);
},
panelStyle() {
return {
minWidth: this.panelMinWidth || '100%',
};
},
},
};
</script>
<style scoped lang="scss">
.name-remote-select {
width: 100%;
position: relative;
}
.option-code {
margin-left: 4px;
color: #999;
}
.selected-panel {
position: absolute;
z-index: 2000;
width: 100%;
margin-top: 4px;
min-width: 100%;
}
/* 参照 ElementUI popper 箭头默认样式packages/theme-chalk/src/popper.scss */
.selected-panel .popper__arrow,
.selected-panel .popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.selected-panel .popper__arrow {
border-width: 6px;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
}
.selected-panel .popper__arrow::after {
content: " ";
border-width: 6px;
}
.selected-panel[x-placement^='bottom'] {
margin-top: 12px;
}
.selected-panel[x-placement^='bottom'] .popper__arrow {
top: -6px;
/* ElementUI 下拉箭头靠左偏移(非居中),与官方样式对齐 */
left: 35px;
border-top-width: 0;
border-bottom-color: #e4e7ed;
}
.selected-panel[x-placement^='bottom'] .popper__arrow::after {
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
</style>

View File

@@ -206,8 +206,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.aiSet == 1 && 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>
@@ -217,13 +234,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>
@@ -2710,3 +2727,43 @@ a.custom2 {
}
}
</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

File diff suppressed because it is too large Load Diff