├── .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 | 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 | 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 | 206 | 207 | 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 | 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 | 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 |
134 | 135 |

{bookDetail.name}

136 |
137 | 138 |
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 &&
84 |
删除{delNum ? `(${delNum})`: ''}
85 |
} 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 | <ul> 10 | {props.booklist.map((item, idx) => 11 | <li key={idx}> 12 | <Link to={`/bookdetail/${item.id}`}> 13 | <div className="book-image"> 14 | <img src={item.images} alt=""/> 15 | </div> 16 | <div className="book-detail"> 17 | <h3>{item.name}</h3> 18 | <p>{item.intro}</p> 19 | <div className="author"> 20 | <i className="iconfont icon-yonghu"> </i> 21 | <span>{item.author}</span> 22 | </div> 23 | <div className="category-r"> 24 | <span>{item.type}</span> 25 | <span>{item.serialize}</span> 26 | <span>{item.wordcount}万字</span> 27 | </div> 28 | </div> 29 | </Link> 30 | </li> 31 | )} 32 | </ul> 33 | </div> 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 | <div className="recommend"> 8 | <Title title={props.title}/> 9 | <div className="list"> 10 | <ul className="list-ul"> 11 | {props.booklist.map((item, idx) => 12 | <li key={idx} className="list-li"> 13 | <Link to={`/bookdetail/${item.id}`}> 14 | <img src={item.images} alt=""/> 15 | <p className="book-name">{item.name}</p> 16 | <p className="book-author">{item.author}</p> 17 | </Link> 18 | </li> 19 | )} 20 | </ul> 21 | </div> 22 | </div> 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 | <Swiper {...params}> 23 | <div><img src={img1} alt=""/></div> 24 | <div><img src={img2} alt=""/></div> 25 | <div><img src={img3} alt=""/></div> 26 | <div><img src={img4} alt=""/></div> 27 | <div><img src={img5} alt=""/></div> 28 | </Swiper> 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 | <div className="title"> 6 | <h3>{props.title}</h3> 7 | </div> 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 | <div className="home"> 69 | {loading && <Loading/>} 70 | <div> 71 | <div className="home-header"> 72 | <img src="http://qidian.gtimg.com/qdm/img/logo-qdm.0.50.svg" alt=""/> 73 | <i className="iconfont icon-bookshelf" onClick={() => this.context.router.push(`/bookshelf`)}></i> 74 | </div> 75 | <Swiper/> 76 | <nav className="home-nav"> 77 | {this.state.type.map((item, idx) => 78 | <Link to={`/category/?type=${idx + 1}`} 79 | className="guide-nav-div" 80 | key={idx}> 81 | <i className="icon icon-sort"></i> 82 | <h4 className="guide-nav-h">{item}</h4> 83 | </Link> 84 | )} 85 | </nav> 86 | {!loading && 87 | <div> 88 | <Recommend booklist={this.filters(booklist, 'hot')} title="热门小说"/> 89 | <Recommend booklist={this.filters(booklist, 'top')} title="排行榜"/> 90 | <Recommend booklist={this.filters(booklist, 'free')} title="限时免费"/> 91 | <BookList booklist={this.filters(booklist, 'new')} title="新书抢鲜"/> 92 | <BookList booklist={this.filters(booklist, 'end')} title="畅销完本"/> 93 | <BookList booklist={this.filters(booklist, 'like')} title="猜你喜欢"/> 94 | </div>} 95 | </div> 96 | </div> 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 | <div className="loading-component"> 41 | <svg className="spinner" style={{width:`${size}px`,height:`${size}px`}} viewBox="0 0 66 66" 42 | xmlns="http://www.w3.org/2000/svg"> 43 | <circle className="path" style={{stroke: color}} fill="none" strokeWidth={stroke} strokeLinecap="round" 44 | cx="33" 45 | cy="33" r="30"></circle> 46 | </svg> 47 | </div> 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 | <div className='NotFound'> 40 | <h1> 41 | {this.props.count} 42 | <button onClick={this.onClickAdd.bind(this)}>+</button> 43 | <button onClick={this.onClickDec.bind(this)}>-</button> 44 | </h1> 45 | </div> 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 | <div> 26 | <PersonInput addPerson={this.props.actions.addPerson}/> 27 | <PersonList people={people}/> 28 | </div> 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 | <div> 7 | {person.firstname} - {person.lastname} 8 | </div> 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 | <div> 34 | <input type="text" id="firstname" placeholder="First Name"/> 35 | <input type="text" id="lastname" placeholder="Last Name"/> 36 | <button onClick={this.onAddPersonClick}>Add Person</button> 37 | </div> 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 | <div> 8 | {people.map((person, idx) => 9 | <Person key={idx} person={person}/> 10 | )} 11 | </div> 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 | <div className="bottom-nav"> 34 | <div className="item menu-button" onClick={this.showListPanel.bind(this)}> 35 | <span className="icon-text"> 36 | <i className="iconfont icon-menu"></i> 37 | 目录 38 | </span> 39 | </div> 40 | <div className="item" id="font-button" onClick={this.showFontPanel.bind(this)}> 41 | <span className={`icon-text ${this.props.font_panel ? 'active': ''}`}> 42 | <i className="iconfont icon-font"></i> 43 | 字体 44 | </span> 45 | </div> 46 | <div className="item" id="night-button" onClick={this.switchNight.bind(this)}> 47 | {this.props.bg_night ? 48 | <span className="icon-text"> 49 | <i className="iconfont icon-day"></i> 50 | 白天 51 | </span> 52 | : 53 | <span className="icon-text"> 54 | <i className="iconfont icon-night"></i> 55 | 夜间 56 | </span> 57 | } 58 | </div> 59 | </div> 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 | <div className="chapterContent"> 5 | {props.content.map((item, idx) => 6 | <p key={idx}>{item}</p> 7 | )} 8 | </div> 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 | <div 32 | style={this.props.list_panel ? cover :Object.assign(cover, hide)} 33 | onClick={this.hideListPanel}></div> 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(<div 39 | className={`bk-container ${i === this.state.now_color ? 'bk-container-current' : ''}`} 40 | key={i}> 41 | <div className="color_btn" onClick={this.changeColor.bind(this, i)}></div> 42 | </div>) 43 | } 44 | return ( 45 | <div className="top-nav-pannel font-container" id="font-container"> 46 | <div className="child-mod"> 47 | <span>字号</span> 48 | <button id="large-font" className="spe-button" onClick={this.addFz}> 49 | 大 50 | </button> 51 | <button id="small-font" className="spe-button" onClick={this.subFz}> 52 | 小 53 | </button> 54 | </div> 55 | <div className="child-mod" id="bk-container"> 56 | <span>背景</span> 57 | {items} 58 | </div> 59 | </div> 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 | <div className={`list-panel${this.props.list_panel ? ' show': ''}`}> 57 | <div className="list"> 58 | <div className="list-nav"> 59 | <i className="iconfont icon-fanhui" onClick={this.hideListPanel}></i> 60 | <h3>目录</h3> 61 | </div> 62 | <div className="list-content"> 63 | <ul> 64 | {this.state.chapterList.map((item, idx) => 65 | <li 66 | key={idx} 67 | onClick={this.redirectTo.bind(this, idx + 1)} 68 | style={this.props.curChapter === idx + 1 ? {color: '#ed424b'}: {}}> 69 | · {idx+1}. {item} 70 | </li> 71 | )} 72 | </ul> 73 | </div> 74 | </div> 75 | </div> 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 | <div className="top-nav"> 16 | <i className="iconfont icon-back" onClick={this.goBack}></i> 17 | <div className="nav-title">返回</div> 18 | </div> 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 | <div id="reader"> 224 | {loading && <Loading className="loading"/>} 225 | {bar && <TopNav/>} 226 | <div 227 | className="read-container" 228 | data-bg={bg_color} 229 | data-night={bg_night} 230 | ref="fz_size" 231 | style={{fontSize: `${fz_size}px`}}> 232 | <h4>{title}</h4> 233 | {!loading && <Content content={content}/>} 234 | {!loading && 235 | <div className="btn-bar"> 236 | <ul className="btn-tab"> 237 | <li className="prev-btn" onClick={this.prevChapter}>上一章</li> 238 | <li className="next-btn" onClick={this.nextChapter}>下一章</li> 239 | </ul> 240 | </div>} 241 | </div> 242 | <div className="page-up" onClick={this.pageUp}></div> 243 | <div className="click-mask" onClick={this.toggleBar.bind(this, !this.state.bar)}></div> 244 | <div className="page-down" onClick={this.pageDown}></div> 245 | {bar && <BottomNav 246 | font_panel={font_panel} 247 | bg_night={bg_night} 248 | showFontPanel={actions.showFontPanel} 249 | switchNight={actions.switchNight} 250 | showListPanel={actions.showListPanel}/>} 251 | {font_panel && <div className="top-nav-pannel-bk font-container"></div>} 252 | {font_panel && <FontNav/>} 253 | <Cover 254 | showListPanel={actions.showListPanel} 255 | list_panel={list_panel}/> 256 | <ListPanel 257 | bookId={params.id} 258 | hideBar={this.toggleBar} 259 | saveBooksInfo={this.saveBooksInfo} 260 | api={api} 261 | list_panel={list_panel} 262 | curChapter={curChapter} 263 | curChapterAction={actions.curChapter} 264 | showListPanel={actions.showListPanel}/> 265 | </div> 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 | <Provider store={store}> 15 | <Routes history={hashHistory}/> 16 | </Provider>, 17 | document.getElementById('root') 18 | ); 19 | 20 | if (module.hot) { 21 | module.hot.accept('./components/App', () => { 22 | ReactDOM.render( 23 | <Provider store={store}> 24 | <Routes history={hashHistory}/> 25 | </Provider>, 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 | <Router {...props}> 17 | <Route path="/" component={App}> 18 | <IndexRoute component={Home}/> 19 | <Route path="bookdetail/:id" component={BookDetail}/> 20 | <Route path="bookshelf" component={BookShelf}/> 21 | <Route path="category" component={Category}/> 22 | <Route path="reader/:id" component={Reader}/> 23 | <Route path="people" component={People}/> 24 | <Route path="about" component={About}/> 25 | <Route path="*" component={NotFound}/> 26 | </Route> 27 | </Router> 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 | --------------------------------------------------------------------------------