├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── api └── readme.md ├── demo ├── data.json └── template.html ├── dist ├── icon.png ├── index.css ├── index.html ├── index.js ├── search.svg └── share.wechat.png ├── package.json ├── postcss.config.js ├── public ├── icon.png ├── index.dev.html ├── index.html ├── search.svg └── share.wechat.png ├── screenshots ├── 00_index.png ├── 01_reader.png ├── 01_reader_close.png ├── 03_chapter.png ├── 04_setting.png ├── 05_search.png ├── 06_delete.png ├── 07_delete_done.png ├── 08_detail.png ├── delete_all.png ├── main_all.png ├── myreader-online-qrcode.png ├── reader_all.png ├── retry.jpg └── source_min.jpg ├── server.js ├── src ├── components │ ├── GaussianBlur │ │ ├── index.js │ │ └── style.less │ ├── InputRange │ │ ├── index.js │ │ └── index.less │ ├── ListItem │ │ ├── index.js │ │ └── index.less │ ├── Loading │ │ ├── index.js │ │ ├── index.less │ │ └── loading.gif │ ├── ProgressLayer │ │ ├── index.js │ │ └── index.less │ ├── SearchBar │ │ ├── index.js │ │ └── style.less │ └── Touch │ │ └── index.js ├── index.dev.js ├── index.js ├── index.less ├── router.js ├── routes │ ├── Chapters │ │ ├── index.js │ │ └── index.less │ ├── Detail │ │ ├── back.svg │ │ ├── index.js │ │ └── index.less │ ├── IndexPage │ │ ├── BookList.js │ │ ├── BookList.less │ │ ├── Current.js │ │ ├── Current.less │ │ ├── index.js │ │ ├── index.less │ │ └── search.svg │ ├── Loading │ │ └── index.js │ ├── Reader │ │ ├── Content.js │ │ ├── Content.less │ │ ├── Head.js │ │ ├── Head.less │ │ ├── Loading.js │ │ ├── Loading.less │ │ ├── Setting.js │ │ ├── Setting.less │ │ ├── close.svg │ │ ├── index.js │ │ ├── index.less │ │ └── loading.svg │ └── Search │ │ ├── Item.js │ │ ├── Item.less │ │ ├── index.js │ │ └── index.less ├── services │ └── reader.js ├── store │ ├── effects │ │ ├── common.js │ │ ├── index.js │ │ ├── reader.js │ │ └── search.js │ ├── index.js │ └── reducer │ │ ├── common.js │ │ ├── index.js │ │ ├── reader.js │ │ ├── search.js │ │ ├── setting.js │ │ └── store.js └── utils │ ├── common.js │ ├── constants.js │ ├── recommond.js │ └── request.js ├── webpack.config.js ├── webpack.dev.config.js └── webpack.dll.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "es2015", { "modules": false } ], 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/components/Ui/Editor/js/wangEditor.js 2 | /node_modules/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "jsx-a11y/href-no-hash": [0], 6 | "no-underscore-dangle": [0], 7 | "generator-star-spacing": [0], 8 | "consistent-return": [0], 9 | "react/forbid-prop-types": [0], 10 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 11 | "global-require": [1], 12 | "import/prefer-default-export": [0], 13 | "react/jsx-no-bind": [0], 14 | "react/prop-types": [0], 15 | "react/prefer-stateless-function": [0], 16 | "no-else-return": [0], 17 | "no-restricted-syntax": [0], 18 | "import/no-extraneous-dependencies": [0], 19 | "no-use-before-define": [0], 20 | "jsx-a11y/no-static-element-interactions": [0], 21 | "no-nested-ternary": [0], 22 | "arrow-body-style": [0], 23 | "import/extensions": [0], 24 | "no-bitwise": [0], 25 | "no-cond-assign": [0], 26 | "import/no-unresolved": [0], 27 | "require-yield": [1], 28 | "no-console": [0] 29 | }, 30 | "parserOptions": { 31 | "ecmaFeatures": { 32 | "experimentalObjectRestSpread": true 33 | } 34 | }, 35 | "env": { 36 | "browser": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /dll 6 | 7 | # production 8 | # /dist 9 | 10 | # misc 11 | .idea 12 | .DS_Store 13 | npm-debug.log* 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /dll 6 | 7 | # production 8 | /dist 9 | 10 | # misc 11 | .DS_Store 12 | npm-debug.log* 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.10' 5 | - '0.12' 6 | - '4' 7 | - '5' 8 | - '6' 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MyReader 绿色版电子书阅读器 2 | 3 | ![main_all](https://raw.githubusercontent.com/liufulin90/myreader/master/screenshots/main_all.png) 4 | 5 | 在线地址:[http://myreader.linxins.com](http://myreader.linxins.com) 6 | 7 | 手机扫码体验: 8 | 9 | ![online-qrcode](https://raw.githubusercontent.com/liufulin90/myreader/master/screenshots/myreader-online-qrcode.png) 10 | 11 | ------- 12 | ## 目录索引 13 | > store的设计与实现 14 | - [阅读器](#阅读器) 15 | - [书架](#书架) 16 | > effects 的逻辑处理 17 | - [获取书源](#获取书源) 18 | - [章节列表 & 章节内容](#章节列表-章节内容) 19 | - [换源实现](#换源实现) 20 | - [切换章节](#切换章节) 21 | - [离线下载](#离线下载) 22 | - [本地存储 redux-persist](#本地存储redux-persist) 23 | > UI部分 24 | - [首页](#首页) 25 | - [阅读器](#阅读器) 26 | - [换肤](#换肤) 27 | - [删除实现](#删除实现) 28 | > 优化 29 | - [移动端优化](#移动端优化) 30 | - [CSS](#css) 31 | - [fetch-polyfill](#fetch-polyfill) 32 | - [fastclick](#fastclick) 33 | - [体积减小](#体积减小) 34 | > 最后 35 | - [后记](#后记) 36 | - [线上环境](#线上环境) 37 | 38 | ### 开始 39 | 40 | 本项目没有使用任何脚手架工具和ui框架,因为本项目比较小,在时间允许的情况下,还是希望尽可能自己走一遍流程。 41 | 42 | 开发环境依然是react全家桶,基于最新版的`webpack3`、`react15.6`、`react-router4`、`redux`、`redux-saga`实现,就是不折腾不痛快。过程中略有小坑,比如热更新啦,dll动态链接库啦,preact不兼容啦,以及最新版本带来的不兼容什么的,不过都已经被社区大神趟平了。 43 | 44 | ### store的设计与实现 45 | 46 | 首先来实现阅读器部分,关于电子阅读器我们可以总结出三个核心概念:**书源**、**章节列表**和**章节内容**。换源就是在书源中切换、跳转章节就是在章节列表中切换,我们只需要记录当前书源和当前章节就可以完整保存用户阅读进度。至于书籍详情当然也不能少,我们得知道当前到底看的是那一本书。 47 | 48 | reader代表阅读器和当前书籍,这里我们跳过优质书源,原因大家都懂。 49 | ╮( ̄▽ ̄)╭ 50 | 51 | #### 阅读器 52 | 53 | - src/store/reducer/reader.js 54 | ```javascript 55 | const initState = { 56 | id: null, // 当前书籍id,默认没有书籍 57 | currentSource: 1, // 当前源下标:默认为1,跳过优质书源 58 | currentChapter: 0, // 当前章节下标 59 | source: [], // 源列表 60 | chapters: [], // 章节列表 61 | chapter: {}, // 当前章节 62 | detail: {}, // 书籍详情 63 | menuState: false, // 底部菜单是否展开,默认不展开 64 | }; 65 | 66 | function reader(state = initState, action) { 67 | switch (action.type) { 68 | case 'reader/save': 69 | return { 70 | ...state, 71 | ...action.payload, 72 | }; 73 | case 'reader/clear': 74 | return initState; 75 | default: 76 | return { 77 | ...state, 78 | }; 79 | } 80 | } 81 | export default reader; 82 | ``` 83 | 84 | #### 书架 85 | 86 | 因为我们并不是要做只能阅读一本书的鸡肋,我们要的是能在多本书籍之间快速切换,不但能够保存阅读进度(当前书源和当前章节),并且可以在缓存中读取数据,过滤掉那些不必要的服务器请求。 87 | 88 | 为此,我们可以模仿现实中的书架来实现这个功能:前面提到的reader是当前正在阅读的书籍,它是完整的包含了一本书籍所有信息的个体,而书架则是很多个这样的个体的集合。因此切换书籍的动作,其实就是将书籍放回书架,再从书架中拿出一本书的过程,如果在书架中找到了这本书,便直接取出,进而得到上次阅读这本书的全部数据,如果没有找到这本书,就从服务器获取并初始化阅读器。 89 | 90 | - src/store/reducer/store.js 91 | ```javascript 92 | function store(state = {}, action) { 93 | switch (action.type) { 94 | case 'store/put': { // 将书籍放入书架 95 | if (action.key) { 96 | return { 97 | ...state, 98 | [action.key]: { 99 | ...state[action.key], 100 | ...action.payload, 101 | }, 102 | }; 103 | } else { 104 | return { 105 | ...state, 106 | }; 107 | } 108 | } 109 | case 'store/save': // 初始化书架 110 | return { 111 | ...state, 112 | ...action.payload, 113 | }; 114 | case 'store/delete': // 删除书籍 115 | return { 116 | ...state, 117 | [action.key]: undefined, 118 | }; 119 | case 'store/clear': // 清空书架 120 | return {}; 121 | default: 122 | return { 123 | ...state, 124 | }; 125 | } 126 | } 127 | 128 | export default store; 129 | ``` 130 | 131 | ### effects 的逻辑处理 132 | 133 | 获取书源,可以说是项目中最核心的功能了。其实这个方法叫换源有些欠妥,应该叫做换书。主要功能就是实现了上文提到的将当前阅读书籍放回书架,并取出新书这个功能。并且这个方法只有在阅读一本新书时才会调用。 134 | 135 | 要考虑的情况基本就是用户第一次打开应用,没有当前阅读书籍,此时直接获取书源进行下一步下一步即可。当用户已经在看一本书,并且切换到同一本书时,直接返回,如果切换到另一本书,则将当前数据连同书籍信息一起打包放回书架,当然在此之前要先查看书架中有无这本书,有则取出,无则继续获取书源。需要注意的是,这里不要使用数组,而是将书籍id作为键值存在书架中,这会使得获取和查找都十分方便。 136 | 137 | 需要注意的一点是,项目本质上是web应用,用户可能从url进入任意页面,所以要做好异常情况的处理,例如没有书籍详情等。 138 | 139 | #### 获取书源 140 | 141 | - src/store/effects/reader.js 142 | ```javascript 143 | /** 144 | * 获取书源 145 | * @param query 146 | */ 147 | function* getSource({ query }) { 148 | try { 149 | const { id } = query; 150 | // 这里获得整个缓存中的store,并对应上reader的store。其reader的store结构参考store/reducer/reader.js initState 151 | const { reader: { id: currentId, detail: { title } } } = yield select(); 152 | if (currentId) { 153 | if (id !== currentId) { 154 | const { reader, store: { [id]: book } } = yield select(); 155 | console.log(`将《${title}》放回书架`); 156 | yield put({ type: 'store/put', payload: { ...reader }, key: currentId }); 157 | yield put({ type: 'reader/clear' }); 158 | if (book && book.detail && book.source) { 159 | console.log(`从书架取回《${book.detail.title}》`); 160 | yield put({ type: 'reader/save', payload: { ...book } }); 161 | return; 162 | } 163 | } else { 164 | return; 165 | } 166 | } 167 | let { search: { detail } } = yield select(); 168 | yield put({ type: 'common/save', payload: { loading: true } }); 169 | if (!detail._id) { 170 | console.log('详情不存在,前往获取'); 171 | detail = yield call(readerServices.getDetail, id); 172 | } 173 | const data = yield call(readerServices.getSource, id); 174 | console.log(`从网络获取《${detail.title}》`); 175 | yield put({ type: 'reader/save', payload: { source: data, id, detail } }); 176 | console.log(`阅读:${detail.title}`); 177 | yield getChapterList(); 178 | } catch (error) { 179 | console.log(error); 180 | } 181 | yield put({ type: 'common/save', payload: { loading: false } }); 182 | } 183 | ``` 184 | 185 | #### 章节列表-章节内容 186 | 187 | 获取章节列表和章节内容比较简单,只需稍稍做些异常情况的处理即可。 188 | 189 | - src/store/effects/reader.js 190 | ```javascript 191 | /** 192 | * 章节列表 193 | */ 194 | function* getChapterList() { 195 | try { 196 | const { reader: { source, currentSource } } = yield select(); 197 | console.log('获取章节列表', currentSource, source.length, JSON.stringify(source)); 198 | if (currentSource >= source.length) { 199 | console.log('走到这里说明所有书源都已经切换完了'); 200 | yield put({ type: 'reader/save', payload: { currentSource: 0 } }); 201 | yield getChapterList(); 202 | return; 203 | } 204 | const { _id, name = '未知来源' } = source[currentSource]; 205 | console.log(`书源: ${name}`); 206 | const { chapters } = yield call(readerServices.getChapterList, _id); 207 | yield put({ type: 'reader/save', payload: { chapters } }); 208 | yield getChapter(); 209 | } catch (error) { 210 | console.log(error); 211 | } 212 | } 213 | 214 | /** 215 | * 获取章节内容 216 | */ 217 | function* getChapter() { 218 | try { 219 | const { reader: { chapters, currentChapter, 220 | downloadStatus, chaptersContent } } = yield select(); 221 | 222 | if (downloadStatus) { // 已下载直接从本地获取 223 | const chapter = chaptersContent[currentChapter || 0]; 224 | console.log(`章节: ${chapter.title}`); 225 | yield put({ type: 'reader/save', payload: { chapter } }); 226 | window.scrollTo(0, 0); 227 | } else { 228 | const { link } = chapters[currentChapter || 0]; 229 | yield put({ type: 'common/save', payload: { loading: true } }); 230 | const { chapter } = yield call(readerServices.getChapter, link); 231 | if (chapter) { 232 | console.log(`章节: ${chapter.title}`); 233 | yield put({ type: 'reader/save', payload: { chapter } }); 234 | window.scrollTo(0, 0); 235 | } else { 236 | console.log('章节获取失败'); 237 | yield getNextSource(); 238 | } 239 | } 240 | } catch (error) { 241 | console.log(error); 242 | } 243 | yield put({ type: 'common/save', payload: { loading: false } }); 244 | } 245 | ``` 246 | 247 | #### 换源实现 248 | 249 | 同是核心功能,这个必须有。换源其实非常简单,做一个智(sha)能(gua)换源吧(根据书源获取`具体章节`,如果获取不到就拿下一个书源再获取`其具体章节`,直到获取到正确的为止)。 250 | 251 | 换源其实就是操作标记书源的指针,这很容易,我们关心的是何时换源。经过测试,发现获取章节列表这一步几乎都没有问题,错误基本上是发生在获取`具体章节`这一步。因此,我们只要在章节列表中稍作判断即可实现自动换源。换源方法如下。 252 | 253 | - src/store/effects/reader.js 254 | ```javascript 255 | /** 256 | * 获取下一个书源。 257 | * 在获取书源后无法获取 具体章节 便会获取下一个书源。直到所有书源换完为止 258 | */ 259 | function* getNextSource() { 260 | try { 261 | const { reader: { source, currentSource } } = yield select(); 262 | let nextSource = (currentSource || 1) + 1; 263 | console.log(`开始第${nextSource}个书源`); 264 | if (nextSource >= source.length) { 265 | console.log('没有可用书源,切换回优质书源'); 266 | nextSource = 0; 267 | } 268 | console.log(`正在尝试切换到书源: ${source[nextSource] && source[nextSource].name}`); 269 | yield put({ type: 'reader/save', payload: { currentSource: nextSource } }); 270 | yield getChapterList(); 271 | } catch (error) { 272 | console.log(error); 273 | } 274 | } 275 | ``` 276 | 效果如下,当1号书源出错后我们自动跳转到下一个书源,很方便有木有。 277 | 278 | ![retry](https://raw.githubusercontent.com/liufulin90/myreader/master/screenshots/retry.jpg) 279 | 280 | #### 切换章节 281 | 282 | 非常简单,稍微做下异常处理就好。 283 | 284 | - src/store/effects/reader.js 285 | ```javascript 286 | function* goToChapter({ payload }) { 287 | try { 288 | const { reader: { chapters } } = yield select(); 289 | const nextChapter = payload.nextChapter; 290 | if (nextChapter > chapters.length) { 291 | console.log('没有下一章啦'); 292 | return; 293 | } 294 | if (nextChapter < 0) { 295 | console.log('没有上一章啦'); 296 | return; 297 | } 298 | yield put({ type: 'reader/save', payload: { currentChapter: nextChapter } }); 299 | yield getChapter(); 300 | } catch (error) { 301 | console.log(error); 302 | } 303 | } 304 | ``` 305 | #### 离线下载 306 | 307 | 考虑到节约流量问题,获取一个可用的书源后对每个章节去下载相应的章节内容,然后存储在本地(chaptersContent)。 308 | - src/store/effects/reader.js 309 | ```javascript 310 | /** 311 | * 离线下载书籍 获取书源 312 | * @param query 313 | */ 314 | function* downGetSource({ query }) { 315 | try { 316 | const { id, download } = query; 317 | // 这里获得整个缓存中的store,并对应上reader的store。其reader的store结构参考store/reducer/reader.js initState 318 | // 同时获取该书是否下载的状态 319 | const { reader: { id: currentId, detail: { title } } } = yield select(); 320 | console.log(`当前书信息currentId:${currentId} , id:${id}, title:${title}`); 321 | if (download) { 322 | const judgeRet = yield findBookByStoreId(id); 323 | console.log('判断返回的结果:', judgeRet); 324 | if (judgeRet.has && judgeRet.downloadStatus) { 325 | console.log('已下载,直接阅读'); 326 | yield put({ type: 'reader/save', payload: { downloadStatus: true } }); 327 | return; 328 | } 329 | 330 | yield put({ type: 'common/save', payload: { loading: true } }); 331 | let { search: { detail } } = yield select(); 332 | if (!detail._id) { 333 | console.log('下载时详情不存在,前往获取'); 334 | detail = yield call(readerServices.getDetail, id); 335 | } 336 | // 获得的所有书源 337 | const sourceList = yield call(readerServices.getSource, id); 338 | let sourceIndex = 0; // 标记书源当前脚标 339 | let chapterList = []; // 初始化可用章节列表 340 | // 循环获得一个可用的书源,达到自动换源的效果 341 | for (let i = 0, len = sourceList.length; i < len; i += 1) { 342 | if (sourceList[i].name !== '优质书源') { 343 | const { chapters } = yield call(readerServices.getChapterList, sourceList[i]._id); 344 | if (chapters.length) { 345 | const { chapter, ok } = yield call(readerServices.getChapter, chapters[i].link); 346 | if (ok && chapter) { 347 | console.log(`成功获取一个书源 index: ${sourceIndex} 章节总数 ${chapters.length}`); 348 | console.log('要下载的书源', sourceList[sourceIndex]); 349 | // 成功获取一个书源,并将相关信息先存下来 350 | yield put({ type: 'reader/save', payload: { source: sourceList, id, detail, chapters, chapter, downloadPercent: 0, currentSource: sourceIndex, currentChapter: 0 } }); 351 | chapterList = chapters; 352 | break; 353 | } 354 | } 355 | } 356 | sourceIndex += 1; 357 | } 358 | // 开始循环章节获得章节内容,并保存在本地 359 | const chaptersContent = []; // 章节列表及其内容 360 | for (let i = 0, len = chapterList.length; i < len; i += 1) { 361 | const { chapter } = yield call(readerServices.getChapter, chapterList[i].link); 362 | chaptersContent[i] = chapter; 363 | // 添加下载进度 364 | yield put({ type: 'reader/save', payload: { downloadPercent: (i / len) * 100 } }); 365 | } 366 | // 取消下载进度 367 | yield put({ type: 'reader/save', payload: { downloadPercent: 0 } }); 368 | 369 | console.log('保存的章节内容', chaptersContent); 370 | yield put({ type: 'reader/save', payload: { chaptersContent } }); 371 | 372 | // 没有下载 373 | if (!judgeRet.downloadStatus) { 374 | const { reader, store: { [id]: book }, search: { detail: searchDetail } } = yield select(); 375 | reader.downloadStatus = true; // 设定已下载 376 | console.log('将书籍存入书架'); 377 | yield put({ type: 'store/put', payload: { ...reader }, key: id }); 378 | yield put({ type: 'reader/clear' }); 379 | if (book && book.detail && book.source) { // 如果原书架中有对应的书则取出,否则用当前的书 380 | console.log(`从书架取回《${book.detail.title}》`); 381 | yield put({ type: 'reader/save', payload: { ...book } }); 382 | } else { 383 | console.log('原书架没书,用当前书'); 384 | yield put({ type: 'reader/save', payload: { ...reader } }); 385 | } 386 | searchDetail.downloadStatus = true; 387 | yield put({ type: 'search/save', payload: { searchDetail } }); 388 | } 389 | } 390 | } catch (error) { 391 | console.log(error); 392 | } 393 | yield put({ type: 'common/save', payload: { loading: false } }); 394 | } 395 | ``` 396 | 397 | #### 本地存储redux-persist 398 | 399 | 这里咱们使用了 `redux-persist` 来做本地存储,非常方便,redux先关数据自动存储和获取 400 | - src/store/effects/reader.js 401 | ```javascript 402 | import { REHYDRATE } from 'redux-persist/constants'; 403 | /** 404 | * 本地存储调用 405 | * @param payload 406 | */ 407 | function* reStore({ payload }) { 408 | try { 409 | const { reader, store, setting } = payload; 410 | yield put({ type: 'reader/save', payload: { ...reader } }); 411 | yield put({ type: 'store/save', payload: { ...store } }); 412 | yield put({ type: 'setting/save', payload: { ...setting } }); 413 | } catch (error) { 414 | console.log(error); 415 | } 416 | } 417 | export default [ 418 | takeLatest(REHYDRATE, reStore), 419 | ]; 420 | ``` 421 | 以上基本上已经完整实现了阅读器的核心部分,至于搜索和详情页,限于篇幅不再赘述。 422 | 423 | ### UI部分 424 | 425 | 本想使用material-ui,但它实在是太重了,而我希望这个项目是轻量且高效的,最后还是决定自行设计ui。 426 | 427 | #### 首页 428 | 429 | 首页比较纠结,曾经放了很多自以为炫酷的高斯模糊和动画,但过多的效果会降低体验,最终还是选择了走了简洁的路子。 430 | 431 | 上半部分是当前阅读书籍,仅显示一些关键信息。下半部分是书架,存放以往的阅读进度。 432 | 433 | 从redux获取数据 434 | 435 | - src/routes/IndexPage/index.js 436 | ```javascript 437 | function mapStateToProps(state) { 438 | const { detail } = state.reader; 439 | const list = state.store; 440 | const store = Object.keys(list).map((id) => { 441 | // 找出书架上所有书籍的详细信息 442 | return list[id] ? list[id].detail : {}; 443 | }).filter((i) => { 444 | // 过滤掉异常数据和当前阅读 445 | return i._id && i._id !== detail._id; 446 | }); 447 | return { 448 | store, 449 | // 如果是一本书都没有,推荐src/utils/recommond.js的第一个《斗破苍穹》 450 | current: detail._id ? detail : recommend, 451 | }; 452 | } 453 | ``` 454 | 455 | #### 阅读器 456 | 457 | ![reader_all](https://raw.githubusercontent.com/liufulin90/myreader/master/screenshots/reader_all.png) 458 | 459 | ok,扯了许久,终于见到本尊了,这是阅读器最核心的页面,谈不上有什么设计,就是追求简洁易用。 460 | 461 | 主体部分就是原生的`body`,这样滚动起来会非常流畅。需要注意下`api`提供的数据如何显示在`react`中。代码很短,大意就是将换行符作为依据转换成数组显示,这样方便设置css样式。 462 | 463 | - src/routes/Reader/Content.js 464 | ```javascript 465 | export default ({ content, style }) => (
466 | { content && content.split('\n').map(i =>

{i}

) } 467 |
); 468 | ``` 469 | 470 | 稍微体验下可以发现,头部可收缩,显示当前书籍和当前章节,以及一个关闭按钮。基于`react-headroom`组件实现。 471 | 472 | 为了追求简洁,我们把菜单做成一个可展开以及关闭的形式,点击右侧的按钮会在页面最下方显示出菜单,这样更方便随时可以查看下一章、上一章、章节列表、设置。 473 | 474 | 菜单只有4个,设置、章节列表、上一章和下一章。点击设置会弹出框,支持换肤和调节字体大小,这些只是基本的,有时间再做亮度调节自动翻页和语音朗读吧。实现方法很简单,贴出这段代码你一定秒懂。 475 | 476 | - src/routes/Reader/Setting.js 477 | ```javascript 478 | this.stopEvent = (e) => { 479 | // 阻止合成事件间的冒泡 480 | e.stopPropagation(); 481 | // 阻止合成事件与最外层document上的事件间的冒泡 482 | e.nativeEvent.stopImmediatePropagation(); 483 | e.preventDefault(); 484 | return false; 485 | }; 486 | ``` 487 | 488 | 章节列表更(mei)加(you)简(yong)易(xin),稍微注意下如何将当前章节显示在列表中吧。我是利用锚点链接实现的,再配合一个`sider`组件,某修仙传几千章节跳转起来也很轻松。 489 | 490 | - src/routes/Chapters/index.js 491 | ```javascript 492 | // 滑动顶部进度条 sider 493 | this.skip = () => { 494 | setTimeout(() => { 495 | document.getElementById(this.range.value).scrollIntoView(false); 496 | }, 100); 497 | } 498 | ``` 499 | 500 | #### 换肤 501 | 502 | 说起来很好实现,无非是先预设一套主题参数,需要哪个点那个。 503 | 504 | - src/utils/constants.js 505 | ```javascript 506 | export const COLORS = [ 507 | { 508 | background: '#b6b6b6', 509 | }, { 510 | background: '#999484', 511 | }, { 512 | background: '#a0b89c', 513 | }, { 514 | background: '#cec0a4', 515 | }, { 516 | background: '#d5b2be', 517 | }, { 518 | color: 'rgba(255,255,255,0.8)', 519 | background: '#011721', 520 | }, { 521 | color: 'rgba(255,255,255,0.7)', 522 | background: '#2c2926', 523 | }, { 524 | background: '#c4ada4', 525 | }, 526 | ]; 527 | ``` 528 | 在`redux`中维护一个setting字段,专门放用户设置。在阅读器中获取并设置为主题即可。 529 | 530 | - src/routes/Reader/index.js 531 | ```javascript 532 | function mapStateToProps(state) { 533 | const { chapter, chapters, currentChapter = 0, detail, menuState } = state.reader; 534 | const { logs } = state.common; 535 | return { 536 | logs, 537 | chapter, 538 | chapters, 539 | detail, 540 | currentChapter, 541 | menuState, 542 | ...state.setting, 543 | }; 544 | } 545 | ``` 546 | 547 | 切换皮肤的时候将新的数据保存到redux就实现了换肤功能。 548 | 549 | - src/routes/Reader/Setting.js 550 | ```javascript 551 | // 设置主题颜色 552 | this.setThemeColor = (key, val) => { 553 | this.props.dispatch({ 554 | type: 'setting/save', 555 | payload: { 556 | [key]: val, 557 | }, 558 | }); 559 | }; 560 | // 调整字体大小 561 | this.setFontSize = (num) => { 562 | const fontSize = this.props.style.fontSize + num; 563 | this.props.dispatch({ 564 | type: 'setting/save', 565 | payload: { 566 | style: { 567 | ...this.props.style, 568 | fontSize, 569 | }, 570 | }, 571 | }); 572 | }; 573 | ``` 574 | 575 | #### 删除实现 576 | 577 | 为了不再增加新的ui,决定使用长按删除。但是这个列表不仅需要支持长按和短按,还需要支持滚动,我又不想使用`hammer.js`这种重型库,只得手写了一个同时支持长按和短按的组件。 578 | 579 | - src/components/Touch/index.js 580 | ```javascript 581 | export default ({ children, onPress, onTap }) => { 582 | let timeout; 583 | let pressed = false; 584 | let cancel = false; 585 | function touchStart() { 586 | timeout = setTimeout(() => { 587 | pressed = true; 588 | if (onPress) onPress(); 589 | }, 500); 590 | return false; 591 | } 592 | function touchEnd() { 593 | clearTimeout(timeout); 594 | if (pressed) { 595 | pressed = false; 596 | return; 597 | } 598 | if (cancel) { 599 | cancel = false; 600 | return; 601 | } 602 | if (onTap) onTap(); 603 | return false; 604 | } 605 | function touchCancel() { 606 | cancel = true; 607 | } 608 | return (
614 | { children } 615 |
); 616 | }; 617 | ``` 618 | 619 | 至于长按弹窗的ui我懒得设计了,短时间也做不出什么好的效果,还是继续使用`sweet-alert2`吧,这个插件着实不错。 620 | 621 | ![delete_all](https://raw.githubusercontent.com/liufulin90/myreader/master/screenshots/delete_all.png) 622 | 623 | 至此我们已经实现了全部功能和ui。 624 | 625 | 626 | ### 优化 627 | 628 | #### 移动端优化 629 | 630 | ```html 631 | 632 | 633 | 634 | // 这个比较重要,可以在ios系统自带safari中添加到主屏幕,这条设置会启用全屏模式,体验不错 635 | 636 | 637 | 638 | 639 | ``` 640 | 641 | #### CSS 642 | 643 | ```css 644 | * { 645 | user-select: none; 646 | // 禁止用户选中文本 647 | 648 | -webkit-appearance: none; 649 | // 改变按钮默认风格 650 | 651 | -webkit-touch-callout: none; 652 | // 禁用系统默认菜单 653 | } 654 | 655 | input { 656 | user-select: auto; 657 | -webkit-touch-callout: auto; 658 | // 解除对input组件的限制,否则无法正常输入 659 | } 660 | ``` 661 | 662 | #### fetch-polyfill 663 | 664 | 解决fetch浏览器不兼容问题 665 | - src/utils/request.js 666 | ```javascript 667 | import 'fetch-polyfill'; 668 | ``` 669 | 670 | #### fastclick 671 | 672 | 如果 `viewport meta` 标签 中设置了 `width=device-width`, `Android` 上的 `Chrome 32+` 会禁用 300ms 延时。 673 | 674 | - myreader/src/router.js 675 | ```javascript 676 | import FastClick from 'fastclick'; 677 | FastClick.attach(document.body); 678 | ``` 679 | 你懂得,移除移动端300毫秒延迟,不过这会带来其他问题,比如长按事件异常,滚动事件异常什么的。因为滑动touchmove触发了touchend事件,需要先取消掉touchstart上挂载的动作。 680 | 681 | 682 | #### 体积减小 683 | 684 | 项目初期打包后竟然有700k+,首次加载速度不忍直视。前面已经提到,放弃各种框架和动画之后,体积已经大幅减少。不过有react,react-router,redux,redux-saga这些依赖在,体积再小也小不到那里去。但好消息是我们可以使用preact替换react,从而节省约120kb左右。 685 | 686 | 只需要安装preact并设置别名即可。此处有几个小坑,一是别名的第三句,找了好久才在有个issue下发现,没有就无法运行。二是preact和react-hot-loader不太兼容,一起用会导致热更新失效。三是preact仍然有不兼容react的地方,需要仔细验证。 687 | 688 | ``` 689 | npm i -S preact preact-compat 690 | 691 | resolve: { 692 | alias: { 693 | react: 'preact-compat', 694 | 'react-dom': 'preact-compat', 695 | 'preact-compat': 'preact-compat/dist/preact-compat', 696 | //比较坑的是最后一句官网并未给出,导致一直报错,找了很久 697 | }, 698 | }, 699 | ``` 700 | 701 | 以及一系列优化以及gzip之后,项目index.js减小到了240kb,相比初期只有十分之一大小。 702 | 703 | 704 | ### 最后 705 | #### 后记 706 | 707 | 项目中所有数据来自追书神器,非常感谢!! 708 | 喜欢的同学可以`star`哦,欢迎提出建议。 709 | 本项目仅作用于在实战中学习前端技术,请勿他用。 710 | 711 | #### 线上环境 712 | - 这里使用node环境做本地server,启动: node server.js & 713 | 714 | 在线地址:[MyReader](http://myreader.linxins.com) 715 | 716 | github:[myreader](https://github.com/liufulin90/myreader) 717 | 718 | 719 | ``` 720 | cnpm i -D babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-stage-0 babel-preset-react webpack webpack-dev-server html-webpack-plugin eslint@^3.19.0 eslint-plugin-import eslint-loader eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-react babel-plugin-import file-loader babel-plugin-transform-runtime babel-plugin-transform-remove-console redux-devtools style-loader less-loader css-loader postcss-loader autoprefixer rimraf extract-text-webpack-plugin copy-webpack-plugin react-hot-loader@next less 721 | 722 | cnpm i -S react react-dom react-router react-router-dom redux react-redux redux-saga material-ui@next material-ui-icons fetch-polyfill 723 | 724 | cnpm i -S preact preact-compat react-router react-router-dom redux react-redux redux-saga 725 | 726 | proxy: { 727 | '/api': { 728 | target: 'http://api.zhuishushenqi.com/', 729 | changeOrigin: true, 730 | pathRewrite: { '^/api': '' }, 731 | }, 732 | '/chapter': { 733 | target: 'http://chapter2.zhuishushenqi.com/', 734 | changeOrigin: true, 735 | pathRewrite: { '^/api': '' }, 736 | }, 737 | '/agent': { 738 | target: 'http://statics.zhuishushenqi.com/', 739 | changeOrigin: true, 740 | pathRewrite: { '^/api': '' }, 741 | }, 742 | }, 743 | ``` 744 | 745 | ## License 746 | (The MIT License) 747 | 748 | Copyright (c) 2017 [linxins](http://www.linxins.com) 749 | 750 | 751 | 752 | -------------------------------------------------------------------------------- /api/readme.md: -------------------------------------------------------------------------------- 1 | ### 设置代理 2 | 3 | ``` 4 | "proxy": { 5 | "/api": { 6 | "target": "http://api.zhuishushenqi.com/", 7 | "changeOrigin": true, 8 | "pathRewrite": { "^/api" : "" } 9 | }, 10 | "/chapter": { 11 | "target": "http://chapter2.zhuishushenqi.com/", 12 | "changeOrigin": true, 13 | "pathRewrite": { "^/api" : "" } 14 | } 15 | } 16 | ``` 17 | 18 | ### 查询书籍列表:`book/fuzzy-search` GET 19 | 20 | params 21 | 22 | ``` 23 | { 24 | query: String, // 书名 25 | start: Number, // 页码 26 | limit: Number // 页长 27 | } 28 | ``` 29 | 30 | example 31 | 32 | ``` 33 | book/fuzzy-search?query=凡人修仙传&start=0&limit=10 34 | ``` 35 | 36 | result 37 | 38 | ``` 39 | { 40 | "books": [{ 41 | "_id": "508662b8d7a545903b000027", 42 | "hasCp": true, 43 | "title": "凡人修仙传", 44 | "cat": "仙侠", 45 | "author": "忘语", 46 | "site": "zhuishuvip", 47 | "cover": "/agent/http://image.cmfu.com/books/107580/107580.jpg", 48 | "shortIntro": "一个普通山村小子,偶然下进入到当地江湖小门派,成了一名记名弟子。他以这样身份,如何在门派中立足,如何以平庸的资质进入到修仙者的行列,从而笑傲三界之中!", 49 | "lastChapter": "第十一卷 真仙降临 第两千四百四十六章 飞升仙界(大结局)", 50 | "retentionRatio": 60.12, 51 | "latelyFollower": 16628, 52 | "wordCount": 7647986 53 | }, { 54 | "_id": "54ff033f350e013b105be1fb", 55 | "hasCp": false, 56 | "title": "凡人修仙传后记", 57 | "cat": "玄幻", 58 | "author": "孤单wjw", 59 | "site": "w17k", 60 | "cover": "/agent/http://img.17k.com/images/bookcover/default_cover1.jpg", 61 | "shortIntro": "本书是忘大《凡人修仙传》的延续,主要讲述了韩立在处处是危机、步步是险境的仙界从一个小仙人经过重重磨难,处处谨小慎微,在各方势力的夹缝中成长的过程,最终成为了仙界...", 62 | "lastChapter": "第七章 仙灵丹和仙籍", 63 | "retentionRatio": 0, 64 | "latelyFollower": 239, 65 | "wordCount": 10353 66 | }], 67 | "ok": true 68 | } 69 | ``` 70 | 71 | ### 查询书籍详情:`book/ID` GET 72 | 73 | params 无 74 | 75 | example 76 | 77 | ``` 78 | /book/508662b8d7a545903b000027 79 | ``` 80 | 81 | result 82 | 83 | ``` 84 | { 85 | "_id": "508662b8d7a545903b000027", 86 | "author": "忘语", 87 | "cover": "/agent/http://image.cmfu.com/books/107580/107580.jpg", 88 | "creater": "iPhone 4", 89 | "longIntro": "一个普通山村小子,偶然下进入到当地江湖小门派,成了一名记名弟子。他以这样身份,如何在门派中立足,如何以平庸的资质进入到修仙者的行列,从而笑傲三界之中!", 90 | "title": "凡人修仙传", 91 | "cat": "幻想修仙", 92 | "majorCate": "仙侠", 93 | "minorCate": "幻想修仙", 94 | "_le": false, 95 | "allowMonthly": false, 96 | "allowVoucher": true, 97 | "allowBeanVoucher": false, 98 | "hasCp": true, 99 | "postCount": 2243, 100 | "latelyFollower": 16628, 101 | "followerCount": 38497, 102 | "wordCount": 7647986, 103 | "serializeWordCount": 0, 104 | "retentionRatio": "60.12", 105 | "updated": "2017-02-16T06:02:34.819Z", 106 | "isSerial": false, 107 | "chaptersCount": 2451, 108 | "lastChapter": "第十一卷 真仙降临 第两千四百四十六章 飞升仙界(大结局)", 109 | "gender": ["male"], 110 | "tags": ["热血", "法宝", "架空", "扮猪吃虎", "奇遇", "凡人", "修炼", "修仙", "仙侠"], 111 | "donate": false, 112 | "copyright": "阅文集团正版授权" 113 | } 114 | 115 | 116 | ``` 117 | 118 | ### 查询书源 119 | 120 | params 121 | ``` 122 | { 123 | view: summary, // 概要 124 | book: 508662b8d7a545903b000027, // id 125 | start: 0, // 页码 126 | limit: 10 // 页长 127 | } 128 | ``` 129 | 130 | example 131 | 132 | ``` 133 | api.zhuishushenqi.com/toc?view=summary&book=508662b8d7a545903b000027 134 | 135 | ``` 136 | result 137 | 138 | ``` 139 | [ 140 | { 141 | "_id": "56f8dbc4176d03ac1984484d", 142 | "source": "zhuishuvip", 143 | "name": "优质书源", 144 | "link": "http://vip.zhuishushenqi.com/toc/56f8dbc4176d03ac1984484d", 145 | "lastChapter": "第十一卷 真仙降临 第两千四百四十六章 飞升仙界(大结局)", 146 | "isCharge": false, 147 | "chaptersCount": 2451, 148 | "updated": "2017-02-16T06:02:34.819Z", 149 | "starting": true, 150 | "host": "vip.zhuishushenqi.com" 151 | }, 152 | { 153 | "_id": "58a60bdc808aaa192e318fe9", 154 | "lastChapter": "更新重要通告", 155 | "link": "http://www.hunhun520.com/book/fanrenxiuxianchuan/", 156 | "source": "hunhun", 157 | "name": "混混小说网", 158 | "isCharge": false, 159 | "chaptersCount": 2464, 160 | "updated": "2017-02-20T14:59:34.821Z", 161 | "starting": false, 162 | "host": "hunhun520.com" 163 | } 164 | ] 165 | ``` 166 | 167 | ### 查询章节列表(先获取书源id) 168 | 169 | params 170 | ``` 171 | { 172 | view: chapters, 173 | } 174 | ``` 175 | 176 | example 177 | 178 | ``` 179 | api.zhuishushenqi.com/toc/508662b8d7a545903b000027?view=chapters 180 | 181 | ``` 182 | result 183 | 184 | ``` 185 | { 186 | [{ 187 | "title": "第十一卷 真仙降临 第两千四百四十五章 飞升之劫", 188 | "link": "http://vip.zhuishushenqi.com/chapter/56f8df26176d03ac1984afc1?cv=1487224944100", 189 | "id": "56f8df26176d03ac1984afc1", 190 | "currency": 15, 191 | "unreadble": false, 192 | "isVip": true 193 | }, { 194 | "title": "第十一卷 真仙降临 第两千四百四十六章 飞升仙界(大结局)", 195 | "link": "http://vip.zhuishushenqi.com/chapter/56f8df27176d03ac1984afcf?cv=1487224944103", 196 | "id": "56f8df27176d03ac1984afcf", 197 | "currency": 15, 198 | "unreadble": false, 199 | "isVip": true 200 | }], 201 | "updated": "2017-02-16T06:02:34.819Z", 202 | "host": "vip.zhuishushenqi.com" 203 | } 204 | ``` 205 | 206 | ### 查询章节内容(先获取书源章节link) 207 | 208 | /chapter/章节link(从章节列表中获得)?k=2124b73d7e2e1945&t=1468223717 209 | 210 | params 211 | ``` 212 | { 213 | k: 2124b73d7e2e1945 214 | t: 1468223717 215 | } 216 | ``` 217 | 218 | example 219 | 220 | ``` 221 | chapter2.zhuishushenqi.com/chapter/http%3a%2f%2fbook.my716.com%2fgetBooks.aspx%3fmethod%3dcontent%26bookId%3d1127281%26chapterFile%3dU_1212539_201701211420571844_4093_2.txt?k=2124b73d7e2e1945&t=1468223717 222 | 223 | ``` 224 | result 225 | 226 | ``` 227 | { 228 | "ok": true, 229 | "chapter": { 230 | "title": ".", 231 | "body": "第二章\n灵溪宗,位于东林洲内,属于通天河的下游支脉所在,立足通天河南北两岸,至今已有万年历史,震慑四方。\n八座云雾缭绕的惊天山峰,横在通天河上,其中北岸有四座山峰,南岸三座,至于中间的通天河上,赫然有一座最为磅礴的山峰。\n此山从中段开始就白雪皑皑,竟看不清尽头,只能看到下半部的山体被掏空,使得金色的河水奔腾而过,如同一座山桥。\n此刻,灵溪宗南岸外,一道长虹疾驰而来,其内中年修士李青候带着白小纯,没入第三峰下的杂役区域,隐隐还可听到长虹内白小纯的惨叫传出。\n白小纯觉得自己要被吓死了,一路飞行,他看到了无数大山,好几次都觉得自己要抓不住对方的大腿。\n眼下面前一花,当清晰时,已到了一处阁楼外,落在了地上后,他双腿颤抖,看着四周与村子里完全不同的世界。\n前方的阁楼旁,竖着一块大石,上面写着龙飞凤舞的三个大字。\n杂役处。\n大石旁坐着一个麻脸女子,眼看李青候到来,立刻起身拜见。\n“将此子送火灶房去。”李青候留下一句话,没有理会白小纯,转身化作长虹远去。\n麻脸女子听到火灶房三字后一怔,目光扫了白小纯一眼,给了白小纯一个宗门杂役的布袋,面无表情的交代一番,便带着白小纯走出阁楼,一路庭院林立,阁楼无数,青石铺路,还有花草清香,如同仙境,看的白小纯心驰荡漾,心底的紧张与忐忑也少了几分。\n“好地方啊,这里可比村子里好多了啊。”白小纯目中露出期待,随着走去,越是向前,四周的美景就越发的美奂绝伦,甚至他还看到一些样子秀美的女子时而路过,让白小纯对于这里,一下子就喜欢的不得了。\n片刻后,白小纯更高兴了,尤其是前方尽头,他看到了一处七层的阁楼,通体晶莹剔透,甚至天空还有仙鹤飞过。\n“师姐,我们到了吧?”白小纯顿时激动的问道。\n“恩,就在那。”麻脸女子依旧面无表情,淡淡开口,一指旁侧的小路。\n白小纯顺着对方所指,满怀期待的看去时,整个人僵住,揉了揉眼睛仔细去看,只见那条小路上,地面多处碎裂,四周更是破破烂烂,几件草房似随时可以坍塌,甚至还有一些怪味从那里飘出……\n白小纯欲哭无泪,抱着最后一丝希望,问了麻脸女子一句。\n“师姐,你指错了吧……”\n“没有。”麻脸女子淡淡开口,当先走上这条小路,白小纯听后,觉得一切美好瞬间坍塌,苦着脸跟了过去。\n没走多远,他就看到这条破破烂烂的小路尽头,有几口大黑锅窜来窜去,仔细一看,那每一口大黑锅下面,都有一个大胖子,脑满肠肥,似乎一挤都可以流油,不是一般的胖,尤其是里面一个最胖的家伙,跟个肉山似的,白小纯都担心能不能爆了。\n那几个胖子的四周,有几百口大锅,这些胖子正在添水放米。\n察觉有人到来,尤其是看到了麻脸女子,那肉山立刻一脸惊喜,拎着大勺,横着就跑了过来,地面都颤了,一身肥膘抖动出无数波澜,白小纯目瞪口呆,下意识的要在身边找斧头。\n“今早小生听到喜鹊在叫,原来是姐姐你来了,莫非姐姐你已回心转意,觉得我有几分才气,趁着今天良辰,要与小生结成道侣。”肉山目中露出色眯眯的光芒,激动的边跑边喊。\n“我送此子加入你们火灶房,人已带到,告辞!”麻脸女子在看到肉山后,面色极为难看,还有几分恼怒,赶紧后退。\n白小纯倒吸口气,那麻脸女子一路上他就留意了,那相貌简直就是鬼斧神工,眼前这大胖子什么口味,居然这样也能一脸色相。\n还没等白小纯想完,那肉山就呼的一声,出现在了他的面前,直接就将阳光遮盖,把白小纯笼罩在了阴影下。\n白小纯抬头看着面前这庞大无比,身上的肉还在颤动的胖子,努力咽了口唾沫,这么胖的人,他还是头一次看到。\n肉山满脸幽怨的将目光从远处麻脸女子离去的方向收回,扫了眼白小纯。\n“嗬呦,居然来新人了,能把原本安排好的许宝财挤下去,不简单啊。”\n“师兄,在下……在下白小纯……”白小纯觉得对方魁梧的身体,让自己压力太大,下意识的退后几步。\n“白小纯?恩……皮肤白,小巧玲珑,模样还很清纯,不错不错,你的名字起的很符合我的口味嘛。”肉山眼睛一亮,拍下了白小纯的肩膀,一下子差点把白小纯直接拍倒。\n“不知师兄大名是?”白小纯倒吸口气,翻了个白眼,鄙夷的看了眼肉山,心底琢磨着也拿对方的名字玩一玩。\n“我叫张大胖,那个是黄二胖,还有黑三胖……”肉山嘿嘿一笑。\n白小纯听到这几个名字,大感人如其名,立刻没了玩一玩的想法。\n“至于你,以后就叫白九……小师弟,你太瘦了!这样出去会丢我们火灶坊的脸啊,不过也没关系,放心好了,最多一年,你也会胖的,以后你就叫白九胖。”张大胖一拍胸口,肥肉乱颤。\n听到白九胖这三个字,白小纯脸都挤出苦水了。\n“既然你已经是九师弟了,那就不是外人了,咱们火灶房向来有背锅的传统,看到我背后这这口锅了吧,它是锅中之王,铁精打造,刻着地火阵法,用这口锅煮出的灵米,味道超出寻常的锅太多太多。你也要去选一口,以后背在身上,那才威风。”张大胖拍了下背后的大黑锅,吹嘘的开口。\n“师兄,背锅的事,我能不能算了……”白小纯瞄了眼张大胖背后的锅,顿时有种火灶房的人,都是背锅的感觉,脑海里想了一下自己背一口大黑锅的样子,连忙说道。\n“那怎么行,背锅是我们火灶房的传统,你以后在宗门内,别人只要看到你背着锅,知道你是火灶房的人,就不敢欺负你,咱们火灶房可是很有来头的!”张大胖向白小纯眨了眨眼,不由分说,拎着白小纯就来到草屋后面,那里密密麻麻叠放着数千口大锅,其中绝大多数都落下厚厚一层灰,显然很久都没人过来。\n“九师弟,你选一口,我们去煮饭了,不然饭糊了,那些外门弟子又要嚷嚷了。”张大胖喊了一声,转身与其他几个胖子,又开始在那上百个锅旁窜来窜去。\n白小纯唉声叹气,看着那一口口锅,正琢磨选哪一个时,忽然看到了在角落里,放着一口被压在下面的锅。\n这口锅有些特别,不是圆的,而是椭圆形,看起来不像是锅,反倒像是一个龟壳,隐隐可见似乎还有一些黯淡的纹路。\n“咦?”白小纯眼睛一亮,快步走了过去,蹲下身子仔细看了看后,将其搬了出来,仔细看后,目中露出满意。\n他自幼就喜欢乌龟,因为乌龟代表长寿,而他之所以来修仙,就是为了长生,如今一看此锅像龟壳,在他认为,这是很吉利的,是好兆头。\n将这口锅搬出去后,张大胖远远的看到,拿着大勺就跑了过来。\n“九师弟你怎么选这口啊,这锅放在那里不知多少年了,没人用过,因为像龟壳,所以也从来没人选背着它在身上,这个……九师弟你确定?”张大胖拍了拍自己的肚子,好心的劝说。\n“确定,我就要这口锅了。”白小纯越看这口锅越喜欢,坚定道。\n张大胖又劝说一番,眼看白小纯执意如此,便古怪的看了看他,不再多说,为白小纯安排了在这火灶房居住的草屋后,就又忙碌去了。\n此刻天色已到黄昏,白小纯在草屋内,将那口龟形的锅仔细的看了看,发现这口锅的背面,有几十条纹路,只是黯淡,若不细看,很难发现。\n他顿时认为这口锅不凡,将其小心的放在了灶上,这才打量居住的屋舍,这房屋很简单,一张小床,一处桌椅,墙上挂着一面日常所需的铜镜,在他环顾房间时,身后那口平淡无奇的锅上,有一道紫光,一闪而逝!\n对于白小纯来说,这一天发生了很多事情,如今虽然来到了梦寐以求的仙人世界,可他心里终究是有些茫然。\n片刻后,他深吸口气,目中露出期望。\n“我要长生!”白小纯坐在一旁取出杂役处麻脸女子给予的口袋。\n里面有一枚丹药,一把木剑,一根燃香,再就是杂役的衣服与令牌,最后则是一本竹书,书上有几个小字。\n“紫气驭鼎功,凝气篇。”\n黄昏时分,火灶房内张大胖等人忙碌时,屋舍内的白小纯正看着竹书,眼中露出期待,他来到这里是为了长生,而长生的大门,此刻就在他的手中,深呼吸几次后,白小纯打开竹书看了起来。\n片刻后,白小纯眼中露出兴奋之芒,这竹书上有三幅图,按照上面的说法,修行分为凝气与筑基两个境界,而这紫气驭鼎功分为十层,分别对应凝气的十层。\n且每修到一层,就可以驭驾外物为己用,当到了第三层后,可以驾驭重量为小半个鼎的物体,到了第六层,则是大半个鼎,而到了第九层,则是一整尊鼎,至于最终的大圆满,则是可以驾驭重量为两尊鼎的物体。\n只不过这竹书上的功法,只有前三层,余下的没有记录,且若要修炼,还需按照特定的呼吸以及动作,才可以修行这紫气驭鼎功。\n白小纯打起精神,调整呼吸,闭目摆出竹书上第一幅图的动作,只坚持了三个呼吸,就全身酸痛的惨叫一声,无法坚持下去,且那种呼吸方式,也让他觉得气不够用。\n“太难了,上面说这修炼这第一幅图,可以感受到体内有一丝气在隐隐游走,可我这里除了难受,什么都没有感觉到。”白小纯有些苦恼,可为了长生,咬牙再次尝试,就这样磕磕绊绊,直至到了傍晚,他始终没有感受到体内的气。\n他不知道,即便是资质绝佳之人,若没有外力,单纯去修行这紫气驭鼎功的第一层,也需要至少一个月的时间,而他这里才几个时辰,根本就不可能有气感。\n此刻全身酸痛,白小纯伸了个懒腰,正要去洗把脸,突然的,从门外传来阵阵吵闹之声,白小纯把头伸出窗外,立刻看到一个面黄肌瘦的青年,一脸铁青的站在火灶房院子的大门外。\n“是谁顶替了我许宝财的名额,给我滚出来!”\n=========\n正式更新啦!新书如小树苗一样鲜嫩,急需呵护啊,求推荐票,求收藏!!!推荐,推荐,推荐,收藏,收藏,收藏,重要的事,三遍三遍!!!" 232 | } 233 | } 234 | ``` 235 | 236 | 237 | [api来源](https://github.com/qq573011406/KindleHelper/blob/master/libZhuishu/api/doc/apidoc.txt) 238 | 239 | 240 | 追书神器API: 241 | 242 | >1.搜索图书 243 | 244 | Host:api.zhuishushenqi.com 245 | Method:GET /book/fuzzy-search 246 | Params: 247 | query:关键词 248 | start:结果开始位置 249 | limit:结果最大数量 250 | response: 251 | { 252 | "books": [ 253 | { 254 | "_id": "508751bef98e8f7446000024", 255 | "hasCp": true, 256 | "title": "神墓", 257 | "cat": "玄幻", 258 | "author": "辰东", 259 | "site": "qidian", 260 | "cover": "/agent/http://image.cmfu.com/books/63856/63856.jpg", 261 | "shortIntro": "一个死去万载岁月的平凡青年从远古神墓中复活而出……", 262 | "lastChapter": "我的新书《完美世界》已上传,请兄弟姐妹来观看", 263 | "retentionRatio": 51.46, 264 | "latelyFollower": 601, 265 | "wordCount": 3124360 266 | }, 267 | { 268 | "_id": "561231284d4192d70a503e27", 269 | "hasCp": false, 270 | "title": "他从神墓来", 271 | "cat": "同人", 272 | "author": "邱则", 273 | "site": "qidian", 274 | "cover": "/agent/http://image.cmfu.com/books/2889151/2889151.jpg", 275 | "shortIntro": "先穿越到神墓历经十世轮回千万载岁月,后与天道一战打破多元宇宙壁垒。穿梭到其他世界······", 276 | "lastChapter": "第二十六章", 277 | "retentionRatio": null, 278 | "latelyFollower": 12, 279 | "wordCount": 99397 280 | } 281 | ], 282 | "ok": true 283 | } 284 | -------------------------------------------------- 285 | >2.书籍详情 286 | 287 | Host:api.zhuishushenqi.com 288 | Method:GET /book/书籍ID 289 | response: 290 | { 291 | "_id": "50bee5172033d09b2f00001b", 292 | "author": "莫默", 293 | "banned": 0, 294 | "cover": "/agent/http://image.cmfu.com/books/2494758/2494758.jpg", 295 | "creater": "iPhone 4S", 296 | "dramaPoint": null, 297 | "followerCount": 14385, 298 | "gradeCount": 0, 299 | "isSerial": true, 300 | "lastChapter": "请安装【追书神器】,本应用已停用", 301 | "latelyFollower": 165101, 302 | "longIntro": "您当前所使用的软件已改名为【追书神器】。\n请搜索“追书神器”下载安装最新版【追书神器】。\n无广告;不闪退;章节更新自动通知。", 303 | "postCount": 28547, 304 | "reviewCount": 618, 305 | "serializeWordCount": 5706, 306 | "tags": [ 307 | "玄幻", 308 | "热血", 309 | "架空", 310 | "巅峰", 311 | "奇遇", 312 | "升级练功", 313 | "东方玄幻" 314 | ], 315 | "title": "武炼巅峰", 316 | "tocs": [ 317 | "50bee5172033d09b2f00001c", 318 | "50c703274a0d32e637000064", 319 | "51776b30fb92a36054000146", 320 | "523070e69e75522764000129", 321 | "52bd2b9029eb81b82500008f", 322 | "52bd2b9029eb81b825000090", 323 | "52bd2b9029eb81b825000091", 324 | "532bf5f63949325379000021" 325 | ], 326 | "totalPoint": null, 327 | "type": "xhqh", 328 | "updated": "2016-07-11T00:49:34.749Z", 329 | "writingPoint": null, 330 | "site": "qidian", 331 | "hasNotice": false, 332 | "tagStuck": 0, 333 | "chaptersCount": 3079, 334 | "tocCount": 10, 335 | "tocUpdated": "2016-07-11T00:49:34.749Z", 336 | "retentionRatio": 73.69, 337 | "followerRank": 70, 338 | "retentionRatioRank": 82, 339 | "hasCmread": true, 340 | "thirdFlagsUpdated": "2014-09-01T05:56:51.009Z", 341 | "categories": [ 342 | "东方玄幻", 343 | "玄幻" 344 | ], 345 | "wordCount": 9294707, 346 | "aliases": [ 347 | "武练巅峰" 348 | ], 349 | "cat": "玄幻", 350 | "gender": [ 351 | "male" 352 | ], 353 | "majorCate": "玄幻", 354 | "minorCate": "东方玄幻", 355 | "monthFollower": { 356 | "11": 6342 357 | }, 358 | "totalFollower": 12734, 359 | "monthRetentionRatio": { 360 | "11": 66.67 361 | }, 362 | "cpOnly": false, 363 | "hasCp": true 364 | } 365 | ------------------------ 366 | >3.书源 367 | 368 | GET /toc?view=summary&book=573d65ab608bed412452ba69 HTTP/1.1 369 | 370 | reponse: 371 | [ 372 | { 373 | "_id": "5679b5debb597f3a47b208f5", 374 | "lastChapter": "请假,暂停一天", 375 | "link": "http://api.easou.com/api/bookapp/chapter_list.m?gid=10645516&nid=1010645516&size=10000&cid=eef_easou_book&version=002&os=android&appverion=1011", 376 | "source": "aeasou", 377 | "name": "宜搜小说", 378 | "isCharge": false, 379 | "chaptersCount": 2063, 380 | "updated": "2016-07-11T18:14:05.380Z", 381 | "starting": false, 382 | "host": "api.easou.com" 383 | }, 384 | { 385 | "_id": "532d0126394932537900222b", 386 | "lastChapter": "请假,暂停一天", 387 | "link": "http://read.shuhaha.com/Html/Book/34/34019/", 388 | "name": "书哈哈小说网", 389 | "source": "shuhaha", 390 | "isCharge": false, 391 | "chaptersCount": 2115, 392 | "updated": "2016-07-11T16:07:35.732Z", 393 | "starting": false, 394 | "host": "read.shuhaha.com" 395 | } 396 | ] 397 | ------------------------- 398 | >4.章节列表 399 | 400 | Host:api.zhuishushenqi.com 401 | Method:GET /toc/书源ID?view=chapters 402 | response: 403 | {"_id":"57398b120d9625ff2f6c2f34","name":"优质书源","link":"http://www.ybdu.com/xiaoshuo/402/402169/index.html","chapters":[{"title":"第1章 战神重生","link":"http://www.ybdu.com/xiaoshuo/402/402169/792.html","id":"57398b127fb8b8705ac36825","currency":10,"unreadble":false,"isVip":false},{"title":"第2章 滚下去","link":"http://www.ybdu.com/xiaoshuo/402/402169/545.html","id":"57398b127fb8b8705ac36826","currency":10,"unreadble":false,"isVip":false},{"title":"第3章 强硬逼婚","link":"http://www.ybdu.com/xiaoshuo/402/402169/376.html","id":"57398b127fb8b8705ac36827","currency":10,"unreadble":false,"isVip":false},{"title":"第4章 聂天的狂","link":"http://www.ybdu.com/xiaoshuo/402/402169/119.html","id":"57398b127fb8b8705ac36828","currency":10,"unreadble":false,"isVip":false},{"title":"第5章 凝聚人心","link":"http://www.ybdu.com/xiaoshuo/402/402169/394.html","id":"57398b127fb8b8705ac36829","currency":10,"unreadble":false,"isVip":false},{"title":"第6章 星辰原石","link":"http://www.ybdu.com/xiaoshuo/402/402169/95.html","id":"57398b127fb8b8705ac3682a","currency":10,"unreadble":false,"isVip":false},{"title":"第7章 星辰之力觉醒","link":"http://www.ybdu.com/xiaoshuo/402/402169/475.html","id":"57398b127fb8b8705ac3682b","currency":10,"unreadble":false,"isVip":false},{"title":"第8章 你叫我废物","link":"http://www.ybdu.com/xiaoshuo/402/402169/51.html","id":"57398b127fb8b8705ac3682c","currency":10,"unreadble":false,"isVip":false},{"title":"第9章 剑绝天斩","link":"http://www.ybdu.com/xiaoshuo/402/402169/680.html","id":"57398b127fb8b8705ac3682d","currency":10,"unreadble":false,"isVip":false},{"title":"第10章 墨如曦","link":"http://www.ybdu.com/xiaoshuo/402/402169/596.html","id":"57398b127fb8b8705ac3682e","currency":10,"unreadble":false,"isVip":false}],"updated":"2016-07-11T02:30:53.450Z"} 404 | ------------------------- 405 | 406 | >5.混合源章节列表 407 | 408 | GET http://api.zhuishushenqi.com/mix-toc/书籍ID 409 | { 410 | "ok": true, 411 | "mixToc": { 412 | "_id": "53a2c43ffda0a68d82ff3d19", 413 | "book": "50864deb9dacd30e3a00001d", 414 | "chaptersUpdated": "2016-07-05T12:09:09.720Z", 415 | "updated": "2016-07-08T12:51:21.385Z", 416 | "chapters": [ 417 | { 418 | "title": "第一章 林动【新书开张,郑重的求收藏!】", 419 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/129.html", 420 | "unreadble": false 421 | }, 422 | { 423 | "title": "第二章 通背拳", 424 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/140.html", 425 | "unreadble": false 426 | }, 427 | { 428 | "title": "第三章 古怪的石池", 429 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/146.html", 430 | "unreadble": false 431 | }, 432 | { 433 | "title": "第四章 石池之秘", 434 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/198.html", 435 | "unreadble": false 436 | }, 437 | { 438 | "title": "第五章 神秘石符", 439 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/517.html", 440 | "unreadble": false 441 | }, 442 | { 443 | "title": "第六章 七响", 444 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/633.html", 445 | "unreadble": false 446 | }, 447 | { 448 | "title": "第七章 淬体第四重", 449 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/139.html", 450 | "unreadble": false 451 | }, 452 | { 453 | "title": "第八章 冲突", 454 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/993.html", 455 | "unreadble": false 456 | }, 457 | { 458 | "title": "第九章 林宏", 459 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/410.html", 460 | "unreadble": false 461 | }, 462 | { 463 | "title": "第十章 金玉枝", 464 | "link": "http://www.ybdu.com/xiaoshuo/402/402169/126.html", 465 | "unreadble": false 466 | } 467 | ] 468 | } 469 | } 470 | 471 | 472 | >6.章节内容 473 | 474 | Host:chapter2.zhuishushenqi.com 475 | Method:GET /chapter/章节link(从章节列表中获得)?k=2124b73d7e2e1945&t=1468223717 476 | response: 477 | { 478 | "ok": true, 479 | "chapter": { 480 | "title": "第1章 他叫白小纯", 481 | "body": "\n\r\n\r\n\r请安装最新版追书 以便使用优质资源", 482 | "isVip": false, 483 | "cpContent": "  帽按时大大说", 484 | "currency": 15, 485 | "id": "5750118aa37701c41f60646f" 486 | } 487 | } 488 | 489 | 490 | >7.Autocomplate 491 | 492 | GET /book/auto-complete?query=%E6%AD%A6%E5%8A%A8 HTTP/1.1 493 | {"keywords":["武动乾坤","武动乾坤续集之大千世界","武动乾坤番外之冰灵族","武动乾坤续集","武动时空","武动韩娱","武动乾坤冰灵族","武动乾坤后续","武动龙珠","武动苍冥"],"ok":true} 494 | -------------------------------------------------------------------------------- /demo/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 20000, 3 | "data": [ 4 | { 5 | "title": "测试标题1", 6 | "price": 212.58, 7 | "sales": 1214, 8 | "desc": "xxxxxxxxxxxxxasdfasdf" 9 | }, 10 | { 11 | "title": "测试标题2", 12 | "price": 132.58, 13 | "sales": 1201, 14 | "desc": "xxxxxxxxxxxxxasdfasdf" 15 | }, 16 | { 17 | "title": "测试标题3", 18 | "price": 412.58, 19 | "sales": 2014, 20 | "desc": "xxxxxxxxxxxxxasdfasdf" 21 | }, 22 | { 23 | "title": "测试标题4", 24 | "price": 182.58, 25 | "sales": 14, 26 | "desc": "xxxxxxxxxxxxxasdfasdf" 27 | }, 28 | { 29 | "title": "测试标题5", 30 | "price": 432.58, 31 | "sales": 114, 32 | "desc": "xxxxxxxxxxxxxasdfasdf" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /demo/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 封装模板引擎 6 | 7 | 8 |
9 |
10 |
11 | 12 | 83 | 84 | 92 | 93 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /dist/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/dist/icon.png -------------------------------------------------------------------------------- /dist/index.css: -------------------------------------------------------------------------------- 1 | body.swal2-shown{overflow-y:hidden}.swal2-container,body.swal2-iosfix{position:fixed;left:0;right:0}.swal2-container{display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center;top:0;bottom:0;padding:10px;background-color:transparent;z-index:1060}.swal2-container.swal2-fade{-webkit-transition:background-color .1s;-o-transition:background-color .1s;transition:background-color .1s}.swal2-container.swal2-shown{background-color:rgba(0,0,0,.4)}.swal2-modal{background-color:#fff;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;border-radius:5px;-webkit-box-sizing:border-box;box-sizing:border-box;text-align:center;margin:auto;overflow-x:hidden;overflow-y:auto;display:none;position:relative;max-width:100%}.swal2-modal:focus{outline:0}.swal2-modal.swal2-loading{overflow-y:hidden}.swal2-modal .swal2-title{color:#595959;font-size:30px;text-align:center;font-weight:600;text-transform:none;position:relative;margin:0 0 .4em;padding:0;display:block;word-wrap:break-word}.swal2-modal .swal2-buttonswrapper{margin-top:15px}.swal2-modal .swal2-buttonswrapper:not(.swal2-loading) .swal2-styled[disabled]{opacity:.4;cursor:no-drop}.swal2-modal .swal2-buttonswrapper.swal2-loading .swal2-styled.swal2-confirm{-webkit-box-sizing:border-box;box-sizing:border-box;border:4px solid transparent;border-color:transparent;width:40px;height:40px;padding:0;margin:7.5px;vertical-align:top;background-color:transparent!important;color:transparent;cursor:default;border-radius:100%;-webkit-animation:rotate-loading 1.5s linear 0s infinite normal;animation:rotate-loading 1.5s linear 0s infinite normal;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.swal2-modal .swal2-buttonswrapper.swal2-loading .swal2-styled.swal2-cancel{margin-left:30px;margin-right:30px}.swal2-modal .swal2-buttonswrapper.swal2-loading :not(.swal2-styled).swal2-confirm:after{display:inline-block;content:"";margin-left:5px;vertical-align:-1px;height:15px;width:15px;border:3px solid #999;-webkit-box-shadow:1px 1px 1px #fff;box-shadow:1px 1px 1px #fff;border-right-color:transparent;border-radius:50%;-webkit-animation:rotate-loading 1.5s linear 0s infinite normal;animation:rotate-loading 1.5s linear 0s infinite normal}.swal2-modal .swal2-styled{border:0;border-radius:3px;-webkit-box-shadow:none;box-shadow:none;color:#fff;cursor:pointer;font-size:17px;font-weight:500;margin:15px 5px 0;padding:10px 32px}.swal2-modal .swal2-image{margin:20px auto;max-width:100%}.swal2-modal .swal2-close{background:0 0;border:0;margin:0;padding:0;width:38px;height:40px;font-size:36px;line-height:40px;font-family:serif;position:absolute;top:5px;right:8px;cursor:pointer;color:#ccc;-webkit-transition:color .1s ease;-o-transition:color .1s ease;transition:color .1s ease}.swal2-modal .swal2-close:hover{color:#d55}.swal2-modal>.swal2-checkbox,.swal2-modal>.swal2-file,.swal2-modal>.swal2-input,.swal2-modal>.swal2-radio,.swal2-modal>.swal2-select,.swal2-modal>.swal2-textarea{display:none}.swal2-modal .swal2-content{font-size:18px;text-align:center;font-weight:300;position:relative;float:none;margin:0;padding:0;line-height:normal;color:#545454;word-wrap:break-word}.swal2-modal .swal2-checkbox,.swal2-modal .swal2-file,.swal2-modal .swal2-input,.swal2-modal .swal2-radio,.swal2-modal .swal2-select,.swal2-modal .swal2-textarea{margin:20px auto}.swal2-modal .swal2-file,.swal2-modal .swal2-input,.swal2-modal .swal2-textarea{width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:18px;border-radius:3px;border:1px solid #d9d9d9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.06);box-shadow:inset 0 1px 1px rgba(0,0,0,.06);-webkit-transition:border-color .3s,-webkit-box-shadow .3s;transition:border-color .3s,-webkit-box-shadow .3s;-o-transition:border-color .3s,box-shadow .3s;transition:border-color .3s,box-shadow .3s;transition:border-color .3s,box-shadow .3s,-webkit-box-shadow .3s}.swal2-modal .swal2-file.swal2-inputerror,.swal2-modal .swal2-input.swal2-inputerror,.swal2-modal .swal2-textarea.swal2-inputerror{border-color:#f27474!important;-webkit-box-shadow:0 0 2px #f27474!important;box-shadow:0 0 2px #f27474!important}.swal2-modal .swal2-file:focus,.swal2-modal .swal2-input:focus,.swal2-modal .swal2-textarea:focus{outline:0;border:1px solid #b4dbed;-webkit-box-shadow:0 0 3px #c4e6f5;box-shadow:0 0 3px #c4e6f5}.swal2-modal .swal2-file::-webkit-input-placeholder,.swal2-modal .swal2-input::-webkit-input-placeholder,.swal2-modal .swal2-textarea::-webkit-input-placeholder{color:#ccc}.swal2-modal .swal2-file:-ms-input-placeholder,.swal2-modal .swal2-input:-ms-input-placeholder,.swal2-modal .swal2-textarea:-ms-input-placeholder{color:#ccc}.swal2-modal .swal2-file::placeholder,.swal2-modal .swal2-input::placeholder,.swal2-modal .swal2-textarea::placeholder{color:#ccc}.swal2-modal .swal2-range input{float:left;width:80%}.swal2-modal .swal2-range output{float:right;width:20%;font-size:20px;font-weight:600;text-align:center}.swal2-modal .swal2-range input,.swal2-modal .swal2-range output{height:43px;line-height:43px;vertical-align:middle;margin:20px auto;padding:0}.swal2-modal .swal2-input{height:43px;padding:0 12px}.swal2-modal .swal2-input[type=number]{max-width:150px}.swal2-modal .swal2-file{font-size:20px}.swal2-modal .swal2-textarea{height:108px;padding:12px}.swal2-modal .swal2-select{color:#545454;font-size:inherit;padding:5px 10px;min-width:40%;max-width:100%}.swal2-modal .swal2-radio{border:0}.swal2-modal .swal2-radio label:not(:first-child){margin-left:20px}.swal2-modal .swal2-radio input,.swal2-modal .swal2-radio span{vertical-align:middle}.swal2-modal .swal2-radio input{margin:0 3px 0 0}.swal2-modal .swal2-checkbox{color:#545454}.swal2-modal .swal2-checkbox input,.swal2-modal .swal2-checkbox span{vertical-align:middle}.swal2-modal .swal2-validationerror{background-color:#f0f0f0;margin:0 -20px;overflow:hidden;padding:10px;color:gray;font-size:16px;font-weight:300;display:none}.swal2-modal .swal2-validationerror:before{content:"!";display:inline-block;width:24px;height:24px;border-radius:50%;background-color:#ea7d7d;color:#fff;line-height:24px;text-align:center;margin-right:10px}@supports (-ms-accelerator:true){.swal2-range input{width:100%!important}.swal2-range output{display:none}}@media (-ms-high-contrast:active),(-ms-high-contrast:none){.swal2-range input{width:100%!important}.swal2-range output{display:none}}.swal2-icon{width:80px;height:80px;border:4px solid transparent;border-radius:50%;margin:20px auto 30px;padding:0;position:relative;-webkit-box-sizing:content-box;box-sizing:content-box;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.swal2-icon.swal2-error{border-color:#f27474}.swal2-icon.swal2-error .swal2-x-mark{position:relative;display:block}.swal2-icon.swal2-error [class^=swal2-x-mark-line]{position:absolute;height:5px;width:47px;background-color:#f27474;display:block;top:37px;border-radius:2px}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=left]{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);left:17px}.swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=right]{-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg);right:16px}.swal2-icon.swal2-warning{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;color:#f8bb86;border-color:#facea8}.swal2-icon.swal2-info,.swal2-icon.swal2-warning{font-size:60px;line-height:80px;text-align:center}.swal2-icon.swal2-info{font-family:Open Sans,sans-serif;color:#3fc3ee;border-color:#9de0f6}.swal2-icon.swal2-question{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;color:#87adbd;border-color:#c9dae1;font-size:60px;line-height:80px;text-align:center}.swal2-icon.swal2-success{border-color:#a5dc86}.swal2-icon.swal2-success [class^=swal2-success-circular-line]{border-radius:50%;position:absolute;width:60px;height:120px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=left]{border-radius:120px 0 0 120px;top:-7px;left:-33px;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:60px 60px;-ms-transform-origin:60px 60px;transform-origin:60px 60px}.swal2-icon.swal2-success [class^=swal2-success-circular-line][class$=right]{border-radius:0 120px 120px 0;top:-11px;left:30px;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:0 60px;-ms-transform-origin:0 60px;transform-origin:0 60px}.swal2-icon.swal2-success .swal2-success-ring{width:80px;height:80px;border:4px solid hsla(98,55%,69%,.2);border-radius:50%;-webkit-box-sizing:content-box;box-sizing:content-box;position:absolute;left:-4px;top:-4px;z-index:2}.swal2-icon.swal2-success .swal2-success-fix{width:7px;height:90px;position:absolute;left:28px;top:8px;z-index:1;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg)}.swal2-icon.swal2-success [class^=swal2-success-line]{height:5px;background-color:#a5dc86;display:block;border-radius:2px;position:absolute;z-index:2}.swal2-icon.swal2-success [class^=swal2-success-line][class$=tip]{width:25px;left:14px;top:46px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.swal2-icon.swal2-success [class^=swal2-success-line][class$=long]{width:47px;right:8px;top:38px;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg)}.swal2-progresssteps{font-weight:600;margin:0 0 20px;padding:0}.swal2-progresssteps li{display:inline-block;position:relative}.swal2-progresssteps .swal2-progresscircle{background:#3085d6;border-radius:2em;color:#fff;height:2em;line-height:2em;text-align:center;width:2em;z-index:20}.swal2-progresssteps .swal2-progresscircle:first-child{margin-left:0}.swal2-progresssteps .swal2-progresscircle:last-child{margin-right:0}.swal2-progresssteps .swal2-progresscircle.swal2-activeprogressstep{background:#3085d6}.swal2-progresssteps .swal2-progresscircle.swal2-activeprogressstep~.swal2-progresscircle,.swal2-progresssteps .swal2-progresscircle.swal2-activeprogressstep~.swal2-progressline{background:#add8e6}.swal2-progresssteps .swal2-progressline{background:#3085d6;height:.4em;margin:0 -1px;z-index:10}[class^=swal2]{-webkit-tap-highlight-color:transparent}@-webkit-keyframes showSweetAlert{0%{-webkit-transform:scale(.7);transform:scale(.7)}45%{-webkit-transform:scale(1.05);transform:scale(1.05)}80%{-webkit-transform:scale(.95);transform:scale(.95)}to{-webkit-transform:scale(1);transform:scale(1)}}@keyframes showSweetAlert{0%{-webkit-transform:scale(.7);transform:scale(.7)}45%{-webkit-transform:scale(1.05);transform:scale(1.05)}80%{-webkit-transform:scale(.95);transform:scale(.95)}to{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes hideSweetAlert{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}to{-webkit-transform:scale(.5);transform:scale(.5);opacity:0}}@keyframes hideSweetAlert{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}to{-webkit-transform:scale(.5);transform:scale(.5);opacity:0}}.swal2-show{-webkit-animation:showSweetAlert .3s;animation:showSweetAlert .3s}.swal2-show.swal2-noanimation{-webkit-animation:none;animation:none}.swal2-hide{-webkit-animation:hideSweetAlert .15s forwards;animation:hideSweetAlert .15s forwards}.swal2-hide.swal2-noanimation{-webkit-animation:none;animation:none}@-webkit-keyframes animate-success-tip{0%{width:0;left:1px;top:19px}54%{width:0;left:1px;top:19px}70%{width:50px;left:-8px;top:37px}84%{width:17px;left:21px;top:48px}to{width:25px;left:14px;top:45px}}@keyframes animate-success-tip{0%{width:0;left:1px;top:19px}54%{width:0;left:1px;top:19px}70%{width:50px;left:-8px;top:37px}84%{width:17px;left:21px;top:48px}to{width:25px;left:14px;top:45px}}@-webkit-keyframes animate-success-long{0%{width:0;right:46px;top:54px}65%{width:0;right:46px;top:54px}84%{width:55px;right:0;top:35px}to{width:47px;right:8px;top:38px}}@keyframes animate-success-long{0%{width:0;right:46px;top:54px}65%{width:0;right:46px;top:54px}84%{width:55px;right:0;top:35px}to{width:47px;right:8px;top:38px}}@-webkit-keyframes rotatePlaceholder{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}5%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}12%{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}to{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}}@keyframes rotatePlaceholder{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}5%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}12%{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}to{-webkit-transform:rotate(-405deg);transform:rotate(-405deg)}}.swal2-animate-success-line-tip{-webkit-animation:animate-success-tip .75s;animation:animate-success-tip .75s}.swal2-animate-success-line-long{-webkit-animation:animate-success-long .75s;animation:animate-success-long .75s}.swal2-success.swal2-animate-success-icon .swal2-success-circular-line-right{-webkit-animation:rotatePlaceholder 4.25s ease-in;animation:rotatePlaceholder 4.25s ease-in}@-webkit-keyframes animate-error-icon{0%{-webkit-transform:rotateX(100deg);transform:rotateX(100deg);opacity:0}to{-webkit-transform:rotateX(0);transform:rotateX(0);opacity:1}}@keyframes animate-error-icon{0%{-webkit-transform:rotateX(100deg);transform:rotateX(100deg);opacity:0}to{-webkit-transform:rotateX(0);transform:rotateX(0);opacity:1}}.swal2-animate-error-icon{-webkit-animation:animate-error-icon .5s;animation:animate-error-icon .5s}@-webkit-keyframes animate-x-mark{0%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}50%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}80%{-webkit-transform:scale(1.15);transform:scale(1.15);margin-top:-6px}to{-webkit-transform:scale(1);transform:scale(1);margin-top:0;opacity:1}}@keyframes animate-x-mark{0%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}50%{-webkit-transform:scale(.4);transform:scale(.4);margin-top:26px;opacity:0}80%{-webkit-transform:scale(1.15);transform:scale(1.15);margin-top:-6px}to{-webkit-transform:scale(1);transform:scale(1);margin-top:0;opacity:1}}.swal2-animate-x-mark{-webkit-animation:animate-x-mark .5s;animation:animate-x-mark .5s}@-webkit-keyframes rotate-loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotate-loading{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}*{-webkit-tap-highlight-color:rgba(255,255,255,0);border-width:thin;-webkit-appearance:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}input{-webkit-user-select:auto;-webkit-touch-callout:auto}body,html{margin:0;padding:0}body{font-weight:400;-webkit-font-smoothing:antialiased;font-family:-apple-system,BlinkMacSystemFont,PingFang-SC-Regular,Hiragino Sans GB,Microsoft Yahei,Arial,sans-serif;max-width:450px;margin:0 auto;background:#fafbfc;position:relative}a{color:#292525;text-decoration:none}h1,h2,h3,h4,h5,h6,p{margin:0;padding:0}._3qBcmkiw6PQHK0_GV9hyKA{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding:20px;padding-bottom:0;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}._1qeiMhwrVhijAzmKDiU46N{height:160px;width:90px;display:block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow:hidden}._1qeiMhwrVhijAzmKDiU46N ._3FwVyuLZFaD-AYDfAfR_h_{height:120px;width:90px;background-size:100% 100%;-webkit-box-shadow:0 2px 5px rgba(0,0,0,.3);box-shadow:0 2px 5px rgba(0,0,0,.3)}._1qeiMhwrVhijAzmKDiU46N p{text-align:center;color:rgba(0,0,0,.5);margin-top:6px;font-weight:100px;font-size:12px;max-width:90px;-o-text-overflow:ellipsis;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}._17kBupHSVWBORDZaea4GIQ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding:20px;padding-bottom:0;height:170px;overflow:hidden;position:relative;background:rgba(0,0,0,.3);-webkit-box-shadow:0 -2px 3px rgba(0,0,0,.05) inset;box-shadow:inset 0 -2px 3px rgba(0,0,0,.05);color:#fff}._17kBupHSVWBORDZaea4GIQ ._2x2NPfr1fQ5Z1sIVaz6aks{width:100%}._17kBupHSVWBORDZaea4GIQ ._3uQMV_x7zZzugGTNcUWg-5{margin-bottom:10px}._17kBupHSVWBORDZaea4GIQ ._2zfhQCbuDgvQGClx7CLy2e,._17kBupHSVWBORDZaea4GIQ ._3uQMV_x7zZzugGTNcUWg-5{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-weight:100}._17kBupHSVWBORDZaea4GIQ ._2zfhQCbuDgvQGClx7CLy2e{font-size:14px;padding-left:30px}._17kBupHSVWBORDZaea4GIQ ._2zfhQCbuDgvQGClx7CLy2e .-GhIJN94VEBGpQ5swhIrO{width:12px;height:12px;margin-right:4px;margin-top:1px}._17kBupHSVWBORDZaea4GIQ ._1jA1RvUb2mInwLfObEy2i1{padding-left:110px}._17kBupHSVWBORDZaea4GIQ ._1jA1RvUb2mInwLfObEy2i1 p{margin:5px 0;font-size:12px}._17kBupHSVWBORDZaea4GIQ img.aDHHsRGrwWks1TNwDxcfI{position:absolute;z-index:0;height:120px;-webkit-box-shadow:0 0 3px rgba(0,0,0,.3);box-shadow:0 0 3px rgba(0,0,0,.3)}._17kBupHSVWBORDZaea4GIQ ._14QbL10VMzFnYizYGc5uqi{position:absolute;left:0;top:0;width:100%;height:200px;background-repeat:no-repeat;background-size:100%;-webkit-filter:blur(30px);filter:blur(30px);z-index:-1;-webkit-animation:eiTO6Ur0O0lXekLM2GuI7 20s;animation:eiTO6Ur0O0lXekLM2GuI7 20s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}@-webkit-keyframes eiTO6Ur0O0lXekLM2GuI7{0%{-webkit-transform:scale(2) translateY(20%);transform:scale(2) translateY(20%)}70%{-webkit-transform:scale(1.5) translateY(-30%);transform:scale(1.5) translateY(-30%);-webkit-filter:blur(20px);-moz-filter:blur(20px);-ms-filter:blur(20px);filter:blur(20px)}to{-webkit-transform:scale(2) translateY(20%);transform:scale(2) translateY(20%)}}@keyframes eiTO6Ur0O0lXekLM2GuI7{0%{-webkit-transform:scale(2) translateY(20%);transform:scale(2) translateY(20%)}70%{-webkit-transform:scale(1.5) translateY(-30%);transform:scale(1.5) translateY(-30%);-webkit-filter:blur(20px);-moz-filter:blur(20px);-ms-filter:blur(20px);filter:blur(20px)}to{-webkit-transform:scale(2) translateY(20%);transform:scale(2) translateY(20%)}}.BHoOxXmFAYEXW_uPsawtf{font-size:12px;text-align:center;color:rgba(0,0,0,.5);margin-bottom:20px}._2pp5m1vQaAYSROkGf7tfRv{height:40px;margin-bottom:20px}._2E5GzdAP_h9D-iCRzxpH4U{padding:5px 8px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;background:#fff;position:fixed;left:0;right:0;width:100%;height:40px;z-index:99}._2E5GzdAP_h9D-iCRzxpH4U ._35mQ3VLthC5zKD2X5Cx_l9{opacity:.5;width:16px;height:16px;margin:0 5px;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNTAxNzM2Nzg1NzUxIiBjbGFzcz0iaWNvbiIgc3R5bGU9IiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjIzNDMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiPjxkZWZzPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+PC9zdHlsZT48L2RlZnM+PHBhdGggZD0iTTk4NS4wMjExNTIgOTUxLjgwNjU3OCA3MTEuMDIxNTA4IDY3Ny43OTU1NTZDNzc4LjM0MzgxOSA2MDYuOTEyIDgxOS44NzI3MDggNTExLjM4NDE3OCA4MTkuODcyNzA4IDQwNS45ODA0NDQgODE5Ljg3MjcwOCAxODcuNzIwNTMzIDY0Mi45MzY4ODUgMTAuNzg0NzExIDQyNC42ODgzNTIgMTAuNzg0NzExIDIwNi40Mjg0NDEgMTAuNzg0NzExIDI5LjQ5MjYxOCAxODcuNzIwNTMzIDI5LjQ5MjYxOCA0MDUuOTgwNDQ0IDI5LjQ5MjYxOCA2MjQuMTk0ODQ0IDIwNi40Mjg0NDEgODAxLjE2NDggNDI0LjY4ODM1MiA4MDEuMTY0OCA1MTEuNTEyMTc0IDgwMS4xNjQ4IDU5MS42MDAzNTIgNzcyLjgyMjc1NiA2NTYuODA2Mzk3IDcyNS4zMjA1MzNMOTM0LjE1MTEwOCAxMDAyLjY3NjYyMkM5NDguMjM2Nzk3IDEwMTYuNzI4MTc4IDk3MC45MTI3MDggMTAxNi43MjgxNzggOTg0Ljk4NzAxOSAxMDAyLjY3NjYyMiA5OTkuMDcyNzA4IDk4OC41NTY4IDk5OS4wNzI3MDggOTY1Ljg5MjI2NyA5ODUuMDIxMTUyIDk1MS44MDY1NzhMOTg1LjAyMTE1MiA5NTEuODA2NTc4Wk00MjQuNjg4MzUyIDczNi44MjM0NjdDMjQ2LjEyNTUwOCA3MzYuODIzNDY3IDEwMS4zNDMyODUgNTkyLjA0MTI0NCAxMDEuMzQzMjg1IDQxMy40Nzg0IDEwMS4zNDMyODUgMjM0Ljg5MjggMjQ2LjEyNTUwOCA5MC4xNDQ3MTEgNDI0LjY4ODM1MiA5MC4xNDQ3MTEgNjAzLjI3Mzk1MiA5MC4xNDQ3MTEgNzQ4LjAyMjA0MSAyMzQuOTI2OTMzIDc0OC4wMjIwNDEgNDEzLjQ3ODQgNzQ4LjAyMjA0MSA1OTIuMDA3MTExIDYwMy4yNzM5NTIgNzM2LjgyMzQ2NyA0MjQuNjg4MzUyIDczNi44MjM0NjdMNDI0LjY4ODM1MiA3MzYuODIzNDY3WiIgcC1pZD0iMjM0NCI+PC9wYXRoPjwvc3ZnPg==);background-size:16px 16px}._2E5GzdAP_h9D-iCRzxpH4U ._1Z6XMLlPw4nm1g8UpA1zkO{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;width:100%;background:rgba(0,0,0,.05);border:none;padding:4px;border-radius:5px}._2E5GzdAP_h9D-iCRzxpH4U ._1Z6XMLlPw4nm1g8UpA1zkO input{font-size:14px;height:100%;background:none;border-radius:0;border:none;width:100%}._2E5GzdAP_h9D-iCRzxpH4U ._24I9zSMk5Wglk0zg3qxYMP{width:70px;height:100%;line-height:40px;padding-left:20px;font-size:14px;color:#999}._3T1adfANu8SSd-zm1b8kna{margin:12px;height:104px;position:relative;padding-left:90px;border-bottom:1px dashed rgba(0,0,0,.1);padding-bottom:12px;color:#333}._3T1adfANu8SSd-zm1b8kna p{font-size:12px;margin:2px 0}._3T1adfANu8SSd-zm1b8kna ._2fNkC2ViSo3OU0OMRa8MUk{font-weight:500}._2SWAdFdRjFBOIZhvPQiK0G{height:100px;position:absolute;top:0;left:0;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.1)}._1kFV2o3vUWqwgZlXtdYyVO{position:relative;overflow:hidden}._3QdV25YnimlEHqkYKsRJTj{display:block;position:absolute;top:-25%;left:-25%;width:150%;height:150%;-webkit-filter:blur(25px);filter:blur(25px);-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;background-repeat:repeat;background-size:100% 100%;z-index:-1;background-position:50%}@-webkit-keyframes _2ESjtj_y-sY-vQYjk4bs65{0%{background-position:50% 10%}70%{-webkit-filter:blur(50px);filter:blur(50px);background-position:50% 90%}to{background-position:50% 10%}}@keyframes _2ESjtj_y-sY-vQYjk4bs65{0%{background-position:50% 10%}70%{-webkit-filter:blur(50px);filter:blur(50px);background-position:50% 90%}to{background-position:50% 10%}}._3jKcSyf244LYzp8icSdLAM{color:#fff;padding:20px;padding-bottom:0;display:block;font-size:14px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}._3jKcSyf244LYzp8icSdLAM img{height:15px}._2IOZTLzdhhbHn60Wy9a0EI{padding:20px}._2IOZTLzdhhbHn60Wy9a0EI ._29wWUa3A_R5M1FyNBPkGt8{margin:0;padding:10px;background:rgba(0,0,0,.03);border-radius:3px;font-size:12px;color:#666;position:relative;margin-bottom:20px}._2IOZTLzdhhbHn60Wy9a0EI ._29wWUa3A_R5M1FyNBPkGt8:after{bottom:0;right:0;position:absolute;content:"";width:0;height:0;border:10px solid red;border-color:rgba(0,0,0,.05) #fff #fff rgba(0,0,0,.05)}._2IOZTLzdhhbHn60Wy9a0EI ._2OGE4rYlRR3vhATktEv1NG{font-size:12px;color:#666;text-align:center}._2IOZTLzdhhbHn60Wy9a0EI ._4I7TMXj3MPD-0fWNvksMP{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}._2IOZTLzdhhbHn60Wy9a0EI ._4I7TMXj3MPD-0fWNvksMP span{background:rgba(0,0,0,.03);margin-right:8px;margin-bottom:8px;padding:1px 8px;font-size:10px;color:#666;border-radius:3px;display:inline-block}._3ZpXx8GimtQCOXTXwjr8Z2{padding:20px;padding-bottom:0;overflow:hidden}._3ZpXx8GimtQCOXTXwjr8Z2 ._1tMAkkU8lyxShA8QjLn3rr{float:left;height:150px;display:block}._3ZpXx8GimtQCOXTXwjr8Z2 ._2fD-6s5B59ATBgnPsEw2M-{float:left;padding:0 20px;padding-bottom:0;opacity:hidden;display:block;color:#fff}._3ZpXx8GimtQCOXTXwjr8Z2 ._2fD-6s5B59ATBgnPsEw2M- h1{margin:0;font-size:16px}._3ZpXx8GimtQCOXTXwjr8Z2 ._2fD-6s5B59ATBgnPsEw2M- p{font-size:14px;margin:5px 0}._2caybAlYjh0tLK8gXdd-w5,.hZkTK7TtefUKEz-noI2Vs{width:100%;display:block;background:#987;padding:10px 0;text-align:center;color:#fff;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.5);box-shadow:0 1px 2px rgba(0,0,0,.5);margin-top:40px}._2caybAlYjh0tLK8gXdd-w5{margin-top:16px;background:#a38877}._1gtYYrPGvs7nmYia7sjdzc{position:fixed;left:0;top:50%;background:hsla(0,0%,100%,.4);height:150px;width:100%;border-radius:20%}._1itqhCEeO6LTSrFaVCR-Z5{position:absolute;z-index:1000;left:5%;top:50%;width:90%;height:14px;background-color:#f4ffe8;-webkit-box-shadow:1px 2px 7px rgba(0,0,0,.4);box-shadow:1px 2px 7px rgba(0,0,0,.4);border-radius:10px}._1itqhCEeO6LTSrFaVCR-Z5 ._2X9s7wuoWx2EfVdL9TYXOO{height:100%;border-radius:10px;background-color:#ff5722}._2RRTLyDLTT4WXVog8OS1VZ{height:60px;position:relative;overflow:hidden;border-bottom:1px dashed rgba(0,0,0,.1);-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}._2RRTLyDLTT4WXVog8OS1VZ,._3GJzv8O18999Xmp8rasUOT,.lgfZuKHFody2sYQ4aOeZn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.lgfZuKHFody2sYQ4aOeZn{height:100%;padding:10px;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.lgfZuKHFody2sYQ4aOeZn h3{font-weight:100;font-size:16px}.lgfZuKHFody2sYQ4aOeZn p{font-size:12px}._1nIswwdhezoo7B679bJChr{height:60px;width:60px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}._1nIswwdhezoo7B679bJChr img{width:20px}._2qbbB_QVQhvZkvt03LeRsM{padding:5px 1px}._2qbbB_QVQhvZkvt03LeRsM p{padding:0 .5em;text-indent:2em;word-wrap:break-word;line-height:1.75}._1qXSomjQ_zW_BMACC37XK9{position:fixed;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:left;-webkit-align-items:left;-ms-flex-align:left;align-items:left;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;top:0;left:0;width:100%;height:100%;background:none;opacity:.5}._1qXSomjQ_zW_BMACC37XK9 p{font-size:10px;padding:0;margin:0}._1qXSomjQ_zW_BMACC37XK9 img._1qXSomjQ_zW_BMACC37XK9{max-width:24px;opacity:.3;-webkit-animation:_3W95wg21DIEa6jzSDX4YzP 2s infinite ease-in-out;animation:_3W95wg21DIEa6jzSDX4YzP 2s infinite ease-in-out;margin-right:20px}@-webkit-keyframes _3W95wg21DIEa6jzSDX4YzP{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(3turn);transform:rotate(3turn)}}@keyframes _3W95wg21DIEa6jzSDX4YzP{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(3turn);transform:rotate(3turn)}}.y6fiUcLKKNbm3gNWsR0Ff{background:rgba(0,0,0,.8);position:fixed;width:100%;height:120px;left:0;bottom:0;-webkit-transition:.3s;-o-transition:.3s;transition:.3s;color:#fff;font-size:14px;padding:10px;-webkit-box-sizing:border-box;box-sizing:border-box}._2IQR0wo2sDl6FhuTtX3b0_{-webkit-transform:translateY(0);-ms-transform:translateY(0);transform:translateY(0)}._3_lT5AfxRni5jb9F4JwI7h{-webkit-transform:translateY(300%);-ms-transform:translateY(300%);transform:translateY(300%)}.C0ASJJGoJEawJiEF2GaGJ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;height:40px;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:10px}.C0ASJJGoJEawJiEF2GaGJ span{width:75px;height:30px;text-align:center;line-height:30px;display:block;border:1px solid #fff;border-radius:5px;color:#fff}.C0ASJJGoJEawJiEF2GaGJ span:active,.C0ASJJGoJEawJiEF2GaGJ span:focus,.C0ASJJGoJEawJiEF2GaGJ span:hover{background:#fff;color:#000}._3QzWn56pZGmRk5v89fbxN3{overflow:scroll}._3QzWn56pZGmRk5v89fbxN3 .RUWbvqPHqeR2ADnLgxI9m{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap}._3QzWn56pZGmRk5v89fbxN3 span{display:block;width:30px;height:30px;margin-bottom:10px;margin-right:10px;border-radius:100px;border:2px solid hsla(0,0%,100%,.5);content:""}._1QDmIbofl7phaDW916Ldl5,._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._2VENiqyy_Rl6so5AUHbo81,._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._3ntdP38CjfXKdyHIUpVCse{position:absolute;bottom:55px;right:10px;width:45px;height:45px;background-color:#ff5722;border-radius:50%;line-height:40px;font-size:40px;text-align:center;color:#fff;overflow:hidden;padding:0}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._1NpvUKKOkEmueYu0r0QnRX,._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._2d1xotmJ5DgvIa0yP5kaL-,._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._3SuIthnyE4AKwNMC29S9tn,._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._46r5tf49BDGcbdF7jwdse,._3TgHeqcuNIWeBrbrcLkGv6{color:#fff;font-size:12px;width:54px;height:54px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;text-align:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;justify-items:center;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;border-radius:50%;background-color:#ff5722;opacity:.9}._3b4xH-KTo8pF4MPElOVGgO{line-height:16px}._3MB623CHqj_g5XPcYmZMDa{background:#fff;color:rgba(0,0,0,.5);min-height:100vh}._3MB623CHqj_g5XPcYmZMDa a{color:rgba(0,0,0,.5)}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND{padding:20px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around;bottom:0;position:fixed;height:40px;width:410px}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._1r_LXnhV1Yz4c1zamB1VyF{height:64px;-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1);-webkit-animation:_1jCst89Z94cNR_HB-0l6_D .5s;animation:_1jCst89Z94cNR_HB-0l6_D .5s}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._1r_LXnhV1Yz4c1zamB1VyF,._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._19-ha--iEXA6Kud40aK_uX{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around;width:100%}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._19-ha--iEXA6Kud40aK_uX{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);-webkit-animation:MDx2SN2cBwZsqNM1x9hRL .3s;animation:MDx2SN2cBwZsqNM1x9hRL .3s}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._2d1xotmJ5DgvIa0yP5kaL-,._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._3SuIthnyE4AKwNMC29S9tn{background-color:#a3b5be}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._3ntdP38CjfXKdyHIUpVCse{-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg);-webkit-animation:AVsR9EOwVihPaYAvv8Cm1 .7s;animation:AVsR9EOwVihPaYAvv8Cm1 .7s}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._2VENiqyy_Rl6so5AUHbo81{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);-webkit-animation:_1TNtuXf2jOqPPxDYEXhrw2 .5s;animation:_1TNtuXf2jOqPPxDYEXhrw2 .5s}@keyframes _1jCst89Z94cNR_HB-0l6_D{0%{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}}@-webkit-keyframes MDx2SN2cBwZsqNM1x9hRL{0%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(0);transform:scale(0)}}@keyframes MDx2SN2cBwZsqNM1x9hRL{0%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(0);transform:scale(0)}}@keyframes AVsR9EOwVihPaYAvv8Cm1{0%{-webkit-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg)}}@-webkit-keyframes _1TNtuXf2jOqPPxDYEXhrw2{0%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes _1TNtuXf2jOqPPxDYEXhrw2{0%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@media screen and (max-width:450px){._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND{width:100%;padding:0;height:60px}._3MB623CHqj_g5XPcYmZMDa ._2C_wYfukcB0AgC3mnm4nND ._3ntdP38CjfXKdyHIUpVCse{bottom:65px;right:10px}}.R2xavzPHj8ETajWpjVin_{margin:0;padding:0;margin-top:60px;margin-bottom:80px}.R2xavzPHj8ETajWpjVin_ li{list-style:none;padding:8px;color:rgba(0,0,0,.3);border-bottom:1px dashed rgba(0,0,0,.1)}.R2xavzPHj8ETajWpjVin_ li a{width:100%;height:100%;display:block}._2Jch7-wZkN8NkeCir_mY5e{position:fixed;top:0;left:0;padding-left:12px;width:100%;height:50px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;background:#fff;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-shadow:0 1px 5px rgba(0,0,0,.3);box-shadow:0 1px 5px rgba(0,0,0,.3)}._2Jch7-wZkN8NkeCir_mY5e a{width:60px;margin-left:20px;height:100%;line-height:50px;font-size:14px;color:#999}._3xem2YnBb8MoKKqUR22xuA{width:100%;height:25px}._3xem2YnBb8MoKKqUR22xuA input[type=range]{-webkit-appearance:none;width:100%;border-radius:0}._3xem2YnBb8MoKKqUR22xuA input[type=range]::-webkit-slider-runnable-track{height:5px;border-radius:10px;background:rgba(0,0,0,.05)}._3xem2YnBb8MoKKqUR22xuA input[type=range]:focus{outline:none}._3xem2YnBb8MoKKqUR22xuA input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;height:25px;width:25px;margin-top:-10px;background:#fff;border-radius:100px;border:none;border:1px solid rgba(0,0,0,.1);-webkit-box-shadow:0 .125em .125em #3b4547;box-shadow:0 .125em .125em #3b4547}._3xem2YnBb8MoKKqUR22xuA input[type=range]::-moz-range-progress{background:linear-gradient(90deg,#059cfa,#fff 100%,#fff);height:13px;border-radius:10px}._3xem2YnBb8MoKKqUR22xuA input[type=range]::-ms-track{height:25px;border-radius:10px;box-shadow:0 1px 1px #def3f8,inset 0 .125em .125em #0d1112;border-color:transparent;color:transparent}._3xem2YnBb8MoKKqUR22xuA input[type=range]::-ms-thumb{border:.125em solid rgba(205,224,230,.5);height:25px;width:25px;border-radius:50%;background:#fff;margin-top:-5px;box-shadow:0 .125em .125em #3b4547}._3xem2YnBb8MoKKqUR22xuA input[type=range]::-ms-fill-lower{height:22px;border-radius:10px;background:linear-gradient(90deg,#059cfa,#fff 100%,#fff)}._3xem2YnBb8MoKKqUR22xuA input[type=range]::-ms-fill-upper{height:22px;border-radius:10px;background:#fff}._3xem2YnBb8MoKKqUR22xuA input[type=range]:focus::-ms-fill-lower{background:linear-gradient(90deg,#059cfa,#fff 100%,#fff)}._3xem2YnBb8MoKKqUR22xuA input[type=range]:focus::-ms-fill-upper{background:#fff}._3vACXCgs8DJfySLltK8__u{position:fixed;width:100%;height:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;left:0;top:0;z-index:1000}._3vACXCgs8DJfySLltK8__u img{width:40px} -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | MyReader 基于React实现的【绿色版电子书阅读器】,可以免费看任何小说,不过还是得支持正版哦!
-------------------------------------------------------------------------------- /dist/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/share.wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/dist/share.wechat.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myReader", 3 | "version": "1.0.0", 4 | "description": "基于React实现的【绿色版电子书阅读器】,可以免费看任何小说,支持离线下载", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --config webpack.dev.config.js --hot --inline --progress", 9 | "dev": "webpack-dev-server --config webpack.dev.config.js --hot --inline --progress --open", 10 | "build": "rimraf dist && webpack -p --colors --profile --display-error-details --display-modules --progress", 11 | "dll": "rimraf dll && webpack --config webpack.dll.config.js -p" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/liufulin90/myreader.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "webpack3" 20 | ], 21 | "author": "liufulin90", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/liufulin90/myreader/issues" 25 | }, 26 | "homepage": "https://github.com/liufulin90/myreader#readme", 27 | "devDependencies": { 28 | "autoprefixer": "^7.1.2", 29 | "babel-core": "^6.25.0", 30 | "babel-eslint": "^7.2.3", 31 | "babel-loader": "^7.1.1", 32 | "babel-plugin-import": "^1.3.1", 33 | "babel-plugin-transform-remove-console": "^6.8.4", 34 | "babel-plugin-transform-runtime": "^6.23.0", 35 | "babel-preset-es2015": "^6.24.1", 36 | "babel-preset-react": "^6.24.1", 37 | "babel-preset-stage-0": "^6.24.1", 38 | "copy-webpack-plugin": "^4.0.1", 39 | "css-loader": "^0.28.4", 40 | "eslint": "^3.19.0", 41 | "eslint-config-airbnb": "^15.1.0", 42 | "eslint-loader": "^1.9.0", 43 | "eslint-plugin-import": "^2.7.0", 44 | "eslint-plugin-jsx-a11y": "^6.0.2", 45 | "eslint-plugin-react": "^7.1.0", 46 | "extract-text-webpack-plugin": "^3.0.0", 47 | "file-loader": "^0.11.2", 48 | "flow-bin": "^0.52.0", 49 | "html-webpack-plugin": "^2.30.1", 50 | "less": "^2.7.2", 51 | "less-loader": "^4.0.5", 52 | "postcss-loader": "^2.0.6", 53 | "react-hot-loader": "^3.0.0-beta.7", 54 | "redux-devtools": "^3.4.0", 55 | "rimraf": "^2.6.1", 56 | "style-loader": "^0.18.2", 57 | "url-loader": "^0.5.9", 58 | "webpack": "^3.4.1", 59 | "webpack-dev-server": "^2.6.1" 60 | }, 61 | "dependencies": { 62 | "fastclick": "^1.0.6", 63 | "fetch-polyfill": "^0.8.2", 64 | "material-ui": "^1.0.0-beta.3", 65 | "material-ui-icons": "^1.0.0-alpha.19", 66 | "preact": "^8.2.1", 67 | "preact-compat": "^3.16.0", 68 | "react": "^15.6.1", 69 | "react-dom": "^15.6.1", 70 | "react-headroom": "^2.1.6", 71 | "react-redux": "^5.0.5", 72 | "react-router": "^4.1.2", 73 | "react-router-dom": "^4.1.2", 74 | "redux": "^3.7.2", 75 | "redux-persist": "^4.8.3", 76 | "redux-saga": "^0.15.6", 77 | "sweetalert2": "^6.6.6" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | autoprefixer({ 6 | browsers: ['last 5 versions'], 7 | }), 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/public/icon.png -------------------------------------------------------------------------------- /public/index.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MyReader 基于React实现的【绿色版电子书阅读器】,可以免费看任何小说,不过还是得支持正版哦! 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MyReader 基于React实现的【绿色版电子书阅读器】,可以免费看任何小说,不过还是得支持正版哦! 13 | 14 | 15 |
16 | 17 |
18 |
19 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/share.wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/public/share.wechat.png -------------------------------------------------------------------------------- /screenshots/00_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/00_index.png -------------------------------------------------------------------------------- /screenshots/01_reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/01_reader.png -------------------------------------------------------------------------------- /screenshots/01_reader_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/01_reader_close.png -------------------------------------------------------------------------------- /screenshots/03_chapter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/03_chapter.png -------------------------------------------------------------------------------- /screenshots/04_setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/04_setting.png -------------------------------------------------------------------------------- /screenshots/05_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/05_search.png -------------------------------------------------------------------------------- /screenshots/06_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/06_delete.png -------------------------------------------------------------------------------- /screenshots/07_delete_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/07_delete_done.png -------------------------------------------------------------------------------- /screenshots/08_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/08_detail.png -------------------------------------------------------------------------------- /screenshots/delete_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/delete_all.png -------------------------------------------------------------------------------- /screenshots/main_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/main_all.png -------------------------------------------------------------------------------- /screenshots/myreader-online-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/myreader-online-qrcode.png -------------------------------------------------------------------------------- /screenshots/reader_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/reader_all.png -------------------------------------------------------------------------------- /screenshots/retry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/retry.jpg -------------------------------------------------------------------------------- /screenshots/source_min.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/screenshots/source_min.jpg -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const proxy = require('http-proxy-middleware'); 3 | 4 | const app = express(); 5 | app.use('/static', express.static('static')); 6 | app.use('/assets', express.static('assets')); 7 | app.use('/api', proxy({ 8 | target: 'http://api.zhuishushenqi.com/', 9 | pathRewrite: { '^/api': '/' }, 10 | changeOrigin: true, 11 | })); 12 | app.use('/chapter', proxy({ 13 | target: 'http://chapter2.zhuishushenqi.com/', 14 | pathRewrite: { '^/chapter': '/chapter' }, 15 | changeOrigin: true, 16 | })); 17 | app.use('/agent', proxy({ 18 | target: 'http://statics.zhuishushenqi.com/', 19 | // pathRewrite: { '^/chapter': '/chapter' }, 20 | changeOrigin: true, 21 | })); 22 | app.use('/', express.static(`${__dirname}/dist`)); 23 | app.listen(8099); 24 | console.log('服务器8099'); 25 | -------------------------------------------------------------------------------- /src/components/GaussianBlur/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './style.less'; 3 | 4 | // 高斯模糊组件(一种滤镜功能) 5 | export default ({ 6 | children, 7 | src, 8 | animation = false, 9 | filter = 25, 10 | }) => (
11 | {children} 12 |
21 |
); 22 | -------------------------------------------------------------------------------- /src/components/GaussianBlur/style.less: -------------------------------------------------------------------------------- 1 | .gaussian { 2 | position: relative; 3 | overflow: hidden; 4 | } 5 | .bg { 6 | display: block; 7 | position: absolute; 8 | top:-25%; 9 | left: -25%; 10 | width: 150%; 11 | height: 150%; 12 | filter: blur(25px); 13 | // animation: run 25s ease-in-out; 14 | animation-iteration-count: infinite; 15 | background-repeat: repeat; 16 | background-size: 100% 100%; 17 | z-index: -1; 18 | background-position: center; 19 | } 20 | 21 | @keyframes run { 22 | 0% { 23 | background-position: 50% 10%; 24 | } 25 | 70% { 26 | filter: blur(50px); 27 | background-position: 50% 90%; 28 | } 29 | 100% { 30 | background-position: 50% 10%; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/InputRange/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.less'; 3 | 4 | export default ({ option }) => (
5 | 6 |
); 7 | -------------------------------------------------------------------------------- /src/components/InputRange/index.less: -------------------------------------------------------------------------------- 1 | .range { 2 | width: 100%; 3 | height: 25px; 4 | input[type=range] { 5 | -webkit-appearance: none; 6 | width: 100%; 7 | border-radius: 0px; /*这个属性设置使填充进度条时的图形为圆角*/ 8 | } 9 | input[type=range]::-webkit-slider-thumb { 10 | -webkit-appearance: none; 11 | } 12 | input[type=range]::-webkit-slider-runnable-track { 13 | height: 5px; 14 | border-radius: 10px; /*将轨道设为圆角的*/ 15 | background: rgba(0, 0, 0, 0.05); 16 | // box-shadow: 0 1px 1px #def3f8, inset 0 .125em .125em #0d1112; /*轨道内置阴影效果*/ 17 | } 18 | input[type=range]:focus { 19 | outline: none; 20 | } 21 | input[type=range]::-webkit-slider-thumb { 22 | -webkit-appearance: none; 23 | height: 25px; 24 | width:25px; 25 | margin-top: -10px; /*使滑块超出轨道部分的偏移量相等*/ 26 | background: #fff; 27 | border-radius: 100px; /*外观设置为圆形*/ 28 | border: none; /*设置边框*/ 29 | border: 1px rgba(0, 0, 0, 0.1) solid; 30 | // box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); 31 | box-shadow: 0 .125em .125em #3b4547; /*添加底部阴影*/ 32 | } 33 | input[type=range]::-moz-range-progress{ 34 | background: linear-gradient(to right, #059CFA, white 100%, white); 35 | height: 13px; 36 | border-radius: 10px; 37 | } 38 | input[type=range]::-ms-track { 39 | height: 25px; 40 | border-radius: 10px; 41 | box-shadow: 0 1px 1px #def3f8, inset 0 .125em .125em #0d1112; 42 | border-color: transparent; /*去除原有边框*/ 43 | color: transparent; /*去除轨道内的竖线*/ 44 | } 45 | 46 | input[type=range]::-ms-thumb { 47 | border: solid 0.125em rgba(205, 224, 230, 0.5); 48 | height: 25px; 49 | width: 25px; 50 | border-radius: 50%; 51 | background: #ffffff; 52 | margin-top: -5px; 53 | box-shadow: 0 .125em .125em #3b4547; 54 | } 55 | 56 | input[type=range]::-ms-fill-lower { 57 | /*进度条已填充的部分*/ 58 | height: 22px; 59 | border-radius: 10px; 60 | background: linear-gradient(to right, #059CFA, white 100%, white); 61 | } 62 | 63 | input[type=range]::-ms-fill-upper { 64 | /*进度条未填充的部分*/ 65 | height: 22px; 66 | border-radius: 10px; 67 | background: #ffffff; 68 | } 69 | 70 | input[type=range]:focus::-ms-fill-lower { 71 | background: linear-gradient(to right, #059CFA, white 100%, white); 72 | } 73 | 74 | input[type=range]:focus::-ms-fill-upper { 75 | background: #ffffff; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ListItem/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.less'; 3 | 4 | import GaussianBlur from '../GaussianBlur'; 5 | 6 | export default ({ _id, cover, title, lastChapter, author, history }) => { 7 | function goToDetail() { 8 | history.push(`/reader/${_id}`); 9 | } 10 | return (
14 |
15 | 16 |
17 |
{title}
18 |

{author}

19 |

最新章节:{lastChapter}

20 |
21 |
22 |
); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/ListItem/index.less: -------------------------------------------------------------------------------- 1 | .book { 2 | display: flex; 3 | padding: 20px; 4 | padding-bottom: 0px; 5 | height: 120px; 6 | overflow: hidden; 7 | position: relative; 8 | background: rgba(0, 0, 0, 0.1); 9 | box-shadow: 0 -5px 5px rgba(0, 0, 0, 0.1) inset; 10 | color: #fff; 11 | .info { 12 | padding-left: 110px; 13 | p{ 14 | font-size: 12px; 15 | } 16 | } 17 | img.cover{ 18 | position: absolute; 19 | z-index: 0; 20 | height: 120px; 21 | box-shadow: 0 0px 3px rgba(0, 0, 0, 0.3); 22 | } 23 | .bg { 24 | position: absolute; 25 | left: 0; 26 | top: 0; 27 | width: 100%; 28 | height: 200px; 29 | background-repeat: no-repeat; 30 | background-size: 100%; 31 | filter: blur(30px); 32 | z-index: -1; 33 | // animation: myfirst 20s; 34 | animation-iteration-count: infinite; 35 | } 36 | } 37 | 38 | @keyframes myfirst { 39 | 0% { 40 | transform: scale(2) translateY(20%); 41 | } 42 | 70% { 43 | transform: scale(1.5) translateY(-30%); 44 | -webkit-filter: blur(20px); /* Chrome, Opera */ 45 | -moz-filter: blur(20px); 46 | -ms-filter: blur(20px); 47 | filter: blur(20px); 48 | } 49 | 100% { 50 | transform: scale(2) translateY(20%); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by liufulin on 17-9-26. 3 | */ 4 | import React from 'react'; 5 | import styles from './index.less'; 6 | import imgage from './loading.gif'; 7 | 8 | const Loading = ({ loading }) => { 9 | return (loading ? (
10 | loading 11 |
) : (

)); 12 | }; 13 | 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /src/components/Loading/index.less: -------------------------------------------------------------------------------- 1 | .loading{ 2 | position: fixed; 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | left: 0; 9 | top: 0; 10 | z-index: 1000; 11 | } 12 | .loading img{ 13 | width: 40px; 14 | } -------------------------------------------------------------------------------- /src/components/Loading/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liufulin90/myreader/08feaba3fddb322e90abcd983ed3f1fd0a801181/src/components/Loading/loading.gif -------------------------------------------------------------------------------- /src/components/ProgressLayer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by liufulin on 17-9-26. 3 | * 进度条 4 | */ 5 | import React from 'react'; 6 | import styles from './index.less'; 7 | 8 | const ProgressLayer = ({ length }) => { 9 | const widths = `${length}%`; 10 | return (

11 |
 
12 |
); 13 | }; 14 | 15 | export default ProgressLayer; 16 | -------------------------------------------------------------------------------- /src/components/ProgressLayer/index.less: -------------------------------------------------------------------------------- 1 | .progressBody{ 2 | position: absolute; 3 | z-index: 1000; 4 | left: 5%; 5 | top: 50%; 6 | width: 90%; 7 | height: 14px; 8 | background-color: rgba(244, 255, 232, 1); 9 | box-shadow: 1px 2px 7px rgba(0,0,0,.4); 10 | border-radius: 10px; 11 | .progress{ 12 | height: 100%; 13 | border-radius: 10px; 14 | background-color: #ff5722; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component } from 'react'; 4 | import styles from './style.less'; 5 | 6 | class SearchBar extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.onSubmit = (e) => { 10 | const value = this.input.value; 11 | if (value) { 12 | if (this.props.onSubmit) this.props.onSubmit(value); 13 | } 14 | e.preventDefault(); 15 | }; 16 | this.onChange = () => { 17 | if (this.props.onChange) this.props.onChange(this.input.value); 18 | }; 19 | } 20 | render() { 21 | const { type } = this.props; 22 | return ( 23 |
24 |
25 |
26 |
 
27 | { this.input = c; }} id="text" placeholder="输入关键字搜索" /> 28 |
29 | 取消 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default SearchBar; 37 | -------------------------------------------------------------------------------- /src/components/SearchBar/style.less: -------------------------------------------------------------------------------- 1 | .search { 2 | height: 40px; 3 | margin-bottom: 20px; 4 | } 5 | .box { 6 | padding: 5px 8px; 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | flex-direction: row; 11 | background: #fff; 12 | position: fixed; 13 | left: 0; 14 | right: 0; 15 | width: 100%; 16 | height: 40px; 17 | z-index: 99; 18 | .icon { 19 | opacity: 0.5; 20 | width: 16px; 21 | height: 16px; 22 | margin: 0 5px; 23 | background-image: url("../../../public/search.svg"); 24 | background-size: 16px 16px; 25 | } 26 | .input { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | flex-direction: row; 31 | width: 100%; 32 | background: rgba(0,0,0,0.05); 33 | border: none; 34 | padding: 4px; 35 | border-radius: 5px; 36 | input { 37 | font-size: 14px; 38 | height: 100%; 39 | background: none; 40 | border-radius: 0; 41 | border: none; 42 | width: 100%; 43 | } 44 | } 45 | .cancel { 46 | width: 70px; 47 | height: 100%; 48 | line-height: 40px; 49 | padding-left: 20px; 50 | font-size: 14px; 51 | color: #999; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Touch/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ children, onPress, onTap }) => { 4 | let timeout; 5 | let pressed = false; 6 | let cancel = false; 7 | function touchStart() { 8 | timeout = setTimeout(() => { 9 | pressed = true; 10 | if (onPress) onPress(); 11 | }, 500); 12 | return false; 13 | } 14 | function touchEnd() { 15 | clearTimeout(timeout); 16 | if (pressed) { 17 | pressed = false; 18 | return; 19 | } 20 | if (cancel) { 21 | cancel = false; 22 | return; 23 | } 24 | if (onTap) onTap(); 25 | return false; 26 | } 27 | function touchCancel() { 28 | cancel = true; 29 | } 30 | return (
38 | { children } 39 |
); 40 | }; 41 | -------------------------------------------------------------------------------- /src/index.dev.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import App from './router.js'; 5 | 6 | const render = (Component) => { 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | }; 14 | 15 | render(App); 16 | 17 | if (module.hot) { 18 | module.hot.accept('./router.js', () => { 19 | render(App); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './router.js'; 4 | 5 | render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-tap-highlight-color:rgba(255,255,255,0); 3 | border-width: thin; 4 | -webkit-appearance: none; 5 | user-select: none; 6 | -webkit-user-select: none; 7 | -webkit-touch-callout: none; 8 | } 9 | input { 10 | -webkit-user-select: auto; 11 | -webkit-touch-callout: auto; 12 | } 13 | html,body { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | body { 19 | // background-color: #fafbfc; 20 | font-weight: 400; 21 | -webkit-font-smoothing: antialiased; 22 | font-family: -apple-system, BlinkMacSystemFont, PingFang-SC-Regular, 'Hiragino Sans GB', 'Microsoft Yahei', Arial, sans-serif; 23 | max-width: 450px; 24 | margin: 0 auto; 25 | background: #fafbfc; 26 | position: relative; 27 | } 28 | 29 | a { 30 | color: #292525; 31 | text-decoration: none 32 | } 33 | 34 | h1,h2,h3,h4,h5,h6,p{ 35 | margin: 0; 36 | padding: 0; 37 | } 38 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | // BrowserRouter as Router, 4 | HashRouter as Router, // 使用hash路由解决一些页面无法刷新问题 5 | Route, // 这是基本的路由块 6 | // Link, // 这是a标签/ 7 | // Switch, // 这是监听空路由的 8 | // Redirect, // 这是重定向 9 | // Prompt, // 防止转换 10 | } from 'react-router-dom'; 11 | import { Provider } from 'react-redux'; 12 | import FastClick from 'fastclick'; 13 | import 'sweetalert2/dist/sweetalert2.min.css'; 14 | 15 | import './index.less'; 16 | import Index from './routes/IndexPage'; 17 | import Search from './routes/Search'; 18 | import Detail from './routes/Detail'; 19 | import Reader from './routes/Reader'; 20 | import Chapters from './routes/Chapters'; 21 | import store from './store'; 22 | import Loading from './routes/Loading'; 23 | 24 | FastClick.attach(document.body); 25 | 26 | // 模板,套路 27 | const RouterConfig = () => ( 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | ); 41 | 42 | export default RouterConfig; 43 | -------------------------------------------------------------------------------- /src/routes/Chapters/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import styles from './index.less'; 4 | 5 | import Rnage from '../../components/InputRange'; 6 | 7 | class Chapters extends Component { 8 | constructor(porps) { 9 | super(porps); 10 | this.back = () => { 11 | this.props.history.goBack(); 12 | }; 13 | this.goToChapter = (nextChapter) => { 14 | this.props.dispatch({ 15 | type: 'reader/goToChapter', 16 | payload: { nextChapter }, 17 | }); 18 | this.props.history.goBack(); 19 | }; 20 | // 滑动顶部进度条 21 | this.skip = () => { 22 | setTimeout(() => { 23 | document.getElementById(this.range.value).scrollIntoView(false); 24 | /* 25 | scrollIntoView()可以在所有的HTML元素上调用,通过滚动浏览器窗口或某个容器元素, 26 | 调用元素就可以出现在视窗中。如果给该方法传入true作为参数,或者不传入任何参数,那么 27 | 窗口滚动之后会让调动元素顶部和视窗顶部尽可能齐平。如果传入false作为参数,调用元素 28 | 会尽可能全部出现在视口中(可能的话,调用元素的底部会与视口的顶部齐平。)不过顶部 29 | 不一定齐平。 30 | 支持该方法的浏览器有 IE、Firefox、Safari和Opera。 31 | */ 32 | }, 100); 33 | }; 34 | } 35 | componentDidMount() { 36 | const { chapters, currentChapter } = this.props; 37 | const chapterLen = chapters.length; 38 | let id = currentChapter + 7; 39 | id = id >= chapterLen ? chapterLen - 1 : id; 40 | setTimeout(() => { 41 | try { 42 | document.getElementById(`${id || 0}`).scrollIntoView(false); 43 | } catch (error) { 44 | console.log(error); 45 | } 46 | }, 100); 47 | } 48 | render() { 49 | const { chapters, currentChapter } = this.props; 50 | return (
51 |
52 | { this.range = c; }, 60 | }} 61 | /> 62 | 取消 63 |
64 | 65 | 80 |
); 81 | } 82 | } 83 | 84 | function mapStateToProps(state) { 85 | const { chapters, currentChapter } = state.reader; 86 | return { 87 | chapters, 88 | currentChapter, 89 | }; 90 | } 91 | 92 | export default connect(mapStateToProps)(Chapters); 93 | -------------------------------------------------------------------------------- /src/routes/Chapters/index.less: -------------------------------------------------------------------------------- 1 | .list { 2 | margin: 0; 3 | padding: 0; 4 | margin-top: 60px; 5 | margin-bottom: 80px; 6 | li { 7 | list-style: none; 8 | padding: 8px; 9 | color: rgba(0, 0, 0, 0.3); 10 | border-bottom: 1px rgba(0, 0, 0, 0.1) dashed; 11 | a { 12 | width: 100%; 13 | height: 100%; 14 | display: block; 15 | } 16 | } 17 | } 18 | .top { 19 | position: fixed; 20 | top: 0px; 21 | left: 0; 22 | padding-left: 12px; 23 | width: 100%; 24 | height: 50px; 25 | display: flex; 26 | justify-content: space-between; 27 | background: #fff; 28 | box-sizing: border-box; 29 | align-items: center; 30 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3); 31 | a { 32 | width: 60px; 33 | margin-left: 20px; 34 | height: 100%; 35 | line-height: 50px; 36 | font-size: 14px; 37 | color: #999; 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/routes/Detail/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/Detail/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import GaussianBlur from '../../components/GaussianBlur'; 4 | 5 | import styles from './index.less'; 6 | // import BackIcon from './back.svg'; 7 | import ProgressLayer from '../../components/ProgressLayer'; 8 | 9 | class Detail extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.readNow = (id) => { 13 | this.props.history.push(`/reader/${id}`); 14 | this.props.dispatch({ 15 | type: 'reader/getSource', 16 | query: { id }, 17 | }); 18 | }; 19 | this.back = () => { 20 | this.props.history.goBack(); 21 | }; 22 | } 23 | componentWillMount() { 24 | this.props.dispatch({ 25 | type: 'reader/getDetail', 26 | query: this.props.match.params, 27 | }); 28 | } 29 | componentWillReceiveProps(nextProps) { 30 | const { downloadStatus, _id } = nextProps; 31 | // 下载完成或当前书籍已经下载到书架中则跳转页面到阅读页 32 | if (downloadStatus) { 33 | this.readNow(_id); 34 | } 35 | } 36 | 37 | /** 38 | * 离线下载 39 | * @param id 40 | */ 41 | downloadNow = () => { 42 | const { _id } = this.props; 43 | this.props.dispatch({ 44 | type: 'reader/downGetSource', 45 | query: { id: _id, download: true }, 46 | }); 47 | } 48 | render() { 49 | const { 50 | _id, 51 | cover, 52 | title, 53 | author, 54 | isSerial, 55 | majorCate, 56 | minorCate, 57 | longIntro, 58 | wordCount, 59 | latelyFollower, 60 | lastChapter, 61 | tags, 62 | downloadStatus, 63 | downloadPercent, 64 | } = this.props; 65 | return (
66 | 67 | 68 | 返回 69 | 70 |
71 | 72 |
73 |

{title}

74 |

{author}

75 |

{majorCate} / {minorCate}

76 |

{wordCount > 10000 ? `${parseInt(wordCount / 10000, 0)} 万` : wordCount}字、{latelyFollower} 人在追

77 |

{isSerial ? '连载中' : '已完结'}

78 |
79 |
80 |
81 |
82 |

{longIntro && longIntro.length > 40 ? `${longIntro.substring(0, 40)}...` : longIntro}

83 |
84 | { 85 | tags && tags.map(i => 86 | ( 87 | {i} 88 | )) 89 | } 90 |
91 |

最新章节:{lastChapter}

92 | 立即阅读 93 | {!downloadStatus ? '离线下载' : '已下载'} 94 |
95 | { 96 | downloadPercent > 1 ? (
) : '' 97 | } 98 |
); 99 | } 100 | } 101 | 102 | function mapStateToProps(state) { 103 | const { detail = {} } = state.search; 104 | const { downloadPercent } = state.reader; 105 | return { 106 | ...detail, 107 | downloadPercent, 108 | }; 109 | } 110 | 111 | export default connect(mapStateToProps)(Detail); 112 | -------------------------------------------------------------------------------- /src/routes/Detail/index.less: -------------------------------------------------------------------------------- 1 | .back { 2 | color: #fff; 3 | padding: 20px; 4 | padding-bottom: 0; 5 | display: block; 6 | font-size: 14px; 7 | display: flex; 8 | align-items: center; 9 | flex-direction: row; 10 | img { 11 | height: 15px; 12 | } 13 | } 14 | 15 | .body { 16 | padding: 20px; 17 | .desc { 18 | margin: 0; 19 | padding: 10px; 20 | background: rgba(0,0,0,0.03); 21 | border-radius: 3px; 22 | font-size: 12px; 23 | color: #666; 24 | position: relative; 25 | margin-bottom: 20px; 26 | &:after{ 27 | bottom: 0; 28 | right: 0; 29 | position: absolute; 30 | content: ''; 31 | width: 0; 32 | height: 0; 33 | border: 10px red solid; 34 | border-color:rgba(0,0,0,0.05) #fff #fff rgba(0,0,0,0.05); 35 | } 36 | } 37 | .last { 38 | font-size: 12px; 39 | color: #666; 40 | text-align: center; 41 | } 42 | .tags { 43 | display: flex; 44 | align-items: center; 45 | justify-content: flex-start; 46 | flex-wrap: wrap; 47 | span { 48 | background: rgba(0,0,0,0.03); 49 | margin-right: 8px; 50 | margin-bottom: 8px; 51 | padding: 1px 8px; 52 | font-size: 10px; 53 | color: #666; 54 | border-radius: 3px; 55 | display: inline-block; 56 | } 57 | } 58 | } 59 | .book { 60 | padding: 20px; 61 | padding-bottom: 0; 62 | overflow: hidden; 63 | .cover { 64 | float: left; 65 | height: 150px; 66 | display: block; 67 | } 68 | .info { 69 | float: left; 70 | display: block; 71 | padding: 0 20px; 72 | padding-bottom: 0; 73 | opacity: hidden; 74 | display: block; 75 | color: #fff; 76 | h1 { 77 | margin: 0; 78 | font-size: 16px; 79 | } 80 | p { 81 | font-size: 14px; 82 | margin: 5px 0; 83 | } 84 | } 85 | } 86 | .read, .download { 87 | width: 100%; 88 | display: block; 89 | background: #987; 90 | padding: 10px 0px; 91 | text-align: center; 92 | color: #fff; 93 | box-shadow: 0 1px 2px rgba(0,0,0,0.5); 94 | margin-top: 40px; 95 | } 96 | .download{ 97 | margin-top: 16px; 98 | background: #a38877; 99 | } 100 | // 进度条 101 | .progressWrap{ 102 | position: fixed; 103 | left: 0px; 104 | top: 50%; 105 | background: rgba(255,255,255,0.4); 106 | height: 150px; 107 | width: 100%; 108 | border-radius: 20%; 109 | } -------------------------------------------------------------------------------- /src/routes/IndexPage/BookList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import swal from 'sweetalert2'; 3 | 4 | import Touch from '../../components/Touch'; 5 | import styles from './BookList.less'; 6 | 7 | export default ({ list = [], history, dispatch }) => { 8 | function goToDetail(id) { 9 | history.push(`/reader/${id}`); 10 | } 11 | function press(_id, title) { 12 | swal({ 13 | title: '确认删除', 14 | type: 'question', 15 | text: `从书架移除《${title}》,并清除阅读进度吗?`, 16 | focusCancel: true, 17 | showCancelButton: true, 18 | confirmButtonText: '删除', 19 | confirmButtonColor: 'red', 20 | cancelButtonText: '朕点错了而已', 21 | }).then(() => { 22 | dispatch({ 23 | type: 'store/delete', 24 | key: _id, 25 | }); 26 | swal( 27 | '已删除!', 28 | `已从书架移除《${title}》`, 29 | 'success', 30 | ); 31 | }).catch((e) => { 32 | console.log(e); 33 | }); 34 | } 35 | return ( 36 |
37 | { 38 | list.map(({ title, _id, cover }) => ( 39 |
40 | 44 |
45 |

{title}

46 | 47 |
48 | )) 49 | } 50 | {(list.length % 3) === 2 &&
} 51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/routes/IndexPage/BookList.less: -------------------------------------------------------------------------------- 1 | .books { 2 | display: flex; 3 | padding: 20px; 4 | padding-bottom: 0; 5 | flex-wrap: wrap; 6 | justify-content: space-between; 7 | } 8 | .book{ 9 | height: 160px; 10 | width: 90px; 11 | display: block; 12 | display: flex; 13 | align-items: center; 14 | flex-direction: column; 15 | overflow: hidden; 16 | .cover{ 17 | height: 120px; 18 | width: 90px; 19 | background-size: 100% 100%; 20 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); 21 | } 22 | p { 23 | text-align: center; 24 | color: rgba(0, 0, 0, 0.5); 25 | margin-top: 6px; 26 | font-weight: 100px; 27 | font-size: 12px; 28 | max-width: 90px; 29 | text-overflow:ellipsis; 30 | overflow:hidden; 31 | white-space:nowrap; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/IndexPage/Current.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Current.less'; 3 | import SearchIcon from './search.svg'; 4 | 5 | export default ({ _id, cover, title, lastChapter, author, wordCount, latelyFollower, history }) => { 6 | function goToDetail() { 7 | history.push(`/reader/${_id}`); 8 | } 9 | function search() { 10 | history.push('/search'); 11 | } 12 | return (
15 |
16 |

17 | MyReader 18 |
19 | 20 | Search 21 |
22 |

23 | 24 |
25 | 26 |
27 |

{title}

28 |

{author}

29 |

{wordCount > 10000 ? `${parseInt(wordCount / 10000, 0)} 万` : wordCount}字、{latelyFollower} 人在追

30 |

最新章节:{lastChapter}

31 |
32 |
33 |
34 |
); 35 | }; 36 | -------------------------------------------------------------------------------- /src/routes/IndexPage/Current.less: -------------------------------------------------------------------------------- 1 | .book { 2 | display: flex; 3 | padding: 20px; 4 | padding-bottom: 0px; 5 | height: 170px; 6 | overflow: hidden; 7 | position: relative; 8 | background: rgba(0, 0, 0, 0.3); 9 | box-shadow: 0 -2px 3px rgba(0, 0, 0, 0.05) inset; 10 | color: #fff; 11 | .wrap { 12 | width: 100%; 13 | } 14 | .title { 15 | margin-bottom: 10px; 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | font-weight: 100; 20 | } 21 | .search { 22 | font-size: 14px; 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | font-weight: 100; 27 | padding-left: 30px; 28 | .icon { 29 | width: 12px; 30 | height: 12px; 31 | margin-right: 4px; 32 | margin-top: 1px; 33 | } 34 | } 35 | .info { 36 | padding-left: 110px; 37 | p{ 38 | margin: 5px 0; 39 | font-size: 12px; 40 | } 41 | } 42 | img.cover{ 43 | position: absolute; 44 | z-index: 0; 45 | height: 120px; 46 | box-shadow: 0 0px 3px rgba(0, 0, 0, 0.3); 47 | } 48 | .bg { 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | width: 100%; 53 | height: 200px; 54 | background-repeat: no-repeat; 55 | background-size: 100%; 56 | filter: blur(30px); 57 | z-index: -1; 58 | animation: myfirst 20s; 59 | animation-iteration-count: infinite; 60 | } 61 | } 62 | 63 | @keyframes myfirst { 64 | 0% { 65 | transform: scale(2) translateY(20%); 66 | } 67 | 70% { 68 | transform: scale(1.5) translateY(-30%); 69 | -webkit-filter: blur(20px); /* Chrome, Opera */ 70 | -moz-filter: blur(20px); 71 | -ms-filter: blur(20px); 72 | filter: blur(20px); 73 | } 74 | 100% { 75 | transform: scale(2) translateY(20%); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/routes/IndexPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import BookList from './BookList'; 5 | import Current from './Current'; 6 | 7 | import styles from './index.less'; 8 | 9 | import recommend from '../../utils/recommond.js'; 10 | 11 | const Index = ({ store, current, history, dispatch }) => (
12 | 13 | 14 |

15 | { 16 | store.length === 0 ? '书架空空如也,点右上角找书哦~' : '点击右上角按钮添加书籍' 17 | } 18 | { 19 | store.length >= 6 && ', 长按可删除书籍~' 20 | } 21 |

22 |
); 23 | 24 | function mapStateToProps(state) { 25 | const { detail } = state.reader; 26 | const list = state.store; 27 | const store = Object.keys(list).map((id) => { 28 | // 找出书架上所有书籍的详细信息 29 | return list[id] ? list[id].detail : {}; 30 | }).filter((i) => { 31 | // 过滤掉异常数据和当前阅读 32 | return i._id && i._id !== detail._id; 33 | }); 34 | return { 35 | store, 36 | // 如果是一本书都没有,推荐src/utils/recommond.js的第一个《斗破苍穹》 37 | current: detail._id ? detail : recommend, 38 | }; 39 | } 40 | 41 | export default connect(mapStateToProps)(Index); 42 | -------------------------------------------------------------------------------- /src/routes/IndexPage/index.less: -------------------------------------------------------------------------------- 1 | .tip { 2 | font-size: 12px; 3 | text-align: center; 4 | color: rgba(0, 0, 0, 0.5); 5 | margin-bottom: 20px; 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/IndexPage/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/Loading/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by liufulin on 17-9-26. 3 | */ 4 | import React, { Component } from 'react'; 5 | import { connect } from 'react-redux'; 6 | 7 | import Loading from '../../components/Loading'; 8 | 9 | class LoadingPage extends Component { 10 | constructor(props) { 11 | super(props); 12 | console.log(this.props); 13 | } 14 | 15 | render() { 16 | const { loading } = this.props; 17 | return (); 18 | } 19 | } 20 | 21 | function mapStateToProps(state) { 22 | const { loading } = state.common; 23 | return { 24 | loading, 25 | }; 26 | } 27 | 28 | export default connect(mapStateToProps)(LoadingPage); 29 | -------------------------------------------------------------------------------- /src/routes/Reader/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Content.less'; 3 | 4 | // 将换行符作为依据转换成数组显示,这样方便设置css样式。 5 | export default ({ content, style }) => (
6 | { content && content.split('\n').map(i =>

{i}

) } 7 |
); 8 | -------------------------------------------------------------------------------- /src/routes/Reader/Content.less: -------------------------------------------------------------------------------- 1 | .content { 2 | padding: 5px 1px; 3 | p { 4 | padding: 0 0.5em; 5 | text-indent: 2em; 6 | word-wrap: break-word; 7 | line-height: 1.75; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/Reader/Head.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Headroom from 'react-headroom'; 3 | import styles from './Head.less'; 4 | import CloseIcon from './close.svg'; 5 | // import MenuIcon from './menu.svg'; 6 | 7 | class Header extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.back = () => { 11 | this.props.history.push('/'); 12 | window.scrollTo(0, 0); 13 | }; 14 | } 15 | render() { 16 | const { bookName, title, color = {} } = this.props; 17 | return ( 18 | 19 |
20 |
21 |

{bookName}

22 |

{title}

23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | export default Header; 36 | -------------------------------------------------------------------------------- /src/routes/Reader/Head.less: -------------------------------------------------------------------------------- 1 | .head { 2 | height: 60px; 3 | position: relative; 4 | overflow: hidden; 5 | // background: #FAF9DE; 6 | border-bottom: 1px dashed rgba(0, 0,0, 0.1); 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | } 11 | 12 | .menus { 13 | display: flex; 14 | } 15 | 16 | .info { 17 | height: 100%; 18 | padding: 10px; 19 | display: flex; 20 | justify-content: center; 21 | flex-direction: column; 22 | h3 { 23 | font-weight: 100; 24 | font-size: 16px; 25 | } 26 | p { 27 | font-size: 12px; 28 | } 29 | } 30 | 31 | .button { 32 | height: 60px; 33 | width: 60px; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | img { 38 | width: 20px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/routes/Reader/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Loading.less'; 3 | 4 | export default ({ logs }) => (
5 |

正在初始化...

6 | {logs.map(i => (

7 | {i.msg} 8 |

))} 9 |
); 10 | -------------------------------------------------------------------------------- /src/routes/Reader/Loading.less: -------------------------------------------------------------------------------- 1 | .loading { 2 | position: fixed; 3 | display: flex; 4 | align-items: left; 5 | justify-content: flex-end; 6 | flex-direction: column; 7 | top: 0; 8 | left: 0; 9 | width: 100%; 10 | height: 100%; 11 | background: none; 12 | opacity: 0.5; 13 | p { 14 | font-size: 10px; 15 | padding: 0; 16 | margin: 0; 17 | } 18 | img.loading { 19 | max-width: 12px * 2; 20 | opacity: 0.3; 21 | animation: turn 2s infinite ease-in-out; 22 | margin-right: 10px * 2; 23 | } 24 | } 25 | 26 | @keyframes turn { 27 | from { 28 | transform: rotateZ(0deg); 29 | } 30 | to { 31 | transform: rotateZ(3*360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/Reader/Setting.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styles from './Setting.less'; 3 | import { COLORS, MODE_COLOR } from '../../utils/constants.js'; 4 | 5 | class Setting extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | visible: false, 11 | }; 12 | this.open = () => { 13 | this.setState({ 14 | visible: true, 15 | }, () => { 16 | document.addEventListener('click', this.close); 17 | }); 18 | }; 19 | this.close = () => { 20 | this.setState({ 21 | visible: false, 22 | }, () => { 23 | document.removeEventListener('click', this.close); 24 | }); 25 | }; 26 | this.stopEvent = (e) => { 27 | // 阻止合成事件间的冒泡 28 | e.stopPropagation(); 29 | // 阻止合成事件与最外层document上的事件间的冒泡 30 | e.nativeEvent.stopImmediatePropagation(); 31 | e.preventDefault(); 32 | return false; 33 | }; 34 | // 设置主题颜色 35 | this.setThemeColor = (key, val) => { 36 | this.props.dispatch({ 37 | type: 'setting/save', 38 | payload: { 39 | [key]: val, 40 | }, 41 | }); 42 | }; 43 | // 设置模式(白天/黑夜) 44 | this.setMode = () => { 45 | const mode = this.props.mode === 'day' ? 'night' : 'day'; 46 | const color = MODE_COLOR[mode]; 47 | this.props.dispatch({ 48 | type: 'setting/save', 49 | payload: { 50 | color, 51 | mode, 52 | }, 53 | }); 54 | }; 55 | // 调整字体大小 56 | this.setFontSize = (num) => { 57 | const fontSize = this.props.style.fontSize + num; 58 | this.props.dispatch({ 59 | type: 'setting/save', 60 | payload: { 61 | style: { 62 | ...this.props.style, 63 | fontSize, 64 | }, 65 | }, 66 | }); 67 | }; 68 | this.clear = () => { 69 | this.props.dispatch({ type: 'setting/clear' }); 70 | }; 71 | } 72 | componentWillUnmount() { 73 | document.removeEventListener('click', this.close); 74 | } 75 | render() { 76 | const { children, mode } = this.props; 77 | const { visible } = this.state; 78 | return ( 79 |
80 | {children} 81 |
82 |
83 | {mode === 'day' ? '黑夜' : '白天'} 84 | Aa+ 85 | Aa- 86 | 默认 87 |
88 |
89 |
90 | { 91 | COLORS.map(i => ()) 95 | } 96 |
97 |
98 |
99 |
100 | ); 101 | } 102 | } 103 | 104 | export default Setting; 105 | -------------------------------------------------------------------------------- /src/routes/Reader/Setting.less: -------------------------------------------------------------------------------- 1 | .setting { 2 | background:rgba(0,0,0,0.8); 3 | position: fixed; 4 | width: 100%; 5 | height: 120px; 6 | left: 0; 7 | bottom: 0; 8 | transition: 0.3s; 9 | color: #fff; 10 | font-size: 14px; 11 | padding: 10px; 12 | box-sizing: border-box; 13 | } 14 | .show { 15 | transform: translateY(0%); 16 | } 17 | .hide { 18 | transform: translateY(300%); 19 | } 20 | .buttons { 21 | display: flex; 22 | justify-content: space-between; 23 | height: 40px; 24 | align-items: center; 25 | margin-bottom: 10px; 26 | span { 27 | width: 75px; 28 | height: 30px; 29 | text-align: center; 30 | line-height: 30px; 31 | display: block; 32 | border: 1px #fff solid; 33 | border-radius: 5px; 34 | color: #fff; 35 | &:active, &:hover, &:focus { 36 | background: #fff; 37 | color: #000; 38 | } 39 | } 40 | } 41 | .colors { 42 | overflow: scroll; 43 | .colorsBox { 44 | display: flex; 45 | flex-wrap: nowrap; 46 | } 47 | span { 48 | display: block; 49 | width: 30px; 50 | height: 30px; 51 | margin-bottom: 10px; 52 | margin-right: 10px; 53 | border-radius: 100px; 54 | border: 2px rgba(255,255,255,0.5) solid; 55 | content: ''; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/routes/Reader/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/Reader/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Head from './Head'; 4 | import Content from './Content'; 5 | import Loading from './Loading'; 6 | import Setting from './Setting'; 7 | 8 | import styles from './index.less'; 9 | 10 | class Search extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.next = () => { 14 | const { currentChapter, chapters } = this.props; 15 | const page = currentChapter + 1; 16 | const chaptersLen = chapters.length; 17 | if (page === chaptersLen) { 18 | return; 19 | } 20 | this.goToChapter(page); 21 | }; 22 | this.prev = () => { 23 | const { currentChapter } = this.props; 24 | if (currentChapter <= 0) { 25 | return; 26 | } 27 | this.goToChapter(currentChapter - 1); 28 | }; 29 | this.goToChapter = (nextChapter) => { 30 | this.props.dispatch({ 31 | type: 'reader/goToChapter', 32 | payload: { nextChapter }, 33 | }); 34 | }; 35 | this.goToChapters = () => { 36 | this.props.history.push('/cps'); 37 | }; 38 | } 39 | componentWillMount() { 40 | this.props.dispatch({ 41 | type: 'reader/getSource', 42 | query: this.props.match.params, 43 | }); 44 | } 45 | componentDidMount() { 46 | window.scrollTo(0, 0); 47 | } 48 | 49 | /** 50 | * 普通的menu,显示在内容底部 51 | * @returns {XML} 52 | */ 53 | optionsMenuNormal = () => { 54 | const { 55 | style, 56 | mode, 57 | dispatch, 58 | } = this.props; 59 | 60 | return (
68 | 73 | 设置 74 | 75 | 章节列表 76 | 上一章 77 | 下一章 78 |
); 79 | } 80 | 81 | /** 82 | * 悬浮的menu,点击伸缩展开 83 | * @returns {XML} 84 | */ 85 | optionsMenuFixed = () => { 86 | const { 87 | style, 88 | mode, 89 | dispatch, 90 | menuState, 91 | currentChapter, 92 | chapters, 93 | } = this.props; 94 | return (
97 |
+ 101 |
102 |
103 | 章节
列表
104 | 上一章 108 | = chapters.length - 1 ? styles.ballNextOff : styles.ballNextOn} 111 | >下一章 112 | 118 | 设置 119 | 120 |
121 |
); 122 | } 123 | /** 124 | * 点击展开关闭菜单 125 | */ 126 | changeMenu = () => { 127 | const { menuState } = this.props; 128 | this.props.dispatch({ 129 | type: 'reader/changeMenu', 130 | payload: { menuState: !menuState }, 131 | }); 132 | } 133 | 134 | render() { 135 | const { 136 | chapter = {}, 137 | detail = {}, 138 | logs = [], 139 | color = {}, 140 | style, 141 | // mode, 142 | history, 143 | // dispatch, 144 | } = this.props; 145 | return (
146 | { 147 | chapter.title ?
148 | 149 | 150 | 151 | {this.optionsMenuFixed()} 152 | 153 |
: 154 | } 155 |
); 156 | } 157 | } 158 | 159 | function mapStateToProps(state) { 160 | const { chapter, chapters, currentChapter = 0, detail, menuState } = state.reader; 161 | const { logs } = state.common; 162 | return { 163 | logs, 164 | chapter, 165 | chapters, 166 | detail, 167 | currentChapter, 168 | menuState, 169 | ...state.setting, 170 | }; 171 | } 172 | 173 | export default connect(mapStateToProps)(Search); 174 | -------------------------------------------------------------------------------- /src/routes/Reader/index.less: -------------------------------------------------------------------------------- 1 | .btnMainBase{ 2 | position: absolute; 3 | bottom: 55px; 4 | right: 10px; 5 | width: 45px; 6 | height: 45px; 7 | background-color: #ff5722; 8 | border-radius: 50%; 9 | line-height: 40px; 10 | font-size: 40px; 11 | text-align: center; 12 | color: #ffffff; 13 | overflow: hidden; 14 | padding: 0px; 15 | } 16 | 17 | .ball { 18 | color: #ffffff; 19 | font-size: 12px; 20 | width: 54px; 21 | height: 54px; 22 | display: flex; 23 | text-align: center; 24 | justify-content: center; 25 | justify-items: center; 26 | align-items: center; 27 | border-radius: 50%; 28 | background-color: #ff5722; 29 | opacity: 0.9; 30 | &first-child { 31 | line-height: 16px; 32 | } 33 | } 34 | 35 | .reader { 36 | background: #fff; 37 | color: rgba(0, 0, 0, 0.5); 38 | min-height: 100vh; 39 | a { 40 | color: rgba(0, 0, 0, 0.5); 41 | } 42 | .fixedMenu { 43 | padding: 20px; 44 | display: flex; 45 | justify-content: space-around; 46 | bottom: 0; 47 | position: fixed; 48 | height: 40px; 49 | width: 410px; 50 | .ballWrap{ 51 | height: 64px; 52 | display: flex; 53 | justify-content: space-around; 54 | width: 100%; 55 | transform: scale(1, 1); 56 | animation: navMenuAnimationOpen .5s; 57 | } 58 | .ballWrapClose{ 59 | display: flex; 60 | justify-content: space-around; 61 | width: 100%; 62 | transform: scale(0, 0); 63 | animation: navMenuAnimationClose .3s; 64 | } 65 | .ballPreOn, .ballNextOn{ 66 | &:extend(.ball); 67 | } 68 | .ballPreOff, .ballNextOff{ 69 | &:extend(.ball); 70 | background-color: #a3b5be; 71 | } 72 | .btnMain { 73 | &:extend(.btnMainBase); 74 | transform: rotate(135deg); 75 | animation: navBtnMainAnimationOpen .7s; 76 | } 77 | .btnMainClose{ 78 | &:extend(.btnMainBase); 79 | transform: rotate(0deg); 80 | animation: navBtnMainAnimationClose .5s; 81 | } 82 | } 83 | } 84 | 85 | // 菜单动画 86 | @keyframes navMenuAnimationOpen{ 87 | from{transform: scale(0, 0);} 88 | to{transform: scale(1, 1);} 89 | } 90 | @keyframes navMenuAnimationClose{ 91 | from{transform: scale(1, 1);} 92 | to{transform: scale(0, 0);} 93 | } 94 | 95 | // 展开按钮动画 96 | @keyframes navBtnMainAnimationOpen{ 97 | from{transform: rotate(0deg);} 98 | to{transform: rotate(135deg);} 99 | } 100 | @keyframes navBtnMainAnimationClose{ 101 | from{transform: rotate(135deg);} 102 | to{transform: rotate(0deg);} 103 | } 104 | 105 | @media screen and (max-width: 450px) { 106 | .reader { 107 | .fixedMenu { 108 | width: 100%; 109 | padding: 0; 110 | height: 60px; 111 | .btnMain { 112 | bottom: 65px; 113 | right: 10px; 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/routes/Reader/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/Search/Item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Item.less'; 3 | 4 | export default ({ title, cover, author, wordCount, shortIntro = '', latelyFollower }) => (
5 | 6 |
{title}
7 |

{author}

8 |

{wordCount > 10000 ? `${parseInt(wordCount / 10000, 0)} 万` : wordCount}字、{latelyFollower} 人在追

9 |

{shortIntro.length > 40 ? `${shortIntro.substring(0, 40)}...` : shortIntro}

10 |
); 11 | -------------------------------------------------------------------------------- /src/routes/Search/Item.less: -------------------------------------------------------------------------------- 1 | .item { 2 | margin: 12px 12px; 3 | height: 104px; 4 | position: relative; 5 | padding-left: 90px; 6 | border-bottom: 1px rgba(0, 0, 0, 0.1) dashed; 7 | padding-bottom: 12px; 8 | color: #333; 9 | p{ 10 | font-size: 12px; 11 | margin: 2px 0; 12 | } 13 | .author { 14 | font-weight: 500; 15 | } 16 | .meta { 17 | } 18 | } 19 | .cover { 20 | height: 100px; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/Search/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import SearchBar from '../../components/SearchBar'; 5 | import Item from './Item'; 6 | 7 | class Search extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.search = (val) => { 11 | this.props.history.push(`/search?keyword=${val}`); 12 | this.props.dispatch({ 13 | type: 'reader/search', 14 | query: { 15 | query: val, 16 | }, 17 | }); 18 | }; 19 | this.goToDetail = (id) => { 20 | this.props.history.push(`/book/${id}`); 21 | }; 22 | } 23 | 24 | componentWillMount() { 25 | const { status, list } = this.props; 26 | this.props.dispatch({ 27 | type: 'search/save', 28 | payload: { 29 | status: false, 30 | list: status ? list : [], 31 | }, 32 | }); 33 | } 34 | componentDidMount() { 35 | this.input.input.focus(); 36 | } 37 | 38 | /** 39 | * 点击搜索框取消按钮 40 | */ 41 | onClickCancle = () => { 42 | // 将搜索列表清空 43 | this.props.dispatch({ 44 | type: 'search/save', 45 | payload: { 46 | status: false, 47 | list: [], 48 | }, 49 | }); 50 | this.props.history.push('/'); 51 | window.scrollTo(0, 0); 52 | } 53 | renderList = (list, status) => { 54 | if (status && !list.length) { 55 | return (
哦噢,没找到你想要的呢 ╮( ̄▽ ̄)╭
); 56 | } 57 | return list.map(i => (
58 | 59 |
)); 60 | } 61 | render() { 62 | const { list = [], status, history } = this.props; 63 | return (
64 | { this.input = c; }} 66 | history={history} 67 | onSubmit={this.search} 68 | onClickCancle={this.onClickCancle} 69 | /> 70 | {this.renderList(list, status)} 71 |
); 72 | } 73 | } 74 | 75 | function mapStateToProps(state) { 76 | const { list, status } = state.search; 77 | return { 78 | list, 79 | status, 80 | }; 81 | } 82 | 83 | export default connect(mapStateToProps)(Search); 84 | -------------------------------------------------------------------------------- /src/routes/Search/index.less: -------------------------------------------------------------------------------- 1 | .search { 2 | padding: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /src/services/reader.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request.js'; 2 | 3 | // 获取书源 4 | export function getSource(id) { 5 | return request(`/api/toc?view=summary&book=${id}`); 6 | } 7 | 8 | // 获取章节列表 9 | export function getChapterList(id) { 10 | return request(`/api/toc/${id}?view=chapters`); 11 | } 12 | 13 | // 获取章节内容 14 | export function getChapter(link) { 15 | return request(`/chapter/${link}?k=2124b73d7e2e1945&t=1468223717`); 16 | } 17 | 18 | // 搜索书籍 19 | export function search(query) { 20 | return request(`/api/book/fuzzy-search?query=${query}&start=0&limit=10`); 21 | } 22 | 23 | // 获取书籍详细信息 24 | export function getDetail(id) { 25 | return request(`/api/book/${id}`); 26 | } 27 | -------------------------------------------------------------------------------- /src/store/effects/common.js: -------------------------------------------------------------------------------- 1 | import { put, takeLatest } from 'redux-saga/effects'; 2 | 3 | function* changeLoading({ query }) { 4 | try { 5 | yield put({ type: 'common/save', payload: { loading: query.loading || false } }); 6 | } catch (error) { 7 | console.log(error); 8 | } 9 | } 10 | 11 | export default [ 12 | takeLatest('common/loading', changeLoading), 13 | ]; 14 | -------------------------------------------------------------------------------- /src/store/effects/index.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import common from './common.js'; 3 | import reader from './reader.js'; 4 | import search from './search.js'; 5 | 6 | function * rootSaga() { 7 | yield all([ 8 | ...common, 9 | ...reader, 10 | ...search, 11 | ]); 12 | } 13 | 14 | export default rootSaga; 15 | -------------------------------------------------------------------------------- /src/store/effects/reader.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import { REHYDRATE } from 'redux-persist/constants'; 3 | import * as readerServices from '../../services/reader.js'; 4 | // import { log } from '../../utils/common.js'; 5 | 6 | // console.log = log; 7 | 8 | /** 9 | * 获取书源 10 | * @param query 11 | */ 12 | function* getSource({ query }) { 13 | try { 14 | const { id } = query; 15 | // 这里获得整个缓存中的store,并对应上reader的store。其reader的store结构参考store/reducer/reader.js initState 16 | const { reader: { id: currentId, detail: { title } } } = yield select(); 17 | if (currentId) { 18 | if (id !== currentId) { 19 | const { reader, store: { [id]: book } } = yield select(); 20 | console.log(`将《${title}》放回书架`); 21 | yield put({ type: 'store/put', payload: { ...reader }, key: currentId }); 22 | yield put({ type: 'reader/clear' }); 23 | if (book && book.detail && book.source) { 24 | console.log(`从书架取回《${book.detail.title}》`); 25 | yield put({ type: 'reader/save', payload: { ...book } }); 26 | return; 27 | } 28 | } else { 29 | return; 30 | } 31 | } 32 | let { search: { detail } } = yield select(); 33 | yield put({ type: 'common/save', payload: { loading: true } }); 34 | if (!detail._id) { 35 | console.log('详情不存在,前往获取'); 36 | detail = yield call(readerServices.getDetail, id); 37 | } 38 | const data = yield call(readerServices.getSource, id); 39 | console.log(`从网络获取《${detail.title}》`); 40 | yield put({ type: 'reader/save', payload: { source: data, id, detail } }); 41 | console.log(`阅读:${detail.title}`); 42 | yield getChapterList(); 43 | } catch (error) { 44 | console.log(error); 45 | } 46 | yield put({ type: 'common/save', payload: { loading: false } }); 47 | } 48 | 49 | /** 50 | * 章节列表 51 | */ 52 | function* getChapterList() { 53 | try { 54 | const { reader: { source, currentSource } } = yield select(); 55 | console.log('获取章节列表', currentSource, source.length, JSON.stringify(source)); 56 | if (currentSource >= source.length) { 57 | console.log('走到这里说明所有书源都已经切换完了'); 58 | yield put({ type: 'reader/save', payload: { currentSource: 0 } }); 59 | yield getChapterList(); 60 | return; 61 | } 62 | const { _id, name = '未知来源' } = source[currentSource]; 63 | console.log(`书源: ${name}`); 64 | const { chapters } = yield call(readerServices.getChapterList, _id); 65 | yield put({ type: 'reader/save', payload: { chapters } }); 66 | yield getChapter(); 67 | } catch (error) { 68 | console.log(error); 69 | } 70 | } 71 | 72 | /** 73 | * 获取章节内容 74 | */ 75 | function* getChapter() { 76 | try { 77 | const { reader: { chapters, currentChapter, 78 | downloadStatus, chaptersContent } } = yield select(); 79 | 80 | if (downloadStatus) { // 已下载直接从本地获取 81 | const chapter = chaptersContent[currentChapter || 0]; 82 | console.log(`章节: ${chapter.title}`); 83 | yield put({ type: 'reader/save', payload: { chapter } }); 84 | window.scrollTo(0, 0); 85 | } else { 86 | const { link } = chapters[currentChapter || 0]; 87 | yield put({ type: 'common/save', payload: { loading: true } }); 88 | const { chapter } = yield call(readerServices.getChapter, link); 89 | if (chapter) { 90 | console.log(`章节: ${chapter.title}`); 91 | yield put({ type: 'reader/save', payload: { chapter } }); 92 | window.scrollTo(0, 0); 93 | } else { 94 | console.log('章节获取失败'); 95 | yield getNextSource(); 96 | } 97 | } 98 | } catch (error) { 99 | console.log(error); 100 | } 101 | yield put({ type: 'common/save', payload: { loading: false } }); 102 | } 103 | 104 | /** 105 | * 跳转至章节内容 106 | * @param payload 107 | */ 108 | function* goToChapter({ payload }) { 109 | try { 110 | const { reader: { chapters } } = yield select(); 111 | const nextChapter = payload.nextChapter; 112 | if (nextChapter > chapters.length) { 113 | console.log('没有下一章啦'); 114 | return; 115 | } 116 | if (nextChapter < 0) { 117 | console.log('没有上一章啦'); 118 | return; 119 | } 120 | yield put({ type: 'reader/save', payload: { currentChapter: nextChapter } }); 121 | yield getChapter(); 122 | } catch (error) { 123 | console.log(error); 124 | } 125 | } 126 | 127 | /** 128 | * 获取下一个书源。 129 | * 在获取书源后无法获取 具体章节 便会获取下一个书源。直到所有书源换完为止 130 | */ 131 | function* getNextSource() { 132 | try { 133 | const { reader: { source, currentSource } } = yield select(); 134 | let nextSource = (currentSource || 1) + 1; 135 | console.log(`开始第${nextSource}个书源`); 136 | if (nextSource >= source.length) { 137 | console.log('没有可用书源,切换回优质书源'); 138 | nextSource = 0; 139 | } 140 | console.log(`正在尝试切换到书源: ${source[nextSource] && source[nextSource].name}`); 141 | yield put({ type: 'reader/save', payload: { currentSource: nextSource } }); 142 | yield getChapterList(); 143 | } catch (error) { 144 | console.log(error); 145 | } 146 | } 147 | 148 | /** 149 | * 本地存储调用 150 | * @param payload 151 | */ 152 | function* reStore({ payload }) { 153 | try { 154 | const { reader, store, setting } = payload; 155 | yield put({ type: 'reader/save', payload: { ...reader } }); 156 | yield put({ type: 'store/save', payload: { ...store } }); 157 | yield put({ type: 'setting/save', payload: { ...setting } }); 158 | } catch (error) { 159 | console.log(error); 160 | } 161 | } 162 | /** 163 | * 菜单伸缩 164 | * @param payload 165 | */ 166 | function* changeMenu({ payload }) { 167 | yield put({ type: 'reader/save', payload: { menuState: payload.menuState } }); 168 | } 169 | 170 | /** 171 | * 根据书籍id查询书架中(是否有该书籍 以及 是否已下载) 172 | * 173 | * @param storeId 174 | * @returns {{has: boolean, downloadStatus: boolean}} 175 | */ 176 | function* findBookByStoreId(storeId) { 177 | const { store: stores } = yield select(); 178 | for (const key in stores) { 179 | if (Object.prototype.hasOwnProperty.call(stores, key)) { // 过滤 180 | console.log('循环查找书籍,key=', stores && stores[key]); 181 | if (stores[key] && stores[key].id === storeId) { 182 | if (stores[key].downloadStatus) { // 书架中有书且已下载 183 | return { 184 | has: true, 185 | downloadStatus: true, 186 | }; 187 | } else { // 书架中有书,但是没有下载 188 | return { 189 | has: true, 190 | downloadStatus: false, 191 | }; 192 | } 193 | } 194 | } 195 | } 196 | return { 197 | has: false, 198 | downloadStatus: false, 199 | }; 200 | } 201 | /** 202 | * 离线下载书籍 获取书源 203 | * @param query 204 | */ 205 | function* downGetSource({ query }) { 206 | try { 207 | const { id, download } = query; 208 | // 这里获得整个缓存中的store,并对应上reader的store。其reader的store结构参考store/reducer/reader.js initState 209 | // 同时获取该书是否下载的状态 210 | const { reader: { id: currentId, detail: { title } } } = yield select(); 211 | console.log(`当前书信息currentId:${currentId} , id:${id}, title:${title}`); 212 | if (download) { 213 | const judgeRet = yield findBookByStoreId(id); 214 | console.log('判断返回的结果:', judgeRet); 215 | if (judgeRet.has && judgeRet.downloadStatus) { 216 | console.log('已下载,直接阅读'); 217 | yield put({ type: 'reader/save', payload: { downloadStatus: true } }); 218 | return; 219 | } 220 | 221 | yield put({ type: 'common/save', payload: { loading: true } }); 222 | let { search: { detail } } = yield select(); 223 | if (!detail._id) { 224 | console.log('下载时详情不存在,前往获取'); 225 | detail = yield call(readerServices.getDetail, id); 226 | } 227 | // 获得的所有书源 228 | const sourceList = yield call(readerServices.getSource, id); 229 | let sourceIndex = 0; // 标记书源当前脚标 230 | let chapterList = []; // 初始化可用章节列表 231 | // 循环获得一个可用的书源,达到自动换源的效果 232 | for (let i = 0, len = sourceList.length; i < len; i += 1) { 233 | if (sourceList[i].name !== '优质书源') { 234 | const { chapters } = yield call(readerServices.getChapterList, sourceList[i]._id); 235 | if (chapters.length) { 236 | const { chapter, ok } = yield call(readerServices.getChapter, chapters[i].link); 237 | if (ok && chapter) { 238 | console.log(`成功获取一个书源 index: ${sourceIndex} 章节总数 ${chapters.length}`); 239 | console.log('要下载的书源', sourceList[sourceIndex]); 240 | // 成功获取一个书源,并将相关信息先存下来 241 | yield put({ type: 'reader/save', payload: { source: sourceList, id, detail, chapters, chapter, downloadPercent: 0, currentSource: sourceIndex, currentChapter: 0 } }); 242 | chapterList = chapters; 243 | break; 244 | } 245 | } 246 | } 247 | sourceIndex += 1; 248 | } 249 | // 开始循环章节获得章节内容,并保存在本地 250 | const chaptersContent = []; // 章节列表及其内容 251 | for (let i = 0, len = chapterList.length; i < len; i += 1) { 252 | const { chapter } = yield call(readerServices.getChapter, chapterList[i].link); 253 | chaptersContent[i] = chapter; 254 | // 添加下载进度 255 | yield put({ type: 'reader/save', payload: { downloadPercent: (i / len) * 100 } }); 256 | } 257 | // 取消下载进度 258 | yield put({ type: 'reader/save', payload: { downloadPercent: 0 } }); 259 | 260 | console.log('保存的章节内容', chaptersContent); 261 | yield put({ type: 'reader/save', payload: { chaptersContent } }); 262 | 263 | // 没有下载 264 | if (!judgeRet.downloadStatus) { 265 | const { reader, store: { [id]: book }, search: { detail: searchDetail } } = yield select(); 266 | reader.downloadStatus = true; // 设定已下载 267 | console.log('将书籍存入书架'); 268 | yield put({ type: 'store/put', payload: { ...reader }, key: id }); 269 | yield put({ type: 'reader/clear' }); 270 | if (book && book.detail && book.source) { // 如果原书架中有对应的书则取出,否则用当前的书 271 | console.log(`从书架取回《${book.detail.title}》`); 272 | yield put({ type: 'reader/save', payload: { ...book } }); 273 | } else { 274 | console.log('原书架没书,用当前书'); 275 | yield put({ type: 'reader/save', payload: { ...reader } }); 276 | } 277 | searchDetail.downloadStatus = true; 278 | yield put({ type: 'search/save', payload: { searchDetail } }); 279 | } 280 | } 281 | } catch (error) { 282 | console.log(error); 283 | } 284 | yield put({ type: 'common/save', payload: { loading: false } }); 285 | } 286 | 287 | export default [ 288 | takeLatest(REHYDRATE, reStore), 289 | takeLatest('reader/downGetSource', downGetSource), 290 | takeLatest('reader/getSource', getSource), 291 | takeLatest('reader/getChapterList', getChapterList), 292 | takeLatest('reader/getChapter', getChapter), 293 | takeLatest('reader/goToChapter', goToChapter), 294 | takeLatest('reader/changeMenu', changeMenu), 295 | ]; 296 | -------------------------------------------------------------------------------- /src/store/effects/search.js: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest, select } from 'redux-saga/effects'; 2 | import * as readerServices from '../../services/reader.js'; 3 | 4 | function* search({ query }) { 5 | try { 6 | yield put({ type: 'common/save', payload: { loading: true } }); 7 | const data = yield call(readerServices.search, query.query); 8 | yield put({ type: 'search/save', payload: { list: data.books || [], status: data.ok || true } }); 9 | } catch (error) { 10 | console.log(error); 11 | } 12 | yield put({ type: 'common/save', payload: { loading: false } }); 13 | } 14 | 15 | function* getDetail({ query }) { 16 | try { 17 | const { id } = query; 18 | const { store: stores } = yield select(); 19 | if (stores[id]) { // 如果书架中有了就不去请求网络 20 | const tempDetail = stores[id].detail; 21 | tempDetail.downloadStatus = stores[id].downloadStatus; // 这里主要是做检索的书籍是否已经下载的判断 22 | yield put({ type: 'search/save', payload: { detail: tempDetail } }); 23 | } else { 24 | yield put({ type: 'common/save', payload: { loading: true } }); 25 | const data = yield call(readerServices.getDetail, id); 26 | yield put({ type: 'search/save', payload: { detail: data } }); 27 | } 28 | yield put({ type: 'reader/save', payload: { downloadPercent: 0 } }); 29 | } catch (error) { 30 | console.log(error); 31 | } 32 | yield put({ type: 'common/save', payload: { loading: false } }); 33 | } 34 | 35 | export default [ 36 | takeLatest('reader/search', search), 37 | takeLatest('reader/getDetail', getDetail), 38 | ]; 39 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { persistStore, autoRehydrate } from 'redux-persist'; 3 | import createSagaMiddleware from 'redux-saga'; 4 | 5 | import reducer from './reducer'; 6 | import effects from './effects'; 7 | 8 | const saga = createSagaMiddleware(); 9 | 10 | const store = createStore( 11 | reducer, 12 | compose( 13 | applyMiddleware( 14 | saga, 15 | ), 16 | autoRehydrate(), 17 | window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : applyMiddleware(), 18 | ), 19 | ); 20 | 21 | console.log('store', store); 22 | 23 | persistStore(store, { whitelist: ['store', 'reader', 'setting', 'common'] }); 24 | 25 | 26 | saga.run(effects); 27 | 28 | // store.dispatch({ type: 'getSource' }); 29 | 30 | export default store; 31 | -------------------------------------------------------------------------------- /src/store/reducer/common.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | logs: ['日志开启'], // 日志 3 | currentBookId: '', // 当前书籍ID 4 | loading: false, // 网络请求加载中 5 | }; 6 | 7 | function common(state = initState, action) { 8 | switch (action.type) { 9 | case 'common/save': 10 | return { 11 | ...state, 12 | ...action.payload, 13 | }; 14 | case 'common/pushLog': 15 | return { 16 | ...state, 17 | ...state.logs.push(action.payload.log), 18 | }; 19 | default: 20 | return { 21 | ...state, 22 | }; 23 | } 24 | } 25 | 26 | export default common; 27 | -------------------------------------------------------------------------------- /src/store/reducer/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import setting from './setting.js'; 4 | import common from './common.js'; 5 | import reader from './reader.js'; 6 | import search from './search.js'; 7 | import store from './store.js'; 8 | 9 | const reducer = combineReducers({ 10 | setting, 11 | common, 12 | reader, 13 | search, 14 | store, 15 | }); 16 | 17 | export default reducer; 18 | -------------------------------------------------------------------------------- /src/store/reducer/reader.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | id: null, // 当前书籍id,默认没有书籍 3 | downloadStatus: false, // 当前书籍是否被下载 true:已下载 false:未下载 4 | downloadPercent: 0, // 下载进度 5 | currentSource: 1, // 当前源下标:默认为1,跳过优质书源 6 | currentChapter: 0, // 当前章节下标 7 | source: [], // 源列表 8 | chapters: [], // 章节列表 9 | chaptersContent: [], // 章节列表并包含其内容 10 | chapter: {}, // 当前章节 11 | detail: {}, // 书籍详情 12 | menuState: false, // 底部菜单是否展开 true:展开 false:收起 13 | }; 14 | 15 | function reader(state = initState, action) { 16 | switch (action.type) { 17 | case 'reader/save': 18 | return { 19 | ...state, 20 | ...action.payload, 21 | }; 22 | case 'reader/clear': 23 | return initState; 24 | default: 25 | return { 26 | ...state, 27 | }; 28 | } 29 | } 30 | 31 | export default reader; 32 | -------------------------------------------------------------------------------- /src/store/reducer/search.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | status: false, // 获取书籍列表返回状态值 true: 检索列表不清空 false:检索列表清空 3 | list: [], // 列表 4 | detail: {}, // 书籍详情 5 | }; 6 | 7 | function search(state = initState, action) { 8 | switch (action.type) { 9 | case 'search/save': 10 | return { 11 | ...state, 12 | ...action.payload, 13 | }; 14 | default: 15 | return { 16 | ...state, 17 | }; 18 | } 19 | } 20 | 21 | export default search; 22 | -------------------------------------------------------------------------------- /src/store/reducer/setting.js: -------------------------------------------------------------------------------- 1 | const initState = { 2 | mode: 'day', // day light 3 | color: { 4 | background: '#FAF9DE', 5 | }, 6 | style: { 7 | fontSize: 20, 8 | }, 9 | }; 10 | 11 | function setting(state = initState, action) { 12 | switch (action.type) { 13 | case 'setting/save': 14 | return { 15 | ...state, 16 | ...action.payload, 17 | }; 18 | case 'setting/clear': 19 | return initState; 20 | default: 21 | return { 22 | ...state, 23 | }; 24 | } 25 | } 26 | 27 | export default setting; 28 | -------------------------------------------------------------------------------- /src/store/reducer/store.js: -------------------------------------------------------------------------------- 1 | 2 | function store(state = {}, action) { 3 | switch (action.type) { 4 | case 'store/put': { // 将书籍放入书架 5 | if (action.key) { 6 | return { 7 | ...state, 8 | [action.key]: { 9 | ...state[action.key], 10 | ...action.payload, 11 | }, 12 | }; 13 | } else { 14 | return { 15 | ...state, 16 | }; 17 | } 18 | } 19 | case 'store/save': // 初始化书架 20 | return { 21 | ...state, 22 | ...action.payload, 23 | }; 24 | case 'store/delete': // 删除书籍 25 | return { 26 | ...state, 27 | [action.key]: undefined, 28 | }; 29 | case 'store/clear': // 清空书架 30 | return {}; 31 | default: 32 | return { 33 | ...state, 34 | }; 35 | } 36 | } 37 | 38 | export default store; 39 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | import store from '../store'; 2 | 3 | console.oldLog = console.log; 4 | 5 | export function log(str) { 6 | console.oldLog(str); 7 | if (typeof (str) === 'string' && store) { 8 | store.dispatch({ 9 | type: 'common/pushLog', 10 | payload: { 11 | log: str, 12 | }, 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const COLORS = [ 2 | { 3 | background: '#b6b6b6', 4 | }, { 5 | background: '#999484', 6 | }, { 7 | background: '#a0b89c', 8 | }, { 9 | background: '#cec0a4', 10 | }, { 11 | background: '#d5b2be', 12 | }, { 13 | color: 'rgba(255,255,255,0.8)', 14 | background: '#011721', 15 | }, { 16 | color: 'rgba(255,255,255,0.7)', 17 | background: '#2c2926', 18 | }, { 19 | background: '#c4ada4', 20 | }, 21 | ]; 22 | 23 | export const MODE_COLOR = { 24 | day: { 25 | background: '#FAF9DE', 26 | }, 27 | night: { 28 | color: 'rgba(255,255,255,0.5)', 29 | background: '#000', 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/recommond.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { _id: '50865988d7a545903b000009', author: '天蚕土豆', cover: '/agent/http%3A%2F%2Fimg.1391.com%2Fapi%2Fv1%2Fbookcenter%2Fcover%2F1%2F41615%2F_41615_067553.jpg%2F', creater: 'iPhone 4S', longIntro: '这里是属于斗气的世界,没有花俏艳丽的魔法,有的,仅仅是繁衍到巅峰的斗气!\r\n新书等级制度:斗者,斗师,大斗师,斗灵,斗王,斗皇,斗宗,斗尊,斗圣,斗帝。\r\n恳请推荐票支持,各位兄弟看完之后,请顺手丢上几张推荐票吧,土豆谢谢!!', title: '斗破苍穹', majorCate: '玄幻', minorCate: '东方玄幻', rating: { count: 7458, score: 9.481, isEffect: true }, sizetype: -1, superscript: '', currency: 0, contentType: 'txt', _le: false, allowMonthly: false, allowVoucher: true, allowBeanVoucher: false, hasCp: true, postCount: 9627, latelyFollower: 45768, followerCount: 21791, wordCount: 5415899, serializeWordCount: -30502, retentionRatio: '54.55', updated: '2017-05-27T08:48:00.139Z', isSerial: false, chaptersCount: 1648, lastChapter: '第一章 五帝破空', gender: ['male'], tags: ['升级练功', '玄幻', ' 巅峰', '奇遇', '热血', '架空', '斗气', '异界大陆', '巅峰', '东方玄幻'], cat: '东方玄幻', donate: false, copyright: '阅文集团正版授权', _gg: false, discount: null }, 3 | { _id: '50c54ad08380e4f81500002a', author: '刘慈欣', cover: '/agent/http%3A%2F%2Fimg.1391.com%2Fapi%2Fv1%2Fbookcenter%2Fcover%2F1%2F41894%2F_41894_371160.jpg%2F', creater: 'iPhone 5', longIntro: '文化大革命如火如荼进行的同时。军方探寻外星文明的绝秘计划“红岸工程”取得了突破性进展。但在按下发射键的那一刻,历经劫难的叶文洁没有意识到,她彻底改变了人类的命运。地球文明向宇宙发出的第一声啼鸣,以太阳为中心,以光速向宇宙深处飞驰……\n四光年外,“三体文明”正苦苦挣扎——三颗无规则运行的太阳主导下的百余次毁灭与重生逼迫他们逃离母星。而恰在此时。他们接收到了地球发来的信息。在运用超技术锁死地球人的基础科学之后。三体人庞大的宇宙舰队开始向地球进发……\n人类的末日悄然来临。', title: '三体', majorCate: '科幻', minorCate: '未来世界', sizetype: -1, superscript: '', currency: 0, contentType: 'txt', _le: false, allowMonthly: false, allowVoucher: true, allowBeanVoucher: false, hasCp: true, postCount: 449, latelyFollower: 2058, followerCount: 2185, wordCount: 186000, serializeWordCount: 0, retentionRatio: '15.06', updated: '2016-03-22T03:47:51.133Z', isSerial: false, chaptersCount: 36, lastChapter: '后记', gender: ['male'], tags: ['刘慈欣', '末日', '星际', '幻想', '危机', '未来', '科幻', '畅销', '三体', '中国'], cat: '未来世界', donate: false, _gg: false, discount: null }, 4 | { _id: '50c02f6b1370dd9055000001', author: '本物天下霸唱', cover: '/agent/http://image.cmfu.com/books/53269/53269.jpg', creater: 'iPhone 4', longIntro: '远古的文明,失落的宝藏,神秘莫测的古墓。\r\n一本主人公家中传下来的秘书残卷为引,三位当代摸金校尉,在离奇诡异的地下世界中,揭开一层层远古的神秘面纱。\r\n昆仑山大冰川下的九层妖楼,这里究竟是什么?藏着消失的古代魔国君王陵寝,诡异殉葬沟,神秘云母,地下九层“金”字高塔,堆满奇特古装的干枯骨骸。这里又有那些未知生物?漫天带火瓢虫袭来,巨大神秘爬行怪物。一群人险死求生!', title: '鬼吹灯', cat: '悬疑探险', majorCate: '灵异', minorCate: '悬疑探险', sizetype: -1, superscript: '', currency: 0, contentType: 'txt', _le: false, allowMonthly: false, allowVoucher: true, allowBeanVoucher: false, hasCp: true, postCount: 436, latelyFollower: 18167, followerCount: 5128, wordCount: 931567, serializeWordCount: 0, retentionRatio: '37.84', updated: '2016-09-12T17:57:26.396Z', isSerial: false, chaptersCount: 242, lastChapter: '234 由眼而生由眼而亡', gender: ['male'], tags: ['悬疑', '鬼吹灯', '盗墓', '现代', '热血', '探险', '恐怖', '原创灵异'], donate: false, _gg: false, discount: null }, 5 | ]; 6 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import 'fetch-polyfill'; 2 | 3 | export default function request(url, options) { 4 | return fetch(url, options) 5 | .then((response) => { 6 | if (response.status >= 200 && response.status < 300) { 7 | return response; 8 | } 9 | const error = new Error(response.statusText); 10 | error.response = response; 11 | throw error; 12 | }) 13 | .then((data) => { 14 | return data.json(); 15 | }) 16 | .catch(err => ({ err })); 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | // const manifest = require('./dll/vendors-manifest.json'); 7 | 8 | module.exports = () => { 9 | return { 10 | entry: { 11 | index: './src/index.js', 12 | }, 13 | output: { 14 | path: resolve(__dirname, 'dist'), 15 | filename: '[name].js', 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(js|jsx)$/, 21 | exclude: /node_modules/, 22 | use: ['babel-loader', 'eslint-loader'], 23 | }, 24 | { 25 | test: /favicon.(png|ico)$/, 26 | use: [ 27 | { 28 | loader: 'file-loader', 29 | options: { 30 | name: '[name].[ext]?[hash]', 31 | }, 32 | }, 33 | ], 34 | }, 35 | { 36 | test: /\.css$/, 37 | use: ExtractTextPlugin.extract({ 38 | fallback: 'style-loader', 39 | use: ['css-loader', 'postcss-loader'], 40 | }), 41 | }, 42 | { 43 | test: /\.less$/, 44 | use: ExtractTextPlugin.extract({ 45 | fallback: 'style-loader', 46 | use: ['css-loader?modules', 'less-loader', 'postcss-loader'], 47 | }), 48 | }, 49 | { 50 | test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, 51 | exclude: /favicon.png$/, 52 | use: [ 53 | { 54 | loader: 'url-loader', 55 | options: { 56 | limit: 10000, 57 | }, 58 | }, 59 | ], 60 | }, 61 | ], 62 | }, 63 | // devtool: 'eval', 64 | // devtool: 'cheap-source-map', 65 | resolve: { 66 | alias: { 67 | react: 'preact-compat', 68 | 'react-dom': 'preact-compat', 69 | 'preact-compat': 'preact-compat/dist/preact-compat', 70 | }, 71 | }, 72 | plugins: [ 73 | new ExtractTextPlugin('index.css'), // 单独打包css 74 | new webpack.DefinePlugin({ 75 | 'process.env.NODE_ENV': '"production"', 76 | }), 77 | new CopyWebpackPlugin([ 78 | // { 79 | // from: './dll/vendors.dll.js', 80 | // to: 'dll.js', 81 | // }, 82 | { 83 | from: './public/**/*', 84 | to: '[name].[ext]', 85 | }, 86 | ], { 87 | ignore: ['index.html', 'index.dev.html'], 88 | copyUnmodified: true, 89 | debug: 'debug', 90 | }), 91 | new HtmlWebpackPlugin({ 92 | template: './public/index.html', 93 | hash: true, 94 | minify: { 95 | removeAttributeQuotes: true, 96 | collapseWhitespace: true, 97 | collapseInlineTagWhitespace: true, 98 | }, 99 | }), 100 | // new webpack.HotModuleReplacementPlugin(), // enable HMR globally 101 | // new webpack.NoEmitOnErrorsPlugin(), // 遇到错误继续 102 | // new webpack.NamedModulesPlugin(), // prints more readable module names 103 | // new CopyWebpackPlugin([{ from: './dll/vendors.dll.js', to: 'dll.js' }]), 104 | // new webpack.DllReferencePlugin({ context: __dirname, manifest }), 105 | // new webpack.optimize.ModuleConcatenationPlugin(), // 模块串联,大幅减少包大小257k =》239k 106 | new webpack.optimize.UglifyJsPlugin({ 107 | beautify: false, // 最紧凑的输出 108 | comments: false, // 删除所有的注释 109 | compress: { 110 | warnings: false, // 在UglifyJs删除没有用到的代码时不输出警告 111 | // support_ie8: false, // 还可以兼容ie浏览器 112 | drop_console: true, // 删除所有的 `console` 语句 113 | collapse_vars: true, // 内嵌定义了但是只用到一次的变量 114 | reduce_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 115 | }, 116 | }), 117 | ], 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const manifest = require('./dll/vendors-manifest.json'); 6 | 7 | module.exports = () => { 8 | return { 9 | entry: { 10 | index: [ 11 | 'react-hot-loader/patch', 12 | 'webpack-dev-server/client?http://localhost:8000', 13 | 'webpack/hot/only-dev-server', 14 | './src/index.dev.js', 15 | ], 16 | }, 17 | output: { 18 | path: resolve(__dirname, 'dist'), 19 | filename: '[name].js', 20 | publicPath: '/', // 同output的publicPath 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(js|jsx)$/, 26 | exclude: /node_modules/, 27 | use: ['react-hot-loader/webpack', 'babel-loader', 'eslint-loader'], 28 | }, 29 | { 30 | test: /favicon.(png|ico)$/, 31 | use: [ 32 | { 33 | loader: 'file-loader', 34 | options: { 35 | name: '[name].[ext]?[hash]', 36 | }, 37 | }, 38 | ], 39 | }, 40 | { 41 | test: /\.css$/, 42 | use: ['style-loader', 'css-loader', 'postcss-loader'], 43 | }, 44 | { 45 | test: /\.less$/, 46 | use: ['style-loader', 'css-loader?modules&localIdentName=[name]__[local]___[hash:base64:5]', 'less-loader', 'postcss-loader'], 47 | }, 48 | { 49 | test: /\.scss$/, 50 | use: ['style-loader', 'css-loader?modules', 'scss-loader', 'postcss-loader'], 51 | }, 52 | { 53 | test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, 54 | exclude: /favicon.png$/, 55 | use: [ 56 | { 57 | loader: 'url-loader', 58 | options: { 59 | limit: 10000, 60 | }, 61 | }, 62 | ], 63 | }, 64 | ], 65 | }, 66 | // devtool: 'eval', 67 | // resolve: { 68 | // alias: { 69 | // react: 'preact-compat', 70 | // 'react-dom': 'preact-compat', 71 | // 'preact-compat': 'preact-compat/dist/preact-compat', 72 | // }, 73 | // }, 74 | plugins: [ 75 | new webpack.DefinePlugin({ 76 | 'process.env.NODE_ENV': '"development"', 77 | }), 78 | new CopyWebpackPlugin([{ from: './dll/vendors.dll.js', to: 'dll.js' }]), 79 | new HtmlWebpackPlugin({ template: './public/index.dev.html' }), 80 | // new webpack.HotModuleReplacementPlugin(), // enable HMR globally 81 | new webpack.NoEmitOnErrorsPlugin(), // 遇到错误继续 82 | new webpack.NamedModulesPlugin(), // prints more readable module names 83 | new webpack.DllReferencePlugin({ context: __dirname, manifest }), 84 | ], 85 | devServer: { 86 | port: 8000, 87 | host: '0.0.0.0', 88 | historyApiFallback: true, 89 | disableHostCheck: true, 90 | contentBase: '/', // 配置服务器目录 91 | publicPath: '/', // 同output的publicPath 92 | proxy: { 93 | '/api': { 94 | target: 'http://api.zhuishushenqi.com/', 95 | changeOrigin: true, 96 | pathRewrite: { '^/api': '' }, 97 | }, 98 | '/chapter': { 99 | target: 'http://chapter2.zhuishushenqi.com/', 100 | changeOrigin: true, 101 | pathRewrite: { '^/api': '' }, 102 | }, 103 | '/agent': { 104 | target: 'http://statics.zhuishushenqi.com/', 105 | changeOrigin: true, 106 | pathRewrite: { '^/api': '' }, 107 | }, 108 | }, 109 | }, 110 | // performance: { 111 | // hints: options.dev ? false : 'warning', 112 | // }, 113 | 114 | }; 115 | }; 116 | -------------------------------------------------------------------------------- /webpack.dll.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | const library = '[name]_lib'; 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | entry: { 8 | vendors: [ 9 | 'react', 10 | 'react-dom', 11 | // 'preact', 12 | // 'preact-compat', 13 | 'react-redux', 14 | 'react-router', 15 | 'react-router-dom', 16 | 'redux', 17 | 'redux-saga', 18 | ], 19 | }, 20 | // resolve: { 21 | // alias: { 22 | // react: 'preact-compat', 23 | // 'react-dom': 'preact-compat', 24 | // 'preact-compat': 'preact-compat/dist/preact-compat', 25 | // }, 26 | // }, 27 | output: { 28 | filename: '[name].dll.js', 29 | path: path.resolve(__dirname, 'dll'), 30 | library, 31 | }, 32 | plugins: [ 33 | new webpack.DefinePlugin({ 34 | 'process.env': { 35 | NODE_ENV: JSON.stringify('production'), 36 | }, 37 | }), 38 | new webpack.DllPlugin({ 39 | path: path.join(__dirname, 'dll/[name]-manifest.json'), 40 | context: __dirname, 41 | name: library, 42 | }), 43 | // new webpack.optimize.ModuleConcatenationPlugin(), // 模块串联,大幅减少包大小257k =》239k 44 | new webpack.optimize.UglifyJsPlugin({ 45 | compress: { 46 | warnings: false, 47 | }, 48 | mangle: { 49 | except: ['exports', 'require'], 50 | }, 51 | }), 52 | ], 53 | }; 54 | --------------------------------------------------------------------------------