Commit b681142d by wufan

feat:新建线上课结构调整

parent f985047c
...@@ -8,8 +8,7 @@ ...@@ -8,8 +8,7 @@
*/ */
import React from 'react' import React from 'react'
import { Button, Input, Radio, message, Modal, Cascader } from 'antd' import { Button, Input, Radio, message, Modal, Cascader, Tooltip, Form, Popconfirm } from 'antd'
import { DISK_MAP, FileTypeIcon, FileVerifyMap } from '@/common/constants/academic/lessonEnum' import { DISK_MAP, FileTypeIcon, FileVerifyMap } from '@/common/constants/academic/lessonEnum'
import { ImgCutModalNew } from '@/components' import { ImgCutModalNew } from '@/components'
import ShowTips from '@/components/ShowTips' import ShowTips from '@/components/ShowTips'
...@@ -27,9 +26,9 @@ import _ from 'underscore' ...@@ -27,9 +26,9 @@ import _ from 'underscore'
import Upload from '@/core/upload' import Upload from '@/core/upload'
import { randomString } from '@/domains/basic-domain/utils' import { randomString } from '@/domains/basic-domain/utils'
import $ from 'jquery' import $ from 'jquery'
// import PhotoClip from 'photoclip';
import './AddVideoCourse.less' import './AddVideoCourse.less'
const { TextArea } = Input;
const EDIT_BOX_KEY = Math.random() const EDIT_BOX_KEY = Math.random()
const fieldNames = { label: 'categoryName', value: 'id', children: 'sonCategoryList' } const fieldNames = { label: 'categoryName', value: 'id', children: 'sonCategoryList' }
...@@ -58,7 +57,7 @@ class AddVideoCourse extends React.Component { ...@@ -58,7 +57,7 @@ class AddVideoCourse extends React.Component {
pageType, // 页面类型: add->新建 edit->编辑 pageType, // 页面类型: add->新建 edit->编辑
imageFile: null, // 需要被截取的图片 imageFile: null, // 需要被截取的图片
courseName: null, // 线上课名称 courseName: null, // 线上课名称
scheduleVideoId: null, // 线上课链接 scheduleVideoIds: null, // 线上课链接
coverId: null, // 视频封面的recourceId coverId: null, // 视频封面的recourceId
coverUrl: null, // 线上课封面 coverUrl: null, // 线上课封面
studentList: [], // 上课学员列表 studentList: [], // 上课学员列表
...@@ -83,7 +82,9 @@ class AddVideoCourse extends React.Component { ...@@ -83,7 +82,9 @@ class AddVideoCourse extends React.Component {
whetherVisitorsJoin: 'NO', // 是否允许游客加入 whetherVisitorsJoin: 'NO', // 是否允许游客加入
showSelectCoverModal: false, showSelectCoverModal: false,
cutImageBlob: null, cutImageBlob: null,
introduce: '' introduce: '',
courseChapterList:[
] // 课节列表
} }
} }
...@@ -124,14 +125,14 @@ class AddVideoCourse extends React.Component { ...@@ -124,14 +125,14 @@ class AddVideoCourse extends React.Component {
courseId courseId
}).then((res) => { }).then((res) => {
const { result = {} } = res || {} const { result = {} } = res || {}
const { courseName, shelfState, whetherVisitorsJoin, courseMediaVOS, categoryOneName, categoryTwoName, categoryId } = result const { courseName, shelfState, whetherVisitorsJoin, courseMediaVOS, categoryOneName, categoryTwoName, categoryId, courseChapterList =[] } = result
let coverId let coverId
let coverUrl let coverUrl
let videoType let videoType
let videoDuration let videoDuration
let videoName let videoName
let scheduleMedia = [] let scheduleMedia = []
let scheduleVideoId let scheduleVideoIds
let scheduleVideoUrl let scheduleVideoUrl
let hasIntro let hasIntro
...@@ -144,7 +145,7 @@ class AddVideoCourse extends React.Component { ...@@ -144,7 +145,7 @@ class AddVideoCourse extends React.Component {
case 'SCHEDULE': case 'SCHEDULE':
videoDuration = item.videoDuration videoDuration = item.videoDuration
videoName = item.mediaName videoName = item.mediaName
scheduleVideoId = item.mediaContent scheduleVideoIds = item.mediaContent
scheduleVideoUrl = item.mediaUrl scheduleVideoUrl = item.mediaUrl
videoType = item.mediaType videoType = item.mediaType
break break
...@@ -173,12 +174,13 @@ class AddVideoCourse extends React.Component { ...@@ -173,12 +174,13 @@ class AddVideoCourse extends React.Component {
videoDuration, videoDuration,
scheduleMedia, scheduleMedia,
courseName, courseName,
scheduleVideoId, scheduleVideoIds,
scheduleVideoUrl, scheduleVideoUrl,
shelfState, shelfState,
whetherVisitorsJoin, whetherVisitorsJoin,
categoryName, categoryName,
categoryId categoryId,
courseChapterList
}) })
}) })
} }
...@@ -196,11 +198,11 @@ class AddVideoCourse extends React.Component { ...@@ -196,11 +198,11 @@ class AddVideoCourse extends React.Component {
} }
handleGoBack = () => { handleGoBack = () => {
const { coverId, videoName, videoDuration, courseName, scheduleMedia, scheduleVideoId, categoryId, shelfState, whetherVisitorsJoin } = this.state const { coverId, videoName, videoDuration, courseName, scheduleMedia, scheduleVideoIds, categoryId, shelfState, whetherVisitorsJoin } = this.state
if ( if (
videoName || videoName ||
videoDuration || videoDuration ||
scheduleVideoId || scheduleVideoIds ||
!_.isEqual(scheduleMedia, defaultScheduleMedia) || !_.isEqual(scheduleMedia, defaultScheduleMedia) ||
categoryId || categoryId ||
courseName || courseName ||
...@@ -269,7 +271,7 @@ class AddVideoCourse extends React.Component { ...@@ -269,7 +271,7 @@ class AddVideoCourse extends React.Component {
// 显示预览弹窗 // 显示预览弹窗
handleShowPreviewModal = () => { handleShowPreviewModal = () => {
const { coverUrl, scheduleVideoUrl, courseName, scheduleMedia, videoDuration, introduce } = this.state const { coverUrl, scheduleVideoUrl, courseName, scheduleMedia, videoDuration, introduce, courseChapterList } = this.state
const courseBasinInfo = { const courseBasinInfo = {
coverUrl, coverUrl,
...@@ -292,6 +294,7 @@ class AddVideoCourse extends React.Component { ...@@ -292,6 +294,7 @@ class AddVideoCourse extends React.Component {
previewCourseModal: null previewCourseModal: null
}) })
}} }}
courseChapterList={courseChapterList}
/> />
) )
...@@ -303,20 +306,43 @@ class AddVideoCourse extends React.Component { ...@@ -303,20 +306,43 @@ class AddVideoCourse extends React.Component {
this.setState({ this.setState({
showSelectFileModal: false showSelectFileModal: false
}) })
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
let { courseChapterList } = this.state;
let _courseChapterList = [...courseChapterList];
_courseChapterList.push({
mediaContent: resourceId,
contentType: 'SCHEDULE',
mediaType: "VIDEO",
mediaName: folderName.replace('.mp4',''),
mediaNameAlias: folderName.replace('.mp4',''),
videoDuration: folderSize,
id: resourceId,
mediaUrl: ossUrl
})
const videoDom = document.createElement('video') const videoDom = document.createElement('video')
videoDom.src = ossUrl this.setState({
videoDom.onloadedmetadata = () => { size: folderSize,
this.setState({ videoName: folderName,
size: folderSize, videoType: folderFormat,
videoName: folderName, scheduleVideoUrl: ossUrl,
videoType: folderFormat, scheduleVideoIds: resourceId,
scheduleVideoUrl: ossUrl, videoDuration: videoDom.duration,
scheduleVideoId: resourceId, courseChapterList: _courseChapterList
videoDuration: videoDom.duration })
}) // videoDom.src = ossUrl
} // videoDom.onloadedmetadata = () => {
// this.setState({
// size: folderSize,
// videoName: folderName,
// videoType: folderFormat,
// scheduleVideoUrl: ossUrl,
// scheduleVideoIds: resourceId,
// videoDuration: videoDom.duration
// })
// }
} }
// 保存 // 保存
...@@ -335,18 +361,19 @@ class AddVideoCourse extends React.Component { ...@@ -335,18 +361,19 @@ class AddVideoCourse extends React.Component {
studentList, studentList,
courseName, courseName,
scheduleMedia, scheduleMedia,
scheduleVideoId, scheduleVideoIds,
scheduleVideoUrl, scheduleVideoUrl,
categoryId, categoryId,
shelfState, shelfState,
whetherVisitorsJoin, whetherVisitorsJoin,
introduce introduce,
courseChapterList
} = this.state } = this.state
const commonParams = { const commonParams = {
videoName, videoName,
videoDuration, videoDuration,
courseMediaId: scheduleVideoId, courseMediaId: scheduleVideoIds,
scheduleMedia: scheduleMedia.filter((item) => !!item.mediaContent), scheduleMedia: scheduleMedia.filter((item) => !!item.mediaContent),
categoryId, categoryId,
courseName, courseName,
...@@ -355,10 +382,11 @@ class AddVideoCourse extends React.Component { ...@@ -355,10 +382,11 @@ class AddVideoCourse extends React.Component {
storeId: User.getStoreId(), storeId: User.getStoreId(),
shelfState, shelfState,
whetherVisitorsJoin, whetherVisitorsJoin,
courseType: 'VOICE' courseType: 'VOICE',
courseChapterList
} }
// 校验必填字段:课程名称, 课程视频 // 校验必填字段:课程名称, 课程视频
this.handleValidate(courseName, scheduleVideoId, categoryId, scheduleMedia).then((res) => { this.handleValidate(courseName, scheduleVideoIds, categoryId, scheduleMedia).then((res) => {
if (!res) return if (!res) return
Upload.uploadTextToOSS(introduce, `${randomString()}.txt`, (introduceId) => { Upload.uploadTextToOSS(introduce, `${randomString()}.txt`, (introduceId) => {
this.submitRemote({ id, pageType, commonParams: { ...commonParams, introduceId } }) this.submitRemote({ id, pageType, commonParams: { ...commonParams, introduceId } })
...@@ -390,14 +418,14 @@ class AddVideoCourse extends React.Component { ...@@ -390,14 +418,14 @@ class AddVideoCourse extends React.Component {
} }
} }
handleValidate = (courseName, scheduleVideoId, categoryId, scheduleMedia) => { handleValidate = (courseName, scheduleVideoIds, categoryId, scheduleMedia) => {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!courseName) { if (!courseName) {
message.warning('请输入课程名称') message.warning('请输入课程名称')
resolve(false) resolve(false)
return false return false
} }
if (!scheduleVideoId) { if (!scheduleVideoIds) {
message.warning('请上传视频') message.warning('请上传视频')
resolve(false) resolve(false)
return false return false
...@@ -525,11 +553,74 @@ class AddVideoCourse extends React.Component { ...@@ -525,11 +553,74 @@ class AddVideoCourse extends React.Component {
} }
return new Blob([ab], { type: 'image/png' }) 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>
}
render() { render() {
const { const {
pageType, pageType,
courseName, courseName,
scheduleVideoId, scheduleVideoIds,
coverId, coverId,
coverUrl, coverUrl,
scheduleVideoUrl, scheduleVideoUrl,
...@@ -552,13 +643,13 @@ class AddVideoCourse extends React.Component { ...@@ -552,13 +643,13 @@ class AddVideoCourse extends React.Component {
cutImageBlob, cutImageBlob,
introduce, introduce,
loadintroduce, loadintroduce,
id id,
courseChapterList
} = this.state } = this.state
// 已选择的上课学员数量
const hasSelectedStu = studentList.length
const courseWareIcon = FileVerifyMap[videoType] ? FileTypeIcon[FileVerifyMap[videoType].type] : FileTypeIcon[videoType] 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 ( return (
<div className='page add-video-course-page'> <div className='page add-video-course-page'>
...@@ -586,51 +677,92 @@ class AddVideoCourse extends React.Component { ...@@ -586,51 +677,92 @@ class AddVideoCourse extends React.Component {
<div className='upload-video mt16'> <div className='upload-video mt16'>
<div className='content flex'> <div className='content flex'>
<span className='label required'>视频上传:</span> <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>
<div className='sub-content'> <div className='sub-content'>
<Button <div className="btn-wrap">
onClick={() => { <Button
this.setState({ onClick={() => {
showSelectFileModal: true if(courseChapterList.length >= 20) {
}) message.warning(`最多只能上传20个文件`);
}}>{`${pageType === 'add' && !scheduleVideoId ? '选择' : '更换'}视频`}</Button> return;
}
<span className='tips'>视频数量限制1个,大小不超过2G</span> this.setState({
showSelectFileModal: true
})
}}>{`${pageType === 'add' && !scheduleVideoIds ? '选择' : '更换'}视频`}</Button>
<div className='course-ware--empty'>从资料云盘中选择视频</div>
</div>
<div className='tips'>视频数量限制20个,每个视频文件大小不超过2G</div>
</div> </div>
<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>{`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}>
<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>
</div>
</If>
</div> </div>
<div className='cover-url flex mt16'> <div className='cover-url flex mt16'>
<div className='label'>视频封面:</div> <div className='label'>课程封面:</div>
<div className='cover-url__wrap'> <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>
<div className='opt-btns'> <div className='opt-btns'>
<Button <div>
onClick={() => { <Button
this.setState({ showSelectCoverModal: true }) onClick={() => {
}}>{`${pageType === 'add' && !scheduleVideoId && !coverUrl ? '上传' : '修改'}封面`}</Button> this.setState({ showSelectCoverModal: true })
}}>{`${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 className='tips'>建议尺寸1280*720px或16:9。封面图最大5M,支持jpg、jpeg和png。</div>
</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> </div>
<div className='course-catalog required'> <div className='course-catalog required'>
...@@ -695,6 +827,7 @@ class AddVideoCourse extends React.Component { ...@@ -695,6 +827,7 @@ class AddVideoCourse extends React.Component {
{/* 选择备课文件弹窗 */} {/* 选择备课文件弹窗 */}
{showSelectFileModal && ( {showSelectFileModal && (
<SelectPrepareFileModal <SelectPrepareFileModal
multiple={true}
operateType='select' operateType='select'
selectTypeList={['MP4']} selectTypeList={['MP4']}
accept='video/mp4' accept='video/mp4'
......
...@@ -49,8 +49,8 @@ ...@@ -49,8 +49,8 @@
} }
} }
.course-catalog{ .course-catalog{
margin-bottom:16px; margin-bottom:24px;
margin-top:16px; margin-top:24px;
} }
.course-ware { .course-ware {
display: flex; display: flex;
...@@ -75,7 +75,19 @@ ...@@ -75,7 +75,19 @@
.img-content { .img-content {
width: 298px; width: 298px;
height: 172px; 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 { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -85,21 +97,26 @@ ...@@ -85,21 +97,26 @@
.empty-img { .empty-img {
width: 298px; width: 298px;
height: 172px; height: 172px;
border: 1px dashed #EBEBEB; border: 1px dashed #ebebeb;
border-radius: 4px; border-radius: 4px;
padding: 12px; padding: 12px;
color: #999; color: #999;
padding: 52px 24px; padding: 52px 24px;
text-align: center; text-align: center;
} }
.opt-btns { .opt-btns {
margin-top: 8px; margin-bottom: 8px;
display: flex; .span {
align-items: center; color: #ccc;
margin-left: 8px;
}
.defalut-span {
cursor: pointer;
color: #2966ff;
}
.tips { .tips {
margin-left: 12px; margin-top: 8px;
color: #999; color: #999;
} }
} }
...@@ -115,14 +132,86 @@ ...@@ -115,14 +132,86 @@
color: #333; color: #333;
} }
} }
.upload-video {
display: flex;
margin-bottom: 24px;
.sub-content {
margin-top: 4px;
.btn-wrap {
display: flex;
.course-ware--empty {
margin-left: 12px;
line-height: 28px;
}
}
.tips {
margin-top: 8px;
color: #999;
}
}
}
.sub-content { .course-chapter-list-wrap {
margin-left: 85px; position: relative;
margin-top: 4px; .course-chapter-total {
height: 40px;
.tips { font-size: 14px;
margin-left: 4px; font-family: PingFangSC-Medium, PingFang SC;
color: #999; 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: 552px;
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 {
cursor: pointer;
}
}
}
} }
} }
} }
......
...@@ -121,8 +121,14 @@ class AddVideoIntro extends React.Component { ...@@ -121,8 +121,14 @@ class AddVideoIntro extends React.Component {
</div> </div>
<div> <div>
<div className="desc"> <div className="desc">
<div>开启:允许未绑定手机号的学员观看</div> <Choose>
<div>关闭:仅限绑定了手机号的学员可以进入观看视频</div> <When condition={whetherVisitorsJoin==="YES"}>
<div>已开启,学员需绑定手机号才可观看</div>
</When>
<Otherwise>
<div>已关闭,学员无需绑定手机号即可观看</div>
</Otherwise>
</Choose>
</div> </div>
</div> </div>
</div> </div>
...@@ -136,8 +142,14 @@ class AddVideoIntro extends React.Component { ...@@ -136,8 +142,14 @@ class AddVideoIntro extends React.Component {
</Col> </Col>
<Col span={21}> <Col span={21}>
<div className="desc"> <div className="desc">
<div>开启:此视频将在学员学院的视频列表中出现</div> <Choose>
<div>关闭:此视频将在学员学院的视频列表中隐藏</div> <When condition={shelfState==="YES"}>
<div>已开启,课程将在该学院的学员课程列表中显示</div>
</When>
<Otherwise>
<div>已关闭,课程将在该学院的学员课程列表中隐藏</div>
</Otherwise>
</Choose>
</div> </div>
</Col> </Col>
</Row> </Row>
......
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