Commit fdf7e07d by zhangleyuan

feat:增加视频课模块

parent 93970161
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:07:47
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-25 15:26:06
* @Description: 视频课新增/编辑页
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Button, Input, Radio, message, Modal } from 'antd';
import { DISK_MAP, FileTypeIcon, FileVerifyMap } from '@/common/constants/academic/lessonEnum';
import { ImgCutModalNew } from '@/components';
import ShowTips from "@/components/ShowTips";
import Breadcrumbs from "@/components/Breadcrumbs";
import AddLiveIntro from '../components/AddLiveIntro';
import SelectStudent from '../modal/select-student';
import SelectPrepareFileModal from '../../prepare-lesson/modal/SelectPrepareFileModal';
import PreviewCourseModal from '../modal/PreviewCourseModal';
import './AddVideoCourse.less';
const EDIT_BOX_KEY = Math.random(16);
class AddVideoCourse extends React.Component {
constructor(props) {
super(props);
const id = getParameterByName("id");
const pageType = getParameterByName("type");
this.state = {
id, // 视频课ID,编辑的时候从URL上带过来
pageType, // 页面类型: add->新建 edit->编辑
imageFile: null, // 需要被截取的图片
scheduleName: null, // 视频课名称
scheduleVideoId: null, // 视频课链接
coverId: null, // 视频封面的recourceId
coverUrl: null, // 视频课封面
joinType: null, // 观看模式, 默认为指定某些学员可以观看
studentList: [], // 上课学员列表
scheduleMedia: [{ // 视频课媒体资源
mediaType: 'TEXT',
mediaContent: '',
key: EDIT_BOX_KEY
}],
diskList: [], // 机构可见磁盘目录
selectedFileList: [], // 已经从资料云盘中勾选的文件
showCutModal: false, // 是否显示截图弹窗
showSelectFileModal: false,
studentModal: false,
}
}
componentWillMount() {
// this.handleFetchDiskList();
const { id, pageType } = this.state;
if (pageType === 'edit') {
this.handleFetchScheudleDetail(id);
this.handleFetchStudentList(id);
}
}
// 获取视频课详情
handleFetchScheudleDetail = (scheduleId) => {
window.axios.Apollo('public/apollo/getLessonScheduleDetail', {
scheduleId
}).then((res) => {
const { result = {} } = res || {};
const {
coverId,
coverUrl,
videoType,
videoDuration,
videoName,
studentIds,
scheduleMedia,
scheduleName,
scheduleVideoId,
joinType,
scheduleVideoUrl,
} = result;
this.setState({
coverId,
coverUrl,
videoType,
videoName,
videoDuration,
studentIds,
scheduleMedia,
scheduleName,
scheduleVideoId,
joinType,
scheduleVideoUrl,
});
})
}
// 获取视频课学员
handleFetchStudentList = (scheduleId) => {
window.axios.Apollo('public/apollo/getLessonStuIdListByScheduleId', {
scheduleId
}).then((res) => {
const studentIds = res.result || [];
const studentList = [];
_.each(studentIds, (item) => {
studentList.push({ studentId: item });
});
this.setState({ studentList })
});
}
// 获取机构可见的磁盘
// handleFetchDiskList = () => {
// axios.Apollo('public/apollo/getUserDisk', {}).then((res) => {
// const { result = [] } = res;
// const diskList = result.map((item) => {
// return {
// ...item,
// folderName: DISK_MAP[item.disk]
// }
// });
// this.setState({ diskList });
// });
// }
handleGoBack = () => {
Modal.confirm({
title: '确认要返回吗?',
content: '返回后,本次编辑的内容将不被保存。',
okText: '确认返回',
cancelText: '留在本页',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
onOk: () => {
RCHistory.goBack();
}
});
}
// 修改表单
handleChangeForm = (field, value, coverUrl) => {
this.setState({
[field]: value,
coverUrl: coverUrl ? coverUrl : this.state.coverUrl
});
}
// 显示选择学员弹窗
handleShowSelectStuModal = () => {
this.setState({ studentModal : true });
const { studentList, selectedStuList } = this.state;
// const _studentList = _.map(studentList, (item) => {
// return item.studentId
// })
const studentModal = (
<SelectStudent
showTabs={true}
type="videoCourse"
onSelect={this.handleSelectStudent}
after={true} //表明是不是上课后的状态
studentList={studentList}
close={() => {
this.setState({
studentModal: null,
});
}}
/>
)
this.setState({ studentModal });
}
handleSelectStudent = (studentIds) => {
let studentList = [];
_.each(studentIds, (item) => {
studentList.push({ studentId: item });
});
// this.setState({ studentModal: null });
this.setState({ studentList });
this.setState({ studentModal : false });
}
// 显示预览弹窗
handleShowPreviewModal = () => {
const {
coverUrl,
scheduleVideoUrl,
scheduleName,
scheduleMedia
} = this.state;
const courseBasinInfo = {
coverUrl,
scheduleVideoUrl,
courseName: scheduleName,
}
const courseIntroInfo = {
liveCourseMediaRequests: scheduleMedia
}
const previewCourseModal = (
<PreviewCourseModal
type="videoCourse"
courseBasinInfo={courseBasinInfo}
courseIntroInfo={courseIntroInfo}
close={() => {
this.setState({
previewCourseModal: null
})
}}
/>
);
this.setState({ previewCourseModal });
}
// 选择视频
handleSelectVideo = (file) => {
this.setState({
showSelectFileModal: false
})
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
const videoDom = document.createElement('video');
videoDom.src = ossUrl;
videoDom.onloadedmetadata = () => {
this.setState({
size: folderSize,
videoName: folderName,
videoType: folderFormat,
scheduleVideoUrl: ossUrl,
scheduleVideoId : resourceId,
videoDuration: videoDom.duration
});
}
}
// 上传封面图
handleShowImgCutModal = (event) => {
const imageFile = event.target.files[0];
if (!imageFile) return;
this.setState({
imageFile,
showCutModal: true,
});
}
// 保存
handleSubmit = () => {
const { instId, adminId } = window.currentUserInstInfo;
const {
id,
size,
coverId,
coverUrl,
pageType,
joinType,
videoName,
videoDuration,
studentList,
scheduleName,
scheduleMedia,
scheduleVideoId,
scheduleVideoUrl,
} = this.state;
const commonParams = {
size,
instId,
coverId,
videoName,
videoDuration,
scheduleName,
scheduleVideoId,
joinType,
teacherId: adminId,
studentIds: _.pluck(studentList, "studentId"),
scheduleMedia: scheduleMedia.filter(item => !!item.mediaContent),
};
// 校验必填字段:课程名称, 课程视频
this.handleValidate(scheduleName, scheduleVideoId, joinType, scheduleMedia).then((res) => {
if (!res) return;
if (pageType === 'add') {
window.axios.Apollo('public/apollo/createLessonSchedule', commonParams).then((res) => {
if (!res) return;
message.success("新建成功");
// localStorage.setItem('videoCourseItem', JSON.stringify({
// ...commonParams,
// coverUrl,
// scheduleVideoUrl,
// id: res.result
// }));
window.RCHistory.goBack();
})
} else {
const editParams = {
id,
...commonParams,
}
window.axios.Apollo('public/apollo/editLessonSchedule', editParams).then((res) => {
if (!res) return;
message.success("保存成功");
window.RCHistory.goBack();
});
}
});
}
handleValidate = (scheduleName, scheduleVideoId, joinType, scheduleMedia) => {
return new Promise((resolve) => {
if (!scheduleName) {
message.warning('请输入课程名称');
resolve(false);
} else if (!scheduleVideoId) {
message.warning('请上传视频');
resolve(false);
} else if (!joinType) {
message.warning('请选择观看设置');
resolve(false);
} else {
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);
}
}
}
resolve(true);
});
}
render() {
const {
pageType, scheduleName, scheduleVideoId, coverId,
coverUrl, scheduleVideoUrl, studentList, scheduleMedia,
showCutModal, showSelectFileModal, diskList,
imageFile, joinType, videoName, videoType ,studentModal
} = this.state;
// 已选择的上课学员数量
const hasSelectedStu = studentList.length;
const courseWareIcon = FileVerifyMap[videoType] ? FileTypeIcon[FileVerifyMap[videoType].type] : FileTypeIcon[videoType];
return (
<div className="page add-video-course-page">
<Breadcrumbs
navList={pageType === "add" ? "新建视频课" : "编辑视频课"}
goBack={this.handleGoBack}
/>
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦助教保有依据国家规定及平台规则进行处理的权利" />
</div>
<div className="form">
<div className="course-name required">
<span className="label">课程名称:</span>
<Input
value={scheduleName}
placeholder="请输入视频课的名称(40字以内)"
maxLength={40}
style={{ width: 240 }}
onChange={(e) => { this.handleChangeForm('scheduleName', e.target.value)}}
/>
</div>
<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">
<Button
type="primary"
onClick={() => {
this.setState({
showSelectFileModal: true
})
}}
>{`${(pageType === 'add' && !scheduleVideoId) ? '选择' : '更换'}视频`}</Button>
<span className="tips">视频数量限制1个,大小不超过500M</span>
</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>
<div className="opt-btns">
<input
type="file"
accept="image/png, image/jpeg, image/jpg"
ref="picInputFile"
style={{display: 'none'}}
onChange={(event) => { this.handleShowImgCutModal(event) }}
/>
<Button onClick={() => {
this.setState({
currentInputFile: this.refs.picInputFile
});
this.refs.picInputFile.click()
}}>{`${(pageType === 'add' && (!scheduleVideoId && !coverUrl)) ? '上传' : '修改'}封面`}</Button>
<div className="tips">建议尺寸1280*720px或16:9。封面图最大5M,支持jpg、jpeg和png。</div>
</div>
</div>
</div>
<div className="watch-setting flex mt16">
<div className="label required">分享设置:</div>
<div className="content">
<Radio.Group
onChange={(e) => this.handleChangeForm('joinType', e.target.value)}
value={joinType}
>
<Radio value={'ALL'}>
<div className="radio-item">
<div className="text mr16">允许任何学员通过视频课链接加入学习</div>
<div className="sub-text">加入后,在读学员可通过家长端和视频课链接观看,非在读学员仅可通过视频课链接观看</div>
</div>
</Radio>
<Radio value={'NORMAL'}>
<div className="radio-item">
<div className="text mr16">仅在读学员可通过视频课链接加入学习</div>
<div className="sub-text">加入后,在读学员可通过家长端和视频课链接观看</div>
</div>
</Radio>
<Radio value={'ASSIGN'}>
<div className="radio-item mb0">
<div className="text mr16">仅指定学员可通过视频课链接加入学习</div>
<div className="sub-text">加入后,在读学员可通过家长端和视频课链接观看,非在读学员仅可通过视频课链接观看</div>
</div>
</Radio>
</Radio.Group>
{
// 当观看模式为指定学员时,显示选择学员的入口
joinType === 'ASSIGN' &&
<div className="select-student flex">
<Button onClick={this.handleShowSelectStuModal}>选择学员</Button>
<div className="has-selected">
已选择 <span style={{ color: "#FF8534" }}>{hasSelectedStu}</span> 名学员
</div>
</div>
}
</div>
</div>
<div className="intro-info mt16">
<AddLiveIntro
data={{
liveCourseMediaRequests: scheduleMedia,
label: '视频课简介'
}}
onChange={this.handleChangeForm}
/>
</div>
</div>
</div>
<div className="footer">
<Button onClick={this.handleGoBack}>取消</Button>
<Button onClick={this.handleShowPreviewModal}>预览</Button>
<Button type="primary" onClick={_.debounce(() => this.handleSubmit(), 3000, true)}>保存</Button>
</div>
{/* 选择备课文件弹窗 */}
<SelectPrepareFileModal
operateType="select"
selectTypeList={['MP4']}
accept="video/mp4"
confirm={{
title: '文件过大,无法上传',
content: '为保障学员的观看体验,上传的视频大小不能超过500M',
}}
tooltip={'格式支持mp4,大小不超过500M'}
isOpen={showSelectFileModal}
diskList={diskList}
addVideo={true}
onClose={() => {
this.setState({ showSelectFileModal: false })
}}
onSelect={this.handleSelectVideo}
/>
<ImgCutModalNew
title="裁剪"
width={550}
cutWidth={500}
cutHeight={282}
cutContentWidth={500}
cutContentHeight={300}
visible={showCutModal}
imageFile={imageFile}
bizCode='LIVE_COURSE_MEDIA'
onOk={(urlStr, resourceId) => {
this.setState({ showCutModal: false });
this.handleChangeForm('coverId', resourceId, urlStr)
this.state.currentInputFile.value = '';
}}
onClose={() => this.setState({ showCutModal: false })}
reUpload={() => { this.state.currentInputFile.click() }}
/>
{/* {
<SelectStudent
addVideo={'addVideo'}
studentModal={studentModal}
showTabs={true}
type="videoCourse"
onSelect={this.handleSelectStudent}
after={true} //表明是不是上课后的状态
savedList={studentList}
close={() => {
this.setState({
// studentModal: null,
studentModal: false,
});
}}
/>
} */}
{ this.state.studentModal }
{ this.state.previewCourseModal }
</div>
)
}
}
export default AddVideoCourse;
.add-video-course-page {
.ant-radio-group {
display: flex;
flex-direction: column;
.radio-item {
margin-bottom: 12px;
.text {
color: #333;
}
.sub-text {
color: #999;
}
}
.ant-radio {
vertical-align: top;
padding-top: 2px;
}
}
.form {
margin-top: 16px;
padding: 0 16px;
.required {
position: relative;
&::before {
position: absolute;
content: '*';
color: red;
left: -10px;
top: 6px;
}
&.label::before {
top: 0;
}
}
.course-ware {
display: flex;
align-items: center;
margin-bottom: 4px;
&__img {
width: 24px;
margin-right: 4px;
}
&__name {
color: #333;
}
}
.flex {
display: flex;
}
.cover-url__wrap {
.img-content {
width: 298px;
height: 172px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.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;
margin-left: 24px;
margin-top: 8px;
.has-selected {
margin-left: 12px;
color: #333;
}
}
.sub-content {
margin-left: 70px;
margin-top: 4px;
.tips {
margin-left: 16px;
color: #999;
}
}
}
.footer {
position: fixed;
bottom: 0;
height: 58px;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 252px;
background: #fff;
border-top: 1px solid #E8E8E8;
z-index: 999;
.ant-btn {
margin-left: 10px;
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:11:57
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-25 14:45:09
* @Description: 视频课-搜索模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Row, Input, Select } from 'antd';
import RangePicker from "@/modules/common/DateRangePicker";
// import TeacherSelectV5 from '@/modules/classManage_V5/classDetail/TeacherSelectV5';
import './VideoCourseFilter.less';
const { Search } = Input;
const { Option } = Select;
const DEFAULT_QUERY = {
scheduleName: null, // 课程名称
teacherId: null, // 创建人
beginTime: null, // 开始日期
endTime: null, // 结束日期
}
class VideoCourseFilter extends React.Component {
constructor(props) {
super(props);
this.state = {
query: { ...DEFAULT_QUERY }, // 使用扩展运算符,避免浅拷贝
}
}
// 改变搜索条件
handleChangeQuery = (field, value) => {
this.setState({
query: {
...this.state.query,
[field]: value,
current: 1,
}
}, () => {
if (field === 'scheduleName') return;
this.props.onChange(this.state.query)
});
}
handleChangeDates = (dates) => {
const query = _.clone(this.state.query);
if (_.isEmpty(dates)) {
delete query.beginTime;
delete query.endTime;
} else {
query.beginTime = dates[0].valueOf();
query.endTime = dates[1].valueOf();
}
this.setState({
query,
current: 1,
}, () => {
this.props.onChange(this.state.query);
})
}
// 重置搜索条件
handleReset = () => {
this.setState({
query: DEFAULT_QUERY,
}, () => {
this.props.onChange(this.state.query);
})
}
render() {
const {
query: {
scheduleName,
operator,
beginTime,
endTime,
teacherId,
}
} = this.state;
return (
<div className="video-course-filter">
<Row type="flex" justify="space-between" align="top">
<div className="search-condition">
<div className="search-condition__item">
<span className="search-name">视频课:</span>
<Search
value={scheduleName}
placeholder="搜索视频课名称"
onChange={(e) => { this.handleChangeQuery('scheduleName', e.target.value)}}
onSearch={ () => { this.props.onChange(this.state.query) } }
style={{ width: "calc(100% - 70px)" }}
/>
</div>
<div className="search-condition__item">
{/* <TeacherSelectV5
ref="TeacherSelect"
showSearch={true}
allowClear={true}
style={{ width: "calc(100% - 70px)" }}
onSelect={(teacherId) => { this.handleChangeQuery('teacherId', teacherId)}}
placeholder='请选择创建人'
label='创建人'
defaultValue={teacherId}
/> */}
</div>
<div className="search-condition__item">
<span className="search-date">创建日期:</span>
<RangePicker
id="course_date_picker"
allowClear={false}
value={ beginTime ? [moment(beginTime), moment(endTime)] : null }
format={"YYYY-MM-DD"}
onChange={(dates) => { this.handleChangeDates(dates) }}
style={{ width: "calc(100% - 70px)" }}
/>
</div>
</div>
<span
className="icon iconfont"
onClick={this.handleReset}
>
&#xe6a3;
</span>
</Row>
</div>
)
}
}
export default VideoCourseFilter;
.video-course-filter {
position: relative;
.search-condition {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
&__item {
width: 30%;
margin-right: 3%;
}
}
.iconfont {
position: absolute;
right: 12px;
cursor: pointer;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:45
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-25 14:54:53
* @Description: 视频课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Table, Modal, message } from 'antd';
import { PageControl } from "@/components";
import { LIVE_SHARE_MAP } from '@/common/constants/academic/cloudClass';
import StudentListModal from '../modal/StudentListModal';
import ShareLiveModal from '@/modules/course-manage/modal/ShareLiveModal';
import './VideoCourseList.less';
const ENV = process.env.DEPLOY_ENV || 'dev';
class VideoCourseList extends React.Component {
constructor(props) {
super(props);
this.state = {
id: '', // 视频课ID
studentIds:[]
}
}
componentDidMount() {
const videoCourseItem = localStorage.getItem('videoCourseItem');
if (videoCourseItem) {
const _videoCourseItem = JSON.parse(videoCourseItem);
this.handleShowShareModal(_videoCourseItem, true);
}
}
// 请求表头
parseColumns = () => {
const columns = [
{
title: '视频课',
key: 'scheduleName',
dataIndex: 'scheduleName',
width: '25%',
render: (val, record) => {
const { coverUrl, scheduleVideoUrl } = record;
return (
<div className="record__item">
{/* 上传了封面的话就用上传的封面, 没有的话就取视频的第一帧 */}
<img className="course-cover" src={coverUrl || `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast`} />
<span className="course-name">{val}</span>
</div>
)
}
},
{
title: '学员人数',
key: "stuNum",
dataIndex: "stuNum",
render: (val, record) => {
return (
<span
className="operate-text"
onClick={() => this.handleShowStudentListModal(record)}
>
{val}
</span>
);
},
},
{
title: '创建人',
key: 'teacherName',
dataIndex: 'teacherName'
},
{
title: '创建时间',
key: 'createTime',
dataIndex: 'createTime',
sorter: true,
render: (val) => {
return formatDate('YYYY-MM-DD H:i', val)
}
},
{
title: '更新时间',
key: 'updateTime',
dataIndex: 'updateTime',
sorter: true,
render: (val) => {
return formatDate('YYYY-MM-DD H:i', val)
}
},
{
title: '操作',
key: 'operate',
dataIndex: 'operate',
render: (val, record) => {
return (
<div className="operate">
<div className="operate__item" onClick={() => this.handleShowShareModal(record)}>分享</div>
<span className="operate__item split"> | </span>
<div
className="operate__item"
onClick={() => {
RCHistory.push(`/cloudclass/video_course/create?type=edit&id=${record.id}`);
}}
>编辑</div>
<span className="operate__item split"> | </span>
<div
className="operate__item"
onClick={() => this.handleDeleteVideoCourse(record.id)}
>删除</div>
</div>
)
}
}
];
return columns;
}
// 删除视频课
handleDeleteVideoCourse = (scheduleId) => {
Modal.confirm({
title: '你确定要删除此视频课吗?',
content: '删除后,学员将不能进行观看。',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
onOk: () => {
window.axios.Apollo('public/apollo/removeLessonSchedule', {
scheduleId
}).then(() => {
message.success('删除成功');
this.props.onChange();
})
}
});
}
// 学员人数弹窗
handleShowStudentListModal = (record) => {
const { studentIds = [], id } = this.state;
if (id !== record.id) {
this.setState({
id : record.id
})
}
const studentListModal = (
<StudentListModal
type='videoCourseList'
close={(studentIds) =>
this.setState({
studentListModal: null,
studentIds
})
}
studentIds={studentIds}
data={record}
refresh={() => {
this.props.onChange();
}}
/>
);
this.setState({ studentListModal });
}
// 显示分享弹窗
handleShowShareModal = (record, needStr = false) => {
const appId = CONFIG.appId[ENV];
const shareUrl = CONFIG.shareUrl[ENV];
const { id, scheduleVideoUrl } = record;
const htmlUrl = `${LIVE_SHARE_MAP[ENV]}videoShare?id=${id}`;
const link = `${appId}&redirect_uri=${encodeURIComponent(htmlUrl)}%26appid%3D${appId}&response_type=code&scope=snsapi_base&state=state#wechat_redirect`;
const longUrl = `${shareUrl}${link}`;
const { coverUrl, scheduleName } = record;
const shareData = {
longUrl,
coverUrl,
scheduleVideoUrl,
courseName: scheduleName,
};
const shareLiveModal = (
<ShareLiveModal
needStr={needStr}
data={shareData}
type="videoClass"
close={() => {
this.setState({
shareLiveModal: null
});
localStorage.setItem('videoCourseItem', '');
}}
/>
);
this.setState({ shareLiveModal });
}
handleChangeTable = (pagination, filters, sorter) => {
const { columnKey, order } = sorter;
const { query } = this.props;
let { order: _order } =query;
// 按创建时间升序排序
if (columnKey === 'createTime' && order === 'ascend') { _order = 'CREATED_ASC'; }
// 按创建时间降序排序
if (columnKey === 'createTime' && order === 'descend') { _order = 'CREATED_DESC'; }
// 按更新时间升序排序
if (columnKey === 'updateTime' && order === 'ascend') { _order = 'UPDATED_ASC'; }
// 按更新时间降序排序
if (columnKey === 'updateTime' && order === 'descend') { _order = 'UPDATED_DESC'; }
const _query = {
...query,
order: _order
};
this.props.onChange(_query);
}
render() {
const { dataSource = [], totalCount, query } = this.props;
const { current, size } = query;
return (
<div className="video-course-list">
<Table
rowKey={record => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
onChange={this.handleChangeTable}
pagination={false}
/>
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={totalCount}
toPage={(page) => {
const _query = {...query, current: page + 1};
this.props.onChange(_query)
}}
/>
</div>
{ this.state.shareLiveModal }
{ this.state.studentListModal }
</div>
)
}
}
export default VideoCourseList;
.video-course-list {
margin-top: 12px;
.operate-text {
color: #FF8534;
cursor: pointer;
}
.operate {
display: flex;
&__item {
color: #FF8534;
cursor: pointer;
&.split {
margin: 0 8px;
color: #BFBFBF;
}
}
}
.record__item {
display: flex;
.course-cover {
min-width: 97px;
max-width: 97px;
height: 50px;
border-radius: 2px;
margin-right: 8px;
background-color: #666;
}
.course-name {
color: #666;
}
}
}
.video-course-more-menu {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border-radius: 4px;
div {
line-height: 30px;
padding: 0 15px;
cursor: pointer;
&:hover {
background: #f3f6fa;
}
}
}
\ No newline at end of file
.video-course-opt {
margin-top: 16px;
.link {
color: #FF8534;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:15
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-25 15:25:32
* @Description: 视频课-操作模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Button } from 'antd';
import './VideoCourseOpt.less';
export default function VideoCourseOpt() {
return (
<div className="video-course-opt">
<Button
type="primary"
onClick={() => {
RCHistory.push('/create-video-course?type=add');
}}
className="mr12"
>新建视频课</Button>
<a
href="https://mp.weixin.qq.com/s/vTfGS8SeRzarrUnF9fkYbA"
className="link"
target="_blank"
>什么是视频课?</a>
</div>
);
}
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:08:06
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-25 15:06:21
* @Description: 云课堂-视频课入口页面
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import VideoCourseFilter from './components/VideoCourseFilter';
import VideoCourseOpt from './components/VieoCourseOpt';
import VideoCourseList from './components/VideoCourseList';
class VideoCourse extends React.Component {
constructor(props) {
super(props);
const { instId } = window.currentUserInstInfo;
this.state = {
query: {
instId,
size: 10,
current: 1,
},
dataSource: [], // 视频课列表
totalCount: 0, // 视频课数据总条数
}
}
componentWillMount() {
// 获取视频课列表
this.handleFetchScheduleList();
}
// 获取视频课列表
handleFetchScheduleList = (_query = {}) => {
const query = {
...this.state.query,
..._query
};
// 更新请求参数
this.setState({ query });
// window.axios.Apollo('public/apollo/lessonScheduleListPage', query).then((res) => {
// const { result = {} } = res || {};
// const { records = [], total = 0 } = result;
// this.setState({
// dataSource: records,
// totalCount: Number(total)
// });
// });
}
render() {
const { dataSource, totalCount, query } = this.state;
return (
<div className="page video-course-page">
<div className="content-header">视频课</div>
<div className="box">
{/* 搜索模块 */}
<VideoCourseFilter
onChange={this.handleFetchScheduleList}
/>
{/* 操作模块 */}
<VideoCourseOpt />
{/* 视频课列表模块 */}
<VideoCourseList
query={query}
dataSource={dataSource}
totalCount={totalCount}
onChange={this.handleFetchScheduleList}
/>
</div>
</div>
)
}
}
export default VideoCourse;
/*
* @Author: 吴文洁
* @Date: 2020-08-14 17:02:59
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-25 14:35:38
* @Description: 视频课学员名单
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Modal, Table, Button, Input, Popconfirm, message } from 'antd';
import Bus from '@/core/bus';
import { PageControl } from "@/components";
// import SelectStudent from "../../modal/select-student";
import hasExportPermission from '../../utils/hasExportPermission';
import dealTimeDuration from '../../utils/dealTimeDuration';
import './StudentListModal.less'
const { Search } = Input;
const STATUS_ENUM = {
'NORMAL': '在读',
'POTENTIAL': '潜在',
'HISTORY': '历史',
'ABANDON': '废弃',
};
class StudentListModal extends React.Component {
constructor(props) {
super(props);
const { id } = props.data;
this.state = {
studentList: [], // 学员列表
query: {
size: 10,
current: 1,
queryText: null,
scheduleId: id,
},
total: 0, // 学员总数
}
}
componentWillMount() {
this.handleFetchStuList();
}
handleFetchStuList = (current = 1) => {
const { query } = this.state;
window.axios.Apollo('public/apollo/getLessonScheduleStuPage', query)
.then((res) => {
const { result = {} } = res || {};
const { records = [], total = 0 } = result;
this.setState({
total: Number(total),
studentList: records,
})
})
}
// 获取对应课次所有相关学生
// handleAllSelectStuId = () => {
// const { id } = this.props.data;
// return axios.Apollo("public/apollo/getLessonStuIdListByScheduleId", { scheduleId: id }).then(res => {
// const tempArray = [];
// res.data.map(current => {
// tempArray.push({studentId: current})
// })
// this.setState({
// _studentList :tempArray
// }, () => {
// const { query: { scheduleId }, _studentList, allSelectStuId = [] } = this.state;
// let { type = '' } = this.props;
// const studentModal = (
// <SelectStudent
// after={true}
// showTabs={true}
// // type="videoCourse"
// type={type}
// liveCourseId={scheduleId}
// allSelectStuId={allSelectStuId}
// studentList={_studentList}
// onSelect={this.handleSelectStudent}
// close={() => {
// this.setState({
// studentModal: null,
// });
// }}
// />
// )
// this.setState({ studentModal });
// })
// })
// }
parseColumns = () => {
const { NewVersion, currentUserInstInfo } = window;
const columns = [
{
title: "姓名",
dataIndex: "name",
key: "name"
},
{
title: "手机号",
dataIndex: "phone",
width: 150,
key: "phone",
render: (val, record) => {
if ((!NewVersion && !currentUserInstInfo.teacherId) ||
(NewVersion && Permission.hasEduStudentPhone())) {
return val;
} else {
return val.replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3");
}
}
},
{
title: '学员类型',
key: 'statusEnum',
dataIndex: 'statusEnum',
render: (val) => {
return STATUS_ENUM[val];
}
},
{
title: '首次观看时间',
key: 'firstWatch',
dataIndex: 'firstWatch',
width: '20%',
sorter: true,
render: (val) => {
return val ? formatDate('YYYY-DD-MM H:i:s', val) : '-';
}
},
{
title: '观看时长',
key: 'watchDuration',
dataIndex: 'watchDuration',
width: '15%',
sorter: true,
render: (val) => {
return val ? dealTimeDuration(val) : '-';
}
},
{
title: "操作",
dataIndex: "operate",
key: "operate",
align:'right',
render: (text, record) => {
return (
<Popconfirm
title={<span>你确定要移出这个学员吗?<br />移出的学员将无法观看视频</span>}
onConfirm={() => {
this.handleRemoveStudent(record);
}}
>
<span className="operate__item">移出</span>
</Popconfirm>
);
},
}
];
return columns;
}
// 移出学员
handleRemoveStudent = (record) => {
const { instId } = window.currentUserInstInfo;
const { id } = this.props.data;
const { studentId } = record;
const params = {
instId,
scheduleId: id,
studentIds: [studentId],
}
window.axios.Apollo('public/apollo/removeLessonScheduleStu', params)
.then((res) => {
message.success("移除学员成功");
this.handleFetchStuList();
this.props.refresh();
})
}
// 按照名称或者手机号搜索学员
handleChangeQueryText = (value) => {
const query = _.clone(this.state.query);
query.queryText = value;
query.pageNo = 0;
if (isNaN(value)) {
query.name = value
delete query.phone
} else {
query.phone = value
delete query.name
}
this.setState({ query }, () => this.handleFetchStuList());
}
// 排序
handleChagneTable = (pagination, filters, sorter) => {
const { columnKey, order } = sorter;
const { query } = this.state;
let { order: _order } = query;
if (columnKey === 'firstWatch' && order === 'ascend') { _order = 'FIRST_WATCH_ASC'; }
if (columnKey === 'firstWatch' && order === 'descend') { _order = 'FIRST_WATCH_DESC'; }
if (columnKey === 'watchDuration' && order === 'ascend') { _order = 'WATCH_DURATION_ASC'; }
if (columnKey === 'watchDuration' && order === 'descend') { _order = 'WATCH_DURATION_DESC'; }
const _query = {
...query,
order: _order
}
this.setState({ query: _query }, () => this.handleFetchStuList());
}
hanldSelect = () => {
const { query: { scheduleId }, studentList, allSelectStuId = [] } = this.state;
let { type = '' } = this.props;
type = !!type ? type : 'videoCourse';
if (type === 'videoCourseList') {
// this.handleAllSelectStuId();
return;
}
// const studentModal = (
// <SelectStudent
// after={true}
// showTabs={true}
// // type="videoCourse"
// type={type}
// liveCourseId={scheduleId}
// allSelectStuId={allSelectStuId}
// studentList={studentList}
// onSelect={this.handleSelectStudent}
// close={() => {
// this.setState({
// studentModal: null,
// });
// }}
// />
// )
this.setState({ studentModal });
};
handleSelectStudent = (studentList, consumeStudentList, savedSelectedRows) => {
const { id } = this.props.data;
const { instId } = window.currentUserInstInfo;
studentList = studentList.filter(item => !!item)
const param = {
instId,
scheduleId: id,
studentIds: studentList
};
this.setState({
savedSelectedRows
})
axios.Apollo("public/apollo/addLessonScheduleStu", param).then(res => {
if (res.success) {
message.success("学员变更成功");
this.setState({ studentModal: null });
this.handleFetchStuList();
this.props.refresh();
}
});
}
// 导5.0出
handleExportV5 = () => {
const { id } = this.props.data;
window.axios.Apollo('public/apollo/exportVideoScheduleSync', {
liveCourseId: id
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“任务中心”进行下载',
okText: '我知道了',
});
})
}
// 4.0导出
handleExport = () => {
const { id } = this.props.data;
window.axios.post('api-b/b/lesson/exportVideoScheduleSync', {
liveCourseId: id
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“导出中心”进行下载',
okText: '我知道了',
});
})
}
render() {
const isTeacher = !!window.currentUserInstInfo.teacherId;
const { studentList, total, query } = this.state;
const { current, size, queryText } = query;
return (
<Modal
title="查看学员名单"
visible={true}
width={680}
footer={null}
className="video-course-student-list-modal"
onCancel={this.props.close}
>
{/* 任意状态都可以添加学员 */}
<div className="video-course-student-list-modal__operate">
{
!isTeacher &&
<Button type="primary" onClick={this.hanldSelect}>
添加上课学员
</Button>
}
{
// 有导出权限的机构显示导出按钮
hasExportPermission('videoClass') &&
<Button onClick={() => {
if (!studentList.length) {
message.warning('暂无数据可导出');
return;
}
if (window.NewVersion) {
this.handleExportV5();
} else {
this.handleExport();
}
}} className="export-btn">导出</Button>
}
<Search
placeholder="搜索学员姓名/手机号"
style={{ width: 200 }}
onSearch={this.handleChangeQueryText}
className="search"
/>
</div>
<Table
size="small"
columns={this.parseColumns()}
dataSource={studentList}
pagination={false}
scroll={{ y: 400 }}
onChange={this.handleChagneTable}
/>
<PageControl
size="small"
current={current - 1}
pageSize={size}
total={total}
toPage={(page) => {
query.current = page + 1;
this.handleFetchStuList();
}}
/>
{this.state.studentModal}
</Modal>
)
}
}
export default StudentListModal;
.video-course-student-list-modal {
&__operate {
position: relative;
margin-bottom: 16px;
.export-btn {
margin-left: 8px;
}
.search {
position: absolute;
right: 0;
}
}
.operate__item {
color: #FF8534;
cursor: pointer;
}
}
\ No newline at end of file
/*
* @Author: wufan
* @Date: 2020-11-30 10:47:38
* @LastEditors: wufan
* @LastEditTime: 2020-11-30 17:35:24
* @Description: 员工管理页面
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { useEffect, useState } from "react";
import { withRouter } from "react-router-dom";
import _ from "underscore";
import PageControl from "@/components/PageControl";
import {
Table,
Modal,
message,
Row,
Col,
Input,
DatePicker,
Button,
} from "antd";
import { QuestionCircleOutlined } from "@ant-design/icons";
import StoreService from "@/domains/store-domain/storeService";
import "./StoreDecorationPage.less";
import moment from "moment";
const { confirm } = Modal;
const { Search } = Input;
const { RangePicker } = DatePicker;
declare var window: any;
interface RecordTypes {
storeUserId: string;
role: string;
}
function StoreDecorationPage() {
const [storeDecorationlist, setStoreDecorationlist] = useState([
{
name: "赵云",
phone: "18767118672",
role: "0",
storeUserId: "",
},
{
name: "吕布",
phone: "18767118672",
role: "0",
storeUserId: "",
},
]);
const [query, setQuery] = useState({
current: 0,
size: 10,
name: "",
phone: "",
identity: "ALL",
instId: "1837447" || window.currentUserInstInfo.instId,
registerStartDate: null,
registerEndDate: null,
});
const [total, setTotal] = useState(0);
const [model, setModel] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isManager, setIsManager] = useState(true);
const [isNormal, setIsNormal] = useState(true);
const [choosedItem, setChooseItem] = useState({});
useEffect(() => {
// getStoreDecorationList();
}, [query]);
function getStoreDecorationList() {
let _query = _.clone(query);
_query.current = query.current + 1;
StoreService.getStoreDecorationList(_query).then((res: any) => {
console.log(res.result.records);
setStoreDecorationlist(res.result.records);
setTotal(res.result.total);
});
}
function handleReplaceDecoration(record: RecordTypes) {}
function handleDeleteDecoration(record: RecordTypes) {}
function handleDeleteDecorationConfirm(record: RecordTypes) {
return confirm({
title: "你确定要删除这个banner吗?",
icon: <span className="icon iconfont default-confirm-icon">&#xe839; </span>,
okText: "删除",
cancelText: "取消",
onOk: () => {
handleDeleteDecoration(record);
},
});
}
function parseColumn() {
return [
{
title: "用户姓名",
dataIndex: "name",
render: (val: string) => {
return (
<div className="coupon-info">
<span className="title">{val}</span>
</div>
);
},
},
{
title: "手机号",
dataIndex: "phone",
key: "phone",
render: (val: string) => {
return <div>{val}</div>;
},
},
{
title: "注册时间",
dataIndex: "registerDate",
key: "registerDate",
render: (val: string) => {
return <div>{moment(val).format("YYYY-MM-DD HH:mm:ss")}</div>;
},
},
{
title: "操作",
dataIndex: "operation",
render: (val: string, record: RecordTypes) => {
return record.role === "" || record.role === "1" ? (
<div className="no-operate">-</div>
) : (
<div className="operation">
<span
className="edit"
onClick={() => handleReplaceDecoration(record)}
>
替换
</span>
<span className="divider-line">{" | "}</span>
<span
className="delete"
onClick={() => handleDeleteDecoration(record)}
>
删除
</span>
</div>
);
},
},
];
}
function handleQuery(name: string, value: any) {
const _query = _.clone(query);
// _query[name] = value;
setQuery(_query);
}
function handleToAddStoreDecoration() {}
return (
<div className="page user-manage-page">
<div className="page-content">
<div className="content-header">店铺装修</div>
<div className="box">
<div className="box-header">
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "15px 0 10px",
}}
>
<div style={{ flex: 1 }}>banner设置</div>
</div>
<Button
onClick={() => {
handleToAddStoreDecoration();
}}
type="primary"
className="add-show-btn"
>
添加Banner
</Button>
</div>
<div className="box-body">
<Table
size={"middle"}
pagination={false}
dataSource={storeDecorationlist}
columns={parseColumn()}
rowKey={(item: any) => item.id}
bordered
/>
</div>
<div className="box-footer">
<PageControl
current={query.current}
pageSize={query.size}
total={total}
toPage={(page) => {
const queryStates = _.clone(query);
queryStates.current = page;
setQuery(queryStates);
}}
/>
</div>
</div>
</div>
</div>
);
}
export default withRouter(StoreDecorationPage);
/*
* @Author: 吴文洁
* @Date: 2020-04-29 10:26:32
* @LastEditors: wufan
* @LastEditTime: 2020-12-16 16:39:03
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-25 15:20:14
* @Description: 内容线路由配置
*/
import EmployeesManagePage from '@/modules/store-manage/EmployeesManagePage';
......@@ -12,8 +12,9 @@ import StoreDecorationPage from '@/modules/store-manage/StoreDecorationPage';
import CourseCatalogPage from '@/modules/store-manage/CourseCatalogPage';
import LiveCoursePage from '@/modules/course-manage/LiveCoursePage';
import AddLivePage from '@/modules/course-manage/AddLive'
import VideoCoursePage from '@/modules/course-manage/video-course'
import AddVideoCoursePage from '@/modules/course-manage/video-course/AddVideoCourse'
import DataList from '@/modules/course-manage/DataList/DataList';
import ClassBook from '@/modules/resource-disk';
const mainRoutes = [
......@@ -45,7 +46,12 @@ const mainRoutes = [
{
path: '/live-course',
component:LiveCoursePage,
name: '课程管理'
name: '直播课'
},
{
path: '/video-course',
component:VideoCoursePage,
name: '视频课'
},
{
path: '/create-live-course',
......@@ -53,10 +59,16 @@ const mainRoutes = [
name: '创建直播课'
},
{
path: '/create-video-course',
component:AddVideoCoursePage,
name: '创建视频课'
},
{
path: '/resource-disk',
component:ClassBook,
name: '资料云盘'
},
]
export default mainRoutes;
\ No newline at end of file
......@@ -9,11 +9,11 @@ export const menuList: any = [
groupCode: "CourseLiveClass",
link: '/live-course'
},
// {
// groupName: "视频课",
// groupCode: "CourseVideoClass",
// link: '/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