Commit dffb2d6d by zhangleyuan

feat:处理课程管理页面

parent c9bfedd3
'use strict';
var extend = require('extend');
var qrcodeAlgObjCache = [];
var QRCodeAlg = require('./qrcodealg');
/**
* 计算矩阵点的前景色
* @param {Obj} config
* @param {Number} config.row 点x坐标
* @param {Number} config.col 点y坐标
* @param {Number} config.count 矩阵大小
* @param {Number} config.options 组件的options
* @return {String}
*/
var getForeGround = function getForeGround(config) {
var options = config.options;
if (options.pdground && (config.row > 1 && config.row < 5 && config.col > 1 && config.col < 5 || config.row > config.count - 6 && config.row < config.count - 2 && config.col > 1 && config.col < 5 || config.row > 1 && config.row < 5 && config.col > config.count - 6 && config.col < config.count - 2)) {
return options.pdground;
}
return options.foreground;
};
/**
* 点是否在Position Detection
* @param {row} 矩阵行
* @param {col} 矩阵列
* @param {count} 矩阵大小
* @return {Boolean}
*/
var inPositionDetection = function inPositionDetection(row, col, count) {
if (row < 7 && col < 7 || row > count - 8 && col < 7 || row < 7 && col > count - 8) {
return true;
}
return false;
};
/**
* 二维码构造函数,主要用于绘制
* @param {参数列表} opt 传递参数
* @return {}
*/
var qrcode = function qrcode(opt) {
if (typeof opt === 'string') {
// 只编码ASCII字符串
opt = {
text: opt
};
}
//设置默认参数
this.options = extend({}, {
text: '',
render: '',
size: 256,
correctLevel: 3,
background: '#ffffff',
foreground: '#000000',
image: '',
imageSize: 30
}, opt);
//使用QRCodeAlg创建二维码结构
var qrCodeAlg = null;
for (var i = 0, l = qrcodeAlgObjCache.length; i < l; i++) {
if (qrcodeAlgObjCache[i].text == this.options.text && qrcodeAlgObjCache[i].text.correctLevel == this.options.correctLevel) {
qrCodeAlg = qrcodeAlgObjCache[i].obj;
break;
}
}
if (i == l) {
qrCodeAlg = new QRCodeAlg(this.options.text, this.options.correctLevel);
qrcodeAlgObjCache.push({ text: this.options.text, correctLevel: this.options.correctLevel, obj: qrCodeAlg });
}
if (this.options.render) {
switch (this.options.render) {
case 'canvas':
return this.createCanvas(qrCodeAlg);
case 'table':
return this.createTable(qrCodeAlg);
case 'svg':
return this.createSVG(qrCodeAlg);
default:
return this.createDefault(qrCodeAlg);
}
}
return this.createDefault(qrCodeAlg);
};
extend(qrcode.prototype, {
// default create canvas -> svg -> table
createDefault: function createDefault(qrCodeAlg) {
var canvas = document.createElement('canvas');
if (canvas.getContext) {
return this.createCanvas(qrCodeAlg);
}
var SVG_NS = 'http://www.w3.org/2000/svg';
if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) {
return this.createSVG(qrCodeAlg);
}
return this.createTable(qrCodeAlg);
},
// canvas create
createCanvas: function createCanvas(qrCodeAlg) {
var options = this.options;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var count = qrCodeAlg.getModuleCount();
// preload img
var loadImage = function loadImage(url, callback) {
var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
img.onload = function () {
callback(this);
img.onload = null;
};
};
//计算每个点的长宽
var tileW = (options.size / count).toPrecision(4);
var tileH = (options.size / count).toPrecision(4);
canvas.width = options.size;
canvas.height = options.size;
//绘制
for (var row = 0; row < count; row++) {
for (var col = 0; col < count; col++) {
var w = Math.ceil((col + 1) * tileW) - Math.floor(col * tileW);
var h = Math.ceil((row + 1) * tileW) - Math.floor(row * tileW);
var foreground = getForeGround({
row: row,
col: col,
count: count,
options: options
});
ctx.fillStyle = qrCodeAlg.modules[row][col] ? foreground : options.background;
ctx.fillRect(Math.round(col * tileW), Math.round(row * tileH), w, h);
}
}
if (options.image) {
loadImage(options.image, function (img) {
var x = ((options.size - options.imageSize) / 2).toFixed(2);
var y = ((options.size - options.imageSize) / 2).toFixed(2);
ctx.drawImage(img, x, y, options.imageSize, options.imageSize);
});
}
return canvas;
},
// table create
createTable: function createTable(qrCodeAlg) {
var options = this.options;
var count = qrCodeAlg.getModuleCount();
// 计算每个节点的长宽;取整,防止点之间出现分离
var tileW = Math.floor(options.size / count);
var tileH = Math.floor(options.size / count);
if (tileW <= 0) {
tileW = count < 80 ? 2 : 1;
}
if (tileH <= 0) {
tileH = count < 80 ? 2 : 1;
}
//创建table节点
//重算码大小
var s = [];
s.push('<table style="border:0px; margin:0px; padding:0px; border-collapse:collapse; background-color:' + options.background + ';">');
// 绘制二维码
for (var row = 0; row < count; row++) {
s.push('<tr style="border:0px; margin:0px; padding:0px; height:' + tileH + 'px">');
for (var col = 0; col < count; col++) {
var foreground = getForeGround({
row: row,
col: col,
count: count,
options: options
});
if (qrCodeAlg.modules[row][col]) {
s.push('<td style="border:0px; margin:0px; padding:0px; width:' + tileW + 'px; background-color:' + foreground + '"></td>');
} else {
s.push('<td style="border:0px; margin:0px; padding:0px; width:' + tileW + 'px; background-color:' + options.background + '"></td>');
}
}
s.push('</tr>');
}
s.push('</table>');
if (options.image) {
// 计算表格的总大小
var width = tileW * count;
var height = tileH * count;
var x = ((width - options.imageSize) / 2).toFixed(2);
var y = ((height - options.imageSize) / 2).toFixed(2);
s.unshift('<div style=\'position:relative; \n width:' + width + 'px; \n height:' + height + 'px;\'>');
s.push('<img src=\'' + options.image + '\' \n width=\'' + options.imageSize + '\' \n height=\'' + options.imageSize + '\' \n style=\'position:absolute;left:' + x + 'px; top:' + y + 'px;\'>');
s.push('</div>');
}
var span = document.createElement('span');
span.innerHTML = s.join('');
return span.firstChild;
},
// create svg
createSVG: function createSVG(qrCodeAlg) {
var options = this.options;
var count = qrCodeAlg.getModuleCount();
var scale = count / options.size;
// create svg
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', options.size);
svg.setAttribute('height', options.size);
svg.setAttribute('viewBox', '0 0 ' + count + ' ' + count);
for (var row = 0; row < count; row++) {
for (var col = 0; col < count; col++) {
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
var foreground = getForeGround({
row: row,
col: col,
count: count,
options: options
});
rect.setAttribute('x', col);
rect.setAttribute('y', row);
rect.setAttribute('width', 1);
rect.setAttribute('height', 1);
rect.setAttribute('stroke-width', 0);
if (qrCodeAlg.modules[row][col]) {
rect.setAttribute('fill', foreground);
} else {
rect.setAttribute('fill', options.background);
}
svg.appendChild(rect);
}
}
// create image
if (options.image) {
var img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', options.image);
img.setAttribute('x', ((count - options.imageSize * scale) / 2).toFixed(2));
img.setAttribute('y', ((count - options.imageSize * scale) / 2).toFixed(2));
img.setAttribute('width', options.imageSize * scale);
img.setAttribute('height', options.imageSize * scale);
svg.appendChild(img);
}
return svg;
}
});
module.exports = qrcode;
\ No newline at end of file
@import '../../core/variables.less';
.common-select-active {
span {
color: @primary;
}
}
.common-select {
cursor: pointer;
.ant-dropdown-link{
// padding-top: 5px;
}
&.border {
flex: 1;
height: 32px;
line-height: 32px;
border: 1px solid #e8e8e8;
border-radius: 4px;
.ant-dropdown-link {
padding: 0 8px;
width: 100%;
.title {
width: calc(~'100% - 18px');
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
.icon {
float: right;
color: rgba(0, 0, 0, 0.25);
}
}
}
.ant-dropdown-link {
.title {
max-width: 180px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
padding-right: 5px;
}
&:hover {
span {
color: @xm-color-text-select-primary;
}
}
span {
float: left;
line-height: 30px !important;
}
}
.selected-list{
.selected-list-item {
text-align: center;
min-width: 60px;
}
.icon {
position: relative;
.close {
position: absolute;
top: -5px;
right: 5px;
display: none;
}
&:hover {
.close {
display: block;
}
}
}
.icon-tip {
width: 100%;
text-align: center;
position: absolute;
bottom: 9px;
line-height: 10px;
span {
font-size: 8px;
line-height: 10px;
background-color: @primary;
padding: 1px 5px;
color: white;
border-radius: 5px;
}
}
.name {
font-size: 12px;
line-height: 15px;
}
}
}
.common-select-menu {
.header {
.ant-dropdown-link {
margin-left: 10px;
}
}
.footer {
padding-bottom: 0px;
line-height: 30px;
}
.list {
max-height: 250px;
overflow-y: auto;
.student {
padding: 5px 10px;
}
.name {
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.student {
.user-name {
padding: 0px 10px;
color: @xm-main-text-color;
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
line-height: 32px;
}
}
.common-select-menu-item {
line-height: 100%;
margin: 10px 0;
.icon {
position: relative;
}
.icon-tip {
width: 100%;
text-align: center;
position: absolute;
bottom: 0px;
line-height: 10px;
span {
font-size: 6px;
line-height: 10px;
background-color: @primary;
padding: 5px;
color: white;
border-radius: 15px;
}
}
}
.common-select-menu-item:hover {
background-color: @xm-select-item-hover;
}
.ant-spin-container:hover {
background-color: white;
}
list:hover {
background-color: white;
}
.class-info {
.name .teacher-name{
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis
}
}
.teacher-info {
.teacher-name, .subject-name {
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis
}
}
}
// 下拉框颜色
.ant-dropdown-menu-item-active.ant-dropdown-menu-item.list:hover {
background-color: white !important;
}
.ant-dropdown-menu-item-active.ant-dropdown-menu-item:hover {
background-color: @xm-select-item-hover!important;
}
.ant-dropdown-menu-item-active.ant-dropdown-menu-item.header:hover {
background-color: #fff !important;
}
.ant-dropdown-menu-item-active.ant-dropdown-menu-item.footer:hover {
background-color: #fff !important;
}
.ant-dropdown-menu-item:hover {
background-color: white !important;
}
.ant-dropdown-menu-item-selected {
color: white;
}
}
\ No newline at end of file
import React from 'react';
import PropTypes from 'prop-types';
import { DatePicker } from 'antd';
import moment from 'moment';
const { RangePicker } = DatePicker;
class DateRangePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false
}
}
render() {
const showTime = { showTime: true }
return (
<RangePicker
{...this.props}
format={this.props.format || 'YYYY-MM-DD'}
allowClear={this.props.allowClear}
ranges={this.props.ranges || { '本月': [moment().startOf('month'), moment().endOf('month')], '本周': [moment().startOf('week'), moment().endOf('week')], '上月': [moment().subtract(1, 'M').startOf('month'), moment().subtract(1, 'M').endOf('month')], '上周': [moment().subtract(1, 'w').startOf('week'), moment().subtract(1, 'w').endOf('week')] }}
onChange={(date) => {
if (!_.isEmpty(date)) {
date[0] = date[0].startOf('day')
date[1] = date[1].endOf('day')
}
this.props.onChange(date)
}}
{...showTime}
/>
)
}
}
DateRangePicker.propTypes = {
};
DateRangePicker.defaultProps = {
allowClear: true,
}
export default DateRangePicker;
\ No newline at end of file
/*
* @Author: leehu
* @Date: 2017-10-12 16:24:11
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-03-20 10:43:55
*/
import React from 'react';
import { Tooltip, Dropdown, Icon, Checkbox, Button, Input, Spin, Select, Avatar, Row, Col } from 'antd';
import "./CommonSelect.less";
import "./TeacherSearchSelect.less";
import _ from "underscore";
const Search = Input.Search;
const baseImg = require('@/common/images/xiaomai-IMG.png')
class TeacherSearchSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
close: true,
dataSource: [],
query: {
month: null,
current: 1,
size: 10,
nameOrPhone: '',
status: 'ON',
CustomOrderType: 2,
courseId: props.courseId,
// instId: window.currentUserInstInfo.instId,
},
dataSet: [],
visible: false,
loading: false,
selectedIds: _.pluck(props.selected, 'teacherId') || [],
selected: props.selected || [],
isAll: false,
oldDataSet: [],
}
this.reset = () => {
this.handleQueryReset();
}
}
componentWillMount() {
this.fetchServerData();
}
componentWillReceiveProps(nextProps) {
if (this.props.courseId !== nextProps.courseId) {
this.state.query.courseId = nextProps.courseId;
if (!nextProps.courseId) {
delete this.state.query.courseId
}
this.fetchServerData();
}
}
handleQuery = () => {
this.fetchServerData();
}
handleQueryReset = () => {
this.setState({
query: {
current: 1,
size: 10,
nameOrPhone: '',
status: 'ON',
CustomOrderType: 2,
courseId: this.props.courseId,
}
}, () => {
this.fetchServerData();
});
}
searchName = () => {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.fetchServerData()
}, 500)
}
fetchServerData(current = 1) {
this.state.query.current = current;
const self = this;
const url = this.props.url || 'public/teacher/getTeacherDigestPage'
const param = _.extend(this.state.query, this.props.query);
// param.courseId = this.props.courseId || null
this.setState({ fetching: true }, () => {
setTimeout(() => {
this.setState({ fetching: false })
})
})
// axios.Business(url, param).then((res) => {
// let data = this.props.filter ? this.props.filter(res.result.records) : res.result.records;
// if (current > 1) {
// data = this.state.dataSet.concat(data)
// }
// if (this.props.multiple && (current == 1)) {
// _.map(this.props.defaultValue, (item) => {
// const list = _.filter(data, (_item) => {
// return item.teacherId == _item.teacherId
// })
// if (!list.length) {
// data.push(item)
// }
// })
// }
// if (this.props.noTeacher) {
// if (data[0] && data[0].teacherId != '-1') {
// data.unshift(
// { teacherId: '-1', name: '待分配' }
// )
// }
// }
// data = _.uniq(data, (item) => {
// return item.teacherId;
// })
// this.setState({
// dataSet: data,
// totalCount: res.result.total,
// isAll: !res.result.hasNext,
// fetching: false
// });
// }).finally((res) => {
// this.setState({ loading: false });
// });
}
handleTeacherSelect = (teacher) => {
const query = this.state.query;
const { oldDataSet, dataSet } = this.state;
const data = _.find(dataSet, item => item.teacherId == teacher);
if (data) {
oldDataSet.push(data);
const _oldDataSet = _.uniq(oldDataSet, item => item.teacherId);
this.setState({ oldDataSet: _oldDataSet });
}
if (query.name) {
query.name = '';
this.setState({ query }, this.fetchServerData);
}
if (!this.props.multiple) {
const items = _.filter(this.state.dataSet, (_item) => {
return _item.teacherId == teacher;
})
this.props.onSelect({ teacherId: teacher, name: !!items[0] && items[0].name ? items[0].name : '' });
return;
} else {
const list = [];
_.map(teacher, (item) => {
const _list = _.filter(this.state.dataSet, (_item) => {
return _item.teacherId == item;
})
list.push({
name: _list[0].name,
teacherId: item
})
})
this.props.onSelect(list);
}
}
hasmore = (dom) => {
clearTimeout(this.scroll)
let height = $(dom.currentTarget).find('ul li').last().position().top
this.scroll = setTimeout(() => {
if (height < 500 && !this.state.isAll) {
this.fetchServerData(this.state.query.current + 1)
}
}, 300)
}
render() {
let defprops = {}
if (this.props.multiple) {
defprops.mode = 'multiple'
}
let value = [];
value = this.props.defaultValue
const list = _.filter(this.state.dataSet, (item) => {
return item.teacherId == this.props.defaultValue
})
if (!list.length) {
value = this.props.teacherName || undefined
}
if (this.props.multiple) {
value = _.pluck(this.props.defaultValue, 'teacherId')
}
let data = _.filter(this.state.dataSet, item => item.name);
data = _.uniq(data, item => item.teacherId);
return (
<div className={("common-select staticSelect teacher-search-select", { 'common-select-active': this.state.visible })} style={this.props.style}>
{
!!this.props.label && <div className='label'> {this.props.label}:</div>
}
<Select
id={this.props.id}
ref='teacher'
{...defprops}
showSearch
// style={{ width: '100%' }}
allowClear
notFoundContent={this.state.fetching ? <Spin size="small" /> : null}
onSearch={
(value) => {
const query = this.state.query;
query.nameOrPhone = value
this.setState({ query }, this.searchName)
}
}
// open
onPopupScroll={(dom) => {
this.hasmore(dom)
}}
placeholder={this.props.placeholder}
value={value}
onChange={this.handleTeacherSelect}
filterOption={(input, option) => option}
>
{
_.map(data, (item, index) => {
if (this.props.showAvatar) {
return <Select.Option id={'teacher_select_item_' + index} key={item.teacherId} value={item.teacherId} title={item.name}>
<Row align="middle">
<Col span={6}>
<Avatar icon="user" src={item.avatar || baseImg} />
</Col>
<Col span={18}>
<div className="teacher-name" title={item.name}>
{item.name}
</div>
<div className="subject-name" title={item.subjectName || ''}>
{item.subjectName || ''}
</div>
</Col>
</Row>
</Select.Option>
} else {
return <Select.Option id={'teacher_select_item_' + index} key={item.teacherId} value={item.teacherId} title={item.name}>
<Tooltip title={`科目:${item.subjectName || '未设置'}`}>{item.name}</Tooltip>
</Select.Option>
}
})
}
{!this.state.isAll &&
<Select.Option disabled style={{ textAlign: 'center' }} value="spin">
<Spin size="small" />
</Select.Option>
}
</Select>
</div>
)
}
}
TeacherSearchSelect.propTypes = {};
TeacherSearchSelect.defaultProps = {
onSelect: () => { },
name: '选择老师',
courseId: null,
selected: [],
multiple: false,
query: {},
filter: null,
renderItem: null,
limit: false,
async: false,
showAvatar: false
}
export default TeacherSearchSelect;
.teacher-search-select {
.common-select-menu{
.teacher-name, .subject-name {
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis
}
}
}
\ No newline at end of file
import React from 'react';
import LiveCourseFilter from './components/LiveCourseFilter';
import LiveCourseOpt from './components/LiveCourseOpt';
// import LiveCourseList from './components/LiveCourseList';
class LiveCoursePage extends React.Component {
constructor(props) {
super(props);
// const { instId, teacherId } = window.currentUserInstInfo;
this.state = {
courseList: [], // 直播课列表
query: {
current: 1,
size: 10,
instId:0,
teacherId:0
},
total: 0,
loading: true,
}
}
// componentWillMount() {
// this.handleFetchLiveList();
// }
// // 获取直播课列表
// handleFetchLiveList = (_query) => {
// const { query } = this.state;
// const { teacherId } = window.currentUserInstInfo;
// const params = {
// teacherId: teacherId ? teacherId : null,
// ...query,
// ..._query,
// };
// this.setState({ query: params });
// window.axios
// .Apollo("public/businessLive/getLargeClassLiveList", params)
// .then((res) => {
// const { result: { records = [], total } } = res;
// this.setState({
// total,
// courseList: records
// });
// }) .finally(() => {
// this.setState({ loading: false });
// });
// }
render() {
const { query, total, courseList } = this.state;
return (
<div className="page big-live-page">
<div className="content-header">大班直播</div>
<div className="box">
<LiveCourseFilter
onChange={this.handleFetchLiveList}
/>
<LiveCourseOpt />
{/* <LiveCourseList
query={query}
total={total}
courseList={courseList}
onChange={this.handleFetchLiveList}
/> */}
</div>
</div>
)
}
}
export default LiveCoursePage;
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-15 17:29:12
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-15 18:16:05
* @Description: 新建/编辑直播课-基本信息
*/
import React from 'react';
import { Input, Button, message } from 'antd';
import UploadOss from "@/core/upload";
import { ImgCutModalNew } from '@/components';
import './AddLiveBasic.less';
const defaultCover = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class AddLiveBasic extends React.Component {
constructor(props) {
super(props);
this.state = {
xhr: null,
imageFile: null,
showCutModal: false
}
}
// 上传封面图
handleShowImgCutModal = (event) => {
const imageFile = event.target.files[0];
if (!imageFile) return;
this.setState({
imageFile,
showCutModal: true,
});
}
// 使用默认封面图
handleResetCoverUrl = () => {
const { data: { coverUrl } } = this.props;
const isDefaultCover = coverUrl === defaultCover;
// 如果已经是默认图的话,不做任何任何处理
if (isDefaultCover) return;
message.success('已替换为默认图');
this.props.onChange('coverId', null, defaultCover);
}
componentWillUnmount() {
const { xhr } = this.state;
xhr && xhr.abort();
}
render() {
const { showCutModal, imageFile } = this.state;
const { data, liveScene } = this.props;
const { courseName, coverUrl } = data;
const fileName = '';
// 是否是互动班课,互动班课不显示封面图
const isInteractive = liveScene === 'interactive';
// 当前是否使用的是默认图片
const isDefaultCover = coverUrl === defaultCover;
return (
<div className="add-live__basic-info">
<div className="course-name">
<span className="label"><span className="require">*</span>课程名称:</span>
<Input
value={courseName}
placeholder="请输入直播名称(40字以内)"
maxLength={40}
style={{ width: 240 }}
onChange={(e) => { this.props.onChange('courseName', e.target.value)}}
/>
</div>
{
!isInteractive &&
<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">
<input
type="file"
value={fileName} // 避免选择同一文件 value不改变 不触发onChange事件
accept="image/png, image/jpeg, image/bmp, image/jpg"
ref="stagePicInputFile"
style={{display: 'none'}}
onChange={(event) => { this.handleShowImgCutModal(event) }}
/>
<Button onClick={() => {
this.setState({
currentInputFile: this.refs.stagePicInputFile
});
this.refs.stagePicInputFile.click();
}}>上传图片</Button>
<span
className={`default-btn ${isDefaultCover ? 'disabled' : ''}`}
onClick={this.handleResetCoverUrl}
>使用默认图</span>
<div className="tips">建议尺寸690*398像素,图片支持jpg、jpeg、png格式。</div>
</div>
</div>
</div>
}
<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.props.onChange('coverId', resourceId, urlStr);
this.state.currentInputFile.value = '';
}}
onClose={() => this.setState({ showCutModal: false })}
reUpload={() => { this.state.currentInputFile.click() }}
/>
</div>
)
}
}
export default AddLiveBasic;
\ No newline at end of file
.add-live__basic-info {
.label {
width: 100px;
text-align: right;
.require {
color: #EC4B35;
}
}
.course-cover {
margin-left: 14px;
display: flex;
margin-top: 16px;
&__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-right: 20px;
width: 299px;
height: 169px;
img {
width: 100%;
height: 100%;
object-fit: contain;
border: 1px solid #E8e8e8;
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #FF7519;
cursor: pointer;
&.disabled {
color: #CCC;
cursor: not-allowed;
}
}
.ant-upload-list {
display: none;
}
}
.tips {
margin-top: 8px;
color: #999;
}
}
}
#imgCutModalNew {
width: 500px;
height: 300px;
}
\ No newline at end of file
.add-live__class-info {
margin-top: 12px;
.label {
width: 100px;
text-align: right;
.require {
color: #EC4B35;
}
}
.class-student {
display: flex;
flex-direction: row;
margin-bottom: 16px;
.class-student-content {
padding: 20px 24px 20px 16px;
border-radius: 2px;
border: 1px solid #eee;
.deduction-student-tips {
margin: 8px 0 16px 100px;
width: fit-content;
height: 52px;
line-height: 52px;
padding: 0 10px;
background-color: #F0F2F5;
.ant-input-number {
margin: 0 4px;
}
}
}
}
.live-mode,
.podium-max-num,
.teacher,
.assistant-teacher{
margin: 12px 0;
}
.watch-setting {
display: flex;
margin-bottom: 16px;
.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;
}
}
}
.label {
width: 100px;
text-align: right;
.require {
color: #EC4B35;
}
}
.tips {
margin-left: 100px;
margin-top: 8px;
color: #666;
}
.uncommon-wrapper .ant-radio-wrapper {
width: 290px;
height: 96px;
padding: 6px 12px;
margin-bottom: 10px;
background-color: #fafafa;
vertical-align: top;
box-sizing: border-box;
margin-right: 16px;
.ant-radio {
vertical-align: -2px;
}
span:last-child {
vertical-align: top;
}
.title {
font-size: 14px;
color: #333;
line-height: 20px;
.tag {
display: inline-block;
padding: 0 6px;
margin-left: 4px;
font-size: 11px;
color: #fff;
line-height: 20px;
background-color: #ccc;
border-radius: 2px;
}
}
.info {
width: 100%;
padding-top: 4px;
margin-left: -22px;
white-space: normal;
font-size: 14px;
color: #666;
line-height: 20px;
}
}
.live-mode {
display: flex;
.name {
color: #333;
}
.info {
color: #666;
}
}
.stage-num {
margin-bottom: 10px;
.content {
display: flex;
align-items: center;
}
.add-live-slider {
.ant-slider-track {
background-color: #FC9C6B;
}
.ant-slider-handle{
background:linear-gradient(180deg,rgba(255,180,103,1) 0%,rgba(255,145,67,1) 100%);
border:none;
width: 10px;
height: 10px;
margin: -3px;
box-shadow: none;
}
}
}
.teacher,
.student,
.deduction-student,
.no-deduction-student {
display: flex;
align-items: center;
.content {
display: flex;
align-items: center;
.ant-btn {
margin-left: 8px;
}
}
}
textarea.ant-input {
min-height: 80px;
}
.look-explain {
color: #FC9C6B;
cursor: pointer;
}
.course {
.day {
display: flex;
flex-direction: row;
margin-bottom: 16px;
.select-day {
margin-bottom: 4px;
.mark-day {
color: #FC9C6B;
}
}
}
}
.iconfont {
color: #bfbfbf;
cursor: pointer;
}
.arrive-rule-input {
margin: 0 4px;
}
.multiple-calendar {
line-height: 40px;
}
}
\ No newline at end of file
.add-live__intro-info {
.playback {
margin-bottom: 10px;
&__text {
color: #999;
}
}
.playback,
.introduce {
display: flex;
flex-direction: row;
}
.radio {
display: block;
height: 30px;
line-height: 30px;
}
.interactive-playback {
display: flex;
margin-bottom: 20px;
}
textarea.ant-input {
min-height: 80px;
}
.intro-list__item {
display: flex;
margin-bottom: 16px;
position: relative;
&.picture {
width: 550px;
padding: 16px;
border: 1px solid #EEE;
border-radius: 4px;
.img__wrap {
width: 299px;
height: 168px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
.little-icon {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
right: -20px;
.iconfont {
width: 20px;
height: 20px;
line-height: 20px;
font-size: 20px;
color: #999;
margin-bottom: 4px;
cursor: pointer;
&.close {
margin-top: 8px;
background-image: url('https://image.xiaomaiketang.com/xm/eesMPhNP3e.png');
background-size: 100% 100%;
}
}
}
}
.operate {
display: flex;
align-items: center;
justify-content: center;
width: 550px;
height: 80px;
line-height: 80px;
padding: 16px;
margin-top: 16px;
border: 1px dashed #EBEBEB;
border-radius: 4px;
.ant-upload {
vertical-align: middle;
}
&__item {
display: flex;
flex-direction: column;
cursor: pointer;
&:not(:last-child) {
margin-right: 82px;
}
.iconfont {
font-size: 22px;
line-height: 22px;
color: #BFBFBF;
text-align: center;
}
.text {
color: #999;
line-height: 20px;
margin-top: 4px;
}
}
}
.tips {
color: #999;
margin-top: 16px;
margin-bottom: 8px;
}
.checkExample {
width: 60px;
color: #FF7519;
cursor: pointer;
}
.warmup {
margin-bottom: 17px;
display: flex;
}
.course-cover__wrap {
display: flex;
flex-direction: row;
}
.img-content {
position: relative;
margin-right: 20px;
width: 300px;
height: 170px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.img-delete-wrap {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
img {
position: absolute;
left: 50%;
top: 50%;
width: 40px;
height: 40px;
transform: translate(-50%, -50%);
}
&:hover {
opacity: 1;
cursor: pointer;
}
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #FF7519;
cursor: pointer;
&.disabled {
color: #CCC;
cursor: not-allowed;
}
}
}
}
.example-wrap {
font-family: PingFangSC-Regular, PingFang SC;
text-align: center;
.title {
margin-bottom: 6px;
font-size: 14px;
color: #333333;
}
.text {
margin-bottom: 16px;
font-size: 12px;
color: #999999;
}
img {
width: 180px;
}
}
.check-record-rule {
width: 120px;
color: #FF7519;
cursor: pointer;
z-index: 2;
}
.record-rule-wrap {
text-align: left;
ul {
margin-top: 10px;
padding-left: 34px;
list-style-type: disc;
li {
color: #999;
}
}
.text {
color: #999;
}
}
.auto-send-class-report {
.label {
width: 57px;
height: 12px;
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 12px;
}
.open-text, .close-text {
width: 572px;
font-size: 14px;
font-weight: 400;
color: #999999;
line-height: 20px;
margin-left: 100px;
margin-top: 5px;
}
.open-text {
margin-top: 8px;
}
.close-text {
margin-bottom: 16px;
}
}
\ No newline at end of file
import React from 'react';
import E from 'wangeditor';
import './EditorBox.less';
class EditorBox extends React.Component {
constructor(props) {
super(props)
this.state = {
editorId: window.random_string(16),
textLength: 0,
}
}
componentDidMount() {
this.renderEditor()
}
renderEditor() {
const { editorId } = this.state;
const { detail, onChange } = this.props;
const editorInt = new E(`#editor${editorId}`);
editorInt.customConfig.menus = [
// 'head', // 标题
'bold', // 粗体
// 'fontSize', // 字号
'fontName', // 字体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'list', // 列表
'justify', // 对齐方式
'emoticon', // 表情
]
editorInt.customConfig.emotions = [
{
title: 'emoji',
type: 'emoji',
content: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '😊', '🙂', '🙃', '😉', '😓', '😅', '😪', '🤔', '😬', '🤐']
}
]
editorInt.customConfig.zIndex = 1;
editorInt.customConfig.pasteFilterStyle = false;
editorInt.customConfig.pasteIgnoreImg = true;
// 自定义处理粘贴的文本内容
editorInt.customConfig.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
}
editorInt.customConfig.onchange = (html) => {
const textLength = editorInt.txt.text().replace(/\&nbsp\;/ig, ' ').length;
this.setState({ textLength }, () => {
onChange(html, this.state.textLength);
})
}
editorInt.create();
editorInt.txt.html(detail.content);
editorInt.change && editorInt.change();
}
render() {
const { editorId, textLength } = this.state;
const { limitLength = 1000 } = this.props;
return <div className="wang-editor-container ">
<div className="editor-box" id={`editor${editorId}`}></div>
{textLength > limitLength && <div className="editor-tips">超了{textLength - limitLength}个字</div>}
</div>
}
}
export default EditorBox;
.wang-editor-container {
border: 1px solid #E8E8E8;
border-radius: 4px;
width: 552px;
position: relative;
.w-e-text p,
.w-e-text h1,
.w-e-text h2,
.w-e-text h3,
.w-e-text h4,
.w-e-text h5,
.w-e-text table,
.w-e-text pre {
margin: 0;
}
.w-e-toolbar {
background-color: #fff !important;
border: none !important;
border-bottom: 1px solid #E8E8E8 !important;
}
.w-e-text-container {
border: none !important;
height: 88px !important;
}
.editor-tips {
position: absolute;
top: 5px;
right: 8px;
color: #f5222d;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-14 15:41:30
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-23 13:45:16
* @Description: 大班直播、互动班课列表的筛选组件
*/
import React from 'react';
import { withRouter } from 'react-router-dom';
import { Row, Input, Select } from 'antd';
import Bus from '@/core/bus';
import TeacherSearchSelect from "@/modules/common/TeacherSearchSelect";
import RangePicker from "@/modules/common/DateRangePicker";
import './LiveCourseFilter.less';
const { Search } = Input;
const { Option } = Select;
const defaultQuery = {
courseName: null,
startTime: null,
teacherName: null,
courseState: undefined,
}
class LiveCourseFilter extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {...defaultQuery}
}
}
componentWillReceiveProps(nextProps) {
const { match: { path } } = nextProps;
const { match: { path: curPath } } = this.props;
if (path !== curPath) {
this.setState({
query: {...defaultQuery}
})
}
}
// 改变搜索条件
handleChangeQuery = (field, value) => {
this.setState({
query: {
...this.state.query,
[field]: value,
current: 1,
}
}, () => {
if (field === 'courseName') return;
this.props.onChange(this.state.query)
});
}
handleChangeDates = (dates) => {
const { query } = this.state;
if (_.isEmpty(dates)) {
delete query.startTime;
delete query.endTime;
} else {
query.startTime = dates[0].valueOf();
query.endTime = dates[1].valueOf();
}
this.setState({
query,
current: 1,
}, () => {
this.props.onChange(this.state.query);
})
}
// 选择老师
handleSelectTeacher = (teacher) => {
const { name: teacherName, teacherId } = teacher;
this.setState({
query: {
...this.state.query,
teacherId,
teacherName,
current: 1,
}
}, () => {
this.props.onChange(this.state.query);
})
}
// 清空搜索条件
handleReset = () => {
this.setState({
query: {
...this.state.query,
courseName: null,
startTime: null,
endTime: null,
teacherId: null,
teacherName: null,
courseState: undefined,
current: 1,
},
}, () => {
this.props.onChange(this.state.query);
})
}
render() {
const {
courseName, startTime, endTime,
courseState, teacherName, teacherId
} = this.state.query;
const { teacherId: _teahcerId } = {};
const isTeacher = !!_teahcerId; // 判断是否是老师身份
return (
<div className="live-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={courseName}
placeholder="搜索直播名称"
onChange={(e) => { this.handleChangeQuery('courseName', e.target.value)}}
onSearch={ () => { this.props.onChange(this.state.query) } }
style={{ width: "calc(100% - 70px)" }}
/>
</div>
<div className="search-condition__item">
<span className="search-date">上课日期:</span>
<RangePicker
id="course_date_picker"
allowClear={false}
value={ startTime ? [moment(startTime), moment(endTime)] : null }
format={"YYYY-MM-DD"}
onChange={(dates) => { this.handleChangeDates(dates) }}
style={{ width: "calc(100% - 70px)" }}
/>
</div>
{!isTeacher &&
<div className="search-condition__item">
<TeacherSearchSelect
id="teacher_select"
ref="TeacherSelect"
label="上课老师"
placeholder="请选择"
teacherName={teacherName}
onSelect={this.handleSelectTeacher}
defaultValue={teacherId}
/>
</div>
}
<div className="search-condition__item">
<span className="select-status">上课状态:</span>
<Select
style={{ width: "calc(100% - 70px)" }}
placeholder="请选择"
allowClear={true}
value={courseState}
onChange={(value) => { this.handleChangeQuery('courseState', value) }}
>
<Option value="UN_START">待上课</Option>
<Option value="STARTING">上课中</Option>
<Option value="FINISH">已完成</Option>
<Option value="EXPIRED">未成功开课</Option>
</Select>
</div>
</div>
<span
className="icon iconfont"
onClick={this.handleReset}
>
&#xe6a3;
</span>
</Row>
</div>
)
}
}
export default withRouter(LiveCourseFilter);
\ No newline at end of file
.live-course-filter {
position: relative;
.search-condition {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
&__item {
width: 30%;
margin-right: 3%;
margin-bottom: 12px;
}
}
.iconfont {
position: absolute;
right: 12px;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-14 15:43:00
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-11-23 20:23:12
* @Description: 大班直播、互动班课的直播课列表
*/
import React from 'react';
import { Table, Modal, Tooltip, Badge, message, Dropdown, Button } from 'antd';
import Bus from '@/core/bus';
import User from "@/core/user";
import User_t from "@/teacher/core/user";
import { PageControl } from "@/components";
// import { LIVE_SHARE_MAP } from '@/common/constants/academic/cloudClass';
import DownloadLiveModal from '@/components/DownloadLiveModal';
import LiveStudentListModal from '../modal/LiveStudentListModal';
import CheckBalanceModal from '../modal/CheckBalanceModal';
import StartLiveModal from '../modal/StartLiveModal';
import ClassRecordModal from '../modal/ClassRecordModal';
import PlayBackRecordModal from '../modal/PlayBackRecordModal';
import ManageCoursewareModal from '../modal/ManageCoursewareModal';
import ShareLiveModal from '../modal/ShareLiveModal';
import AccountChargeModal from '../modal/AccountChargeModal';
import SelectStudent from '../modal/select-student';
import './LiveCourseList.less';
const { teacherId, instId, adminId, adminName, name } = window.currentUserInstInfo;
const isTeacher = !!teacherId;
const courseStateShow = {
UN_START: {
code: 1,
title: "待上课",
color: "#FDBE31",
},
STARTING: {
code: 2,
title: "上课中",
color: "#238FFF",
},
FINISH: {
code: 3,
title: "已完成",
color: "#2FC83C",
},
EXPIRED: {
code: 4,
title: "未成功开课",
color: "#CCCCCC",
},
};
const ENV = process.env.DEPLOY_ENV || 'dev';
class LiveCourseList extends React.Component {
constructor(props) {
super(props);
this.state = {
url: '',
editData: {},
columns: [],
isXiaoMai: false,
downloadUrl: null,
openCoursewareModal: false,
currentTeacherId: teacherId
}
}
componentWillMount() {
}
componentDidMount() {
}
// 获取直播间类型
handleCheckLiveVersion = () => {
}
// 获取当前登录帐号的teacherId
getTeacherId = () => {
}
getDownloadVersion() {
}
parseColumns = () => {
const menu = (item) => (
<div className="live-course-more-menu">
<div
onClick={() => {
this.handleShowClassModal(item);
}}
>
上课记录
</div>
{item.haveRecord === 'YES' &&
<div
onClick={() => {
this.handleShowRepeatModal(item);
}}
>
回放记录
</div>
}
</div>
);
const columns = [
{
title: "课程名称",
width: "20%",
key: "courseName",
dataIndex: "courseName",
render: (val, record) => {
const { coverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png' } = record;
return (
<div className="record__item">
<img className="course-cover" src={coverUrl} />
<span className="course-name">{val}</span>
</div>
)
}
},
{
title: "上课时间",
width: "10%",
key: "classTime",
dataIndex: "classTime",
render: (val, item) => {
return `${formatDate("YYYY-MM-DD H:i",parseInt(item.startTime))}~${formatDate("H:i", parseInt(item.endTime))}`
},
},
{
title: "上课老师",
width: "8%",
key: "nickname",
dataIndex: "nickname",
},
{
title: (window.NewVersion && isXiaoMai) ? '学员管理' : '学员人数',
width: "9%",
key: "quota",
dataIndex: "quota",
render: (val, item) => {
return (
<span
className="operate-text"
onClick={() => {
this.handleLinkToClassData(item)
}}
>
{`${val}人`}
</span>
);
},
},
{
title: "课件管理",
width: "7%",
dataIndex: "courseware",
render: (val, item, index) => {
return item.channel === "XIAOMAI" ? (
<span
className="operate-text"
onClick={() => {
this.setState({
editData: item,
openCoursewareModal: true,
});
}}
>
{item.courseDocumentCount || 0}
</span>
) : (
<span style={{ color: "#999" }}>暂不支持</span>
);
},
},
{
title: "上课状态",
width: "10%",
key: "courseState",
dataIndex: "courseState",
render: (val, item) => {
const { currentTeacherId } = this.state;
const teacherPermission = item.teacherId === currentTeacherId;
return (
<div className="course-status">
<Badge
className="badge"
color={courseStateShow[val].color}
text={courseStateShow[val].title}
status={val == 'STARTING' ? 'processing' : undefined}
/>
</div>
);
},
},
{
title: "操作",
width: "20%",
key: "operate",
dataIndex: "operate",
render: (val, item) => {
return (
<div className="operate">
<div
key="enter_live_room1"
className="operate__item"
onClick={() => { this.handleCheckPreEnterLiveRoom(item, teacherPermission ? 1 : 2) }}
>进入直播间
</div>
<span className="operate__item split" key="enter_live_room1_split"> | </span>
<div
key="view_play_back"
className="operate__item"
onClick={() => { this.handleStartPlayBack(item); }}
>查看回放</div>,
<span className="operate__item split" key="view_play_back_split"> | </span>
<div
key="share"
className="operate__item"
onClick={() => { this.handleShowShareModal(item); }}
>
分享
</div>,
<span key="split1" className="operate__item split"> | </span>
<div className="big-live">
<Dropdown overlay={this.renderMoreOperate(item, isXiaoMai, otherPermission, isInteractive)}>
<span className="more-operate">
<span className="operate-text">更多</span>
<span
className="iconfont icon"
style={{ color: "#FC9C6B" }}
>
&#xe824;
</span>
</span>
</Dropdown>
</div>
</div>
)
}
}
];
}
renderMoreOperate = (item, isXiaoMai, otherPermission, isInteractive) => {
return (
<div className="live-course-more-menu">
<div
className="operate__item"
onClick={() => this.handleRemindClass(item)}
>群发通知</div>
<div
className="operate__item"
onClick={() => this.handleEditLiveClass(item, isInteractive)}
>编辑</div>
<div
className="operate__item"
onClick={() => this.handleDeleteLiveClass(item.liveCourseId)}
>删除</div>
</div>
)
}
// 显示添加学员的更多操作
renderAddStuOverLay = (item) => {
return (
<div className="live-course-more-menu">
<div
className="operate__item"
onClick={() => this.handleShowSelectStuModal(item, 'DEDUCTION')}
>
添加扣课时的学员
</div>
<div
className="operate__item"
onClick={() => this.handleShowSelectStuModal(item)}
>
添加不扣课时的学员
</div>
</div>
)
}
render() {
const { total, query, courseList, loading, type } = this.props;
const { current, size } = query;
const {
openCoursewareModal, openDownloadModal, editData,
downloadUrl, url, columns,
} = this.state;
return (
<div className="live-course-list">
<Table
bordered
size="middle"
pagination={false}
columns={columns}
loading={loading}
dataSource={courseList}
rowKey={(row) => row.liveCourseId}
/>
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={parseInt(total)}
toPage={(page) => {
const _query = {...query, current: page + 1};
this.props.onChange(_query)
}}
/>
</div>
</div>
)
}
}
export default LiveCourseList;
\ No newline at end of file
.live-course-list {
margin-top: 16px;
.record__item {
display: flex;
align-items: center;
}
.operate {
display: flex;
align-items: center;
flex-wrap: wrap;
.operate__item {
color: #ff7519;
cursor: pointer;
&.split {
margin: 0 8px;
color: #BFBFBF;
}
}
}
.operate-text {
color: #ff7519;
cursor: pointer;
}
.course-cover {
min-width: 90px;
max-width: 90px;
height: 50px;
border-radius: 2px;
margin-right: 8px;
}
.course-status {
display: flex;
.badge {
transform: none !important;
display: flex;
align-items: center;
.ant-badge-status-dot {
top: 0;
}
.ant-badge-status-text {
white-space: nowrap;
}
}
}
.course-start-end {
margin-left: 16px;
width: 78px;
height: 20px;
border-radius: 2px;
border: 1px solid rgba(204, 204, 204, 1);
display: flex;
align-items: center;
cursor: pointer;
white-space: nowrap;
.start-icon {
color: #3296fa;
font-size: 12px;
transform: scale(0.8);
margin: 0 5px;
}
.end-icon {
color: #00d700;
font-size: 12px;
transform: scale(0.8);
margin: 0 5px;
}
.start-end-text {
font-size: 12px;
}
}
}
.live-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;
}
}
}
.tipTitle {
.type {
font-weight: 700;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-14 15:42:24
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-23 14:35:28
* @Description: 大班直播、互动班课的操作区
*/
import React from 'react';
import { Button, Modal, message } from 'antd';
import './liveCourseOpt.less';
class LiveCourseOpt extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="live-course-opt">
<div className="opt__left">
<Button type="primary" onClick={this.handleCreateLiveCouese}>新建直播课</Button>
<Button onClick={this.handleDownloadClient}>下载直播客户端</Button>
</div>
</div>
)
}
}
export default LiveCourseOpt;
\ No newline at end of file
import React, { useEffect, useState } from 'react';
import { Modal, Button } from 'antd';
import qrcode from "@/libs/qrcode/qrcode.js";
import './StudentClassReportModal.less';
const StudentClassReportModal = (props) => {
const url = window.CONFIG.parentHref[window.CONFIG.env];
const previewUrl = `${url}#/live/pc-cloud-class-report?courseId=${props.courseId}&studentId=${props.studentId}`
const mobileUrl = `${url}#/live/cloud-class-report?courseId=${props.courseId}&studentId=${props.studentId}`
useEffect(() => {
window.axios.Sales('public/businessShow/convertShortUrls', {
urls: [mobileUrl]
}).then((res) => {
const { result = [] } = res;
const qrcodeNode = new qrcode({
text: result[0].shortUrl,
size: 106
});
document.querySelector('#qrcode').appendChild(qrcodeNode)
})
}, [])
return (
<Modal
visible={true}
title={props.studentId? "TA的课堂报告":"老师的课堂报告"}
width={560}
onCancel={props.onCancel}
className="student-class-report-modal"
footer={ <div onMouseEnter={(e) => {
e.preventDefault();
}}
>
<Button
key="cancel"
onMouseEnter={(e) => {
e.preventDefault();
let qrcode = document.getElementsByClassName("qrcode")[0];
qrcode.style.display = "block";
}}
onMouseLeave={(e) => {
e.preventDefault();
let qrcode = document.getElementsByClassName("qrcode")[0];
qrcode.style.display = "none";
}}
>
分享TA的课堂报告
</Button>
</div>}
>
<div className="modal-content">
<iframe
src={previewUrl}
style={{ width: '100%', height: '70vh', border: 'none' }}
/>
</div>
<div className="qrcode">
<div className="qrcode-text">微信扫码查看/分享</div>
<div id="qrcode"></div>
<div className="triangle"></div>
</div>
</Modal>
)
}
export default StudentClassReportModal;
\ No newline at end of file
.student-class-report-modal {
.modal-content {
height: 70vh;
margin-left: -3px;
margin-right: -3px;
position: relative;
}
.share-entry {
width: 560px;
height: 48px;
background: #ffffff;
border-radius: 0px 0px 6px 6px;
position: fixed;
left: 0;
bottom: -48px;
.share-btn {
width: 148px;
height: 28px;
background: #ffffff;
border: 1px solid #e8e8e8;
position: absolute;
right: 23px;
top: 10px;
width: 116px;
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 28px;
}
}
.qrcode {
display: none;
position: absolute;
bottom:70px;
right:0;
width: 200px;
height: 191px;
background: #FFFFFF;
padding: 14px 40px 18px 40px;
text-align: center;
border-radius:8px;
box-shadow: 0 0 6px 0 rgba(0,0,0,.2);
.qrcode-text {
width: 126px;
height: 21px;
font-size: 15px;
color: #333333;
line-height: 21px;
margin-bottom: 22px;
}
#qrcode {
width: 106px;
height: 106px;
margin: 0 auto;
}
.triangle {
position: absolute;
width: 10px;
height: 10px;
transform: rotate(45deg);
bottom: -5px;
left: 95px;
background-color: #fff;
box-shadow: 0 0 6px 0 rgba(0,0,0,.2);
}
}
}
.class-report-modal {
.modal-content {
height: 70vh;
margin-left: -3px;
margin-right: -3px;
}
}
import React, { useEffect, useState } from 'react';
import { Modal } from 'antd';
import './TeacherClassReportModal.less';
import TeacherClassReportPage from './TeacherClassReportPage';
interface TeacherClassReportModal {
onCancel: Function,
courseId: string,
}
const TeacherClassReportModal = (props: TeacherClassReportModal) => {
return (
<Modal
visible={true}
// title={props.studentId? "TA的课堂报告":"老师的课堂报告"}
width={560}
footer={null}
// onCancel={props.onCancel}
className="class-report-modal"
>
<div className="modal-content">
<TeacherClassReportPage courseId={props.courseId}/>
</div>
</Modal>
)
}
export default TeacherClassReportModal;
\ No newline at end of file
.teacher-class-report-page {
.class-loading-page {
position: absolute;
z-index: 1;
left: 20px;
top: 60px;
right: 20px;
bottom: 0;
background: #fff;
#lottie-box {
width: 88px;
height: 106px;
margin: 0 auto;
}
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
.app-logo {
display: block;
margin: 0 auto;
width: 240px;
height: 240px;
}
.h5-logo {
display: block;
width: 220px;
height: 220px;
}
.box-tip {
margin-top: 16px;
display: block;
color: #666;
font-size: 26px;
.tip-button {
font-size: 26px;
color: #ffb100;
margin: 0 8px;
}
}
}
}
width: 512px;
text-align: center;
background-color: #fff;
.iconfont {
color: rgba(191, 191, 191, 1);
}
.teacher-title {
width: 512px;
background: #fff0e7;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 44px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: flex;
justify-content: center;
.teacher-name {
font-size: 16px;
font-weight: 400;
color: #ff7519;
line-height: 44px;
max-width: 192px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.class-detail {
margin-top: 20px;
margin-bottom: 21px;
.class-title {
width: 480px;
margin: 0 auto;
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #242b33;
line-height: 22px;
text-align: center;
}
.class-duration {
text-align: center;
height: 22px;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #8c8e93;
line-height: 22px;
margin-top: 8px;
}
}
.sign-in-detail {
.title {
height: 24px;
font-size: 17px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 24px;
text-align: center;
position: relative;
margin-bottom: 29px;
&::before {
content: '';
width: 24px;
height: 4px;
background: #fc9c6b;
border-radius: 8px;
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
}
}
.content {
.line-height {
color: rgba(255, 117, 25, 1);
}
.rate-block {
display: flex;
justify-content: space-between;
flex-flow: wrap;
.item {
width: 50%;
margin-bottom: 20px;
.rate {
height: 33px;
font-size: 24px;
font-weight: 500;
color: #333333;
line-height: 33px;
.tiny {
font-size: 18px;
}
}
.explaination {
text-align: center;
height: 20px;
font-size: 14px;
font-weight: 400;
color: #999999;
line-height: 20px;
}
}
}
.progress-block {
.item {
.tip {
display: flex;
justify-content: space-between;
.text {
height: 22px;
font-size: 14px;
color: rgba(140, 142, 147, 1);
line-height: 22px;
.tip-title {
color: rgba(51, 51, 51, 1);
font-weight: 500;
}
}
.num {
color: #333333;
line-height: 22px;
.left-num {
color: rgba(51, 51, 51, 1);
font-weight: 500;
font-size: 24px;
}
}
}
.progress {
position: relative;
width: 512px;
.up-progress {
position: absolute;
z-index: 10;
width: 80%;
height: 10px;
border-radius: 5px;
background-color: rgba(252, 156, 107, 1);
}
.down-progress {
width: 512px;
height: 10px;
background: #f6f7f8;
border-radius: 5px;
}
}
}
.total-class {
width: 512px;
height: 44px;
background: #f6f7f8;
border-radius: 2px;
display: flex;
justify-content: space-between;
margin-top: 10px;
margin-bottom: 20px;
line-height: 44px;
padding-left: 10px;
padding-right: 10px;
.text {
}
.num {
.left-num {
color: rgba(51, 51, 51, 1);
font-weight: 500;
font-size: 16px;
}
}
}
}
}
}
.class-data-detail {
margin-top: 30px;
.title {
height: 24px;
font-size: 17px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 24px;
text-align: center;
position: relative;
margin-bottom: 11px;
&::before {
content: '';
width: 24px;
height: 4px;
background: #fc9c6b;
border-radius: 8px;
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
}
}
.content {
.data-block {
.item {
width: 512px;
padding: 26px 20px;
display: flex;
justify-content: flex-start;
text-align: left;
border-bottom: 1px dashed rgba(232, 232, 232, 1);
&:last-child {
border-bottom: none;
}
&.column-center {
align-items: center;
}
img {
display: inline-block;
width: 40px;
height: 42px;
margin-right: 16px;
}
.large-line-height {
color: rgba(255, 117, 25, 1);
font-size: 24px;
line-height: 22px;
}
.line-height {
color: rgba(255, 117, 25, 1);
font-size: 14px;
line-height: 22px;
}
}
}
}
}
}
.class-high-light {
color:rgba(255, 117, 25, 1);
margin-left: 4px;
margin-right: 4px;
}
.live-course-opt {
display: flex;
align-items: center;
position: relative;
margin-top: 4px;
.opt__right {
margin-left: 12px;
color: #FF8534;
}
.ant-btn {
margin-right: 12px;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-05-19 11:01:31
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-05-25 16:50:47
* @Description 余额异常弹窗
*/
import React from 'react';
import { Modal } from 'antd';
import AccountChargeModal from './AccountChargeModal';
import './AbnormalModal.less';
class AbnormalModal extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
handleRecharge = () => {
const chargeModal = (
<AccountChargeModal
close={() => {
this.setState({
chargeModal: null,
});
this.props.onClose()
}}
refresh={() => { window.location.reload(); }}
/>
);
this.setState({ chargeModal });
};
render() {
const {
title, onClose, balance = 0, outDays, hasOwedFee
} = this.props;
const content = hasOwedFee ? (
<div className="content">
你的云课堂账户余额已持续欠费 <span className="high-light">{outDays}</span> 天,请及时充值。持续欠费 <span className="high-light">90</span> 天,系统将自动清除系统数据与回放视频。
</div>
) : (
<div
className="content"> 你的小麦云课堂余额不足(<span className="high-light">{balance.toFixed(2)}</span>元),为保障您直播间的正常使用,请您及时充值。余额耗尽后将无法进行直播,学员无法观看回放视频。
</div>
);
return (
<div className="abnormal-modal-wrapper">
<Modal
title={title}
visible={true}
footer={null}
onCancel={onClose}
className="abnormal-modal"
>
{ content }
<div
className="charge-btn"
onClick={() => { this.handleRecharge() }}
>去充值</div>
</Modal>
{ this.state.chargeModal }
</div>
)
}
}
export default AbnormalModal;
\ No newline at end of file
.abnormal-modal {
.high-light {
color: #EC4B35
}
.charge-btn {
width: 136px;
height: 40px;
line-height: 40px;
margin: auto;
margin-top: 40px;
text-align: center;
font-size: 16px;
color: #FFF;
background-color: #FC9C6B;
cursor: pointer;
}
}
\ No newline at end of file
.account-charge-modal {
.pay-item {
border: 1px solid #e8e8e8;
padding: 10px 12px;
min-width: 112px;
display: flex;
justify-items: flex-start;
align-items: center;
font-size: 14px;
font-weight: 400;
color: rgba(51, 51, 51, 1);
line-height: 20px;
border-radius: 4px;
cursor: pointer;
.icon-weixin {
color: #42ae3c;
margin-right: 4px;
}
.icon-ali {
color: #58b7ef;
margin-right: 4px;
}
.pay-radio-inner {
display: inline-block;
width: 16px;
height: 16px;
border: 1px solid #eeeeee;
border-radius: 100%;
position: relative;
margin-right: 8px;
}
&.ali-pay {
margin-left: 8px;
}
&.active {
.pay-radio-inner {
border-color: #fc9c6b;
&::after {
content: " ";
display: inline-block;
width: 10px;
height: 10px;
background-color: #fc9c6b;
border-radius: 100%;
position: absolute;
top: 3px;
left: 3px;
}
}
}
}
.charge-tips {
margin-top: 10px;
font-size: 14px;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
text-align: center;
span {
color: #fc9c6b;
cursor: pointer;
}
}
.pay-success {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.pay-success-name {
color: #333333;
}
.pay-success-tip {
color: #666666;
margin-bottom: 40px;
margin-top: 8px;
}
}
.pay-wrapper {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.money {
font-size: 24px;
font-weight: 500;
color: rgba(255, 133, 52, 1);
line-height: 33px;
margin-top: 8px;
margin-bottom: 16px;
}
}
}
.charge-explain-modal {
.explain-title {
font-size: 14px;
margin-bottom: 8px;
font-weight: 500;
color: rgba(51, 51, 51, 1);
line-height: 20px;
}
.other-explain {
font-size: 14px;
border-top: 1px solid #e8e8e8;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
padding-top: 15px;
margin-top: 16px;
}
.main-explain-text {
font-size: 14px;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
margin-left: 21px;
}
.main-explain-block {
width: 816px;
height: 32px;
line-height: 32px;
background: rgba(247, 248, 249, 1);
border-radius: 4px;
padding-left: 16px;
margin-left: 21px;
margin-bottom: 8px;
}
ul {
margin-bottom: 8px;
margin-top: 8px;
}
ul > li {
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
margin-bottom: 0;
margin-left: 21px;
display: flex;
justify-self: flex-start;
align-items: center;
.spot {
width: 4px;
height: 4px;
border-radius: 100%;
background-color: #666666;
margin-right: 10px;
}
}
table {
border: 1px solid #e8e8e8;
margin-left: 33px;
margin-bottom: 16px;
tr > th,
tr > td {
height: 40px;
padding-left: 24px;
border-right: 1px solid #e8e8e8;
border-bottom: 1px solid #e8e8e8;
}
}
}
.charging-detail-modal {
.detail-title {
font-size: 16px;
font-weight: 500;
color: rgba(51, 51, 51, 1);
line-height: 22px;
margin-bottom: 18px;
}
.xm-page-control {
padding-bottom: 0;
margin-bottom: 0;
}
}
.charge-agreement-modal{
.agreement-title {
text-align: center;
font-size: 24px;
line-height: 30px;
color: #333333;
font-weight: bold;
margin-bottom: 30px;
}
.agreement-sub-title {
margin-top: 16px;
margin-bottom: 8px;
font-size: 16px;
line-height: 22px;
color: #333333;
font-weight: bold;
}
.agreement-main {
font-size: 15px;
line-height: 21px;
color: #666;
margin-top: 8px;
text-indent: 16px;
}
.agreement-main-text {
font-size: 15px;
line-height: 21px;
color: #666;
text-indent: 30px;
margin-top: 4px;
}
.agreement-tip {
margin-left: 24px;
}
li {
margin-bottom: 4px;
}
.border-all {
padding: 16px 0;
border-top: 1px solid #eeeeee;
border-bottom: 1px solid #eeeeee;
margin-top: 16px;
margin-bottom: 16px;
}
}
\ No newline at end of file
import React from 'react';
import { Modal, Table } from "antd";
import ChargeArgeement from "./ChargeArgeement";
import "./AccountChargeModal.less";
class AccountChargeRecords extends React.Component{
constructor(props) {
super(props);
this.state = {
list: [],
};
}
componentDidMount() {
this.getList();
}
getList = () => {
const { instId } = window.currentUserInstInfo;
axios
.Business("public/liveAssets/rechargeProtocol", { instId })
.then((res) => {
const list = res.result;
this.setState({
list,
});
});
};
handleProtcol = (id) => {
const agreement = (
<ChargeArgeement
id={id}
close={() => {
this.setState({
agreement: null,
});
}}
/>
);
this.setState({
agreement,
});
};
render() {
const columns = [
{
title: "签订人",
dataIndex: "operatorName",
width: 140
},
{ title: "关联订单ID", dataIndex: "orderId" },
{
title: "签订时间",
dataIndex: "createTime",
render: (text, record) => {
return <span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>;
},
},
{
title: "签订协议",
dataIndex: "operate",
render: (text, record) => {
return (
<div
style={{ cursor: "pointer", color: "#FC9C6B" }}
onClick={() => {
this.handleProtcol(record.protocolId);
}}
>
《服务协议》
</div>
);
},
},
];
const { list } = this.state;
return (
<Modal
title="服务协议签订记录"
visible={true}
width={680}
footer={null}
onCancel={() => {
this.props.close();
}}
>
<div>
<div
style={{
fontSize: "14px",
color: "#666666",
lineHeight: "20px",
marginBottom: 16,
}}
>
以下是本校区自助充值时签订协议的记录
</div>
<Table
size="middle"
columns={columns}
dataSource={list}
pagination={false}
bordered
/>
</div>
{this.state.agreement}
</Modal>
);
}
}
export default AccountChargeRecords;
/*
* @Description: 计费说明
* @Author: zhangyi
* @Date: 2020-05-09 15:21:22
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 15:14:28
*/
import { Modal, Button, Table } from "antd";
import "./AccountChargeModal";
const data = [
{ person: "上台人数1v1", price: "3元/人/小时" },
{ person: "上台人数1v2", price: "3元/人/小时" },
{ person: "上台人数1v3", price: "3元/人/小时" },
{ person: "上台人数1v4", price: "3元/人/小时" },
{ person: "上台人数1v5", price: "3元/人/小时" },
{ person: "上台人数1v6", price: "3元/人/小时" },
{ person: "上台人数1v7", price: "4元/人/小时" },
{ person: "上台人数1v8", price: "4元/人/小时" },
{ person: "上台人数1v9", price: "8元/人/小时" },
{ person: "上台人数1v10", price: "8元/人/小时" },
{ person: "上台人数1v11", price: "8元/人/小时" },
{ person: "上台人数1v12", price: "8元/人/小时" },
];
function ChargeExplainModal(props) {
return (
<Modal
title="计费说明"
visible={true}
className="charge-explain-modal"
width={880}
onCancel={() => {
props.close();
}}
footer={[
<Button
type="primary"
onClick={() => {
props.close();
}}
>
关闭
</Button>,
]}
>
<div>
<div className="explain-title">1)直播课时费</div>
<p className="main-explain-block">
每节课上课费用 = 上台人数单价 × 有效出勤学生和老师人数 × 排课时长
</p>
<ul>
<li>
1. 上课老师、学生和助教在教室中的累计时长满10分钟即为有效出勤;
</li>
<li>
2.
排课时长指创建课节设置的课节时长,最小计费单位为0.5小时,不足0.5小时按0.5小时计算;
</li>
<li>
3.
上台人数单价:以排课时设定的上台人数上限为准,不同直播教室类型的课时单价不同。
</li>
</ul>
<p className="main-explain-text" style={{marginTop: 16,marginBottom: 8}}>
温馨提醒:上台人数1vN,1为授课老师,N为同时与老师视频互动学生数;直播课时费将在老师下课后立即结算。
</p>
<table style={{ width: 333 }}>
<thead>
<tr>
<th>上台人数</th>
<th>价格标准</th>
</tr>
</thead>
<tbody>
{data.map((item) => {
return (
<tr>
<td>{item.person}</td>
<td>{item.price}</td>
</tr>
);
})}
</tbody>
</table>
<p className="main-explain-text">
示例:王老师排了一节45分钟的课程,选择的直播教室类型为5人上台,安排了1位助教、12位学生。那么,排课时长45分钟记为1小时;全员累计在线时长≥10min,老师+助教+学生共计1+1+12=14人;课时单价3元/人/小时。所以,此节课的直播课时费=1x14x3=42元
</p>
<div className="explain-title mt16">2)录制费</div>
<p className="main-explain-block">录制费 = 录课单价 × 回放视频时长</p>
<ul>
<li>1. 结算时间:从回放视频生成后立即结算</li>
<li> 2. 回放视频时长:0.5h起收,不足0.5h的按0.5h结算</li>
<li> 3. 单价:2元/小时</li>
</ul>
<p className="main-explain-text">
示例:生成了49分26秒的回放视频,不足1h按1h计算,总费用=1(h)*2元/h=2元
</p>
<div className="explain-title mt16">3)流量费</div>
<p className="main-explain-block">
观看回放视频流量费 = 流量单价 × 回放流量
</p>
<p className="main-explain-text">
目前收费报价:0元(限时免费)
<br /> 若后续变更收费,将提前30日通告收费方式及价格标准
</p>
<div className="explain-title mt16">4)存储费</div>
<p className="main-explain-block">存储实际扣费 = 存储单价 × 存储文件大小</p>
<p className="main-explain-text">
目前收费报价:0元(限时免费)
<br />
若后续变更收费,将提前30日通告收费方式及价格标准
</p>
<div className="explain-title mt16">5)其他说明</div>
<div className="main-explain-text">
余额不足时将限制使用创建直播课、老师和学生进入直播间、老师和学生观看回放视频、在资料云盘或直播间上传文件等功能,请注意及时充值。(余额<300元时将发送短信提醒,请注意查看。)
</div>
</div>
</Modal>
);
}
export default ChargeExplainModal;
import React from 'react';
import { Modal, Table, Tooltip } from "antd";
import { ShowTips, PageControl } from "@/components";
import "./AccountChargeModal.less";
class ChargingDetailModal extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 10,
liveCourseId: props.liveCourseId,
},
totalCount: 0,
list: [],
teacherList: [],
};
}
componentDidMount() {
// 发请求
this.handleToPage();
this.getTeacherData();
}
handleToPage = (page = 1) => {
const params = _.clone(this.state.query);
params.current = page;
axios
.Apollo("public/businessLive/queryStudentVisitData", params)
.then((res) => {
if (res.result) {
const { records = [], total } = res.result;
this.setState({
list: records,
totalCount: total,
query: params,
});
}
});
};
getTeacherData = () => {
window.axios
.Apollo("public/businessLive/queryTeacherVisitData", {
liveCourseId: this.props.liveCourseId,
})
.then((res) => {
if (res.result) {
const teacherList = [res.result];
this.setState({
teacherList,
});
}
});
};
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;
};
getColumns = (type) => {
const columns = [
{
title: type == "student" ? "学生姓名" : "老师姓名",
dataIndex: "userName",
},
{
title: "手机号",
dataIndex: "phone",
render: (text, record) => {
return <p>{text}</p>;
},
},
{
title: "累计在线时长",
dataIndex: "totalDuration",
render: (text, record) => {
return <span>{text ? this.dealTimeDuration(text) : '-'}</span>;
},
},
{
title: (
<span>
是否计费&nbsp;
<Tooltip title="仅对累计在线时长≥10分钟的老师或学员计费">
<span className="icon iconfont">&#xe6f2;</span>
</Tooltip>
</span>
),
dataIndex: "type",
render: (text, record) => {
return <span>{record.totalDuration > 600 ? "计费" : "不计费"}</span>; //大于十分钟的计费
},
},
];
return columns;
};
render() {
const { list, query, totalCount, teacherList } = this.state;
return (
<Modal
title="计费人数详情"
visible={true}
width={680}
className="charging-detail-modal"
footer={null}
onCancel={() => {
this.props.close();
}}
>
<div>
<div style={{ marginBottom: 16 }}>
<div className="detail-title">老师详情</div>
<Table
size="middle"
columns={this.getColumns("teacher")}
dataSource={teacherList}
pagination={false}
bordered
/>
</div>
<div className="detail-title">学生详情</div>
<Table
size="middle"
columns={this.getColumns("student")}
dataSource={list}
pagination={false}
bordered
/>
<PageControl
size="small"
current={query.current - 1}
pageSize={query.size}
total={totalCount}
toPage={(page) => {
this.handleToPage(page + 1);
}}
/>
</div>
</Modal>
);
}
}
export default ChargingDetailModal;
/*
* @Description: 直播开始上课之前对余额的校验
* @Author: zhangyi
* @Date: 2020-05-18 13:47:42
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 16:36:33
*/
import React from 'react';
import { Modal, Button } from "antd";
import AccountChargeModal from "./AccountChargeModal";
const textStyle = {
lineHeight: "20px",
fontSize: "14px",
color: "#666666",
};
class CheckBalanceModal extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}
handleReCharge = () => {
const chargeModal = (
<AccountChargeModal
close={() => {
this.setState({
chargeModal: null,
});
}}
refresh={() => {
this.props.close();
}}
/>
);
this.setState({
chargeModal,
});
};
render() {
const { balance, currentClass, undoClass } = this.props;
return (
<Modal
visible={true}
footer={null}
width={550}
title="余额不足"
onCancel={() => {
this.props.close();
}}
>
<div>
<div style={textStyle} className="mb8">
本次直播您将扣除{currentClass.toFixed(2)}元费用,预计当前账户余额不足抵扣本次课次及其他结算课次费用,请您尽快充值。
</div>
<div style={textStyle}>
当前余额:
<span style={{ color: "#EC4B35" }}>{balance.toFixed(2)}</span>
</div>
<div style={textStyle}>
本课次预估消费:<span>{currentClass.toFixed(2)}</span>
</div>
<div style={textStyle}>
其他未结算课次预估消费:<span>{undoClass.toFixed(2)}</span>
</div>
<div style={{ textAlign: "center", marginTop: 25 }}>
<Button
style={{
width:136,
height:'40px',
lineHeight: '40px',
borderRadius:'4px'
}}
type="primary"
onClick={() => {
this.handleReCharge();
}}
>
立即充值
</Button>
</div>
</div>
{this.state.chargeModal}
</Modal>
);
}
}
export default CheckBalanceModal;
.play-back-modal {
.table-no-scrollbar {
margin-top: 16px;
}
.xm-page-control {
padding-bottom: 0;
margin-bottom: 0;
}
}
.class-record-modal {
.xm-page-control {
padding-bottom: 0;
margin-bottom: 0;
}
.student-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 13px;
margin-top: 25px;
&__search {
margin-right: 12px;
}
}
.class-record-title {
font-size: 16px;
font-weight: 500;
color: rgba(51, 51, 51, 1);
line-height: 22px;
}
.expanded-table {
* {
border: none;
}
}
.live-table--empty {
padding: 20px;
text-align: center;
margin-left: -50px;
color: #999999;
}
.iconfont {
cursor: pointer;
color: #bfbfbf;
}
.bulge {
color: #ff7519;
}
}
import React from 'react';
import { Modal } from 'antd';
import './LackConsumeStudentModal.less';
class LackConsumeStudentModal extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
render() {
const {onOk, onClose, consumeHourNum, calendarTime, lackConsumeStudentList} = this.props;
return (
<Modal
title="学员剩余课时数不足"
visible={true}
okText="继续保存"
onOk={onOk}
onCancel={onClose}
className="lack-consume-student-modal"
>
<p className="desc">每位扣课时学员预计消耗<span className="sign">{(consumeHourNum * calendarTime.length).toFixed(1)}</span>课时(此次排课共计<span className="sign">{calendarTime.length}</span>节课,每节课消耗<span className="sign">{consumeHourNum}</span>课时)</p>
<div className="list-wrap">
<p>以下学员的剩余课时数不足,请提醒学员及时续费</p>
<div className="list">
{lackConsumeStudentList.map(item => {
return <p>{item.name} {item.phone} 剩余{item.consumeHourNum}课时</p>
})}
</div>
</div>
</Modal>
)
}
}
export default LackConsumeStudentModal;
.lack-consume-student-modal {
.desc {
margin-bottom: 12px;
line-height: 22px;
color: #333;
.sign {
color: #ff7519;
}
}
.list-wrap {
padding: 8px;
line-height: 22px;
background: #F3F6FA;
color: #666;
.list {
margin-top: 8px;
max-height: 108px;
overflow: auto;
p {
color: #333;
}
}
}
}
/*
* @Author: Michael
* @Date: 2020-01-29 11:27:34
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-22 16:25:30
* 查看学员名单
*/
import React from "react";
import {
Modal,
Button,
Input,
Table,
Popconfirm,
message,
Tooltip,
} from "antd";
import PropTypes from "prop-types";
import { PageControl } from "@/components";
import SelectStudent from "./select-student/index";
import "./LiveStudentListModal.less";
const { Search } = Input;
const isTeacher = !!window.currentUserInstInfo.teacherId;
const liveTypeMap = {
LIVE: "直播",
PLAYBACK: "回放",
};
const expandedColumns = [
{
title: "类型",
dataIndex: "liveType",
key: "liveType",
render: (text) => <span>{liveTypeMap[text]}</span>,
},
{
title: "进入时间",
dataIndex: "entryTime",
key: "entryTime",
render: (text) => (
<span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>
),
},
{
title: "离开时间",
dataIndex: "leaveTime",
key: "leaveTime",
render: (text) => (
<span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>
),
},
{ title: "观看时长", dataIndex: "lookingTime", key: "lookingTime" },
];
const STATUS_ENUM = {
'NORMAL': '在读',
'POTENTIAL': '潜在',
'HISTORY': '历史',
'ABANDON': '废弃',
};
class LiveStudentListModal extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 10,
nameOrPhone: null,
liveCourseId: props.liveItem.liveCourseId,
},
total: 1,
studentList: [],
studentModal: null,
after: true,
};
}
componentDidMount() {
this.fetchStudentList();
}
fetchStudentList = (current = 1) => {
const query = _.clone(this.state.query);
query.current = current;
window.axios
.Apollo("public/businessLive/getStudentList", query)
.then((res) => {
const { records = [], total } = res.result;
this.setState({
studentList: records,
total,
query,
});
});
};
hanldSelect = () => {
const { query: { liveCourseId } } = this.state;
axios.Apollo('public/businessLive/getCourseDetail', { liveCourseId})
.then((res) => {
const { result = {} } = res;
const { consumeStudentIds, studentIds } = result;
const studentList = [];
const excludeStudentIds = studentIds;
const excludeConsumeStudentIds = _.pluck(consumeStudentIds, 'studentId');
this.setState({ excludeStudentIds, excludeConsumeStudentIds});
_.each(studentIds, (item) => {
studentList.push({ studentId: item });
});
const studentModal = (
<SelectStudent
liveCourseId={liveCourseId}
studentList={studentList}
excludeStudentIds={excludeStudentIds}
after={true}
close={() => { this.setState({ studentModal: null }); }}
onSelect={(studentIds) => {
this.handleSelectStudent(studentIds)
}}
/>
)
this.setState({ studentModal });
})
};
handleSelectStudent = (studentIds) => {
const {
liveType,
liveCourseId,
podium,
quota,
} = this.props.liveItem;
if (liveType !== "SMALL_CLASS_INTERACTION" && (studentIds.length) > 1000) {
message.info(`最多选择1000人`);
return;
} else if (liveType == "SMALL_CLASS_INTERACTION" && (studentIds.length) > podium) {
message.info(`最多选择${podium}人`);
return;
} else {
const param = {
liveCourseId: liveCourseId,
studentIds: studentIds
};
axios.Apollo("public/businessLive/addCourseStu", param).then(res => {
if (res.success) {
this.setState({
studentModal: null
});
message.success("学员变更成功");
this.fetchStudentList();
this.props.refresh();
}
});
}
};
// 移除学员
removeStudent = (studentId) => {
const { liveCourseId } = this.props.liveItem;
const param = {
liveCourseId,
studentId,
};
window.axios
.Apollo("public/businessLive/moveCourseStu", param)
.then((res) => {
message.success("移除学员成功");
this.fetchStudentList(1);
this.props.refresh();
});
};
parseColumns = () => {
const { type, liveItem } = this.props;
const columns = [
{ title: "姓名", dataIndex: "studentName", key: "studentName" },
{
title: "手机号",
dataIndex: "phone",
width: 150,
key: "phone",
render: (text, record) => {
return (
<p>
{!(
(!window.NewVersion && !window.currentUserInstInfo.teacherId) ||
(window.NewVersion && Permission.hasEduStudentPhone())
)
? (text || "").replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3")
: text}
<Tooltip
title={`${record.wechatStatus ? "已绑定微信" : "未绑定微信"}`}
>
<span
className="icon iconfont"
style={
record.wechatStatus
? {
color: "#00D20D",
fontSize: "16px",
marginLeft: 6,
}
: {
color: "#BFBFBF",
fontSize: "16px",
marginLeft: 6,
}
}
>
&#xe68d;
</span>
</Tooltip>
</p>
);
},
}
];
// 非互动班课类型增加学员类型
if (type !== 'interactive') {
columns.push({
title: '学员类型',
key: 'statusEnum',
dataIndex: 'statusEnum',
render: (val) => {
return STATUS_ENUM[val];
}
});
}
// 如果是非视频课, 显示操作的条件是课程未开始,且不是T端
// 如果是视频课,那么只要满足不是T端就可以了
if ((liveItem.courseState === "UN_START" || type === 'videoCourse') && !isTeacher) {
// 未开始
columns.push({
title: "操作",
dataIndex: "operate",
key: "operate",
align:'right',
render: (text, record) => {
return (
<Popconfirm
title="你确定要移出这个学员吗?"
onConfirm={() => {
// 如果是非视频课,且直播间类型是自研, 且晚于开课前30分钟, 不允许移出
if (
liveItem.channel == "XIAOMAI" &&
liveItem.startTime - Date.now() < 1800000
) {
Modal.warning({
title: "不可移出",
icon: (
<span className="icon iconfont default-confirm-icon">
&#xe6f4;
</span>
),
content: "晚于开课前30分钟,不能移出学员",
});
} else {
this.removeStudent(record.studentId);
}
}}
>
<span className="live-operate">移出</span>
</Popconfirm>
);
},
});
}
return columns;
}
render() {
const { studentList, query, total } = this.state;
const { current, size } = query;
return (
<Modal
title="查看学员名单"
visible={true}
width={680}
footer={null}
className="live-student-list-modal"
onCancel={this.props.close}
>
{/* 任意状态都可以添加学员 */}
<div className="live-student-list-modal__operate">
{
!isTeacher &&
<Button type="primary" onClick={this.hanldSelect}>
添加上课学员
</Button>
}
<Search
placeholder="搜索学员姓名/手机号"
style={{ width: 200 }}
onSearch={(value) => {
this.setState({
query: {
...this.state.query,
nameOrPhone: value
}
}, () => {
this.fetchStudentList(1);
});
}}
className="search"
/>
</div>
<Table
size="small"
columns={this.parseColumns()}
dataSource={studentList}
pagination={false}
scroll={{ y: 400 }}
className="live-student-table table-no-scrollbar"
/>
<PageControl
size="small"
current={current - 1}
pageSize={size}
total={Number(total)}
toPage={(page) => {
this.fetchStudentList(page + 1);
}}
/>
{this.state.studentModal}
</Modal>
);
}
}
LiveStudentListModal.propTypes = {
liveItem: PropTypes.object,
status: PropTypes.string,
close: PropTypes.func,
refresh: PropTypes.func, // 刷新列表
};
export default LiveStudentListModal;
.live-student-list-modal {
.student-list-tip {
margin: 0 0 12px;
width: 100%;
height: 32px;
line-height: 32px;
background: #fff0e7;
padding: 0 16px;
.icon {
color: #ff8534;
margin-right: 8px;
}
}
&__operate {
margin-bottom: 12px;
position: relative;
height: 28px;
.export-btn {
margin-left: 12px;
}
.search {
position: absolute;
right: 0;
}
}
&--orange {
color: #FF7519;
}
&--normal{
color: #666666;
}
.live-operate {
color: #FF7519;
cursor: pointer;
}
.expanded-table {
* {
border: none;
}
}
.live-table--empty {
padding: 20px;
text-align: center;
margin-left: -50px;
color: #999999;
}
.live-student-table {
.ant-table-hide-scrollbar {
min-width: 0 !important;
}
::-webkit-scrollbar {
width: 0;
}
.ant-table tbody tr:nth-child(even) {
background: transparent !important;
}
.ant-table-bordered .ant-table-body {
border: none !important;
}
}
}
\ No newline at end of file
.manage-courseware-modal {
.empty-body {
padding: 16px 0;
.empty-tip {
text-align: center;
color: #333;
}
.empty-image {
display: block;
margin: 24px auto 12px;
}
.empty-button {
display: block;
margin: 0 auto 8px;
}
.empty-text {
color: #999;
text-align: center;
}
}
.manage-body {
.header {
margin-bottom: 16px;
display: flex;
align-items: center;
.header-button {
margin-right: 8px;
}
.header-tip {
color: #666;
}
}
.ant-table-bordered .ant-table-thead tr th:last-child {
border-right: none !important;
}
.ant-table-bordered .ant-table-tbody tr td:last-child {
border-right: none !important;
}
td {
color: #666;
}
.courseware-name {
display: flex;
align-items: center;
cursor: pointer;
.item-img {
width: 24px;
height: 24px;
object-fit: cover;
border-radius: 2px;
margin-right: 8px;
}
.name {
max-width: 154px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
.name {
color: #FF7519;
}
}
}
}
}
\ No newline at end of file
/*
* @Description: 回放记录
* @Author: zhangyi
* @Date: 2020-05-12 09:43:48
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 16:23:05
*/
import React, { useState, useEffect } from "react";
import { Modal, Table, Button, message } from "antd";
import Bus from '@/core/bus';
import { PageControl } from "@/components";
import hasExportPermission from '../utils/hasExportPermission';
import dealTimeDuration from '../utils/dealTimeDuration';
import "./ClassRecordModal.less";
const liveTypeMap = {
USER: "学生",
ANCHOR: "老师",
ADMIN: "助教",
};
class PlayBackRecordModal extends React.Component {
constructor(props) {
super(props);
this.state = {
playBackList: [],
query: {
current: 1,
size: 10,
liveCourseId: props.liveItem.liveCourseId,
},
total: 0,
recordDuration: null,
totalWatchNum: 0,
};
}
componentDidMount() {
this.fetchPlayBackList();
}
fetchPlayBackList = (page = 1) => {
const params = _.clone(this.state.query);
params.current = page;
window.axios
.Apollo("public/businessLive/queryUserReplayRecordPage", params)
.then((res) => {
const { records = [], total } = res.result;
this.setState({
query: params,
total,
playBackList: records,
});
});
};
fetchAllStatistics = () => {
const { liveCourseId } = this.props.liveItem;
window.axios
.Apollo("public/businessLive/queryReplayStatistics", {
liveCourseId,
})
.then((res) => {
if (res.result) {
const { recordDuration = 0, totalWatchNum = 0 } = res.result;
this.setState({
recordDuration,
totalWatchNum,
});
}
});
};
getLastTime = (time = 0) => {
const diff = Math.floor(time % 3600);
const hours = Math.floor(time / 3600);
const mins = Math.floor(diff / 60);
const seconds = Math.floor(time % 60);
return hours + "小时" + mins + "分";
};
// 导出
handleExport = () => {
const { liveItem, type } = this.props;
const { liveCourseId } = liveItem;
const url = !type ? 'api-b/b/lesson/exportLargeClassLiveAsync' : 'api-b/b/lesson/exportClassInteractionLiveSync';
window.axios.post(url, {
liveCourseId,
exportLiveType: 0
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“导出中心”进行下载',
okText: '我知道了',
});
});
}
handleExportV5 = () => {
const { liveItem, type } = this.props;
const { liveCourseId } = liveItem;
const url = !type ? 'public/businessLive/exportLargeClassLiveAsync' : 'public/businessLive/exportClassInteractionLiveSync';
window.axios.Apollo(url, {
liveCourseId,
exportLiveType: 'PLAY_BACK'
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“任务中心”进行下载',
okText: '我知道了',
});
});
}
render() {
const columns = [
{
title: "观看者姓名",
dataIndex: "userName",
},
{
title: "观看者手机号",
dataIndex: "phone",
render: (text, record) => {
return (
<p>
{!(
(!window.NewVersion && !window.currentUserInstInfo.teacherId) ||
(window.NewVersion && Permission.hasEduStudentPhone())
)
? (text || "").replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3")
: text}
</p>
);
},
},
{
title: "观看者类型",
dataIndex: "liveRole",
key: "liveRole",
render: (text) => <span>{liveTypeMap[text]}</span>,
},
{
title: "开始观看时间",
dataIndex: "entryTime",
key: "entryTime",
render: (text) => (
<span>{text ? formatDate("YYYY-MM-DD H:i", parseInt(text)) : '-'}</span>
),
},
{
title: "观看时长",
dataIndex: "lookingDuration",
key: "lookingDuration",
render: (text) => {
return <span>{text ? dealTimeDuration(text) : '-'}</span>;
},
},
];
const {
query,
total,
playBackList,
totalWatchNum,
recordDuration,
} = this.state;
const { type } = this.props;
return (
<Modal
title="回放记录"
className="play-back-modal"
width={680}
visible={true}
footer={null}
onCancel={() => {
this.props.close();
}}
>
{
hasExportPermission(type) &&
<Button onClick={_.debounce(() => {
if (!playBackList.length) {
message.warning('暂无数据可导出');
return;
}
if (window.NewVersion) {
this.handleExportV5();
} else {
this.handleExport();
}
}, 500, true)}>导出</Button>
}
<Table
size="small"
columns={columns}
dataSource={playBackList}
pagination={false}
className="table-no-scrollbar"
/>
<PageControl
size="small"
current={query.current - 1}
pageSize={query.size}
total={total}
toPage={(page) => {
this.fetchPlayBackList(page + 1);
}}
/>
</Modal>
);
}
}
export default PlayBackRecordModal;
/*
* @Author: 吴文洁
* @Date: 2020-07-23 14:54:16
* @LastEditors: 吴文洁
* @LastEditTime: 2020-08-28 10:49:49
* @Description: 大班直播课预览弹窗
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Modal } from 'antd';
import './PreviewCourseModal.less';
class PreviewCourseModal extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
dealWithTime = (startTime, 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 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 liveDateStr = `${year}-${month}-${day}`;
const startTimeStr = `${startHour}:${startMinute}`;
const endTimeStr = `${endHour}:${endMinute}`;
return {
liveDateStr,
startTimeStr,
endTimeStr,
};
}
render() {
const { courseBasinInfo, courseClassInfo = {}, courseIntroInfo, type } = this.props;
const { coverUrl, courseName, scheduleVideoUrl } = courseBasinInfo;
const { liveDate, timeHorizonStart, timeHorizonEnd, nickname } = courseClassInfo;
const { liveCourseMediaRequests } = courseIntroInfo;
let liveDateStr, startTimeStr, endTimeStr;
if (liveDate) {
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="预览"
visible={true}
width={680}
onCancel={this.props.close}
footer={null}
className="preview-live-course-modal"
>
<div className="container__wrap">
<div className="container">
<div className="container__header">
{
type === 'videoCourse' ?
<video
controls
src={scheduleVideoUrl}
poster={coverUrl ? coverUrl : `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast`}
className="course-url"
/> :
<img src={coverUrl} className="course-cover" />
}
</div>
{
type === 'videoCourse' ?
<div className="container__body">
<div className="title__name">{courseName}</div>
<div className="title__inst-name">{window.currentUserInstInfo.name}</div>
</div> :
<div className="container__body">
<div className="container__body__title">
<div className="title__name">{courseName}</div>
<div className="title__state">待开课</div>
</div>
<div className="container__body__time">
<span className="time__label">上课时间:</span>
<span className="time__value">
{
liveDate && timeHorizonStart && timeHorizonEnd &&
[
<span>{liveDateStr}&nbsp;</span>,
<span>{startTimeStr}~{endTimeStr }</span>
]
}
</span>
</div>
<div className="container__body__teacher">
<span className="teacher__label">上课老师:</span>
<span className="teacher__value">{nickname}</span>
</div>
</div>
}
<div className="container__introduction">
<div className="container__introduction__title">直播简介</div>
<div className="container__introduction__list editor-box">
{
liveCourseMediaRequests.map((item, index) => {
if (item.mediaType === 'TEXT') {
return (
<div
className="intro-item text"
dangerouslySetInnerHTML={{
__html: item.mediaContent
}}
/>
)
}
if (item.mediaType === 'PICTURE') {
return (
<div className="intro-item picture">
<img src={item.mediaUrl} />
</div>
)
}
})
}
</div>
</div>
</div>
</div>
</Modal>
)
}
}
export default PreviewCourseModal;
.preview-live-course-modal {
.ant-modal-body {
background-image: url('https://image.xiaomaiketang.com/xm/xZWdziTCAf.png');
background-size: 100% 100%;
}
.container__wrap {
width: 340px;
height: 618px;
padding: 67px 46px 48px 47px;
margin: auto;
background-image: url('https://image.xiaomaiketang.com/xm/DHMzHiGc2E.png');
background-size: 100% 100%;
}
.container {
overflow: scroll;
height: 100%;;
.course-cover, .course-url {
width: 100%;
height: 141px;
background: #000;
}
&__body {
background-color: #FFF;
padding: 7px 0 11px 0;;
.title__name {
color: #000;
}
.title__inst-name {
color: #666;
font-size: 12px;
margin-top: 4px;
}
&__title {
display: flex;
justify-content: space-between;
.title__name {
line-height: 20px;
color: #000;
font-weight: 500;
}
.title__state {
min-width: 40px;
height: 17px;
padding: 0 2px;
margin-left: 6px;
font-size: 12px;
color: #FFF;
background-color: #34B88B;
}
}
&__time, &__teacher {
display: flex;
align-items: center;
margin-top: 8px;
color: #999;
line-height: 17px;
span {
font-size: 12px;
}
}
&__teacher {
margin-top: 4px;
}
}
&__introduction {
margin-top: 10px;
padding: 12px 0;
position: relative;
&::before {
content: '';
position: absolute;
top: -12px;
left: 0;
width: 100%;
height: 10px;
background-color: #F4F6FA;
}
&__title {
font-size: 12px;
color: #333;
text-align: center;
line-height: 17px;
position: relative;
&::before,
&::after {
position: absolute;
content: "";
top: 8px;
width: 13px;
height: 1px;
background-color: #ccc;
}
&::before {
left: 32%;
}
&::after {
right: 32%;
}
}
&__list {
margin-top: 12px;
.intro-item:not(:first-child) {
margin-top: 13px;
}
.text {
color: #666;
line-height: 17px;
p {
font-size: 12px;
}
}
.picture {
img {
width: 100%;
}
}
}
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-20 19:12:49
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-20 20:25:13
* @Description: 大班直播分享弹窗
*/
import React from 'react';
import { Modal, Input, Button, message } from 'antd';
import domtoimage from 'dom-to-image';
import html2canvas from 'html2canvas';
import qrcode from "@/libs/qrcode/qrcode.js";
import './ShareLiveModal.less';
const BASE_IMG = require('@/images/xiaomai-IMG.png');
const { name, banner = BASE_IMG } = currentUserInstInfo;
const DEFAULT_COVER = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class ShareLiveModal extends React.Component {
constructor(props) {
super(props);
this.state = {
shareUrl: 'https://xiaomai5.com/liveShare?courseId=12'
}
}
componentDidMount() {
// 获取短链接
this.handleConvertShortUrl();
}
handleConvertShortUrl = () => {
const { longUrl } = this.props.data;
// 发请求
axios.Sales('public/businessShow/convertShortUrls', {
urls: [longUrl]
}).then((res) => {
const { result = [] } = res;
this.setState({
shareUrl: result[0].shortUrl
}, () => {
const qrcodeWrapDom = document.querySelector('#qrcodeWrap');
const qrcodeNode = new qrcode({
text: this.state.shareUrl,
size: 98,
})
qrcodeWrapDom.appendChild(qrcodeNode);
});
})
}
componentWillUnmount() {
// 页面销毁之前清空定时器
clearTimeout(this.timer);
}
// 下载海报
handleDownloadPoster = () => {
const dom = document.querySelector('#poster');
html2canvas(dom, {
useCORS: true,
}).then(canvas => {
const download = document.createElement('a');
const { courseName } = this.props.data;
const dataUrl = canvas.toDataURL('image/png');
$(download).attr('href', dataUrl).attr('download', `${courseName}.png`).get(0).click();
})
}
// 复制分享链接
handleCopy = () => {
const textContent = document.getElementById('shareUrl').innerText;
window.copyText(textContent);
message.success('复制成功!');
}
render() {
const { needStr, data, type } = this.props;
const { courseName, coverUrl = DEFAULT_COVER, scheduleVideoUrl } = data;
const { shareUrl } = this.state;
// 判断是否是默认图, 默认图不需要在URL后面增加字符串
const isDefaultCover = coverUrl === DEFAULT_COVER;
const coverImgSrc = type === 'videoClass'
// 如果是默认图, 显示视频的第一帧, 否则显示上传的视频封面
? ((!coverUrl || isDefaultCover)
? `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast&anystring=anystring`
: `${coverUrl}${!needStr ? '&anystring=anystring': ''}`)
: `${coverUrl}${(!needStr && !isDefaultCover) ? '&anystring=anystring' : ''}`
return (
<Modal
title={type === 'videoClass' ? '分享视频课' : '分享直播课'}
width={680}
visible={true}
footer={null}
className="share-live-modal"
onCancel={this.props.close}
>
<div className="left" id="poster">
<div className="course-name">{`【${courseName}】开课啦,快来学习!`}</div>
<img
src={coverImgSrc}
crossOrigin="*"
className="course-cover"
/>
<div className="qrcode-wrap">
<div className="qrcode-wrap__left">
<div className="text">长按识别二维码进入观看</div>
<img className="finger" src="https://image.xiaomaiketang.com/xm/thpkWDwJsC.png"/>
</div>
<div className="qrcode-wrap__right" id="qrcodeWrap">
</div>
</div>
<div className="inst-name">
<span className="icon iconfont">&#xe7b1;</span>
<span className="text">{name}</span>
</div>
</div>
<div className="right">
<div className="share-url right__item">
<div className="title">① 链接分享</div>
<div className="sub-title">学生可通过微信打开链接,报名观看直播</div>
<div className="content">
<div className="share-url" id="shareUrl">{shareUrl}</div>
<Button type="primary" onClick={this.handleCopy}>复制</Button>
</div>
</div>
<div className="share-poster right__item">
<div className="title">② 海报分享</div>
<div className="sub-title">学生可通过微信识别二维码,报名观看直播</div>
<div className="content" onClick={this.handleDownloadPoster}>下载海报</div>
</div>
</div>
</Modal>
)
}
}
export default ShareLiveModal;
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