page.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import React from 'react';
  2. import { Tooltip } from 'antd';
  3. import './index.less';
  4. import Page from '@src/containers/Page';
  5. import Icon from '../../../components/Icon';
  6. import Progress from '../../../components/Progress';
  7. import Assets from '../../../../../src/components/Assets';
  8. import { Sentence } from '../../../stores/sentence';
  9. export default class extends Page {
  10. constructor(props) {
  11. super(props);
  12. this.page = 0;
  13. this.inited = false;
  14. this.timeout = null;
  15. this.articleMap = {};
  16. this.pageLine = 100;
  17. }
  18. init() {
  19. this.lastTime = new Date();
  20. }
  21. initState() {
  22. return { showJump: false, showMenu: false, currentPage: 0, totalPage: 0 };
  23. }
  24. initData() {
  25. const { chapter = 0, part = 0 } = this.state.search;
  26. let { page = 0 } = this.state.search;
  27. const handler = this.refreshSentence();
  28. handler.then(() => {
  29. if (chapter > 0) {
  30. if (!part) {
  31. ({ page } = this.searchArticle(chapter, 1));
  32. } else {
  33. ({ page } = this.searchArticle(chapter, part));
  34. }
  35. }
  36. this.jumpPage(page);
  37. });
  38. }
  39. refreshSentence() {
  40. if (this.inited) return Promise.resolve();
  41. return Sentence.listArticle()
  42. .then(result => {
  43. const articleMap = {};
  44. let totalPage = 0;
  45. let maxChapter = 0;
  46. let introduction = null;
  47. result.forEach(article => {
  48. if (article.chapter === 0) introduction = article;
  49. if (!articleMap[article.chapter]) {
  50. articleMap[article.chapter] = [];
  51. if (article.chapter > maxChapter) maxChapter = article.chapter;
  52. }
  53. article.startPage = totalPage + 1;
  54. article.endPage = totalPage + article.pages;
  55. totalPage += article.pages;
  56. articleMap[article.chapter].push(article);
  57. });
  58. this.setState({ articleMap, totalPage, maxChapter });
  59. return introduction;
  60. })
  61. .then(introduction => {
  62. return Sentence.getInfo().then(result => {
  63. const map = {};
  64. // 添加前言
  65. if (introduction) {
  66. result.chapters.unshift({
  67. title: introduction.title,
  68. value: 0,
  69. });
  70. }
  71. result.chapters.forEach(row => {
  72. map[row.value] = row;
  73. });
  74. this.setState({ sentence: result, chapterMap: map });
  75. });
  76. })
  77. .then(() => {
  78. this.inited = true;
  79. });
  80. }
  81. refreshArticle(articleId) {
  82. if (this.articleMap[articleId]) return Promise.resolve(this.articleMap[articleId]);
  83. return Sentence.detailArticle(articleId).then(result => {
  84. this.articleMap[articleId] = result;
  85. return result;
  86. });
  87. }
  88. prevPage() {
  89. const { currentPage } = this.state;
  90. if (currentPage > 1) {
  91. this.jumpPage(currentPage - 1);
  92. }
  93. }
  94. nextPage() {
  95. const { currentPage, totalPage } = this.state;
  96. if (currentPage < totalPage) {
  97. this.jumpPage(currentPage + 1);
  98. }
  99. }
  100. jumpPage(targetPage) {
  101. // 计算哪篇文章
  102. const { target, index, allow } = this.computeArticle(targetPage);
  103. if (!allow) {
  104. // todo 无法访问:非试用
  105. return;
  106. }
  107. this.page = targetPage;
  108. this.setState({ inputPage: targetPage });
  109. const { article = {} } = this.state;
  110. this.updateProgress(target, index, article);
  111. this.refreshArticle(target.id).then(row => {
  112. this.setState({ article: row, index, currentPage: targetPage });
  113. });
  114. }
  115. computeArticle(page) {
  116. const { articleMap, maxChapter, sentence } = this.state;
  117. let target = null;
  118. let index = 0;
  119. const allow = true || sentence.code || page <= sentence.trailPages;
  120. for (let i = 0; i <= maxChapter; i += 1) {
  121. const list = articleMap[i];
  122. if (!list || list.length === 0) continue;
  123. for (let j = 0; j < list.length; j += 1) {
  124. const article = list[j];
  125. if (article.endPage >= page) {
  126. target = article;
  127. index = page - article.startPage;
  128. // if (!sentence.code) {
  129. // if (!article.isTrail) {
  130. // allow = false;
  131. // } else if (index < article.trailStart - 1) {
  132. // allow = false;
  133. // } else if (index > article.trailEnd - 1) {
  134. // allow = false;
  135. // }
  136. // }
  137. return { target, index, allow };
  138. }
  139. }
  140. }
  141. return { allow };
  142. }
  143. searchArticle(chapter, part) {
  144. const { articleMap } = this.state;
  145. let target = null;
  146. let page = 0;
  147. if (!part) part = 1;
  148. const list = articleMap[chapter];
  149. for (let j = 0; j < list.length; j += 1) {
  150. const article = list[j];
  151. if (article.part === Number(part)) {
  152. target = article;
  153. page = article.startPage;
  154. // if (sentence.code) {
  155. // page = article.startPage;
  156. // } else {
  157. // // 试用章节页码
  158. // page = article.startPage + article.trailPage - 1;
  159. // }
  160. break;
  161. }
  162. }
  163. return { target, page };
  164. }
  165. updateProgress(target, index, current) {
  166. if (this.timeout) {
  167. clearTimeout(this.timeout);
  168. this.timeout = null;
  169. }
  170. const now = new Date();
  171. const time = (now.getTime() - this.lastTime.getTime()) / 1000;
  172. this.lastTime = now;
  173. const progress = ((index + 1) * 100) / target.pages;
  174. Sentence.updateProgress(target.chapter, target.part, progress, time, current.chapter, current.page);
  175. this.timeout = setTimeout(() => {
  176. // 最长5分钟阅读时间
  177. Sentence.updateProgress(0, 0, 0, 5 * 60, target.chapter, target.part);
  178. }, 5 * 60 * 1000);
  179. }
  180. renderView() {
  181. return (
  182. <div className="layout">
  183. {this.renderBody()}
  184. {this.renderRight()}
  185. {this.renderBottom()}
  186. {this.renderProgress()}
  187. </div>
  188. );
  189. }
  190. renderBody() {
  191. const { showMenu, article, index, chapterMap = {} } = this.state;
  192. return article ? (
  193. <div className="layout-body">
  194. <div className="crumb">千行长难句解析 >> {(chapterMap[article.chapter] || {}).title}</div>
  195. {article.chapter > 0 && <div className="title">{article.title}</div>}
  196. <div className="overload" style={{ top: index * -20 * this.pageLine }}>
  197. <div className="text" dangerouslySetInnerHTML={{ __html: article.content }} />
  198. </div>
  199. {showMenu && this.renderMenu()}
  200. </div>
  201. ) : (
  202. <div className="layout-body">
  203. <div className="free-over">
  204. <div className="free-over-title">试读已结束,购买后可继续阅读。</div>
  205. <div className="free-over-btn">¥ 20.00 | 立即购买</div>
  206. <div className="free-over-desc">
  207. <div className="free-over-desc-title">张小爱笑笑笑 2019-07-13</div>
  208. <div className="free-over-desc-content">
  209. 韩瑞祥/文 移民文学(Migrations
  210. literatur)成为当今德国文坛上一个备受关注的文学现象,一批又一批脱颖而出的移民文学作家为当今德国文学的发展不断地
  211. </div>
  212. </div>
  213. </div>
  214. </div>
  215. );
  216. }
  217. renderMenu() {
  218. const { sentence = {}, articleMap } = this.state;
  219. const { chapters = [] } = sentence;
  220. let page = 1;
  221. const code = !!sentence.code;
  222. const message = '购买后访问';
  223. return (
  224. <div className="layout-menu">
  225. <div className="title">目录</div>
  226. <div
  227. className="close"
  228. onClick={() => {
  229. this.setState({ showMenu: false });
  230. }}
  231. >
  232. x
  233. </div>
  234. <div className="chapter">
  235. {chapters.map(chapter => {
  236. if (chapter.exercise) {
  237. return [<div className={'chapter-item trail'}>{chapter.title}</div>];
  238. }
  239. chapter.startPage = page;
  240. const _item = code ? (
  241. <div
  242. className={'chapter-item'}
  243. onClick={() => {
  244. this.jumpPage(chapter.startPage);
  245. }}
  246. >
  247. {chapter.title}
  248. <div className="page">{chapter.startPage}</div>
  249. </div>
  250. ) : (
  251. <Tooltip title={message}>
  252. <div className={'chapter-item trail'}>
  253. {chapter.title}
  254. <div className="page">{chapter.startPage}</div>
  255. </div>
  256. </Tooltip>
  257. );
  258. const list = [_item];
  259. if (chapter.value) {
  260. (articleMap[chapter.value] || []).forEach(article => {
  261. // 得到下一章节page
  262. page += article.pages;
  263. const item = code ? (
  264. <div
  265. className={'part-item'}
  266. onClick={() => {
  267. if (code) this.jumpPage(article.startPage);
  268. }}
  269. >
  270. {article.title}
  271. <div className="page">{article.startPage}</div>
  272. </div>
  273. ) : (
  274. <Tooltip title={message}>
  275. <div className={'part-item trail'}>
  276. {article.title}
  277. <div className="page">{article.startPage}</div>
  278. </div>
  279. </Tooltip>
  280. );
  281. list.push(item);
  282. });
  283. }
  284. return list;
  285. })}
  286. </div>
  287. </div>
  288. );
  289. }
  290. renderRight() {
  291. return (
  292. <div className="layout-right">
  293. <div
  294. className="m-b-1"
  295. onClick={() => {
  296. this.setState({ showMenu: true });
  297. }}
  298. >
  299. <Icon name="menu" />
  300. </div>
  301. <div
  302. className="m-b-1"
  303. onClick={() => {
  304. this.prevPage();
  305. }}
  306. >
  307. <Icon name="down_turn" />
  308. </div>
  309. <div
  310. className="m-b-1"
  311. onClick={() => {
  312. this.nextPage();
  313. }}
  314. >
  315. <Icon name="up_turn" />
  316. </div>
  317. </div>
  318. );
  319. }
  320. renderBottom() {
  321. const { showJump, currentPage, totalPage } = this.state;
  322. return (
  323. <div className="layout-bottom">
  324. <span className="per">{totalPage ? parseInt((currentPage * 100) / totalPage, 10) : 0}%</span>
  325. <span className="num">
  326. {currentPage}/{totalPage}
  327. </span>
  328. <span className="btn">
  329. <Assets
  330. name={showJump ? 'unfold_icon_down' : 'unfold_icon_up'}
  331. onClick={() => {
  332. this.setState({ showJump: !showJump });
  333. }}
  334. />
  335. <div hidden={!showJump} className="jump">
  336. <span className="text">当前页</span>
  337. <input
  338. className="input"
  339. value={this.state.inputPage}
  340. onChange={e => {
  341. this.page = Number(e.target.value);
  342. this.setState({ inputPage: e.target.value });
  343. }}
  344. />
  345. <Assets
  346. name="yes_icon"
  347. onClick={() => {
  348. if (this.page < 1 || this.page > totalPage) return;
  349. this.jumpPage(this.page);
  350. }}
  351. />
  352. </div>
  353. </span>
  354. </div>
  355. );
  356. }
  357. renderProgress() {
  358. const { currentPage, totalPage } = this.state;
  359. return (
  360. <div className="layout-progress">
  361. <Progress
  362. size="small"
  363. theme="theme"
  364. radius={false}
  365. progress={totalPage ? parseInt((currentPage * 100) / totalPage, 10) : 0}
  366. />
  367. </div>
  368. );
  369. }
  370. }