page.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. import React from 'react';
  2. import { Link } from 'react-router-dom';
  3. import moment from 'moment';
  4. import { Form, Input, Button, Row, Col, InputNumber, Upload, DatePicker, Checkbox, Icon } from 'antd';
  5. import './index.less';
  6. import Editor from '@src/components/Editor';
  7. import Page from '@src/containers/Page';
  8. import Block from '@src/components/Block';
  9. import TreeSelect from '@src/components/TreeSelect';
  10. import Select from '@src/components/Select';
  11. import EditTableCell from '@src/components/EditTableCell';
  12. import Radio from '@src/components/Radio';
  13. import ActionLayout from '@src/layouts/ActionLayout';
  14. import TableLayout from '@src/layouts/TableLayout';
  15. // import FileUpload from '@src/components/FileUpload';
  16. import { formatFormError, formatSeconds, formatTreeData, getMap } from '@src/services/Tools';
  17. import { asyncSMessage } from '@src/services/AsyncTools';
  18. import { CrowdList, CourseStatus, CourseVideoType } from '../../../../Constant';
  19. import { Course } from '../../../stores/course';
  20. import { System } from '../../../stores/system';
  21. import { Exercise } from '../../../stores/exercise';
  22. const CourseStatusMap = getMap(CourseStatus, 'value', 'label');
  23. export default class extends Page {
  24. init() {
  25. this.exerciseMap = {};
  26. this.actionList = [{
  27. key: 'add',
  28. type: 'primary',
  29. name: '上传视频',
  30. render: (item) => {
  31. return <Upload
  32. showUploadList={false}
  33. beforeUpload={(file) => System.uploadVideo(file).then((result) => {
  34. return Course.addNo({ courseId: this.params.id, resource: result.url, time: result.time, title: file.name.split('.')[0] });
  35. }).then(() => {
  36. this.refreshNo();
  37. })}
  38. >
  39. <Button>{item.name}</Button>
  40. </Upload>;
  41. },
  42. }];
  43. this.noColumns = [{
  44. title: '课程序号',
  45. dataIndex: 'no',
  46. }, {
  47. title: '名称',
  48. dataIndex: 'title',
  49. render: (text, record) => {
  50. return <EditTableCell value={text} onChange={(v) => {
  51. this.changeNo('title', record, v);
  52. }} />;
  53. },
  54. }, {
  55. title: '时长',
  56. dataIndex: 'time',
  57. render: (text) => {
  58. return formatSeconds(text);
  59. },
  60. }, {
  61. title: '视频地址',
  62. dataIndex: 'resource',
  63. render: (text) => {
  64. return <a href={text} target='_blank'>访问</a>;
  65. },
  66. }, {
  67. title: '试用视频',
  68. dataIndex: 'isTrail',
  69. render: (text, record) => {
  70. return <div>
  71. <Checkbox checked={!!text} onChange={(v) => {
  72. this.changeNo('isTrail', record, v.target.checked ? 1 : 0);
  73. }} />
  74. {text > 0 && <InputNumber value={record.startTrail} onChange={(v) => {
  75. this.changeNo('startTrail', record, v);
  76. }} />}
  77. {text > 0 && <InputNumber value={record.endTrail} onChange={(v) => {
  78. this.changeNo('endTrail', record, v);
  79. }} />}
  80. {text > 0 && <Upload
  81. showUploadList={false}
  82. beforeUpload={(file) => System.uploadVideo(file).then((result) => {
  83. return this.changeNo('trailResource', record, result.url);
  84. })}
  85. >
  86. <Button>上传视频{record.trailResource ? '(已上传)' : ''}</Button>
  87. </Upload>}
  88. </div>;
  89. },
  90. }, {
  91. title: '操作',
  92. dataIndex: 'handler',
  93. render: (text, record) => {
  94. const { total } = this.state;
  95. return <div className="table-button">
  96. {record.no > 1 && (
  97. <a onClick={() => {
  98. this.changeNo('no', record, record.no - 1);
  99. }}>上移</a>
  100. )}
  101. {record.no <= total && (
  102. <a onClick={() => {
  103. this.changeNo('no', record, record.no + 1);
  104. }}>下移</a>
  105. )}
  106. {(
  107. <a onClick={() => {
  108. this.delNo(record.id);
  109. }}>删除</a>
  110. )}
  111. </div>;
  112. },
  113. }];
  114. this.timeColumns = [{
  115. title: '时间段',
  116. dataIndex: 'time',
  117. render: (text, record) => {
  118. return <DatePicker.RangePicker value={[record.startTime, record.endTime]} onChange={(value) => {
  119. this.changeTime(record.id, { startTime: value[0], endTime: value[1] });
  120. }} />;
  121. },
  122. }, {
  123. title: '学员数量',
  124. dataIndex: 'studentNumber',
  125. render: (text, record) => {
  126. return <Link to={`/course/student/${this.params.id}?timeId=${record.id}`}>{text || 0}</Link>;
  127. },
  128. }, {
  129. title: '状态',
  130. dataIndex: 'status',
  131. render: (text, record) => {
  132. let status = 0;
  133. const start = new Date(record.startTime);
  134. const end = new Date(record.endTime);
  135. if (new Date().getTime() > start.getTime()) {
  136. status = 1;
  137. if (new Date().getTime() > end.getTime()) {
  138. status = 2;
  139. }
  140. }
  141. return CourseStatusMap[status] || status;
  142. },
  143. }];
  144. Exercise.courseStruct().then((result) => {
  145. const list = result.map(row => { row.title = `${row.titleZh}`; row.value = row.id; return row; });
  146. const tree = formatTreeData(list, 'id', 'title', 'parentId');
  147. this.exerciseMap = getMap(result.map(row => {
  148. row.title = `${row.titleZh}`;
  149. row.value = row.id;
  150. return row;
  151. }), 'id');
  152. this.setState({ exercise: tree });
  153. });
  154. }
  155. initData() {
  156. const { id } = this.params;
  157. const { module } = this.state.search;
  158. let handler;
  159. if (id) {
  160. handler = Course.get({ id });
  161. } else {
  162. handler = Promise.resolve({ structId: '', courseModule: module, expireDays: 180 });
  163. }
  164. handler
  165. .then(result => {
  166. this.setState({ module: result.courseModule });
  167. const { form } = this.props;
  168. result.structId = `${result.structId}`;
  169. form.setFieldsValue(result);
  170. if (id) {
  171. switch (result.courseModule) {
  172. case 'video':
  173. this.refreshNo();
  174. break;
  175. case 'online':
  176. this.refreshTime();
  177. break;
  178. default:
  179. }
  180. }
  181. this.setState({ data: result });
  182. });
  183. }
  184. refreshNo() {
  185. const { id } = this.params;
  186. Course.allNo({ courseId: id }).then(result => {
  187. this.setState({ list: result, total: result.length, no: true });
  188. });
  189. }
  190. changeNo(field, record, value) {
  191. Course.editNo({ id: record.id, [field]: value, courseId: record.courseId }).then(() => {
  192. this.refreshNo();
  193. });
  194. }
  195. delNo(id) {
  196. Course.delNo({ id }).then(() => {
  197. this.refreshNo();
  198. });
  199. }
  200. refreshTime() {
  201. const { id } = this.params;
  202. Course.listTime({ courseId: id }).then(result => {
  203. result.list = result.list.map(row => {
  204. row.startTime = moment(row.startTime);
  205. row.endTime = moment(row.endTime);
  206. return row;
  207. });
  208. this.setState({ list: result.list, total: result.total, time: true });
  209. });
  210. }
  211. changeTime(id, data) {
  212. data.id = id;
  213. Course.editTime(data).then(() => {
  214. this.refreshTime();
  215. });
  216. }
  217. submit() {
  218. const { form } = this.props;
  219. const { module } = this.state;
  220. form.validateFields((err) => {
  221. if (!err) {
  222. const data = form.getFieldsValue();
  223. data.parentStructId = this.exerciseMap[data.structId] ? this.exerciseMap[data.structId].parentId : 0;
  224. data.extend = this.exerciseMap[data.structId] ? this.exerciseMap[data.structId].extend : '';
  225. let handler;
  226. if (data.id) {
  227. handler = Course.edit(data);
  228. } else {
  229. data.courseModule = module;
  230. handler = Course.add(data);
  231. }
  232. handler.then((result) => {
  233. asyncSMessage('保存成功');
  234. if (data.id) {
  235. linkTo(`/course/detail/${data.id}`);
  236. } else {
  237. linkTo(`/course/detail/${result.id}`);
  238. }
  239. }).catch((e) => {
  240. if (e.result) form.setFields(formatFormError(data, e.result));
  241. });
  242. }
  243. });
  244. }
  245. renderVideo() {
  246. const { exercise, data } = this.state;
  247. const { getFieldDecorator, getFieldValue, setFieldsValue } = this.props.form;
  248. const structIdConfig = {
  249. rules: [{
  250. required: true, message: '请选择学科',
  251. }],
  252. };
  253. if (data.structId) {
  254. structIdConfig.initialValue = data.structId;
  255. }
  256. const cover = getFieldValue('cover');
  257. return <Block>
  258. <Form>
  259. {getFieldDecorator('id')(<input hidden />)}
  260. {exercise && data && <Form.Item key={data.structId} labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='学科'>
  261. {getFieldDecorator('structId', structIdConfig)(
  262. <TreeSelect treeData={exercise} />,
  263. )}
  264. </Form.Item>}
  265. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='类型'>
  266. {getFieldDecorator('videoType', {
  267. rules: [
  268. { required: true, message: '请选择' },
  269. ],
  270. })(
  271. <Select select={CourseVideoType} />,
  272. )}
  273. </Form.Item>
  274. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='适合人群'>
  275. {getFieldDecorator('crowd', {
  276. rules: [
  277. { required: true, message: '请选择' },
  278. ],
  279. })(
  280. <Radio select={CrowdList} />,
  281. )}
  282. </Form.Item>
  283. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='定价'>
  284. {getFieldDecorator('price', {
  285. rules: [
  286. { required: true, message: '请输入价格' },
  287. ],
  288. })(
  289. <InputNumber placeholder='请输入' />,
  290. )}
  291. </Form.Item>
  292. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程名称'>
  293. {getFieldDecorator('title', {
  294. rules: [
  295. { required: true, message: '请输入课程名称' },
  296. ],
  297. })(
  298. <Input placeholder='请输入课程名称' />,
  299. )}
  300. </Form.Item>
  301. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='开通有效期'>
  302. {getFieldDecorator('expireDays', {
  303. rules: [
  304. { required: true, message: '请输入有效期' },
  305. ],
  306. })(
  307. <InputNumber placeholder='天' min={0} />,
  308. )}
  309. </Form.Item>
  310. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='使用有效期'>
  311. {getFieldDecorator('useExpireDays', {
  312. rules: [
  313. { required: true, message: '请输入有效期' },
  314. ],
  315. })(
  316. <InputNumber placeholder='天' min={0} />,
  317. )}
  318. </Form.Item>
  319. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='教师'>
  320. {getFieldDecorator('teacher', {
  321. rules: [
  322. { required: true, message: '请输入教师' },
  323. ],
  324. })(
  325. <Input placeholder='请输入教师' />,
  326. )}
  327. </Form.Item>
  328. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程封面'>
  329. {getFieldDecorator('cover', {
  330. rules: [
  331. { required: true, message: '上传图片' },
  332. ],
  333. })(
  334. <Upload
  335. listType="picture-card"
  336. showUploadList={false}
  337. beforeUpload={(file) => System.uploadImage(file).then((result) => {
  338. setFieldsValue({ cover: result.url });
  339. return Promise.reject();
  340. })}
  341. >
  342. {cover ? <img src={cover} alt="avatar" /> : <div>
  343. <Icon type={this.state.loading ? 'loading' : 'plus'} />
  344. <div className="ant-upload-text">Upload</div>
  345. </div>}
  346. </Upload>,
  347. )}
  348. </Form.Item>
  349. </Form>
  350. </Block>;
  351. }
  352. renderNo() {
  353. const { no } = this.state;
  354. if (!no) return null;
  355. return <Block>
  356. <h1>上传正式视频</h1>
  357. <ActionLayout
  358. itemList={this.actionList}
  359. selectedKeys={this.state.selectedKeys}
  360. onAction={key => this.onAction(key)}
  361. />
  362. <TableLayout
  363. columns={this.noColumns}
  364. list={this.state.list}
  365. pagination={false}
  366. loading={this.props.core.loading}
  367. onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
  368. onSelect={(keys, rows) => this.tableSelect(keys, rows)}
  369. selectedKeys={this.state.selectedKeys}
  370. />
  371. </Block>;
  372. }
  373. renderInfoVideo() {
  374. const { getFieldDecorator } = this.props.form;
  375. return <Block flex>
  376. <h1>课程介绍</h1>
  377. <Form>
  378. <Form.Item label='老师资历'>
  379. {getFieldDecorator('teacherContent', {
  380. })(
  381. <Editor placeholder='输入内容' />,
  382. )}
  383. </Form.Item>
  384. <Form.Item label='基本参数'>
  385. {getFieldDecorator('baseContent', {
  386. })(
  387. <Editor placeholder='输入内容' />,
  388. )}
  389. </Form.Item>
  390. <Form.Item label='授课内容'>
  391. {getFieldDecorator('courseContent', {
  392. })(
  393. <Editor placeholder='输入内容' />,
  394. )}
  395. </Form.Item>
  396. <Form.Item label='授课重点'>
  397. {getFieldDecorator('pointContent', {
  398. })(
  399. <Editor placeholder='输入内容' />,
  400. )}
  401. </Form.Item>
  402. <Form.Item label='适合人群'>
  403. {getFieldDecorator('crowdContent', {
  404. })(
  405. <Editor placeholder='输入内容' />,
  406. )}
  407. </Form.Item>
  408. </Form>
  409. </Block>;
  410. }
  411. renderSyllabus() {
  412. const { getFieldDecorator } = this.props.form;
  413. return <Block flex>
  414. <Form>
  415. <Form.Item label='授课大纲'>
  416. {getFieldDecorator('syllabusContent', {
  417. })(
  418. <Editor placeholder='输入内容' />,
  419. )}
  420. </Form.Item>
  421. </Form>
  422. </Block>;
  423. }
  424. renderPromote() {
  425. const { getFieldDecorator } = this.props.form;
  426. return <Block flex>
  427. <Form>
  428. <Form.Item label='优惠信息'>
  429. {getFieldDecorator('promoteContent', {
  430. })(
  431. <Editor placeholder='输入内容' />,
  432. )}
  433. </Form.Item>
  434. </Form>
  435. </Block>;
  436. }
  437. renderOnline() {
  438. const { exercise, data } = this.state;
  439. const { getFieldDecorator } = this.props.form;
  440. const structIdConfig = {
  441. rules: [{
  442. required: true, message: '请选择学科',
  443. }],
  444. };
  445. if (data.structId) {
  446. structIdConfig.initialValue = data.structId;
  447. }
  448. return <Block>
  449. <Form>
  450. {getFieldDecorator('id')(<input hidden />)}
  451. {exercise && <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='学科'>
  452. {getFieldDecorator('structId', structIdConfig)(
  453. <TreeSelect treeData={exercise} />,
  454. )}
  455. </Form.Item>}
  456. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='课程名称'>
  457. {getFieldDecorator('title', {
  458. rules: [
  459. { required: true, message: '请输入课程名称' },
  460. ],
  461. })(
  462. <Input placeholder='请输入课程名称' />,
  463. )}
  464. </Form.Item>
  465. <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='提问权限延迟'>
  466. {getFieldDecorator('askExtendDays', {
  467. rules: [
  468. { required: true, message: '请输入扩展天数' },
  469. ],
  470. })(
  471. <InputNumber placeholder='天' />,
  472. )}
  473. </Form.Item>
  474. </Form>
  475. </Block>;
  476. }
  477. renderTime() {
  478. const { time, timerange } = this.state;
  479. if (!time) return null;
  480. return <Block>
  481. <h1>课时管理</h1>
  482. <TableLayout
  483. columns={this.timeColumns}
  484. list={this.state.list}
  485. pagination={false}
  486. loading={this.props.core.loading}
  487. onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
  488. onSelect={(keys, rows) => this.tableSelect(keys, rows)}
  489. selectedKeys={this.state.selectedKeys}
  490. />
  491. <Row>
  492. <Col span={8}>
  493. <Form.Item>
  494. <DatePicker.RangePicker value={timerange} onChange={(value) => {
  495. this.setState({ timerange: value });
  496. }} />
  497. </Form.Item>
  498. </Col>
  499. <Col span={1}>
  500. <Form.Item>
  501. <Button onClick={() => {
  502. Course.addTime({ courseId: this.params.id, startTime: timerange[0], endTime: timerange[1] }).then(() => {
  503. this.refreshTime();
  504. this.setState({ timerange: null });
  505. });
  506. }}>添加</Button>
  507. </Form.Item>
  508. </Col>
  509. </Row>
  510. </Block>;
  511. }
  512. renderContent() {
  513. switch (this.state.module) {
  514. case 'online':
  515. return [this.renderOnline(), this.renderTime()];
  516. case 'video':
  517. return [this.renderVideo(), this.renderNo(), this.renderInfoVideo(), this.renderSyllabus(), this.renderPromote()];
  518. default:
  519. return <div />;
  520. }
  521. }
  522. renderView() {
  523. return <div flex>
  524. {this.renderContent()}
  525. <Row type="flex" justify="center">
  526. <Col>
  527. <Button type="primary" onClick={() => {
  528. this.submit();
  529. }}>保存</Button>
  530. </Col>
  531. </Row>
  532. </div>;
  533. }
  534. }