├── .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 | 
4 |
5 | 在线地址:[http://myreader.linxins.com](http://myreader.linxins.com)
6 |
7 | 手机扫码体验:
8 |
9 | 
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 | 
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 | 
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 | 
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 |