Commit d975b425 by wufan

Merge branch 'feature/wufan/20210701/course-B-reform' into dev

parents 6a8415cf fd3041ba
......@@ -13,8 +13,9 @@
}
.container {
overflow: scroll;
height: 100%;;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
.course-cover, .course-url {
width: 100%;
......
......@@ -133,12 +133,13 @@ const FileTypeIcon = {
PPTX: "https://image.xiaomaiketang.com/xm/847pFAdYGW.png",
PDF: "https://image.xiaomaiketang.com/xm/rrEJMNkhTG.png",
MP3: "https://image.xiaomaiketang.com/xm/ykjnSWDyQ6.png",
MP4: "https://image.xiaomaiketang.com/xm/whSYMTdR57.png",
MP4: "https://image.xiaomaiketang.com/xm/TKwbQGYDBR.png",
JPG: "https://image.xiaomaiketang.com/xm/XRkX8JBTPs.png",
JPEG: "https://image.xiaomaiketang.com/xm/XRkX8JBTPs.png",
PNG: "https://image.xiaomaiketang.com/xm/XRkX8JBTPs.png",
GIF: "https://image.xiaomaiketang.com/xm/XRkX8JBTPs.png",
BMP: "https://image.xiaomaiketang.com/xm/XRkX8JBTPs.png",
VIDEO: "https://image.xiaomaiketang.com/xm/TKwbQGYDBR.png"
};
const UploadIcon = "https://image.xiaomaiketang.com/xm/4DXNrZWWsd.png";
......
......@@ -1156,3 +1156,15 @@ window.XMShowClassName = (date, itemName) => {
}
return 'new-icon'
}
// 格式化时间段为时分
window.formatDuration = function (time) {
const diff = Math.floor(time % 3600);
let hours = Math.floor(time / 3600);
let mins = Math.floor(diff / 60);
let seconds = Math.floor(time % 60);
hours = hours < 10 ? ("0" + hours) : hours;
mins = mins < 10 ? ("0" + mins) : mins;
seconds = seconds < 10 ? ("0" + seconds) : seconds;
return hours + ":" + mins + ":" + seconds;
};
\ No newline at end of file
/*
* @Author: wufan
* @Date: 2020-12-12 11:57:10
* @LastEditors: zangsuyun
* @LastEditTime: 2021-03-22 13:54:20
* @LastEditors: wufan
* @LastEditTime: 2021-07-05 15:07:13
* @Description: Description
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -57,7 +57,7 @@ export function getLiveCloudCourseBasePage(params: object) {
return Service.Hades("public/courseCloud/getLiveCloudCourseBasePage", params);
}
//视频课相关接口
//线上课相关接口
export function changeVideoShelfState(params: object) {
return Service.Hades("public/hades/changeVideoShelfState", params);
}
......@@ -73,6 +73,9 @@ export function editVideoSchedule(params: object) {
export function userWatchInfo(params: object) {
return Service.Hades("public/hades/userWatchInfo", params);
}
export function lineDetailWatchInfo(params: object) {
return Service.Hades("public/hades/lineDetailWatchInfo", params);
}
export function videoScheduleDetail(params: object) {
return Service.Hades("public/hades/videoScheduleDetail", params);
}
......
......@@ -81,7 +81,7 @@ class KnowledgeAPI {
exportPicLearnSync = (params: object) => {
return Service.Hades("public/knowledge/exportPicLearnSync", params);
}
// 视频课观看记录导出
// 线上课观看记录导出
exportVideoLearnSync = (params: object) => {
return Service.Hades("public/knowledge/exportVideoLearnSync", params);
}
......
/*
* @Author: zhangleyuan
* @Date: 2021-02-21 16:08:38
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-03-09 12:54:31
* @LastEditors: yuananting
* @LastEditTime: 2021-07-06 11:26:27
* @Description: 描述一下
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import Service from "@/common/js/service";
import Service from '@/common/js/service';
export function getTrainingPlanPage(params: object) {
return Service.Hades("public/hades/getTrainingPlanPage", params);
return Service.Hades('public/hades/getTrainingPlanPage', params);
}
export function createTrainingPlan(params: object) {
return Service.Hades("public/hades/createTrainingPlan", params);
return Service.Hades('public/hades/createTrainingPlan', params);
}
export function updateStateTrainingPlan(params: object) {
return Service.Hades("public/hades/updateStateTrainingPlan", params);
return Service.Hades('public/hades/updateStateTrainingPlan', params);
}
export function getTrainingPlanDetail(params: object) {
return Service.Hades("public/hades/getTrainingPlanDetail", params);
return Service.Hades('public/hades/getTrainingPlanDetail', params);
}
export function updateTrainingPlan(params: object) {
return Service.Hades("public/hades/updateTrainingPlan", params);
return Service.Hades('public/hades/updateTrainingPlan', params);
}
export function deleteTrainingPlan(params: object) {
return Service.Hades("public/hades/deleteTrainingPlan", params);
return Service.Hades('public/hades/deleteTrainingPlan', params);
}
export function getPlanUserRecordPage(params: object) {
return Service.Hades("public/hades/getPlanUserRecordPage", params);
return Service.Hades('public/hades/getPlanUserRecordPage', params);
}
export function getPlanCustomerRecordPage(params: object) {
return Service.Hades("public/hades/getPlanCustomerRecordPage", params);
return Service.Hades('public/hades/getPlanCustomerRecordPage', params);
}
export function getPlanCustomerDetail(params: object) {
return Service.Hades("public/hades/getPlanCustomerDetail", params);
return Service.Hades('public/customerHades/getPlanCustomerDetail', params);
}
export function getPlanCustomerAboutUser(params: object) {
return Service.Hades("public/hades/getPlanCustomerAboutUser", params);
return Service.Hades('public/hades/getPlanCustomerAboutUser', params);
}
export function removePlanCustomer(params: object) {
return Service.Hades("public/hades/removePlanCustomer", params);
return Service.Hades('public/hades/removePlanCustomer', params);
}
export function getStorePlanAll(params: object) {
return Service.Hades("public/hades/getStorePlanAll", params);
return Service.Hades('public/hades/getStorePlanAll', params);
}
export function getTrainingCourseAutoCancel(params: object) {
return Service.Hades("public/hades/getTrainingCourseAutoCancel", params);
return Service.Hades('public/hades/getTrainingCourseAutoCancel', params);
}
......@@ -9,7 +9,8 @@
import {
fetchLecturerData, getCategoryTree, knowledgeMediaCoursePage, fetchUserData, exportStudentCourseData, exportPlayBackCourseData, fetchPlaybackList, createLiveCloudCourse, getLiveCloudCoursePage,
getLiveCloudCourseDetail, updateLiveCloudCourse, turnOnOrOffLiveCloudCourse, delLiveCloudCourse, changeVideoShelfState, createVideoSchedule, delVideoSchedule,
editVideoSchedule, userWatchInfo, videoSchedulePage, videoScheduleDetail, videoWatchInfo, getQrcode, getLiveCloudCourseBasePage, videoScheduleBasePage, relatedCourseToPlan
editVideoSchedule, userWatchInfo, videoSchedulePage, videoScheduleDetail, videoWatchInfo, getQrcode, getLiveCloudCourseBasePage, videoScheduleBasePage, relatedCourseToPlan,
lineDetailWatchInfo
} from '@/data-source/course/request-api';
export default class courseService {
......@@ -85,6 +86,9 @@ export default class courseService {
static videoWatchInfo(params: any) {
return videoWatchInfo(params);
}
static lineDetailWatchInfo(params: any) {
return lineDetailWatchInfo(params);
}
static getLiveCloudCourseBasePage(params: any) {
return getLiveCloudCourseBasePage(params);
}
......
......@@ -19,7 +19,7 @@
/>
<meta
name="keywords"
content="小麦企学院,企业培训,员工培训,企业大学,企业内训,企业外训,培训计划,培训素材,企培,企训,资料云盘,培训课程,培训任务,直播课,视频课,图文课,线下课,知识库,作业,考试,排行榜,培训类别管理,定制培训计划,管理数据,学习数据,企学院,资料共享,培训数字化,数字化培训,培训工具,在线培训,线上培训,培训saas,培训管理,企业微信培训,对客培训,客户培训,直播培训,互联网培训,新员工培训,管理培训,管理者培训,工人培训,制造业培训,餐饮培训,服务业培训,零售培训,门店培训,工厂培训,车间培训,培训补贴,人事培训,财务培训,职场培训,企业学院平台,教育企业学院,教育企业平台,教育平台学院,企业学习,酷学院,小鹅通,企业学院,云学堂,时代光华,云课堂,魔学院,云大学,米知云,授课学堂"
content="小麦企学院,企业培训,员工培训,企业大学,企业内训,企业外训,培训计划,培训素材,企培,企训,资料云盘,培训课程,培训任务,直播课,线上课,图文课,线下课,知识库,作业,考试,排行榜,培训类别管理,定制培训计划,管理数据,学习数据,企学院,资料共享,培训数字化,数字化培训,培训工具,在线培训,线上培训,培训saas,培训管理,企业微信培训,对客培训,客户培训,直播培训,互联网培训,新员工培训,管理培训,管理者培训,工人培训,制造业培训,餐饮培训,服务业培训,零售培训,门店培训,工厂培训,车间培训,培训补贴,人事培训,财务培训,职场培训,企业学院平台,教育企业学院,教育企业平台,教育平台学院,企业学习,酷学院,小鹅通,企业学院,云学堂,时代光华,云课堂,魔学院,云大学,米知云,授课学堂"
/>
<!-- <link rel="apple-touch-icon" href="../src/common/images/logo.png" /> -->
<link rel="shortcut icon" href="https://image.xiaomaiketang.com/xm/c4KiP2epBP.png" />
......
......@@ -7,12 +7,12 @@
*/
import React from 'react';
import { withRouter } from "react-router-dom";
import { withRouter } from 'react-router-dom';
import { Button, message, Modal } from 'antd';
import ShowTips from "@/components/ShowTips";
import Breadcrumbs from "@/components/Breadcrumbs";
import Bus from '../../core/bus'
import ShowTips from '@/components/ShowTips';
import Breadcrumbs from '@/components/Breadcrumbs';
import Bus from '../../core/bus';
import AddLiveBasic from './components/AddLiveBasic';
import AddLiveClass from './components/AddLiveClass';
......@@ -20,10 +20,10 @@ import AddLiveIntro from './components/AddLiveIntro';
import { randomString } from '@/domains/basic-domain/utils';
import Upload from '@/core/upload';
import PreviewCourseModal from './modal/PreviewCourseModal';
import CourseService from "@/domains/course-domain/CourseService";
import CourseService from '@/domains/course-domain/CourseService';
import moment from 'moment';
import User from '@/common/js/user';
import _ from "underscore";
import _ from 'underscore';
import $ from 'jquery';
import './AddLive.less';
......@@ -31,14 +31,14 @@ const defaultCover = 'https://image.xiaomaiketang.com/xm/Yip2YtFDwH.png';
const defaultBasicInfo = {
courseName: null, // 课程名称
coverUrl: defaultCover,
coverId:null,
categoryId:null,
categoryName:null,
coverId: null,
categoryId: null,
categoryName: null,
};
const defaultClassInfo = {
teacherId: null, //讲师的Id
assistant:[], //助教
assistant: [], //助教
teacherName: null,
liveDate: null,
timeHorizonStart: null,
......@@ -50,61 +50,63 @@ const defaultClassInfo = {
const defaultIntroInfo = {
needRecord: 'YES',
whetherVisitorsJoin:'NO',
whetherVisitorsJoin: 'NO',
liveCourseWarmMedia: {},
// 讲师简介
liveCourseMediaRequests: [{
contentType:"INTRO",
liveCourseMediaRequests: [
{
contentType: 'INTRO',
mediaType: 'TEXT',
mediaContent: '',
key: Math.random()
}],
}
key: Math.random(),
},
],
};
class AddLive extends React.Component {
constructor(props) {
super(props);
const id = getParameterByName("id");
const type = getParameterByName("type");
const id = getParameterByName('id');
const type = getParameterByName('type');
this.state = {
id,
type,
loading: false,
isEdit: true,
courseState:'UN_START',
courseState: 'UN_START',
// 直播课基本信息
addLiveBasicInfo: {
courseName: null, // 课程名称
coverUrl: defaultCover,
coverId:null,
categoryId:null,
categoryName:null,
coverId: null,
categoryId: null,
categoryName: null,
},
// 直播课上课信息
addLiveClassInfo: {
teacherId: null,
teacherName: null,
assistant:[],
assistantStoreUserId:[],
assistantNames:[],
assistant: [],
assistantStoreUserId: [],
assistantNames: [],
liveDate: null,
timeHorizonStart: null,
timeHorizonEnd: null,
calendarTime: [], // 批量排课
startTime: new Date().getTime() + 300000, // 批量开始时分
endTime: new Date().getTime() + 300000 // 批量结束时分
endTime: new Date().getTime() + 300000, // 批量结束时分
},
// 直播课简介
addLiveIntroInfo: {
needRecord: 'YES',
whetherVisitorsJoin:'NO',
whetherVisitorsJoin: 'NO',
liveCourseWarmMedia: {},
introduce: '',
liveCourseMediaRequests: [],
},
}
};
}
componentDidMount() {
......@@ -112,13 +114,18 @@ class AddLive extends React.Component {
if (type === 'edit') {
this.getCourseDetail();
}
Bus.bind('editorLimit', (editorTextLength) => {
this.setState({
editorTextLength,
});
});
}
getCourseDetail = () => {
let { isEdit } = this.state;
this.setState({ loading: true });
CourseService.getLiveCloudCourseDetail({
liveCourseId: this.state.id
liveCourseId: this.state.id,
}).then((res) => {
const {
teacherId,
......@@ -134,7 +141,7 @@ class AddLive extends React.Component {
categoryId,
categoryName,
admins,
courseState
courseState,
} = res.result;
let coverId;
let coverUrl;
......@@ -142,15 +149,15 @@ class AddLive extends React.Component {
let liveCourseWarmMedia;
let hasIntro = false;
courseMediaVOS.map((item) => {
switch (item.contentType){
case "COVER":
switch (item.contentType) {
case 'COVER':
coverId = item.mediaContent;
coverUrl = item.mediaUrl;
break;
case "WARMUP":
case 'WARMUP':
liveCourseWarmMedia = item;
break;
case "INTRO":
case 'INTRO':
hasIntro = true;
this.getTextDetail('introduce', item.mediaUrl);
break;
......@@ -159,22 +166,22 @@ class AddLive extends React.Component {
}
return item;
})
});
const addLiveBasicInfo = {
courseName,
coverUrl: coverUrl || defaultCover,
coverId,
categoryId,
categoryName
categoryName,
};
const liveDate = startTime;
const timeHorizonStart = startTime;
const timeHorizonEnd = endTime;
const assistant = _.pluck(admins, "adminId");
const assistantStoreUserId = _.pluck(admins, "adminStoreUserId"); //编辑时的选中的助教的查询用storeUserId查询
const assistantNames = _.pluck(admins, "adminName");
const assistant = _.pluck(admins, 'adminId');
const assistantStoreUserId = _.pluck(admins, 'adminStoreUserId'); //编辑时的选中的助教的查询用storeUserId查询
const assistantNames = _.pluck(admins, 'adminName');
const addLiveClassInfo = {
assistant,
liveDate,
......@@ -186,20 +193,19 @@ class AddLive extends React.Component {
startTime,
endTime,
assistantNames,
assistantStoreUserId
}
assistantStoreUserId,
};
const addLiveIntroInfo = {
liveCourseWarmMedia,
needRecord,
whetherVisitorsJoin,
liveCourseMediaRequests,
}
};
// 晚于开课前30分钟
if(new Date().getTime() > startTime - 1800000) {
isEdit = false
if (new Date().getTime() > startTime - 1800000) {
isEdit = false;
}
this.setState({
loadintroduce: !hasIntro,
......@@ -210,219 +216,210 @@ class AddLive extends React.Component {
addLiveClassInfo,
addLiveBasicInfo,
});
})
}
});
};
getTextDetail = (key, url) => {
$.ajax({
data: {},
type: 'GET',
url,
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
success: (res) => {
this.setState({ addLiveIntroInfo: { ...this.state.addLiveIntroInfo, [key]: res }, [`load${key}`]: true });
},
error: () => {
message.warning('获取简介失败')
}
})
}
message.warning('获取简介失败');
},
});
};
// 修改基本信息
// 修改基本信息
handleChangeBasicInfo = (field, value) => {
// 修改基本信息
handleChangeBasicInfo = (field, value) => {
const { coverUrl } = this.state.addLiveBasicInfo;
this.setState({
addLiveBasicInfo: {
...this.state.addLiveBasicInfo,
[field]: value,
}
})
}
},
});
};
// 修改上课信息
handleChangeClassInfo = (field, value ,type, optionValue) => {
handleChangeClassInfo = (field, value, type, optionValue) => {
const _value = value ? value.valueOf() : null;
const { teacherName } = this.state.addLiveClassInfo;
const { assistantNames } = this.state.addLiveClassInfo;
const { assistantStoreUserId } = this.state.addLiveClassInfo
const { assistantStoreUserId } = this.state.addLiveClassInfo;
this.setState({
addLiveClassInfo: {
...this.state.addLiveClassInfo,
[field]: _value,
teacherName:type==='teacherType'?optionValue:teacherName,
assistantNames:type==='assistantType'?_.pluck(optionValue, "children"):assistantNames,
assistantStoreUserId:type==='assistantType'?_.pluck(optionValue, "key"):assistantStoreUserId,
}
teacherName: type === 'teacherType' ? optionValue : teacherName,
assistantNames: type === 'assistantType' ? _.pluck(optionValue, 'children') : assistantNames,
assistantStoreUserId: type === 'assistantType' ? _.pluck(optionValue, 'key') : assistantStoreUserId,
},
});
}
};
// 修改简介
handleChangeIntroInfo = (field, value) => {
this.setState({
addLiveIntroInfo: {
...this.state.addLiveIntroInfo,
[field]: value
}
})
}
[field]: value,
},
});
};
// 完成创建/编辑
handleSubmit = () => {
//过期判断
if (User.getExpirationTime() && moment().valueOf() > Number(User.getExpirationTime())) {
Modal.warning({
title:"服务已到期",
content: "当前企业购买的小麦企学院服务已到期,如需继续使用学院功能,请尽快续费购买",
okText: "我知道了"
})
return
title: '服务已到期',
content: '当前企业购买的小麦企学院服务已到期,如需继续使用学院功能,请尽快续费购买',
okText: '我知道了',
});
return;
}
const { addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, id, isEdit, type } = this.state;
const {liveDate, timeHorizonStart} = addLiveClassInfo;
const _liveDate = moment(liveDate).format("YYYY-MM-DD");
const { addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, id, isEdit, type, editorTextLength } = this.state;
const { liveDate, timeHorizonStart } = addLiveClassInfo;
const _liveDate = moment(liveDate).format('YYYY-MM-DD');
const _timeHorizonStart = moment(timeHorizonStart).format('HH:mm');
const startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x');
if(type === 'edit' && isEdit && new Date().getTime() > startTime - 1800000) {
if (type === 'edit' && isEdit && new Date().getTime() > startTime - 1800000) {
Modal.info({
title: "提示",
icon: (
<span className="icon iconfont default-confirm-icon">
&#xe6f4;
</span>
),
content: "晚于开课前30分钟,部分信息不可修改",
title: '提示',
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>,
content: '晚于开课前30分钟,部分信息不可修改',
okText: '我知道了',
onOk: () => {
this.getCourseDetail();
}
},
});
return
return;
}
this.handleValidate(addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit).then((res) => {
this.handleValidate(addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit, editorTextLength).then((res) => {
if (!res) return;
Upload.uploadTextToOSS(addLiveIntroInfo.introduce, `${randomString()}.txt`, (introduceId) => {
Upload.uploadTextToOSS(
addLiveIntroInfo.introduce,
`${randomString()}.txt`,
(introduceId) => {
this.submitRemote({ introduceId, addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, id });
}, () => message.warning('上传课程简介失败'));
})
}
},
() => message.warning('上传课程简介失败')
);
});
};
submitRemote = ({ introduceId, addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, id }) => {
const { type } = this.state;
const { courseName,coverUrl,coverId,categoryId} = addLiveBasicInfo;
const {
liveDate,
teacherId,
assistant,
timeHorizonEnd,
timeHorizonStart,
calendarTime,
} = addLiveClassInfo;
const { courseName, coverUrl, coverId, categoryId } = addLiveBasicInfo;
const { liveDate, teacherId, assistant, timeHorizonEnd, timeHorizonStart, calendarTime } = addLiveClassInfo;
let { startTime, endTime } = addLiveClassInfo;
const { needRecord,whetherVisitorsJoin,liveCourseWarmMedia} = addLiveIntroInfo;
const { needRecord, whetherVisitorsJoin, liveCourseWarmMedia } = addLiveIntroInfo;
if(type === 'add') {
if (type === 'add') {
startTime = startTime;
endTime = endTime;
} else {
const _liveDate = moment(liveDate).format("YYYY-MM-DD");
const _liveDate = moment(liveDate).format('YYYY-MM-DD');
const _timeHorizonStart = moment(timeHorizonStart).format('HH:mm');
const _timeHorizonEnd = moment(timeHorizonEnd).format('HH:mm');
startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x');
endTime = moment(_liveDate + ' ' + _timeHorizonEnd).format('x');
}
let coverObj ={
contentType:'COVER',
mediaContent:coverId,
mediaType:'PICTURE',
let coverObj = {
contentType: 'COVER',
mediaContent: coverId,
mediaType: 'PICTURE',
mediaUrl: coverUrl,
}
};
let scheduleMediaRequests = [];
if(coverId){
scheduleMediaRequests = [coverObj,...scheduleMediaRequests]
if (coverId) {
scheduleMediaRequests = [coverObj, ...scheduleMediaRequests];
}
if(liveCourseWarmMedia && liveCourseWarmMedia.mediaUrl){
scheduleMediaRequests = [liveCourseWarmMedia,...scheduleMediaRequests]
if (liveCourseWarmMedia && liveCourseWarmMedia.mediaUrl) {
scheduleMediaRequests = [liveCourseWarmMedia, ...scheduleMediaRequests];
}
const commonParams = {
adminIds:assistant,
adminIds: assistant,
calendarTime,
categoryId,
endTime,
needRecord,
startTime,
courseName: courseName.trim(),
storeId:User.getStoreId(),
teacherId:teacherId,
storeId: User.getStoreId(),
teacherId: teacherId,
whetherVisitorsJoin,
scheduleMediaRequests
}
scheduleMediaRequests,
};
if (type === 'add') {
const params = {
...commonParams,
operatorId: User.getUserId(),
introduceId,
}
};
CourseService.createLiveCloudCourse(params).then((res) => {
if (res.success){
message.success("新建成功");
if (res.success) {
message.success('新建成功');
window.RCHistory.push({
pathname: `/live-course`,
});
}
});
} else {
const params = {
...commonParams,
updateUserId:User.getUserId(),
updateUserId: User.getUserId(),
liveCourseId: id,
introduceId,
}
};
CourseService.updateLiveCloudCourse(params).then((res) => {
if (res.success){
message.success("更新成功");
if (res.success) {
message.success('更新成功');
window.RCHistory.push({
pathname: `/live-course`,
});
}
});
}
}
};
handleValidate = (addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit) => {
handleValidate = (addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit, editorTextLength) => {
return new Promise((resolve) => {
const { type } = this.state;
const { courseName,categoryId} = addLiveBasicInfo;
const {
liveDate, timeHorizonStart, timeHorizonEnd, teacherId, calendarTime
} = addLiveClassInfo;
const { courseName, categoryId } = addLiveBasicInfo;
const { liveDate, timeHorizonStart, timeHorizonEnd, teacherId, calendarTime } = addLiveClassInfo;
const { liveCourseMediaRequests } = addLiveIntroInfo;
const currentTime = +new Date();
if(!courseName) {
if (!courseName) {
message.warning('请输入课程名称');
resolve(false);
return;
}
if(!categoryId){
if (!categoryId) {
message.warning('请选择课程分类');
resolve(false);
return;
}
if(type === 'add') {
if (type === 'add') {
const { startTime, endTime } = addLiveClassInfo;
if(calendarTime.length === 0) {
if (calendarTime.length === 0) {
message.warning('请选择上课日期');
resolve(false);
return;
} else if(startTime === endTime || startTime > endTime) {
} else if (startTime === endTime || startTime > endTime) {
message.warning('结束时间必须晚于开始时间');
resolve(false);
return;
......@@ -432,12 +429,12 @@ handleChangeBasicInfo = (field, value) => {
const itemToday = _.find(calendarTime, (item) => {
const itemDay = moment(item).format('YYYY-MM-DD');
return itemDay === currentDay;
})
if(itemToday) {
});
if (itemToday) {
const itemDay = moment(itemToday).format('YYYY-MM-DD');
const itemHour = moment(startTime).format('HH:mm');
if(itemDay === currentDay) {
if(moment(itemDay + ' ' + itemHour).format('x') < currentTime) {
if (itemDay === currentDay) {
if (moment(itemDay + ' ' + itemHour).format('x') < currentTime) {
message.warning('开始时间不能早于现在');
resolve(false);
return;
......@@ -445,13 +442,13 @@ handleChangeBasicInfo = (field, value) => {
}
}
} else {
const _liveDate = moment(liveDate).format("YYYY-MM-DD");
const _liveDate = moment(liveDate).format('YYYY-MM-DD');
const _timeHorizonStart = moment(timeHorizonStart).format('HH:mm');
const _timeHorizonEnd = moment(timeHorizonEnd).format('HH:mm');
const startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x');
const endTime = moment(_liveDate + ' ' + _timeHorizonEnd).format('x');
if(!startTime || !endTime) {
if (!startTime || !endTime) {
message.warning('日期不能为空');
resolve(false);
return;
......@@ -472,23 +469,29 @@ handleChangeBasicInfo = (field, value) => {
resolve(false);
return;
} else if (isEdit && endTime <= startTime) {
message.warning("结束时间不能早于开始时间");
message.warning('结束时间不能早于开始时间');
resolve(false);
return;
}
}
if(!teacherId){
if (!teacherId) {
message.warning('请选择讲师');
resolve(false);
return;
}
resolve(true)
});
if (editorTextLength > 1000) {
message.warning('课程简介超过字数限定');
resolve(false);
return;
}
resolve(true);
});
};
// 显示预览课程弹窗
handleShowPreviewModal = () => {
const { addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo,type,courseState} = this.state;
const { addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, type, courseState } = this.state;
const previewLiveCourseModal = (
<PreviewCourseModal
courseBasicInfo={addLiveBasicInfo}
......@@ -498,104 +501,77 @@ handleChangeBasicInfo = (field, value) => {
courseState={courseState}
close={() => {
this.setState({
previewLiveCourseModal: null
})
previewLiveCourseModal: null,
});
}}
/>
);
this.setState({ previewLiveCourseModal });
}
};
// 取消编辑并返回上一级路由
handleGoBack = () => {
// 比较state的addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo和默认数据是否相等
const { addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo } = this.state;
if (!_.isEqual(addLiveBasicInfo, defaultBasicInfo) ||
!_.isEqual(addLiveClassInfo, defaultClassInfo) ||
!_.isEqual(addLiveIntroInfo, defaultIntroInfo)
) {
if (!_.isEqual(addLiveBasicInfo, defaultBasicInfo) || !_.isEqual(addLiveClassInfo, defaultClassInfo) || !_.isEqual(addLiveIntroInfo, defaultIntroInfo)) {
Modal.confirm({
title: '确定要返回吗?',
content: '返回后,本次编辑的内容将不被保存',
okText: '确认返回',
cancelText: '留在本页',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>,
onOk: () => {
window.RCHistory.push({
pathname: `/live-course`,
});
}
})
},
});
} else {
window.RCHistory.push({
pathname: `/live-course`,
});
}
}
};
render() {
const {
id,
type,
addLiveBasicInfo,
addLiveClassInfo,
addLiveIntroInfo,
isEdit,
loadintroduce,
} = this.state;
const { id, type, addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit, loadintroduce } = this.state;
return (
<div className="page add-live-page">
<Breadcrumbs
navList={type == "add" ? "新建直播课" : "编辑直播课"}
goBack={this.handleGoBack}
/>
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企学院保有依据国家规定及平台规则进行处理的权利" />
<div className='page add-live-page'>
<Breadcrumbs navList={type == 'add' ? '新建直播课' : '编辑直播课'} goBack={this.handleGoBack} />
<div className='box'>
<div className='show-tips'>
<ShowTips message='请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企学院保有依据国家规定及平台规则进行处理的权利' />
</div>
<div className="add-live-page__form">
<div className="basic-info__wrap">
<div className="title">基本信息</div>
<AddLiveBasic
isEdit={isEdit}
pageType={type}
data={addLiveBasicInfo}
onChange={this.handleChangeBasicInfo}
/>
<div className='add-live-page__form'>
<div className='basic-info__wrap'>
<div className='title'>基本信息</div>
<AddLiveBasic isEdit={isEdit} pageType={type} data={addLiveBasicInfo} onChange={this.handleChangeBasicInfo} />
</div>
<div className="class-info__wrap">
<div className="title">上课信息</div>
<AddLiveClass
isEdit={isEdit}
pageType={type}
data={{...addLiveClassInfo, id} }
onChange={this.handleChangeClassInfo}
/>
<div className='class-info__wrap'>
<div className='title'>上课信息</div>
<AddLiveClass isEdit={isEdit} pageType={type} data={{ ...addLiveClassInfo, id }} onChange={this.handleChangeClassInfo} />
</div>
<div className="intro-info__wrap">
<div className="title">更多信息</div>
<AddLiveIntro
isEdit={isEdit}
data={{ ...addLiveIntroInfo, loadintroduce, id }}
onChange={this.handleChangeIntroInfo}
/>
<div className='intro-info__wrap'>
<div className='title'>更多信息</div>
<AddLiveIntro isEdit={isEdit} data={{ ...addLiveIntroInfo, loadintroduce, id }} onChange={this.handleChangeIntroInfo} />
</div>
</div>
</div>
<div className="footer shrink-footer">
<div className='footer shrink-footer'>
<Button onClick={this.handleGoBack}>取消</Button>
<Button onClick={this.handleShowPreviewModal}>预览</Button>
<Button type="primary" onClick={_.debounce(() => this.handleSubmit(), 3000, true)}>保存</Button>
<Button type='primary' onClick={_.debounce(() => this.handleSubmit(), 3000, true)}>
保存
</Button>
</div>
{ this.state.previewLiveCourseModal }
{ this.state.lackConsumeStudentModal }
{this.state.previewLiveCourseModal}
{this.state.lackConsumeStudentModal}
</div>
)
);
}
}
......
......@@ -7,10 +7,10 @@
*/
import React from 'react';
import { Input, Button, message ,Cascader,Modal} from 'antd';
import UploadOss from "@/core/upload";
import { Input, Button, message, Cascader, Modal } from 'antd';
import UploadOss from '@/core/upload';
import { ImgCutModalNew } from '@/components';
import StoreService from "@/domains/store-domain/storeService";
import StoreService from '@/domains/store-domain/storeService';
import SelectPrepareFileModal from '@/modules/prepare-lesson/modal/SelectPrepareFileModal';
import Upload from '@/core/upload';
// import PhotoClip from 'photoclip'
......@@ -20,34 +20,31 @@ import './AddLiveBasic.less';
const defaultCover = 'https://image.xiaomaiketang.com/xm/Yip2YtFDwH.png';
const fieldNames = { label: 'categoryName', value: 'id', children: 'sonCategoryList' };
let cutFlag = false;
let timer = null
let timer = null;
class AddLiveBasic extends React.Component {
constructor(props) {
super(props);
this.state = {
imageFile: null,
showCutModal: false,
courseCatalogList:[],
courseCatalogList: [],
showSelectFileModal: false,
cutImageBlob: null,
hasImgReady: false // 图片是否上传成功
}
}
componentWillUnmount() {
hasImgReady: false, // 图片是否上传成功
};
}
componentWillUnmount() {}
componentDidMount(){
componentDidMount() {
this.getCourseCatalogList();
}
getCourseCatalogList = ()=>{
StoreService.getCourseCatalogList({current:1,size:1000}).then((res) => {
getCourseCatalogList = () => {
StoreService.getCourseCatalogList({ current: 1, size: 1000 }).then((res) => {
this.setState({
courseCatalogList:res.result.records
})
courseCatalogList: res.result.records,
});
}
});
};
// 上传封面图
handleShowImgCutModal = (event) => {
......@@ -57,44 +54,45 @@ class AddLiveBasic extends React.Component {
imageFile,
showCutModal: true,
});
}
};
// 使用默认封面图
handleResetCoverUrl = () => {
const { data: { coverUrl } } = this.props;
const {
data: { coverUrl },
} = this.props;
const isDefaultCover = coverUrl === defaultCover;
// 如果已经是默认图的话,不做任何任何处理
if (isDefaultCover) return;
message.success('已替换为默认图');
this.props.onChange('coverUrl',defaultCover);
setTimeout(()=>{
this.props.onChange('coverUrl', defaultCover);
setTimeout(() => {
this.props.onChange('coverId', null);
},1000)
}
}, 1000);
};
catalogChange= (value) => {
catalogChange = (value) => {
const changeValueLength = value.length;
switch (changeValueLength){
switch (changeValueLength) {
case 1:
this.props.onChange('categoryId',value[0]);
this.props.onChange('categoryId', value[0]);
break;
case 2:
this.props.onChange('categoryId',value[1]);
this.props.onChange('categoryId', value[1]);
break;
default:
this.props.onChange('categoryId',null);
this.props.onChange('categoryId', null);
break;
}
}
};
handleSelectCover = (file) => {
this.uploadImage(file);
}
};
//上传图片
uploadImage = (imageFile) => {
const { folderName } = imageFile;
const fileName =
window.random_string(16) + folderName.slice(folderName.lastIndexOf("."));
const fileName = window.random_string(16) + folderName.slice(folderName.lastIndexOf('.'));
const self = this;
this.setState(
{
......@@ -102,57 +100,55 @@ class AddLiveBasic extends React.Component {
},
() => {
setTimeout(() => {
const okBtnDom = document.querySelector("#headPicModal");
const okBtnDom = document.querySelector('#headPicModal');
const options = {
size: [500, 282],
ok: okBtnDom,
maxZoom: 3,
style: {
jpgFillColor: "transparent",
jpgFillColor: 'transparent',
},
done: function (dataUrl) {
clearTimeout(self.timer);
self.timer = setTimeout(() => {
if ((self.state.rotate != this.rotate()) || (self.state.scale != this.scale())) {
console.log(this.scale(), 'scale')
const _dataUrl = this.clip()
if (self.state.rotate != this.rotate() || self.state.scale != this.scale()) {
console.log(this.scale(), 'scale');
const _dataUrl = this.clip();
const cutImageBlob = self.convertBase64UrlToBlob(_dataUrl);
self.setState({
cutImageBlob,
dataUrl: _dataUrl,
rotate: this.rotate(),
scale: this.scale()
})
scale: this.scale(),
});
}
}, 500)
}, 500);
const cutImageBlob = self.convertBase64UrlToBlob(dataUrl);
self.setState({
cutImageBlob,
dataUrl
})
dataUrl,
});
setTimeout(() => {
cutFlag = false;
}, 2000);
},
fail: (failInfo) => {
message.error("图片上传失败了,请重新上传");
message.error('图片上传失败了,请重新上传');
},
loadComplete: function (img) {
setTimeout(() => {
const _dataUrl = this.clip()
const _dataUrl = this.clip();
self.setState({
dataUrl: _dataUrl,
hasImgReady: true
})
}, 100)
hasImgReady: true,
});
}, 100);
},
};
const imgUrl = `${imageFile.ossUrl}?${new Date().getTime()}`
const imgUrl = `${imageFile.ossUrl}?${new Date().getTime()}`;
if (!this.state.photoclip) {
const _photoclip = new PhotoClip("#headPicModal", options);
const _photoclip = new PhotoClip('#headPicModal', options);
_photoclip.load(imgUrl);
this.setState({
photoclip: _photoclip,
......@@ -161,7 +157,6 @@ class AddLiveBasic extends React.Component {
this.state.photoclip.clear();
this.state.photoclip.load(imgUrl);
}
}, 200);
}
);
......@@ -170,92 +165,123 @@ class AddLiveBasic extends React.Component {
//获取resourceId
getSignature = (blob, fileName) => {
const { choosedBannerId } = this.state;
Upload.uploadBlobToOSS(blob, 'cover' + (new Date()).valueOf(),null,'signInfo').then((signInfo) => {
this.setState({
coverClicpPath:signInfo.fileUrl,
coverId:signInfo.resourceId,
visible: false
},()=>this.updateCover())
Upload.uploadBlobToOSS(blob, 'cover' + new Date().valueOf(), null, 'signInfo').then((signInfo) => {
this.setState(
{
coverClicpPath: signInfo.fileUrl,
coverId: signInfo.resourceId,
visible: false,
},
() => this.updateCover()
);
});
};
// base64转换成blob
convertBase64UrlToBlob = (urlData) => {
const bytes = window.atob(urlData.split(",")[1]);
const bytes = window.atob(urlData.split(',')[1]);
const ab = new ArrayBuffer(bytes.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: "image/png" });
return new Blob([ab], { type: 'image/png' });
};
updateCover = () =>{
const {coverClicpPath,coverId} = this.state
updateCover = () => {
const { coverClicpPath, coverId } = this.state;
this.setState({
showSelectFileModal: false
})
showSelectFileModal: false,
});
this.props.onChange('coverUrl', coverClicpPath);
setTimeout(()=>{
setTimeout(() => {
this.props.onChange('coverId', coverId);
},1000)
}
}, 1000);
};
render() {
const { showCutModal, imageFile,courseCatalogList,showSelectFileModal,visible,cutImageBlob,hasImgReady
} = this.state;
const { data,pageType,isEdit} = this.props;
const { courseName,categoryName,coverUrl} = data;
const { showCutModal, imageFile, courseCatalogList, showSelectFileModal, visible, cutImageBlob, hasImgReady } = this.state;
const { data, pageType, isEdit } = this.props;
const { courseName, categoryName, coverUrl } = data;
const fileName = '';
// 当前是否使用的是默认图片
const isDefaultCover = coverUrl === defaultCover;
return (
<div className="add-live__basic-info">
<div className="course-name">
<span className="label"><span className="require">*</span>课程名称:</span>
<div className='add-live__basic-info'>
<div className='course-name'>
<span className='label'>
<span className='require'>*</span>课程名称:
</span>
<Input
value={courseName}
placeholder="请输入直播名称(40字以内)"
placeholder='请输入直播名称(40字以内)'
maxLength={40}
style={{ width: 240 }}
onChange={(e) => { this.props.onChange('courseName', e.target.value)}}
onChange={(e) => {
this.props.onChange('courseName', e.target.value);
}}
/>
</div>
<div className="course-cover">
<span className="label">封面图:</span>
<div className="course-cover__wrap">
<div className='course-cover'>
<span className='label'>封面图:</span>
<div className="img-content">
{
isDefaultCover && <span className="tag">默认图</span>
}
<img src={coverUrl} />
</div>
<div className="opt-btns">
<Button onClick={() => {
<div className='course-cover__wrap'>
<div className='opt-btns'>
<Button
onClick={() => {
this.setState({
showSelectFileModal: true
})
}}>上传图片</Button>
<span
className={`default-btn ${isDefaultCover ? 'disabled' : ''}`}
onClick={this.handleResetCoverUrl}
>使用默认图</span>
<div className="tips">建议尺寸1280*720px,图片支持jpg、jpeg、png格式。</div>
showSelectFileModal: true,
});
}}>
上传图片
</Button>
<span className={`default-btn ${isDefaultCover ? 'disabled' : ''}`} onClick={this.handleResetCoverUrl}>
使用默认图
</span>
<div className='tips'>建议尺寸1280*720px,图片支持jpg、jpeg、png格式。</div>
</div>
<div className='img-content'>
{isDefaultCover && <span className='tag'>默认图</span>}
<img src={coverUrl} />
</div>
</div>
</div>
<div className="course-catalog">
<span className="label"><span className="require">*</span>课程分类:</span>
{ pageType === 'add' &&
<Cascader options={courseCatalogList} displayRender={ label => label.join('-')} fieldNames={fieldNames} onChange={this.catalogChange} style={{ width: 240 }} placeholder="请选择课程分类" suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}/>
<div className='course-catalog'>
<span className='label'>
<span className='require'>*</span>课程分类:
</span>
{pageType === 'add' && (
<Cascader
options={courseCatalogList}
displayRender={(label) => label.join('-')}
fieldNames={fieldNames}
onChange={this.catalogChange}
style={{ width: 240 }}
placeholder='请选择课程分类'
suffixIcon={
<span className='icon iconfont' style={{ fontSize: '12px', color: '#BFBFBF' }}>
&#xe835;
</span>
}
{ (pageType === 'edit' && categoryName) &&
<Cascader disabled={!isEdit ? true: false} defaultValue={[categoryName]} options={courseCatalogList} displayRender={ label => label.join('-')} fieldNames={fieldNames} onChange={this.catalogChange} style={{ width: 240 }} placeholder="请选择课程分类" suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>} />
/>
)}
{pageType === 'edit' && categoryName && (
<Cascader
disabled={!isEdit ? true : false}
defaultValue={[categoryName]}
options={courseCatalogList}
displayRender={(label) => label.join('-')}
fieldNames={fieldNames}
onChange={this.catalogChange}
style={{ width: 240 }}
placeholder='请选择课程分类'
suffixIcon={
<span className='icon iconfont' style={{ fontSize: '12px', color: '#BFBFBF' }}>
&#xe835;
</span>
}
/>
)}
</div>
<ImgCutModalNew
title="裁剪"
title='裁剪'
width={530}
cutWidth={500}
cutHeight={282}
......@@ -270,45 +296,46 @@ class AddLiveBasic extends React.Component {
this.state.currentInputFile.value = '';
}}
onClose={() => this.setState({ showCutModal: false })}
reUpload={() => { this.state.currentInputFile.click() }}
reUpload={() => {
this.state.currentInputFile.click();
}}
/>
{showSelectFileModal &&
{showSelectFileModal && (
<SelectPrepareFileModal
key="basic"
operateType="select"
key='basic'
operateType='select'
multiple={false}
accept="image/jpeg,image/png,image/jpg"
accept='image/jpeg,image/png,image/jpg'
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectFileModal}
onClose={() => {
this.setState({ showSelectFileModal: false })
this.setState({ showSelectFileModal: false });
}}
onSelect={this.handleSelectCover}
/>
}
)}
<Modal
title="设置图片"
title='设置图片'
width={1080}
visible={visible}
maskClosable={false}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
closeIcon={<span className='icon iconfont modal-close-icon'>&#xe6ef;</span>}
onCancel={() => {
this.setState({ visible: false });
}}
zIndex={10001}
footer={[
<Button
key="back"
key='back'
onClick={() => {
this.setState({ visible: false });
}}
>
}}>
重新上传
</Button>,
<Button
key="submit"
type="primary"
key='submit'
type='primary'
disabled={!hasImgReady}
onClick={() => {
if (!cutFlag) {
......@@ -316,39 +343,36 @@ class AddLiveBasic extends React.Component {
this.refs.hiddenBtn.click();
}
this.getSignature(cutImageBlob);
}}
>
}}>
确定
</Button>,
]}
>
<div className="clip-box">
]}>
<div className='clip-box'>
<div
id="headPicModal"
ref="headPicModal"
id='headPicModal'
ref='headPicModal'
style={{
width: "500px",
height: "430px",
width: '500px',
height: '430px',
marginBottom: 0,
}}
></div>
<div id="clipBtn" style={{ display: "none" }} ref="hiddenBtn"></div>
<div className="preview-img">
<div className="title">效果预览</div>
}}></div>
<div id='clipBtn' style={{ display: 'none' }} ref='hiddenBtn'></div>
<div className='preview-img'>
<div className='title'>效果预览</div>
{/* <img src={previewUrl} alt="图片预览" className="preview-url" /> */}
<div id="preview-url-box" style={{width:500,height:282}}>
<img src={this.state.dataUrl} style={{ width: '100%' }} alt="" />
<div id='preview-url-box' style={{ width: 500, height: 282 }}>
<img src={this.state.dataUrl} style={{ width: '100%' }} alt='' />
</div>
<div className="tip-box">
<div className="tip">温馨提示</div>
<div className="tip">①预览效果图时可能存在延迟,单击左侧图片刷新即可</div>
<div className="tip">②设置图片时双击可旋转图片,滚动可放大或缩小图片</div>
<div className='tip-box'>
<div className='tip'>温馨提示</div>
<div className='tip'>①预览效果图时可能存在延迟,单击左侧图片刷新即可</div>
<div className='tip'>②设置图片时双击可旋转图片,滚动可放大或缩小图片</div>
</div>
</div>
</div>
</Modal>
</div>
)
);
}
}
......
......@@ -2,41 +2,25 @@
.label {
width: 100px;
text-align: right;
display:inline-block;
display: inline-block;
.require {
color: #EC4B35;
color: #ec4b35;
}
}
.course-cover {
margin-left: 14px;
display: flex;
margin-top: 16px;
margin-top: 24px;
&__wrap {
position: relative;
.tag {
border-radius: 2px;
background: #D6D6D6;
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #FFF;
position: absolute;
top: 8px;
left: 8px;
}
}
.course-cover__wrap {
display: flex;
flex-direction: row;
}
.img-content {
margin-top: 8px;
margin-right: 20px;
width: 299px;
height: 169px;
position: relative;
img {
width: 100%;
......@@ -44,17 +28,28 @@
object-fit: contain;
border-radius: 4px;
}
.tag {
border-radius: 2px;
background: #d6d6d6;
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #fff;
position: absolute;
top: 8px;
left: 8px;
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #2966FF;
margin-left: 14px;
color: #2966ff;
cursor: pointer;
&.disabled {
color: #CCC;
color: #ccc;
cursor: not-allowed;
}
}
......@@ -62,21 +57,21 @@
.ant-upload-list {
display: none;
}
}
.tips {
margin-top: 8px;
color: #999;
}
}
.course-catalog{
margin:20px 0 0 14px;
}
}
.course-catalog {
margin: 20px 0 0 14px;
}
}
.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled){
font-weight:normal !important;
color:#2966FF !important;
.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled) {
font-weight: normal !important;
color: #2966ff !important;
}
#imgCutModalNew {
width: 500px;
......
......@@ -17,19 +17,17 @@ import SelectPrepareFileModal from '@/modules/prepare-lesson/modal/SelectPrepare
import { DISK_MAP } from '@/common/constants/academic/lessonEnum';
import { ImgCutModalNew } from '@/components';
const { TextArea } = Input;
const defaultCover = 'https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1599635741526.png';
class AddLiveIntro extends React.Component {
constructor(props) {
super(props);
this.state = {
warmUrl: defaultCover,
showSelectFileModal: false,
diskList: [],
selectType:null
}
selectType: null,
};
}
// 上传封面图
......@@ -40,34 +38,34 @@ class AddLiveIntro extends React.Component {
imageFile,
showCutModal: true,
});
}
};
// 选择暖场资源
handleSelectVideo = (file) => {
const { selectType } = this.state;
this.setState({
showSelectFileModal: false
})
showSelectFileModal: false,
});
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
if(selectType === 'WARMUP'){
if (selectType === 'WARMUP') {
const liveCourseWarmMedia = {
contentType: 'WARMUP',
mediaType: folderFormat === 'MP4' || folderFormat === 'video/mp4' ? 'VIDEO' : 'PICTURE',
mediaContent: resourceId,
mediaUrl: ossUrl,
mediaName: folderName,
size: folderSize
}
size: folderSize,
};
this.props.onChange('liveCourseWarmMedia', liveCourseWarmMedia);
}else{
} else {
// 最多添加九图片
const { liveCourseMediaRequests } = this.props.data;
const list = _.filter(liveCourseMediaRequests, (item) => {
return item.mediaType == "PICTURE";
return item.mediaType == 'PICTURE';
});
if (list.length > 8) {
message.warning("最多添加9张图片");
message.warning('最多添加9张图片');
return;
}
liveCourseMediaRequests.push({
......@@ -80,146 +78,161 @@ class AddLiveIntro extends React.Component {
});
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
}
};
changeIntro = (value) => {
this.props.onChange('introduce', value);
}
};
whetherVisitorsJoinChange = ()=>{
if(this.props.data.whetherVisitorsJoin==="NO"){
this.props.onChange('whetherVisitorsJoin','YES')
}else{
this.props.onChange('whetherVisitorsJoin','NO')
}
whetherVisitorsJoinChange = () => {
if (this.props.data.whetherVisitorsJoin === 'NO') {
this.props.onChange('whetherVisitorsJoin', 'YES');
} else {
this.props.onChange('whetherVisitorsJoin', 'NO');
}
};
render() {
const {liveType, isXiaomai, isEdit, data: { id, introduce, needRecord,whetherVisitorsJoin, loadintroduce, liveCourseWarmMedia = {} } } = this.props;
const { showCutModal, warmUrl, showSelectFileModal, diskList, imageFile,selectType} = this.state
const {
liveType,
isXiaomai,
isEdit,
data: { id, introduce, needRecord, whetherVisitorsJoin, loadintroduce, liveCourseWarmMedia = {} },
} = this.props;
const { showCutModal, warmUrl, showSelectFileModal, diskList, imageFile, selectType } = this.state;
return (
<div className="add-live__intro-info">
<div className="playback" >
<span className="label"><span className="require">*</span>直播回放:</span>
<div className="content">
<Radio.Group value={needRecord} onChange={(e) => { this.props.onChange('needRecord', e.target.value) }} disabled={!isEdit ? true: false}>
<div className='add-live__intro-info'>
<div className='playback'>
<span className='label'>
<span className='require'>*</span>直播回放:
</span>
<div className='content'>
<Radio.Group
value={needRecord}
onChange={(e) => {
this.props.onChange('needRecord', e.target.value);
}}
disabled={!isEdit ? true : false}>
<Row style={{ marginBottom: '5px' }}>
<Col span={24}>
<Radio value="YES">
<Radio value='YES'>
自动录制
<span className="playback__text">系统自助进行全程直播录制</span>
<span className='playback__text'>系统自助进行全程直播录制</span>
</Radio>
</Col>
</Row>
<Row>
<Col span={24}>
<Radio value="NO">
<Radio value='NO'>
手动录制
<span className="playback__text">讲师手动选择何时开始录制</span>
<span className='playback__text'>讲师手动选择何时开始录制</span>
</Radio>
</Col>
</Row>
</Radio.Group>
</div>
</div>
<div className="allow-tourist-join">
<span className="label">观看设置:</span>
<div className="content">
<div>
<Switch checked={whetherVisitorsJoin==="YES"? true:false} onChange={this.whetherVisitorsJoinChange}/>
</div>
<div>
<div class="instro-text">
<div>开启:允许未绑定手机号的学员进入直播间观看直播</div>
<div>关闭:仅限绑定了手机号的学员可以进入直播间观看直播</div>
</div>
</div>
</div>
</div>
<div className="warmup">
<span className="label">直播暖场图:</span>
<div className="course-cover__wrap">
<div className="img-content" style={ liveCourseWarmMedia.mediaUrl ? {background: '#000'} : {} }>
<img src={liveCourseWarmMedia.mediaType === 'VIDEO' ? `${liveCourseWarmMedia.mediaUrl}?x-oss-process=video/snapshot,t_0,m_fast` : (liveCourseWarmMedia.mediaUrl ? liveCourseWarmMedia.mediaUrl : defaultCover )} />
{
liveCourseWarmMedia.mediaUrl &&
<div className="img-delete-wrap">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1600073872956.png" onClick={() => {
<div className='allow-tourist-join'>
<span className='label'>观看设置:</span>
<div className='content'>
<Switch checked={whetherVisitorsJoin === 'NO' ? true : false} onChange={this.whetherVisitorsJoinChange} />
<div class='instro-text'>{whetherVisitorsJoin === 'NO' ? '已开启,学员需绑定手机号才可观看' : '已关闭,学员无需绑定手机号即可观看'}</div>
</div>
</div>
<div className='warmup'>
<span className='label'>直播暖场图:</span>
<div className='course-cover__wrap'>
<div className='img-content' style={liveCourseWarmMedia.mediaUrl ? { background: '#000' } : {}}>
<img
src={
liveCourseWarmMedia.mediaType === 'VIDEO'
? `${liveCourseWarmMedia.mediaUrl}?x-oss-process=video/snapshot,t_0,m_fast`
: liveCourseWarmMedia.mediaUrl
? liveCourseWarmMedia.mediaUrl
: defaultCover
}
/>
{liveCourseWarmMedia.mediaUrl && (
<div className='img-delete-wrap'>
<img
src='https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1600073872956.png'
onClick={() => {
this.props.onChange('liveCourseWarmMedia', {});
}}/>
}}
/>
</div>
}
)}
</div>
<div className="opt-btns">
<div className='opt-btns'>
<Button
disabled={!isEdit}
onClick={() => {
this.setState({
showSelectFileModal: true,
selectType:'WARMUP'
})
}}>上传图片/视频</Button>
selectType: 'WARMUP',
});
}}>
上传图片/视频
</Button>
<div className="tips">
<div className='tips'>
<div>建议尺寸1280*720px或16:9。图片最大5M,支持jpg、jpeg和png;视频最大2G,</div>
<div>支持mp4。</div>
</div>
<Popover content={
<div className="example-wrap">
<p className="title">直播间暖场图示例</p>
<p className="text">直播开始前,展示在直播间视频区域</p>
<Popover
content={
<div className='example-wrap'>
<p className='title'>直播间暖场图示例</p>
<p className='text'>直播开始前,展示在直播间视频区域</p>
<img src='https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1599644482652.png'></img>
</div>
}>
<div className="checkExample">查看示例</div>
<div className='checkExample'>查看示例</div>
</Popover>
</div>
</div>
</div>
<div className="introduce">
<span className="label">直播课简介:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item introduce-editor">
{(!id || loadintroduce) &&
<div className='introduce'>
<span className='label'>课程简介:</span>
<div className='content'>
<div className='intro-list'>
<div className='intro-list__item introduce-editor'>
{(!id || loadintroduce) && (
<GraphicsEditor
id="intro"
id='intro'
isIntro={true}
maxLimit={1000}
detail={{
content: introduce
content: introduce,
}}
onChange={(val) => {
this.changeIntro(val);
}}
onChange={(val) => { this.changeIntro(val) }}
/>
}
)}
</div>
</div>
</div>
</div>
{/* 选择暖场图文件弹窗 */}
{ showSelectFileModal &&
{showSelectFileModal && (
<SelectPrepareFileModal
key="instro"
operateType="select"
accept={selectType==="INTRO"?"image/jpeg,image/png,image/jpg":"video/mp4,image/jpeg,image/png,image/jpg"}
selectTypeList={ selectType==="INTRO" ? ['JPG', 'JPEG', 'PNG']: ['MP4', 'JPG', 'JPEG', 'PNG'] }
tooltip={ selectType==="INTRO"?'支持文件类型:jpg、jpeg、png':'支持文件类型:jpg、jpeg、png、mp4'}
key='instro'
operateType='select'
accept={selectType === 'INTRO' ? 'image/jpeg,image/png,image/jpg' : 'video/mp4,image/jpeg,image/png,image/jpg'}
selectTypeList={selectType === 'INTRO' ? ['JPG', 'JPEG', 'PNG'] : ['MP4', 'JPG', 'JPEG', 'PNG']}
tooltip={selectType === 'INTRO' ? '支持文件类型:jpg、jpeg、png' : '支持文件类型:jpg、jpeg、png、mp4'}
isOpen={showSelectFileModal}
onClose={() => {
this.setState({ showSelectFileModal: false })
this.setState({ showSelectFileModal: false });
}}
onSelect={this.handleSelectVideo}
/>
}
)}
</div>
)
);
}
}
......
/*
* @Author: yuananting
* @Date: 2021-07-05 10:47:19
* @LastEditors: yuananting
* @LastEditTime: 2021-07-06 17:36:02
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import { message } from 'antd';
import React from 'react';
import E from 'wangeditor';
......@@ -9,25 +18,23 @@ const { BtnMenu } = E;
class GraphicsEditor extends React.Component {
constructor(props) {
super(props)
super(props);
this.state = {
editorId: window.random_string(16),
textLength: 0,
showSelectImageModal: false,
showSelectVideoModal: false,
diskList: [],
}
};
this.editorInt = null;
}
componentDidMount() {
this.renderEditor()
this.renderEditor();
this.resetIndex(true);
this.initBus();
}
componentWillUnmount() {
this.resetIndex();
this.removeBus();
......@@ -38,11 +45,11 @@ class GraphicsEditor extends React.Component {
const leftDom = document.querySelector('.left-container');
// topDom.style.zIndex = bool ? 'auto' : 112;
// leftDom.style.zIndex = bool ? 'auto' : 2;
}
};
renderEditor() {
const { editorId } = this.state;
const { detail, onChange, isIntro, maxLimit } = this.props;
const { detail, onChange, isIntro, maxLimit, editorType } = this.props;
class ImageMenu extends BtnMenu {
constructor(editor) {
// data-title属性表示当鼠标悬停在该按钮上时提示该按钮的功能简述
......@@ -50,17 +57,15 @@ class GraphicsEditor extends React.Component {
`<div class="w-e-menu" data-title="图片">
<i class="w-e-icon-image"></i>
</div>`
)
super($elem, editor)
);
super($elem, editor);
}
// 菜单点击事件
clickHandler() {
Bus.trigger(`graphicsEditorImage${isIntro ? '' : 'Content'}`)
Bus.trigger(`graphicsEditorImage${isIntro ? '' : 'Content'}`);
}
tryChangeActive() {
}
tryChangeActive() {}
}
class VideoMenu extends BtnMenu {
......@@ -70,40 +75,24 @@ class GraphicsEditor extends React.Component {
`<div class="w-e-menu" data-title="视频">
<i class="w-e-icon-play"></i>
</div>`
)
super($elem, editor)
);
super($elem, editor);
}
// 菜单点击事件
clickHandler() {
Bus.trigger('graphicsEditorVideo')
Bus.trigger('graphicsEditorVideo');
}
tryChangeActive() {
}
tryChangeActive() {}
}
this.editorInt = new E(`#editor${editorId}`);
this.editorInt.config.focus = false;
this.editorInt.config.showFullScreen = !isIntro
this.editorInt.config.showFullScreen = !isIntro;
this.editorInt.menus.extend('xmimage', ImageMenu);
!isIntro && this.editorInt.menus.extend('xmvideo', VideoMenu);
this.editorInt.config.menus = isIntro ?
[
'head',
'bold',
'fontSize',
'fontName',
'italic',
'underline',
'strikeThrough',
'foreColor',
'backColor',
'list',
'justify',
'emoticon',
'xmimage',
]
this.editorInt.config.menus = isIntro
? ['head', 'bold', 'fontSize', 'fontName', 'italic', 'underline', 'strikeThrough', 'foreColor', 'backColor', 'list', 'justify', 'emoticon', 'xmimage']
: [
'head',
'bold',
......@@ -134,33 +123,45 @@ class GraphicsEditor extends React.Component {
{
title: 'emoji',
type: 'emoji',
content: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '😊', '🙂', '🙃', '😉', '😓', '😅', '😪', '🤔', '😬', '🤐']
}
]
content: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '😊', '🙂', '🙃', '😉', '😓', '😅', '😪', '🤔', '😬', '🤐'],
},
];
this.editorInt.config.zIndex = 1;
this.editorInt.config.pasteFilterStyle = false;
this.editorInt.config.pasteIgnoreImg = true;
// 自定义处理粘贴的文本内容
this.editorInt.config.pasteTextHandle = function (content) {
if (content == '' && !content) return ''
var str = content
str = str.replace(/<xml>[\s\S]*?<\/xml>/ig, '')
str = str.replace(/<style>[\s\S]*?<\/style>/ig, '')
str = str.replace(/[ | ]*\n/g, '\n')
str = str.replace(/\&nbsp\;/ig, ' ')
return str
this.editorInt.config.pasteTextHandle = (content) => {
if (content == '' && !content) return '';
var str1 = content; // 去除所有特殊字符
str1 = str1.replace(/<xml>[\s\S]*?<\/xml>/gi, '');
str1 = str1.replace(/<style>[\s\S]*?<\/style>/gi, '');
str1 = str1.replace(/<\/?[^>]*>/g, '');
str1 = str1.replace(/[ | ]*\n/g, '\n');
str1 = str1.replace(/\&nbsp\;/gi, ' ');
str1 = str1.replace(/[\r\n]/g, '');
str1 = str1.replace(/<\/?a.*?>/g, '');
var str2 = content; // 保留空格和换行的其他字符
str2 = str2.replace(/<xml>[\s\S]*?<\/xml>/gi, '');
str2 = str2.replace(/<style>[\s\S]*?<\/style>/gi, '');
str2 = str2.replace(/<\/?a.*?>/g, '');
const videoCount = ((content || '').match(/<iframe/g) || []).length;
const imageCount = ((content || '').match(/<img/g) || []).length;
const textLength = this.editorInt.txt.text().length + str1.length + videoCount + imageCount;
if (textLength > maxLimit) {
message.warning(`内容过长,不能超过${maxLimit}字`);
}
return str2;
};
this.editorInt.config.onchange = (html) => {
const videoCount = ((html || '').match(/<iframe/g) || []).length;
const imageCount = ((html || '').match(/<img/g) || []).length;
const textLength = this.editorInt.txt.text().replace(/\&nbsp\;/ig, ' ').length + videoCount + imageCount;
const textLength = this.editorInt.txt.text().replace(/\&nbsp\;/gi, ' ').length + videoCount + imageCount;
this.setState({ textLength }, () => {
if (textLength > maxLimit) {
// message.warning('超过字数限定');
}
Bus.trigger('editorLimit', textLength, editorType);
onChange(html, this.state.textLength);
})
}
});
};
this.editorInt.create();
this.editorInt.txt.html(detail.content);
}
......@@ -170,59 +171,60 @@ class GraphicsEditor extends React.Component {
const { ossUrl } = file || {};
if (!ossUrl) return null;
this.setState({
showSelectVideoModal: false
})
showSelectVideoModal: false,
});
const { detail } = this.props;
this.editorInt && this.editorInt.txt.html(`${detail.content}<p style="width: 100%;padding-top: 56.25%;position: relative;"><iframe style="position: absolute;width: 100%;height: 100%;top: 0;left: 0;" src="${ossUrl}"></iframe><br/></p><p><br/></p>`)
}
this.editorInt &&
this.editorInt.txt.html(
`${detail.content}<p style="width: 100%;padding-top: 56.25%;position: relative;"><iframe style="position: absolute;width: 100%;height: 100%;top: 0;left: 0;" src="${ossUrl}"></iframe><br/></p><p><br/></p>`
);
};
handleSelectImage = (file) => {
const { ossUrl } = file || {};
if (!ossUrl) return null;
this.setState({
showSelectImageModal: false
})
showSelectImageModal: false,
});
const { detail } = this.props;
this.editorInt && this.editorInt.txt.html(`${detail.content}<p><img style="max-width: 100%;" src="${ossUrl}" /><br/><p>`)
}
this.editorInt && this.editorInt.txt.html(`${detail.content}<p><img style="max-width: 100%;" src="${ossUrl}" /><br/><p>`);
};
initBus = () => {
const { isIntro } = this.props;
Bus.bind(`graphicsEditorImage${isIntro ? '' : 'Content'}`, this.uploadImage)
!isIntro && Bus.bind('graphicsEditorVideo', this.uploadVideo)
}
Bus.bind(`graphicsEditorImage${isIntro ? '' : 'Content'}`, this.uploadImage);
!isIntro && Bus.bind('graphicsEditorVideo', this.uploadVideo);
};
removeBus = () => {
const { isIntro } = this.props;
Bus.unbind(`graphicsEditorImage${isIntro ? '' : 'Content'}`, this.uploadImage)
!isIntro && Bus.unbind('graphicsEditorVideo', this.uploadVideo)
}
Bus.unbind(`graphicsEditorImage${isIntro ? '' : 'Content'}`, this.uploadImage);
!isIntro && Bus.unbind('graphicsEditorVideo', this.uploadVideo);
};
uploadImage = () => {
this.setState({ showSelectImageModal: true })
}
this.setState({ showSelectImageModal: true });
};
uploadVideo = () => {
this.setState({ showSelectVideoModal: true })
}
this.setState({ showSelectVideoModal: true });
};
render() {
const {
editorId,
textLength,
showSelectImageModal,
showSelectVideoModal,
diskList,
} = this.state;
const { limitLength = 1000, isIntro, maxLimit } = this.props;
return <div className={`graphics-editor-container${isIntro ? ' introduce' : ''} ${(textLength > maxLimit)&& 'warning'}`}>
<div className="editor-box" id={`editor${editorId}`} ></div>
<div className="editor-tips">( {(textLength > maxLimit) ? <span style={{ color: 'red' }} >{textLength}</span> : textLength}/{maxLimit || 100000})</div>
{showSelectVideoModal &&
const { editorId, textLength, showSelectImageModal, showSelectVideoModal, diskList } = this.state;
const { isIntro, maxLimit } = this.props;
return (
<div className={`graphics-editor-container${isIntro ? ' introduce' : ''} ${textLength > maxLimit && 'warning'}`}>
<div className='editor-box' id={`editor${editorId}`}></div>
<div className='editor-tips'>
({textLength > maxLimit ? <span style={{ color: 'red' }}>{textLength}</span> : textLength}/{maxLimit || 1000})
</div>
{textLength > maxLimit && <div className='editor-warning'>最多只能输入{maxLimit || 1000}</div>}
{showSelectVideoModal && (
<SelectPrepareFileModal
operateType="select"
operateType='select'
selectTypeList={['MP4']}
accept="video/mp4"
accept='video/mp4'
confirm={{
title: '文件过大,无法上传',
content: '为保障学员的观看体验,上传的图文大小不能超过2G',
......@@ -232,27 +234,28 @@ class GraphicsEditor extends React.Component {
diskList={diskList}
addVideo={true}
onClose={() => {
this.setState({ showSelectVideoModal: false })
this.setState({ showSelectVideoModal: false });
}}
onSelect={this.handleSelectVideo}
/>
}
{showSelectImageModal &&
)}
{showSelectImageModal && (
<SelectPrepareFileModal
key="basic"
operateType="select"
key='basic'
operateType='select'
multiple={false}
accept="image/jpeg,image/png,image/jpg"
accept='image/jpeg,image/png,image/jpg'
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectImageModal}
onClose={() => {
this.setState({ showSelectImageModal: false })
this.setState({ showSelectImageModal: false });
}}
onSelect={this.handleSelectImage}
/>
}
)}
</div>
);
}
}
......
.graphics-editor-container {
border: 1px solid #E8E8E8;
border: 1px solid #e8e8e8;
border-radius: 4px;
width: 702px;
height: 510px;
......@@ -23,7 +23,7 @@
.w-e-toolbar {
background-color: #fff !important;
border: none !important;
border-bottom: 1px solid #E8E8E8 !important;
border-bottom: 1px solid #e8e8e8 !important;
}
.w-e-text-container {
......@@ -38,19 +38,27 @@
color: #666;
z-index: 1;
}
.editor-warning {
position: absolute;
right: 0;
top: 205px;
color: red;
}
.w-e-full-screen-editor {
.w-e-text-container {
height: ~'calc(100vh - 109px)' !important;
}
}
&.introduce {
height: 200px;
height: 240px;
.w-e-text-container {
height: ~'calc(100% - 69px)' !important;
}
}
&.warning{
&.warning {
border-color: red;
}
}
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:07:47
* @LastEditors: fusanqiasng
* @LastEditTime: 2021-06-16 18:16:14
* @LastEditors: yuananting
* @LastEditTime: 2021-07-07 14:49:24
* @Description: 图文课新增/编辑页
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......@@ -26,6 +26,7 @@ import User from '@/common/js/user';
import _ from 'underscore';
import Upload from '@/core/upload';
import './AddGraphicsCourse.less';
import Bus from '@/core/bus';
const EDIT_BOX_KEY = Math.random();
const fieldNames = { label: 'categoryName', value: 'id', children: 'sonCategoryList' };
......@@ -33,7 +34,7 @@ const fieldNames = { label: 'categoryName', value: 'id', children: 'sonCategoryL
//添加课程时课程默认的一些值
const defaultShelfState = 'YES';
const whetherVisitorsJoin = 'NO';
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/wFnpZtp2yB.png';
const defaultCover = 'https://image.xiaomaiketang.com/xm/wFnpZtp2yB.png';
let cutFlag = false;
class AddGraphicsCourse extends React.Component {
......@@ -52,7 +53,7 @@ class AddGraphicsCourse extends React.Component {
introduce: '',
courseMediaId: null, // 图文课链接
coverId: null, // 图文封面的recourceId
coverUrl: defaultCoverUrl, // 图文课封面
coverUrl: defaultCover, // 图文课封面
studentList: [], // 上课学员列表
shelfState: 'YES', //是否开启学院展示
selectedFileList: [], // 已经从资料云盘中勾选的文件
......@@ -72,6 +73,11 @@ class AddGraphicsCourse extends React.Component {
if (pageType === 'edit') {
this.handleFetchScheudleDetail(id);
}
Bus.bind('editorLimit', (editorTextLength, editorType) => {
this.setState({
[editorType]: editorTextLength,
});
});
}
initBus = () => {
......@@ -393,11 +399,11 @@ class AddGraphicsCourse extends React.Component {
//过期判断
if (User.getExpirationTime() && moment().valueOf() > Number(User.getExpirationTime())) {
Modal.warning({
title:"服务已到期",
content: "当前企业购买的小麦企学院服务已到期,如需继续使用学院功能,请尽快续费购买",
okText: "我知道了"
})
return
title: '服务已到期',
content: '当前企业购买的小麦企学院服务已到期,如需继续使用学院功能,请尽快续费购买',
okText: '我知道了',
});
return;
}
const { id, coverId, pageType, courseName, courseMedia, introduce, categoryId, shelfState, whetherVisitorsJoin } = this.state;
......@@ -466,6 +472,7 @@ class AddGraphicsCourse extends React.Component {
};
handleValidate = (courseName, courseMedia, categoryId) => {
const { graphicsCourseIntor, graphicsCourseContent } = this.state;
return new Promise((resolve) => {
if (!courseName) {
message.warning('请输入课程名称');
......@@ -482,6 +489,18 @@ class AddGraphicsCourse extends React.Component {
resolve(false);
return false;
}
if (graphicsCourseContent > 1000) {
message.warning('课程内容超过字数限定');
resolve(false);
return;
}
if (graphicsCourseIntor > 1000) {
message.warning('课程简介超过字数限定');
resolve(false);
return;
}
// const textMedia = scheduleMedia.filter((item) => item.mediaType === 'TEXT');
// for (let i = 0, len = textMedia.length; i < len; i++) {
// if (textMedia[i].mediaContentLength && textMedia[i].mediaContentLength.length > 1000) {
......@@ -494,6 +513,17 @@ class AddGraphicsCourse extends React.Component {
});
};
// 使用默认封面图
handleResetCoverUrl = () => {
const { coverUrl } = this.state;
const isDefaultCover = coverUrl === defaultCover;
// 如果已经是默认图的话,不做任何任何处理
if (isDefaultCover) return;
this.setState({ coverUrl: defaultCover, coverId: null }, () => {
message.success('已替换为默认图');
});
};
render() {
const {
id,
......@@ -521,6 +551,8 @@ class AddGraphicsCourse extends React.Component {
const hasSelectedStu = studentList.length;
const courseWareIcon = FileVerifyMap[videoType] ? FileTypeIcon[FileVerifyMap[videoType].type] : FileTypeIcon[videoType];
// 当前是否使用的是默认图片
const isDefaultCover = coverUrl === defaultCover;
return (
<div className='page add-graphics-course-page'>
......@@ -547,19 +579,39 @@ class AddGraphicsCourse extends React.Component {
<div className='cover-url flex mt16'>
<div className='label'>封面图:</div>
<div className='cover-url__wrap'>
{/* <div className='cover-url__wrap'>
<div className='opt-btns'>
<Button
onClick={() => {
this.setState({
showSelectCoverModal: true,
});
}}>{`${pageType === 'add' && !coverUrl ? '上传' : '修改'}封面`}</Button>
<div className='tips'></div>
</div>
<div className='img-content'>
<img src={coverUrl} />
</div>
</div> */}
<div className='course-cover__wrap'>
<div className='opt-btns'>
<Button
onClick={() => {
this.setState({
showSelectCoverModal: true,
});
}}>{`${pageType === 'add' && !coverUrl ? '上传' : '修改'}封面`}</Button>
}}>
上传图片
</Button>
<span className={`default-btn ${isDefaultCover ? 'disabled' : ''}`} onClick={this.handleResetCoverUrl}>
使用默认图
</span>
<div className='tips'>建议尺寸1280*720px或16:9。封面图最大5M,支持jpg、jpeg和png。</div>
</div>
<div className='img-content'>
{isDefaultCover && <span className='tag'>默认图</span>}
<img src={coverUrl} />
</div>
</div>
</div>
<div className='course-catalog required'>
......
.add-graphics-course-page {
position:relative !important;
.box{
margin-bottom:52px !important;
position: relative !important;
.box {
margin-bottom: 52px !important;
}
.ant-radio-group {
display: flex;
flex-direction: column;
.radio-item {
margin-bottom: 12px;
......@@ -28,10 +27,10 @@
.form {
margin-top: 16px;
padding: 0 16px;
.label{
display:inline-block;
text-align:right;
width:85px;
.label {
display: inline-block;
text-align: right;
width: 85px;
}
.required {
position: relative;
......@@ -48,9 +47,9 @@
top: 0;
}
}
.course-catalog{
margin-bottom:16px;
margin-top:16px;
.course-catalog {
margin-bottom: 16px;
margin-top: 16px;
}
.course-ware {
display: flex;
......@@ -71,10 +70,20 @@
display: flex;
}
.cover-url__wrap {
.course-cover {
margin-left: 14px;
display: flex;
margin-top: 24px;
&__wrap {
position: relative;
.img-content {
width: 298px;
height: 172px;
margin-top: 8px;
margin-right: 20px;
width: 299px;
height: 169px;
position: relative;
img {
width: 100%;
......@@ -82,29 +91,78 @@
object-fit: contain;
border-radius: 4px;
}
}
.empty-img {
width: 298px;
height: 172px;
border: 1px dashed #EBEBEB;
border-radius: 4px;
padding: 12px;
color: #999;
padding: 52px 24px;
.tag {
border-radius: 2px;
background: #d6d6d6;
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #fff;
position: absolute;
top: 8px;
left: 8px;
}
}
.opt-btns {
margin-top: 8px;
display: flex;
align-items: center;
.default-btn {
margin-left: 14px;
color: #2966ff;
cursor: pointer;
&.disabled {
color: #ccc;
cursor: not-allowed;
}
}
.ant-upload-list {
display: none;
}
.tips {
margin-left: 12px;
margin-top: 8px;
color: #999;
}
}
}
}
// .cover-url__wrap {
// .img-content {
// width: 298px;
// height: 172px;
// img {
// width: 100%;
// height: 100%;
// object-fit: contain;
// border-radius: 4px;
// }
// }
// .empty-img {
// width: 298px;
// height: 172px;
// border: 1px dashed #EBEBEB;
// border-radius: 4px;
// padding: 12px;
// color: #999;
// padding: 52px 24px;
// text-align: center;
// }
// .opt-btns {
// margin-top: 8px;
// display: flex;
// align-items: center;
// .tips {
// margin-left: 12px;
// color: #999;
// }
// }
// }
.select-student {
align-items: center;
......@@ -139,7 +197,7 @@
justify-content: flex-end;
padding-right: 72px;
background: #fff;
border-top: 1px solid #E8E8E8;
border-top: 1px solid #e8e8e8;
z-index: 999;
.ant-btn {
margin-left: 10px;
......
......@@ -17,17 +17,15 @@ import SelectPrepareFileModal from '@/modules/prepare-lesson/modal/SelectPrepare
import { DISK_MAP } from '@/common/constants/academic/lessonEnum';
import { ImgCutModalNew } from '@/components';
const { TextArea } = Input;
class AddGraphicsIntro extends React.Component {
constructor(props) {
super(props);
this.state = {
showSelectFileModal: false,
diskList: [],
selectType: null,
}
};
}
// 上传封面图
......@@ -38,34 +36,34 @@ class AddGraphicsIntro extends React.Component {
imageFile,
showCutModal: true,
});
}
};
// 选择暖场资源
handleSelectVideo = (file) => {
const { selectType } = this.state;
this.setState({
showSelectFileModal: false
})
showSelectFileModal: false,
});
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
if(selectType === 'WARMUP'){
if (selectType === 'WARMUP') {
const liveCourseWarmMedia = {
contentType: 'WARMUP',
mediaType: folderFormat === 'MP4' ? 'VIDEO' : 'PICTURE',
mediaContent: resourceId,
mediaUrl: ossUrl,
mediaName: folderName,
size: folderSize
}
size: folderSize,
};
this.props.onChange('liveCourseWarmMedia', liveCourseWarmMedia);
}else{
} else {
// 最多添加九图片
const { liveCourseMediaRequests } = this.props.data;
const list = _.filter(liveCourseMediaRequests, (item) => {
return item.mediaType == "PICTURE";
return item.mediaType == 'PICTURE';
});
if (list.length > 8) {
message.warning("最多添加9张图片");
message.warning('最多添加9张图片');
return;
}
liveCourseMediaRequests.push({
......@@ -78,121 +76,116 @@ class AddGraphicsIntro extends React.Component {
});
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
}
};
changeDetail = (value) => {
this.props.onChange('courseMedia', value);
}
};
changeIntro = (value) => {
this.props.onChange('introduce', value);
}
};
whetherVisitorsJoinChange = ()=>{
if(this.props.data.whetherVisitorsJoin==="NO"){
this.props.onChange('whetherVisitorsJoin','YES')
}else{
this.props.onChange('whetherVisitorsJoin','NO')
}
whetherVisitorsJoinChange = () => {
if (this.props.data.whetherVisitorsJoin === 'NO') {
this.props.onChange('whetherVisitorsJoin', 'YES');
} else {
this.props.onChange('whetherVisitorsJoin', 'NO');
}
};
shelfStateChange = ()=>{
if(this.props.data.shelfState==="NO"){
this.props.onChange('shelfState','YES')
}else{
this.props.onChange('shelfState','NO')
}
shelfStateChange = () => {
if (this.props.data.shelfState === 'NO') {
this.props.onChange('shelfState', 'YES');
} else {
this.props.onChange('shelfState', 'NO');
}
};
render() {
const {data: { id, whetherVisitorsJoin, courseMedia, introduce, shelfState, loadcourseMedia, loadintroduce } } = this.props;
const {
data: { id, whetherVisitorsJoin, courseMedia, introduce, shelfState, loadcourseMedia, loadintroduce },
} = this.props;
const { showSelectFileModal, selectType } = this.state;
return (
<div className="add-video__intro-info">
<div className="allow-tourist-join">
<span className="label">观看设置:</span>
<div className="content">
<div>
<Switch checked={whetherVisitorsJoin==="YES"? true:false} onChange={this.whetherVisitorsJoinChange}/>
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的学员观看</div>
<div>关闭:仅限绑定了手机号的学员可以进入观看图文课</div>
</div>
</div>
</div>
</div>
<div className="store-show">
<span className="label">学院展示:</span>
<div className="content">
<Row>
<Col span={3}>
<Switch checked={shelfState==="YES"? true:false} onChange={this.shelfStateChange}/>
</Col>
<Col span={21}>
<div className="desc">
<div>开启:图文课将在学员学院图文课列表中展示</div>
<div>关闭:图文课将在学员学院图文课列表中隐藏</div>
</div>
</Col>
</Row>
</div>
</div>
<div className="introduce required">
<span className="label" style={{ marginTop: 5 }}>课程内容:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item content-editor">
{(!id || loadcourseMedia) &&
<div className='add-graphic__intro-info'>
<div className='allow-tourist-join'>
<span className='label'>观看设置:</span>
<div className='content'>
<Switch checked={whetherVisitorsJoin === 'NO' ? true : false} onChange={this.whetherVisitorsJoinChange} />
<div className='desc'>{whetherVisitorsJoin === 'NO' ? '已开启,学员需绑定手机号才可观看' : '已关闭,学员无需绑定手机号即可观看'}</div>
</div>
</div>
<div className='store-show'>
<span className='label'>学院展示:</span>
<div className='content'>
<Switch checked={shelfState === 'YES' ? true : false} onChange={this.shelfStateChange} />
<div className='desc'>{shelfState === 'YES' ? '已开启,课程将在该学院的学员课程列表中显示' : '已关闭,课程将在该学院的学员课程列表中隐藏'}</div>
</div>
</div>
<div className='introduce required'>
<span className='label' style={{ marginTop: 5 }}>
课程内容:
</span>
<div className='content'>
<div className='intro-list'>
<div className='intro-list__item content-editor'>
{(!id || loadcourseMedia) && (
<GraphicsEditor
id="content"
id='content'
maxLimit={1000}
editorType={'graphicsCourseContent'}
detail={{
content: courseMedia
content: courseMedia,
}}
onChange={(val) => {
this.changeDetail(val);
}}
onChange={(val) => { this.changeDetail(val) }}
/>
}
)}
</div>
</div>
</div>
</div>
<div className="introduce">
<span className="label">课程简介:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item introduce-editor">
{(!id || loadintroduce) &&
<div className='introduce'>
<span className='label'>课程简介:</span>
<div className='content'>
<div className='intro-list'>
<div className='intro-list__item introduce-editor'>
{(!id || loadintroduce) && (
<GraphicsEditor
id="intro"
id='intro'
isIntro={true}
maxLimit={1000}
editorType={'graphicsCourseIntor'}
detail={{
content: introduce
content: introduce,
}}
onChange={(val) => {
this.changeIntro(val);
}}
onChange={(val) => { this.changeIntro(val) }}
/>
}
)}
</div>
</div>
</div>
</div>
{/* 选择暖场图文件弹窗 */}
{ showSelectFileModal &&
{showSelectFileModal && (
<SelectPrepareFileModal
operateType="select"
accept={selectType==="INTRO"?"image/jpeg,image/png,image/jpg":"video/mp4,image/jpeg,image/png,image/jpg"}
selectTypeList={ selectType==="INTRO" ? ['JPG', 'JPEG', 'PNG']: ['MP4', 'JPG', 'JPEG', 'PNG'] }
tooltip={ selectType==="INTRO"?'支持文件类型:jpg、jpeg、png':'支持文件类型:jpg、jpeg、png、mp4'}
operateType='select'
accept={selectType === 'INTRO' ? 'image/jpeg,image/png,image/jpg' : 'video/mp4,image/jpeg,image/png,image/jpg'}
selectTypeList={selectType === 'INTRO' ? ['JPG', 'JPEG', 'PNG'] : ['MP4', 'JPG', 'JPEG', 'PNG']}
tooltip={selectType === 'INTRO' ? '支持文件类型:jpg、jpeg、png' : '支持文件类型:jpg、jpeg、png、mp4'}
isOpen={showSelectFileModal}
onClose={() => {
this.setState({ showSelectFileModal: false })
this.setState({ showSelectFileModal: false });
}}
onSelect={this.handleSelectVideo}
/>
}
)}
</div>
)
);
}
}
......
.add-video__intro-info {
.add-graphic__intro-info {
.w-e-full-screen-editor {
background: #fff !important;
}
.playback {
margin-bottom: 10px;
.require {
color: #EC4B35;
color: #ec4b35;
}
&__text {
color: #999;
}
}
.label{
display:inline-block;
text-align:right;
width:85px;
.label {
display: inline-block;
text-align: right;
width: 85px;
}
.playback,
.introduce {
display: flex;
flex-direction: row;
}
.allow-tourist-join{
display:flex;
.content{
display:flex;
.allow-tourist-join {
display: flex;
.content {
display: flex;
}
.desc{
margin-left:16px;
font-size:14px;
color:#999;
display:inline-block;
.desc {
margin-left: 16px;
font-size: 14px;
color: #999;
display: inline-block;
}
}
.store-show{
display:flex;
margin-top:16px;
margin-bottom:16px;
.desc{
margin-left:16px;
font-size:14px;
color:#999;
display:inline-block;
.store-show {
display: flex;
margin-top: 16px;
margin-bottom: 16px;
.content {
display: flex;
}
.desc {
margin-left: 16px;
font-size: 14px;
color: #999;
display: inline-block;
}
}
.radio {
......@@ -65,7 +68,7 @@
&.picture {
width: 550px;
padding: 16px;
border: 1px solid #EEE;
border: 1px solid #eee;
border-radius: 4px;
.img__wrap {
......@@ -114,7 +117,7 @@
line-height: 80px;
padding: 16px;
margin-top: 16px;
border: 1px dashed #EBEBEB;
border: 1px dashed #ebebeb;
border-radius: 4px;
.ant-upload {
......@@ -133,7 +136,7 @@
.iconfont {
font-size: 22px;
line-height: 22px;
color: #BFBFBF;
color: #bfbfbf;
text-align: center;
}
......@@ -152,7 +155,7 @@
}
.checkExample {
width: 60px;
color: #FF7519;
color: #ff7519;
cursor: pointer;
}
.warmup {
......@@ -197,21 +200,19 @@
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #FF7519;
color: #ff7519;
cursor: pointer;
&.disabled {
color: #CCC;
color: #ccc;
cursor: not-allowed;
}
}
}
}
.example-wrap {
}
.example-wrap {
font-family: PingFangSC-Regular, PingFang SC;
text-align: center;
.title {
......@@ -227,14 +228,14 @@
img {
width: 180px;
}
}
.check-record-rule {
}
.check-record-rule {
width: 120px;
color: #FF7519;
color: #ff7519;
cursor: pointer;
z-index: 2;
}
.record-rule-wrap {
}
.record-rule-wrap {
text-align: left;
ul {
margin-top: 10px;
......@@ -247,9 +248,9 @@
.text {
color: #999;
}
}
}
.auto-send-class-report {
.auto-send-class-report {
.label {
width: 57px;
height: 12px;
......@@ -258,7 +259,8 @@
color: #666666;
line-height: 12px;
}
.open-text, .close-text {
.open-text,
.close-text {
width: 572px;
font-size: 14px;
font-weight: 400;
......@@ -273,4 +275,4 @@
.close-text {
margin-bottom: 16px;
}
}
\ No newline at end of file
}
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:45
* @LastEditors: Please set LastEditors
* @LastEditTime: 2021-06-15 20:01:05
* @Description: 视频课-列表模块
* @LastEditors: wufan
* @LastEditTime: 2021-07-05 10:23:10
* @Description: 线上课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import User from '@/common/js/user';
......@@ -333,7 +333,7 @@ class GraphicsCourseList extends React.Component {
// 删除视频课
handleDeleteGraphicsCourse = (scheduleId) => {
Modal.confirm({
title: '你确定要删除此视频课吗?',
title: '你确定要删除此线上课吗?',
content: '删除后,学员将不能进行观看。',
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>,
okText: '确定',
......
......@@ -3,7 +3,7 @@
* @Date: 2020-08-05 10:12:15
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-26 16:07:27
* @Description: 视频课-操作模块
* @Description: 线上课-操作模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......
......@@ -17,13 +17,13 @@ class GraphicsCourse extends React.Component {
courseType: 'PICTURE',
storeId:User.getStoreId()
},
dataSource: [], // 视频课列表
totalCount: 0, // 视频课数据总条数
dataSource: [], // 线上课列表
totalCount: 0, // 线上课数据总条数
}
}
componentWillMount() {
// 获取视频课列表
// 获取线上课列表
this.handleFetchScheduleList();
}
......@@ -76,7 +76,7 @@ class GraphicsCourse extends React.Component {
{/* 操作模块 */}
<GraphicsCourseOpt />
{/* 视频课列表模块 */}
{/* 线上课列表模块 */}
<GraphicsCourseList
query={query}
dataSource={dataSource}
......
......@@ -7,130 +7,186 @@
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react'
import { Modal } from 'antd'
import moment from 'moment'
import './PreviewCourseModal.less'
import React from "react";
import { Modal, Tabs } from "antd";
import moment from "moment";
import ChapterList from "../video-course/components/ChapterList";
import "./PreviewCourseModal.less";
const { TabPane } = Tabs;
const courseStateShow = {
UN_START: {
title: '待开课'
title: "待开课",
},
STARTING: {
title: '上课中'
title: "上课中",
},
FINISH: {
title: '已完成'
title: "已完成",
},
EXPIRED: {
code: 4,
title: '未成功开课',
color: '#CCCCCC'
}
}
title: "未成功开课",
color: "#CCCCCC",
},
};
class PreviewCourseModal extends React.Component {
constructor(props) {
super(props)
this.state = {}
super(props);
this.state = {
activeTab: "courseChapter",
};
}
dealTimeDuration = (time) => {
const diff = Math.floor(time % 3600)
let hours = Math.floor(time / 3600)
let mins = Math.floor(diff / 60)
let seconds = Math.floor(time % 60)
hours = hours < 10 ? '0' + hours : hours
mins = mins < 10 ? '0' + mins : mins
seconds = seconds < 10 ? '0' + seconds : seconds
return hours + ':' + mins + ':' + seconds
}
const diff = Math.floor(time % 3600);
let hours = Math.floor(time / 3600);
let mins = Math.floor(diff / 60);
let seconds = Math.floor(time % 60);
hours = hours < 10 ? "0" + hours : hours;
mins = mins < 10 ? "0" + mins : mins;
seconds = seconds < 10 ? "0" + seconds : seconds;
return hours + ":" + mins + ":" + seconds;
};
dealWithTime = (startTime, endTime) => {
const startDate = new Date(Number(startTime))
const endDate = new Date(Number(endTime))
const startDate = new Date(Number(startTime));
const endDate = new Date(Number(endTime));
const year = startDate.getFullYear()
const month = startDate.getMonth() + 1 < 10 ? `0${startDate.getMonth() + 1}` : startDate.getMonth() + 1
const day = startDate.getDate() < 10 ? `0${startDate.getDate()}` : startDate.getDate()
const year = startDate.getFullYear();
const month =
startDate.getMonth() + 1 < 10
? `0${startDate.getMonth() + 1}`
: startDate.getMonth() + 1;
const day =
startDate.getDate() < 10
? `0${startDate.getDate()}`
: startDate.getDate();
const startHour = startDate.getHours() < 10 ? `0${startDate.getHours()}` : startDate.getHours()
const startMinute = startDate.getMinutes() < 10 ? `0${startDate.getMinutes()}` : startDate.getMinutes()
const startHour =
startDate.getHours() < 10
? `0${startDate.getHours()}`
: startDate.getHours();
const startMinute =
startDate.getMinutes() < 10
? `0${startDate.getMinutes()}`
: startDate.getMinutes();
const endHour = endDate.getHours() < 10 ? `0${endDate.getHours()}` : endDate.getHours()
const endMinute = endDate.getMinutes() < 10 ? `0${endDate.getMinutes()}` : endDate.getMinutes()
const endHour =
endDate.getHours() < 10 ? `0${endDate.getHours()}` : endDate.getHours();
const endMinute =
endDate.getMinutes() < 10
? `0${endDate.getMinutes()}`
: endDate.getMinutes();
const liveDateStr = `${year}-${month}-${day}`
const startTimeStr = `${startHour}:${startMinute}`
const endTimeStr = `${endHour}:${endMinute}`
const liveDateStr = `${year}-${month}-${day}`;
const startTimeStr = `${startHour}:${startMinute}`;
const endTimeStr = `${endHour}:${endMinute}`;
return {
liveDateStr,
startTimeStr,
endTimeStr
}
}
endTimeStr,
};
};
render() {
const { courseBasicInfo, courseClassInfo = {}, courseIntroInfo, type, courseState, origin } = this.props
const { coverUrl, courseName, scheduleVideoUrl, videoDuration } = courseBasicInfo
const { liveDate, calendarTime, startTime, endTime, timeHorizonStart, timeHorizonEnd, teacherName } = courseClassInfo
const { introduce } = courseIntroInfo
let liveDateStr, startTimeStr, endTimeStr
if (type === 'add') {
const _liveDate = moment(calendarTime[0]).format('YYYY-MM-DD')
console.log('_liveDate', _liveDate)
const _timeHorizonStart = moment(startTime).format('HH:mm')
const _timeHorizonEnd = moment(endTime).format('HH:mm')
const _startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x')
const _endTime = moment(_liveDate + ' ' + _timeHorizonEnd).format('x')
const { liveDateStr: _liveDateStr, startTimeStr: _startTimeStr, endTimeStr: _endTimeStr } = this.dealWithTime(_startTime, _endTime)
liveDateStr = _liveDateStr
startTimeStr = _startTimeStr
endTimeStr = _endTimeStr
const {
courseBasicInfo,
courseClassInfo = {},
courseIntroInfo,
type,
courseState,
courseChapterList = [],
} = this.props;
const { coverUrl, courseName, scheduleVideoUrl, videoDuration } =
courseBasicInfo;
const {
liveDate,
calendarTime,
startTime,
endTime,
timeHorizonStart,
timeHorizonEnd,
teacherName,
} = courseClassInfo;
const { introduce, categoryName } = courseIntroInfo;
let { activeTab } = this.state;
let liveDateStr, startTimeStr, endTimeStr;
if (type === "add") {
const _liveDate = moment(calendarTime[0]).format("YYYY-MM-DD");
console.log("_liveDate", _liveDate);
const _timeHorizonStart = moment(startTime).format("HH:mm");
const _timeHorizonEnd = moment(endTime).format("HH:mm");
const _startTime = moment(_liveDate + " " + _timeHorizonStart).format(
"x"
);
const _endTime = moment(_liveDate + " " + _timeHorizonEnd).format("x");
const {
liveDateStr: _liveDateStr,
startTimeStr: _startTimeStr,
endTimeStr: _endTimeStr,
} = this.dealWithTime(_startTime, _endTime);
liveDateStr = _liveDateStr;
startTimeStr = _startTimeStr;
endTimeStr = _endTimeStr;
} else {
const _liveDate = moment(liveDate).format('YYYY-MM-DD')
const _timeHorizonStart = moment(timeHorizonStart).format('HH:mm')
const _timeHorizonEnd = moment(timeHorizonEnd).format('HH:mm')
const startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x')
const endTime = moment(_liveDate + ' ' + _timeHorizonEnd).format('x')
const { liveDateStr: _liveDateStr, startTimeStr: _startTimeStr, endTimeStr: _endTimeStr } = this.dealWithTime(startTime, endTime)
liveDateStr = _liveDateStr
startTimeStr = _startTimeStr
endTimeStr = _endTimeStr
const _liveDate = moment(liveDate).format("YYYY-MM-DD");
const _timeHorizonStart = moment(timeHorizonStart).format("HH:mm");
const _timeHorizonEnd = moment(timeHorizonEnd).format("HH:mm");
const startTime = moment(_liveDate + " " + _timeHorizonStart).format("x");
const endTime = moment(_liveDate + " " + _timeHorizonEnd).format("x");
const {
liveDateStr: _liveDateStr,
startTimeStr: _startTimeStr,
endTimeStr: _endTimeStr,
} = this.dealWithTime(startTime, endTime);
liveDateStr = _liveDateStr;
startTimeStr = _startTimeStr;
endTimeStr = _endTimeStr;
}
return (
<Modal
title='预览'
title="预览"
visible={true}
width={680}
onCancel={this.props.close}
footer={null}
maskClosable={false}
closeIcon={<span className='icon iconfont modal-close-icon'>&#xe6ef;</span>}
className='preview-live-course-modal'>
<div className='container__wrap'>
<div className='container'>
closeIcon={
<span className="icon iconfont modal-close-icon">&#xe6ef;</span>
}
className="preview-live-course-modal"
>
<div className="container__wrap">
<div className="container">
<div className='container__header'>
{type === 'videoCourse' ? (
<Choose>
<When condition={type === 'videoCourse'}>
<video
controls
src={scheduleVideoUrl}
poster={coverUrl ? coverUrl : `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast`}
src={courseChapterList.length && courseChapterList[0].mediaUrl || scheduleVideoUrl }
poster={coverUrl ? coverUrl : `https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png`}
className='course-url'
/>
) : (
<img src={coverUrl} className='course-cover' />
)}
</When>
<Otherwise>
<img src={coverUrl} className='course-cover' alt='' />
</Otherwise>
</Choose>
</div>
{type === 'videoCourse' ? (
<Choose>
<When condition={type === 'videoCourse'}>
<div className='container__body'>
<div className='title__name'>{courseName}</div>
{videoDuration && <div>视频时长:{this.dealTimeDuration(videoDuration)}</div>}
<div className='title__category'>{categoryName}</div>
<div className='title__chapter'>{courseChapterList.length}小节</div>
</div>
) : (
</When>
<Otherwise>
<div className='container__body'>
<div className='container__body__title'>
<div className='title__name'>{courseName}</div>
......@@ -152,28 +208,71 @@ class PreviewCourseModal extends React.Component {
<span className='teacher__value'>{teacherName}</span>
</div>
</div>
)}
</Otherwise>
</Choose>
<div className='container__introduction'>
{type === 'videoCourse' ? (
<div className='container__introduction__title'>视频课简介</div>
) : (
<div className='container__introduction__title'>直播课简介</div>
<div className="container__introduction">
<Choose>
<When condition={type === "videoCourse"}>
<Tabs
activeKey={activeTab}
onChange={(key) => {
this.setState({
activeTab: key,
});
}}
>
<TabPane tab="课程目录" key="courseChapter"></TabPane>
<TabPane tab="课程简介" key="courseIntro"></TabPane>
</Tabs>
</When>
<Otherwise>
<div className="container__introduction__title">
直播课简介
</div>
</Otherwise>
</Choose>
<Choose>
<When condition={type === "videoCourse"}>
{activeTab === "courseChapter" && (
<div className="container__chapter">
{
<ChapterList
chapterType="VIDEO"
courseChapterList={courseChapterList}
/>
}
</div>
)}
{activeTab === "courseIntro" && (
<div className="container__introduction__list editor-box">
<div
className="intro-item text"
dangerouslySetInnerHTML={{
__html: introduce,
}}
/>
</div>
)}
<div className='container__introduction__list editor-box'>
</When>
<Otherwise>
<div className="container__introduction__list editor-box">
<div
className='intro-item text'
className="intro-item text"
dangerouslySetInnerHTML={{
__html: introduce
__html: introduce,
}}
/>
</div>
</Otherwise>
</Choose>
</div>
</div>
</div>
</Modal>
)
);
}
}
export default PreviewCourseModal
export default PreviewCourseModal;
......@@ -13,7 +13,8 @@
}
.container {
overflow: scroll;
overflow-y: auto;
overflow-x: hidden;
height: 100%;;
.course-cover, .course-url {
......@@ -30,6 +31,16 @@
color: #000;
}
.title__category {
color: #999999;
margin-top: 4px;
margin-bottom: 4px;
}
.title__chapter {
color: #999999;
margin-bottom: 4px;
}
.title__inst-name {
color: #666;
font-size: 12px;
......
......@@ -116,11 +116,7 @@ class ShareLiveModal extends React.Component {
}
break;
case 'videoClass': // 视频课
coverImgSrc =
coverUrl ||
(courseDivision === 'internal'
? `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast&anystring=anystring`
: 'https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png');
coverImgSrc = coverUrl || 'https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png';
break;
case 'graphicsClass': // 图文课
coverImgSrc = coverUrl || 'https://image.xiaomaiketang.com/xm/wFnpZtp2yB.png';
......
......@@ -2,43 +2,32 @@
* @Author: 吴文洁
* @Date: 2020-08-05 10:07:47
* @LastEditors: yuananting
* @LastEditTime: 2021-06-07 15:06:26
* @LastEditTime: 2021-07-07 14:51:41
* @Description: 线下课新增/编辑页
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import {
Button,
Input,
Radio,
message,
Modal,
TreeSelect,
Select,
Switch,
TimePicker,
InputNumber,
Tooltip,
} from 'antd';
import { Button, Input, Radio, message, Modal, TreeSelect, Select, Switch, TimePicker, InputNumber, Tooltip } from 'antd';
import $ from 'jquery';
import RangePicker from "@/modules/common/DateRangePicker";
import ShowTips from "@/components/ShowTips";
import Breadcrumbs from "@/components/Breadcrumbs";
import RangePicker from '@/modules/common/DateRangePicker';
import ShowTips from '@/components/ShowTips';
import Breadcrumbs from '@/components/Breadcrumbs';
import SelectStudent from '../modal/select-student';
import SelectPrepareFileModal from '../../prepare-lesson/modal/SelectPrepareFileModal';
import PreviewOfflineModal from './modal/PreviewOfflineModal';
import StoreService from "@/domains/store-domain/storeService";
import StoreService from '@/domains/store-domain/storeService';
import Service from '@/common/js/service';
import { randomString } from '@/domains/basic-domain/utils';
import User from '@/common/js/user';
import _ from "underscore";
import _ from 'underscore';
import moment from 'moment';
import Upload from '@/core/upload';
import GraphicsEditor from '../components/GraphicsEditor';
import MultipleDatePicker from '@/components/MultipleDatePicker';
import './AddOfflineCourse.less';
import Bus from '@/core/bus';
const { Option } = Select;
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/pxbWKsYA87.png';
......@@ -46,15 +35,14 @@ let cutFlag = false;
const unitList = [
{ key: 'HOUR', value: '小时' },
{ key: 'MINUTE', value: '分钟' },
]
];
class AddOfflineCourse extends React.Component {
constructor(props) {
super(props);
const courseId = getParameterByName("id");
const pageType = getParameterByName("type");
const courseId = getParameterByName('id');
const pageType = getParameterByName('type');
this.state = {
courseId, // 线下课ID,编辑的时候从URL上带过来
......@@ -70,17 +58,17 @@ class AddOfflineCourse extends React.Component {
selectedFileList: [], // 已经从资料云盘中勾选的文件
showCutModal: false, // 是否显示截图弹窗
studentModal: false,
categoryName:null, //分类名称
categoryName: null, //分类名称
categoryList: [],
courseCatalogList:[], //分类列表
categoryId:null, //分类的Id值
courseCatalogList: [], //分类列表
categoryId: null, //分类的Id值
whetherVisitorsJoin: 'NO', // 是否允许游客加入
isContent: true,
teacherList: [],
teacherQuery: {
size: 15,
current: 1,
nickName:null
nickName: null,
},
calendarTime: [],
offlineCourseType: 'ALL_DAY_OFFLINE',
......@@ -100,7 +88,7 @@ class AddOfflineCourse extends React.Component {
isEditDisablie: false,
startTime: new Date().getTime() + 300000, // 批量开始时分
endTime: new Date().getTime() + 300000, // 批量结束时分
}
};
}
componentWillMount() {
......@@ -110,30 +98,35 @@ class AddOfflineCourse extends React.Component {
if (pageType === 'edit') {
this.handleFetchScheudleDetail(courseId);
}
Bus.bind('editorLimit', (editorTextLength) => {
this.setState({
editorTextLength,
});
});
}
initBus = () => {
Bus.bind('offlineEditorImage', this.uploadImage)
}
Bus.bind('offlineEditorImage', this.uploadImage);
};
removeBus = () => {
Bus.unbind('offlineEditorImage', this.uploadImage)
}
Bus.unbind('offlineEditorImage', this.uploadImage);
};
uploadImage = () => {
this.setState({ showSelectImageModal: true })
}
this.setState({ showSelectImageModal: true });
};
//获取分类列表
getCourseCatalogList = ()=>{
getCourseCatalogList = () => {
Service.Hades('public/hades/queryCategoryTree', { source: 0, tenantId: User.getStoreId(), count: false, userId: User.getUserId() }).then((res) => {
const { categoryList = [] } = res.result;
this.setState({
categoryList,
courseCatalogList: this.renderTreeNodes(categoryList),
})
});
}
});
};
renderTreeNodes = (data) => {
let newTreeData = data.map((item) => {
......@@ -150,17 +143,17 @@ class AddOfflineCourse extends React.Component {
checkDetail = (courseId) => {
return Service.Hades('public/hades/getOfflineCourseDetail', {
courseId
courseId,
}).then((res) => {
const { courseState } = res.result;
return courseState === 'UN_START';
});
}
};
// 获取线下课详情
handleFetchScheudleDetail = (courseId) => {
return Service.Hades('public/hades/getOfflineCourseDetail',{
courseId
return Service.Hades('public/hades/getOfflineCourseDetail', {
courseId,
}).then((res) => {
const { result = {} } = res || {};
const {
......@@ -198,15 +191,15 @@ class AddOfflineCourse extends React.Component {
let hasIntro = false;
courseMediaVOS.map((item) => {
switch (item.contentType){
case "COVER":
switch (item.contentType) {
case 'COVER':
coverId = item.mediaContent;
coverUrl = item.mediaUrl;
break;
case "SCHEDULE":
case 'SCHEDULE':
this.getTextDetail('courseMedia', item.mediaUrl);
break;
case "INTRO":
case 'INTRO':
hasIntro = true;
this.getTextDetail('introduce', item.mediaUrl);
break;
......@@ -214,7 +207,7 @@ class AddOfflineCourse extends React.Component {
break;
}
return item;
})
});
this.setState({
loadintroduce: !hasIntro,
coverId,
......@@ -247,49 +240,42 @@ class AddOfflineCourse extends React.Component {
signOutType,
isEditDisablie: whetherHaveApply === 'YES',
});
})
}
});
};
getTextDetail = (key, url) => {
$.ajax({
data: {},
type: 'GET',
url,
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
success: (res) => {
this.setState({ [key]: res, [`load${key}`]: true });
}
})
}
},
});
};
handleGoBack = () => {
const {
coverId,
videoName,
videoDuration,
courseName,
categoryId,
whetherVisitorsJoin
} = this.state;
if(videoName || videoDuration || categoryId || courseName || coverId || whetherVisitorsJoin !== whetherVisitorsJoin ){
const { coverId, videoName, videoDuration, courseName, categoryId, whetherVisitorsJoin } = this.state;
if (videoName || videoDuration || categoryId || courseName || coverId || whetherVisitorsJoin !== whetherVisitorsJoin) {
Modal.confirm({
title: '确认要返回吗?',
content: '返回后,本次编辑的内容将不被保存。',
okText: '确认返回',
cancelText: '留在本页',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>,
onOk: () => {
window.RCHistory.push({
pathname: `/offline-course`,
});
}
},
});
}else{
} else {
window.RCHistory.push({
pathname: `/offline-course`,
});
}
}
};
// 显示预览弹窗
handleShowPreviewModal = () => {
......@@ -339,29 +325,29 @@ class AddOfflineCourse extends React.Component {
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
}
};
const previewOfflineModal = (
<PreviewOfflineModal
data={data}
close={() => {
this.setState({
previewOfflineModal: null
})
previewOfflineModal: null,
});
}}
/>
);
this.setState({ previewOfflineModal });
}
};
handleSelectCover = (file)=> {
handleSelectCover = (file) => {
this.uploadCoverImage(file);
}
};
//上传图片
uploadCoverImage = (imageFile) => {
const { folderName } = imageFile;
const fileName = window.random_string(16) + folderName.slice(folderName.lastIndexOf("."));
const fileName = window.random_string(16) + folderName.slice(folderName.lastIndexOf('.'));
const self = this;
this.setState(
{
......@@ -369,56 +355,54 @@ class AddOfflineCourse extends React.Component {
},
() => {
setTimeout(() => {
const okBtnDom = document.querySelector("#headPicModal");
const okBtnDom = document.querySelector('#headPicModal');
const options = {
size: [500, 282],
ok: okBtnDom,
maxZoom: 3,
style: {
jpgFillColor: "transparent",
jpgFillColor: 'transparent',
},
done: function (dataUrl) {
clearTimeout(self.timer);
self.timer = setTimeout(() => {
if ((self.state.rotate != this.rotate()) || (self.state.scale != this.scale())) {
const _dataUrl = this.clip()
if (self.state.rotate != this.rotate() || self.state.scale != this.scale()) {
const _dataUrl = this.clip();
const cutImageBlob = self.convertBase64UrlToBlob(_dataUrl);
self.setState({
cutImageBlob,
dataUrl: _dataUrl,
rotate: this.rotate(),
scale: this.scale()
})
scale: this.scale(),
});
}
}, 500)
}, 500);
const cutImageBlob = self.convertBase64UrlToBlob(dataUrl);
self.setState({
cutImageBlob,
dataUrl
})
dataUrl,
});
setTimeout(() => {
cutFlag = false;
}, 2000);
},
fail: (failInfo) => {
message.error("图片上传失败了,请重新上传");
message.error('图片上传失败了,请重新上传');
},
loadComplete: function (img) {
setTimeout(() => {
const _dataUrl = this.clip()
const _dataUrl = this.clip();
self.setState({
dataUrl: _dataUrl,
hasImgReady: true
})
}, 100)
hasImgReady: true,
});
}, 100);
},
};
const imgUrl = `${imageFile.ossUrl}?${new Date().getTime()}`
const imgUrl = `${imageFile.ossUrl}?${new Date().getTime()}`;
if (!this.state.photoclip) {
const _photoclip = new PhotoClip("#headPicModal", options);
const _photoclip = new PhotoClip('#headPicModal', options);
_photoclip.load(imgUrl);
this.setState({
photoclip: _photoclip,
......@@ -427,7 +411,6 @@ class AddOfflineCourse extends React.Component {
this.state.photoclip.clear();
this.state.photoclip.load(imgUrl);
}
}, 200);
}
);
......@@ -435,53 +418,55 @@ class AddOfflineCourse extends React.Component {
//获取resourceId
getSignature = (blob, fileName) => {
Upload.uploadBlobToOSS(blob, 'cover' + (new Date()).valueOf(),null,'signInfo').then((signInfo) => {
this.setState({
coverClicpPath:signInfo.fileUrl,
coverId:signInfo.resourceId,
visible: false
},()=>this.updateCover())
Upload.uploadBlobToOSS(blob, 'cover' + new Date().valueOf(), null, 'signInfo').then((signInfo) => {
this.setState(
{
coverClicpPath: signInfo.fileUrl,
coverId: signInfo.resourceId,
visible: false,
},
() => this.updateCover()
);
});
};
updateCover = () =>{
const {coverClicpPath,coverId} = this.state
updateCover = () => {
const { coverClicpPath, coverId } = this.state;
this.setState({
showSelectCoverModal: false,
coverUrl:coverClicpPath,
coverId:coverId
})
}
coverUrl: coverClicpPath,
coverId: coverId,
});
};
// base64转换成blob
convertBase64UrlToBlob = (urlData) => {
const bytes = window.atob(urlData.split(",")[1]);
const bytes = window.atob(urlData.split(',')[1]);
const ab = new ArrayBuffer(bytes.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: "image/png" });
return new Blob([ab], { type: 'image/png' });
};
preSubmit = () => {
//过期判断
if (User.getExpirationTime() && moment().valueOf() > Number(User.getExpirationTime())) {
Modal.warning({
title:"服务已到期",
content: "当前企业购买的小麦企学院服务已到期,如需继续使用学院功能,请尽快续费购买",
okText: "我知道了"
})
return
title: '服务已到期',
content: '当前企业购买的小麦企学院服务已到期,如需继续使用学院功能,请尽快续费购买',
okText: '我知道了',
});
return;
}
const { courseId } = this.state;
if (courseId) {
this.checkDetail(courseId).then(bool => bool ? this.handleSubmit() : message.warning('课程已开始,无法继续编辑'))
this.checkDetail(courseId).then((bool) => (bool ? this.handleSubmit() : message.warning('课程已开始,无法继续编辑')));
} else {
this.handleSubmit();
}
}
};
// 保存
handleSubmit = () => {
......@@ -514,23 +499,24 @@ class AddOfflineCourse extends React.Component {
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
isMore,
// isMore,
editorTextLength,
} = this.state;
let coverObj ={
contentType:'COVER',
let coverObj = {
contentType: 'COVER',
mediaContent: coverId,
mediaType:'PICTURE',
mediaType: 'PICTURE',
mediaUrl: coverUrl,
}
};
let scheduleMediaRequests = [];
if(coverId){
scheduleMediaRequests = [coverObj]
if (coverId) {
scheduleMediaRequests = [coverObj];
}
// 编辑且使用默认图时不传
if (pageType === 'edit' && coverUrl === defaultCoverUrl) {
scheduleMediaRequests = []
scheduleMediaRequests = [];
}
const commonParams = {
categoryId,
......@@ -547,7 +533,8 @@ class AddOfflineCourse extends React.Component {
startTime,
endTime,
calendarTime,
isMore,
editorTextLength,
// isMore,
};
if (whetherSetApply === 'YES') {
......@@ -575,16 +562,21 @@ class AddOfflineCourse extends React.Component {
// 校验必填字段:课程名称, 课程线下
this.handleValidate(commonParams).then((res) => {
if (!res) return;
Upload.uploadTextToOSS(introduce, `${randomString()}.txt`, (introduceId) => {
Upload.uploadTextToOSS(
introduce,
`${randomString()}.txt`,
(introduceId) => {
this.submitRemote({
courseId,
pageType,
commonParams,
introduceId,
});
}, () => message.warning('上传课程简介失败'));
},
() => message.warning('上传课程简介失败')
);
});
}
};
submitRemote = (data) => {
const { courseId, pageType, commonParams, introduceId } = data;
......@@ -592,75 +584,78 @@ class AddOfflineCourse extends React.Component {
if (pageType === 'add') {
Service.Hades('public/hades/createOfflineCourse', commonParams).then((res) => {
if (!res) return;
message.success("新建成功");
message.success('新建成功');
window.RCHistory.push({
pathname: `/offline-course`,
});
})
});
} else {
const editParams = {
courseId:courseId,
courseId: courseId,
...commonParams,
}
};
Service.Hades('public/hades/updateOfflineCourse', editParams).then((res) => {
if (!res) return;
message.success("保存成功");
message.success('保存成功');
window.RCHistory.push({
pathname: `/offline-course`,
});
});
}
}
};
handleValidate = (data) => {
return new Promise((resolve) => {
if (!data.courseName) {
message.warning('请输入课程名称');
resolve(false);
} else if(!data.categoryId){
} else if (!data.categoryId) {
message.warning('请选择课程分类');
resolve(false);
} else if(!data.offlinePlace){
} else if (!data.offlinePlace) {
message.warning('请输入上课地点');
resolve(false);
} else if(!data.teacherId ){
} else if (!data.teacherId) {
message.warning('请选择讲师');
resolve(false);
} else if(_.isEmpty(data.calendarTime)){
} else if (_.isEmpty(data.calendarTime)) {
message.warning('请选择上课日期');
resolve(false);
} else if(!data.startTime || !data.endTime){
} else if (!data.startTime || !data.endTime) {
message.warning('请选择上课时间');
resolve(false);
} else if(moment(moment(data.calendarTime[0]).format('YYYY-MM-DD') + moment(data.startTime).format(' HH:mm')).valueOf() < Date.now()){
} else if (moment(moment(data.calendarTime[0]).format('YYYY-MM-DD') + moment(data.startTime).format(' HH:mm')).valueOf() < Date.now()) {
message.warning('上课时间不能早于现在');
resolve(false);
} else if(data.startTime >= data.endTime){
} else if (data.startTime >= data.endTime) {
message.warning('上课结束时间不能早于上课开始时间');
resolve(false);
} else if(data.whetherSetApply === 'YES' && !data.startTimeApply){
} else if (data.whetherSetApply === 'YES' && !data.startTimeApply) {
message.warning('请选择报名时间');
resolve(false);
} else if(data.whetherSetApply === 'YES' && data.startTimeApply >= data.endTimeApply){
} else if (data.whetherSetApply === 'YES' && data.startTimeApply >= data.endTimeApply) {
message.warning('报名结束时间需大于报名开始时间');
resolve(false);
} else if(data.whetherSetApply === 'YES' && data.endTimeApply > moment(moment(data.calendarTime[0]).format('YYYY-MM-DD') + moment(data.endTime).format(' HH:mm:ss')).valueOf()){
} else if (
data.whetherSetApply === 'YES' &&
data.endTimeApply > moment(moment(data.calendarTime[0]).format('YYYY-MM-DD') + moment(data.endTime).format(' HH:mm:ss')).valueOf()
) {
message.warning('报名结束时间需小于上课开始时间');
resolve(false);
} else if(data.whetherSetSignIn === 'YES' && !data.signInTimeNum){
} else if (data.whetherSetSignIn === 'YES' && !data.signInTimeNum) {
message.warning('请输入签到时间');
resolve(false);
} else if(data.whetherSetSignOut === 'YES' && ((data.signOutType === 'START_LATER' && !data.signOutStartTimeNum) || !data.signOutEndTimeNum)){
} else if (data.whetherSetSignOut === 'YES' && ((data.signOutType === 'START_LATER' && !data.signOutStartTimeNum) || !data.signOutEndTimeNum)) {
message.warning('请输入签退时间');
resolve(false);
} else if (data.isMore) {
message.warning('简介超过字数限定');
} else if (data.editorTextLength > 1000) {
message.warning('课程简介超过字数限定');
resolve(false);
} else {
resolve(true);
}
});
}
};
// 使用默认封面图
handleResetCoverUrl = () => {
......@@ -670,7 +665,7 @@ class AddOfflineCourse extends React.Component {
if (isDefaultCover) return;
message.success('已替换为默认图');
this.setState({ coverUrl: defaultCoverUrl });
}
};
// 滑动加载更多讲师列表
handleScrollTeacherList = (e) => {
......@@ -682,46 +677,51 @@ class AddOfflineCourse extends React.Component {
if (scrollToBottom && hasNext) {
const { teacherQuery } = this.state;
let _teacherQuery = teacherQuery;
_teacherQuery.current = _teacherQuery.current + 1
this.setState({
teacherQuery:{..._teacherQuery}
},()=>{this.getTeacherList(_teacherQuery.current)})
_teacherQuery.current = _teacherQuery.current + 1;
this.setState(
{
teacherQuery: { ..._teacherQuery },
},
() => {
this.getTeacherList(_teacherQuery.current);
}
);
}
};
getTeacherList(current = 1, selectList){
const { teacherQuery,teacherList} = this.state;
getTeacherList(current = 1, selectList) {
const { teacherQuery, teacherList } = this.state;
const _query = {
...teacherQuery,
current,
size:15
size: 15,
};
StoreService.getStoreUserBasicPage( _query).then((res) => {
StoreService.getStoreUserBasicPage(_query).then((res) => {
const { result = {} } = res;
const { records = [], total = 0, hasNext } = result;
const list = current > 1 ? teacherList.concat(records) : records;
this.setState({
hasNext,
teacherList: list,
teacherQuery:{..._query}
})
teacherQuery: { ..._query },
});
});
}
changeIntro = (value, textLength) => {
const isMore = textLength > 1000;
if (isMore) {
message.warning('内容过长,不能超过1000字');
}
this.setState({ introduce: value, isMore });
}
// const isMore = textLength > 1000;
// if (isMore) {
// message.warning('内容过长,不能超过1000字');
// }
this.setState({ introduce: value });
};
selectMultiDate = (calendarTime) => {
const dateList = _.sortBy(calendarTime);
this.setState({
calendarTime: dateList,
})
}
});
};
handleChangeDates = (dates) => {
const data = {};
......@@ -733,15 +733,15 @@ class AddOfflineCourse extends React.Component {
data.endTimeApply = dates[1].startOf('minute').valueOf() + 59000;
}
this.setState(data);
}
};
whetherVisitorsJoinChange = ()=>{
if(this.state.whetherVisitorsJoin === "NO"){
whetherVisitorsJoinChange = () => {
if (this.state.whetherVisitorsJoin === 'NO') {
this.setState({ whetherVisitorsJoin: 'YES' });
}else{
} else {
this.setState({ whetherVisitorsJoin: 'NO' });
}
}
};
handleChangeCatalogList = (value, label) => {
this.setState({ categoryId: value, categoryName: label[0] });
......@@ -789,64 +789,66 @@ class AddOfflineCourse extends React.Component {
} = this.state;
const isDefaultCover = coverUrl === defaultCoverUrl;
return (
<div className="page add-offline-course-page">
<Breadcrumbs
navList={pageType === "add" ? "新建线下课" : "编辑线下课"}
goBack={this.handleGoBack}
/>
<div className='page add-offline-course-page'>
<Breadcrumbs navList={pageType === 'add' ? '新建线下课' : '编辑线下课'} goBack={this.handleGoBack} />
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企学院保有依据国家规定及平台规则进行处理的权利" />
<div className='box'>
<div className='show-tips'>
<ShowTips message='请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企学院保有依据国家规定及平台规则进行处理的权利' />
</div>
<div className="form">
<div className="basic-info__wrap">
<div className="title">基本信息</div>
<div className="course-name">
<span className="label"><span className="require">*</span>课程名称:</span>
<div className='form'>
<div className='basic-info__wrap'>
<div className='title'>基本信息</div>
<div className='course-name'>
<span className='label'>
<span className='require'>*</span>课程名称:
</span>
<Input
value={courseName}
placeholder="请输入线下课的名称(40字以内)"
placeholder='请输入线下课的名称(40字以内)'
maxLength={40}
style={{ width: 240 }}
onChange={(e) => { this.setState({ courseName: e.target.value }) }}
onChange={(e) => {
this.setState({ courseName: e.target.value });
}}
/>
</div>
<div className="course-cover">
<span className="label">封面图:</span>
<div className='course-cover'>
<span className='label'>封面图:</span>
<div className="course-cover__wrap">
<div className="img-content">
{
isDefaultCover && <span className="tag">默认图</span>
}
<img src={coverUrl} />
</div>
<div className="opt-btns">
<Button onClick={() => {
<div className='course-cover__wrap'>
<div className='opt-btns'>
<Button
onClick={() => {
this.setState({
showSelectCoverModal: true
})
}}>上传图片</Button>
<span
className={`default-btn ${isDefaultCover ? 'disabled' : ''}`}
onClick={this.handleResetCoverUrl}
>使用默认图</span>
<div className="tips">建议尺寸1280*720px,图片支持jpg、jpeg、png格式。</div>
showSelectCoverModal: true,
});
}}>
上传图片
</Button>
<span className={`default-btn ${isDefaultCover ? 'disabled' : ''}`} onClick={this.handleResetCoverUrl}>
使用默认图
</span>
<div className='tips'>建议尺寸1280*720px,图片支持jpg、jpeg、png格式。</div>
</div>
<div className='img-content'>
{isDefaultCover && <span className='tag'>默认图</span>}
<img src={coverUrl} />
</div>
</div>
</div>
<div className="course-catalog">
<span className="label special"><span className="require">*</span>课程分类:</span>
<div className='course-catalog'>
<span className='label special'>
<span className='require'>*</span>课程分类:
</span>
<TreeSelect
showSearch
treeNodeFilterProp="title"
treeNodeFilterProp='title'
style={{ width: 240 }}
dropdownStyle={{ maxHeight: 300, overflow: "auto" }}
dropdownStyle={{ maxHeight: 300, overflow: 'auto' }}
treeData={courseCatalogList}
placeholder="请选择课程类型"
placeholder='请选择课程类型'
allowClear
value={categoryId}
treeDefaultExpandAll
......@@ -855,184 +857,182 @@ class AddOfflineCourse extends React.Component {
}}
/>
</div>
<div className="course-catalog">
<span className="label special"><span className="require">*</span>上课地点:</span>
<div className='course-catalog'>
<span className='label special'>
<span className='require'>*</span>上课地点:
</span>
<Input
value={offlinePlace}
maxLength={40}
style={{ width: 240 }}
placeholder="请输入上课地点(40字以内)"
placeholder='请输入上课地点(40字以内)'
onChange={(e) => {
this.setState({ offlinePlace: e.target.value })
this.setState({ offlinePlace: e.target.value });
}}
/>
</div>
<div className="course-catalog" id="teacher">
<span className="label special"><span className="require">* </span>讲师:</span>
<div className='course-catalog' id='teacher'>
<span className='label special'>
<span className='require'>* </span>讲师:
</span>
<Select
placeholder="请选择讲师"
placeholder='请选择讲师'
value={teacherId}
style={{ width: 240 }}
showSearch
allowClear
filterOption={(input, option) => option}
dropdownClassName="offline-dropdown-box"
dropdownClassName='offline-dropdown-box'
onPopupScroll={this.handleScrollTeacherList}
suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}
suffixIcon={
<span className='icon iconfont' style={{ fontSize: '12px', color: '#BFBFBF' }}>
&#xe835;
</span>
}
onChange={(value, option) => {
if (option) {
this.setState({ teacherId: value, teacherName: option.children });
}else{
this.setState({ teacherId: value, teacherName: "" });
} else {
this.setState({ teacherId: value, teacherName: '' });
}
}}
onSearch={(value) => {
let _teacherQuery = {...this.state.teacherQuery};
_teacherQuery.nickName = value
this.setState({
teacherQuery: _teacherQuery
}, () => {
this.getTeacherList()
})
let _teacherQuery = { ...this.state.teacherQuery };
_teacherQuery.nickName = value;
this.setState(
{
teacherQuery: _teacherQuery,
},
() => {
this.getTeacherList();
}
);
}}
onClear ={(value)=>{
this.setState({
teacherQuery:{
onClear={(value) => {
this.setState(
{
teacherQuery: {
size: 15,
current: 1,
nickName:null
}
}, () => {
this.getTeacherList()
})
}
}
getPopupContainer={() =>
document.getElementById("teacher")
nickName: null,
},
},
() => {
this.getTeacherList();
}
>
);
}}
getPopupContainer={() => document.getElementById('teacher')}>
{_.map(teacherList, (item, index) => {
return (
<Option value={item.id} key={item.id}>{item.nickName}</Option>
<Option value={item.id} key={item.id}>
{item.nickName}
</Option>
);
})}
</Select>
</div>
<div className="allow-tourist-join">
<span className="label">观看设置:</span>
<div className="content">
<div>
<Switch
checked={whetherVisitorsJoin === "YES" ? true : false}
onChange={this.whetherVisitorsJoinChange}
/>
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的学员观看</div>
<div>关闭:仅限绑定了手机号的学员可以进入观看线下课</div>
</div>
<div className='allow-tourist-join'>
<span className='label'>观看设置:</span>
<div className='content'>
<Switch checked={whetherVisitorsJoin === 'NO' ? true : false} onChange={this.whetherVisitorsJoinChange} />
<div className='desc'>{whetherVisitorsJoin === 'NO' ? '已开启,学员需绑定手机号才可观看' : '已关闭,学员无需绑定手机号即可观看'}</div>
</div>
</div>
</div>
<div className="introduce">
<span className="label">课程简介:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item introduce-editor">
{(!courseId || loadintroduce) &&
<div className='introduce'>
<span className='label'>课程简介:</span>
<div className='content'>
<div className='intro-list'>
<div className='intro-list__item introduce-editor'>
{(!courseId || loadintroduce) && (
<GraphicsEditor
id="intro"
id='intro'
isIntro={true}
maxLimit={1000}
detail={{
content: introduce
content: introduce,
}}
onChange={(val, textLength) => {
this.changeIntro(val, textLength)
onChange={(val) => {
this.changeIntro(val);
}}
/>
}
)}
</div>
</div>
</div>
</div>
<div className="title" style={{ marginTop: 24 }}>课程设置</div>
<div className="day">
<span className="label">
<span className="require">*</span>
<div className='title' style={{ marginTop: 24 }}>
课程设置
</div>
<div className='day'>
<span className='label'>
<span className='require'>*</span>
上课日期:
</span>
<div>
<div className='select-day'>
已选 <span className="mark-day">{isLongArr(calendarTime) ? calendarTime.length : 0}</span>
已选 <span className='mark-day'>{isLongArr(calendarTime) ? calendarTime.length : 0}</span>
</div>
<MultipleDatePicker
disabled={isEditDisablie}
selectDateList={calendarTime}
onSelect={this.selectMultiDate}
canSelectTodayBefore={false}
/>
<MultipleDatePicker disabled={isEditDisablie} selectDateList={calendarTime} onSelect={this.selectMultiDate} canSelectTodayBefore={false} />
</div>
</div>
<div className="hour" id="hour">
<span className="label"><span className="require">*</span>上课时间:</span>
<div className='hour' id='hour'>
<span className='label'>
<span className='require'>*</span>上课时间:
</span>
<TimePicker
disabled={isEditDisablie}
className="time-picker"
format="HH:mm"
className='time-picker'
format='HH:mm'
value={startTime ? moment(startTime) : null}
placeholder="开始时间"
placeholder='开始时间'
showNow={false}
style={{ width: 100, minWidth: 100}}
style={{ width: 100, minWidth: 100 }}
onSelect={(time) => {
this.setState({ startTime: time });
}}
getPopupContainer={() =>
document.getElementById("hour")
}
/>&nbsp;&nbsp;~&nbsp;&nbsp;
getPopupContainer={() => document.getElementById('hour')}
/>
&nbsp;&nbsp;~&nbsp;&nbsp;
<TimePicker
disabled={isEditDisablie}
className="time-picker"
format="HH:mm"
className='time-picker'
format='HH:mm'
value={endTime ? moment(endTime) : null}
placeholder="结束时间"
placeholder='结束时间'
showNow={false}
style={{ width: 100, minWidth: 100 }}
onSelect={(time) => {
this.setState({ endTime: time });
}}
getPopupContainer={() =>
document.getElementById("hour")
}
getPopupContainer={() => document.getElementById('hour')}
/>
</div>
<div className="course-catalog">
<span className="label"><span className="require">* </span>学员上课方式:</span>
<div className='course-catalog'>
<span className='label'>
<span className='require'>* </span>学员上课方式:
</span>
<Radio.Group
style={{ display: 'inline-block' }}
value={offlineCourseType}
onChange={(e) => {
this.setState({ offlineCourseType: e.target.value });
}}
className="mt5"
disabled={isEditDisablie}
>
<Radio value="ALL_DAY_OFFLINE" className="mr-16">
<span style={{ color: "#333" }}>所选日期都要上课</span>
className='mt5'
disabled={isEditDisablie}>
<Radio value='ALL_DAY_OFFLINE' className='mr-16'>
<span style={{ color: '#333' }}>所选日期都要上课</span>
</Radio>
<Radio value="ANY_DAY_POFFLINE" className="mr-16">
<span style={{ color: "#333" }}>选择任意1天上课</span>
<Radio value='ANY_DAY_POFFLINE' className='mr-16'>
<span style={{ color: '#333' }}>选择任意1天上课</span>
</Radio>
</Radio.Group>
</div>
<div className="course-catalog">
<span className="label">课程报名:</span>
<div className="switch-box">
<div className="switch-item" key="1">
<div className='course-catalog'>
<span className='label'>课程报名:</span>
<div className='switch-box'>
<div className='switch-item' key='1'>
<Switch
disabled={isEditDisablie}
checked={whetherSetApply === 'YES'}
......@@ -1045,56 +1045,94 @@ class AddOfflineCourse extends React.Component {
});
}}
/>
<span className="switch-tip">开启后可设置课程报名时间,获取报名数据</span>
<span className='switch-tip'>开启后可设置课程报名时间,获取报名数据</span>
</div>
{whetherSetApply === 'YES' && <div className="switch-item" key="2">
<span className="switch-label">报名日期:</span>
{whetherSetApply === 'YES' && (
<div className='switch-item' key='2'>
<span className='switch-label'>报名日期:</span>
<RangePicker
id="course_date_picker"
id='course_date_picker'
showTime={{ showTime: 'HH:mm' }}
allowClear={false}
value={startTimeApply ? [moment(startTimeApply), moment(endTimeApply)] : null }
format={"YYYY-MM-DD HH:mm"}
onChange={(dates) => { this.handleChangeDates(dates) }}
renderExtraFooter={() => calendarTime[0] ? <div style={{ position: 'absolute', bottom: 8, cursor: 'pointer' }}>
value={startTimeApply ? [moment(startTimeApply), moment(endTimeApply)] : null}
format={'YYYY-MM-DD HH:mm'}
onChange={(dates) => {
this.handleChangeDates(dates);
}}
renderExtraFooter={() =>
calendarTime[0] ? (
<div style={{ position: 'absolute', bottom: 8, cursor: 'pointer' }}>
<span
onClick={() => this.setState({ startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).subtract(1, 'days').valueOf(), endTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000 })}
onClick={() =>
this.setState({
startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`)
.subtract(1, 'days')
.valueOf(),
endTimeApply:
moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000,
})
}
style={{
color: '#2966FF',
border: '1px solid #2966FF',
padding: '2px 8px',
borderRadius: '2px',
marginRight: 8,
}}
>上课前1天</span>
}}>
上课前1天
</span>
<span
onClick={() => this.setState({ startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).subtract(2, 'days').valueOf(), endTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000 })}
onClick={() =>
this.setState({
startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`)
.subtract(2, 'days')
.valueOf(),
endTimeApply:
moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000,
})
}
style={{
color: '#2966FF',
border: '1px solid #2966FF',
padding: '2px 8px',
borderRadius: '2px',
marginRight: 8,
}}
>上课前2天</span>
}}>
上课前2天
</span>
<span
onClick={() => this.setState({ startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).subtract(3, 'days').valueOf(), endTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000 })}
onClick={() =>
this.setState({
startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`)
.subtract(3, 'days')
.valueOf(),
endTimeApply:
moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000,
})
}
style={{
color: '#2966FF',
border: '1px solid #2966FF',
padding: '2px 8px',
borderRadius: '2px',
marginRight: 8,
}}
>上课前3天</span>
</div> : null}
}}>
上课前3天
</span>
</div>
) : null
}
/>
</div>}
{whetherSetApply === 'YES' && <div className="switch-item" key="3">
<span className="switch-label">
</div>
)}
{whetherSetApply === 'YES' && (
<div className='switch-item' key='3'>
<span className='switch-label'>
报名人数
<Tooltip title="报名一旦开始,报名人数不支持减少">
<span style={{ margin: '0 4px', color: '#999' }} className="icon iconfont">&#xe7c4;</span>
<Tooltip title='报名一旦开始,报名人数不支持减少'>
<span style={{ margin: '0 4px', color: '#999' }} className='icon iconfont'>
&#xe7c4;
</span>
</Tooltip>
:最多
</span>
......@@ -1106,18 +1144,19 @@ class AddOfflineCourse extends React.Component {
style={{ margin: '0 4px', width: 90 }}
disabled={oldQuta < 0}
onChange={(value) => {
this.setState({ quota: value })
this.setState({ quota: value });
}}
/>
<span className="switch-label"></span>
<span className="switch-tip">未填写时默认为不限制</span>
</div>}
<span className='switch-label'></span>
<span className='switch-tip'>未填写时默认为不限制</span>
</div>
)}
</div>
</div>
<div className="course-catalog">
<span className="label">考勤签到:</span>
<div className="switch-box">
<div className="switch-item" key="1">
<div className='course-catalog'>
<span className='label'>考勤签到:</span>
<div className='switch-box'>
<div className='switch-item' key='1'>
<Switch
checked={whetherSetSignIn === 'YES'}
onChange={(value) => {
......@@ -1126,31 +1165,33 @@ class AddOfflineCourse extends React.Component {
signInType: 'START_AGO',
signInTimeNum: null,
signInTimeUnit: 'MINUTE',
})
});
}}
/>
<span className="switch-tip">开启后可设置获取签到考勤数据</span>
<span className='switch-tip'>开启后可设置获取签到考勤数据</span>
</div>
{whetherSetSignIn === 'YES' && <div className="switch-item" key="2">
<span className="switch-label">签到时间:</span>
{whetherSetSignIn === 'YES' && (
<div className='switch-item' key='2'>
<span className='switch-label'>签到时间:</span>
<Radio.Group
style={{ display: 'inline-block' }}
value={signInType}
onChange={(e) => {
this.setState({ signInType: e.target.value });
}}
className="mt5"
>
<Radio value="START_AGO" className="mr-16">
<span style={{ color: "#333" }}>课程开始前</span>
className='mt5'>
<Radio value='START_AGO' className='mr-16'>
<span style={{ color: '#333' }}>课程开始前</span>
</Radio>
<Radio value="END_AGO" className="mr-16">
<span style={{ color: "#333" }}>课程结束前</span>
<Radio value='END_AGO' className='mr-16'>
<span style={{ color: '#333' }}>课程结束前</span>
</Radio>
</Radio.Group>
</div>}
{whetherSetSignIn === 'YES' && <div className="switch-item" key="3">
<span className="switch-label">课程{signInType === 'START_AGO' ? '开始' : '结束'}</span>
</div>
)}
{whetherSetSignIn === 'YES' && (
<div className='switch-item' key='3'>
<span className='switch-label'>课程{signInType === 'START_AGO' ? '开始' : '结束'}</span>
<InputNumber
value={signInTimeNum}
min={1}
......@@ -1158,32 +1199,34 @@ class AddOfflineCourse extends React.Component {
precision={0}
style={{ margin: '0 4px', width: 90 }}
onChange={(value) => {
this.setState({ signInTimeNum: value })
this.setState({ signInTimeNum: value });
}}
/>
<Select
style={{ width: 72, marginRight: 4 }}
value={signInTimeUnit}
onChange={(value) => {
const data = { signInTimeUnit: value }
const data = { signInTimeUnit: value };
if (value === 'HOUR' && signInTimeNum > 24) {
data.signInTimeNum = 24;
}
this.setState(data);
}}
>
{unitList.map(item => (
<Option value={item.key} key={item.key}>{item.value}</Option>
}}>
{unitList.map((item) => (
<Option value={item.key} key={item.key}>
{item.value}
</Option>
))}
</Select>
<span className="switch-label">内可签到</span>
</div>}
<span className='switch-label'>内可签到</span>
</div>
)}
</div>
</div>
<div className="course-catalog">
<span className="label">考勤签退:</span>
<div className="switch-box">
<div className="switch-item" key="1">
<div className='course-catalog'>
<span className='label'>考勤签退:</span>
<div className='switch-box'>
<div className='switch-item' key='1'>
<Switch
checked={whetherSetSignOut === 'YES'}
onChange={(value) => {
......@@ -1194,57 +1237,64 @@ class AddOfflineCourse extends React.Component {
signOutStartTimeUnit: 'MINUTE',
signOutEndTimeNum: null,
signOutEndTimeUnit: 'MINUTE',
})
});
}}
/>
<span className="switch-tip">开启后可设置获取签退考勤数据</span>
<span className='switch-tip'>开启后可设置获取签退考勤数据</span>
</div>
{whetherSetSignOut === 'YES' && <div className="switch-item" key="2">
<span className="switch-label">签退时间:</span>
{whetherSetSignOut === 'YES' && (
<div className='switch-item' key='2'>
<span className='switch-label'>签退时间:</span>
<Radio.Group
style={{ display: 'inline-block' }}
value={signOutType}
onChange={(e) => {
this.setState({ signOutType: e.target.value });
}}
className="mt5"
>
<Radio value="START_LATER" className="mr-16">
<span style={{ color: "#333" }}>课程开始后</span>
className='mt5'>
<Radio value='START_LATER' className='mr-16'>
<span style={{ color: '#333' }}>课程开始后</span>
</Radio>
<Radio value="END_LATER" className="mr-16">
<span style={{ color: "#333" }}>课程结束后</span>
<Radio value='END_LATER' className='mr-16'>
<span style={{ color: '#333' }}>课程结束后</span>
</Radio>
</Radio.Group>
</div>}
{whetherSetSignOut === 'YES' && <div className="switch-item" key="3">
<span className="switch-label">课程{signOutType === 'START_LATER' ? '开始' : '结束'}</span>
{signOutType === 'START_LATER' && <InputNumber
</div>
)}
{whetherSetSignOut === 'YES' && (
<div className='switch-item' key='3'>
<span className='switch-label'>课程{signOutType === 'START_LATER' ? '开始' : '结束'}</span>
{signOutType === 'START_LATER' && (
<InputNumber
value={signOutStartTimeNum}
min={1}
max={signOutStartTimeUnit === 'MINUTE' ? 1440 : 24}
precision={0}
style={{ margin: '0 4px', width: 90 }}
onChange={(value) => {
this.setState({ signOutStartTimeNum: value })
this.setState({ signOutStartTimeNum: value });
}}
/>}
{signOutType === 'START_LATER' && <Select
/>
)}
{signOutType === 'START_LATER' && (
<Select
style={{ width: 72, marginRight: 4 }}
value={signOutStartTimeUnit}
onChange={(value) => {
const data = { signOutStartTimeUnit: value }
const data = { signOutStartTimeUnit: value };
if (value === 'HOUR' && signOutStartTimeNum > 24) {
data.signOutStartTimeNum = 24;
}
this.setState(data);
}}
>
{unitList.map(item => (
<Option value={item.key} key={item.key}>{item.value}</Option>
}}>
{unitList.map((item) => (
<Option value={item.key} key={item.key}>
{item.value}
</Option>
))}
</Select>}
{signOutType === 'START_LATER' && <span className="switch-label">就可签退,截止签退时间为下课后</span>}
</Select>
)}
{signOutType === 'START_LATER' && <span className='switch-label'>就可签退,截止签退时间为下课后</span>}
<InputNumber
value={signOutEndTimeNum}
min={1}
......@@ -1252,77 +1302,78 @@ class AddOfflineCourse extends React.Component {
precision={0}
style={{ margin: '0 4px', width: 90 }}
onChange={(value) => {
this.setState({ signOutEndTimeNum: value })
this.setState({ signOutEndTimeNum: value });
}}
/>
<Select
style={{ width: 72, marginRight: 4 }}
value={signOutEndTimeUnit}
onChange={(value) => {
const data = { signOutEndTimeUnit: value }
const data = { signOutEndTimeUnit: value };
if (value === 'HOUR' && signOutEndTimeNum > 24) {
data.signOutEndTimeNum = 24;
}
this.setState(data);
}}
>
{unitList.map(item => (
<Option value={item.key} key={item.key}>{item.value}</Option>
}}>
{unitList.map((item) => (
<Option value={item.key} key={item.key}>
{item.value}
</Option>
))}
</Select>
{signOutType !== 'START_LATER' &&
<span className="switch-label">内可签退</span>
}
</div>}
{signOutType !== 'START_LATER' && <span className='switch-label'>内可签退</span>}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<div className="footer">
<div className='footer'>
<Button onClick={this.handleGoBack}>取消</Button>
<Button onClick={this.handleShowPreviewModal}>预览</Button>
<Button type="primary" onClick={_.debounce(() => this.preSubmit(), 3000, true)}>保存</Button>
<Button type='primary' onClick={_.debounce(() => this.preSubmit(), 3000, true)}>
保存
</Button>
</div>
{showSelectCoverModal &&
{showSelectCoverModal && (
<SelectPrepareFileModal
key="basic"
operateType="select"
key='basic'
operateType='select'
multiple={false}
accept="image/jpeg,image/png,image/jpg"
accept='image/jpeg,image/png,image/jpg'
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectCoverModal}
onClose={() => {
this.setState({ showSelectCoverModal: false })
this.setState({ showSelectCoverModal: false });
}}
onSelect={this.handleSelectCover}
/>
}
)}
<Modal
title="设置图片"
title='设置图片'
width={1080}
visible={visible}
maskClosable={false}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
closeIcon={<span className='icon iconfont modal-close-icon'>&#xe6ef;</span>}
onCancel={() => {
this.setState({ visible: false });
}}
zIndex={10001}
footer={[
<Button
key="back"
key='back'
onClick={() => {
this.setState({ visible: false });
}}
>
}}>
重新上传
</Button>,
<Button
key="submit"
type="primary"
key='submit'
type='primary'
disabled={!hasImgReady}
onClick={() => {
if (!cutFlag) {
......@@ -1330,39 +1381,36 @@ class AddOfflineCourse extends React.Component {
this.refs.hiddenBtn.click();
}
this.getSignature(cutImageBlob);
}}
>
}}>
确定
</Button>,
]}
>
<div className="clip-box">
]}>
<div className='clip-box'>
<div
id="headPicModal"
ref="headPicModal"
id='headPicModal'
ref='headPicModal'
style={{
width: "500px",
height: "430px",
width: '500px',
height: '430px',
marginBottom: 0,
}}
></div>
<div id="clipBtn" style={{ display: "none" }} ref="hiddenBtn"></div>
<div className="preview-img">
<div className="title">效果预览</div>
<div id="preview-url-box" style={{width:500,height:282}}>
<img src={this.state.dataUrl} style={{ width: '100%' }} alt="" />
}}></div>
<div id='clipBtn' style={{ display: 'none' }} ref='hiddenBtn'></div>
<div className='preview-img'>
<div className='title'>效果预览</div>
<div id='preview-url-box' style={{ width: 500, height: 282 }}>
<img src={this.state.dataUrl} style={{ width: '100%' }} alt='' />
</div>
<div className="tip-box">
<div className="tip">温馨提示</div>
<div className="tip">①预览效果图时可能存在延迟,单击左侧图片刷新即可</div>
<div className="tip">②设置图片时双击可旋转图片,滚动可放大或缩小图片</div>
<div className='tip-box'>
<div className='tip'>温馨提示</div>
<div className='tip'>①预览效果图时可能存在延迟,单击左侧图片刷新即可</div>
<div className='tip'>②设置图片时双击可旋转图片,滚动可放大或缩小图片</div>
</div>
</div>
</div>
</Modal>
{ this.state.previewOfflineModal }
{this.state.previewOfflineModal}
</div>
)
);
}
}
......
.add-offline-course-page {
position:relative !important;
.box{
margin-bottom:52px !important;
position: relative !important;
.box {
margin-bottom: 52px !important;
}
.ant-radio-group {
display: flex;
......@@ -39,18 +39,18 @@
position: relative;
padding-left: 14px;
&::before {
content: "";
content: '';
position: absolute;
left: 0px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 10px;
background: #2966FF;
background: #2966ff;
}
}
}
.label{
.label {
display: inline-block;
text-align: right;
width: 120px;
......@@ -75,8 +75,8 @@
}
}
.course-catalog {
margin-bottom:16px;
margin-top:16px;
margin-bottom: 16px;
margin-top: 16px;
display: flex;
.switch-box {
.switch-item {
......@@ -110,19 +110,19 @@
.select-day {
margin-bottom: 4px;
.mark-day {
color: #2966FF;
color: #2966ff;
}
}
}
.allow-tourist-join{
display:flex;
margin-bottom:16px;
.allow-tourist-join {
display: flex;
margin-bottom: 16px;
.desc {
color:#999;
margin-left:12px;
color: #999;
margin-left: 12px;
}
.content{
display:flex;
.content {
display: flex;
}
}
.course-ware {
......@@ -150,45 +150,41 @@
&__wrap {
position: relative;
.tag {
border-radius: 2px;
background: #D6D6D6;
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #FFF;
position: absolute;
top: 8px;
left: 8px;
}
}
.course-cover__wrap {
display: flex;
flex-direction: row;
}
.img-content {
margin-top: 8px;
margin-right: 20px;
width: 299px;
height: 169px;
position: relative;
img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 4px;
}
.tag {
border-radius: 2px;
background: #d6d6d6;
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #fff;
position: absolute;
top: 8px;
left: 8px;
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #2966FF;
margin-left: 14px;
color: #2966ff;
cursor: pointer;
&.disabled {
color: #CCC;
color: #ccc;
cursor: not-allowed;
}
}
......@@ -196,13 +192,14 @@
.ant-upload-list {
display: none;
}
}
.tips {
margin-top: 8px;
color: #999;
}
}
}
}
.select-student {
align-items: center;
......@@ -236,7 +233,7 @@
justify-content: flex-end;
padding-right: 72px;
background: #fff;
border-top: 1px solid #E8E8E8;
border-top: 1px solid #e8e8e8;
z-index: 999;
.ant-btn {
margin-left: 10px;
......
......@@ -109,7 +109,7 @@ class AddGraphicsIntro extends React.Component {
const {data: { id, whetherVisitorsJoin, courseMedia, introduce, shelfState, loadcourseMedia, loadintroduce } } = this.props;
const { showSelectFileModal, selectType } = this.state;
return (
<div className="add-video__intro-info">
<div className="add-offline__intro-info">
<div className="allow-tourist-join">
<span className="label">观看设置:</span>
<div className="content">
......
.add-video__intro-info {
.add-offline__intro-info {
.w-e-full-screen-editor {
background: #fff !important;
}
......
......@@ -3,7 +3,7 @@
* @Date: 2020-08-05 10:12:45
* @LastEditors: Please set LastEditors
* @LastEditTime: 2021-06-11 16:44:42
* @Description: 视频课-列表模块
* @Description: 线上课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import Service from '@/common/js/service';
......@@ -29,7 +29,7 @@ class OfflineCourseList extends React.Component {
constructor(props) {
super(props);
this.state = {
id: '', // 视频课ID
id: '', // 线上课ID
studentIds: [],
};
}
......
......@@ -3,7 +3,7 @@
* @Date: 2020-08-05 10:12:15
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-26 16:07:27
* @Description: 视频课-操作模块
* @Description: 线上课-操作模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......
......@@ -14,7 +14,7 @@ const hasExportPermission = (type) => {
return Permission.hasInteractiveExport();
}
// 视频课导出权限
// 线上课导出权限
if (type === 'videoClass') {
return Permission.hasVideoClassExport();
}
......
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:07:47
* @LastEditors: wufan
* @LastEditTime: 2021-05-30 20:35:49
* @Description: 视频课新增/编辑页
* @LastEditors: yuananting
* @LastEditTime: 2021-07-07 15:01:40
* @Description: 线上课新增/编辑页
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react'
import { Button, Input, Radio, message, Modal, Cascader } from 'antd'
import { DISK_MAP, FileTypeIcon, FileVerifyMap } from '@/common/constants/academic/lessonEnum'
import { ImgCutModalNew } from '@/components'
import { Button, Input, message, Modal, Cascader, Tooltip, Form, Popconfirm } from 'antd'
import { FileTypeIcon, FileVerifyMap } from '@/common/constants/academic/lessonEnum'
import ShowTips from '@/components/ShowTips'
import Breadcrumbs from '@/components/Breadcrumbs'
import moment from 'moment'
......@@ -27,9 +25,10 @@ import _ from 'underscore'
import Upload from '@/core/upload'
import { randomString } from '@/domains/basic-domain/utils'
import $ from 'jquery'
// import PhotoClip from 'photoclip';
import './AddVideoCourse.less'
import Bus from '@/core/bus'
const { TextArea } = Input;
const EDIT_BOX_KEY = Math.random()
const fieldNames = { label: 'categoryName', value: 'id', children: 'sonCategoryList' }
......@@ -54,18 +53,17 @@ class AddVideoCourse extends React.Component {
const pageType = getParameterByName('type')
this.state = {
id, // 视频课ID,编辑的时候从URL上带过来
id, // 线上课ID,编辑的时候从URL上带过来
pageType, // 页面类型: add->新建 edit->编辑
imageFile: null, // 需要被截取的图片
courseName: null, // 视频课名称
scheduleVideoId: null, // 视频课链接
courseName: null, // 线上课名称
coverId: null, // 视频封面的recourceId
coverUrl: null, // 视频课封面
coverUrl: null, // 线上课封面
studentList: [], // 上课学员列表
shelfState: 'YES', //是否开启学院展示
scheduleMedia: [
{
// 视频课媒体资源
// 线上课媒体资源
contentType: 'INTRO',
mediaType: 'TEXT',
mediaContent: '',
......@@ -83,7 +81,10 @@ class AddVideoCourse extends React.Component {
whetherVisitorsJoin: 'NO', // 是否允许游客加入
showSelectCoverModal: false,
cutImageBlob: null,
introduce: ''
introduce: '',
courseChapterList:[
], // 课节列表
videoType: "MP4",
}
}
......@@ -93,6 +94,11 @@ class AddVideoCourse extends React.Component {
if (pageType === 'edit') {
this.handleFetchScheudleDetail(id)
}
Bus.bind('editorLimit', (editorTextLength) => {
this.setState({
editorTextLength,
});
});
}
//获取分类列表
......@@ -104,34 +110,32 @@ class AddVideoCourse extends React.Component {
})
}
catalogChange = (value) => {
catalogChange = (value, categoryName) => {
const changeValueLength = value.length
switch (changeValueLength) {
case 1:
this.setState({ categoryId: value[0] })
this.setState({ categoryId: value[0], categoryName })
break
case 2:
this.setState({ categoryId: value[1] })
this.setState({ categoryId: value[1], categoryName })
break
default:
this.setState({ categoryId: null })
break
}
}
// 获取视频课详情
// 获取线上课详情
handleFetchScheudleDetail = (courseId) => {
CourseService.videoScheduleDetail({
courseId
}).then((res) => {
const { result = {} } = res || {}
const { courseName, shelfState, whetherVisitorsJoin, courseMediaVOS, categoryOneName, categoryTwoName, categoryId } = result
const { courseName, shelfState, whetherVisitorsJoin, courseMediaVOS, categoryOneName, categoryTwoName, categoryId, courseChapterVOList =[] } = result
let coverId
let coverUrl
let videoType
let videoDuration
let videoName
let scheduleMedia = []
let scheduleVideoId
let scheduleVideoUrl
let hasIntro
......@@ -141,13 +145,6 @@ class AddVideoCourse extends React.Component {
coverId = item.mediaContent
coverUrl = item.mediaUrl
break
case 'SCHEDULE':
videoDuration = item.videoDuration
videoName = item.mediaName
scheduleVideoId = item.mediaContent
scheduleVideoUrl = item.mediaUrl
videoType = item.mediaType
break
case 'INTRO':
hasIntro = true
this.getTextDetail('introduce', item.mediaUrl)
......@@ -164,21 +161,27 @@ class AddVideoCourse extends React.Component {
} else {
categoryName = `${categoryOneName}`
}
const _courseChapterVOList = courseChapterVOList.map(item => {
item.mediaName = item.name;
item.mediaNameAlias = item.name;
return item
})
this.setState({
loadintroduce: !hasIntro,
coverId,
coverUrl,
videoType,
videoName,
videoDuration,
scheduleMedia,
courseName,
scheduleVideoId,
scheduleVideoUrl,
shelfState,
whetherVisitorsJoin,
categoryName,
categoryId
categoryId,
courseChapterList: _courseChapterVOList
})
})
}
......@@ -196,11 +199,11 @@ class AddVideoCourse extends React.Component {
}
handleGoBack = () => {
const { coverId, videoName, videoDuration, courseName, scheduleMedia, scheduleVideoId, categoryId, shelfState, whetherVisitorsJoin } = this.state
const { coverId, videoName, videoDuration, courseName, scheduleMedia, courseChapterList, categoryId, shelfState, whetherVisitorsJoin } = this.state
if (
videoName ||
videoDuration ||
scheduleVideoId ||
!courseChapterList.length ||
!_.isEqual(scheduleMedia, defaultScheduleMedia) ||
categoryId ||
courseName ||
......@@ -269,7 +272,7 @@ class AddVideoCourse extends React.Component {
// 显示预览弹窗
handleShowPreviewModal = () => {
const { coverUrl, scheduleVideoUrl, courseName, scheduleMedia, videoDuration, introduce } = this.state
const { coverUrl, scheduleVideoUrl, courseName, scheduleMedia, videoDuration, introduce, courseChapterList, categoryName } = this.state
const courseBasinInfo = {
coverUrl,
......@@ -279,7 +282,8 @@ class AddVideoCourse extends React.Component {
}
const courseIntroInfo = {
liveCourseMediaRequests: scheduleMedia,
introduce
introduce,
categoryName
}
const previewCourseModal = (
......@@ -292,6 +296,7 @@ class AddVideoCourse extends React.Component {
previewCourseModal: null
})
}}
courseChapterList={courseChapterList}
/>
)
......@@ -299,24 +304,76 @@ class AddVideoCourse extends React.Component {
}
// 选择视频
handleSelectVideo = (file) => {
handleSelectVideo = (addFolderIds,selectedFileList) => {
console.log("file-----",selectedFileList);
this.setState({
showSelectFileModal: false
})
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file
let { courseChapterList } = this.state;
let _courseChapterList = [...courseChapterList];
selectedFileList.map((file,index) => {
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
const videoDom = document.createElement('video')
videoDom.src = ossUrl
videoDom.onloadedmetadata = () => {
_courseChapterList.push({
mediaContent: resourceId,
contentType: 'SCHEDULE',
mediaType: "VIDEO",
mediaName: folderName.replace('.mp4',''),
mediaNameAlias: folderName.replace('.mp4',''),
videoDuration: videoDom.duration,
id: resourceId,
mediaUrl: ossUrl,
sort: _courseChapterList.length
})
this.setState({
size: folderSize,
videoName: folderName,
videoType: folderFormat,
scheduleVideoUrl: ossUrl,
scheduleVideoId: resourceId,
videoDuration: videoDom.duration
courseChapterList: _courseChapterList
})
}
})
}
// 校验课节名称
handleValidateChapterName = (chapterName)=> {
let hasError = false;
return new Promise((resolve) => {
if(!chapterName) {
this.setState({
chapterNameValidateStatus: "error",
chapterNameHelpMsg: '请输入课节名称'
})
hasError = true;
resolve(false)
return false
}
if(chapterName.length > 40) {
this.setState({
chapterNameValidateStatus: "error",
chapterNameHelpMsg: '不要超过40字'
})
hasError = true;
resolve(false)
return false
}
if(!hasError){
resolve(true)
this.setState({
chapterNameValidateStatus: "",
chapterNameHelpMsg: ""
})
}
})
}
// 保存
......@@ -334,28 +391,23 @@ class AddVideoCourse extends React.Component {
const {
id,
size,
coverId,
coverUrl,
pageType,
joinType,
videoName,
videoDuration,
studentList,
courseName,
scheduleMedia,
scheduleVideoId,
scheduleVideoUrl,
categoryId,
shelfState,
whetherVisitorsJoin,
introduce
introduce,
courseChapterList,
editorTextLength,
} = this.state
const commonParams = {
videoName,
videoDuration,
courseMediaId: scheduleVideoId,
scheduleMedia: scheduleMedia.filter((item) => !!item.mediaContent),
categoryId,
courseName,
......@@ -364,10 +416,11 @@ class AddVideoCourse extends React.Component {
storeId: User.getStoreId(),
shelfState,
whetherVisitorsJoin,
courseType: 'VOICE'
courseType: 'VOICE',
courseChapterList
}
// 校验必填字段:课程名称, 课程视频
this.handleValidate(courseName, scheduleVideoId, categoryId, scheduleMedia).then((res) => {
this.handleValidate(courseName, courseChapterList, categoryId, scheduleMedia, editorTextLength).then((res) => {
if (!res) return
Upload.uploadTextToOSS(introduce, `${randomString()}.txt`, (introduceId) => {
this.submitRemote({ id, pageType, commonParams: { ...commonParams, introduceId } })
......@@ -377,7 +430,7 @@ class AddVideoCourse extends React.Component {
submitRemote = ({ id, pageType, commonParams }) => {
if (pageType === 'add') {
Service.Hades('public/hades/createMediaCourse', commonParams).then((res) => {
Service.Hades('public/hades/createVideoSchedule', commonParams).then((res) => {
if (!res) return
message.success('新建成功')
window.RCHistory.push({
......@@ -389,7 +442,7 @@ class AddVideoCourse extends React.Component {
courseId: id,
...commonParams
}
Service.Hades('public/hades/editMediaCourse', editParams).then((res) => {
Service.Hades('public/hades/editVideoSchedule', editParams).then((res) => {
if (!res) return
message.success('保存成功')
window.RCHistory.push({
......@@ -399,14 +452,14 @@ class AddVideoCourse extends React.Component {
}
}
handleValidate = (courseName, scheduleVideoId, categoryId, scheduleMedia) => {
handleValidate = (courseName, courseChapterList, categoryId, scheduleMedia, editorTextLength) => {
return new Promise((resolve) => {
if (!courseName) {
message.warning('请输入课程名称')
resolve(false)
return false
}
if (!scheduleVideoId) {
if (!courseChapterList.length) {
message.warning('请上传视频')
resolve(false)
return false
......@@ -416,14 +469,19 @@ class AddVideoCourse extends React.Component {
resolve(false)
return false
}
const textMedia = scheduleMedia.filter((item) => item.mediaType === 'TEXT')
for (let i = 0, len = textMedia.length; i < len; i++) {
if (textMedia[i].mediaContentLength && textMedia[i].mediaContentLength.length > 1000) {
message.warning(`第${i + 1}个文字简介的字数超过了1000个字`)
resolve(false)
return false
}
}
if (editorTextLength > 1000) {
message.warning('课程简介超过字数限定');
resolve(false);
return;
}
// const textMedia = scheduleMedia.filter((item) => item.mediaType === 'TEXT')
// for (let i = 0, len = textMedia.length; i < len; i++) {
// if (textMedia[i].mediaContentLength && textMedia[i].mediaContentLength.length > 1000) {
// message.warning(`第${i + 1}个文字简介的字数超过了1000个字`)
// resolve(false)
// return false
// }
// }
resolve(true)
})
}
......@@ -534,22 +592,107 @@ class AddVideoCourse extends React.Component {
}
return new Blob([ab], { type: 'image/png' })
}
handleRenameCourseChapter = (chapterId, isFromTextArea, value) => {
let val = value && value.trim();
this.handleValidateChapterName(val).then(res => {
// 校验不通过不能点确定保存修改课节名称
if (!res && !isFromTextArea) return;
let { courseChapterList } = this.state;
let _courseChapterList = [];
_courseChapterList = courseChapterList.map((item,index)=>{
if(item.id === chapterId){
// 若是文本框编辑则修改的是视频别名,否则是点击确认修改视频本名
if(isFromTextArea){
item.mediaNameAlias = val;
} else {
item.mediaName = item.mediaNameAlias;
}
}
return item
})
this.setState({
courseChapterList: _courseChapterList
})
});
}
handleDeleteCourseChapter = (chapterId) => {
console.log('chapterId---',chapterId);
let { courseChapterList } = this.state;
let _courseChapterList = courseChapterList.filter((item,index) => {
return item.id !== chapterId
})
this.setState({
courseChapterList :_courseChapterList
})
}
renderChapterTitle = (item) => {
const { chapterNameValidateStatus, chapterNameHelpMsg} = this.state;
return <div className="course-chapter-title-popover">
<div className="tag-title">课节名称</div>
<Form>
<Form.Item
validateStatus={chapterNameValidateStatus}
help={chapterNameHelpMsg}
>
<TextArea
value={item.mediaNameAlias}
placeholder="请输入课节名称"
maxLength={40}
autoSize
// autoSize={{ minRows: 1, maxRows: 3 }}
style={{ width: '318px'}}
onChange={(e) => {this.handleRenameCourseChapter(item.id, true, e.target.value) }}
/>
</Form.Item>
</Form>
</div>
}
// 上下移动
handleChangeIndex = (isUp, sortIndex) => {
const { courseChapterList} = this.state;
// 第一个上移和最后一个下移不能使用
if((isUp && sortIndex === 0) || (!isUp && sortIndex === (courseChapterList.length -1))){
return;
}
let _courseChapterList = [...courseChapterList];
const temp = courseChapterList[sortIndex];
// 若上移
if(isUp){
_courseChapterList[sortIndex -1] = temp;
_courseChapterList[sortIndex -1].sort = sortIndex -1;
_courseChapterList[sortIndex] = courseChapterList[sortIndex - 1];
_courseChapterList[sortIndex].sort = sortIndex;
} else { // 若下移
_courseChapterList[sortIndex + 1] = temp;
_courseChapterList[sortIndex + 1].sort = sortIndex + 1;
_courseChapterList[sortIndex] = courseChapterList[sortIndex + 1];
_courseChapterList[sortIndex].sort = sortIndex;
}
this.setState({
courseChapterList: _courseChapterList
})
}
render() {
const {
pageType,
courseName,
scheduleVideoId,
coverId,
coverUrl,
scheduleVideoUrl,
studentList,
scheduleMedia,
showCutModal,
showSelectFileModal,
diskList,
imageFile,
joinType,
videoName,
videoType,
shelfState,
categoryName,
......@@ -561,17 +704,18 @@ class AddVideoCourse extends React.Component {
cutImageBlob,
introduce,
loadintroduce,
id
id,
courseChapterList,
imageFile
} = this.state
// 已选择的上课学员数量
const hasSelectedStu = studentList.length
const courseWareIcon = FileVerifyMap[videoType] ? FileTypeIcon[FileVerifyMap[videoType].type] : FileTypeIcon[videoType]
const defaultCover = 'https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png';
const isDefaultCover = coverUrl === defaultCover || coverUrl == null;
return (
<div className='page add-video-course-page'>
<Breadcrumbs navList={pageType === 'add' ? '新建视频课' : '编辑视频课'} goBack={this.handleGoBack} />
<Breadcrumbs navList={pageType === 'add' ? '新建线上课' : '编辑线上课'} goBack={this.handleGoBack} />
<div className='box'>
<div className='show-tips'>
......@@ -583,7 +727,7 @@ class AddVideoCourse extends React.Component {
<span className='label'>课程名称:</span>
<Input
value={courseName}
placeholder='请输入视频课的名称(40字以内)'
placeholder='请输入线上课的名称(40字以内)'
maxLength={40}
style={{ width: 240 }}
onChange={(e) => {
......@@ -595,58 +739,103 @@ class AddVideoCourse extends React.Component {
<div className='upload-video mt16'>
<div className='content flex'>
<span className='label required'>视频上传:</span>
<div className='value'>
{scheduleVideoId ? (
<div className='course-ware'>
<img className='course-ware__img' src={courseWareIcon} />
<span className='course-ware__name'>{videoName}</span>
</div>
) : (
<div className='course-ware--empty'>从资料云盘中选择视频</div>
)}
</div>
</div>
<div className='sub-content'>
<div className="btn-wrap">
<Button
onClick={() => {
if(courseChapterList.length >= 20) {
message.warning(`最多只能上传20个文件`);
return;
}
this.setState({
showSelectFileModal: true
})
}}>{`${pageType === 'add' && !scheduleVideoId ? '选择' : '更换'}视频`}</Button>
}}>添加视频</Button>
<div className='course-ware--empty'>从资料云盘中选择视频</div>
</div>
<div className='tips'>视频数量限制20个,每个视频文件大小不超过2G</div>
</div>
</div>
<span className='tips'>视频数量限制1个,大小不超过2G</span>
<If condition={courseChapterList.length > 0}>
<div className="course-chapter-list-wrap">
<div className="course-chapter-total">{`共${courseChapterList.length}个课节`}</div>
<div className='course-chapter-list' id="course-chapter-list">
{
_.map(courseChapterList,(item,index) => {
return <div className='course-ware' key={index}>
<div className="course-ware__index">{`0${index + 1 } `}</div>
<img className='course-ware__img' src={courseWareIcon} alt='' />
<div className='course-ware__name'>{item.mediaName && item.mediaName.length > 24 ? <Tooltip title={item.mediaName}>{item.mediaName}</Tooltip>:item.mediaName}</div>
<div className="course-chapter__opt" id={item.id}>
<div className={`up ${Number(index) === 0 ? 'disabled':''}`} onClick={()=> this.handleChangeIndex(true,item.sort,item.id)}>上移</div>
<div className="line">|</div>
<div className={`down ${Number(index) === (courseChapterList.length - 1) ? 'disabled':''}`} onClick={()=> this.handleChangeIndex(false,item.sort,item.id)}>下移</div>
<div className="line">|</div>
<Popconfirm
placement="topLeft"
className="course-chapter-tooltip"
title={this.renderChapterTitle(item)}
color='#fff' trigger="click"
overlayClassName="chapter-popover"
getPopupContainer={() =>
document.getElementById('course-chapter-list')
}
onConfirm={() => this.handleRenameCourseChapter(item.id,false,item.mediaNameAlias)}
icon={null}
maskClosable={false}
>
<div className="rename">重命名</div>
</Popconfirm>
<div className="line">|</div>
<div className="delete" onClick={()=>this.handleDeleteCourseChapter(item.id)}>移除</div>
</div>
</div>
})
}
<div className='cover-url flex mt16'>
<div className='label'>视频封面:</div>
<div className='cover-url__wrap'>
<div className='img-content'>
{/* 如果视频和封面都没有上传的话, 那么就显示缺省, 如果上传了视频, 那么封面图就默认显示视频的第一帧, 如果上传了封面图, 那么就显示上传的封面图 */}
{scheduleVideoUrl || coverUrl ? (
<img src={coverUrl || `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast`} />
) : (
<div className='empty-img'>
若不上传
<br />
系统默认将视频首帧作为封面图
</div>
)}
</div>
</If>
<div className='cover-url flex mt16'>
<div className='label'>课程封面:</div>
<div className='cover-url__wrap'>
<div className='opt-btns'>
<div>
<Button
onClick={() => {
this.setState({ showSelectCoverModal: true })
}}>{`${pageType === 'add' && !scheduleVideoId && !coverUrl ? '上传' : '修改'}封面`}</Button>
}}>{`${pageType === 'add' && !coverUrl ? '上传' : '修改'}封面`}</Button>
<span
className={`span ${coverUrl ? 'defalut-span' : ''}`}
onClick={() => {
this.setState({
coverUrl: '',
coverId: ''
})
}}>
使用默认图
</span>
</div>
<div className='tips'>建议尺寸1280*720px或16:9。封面图最大5M,支持jpg、jpeg和png。</div>
</div>
<div className='img-content'>
{
isDefaultCover && <span className="tag">默认图</span>
}
{/* 如果视频和封面都没有上传的话, 那么就显示缺省, 如果上传了视频, 那么封面图就默认显示视频的第一帧, 如果上传了封面图, 那么就显示上传的封面图 */}
<img src={coverUrl || `https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png`} alt='' />
</div>
</div>
</div>
<div className='course-catalog required'>
<span className='label'>课程分类:</span>
{pageType === 'add' && (
<Cascader
defaultValue={[categoryName]}
defaultValue={[]}
options={courseCatalogList}
displayRender={(label) => label.join('-')}
fieldNames={fieldNames}
......@@ -704,6 +893,7 @@ class AddVideoCourse extends React.Component {
{/* 选择备课文件弹窗 */}
{showSelectFileModal && (
<SelectPrepareFileModal
multiple={true}
operateType='select'
selectTypeList={['MP4']}
accept='video/mp4'
......
......@@ -49,14 +49,16 @@
}
}
.course-catalog{
margin-bottom:16px;
margin-top:16px;
margin-bottom:24px;
margin-top:24px;
}
.course-ware {
display: flex;
align-items: center;
margin-bottom: 4px;
&__index {
width: 18px;
}
&__img {
width: 24px;
margin-right: 4px;
......@@ -75,7 +77,19 @@
.img-content {
width: 298px;
height: 172px;
position: relative;
.tag {
border-radius: 2px;
background: rgba(51, 51, 51, .2);
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #FFF;
position: absolute;
top: 8px;
left: 8px;
}
img {
width: 100%;
height: 100%;
......@@ -85,7 +99,7 @@
.empty-img {
width: 298px;
height: 172px;
border: 1px dashed #EBEBEB;
border: 1px dashed #ebebeb;
border-radius: 4px;
padding: 12px;
color: #999;
......@@ -94,12 +108,17 @@
}
.opt-btns {
margin-top: 8px;
display: flex;
align-items: center;
margin-bottom: 8px;
.span {
color: #ccc;
margin-left: 8px;
}
.defalut-span {
cursor: pointer;
color: #2966ff;
}
.tips {
margin-left: 12px;
margin-top: 8px;
color: #999;
}
}
......@@ -115,18 +134,100 @@
color: #333;
}
}
.upload-video {
display: flex;
margin-bottom: 24px;
.sub-content {
margin-left: 85px;
margin-top: 4px;
.btn-wrap {
display: flex;
.course-ware--empty {
margin-left: 12px;
line-height: 28px;
}
}
.tips {
margin-left: 4px;
margin-top: 8px;
color: #999;
}
}
}
.course-chapter-list-wrap {
position: relative;
margin-left: 85px;
.course-chapter-total {
height: 40px;
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
margin-bottom: 18px;
position: absolute;
top: 1px;
left: 1px;
z-index: 10;
background: #fff;
width: 550px;
text-indent: 16px;
line-height: 40px;
}
.course-chapter-list {
margin-top: 8px;
width: 630px;
max-height: 247px;
min-height: 130px;
overflow-y: auto ;
border-radius: 4px;
border: 1px solid #E8E8E8;
padding: 50px 16px 20px 16px;
.course-ware {
display: flex;
align-items: center;
margin-bottom: 20px;
color: #666;
&:last-child {
margin-bottom: 0;
}
&__img {
width: 24px;
margin-right: 4px;
margin-left: 16px;
}
&__name {
color: #333;
width: 356px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.course-chapter__opt {
display: flex;
color: #2966ff;
.line {
color: #bfbfbf;
margin-left: 4px;
margin-right: 4px;
}
.delete,
.rename,
.up,
.down {
cursor: pointer;
}
.up,
.down {
&.disabled {
color: rgba(204, 204, 204, 1);
}
}
}
}
}
}
}
.footer {
position: fixed;
left: 196px;
......
.video-course-detail {
.box {
.course-detail {
display: flex;
border-bottom: 2px solid #E8E8E8;
padding-bottom: 24px;
&__img {
margin-right: 24px;
width: 300px;
height: 170px;
background: #FFFFFF;
border-radius: 4px;
img {
width: 300px;
height: 170px;
border-radius: 4px;
}
}
&__info {
display: flex;
flex-direction: column;
.info__title {
width: 680px;
height: 28px;
font-size: 20px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 28px;
margin-bottom: 16px;
}
.info__category, .info__chapterNum {
width: 100%;
height: 20px;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #333333;
line-height: 20px;
margin-bottom: 10px;
}
}
}
.course-chapter {
padding-top: 24px;
&__total {
width: 100%;
height: 20px;
font-size: 14px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 20px;
margin-bottom: 24px;
}
&__list {
.course-ware {
display: flex;
height: 20px;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #666666;
line-height: 20px;
margin-bottom: 28px;
cursor: pointer;
&:hover {
color: #2966FF;
}
&__index {
width: 20px;
}
&__img {
width: 20px;
height: 20px;
margin-left: 16px;
margin-right: 8px;
}
&__name {
width: 700px;
}
}
}
}
}
}
\ No newline at end of file
/*
* @Author: wufan
* @Date: 2020-04-28 18:05:30
* @LastEditors: wufan
* @LastEditTime: 2021-07-05 10:36:15
* @Description: 线上课课程课节详情
*/
import React,{useEffect, useState} from 'react';
import Breadcrumbs from "@/components/Breadcrumbs";
import { withRouter } from "react-router-dom";
import CourseService from '@/domains/course-domain/CourseService'
import underscore from 'underscore';
import ScanFileModal from '@/modules/resource-disk/modal/ScanFileModal.jsx';
import './VideoCourseDetail.less';
declare var getParameterByName: any;
declare var window: any;
function VideoCourseDetail(){
const courseId = window.getParameterByName("courseId"); // 课程ID
const [coverUrl,setCoverUrl] = useState("");
const [videoName,setVideoName] = useState("");
const [scheduleVideoUrl,setScheduleVideoUrl] = useState("");
const [courseChapterList,setCourseChapterList] = useState<Array<Object>>([]);
const [courseName,setCourseName] = useState("");
const [categoryName,setCategoryName] = useState("");
const [scanFileModal,setScanFileModal] = useState<any>(null);
useEffect(()=>{
handleFetchScheudleDetail(courseId);
},[courseId])
// 获取线上课详情
function handleFetchScheudleDetail(courseId: string){
CourseService.videoScheduleDetail({
courseId
}).then((res) => {
const { result = {} } = res || {}
const { courseName, courseMediaVOS, categoryName, courseChapterVOList =[] } = result
let coverId:string = "";
let coverUrl: string = "";
let videoType: string = "";
let videoDuration: number = 0;
let videoName: string = "";
let scheduleVideoUrl: string = "";
courseMediaVOS.map((item:any) => {
switch (item.contentType) {
case 'COVER':
coverId = item.mediaContent
coverUrl = item.mediaUrl
break
case 'SCHEDULE':
videoDuration = item.videoDuration
videoName = item.mediaName
scheduleVideoUrl = item.mediaUrl
videoType = item.mediaType
break
default:
break
}
return item
})
setCoverUrl(coverUrl);
setVideoName(videoName);
setScheduleVideoUrl(scheduleVideoUrl);
setCourseName(courseName);
setCategoryName(categoryName);
setCourseChapterList(courseChapterVOList);
})
}
function handleScanFileModal(fileType: string = "MP4", fileObj: Object){
const scanFileModal = (
<ScanFileModal
fileType={fileType}
item={fileObj}
close={() => {
setScanFileModal(null);
}}
/>
);
setScanFileModal(scanFileModal)
}
return <div className="page video-course-detail">
<Breadcrumbs
navList="课程详情"
goBack={() => {
window.RCHistory.goBack();
}}
/>
<div className="box">
<div className="course-detail">
<div className='course-detail__img'>
{/* 如果视频和封面都没有上传的话, 那么就显示缺省, 如果上传了视频, 那么封面图就默认显示视频的第一帧, 如果上传了封面图, 那么就显示上传的封面图 */}
<img src={coverUrl || `https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png`} alt='' />
</div>
<div className="course-detail__info">
<div className="info__title">{courseName}</div>
<div className="info__category">{`分类:${categoryName}`}</div>
<div className="info__chapterNum">{`课节数量:${courseChapterList.length}`}</div>
</div>
</div>
<div className="course-chapter">
<div className="course-chapter__total">{`共${courseChapterList.length}个课节`}</div>
<div className="course-chapter__list">
<If condition={courseChapterList.length > 0}>
{
underscore.map(courseChapterList,(item: any,index: number) => {
return <div className='course-ware' onClick={()=>{handleScanFileModal("MP4",item)}} key={index}>
<div className="course-ware__index">{`${index > 9 ? index + 1 : `0${index + 1 }`} `}</div>
<img className='course-ware__img' src='https://image.xiaomaiketang.com/xm/TKwbQGYDBR.png' alt='' />
<div className='course-ware__name'>{item.name}</div>
</div>
})
}
</If>
</div>
</div>
</div>
{scanFileModal}
</div>
}
export default withRouter(VideoCourseDetail);
\ No newline at end of file
......@@ -117,12 +117,18 @@ class AddVideoIntro extends React.Component {
<span className="label">观看设置:</span>
<div className="content">
<div>
<Switch checked={whetherVisitorsJoin==="YES"? true:false} onChange={this.whetherVisitorsJoinChange}/>
<Switch checked={whetherVisitorsJoin==="NO"? true:false} onChange={this.whetherVisitorsJoinChange}/>
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的学员观看</div>
<div>关闭:仅限绑定了手机号的学员可以进入观看视频</div>
<Choose>
<When condition={whetherVisitorsJoin==="NO"}>
<div>已开启,学员需绑定手机号才可观看</div>
</When>
<Otherwise>
<div>已关闭,学员无需绑定手机号即可观看</div>
</Otherwise>
</Choose>
</div>
</div>
</div>
......@@ -136,20 +142,27 @@ class AddVideoIntro extends React.Component {
</Col>
<Col span={21}>
<div className="desc">
<div>开启:此视频将在学员学院的视频列表中出现</div>
<div>关闭:此视频将在学员学院的视频列表中隐藏</div>
<Choose>
<When condition={shelfState==="YES"}>
<div>已开启,课程将在该学院的学员课程列表中显示</div>
</When>
<Otherwise>
<div>已关闭,课程将在该学院的学员课程列表中隐藏</div>
</Otherwise>
</Choose>
</div>
</Col>
</Row>
</div>
</div>
<div className="introduce">
<span className="label">视频课简介:</span>
<span className="label">课程简介:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item introduce-editor">
{(!id || loadintroduce) &&
<GraphicsEditor
maxLimit={1000}
id="intro"
isIntro={true}
detail={{
......
......@@ -32,14 +32,17 @@
}
.store-show{
display:flex;
margin-top:16px;
margin-bottom:16px;
margin-top:24px;
margin-bottom:24px;
.desc{
margin-left:16px;
margin-left:11px;
font-size:14px;
color:#999;
display:inline-block;
}
.content {
width: 400px;
}
}
.radio {
display: block;
......
import React from 'react';
import './ChapterList.less';
function ChapterList(props){
const { courseChapterList } = props;
return <div className='chapter-list-component'>
<If condition={courseChapterList.length > 0}>
{
_.map(courseChapterList,(item,index) => {
return <div className='course-ware'>
<div className='course-ware__index'>{`0${index + 1 } `}</div>
<div className="course-ware__detail">
<div className='course-ware__detail__name'>{item.mediaName}</div>
<div className='course-ware__detail__duration'>{window.formatDuration(item.videoDuration)}</div>
</div>
</div>
})
}
</If>
</div>
}
export default ChapterList;
\ No newline at end of file
.chapter-list-component {
.course-ware {
display: flex;
padding: 10px 0;
border-bottom: 1px dashed #EEEEEE;
&:last-child {
border-bottom: none;
}
&__index {
width: 18px;
height: 18px;
font-size: 13px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #999999;
line-height: 18px;
}
&__detail {
display: flex;
flex-direction: column;
width: calc(~'100% - 18px');
&__name {
width: 267px;
font-size: 13px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #333333;
line-height: 21px;
margin-bottom: 4px;
}
&__duration {
color: #999999;
}
}
}
}
\ No newline at end of file
import User from "@/common/js/user";
import CourseService from "@/domains/course-domain/CourseService";
import React from "react";
import { Modal } from "antd";
import "./LearningDetailModal.less";
class LearningDetailModal extends React.Component {
constructor(props) {
super(props);
this.state = {
courseChapterList: [],
};
}
componentDidMount() {
this.handleFetchDataList();
}
// 获取观看视频数据列表
handleFetchDataList = () => {
const { courseId, data } = this.props;
const params = {
courseId,
storeId: User.getStoreId(),
storeCustomerId: data.storeCustomerId,
};
CourseService.lineDetailWatchInfo(params).then((res) => {
const { result = [] } = res;
this.setState({
courseChapterList: result
});
});
};
render() {
const { title, onClose, onOk } = this.props;
const { courseChapterList } = this.state;
return (
<Modal
footer={null}
visible={true}
title={title}
width={680}
maskClosable={false}
closeIcon={
<span className="icon iconfont modal-close-icon">&#xe6ef;</span>
}
onCancel={onClose}
onOk={onOk}
className="learning-detail-modal"
>
<div className="course-chapter__list">
<If condition={courseChapterList.length > 0}>
{_.map(courseChapterList, (item, index) => {
return (
<div className="course-wrap">
<div className="course-ware" key={index}>
<div className="course-ware__index">{`${
index > 9 ? index + 1 : `0${index + 1}`
} `}</div>
<img
className="course-ware__img"
src="https://image.xiaomaiketang.com/xm/TKwbQGYDBR.png"
alt=""
/>
<div className="course-ware__name">{item.courseChapterName && item.courseChapterName.replace('.MP4','')}</div>
</div>
<div className={`progress ${item.progress === 100 ? 'finish' :''}`}>{`${item.progress === 100 ? '已完成' : `${item.progress}%`}`}</div>
</div>
);
})}
</If>
</div>
</Modal>
);
}
}
export default LearningDetailModal;
.learning-detail-modal {
.course-chapter {
padding-top: 24px;
&__list {
.course-wrap {
display: flex;
justify-content: space-between;
.course-ware {
display: flex;
height: 20px;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #666666;
line-height: 20px;
margin-bottom: 28px;
&__index {
width: 20px;
}
&__img {
width: 20px;
height: 20px;
margin-left: 12px;
margin-right: 8px;
}
&__name {
width: 500px;
}
}
.progress {
&.finish {
color: RGBA(101, 202, 168, 1);
}
}
}
}
}
}
......@@ -3,7 +3,7 @@
* @Date: 2020-08-05 10:11:57
* @LastEditors: fusanqiasng
* @LastEditTime: 2021-05-28 20:14:37
* @Description: 视频课-搜索模块
* @Description: 线上课-搜索模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......@@ -192,10 +192,10 @@ class VideoCourseFilter extends React.Component {
<Row type='flex' justify='space-between' align='top'>
<div className='search-condition'>
<div className='search-condition__item'>
<span className='search-name'>视频课名称:</span>
<span className='search-name'>线上课名称:</span>
<Search
value={courseName}
placeholder='搜索视频课名称'
placeholder='搜索线上课名称'
onChange={(e) => {
this.handleChangeQuery('courseName', e.target.value)
}}
......
import { Dropdown, message, Modal, Switch, Table, Tooltip } from 'antd';
import User from '@/common/js/user';
import { PageControl } from '@/components';
import { LIVE_SHARE } from '@/domains/course-domain/constants';
import CourseService from '@/domains/course-domain/CourseService';
import ShareLiveModal from '@/modules/course-manage/modal/ShareLiveModal';
import React from 'react';
import RelatedPlanModal from '../../modal/RelatedPlanModal';
import WatchDataModal from '../modal/WatchDataModal';
import React from "react"
import { Table, Modal, message, Tooltip, Switch, Dropdown } from "antd"
import _ from "underscore"
import { PageControl } from "@/components"
import { LIVE_SHARE } from "@/domains/course-domain/constants"
import { Route, withRouter } from 'react-router-dom';
import ShareLiveModal from "@/modules/course-manage/modal/ShareLiveModal"
import WatchDataModal from "../modal/WatchDataModal"
import CourseService from "@/domains/course-domain/CourseService"
import RelatedPlanModal from "../../modal/RelatedPlanModal"
import User from "@/common/js/user"
import VideoCourseDetail from '../VideoCourseDetail';
import WatchData from "./WatchData";
import { XMTable } from '@/components';
import college from '@/common/lottie/college';
import './VideoCourseList.less';
import "./VideoCourseList.less"
class VideoCourseList extends React.Component {
constructor(props) {
......@@ -32,21 +36,20 @@ class VideoCourseList extends React.Component {
}
}
// 跳转课程详情页
handleLinkToCourseDetail = (courseId) => {
const { match } = this.props;
window.RCHistory.push(`${match.url}/video-course-detail?courseId=${courseId}`)
}
// 观看数据弹窗
handleShowWatchDataModal = (record) => {
const watchDataModal = (
<WatchDataModal
type='videoCourseList'
data={record}
close={() => {
this.setState({
watchDataModal: null,
});
}}
/>
);
this.setState({ watchDataModal });
};
handleShowWatchDataModal = (item) => {
const { match } = this.props;
window.RCHistory.push({
pathname: `${match.url}/course-data?courseName=${item.courseName}&courseId=${item.id}`
})
}
// 请求表头
parseColumns = () => {
......@@ -54,23 +57,21 @@ class VideoCourseList extends React.Component {
const { ShelfLoading } = this.state;
const columns = [
{
title: '视频课',
key: 'scheduleName',
dataIndex: 'scheduleName',
title: "线上课",
key: "scheduleName",
dataIndex: "scheduleName",
width: 321,
fixed: 'left',
render: (val, record) => {
const { coverUrl, scheduleVideoUrl } = record;
const { coverUrl } = record;
return (
<div className='record__item'>
{/* 上传了封面的话就用上传的封面, 没有的话就取视频的第一帧 */}
<img
className='course-cover'
src={
coverUrl ||
(type === 'internal' ? `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast` : 'https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png')
coverUrl || 'https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png'
}
alt=''
alt='封面图'
/>
<Choose>
<When condition={record.courseName.length > 25}>
......@@ -99,9 +100,9 @@ class VideoCourseList extends React.Component {
</If>
</span>
),
key: 'categoryName',
dataIndex: 'categoryName',
width: 200,
key: "categoryName",
dataIndex: "categoryName",
width: 150,
render: (val, record) => {
return (
<Choose>
......@@ -119,21 +120,15 @@ class VideoCourseList extends React.Component {
},
},
{
title: '创建人',
key: 'createName',
dataIndex: 'createName',
title: '课节数',
key: 'chapterNum',
dataIndex: 'chapterNum',
className: "chapterNum",
width: 100,
render: (val) => {
return (
<div>
{val && (
<Tooltip title={val}>
<div>{val.length > 4 ? `${val.slice(0, 4)}...` : val}</div>
</Tooltip>
)}
</div>
);
},
align: 'right',
render: (val,item) => {
return <div onClick={()=>this.handleLinkToCourseDetail(item.id)}>{val || 1}</div>
}
},
{
title: (
......@@ -170,13 +165,31 @@ class VideoCourseList extends React.Component {
},
},
{
title: '观看学员数',
width: 110,
key: 'watchUserCount',
dataIndex: 'watchUserCount',
title: "观看学员数",
width: 150,
key: "watchUserCount",
dataIndex: "watchUserCount",
align: 'right',
render: (val, item) => {
return <div className='watchUserCount'>{val || 0}</div>;
return <div className='watchUserCount' onClick={() => this.handleShowWatchDataModal(item)}>{val || 0}</div>
}
},
{
title: "创建人",
key: "createName",
dataIndex: "createName",
width: 100,
render: (val) => {
return (
<div>
{val && (
<Tooltip title={val}>
<div>{val.length > 4 ? `${val.slice(0, 4)}...` : val}</div>
</Tooltip>
)}
</div>
)
}
},
{
title: '创建时间',
......@@ -230,22 +243,17 @@ class VideoCourseList extends React.Component {
title: '操作',
key: 'operate',
dataIndex: 'operate',
width: 210,
width: 160,
fixed: 'right',
render: (val, record) => {
return (
<div className='operate'>
<div className='operate__item' onClick={() => this.handleShowWatchDataModal(record)}>
观看数据
</div>
<If condition={type === 'internal'}>
<span className='operate__item split'> | </span>
<If condition={type === "internal"}>
<div className='operate__item' onClick={() => this.handleShowShareModal(record)}>
分享
</div>
</If>
<span className='operate__item split'> | </span>
</If>
<Dropdown overlay={this.renderMoreOperate(record)}>
<span className='more-operate'>
<span className='operate-text'>更多</span>
......@@ -327,7 +335,7 @@ class VideoCourseList extends React.Component {
});
};
// 删除视频
// 删除线上
handleDeleteVideoCourse = (scheduleId) => {
Modal.confirm({
title: '你确定要删除此视频课吗?',
......@@ -369,7 +377,7 @@ class VideoCourseList extends React.Component {
data={shareData}
type='videoClass'
courseDivision={type}
title='视频课'
title='线上课'
close={() => {
this.setState({
shareLiveModal: null,
......@@ -446,7 +454,7 @@ class VideoCourseList extends React.Component {
);
};
render() {
const { dataSource = [], totalCount, query, type } = this.props;
const { dataSource = [], totalCount, query, type, match } = this.props;
const { current, size } = query;
const { RelatedPlanModalVisible, selectPlanList, selectCourseId } = this.state;
return (
......@@ -465,16 +473,6 @@ class VideoCourseList extends React.Component {
scroll={{ x: 1500 }}
className='video-list-table'
/>
{/* <Table
rowKey={(record) => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
onChange={this.handleChangeTable}
pagination={false}
scroll={{ x: 1500 }}
bordered
className='video-list-table'
/> */}
<div className='box-footer'>
<PageControl
......@@ -499,9 +497,11 @@ class VideoCourseList extends React.Component {
)}
{this.state.shareLiveModal}
{this.state.watchDataModal}
<Route path={`${match.url}/video-course-detail`} component={VideoCourseDetail} />
<Route path={`${match.url}/course-data`} component={WatchData} />
</div>
);
}
}
export default VideoCourseList;
export default withRouter(VideoCourseList);
......@@ -24,11 +24,17 @@
}
}
}
.chapterNum {
color: #2966ff;
cursor: pointer;
}
}
}
.watchUserCount {
text-align: right;
padding: 16px;
color: #2966ff;
cursor: pointer;
}
.operate-text {
color: #2966FF;
......
......@@ -3,7 +3,7 @@
* @Date: 2020-08-05 10:12:15
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-26 16:07:27
* @Description: 视频课-操作模块
* @Description: 线上课-操作模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......@@ -22,7 +22,7 @@ export default function VideoCourseOpt() {
RCHistory.push('/create-video-course?type=add');
}}
className="mr12"
>新建视频</Button>
>新建线上</Button>
</div>
);
}
import User from "@/common/js/user";
import college from "@/common/lottie/college";
import { PageControl, XMTable } from "@/components";
import Breadcrumbs from "@/components/Breadcrumbs";
import CourseService from "@/domains/course-domain/CourseService";
import { Input, Select } from "antd";
import React from "react";
import { withRouter } from "react-router-dom";
import LearningDetailModal from "./LearningDetailModal";
import "./WatchData.less";
const { Search } = Input;
class WatchData extends React.Component {
constructor(props) {
super(props);
const courseId = window.getParameterByName("courseId");
const courseName = window.getParameterByName("courseName");
this.state = {
courseName,
courseId,
visible: true,
dataSource: [],
query: {
current: 1,
size: 10,
sourceEnum: null,
},
totalCount: 0,
learningDetailModal: null,
};
}
componentDidMount() {
this.handleFetchDataList();
}
// 获取观看视频数据列表
handleFetchDataList = () => {
const { query, courseId } = this.state;
const params = {
...query,
courseId: courseId,
storeId: User.getStoreId(),
};
CourseService.videoWatchInfo(params).then((res) => {
const { result = {} } = res;
const { records = [], total = 0 } = result;
this.setState({
dataSource: records,
totalCount: Number(total),
});
});
};
parseColumns = () => {
const SOURCENUM = {
WE_CHAT: "微信",
WORK_WE_CHAT: "企业微信",
};
const columns = [
{
title: "观看学员",
key: "name",
dataIndex: "name",
render: (val, item) => {
return (
<div className="watcher">
<div className="watcher__name">{val}</div>
<div className={`watcher__type ${item.sourceEnum}`}>
@{SOURCENUM[item.sourceEnum]}
</div>
</div>
);
},
},
{
title: "手机号",
key: "phone",
dataIndex: "phone",
},
{
title: "首次观看时间",
key: "firstWatch",
dataIndex: "firstWatch",
render: (val) => {
return formatDate("YYYY-MM-DD H:i", val);
},
},
{
title: "学习进度",
key: "progress",
dataIndex: "progress",
render: (val) => {
return <span>{val}%</span>;
},
},
{
title: "操作",
key: "operate",
dataIndex: "operate",
render: (val, record) => {
return (
<div className="operate">
<div
className="operate__item"
onClick={() => this.ShowLearningDetailModal(record)}
>
学习详情
</div>
</div>
);
},
},
];
return columns;
};
// 显示学员数据详情弹窗
ShowLearningDetailModal = (item) => {
const learningDetailModal = (
<LearningDetailModal
data={item}
title="学习详情"
onClose={() => {
this.setState({
learningDetailModal: null
});
}}
courseId={this.state.courseId}
/>
);
this.setState({ learningDetailModal });
};
// 搜索学员姓名或手机号
handleChangNickname = (value) => {
const isPhone = (value || "").match(/^\d+$/);
const { query } = this.state;
let _query = { ...query };
if (isPhone) {
_query.phone = value;
_query.nickName = null;
} else {
_query.nickName = value;
_query.phone = null;
}
_query.current = 1;
this.setState(
{
query: _query,
},
this.handleFetchDataList
);
};
// 搜索学员类型
handleWatchTypeSelect = (sourceEnum) => {
const { query } = this.state;
let _query = { ...query };
_query.sourceEnum = sourceEnum;
this.setState(
{
query: _query,
},
this.handleFetchDataList
);
};
render() {
const { dataSource, totalCount, query, courseName } = this.state;
const { current, size, sourceEnum } = query;
return (
<div className="page video-course-watch-data">
<Breadcrumbs
navList="观看学员数据"
goBack={() => {
window.RCHistory.goBack();
}}
/>
<div className="box">
<div className="box-header">
<div className="course-detail">
<div className="detail__line"></div>
<div className="detail__name">{courseName}</div>
</div>
<div className="filter-box">
<div className="watcher-name">
<span className="label">学员:</span>
<Search
placeholder="搜索学员姓名/手机号"
style={{ width: 200 }}
onChange={(e) => {
this.handleChangNickname(e.target.value);
}}
onSearch={() => {
this.handleFetchDataList();
}}
enterButton={<span className="icon iconfont">&#xe832;</span>}
/>
</div>
<div className="watch-type">
<span className="label">学员类型:</span>
<Select
showSearch
style={{ width: 200 }}
allowClear
onPopupScroll={this.handleFetchMore}
placeholder="请选择学员类型"
value={sourceEnum}
onChange={this.handleWatchTypeSelect}
>
<Select.Option tyle={{ textAlign: "center" }} value="WE_CHAT">
微信
</Select.Option>
<Select.Option
tyle={{ textAlign: "center" }}
value="WORK_WE_CHAT"
>
企业微信
</Select.Option>
</Select>
</div>
</div>
</div>
<XMTable
renderEmpty={{
image: college,
description: "暂无数据",
}}
rowKey={(record) => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
pagination={false}
bordered
className="video-list-table"
/>
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={totalCount}
toPage={(page) => {
const _query = { ...query, current: page + 1 };
this.setState(
{
query: _query,
},
() => {
this.handleFetchDataList();
}
);
}}
/>
</div>
</div>
{this.state.learningDetailModal}
</div>
);
}
}
export default withRouter(WatchData);
.video-course-watch-data {
.watcher {
display: flex;
&__type {
margin-left: 8px;
&.WE_CHAT {
color:rgba(28, 172, 27, 1);
}
&.WORK_WE_CHAT {
color: rgba(41, 102, 255, 1);
}
}
}
.filter-box {
display: flex;
margin-bottom: 4px;
.watcher-name {
margin-right: 40px;
}
}
.course-detail {
display: flex;
align-items: center;
margin-bottom: 16px;
.detail__line {
width: 4px;
height: 12px;
background-image: linear-gradient(#2966ff 83.5%, #0acca4 16.5%);
margin-right: 8px;
}
.detail__name {
width: 880px;
height: 26px;
font-size: 19px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 26px;
}
}
}
\ No newline at end of file
......@@ -15,18 +15,18 @@ class VideoCourse extends React.Component {
current: 1,
storeId: User.getStoreId(),
},
dataSource: [], // 视频课列表
totalCount: 0, // 视频课数据总条数
dataSource: [], // 线上课列表
totalCount: 0, // 线上课数据总条数
currentTabKey: 'internal',
};
}
componentWillMount() {
// 获取视频课列表
// 获取线上课列表
this.handleFetchScheduleList();
}
// 获取视频课列表
// 获取线上课列表
handleFetchScheduleList = (_query = {}) => {
const { currentTabKey } = this.state;
const query = {
......@@ -81,7 +81,7 @@ class VideoCourse extends React.Component {
const { dataSource, totalCount, query, currentTabKey } = this.state;
return (
<div className='page video-course-page'>
<div className='content-header'>视频</div>
<div className='content-header'>线上</div>
<div className='box'>
<Tabs onChange={this.currenTabChange} activeKey={currentTabKey}>
......@@ -95,7 +95,7 @@ class VideoCourse extends React.Component {
<If condition={currentTabKey === 'internal'}>
<VideoCourseOpt />
</If>
{/* 视频课列表模块 */}
{/* 线上课列表模块 */}
<VideoCourseList
type={currentTabKey}
query={query}
......
......@@ -3,7 +3,7 @@
* @Date: 2020-05-19 11:01:31
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-05-25 16:50:47
* @Description 余额异常弹窗
* @Description 学员观看数据弹窗
*/
import User from '@/common/js/user';
import { PageControl, XMTable } from '@/components';
......@@ -128,65 +128,52 @@ class WatchDataModal extends React.Component {
const { visible, size, dataSource, totalCount, query } = this.state;
return (
<Modal
title='视频课观看数据'
title="线上课观看数据"
visible={visible}
footer={null}
onCancel={this.onClose}
maskClosable={false}
className='watch-data-modal'
className="watch-data-modal"
closable={true}
width={800}
closeIcon={<span className='icon iconfont modal-close-icon'>&#xe6ef;</span>}>
<div className='search-container'>
<Search
placeholder='搜索学员姓名/手机号'
style={{ width: 200 }}
onChange={(e) => {
this.handleChangNickname(e.target.value);
}}
onSearch={() => {
this.handleFetchDataList();
}}
enterButton={<span className='icon iconfont'>&#xe832;</span>}
/>
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
>
<div className="search-container">
<Search placeholder="搜索学员姓名/手机号" style={{ width: 200 }} onChange={(e) => { this.handleChangNickname(e.target.value)}} onSearch={ () => { this.handleFetchDataList()}} enterButton={<span className="icon iconfont">&#xe832;</span>}/>
</div>
<div>
<XMTable
renderEmpty={{
image: college,
description: '暂无数据',
}}
rowKey={(record) => record.id}
rowKey={record => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
pagination={false}
bordered
/>
{dataSource.length > 0 && (
<div className='box-footer'>
{dataSource.length >0 &&
<div className="box-footer">
<PageControl
current={query.current - 1}
pageSize={size}
total={totalCount}
size='small'
size="small"
toPage={(page) => {
const _query = { ...query, current: page + 1 };
this.setState(
{
query: _query,
},
() => {
this.handleFetchDataList();
}
);
const _query = {...query, current: page + 1};
this.setState({
query:_query
},()=>{ this.handleFetchDataList()})
}}
onShowSizeChange={this.onShowSizeChange}
/>
</div>
)}
}
</div>
</Modal>
);
)
}
}
......
......@@ -343,7 +343,7 @@ class Home extends React.Component {
</div>
</div>
<div className='course-item'>
<div className='course-title'>视频</div>
<div className='course-title'>线上</div>
<div className='data'>
<span className='course-number'>{videoCourseNum}</span>
{incVideoCourseNum > 0 && <span className='icon iconfont'>&#xe635;</span>}
......@@ -409,9 +409,8 @@ class Home extends React.Component {
</span>
<span
className={`tab${scheduleType === 'VOICE' ? ' selected' : ''}`}
onClick={() => this.setState({ scheduleType: 'VOICE' }, () => this.getHotCourse())}>
视频课
</span>
onClick={() => this.setState({ scheduleType: 'VOICE' }, () => this.getHotCourse())}
>线上课</span>
<span
className={`tab${scheduleType === 'PICTURE' ? ' selected' : ''}`}
onClick={() => this.setState({ scheduleType: 'PICTURE' }, () => this.getHotCourse())}>
......
......@@ -55,7 +55,7 @@ const ENUM = {
CourseTypeEnum: {
LIVE: "直播课",
VOICE: "视频课",
VOICE: "线上课",
PICTURE: "图文课",
FOLDER: "学习资料",
},
......
......@@ -452,7 +452,6 @@ class KnowledgeBaseList extends React.Component {
const { match } = this.props;
localStorage.setItem("WatchData_CourseName", item.name);
window.RCHistory.push({
// pathname: `${match.url}/course-data?type=${item.courseType}&id=${item.liveCourseId}`,
pathname: `${match.url}/course-data?type=${item.type}&id=${item.id}`
})
}
......
......@@ -107,11 +107,11 @@ class AddCourse extends React.Component {
selectVideo: {
external: [],
internal: [],
}, //弹窗内已选择的视频课程
}, //弹窗内已选择的线上课程
currentVideoCourseListData: {
external: [],
internal: [],
}, //页面中已关联的视频课程
}, //页面中已关联的线上课程
pictureDataSource: [],
pictureSize: 10,
......@@ -201,7 +201,7 @@ class AddCourse extends React.Component {
});
};
// 获取视频课列表
// 获取线上课列表
handleFetchVideoList = () => {
const { videoQuery, videoSize, videoCourseDivision, videoDataSource, videoTotalCount } = this.state;
......@@ -866,7 +866,7 @@ class AddCourse extends React.Component {
)}
</div>
</TabPane>
<TabPane tab='视频课' key='VIDEO'>
<TabPane tab='线上课' key='VIDEO'>
<Radio.Group value={videoCourseDivision} onChange={this.videoCourseDivisionChange} style={{ marginBottom: 8 }}>
<Radio.Button value='internal'>内部课程</Radio.Button>
<Radio.Button value='external'>外部课程</Radio.Button>
......
......@@ -25,7 +25,7 @@ class VideoList extends React.Component {
constructor(props) {
super(props);
this.state = {
id: "", // 视频课ID
id: "", // 线上课ID
studentIds: [],
selectedRowKeys: [],
query: {
......@@ -33,8 +33,8 @@ class VideoList extends React.Component {
current: 1,
storeId: User.getStoreId(),
},
dataSource: [], // 视频课列表
totalCount: 0, // 视频课数据总条数
dataSource: [], // 线上课列表
totalCount: 0, // 线上课数据总条数
};
}
......@@ -50,11 +50,11 @@ class VideoList extends React.Component {
}
componentWillMount() {
// 获取视频课列表
// 获取线上课列表
this.handleFetchScheduleList();
}
// 获取视频课列表
// 获取线上课列表
handleFetchScheduleList = (_query = {}) => {
const query = {
...this.state.query,
......
/*
* @Author: zhangleyuan
* @Date: 2021-02-20 16:13:39
* @LastEditors: wufan
* @LastEditTime: 2021-05-30 20:39:16
* @LastEditors: yuananting
* @LastEditTime: 2021-07-07 14:59:59
* @Description: 描述一下
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { useEffect, useState } from 'react'
import { Button, message, Modal } from 'antd'
import ShowTips from '@/components/ShowTips'
import Breadcrumbs from '@/components/Breadcrumbs'
import BasicInfo from './components/BasicInfo'
import TrainingTask from './components/TrainingTask'
import ExpiredCourseList from './components/ExpiredCourseList'
import PlanService from '@/domains/plan-domain/planService'
import User from '@/common/js/user'
import _ from 'underscore'
import './AddPlan.less'
import React, { useEffect, useState } from 'react';
import { Button, message, Modal } from 'antd';
import ShowTips from '@/components/ShowTips';
import Breadcrumbs from '@/components/Breadcrumbs';
import BasicInfo from './components/BasicInfo';
import TrainingTask from './components/TrainingTask';
import ExpiredCourseList from './components/ExpiredCourseList';
import PlanService from '@/domains/plan-domain/planService';
import User from '@/common/js/user';
import _ from 'underscore';
import './AddPlan.less';
import bus from '@/core/bus';
const defaultCover = 'https://image.xiaomaiketang.com/xm/rEAetaTEh3.png';
const defaultBasicData = {
planName: '',
......@@ -28,35 +29,39 @@ const defaultBasicData = {
operateType: 'All_Operate',
percentCompleteLive: 80,
percentCompleteVideo: 80,
percentCompletePicture: 100
}
const defaultTaskList = []
percentCompletePicture: 100,
};
const defaultTaskList = [];
function AddPlan() {
const id = getParameterByName('id')
const type = getParameterByName('type')
const [basicData, setBasicData] = useState(defaultBasicData)
const [taskList, setTaskList] = useState(defaultTaskList)
const [expiredCourseList, setExpiredCourseList] = useState([])
const [hasGetDetail, setHasGetDetail] = useState(false)
const [submitDisabled, setSubmitDisabled] = useState(false)
const id = getParameterByName('id');
const type = getParameterByName('type');
const [basicData, setBasicData] = useState(defaultBasicData);
const [taskList, setTaskList] = useState(defaultTaskList);
const [expiredCourseList, setExpiredCourseList] = useState([]);
const [hasGetDetail, setHasGetDetail] = useState(false);
const [editorTextLength, setEditorTextLength] = useState(0);
useEffect(() => {
if (type === 'edit') {
getPlanDetail()
getPlanCustomerState()
getPlanDetail();
getPlanCustomerState();
}
}, id)
Bus.bind('editorLimit', (editorTextLength) => {
setEditorTextLength(editorTextLength);
});
}, id);
function getPlanCustomerState() {
PlanService.getTrainingCourseAutoCancel({
planId: id
planId: id,
}).then((res) => {
const expiredCourseList = res.result
setExpiredCourseList(expiredCourseList)
})
const expiredCourseList = res.result;
setExpiredCourseList(expiredCourseList);
});
}
function getPlanDetail() {
PlanService.getTrainingPlanDetail({
planId: id
planId: id,
}).then((res) => {
const {
planName,
......@@ -67,34 +72,34 @@ function AddPlan() {
percentCompleteVideo,
percentCompletePicture,
courseMediaVOS,
trainingTaskList
} = res.result
let coverId
let coverUrl
let instro
trainingTaskList,
} = res.result;
let coverId;
let coverUrl;
let instro;
courseMediaVOS.map((item) => {
switch (item.contentType) {
case 'COVER':
coverId = item.mediaContent
coverUrl = item.mediaUrl
break
coverId = item.mediaContent;
coverUrl = item.mediaUrl;
break;
case 'INTRO':
instro = item.mediaContent
break
instro = item.mediaContent;
break;
default:
break
break;
}
return item
})
let _selectOperatorList = []
return item;
});
let _selectOperatorList = [];
if (operateIds) {
_selectOperatorList = operateIds.map((item, index) => {
let _item = {}
_item.id = item
return _item
})
let _item = {};
_item.id = item;
return _item;
});
}
setTaskList(trainingTaskList)
setTaskList(trainingTaskList);
setBasicData({
planName,
coverUrl: coverUrl || defaultCover,
......@@ -105,20 +110,20 @@ function AddPlan() {
operateType,
percentCompleteLive,
percentCompleteVideo,
percentCompletePicture
})
setHasGetDetail(true)
})
percentCompletePicture,
});
setHasGetDetail(true);
});
}
function handleChangeBasicInfo(field, value, option) {
setBasicData({
...basicData,
[field]: value
})
[field]: value,
});
}
function handleChangeTaskInfo(value) {
setTaskList(value)
setTaskList(value);
}
function submitInfo() {
......@@ -132,37 +137,37 @@ function AddPlan() {
percentCompleteVideo,
percentCompletePicture,
coverId,
coverUrl
} = basicData
let input = /^[\s]*$/
coverUrl,
} = basicData;
let input = /^[\s]*$/;
if (!planName || input.test(planName)) {
message.warning('请输入的培训计划名称')
return
message.warning('请输入的培训计划名称');
return;
}
if (operateType === 'Assign_Operate' && selectOperatorList.length === 0) {
message.warning('请选择指定运营师')
return
message.warning('请选择指定运营师');
return;
}
if (!percentCompleteLive && percentCompleteLive !== 0) {
message.warning('请输入完成标准')
return
message.warning('请输入完成标准');
return;
}
if (!percentCompleteVideo && percentCompleteVideo !== 0) {
message.warning('请输入完成标准')
return
message.warning('请输入完成标准');
return;
}
if (!percentCompletePicture && percentCompletePicture !== 0) {
message.warning('请输入完成标准')
return
message.warning('请输入完成标准');
return;
}
if (taskList.length === 0) {
message.warning('请输入培训计划内容')
return
message.warning('请输入培训计划内容');
return;
}
for (let i = 0; i < taskList.length; i++) {
if (input.test(taskList[i].taskName)) {
message.warning('培训任务名称不能为空')
return false
message.warning('培训任务名称不能为空');
return false;
}
if (taskList[i].courseList.length === 0) {
Modal.confirm({
......@@ -170,28 +175,33 @@ function AddPlan() {
content: '每个任务下至少关联一个课程',
okText: '确定',
cancelText: '取消',
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>
})
return false
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>,
});
return false;
}
}
if (editorTextLength > 1000) {
message.warning('简介超过字数限定');
return;
}
let scheduleMediaRequests = []
let scheduleMediaRequests = [];
let coverObj = {
contentType: 'COVER',
mediaContent: coverId,
mediaType: 'PICTURE',
mediaUrl: coverUrl
}
mediaUrl: coverUrl,
};
if (coverId) {
scheduleMediaRequests = [...scheduleMediaRequests, coverObj]
scheduleMediaRequests = [...scheduleMediaRequests, coverObj];
}
let instroObj = {
contentType: 'INTRO',
mediaType: 'TEXT',
mediaContent: instro
}
mediaContent: instro,
};
if (instro) {
scheduleMediaRequests = [...scheduleMediaRequests, instroObj]
scheduleMediaRequests = [...scheduleMediaRequests, instroObj];
}
const params = {
......@@ -205,43 +215,43 @@ function AddPlan() {
planName,
scheduleMediaRequests,
storeId: User.getStoreId(),
trainingTaskList: handleSubmitTaskData(taskList)
}
trainingTaskList: handleSubmitTaskData(taskList),
};
if (type === 'add') {
PlanService.createTrainingPlan(params).then((res) => {
if (res.success) {
message.success('新建成功')
setSubmitDisabled(true)
window.RCHistory.goBack()
message.success('新建成功');
setSubmitDisabled(true);
window.RCHistory.goBack();
}
})
});
} else {
const _params = {
...params,
id
}
id,
};
PlanService.updateTrainingPlan(_params).then((res) => {
if (res.success) {
message.success('更新成功')
window.RCHistory.goBack()
message.success('更新成功');
window.RCHistory.goBack();
}
})
});
}
}
function handleSubmitTaskData(taskData) {
return taskData.map((item, index) => {
let _item = {}
_item.taskId = item.taskId
_item.taskName = item.taskName
let _item = {};
_item.taskId = item.taskId;
_item.taskName = item.taskName;
_item.courseList = item.courseList.map((childItem, index) => {
let _childItem = {}
_childItem.courseId = childItem.courseId
_childItem.courseName = childItem.courseName
_childItem.courseType = childItem.courseType
return _childItem
})
return _item
})
let _childItem = {};
_childItem.courseId = childItem.courseId;
_childItem.courseName = childItem.courseName;
_childItem.courseType = childItem.courseType;
return _childItem;
});
return _item;
});
}
// 取消编辑并返回上一级路由
function handleGoBack() {
......@@ -253,11 +263,11 @@ function AddPlan() {
cancelText: '留在本页',
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>,
onOk: () => {
window.RCHistory.goBack()
}
})
window.RCHistory.goBack();
},
});
} else {
window.RCHistory.goBack()
window.RCHistory.goBack();
}
}
......@@ -293,6 +303,6 @@ function AddPlan() {
</Button>
</div>
</div>
)
);
}
export default AddPlan
export default AddPlan;
/*
* @Author: yuananting
* @Date: 2021-07-05 10:48:08
* @LastEditors: yuananting
* @LastEditTime: 2021-07-07 15:00:20
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
/*
* @Author: zhangleyuan
* @Date: 2021-02-20 16:45:51
* @LastEditors: fusanqiasng
* @LastEditTime: 2021-06-01 15:20:33
* @LastEditors: yuananting
* @LastEditTime: 2021-07-02 17:16:06
* @Description: 描述一下
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -12,7 +21,7 @@ import { withRouter } from 'react-router-dom';
import SelectOperatorModal from '../modal/SelectOperatorModal';
import SelectPrepareFileModal from '@/modules/prepare-lesson/modal/SelectPrepareFileModal';
import Upload from '@/core/upload';
// import PhotoClip from 'photoclip'
import GraphicsEditor from '@/modules/course-manage/components/GraphicsEditor';
import './BasicInfo.less';
const { TextArea } = Input;
......@@ -202,10 +211,14 @@ class BasicInfo extends React.Component {
this.props.onChange(field, _percentCompleteLive);
};
changeIntro = (value) => {
this.props.onChange('introduce', value);
};
render() {
const { operatorModalVisible, showSelectFileModal, visible, hasImgReady, cutImageBlob } = this.state;
const { data } = this.props;
const { planName, coverUrl, instro, enableState, operateType, selectOperatorList, percentCompleteLive, percentCompleteVideo, percentCompletePicture } =
const { planName, coverUrl, introduce, enableState, operateType, selectOperatorList, percentCompleteLive, percentCompleteVideo, percentCompletePicture } =
data;
// 当前是否使用的是默认图片
const isDefaultCover = coverUrl === defaultCover;
......@@ -226,10 +239,6 @@ class BasicInfo extends React.Component {
<div className='cover'>
<span className='label'>封面图:</span>
<div className='cover__wrap'>
<div className='img-content'>
{isDefaultCover && <span className='tag'>默认图</span>}
<img src={coverUrl} width='690' alt='' />
</div>
<div className='opt-btns'>
<Button
onClick={() => {
......@@ -244,36 +253,44 @@ class BasicInfo extends React.Component {
</span>
<div className='tips'>建议尺寸1280*720px或16:9。封面图最大5M,支持jpg、jpeg和png。</div>
</div>
<div className='img-content'>
{isDefaultCover && <span className='tag'>默认图</span>}
<img src={coverUrl} width='690' alt='' />
</div>
</div>
</div>
<div className='introduction'>
<span className='label'>简介:</span>
<TextArea
{/* <TextArea
placeholder='请输入培训计划简介'
maxLength={200}
style={{ width: '552px', height: '110px' }}
className='instro-textarea'
value={instro}
onChange={(e) => this.props.onChange('instro', e.target.value)}
/> */}
<GraphicsEditor
id='intro'
isIntro={true}
maxLimit={1000}
detail={{
content: introduce,
}}
onChange={(val) => {
this.changeIntro(val);
}}
/>
</div>
<div className='wether-use'>
<span className='label'>是否启用:</span>
<div className='content'>
<div>
<Switch
checked={enableState === 'YES' ? true : false}
onChange={() => {
this.enableStateChange();
}}
/>
</div>
<div>
<div className='instro-text'>
<div>开启:此培训计划可以分享给学员进行学习</div>
<div>关闭:此培训计划暂不可分享给学员进行学习,后续可开启</div>
</div>
</div>
<div className='instro-text'>{enableState === 'YES' ? '已开启,培训计划可正常分享给学员学习' : '已关闭,培训计划暂不进行分享和学习'}</div>
</div>
</div>
<div className='view-range'>
......@@ -291,7 +308,7 @@ class BasicInfo extends React.Component {
onChange={(e) => {
this.props.onChange('operateType', e.target.value);
}}>
<Row style={{ marginBottom: '5px' }}>
<Row style={{ marginBottom: '16px' }}>
<Col span={24}>
<Radio value='All_Operate'>
所有运营师
......@@ -317,7 +334,7 @@ class BasicInfo extends React.Component {
选择运营师
</Button>
<span>
已选择<span>{selectOperatorList.length}</span>名运营师
已选择 <span style={{ color: '#2966FF' }}>{selectOperatorList.length}</span> 名运营师
</span>
</div>
)}
......@@ -347,7 +364,7 @@ class BasicInfo extends React.Component {
<div className='live-standard-info'>
<span className='icon iconfont'>&#xe864;</span>
<span className='instro'>
视频课单个课程,学员学习进度达到
线上课单个课节,学员学习进度达到
<Input
width='40'
value={percentCompleteVideo}
......@@ -357,7 +374,7 @@ class BasicInfo extends React.Component {
onBlur={(e) => this.percentCompleteBlur(e, 'percentCompleteVideo')}
className='input-box'
/>
%,即视为"已完成"学习
%,即课节视为"已完成"学习
</span>
</div>
<div className='live-standard-info'>
......
.plan-basic-info{
.plan-basic-info {
.label {
width: 110px;
text-align: right;
display:inline-block;
font-size:14px;
color:#666;
display: inline-block;
font-size: 14px;
color: #666;
.require {
color: #EC4B35;
color: #ec4b35;
}
.iconfont{
font-size:14px;
color:#BFBFBF;
.iconfont {
font-size: 14px;
color: #bfbfbf;
}
}
.cover {
display: flex;
margin-top: 16px;
margin-top: 24px;
&__wrap {
position: relative;
display: flex;
.tag {
border-radius: 2px;
background: #D6D6D6;
background: #d6d6d6;
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #FFF;
color: #fff;
position: absolute;
top: 8px;
left: 8px;
}
}
.img-content {
margin-right: 20px;
position: relative;
margin-top: 8px;
width: 299px;
height: 169px;
img {
......@@ -42,90 +42,93 @@
object-fit: contain;
}
}
.opt-btns{
.opt-btns {
.default-btn {
margin:0 8px;
color: #2966FF;
margin-left: 14px;
color: #2966ff;
cursor: pointer;
&.disabled {
color: #CCC;
color: #ccc;
cursor: not-allowed;
}
}
.tips{
margin-top:8px;
font-size:14px;
color:#999;
.tips {
margin-top: 8px;
font-size: 14px;
color: #999;
}
}
}
.introduction{
margin-top:16px;
.instro-textarea{
.introduction {
display: flex;
margin-top: 24px;
.instro-textarea {
vertical-align: top;
}
}
.wether-use{
display:flex;
margin-top:16px;
.instro-text{
color:#999;
margin-left:12px;
.wether-use {
display: flex;
margin-top: 34px;
.instro-text {
color: #999;
margin-left: 12px;
}
.content{
display:flex;
.content {
display: flex;
}
}
.view-range{
display:flex;
margin-top:16px;
.label{
margin-top:2px;
.view-range {
display: flex;
margin-top: 24px;
.label {
margin-top: 2px;
}
.instro-text{
color:#999;
margin-left:12px;
.instro-text {
color: #999;
margin-left: 12px;
}
.choose-business{
margin-top:16px;
.choose-business {
margin-top: 12px;
.ant-btn {
margin-right: 12px;
}
.playback__text{
margin-left:12px;
color:#999999;
}
.playback__text {
margin-left: 12px;
color: #999999;
}
.done-standard{
}
.done-standard {
display: flex;
margin-top:22px;
.standard-label{
margin-top:3px;
margin-top: 22px;
.standard-label {
margin-top: 3px;
}
.live-standard-info{
margin-bottom:10px;
.live-standard-info {
margin-bottom: 10px;
}
input{
display:inline-block;
width:90px;
height:32px;
input {
display: inline-block;
width: 90px;
height: 32px;
}
.icon{
color:#A0A0A0;
font-size:14px;
margin-right:4px;
.icon {
color: #a0a0a0;
font-size: 14px;
margin-right: 4px;
}
.instro{
color:#333333;
font-size:14px;
.instro {
color: #333333;
font-size: 14px;
}
.input-box{
.input-box {
width: 60px;
height: 32px;
border-radius: 4px;
border: 1px solid #E8E8E8;
color:#333333;
font-size:14px;
margin:0 2px;
border: 1px solid #e8e8e8;
color: #333333;
font-size: 14px;
margin: 0 2px;
}
}
}
......@@ -18,7 +18,7 @@ function ExpiredCourseList(props) {
<span>直播课</span>
}
{ item.courseType === "VOICE " &&
<span>视频</span>
<span>线上</span>
}
</div>
<div className="course-instro">
......
/*
* @Author: yuananting
* @Date: 2021-07-05 10:49:01
* @LastEditors: yuananting
* @LastEditTime: 2021-07-05 11:02:04
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
/*
* @Author: zhangleyuan
* @Date: 2021-02-20 16:45:51
* @LastEditors: fusanqiasng
* @LastEditTime: 2021-05-24 15:15:06
* @LastEditors: yuananting
* @LastEditTime: 2021-07-01 17:16:50
* @Description: 描述一下
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React from 'react'
import { Input, Form, Modal } from 'antd'
import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc'
import React from 'react';
import { Input, Form, Modal } from 'antd';
import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc';
import arrayMove from 'array-move'
import RelatedCourseModal from '../modal/relatedCourseModal'
import { withRouter } from 'react-router-dom'
import './TrainingTask.less'
import arrayMove from 'array-move';
import RelatedCourseModal from '../modal/relatedCourseModal';
import { withRouter } from 'react-router-dom';
import './TrainingTask.less';
const { confirm } = Modal
const { confirm } = Modal;
const CourseType = {
LIVE: {
text: '直播课'
text: '直播课',
},
VOICE: {
text: '视频课'
text: '线上课',
},
RECORD: {
text: '录播课'
text: '录播课',
},
PICTURE: {
text: '图文课'
}
}
text: '图文课',
},
};
const courseStateShow = {
UN_START: {
title: '待开播'
title: '待开播',
},
STARTING: {
title: '直播中'
title: '直播中',
},
FINISH: {
title: '回放'
title: '回放',
},
EXPIRED: {
title: '未成功开课'
}
}
title: '未成功开课',
},
};
const DragHandle = sortableHandle(() => (
<span className='operate__item'>
<span className='icon iconfont'>&#xe7cd;</span>
<span className='text'>移动</span>
</span>
))
));
const SortableTaskItem = sortableElement((props) => <div {...props}>{props.taskitem}</div>)
const SortableTaskContainer = sortableContainer((props) => <div {...props}></div>)
const SortableTaskItem = sortableElement((props) => <div {...props}>{props.taskitem}</div>);
const SortableTaskContainer = sortableContainer((props) => <div {...props}></div>);
const SortableCourseItem = sortableElement((props) => <div {...props}>{props.courseitem}</div>)
const SortableCourseContainer = sortableContainer((props) => <div {...props}></div>)
const SortableCourseItem = sortableElement((props) => <div {...props}>{props.courseitem}</div>);
const SortableCourseContainer = sortableContainer((props) => <div {...props}></div>);
class TrainingTask extends React.Component {
constructor(props) {
super(props)
super(props);
this.state = {
dataSource: this.props.data,
selectedTaskIndex: 0,
relatedCourseModalVisible: false
}
relatedCourseModalVisible: false,
};
}
componentWillMount() {}
componentWillReceiveProps(nextProps) {}
onTaskSortEnd = ({ oldIndex, newIndex }) => {
const { dataSource } = this.state
const { dataSource } = this.state;
if (oldIndex !== newIndex) {
const newData = arrayMove([].concat(dataSource), oldIndex, newIndex).filter((el) => !!el)
const newData = arrayMove([].concat(dataSource), oldIndex, newIndex).filter((el) => !!el);
this.setState(
{
dataSource: newData
dataSource: newData,
},
() => {
this.props.onChange(newData)
}
)
this.props.onChange(newData);
}
);
}
};
onCourseSortEnd = ({ oldIndex, newIndex }, parentIndex) => {
const { dataSource } = this.state
const { dataSource } = this.state;
const _dataSource = [...dataSource]
const _dataSource = [...dataSource];
if (oldIndex !== newIndex) {
_dataSource[parentIndex].courseList = arrayMove([].concat(dataSource[parentIndex].courseList), oldIndex, newIndex).filter((el) => !!el)
_dataSource[parentIndex].courseList = arrayMove([].concat(dataSource[parentIndex].courseList), oldIndex, newIndex).filter((el) => !!el);
this.setState(
{
dataSource: _dataSource
dataSource: _dataSource,
},
() => {
this.props.onChange(_dataSource)
}
)
this.props.onChange(_dataSource);
}
);
}
};
addTask = () => {
const { dataSource } = this.state
const { dataSource } = this.state;
const taskObj = {
taskName: '',
index: dataSource.length,
type: 'input',
open: true,
courseList: []
}
const newData = [...dataSource, taskObj]
courseList: [],
};
const newData = [...dataSource, taskObj];
this.setState(
{
dataSource: newData
dataSource: newData,
},
() => {
this.props.onChange(newData)
}
)
this.props.onChange(newData);
}
);
};
handleRenameTaskName = (e, record) => {
const { value } = e.target
const { dataSource } = this.state
record.taskName = value
const { value } = e.target;
const { dataSource } = this.state;
record.taskName = value;
this.setState(
{
dataSource
dataSource,
},
() => {
this.props.onChange(dataSource)
}
)
this.props.onChange(dataSource);
}
);
};
handleTaskNameBlur = (e, record) => {
const { value } = e.target
const { dataSource } = this.state
let input = /^[\s]*$/
const { value } = e.target;
const { dataSource } = this.state;
let input = /^[\s]*$/;
if (value && !input.test(value)) {
record.type = 'text'
record.type = 'text';
this.setState(
{
dataSource
dataSource,
},
() => {
this.props.onChange(dataSource)
}
)
this.props.onChange(dataSource);
}
);
}
};
handleRenameCourseName = (e, record) => {
const { value } = e.target
const { dataSource } = this.state
record.courseName = value
const { value } = e.target;
const { dataSource } = this.state;
record.courseName = value;
this.setState(
{
dataSource
dataSource,
},
() => {
this.props.onChange(dataSource)
}
)
this.props.onChange(dataSource);
}
);
};
handleCourseNameBlur = (e, record) => {
const { value } = e.target
const { dataSource } = this.state
let input = /^[\s]*$/
const { value } = e.target;
const { dataSource } = this.state;
let input = /^[\s]*$/;
if (value && !input.test(value)) {
record.type = 'text'
record.type = 'text';
this.setState(
{
dataSource
dataSource,
},
() => {
this.props.onChange(dataSource)
}
)
this.props.onChange(dataSource);
}
);
}
};
handleDeleteTask = (index) => {
return confirm({
title: '删除任务',
......@@ -191,23 +200,23 @@ class TrainingTask extends React.Component {
okType: 'danger',
cancelText: '取消',
onOk: () => {
this.handleConfirmDeleteTask(index)
}
})
}
this.handleConfirmDeleteTask(index);
},
});
};
handleConfirmDeleteTask = (index) => {
const { dataSource } = this.state
const newData = [...dataSource]
newData.splice(index, 1)
const { dataSource } = this.state;
const newData = [...dataSource];
newData.splice(index, 1);
this.setState(
{
dataSource: newData
dataSource: newData,
},
() => {
this.props.onChange(newData)
}
)
this.props.onChange(newData);
}
);
};
handleDeleteCourse = (parentIndex, index) => {
return confirm({
......@@ -218,82 +227,82 @@ class TrainingTask extends React.Component {
okType: 'danger',
cancelText: '取消',
onOk: () => {
this.handleConfirmDeleteCourse(parentIndex, index)
}
})
}
this.handleConfirmDeleteCourse(parentIndex, index);
},
});
};
handleConfirmDeleteCourse = (parentIndex, index) => {
const { dataSource } = this.state
const newData = [...dataSource]
const selectData = [...newData[parentIndex].courseList]
selectData.splice(index, 1)
newData[parentIndex].courseList = selectData
const { dataSource } = this.state;
const newData = [...dataSource];
const selectData = [...newData[parentIndex].courseList];
selectData.splice(index, 1);
newData[parentIndex].courseList = selectData;
this.setState(
{
dataSource: newData
dataSource: newData,
},
() => {
this.props.onChange(newData)
}
)
this.props.onChange(newData);
}
);
};
showRelatedCourseModal = (index) => {
this.setState({
selectedTaskIndex: index,
relatedCourseModalVisible: true
})
}
relatedCourseModalVisible: true,
});
};
closeRelatedCourseModal = (index) => {
this.setState({
relatedCourseModalVisible: false
})
}
relatedCourseModalVisible: false,
});
};
confirmSelectCourse = (selectList) => {
console.log('selectList', selectList)
const { selectedTaskIndex } = this.state
const { dataSource } = this.state
const newData = [...dataSource]
const selectData = [...newData[selectedTaskIndex].courseList]
const _selectData = [...selectData, ...selectList]
newData[selectedTaskIndex].courseList = _selectData
console.log('selectList', selectList);
const { selectedTaskIndex } = this.state;
const { dataSource } = this.state;
const newData = [...dataSource];
const selectData = [...newData[selectedTaskIndex].courseList];
const _selectData = [...selectData, ...selectList];
newData[selectedTaskIndex].courseList = _selectData;
this.setState(
{
relatedCourseModalVisible: false,
dataSource: newData
dataSource: newData,
},
() => {
this.props.onChange(newData)
}
)
this.props.onChange(newData);
}
);
};
openOrCloseTask = (index) => {
const { dataSource } = this.state
const newData = [...dataSource]
newData[index].open = !newData[index].open
const { dataSource } = this.state;
const newData = [...dataSource];
newData[index].open = !newData[index].open;
this.setState(
{
dataSource: newData
dataSource: newData,
},
() => {
this.props.onChange(newData)
}
)
this.props.onChange(newData);
}
);
};
handleValidatorTaskName = (rule, value) => {
let input = /^[\s]*$/
let input = /^[\s]*$/;
if (input.test(value) || !value) {
return Promise.reject(new Error('请输入任务名称'))
}
return Promise.resolve()
return Promise.reject(new Error('请输入任务名称'));
}
return Promise.resolve();
};
handleValidatorCourseName = (rule, value) => {
let input = /^[\s]*$/
let input = /^[\s]*$/;
if (input.test(value) || !value) {
return Promise.reject(new Error('请输入课程名称'))
}
return Promise.resolve()
return Promise.reject(new Error('请输入课程名称'));
}
return Promise.resolve();
};
renderTaskItem = (record, index) => {
return (
......@@ -314,8 +323,8 @@ class TrainingTask extends React.Component {
name={['taskName']}
rules={[
{
validator: (rule, value) => this.handleValidatorTaskName(rule, value)
}
validator: (rule, value) => this.handleValidatorTaskName(rule, value),
},
]}>
<Input
className='task-name-input'
......@@ -323,10 +332,10 @@ class TrainingTask extends React.Component {
placeholder='请输入任务名称(20字以内)'
maxLength={20}
onChange={(e) => {
this.handleRenameTaskName(e, record)
this.handleRenameTaskName(e, record);
}}
onBlur={(e) => {
this.handleTaskNameBlur(e, record)
this.handleTaskNameBlur(e, record);
}}
/>
</Form.Item>
......@@ -346,9 +355,9 @@ class TrainingTask extends React.Component {
<span
className='operate__item'
onClick={() => {
const { dataSource } = this.state
record.type = 'input'
this.setState({ dataSource })
const { dataSource } = this.state;
record.type = 'input';
this.setState({ dataSource });
}}>
<span className='icon iconfont'>&#xe6f5;</span>
<span className='text'>重命名</span>
......@@ -356,7 +365,7 @@ class TrainingTask extends React.Component {
<span
className='operate__item'
onClick={() => {
this.handleDeleteTask(index)
this.handleDeleteTask(index);
}}>
<span className='icon iconfont'>&#xe6f6;</span>
<span className='text'>删除</span>
......@@ -377,7 +386,7 @@ class TrainingTask extends React.Component {
<span
className='add-course-btn-disabled'
onClick={() => {
this.showRelatedCourseModal(index)
this.showRelatedCourseModal(index);
}}>
<span>+</span>
<span>关联课程</span>
......@@ -387,7 +396,7 @@ class TrainingTask extends React.Component {
<span
className='add-course-btn'
onClick={() => {
this.showRelatedCourseModal(index)
this.showRelatedCourseModal(index);
}}>
<span>+</span>
<span>关联课程</span>
......@@ -398,9 +407,10 @@ class TrainingTask extends React.Component {
</div>
)}
</div>
)
}
);
};
renderCourseItem = (record, index, parentIndex) => {
console.log(record);
return (
<div className='plan-course-sort-item'>
<div className='course-info'>
......@@ -414,8 +424,8 @@ class TrainingTask extends React.Component {
name={['courseName']}
rules={[
{
validator: (rule, value) => this.handleValidatorCourseName(rule, value)
}
validator: (rule, value) => this.handleValidatorCourseName(rule, value),
},
]}>
<Input
className='course-name-input'
......@@ -423,10 +433,10 @@ class TrainingTask extends React.Component {
placeholder='请输入课程名称(40字以内)'
maxLength={40}
onChange={(e) => {
this.handleRenameCourseName(e, record)
this.handleRenameCourseName(e, record);
}}
onBlur={(e) => {
this.handleCourseNameBlur(e, record)
this.handleCourseNameBlur(e, record);
}}
/>
</Form.Item>
......@@ -441,6 +451,7 @@ class TrainingTask extends React.Component {
{record.courseState === 'EXPIRED' && <span className='icon iconfont tip'>&#xe834;</span>}
{record.courseType === 'LIVE' && <span className='course-state'>{courseStateShow[record.courseState].title}</span>}
{record.courseType === 'VOICE' && <span>(共{record.courseChapterNum || 1}小节)</span>}
</div>
<div className='course-operate'>
<DragHandle />
......@@ -448,18 +459,18 @@ class TrainingTask extends React.Component {
<span
className='operate__item'
onClick={() => {
this.handleDeleteCourse(parentIndex, index)
this.handleDeleteCourse(parentIndex, index);
}}>
<span className='icon iconfont'>&#xe6f6;</span>
<span className='text'>删除</span>
</span>
</div>
</div>
)
}
);
};
render() {
const { dataSource, selectedTaskIndex, relatedCourseModalVisible } = this.state
console.log('dataSource', dataSource)
const { dataSource, selectedTaskIndex, relatedCourseModalVisible } = this.state;
console.log('dataSource', dataSource);
return (
<div className='training-task'>
<SortableTaskContainer useDragHandle disableAutoscroll helperClass='row-dragging' onSortEnd={this.onTaskSortEnd} className='plan-task-sort-container'>
......@@ -494,8 +505,8 @@ class TrainingTask extends React.Component {
/>
)}
</div>
)
);
}
}
export default withRouter(TrainingTask)
export default withRouter(TrainingTask);
......@@ -8,11 +8,11 @@
import React, { useState, useRef, useEffect } from 'react';
import { withRouter } from 'react-router-dom';
import { Row, Input, Select ,Tooltip} from 'antd';
import RangePicker from "@/modules/common/DateRangePicker";
import { Row, Input, Select, Tooltip } from 'antd';
import RangePicker from '@/modules/common/DateRangePicker';
import moment from 'moment';
import StoreService from "@/domains/store-domain/storeService";
import User from '@/common/js/user'
import StoreService from '@/domains/store-domain/storeService';
import User from '@/common/js/user';
import Bus from '@/core/bus';
import './UserLearningDataFilter.less';
......@@ -22,55 +22,52 @@ const userRole = User.getUserRole();
const DEFAULT_QUERY = {
customerName: null,
startTime: null,
endTime:null,
learnState:null,
endTime: null,
learnState: null,
operateId: null,
}
};
const defaultCreatorQuery = {
size: 10,
current: 1,
nickName:null
}
nickName: null,
};
function UserLearningDataFilter(props) {
const [expandFilter, setExpandFilter] = useState(false);
const [query,setQuery] = useState(DEFAULT_QUERY);
const [operateName,setOperateName] = useState(null)
const [hasNext,setHasNext] = useState(false);
const [creatorQuery,setCreatorQuery] = useState(defaultCreatorQuery);
const [creatorList,setCreatorList] = useState([]);
const [query, setQuery] = useState(DEFAULT_QUERY);
const [operateName, setOperateName] = useState(null);
const [hasNext, setHasNext] = useState(false);
const [creatorQuery, setCreatorQuery] = useState(defaultCreatorQuery);
const [creatorList, setCreatorList] = useState([]);
useEffect(() => {
Bus.bind('watchDataView',(record) => handleChangeCreatorQuery(record))
});
Bus.bind('watchDataView', (record) => handleChangeCreatorQuery(record));
}, []);
useEffect(() => {
getCreatorList();
}, []);
function handleChangeCreatorQuery (record){
const _creatorQuery = {...creatorQuery};
function handleChangeCreatorQuery(record) {
const _creatorQuery = { ...creatorQuery };
_creatorQuery.operateId = record.storeUserId;
setCreatorQuery(_creatorQuery);
handleChangeQuery('operateId', record.storeUserId,record.storeUserName)
handleChangeQuery('operateId', record.storeUserId, record.storeUserName);
}
// 改变搜索条件
function handleChangeQuery(field,value,optionValue){
const _query ={
function handleChangeQuery(field, value, optionValue) {
const _query = {
...query,
[field]: value,
current: 1,
}
};
setQuery(_query);
if(field === 'operateId'){
setOperateName(optionValue)
if (field === 'operateId') {
setOperateName(optionValue);
}
if (field === 'customerName') return;
props.onChange( _query);
props.onChange(_query);
}
function handleChangeDates (dates){
function handleChangeDates(dates) {
const _query = _.clone(query);
if (_.isEmpty(dates)) {
delete _query.startTime;
......@@ -79,27 +76,27 @@ function UserLearningDataFilter(props) {
_query.startTime = dates[0].valueOf();
_query.endTime = dates[1].valueOf();
}
const param ={
const param = {
..._query,
current: 1,
}
};
setQuery(param);
props.onChange(param);
}
// 重置搜索条件
function handleReset(){
function handleReset() {
setQuery(DEFAULT_QUERY);
props.onChange(DEFAULT_QUERY);
}
function getCreatorList(current = 1, selectList){
function getCreatorList(current = 1, selectList) {
const _query = {
...creatorQuery,
current,
size:10
size: 10,
};
StoreService.getStoreUserBasicPage( _query).then((res) => {
StoreService.getStoreUserBasicPage(_query).then((res) => {
const { result = {} } = res;
const { records = [], total = 0, hasNext } = result;
const list = current > 1 ? creatorList.concat(records) : records;
......@@ -109,119 +106,148 @@ function UserLearningDataFilter(props) {
}
// 滑动加载更多讲师列表
function handleScrollCreatorList(e){
function handleScrollCreatorList(e) {
const container = e.target;
const scrollToBottom = container && container.scrollHeight <= container.clientHeight + container.scrollTop;
if (scrollToBottom && hasNext) {
const _creatorQuery = {...creatorQuery};
const _creatorQuery = { ...creatorQuery };
_creatorQuery.current = creatorQuery.current + 1;
setCreatorQuery(_creatorQuery);
getCreatorList(creatorQuery.current + 1);
}
}
return (
<div className="user-learn-data-filter">
<div className='user-learn-data-filter'>
<Row>
<div className="search-condition">
<div className="search-condition__item">
<span className="label customer-label">学员:</span>
<div className='search-condition'>
<div className='search-condition__item'>
<span className='label customer-label'>学员:</span>
<Search
value={query.customerName}
placeholder="搜索学员名称"
onChange={(e) => { handleChangeQuery('customerName', e.target.value)}}
onSearch={ () => { props.onChange(query) } }
style={{ width: "calc(100% - 70px)" }}
enterButton={<span className="icon iconfont">&#xe832;</span>}
placeholder='搜索学员名称'
onChange={(e) => {
handleChangeQuery('customerName', e.target.value);
}}
onSearch={() => {
props.onChange(query);
}}
style={{ width: 'calc(100% - 70px)' }}
enterButton={<span className='icon iconfont'>&#xe832;</span>}
/>
</div>
{(User.getUserRole() === "CloudManager" || User.getUserRole() === "StoreManager")&&
<div className="search-condition__item">
<span className="label lead-label">负责人:</span>
{(User.getUserRole() === 'CloudManager' || User.getUserRole() === 'StoreManager') && (
<div className='search-condition__item'>
<span className='label lead-label'>负责人:</span>
<Select
id="leadSelect"
placeholder="请选择员工"
style={{width:"calc(100% - 70px)"}}
id='leadSelect'
placeholder='请选择员工'
style={{ width: 'calc(100% - 70px)' }}
showSearch
allowClear
filterOption={(input, option) => option}
suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}
suffixIcon={
<span className='icon iconfont' style={{ fontSize: '12px', color: '#BFBFBF' }}>
&#xe835;
</span>
}
onPopupScroll={handleScrollCreatorList}
value={operateName}
onChange={(value,option) => {
if(option){
handleChangeQuery('operateId',value,option.children)
}else{
handleChangeQuery('operateId',value,"")
onChange={(value, option) => {
if (option) {
handleChangeQuery('operateId', value, option.children);
} else {
handleChangeQuery('operateId', value, '');
}
}}
onSearch={(value) => {
creatorQuery.nickName = value
setCreatorQuery(creatorQuery)
creatorQuery.nickName = value;
setCreatorQuery(creatorQuery);
getCreatorList();
}
}
onClear ={(value)=>{
}}
onClear={(value) => {
setCreatorQuery({
size: 10,
current: 1,
nickName:null
})
getCreatorList()
}
}
>
nickName: null,
});
getCreatorList();
}}>
{_.map(creatorList, (item, index) => {
return (
<Select.Option value={item.id} key={item.id}>{item.nickName}</Select.Option>
<Select.Option value={item.id} key={item.id}>
{item.nickName}
</Select.Option>
);
})}
</Select>
</div>
}
<div className="search-condition__item">
<span className="label learn-date-label">最近学习日期:</span>
)}
<div className='search-condition__item'>
<span className='label learn-date-label'>最近学习日期:</span>
<RangePicker
id="course_date_picker"
id='course_date_picker'
allowClear={false}
value={ query.startTime ? [moment(query.startTime), moment(query.endTime)] : null }
format={"YYYY-MM-DD"}
onChange={(dates) => { handleChangeDates(dates) }}
style={{ width: "calc(100% - 98px)" }}
value={query.startTime ? [moment(query.startTime), moment(query.endTime)] : null}
format={'YYYY-MM-DD'}
onChange={(dates) => {
handleChangeDates(dates);
}}
style={{ width: 'calc(100% - 98px)' }}
/>
</div>
{ ((expandFilter && (User.getUserRole() === "CloudManager" || User.getUserRole() === "StoreManager")) || User.getUserRole === "CloudOperator")&&
<div className="search-condition__item">
<span className="label learn-status-label">学习状态:</span>
{((expandFilter && (User.getUserRole() === 'CloudManager' || User.getUserRole() === 'StoreManager')) || User.getUserRole === 'CloudOperator') && (
<div className='search-condition__item'>
<span className='label learn-status-label'>学习状态:</span>
<Select
style={{ width: "calc(100% - 70px)" }}
placeholder="请选择当前状态"
style={{ width: 'calc(100% - 70px)' }}
placeholder='请选择当前状态'
allowClear={true}
value={query.learnState}
onChange={(value) => { handleChangeQuery('learnState', value) }}
suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}
>
<Option value="UN_PLAY">未开始</Option>
<Option value="UNDER_WAY">进行中</Option>
<Option value="FINISH">已完成</Option>
onChange={(value) => {
handleChangeQuery('learnState', value);
}}
suffixIcon={
<span className='icon iconfont' style={{ fontSize: '12px', color: '#BFBFBF' }}>
&#xe835;
</span>
}>
<Option value='UN_PLAY'>未开始</Option>
<Option value='UNDER_WAY'>进行中</Option>
<Option value='FINISH'>已完成</Option>
</Select>
</div>
}
)}
</div>
{( User.getUserRole() === "CloudManager" || User.getUserRole() === "StoreManager")&&
<div className="reset-fold-area">
<Tooltip title="清空筛选"><span className="resetBtn iconfont icon" onClick={handleReset}>&#xe61b; </span></Tooltip>
<span style={{ cursor: 'pointer' }} className="fold-btn" onClick={() => {
setExpandFilter(!expandFilter)
}}>{expandFilter ? <span><span>收起</span><span className="iconfont icon fold-icon" >&#xe82d; </span> </span> : <span>展开<span className="iconfont icon fold-icon" >&#xe835; </span></span>}</span>
{(User.getUserRole() === 'CloudManager' || User.getUserRole() === 'StoreManager') && (
<div className='reset-fold-area'>
<Tooltip title='清空筛选'>
<span className='resetBtn iconfont icon' onClick={handleReset}>
&#xe61b;{' '}
</span>
</Tooltip>
<span
style={{ cursor: 'pointer' }}
className='fold-btn'
onClick={() => {
setExpandFilter(!expandFilter);
}}>
{expandFilter ? (
<span>
<span>收起</span>
<span className='iconfont icon fold-icon'>&#xe82d; </span>{' '}
</span>
) : (
<span>
展开<span className='iconfont icon fold-icon'>&#xe835; </span>
</span>
)}
</span>
</div>
}
)}
</Row>
</div>
)
);
}
export default withRouter(UserLearningDataFilter);
/*
* @Author: yuananting
* @Date: 2021-07-05 10:50:30
* @LastEditors: yuananting
* @LastEditTime: 2021-07-06 15:16:47
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import {Table, Modal,Input} from 'antd';
import { PageControl } from "@/components";
import PlanService from '@/domains/plan-domain/planService'
import User from '@/common/js/user'
import { Table, Modal, Input } from 'antd';
import PlanService from '@/domains/plan-domain/planService';
import User from '@/common/js/user';
import './UserLearnDetailModal.less';
import _ from "underscore";
const { Search } = Input;
const defaultCover = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
import _ from 'underscore';
const defaultCover = 'https://image.xiaomaiketang.com/xm/rEAetaTEh3.png';
const CourseType = {
LIVE: {
text: "直播课"
text: '直播课',
},
VOICE : {
text:"视频课"
VOICE: {
text: '线上课',
},
RECORD : {
text:'录播课'
RECORD: {
text: '录播课',
},
PICTURE: {
text: '图文课',
},
PICTURE:{
text:'图文课'
}
};
const courseStateShow = {
UN_START: {
title: "待开播",
title: '待开播',
},
STARTING: {
title: "直播中",
title: '直播中',
},
FINISH: {
title: "回放",
title: '回放',
},
EXPIRED: {
title: "未成功开课",
title: '未成功开课',
},
};
class UserLearnDetailModal extends React.Component {
constructor(props) {
super(props);
this.state = {
planDataSource:{},
taskDataSource:[],
taskSize:10,
planDataSource: {},
taskDataSource: [],
taskSize: 10,
taskQuery: {
current: 1,
},
taskTotalCount:0,
courseDataSource:[],
storeCustomerName:'',
storeCustomerPhone:''
taskTotalCount: 0,
courseDataSource: [],
storeCustomerName: '',
storeCustomerPhone: '',
};
}
componentDidMount() {
this.getPlanCustomerDetail();
}
getPlanCustomerDetail = ()=>{
getPlanCustomerDetail = () => {
PlanService.getPlanCustomerDetail({
planId:getParameterByName("id"),
storeCustomerId:this.props.storeCustomerId,
storeId:User.getStoreId()
planId: getParameterByName('id'),
storeCustomerId: this.props.storeCustomerId,
storeId: User.getStoreId(),
}).then((res) => {
const {
storeCustomerName,
storeCustomerPhone,
planName,
learnFinishPercentage,
taskCustomerVOList,
courseMediaVOS,
}=res.result;
const { storeCustomerName, storeCustomerPhone, planName, learnFinishPercentage, taskCustomerVOList, courseMediaVOS } = res.result;
let coverUrl;
courseMediaVOS.map((item) => {
if(item.contentType === "COVER"){
if (item.contentType === 'COVER') {
coverUrl = item.mediaUrl;
}
return item;
})
});
const planDataSource = {
planName,
learnFinishPercentage,
coverUrl:coverUrl || defaultCover
}
coverUrl: coverUrl || defaultCover,
};
this.setState({
storeCustomerName,
storeCustomerPhone,
planDataSource,
taskDataSource:taskCustomerVOList
})
})
}
taskDataSource: taskCustomerVOList,
});
});
};
parsePlanDataColumns = () => {
const columns = [
{
title: '培训计划名称',
key: 'planName',
dataIndex: 'planName',
width: '77%',
render: (val, record, index) => {
return (
<div className='record-name'>
<div className='img-con'>
<img src={record.coverUrl} />
</div>
<div>{record.planName}</div>
</div>
);
},
},
{
title: '学习进度',
key: 'learnFinishPercentage',
dataIndex: 'learnFinishPercentage',
render: (val, record, index) => {
return <div>{val}%</div>;
},
},
];
return columns;
};
parseTaskColumns = () => {
const columns = [
......@@ -95,133 +125,169 @@ class UserLearnDetailModal extends React.Component {
title: '培训任务',
key: 'taskName',
dataIndex: 'taskName',
render: (val, record,index) => {
width: '68%',
render: (val, record, index) => {
return (
<div className="taskName">
<div className='taskName'>
{index + 1}.{record.taskName}
</div>
)
}
);
},
},
{
title: '学习进度',
key: 'learnFinishPercentage',
dataIndex: 'learnFinishPercentage',
width:167,
render: (val, record) => {
return (
<div className="task-learn-percentage">
{ val === 100 ?<span>已完成</span>:<span>{val}%</span>}
</div>
)
}
}
return <div className='task-learn-percentage'>{val === 100 ? <span>已完成</span> : <span>{val}%</span>}</div>;
},
},
];
return columns;
}
parseCoursecolumns = (parentIndex) => {
};
parseCourseColumns = (parentIndex) => {
const columns = [
{
title: '课程',
key: 'courseName',
dataIndex: 'courseName',
render: (val, record,index) => {
width: '70%',
render: (val, record, index) => {
return (
<div className="course-info">
<div className='course-info'>
<div>
<span className="course-type">{CourseType[record.courseType].text}</span>
<span>{parentIndex + 1}.{index + 1}&nbsp;</span>
<span className='course-type'>{CourseType[record.courseType].text}</span>
<span>
{parentIndex + 1}.{index + 1}&nbsp;
</span>
</div>
<div className="name-and-state">
<span className="course-name">{record.courseName}</span>
{ record.courseType==="LIVE" &&
<span className="course-state">{courseStateShow[record.courseState].title}</span>
}
<div className='name-and-state'>
<span className='course-name'>{record.courseName}</span>
{record.courseType === 'LIVE' && <span className='course-state'>{courseStateShow[record.courseState].title}</span>}
</div>
</div>
)
}
);
},
},
{
title: '学习进度',
key: 'learnFinishPercentage',
dataIndex: 'learnFinishPercentage',
width:152,
render: (val, record) => {
return <div className='course-learn-percentage'>{record.learnState === 'FINISH' ? <span>已完成</span> : <span>{val}%</span>}</div>;
},
},
];
return columns;
};
parseChapterColumns = (chapterIndex) => {
const columns = [
{
title: '课节名称',
key: 'name',
dataIndex: 'name',
width: '80%',
render: (val, record, index) => {
return (
<div className="course-learn-percentage">
{ record.learnState === "FINISH" ?<span>已完成</span>:<span>{val}%</span>}
<div className='chapter-record'>
<span>
{chapterIndex + 1}.{index + 1}&nbsp;
</span>
<img className='chapter-img' src='https://image.xiaomaiketang.com/xm/TKwbQGYDBR.png' />
<span>{record.name}</span>
</div>
)
}
}
);
},
},
{
title: '学习进度',
key: 'watchProgress',
dataIndex: 'watchProgress',
render: (val, record) => {
return <span>{val}%</span>;
},
},
];
return columns;
}
};
render() {
const {storeCustomerName,storeCustomerPhone,planDataSource,taskDataSource,taskQuery,taskTotalCount} = this.state;
const { storeCustomerName, storeCustomerPhone, planDataSource, taskDataSource, taskQuery, taskTotalCount } = this.state;
const { visible } = this.props;
return (
<Modal
title="学员学习详情"
title='学员学习详情'
onCancel={this.props.onClose}
onOk={this.props.onClose}
maskClosable={false}
visible={visible}
className="user-Learn-modal"
className='user-Learn-modal'
closable={true}
width={800}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
>
<div className="customer-info">
<span className="customer-name">
closeIcon={<span className='icon iconfont modal-close-icon'>&#xe6ef;</span>}>
<div className='customer-info'>
<span className='customer-name'>
<span>学员:</span>
<span>{storeCustomerName}</span>
</span>
<span className="customer-phone">
<span className='customer-phone'>
<span>手机号:</span>
<span>{storeCustomerPhone}</span>
</span>
</div>
<div className="plan-instro">
<div className="img-con">
<img src={planDataSource.coverUrl}/>
</div>
<div>
<div className="plan-name">{planDataSource.planName}</div>
<div className="task-learn-percentage">
<span>学习进度: {planDataSource.learnFinishPercentage}%</span>
</div>
</div>
</div>
<div>
<div className='table-box'>
<Table className='plan-table' dataSource={[planDataSource]} columns={this.parsePlanDataColumns()} pagination={false} />
<Table
className='task-table'
rowKey={(record) => record.taskId}
className="task-table"
dataSource={taskDataSource}
columns={this.parseTaskColumns()}
pagination={false}
expandedRowRender={(record,index) => {
if(!record.courseVOList){
return
}
if (record.courseVOList.length !== 0 ){
return <div>
expandable={{
expandedRowRender: (record, index) => {
return (
<div>
<Table
pagination={false}
showHeader={false}
className='child-table'
rowKey={(record) => record.courseId}
dataSource={record.courseVOList}
columns={this.parseCoursecolumns(index)}
className="child-table"
columns={this.parseCourseColumns(index)}
pagination={false}
expandable={{
expandedRowRender: (chapterRecord, chapterIndex) => {
return (
<div>
<Table
showHeader={false}
dataSource={chapterRecord.courseChapterList}
columns={this.parseChapterColumns(chapterIndex)}
pagination={false}
/>
</div>
);
},
rowExpandable: (record) => record.courseChapterList && record.courseChapterList.length > 1,
}}
/>
</div>
);
},
rowExpandable: (record) => record.courseVOList && record.courseVOList.length !== 0,
}}
rowClassName={(record, index) => {
if (index % 2 === 0) {
return 'odd-row';
} else {
return 'even-row';
}
}}
rowClassName={(record,index)=>{if(index%2===0){return 'odd-row'}else{ return 'even-row'}}}
/>
</div>
</Modal>
)
);
}
}
......
.user-Learn-modal{
.customer-info{
margin-bottom:16px;
.customer-name{
font-size:14px;
color:#333;
margin-right:32px;
.user-Learn-modal {
.customer-info {
margin-bottom: 16px;
.customer-name {
font-size: 14px;
color: #333;
margin-right: 32px;
}
.customer-phone{
font-size:14px;
color:#333;
.customer-phone {
font-size: 14px;
color: #333;
}
}
.plan-instro{
.plan-instro {
display: flex;
align-items: center;
margin-bottom:16px;
.img-con{
margin-right:8px;
img{
margin-bottom: 16px;
.img-con {
margin-right: 8px;
img {
width: 97px;
height: 54px;
display: inline-block;
border-radius:4px;
}
}
.plan-name{
color:#333;
font-size:16px;
}
.plan-learn-percentage{
color:#333;
font-size:14px;
}
}
.task-table{
.ant-table-thead{
tr {
th{
padding:9px 16px;
}
}
}
tr{
td{
padding:14px 16px;
}
}
.taskName{
color:#333;
font-size:14px;
}
.task-learn-percentage{
color:#333;
font-size:14px;
}
.course-info{
display:flex;
margin-left:57px;
align-items: center;
.course-type{
font-size:11px;
color:#666666;
padding:0px 6px;
border: 1px solid #999999;
margin-right:4px;
border-radius: 2px;
line-height: 16px;
}
.name-and-state{
flex:1;
.course-name{
color:#666666;
font-size:14px;
margin-right:8px;
}
.tip{
font-size:14px;
color:#FF4F4F;
margin-right:2px;
}
.course-state{
color:#999;
font-size:14px;
}
}
}
.ant-table-expanded-row{
td{
padding:0 16px;
}
border-radius: 4px;
}
.ant-table-content{
border:1px solid #e8e8e8;
tr{
td{
border:none;
}
.child-table{
.ant-table-content{
border:none;
thead{
display:none;
}
tbody tr {
td{
border-bottom:none;
padding:14px 16px;
.plan-name {
color: #333;
font-size: 16px;
}
.plan-learn-percentage {
color: #333;
font-size: 14px;
}
}
.table-box {
.child-table {
margin-left: 40px;
tr > td {
&:first-child {
padding-right: 0 !important;
}
}
.odd-row{
background:transparent;
td{
background: #FFF;
}
& + .ant-table-expanded-row{
background:transparent;
td{
background: #FFF;
}
}
&:hover{
& + .ant-table-expanded-row{
background:transparent;
td{
background: #F3f6fa !important;
}
&:nth-child(2) {
padding-left: 0 !important;
}
}
.course-info {
display: flex;
.course-type {
border-radius: 2px;
border: 1px solid #999999;
font-size: 11px;
color: #666666;
padding: 1px 8px;
margin-right: 6px;
}
.even-row{
background:transparent;
td{
background: #FAFAFA;
}
& + .ant-table-expanded-row{
background:transparent;
td{
background: #FAFAFA;
}
}
&:hover{
& + .ant-table-expanded-row{
background:transparent;
td{
background: #F3f6fa !important;
.chapter-record {
display: flex;
align-items: center;
.chapter-img {
width: 18px;
height: 18px;
margin: 0 10px;
}
}
.record-name {
display: flex;
align-items: center;
.img-con {
margin-right: 8px;
img {
width: 97px;
height: 54px;
border-radius: 4px;
}
}
}
......
/*
* @Author: yuananting
* @Date: 2021-07-05 10:50:10
* @LastEditors: yuananting
* @LastEditTime: 2021-07-05 11:02:28
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import _ from 'underscore';
import { Table, Radio, Tabs, Modal, Input, message, Button, Tooltip } from 'antd';
import { PageControl, XMTable } from '@/components';
import college from '@/common/lottie/college';
import { PageControl } from '@/components';
import CourseService from '@/domains/course-domain/CourseService';
import User from '@/common/js/user';
......@@ -79,11 +87,11 @@ class SelectOperatorModal extends React.Component {
selectVideo: {
external: [],
internal: [],
}, //弹窗内已选择的视频课程
}, //弹窗内已选择的线上课程
currentVideoCourseListData: {
external: [],
internal: [],
}, //页面中已关联的视频课程
}, //页面中已关联的线上课程
pictureDataSource: [],
pictureSize: 10,
......@@ -91,8 +99,8 @@ class SelectOperatorModal extends React.Component {
current: 1,
},
pictureTotalCount: 0,
selectPicture: [], //弹窗内已选择的视频课程
currentPictureCourseListData: [], //页面中已关联的视频课程
selectPicture: [], //弹窗内已选择的线上课程
currentPictureCourseListData: [], //页面中已关联的线上课程
activeKey: 'video',
currentTaskCourseData: this.props.data[this.props.selectedTaskIndex].courseList || [],
......@@ -136,7 +144,7 @@ class SelectOperatorModal extends React.Component {
});
};
// 获取视频课列表
// 获取线上课列表
handleFetchVideoDataList = () => {
const { videoQuery, videoSize, videoDataSource, videoTotalCount, videoCourseDivision } = this.state;
......@@ -408,12 +416,13 @@ class SelectOperatorModal extends React.Component {
},
},
{
title: '课程时长',
key: 'courseTime',
dataIndex: 'courseTime',
title: '课节数',
key: 'courseChapterNum',
dataIndex: 'courseChapterNum',
width: '20%',
align: 'right',
render: (val, record) => {
return <span className='course-status'>{dealTimeDuration(record.videoDuration)}</span>;
return <span>{val || 1}</span>;
},
},
{
......@@ -616,7 +625,7 @@ class SelectOperatorModal extends React.Component {
className='link-create-course'
href={window.location.origin + window.location.pathname + '#/create-video-course?type=add'}
onClick={this.props.onClose}>
没有找到需要的视频课?<span>去创建</span>
没有找到需要的线上课?<span>去创建</span>
</a>
);
break;
......@@ -734,11 +743,7 @@ class SelectOperatorModal extends React.Component {
</div>
</div>
<div>
<XMTable
renderEmpty={{
image: college,
description: '暂无数据',
}}
<Table
rowKey={(record) => record.liveCourseId}
dataSource={liveDataSource}
columns={this.parseLiveColumns()}
......@@ -791,7 +796,7 @@ class SelectOperatorModal extends React.Component {
</div>
</TabPane>
<TabPane tab='视频课' key='video'>
<TabPane tab='线上课' key='video'>
<Radio.Group value={videoCourseDivision} onChange={this.videoCourseDivisionChange} style={{ marginBottom: 16 }}>
<Radio.Button value='internal'>内部课程</Radio.Button>
<Radio.Button value='external'>外部课程</Radio.Button>
......@@ -829,11 +834,7 @@ class SelectOperatorModal extends React.Component {
</div>
</div>
<div>
<XMTable
renderEmpty={{
image: college,
description: '暂无数据',
}}
<Table
rowKey={(record) => record.id}
dataSource={videoDataSource[videoCourseDivision]}
columns={this.parseVideoColumns()}
......@@ -928,11 +929,7 @@ class SelectOperatorModal extends React.Component {
</div>
</div>
<div>
<XMTable
renderEmpty={{
image: college,
description: '暂无数据',
}}
<Table
rowKey={(record) => record.id}
dataSource={pictureDataSource}
columns={this.parsePictureColumns()}
......
......@@ -37,7 +37,7 @@ class ScanFileModal extends React.Component {
{fileType === "MP4" && (
<div>
<Player
src={item.ossAddress || item.ossUrl}
src={item.mediaUrl || item.ossAddress || item.ossUrl}
fluid={false}
height={306}
width={"100%"}
......
import React, { useState, useRef, useEffect, useContext } from 'react'
import Breadcrumbs from "@/components/Breadcrumbs";
import React, { useState, useRef, useEffect, useContext } from 'react';
import Breadcrumbs from '@/components/Breadcrumbs';
import { Form, Alert, Input, Button, InputNumber, DatePicker, Switch, Radio, message, Modal, Tooltip } from 'antd';
import { Route, withRouter } from 'react-router-dom';
import User from "@/common/js/user";
import moment from 'moment'
import Service from "@/common/js/service";
import _ from 'underscore'
import User from '@/common/js/user';
import moment from 'moment';
import Service from '@/common/js/service';
import _ from 'underscore';
import GraphicsEditor from '../../course-manage/components/GraphicsEditor';
import SelectPaperModal from './SelectPaperModal'
import PreviewModal from './PreviewModal'
import ShowTips from "@/components/ShowTips";
import SelectPaperModal from './SelectPaperModal';
import PreviewModal from './PreviewModal';
import ShowTips from '@/components/ShowTips';
import './AddExam.less';
......@@ -20,7 +20,7 @@ function AddExam(props: any) {
const [showModal, setShowModal] = useState(false);
const [paperInfo, setPaperInfo] = useState(paperInfoInit);
const [paperId, setPaperId] = useState('');
const [passRate, setPassRate] = useState(60);//及格线
const [passRate, setPassRate] = useState(60); //及格线
const [examStartTime, setStartTime] = useState('');
const [examEndTime, setExamEndTime] = useState('');
const [examName, setExamName] = useState('');
......@@ -43,29 +43,28 @@ function AddExam(props: any) {
useEffect(() => {
switch (props.type) {
case "copy": // 考试列表-复制考试进入
case "edit": // 考试列表-编辑考试进入
case 'copy': // 考试列表-复制考试进入
case 'edit': // 考试列表-编辑考试进入
queryExamDetail();
break;
case "organizeExam": // 试卷列表-组织考试进入
case "newPaperToAddExam": // 组卷页面-新建保存试卷并组织考试
case "editPaperToAddExam": // 组卷页面-编辑保存试卷并组织考试
case 'organizeExam': // 试卷列表-组织考试进入
case 'newPaperToAddExam': // 组卷页面-新建保存试卷并组织考试
case 'editPaperToAddExam': // 组卷页面-编辑保存试卷并组织考试
setGetData(true);
setPaperInfo(props.paperInfo);
break;
}
}, [])
}, []);
useEffect(() => {
setPaperId(paperInfo.paperId)
setPassRate(paperInfo.passRate)
}, [paperInfo.paperId])
setPaperId(paperInfo.paperId);
setPassRate(paperInfo.passRate);
}, [paperInfo.paperId]);
useEffect(() => {
setPassScore(Math.round((paperInfo.totalScore || 0) * (passRate || 0) as any / 100))
setExamTotal(paperInfo.singleChoiceCnt + paperInfo.multiChoiceCnt + paperInfo.judgeCnt + paperInfo.gapFillingCnt + paperInfo.indefiniteChoiceCnt || 0)
}, [paperInfo.paperId, passRate])
setPassScore(Math.round((((paperInfo.totalScore || 0) * (passRate || 0)) as any) / 100));
setExamTotal(paperInfo.singleChoiceCnt + paperInfo.multiChoiceCnt + paperInfo.judgeCnt + paperInfo.gapFillingCnt + paperInfo.indefiniteChoiceCnt || 0);
}, [paperInfo.paperId, passRate]);
function disabledDate(current: any) {
// Can not select days before today and today
......@@ -73,35 +72,34 @@ function AddExam(props: any) {
}
function queryExamDetail() {
Service.Hades("public/hades/queryExamDetail", {
Service.Hades('public/hades/queryExamDetail', {
examId: match.params.id,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
source: 0
source: 0,
}).then((res) => {
const { result } = res
setPaperInfo(result.examPaper)
setPaperId(result.examPaper.paperId)
setStartTime(props.type === 'edit' ? result.examStartTime : '')
setExamEndTime(props.type === 'edit' ? result.examEndTime : '')
setExamName(props.type === 'edit' ? result.examName : `${result.examName}(复制)`)
setPassRate(result.passRate * 100)
setNeedPhone(result.needPhone)
setExamDesc(result.examDesc)
setExamDuration(result.examDuration / 60 / 1000 as any)
setAnswerAnalysis(result.answerAnalysis)
setNeedOptionDisorder(result.needOptionDisorder)
setPassScore(result.passScore)
setResultContent(result.resultContent)
setResultShow(result.resultShow)
setGetData(true)
})
const { result } = res;
setPaperInfo(result.examPaper);
setPaperId(result.examPaper.paperId);
setStartTime(props.type === 'edit' ? result.examStartTime : '');
setExamEndTime(props.type === 'edit' ? result.examEndTime : '');
setExamName(props.type === 'edit' ? result.examName : `${result.examName}(复制)`);
setPassRate(result.passRate * 100);
setNeedPhone(result.needPhone);
setExamDesc(result.examDesc);
setExamDuration((result.examDuration / 60 / 1000) as any);
setAnswerAnalysis(result.answerAnalysis);
setNeedOptionDisorder(result.needOptionDisorder);
setPassScore(result.passScore);
setResultContent(result.resultContent);
setResultShow(result.resultShow);
setGetData(true);
});
}
function handleSave() {
if (request.current) {
return
return;
}
setCheck(true);
......@@ -124,87 +122,82 @@ function AddExam(props: any) {
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
source: 0,
examId: ''
}
examId: '',
};
if (!param.examName) {
message.warning('请输入考试名称');
return
return;
}
if (param.examName && param.examName.length > 40) {
message.warning('考试名称最多40字');
return
return;
}
if (!paperId) {
message.warning('请选择试卷');
return
return;
}
if (!passRate) {
message.warning('请输入及格线');
return
return;
}
if (!examStartTime || !examEndTime) {
message.warning('请选择考试起止时间');
return
return;
}
if (Number(examStartTime) < moment().valueOf()) {
message.warning('开始时间不能早于现在');
return
return;
}
if (!examDuration) {
message.warning('请输入考试时长');
return
return;
}
if (examStartTime + (examDuration as any) * 60 * 1000 > examEndTime) {
message.warning('考试时长不得超过考试有效期时长');
return
return;
}
if (desclen > 1000) {
message.warning('内容过长,不能超过1000字');
return
return;
}
request.current = true;
setTimeout(() => {
request.current = false
}, 2000)
request.current = false;
}, 2000);
if (props.type === 'edit') {
param.examId = match.params.id;
}
Service.Hades(props.type === 'edit' ? 'public/hades/editExam' : "public/hades/createExam", param).then((res) => {
Service.Hades(props.type === 'edit' ? 'public/hades/editExam' : 'public/hades/createExam', param).then((res) => {
message.success(props.type === 'edit' ? '编辑成功' : '创建成功');
switch (props.type) {
case "organizeExam": // 试卷列表-组织考试进入
case "newPaperToAddExam": // 组卷保存组织考试
case "editPaperToAddExam":
window.RCHistory.push("/examination-manage-index")
case 'organizeExam': // 试卷列表-组织考试进入
case 'newPaperToAddExam': // 组卷保存组织考试
case 'editPaperToAddExam':
window.RCHistory.push('/examination-manage-index');
break;
case "add":
case "edit": // 考试列表-新建或编辑
case "copy": // 考试列表-新建或编辑
props.freshList()
case 'add':
case 'edit': // 考试列表-新建或编辑
case 'copy': // 考试列表-新建或编辑
props.freshList();
props.history.goBack();
break;
}
})
});
}
function disabledRangeTime(date: any, type: any) {
if (moment(date).isSame(moment(), 'day')) {
return {
disabledHours: () => {
......@@ -225,7 +218,6 @@ function AddExam(props: any) {
}
return minutes;
},
};
}
return {
......@@ -235,27 +227,25 @@ function AddExam(props: any) {
};
}
function handleGoBack() {
Modal.confirm({
title: '确定要返回吗?',
content: '返回后,本次编辑的内容将不被保存',
okText: '确认返回',
cancelText: '留在本页',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
icon: <span className='icon iconfont default-confirm-icon'>&#xe6f4;</span>,
onOk: () => {
window.RCHistory.push("/examination-manage-index")
}
})
window.RCHistory.push('/examination-manage-index');
},
});
}
let title = '';
switch (props.type) {
case 'add':
case "organizeExam":
case "newPaperToAddExam":
case "editPaperToAddExam":
case 'organizeExam':
case 'newPaperToAddExam':
case 'editPaperToAddExam':
title = '新建考试';
break;
case 'edit':
......@@ -267,213 +257,249 @@ function AddExam(props: any) {
default:
break;
}
return <div className="page examPage">
return (
<div className='page examPage'>
<Breadcrumbs navList={title} goBack={handleGoBack} />
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企学院保有依据国家规定及平台规则进行处理的权利" />
</div> <div className="form">
<div className="title">基本信息</div>
<Form
labelCol={{ span: 3 }}
wrapperCol={{ span: 14 }}
layout="horizontal"
>
<Form.Item label="考试名称"
validateStatus={(check && (!examName ? '请输入考试名称' : (examName.length > 40) && '考试名称最多40字')) ? 'error' : ''}
help={check && (!examName ? '请输入考试名称' : (examName.length > 40) && '考试名称最多40字')}
<div className='box'>
<div className='show-tips'>
<ShowTips message='请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企学院保有依据国家规定及平台规则进行处理的权利' />
</div>{' '}
<div className='form'>
<div className='title'>基本信息</div>
<Form labelCol={{ span: 3 }} wrapperCol={{ span: 14 }} layout='horizontal'>
<Form.Item
label='考试名称'
validateStatus={check && (!examName ? '请输入考试名称' : examName.length > 40 && '考试名称最多40字') ? 'error' : ''}
help={check && (!examName ? '请输入考试名称' : examName.length > 40 && '考试名称最多40字')}
required>
<Input placeholder='请输入考试名称(40字以内)' maxLength={40} value={examName} onChange={(e) => {
setExamName(e.target.value)
}} style={{ width: 320 }} />
<Input
placeholder='请输入考试名称(40字以内)'
maxLength={40}
value={examName}
onChange={(e) => {
setExamName(e.target.value);
}}
style={{ width: 320 }}
/>
</Form.Item>
<Form.Item label="选择试卷"
validateStatus={(check && !paperId) ? 'error' : ''}
help={check && !paperId && '请选择试卷'}
required>
<Button onClick={() => { setShowModal(true) }} >{paperInfo.paperId ? '重新选择' : '选择试卷'}</Button>
{
paperInfo.paperId && <div className="paperTitle"><img src="https://image.xiaomaiketang.com/xm/pY5imEhjzw.png" alt="" /> {paperInfo.paperName}</div>
}
{
paperInfo.paperId && <div className="table">
<div className="header">
<div className="item">单选题</div>
<div className="item">多选题</div>
<div className="item">判断题</div>
<div className="item">填空题</div>
<div className="item long">不定项选择题</div>
<div className="item">合计</div>
<Form.Item label='选择试卷' validateStatus={check && !paperId ? 'error' : ''} help={check && !paperId && '请选择试卷'} required>
<Button
onClick={() => {
setShowModal(true);
}}>
{paperInfo.paperId ? '重新选择' : '选择试卷'}
</Button>
{paperInfo.paperId && (
<div className='paperTitle'>
<img src='https://image.xiaomaiketang.com/xm/pY5imEhjzw.png' alt='' /> {paperInfo.paperName}
</div>
<div className="body-list">
<div className="item">{paperInfo.singleChoiceCnt || 0}</div>
<div className="item">{paperInfo.multiChoiceCnt || 0}</div>
<div className="item">{paperInfo.judgeCnt || 0}</div>
<div className="item">{paperInfo.gapFillingCnt || 0}</div>
<div className="item long">{paperInfo.indefiniteChoiceCnt || 0}</div>
<div className="item">{examTotal}</div>
)}
{paperInfo.paperId && (
<div className='table'>
<div className='header'>
<div className='item'>单选题</div>
<div className='item'>多选题</div>
<div className='item'>判断题</div>
<div className='item'>填空题</div>
<div className='item long'>不定项选择题</div>
<div className='item'>合计</div>
</div>
<div className="body-list">
<div className="item">{paperInfo.singleChoiceScore || 0}</div>
<div className="item">{paperInfo.multiChoiceScore || 0}</div>
<div className="item">{paperInfo.judgeScore || 0}</div>
<div className="item">{paperInfo.gapFillingScore || 0}</div>
<div className="item long">{paperInfo.indefiniteChoiceScore || 0}</div>
<div className="item">{paperInfo.totalScore || 0}</div>
<div className='body-list'>
<div className='item'>{paperInfo.singleChoiceCnt || 0}</div>
<div className='item'>{paperInfo.multiChoiceCnt || 0}</div>
<div className='item'>{paperInfo.judgeCnt || 0}</div>
<div className='item'>{paperInfo.gapFillingCnt || 0}</div>
<div className='item long'>{paperInfo.indefiniteChoiceCnt || 0}</div>
<div className='item'>{examTotal}</div>
</div>
<div className='body-list'>
<div className='item'>{paperInfo.singleChoiceScore || 0}</div>
<div className='item'>{paperInfo.multiChoiceScore || 0}</div>
<div className='item'>{paperInfo.judgeScore || 0}</div>
<div className='item'>{paperInfo.gapFillingScore || 0}</div>
<div className='item long'>{paperInfo.indefiniteChoiceScore || 0}</div>
<div className='item'>{paperInfo.totalScore || 0}</div>
</div>
}
</div>
)}
</Form.Item>
<Form.Item
label={<div>
label={
<div>
<span>及格线</span>
<Tooltip title="默认为选中试卷所设置的及格线,可修改">
<span className="icon iconfont" style={{ color: '#BFBFBF', marginLeft: 4 }}>&#xe61d;</span>
<Tooltip title='默认为选中试卷所设置的及格线,可修改'>
<span className='icon iconfont' style={{ color: '#BFBFBF', marginLeft: 4 }}>
&#xe61d;
</span>
</Tooltip>
</div>}
</div>
}
style={{ marginTop: 24 }}
validateStatus={(check && !passRate) ? 'error' : ''}
validateStatus={check && !passRate ? 'error' : ''}
help={check && !passRate && '请输入及格线'}
required
>
<InputNumber value={passRate} min={0} max={100} onChange={(value: any) => { setPassRate(parseInt(value)) }} style={{ width: 100 }} />
<span style={{ marginLeft: 4 }}>%
required>
<InputNumber
value={passRate}
min={0}
max={100}
onChange={(value: any) => {
setPassRate(parseInt(value));
}}
style={{ width: 100 }}
/>
<span style={{ marginLeft: 4 }}>%</span>
<span style={{ marginLeft: 16, color: '#999' }}>
{` 总分(${paperInfo.totalScore || 0})*及格线(${passRate || 0}%)=及格分数(${passScore})`}
</span>
<span style={{ marginLeft: 16, color: "#999" }}>
{` 总分(${paperInfo.totalScore || 0})*及格线(${passRate || 0}%)=及格分数(${passScore})`}</span>
</Form.Item>
<Form.Item label="考试有效期"
validateStatus={(check && !examStartTime) ? 'error' : ''}
<Form.Item
label='考试有效期'
validateStatus={check && !examStartTime ? 'error' : ''}
help={check && !examStartTime && '请选择考试起止时间'}
required>
<RangePicker
style={{ width: 320 }}
showTime={{ defaultValue: [moment().add(5, 'minutes'), moment().add(5, 'minutes')] }}
ranges={{
'近七天': [moment().add(5, 'minute'), moment().add(6, 'day').endOf('day')],
'近1个月': [moment().add(5, 'minute'), moment().add(1, 'month').endOf('day')],
'近3个月': [moment().add(5, 'minute'), moment().add(3, 'month').endOf('day')],
近七天: [moment().add(5, 'minute'), moment().add(6, 'day').endOf('day')],
1个月: [moment().add(5, 'minute'), moment().add(1, 'month').endOf('day')],
3个月: [moment().add(5, 'minute'), moment().add(3, 'month').endOf('day')],
}}
disabledDate={disabledDate}
value={[
examStartTime ? moment(Number(examStartTime)) : null,
examEndTime ? moment(Number(examEndTime)) : null
]}
value={[examStartTime ? moment(Number(examStartTime)) : null, examEndTime ? moment(Number(examEndTime)) : null]}
disabledTime={disabledRangeTime}
format="YYYY/MM/DD HH:mm"
format='YYYY/MM/DD HH:mm'
onChange={(date: any) => {
setStartTime(date && date[0]?.valueOf());
setExamEndTime(date && date[1]?.valueOf());
}}
/>
</Form.Item>
<Form.Item label="考试时长"
validateStatus={(check && !examDuration) ? 'error' : ''}
help={check && !examDuration && '请输入考试时长'}
required>
<InputNumber value={examDuration} max={1440} min={1} onChange={(value: any) => { setExamDuration(parseInt(value) as any) }} style={{ width: 100 }} />
<span style={{ marginLeft: 4 }}>分钟
</span>
<span style={{ marginLeft: 16, color: "#999" }}>
{` 时长不能超过1440分钟(24小时)`}</span>
<Form.Item label='考试时长' validateStatus={check && !examDuration ? 'error' : ''} help={check && !examDuration && '请输入考试时长'} required>
<InputNumber
value={examDuration}
max={1440}
min={1}
onChange={(value: any) => {
setExamDuration(parseInt(value) as any);
}}
style={{ width: 100 }}
/>
<span style={{ marginLeft: 4 }}>分钟</span>
<span style={{ marginLeft: 16, color: '#999' }}>{` 时长不能超过1440分钟(24小时)`}</span>
</Form.Item>
<Form.Item label="考试说明"
validateStatus={(check && (desclen > 1000)) ? 'error' : ''}
help={check && (desclen > 1000) && '最多只能输入1000个字'}
>
{
(getData || (props.type === 'add')) && <GraphicsEditor
<Form.Item label='考试说明' validateStatus={check && desclen > 1000 ? 'error' : ''} help={check && desclen > 1000 && '最多只能输入1000个字'}>
{(getData || props.type === 'add') && (
<GraphicsEditor
maxLimit={1000}
isIntro={true}
detail={{
content: examDesc
content: examDesc,
}}
onChange={(val: any, len: any) => {
setExamDesc(val);
setDescLen(len);
}}
onChange={(val: any, len: any) => { setExamDesc(val); setDescLen(len) }}
/>
}
)}
</Form.Item>
<div className="title" style={{ marginTop: 40 }}>考试设置</div>
<Form.Item label="身份验证" required>
<div style={{ display: 'flex', marginLeft: 4, }}>
<Switch style={{ position: 'relative', top: 6 }}
<div className='title' style={{ marginTop: 40 }}>
考试设置
</div>
<Form.Item label='身份验证' required>
<div style={{ display: 'flex', marginLeft: 4 }}>
<Switch
checked={needPhone == 'NEED_PHONE_VERIFY'}
onChange={(val) => { setNeedPhone(val ? 'NEED_PHONE_VERIFY' : 'DO_NOT_NEED_PHONE_VERIFY') }}
></Switch>
<div style={{ position: 'relative', top: 3, left: 8, color: "#999" }}><p>开启:需要绑定手机号的学员才能参加考试</p>
<p>关闭:微信/企业微信登陆直接参加考试</p></div>
onChange={(val) => {
setNeedPhone(val ? 'NEED_PHONE_VERIFY' : 'DO_NOT_NEED_PHONE_VERIFY');
}}></Switch>
<div style={{ position: 'relative', left: 8, color: '#999' }}>
{needPhone == 'NEED_PHONE_VERIFY' ? '已开启,学员需绑定手机号才可参与考试' : '已关闭,学员无需绑定手机号即可参与考试'}
</div>
</div>
</Form.Item>
<Form.Item label="选项乱序" required>
<div style={{ display: 'flex', marginLeft: 4, }}>
<Switch style={{ position: 'relative', top: 6 }}
<Form.Item label='选项乱序' required>
<div style={{ display: 'flex', marginLeft: 4 }}>
<Switch
checked={needOptionDisorder == 'OPTION_RANDOM'}
onChange={(val) => { setNeedOptionDisorder(val ? 'OPTION_RANDOM' : 'OPTION_SORT') }}
></Switch>
<div style={{ position: 'relative', top: 3, left: 8, color: "#999" }}><p>开启:选择题的选项随机排序</p>
<p>关闭:选择题按题目原有顺序展示</p></div>
onChange={(val) => {
setNeedOptionDisorder(val ? 'OPTION_RANDOM' : 'OPTION_SORT');
}}></Switch>
<div style={{ position: 'relative', left: 8, color: '#999' }}>
{needOptionDisorder == 'OPTION_RANDOM' ? '已开启,选项随机排序' : '已关闭,选项按设置顺序排序'}
</div>
</div>
</Form.Item>
<Form.Item label="考试结果查看" required>
<Radio.Group onChange={(e: any) => { setResultShow(e.target.value) }} value={resultShow}>
<Form.Item label='考试结果查看' required>
<Radio.Group
onChange={(e: any) => {
setResultShow(e.target.value);
}}
value={resultShow}>
<Radio value={'IMMEDIATELY'}>交卷后立即显示考试结果</Radio>
<Radio value={'AFTER_EXAM_END'}>到达考试截止日期才显示结果</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label=" 考试结果内容" required>
<Radio.Group onChange={(e: any) => { setResultContent(e.target.value) }} value={resultContent}>
<Form.Item label=' 考试结果内容' required>
<Radio.Group
onChange={(e: any) => {
setResultContent(e.target.value);
}}
value={resultContent}>
<Radio value={'PASS_AND_SCORE'}>显示考试分数和是否及格</Radio>
<Radio value={'ONLY_SCORE'}>仅显示考试分数</Radio>
<Radio value={'ONLY_PASS'}>仅显示是否及格</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="答案与解析" required>
<Radio.Group onChange={(e: any) => { setAnswerAnalysis(e.target.value) }} value={answerAnalysis}>
<Form.Item label='答案与解析' required>
<Radio.Group
onChange={(e: any) => {
setAnswerAnalysis(e.target.value);
}}
value={answerAnalysis}>
<Radio value={'ANALYSE_AND_RIGHT_OR_WRONG'}>显示对错与解析</Radio>
<Radio value={'RIGHT_OR_WRONG'}>仅显示对错</Radio>
<Radio value={'CAN_NOT_CHECK'}>都不显示</Radio>
</Radio.Group>
</Form.Item>
</Form>
</div>
</div>
{
showModal && <SelectPaperModal onSelect={(info: any) => {
setPaperInfo(info)
}} paperInfo={paperInfo} close={() => { setShowModal(false) }}></SelectPaperModal>
}
<div className="footer shrink-footer">
{showModal && (
<SelectPaperModal
onSelect={(info: any) => {
setPaperInfo(info);
}}
paperInfo={paperInfo}
close={() => {
setShowModal(false);
}}></SelectPaperModal>
)}
<div className='footer shrink-footer'>
<Button onClick={handleGoBack}>取消</Button>
<Button onClick={() => { setPreview(true) }}>预览</Button>
<Button type="primary" onClick={handleSave}>保存</Button>
<Button
onClick={() => {
setPreview(true);
}}>
预览
</Button>
<Button type='primary' onClick={handleSave}>
保存
</Button>
</div>
{
preview && <PreviewModal
{preview && (
<PreviewModal
info={{
paperId,
startTime: examStartTime,
......@@ -491,12 +517,14 @@ function AddExam(props: any) {
examDuration,
passScore,
examTotal,
totalScore: paperInfo.totalScore
totalScore: paperInfo.totalScore,
}}
onClose={() => { setPreview(false) }}></PreviewModal>
}
onClose={() => {
setPreview(false);
}}></PreviewModal>
)}
</div>
);
}
export default withRouter(AddExam);
......@@ -81,7 +81,7 @@ const mainRoutes = [
{
path: '/video-course',
component: VideoCoursePage,
name: '视频课',
name: '线上课',
},
{
path: '/graphics-course',
......@@ -101,7 +101,7 @@ const mainRoutes = [
{
path: '/create-video-course',
component: AddVideoCoursePage,
name: '创建视频课',
name: '创建线上课',
},
{
path: '/knowledge-base',
......@@ -163,7 +163,7 @@ const mainRoutes = [
{
path: '/create-plan',
component: AddPlanPage,
name: '创建视频课',
name: '创建线上课',
},
{
path: '/store-info',
......
/*
* @Author: yuananting
* @Date: 2021-02-21 15:53:31
* @LastEditors: Please set LastEditors
* @LastEditTime: 2021-06-08 16:46:15
* @LastEditors: wufan
* @LastEditTime: 2021-07-05 10:29:23
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -28,8 +28,8 @@ export const menuList: any = [
link: '/live-course',
},
{
groupName: '视频课',
groupCode: 'CourseVideoClass',
groupName: "线上课",
groupCode: "CourseVideoClass",
link: '/video-course',
},
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment