Commit a468e88b by wufan

feat:完成学员观看详情页

parent 7b78f5e2
...@@ -1156,3 +1156,15 @@ window.XMShowClassName = (date, itemName) => { ...@@ -1156,3 +1156,15 @@ window.XMShowClassName = (date, itemName) => {
} }
return 'new-icon' 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
...@@ -7,173 +7,273 @@ ...@@ -7,173 +7,273 @@
* @Copyright: 杭州杰竞科技有限公司 版权所有 * @Copyright: 杭州杰竞科技有限公司 版权所有
*/ */
import React from 'react' import React from "react";
import { Modal } from 'antd' import { Modal, Tabs } from "antd";
import moment from 'moment' import moment from "moment";
import './PreviewCourseModal.less' import ChapterList from "../video-course/components/ChapterList";
import "./PreviewCourseModal.less";
const { TabPane } = Tabs;
const courseStateShow = { const courseStateShow = {
UN_START: { UN_START: {
title: '待开课' title: "待开课",
}, },
STARTING: { STARTING: {
title: '上课中' title: "上课中",
}, },
FINISH: { FINISH: {
title: '已完成' title: "已完成",
}, },
EXPIRED: { EXPIRED: {
code: 4, code: 4,
title: '未成功开课', title: "未成功开课",
color: '#CCCCCC' color: "#CCCCCC",
} },
} };
class PreviewCourseModal extends React.Component { class PreviewCourseModal extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props);
this.state = {} this.state = {
activeTab: "courseChapter",
};
} }
dealTimeDuration = (time) => { dealTimeDuration = (time) => {
const diff = Math.floor(time % 3600) const diff = Math.floor(time % 3600);
let hours = Math.floor(time / 3600) let hours = Math.floor(time / 3600);
let mins = Math.floor(diff / 60) let mins = Math.floor(diff / 60);
let seconds = Math.floor(time % 60) let seconds = Math.floor(time % 60);
hours = hours < 10 ? '0' + hours : hours hours = hours < 10 ? "0" + hours : hours;
mins = mins < 10 ? '0' + mins : mins mins = mins < 10 ? "0" + mins : mins;
seconds = seconds < 10 ? '0' + seconds : seconds seconds = seconds < 10 ? "0" + seconds : seconds;
return hours + ':' + mins + ':' + seconds return hours + ":" + mins + ":" + seconds;
} };
dealWithTime = (startTime, endTime) => { dealWithTime = (startTime, endTime) => {
const startDate = new Date(Number(startTime)) const startDate = new Date(Number(startTime));
const endDate = new Date(Number(endTime)) const endDate = new Date(Number(endTime));
const year = startDate.getFullYear() const year = startDate.getFullYear();
const month = startDate.getMonth() + 1 < 10 ? `0${startDate.getMonth() + 1}` : startDate.getMonth() + 1 const month =
const day = startDate.getDate() < 10 ? `0${startDate.getDate()}` : startDate.getDate() 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 startHour =
const startMinute = startDate.getMinutes() < 10 ? `0${startDate.getMinutes()}` : startDate.getMinutes() 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 endHour =
const endMinute = endDate.getMinutes() < 10 ? `0${endDate.getMinutes()}` : endDate.getMinutes() endDate.getHours() < 10 ? `0${endDate.getHours()}` : endDate.getHours();
const endMinute =
endDate.getMinutes() < 10
? `0${endDate.getMinutes()}`
: endDate.getMinutes();
const liveDateStr = `${year}-${month}-${day}` const liveDateStr = `${year}-${month}-${day}`;
const startTimeStr = `${startHour}:${startMinute}` const startTimeStr = `${startHour}:${startMinute}`;
const endTimeStr = `${endHour}:${endMinute}` const endTimeStr = `${endHour}:${endMinute}`;
return { return {
liveDateStr, liveDateStr,
startTimeStr, startTimeStr,
endTimeStr endTimeStr,
} };
} };
render() { render() {
const { courseBasicInfo, courseClassInfo = {}, courseIntroInfo, type, courseState, origin } = this.props const {
const { coverUrl, courseName, scheduleVideoUrl, videoDuration } = courseBasicInfo courseBasicInfo,
const { liveDate, calendarTime, startTime, endTime, timeHorizonStart, timeHorizonEnd, teacherName } = courseClassInfo courseClassInfo = {},
const { introduce } = courseIntroInfo courseIntroInfo,
type,
let liveDateStr, startTimeStr, endTimeStr courseState,
courseChapterList = [],
if (type === 'add') { } = this.props;
const _liveDate = moment(calendarTime[0]).format('YYYY-MM-DD') const { coverUrl, courseName, scheduleVideoUrl, videoDuration } =
console.log('_liveDate', _liveDate) courseBasicInfo;
const _timeHorizonStart = moment(startTime).format('HH:mm') const {
const _timeHorizonEnd = moment(endTime).format('HH:mm') liveDate,
const _startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x') calendarTime,
const _endTime = moment(_liveDate + ' ' + _timeHorizonEnd).format('x') startTime,
const { liveDateStr: _liveDateStr, startTimeStr: _startTimeStr, endTimeStr: _endTimeStr } = this.dealWithTime(_startTime, _endTime) endTime,
timeHorizonStart,
liveDateStr = _liveDateStr timeHorizonEnd,
startTimeStr = _startTimeStr teacherName,
endTimeStr = _endTimeStr } = courseClassInfo;
const { introduce } = 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 { } 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 _timeHorizonStart = moment(timeHorizonStart).format("HH:mm");
const _timeHorizonEnd = moment(timeHorizonEnd).format('HH:mm') const _timeHorizonEnd = moment(timeHorizonEnd).format("HH:mm");
const startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x') const startTime = moment(_liveDate + " " + _timeHorizonStart).format("x");
const endTime = moment(_liveDate + ' ' + _timeHorizonEnd).format('x') const endTime = moment(_liveDate + " " + _timeHorizonEnd).format("x");
const { liveDateStr: _liveDateStr, startTimeStr: _startTimeStr, endTimeStr: _endTimeStr } = this.dealWithTime(startTime, endTime) const {
liveDateStr: _liveDateStr,
liveDateStr = _liveDateStr startTimeStr: _startTimeStr,
startTimeStr = _startTimeStr endTimeStr: _endTimeStr,
endTimeStr = _endTimeStr } = this.dealWithTime(startTime, endTime);
liveDateStr = _liveDateStr;
startTimeStr = _startTimeStr;
endTimeStr = _endTimeStr;
} }
return ( return (
<Modal <Modal
title='预览' title="预览"
visible={true} visible={true}
width={680} width={680}
onCancel={this.props.close} onCancel={this.props.close}
footer={null} footer={null}
maskClosable={false} maskClosable={false}
closeIcon={<span className='icon iconfont modal-close-icon'>&#xe6ef;</span>} closeIcon={
className='preview-live-course-modal'> <span className="icon iconfont modal-close-icon">&#xe6ef;</span>
<div className='container__wrap'> }
<div className='container'> className="preview-live-course-modal"
<div className='container__header'> >
{type === 'videoCourse' ? ( <div className="container__wrap">
<div className="container">
<div className="container__header">
{type === "videoCourse" ? (
<video <video
controls controls
src={scheduleVideoUrl} src={courseChapterList.length && courseChapterList[0].mediaUrl || scheduleVideoUrl }
poster={coverUrl ? coverUrl : `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast`} poster={
className='course-url' coverUrl
? coverUrl
: "https://image.xiaomaiketang.com/xm/mt3ZQRxGKB.png"
}
className="course-url"
/> />
) : ( ) : (
<img src={coverUrl} className='course-cover' /> <img src={coverUrl} className="course-cover" />
)} )}
</div> </div>
{type === 'videoCourse' ? ( {type === "videoCourse" ? (
<div className='container__body'> <div className="container__body">
<div className='title__name'>{courseName}</div> <div className="title__name">{courseName}</div>
{videoDuration && <div>视频时长:{this.dealTimeDuration(videoDuration)}</div>} {videoDuration && (
<div>视频时长:{this.dealTimeDuration(videoDuration)}</div>
)}
</div> </div>
) : ( ) : (
<div className='container__body'> <div className="container__body">
<div className='container__body__title'> <div className="container__body__title">
<div className='title__name'>{courseName}</div> <div className="title__name">{courseName}</div>
<div className='title__state'>{courseStateShow[courseState].title}</div> <div className="title__state">
{courseStateShow[courseState].title}
</div>
</div> </div>
<div className='container__body__time'> <div className="container__body__time">
<span className='time__label'>上课时间:</span> <span className="time__label">上课时间:</span>
<span className='time__value'> <span className="time__value">
{[ {[
<span>{liveDateStr}&nbsp;</span>, <span>{liveDateStr}&nbsp;</span>,
<span> <span>
{startTimeStr}~{endTimeStr} {startTimeStr}~{endTimeStr}
</span> </span>,
]} ]}
</span> </span>
</div> </div>
<div className='container__body__teacher'> <div className="container__body__teacher">
<span className='teacher__label'>上课老师:</span> <span className="teacher__label">上课老师:</span>
<span className='teacher__value'>{teacherName}</span> <span className="teacher__value">{teacherName}</span>
</div> </div>
</div> </div>
)} )}
<div className='container__introduction'> <div className="container__introduction">
{type === 'videoCourse' ? ( <Choose>
<div className='container__introduction__title'>线上课简介</div> <When condition={type === "videoCourse"}>
) : ( <Tabs
<div className='container__introduction__title'>直播课简介</div> 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 <div
className='intro-item text' className="intro-item text"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: introduce __html: introduce,
}} }}
/> />
</div> </div>
</Otherwise>
</Choose>
</div> </div>
</div> </div>
</div> </div>
</Modal> </Modal>
) );
} }
} }
export default PreviewCourseModal export default PreviewCourseModal;
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
...@@ -10,6 +10,7 @@ import CourseService from "@/domains/course-domain/CourseService" ...@@ -10,6 +10,7 @@ import CourseService from "@/domains/course-domain/CourseService"
import RelatedPlanModal from "../../modal/RelatedPlanModal" import RelatedPlanModal from "../../modal/RelatedPlanModal"
import User from "@/common/js/user" import User from "@/common/js/user"
import VideoCourseDetail from '../VideoCourseDetail'; import VideoCourseDetail from '../VideoCourseDetail';
import WatchData from "./WatchData";
import "./VideoCourseList.less" import "./VideoCourseList.less"
...@@ -42,19 +43,11 @@ class VideoCourseList extends React.Component { ...@@ -42,19 +43,11 @@ class VideoCourseList extends React.Component {
} }
// 观看数据弹窗 // 观看数据弹窗
handleShowWatchDataModal = (record) => { handleShowWatchDataModal = (item) => {
const watchDataModal = ( const { match } = this.props;
<WatchDataModal window.RCHistory.push({
type='videoCourseList' pathname: `${match.url}/course-data?type=${item.type}&id=${item.id}`
data={record}
close={() => {
this.setState({
watchDataModal: null
}) })
}}
/>
)
this.setState({ watchDataModal })
} }
// 请求表头 // 请求表头
...@@ -503,6 +496,7 @@ class VideoCourseList extends React.Component { ...@@ -503,6 +496,7 @@ class VideoCourseList extends React.Component {
{this.state.shareLiveModal} {this.state.shareLiveModal}
{this.state.watchDataModal} {this.state.watchDataModal}
<Route path={`${match.url}/video-course-detail`} component={VideoCourseDetail} /> <Route path={`${match.url}/video-course-detail`} component={VideoCourseDetail} />
<Route path={`${match.url}/course-data`} component={WatchData} />
</div> </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 } from 'antd';
import React from "react";
import { withRouter } from "react-router-dom";
const { Search } = Input;
class WatchData extends React.Component {
constructor(props) {
super(props);
const id = window.getParameterByName("id");
this.state = {
id,
visible: true,
dataSource: [],
query: {
current: 1,
size: 10,
},
totalCount: 0,
};
}
componentDidMount() {
this.handleFetchDataList();
}
// 获取观看视频数据列表
handleFetchDataList = () => {
const { query, id } = this.state;
const params = {
...query,
courseId: id,
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 columns = [
{
title: '观看学员',
key: 'name',
dataIndex: 'name'
},
{
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",
width: 210,
render: (val, record) => {
return (
<div className="operate">
<div
className="operate__item"
// onClick={() => this.handleShowShareModal(record)}
>
学习详情
</div>
</div>
);
},
},
];
return columns;
};
render() {
const { dataSource, totalCount, query } = this.state;
const { current, size } = query;
return (
<div className="page data-list">
<Breadcrumbs
navList="观看学员数据"
goBack={() => {
window.RCHistory.goBack();
}}
/>
<div className="box-header">
<div className="course-title"></div>
<div className="filter-box">
<Search placeholder="搜索学员姓名/手机号" style={{ width: 200 }} onChange={(e) => { this.handleChangNickname(e.target.value)}} onSearch={ () => { this.handleFetchDataList()}} enterButton={<span className="icon iconfont">&#xe832;</span>}/>
</div>
</div>
<div className="box">
<XMTable
renderEmpty={{
image: college,
description: "暂无数据",
}}
rowKey={(record) => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
pagination={false}
scroll={{ x: 1500 }}
bordered
className="video-list-table"
/>
</div>
<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>
);
}
}
export default withRouter(WatchData);
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* @Date: 2020-05-19 11:01:31 * @Date: 2020-05-19 11:01:31
* @Last Modified by: 吴文洁 * @Last Modified by: 吴文洁
* @Last Modified time: 2020-05-25 16:50:47 * @Last Modified time: 2020-05-25 16:50:47
* @Description 余额异常弹窗 * @Description 学员观看数据弹窗
*/ */
import React from 'react'; import React from 'react';
import {Table, Modal,Input} from 'antd'; import {Table, Modal,Input} from 'antd';
......
...@@ -452,7 +452,6 @@ class KnowledgeBaseList extends React.Component { ...@@ -452,7 +452,6 @@ class KnowledgeBaseList extends React.Component {
const { match } = this.props; const { match } = this.props;
localStorage.setItem("WatchData_CourseName", item.name); localStorage.setItem("WatchData_CourseName", item.name);
window.RCHistory.push({ 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}` pathname: `${match.url}/course-data?type=${item.type}&id=${item.id}`
}) })
} }
......
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