├── .gitignore
├── .idea
├── abcd.iml
├── jsLibraryMappings.xml
├── misc.xml
├── modules.xml
├── watcherTasks.xml
└── workspace.xml
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── fonts
│ ├── slick.eot
│ ├── slick.svg
│ ├── slick.ttf
│ └── slick.woff
├── index.html
├── manifest.json
├── slick-theme.min.css
└── slick.min.css
├── screen
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── 6.png
├── server
├── app.js
└── index.js
└── src
├── actions
├── action-types.js
└── actions.js
├── assets
├── css
│ ├── util.css
│ └── util.scss
├── images
│ ├── 1.jpg
│ ├── 2.jpg
│ ├── 3.jpg
│ ├── 4.jpg
│ ├── 5.jpg
│ └── sprite.0.50.png
└── js
│ ├── imgError.js
│ └── local.js
├── components
├── About
│ ├── index.js
│ └── style.css
├── App
│ ├── index.js
│ ├── style.css
│ └── style.scss
├── BookDetail
│ ├── Rating.jsx
│ ├── Similar.jsx
│ ├── index.css
│ ├── index.jsx
│ ├── index.scss
│ ├── star_half.png
│ ├── star_off.png
│ └── star_on.png
├── BookShelf
│ ├── ShelfItem.js
│ ├── index.css
│ ├── index.jsx
│ └── index.scss
├── Category
│ ├── index.css
│ ├── index.jsx
│ └── index.scss
├── Home
│ ├── BookList.js
│ ├── Recommend.js
│ ├── Swiper.jsx
│ ├── Title.js
│ ├── images
│ │ ├── 1.jpg
│ │ ├── 1.js
│ │ ├── 2.jpg
│ │ ├── 3.jpg
│ │ ├── 4.jpg
│ │ └── 5.jpg
│ ├── index.css
│ ├── index.jsx
│ └── index.scss
├── Loading
│ ├── index.css
│ ├── index.js
│ └── index.scss
├── NotFound
│ ├── index.js
│ └── style.css
├── People
│ ├── PeopleContainer.js
│ ├── Person.js
│ ├── PersonInput.js
│ └── PersonList.js
└── Reader
│ ├── BottomNav.jsx
│ ├── Content.js
│ ├── Cover.jsx
│ ├── FontNav.jsx
│ ├── ListPanel.jsx
│ ├── TopNav.jsx
│ ├── index.css
│ ├── index.jsx
│ └── index.scss
├── index.css
├── index.js
├── reducers
├── counter.js
├── index.js
├── people-reducer.js
└── state.js
├── routes.js
└── store.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 | .idea/
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.idea/abcd.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
156 |
157 |
158 |
159 | history
160 | category-r
161 | px
162 | [bg
163 | bottom-nav
164 | @color
165 | .bac
166 | top-nav-pannel
167 | class=
168 | icon-text
169 | top-nav
170 | category-header
171 | bind(this)
172 | content
173 | push
174 | bookDetail
175 | context
176 | this.props
177 | bind(this
178 | actions
179 | getData
180 | curChapter
181 | info[id]
182 | setState
183 | read-btn
184 | home-header
185 | check
186 | checkbox
187 | selectAll
188 | shelf-detail
189 |
190 |
191 | 1
192 | $1px
193 | className
194 | $
195 | pr($1)
196 | @include bac
197 | [data-bg
198 | $color
199 | @mixin bac
200 | className=
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | true
269 |
270 | true
271 | true
272 |
273 |
274 | true
275 | DEFINITION_ORDER
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 | Assignment issuesJavaScript
295 |
296 |
297 | Code style issuesJavaScript
298 |
299 |
300 | CoffeeScript
301 |
302 |
303 | Control flow issuesJavaScript
304 |
305 |
306 | DOM issuesJavaScript
307 |
308 |
309 | Data flow issuesJavaScript
310 |
311 |
312 | ECMAScript 6 migration aidsJavaScript
313 |
314 |
315 | Error handlingJavaScript
316 |
317 |
318 | Flow type checkerJavaScript
319 |
320 |
321 | General
322 |
323 |
324 | GeneralCoffeeScript
325 |
326 |
327 | GeneralJavaScript
328 |
329 |
330 | Internationalization issues
331 |
332 |
333 | JavaScript
334 |
335 |
336 | JavaScript function metricsJavaScript
337 |
338 |
339 | Naming conventionsJavaScript
340 |
341 |
342 | Potentially confusing code constructsJavaScript
343 |
344 |
345 | Probable bugsCoffeeScript
346 |
347 |
348 | Probable bugsJavaScript
349 |
350 |
351 | Spelling
352 |
353 |
354 | TypeScript
355 |
356 |
357 | XML
358 |
359 |
360 | XSLT
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 | 1502106228054
470 |
471 |
472 | 1502106228054
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 | 1502701029505
505 |
506 |
507 |
508 | 1502701029505
509 |
510 |
511 | 1502723019933
512 |
513 |
514 |
515 | 1502723019933
516 |
517 |
518 | 1502727170124
519 |
520 |
521 |
522 | 1502727170124
523 |
524 |
525 | 1502783391748
526 |
527 |
528 |
529 | 1502783391748
530 |
531 |
532 | 1502815857449
533 |
534 |
535 |
536 | 1502815857449
537 |
538 |
539 | 1502896915607
540 |
541 |
542 |
543 | 1502896915607
544 |
545 |
546 | 1502961050273
547 |
548 |
549 |
550 | 1502961050274
551 |
552 |
553 | 1503024162279
554 |
555 |
556 |
557 | 1503024162279
558 |
559 |
560 | 1503228789420
561 |
562 |
563 |
564 | 1503228789420
565 |
566 |
567 | 1503234589120
568 |
569 |
570 |
571 | 1503234589120
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
663 |
664 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 |
924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 |
945 |
946 |
947 |
948 |
949 |
950 |
951 |
952 |
953 |
954 |
955 |
956 |
957 |
958 |
959 |
960 |
961 |
962 |
963 |
964 |
965 |
966 |
967 |
968 |
969 |
970 |
971 |
972 |
973 |
974 |
975 |
976 |
977 |
978 |
979 |
980 |
981 |
982 |
983 |
984 |
985 |
986 |
987 |
988 |
989 |
990 |
991 |
992 |
993 |
994 |
995 |
996 |
997 |
998 |
999 |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
1009 |
1010 |
1011 |
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 |
1024 |
1025 |
1026 |
1027 |
1028 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 基于react + express + nodejs爬虫的移动书城(vue版本修改而来)
2 |
3 | > 技术栈:react + react-router + redux + webpack + fetch + scss + flex + express + nodejs + mysql + localStorage
4 |
5 | > [预览地址](http://tgxhx.xyz/book)
6 |
7 | > [api地址](https://github.com/tgxhx/node-book-api)
8 |
9 | > [爬虫地址](https://github.com/tgxhx/node-crawler)
10 |
11 | ## 使用说明
12 |
13 | ``` bash
14 | #克隆项目
15 | git clone git@github.com:tgxhx/react-reader.git
16 |
17 | # 安装依赖
18 | npm install
19 |
20 | # 本地开发环境 访问http://localhost:3000
21 | npm start
22 |
23 | # 构建生产
24 | npm run build
25 |
26 | ```
27 |
28 | ## 项目说明
29 | 关于爬虫和api就不详细说明了,完全跟vue版本的一样,[查看地址](https://github.com/tgxhx/vue-reader)
30 |
31 | 在写这个之前对于vue算是比较熟练了,转到react,jsx的语法研究了两天,然后研究了一下redux,发现跟vuex是类似的东西,上手起来也比较快。
32 |
33 | 然后开始用react全家桶重写这个项目,利用空余时间前后花了大概不到一个星期的样子,再之后花了一两天新增了vue版本没有的书架功能。
34 |
35 | 项目是基于create-react-app构建的,增加了sass的支持,组件热重载还未支持,加入了react-router和redux。
36 |
37 | 总结一下,vue和react的理念我认为是差不多的,重点都是组件化,state、props也是类似的作用,vuex和redux也有一定程度的类似,二者区别可能就是语法不一样了,vue写起来更像传统的html、js、css开发方式,jsx的写法有的人可能难以接受,但是也不难掌握,另外可能react对于js的掌握程度要求更高一些。
38 |
39 | 所以我认为,vue和react如果你熟悉其中之一,我相信上手另一个是很快的,因为核心理念你已经掌握了,剩下了就是语法了,vuex和redux也是一样。
40 |
41 | 本项目难点我认为是书架功能,也都写了注释,有类似想法的可以相互印证。
42 |
43 | ## 功能
44 | - [x] 首页推荐
45 | - [x] 书籍详情
46 | - [x] 相似推荐
47 | - [x] 分类查看
48 | - [x] 阅读器
49 | - [x] 章节跳转
50 | - [x] 更改字体
51 | - [x] 更换主题
52 | - [x] 夜间模式
53 | - [x] 翻页浏览
54 | - [x] 本地存储(存储每本书的阅读进度)
55 | - [x] 书架
56 |
57 | ## 项目截图
58 | 
59 | 
60 | 
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "abcd",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "classnames": "^2.2.5",
7 | "express": "^4.15.4",
8 | "morgan": "^1.8.2",
9 | "node-sass-chokidar": "0.0.3",
10 | "normalize.css": "^7.0.0",
11 | "npm-run-all": "^4.0.2",
12 | "nzh": "^1.0.0",
13 | "prop-types": "^15.5.10",
14 | "react": "^15.6.1",
15 | "react-dom": "^15.6.1",
16 | "react-id-swiper": "^1.4.0",
17 | "react-redux": "^5.0.5",
18 | "react-router": "^2.8.1",
19 | "react-router-dom": "^4.1.2",
20 | "react-scripts": "1.0.10",
21 | "react-slick": "^0.14.11",
22 | "redux": "^3.7.2",
23 | "redux-thunk": "^2.2.0",
24 | "reset-css": "^2.2.1"
25 | },
26 | "scripts": {
27 | "build-css": "node-sass-chokidar src/ -o src/",
28 | "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
29 | "start-js": "react-scripts start",
30 | "start": "npm-run-all -p watch-css start-js",
31 | "build": "npm run build-css && react-scripts build",
32 | "test": "react-scripts test --env=jsdom",
33 | "eject": "react-scripts eject"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/public/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/slick.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/public/fonts/slick.eot
--------------------------------------------------------------------------------
/public/fonts/slick.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
--------------------------------------------------------------------------------
/public/fonts/slick.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/public/fonts/slick.ttf
--------------------------------------------------------------------------------
/public/fonts/slick.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/public/fonts/slick.woff
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 | React App
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/slick-theme.min.css:
--------------------------------------------------------------------------------
1 | @charset 'UTF-8';.slick-dots,.slick-next,.slick-prev{position:absolute;display:block;padding:0}.slick-dots li button:before,.slick-next:before,.slick-prev:before{font-family:slick;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-loading .slick-list{background:url(ajax-loader.gif) center center no-repeat #fff}@font-face{font-family:slick;font-weight:400;font-style:normal;src:url(fonts/slick.eot);src:url(fonts/slick.eot?#iefix) format('embedded-opentype'),url(fonts/slick.woff) format('woff'),url(fonts/slick.ttf) format('truetype'),url(fonts/slick.svg#slick) format('svg')}.slick-next,.slick-prev{font-size:0;line-height:0;top:50%;width:20px;height:20px;-webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);cursor:pointer;color:transparent;border:none;outline:0;background:0 0}.slick-next:focus,.slick-next:hover,.slick-prev:focus,.slick-prev:hover{color:transparent;outline:0;background:0 0}.slick-next:focus:before,.slick-next:hover:before,.slick-prev:focus:before,.slick-prev:hover:before{opacity:1}.slick-next.slick-disabled:before,.slick-prev.slick-disabled:before{opacity:.25}.slick-next:before,.slick-prev:before{font-size:20px;line-height:1;opacity:.75;color:#fff}.slick-prev{left:-25px}[dir=rtl] .slick-prev{right:-25px;left:auto}.slick-prev:before{content:'←'}.slick-next:before,[dir=rtl] .slick-prev:before{content:'→'}.slick-next{right:-25px}[dir=rtl] .slick-next{right:auto;left:-25px}[dir=rtl] .slick-next:before{content:'←'}.slick-dotted.slick-slider{margin-bottom:30px}.slick-dots{bottom:-25px;width:100%;margin:0;list-style:none;text-align:center}.slick-dots li{position:relative;display:inline-block;width:20px;height:20px;margin:0 5px;padding:0;cursor:pointer}.slick-dots li button{font-size:0;line-height:0;display:block;width:20px;height:20px;padding:5px;cursor:pointer;color:transparent;border:0;outline:0;background:0 0}.slick-dots li button:focus,.slick-dots li button:hover{outline:0}.slick-dots li button:focus:before,.slick-dots li button:hover:before{opacity:1}.slick-dots li button:before{font-size:6px;line-height:20px;position:absolute;top:0;left:0;width:20px;height:20px;content:'•';text-align:center;opacity:.25;color:#000}.slick-dots li.slick-active button:before{opacity:.75;color:#000}/*# sourceMappingURL=slick-theme.min.css.map */
--------------------------------------------------------------------------------
/public/slick.min.css:
--------------------------------------------------------------------------------
1 | .slick-list,.slick-slider,.slick-track{position:relative;display:block}.slick-loading .slick-slide,.slick-loading .slick-track{visibility:hidden}.slick-slider{box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;-khtml-user-select:none;-ms-touch-action:pan-y;touch-action:pan-y;-webkit-tap-highlight-color:transparent}.slick-list{overflow:hidden;margin:0;padding:0}.slick-list:focus{outline:0}.slick-list.dragging{cursor:pointer;cursor:hand}.slick-slider .slick-list,.slick-slider .slick-track{-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.slick-track{top:0;left:0}.slick-track:after,.slick-track:before{display:table;content:''}.slick-track:after{clear:both}.slick-slide{display:none;float:left;height:100%;min-height:1px}[dir=rtl] .slick-slide{float:right}.slick-slide img{display:block}.slick-slide.slick-loading img{display:none}.slick-slide.dragging img{pointer-events:none}.slick-initialized .slick-slide{display:block}.slick-vertical .slick-slide{display:block;height:auto;border:1px solid transparent}.slick-arrow.slick-hidden{display:none}/*# sourceMappingURL=slick.min.css.map */
--------------------------------------------------------------------------------
/screen/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/screen/1.png
--------------------------------------------------------------------------------
/screen/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/screen/2.png
--------------------------------------------------------------------------------
/screen/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/screen/3.png
--------------------------------------------------------------------------------
/screen/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/screen/4.png
--------------------------------------------------------------------------------
/screen/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/screen/5.png
--------------------------------------------------------------------------------
/screen/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/screen/6.png
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const morgan = require('morgan')
3 | const path = require('path')
4 |
5 | const app = express()
6 |
7 | app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] :response-time ms'));
8 |
9 | // Serve static assets
10 | app.use(express.static(path.resolve(__dirname, '..', 'build')));
11 |
12 | // Always return the main index.html, so react-router render the route in the client
13 | app.get('*', (req, res) => {
14 | res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html'));
15 | });
16 |
17 | module.exports = app;
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | // server/index.js
2 | 'use strict';
3 |
4 | const app = require('./app');
5 |
6 | const PORT = process.env.PORT || 9000;
7 |
8 | app.listen(PORT, () => {
9 | console.log(`App listening on port ${PORT}!`);
10 | });
--------------------------------------------------------------------------------
/src/actions/action-types.js:
--------------------------------------------------------------------------------
1 | export const ADD_PERSON = 'ADD_PERSON'
2 | export const INCREMENT = 'INCREMENT'
3 | export const DECRMENT = 'DECREMENT'
4 |
5 | //切换上下面板显示隐藏
6 | export const TOGGLE_BAR = 'TOGGLE_BAR'
7 | //显示目录面板
8 | export const SHOW_LIST_PANEL = 'SHOW_LIST_PANEL'
9 | //显示字体面板
10 | export const SHOW_FONT_PANEL = 'SHOW_FONT_PANEL'
11 | //增加一号字体
12 | export const FZ_SIZE_ADD = 'FZ_SIZE_ADD'
13 | //减小一号字体
14 | export const FZ_SIZE_SUB = 'FZ_SIZE_SUB'
15 | //更换背景
16 | export const CHANGE_BG = 'CHANGE_BG'
17 | //修改为指定字体
18 | export const FZ_SIZE_MODIRY = 'FZ_SIZE_MODIRY'
19 | //切换夜间模式
20 | export const SWITCH_NIGHT = 'SWITCH_NIGHT'
21 | //上一章
22 | export const PREV_CHAPTER = 'PREV_CHAPTER'
23 | //下一章
24 | export const NEXT_CHAPTER = 'NEXT_CHAPTER'
25 | //触发书籍详情
26 | export const CHOOSE_BOOK = 'CHOOSE_BOOK'
27 | //跳转书籍详情
28 | export const SHOW_BOOK_DETAIL = 'SHOW_BOOK_DETAIL'
29 | //当前章节
30 | export const CUR_CHAPTER = 'CUR_CHAPTER'
31 |
--------------------------------------------------------------------------------
/src/actions/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from './action-types'
2 |
3 | export const addPerson = (person) => {
4 | return {
5 | type: types.ADD_PERSON,
6 | person
7 | }
8 | }
9 |
10 | export const increment = (counter) => {
11 | return {
12 | type: types.INCREMENT,
13 | counter
14 | }
15 | }
16 |
17 | export const decrement = (counter) => {
18 | return {
19 | type: types.DECRMENT,
20 | counter
21 | }
22 | }
23 |
24 | export const fzSizeAdd = fz_size => ({type: types.FZ_SIZE_ADD, fz_size})
25 |
26 | export const fzSizeSub = fz_size => ({type: types.FZ_SIZE_SUB, fz_size})
27 |
28 | export const fzSizeModify = fz_size => ({type: types.FZ_SIZE_MODIRY, fz_size})
29 |
30 | export const nextChapter = (curChapter, max) => ({type: types.NEXT_CHAPTER, curChapter, max})
31 |
32 | export const prevChapter = curChapter => ({type: types.PREV_CHAPTER, curChapter})
33 |
34 | export const curChapter = num => ({type: types.CUR_CHAPTER, num})
35 |
36 | export const changeBG = num => ({type: types.CHANGE_BG, num})
37 |
38 | export const showFontPanel = state => ({type: types.SHOW_FONT_PANEL, state})
39 |
40 | export const switchNight = state => ({type: types.SWITCH_NIGHT, state})
41 |
42 | export const showListPanel = state => ({type: types.SHOW_LIST_PANEL, state})
--------------------------------------------------------------------------------
/src/assets/css/util.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/assets/css/util.css
--------------------------------------------------------------------------------
/src/assets/css/util.scss:
--------------------------------------------------------------------------------
1 | $red: #ed424b;
2 |
3 | @function pr($px, $base-font-size: 50px) {
4 | @if (unitless($px)) {
5 | //@warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels for you";
6 | @return pr($px + 0px); // That may fail.
7 | } @else if (unit($px) == rem) {
8 | @return $px;
9 | }
10 | @return ($px / $base-font-size) * 1rem;
11 | }
12 |
13 | @mixin ell() {
14 | overflow: hidden;
15 | -ms-text-overflow: ellipsis;
16 | text-overflow: ellipsis;
17 | white-space: nowrap;
18 | }
19 |
20 | @mixin wh($w,$h) {
21 | width: $w;
22 | height: $h;
23 | }
24 |
25 | @mixin ava($w) {
26 | width: $w;
27 | height: $w;
28 | img {
29 | width: 100%;
30 | height: 100%;
31 | }
32 | }
33 |
34 | @mixin dfcc() {
35 | display: flex;
36 | justify-content: center;
37 | align-items: center;
38 | }
39 |
40 | @mixin overhide($n) {
41 | overflow: hidden;
42 | text-overflow: ellipsis;
43 | display: -webkit-box;
44 | -webkit-line-clamp: $n;
45 | -webkit-box-orient: vertical;
46 | }
47 |
48 | @mixin border-1px($position) {
49 | position: relative;
50 | &:after {
51 | border-top: 1px solid #c8c7cc;
52 | content: ' ';
53 | display: block;
54 | width: 100%;
55 | position: absolute;
56 | left: 0;
57 | }
58 | @if $position == bottom {
59 | &:after {
60 | bottom: 0;
61 | }
62 | } @else {
63 | &:after {
64 | top:0;
65 | }
66 | }
67 |
68 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5) {
69 | &::after {
70 | -webkit-transform: scaleY(.7);
71 | -webkit-transform-origin: 0 0;
72 | transform: scaleY(.7);
73 | }
74 | &::after {
75 | -webkit-transform-origin: left bottom;
76 | }
77 | }
78 | @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
79 | &::after {
80 | -webkit-transform: scaleY(.5);
81 | transform: scaleY(.5);
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/src/assets/images/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/assets/images/1.jpg
--------------------------------------------------------------------------------
/src/assets/images/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/assets/images/2.jpg
--------------------------------------------------------------------------------
/src/assets/images/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/assets/images/3.jpg
--------------------------------------------------------------------------------
/src/assets/images/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/assets/images/4.jpg
--------------------------------------------------------------------------------
/src/assets/images/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/assets/images/5.jpg
--------------------------------------------------------------------------------
/src/assets/images/sprite.0.50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/assets/images/sprite.0.50.png
--------------------------------------------------------------------------------
/src/assets/js/imgError.js:
--------------------------------------------------------------------------------
1 | const imgError = (e) => {
2 | e.target.src = 'http://www.zwdu.com/files/article/image/20/20962/20962s.jpg'
3 | }
4 |
5 | export default imgError
--------------------------------------------------------------------------------
/src/assets/js/local.js:
--------------------------------------------------------------------------------
1 | const localEvent = {
2 | StorageGetter: function (key) {
3 | return JSON.parse(localStorage.getItem(key))
4 | },
5 | StorageSetter: function (key, val) {
6 | return localStorage.setItem(key, JSON.stringify(val))
7 | }
8 | }
9 |
10 | export default localEvent
11 |
--------------------------------------------------------------------------------
/src/components/About/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 |
3 | import './style.css'
4 |
5 | export default class About extends Component {
6 | constructor() {
7 | super()
8 |
9 | this.state = {
10 | page: 1,
11 | aaa: 'aaa',
12 | input: ''
13 | }
14 | }
15 |
16 | addPage() {
17 | this.setState({
18 | page: this.state.page + 1
19 | })
20 | }
21 |
22 | componentWillUpdate(props, state) {
23 | if (this.state.aaa === state.aaa) {
24 | console.log(true)
25 | }
26 | }
27 |
28 | onChangeHandler(test, e) {
29 | this.setState({
30 | input:e.target.value
31 | })
32 | console.log(e)
33 | console.log(test)
34 | }
35 |
36 | render() {
37 | return (
38 |
39 |
40 |
41 | {this.state.input}
42 |
43 |
44 |
45 | )
46 | }
47 | }
--------------------------------------------------------------------------------
/src/components/About/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/About/style.css
--------------------------------------------------------------------------------
/src/components/App/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import './style.css';
4 |
5 | class App extends Component {
6 | render() {
7 | return (
8 |
9 | {this.props.children}
10 |
11 | );
12 | }
13 | }
14 |
15 | export default App;
16 |
--------------------------------------------------------------------------------
/src/components/App/style.css:
--------------------------------------------------------------------------------
1 | /*
2 | .App {
3 | text-align: center;
4 | }
5 |
6 | .App-logo {
7 | animation: App-logo-spin infinite 20s linear;
8 | height: 80px;
9 | }
10 |
11 | .App-header {
12 | background-color: #222;
13 | height: 150px;
14 | padding: 20px;
15 | color: white;
16 | }
17 |
18 | .App-intro {
19 | font-size: large;
20 | .active {
21 | color: red;
22 | }
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from {
27 | transform: rotate(0deg);
28 | }
29 | to {
30 | transform: rotate(360deg);
31 | }
32 | }*/
33 |
--------------------------------------------------------------------------------
/src/components/App/style.scss:
--------------------------------------------------------------------------------
1 | /*
2 | .App {
3 | text-align: center;
4 | }
5 |
6 | .App-logo {
7 | animation: App-logo-spin infinite 20s linear;
8 | height: 80px;
9 | }
10 |
11 | .App-header {
12 | background-color: #222;
13 | height: 150px;
14 | padding: 20px;
15 | color: white;
16 | }
17 |
18 | .App-intro {
19 | font-size: large;
20 | .active {
21 | color: red;
22 | }
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from {
27 | transform: rotate(0deg);
28 | }
29 | to {
30 | transform: rotate(360deg);
31 | }
32 | }*/
33 |
--------------------------------------------------------------------------------
/src/components/BookDetail/Rating.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export default class Rating extends Component {
5 | static propTypes = {
6 | score: PropTypes.number
7 | }
8 |
9 | itemClasses() {
10 | let result = []
11 | let score = Math.floor(this.props.score * 2) / 2
12 | let hasDecimal = score % 1 !== 0
13 | let integer = Math.floor(score)
14 | for (var i = 0; i < integer; i++) {
15 | result.push('on')
16 | }
17 | if (hasDecimal) {
18 | result.push('half')
19 | }
20 | while (result.length < 5) {
21 | result.push('off')
22 | }
23 | return result
24 | }
25 |
26 | render() {
27 | return (
28 |
29 | {this.itemClasses().map((item, idx) =>
30 |
31 | )}
32 | {this.props.score}
33 |
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/BookDetail/Similar.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | // import {hashHistory} from 'react-router'
4 | import imgError from '../../assets/js/imgError'
5 |
6 | class Similar extends Component {
7 | constructor(props) {
8 | super(props)
9 | this.state = {
10 | bookDetail: {}
11 | }
12 | }
13 |
14 | static propTypes = {
15 | like: PropTypes.string.isRequired,
16 | api: PropTypes.string.isRequired
17 | }
18 |
19 | static contextTypes = {
20 | router: PropTypes.object
21 | }
22 |
23 | componentDidMount() {
24 | this.getBookDetail(this.props.like)
25 | }
26 |
27 | getBookDetail(id) {
28 | fetch(`${this.props.api}/booklist?id=${id}`)
29 | .then(res => res.json())
30 | .then(res => {
31 | this.setState({
32 | bookDetail: res
33 | })
34 | })
35 | }
36 |
37 | toBookDetail(id) {
38 | // hashHistory.push('/bookdetail/' + id)
39 | this.context.router.push('/bookdetail/' + id)
40 | }
41 |
42 | render() {
43 | const bookDetail = this.state.bookDetail
44 | return (
45 |
46 |
47 |

48 |
49 |
{bookDetail.name}
50 |
51 | );
52 | }
53 | }
54 |
55 |
56 | export default Similar
--------------------------------------------------------------------------------
/src/components/BookDetail/index.css:
--------------------------------------------------------------------------------
1 | .book-detail {
2 | padding: 0 0.4rem; }
3 |
4 | .loading {
5 | position: fixed;
6 | top: 0;
7 | left: 0;
8 | bottom: 0;
9 | right: 0;
10 | z-index: 999;
11 | background-color: #fff;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center; }
15 |
16 | .detail-linear {
17 | background: -webkit-linear-gradient(bottom, #fff, rgba(255, 255, 255, 0) 2.16rem) no-repeat center bottom;
18 | background: linear-gradient(to top, #fff, rgba(255, 255, 255, 0) 8rem) no-repeat center bottom; }
19 | .detail-linear .detail-top {
20 | position: fixed;
21 | display: flex;
22 | justify-content: space-between;
23 | left: 0;
24 | top: 0;
25 | right: 0;
26 | height: 1rem;
27 | background-color: #eee; }
28 | .detail-linear .detail-top a:first-of-type {
29 | flex: 1; }
30 | .detail-linear .detail-top a:first-of-type .iconfont {
31 | margin-right: 0.1rem;
32 | font-size: 0.36rem;
33 | color: #ed424b; }
34 | .detail-linear .detail-top h2 {
35 | margin: 0 0.4rem;
36 | font-size: 0.36rem;
37 | line-height: 1rem;
38 | color: #ed424b; }
39 | .detail-linear .detail-con {
40 | display: flex;
41 | margin-top: 1rem;
42 | padding: 0.3rem 0 0.36rem; }
43 | .detail-linear .detail-con .detail-img {
44 | width: 2rem;
45 | margin-right: 0.5rem; }
46 | .detail-linear .detail-con .detail-img img {
47 | width: 100%; }
48 | .detail-linear .detail-con .detail-main {
49 | flex: 1; }
50 | .detail-linear .detail-con .detail-main h3 {
51 | font-size: 0.36rem;
52 | line-height: 1.5;
53 | overflow: hidden;
54 | -ms-text-overflow: ellipsis;
55 | text-overflow: ellipsis;
56 | white-space: nowrap; }
57 | .detail-linear .detail-con .detail-main p {
58 | font-size: 0.28rem;
59 | line-height: 1.8;
60 | overflow: hidden;
61 | -ms-text-overflow: ellipsis;
62 | text-overflow: ellipsis;
63 | white-space: nowrap; }
64 | .detail-linear .read-btn {
65 | display: flex; }
66 | .detail-linear .read-btn > div {
67 | flex: 1;
68 | padding-bottom: 0.4rem;
69 | border-bottom: 1px solid #ddd; }
70 | .detail-linear .read-btn > div:first-child {
71 | margin-right: 0.3rem; }
72 | .detail-linear .read-btn > div button {
73 | display: block;
74 | margin: 0 auto;
75 | width: 100%;
76 | height: 0.66rem;
77 | line-height: 0.66rem;
78 | font-size: 0.3rem;
79 | vertical-align: middle;
80 | border: none;
81 | border-radius: 0.06rem; }
82 | .detail-linear .read-btn > div button:focus {
83 | outline: none; }
84 | .detail-linear .read-btn > div button.added {
85 | background-color: #f3f3f3 !important;
86 | cursor: not-allowed; }
87 | .detail-linear .read-btn > div:first-of-type button {
88 | background-color: #ed424b; }
89 | .detail-linear .read-btn > div:first-of-type button a {
90 | color: #fff; }
91 | .detail-linear .read-btn > div:nth-child(2) button {
92 | color: #333;
93 | background-color: #fff;
94 | border: 1px solid #ddd; }
95 |
96 | .home-btn {
97 | padding: 0px 0.3rem;
98 | display: flex;
99 | justify-content: center;
100 | align-items: center; }
101 | .home-btn .iconfont {
102 | font-size: 0.44rem;
103 | color: #ed424b; }
104 |
105 | .detail-intro {
106 | padding: 0.4rem 0;
107 | font-size: 0.32rem;
108 | text-indent: 2em;
109 | line-height: 1.6;
110 | border-bottom: 1px solid #ddd; }
111 | .detail-intro p.show5 {
112 | overflow: hidden;
113 | text-overflow: ellipsis;
114 | display: -webkit-box;
115 | line-clamp: 5;
116 | -webkit-line-clamp: 5;
117 | -webkit-box-orient: vertical; }
118 |
119 | .detail-tag {
120 | padding: 0.4rem 0;
121 | border-bottom: 1px solid #ddd; }
122 | .detail-tag h3 {
123 | font-size: 0.32rem;
124 | margin-bottom: 0.2rem; }
125 | .detail-tag ul li {
126 | float: left;
127 | padding: 0.06rem 0.2rem;
128 | margin-right: 0.2rem;
129 | color: #333;
130 | border: 1px solid #ccc;
131 | border-radius: 0.1rem;
132 | font-size: 0.28rem; }
133 |
134 | .detail-like {
135 | padding: 0.4rem 0; }
136 | .detail-like h3 {
137 | font-size: 0.32rem;
138 | margin-bottom: 0.4rem; }
139 | .detail-like .like-list {
140 | display: flex; }
141 | .detail-like .like-list li {
142 | flex: 1;
143 | margin-right: 0.1rem; }
144 |
145 | .similar {
146 | height: 3.3rem; }
147 | .similar .similar-img {
148 | height: 2.8rem; }
149 | .similar img {
150 | width: 100%;
151 | height: 100%; }
152 | .similar img[src=""] {
153 | opacity: 0; }
154 | .similar p {
155 | margin-top: 0.1rem;
156 | font-size: 0.28rem; }
157 |
158 | .rate-score {
159 | display: flex;
160 | align-items: center;
161 | font-size: 16px; }
162 | .rate-score .star-item {
163 | display: inline-block;
164 | width: 20px;
165 | height: 20px;
166 | background-size: 100% 100%; }
167 | .rate-score .star-item.on {
168 | background: url("./star_on.png"); }
169 | .rate-score .star-item.half {
170 | background: url("./star_half.png"); }
171 | .rate-score .star-item.off {
172 | background: url("./star_off.png"); }
173 | .rate-score .star-item:last-of-type {
174 | margin-right: 10px; }
175 |
--------------------------------------------------------------------------------
/src/components/BookDetail/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import {connect} from 'react-redux'
4 | import {Link} from 'react-router'
5 | import localEvent from '../../assets/js/local'
6 |
7 | import Similar from './Similar'
8 | import imgError from '../../assets/js/imgError'
9 | import Loading from '../Loading'
10 | import Rating from './Rating'
11 |
12 | import './index.css'
13 |
14 |
15 | class BookDetail extends Component {
16 | constructor() {
17 | super()
18 |
19 | this.state = {
20 | loading: false,
21 | bookDetail: {},
22 | likes: [], //相似推荐
23 | showmore: false, //简介显示更多,
24 | hasRead: false, //是否有阅读进度
25 | shelf: [], //书架列表
26 | addShelf: false //是否在书架中
27 | }
28 | }
29 |
30 | static contextTypes = {
31 | router: PropTypes.object
32 | }
33 |
34 | componentDidMount() {
35 | const id = this.props.params.id
36 | this.getBookDetail(id)
37 | this.setHasRead(id)
38 | //从localStorage获取书架信息保存至shelf,然后判断当前书籍是否在书架中,是则设置addShelf为true
39 | if (localEvent.StorageGetter('bookShelf')) {
40 | this.setState({
41 | shelf: localEvent.StorageGetter('bookShelf')
42 | }, () => {
43 | if (this.state.shelf.find(el => el.id === +id)) {
44 | this.setState({addShelf: true})
45 | }
46 | })
47 | }
48 | }
49 |
50 | componentWillUpdate(nextProps) {
51 | //点击底部相似书籍,监听书籍的id变化并获取内容
52 | const pid = nextProps.params.id
53 | if (pid !== this.props.params.id) {
54 | this.getBookDetail(pid)
55 | this.setHasRead(pid)
56 | }
57 | }
58 |
59 | getBookDetail(bookId) {
60 | this.setState({
61 | loading: true
62 | })
63 | fetch(`${this.props.api}/booklist?id=${bookId}`)
64 | .then(res => res.json())
65 | .then((res) => {
66 | this.setState({
67 | loading: false,
68 | bookDetail: res,
69 | likes: res.like.split('-')
70 | })
71 | })
72 | }
73 |
74 | //判断是否有阅读进度
75 | setHasRead(id) {
76 | this.setState({
77 | hasRead: false
78 | }, () => {
79 | const info = localEvent.StorageGetter('bookreaderinfo')
80 | if (info[id]) {
81 | this.setState({
82 | hasRead: true
83 | })
84 | }
85 | })
86 | }
87 |
88 | goBack = () => {
89 | this.context.router.goBack()
90 | }
91 |
92 | goHome = () => {
93 | this.context.router.push('/')
94 | }
95 |
96 | //书籍简介切换显示5行
97 | toggleMore = () => {
98 | this.setState(prevState => ({
99 | showmore: !prevState.showmore
100 | }))
101 | }
102 |
103 | //加入书架
104 | addToShelf = () => {
105 | const detail = this.state.bookDetail
106 | if (this.state.shelf.find(el => el.id === detail.id)) {
107 | return
108 | }
109 | const bookInfo = {
110 | id: detail.id,
111 | name: detail.name,
112 | author: detail.author,
113 | images: detail.images,
114 | recent: '', //阅读进度
115 | checked: false //判断删除时是否选中
116 | }
117 | this.state.shelf.push(bookInfo)
118 | localEvent.StorageSetter('bookShelf', this.state.shelf)
119 | this.setState({addShelf: true})
120 | }
121 |
122 |
123 | render() {
124 | const {bookDetail, showmore, loading, likes, hasRead, addShelf} = this.state
125 | const id = this.props.params.id
126 | const style5 = {'WebkitBoxOrient': 'vertical'}
127 | return (
128 |
129 | {loading &&
}
130 | {!loading &&
131 |
132 |
133 |
139 |
140 |
141 |

142 |
143 |
144 |
{bookDetail.name}
145 |
作者:{bookDetail.author}
146 |
分类:{bookDetail.type}
147 |
{bookDetail.wordcount}万字
148 |
149 |
150 |
151 |
152 |
153 |
156 |
157 |
162 |
163 |
164 |
165 |
166 |
{bookDetail.intro}
170 |
171 |
172 |
类别标签
173 |
174 | -
175 | {bookDetail.type}
176 |
177 | -
178 | 东方玄幻
179 |
180 |
181 |
182 |
183 |
喜欢本书的人也喜欢
184 |
185 | {likes.map((item, idx) =>
186 | -
187 |
188 |
189 | )}
190 |
191 |
192 |
193 |
}
194 |
195 | )
196 | }
197 | }
198 |
199 |
200 | const mapStateToProps = (state) => ({
201 | api: state.api
202 | })
203 |
204 | export default connect(mapStateToProps)(BookDetail)
205 |
--------------------------------------------------------------------------------
/src/components/BookDetail/index.scss:
--------------------------------------------------------------------------------
1 | @import "../../assets/css/util.scss";
2 |
3 | .book-detail {
4 | padding: 0 pr(20);
5 | }
6 |
7 | .loading {
8 | position: fixed;
9 | top: 0;
10 | left: 0;
11 | bottom: 0;
12 | right: 0;
13 | z-index: 999;
14 | background-color: #fff;
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | .detail-linear {
21 | background: -webkit-linear-gradient(bottom, #fff, rgba(255, 255, 255, 0) pr(108)) no-repeat center bottom;
22 | background: linear-gradient(to top, #fff, rgba(255, 255, 255, 0) 8rem) no-repeat center bottom;
23 | .detail-top {
24 | position: fixed;
25 | display: flex;
26 | justify-content:space-between;
27 | left: 0;
28 | top: 0;
29 | right: 0;
30 | height: pr(50);
31 | background-color: #eee;
32 | a:first-of-type {
33 | flex: 1;
34 | .iconfont {
35 | margin-right:pr(5);
36 | font-size:pr(18);
37 | color:$red;
38 | }
39 | }
40 | h2 {
41 | margin: 0 pr(20);
42 | font-size: pr(18);
43 | line-height: pr(50);
44 | color: #ed424b;
45 | }
46 | }
47 | .detail-con {
48 | display: flex;
49 | margin-top: pr(50);
50 | padding: pr(15) 0 pr(18);
51 | .detail-img {
52 | width: pr(100);
53 | margin-right: pr(25);
54 | img {
55 | width: 100%;
56 | }
57 | }
58 | .detail-main {
59 | flex: 1;
60 | h3 {
61 | font-size: pr(18);
62 | line-height: 1.5;
63 | @include ell;
64 | }
65 | p {
66 | font-size: pr(14);
67 | line-height: 1.8;
68 | @include ell;
69 | }
70 | }
71 | }
72 | .read-btn {
73 | display: flex;
74 | > div {
75 | flex: 1;
76 | padding-bottom: pr(20);
77 | border-bottom: 1px solid #ddd;
78 | &:first-child {
79 | margin-right: pr(15);
80 | }
81 | button {
82 | display: block;
83 | margin: 0 auto;
84 | width: 100%;
85 | height: pr(33);
86 | line-height: pr(33);
87 | font-size: pr(15);
88 | vertical-align: middle;
89 | border: none;
90 | border-radius: pr(3);
91 | &:focus {
92 | outline:none;
93 | }
94 | &.added {
95 | background-color: #f3f3f3 !important;
96 | cursor: not-allowed;
97 | }
98 | }
99 | &:first-of-type {
100 | button {
101 | background-color: #ed424b;
102 | a {
103 | color: #fff;
104 | }
105 | }
106 | }
107 | &:nth-child(2) {
108 | button {
109 | color: #333;
110 | background-color: #fff;
111 | border: 1px solid #ddd;
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | .home-btn {
119 | padding: 0px pr(15);
120 | display: flex;
121 | justify-content: center;
122 | align-items: center;
123 | .iconfont {
124 | font-size: pr(22);
125 | color: #ed424b;
126 | }
127 | }
128 |
129 | .detail-intro {
130 | padding: pr(20) 0;
131 | font-size: pr(16);
132 | text-indent: 2em;
133 | line-height: 1.6;
134 | border-bottom: 1px solid #ddd;
135 | p.show5 {
136 | overflow: hidden;
137 | text-overflow: ellipsis;
138 | display: -webkit-box;
139 | line-clamp: 5;
140 | -webkit-line-clamp: 5;
141 | -webkit-box-orient: vertical;
142 | }
143 | }
144 |
145 | .detail-tag {
146 | padding: pr(20) 0;
147 | border-bottom: 1px solid #ddd;
148 | h3 {
149 | font-size: pr(16);
150 | margin-bottom: pr(10);
151 | }
152 | ul li {
153 | float: left;
154 | padding: pr(3) pr(10);
155 | margin-right: pr(10);
156 | color: #333;
157 | border: 1px solid #ccc;
158 | border-radius: pr(5);
159 | font-size:pr(14);
160 | }
161 | }
162 |
163 | .detail-like {
164 | padding: pr(20) 0;
165 | h3 {
166 | font-size: pr(16);
167 | margin-bottom: pr(20);
168 | }
169 | .like-list {
170 | display: flex;
171 | li {
172 | flex: 1;
173 | margin-right:pr(5);
174 | }
175 | }
176 | }
177 |
178 | .similar {
179 | height: pr(165);
180 | .similar-img {
181 | height: pr(140);
182 | }
183 | img {
184 | width: 100%;
185 | height: 100%;
186 | &[src=""] {
187 | opacity: 0;
188 | }
189 | }
190 | p {
191 | margin-top:pr(5);
192 | font-size:pr(14);
193 | }
194 | }
195 |
196 | .rate-score {
197 | display: flex;
198 | align-items: center;
199 | font-size:16px;
200 | .star-item {
201 | display: inline-block;
202 | width: 20px;
203 | height: 20px;
204 | background-size: 100% 100%;
205 | &.on {
206 | background: url("./star_on.png");
207 | }
208 | &.half {
209 | background: url("./star_half.png");
210 | }
211 | &.off {
212 | background: url("./star_off.png");
213 | }
214 | &:last-of-type {
215 | margin-right:10px;
216 | }
217 | }
218 | }
--------------------------------------------------------------------------------
/src/components/BookDetail/star_half.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/BookDetail/star_half.png
--------------------------------------------------------------------------------
/src/components/BookDetail/star_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/BookDetail/star_off.png
--------------------------------------------------------------------------------
/src/components/BookDetail/star_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/BookDetail/star_on.png
--------------------------------------------------------------------------------
/src/components/BookShelf/ShelfItem.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class ShelfItem extends Component {
5 | static contextTypes = {
6 | router: PropTypes.object
7 | }
8 |
9 | static propTypes = {
10 | editing: PropTypes.bool.isRequired,
11 | item: PropTypes.object.isRequired,
12 | idx: PropTypes.number.isRequired,
13 | toggleCheck: PropTypes.func.isRequired
14 | }
15 |
16 | //点击右侧时,判断时候在编辑状态,不是则跳转到书籍详情,是则选中元素进行删除
17 | redirect = (idx, state) => {
18 | if (this.props.editing) {
19 | this.props.toggleCheck(idx, state)
20 | } else {
21 | this.context.router.push(`/bookdetail/${this.props.item.id}`)
22 | }
23 | }
24 |
25 | //点击按钮切换选中状态
26 | toggleCheck = (idx, state) => {
27 | this.props.toggleCheck(idx, state)
28 | }
29 |
30 | render() {
31 | const {item} = this.props
32 | return (
33 |
34 | {this.props.editing &&
35 |
36 |
43 |
}
44 |

45 |
46 |
{item.name}立即阅读
47 |
{item.author}
48 |
{item.recent ? `阅读至 ${item.recent}` : '未阅读'}
49 |
50 |
51 | )
52 | }
53 | }
54 |
55 | export default ShelfItem
--------------------------------------------------------------------------------
/src/components/BookShelf/index.css:
--------------------------------------------------------------------------------
1 | .shelf header {
2 | left: 0;
3 | top: 0;
4 | right: 0;
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: center;
8 | padding: 0 0.3rem 0 0;
9 | height: 1rem;
10 | background-color: #fff;
11 | position: relative;
12 | position: fixed; }
13 | .shelf header:after {
14 | border-top: 1px solid #c8c7cc;
15 | content: ' ';
16 | display: block;
17 | width: 100%;
18 | position: absolute;
19 | left: 0; }
20 | .shelf header:after {
21 | bottom: 0; }
22 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5) {
23 | .shelf header::after {
24 | -webkit-transform: scaleY(0.7);
25 | -webkit-transform-origin: 0 0;
26 | transform: scaleY(0.7); }
27 | .shelf header::after {
28 | -webkit-transform-origin: left bottom; } }
29 | @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
30 | .shelf header::after {
31 | -webkit-transform: scaleY(0.5);
32 | transform: scaleY(0.5); } }
33 | .shelf header > .iconfont {
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | width: 0.8rem;
38 | height: 0.8rem;
39 | color: #ed424b; }
40 | .shelf header .checkAll {
41 | margin-left: 0.3rem; }
42 |
43 | .shelf span {
44 | font-size: 0.28rem; }
45 |
46 | .shelf .shelf-content {
47 | padding-top: 1rem; }
48 |
49 | .shelf-item {
50 | display: flex;
51 | padding: 0.32rem;
52 | position: relative; }
53 | .shelf-item:after {
54 | border-top: 1px solid #c8c7cc;
55 | content: ' ';
56 | display: block;
57 | width: 100%;
58 | position: absolute;
59 | left: 0; }
60 | .shelf-item:after {
61 | bottom: 0; }
62 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5) {
63 | .shelf-item::after {
64 | -webkit-transform: scaleY(0.7);
65 | -webkit-transform-origin: 0 0;
66 | transform: scaleY(0.7); }
67 | .shelf-item::after {
68 | -webkit-transform-origin: left bottom; } }
69 | @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
70 | .shelf-item::after {
71 | -webkit-transform: scaleY(0.5);
72 | transform: scaleY(0.5); } }
73 | .shelf-item.hover {
74 | background-color: #f0f0f0; }
75 | .shelf-item .edit {
76 | width: 0.6rem; }
77 | .shelf-item .edit .label-checkbox, .shelf-item .edit .checkbox-wrap {
78 | display: flex;
79 | justify-content: center;
80 | align-items: center;
81 | width: 100%;
82 | height: 100%; }
83 | .shelf-item .edit .edit-btn {
84 | display: none; }
85 | .shelf-item .edit .edit-btn.checked + .checkbox-wrap .checkbox::after {
86 | background-color: #ed424b;
87 | border-radius: 100%;
88 | content: "";
89 | display: inline-block;
90 | height: 12px;
91 | margin: 2px;
92 | width: 12px; }
93 | .shelf-item .edit .checkbox {
94 | background-color: #fff;
95 | border: 1px solid rgba(0, 0, 0, 0.15);
96 | display: inline-block;
97 | border-radius: 50%;
98 | height: 16px;
99 | margin-right: 10px;
100 | width: 16px; }
101 | .shelf-item > img {
102 | margin-right: 0.3rem;
103 | width: 1.2rem;
104 | height: 1.5rem; }
105 | .shelf-detail {
106 | display: flex;
107 | flex-direction: column;
108 | justify-content: space-between;
109 | flex: 1;
110 | padding: 0.06rem 0; }
111 | .shelf-detail p {
112 | height: 0.3rem;
113 | font-size: 0.28rem;
114 | color: #aaa;
115 | overflow: hidden;
116 | -ms-text-overflow: ellipsis;
117 | text-overflow: ellipsis;
118 | white-space: nowrap; }
119 | .shelf-detail p.title {
120 | display: flex;
121 | justify-content: space-between;
122 | height: 0.36rem; }
123 | .shelf-detail p.title span:first-child {
124 | font-size: 0.32rem;
125 | color: #333; }
126 | .shelf-detail p > * {
127 | color: #aaa;
128 | font-size: 0.28rem; }
129 | .shelf-detail p > * .iconfont {
130 | color: #aaa; }
131 | .shelf-detail p > * .iconfont:before {
132 | display: inline-block;
133 | transform: rotate(-180deg); }
134 | .shelf-detail p .icon-yonghu {
135 | position: relative;
136 | top: -0.02rem;
137 | margin-right: 0.06rem; }
138 |
139 | .shelf footer {
140 | position: relative;
141 | position: fixed;
142 | left: 0;
143 | bottom: 0;
144 | right: 0;
145 | display: flex;
146 | align-items: center;
147 | justify-content: flex-end;
148 | height: 0.8rem; }
149 | .shelf footer:after {
150 | border-top: 1px solid #c8c7cc;
151 | content: ' ';
152 | display: block;
153 | width: 100%;
154 | position: absolute;
155 | left: 0; }
156 | .shelf footer:after {
157 | top: 0; }
158 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5) {
159 | .shelf footer::after {
160 | -webkit-transform: scaleY(0.7);
161 | -webkit-transform-origin: 0 0;
162 | transform: scaleY(0.7); }
163 | .shelf footer::after {
164 | -webkit-transform-origin: left bottom; } }
165 | @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
166 | .shelf footer::after {
167 | -webkit-transform: scaleY(0.5);
168 | transform: scaleY(0.5); } }
169 | .shelf footer .delete {
170 | width: 50%;
171 | display: flex;
172 | align-items: center;
173 | justify-content: center;
174 | font-size: 0.28rem;
175 | color: #ed424b; }
176 | .shelf footer .iconfont {
177 | margin-top: 0.08rem;
178 | margin-right: 0.04rem;
179 | font-size: 0.36rem;
180 | color: #ed424b; }
181 |
--------------------------------------------------------------------------------
/src/components/BookShelf/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import localEvent from '../../assets/js/local'
4 |
5 | import ShelfItem from './ShelfItem'
6 |
7 | import './index.css'
8 |
9 | export default class BookShelf extends Component {
10 | constructor() {
11 | super()
12 | this.state = {
13 | editing: false, //编辑状态
14 | shelfList: [], //保存书架中书籍列表
15 | delNum: 0 //删除按钮上的删除数字
16 | }
17 | }
18 |
19 | static contextTypes = {
20 | router: PropTypes.object
21 | }
22 |
23 | componentWillMount() {
24 | if (localEvent.StorageGetter('bookShelf')) {
25 | this.setState({
26 | shelfList: localEvent.StorageGetter('bookShelf')
27 | })
28 | }
29 | }
30 |
31 | //切换编辑状态
32 | toggleEdit = () => {
33 | this.setState(prevState => ({
34 | editing: !prevState.editing
35 | }))
36 | }
37 |
38 | //编辑状态下,选择时触发,给点击的元素的checked属性设为true,并获取被选中的数量
39 | toggleCheck = (idx, state) => {
40 | const shelfList = this.state.shelfList
41 | shelfList[idx].checked = state
42 | this.setState({shelfList}, () => {
43 | this.setState({
44 | delNum: this.state.shelfList.filter(item => item.checked).length
45 | })
46 | })
47 | }
48 |
49 | //全选,给shelfList的所有元素的checked设为true,再获取数组的长度
50 | checkAll = () => {
51 | let shelfList = this.state.shelfList
52 | shelfList = shelfList.map(item => ({...item,checked:true}))
53 | this.setState({shelfList}, () => {
54 | this.setState({
55 | delNum: this.state.shelfList.length
56 | })
57 | })
58 | }
59 |
60 | //通过filter,过滤掉checked属性为true的元素,达到删除的效果,并保存到localStorage中
61 | deleteBook = () => {
62 | let shelfList = this.state.shelfList
63 | shelfList = shelfList.filter(item => !item.checked)
64 | this.setState({shelfList, delNum: 0}, () => {
65 | localEvent.StorageSetter('bookShelf', this.state.shelfList)
66 | })
67 | }
68 |
69 | render() {
70 | const {editing,delNum} = this.state
71 | return (
72 |
73 |
74 | {editing ? 全选 :
75 | this.context.router.goBack()}>}
76 | {editing ? '取消' : '编辑'}
77 |
78 |
79 | {this.state.shelfList.map((item, idx) =>
80 |
81 | )}
82 |
83 | {editing &&
}
86 |
87 | )
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/BookShelf/index.scss:
--------------------------------------------------------------------------------
1 | @import "../../assets/css/util.scss";
2 |
3 | .shelf {
4 | header {
5 | left:0;
6 | top: 0;
7 | right: 0;
8 | display: flex;
9 | justify-content: space-between;
10 | align-items: center;
11 | padding: 0 pr(15) 0 0;
12 | height: pr(50);
13 | background-color: #fff;
14 | @include border-1px(bottom);
15 | position: fixed;
16 | > .iconfont {
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | width:pr(40);
21 | height:pr(40);
22 | color: $red;
23 | }
24 | .checkAll {
25 | margin-left:pr(15);
26 | }
27 | }
28 | span {
29 | font-size: pr(14);
30 | }
31 | .shelf-content {
32 | padding-top:pr(50);
33 | }
34 | @at-root .shelf-item {
35 | display: flex;
36 | padding: pr(16);
37 | @include border-1px(bottom);
38 | &.hover {
39 | background-color: #f0f0f0;
40 | }
41 | .edit {
42 | width:pr(30);
43 | .label-checkbox {
44 | @include dfcc;
45 | @include wh(100%, 100%);
46 | }
47 | .checkbox-wrap {
48 | @extend .label-checkbox;
49 | }
50 | .edit-btn {
51 | display: none;
52 | &.checked + .checkbox-wrap {
53 | .checkbox {
54 | &::after {
55 | background-color: $red;
56 | border-radius: 100%;
57 | content: "";
58 | display: inline-block;
59 | height: 12px;
60 | margin: 2px;
61 | width: 12px;
62 | }
63 | }
64 | }
65 | }
66 | .checkbox {
67 | background-color: #fff;
68 | border: 1px solid rgba(0,0,0,0.15);
69 | display: inline-block;
70 | border-radius: 50%;
71 | height: 16px;
72 | margin-right: 10px;
73 | width: 16px;
74 | }
75 | }
76 | > img {
77 | margin-right:pr(15);
78 | width: pr(60);
79 | height: pr(75);
80 | }
81 |
82 | @at-root .shelf-detail {
83 | display: flex;
84 | flex-direction: column;
85 | justify-content:space-between;
86 | flex:1;
87 | padding: pr(3) 0;
88 | p {
89 | height:pr(15);
90 | font-size:pr(14);
91 | color:#aaa;
92 | @include ell;
93 | &.title {
94 | display: flex;
95 | justify-content:space-between;
96 | height:pr(18);
97 | span:first-child {
98 | font-size:pr(16);
99 | color:#333;
100 | }
101 | }
102 | > * {
103 | color:#aaa;
104 | font-size:pr(14);
105 | .iconfont {
106 | color:#aaa;
107 | &:before {
108 | display: inline-block;
109 | transform: rotate(-180deg);
110 | }
111 | }
112 | }
113 | .icon-yonghu {
114 | position: relative;
115 | top:pr(-1);
116 | margin-right:pr(3);
117 | }
118 | }
119 | }
120 | }
121 | footer {
122 | @include border-1px(top);
123 | position: fixed;
124 | left:0;
125 | bottom:0;
126 | right:0;
127 | display: flex;
128 | align-items: center;
129 | justify-content:flex-end;
130 | height:pr(40);
131 | .delete {
132 | width:50%;
133 | display: flex;
134 | align-items: center;
135 | justify-content: center;
136 | font-size:pr(14);
137 | color: $red;
138 | }
139 | .iconfont {
140 | margin-top:pr(4);
141 | margin-right:pr(2);
142 | font-size:pr(18);
143 | color: $red;
144 | }
145 | }
146 | }
147 |
148 |
149 |
--------------------------------------------------------------------------------
/src/components/Category/index.css:
--------------------------------------------------------------------------------
1 | .category {
2 | background-color: #f6f7f9; }
3 |
4 | .category-header {
5 | height: 1rem;
6 | background-color: #eee; }
7 | .category-header h2 {
8 | margin: 0 0.4rem;
9 | font-size: 0.36rem;
10 | line-height: 1rem;
11 | color: #ed424b; }
12 | .category-header h2 .iconfont {
13 | color: #ed424b;
14 | margin-right: 0.1rem;
15 | font-size: 0.32rem; }
16 |
17 | .category-list {
18 | margin-top: 0.3rem;
19 | padding: 0.3rem;
20 | background-color: #fff; }
21 | .category-list ul li {
22 | display: flex;
23 | padding-bottom: 0.2rem;
24 | margin-bottom: 0.28rem;
25 | border-bottom: 1px solid #ddd; }
26 | .category-list ul li a {
27 | display: flex; }
28 | .category-list ul li .book-image {
29 | width: 1.6rem; }
30 | .category-list ul li .book-image img {
31 | width: 100%; }
32 | .category-list ul li .book-detail {
33 | position: relative;
34 | flex: 1;
35 | padding: 0;
36 | margin-left: 0.4rem; }
37 | .category-list ul li .book-detail h3 {
38 | font-size: 0.36rem;
39 | margin-bottom: 0.2rem; }
40 | .category-list ul li .book-detail p {
41 | line-height: 1.3;
42 | max-height: 3.8em;
43 | overflow: hidden;
44 | text-overflow: ellipsis;
45 | display: -webkit-box;
46 | line-clamp: 2;
47 | font-size: 0.28rem;
48 | color: #969ba3;
49 | -webkit-line-clamp: 2;
50 | -webkit-box-orient: vertical; }
51 | .category-list ul li .book-detail .author {
52 | display: flex;
53 | align-items: center;
54 | position: absolute;
55 | left: 0rem;
56 | bottom: 0.1rem;
57 | color: #969ba3;
58 | font-size: 0.26rem; }
59 | .category-list ul li .book-detail .author > * {
60 | color: #969ba3; }
61 | .category-list ul li .book-detail .author .iconfont {
62 | margin-right: 0.06rem;
63 | font-size: 0.24rem; }
64 | .category-list ul li .book-detail .category-r {
65 | position: absolute;
66 | right: 0;
67 | bottom: 0.1rem;
68 | float: right;
69 | color: #969ba3;
70 | font-size: 0.2rem; }
71 | .category-list ul li .book-detail .category-r span {
72 | color: #969ba3;
73 | border: 1px solid #ccc;
74 | border-radius: 0.04rem;
75 | padding: 0 0.04rem; }
76 | .category-list ul li .book-detail .category-r span:nth-child(2) {
77 | color: #ed424b; }
78 | .category-list ul li .book-detail .category-r span:nth-child(3) {
79 | color: #4284ed; }
80 |
--------------------------------------------------------------------------------
/src/components/Category/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import {connect} from 'react-redux'
4 | import {Link} from 'react-router'
5 | import Loading from '../Loading'
6 | import imgError from '../../assets/js/imgError'
7 |
8 | import './index.css'
9 |
10 | class Category extends Component {
11 | constructor() {
12 | super()
13 |
14 | this.state = {
15 | categoryList: [],
16 | loading: false
17 | }
18 | }
19 |
20 | static contextTypes = {
21 | router: PropTypes.object
22 | }
23 |
24 | componentDidMount() {
25 | this.getCategory(this.props.location.query.type)
26 | }
27 |
28 | getCategory(type) {
29 | this.setState({
30 | loading: true
31 | })
32 | fetch(`${this.props.api}/type?type=${type}`)
33 | .then(res => res.json())
34 | .then(res => {
35 | // this.loading = false
36 | this.setState({
37 | categoryList: res,
38 | loading: false
39 | })
40 | })
41 | }
42 |
43 | title() {
44 | switch (this.props.location.query.type) {
45 | case '1':
46 | return '玄幻'
47 | case '2':
48 | return '修真'
49 | case '3':
50 | return '都市'
51 | case '4':
52 | return '历史'
53 | case '5':
54 | return '网游'
55 | default:
56 | return '分类'
57 | }
58 | }
59 |
60 | goBack = () => {
61 | this.context.router.goBack()
62 | }
63 |
64 | render() {
65 | const {loading} = this.state
66 | return (
67 |
68 | {loading &&
}
69 | {!loading &&
70 |
}
75 | {!loading &&
76 | < div className="category-list">
77 | < ul>
78 | {this.state.categoryList.map((item, idx) =>
79 |
80 |
81 |
82 |

83 |
84 |
85 |
{item.name}
86 |
{item.intro}
87 |
88 |
89 | {item.author}
90 |
91 |
92 | {this.title()}
93 | {item.serialize}
94 | {item.wordcount}万字
95 |
96 |
97 |
98 |
99 | )}
100 |
101 |
}
102 |
103 | )
104 | }
105 | }
106 |
107 | const mapStateToProps = (state) => ({
108 | api: state.api
109 | })
110 |
111 | export default connect(mapStateToProps)(Category)
112 |
--------------------------------------------------------------------------------
/src/components/Category/index.scss:
--------------------------------------------------------------------------------
1 | @import "../../assets/css/util.scss";
2 |
3 | .category {
4 | background-color: #f6f7f9;
5 | }
6 |
7 | .category-header {
8 | height: pr(50);
9 | background-color: #eee;
10 | h2 {
11 | margin: 0 pr(20);
12 | font-size: pr(18);
13 | line-height: pr(50);
14 | color: $red;
15 | .iconfont {
16 | color:$red;
17 | margin-right:pr(5);
18 | font-size:pr(16);
19 | }
20 | }
21 | }
22 |
23 | .category-list {
24 | margin-top: pr(15);
25 | padding: pr(15);
26 | background-color: #fff;
27 |
28 | ul li {
29 | display: flex;
30 | padding-bottom: pr(10);
31 | margin-bottom: pr(14);
32 | border-bottom: 1px solid #ddd;
33 | a {
34 | display: flex;
35 | }
36 | .book-image {
37 | width: pr(80);
38 | img {
39 | width: 100%;
40 | }
41 | }
42 | .book-detail {
43 | position: relative;
44 | flex: 1;
45 | padding: 0;
46 | margin-left: pr(20);
47 | h3 {
48 | font-size: pr(18);
49 | margin-bottom: pr(10);
50 | }
51 | p {
52 | line-height:1.3;
53 | max-height:3.8em;
54 | overflow: hidden;
55 | text-overflow: ellipsis;
56 | display: -webkit-box;
57 | line-clamp: 2;
58 | font-size: pr(14);
59 | color: #969ba3;
60 | -webkit-line-clamp: 2;
61 | -webkit-box-orient: vertical;
62 | }
63 | .author {
64 | display: flex;
65 | align-items: center;
66 | position: absolute;
67 | left: pr(0);
68 | bottom: pr(5);
69 | color: #969ba3;
70 | font-size: pr(13);
71 | > * {
72 | color:#969ba3;
73 | }
74 | .iconfont {
75 | margin-right:pr(3);
76 | font-size:pr(12);
77 | }
78 | }
79 | .category-r {
80 | position: absolute;
81 | right: 0;
82 | bottom: pr(5);
83 | float: right;
84 | color: #969ba3;
85 | font-size: pr(10);
86 | span {
87 | color: #969ba3;
88 | border: 1px solid #ccc;
89 | border-radius: pr(2);
90 | padding: 0 pr(2);
91 | }
92 | span:nth-child(2) {
93 | color: #ed424b;
94 | }
95 | span:nth-child(3) {
96 | color: #4284ed;
97 | }
98 | }
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/src/components/Home/BookList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Title from './Title'
4 | import {Link} from 'react-router'
5 |
6 | const Recommend = (props) =>
7 |
8 |
9 |
10 | {props.booklist.map((item, idx) =>
11 | -
12 |
13 |
14 |

15 |
16 |
17 |
{item.name}
18 |
{item.intro}
19 |
20 |
21 | {item.author}
22 |
23 |
24 | {item.type}
25 | {item.serialize}
26 | {item.wordcount}万字
27 |
28 |
29 |
30 |
31 | )}
32 |
33 |
34 |
35 | Title.propTypes = {
36 | booklist: PropTypes.array, //加上isRequired会报错
37 | title: PropTypes.string.isRequired
38 | }
39 |
40 | export default Recommend
--------------------------------------------------------------------------------
/src/components/Home/Recommend.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Title from './Title'
4 | import {Link} from 'react-router'
5 |
6 | const Recommend = (props) =>
7 |
8 |
9 |
10 |
11 | {props.booklist.map((item, idx) =>
12 | -
13 |
14 |
15 | {item.name}
16 | {item.author}
17 |
18 |
19 | )}
20 |
21 |
22 |
23 |
24 | Title.propTypes = {
25 | booklist: PropTypes.array, //加上isRequired会报错
26 | title: PropTypes.string.isRequired
27 | }
28 |
29 | export default Recommend
--------------------------------------------------------------------------------
/src/components/Home/Swiper.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import Swiper from 'react-slick'
3 |
4 | import img1 from './images/1.jpg'
5 | import img2 from './images/2.jpg'
6 | import img3 from './images/3.jpg'
7 | import img4 from './images/4.jpg'
8 | import img5 from './images/5.jpg'
9 |
10 | export default class Swipe extends Component {
11 | render() {
12 | const params = {
13 | dots: true,
14 | infinite: true,
15 | speed: 500,
16 | slidesToShow: 1,
17 | slidesToScroll: 1,
18 | autoplay: true,
19 | autoplaySpeed: 3000
20 | }
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Home/Title.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Title = (props) =>
5 |
6 |
{props.title}
7 |
8 |
9 | Title.propTypes = {
10 | title: PropTypes.string.isRequired
11 | }
12 |
13 | export default Title
--------------------------------------------------------------------------------
/src/components/Home/images/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/Home/images/1.jpg
--------------------------------------------------------------------------------
/src/components/Home/images/1.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/Home/images/1.js
--------------------------------------------------------------------------------
/src/components/Home/images/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/Home/images/2.jpg
--------------------------------------------------------------------------------
/src/components/Home/images/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/Home/images/3.jpg
--------------------------------------------------------------------------------
/src/components/Home/images/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/Home/images/4.jpg
--------------------------------------------------------------------------------
/src/components/Home/images/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/Home/images/5.jpg
--------------------------------------------------------------------------------
/src/components/Home/index.css:
--------------------------------------------------------------------------------
1 | .home-header {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | height: 0.9rem;
6 | font-size: 0.5rem;
7 | padding: 0 0.4rem; }
8 | .home-header img {
9 | width: 1.8rem; }
10 | .home-header .iconfont {
11 | font-size: 0.4rem;
12 | color: #ed424b; }
13 |
14 | .home .slick-initialized.slick-slider {
15 | overflow: hidden; }
16 |
17 | .home .slick-list img {
18 | width: 100%; }
19 |
20 | .home .slick-dots li {
21 | margin: 0 2px; }
22 |
23 | .home .slick-dots li.slick-active button:before {
24 | opacity: 1;
25 | color: #ed424b; }
26 |
27 | .home .slick-dots li button:before {
28 | font-size: 0.7rem;
29 | font-family: Arial;
30 | opacity: 1;
31 | color: #ddd; }
32 |
33 | .home .slick-dots {
34 | bottom: 0; }
35 |
36 | .home .title h3 {
37 | text-indent: 0.2rem;
38 | border-left: 3px solid #ed424b;
39 | font-size: 0.32rem; }
40 |
41 | .home .home-nav {
42 | display: flex;
43 | padding: 10px 0;
44 | margin: 10px 0;
45 | background-color: #fff; }
46 |
47 | .home .guide-nav-div {
48 | flex: 1;
49 | display: flex;
50 | flex-direction: column;
51 | justify-content: center;
52 | align-items: center; }
53 | .home .guide-nav-div > i {
54 | width: 24px;
55 | height: 24px;
56 | background-image: url(../../assets/images/sprite.0.50.png); }
57 | .home .guide-nav-div:nth-of-type(1) i {
58 | background-position: -63px -28px; }
59 | .home .guide-nav-div:nth-of-type(2) i {
60 | background-position: 0 0; }
61 | .home .guide-nav-div:nth-of-type(3) i {
62 | background-position: 0 -30px; }
63 | .home .guide-nav-div:nth-of-type(4) i {
64 | background-position: 0 -60px; }
65 | .home .guide-nav-div:nth-of-type(5) i {
66 | background-position: -30px -30px; }
67 | .home .guide-nav-div h4 {
68 | font-size: 14px;
69 | margin: 10px 0;
70 | color: #333; }
71 |
72 | .recommend {
73 | padding: 0.3rem 0;
74 | margin-bottom: 0.3rem;
75 | background-color: #fff; }
76 | .recommend .title {
77 | margin-left: 0.3rem;
78 | margin-bottom: 0.2rem;
79 | border-left: 2px solid #ed424b;
80 | text-indent: 0.1rem;
81 | font-size: 0.32rem;
82 | line-height: 0.32rem; }
83 | .recommend .list .list-ul {
84 | position: relative;
85 | overflow-x: auto;
86 | overflow-y: hidden;
87 | white-space: nowrap;
88 | text-indent: 0.14rem; }
89 | .recommend .list .list-ul .list-li {
90 | display: inline-block;
91 | margin-right: 0.16rem;
92 | width: 2rem;
93 | white-space: normal; }
94 | .recommend .list .list-ul .list-li img {
95 | width: 100%;
96 | height: 2.5rem; }
97 | .recommend .list .list-ul .list-li p {
98 | font-size: 0.28rem;
99 | overflow: hidden;
100 | text-overflow: ellipsis;
101 | white-space: nowrap;
102 | line-height: 1.2; }
103 |
104 | .book-list {
105 | margin-top: 0.3rem;
106 | padding: 0.3rem;
107 | background-color: #fff; }
108 | .book-list .title {
109 | margin-left: 0px;
110 | margin-bottom: 0.2rem;
111 | border-left: 2px solid #ed424b;
112 | text-indent: 0.1rem;
113 | font-size: 0.32rem;
114 | line-height: 0.32rem; }
115 | .book-list ul li {
116 | display: flex;
117 | padding-bottom: 0.2rem;
118 | margin-bottom: 0.28rem;
119 | border-bottom: 1px solid #ddd; }
120 | .book-list ul li:last-of-type {
121 | border-bottom: none; }
122 | .book-list ul li a {
123 | display: flex; }
124 | .book-list ul li .book-image {
125 | width: 1.6rem; }
126 | .book-list ul li .book-image img {
127 | width: 100%; }
128 | .book-list ul li .book-detail {
129 | position: relative;
130 | flex: 1;
131 | padding: 0;
132 | margin-left: 0.4rem; }
133 | .book-list ul li .book-detail h3 {
134 | font-size: 0.36rem;
135 | margin-bottom: 0.2rem; }
136 | .book-list ul li .book-detail p {
137 | line-height: 1.5em;
138 | max-height: 2.8em;
139 | overflow: hidden;
140 | text-overflow: ellipsis;
141 | display: -webkit-box;
142 | line-clamp: 2;
143 | font-size: 0.28rem;
144 | color: #969ba3;
145 | -webkit-box-orient: vertical;
146 | -webkit-line-clamp: 2;
147 | -webkit-box-orient: vertical;
148 | -webkit-box-orient: vertical; }
149 | .book-list ul li .book-detail .author {
150 | position: absolute;
151 | left: 0px;
152 | bottom: 0.1rem;
153 | color: #969ba3;
154 | font-size: 0.26rem; }
155 | .book-list ul li .book-detail .author > * {
156 | color: #969ba3; }
157 | .book-list ul li .book-detail .category-r {
158 | position: absolute;
159 | right: 0;
160 | bottom: 0.1rem;
161 | float: right;
162 | color: #969ba3;
163 | font-size: 0.2rem; }
164 | .book-list ul li .book-detail .category-r span {
165 | border: 1px solid #ccc;
166 | border-radius: 0.04rem;
167 | padding: 0 0.04rem; }
168 | .book-list ul li .book-detail .category-r span:nth-child(2) {
169 | color: #ed424b; }
170 | .book-list ul li .book-detail .category-r span:nth-child(3) {
171 | color: #4284ed; }
172 |
--------------------------------------------------------------------------------
/src/components/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import {connect} from 'react-redux'
4 | import {Link} from 'react-router'
5 |
6 | import Swiper from './Swiper'
7 | import './index.css'
8 | import Recommend from './Recommend'
9 | import BookList from './BookList'
10 | import Loading from '../Loading'
11 |
12 | class Home extends Component {
13 | constructor() {
14 | super()
15 | this.state = {
16 | type: ['玄幻', '修真', '都市', '历史', '游戏'],
17 | booklist: [],
18 | loading: false
19 | }
20 | }
21 |
22 | static contextTypes = {
23 | router: PropTypes.object
24 | }
25 |
26 | componentDidMount() {
27 | this.getData()
28 | }
29 |
30 | getData() {
31 | this.setState({
32 | loading: true
33 | })
34 | fetch(`${this.props.api}/booklist`)
35 | .then(res => res.json())
36 | .then(res => {
37 | this.setState({
38 | booklist: res,
39 | loading: false
40 | })
41 | })
42 | }
43 |
44 | //简单过滤列表,显示不同书籍
45 | filters(list, type) {
46 | if (!list) return ''
47 | switch (type) {
48 | case 'hot':
49 | return list.filter((item, index) => index < 20 && index % 2 === 1)
50 | case 'top':
51 | return list.filter((item, index) => index < 20 && index % 2 === 0)
52 | case 'free':
53 | return list.filter((item, index) => index < 20 && index % 3 === 2)
54 | case 'new':
55 | return list.filter((item, index) => index % 3 === 1).splice(0, 3)
56 | case 'end':
57 | return list.filter((item, index) => item.serialize === '完本')
58 | case 'like':
59 | return list.filter((item, index) => index % 4 === 2).splice(0, 3)
60 | default:
61 | list.splice(0, 3)
62 | }
63 | }
64 |
65 | render() {
66 | const {booklist, loading} = this.state
67 | return (
68 |
69 | {loading &&
}
70 |
71 |
72 |

73 |
this.context.router.push(`/bookshelf`)}>
74 |
75 |
76 |
86 | {!loading &&
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
}
95 |
96 |
97 | )
98 | }
99 | }
100 |
101 | const mapStateToProps = (state) => ({
102 | api: state.api
103 | })
104 |
105 | export default connect(mapStateToProps)(Home)
--------------------------------------------------------------------------------
/src/components/Home/index.scss:
--------------------------------------------------------------------------------
1 | @import "../../assets/css/util.scss";
2 |
3 | .home {
4 | @at-root .home-header {
5 | display: flex;
6 | align-items: center;
7 | justify-content:space-between;
8 | height:pr(45);
9 | font-size:pr(25);
10 | padding:0 pr(20);
11 | img {
12 | width:pr(90);
13 | }
14 | .iconfont {
15 | font-size: pr(20);
16 | color: $red;
17 | }
18 | }
19 |
20 | .slick-initialized.slick-slider {
21 | overflow: hidden;
22 | }
23 | .slick-list {
24 | img {
25 | width:100%;
26 | }
27 | }
28 | .slick-dots li {
29 | margin:0 2px;
30 | }
31 | .slick-dots li.slick-active button:before {
32 | opacity:1;
33 | color:$red;
34 | }
35 | .slick-dots li button:before {
36 | font-size:pr(35);
37 | font-family: Arial;
38 | opacity: 1;
39 | color:#ddd;
40 | }
41 | .slick-dots {
42 | bottom:0;
43 | }
44 | .title {
45 | h3 {
46 | text-indent: pr(10);
47 | border-left:3px solid $red;
48 | font-size:pr(16);
49 | }
50 | }
51 |
52 | .home-nav {
53 | display: flex;
54 | padding: 10px 0;
55 | margin: 10px 0;
56 | background-color: #fff;
57 |
58 | }
59 | .guide-nav-div {
60 | flex: 1;
61 | display: flex;
62 | flex-direction: column;
63 | justify-content: center;
64 | align-items: center;
65 | > i {
66 | width: 24px;
67 | height: 24px;
68 | background-image: url(../../assets/images/sprite.0.50.png);
69 | }
70 | &:nth-of-type(1) {
71 | i {
72 | background-position: -63px -28px;
73 | }
74 | }
75 | &:nth-of-type(2) {
76 | i {
77 | background-position: 0 0;
78 | }
79 | }
80 | &:nth-of-type(3) {
81 | i {
82 | background-position: 0 -30px;
83 | }
84 | }
85 | &:nth-of-type(4) {
86 | i {
87 | background-position: 0 -60px;
88 | }
89 | }
90 | &:nth-of-type(5) {
91 | i {
92 | background-position: -30px -30px;
93 | }
94 | }
95 |
96 | h4 {
97 | font-size:14px;
98 | margin:10px 0;
99 | color:#333;
100 | }
101 | }
102 |
103 | @at-root .recommend {
104 | padding: pr(15) 0;
105 | margin-bottom: pr(15);
106 | background-color: #fff;
107 | .title {
108 | margin-left: pr(15);
109 | margin-bottom: pr(10);
110 | border-left: 2px solid #ed424b;
111 | text-indent: pr(5);
112 | font-size: pr(16);
113 | line-height: pr(16);
114 | }
115 | .list {
116 | .list-ul {
117 | position: relative;
118 | overflow-x: auto;
119 | overflow-y: hidden;
120 | white-space: nowrap;
121 | text-indent: pr(7);
122 | .list-li {
123 | display: inline-block;
124 | margin-right: pr(8);
125 | width: pr(100);
126 | white-space: normal;
127 | img {
128 | width: 100%;
129 | height:pr(125);
130 | }
131 | p {
132 | font-size:pr(14);
133 | overflow: hidden;
134 | text-overflow: ellipsis;
135 | white-space: nowrap;
136 | line-height:1.2;
137 | }
138 | }
139 | }
140 | }
141 | }
142 |
143 | @at-root .book-list {
144 | margin-top: pr(15);
145 | padding: pr(15);
146 | background-color: #fff;
147 | .title {
148 | margin-left: 0px;
149 | margin-bottom: pr(10);
150 | border-left: 2px solid #ed424b;
151 | text-indent: pr(5);
152 | font-size: pr(16);
153 | line-height: pr(16);
154 | }
155 | ul li {
156 | display: flex;
157 | padding-bottom: pr(10);
158 | margin-bottom: pr(14);
159 | border-bottom: 1px solid #ddd;
160 | &:last-of-type {
161 | border-bottom: none;
162 | }
163 | a {
164 | display: flex;
165 | }
166 | .book-image {
167 | width: pr(80);
168 | img {
169 | width: 100%;
170 | }
171 | }
172 | .book-detail {
173 | position: relative;
174 | flex: 1;
175 | padding: 0;
176 | margin-left: pr(20);
177 | h3 {
178 | font-size: pr(18);
179 | margin-bottom: pr(10);
180 | }
181 | p {
182 | line-height:1.5em;
183 | max-height:2.8em;
184 | overflow: hidden;
185 | text-overflow: ellipsis;
186 | display: -webkit-box;
187 | line-clamp: 2;
188 | font-size: pr(14);
189 | color: #969ba3;
190 | -webkit-box-orient: vertical;
191 | -webkit-line-clamp: 2;
192 | -webkit-box-orient: vertical;
193 | -webkit-box-orient:vertical;
194 | }
195 | .author {
196 | position: absolute;
197 | left: 0px;
198 | bottom: pr(5);
199 | color: #969ba3;
200 | font-size: pr(13);
201 | > * {
202 | color: #969ba3;
203 | }
204 | }
205 | .category-r {
206 | position: absolute;
207 | right: 0;
208 | bottom: pr(5);
209 | float: right;
210 | color: #969ba3;
211 | font-size: pr(10);
212 | span {
213 | border: 1px solid #ccc;
214 | border-radius: pr(2);
215 | padding: 0 pr(2);
216 | }
217 | span:nth-child(2) {
218 | color: #ed424b;
219 | }
220 | span:nth-child(3) {
221 | color: #4284ed;
222 | }
223 | }
224 | }
225 | }
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/components/Loading/index.css:
--------------------------------------------------------------------------------
1 | .loading-component {
2 | display: inline-block;
3 | pointer-events: none;
4 | will-change: transform, opacity;
5 | position: fixed;
6 | left: 50%;
7 | top: 50%;
8 | transform: translate(-50%, -50%); }
9 |
10 | .spinner {
11 | animation: rotator 1.4s linear infinite; }
12 |
13 | @keyframes rotator {
14 | 0% {
15 | transform: rotate(0deg); }
16 | 100% {
17 | transform: rotate(270deg); } }
18 |
19 | .path {
20 | stroke-dasharray: 187;
21 | stroke-dashoffset: 0;
22 | transform-origin: center;
23 | animation: dash 1.4s ease-in-out infinite; }
24 |
25 | @keyframes colors {
26 | 0% {
27 | stroke: #4285F4; }
28 | 25% {
29 | stroke: #DE3E35; }
30 | 50% {
31 | stroke: #F7C223; }
32 | 75% {
33 | stroke: #1B9A59; }
34 | 100% {
35 | stroke: #4285F4; } }
36 |
37 | @keyframes dash {
38 | 0% {
39 | stroke-dashoffset: 187; }
40 | 50% {
41 | stroke-dashoffset: 46.75;
42 | transform: rotate(135deg); }
43 | 100% {
44 | stroke-dashoffset: 187;
45 | transform: rotate(450deg); } }
46 |
--------------------------------------------------------------------------------
/src/components/Loading/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import './index.css'
5 |
6 | class Loading extends Component {
7 | /*constructor(props) {
8 | super(props)
9 | }*/
10 |
11 | static defaultProps = {
12 | size: 50,
13 | stroke: 3.5,
14 | color: '#ed424b'
15 | }
16 |
17 | static propTypes = {
18 | size: PropTypes.number,
19 | stroke: PropTypes.number,
20 | color: PropTypes.string
21 | }
22 |
23 | loadingSize() {
24 | const newSize = this.props.size + 'px'
25 | return {
26 | width: newSize,
27 | height: newSize
28 | }
29 | }
30 |
31 | loadingColor() {
32 | return {
33 | stroke: this.props.color
34 | }
35 | }
36 |
37 | render() {
38 | const {size, stroke, color} = this.props
39 | return (
40 |
41 |
47 |
48 | )
49 | }
50 | }
51 |
52 |
53 | export default Loading
--------------------------------------------------------------------------------
/src/components/Loading/index.scss:
--------------------------------------------------------------------------------
1 | .loading-component {
2 | display: inline-block;
3 | pointer-events: none;
4 | will-change: transform, opacity;
5 | position: fixed;
6 | left: 50%;
7 | top: 50%;
8 | transform: translate(-50%, -50%);
9 | }
10 |
11 | $offset: 187;
12 | $duration: 1.4s;
13 |
14 | .spinner {
15 | animation: rotator $duration linear infinite;
16 | }
17 |
18 | @keyframes rotator {
19 | 0% {
20 | transform: rotate(0deg);
21 | }
22 | 100% {
23 | transform: rotate(270deg);
24 | }
25 | }
26 |
27 | .path {
28 | stroke-dasharray: $offset;
29 | stroke-dashoffset: 0;
30 | transform-origin: center;
31 | animation: dash $duration ease-in-out infinite;
32 | }
33 |
34 | @keyframes colors {
35 | 0% {
36 | stroke: #4285F4;
37 | }
38 | 25% {
39 | stroke: #DE3E35;
40 | }
41 | 50% {
42 | stroke: #F7C223;
43 | }
44 | 75% {
45 | stroke: #1B9A59;
46 | }
47 | 100% {
48 | stroke: #4285F4;
49 | }
50 | }
51 |
52 | @keyframes dash {
53 | 0% {
54 | stroke-dashoffset: $offset;
55 | }
56 | 50% {
57 | stroke-dashoffset: $offset/4;
58 | transform: rotate(135deg);
59 | }
60 | 100% {
61 | stroke-dashoffset: $offset;
62 | transform: rotate(450deg);
63 | }
64 | }
--------------------------------------------------------------------------------
/src/components/NotFound/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import {connect} from 'react-redux'
4 | import {bindActionCreators} from 'redux'
5 | import * as peopleAction from '../../actions/actions'
6 |
7 | import './style.css'
8 |
9 | class NotFound extends Component {
10 | /*constructor(props) {
11 | super(props)
12 | }*/
13 |
14 | componentDidMount() {
15 | console.log(this.props)
16 | }
17 |
18 | componentWillUpdate(props, state) {
19 | console.log(props.count)
20 | console.log(state)
21 | }
22 |
23 | componentDidUpdate(props, state) {
24 | console.log(props.count)
25 | console.log(state)
26 | }
27 |
28 | onClickAdd() {
29 | this.props.actions.increment(1)
30 | }
31 |
32 | onClickDec() {
33 | this.props.actions.decrement(-1)
34 | }
35 |
36 |
37 | render() {
38 | return (
39 |
40 |
41 | {this.props.count}
42 |
43 |
44 |
45 |
46 | )
47 | }
48 | }
49 |
50 | NotFound.propTypes = {
51 | count: PropTypes.number.isRequired,
52 | }
53 |
54 | function mapStateToProps(state) {
55 | return {
56 | count: state.counter.count,
57 | }
58 | }
59 |
60 | function mapDispatchToProps(dispatch) {
61 | return {
62 | actions: bindActionCreators(peopleAction, dispatch)
63 | }
64 | }
65 |
66 | export default connect(mapStateToProps, mapDispatchToProps)(NotFound)
--------------------------------------------------------------------------------
/src/components/NotFound/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tgxhx/react-reader/e63972dea82b46ce29bff76e7e9549df17aa9851/src/components/NotFound/style.css
--------------------------------------------------------------------------------
/src/components/People/PeopleContainer.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import {connect} from 'react-redux'
4 | import {bindActionCreators} from 'redux'
5 | import * as peopleActions from '../../actions/actions'
6 |
7 | import PersonInput from './PersonInput'
8 | import PersonList from './PersonList'
9 |
10 | class PeopleContainer extends Component {
11 | constructor(props) {
12 | super(props)
13 | this.state = {
14 | people: []
15 | }
16 | }
17 |
18 | componentDidMount() {
19 | console.log(this.props)
20 | }
21 |
22 | render() {
23 | const {people} = this.props
24 | return (
25 |
29 | )
30 | }
31 | }
32 |
33 | PeopleContainer.propTypes = {
34 | people:PropTypes.array.isRequired,
35 | actions: PropTypes.object.isRequired
36 | }
37 |
38 | function mapStateToProps(state, props) {
39 | return {
40 | people: state.people
41 | }
42 | }
43 |
44 | function mapDispatchToProps(dispatch) {
45 | return {
46 | actions: bindActionCreators(peopleActions, dispatch)
47 | }
48 | }
49 |
50 | export default connect(mapStateToProps, mapDispatchToProps)(PeopleContainer)
--------------------------------------------------------------------------------
/src/components/People/Person.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Person = ({person}) => {
5 | return (
6 |
7 | {person.firstname} - {person.lastname}
8 |
9 | )
10 | }
11 |
12 | Person.propTypes = {
13 | person: PropTypes.object.isRequired
14 | }
15 |
16 | export default Person
--------------------------------------------------------------------------------
/src/components/People/PersonInput.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class PersonInput extends Component {
5 | constructor(props) {
6 | super(props)
7 |
8 | this.onAddPersonClick = this.onAddPersonClick.bind(this)
9 | }
10 |
11 | onAddPersonClick() {
12 | const first = document.getElementById('firstname')
13 | const last = document.getElementById('lastname')
14 |
15 | this.props.addPerson({
16 | firstname: first.value,
17 | lastname: last.value
18 | })
19 |
20 | first.value = ''
21 | last.value = ''
22 |
23 | first.focus()
24 | }
25 |
26 | componentDidMount() {
27 | document.getElementById('firstname').focus()
28 | console.log(this.props)
29 | }
30 |
31 | render() {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | PersonInput.propTypes = {
43 | addPerson: PropTypes.func.isRequired
44 | }
45 |
46 | export default PersonInput
--------------------------------------------------------------------------------
/src/components/People/PersonList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Person from './Person'
4 |
5 | const PeopleList = ({people}) => {
6 | return (
7 |
8 | {people.map((person, idx) =>
9 |
10 | )}
11 |
12 | )
13 | }
14 |
15 | PeopleList.propTypes = {
16 | people: PropTypes.array.isRequired
17 | }
18 |
19 | export default PeopleList
--------------------------------------------------------------------------------
/src/components/Reader/BottomNav.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class BottomNav extends Component {
5 |
6 | static propTypes = {
7 | font_panel: PropTypes.bool.isRequired,
8 | bg_night: PropTypes.bool.isRequired,
9 | showFontPanel: PropTypes.func.isRequired,
10 | switchNight: PropTypes.func.isRequired,
11 | showListPanel: PropTypes.func.isRequired
12 | }
13 |
14 | //切换字体面板
15 | showFontPanel() {
16 | this.props.showFontPanel(!this.props.font_panel)
17 | }
18 |
19 | //切换夜间模式
20 | switchNight() {
21 | this.props.switchNight(!this.props.bg_night)
22 | }
23 |
24 | //打开目录列表
25 | showListPanel() {
26 | this.props.showListPanel(true)
27 | //同时隐藏字体面板
28 | this.props.showFontPanel(false)
29 | }
30 |
31 | render() {
32 | return (
33 |
34 |
35 |
36 |
37 | 目录
38 |
39 |
40 |
41 |
42 |
43 | 字体
44 |
45 |
46 |
47 | {this.props.bg_night ?
48 |
49 |
50 | 白天
51 |
52 | :
53 |
54 |
55 | 夜间
56 |
57 | }
58 |
59 |
60 | )
61 | }
62 | }
63 |
64 | export default BottomNav
65 |
--------------------------------------------------------------------------------
/src/components/Reader/Content.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Content = (props) =>
4 |
5 | {props.content.map((item, idx) =>
6 |
{item}
7 | )}
8 |
9 |
10 | export default Content
--------------------------------------------------------------------------------
/src/components/Reader/Cover.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class Cover extends Component {
5 | static propTypes = {
6 | showListPanel: PropTypes.func.isRequired,
7 | list_panel: PropTypes.bool.isRequired
8 | }
9 |
10 | hideListPanel = () => {
11 | this.props.showListPanel(false)
12 | }
13 |
14 | render() {
15 | const cover = {
16 | position: 'fixed',
17 | top: '0',
18 | left: '0',
19 | bottom: '0',
20 | right: '0',
21 | opacity: '1',
22 | zIndex: '10',
23 | backgroundColor: 'rgba(0,0,0,.5)',
24 | transition: 'all .5s'
25 | },
26 | hide = {
27 | position: 'static',
28 | opacity: '0'
29 | }
30 | return (
31 |
34 | )
35 | }
36 | }
37 |
38 | export default Cover
39 |
--------------------------------------------------------------------------------
/src/components/Reader/FontNav.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {connect} from 'react-redux'
3 | import {bindActionCreators} from 'redux'
4 | import * as actions from '../../actions/actions'
5 |
6 | import localEvent from '../../assets/js/local'
7 |
8 | class FontNav extends Component {
9 | constructor() {
10 | super()
11 | this.state = {
12 | now_color: 0
13 | }
14 | }
15 |
16 | //改变字体
17 | addFz = () => {
18 | this.props.actions.fzSizeAdd()
19 | }
20 |
21 | subFz = () => {
22 | this.props.actions.fzSizeSub()
23 | }
24 |
25 | //更换背景
26 | changeColor = (index) => {
27 | this.setState({
28 | now_color: index
29 | })
30 | // this.$store.state.bg_color = index + 1
31 | this.props.actions.changeBG(index + 1)
32 | localEvent.StorageSetter('bg_color', index + 1)
33 | }
34 |
35 | render() {
36 | const items = []
37 | for (var i = 0; i < 6; i++) {
38 | items.push()
43 | }
44 | return (
45 |
46 |
47 | 字号
48 |
51 |
54 |
55 |
56 | 背景
57 | {items}
58 |
59 |
60 | )
61 | }
62 | }
63 |
64 | const mapStateToProps = (state) => ({
65 | font_panel: state.font_panel
66 | })
67 |
68 | const mapDispatchToProps = (dispatch) => {
69 | return {
70 | actions: bindActionCreators(actions, dispatch)
71 | }
72 | }
73 |
74 | export default connect(mapStateToProps, mapDispatchToProps)(FontNav)
--------------------------------------------------------------------------------
/src/components/Reader/ListPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class ListPanel extends Component {
5 | constructor(props) {
6 | super(props)
7 |
8 | this.state = {
9 | chapterList:[]
10 | }
11 | }
12 |
13 | static propTypes = {
14 | bookId: PropTypes.string.isRequired,
15 | hideBar: PropTypes.func.isRequired,
16 | saveBooksInfo: PropTypes.func.isRequired,
17 | api: PropTypes.string.isRequired,
18 | list_panel: PropTypes.bool.isRequired,
19 | curChapter:PropTypes.number,
20 | curChapterAction: PropTypes.func,
21 | showListPanel: PropTypes.func
22 | }
23 |
24 | componentDidMount() {
25 | this.getList(this.props.bookId)
26 | }
27 |
28 | getList() {
29 | fetch(`${this.props.api}/titles?id=${this.props.bookId}`)
30 | .then(res => res.json())
31 | .then(res => {
32 | this.setState({
33 | chapterList: res.titles.split('-')
34 | })
35 | })
36 | }
37 |
38 | hideListPanel = () => {
39 | this.props.showListPanel(false)
40 | }
41 |
42 | //跳转到指定章节
43 | redirectTo (index) {
44 | this.hideListPanel()
45 | this.props.hideBar(false) //点击隐藏上下面板,调用父元素的方法
46 | index = Math.min(index, 50) //
47 | this.props.curChapterAction(index)
48 | setTimeout(() => {
49 | document.body.scrollTop = 0
50 | this.props.saveBooksInfo() //点击保存阅读进度,调用父元素的方法
51 | }, 300)
52 | }
53 |
54 | render() {
55 | return (
56 |
57 |
58 |
59 |
60 |
目录
61 |
62 |
63 |
64 | {this.state.chapterList.map((item, idx) =>
65 | -
69 | · {idx+1}. {item}
70 |
71 | )}
72 |
73 |
74 |
75 |
76 | )
77 | }
78 | }
79 |
80 | export default ListPanel
--------------------------------------------------------------------------------
/src/components/Reader/TopNav.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class TopNav extends Component {
5 | static contextTypes = {
6 | router: PropTypes.object
7 | }
8 |
9 | goBack = () => {
10 | this.context.router.goBack()
11 | }
12 |
13 | render() {
14 | return (
15 |
19 | )
20 | }
21 | }
22 |
23 | export default TopNav
--------------------------------------------------------------------------------
/src/components/Reader/index.css:
--------------------------------------------------------------------------------
1 | #reader {
2 | height: 100%; }
3 |
4 | .read-container {
5 | font-size: 16px;
6 | color: #555;
7 | line-height: 31px;
8 | min-height: 100%;
9 | padding: 15px; }
10 | .read-container h4 {
11 | position: fixed;
12 | top: 0;
13 | left: 15px;
14 | right: 15px;
15 | height: 50px;
16 | line-height: 50px;
17 | font-size: 20px;
18 | color: #736357;
19 | /*border-bottom: solid 1px #736357;*/
20 | margin: 0 0 1em 0;
21 | letter-spacing: 2px;
22 | white-space: nowrap;
23 | overflow: hidden;
24 | text-overflow: ellipsis; }
25 | .read-container p {
26 | text-indent: 2em;
27 | margin: 0.5em 0;
28 | text-align: justify;
29 | letter-spacing: 0px;
30 | line-height: 1.5; }
31 | .read-container p:first-of-type {
32 | margin-top: 43px; }
33 | .read-container .btn-bar {
34 | z-index: 80;
35 | width: 80%;
36 | margin: 20px auto 0;
37 | max-width: 800px; }
38 | .read-container .btn-bar .btn-tab {
39 | padding-left: 0;
40 | height: 34px;
41 | line-height: 34px;
42 | background-color: #000;
43 | border-radius: 8px;
44 | border: 1px solid #858382;
45 | font-size: 14px;
46 | opacity: 0.9; }
47 | .read-container .btn-bar .btn-tab li {
48 | list-style-type: none;
49 | display: inline-block;
50 | width: 49%;
51 | text-align: center;
52 | color: #fff; }
53 | .read-container .btn-bar .btn-tab li:nth-child(1) {
54 | border-right: 1px solid #858382; }
55 |
56 | .read-container[data-bg='1'] {
57 | background-color: #e9dfc7; }
58 | .read-container[data-bg='1'] h4 {
59 | background-color: #e9dfc7; }
60 |
61 | .read-container[data-bg='2'] {
62 | background-color: #e7eee5; }
63 | .read-container[data-bg='2'] h4 {
64 | background-color: #e7eee5; }
65 |
66 | .read-container[data-bg='3'] {
67 | background-color: #a4a4a4; }
68 | .read-container[data-bg='3'] h4 {
69 | background-color: #a4a4a4; }
70 |
71 | .read-container[data-bg='4'] {
72 | background-color: #cdefcd; }
73 | .read-container[data-bg='4'] h4 {
74 | background-color: #cdefcd; }
75 |
76 | .read-container[data-bg='5'] {
77 | background-color: #283548; }
78 | .read-container[data-bg='5'] h4 {
79 | background-color: #283548; }
80 |
81 | .read-container[data-bg='6'] {
82 | background-color: #0f1410; }
83 | .read-container[data-bg='6'] h4 {
84 | background-color: #0f1410; }
85 |
86 | .read-container[data-night='true'] {
87 | background-color: #0f1410; }
88 | .read-container[data-night='true'] h4 {
89 | background-color: #0f1410; }
90 |
91 | .page-up {
92 | position: fixed;
93 | width: 100%;
94 | height: 35%;
95 | top: 0;
96 | color: rgba(0, 0, 0, 0.1);
97 | z-index: 5; }
98 |
99 | .click-mask {
100 | position: fixed;
101 | width: 100%;
102 | height: 25%;
103 | top: 35%;
104 | color: rgba(0, 0, 0, 0.1); }
105 |
106 | .page-down {
107 | position: fixed;
108 | width: 100%;
109 | height: 30%;
110 | bottom: 65px;
111 | color: rgba(0, 0, 0, 0.1);
112 | z-index: 5; }
113 |
114 | .top-nav-pannel-bk {
115 | position: fixed;
116 | bottom: 70px;
117 | height: 135px;
118 | background: #000;
119 | width: 100%;
120 | color: #fff;
121 | opacity: 0.9;
122 | z-index: 10003; }
123 |
124 | .top-nav {
125 | display: flex;
126 | align-items: center;
127 | position: fixed;
128 | top: 0px;
129 | height: 50px;
130 | background: #000;
131 | width: 100%;
132 | opacity: 1;
133 | z-index: 9; }
134 | .top-nav .iconfont {
135 | color: #ddd;
136 | padding: 10px 10px 10px 20px; }
137 | .top-nav .nav-title {
138 | color: #fff;
139 | font-size: 14px; }
140 |
141 | .bottom-nav {
142 | display: flex;
143 | align-items: center;
144 | position: fixed;
145 | bottom: 0px;
146 | height: 70px;
147 | background: #000000;
148 | width: 100%;
149 | opacity: 1;
150 | z-index: 9;
151 | margin: 0 auto;
152 | text-align: center; }
153 | .bottom-nav .item {
154 | flex: 1;
155 | display: flex;
156 | flex-direction: column;
157 | justify-content: center;
158 | align-items: center;
159 | color: #fff;
160 | text-align: center;
161 | margin: 0 auto;
162 | font-size: 18px; }
163 | .bottom-nav .iconfont, .bottom-nav .icon-text {
164 | color: #fff; }
165 | .bottom-nav .iconfont {
166 | margin-bottom: 8px;
167 | font-size: 24px; }
168 | .bottom-nav .icon-text {
169 | display: flex;
170 | flex-direction: column;
171 | font-size: 12px; }
172 | .bottom-nav .icon-text.active {
173 | color: #ef7000; }
174 | .bottom-nav .icon-text.active .iconfont {
175 | color: #ef7000; }
176 |
177 | .top-nav-pannel {
178 | position: fixed;
179 | bottom: 70px;
180 | height: 135px;
181 | background: none;
182 | width: 100%;
183 | color: #fff;
184 | font-size: 14px;
185 | z-index: 10004; }
186 | .top-nav-pannel button {
187 | background: none;
188 | border: 1px #8c8c8c solid;
189 | padding: 5px 40px;
190 | color: #fff;
191 | display: inline-block;
192 | border-radius: 16px; }
193 | .top-nav-pannel button:focus {
194 | outline: none; }
195 | .top-nav-pannel .child-mod {
196 | padding: 5px 20px;
197 | margin-top: 15px; }
198 | .top-nav-pannel .child-mod > span {
199 | color: #fff; }
200 | .top-nav-pannel .child-mod > span:first-child {
201 | margin-right: 20px; }
202 | .top-nav-pannel .child-mod #small-font {
203 | margin-left: 10px; }
204 | .top-nav-pannel .bk-container {
205 | position: relative;
206 | height: 30px;
207 | width: 30px;
208 | background: #fff;
209 | border-radius: 15px;
210 | display: inline-block;
211 | vertical-align: -14px;
212 | margin-left: 10px; }
213 | .top-nav-pannel .bk-container .color_btn {
214 | height: 30px;
215 | width: 30px;
216 | border-radius: 15px; }
217 | .top-nav-pannel .bk-container-current {
218 | height: 31px;
219 | width: 32px;
220 | border-radius: 16px;
221 | border: 1px #ff7800 solid; }
222 | .top-nav-pannel .bk-container:nth-child(2) .color_btn {
223 | background-color: #e9dfc7; }
224 | .top-nav-pannel .bk-container:nth-child(3) .color_btn {
225 | background-color: #e7eee5; }
226 | .top-nav-pannel .bk-container:nth-child(4) .color_btn {
227 | background-color: #a4a4a4; }
228 | .top-nav-pannel .bk-container:nth-child(5) .color_btn {
229 | background-color: #cdefcd; }
230 | .top-nav-pannel .bk-container:nth-child(6) .color_btn {
231 | background-color: #283548; }
232 | .top-nav-pannel .bk-container:nth-child(7) .color_btn {
233 | background-color: #0f1410; }
234 |
235 | .list-panel {
236 | position: fixed;
237 | transition: all .3s;
238 | left: 0;
239 | top: 0;
240 | bottom: 0;
241 | right: 50px;
242 | z-index: 10;
243 | overflow: auto;
244 | transform: translateX(-100%); }
245 | .list-panel.show {
246 | transform: translateX(0); }
247 | .list-panel .list {
248 | position: absolute;
249 | left: 0;
250 | top: 0;
251 | bottom: 0;
252 | width: 100%;
253 | background-color: #fff;
254 | opacity: 1; }
255 | .list-panel .list .list-nav {
256 | display: flex;
257 | align-items: center;
258 | height: 50px;
259 | background-color: #fff;
260 | border-bottom: 1px solid #ed424b; }
261 | .list-panel .list .list-nav > * {
262 | color: #ed424b; }
263 | .list-panel .list .list-nav .iconfont {
264 | display: flex;
265 | justify-content: center;
266 | align-items: center;
267 | width: 50px; }
268 | .list-panel .list .list-nav h3 {
269 | font-size: 18px;
270 | flex: 1; }
271 | .list-panel .list .list-content {
272 | height: 100%;
273 | background-color: #fff;
274 | overflow: auto; }
275 | .list-panel .list .list-content ul {
276 | padding: 0 15px; }
277 | .list-panel .list .list-content li {
278 | color: #333;
279 | height: 50px;
280 | line-height: 50px;
281 | border-bottom: 1px solid #ccc;
282 | white-space: nowrap;
283 | overflow: hidden;
284 | text-overflow: ellipsis;
285 | font-size: 14px; }
286 |
--------------------------------------------------------------------------------
/src/components/Reader/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {connect} from 'react-redux'
3 | import {bindActionCreators} from 'redux'
4 | import * as actions from '../../actions/actions'
5 | import localEvent from '../../assets/js/local'
6 |
7 | import Content from './Content'
8 | import Loading from '../Loading'
9 | import TopNav from './TopNav'
10 | import BottomNav from './BottomNav'
11 | import FontNav from './FontNav'
12 | import ListPanel from './ListPanel'
13 | import Cover from './Cover'
14 |
15 | import './index.css'
16 |
17 | class Reader extends Component {
18 | constructor() {
19 | super()
20 |
21 | this.state = {
22 | loading: false,
23 | title: '',
24 | content: [],
25 | bar: false,
26 | booksReadInfo: {}, //所有书籍的阅读信息和阅读进度
27 | firstUpdate: false, //初次加载的判断状态
28 | shelfList: [], //书架信息
29 | isSave: false, //是否需要保存进度,刚进入不保存,点击更换章节后再根据这个值保存
30 | isInShelf: false //当前书籍是否在书架中
31 | }
32 | }
33 |
34 | componentWillMount() {
35 | //初次加载时防止willupdate多次请求,由于componentWillUpdate监听的章节数字,初次加载会触发两次,因此加上判断状态
36 | const setFirstUpdate = (id, chapter) => {
37 | this.getData(id, chapter)
38 | this.props.actions.curChapter(chapter)
39 | this.setState({
40 | firstUpdate: true
41 | })
42 | }
43 |
44 | const id = this.props.params.id
45 |
46 | //判断当前书籍是否在书架中,是则将localStorage的数据存入shelfList中,并将isInshelf状态设为true
47 | const bs = localEvent.StorageGetter('bookShelf')
48 | if (bs && bs.some(el => el.id === +id)) {
49 | this.setState({
50 | shelfList: bs,
51 | isInShelf: true
52 | })
53 | }
54 |
55 | //判断本地是否存储了阅读器文字大小
56 | const fz = localEvent.StorageGetter('fz_size')
57 | if (fz) {
58 | this.props.actions.fzSizeModify(fz)
59 | }
60 | //判断本地是否存储了阅读器主题色
61 | const bg = localEvent.StorageGetter('bg_color')
62 | if (bg) {
63 | this.props.actions.changeBG(bg)
64 | }
65 |
66 | //加载时从localStorage中加载所有书籍阅读进度
67 | const localBookReaderInfo = localEvent.StorageGetter('bookreaderinfo')
68 |
69 | //当前书籍以前读过并有阅读进度
70 | if (localBookReaderInfo && localBookReaderInfo[id]) {
71 | this.setState({
72 | booksReadInfo: localBookReaderInfo
73 | }, () => {
74 | const chapter = this.state.booksReadInfo[id].chapter
75 | setFirstUpdate(id, chapter)
76 | })
77 | } else {
78 | //当前书籍没有读过但是localStorage保存了其他书籍进度
79 | if (localBookReaderInfo) {
80 | this.setState({
81 | booksReadInfo: localBookReaderInfo
82 | })
83 | setFirstUpdate(id, 1)
84 | } else { //第一次进入阅读
85 | let booksReadInfo = this.state.booksReadInfo
86 | booksReadInfo[id] = {
87 | book: id,
88 | chapter: 1
89 | }
90 | this.setState({booksReadInfo})
91 | setFirstUpdate(id, 1)
92 | }
93 | }
94 | }
95 |
96 | componentDidMount() {
97 | }
98 |
99 | componentWillUpdate(nextProps, nextState) {
100 | //监听字体大小的变化,并存入localStorage中
101 | if (nextProps.fz_size !== this.props.fz_size) {
102 | localEvent.StorageSetter('fz_size', nextProps.fz_size)
103 | }
104 | //监听章节的变化
105 | if (nextProps.curChapter !== this.props.curChapter && this.state.firstUpdate) {
106 | this.getData(this.props.params.id, nextProps.curChapter)
107 | //进入阅读后,不会立即更新书架中进度信息,先判断当前书籍是否在书架中,然后当切换章节时候再通过isSave的状态告诉getData获取数据后更新书架的阅读进度
108 | if (nextState.isInShelf) {
109 | this.setState({
110 | isSave: true
111 | })
112 | }
113 | }
114 | }
115 |
116 | //获取数据
117 | getData(id, chapter = 1) {
118 | this.setState({
119 | loading: true
120 | })
121 | fetch(`${this.props.api}/book?book=${id}&id=${chapter}`)
122 | .then(res => res.json())
123 | .then(res => {
124 | this.setState({
125 | loading: false,
126 | title: res.title,
127 | content: res.content.split('-')
128 | }, () => {
129 | //当前书籍在书架中,且切换章节后isSave状态为true时,更新书架信息
130 | if (this.state.isInShelf && this.state.isSave) {
131 | const state = this.state
132 | const index = state.shelfList.findIndex(el => el.id === +this.props.params.id)
133 | state.shelfList[index].recent = state.title
134 | this.setState({
135 | shelfList: state.shelfList
136 | }, () => {
137 | localEvent.StorageSetter('bookShelf', this.state.shelfList)
138 | })
139 | }
140 | })
141 | })
142 | }
143 |
144 | //向上翻页
145 | pageUp = () => {
146 | let target = document.body.scrollTop - window.screen.height - 80
147 | this.startScroll(target, -20)
148 | }
149 |
150 | //向下翻页
151 | pageDown = () => {
152 | let target = document.body.scrollTop + window.screen.height - 80
153 | this.startScroll(target, 20)
154 | }
155 |
156 | //滚动
157 | startScroll(target, speed) {
158 | let times = null
159 | times = setInterval(function () {
160 | if (speed > 0) {
161 | if (document.body.scrollTop <= target) {
162 | document.body.scrollTop += speed
163 | }
164 | if (document.body.scrollTop > target || document.body.scrollTop + window.screen.height >= document.body.scrollHeight) {
165 | clearInterval(times)
166 | }
167 | } else {
168 | if (document.body.scrollTop >= target) {
169 | document.body.scrollTop += speed
170 | }
171 | if (document.body.scrollTop < target || document.body.scrollTop <= 0) {
172 | clearInterval(times)
173 | }
174 | }
175 | }, 1)
176 | }
177 |
178 | //修改章节
179 | nextChapter = () => {
180 | this.props.actions.nextChapter('', 50)
181 | setTimeout(() => {
182 | document.body.scrollTop = 0
183 | //设置redux的state不会立即生效,暂时加延迟实现
184 | this.saveBooksInfo()
185 | }, 300)
186 | }
187 |
188 | //上一章
189 | prevChapter = () => {
190 | this.props.actions.prevChapter()
191 | setTimeout(() => {
192 | document.body.scrollTop = 0
193 | this.saveBooksInfo()
194 | }, 300)
195 | }
196 |
197 | saveBooksInfo = () => {
198 | //可用localStorage保存每本小说阅读进度
199 | let id = this.props.params.id
200 | //不可直接修改state,所有保存一个state.booksReadInfo的副本,修改副本完毕后再设置state
201 | let booksReadInfo = this.state.booksReadInfo
202 | booksReadInfo[id] = {
203 | book: id,
204 | chapter: this.props.curChapter
205 | }
206 | this.setState({booksReadInfo}, () => {
207 | localEvent.StorageSetter('bookreaderinfo', this.state.booksReadInfo)
208 | })
209 | }
210 |
211 | //显示隐藏面板
212 | toggleBar = (bool) => {
213 | this.setState({
214 | bar: bool
215 | })
216 | this.props.actions.showFontPanel(false)
217 | }
218 |
219 | render() {
220 | const {loading, title, bar, content} = this.state
221 | const {api,fz_size, bg_color, font_panel, list_panel, bg_night,curChapter, params, actions} = this.props
222 | return (
223 |
224 | {loading &&
}
225 | {bar &&
}
226 |
232 |
{title}
233 | {!loading &&
}
234 | {!loading &&
235 |
236 |
237 | - 上一章
238 | - 下一章
239 |
240 |
}
241 |
242 |
243 |
244 |
245 | {bar &&
}
251 | {font_panel && }
252 | {font_panel && }
253 |
256 |
265 |
266 | )
267 | }
268 | }
269 |
270 | const mapStateToProps = (state) => ({
271 | api: state.api,
272 | fz_size: state.fz_size,
273 | curChapter: state.curChapter,
274 | bg_color: state.bg_color,
275 | font_panel: state.font_panel,
276 | bg_night: state.bg_night,
277 | list_panel: state.list_panel
278 | })
279 |
280 | const mapDispatchToProps = (dispatch) => {
281 | return {
282 | actions: bindActionCreators(actions, dispatch)
283 | }
284 | }
285 |
286 | export default connect(mapStateToProps, mapDispatchToProps)(Reader)
287 |
--------------------------------------------------------------------------------
/src/components/Reader/index.scss:
--------------------------------------------------------------------------------
1 | #reader {
2 | height:100%;
3 | }
4 | .read-container {
5 | font-size: 16px;
6 | color: #555;
7 | line-height: 31px;
8 | min-height: 100%;
9 | padding: 15px;
10 | h4 {
11 | position: fixed;
12 | top: 0;
13 | left: 15px;
14 | right: 15px;
15 | height: 50px;
16 | line-height: 50px;
17 | font-size: 20px;
18 | color: #736357;
19 | /*border-bottom: solid 1px #736357;*/
20 | margin: 0 0 1em 0;
21 | letter-spacing: 2px;
22 | white-space: nowrap;
23 | overflow: hidden;
24 | text-overflow: ellipsis;
25 | }
26 | p {
27 | text-indent: 2em;
28 | margin: 0.5em 0;
29 | text-align: justify;
30 | letter-spacing: 0px;
31 | line-height: 1.5;
32 | }
33 | p:first-of-type {
34 | margin-top: 43px;
35 | }
36 | .btn-bar {
37 | z-index: 80;
38 | width: 80%;
39 | margin: 20px auto 0;
40 | max-width: 800px;
41 | .btn-tab {
42 | padding-left: 0;
43 | height: 34px;
44 | line-height: 34px;
45 | background-color: #000;
46 | border-radius: 8px;
47 | border: 1px solid #858382;
48 | font-size: 14px;
49 | opacity: 0.9;
50 | li {
51 | list-style-type: none;
52 | display: inline-block;
53 | width: 49%;
54 | text-align: center;
55 | color: #fff;
56 | &:nth-child(1) {
57 | border-right: 1px solid #858382;
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
64 | @mixin bac($color) {
65 | background-color: $color;
66 | }
67 |
68 | .read-container[data-bg='1'] {
69 | @include bac(#e9dfc7);
70 | h4 {
71 | @include bac(#e9dfc7);
72 | }
73 | }
74 |
75 | .read-container[data-bg='2'] {
76 | @include bac(#e7eee5);
77 | h4 {
78 | @include bac(#e7eee5);
79 | }
80 | }
81 |
82 | .read-container[data-bg='3'] {
83 | @include bac(#a4a4a4);
84 | h4 {
85 | @include bac(#a4a4a4);
86 | }
87 | }
88 |
89 | .read-container[data-bg='4'] {
90 | @include bac(#cdefcd);
91 | h4 {
92 | @include bac(#cdefcd);
93 | }
94 | }
95 |
96 | .read-container[data-bg='5'] {
97 | @include bac(#283548);
98 | h4 {
99 | @include bac(#283548);
100 | }
101 | }
102 |
103 | .read-container[data-bg='6'] {
104 | @include bac(#0f1410);
105 | h4 {
106 | @include bac(#0f1410);
107 | }
108 | }
109 |
110 | .read-container[data-night='true'] {
111 | @include bac(#0f1410);
112 | h4 {
113 | @include bac(#0f1410);
114 | }
115 | }
116 |
117 | .page-up {
118 | position: fixed;
119 | width: 100%;
120 | height: 35%;
121 | top: 0;
122 | color: rgba(0, 0, 0, .1);
123 | z-index: 5;
124 | }
125 |
126 | .click-mask {
127 | position: fixed;
128 | width: 100%;
129 | height: 25%;
130 | top: 35%;
131 | color: rgba(0, 0, 0, .1);
132 | }
133 |
134 | .page-down {
135 | position: fixed;
136 | width: 100%;
137 | height: 30%;
138 | bottom: 65px;
139 | color: rgba(0, 0, 0, .1);
140 | z-index: 5;
141 | }
142 |
143 | .top-nav-pannel-bk {
144 | position: fixed;
145 | bottom: 70px;
146 | height: 135px;
147 | background: #000;
148 | width: 100%;
149 | color: #fff;
150 | opacity: 0.9;
151 | z-index: 10003
152 | }
153 |
154 | .top-nav {
155 | display: flex;
156 | align-items: center;
157 | position: fixed;
158 | top: 0px;
159 | height: 50px;
160 | background: #000;
161 | width: 100%;
162 | opacity: 1;
163 | z-index: 9;
164 | .iconfont {
165 | color:#ddd;
166 | //margin-top:3px;
167 | padding:10px 10px 10px 20px;
168 | }
169 | .nav-title {
170 | color: #fff;
171 | font-size: 14px;
172 | }
173 |
174 | }
175 |
176 | .bottom-nav {
177 | display: flex;
178 | align-items: center;
179 | position: fixed;
180 | bottom: 0px;
181 | height: 70px;
182 | background: #000000;
183 | width: 100%;
184 | opacity: 1;
185 | z-index: 9;
186 | margin: 0 auto;
187 | text-align: center;
188 | .item {
189 | flex:1;
190 | display: flex;
191 | flex-direction:column;
192 | justify-content: center;
193 | align-items: center;
194 | color: #fff;
195 | text-align: center;
196 | margin: 0 auto;
197 | font-size:18px;
198 | }
199 | .iconfont,.icon-text {
200 | color:#fff;
201 | }
202 | .iconfont {
203 | margin-bottom:8px;
204 | font-size:24px;
205 | }
206 | .icon-text {
207 | display: flex;
208 | flex-direction: column;
209 | font-size:12px;
210 | &.active {
211 | color: #ef7000;
212 | .iconfont {
213 | color: #ef7000;
214 | }
215 | }
216 | }
217 | }
218 |
219 | .top-nav-pannel {
220 | position: fixed;
221 | bottom: 70px;
222 | height: 135px;
223 | background: none;
224 | width: 100%;
225 | color: #fff;
226 | font-size: 14px;
227 | z-index: 10004;
228 | button {
229 | background: none;
230 | border: 1px #8c8c8c solid;
231 | padding: 5px 40px;
232 | color: #fff;
233 | display: inline-block;
234 | border-radius: 16px;
235 | &:focus {
236 | outline:none;
237 | }
238 | }
239 | .child-mod {
240 | padding: 5px 20px;
241 | margin-top: 15px;
242 | & > span {
243 | color:#fff;
244 | }
245 | & > span:first-child {
246 | margin-right: 20px;
247 | }
248 | #small-font {
249 | margin-left:10px;
250 | }
251 | }
252 | .bk-container {
253 | position: relative;
254 | height: 30px;
255 | width: 30px;
256 | background: #fff;
257 | border-radius: 15px;
258 | display: inline-block;
259 | vertical-align: -14px;
260 | margin-left: 10px;
261 | .color_btn {
262 | height: 30px;
263 | width: 30px;
264 | border-radius: 15px;
265 | }
266 | }
267 | .bk-container-current {
268 | height: 31px;
269 | width: 32px;
270 | border-radius: 16px;
271 | border: 1px #ff7800 solid;
272 | }
273 | @mixin bac($color) {
274 | background-color: $color;
275 | }
276 | .bk-container:nth-child(2) .color_btn {
277 | @include bac(#e9dfc7);
278 | }
279 | .bk-container:nth-child(3) .color_btn {
280 | @include bac(#e7eee5);
281 | }
282 | .bk-container:nth-child(4) .color_btn {
283 | @include bac(#a4a4a4);
284 | }
285 | .bk-container:nth-child(5) .color_btn {
286 | @include bac(#cdefcd);
287 | }
288 | .bk-container:nth-child(6) .color_btn {
289 | @include bac(#283548);
290 | }
291 | .bk-container:nth-child(7) .color_btn {
292 | @include bac(#0f1410);
293 | }
294 | }
295 |
296 | .list-panel {
297 | position: fixed;
298 | transition: all .3s;
299 | left: 0;
300 | top: 0;
301 | bottom: 0;
302 | right: 50px;
303 | z-index: 10;
304 | overflow: auto;
305 | transform: translateX(-100%);
306 | &.show {
307 | transform: translateX(0);
308 | }
309 | .list {
310 | position: absolute;
311 | left: 0;
312 | top: 0;
313 | bottom: 0;
314 | width:100%;
315 | background-color: #fff;
316 | opacity: 1;
317 | .list-nav {
318 | display: flex;
319 | align-items: center;
320 | height: 50px;
321 | background-color: #fff;
322 | border-bottom: 1px solid #ed424b;
323 | > * {
324 | color:#ed424b;
325 | }
326 | .iconfont {
327 | display: flex;
328 | justify-content: center;
329 | align-items: center;
330 | width:50px;
331 | }
332 | h3 {
333 | font-size:18px;
334 | flex:1;
335 | }
336 | }
337 | .list-content {
338 | height:100%;
339 | background-color: #fff;
340 | overflow: auto;
341 | ul {
342 | padding: 0 15px;
343 | }
344 | li {
345 | color: #333;
346 | height: 50px;
347 | line-height: 50px;
348 | border-bottom: 1px solid #ccc;
349 | white-space: nowrap;
350 | overflow: hidden;
351 | text-overflow: ellipsis;
352 | font-size:14px;
353 | }
354 | }
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | color:#333;
3 | }
4 |
5 | a {
6 | text-decoration: none;
7 | }
8 |
9 | .clearfix:after,.clearfix:before {
10 | display: table;
11 | content: '';
12 | }
13 | .clearfix:after {
14 | clear: both;
15 | }
16 |
17 | html,body,#root,.App {
18 | height:100%;
19 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {hashHistory} from 'react-router'
4 | // import {browserHistory} from 'react-router'
5 | import Routes from './routes'
6 |
7 | import {Provider} from 'react-redux'
8 | import store from './store'
9 |
10 | import 'reset-css';
11 | import './index.css'
12 |
13 | ReactDOM.render(
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | );
19 |
20 | if (module.hot) {
21 | module.hot.accept('./components/App', () => {
22 | ReactDOM.render(
23 |
24 |
25 | ,
26 | document.getElementById('root'),
27 | )
28 | })
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/reducers/counter.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/action-types'
2 |
3 | const initialState = {
4 | count: 0,
5 | }
6 |
7 | export default (state = initialState, action) => {
8 | switch (action.type) {
9 | case types.INCREMENT:
10 | return {
11 | ...state,
12 | count: state.count < 10 ? state.count + 1 : 10,
13 | }
14 | case types.DECRMENT:
15 | return {
16 | ...state,
17 | count: state.count > 0 ? state.count - 1 : 0
18 | }
19 | default:
20 | return state
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux'
2 | import people from './people-reducer'
3 | import counter from './counter'
4 |
5 | import * as types from '../actions/action-types'
6 |
7 | import initState from './state'
8 |
9 | //请求地址
10 | const api = (state = initState.api) => {
11 | return state
12 | }
13 |
14 | //字体修改
15 | const fz_size = (state = initState.fz_size, action) => {
16 | switch (action.type) {
17 | case types.FZ_SIZE_ADD:
18 | return state >= 24 ? 24 : state + 1
19 | case types.FZ_SIZE_SUB:
20 | return state <= 14 ? 14 : state - 1
21 | case types.FZ_SIZE_MODIRY:
22 | return action.fz_size
23 | default:
24 | return state
25 | }
26 | }
27 |
28 | //章节修改
29 | const curChapter = (state = initState.curChapter, action) => {
30 | switch (action.type) {
31 | case types.NEXT_CHAPTER:
32 | return state >= action.max ? action.max : state + 1
33 | case types.PREV_CHAPTER:
34 | return state <= 0 ? 0 : state - 1
35 | case types.CUR_CHAPTER:
36 | return action.num
37 | default:
38 | return state
39 | }
40 | }
41 |
42 | //更换背景
43 | const bg_color = (state = initState.bg_color, action) => {
44 | switch (action.type) {
45 | case types.CHANGE_BG:
46 | return action.num
47 | default:
48 | return state
49 | }
50 | }
51 |
52 | //切换字体面板
53 | const font_panel = (state = initState.font_panel, action) => {
54 | switch (action.type) {
55 | case types.SHOW_FONT_PANEL:
56 | return action.state
57 | default:
58 | return state
59 | }
60 | }
61 |
62 | const bg_night = (state = initState.bg_night, action) => {
63 | switch (action.type) {
64 | case types.SWITCH_NIGHT:
65 | return action.state
66 | default:
67 | return state
68 | }
69 | }
70 |
71 | //目录
72 | const list_panel = (state = initState.list_panel, action) => {
73 | switch (action.type) {
74 | case types.SHOW_LIST_PANEL:
75 | return action.state
76 | default:
77 | return state
78 | }
79 | }
80 |
81 | const rootReducer = combineReducers({
82 | people,
83 | counter,
84 | api,
85 | fz_size,
86 | curChapter,
87 | bg_color,
88 | font_panel,
89 | bg_night,
90 | list_panel
91 | })
92 |
93 | export default rootReducer
--------------------------------------------------------------------------------
/src/reducers/people-reducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/action-types'
2 |
3 | export default (state = [], action) => {
4 | switch (action.type) {
5 | case types.ADD_PERSON:
6 | return [...state, Object.assign({}, action.person)]
7 | default:
8 | return state
9 | }
10 | }
--------------------------------------------------------------------------------
/src/reducers/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // api: 'http://39.108.14.248:3333',
3 | api: '/book',
4 | font_panel: false,
5 | font_icon: false,
6 | bg_color: 1,
7 | bg_night: false,
8 | fz_size: 18,
9 | curChapter: 1,
10 | windowHeight: '',
11 | list_panel: false,
12 | }
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Router, Route, IndexRoute} from 'react-router';
3 |
4 | import App from './components/App'
5 | import Home from './components/Home'
6 | import BookDetail from './components/BookDetail'
7 | import Category from './components/Category'
8 | import Reader from './components/Reader'
9 | import BookShelf from './components/BookShelf'
10 |
11 | import People from './components/People/PeopleContainer'
12 | import About from './components/About'
13 | import NotFound from './components/NotFound'
14 |
15 | const Routes = (props) => (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 |
30 | export default Routes
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore,applyMiddleware, compose} from 'redux'
2 | import rootReducer from './reducers'
3 | import thunk from 'redux-thunk'
4 |
5 | const initialState = {}
6 |
7 | const enhancers = compose(
8 | window.devToolsExtension ? window.devToolsExtension() : f => f
9 | )
10 |
11 | const store = createStore(
12 | rootReducer,
13 | initialState,
14 | enhancers,
15 | compose(applyMiddleware(thunk))
16 | )
17 |
18 | if (process.env.NODE_ENV !== "production") {
19 | if (module.hot) {
20 | module.hot.accept('./reducers', () => {
21 | store.replaceReducer(rootReducer)
22 | })
23 | }
24 | }
25 |
26 | /*const store = () => {
27 | const storeConfig = createStore(
28 | rootReducer,
29 | initialState,
30 | enhancers,
31 | compose(applyMiddleware(thunk))
32 | )
33 | /!*if (process.env.NODE_ENV !== "production") {
34 | if (module.hot) {
35 | module.hot.accept('./reducers', () => {
36 | store.replaceReducer(rootReducer)
37 | })
38 | }
39 | }*!/
40 | return storeConfig
41 | }*/
42 |
43 | export default store
44 |
--------------------------------------------------------------------------------