├── .gitignore ├── README.md ├── dist └── kabi-novel.html ├── gulpfile.js ├── package.json ├── pnpm-lock.yaml ├── src ├── article │ └── article.ts ├── bookshelf │ └── bookshelf.ts ├── catalogue │ └── catalogue.ts ├── common │ ├── api │ │ └── api.ts │ ├── bar │ │ ├── bar.less │ │ └── bar.ts │ ├── bind │ │ └── bind.ts │ ├── common.ts │ ├── debugger │ │ └── debugger.ts │ ├── layout │ │ └── layout.ts │ ├── message │ │ ├── message.less │ │ └── message.ts │ ├── modal │ │ ├── modal.less │ │ └── modal.ts │ ├── pagination │ │ ├── pagination.less │ │ └── pagination.ts │ ├── router │ │ └── router.ts │ └── store │ │ └── store.ts ├── config │ └── config.ts ├── global.d.ts ├── index.html ├── main.ts └── style.less ├── temp ├── bundle.js ├── kabi-novel.html └── style.css └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | node_modules/ 3 | **/node_modules 4 | **/node_modules/ 5 | 6 | .pnpm-debug.log 7 | .vscode/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 阅读APP网页版的kindle适配 3 | 也相当于写了一套旧内核的简易开发框架,看上的可以搬去写点旧设备的简单页面 4 | 为爱发电 5 | 回馈社区 6 | 如有bug 7 | 多多包涵 8 | 9 | # 困难 10 | kindle浏览器内核老旧,许多新东西用不了 11 | 个人kindle缓存限制大小为2600000字节,约2.48M 12 | 不支持元素滚动条 13 | 不支持Multiple-column多列布局 14 | 不支持calc样式计算 15 | 性能低下,大量数据处理时卡顿 16 | kindle浏览器自带无法取消的滚动条 17 | kindle浏览器输入以及双击会强制缩放 18 | 未进行逐字排版,难以计算进度 19 | 滚动条分页+自定义行距字号易断字 20 | github被墙,没有免费的 21 | 22 | # 解决的问题 23 | 放弃滚动翻页,尝试点击切屏 24 | 放弃css自动分页,尝试滚动条分页 25 | 骚操作排版防止断字 26 | 骚操作计算进度,与app同步进度 27 | 目录改为虚拟分页,提高页面性能 28 | 29 | # 使用方法 30 | 31 | ## 在线试用 32 | 试用渠道(资源有限仅供检测设备可用性,不稳定请轻薅): 33 | [github pages试用渠道](https://cyx7788414.github.io/kabi-novel.html) 34 | 试用成功后,有能力的朋友,请自行部署kabi-novel.html文件至个人域名或本地内网以免丢失缓存 35 | 如果有大佬开放了免费公众服务,可以与我联系更新文档 36 | ### 优点 37 | 不会丢失缓存 38 | ### 缺点 39 | 初始化时需要联网 40 | 41 | ## 静态使用 42 | 将dist目录中的kabi-novel.html拷入kindle根目录 43 | 启动kindle浏览器,地址栏输入file:///mnt/us/kabi-novel.html 44 | 可自行更名,添加书签 45 | ### 优点 46 | 无需外网,可内网使用 47 | ### 缺点 48 | 每次关闭浏览器后缓存丢失,下次使用需要重新配置 49 | 50 | ## 使用流程 51 | 初始化时在配置页输入服务端地址,并进行校验 52 | 在配置页检测缓存容量,以获得容量告警 53 | 在配置页调整字号行距(仅对小说正文有效) 54 | 进入书架,初始化时需手动点击刷新才有数据(为了避免自动刷新误删本地缓存) 55 | 点击书籍,开始阅读 56 | 点击目录,可跳章,管理本书缓存 57 | 58 | ## 说明 59 | 保持与服务端联网,可以同步进度 60 | 离线时进度保存在本地,再次联网刷新书架时,本地与服务端之间进度根据时间戳匹配,取其新者 61 | 每次翻页/跳章时尝试联网保存进度 62 | 63 | # 仍然存在的问题 64 | 配置页输入服务端地址时仍然会缩放,需要手动恢复 65 | 快速点击多次仍然会触发双击自动缩放,需要手动恢复 66 | 配置页检测缓存容量仍然卡顿 67 | 无法取消的右侧滚动条 68 | 69 | -------------------------------------------------------------------------------- /dist/kabi-novel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | kabi novel 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
配置
31 |
刷新
32 |
33 |
34 |
<
35 |
>
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
书架
48 |
目录
49 |
刷新
50 |
51 |
52 |
<
53 |
>
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
书架
66 |
文章
67 |
刷新
68 |
缓存
69 |
70 |
71 |
<
72 |
>
73 |
74 |
75 |
76 |
77 |
78 |
79 |
服务器地址
80 |
81 | 82 |
测试
83 |
84 |
85 |
86 |
缓存容量
87 |
88 | - 89 | / 90 | - 91 | 92 |
检测
93 |
94 |
95 |
96 |
字号
97 |
98 | 16 99 | px 100 |
重置
101 |
-
102 |
+
103 |
104 |
105 |
106 |
行距
107 |
108 | 20 109 | px 110 |
重置
111 |
-
112 |
+
113 |
114 |
115 |
116 |
效果展示
117 |
118 |

测试文本

119 |
120 |
121 |
122 |
123 |
124 |
书架
125 |
126 |
127 | 128 |
129 |
130 |
131 | 132 | 133 |
134 | 135 | 136 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { dest, series, src, parallel, watch } = require("gulp"); 2 | const { createProject } = require("gulp-typescript"); 3 | const browserify = require("browserify"); 4 | const tsify = require("tsify"); 5 | const source = require('vinyl-source-stream'); 6 | const inject = require('gulp-inject'); 7 | const clean = require('gulp-clean'); 8 | const less = require('gulp-less'); 9 | const cssmin = require('gulp-minify-css'); 10 | const uglify = require('gulp-uglify'); 11 | const buffer = require('vinyl-buffer'); 12 | const rename = require('gulp-rename'); 13 | 14 | let debug = false; 15 | 16 | function cleanDir(cb) { 17 | return src(debug?['temp/*']:['dist/*', 'temp/*']).pipe(clean()); 18 | } 19 | 20 | function buildLess(cb) { 21 | return src('src/style.less') 22 | .pipe(less()) 23 | .pipe(cssmin()) 24 | .pipe(dest('temp')); 25 | } 26 | 27 | function buildTs(cb) { 28 | let a = browserify({ 29 | basedir: '.', 30 | debug: debug, 31 | entries: ['src/main.ts'], 32 | cache: {}, 33 | packageCache: {} 34 | }) 35 | .plugin(tsify) 36 | .bundle() 37 | .pipe(source('bundle.js')) 38 | .pipe(buffer()); 39 | if (!debug) { 40 | a = a.pipe(uglify()); 41 | } 42 | // .pipe(uglify()) 43 | return a.pipe(dest("temp")); 44 | } 45 | 46 | function collect(cb) { 47 | var target = src('./src/index.html'); 48 | var sources = src(['./temp/**/*.js', './temp/**/*.css'], { read: true }); 49 | 50 | return target.pipe(inject(sources, { 51 | transform: function (filePath, file) { 52 | if (filePath.slice(-3) === '.js') { 53 | return ''; 54 | } 55 | if (filePath.slice(-4) === '.css') { 56 | return ''; 57 | } 58 | return file.contents.toString('utf8'); 59 | } 60 | })) 61 | .pipe(rename('kabi-novel.html')) 62 | .pipe(dest(debug?'temp':'dist')); 63 | } 64 | 65 | const task = series(cleanDir, parallel(buildTs, buildLess), collect); 66 | 67 | function setFlag(cb) { 68 | debug = true; 69 | watch('src', task); 70 | task(); 71 | cb(); 72 | } 73 | 74 | exports.default = task; 75 | exports.debug = setFlag; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kabi-novel", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "gulp", 7 | "debug": "gulp debug", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.0.0", 15 | "babel-core": "^6.26.3", 16 | "babel-preset-es2015": "^6.24.1", 17 | "babelify": "^10.0.0", 18 | "browserify": "^17.0.0", 19 | "gulp": "^4.0.2", 20 | "gulp-clean": "^0.4.0", 21 | "gulp-inject": "^5.0.5", 22 | "gulp-less": "^5.0.0", 23 | "gulp-minify-css": "^1.2.4", 24 | "gulp-rename": "^2.0.0", 25 | "gulp-sourcemaps": "^3.0.0", 26 | "gulp-typescript": "6.0.0-alpha.1", 27 | "gulp-uglify": "^3.0.2", 28 | "gulp-util": "^3.0.8", 29 | "less": "^4.1.3", 30 | "tsify": "^5.0.4", 31 | "typescript": "^4.9.5", 32 | "vinyl-buffer": "^1.0.1", 33 | "vinyl-source-stream": "^2.0.0", 34 | "watchify": "^4.0.0" 35 | }, 36 | "dependencies": { 37 | "lz-string": "^1.5.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/article/article.ts: -------------------------------------------------------------------------------- 1 | import Catalogue from "../catalogue/catalogue"; 2 | import Bar from "../common/bar/bar"; 3 | import { Book, CatalogueItem, changeValueWithNewObj, Progress } from "../common/common"; 4 | import Pagination from "../common/pagination/pagination"; 5 | 6 | class Article { 7 | element: HTMLElement; 8 | bar: Bar; 9 | pagination: Pagination; 10 | 11 | currentBook: Book; 12 | 13 | progress: Progress; 14 | 15 | catalogue: CatalogueItem[] = []; 16 | 17 | content: string; 18 | 19 | loading: boolean = false; 20 | 21 | constructor() { 22 | this.element = document.querySelector('.page.article'); 23 | 24 | this.pagination = new Pagination({ 25 | root: this.element.querySelector('.content') 26 | }); 27 | 28 | this.bar = new Bar({ 29 | element: this.element.querySelector('.bar'), 30 | pagination: this.pagination 31 | }); 32 | 33 | window.Bind.bindView(this.element.querySelector('.content-inner'), this, 'content', (content: string) => { 34 | if (!content) { 35 | return ''; 36 | } 37 | 38 | let html = ` 39 | 41 | `; 42 | content.split('\n').map(v => { 43 | return v.trim() 44 | }).filter(v => !!v).forEach(v => { 45 | html += ` 46 |

${v}

47 | `; 48 | }); 49 | window.setTimeout(() => { 50 | this.pagination.checkPage(); 51 | this.setPageByProgress(); 52 | }); 53 | return html; 54 | }); 55 | 56 | window.Bind.bind(this, 'progress', (newV: any, oldV: any) => { 57 | if (!oldV) { 58 | return; 59 | } 60 | window.Store.setObj(`p_${this.currentBook.id}`, newV); 61 | if (this.progress.pos > this.content.length) { 62 | return; 63 | } 64 | window.Api.saveProgress(this.currentBook, this.progress); 65 | }); 66 | 67 | const current: HTMLElement = this.element.querySelector('.current-info'); 68 | const changeInfo = () => { 69 | return `${this.currentBook?.name} - ${this.currentBook?.author} - ${this.progress?.title}`; 70 | }; 71 | window.Bind.bindView(current, this, 'currentBook', changeInfo); 72 | window.Bind.bindView(current, this, 'progress', changeInfo); 73 | window.Bind.bindView(current, this, 'catalogue', changeInfo); 74 | 75 | let content: HTMLElement = this.element.querySelector('.content'); 76 | let contentInner: HTMLElement = content.querySelector('.content-inner'); 77 | window.Bind.bindStyle(contentInner, window.Layout, 'fontSize', 'fontSize', (v: any) => `${v}px`); 78 | window.Bind.bindStyle(contentInner, window.Layout, 'lineHeight', 'lineHeight', (v: any) => `${v}px`); 79 | window.Bind.bindStyle(content, window.Layout, 'lineHeight', 'height', (v: any) => { 80 | if (!this.element.offsetHeight) { 81 | return ''; 82 | } 83 | let base = this.element.offsetHeight - 230 - 20; 84 | let oo = base % window.Layout.lineHeight; 85 | if (oo < 10) { 86 | oo += window.Layout.lineHeight; 87 | } 88 | let height = base - oo + 20; 89 | current.style.height = `${oo}px`; 90 | current.style.lineHeight = `${oo}px`; 91 | window.setTimeout(() => this.pagination.checkPage()); 92 | return `${height}px`; 93 | }); 94 | 95 | let func = () => { 96 | let current = window.Store.get('current'); 97 | this.currentBook = window.BookShelf.bookMap[current]; 98 | if (!this.currentBook) { 99 | if (window.Router.current === 'article') { 100 | window.Router.go('bookshelf'); 101 | } 102 | return; 103 | } 104 | 105 | window.Layout.lineHeight = window.Layout.lineHeight; 106 | 107 | this.catalogue = window.Store.getObj(`c_${current}`) || []; 108 | 109 | this.progress = window.Store.getObj(`p_${this.currentBook.id}`); 110 | 111 | this.getContent(); 112 | }; 113 | 114 | window.Router.cbMap.article = func; 115 | func(); 116 | } 117 | 118 | getContent(): void { 119 | this.content = window.Store.get(`a_${this.currentBook.id}_${this.progress.index}`) || ''; 120 | let cb = () => { 121 | window.setTimeout(() => { 122 | window.Catalogue?.doCache(5); 123 | }); 124 | }; 125 | if (!this.content) { 126 | this.getArticle(cb); 127 | } else { 128 | cb(); 129 | } 130 | } 131 | 132 | pageChange(num: 1 | -1): void { 133 | let target = this.pagination.pageIndex + num; 134 | if (target < 0 || target >= this.pagination.pageLimit) { 135 | let index = this.progress.index + num; 136 | let pos = num === -1?999999999999:0;// to the end 137 | this.progress = changeValueWithNewObj(this.progress, {index: index, title: this.catalogue[index].title, time: new Date().getTime(), pos: pos}); 138 | this.getContent(); 139 | } else { 140 | this.pagination.setPage(target); 141 | this.getPagePos(target); 142 | } 143 | } 144 | 145 | getPagePos(target: number): void { 146 | let top = target * this.pagination.pageStep; 147 | let ps = this.element.querySelectorAll('.content-inner p'); 148 | let str = ''; 149 | for (let i = 0; i < ps.length; i++) { 150 | if ((ps[i] as HTMLElement).offsetTop >= top) { 151 | str = ps[i].innerHTML; 152 | break; 153 | } 154 | } 155 | let pos = this.content.indexOf(str); 156 | this.progress = changeValueWithNewObj(this.progress, {time: new Date().getTime(), pos: pos}); 157 | } 158 | 159 | setPageByProgress(): void { 160 | let target = this.content.slice(0, this.progress.pos).split('\n').length - 1; 161 | let ele: HTMLElement = this.element.querySelectorAll('.content-inner p')[target] as HTMLElement; 162 | let top = ele.offsetTop; 163 | let index = Math.floor(top / this.pagination.pageStep); 164 | this.pagination.setPage(index); 165 | if (this.progress.pos > this.content.length) {//reset to right 166 | this.getPagePos(index); 167 | } 168 | } 169 | 170 | getArticle(cb?: Function): void { 171 | if (this.loading === true) { 172 | window.Message.add({content: '正在加载章节内容'}); 173 | return; 174 | } 175 | this.loading = true; 176 | window.Api.getArticle(this.currentBook.source, this.progress.index, { 177 | success: (res: any) => { 178 | this.loading = false; 179 | this.content = res.data; 180 | window.Store.set(`a_${this.currentBook.id}_${this.progress.index}`, this.content); 181 | cb && cb(); 182 | }, 183 | error: (err: any) => { 184 | this.loading = false; 185 | } 186 | }); 187 | } 188 | }; 189 | 190 | export default Article; -------------------------------------------------------------------------------- /src/bookshelf/bookshelf.ts: -------------------------------------------------------------------------------- 1 | import Bar from '../common/bar/bar'; 2 | import { Book, getObject, getSpecialParent, Progress } from '../common/common'; 3 | import Pagination from '../common/pagination/pagination'; 4 | 5 | class BookShelf { 6 | element: HTMLElement; 7 | bar: Bar; 8 | pagination: Pagination; 9 | 10 | bookMap: {[key: string]: Book} = {}; 11 | bookList: Book[] = []; 12 | 13 | 14 | loading: boolean = false; 15 | 16 | pageHeight: number; 17 | 18 | constructor() { 19 | this.element = document.querySelector('.page.bookshelf'); 20 | 21 | this.pagination = new Pagination({ 22 | root: this.element.querySelector('.content') 23 | }); 24 | 25 | this.bar = new Bar({ 26 | element: this.element.querySelector('.bar'), 27 | pagination: this.pagination 28 | }); 29 | 30 | this.bookList = window.Store.getObj('bookshelf') || []; 31 | // this.bookList = window.Store.getByHead('b_').map(v => JSON.parse(window.Store.get(v) || ''));//wait 32 | 33 | window.Bind.bindView(this.element.querySelector('.book-list'), this, 'bookList', (bookList: Book[], oldV: Book[] = []) => { 34 | this.compareBookList(bookList, oldV); 35 | let height = (this.element.querySelector('.pagination-box') as HTMLElement).offsetHeight / 4; 36 | let imgWidth = height * 3 / 4; 37 | let width = Math.floor((this.element.querySelector('.book-list') as HTMLElement).offsetWidth / 2); 38 | let html = ` 39 | 44 | `; 45 | bookList.forEach(book => { 46 | let date = new Date(book.latestChapterTime); 47 | let progress: Progress = window.Store.getObj(`p_${book.id}`); 48 | html += ` 49 |
50 |
51 | ${book.name} 52 |
53 |
54 |
${book.name}
55 |
${book.author}
56 |
${progress.title}
57 |
${book.latestChapterTitle}
58 |
更新时间:${date.getFullYear()}-${date.getMonth() + 1}-${date.getDay()}
59 |
60 |
61 | `; 62 | }); 63 | window.setTimeout(() => { 64 | this.pagination.checkPage(); 65 | }); 66 | return html; 67 | }); 68 | 69 | window.Router.cbMap.bookshelf = () => { 70 | this.bookList = [].concat(this.bookList); 71 | }; 72 | 73 | } 74 | 75 | bookDelete(book: Book, onlySource?: boolean): void { 76 | if (!onlySource) { 77 | window.Store.del(`p_${book.id}`); 78 | } 79 | window.Store.del(`c_${book.id}`); 80 | window.Store.getByHead(`a_${book.id}`).forEach(v => window.Store.del(v)); 81 | } 82 | 83 | compareBookList(newV: Book[], oldV: Book[]): void { 84 | let oldMap = this.bookMap; 85 | this.bookMap = {}; 86 | newV.forEach(book => { 87 | this.bookMap[book.id] = book; 88 | if (oldMap[book.id]) { 89 | if (book.source !== oldMap[book.id].source) { 90 | this.bookDelete(oldMap[book.id], true); 91 | } 92 | delete oldMap[book.id]; 93 | } 94 | }); 95 | Object.keys(oldMap).forEach((id: string) => { 96 | this.bookDelete(oldMap[id]); 97 | }); 98 | } 99 | 100 | getBookShelf(): void { 101 | if (this.loading === true) { 102 | window.Message.add({content: '正在加载书架数据'}); 103 | return; 104 | } 105 | this.loading = true; 106 | window.Api.getBookshelf({ 107 | success: (res: any) => { 108 | this.loading = false; 109 | let bookList: Book[] = res.data.map((book: any) => { 110 | let id = window.Store.compress(`${book.name}_${book.author}`); 111 | let keys: string[] = ['name', 'author', 'coverUrl', 'customCoverUrl', 'latestChapterTime', 'latestChapterTitle']; 112 | let pobj: Progress = getObject(book, [], { 113 | index: book.durChapterIndex, 114 | pos: book.durChapterPos, 115 | time: new Date(book.durChapterTime).getTime(), 116 | title: book.durChapterTitle 117 | }); 118 | let old = window.Store.getObj(`p_${id}`); 119 | if (!old || old.time < pobj.time) { 120 | window.Store.setObj(`p_${id}`, pobj); 121 | } 122 | return getObject(book, keys, { 123 | id: id, 124 | source: book.bookUrl 125 | }); 126 | }); 127 | this.bookList = [].concat(bookList); 128 | window.Store.setObj('bookshelf', this.bookList); 129 | }, 130 | error: (err: any) => { 131 | this.loading = false; 132 | } 133 | }); 134 | } 135 | 136 | clickItem(event: Event): void { 137 | let item = getSpecialParent((event.target || event.srcElement) as HTMLElement, (ele: HTMLElement) => { 138 | return ele.classList.contains('book-item'); 139 | }); 140 | let id = item.getAttribute('key'); 141 | window.Store.set('current', id); 142 | window.Router.go('article'); 143 | } 144 | }; 145 | 146 | export default BookShelf; -------------------------------------------------------------------------------- /src/catalogue/catalogue.ts: -------------------------------------------------------------------------------- 1 | import Bar from "../common/bar/bar"; 2 | import { Book, CatalogueItem, changeValueWithNewObj, getSpecialParent, Progress } from "../common/common"; 3 | import Pagination from "../common/pagination/pagination"; 4 | 5 | class Catalogue { 6 | element: HTMLElement; 7 | bar: Bar; 8 | pagination: Pagination; 9 | 10 | currentBook: Book; 11 | progress: Progress; 12 | 13 | linePerPage: number; 14 | 15 | list: CatalogueItem[] = []; 16 | pageList: CatalogueItem[] = []; 17 | 18 | oo: number = 10; 19 | 20 | loading: boolean = false; 21 | 22 | cacheFlag: boolean = false; 23 | 24 | constructor() { 25 | this.element = document.querySelector('.page.catalogue'); 26 | 27 | this.pagination = new Pagination({ 28 | root: this.element.querySelector('.content'), 29 | fake: true, 30 | pageChange: (index: number) => { 31 | let start = index * this.linePerPage; 32 | this.pageList = this.list.slice(start, start + this.linePerPage); 33 | } 34 | }); 35 | 36 | this.bar = new Bar({ 37 | element: this.element.querySelector('.bar'), 38 | pagination: this.pagination 39 | }); 40 | 41 | const current: HTMLElement = this.element.querySelector('.current-info'); 42 | window.Bind.bind(this, 'list', (list: CatalogueItem[]) => { 43 | if (!this.linePerPage) { 44 | return; 45 | } 46 | this.pagination.checkPage(Math.ceil(list.length / this.linePerPage)); 47 | this.pagination.setPage(Math.floor(this.progress.index / this.linePerPage)); 48 | }); 49 | window.Bind.bindView(this.element.querySelector('.article-list'), this, 'pageList', (list: CatalogueItem[]) => { 50 | let html = ` 51 | 54 | `; 55 | list.forEach((article) => { 56 | let current = article.index === this.progress.index?'current':''; 57 | let cached = window.Store.has(`a_${this.currentBook.id}_${article.index}`)?'cached':''; 58 | html += ` 59 |
${article.title}
60 | `; 61 | }); 62 | return html; 63 | }); 64 | 65 | window.Bind.bindView(current, this, 'currentBook', () => { 66 | return `${this.currentBook?.name} - ${this.currentBook?.author}`; 67 | }); 68 | 69 | window.Bind.bind(this, 'progress', (newV: any, oldV: any) => { 70 | window.Store.setObj(`p_${this.currentBook.id}`, newV); 71 | }); 72 | 73 | 74 | let func = () => { 75 | this.checkCurrent(); 76 | 77 | this.checkHeight(); 78 | }; 79 | 80 | window.Router.cbMap.catalogue = func; 81 | func(); 82 | } 83 | 84 | checkCurrent(): void { 85 | this.currentBook = window.BookShelf.bookMap[window.Store.get('current')]; 86 | 87 | if (!this.currentBook) { 88 | if (window.Router.current === 'catalogue') { 89 | window.Router.go('bookshelf'); 90 | } 91 | return; 92 | } 93 | 94 | this.progress = window.Store.getObj(`p_${this.currentBook.id}`); 95 | 96 | this.list = window.Store.getObj(`c_${this.currentBook.id}`) || []; 97 | 98 | if (this.list.length === 0) { 99 | this.getCatalogue(); 100 | } 101 | } 102 | 103 | checkHeight(): void { 104 | let height = this.element.offsetHeight - 230 - 20; 105 | let oo = height % 80; 106 | if (oo < 10) { 107 | oo += 80; 108 | } 109 | this.oo = oo; 110 | this.linePerPage = Math.round((height - oo) / 80) * 2; 111 | const current: HTMLElement = this.element.querySelector('.current-info'); 112 | const content: HTMLElement = this.element.querySelector('.content'); 113 | current.style.height = `${oo}px`; 114 | current.style.lineHeight = `${oo}px`; 115 | content.style.height = `${height - oo + 20}px`; 116 | } 117 | 118 | 119 | getCatalogue(): void { 120 | if (this.loading === true) { 121 | window.Message.add({content: '正在加载目录数据'}); 122 | return; 123 | } 124 | this.loading = true; 125 | window.Api.getCatalogue(this.currentBook.source, { 126 | success: (res: any) => { 127 | this.loading = false; 128 | this.list = res.data.map((v: any) => { 129 | return { 130 | index: v.index, 131 | title: v.title 132 | }; 133 | }); 134 | window.Store.setObj(`c_${this.currentBook.id}`, this.list); 135 | }, 136 | error: (err: any) => { 137 | this.loading = false; 138 | } 139 | }); 140 | } 141 | 142 | clickItem(event: Event): void { 143 | let item = getSpecialParent((event.target || event.srcElement) as HTMLElement, (ele: HTMLElement) => { 144 | return ele.classList.contains('article-item'); 145 | }); 146 | let index = parseInt(item.getAttribute('key')); 147 | this.progress = changeValueWithNewObj(this.progress, {index: index, title: this.list[index].title, time: new Date().getTime(), pos: 0}); 148 | window.setTimeout(() => { 149 | window.Router.go('article'); 150 | }); 151 | } 152 | 153 | makeCache(start: number, end: number): void { 154 | if (start > end) { 155 | this.cacheFlag = false; 156 | window.Message.add({ 157 | content: '缓存任务完成' 158 | }); 159 | return; 160 | } 161 | if (window.Store.has(`a_${this.currentBook.id}_${start}`)) { 162 | this.makeCache(start + 1, end); 163 | return; 164 | } 165 | window.Api.getArticle(this.currentBook.source, start, { 166 | success: (res: any) => { 167 | window.Store.set(`a_${this.currentBook.id}_${start}`, res.data); 168 | this.element.querySelector(`.article-item[key="${start}"]`)?.classList.add('cached'); 169 | this.makeCache(start + 1, end); 170 | }, 171 | error: (err: any) => { 172 | window.Message.add({ 173 | content: `缓存章节《${this.list[start].title}》失败` 174 | }); 175 | this.makeCache(start + 1, end); 176 | } 177 | }); 178 | } 179 | 180 | doCache(val: number | 'end' | 'all'): void { 181 | if (this.cacheFlag) { 182 | window.Message.add({ 183 | content: '正在缓存,请勿重复操作' 184 | }); 185 | return; 186 | } 187 | this.checkCurrent(); 188 | this.cacheFlag = true; 189 | let start = this.progress?.index; 190 | let last = this.list[this.list.length - 1]?.index || 0; 191 | if (val === 'all') { 192 | start = 0; 193 | } 194 | if (typeof val === 'number') { 195 | last = Math.min(last, start + val); 196 | } 197 | this.makeCache(start, last); 198 | } 199 | 200 | deleteCache(type: 'readed' | 'all'): void { 201 | if (this.cacheFlag) { 202 | window.Message.add({ 203 | content: '正在缓存,禁用删除操作' 204 | }); 205 | return; 206 | } 207 | window.Store.getByHead(`a_${this.currentBook.id}_`).filter(v => !(type === 'readed' && parseInt(v.split('_')[2]) >= this.progress.index)).forEach(v => { 208 | window.Store.del(v); 209 | this.element.querySelector(`.article-item[key="${v.split('_')[2]}"]`)?.classList.remove('cached'); 210 | }); 211 | window.Message.add({ 212 | content: '删除指定缓存完成' 213 | }); 214 | } 215 | 216 | cache(): void { 217 | window.Modal.add({ 218 | content: ` 219 | 228 |
缓存20章
229 |
缓存50章
230 |
缓存100章
231 |
缓存200章
232 |
缓存未读
233 |
缓存全文
234 |
删除已读
235 |
删除全部
236 | `, 237 | }) 238 | } 239 | }; 240 | 241 | export default Catalogue; -------------------------------------------------------------------------------- /src/common/api/api.ts: -------------------------------------------------------------------------------- 1 | import { Book, Progress } from "../common"; 2 | 3 | class Api { 4 | url: string; 5 | 6 | apiMap: {[key: string]: string} = { 7 | bookshelf: '/getBookshelf', 8 | catalogue: '/getChapterList', 9 | article: '/getBookContent', 10 | save: '/saveBookProgress' 11 | }; 12 | 13 | private _checkXHR: XMLHttpRequest; 14 | 15 | constructor() { 16 | if (window.Api) { 17 | throw Error('api has been inited'); 18 | } 19 | window.Api = this; 20 | 21 | this.url = window.Store.get('url') || ''; 22 | } 23 | 24 | saveProgress(book: Book, progress: Progress, cb?: {success?: Function, error?: Function}): void { 25 | this.post(this.url + this.apiMap.save, { 26 | author: book.author, 27 | durChapterIndex: progress.index, 28 | durChapterPos: progress.pos, 29 | durChapterTime: progress.time, 30 | durChapterTitle: progress.title, 31 | name: book.name 32 | }, { 33 | success: (data: any) => { 34 | cb && cb.success && cb.success(data); 35 | }, 36 | error: (err: any) => { 37 | console.log(err); 38 | cb && cb.error && cb.error(err); 39 | window.Message.add({content: '保存阅读进度到服务端失败'}); 40 | } 41 | }) 42 | } 43 | 44 | getArticle(url: string, index: number, cb?: {success?: Function, error?: Function}): void { 45 | this.get(this.url + this.apiMap.article, {url: url, index: index}, { 46 | success: (data: any) => { 47 | cb && cb.success && cb.success(data); 48 | }, 49 | error: (err: any) => { 50 | console.log(err); 51 | cb && cb.error && cb.error(err); 52 | window.Message.add({content: '获取章节内容失败'}); 53 | } 54 | }); 55 | } 56 | 57 | getCatalogue(url: string, cb?: {success?: Function, error?: Function}): void { 58 | this.get(this.url + this.apiMap.catalogue, {url: url}, { 59 | success: (data: any) => { 60 | cb && cb.success && cb.success(data); 61 | }, 62 | error: (err: any) => { 63 | console.log(err); 64 | cb && cb.error && cb.error(err); 65 | window.Message.add({content: '获取目录内容失败'}); 66 | } 67 | }); 68 | } 69 | 70 | getBookshelf(cb?: {success?: Function, error?: Function}): void { 71 | this.get(this.url + this.apiMap.bookshelf, {}, { 72 | success: (data: any) => { 73 | cb && cb.success && cb.success(data); 74 | }, 75 | error: (err: any) => { 76 | console.log(err); 77 | cb && cb.error && cb.error(err); 78 | window.Message.add({content: '获取书架内容失败'}); 79 | } 80 | }); 81 | } 82 | 83 | post(url: string, data: { [key: string]: any }, cb?: {success?: Function, error?: Function, check?: boolean}) { 84 | return this.http('POST', url, data, cb); 85 | } 86 | 87 | get(url: string, data: { [key: string]: any }, cb?: {success?: Function, error?: Function, check?: boolean}) { 88 | return this.http('GET', url, data, cb); 89 | } 90 | 91 | // get(url: string, data: { [key: string]: any }, cb?: {success?: Function, error?: Function, check?: boolean}) { 92 | // if (!this.url && !(cb && cb.check)) { 93 | // window.Message.add({content: '当前未配置服务器地址'}); 94 | // cb && cb.error && cb.error(null); 95 | // return; 96 | // } 97 | 98 | // // 创建 XMLHttpRequest,相当于打开浏览器 99 | // const xhr = new XMLHttpRequest() 100 | 101 | // // 打开一个与网址之间的连接 相当于输入网址 102 | // // 利用open()方法,第一个参数是对数据的操作,第二个是接口 103 | // xhr.open("GET", `${url}?${Object.keys(data).map(v => `${v}=${data[v]}`).join('&')}`); 104 | 105 | // // 通过连接发送请求 相当于点击回车或者链接 106 | // xhr.send(null); 107 | 108 | // // 指定 xhr 状态变化事件处理函数 相当于处理网页呈现后的操作 109 | // // 全小写 110 | // xhr.onreadystatechange = function () { 111 | // // 通过readyState的值来判断获取数据的情况 112 | // if (this.readyState === 4) { 113 | // // 响应体的文本 responseText 114 | // let response; 115 | // try { 116 | // response = JSON.parse(this.responseText); 117 | // } catch(e) { 118 | // response = this.responseText; 119 | // } 120 | // if (this.status === 200 && response.isSuccess) { 121 | // cb && cb.success && cb.success(response); 122 | // } else { 123 | // cb && cb.error && cb.error(response); 124 | // } 125 | // } 126 | // } 127 | 128 | // return xhr; 129 | // } 130 | 131 | http(method: 'GET' | 'POST',url: string, data: { [key: string]: any }, cb?: {success?: Function, error?: Function, check?: boolean}) { 132 | if (!this.url && !(cb && cb.check)) { 133 | window.Message.add({content: '当前未配置服务器地址'}); 134 | cb && cb.error && cb.error(null); 135 | return; 136 | } 137 | 138 | // 创建 XMLHttpRequest,相当于打开浏览器 139 | const xhr = new XMLHttpRequest(); 140 | 141 | // 打开一个与网址之间的连接 相当于输入网址 142 | // 利用open()方法,第一个参数是对数据的操作,第二个是接口 143 | // xhr.open(method, `${url}?${Object.keys(data).map(v => `${v}=${data[v]}`).join('&')}`); 144 | let param: string = Object.keys(data).map(v => `${v}=${data[v]}`).join('&'); 145 | xhr.open(method, method === 'GET'?`${url}?${param}`:url); 146 | 147 | if (method === 'POST') { 148 | xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 149 | } 150 | 151 | // 通过连接发送请求 相当于点击回车或者链接 152 | xhr.send(method === 'GET'?null:JSON.stringify(data)); 153 | 154 | // 指定 xhr 状态变化事件处理函数 相当于处理网页呈现后的操作 155 | // 全小写 156 | xhr.onreadystatechange = function () { 157 | // 通过readyState的值来判断获取数据的情况 158 | if (this.readyState === 4) { 159 | // 响应体的文本 responseText 160 | let response; 161 | try { 162 | response = JSON.parse(this.responseText); 163 | } catch(e) { 164 | response = this.responseText; 165 | } 166 | if (this.status === 200 && response.isSuccess) { 167 | cb && cb.success && cb.success(response); 168 | } else { 169 | cb && cb.error && cb.error(response); 170 | } 171 | } 172 | } 173 | 174 | return xhr; 175 | } 176 | 177 | setUrl(url: string) { 178 | this.url = url; 179 | window.Store.set('url', url); 180 | } 181 | 182 | checkUrl(url: string) { 183 | if (this._checkXHR) { 184 | this._checkXHR.abort(); 185 | } 186 | this._checkXHR = this.get(url + this.apiMap.bookshelf, {}, { 187 | success: (data: any) => { 188 | window.Message.add({content: '服务器地址测试成功'}); 189 | this.setUrl(url); 190 | }, 191 | error: (err: any) => { 192 | console.log(err); 193 | window.Message.add({content: '服务器地址测试失败'}); 194 | }, 195 | check: true 196 | }); 197 | } 198 | }; 199 | 200 | export default Api; -------------------------------------------------------------------------------- /src/common/bar/bar.less: -------------------------------------------------------------------------------- 1 | .bar { 2 | height: @barHeight; 3 | position: absolute; 4 | bottom: @controlHeight; 5 | width: 100%; 6 | background-color: #666; 7 | text-align: center; 8 | 9 | .bar-progress { 10 | display: inline-block; 11 | height: 100%; 12 | background-color: #000; 13 | float: left; 14 | } 15 | .bar-text { 16 | position: absolute; 17 | width: 100%; 18 | height: 100%; 19 | line-height: @barHeight; 20 | color: #fff; 21 | } 22 | } -------------------------------------------------------------------------------- /src/common/bar/bar.ts: -------------------------------------------------------------------------------- 1 | import Pagination from "../pagination/pagination"; 2 | 3 | class Bar { 4 | element: HTMLElement; 5 | pagination: Pagination; 6 | percent: number; 7 | 8 | constructor(config: { 9 | element: HTMLElement, 10 | pagination: Pagination 11 | }) { 12 | this.element = config.element; 13 | this.pagination = config.pagination; 14 | this.percent = 0; 15 | 16 | this.element.innerHTML = ` 17 |
18 |
/
19 | `; 20 | 21 | let index: HTMLElement = this.element.querySelector('.bar-current'); 22 | let total: HTMLElement = this.element.querySelector('.bar-total'); 23 | let progress: HTMLElement = this.element.querySelector('.bar-progress'); 24 | 25 | window.Bind.bindView(index, this.pagination, 'pageIndex', (value: number) => { 26 | let v = value + 1; 27 | this.percent = v / this.pagination.pageLimit; 28 | return v; 29 | }); 30 | window.Bind.bindView(total, this.pagination, 'pageLimit', (value: number) => { 31 | this.percent = (this.pagination.pageIndex + 1) / value; 32 | return value; 33 | }); 34 | 35 | window.Bind.bindStyle(progress, this, 'percent', 'width', (v: any) => `${v * 100}%`); 36 | 37 | this.element.onclick =(event: MouseEvent) => { 38 | let width = this.element.offsetWidth; 39 | let x = event.pageX; 40 | let index = Math.floor(this.pagination.pageLimit * x / width); 41 | this.pagination.setPage(index); 42 | }; 43 | } 44 | }; 45 | 46 | export default Bar; -------------------------------------------------------------------------------- /src/common/bind/bind.ts: -------------------------------------------------------------------------------- 1 | class Bind { 2 | cbMap: any = {}; 3 | objIndex: number = 0; 4 | objMap: any = {}; 5 | 6 | constructor() { 7 | if (window.Bind) { 8 | throw Error('bind has been inited'); 9 | } 10 | window.Bind = this; 11 | } 12 | 13 | private handleObj(obj: any, prop: string) { 14 | if (!obj.hasOwnProperty('_bindId')) { 15 | obj._bindId = this.objIndex++; 16 | } 17 | if (this.cbMap[obj._bindId + prop]) { 18 | return; 19 | } 20 | let index = '_' + prop; 21 | obj[index] = obj[prop]; 22 | this.cbMap[obj._bindId + prop] = []; 23 | Object.defineProperty(obj, prop, { 24 | get: () => { 25 | return obj[index]; 26 | }, 27 | set: (value: any) => { 28 | let temp = obj[index]; 29 | obj[index] = value; 30 | this.run(obj, prop, value, temp); 31 | } 32 | }) 33 | } 34 | 35 | bindInput(element: HTMLInputElement, obj: any, prop: string) { 36 | if (!element) { 37 | throw new Error('element is null'); 38 | } 39 | this.bind(obj, prop, (newV: any, oldV: any) => { 40 | element.value = newV; 41 | }, true); 42 | element.onchange = (event: InputEvent) => { 43 | obj[prop] = (event.target as HTMLInputElement).value; 44 | }; 45 | } 46 | 47 | bindStyle(element: HTMLElement, obj: any, prop: string, target: any, handle?: Function) { 48 | if (!element) { 49 | throw new Error('element is null'); 50 | } 51 | this.bind(obj, prop, (newV: any, oldV: any) => { 52 | element.style[target] = handle?handle(newV, oldV):newV; 53 | }, true); 54 | } 55 | 56 | bindView(element: HTMLElement, obj: any, prop: string, formatter?: Function) { 57 | if (!element) { 58 | throw new Error('element is null'); 59 | } 60 | this.bind(obj, prop, (newV: any, oldV: any) => { 61 | element.innerHTML = formatter?formatter(newV, oldV):newV; 62 | }, true); 63 | } 64 | 65 | bind(obj: any, prop: string, callback: Function, immediately?: boolean) { 66 | this.handleObj(obj, prop); 67 | this.cbMap[obj._bindId + prop].push(callback); 68 | immediately && callback(obj[prop], undefined); 69 | } 70 | 71 | run(obj: any, prop: string, newV?: any, oldV?: any) { 72 | this.cbMap[obj._bindId + prop].forEach((callback: Function) => { 73 | try { 74 | callback(newV, oldV); 75 | } catch (error) { 76 | throw error; 77 | } 78 | }); 79 | } 80 | }; 81 | 82 | export default Bind; -------------------------------------------------------------------------------- /src/common/common.ts: -------------------------------------------------------------------------------- 1 | function strToDom(str: string): HTMLCollection { 2 | let div = document.createElement('div'); 3 | div.innerHTML = str; 4 | return div.children; 5 | } 6 | 7 | function makeDisplayText(time: number): string { 8 | let text = '测试文本'; 9 | 10 | let result = new Array(time + 1).join(text); 11 | 12 | return result; 13 | } 14 | 15 | function getSpecialParent(ele: HTMLElement,checkFun: Function): HTMLElement | null { 16 | if (ele && ele !== document as unknown && checkFun(ele)) { 17 | return ele; 18 | } 19 | let parent = ele.parentElement || ele.parentNode; 20 | return parent?getSpecialParent(parent as HTMLElement, checkFun):null; 21 | } 22 | 23 | function getObject(source: any, keys: string[], others?: {[key: string]: any}): any { 24 | let obj: any = {}; 25 | keys.forEach(key => { 26 | obj[key] = source[key]; 27 | }); 28 | others && Object.keys(others).forEach(key => { 29 | obj[key] = others[key]; 30 | }); 31 | return obj; 32 | } 33 | 34 | function changeValueWithNewObj(obj: any, target: {[key: string]: any}): any { 35 | let result = JSON.parse(JSON.stringify(obj)); 36 | Object.keys(target).forEach(v => { 37 | result[v] = target[v]; 38 | }); 39 | return result; 40 | } 41 | 42 | interface Book { 43 | id: string; 44 | source: string; 45 | name: string; 46 | author: string; 47 | bookUrl: string; 48 | coverUrl: string; 49 | customCoverUrl: string; 50 | durChapterTitle: string; 51 | latestChapterTime: string; 52 | latestChapterTitle: string; 53 | } 54 | 55 | interface CatalogueItem { 56 | index: number; 57 | title: string; 58 | } 59 | 60 | interface Progress { 61 | index: number; 62 | pos: number; 63 | time: number; 64 | title: string; 65 | } 66 | 67 | 68 | export { strToDom, makeDisplayText, getSpecialParent, getObject, changeValueWithNewObj, Book, CatalogueItem, Progress }; -------------------------------------------------------------------------------- /src/common/debugger/debugger.ts: -------------------------------------------------------------------------------- 1 | class Debugger { 2 | constructor() { 3 | window.onerror = function (error) { 4 | console.error(error); 5 | 6 | window.Modal && window.Modal.add({ 7 | content: error.toString() 8 | }); 9 | } 10 | } 11 | }; 12 | 13 | export default Debugger; -------------------------------------------------------------------------------- /src/common/layout/layout.ts: -------------------------------------------------------------------------------- 1 | interface LayoutInterface { 2 | fontSize: number; 3 | lineHeight: number; 4 | }; 5 | 6 | class Layout { 7 | 8 | fontSize: number; 9 | 10 | lineHeight: number; 11 | 12 | limit: LayoutInterface = { 13 | fontSize: 20, 14 | lineHeight: 24 15 | }; 16 | base: LayoutInterface = { 17 | fontSize: 30, 18 | lineHeight: 40 19 | }; 20 | 21 | constructor() { 22 | if (window.Layout) { 23 | throw Error('layout has been inited'); 24 | } 25 | window.Layout = this; 26 | 27 | this.fontSize = parseInt(window.Store.get('fontSize') || this.base.fontSize.toString()); 28 | this.lineHeight = parseInt(window.Store.get('lineHeight') || this.base.lineHeight.toString()); 29 | } 30 | 31 | set(target: 'fontSize' | 'lineHeight', value?: number): void { 32 | this[target] = value || this.base[target]; 33 | window.Store.set(target, this[target].toString()); 34 | } 35 | 36 | add(target: 'fontSize' | 'lineHeight', num: number): void { 37 | let current = this[target]; 38 | current += num; 39 | 40 | if (current < this.limit[target]) { 41 | current = this.limit[target]; 42 | } 43 | 44 | this.set(target, current); 45 | } 46 | 47 | reset(target?: 'fontSize' | 'lineHeight'): void { 48 | if (target) { 49 | this.set(target); 50 | return; 51 | } 52 | this.set('fontSize'); 53 | this.set('lineHeight'); 54 | } 55 | }; 56 | 57 | export default Layout; -------------------------------------------------------------------------------- /src/common/message/message.less: -------------------------------------------------------------------------------- 1 | .message { 2 | text-align: center; 3 | padding: 5px; 4 | .message-content { 5 | display: inline-block; 6 | height: 60px; 7 | background-color: #666; 8 | font-size: 50px; 9 | line-height: 60px; 10 | color: #fff; 11 | border-radius: 8px; 12 | } 13 | } -------------------------------------------------------------------------------- /src/common/message/message.ts: -------------------------------------------------------------------------------- 1 | import { strToDom } from '../common'; 2 | 3 | interface MessageOption { 4 | content: string; 5 | onOk?: Function; 6 | onCancle?: Function; 7 | banAutoRemove?: boolean; 8 | }; 9 | 10 | class MessageItem { 11 | body: Element; 12 | constructor(option: MessageOption) { 13 | let str = ` 14 |
15 |
16 |
17 |
18 | `; 19 | let message: Element = strToDom(str)[0]; 20 | this.body = message; 21 | let content: HTMLDivElement = message.querySelector('.message-content'); 22 | content.innerHTML = option.content; 23 | 24 | content.onclick = () => { 25 | option.onOk && option.onOk(); 26 | this.remove(); 27 | }; 28 | 29 | if (option.banAutoRemove) { 30 | return; 31 | } 32 | 33 | window.setTimeout(() => { 34 | option.onCancle && option.onCancle(); 35 | this.remove(); 36 | }, 2000); 37 | } 38 | 39 | remove() { 40 | let parent = this.body.parentElement; 41 | parent && parent.removeChild(this.body); 42 | } 43 | }; 44 | 45 | class Message { 46 | element: HTMLElement; 47 | list: any[]; 48 | constructor() { 49 | if (window.Message) { 50 | throw Error('modal has been inited'); 51 | } 52 | this.list = []; 53 | window.Message = this; 54 | this.element = document.querySelector('.message-box'); 55 | } 56 | 57 | add(option: MessageOption) { 58 | let item = new MessageItem(option); 59 | this.list.push(item); 60 | this.element.appendChild(item.body); 61 | return item; 62 | } 63 | 64 | remove(item: MessageItem): void { 65 | item.remove(); 66 | let index = this.list.indexOf(item); 67 | this.list.splice(index, 1); 68 | } 69 | 70 | clear(): void { 71 | this.list = []; 72 | this.element.innerHTML = ''; 73 | } 74 | }; 75 | 76 | export default Message; -------------------------------------------------------------------------------- /src/common/modal/modal.less: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: absolute; 3 | min-width: 600px; 4 | min-height: 600px; 5 | left: 50px; 6 | right: 50px; 7 | top: 40%; 8 | // margin-left: -300px; 9 | margin-top: -400px; 10 | border: 1px solid #ccc; 11 | background-color: #fff; 12 | 13 | .modal-content { 14 | min-height: 500px; 15 | padding: 10px; 16 | word-break: break-all; 17 | font-size: 20px; 18 | line-height: 40px; 19 | 20 | * { 21 | word-break: break-all; 22 | } 23 | } 24 | .modal-footer { 25 | height: 100px; 26 | text-align: center; 27 | border-top: 1px solid #ccc; 28 | line-height: 100px; 29 | 30 | .button { 31 | height: 90px; 32 | margin: 0 20px; 33 | vertical-align: middle; 34 | line-height: 90px; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/common/modal/modal.ts: -------------------------------------------------------------------------------- 1 | import { strToDom } from '../common'; 2 | 3 | 4 | interface ModalOption { 5 | content: string | HTMLElement; 6 | onOk?: Function; 7 | onCancel?: Function; 8 | zIndex?: number; 9 | }; 10 | 11 | class ModalItem { 12 | body: Element; 13 | zIndex: number; 14 | constructor(option: ModalOption) { 15 | this.zIndex = option.zIndex; 16 | let str = ` 17 | 25 | `; 26 | let modal: Element = strToDom(str)[0]; 27 | this.body = modal; 28 | let content: HTMLDivElement = modal.querySelector('.modal-content'); 29 | let btnConfirm: HTMLButtonElement = modal.querySelector('.modal-confirm'); 30 | let btnCancel: HTMLButtonElement = modal.querySelector('.modal-cancel'); 31 | if (typeof option.content === 'string') { 32 | content.innerHTML = option.content; 33 | } else { 34 | content.appendChild(option.content); 35 | } 36 | btnCancel.onclick = () => { 37 | option.onCancel && option.onCancel(); 38 | this.remove(); 39 | }; 40 | 41 | btnConfirm.onclick = () => { 42 | option.onOk && option.onOk(); 43 | this.remove(); 44 | }; 45 | } 46 | 47 | remove() { 48 | let parent = this.body.parentElement; 49 | parent.removeChild(this.body); 50 | } 51 | }; 52 | 53 | 54 | class Modal { 55 | element: HTMLElement; 56 | list: ModalItem[] = []; 57 | constructor() { 58 | if (window.Modal) { 59 | throw Error('modal has been inited'); 60 | } 61 | window.Modal = this; 62 | this.element = document.querySelector('.modal-box'); 63 | } 64 | 65 | add(option: ModalOption): ModalItem { 66 | if (!('zIndex' in option)) { 67 | let length = this.list.length; 68 | option.zIndex = (length?this.list[length - 1].zIndex:100) + 1; 69 | } 70 | let item = new ModalItem(option); 71 | this.list.push(item); 72 | this.element.appendChild(item.body); 73 | return item; 74 | } 75 | 76 | remove(item: ModalItem): void { 77 | item.remove(); 78 | let index = this.list.indexOf(item); 79 | this.list.splice(index, 1); 80 | } 81 | 82 | clear(): void { 83 | this.list = []; 84 | this.element.innerHTML = ''; 85 | } 86 | } 87 | 88 | export default Modal; -------------------------------------------------------------------------------- /src/common/pagination/pagination.less: -------------------------------------------------------------------------------- 1 | .pagination-box { 2 | height: 100%; 3 | overflow: hidden; 4 | 5 | .pagination-body { 6 | min-height: 100%; 7 | } 8 | } -------------------------------------------------------------------------------- /src/common/pagination/pagination.ts: -------------------------------------------------------------------------------- 1 | class Pagination { 2 | root: HTMLElement; 3 | box: HTMLElement; 4 | padding: HTMLElement; 5 | 6 | pageStep: number; 7 | 8 | pageIndex: number = 0; 9 | 10 | pageLimit: number = 1; 11 | 12 | pagePadding: number = 0; 13 | 14 | fakePage: boolean = false; 15 | 16 | constructor(config: { 17 | root: HTMLElement, 18 | fake?: boolean 19 | pageChange?: Function 20 | }) { 21 | this.root = config.root; 22 | this.handleHtml(config.root); 23 | 24 | this.fakePage = config.fake || false; 25 | 26 | this.pageStep = this.box.offsetHeight; 27 | 28 | this.checkPage(); 29 | 30 | window.Bind.bindStyle(this.padding, this, 'pagePadding', 'height', (v: any) => `${v}px`); 31 | window.Bind.bind(this, 'pageIndex', (value: number) => { 32 | if (this.fakePage) { 33 | config?.pageChange(value); 34 | return; 35 | } 36 | this.box.scrollTop = this.pageStep * value; 37 | }); 38 | } 39 | 40 | private handleHtml(root: HTMLElement) { 41 | let inner = root.innerHTML; 42 | root.innerHTML = ` 43 |
44 |
45 |
46 |
47 |
48 |
`; 49 | let content: HTMLElement = root.querySelector('.pagination-content'); 50 | content.innerHTML = inner; 51 | this.box = root.querySelector('.pagination-box'); 52 | this.padding = root.querySelector('.pagination-padding'); 53 | } 54 | 55 | checkPage(limit?: number): void { 56 | this.pageStep = this.box.offsetHeight; 57 | if (this.fakePage) { 58 | this.pageLimit = limit || 1; 59 | this.pagePadding = 0; 60 | return; 61 | } 62 | this.pageLimit = Math.ceil(this.box.scrollHeight / this.pageStep) || 1; 63 | this.pagePadding = this.pageStep * this.pageLimit - this.box.scrollHeight; 64 | } 65 | 66 | setPage(num: number) { 67 | let target = num; 68 | if (num < 0) { 69 | target = 0; 70 | } 71 | if (num >= this.pageLimit) { 72 | target = this.pageLimit - 1; 73 | } 74 | this.pageIndex = target; 75 | } 76 | 77 | pageChange(add: number) { 78 | this.setPage(this.pageIndex + add); 79 | } 80 | }; 81 | 82 | export default Pagination; -------------------------------------------------------------------------------- /src/common/router/router.ts: -------------------------------------------------------------------------------- 1 | class Router { 2 | 3 | current: string; 4 | 5 | pages: string[] = []; 6 | 7 | cbMap: {[key: string]: Function} = {}; 8 | 9 | constructor(pages: string[]) { 10 | if (window.Router) { 11 | throw Error('router has been inited'); 12 | } 13 | window.Router = this; 14 | 15 | this.pages = pages; 16 | 17 | let func = (event?: HashChangeEvent) => { 18 | let hash = window.location.hash; 19 | let index = hash.lastIndexOf('#'); 20 | if (index > -1) { 21 | hash = hash.slice(index + 1); 22 | } 23 | if (this.pages.length === 0) { 24 | return; 25 | } 26 | if (this.pages.indexOf(hash) === -1) { 27 | window.location.hash = this.pages[0]; 28 | return; 29 | } 30 | 31 | this.switchPage(hash); 32 | }; 33 | window.onhashchange = func; 34 | func(); 35 | } 36 | 37 | private switchPage(str: string) { 38 | document.querySelector('.show')?.classList.remove('show'); 39 | document.querySelector(`.${str}`)?.classList.add('show'); 40 | this.current = str; 41 | this.cbMap[str] && this.cbMap[str](); 42 | } 43 | 44 | go(target: string): void { 45 | window.location.hash = target; 46 | } 47 | } 48 | 49 | export default Router; -------------------------------------------------------------------------------- /src/common/store/store.ts: -------------------------------------------------------------------------------- 1 | // import * as LzString from 'lz-string'; 2 | import { compress, decompress } from 'lz-string'; 3 | import { Book } from '../common'; 4 | 5 | // prefix map 6 | // a article 7 | // b book 8 | // c catalogue 9 | // p progress 10 | 11 | class Store { 12 | data: any; 13 | 14 | limitChecking: boolean = false; 15 | limit: number = 0; 16 | 17 | usage: number = 0; 18 | 19 | percent: number = 0; 20 | 21 | compress: Function = compress; 22 | decompress: Function = decompress; 23 | 24 | checkFlag: number; 25 | 26 | constructor() { 27 | if (window.Store) { 28 | throw Error('store has been inited'); 29 | } 30 | window.Store = this; 31 | this.limit = parseInt(this.get('limit') || '0'); 32 | 33 | this.checkUsage(); 34 | if (this.limit === 0) { 35 | // this.checkLimit(); 36 | window.Message.add({content: '缓存未初始化请手动检测'}); 37 | } 38 | } 39 | 40 | bookDelete(book: Book, onlySource?: boolean): void { 41 | if (!onlySource) { 42 | this.del(`p_${book.id}`); 43 | } 44 | this.del(`c_${book.id}`); 45 | this.getByHead(`a_${book.id}`).forEach(v => this.del(v)); 46 | } 47 | 48 | del(key: string): void { 49 | localStorage.removeItem(key); 50 | this.checkUsage(); 51 | } 52 | 53 | has(key: string): boolean { 54 | return localStorage.hasOwnProperty(key); 55 | } 56 | 57 | getObj(key: string): any | null { 58 | return JSON.parse(this.get(key)); 59 | } 60 | 61 | setObj(key: string, value: any, cb?: {success?: Function, fail?: Function}): void { 62 | this.set(key, JSON.stringify(value), cb); 63 | } 64 | 65 | set(key: string, value: string, cb?: {success?: Function, fail?: Function}): void { 66 | try { 67 | // let ckey = compress(key); 68 | let cvalue = compress(value); 69 | // localStorage.setItem(ckey, cvalue); 70 | localStorage.setItem(key, cvalue); 71 | this.checkUsage(); 72 | cb && cb.success && cb.success(); 73 | } catch(e) { 74 | window.Message.add({content: '缓存失败,空间不足'}); 75 | cb && cb.fail && cb.fail(); 76 | } 77 | } 78 | 79 | get(key: string): string | null { 80 | // let store = localStorage.getItem(compress(key)); 81 | let store = localStorage.getItem(key); 82 | if (store) { 83 | return decompress(store); 84 | } 85 | return null; 86 | } 87 | 88 | getByHead(head: string): string[] { 89 | return Object.keys(localStorage).filter(v => v.indexOf(head) === 0); 90 | } 91 | 92 | checkUsage(): void { 93 | if (this.checkFlag) { 94 | window.clearTimeout(this.checkFlag); 95 | } 96 | this.checkFlag = window.setTimeout(() => { 97 | this.usage = Object.keys(localStorage).map(v => v + localStorage.getItem(v)).join('').length; 98 | this.percent = this.limit?Math.round(this.usage / (this.limit) * 100):0; 99 | if (this.percent > 95) { 100 | window.Message.add({ 101 | content: `缓存已使用${this.percent}%,请注意` 102 | }); 103 | } 104 | }, 500); 105 | } 106 | 107 | checkLimit(): void { 108 | window.Message.add({content: '正在检测缓存容量'}); 109 | if (this.limitChecking) { 110 | return; 111 | } 112 | this.limitChecking = true; 113 | 114 | window.setTimeout(() => { 115 | 116 | let base = this.usage; 117 | let addLength = 1000000; 118 | let index = 0; 119 | 120 | while (addLength > 2) { 121 | try { 122 | let key = `_test${index++}`; 123 | if (addLength < key.length) {break;} 124 | localStorage.setItem(key, new Array(addLength - key.length + 1).join('a')); 125 | base += addLength; 126 | } catch(e) { 127 | console.log(e); 128 | index--; 129 | addLength = Math.round(addLength / 2); 130 | } 131 | } 132 | this.limit = base; 133 | 134 | this.getByHead('_test').forEach(v => { 135 | this.del(v) 136 | }); 137 | 138 | this.set('limit', this.limit.toString()); 139 | 140 | this.limitChecking = false; 141 | 142 | window.Message.add({content: '检测完成'}); 143 | }, 1000); 144 | } 145 | }; 146 | 147 | export default Store; -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import {makeDisplayText} from '../common/common'; 2 | 3 | class Config { 4 | element: HTMLElement; 5 | 6 | displayText: string; 7 | 8 | url: string; 9 | 10 | constructor() { 11 | this.element = document.querySelector('.page.config'); 12 | 13 | this.url = window.Api.url; 14 | 15 | this.displayText = makeDisplayText(200); 16 | 17 | window.Bind.bindInput(this.element.querySelector('.url input'), this, 'url'); 18 | 19 | window.Bind.bindView(this.element.querySelector('.store-usage'), window.Store, 'usage'); 20 | window.Bind.bindView(this.element.querySelector('.store-total'), window.Store, 'limit'); 21 | window.Bind.bindView(this.element.querySelector('.store-percent'), window.Store, 'percent', (v: number) => ` ( ${v}% )`); 22 | 23 | window.Bind.bindView(this.element.querySelector('.font-size'), window.Layout, 'fontSize'); 24 | window.Bind.bindView(this.element.querySelector('.line-height'), window.Layout, 'lineHeight'); 25 | 26 | let display: HTMLElement = this.element.querySelector('.display .text p'); 27 | window.Bind.bindView(display, this, 'displayText'); 28 | window.Bind.bindStyle(display, window.Layout, 'fontSize', 'fontSize', (v: any) => `${v}px`); 29 | window.Bind.bindStyle(display, window.Layout, 'lineHeight', 'lineHeight', (v: any) => `${v}px`); 30 | 31 | if (!this.url) { 32 | window.Message.add({content: '当前未配置服务器地址'}); 33 | } else { 34 | this.checkUrl(); 35 | } 36 | } 37 | 38 | 39 | checkUrl() { 40 | window.Api.checkUrl(this.url); 41 | } 42 | }; 43 | 44 | export default Config; -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import Bind from "./common/bind/bind"; 2 | import Message from "./common/message/message"; 3 | import Modal from "./common/modal/modal"; 4 | import Router from "./common/router/router"; 5 | import Store from "./common/store/store"; 6 | import Config from "./config/config"; 7 | import Layout from "./common/layout/layout"; 8 | import Api from './common/api/api'; 9 | import BookShelf from "./bookshelf/bookshelf"; 10 | import Article from './article/article'; 11 | import Catalogue from './catalogue/catalogue'; 12 | 13 | declare global { 14 | interface Window { 15 | init?: Function; 16 | 17 | Bind?: Bind; 18 | Modal?: Modal; 19 | Message?: Message; 20 | Router?: Router; 21 | Store?: Store; 22 | Layout?: Layout; 23 | Api?: Api; 24 | 25 | Config?: Config; 26 | BookShelf?: BookShelf; 27 | Article?: Article; 28 | Catalogue?: Catalogue; 29 | 30 | } 31 | } 32 | 33 | export { }; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | kabi novel 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
配置
29 |
刷新
30 |
31 |
32 |
<
33 |
>
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
书架
46 |
目录
47 |
刷新
48 |
49 |
50 |
<
51 |
>
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
书架
64 |
文章
65 |
刷新
66 |
缓存
67 |
68 |
69 |
<
70 |
>
71 |
72 |
73 |
74 |
75 |
76 |
77 |
服务器地址
78 |
79 | 80 |
测试
81 |
82 |
83 |
84 |
缓存容量
85 |
86 | - 87 | / 88 | - 89 | 90 |
检测
91 |
92 |
93 |
94 |
字号
95 |
96 | 16 97 | px 98 |
重置
99 |
-
100 |
+
101 |
102 |
103 |
104 |
行距
105 |
106 | 20 107 | px 108 |
重置
109 |
-
110 |
+
111 |
112 |
113 |
114 |
效果展示
115 |
116 |

测试文本

117 |
118 |
119 |
120 |
121 |
122 |
书架
123 |
124 |
125 | 126 |
127 |
128 |
129 | 130 | 131 |
132 | 133 | 134 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import BookShelf from './bookshelf/bookshelf'; 2 | import Config from './config/config'; 3 | import Router from './common/router/router'; 4 | import Debugger from './common/debugger/debugger'; 5 | import Modal from './common/modal/modal'; 6 | import Message from './common/message/message'; 7 | import Store from './common/store/store'; 8 | import Bind from './common/bind/bind'; 9 | import Layout from './common/layout/layout'; 10 | import Api from './common/api/api'; 11 | import Article from './article/article'; 12 | import Catalogue from './catalogue/catalogue'; 13 | 14 | const pages: string[] = ['config', 'bookshelf', 'article', 'catalogue']; 15 | 16 | function init() { 17 | new Debugger(); 18 | 19 | new Bind(); 20 | 21 | new Modal(); 22 | new Message(); 23 | 24 | new Router(pages); 25 | 26 | new Store(); 27 | 28 | new Layout(); 29 | 30 | new Api(); 31 | 32 | document.querySelector('.global-style').innerHTML = ` 33 | 38 | `; 39 | 40 | window.Config = new Config(); 41 | 42 | window.BookShelf = new BookShelf(); 43 | 44 | window.Catalogue = new Catalogue(); 45 | 46 | window.Article = new Article(); 47 | 48 | } 49 | 50 | window.init = init; 51 | 52 | 53 | 54 | window.ondblclick = function(event: Event) { 55 | event.preventDefault(); 56 | } -------------------------------------------------------------------------------- /src/style.less: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | overflow: hidden; 5 | margin: 0; 6 | padding: 0; 7 | background-color: #fff; 8 | position: relative; 9 | font-size: 20px; 10 | line-height: 24px; 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | .button { 17 | min-width: 150px; 18 | padding: 0 10px; 19 | font-size: 60px; 20 | display: inline-block; 21 | border: 1px solid #333; 22 | text-align: center; 23 | justify-content: center; 24 | } 25 | } 26 | 27 | .page { 28 | width: 100%; 29 | height: 100%; 30 | display: none; 31 | position: relative; 32 | 33 | @scrollHack: 40px; 34 | @barHeight: 50px; 35 | @controlHeight: 140px + @scrollHack; 36 | 37 | &.show { 38 | display: block; 39 | } 40 | 41 | .content { 42 | // height: 100%; 43 | // padding-bottom: @controlHeight + @barHeight; 44 | // padding-top: @scrollHack; 45 | padding: 10px; 46 | padding-right: 15px; 47 | 48 | @import url(common/pagination/pagination.less); 49 | } 50 | 51 | .current-info { 52 | height: 20px; 53 | padding: 0 20px; 54 | line-height: 20px; 55 | } 56 | 57 | @import url(common/bar/bar.less); 58 | 59 | 60 | .control { 61 | height: @controlHeight; 62 | background-color: #eee; 63 | position: absolute; 64 | bottom: 0; 65 | width: 100%; 66 | padding-top: 20px; 67 | 68 | .left-box { 69 | float: left; 70 | } 71 | 72 | .right-box { 73 | float: right; 74 | } 75 | 76 | .button { 77 | line-height: @controlHeight - 20px; 78 | height: 100%; 79 | margin: 0 10px; 80 | } 81 | } 82 | } 83 | 84 | .article { 85 | .content { 86 | .content-inner { 87 | >p { 88 | margin: 0; 89 | text-indent: 2em; 90 | } 91 | } 92 | } 93 | } 94 | 95 | .catalogue { 96 | .article-list { 97 | font-size: 0; 98 | .article-item { 99 | display: inline-block; 100 | width: 50%; 101 | font-size: 40px; 102 | // line-height: 80px; 103 | white-space: nowrap; 104 | text-overflow: ellipsis; 105 | overflow: hidden; 106 | vertical-align: middle; 107 | color: #999; 108 | padding: 0 10px; 109 | 110 | &.current { 111 | color: #fff !important; 112 | background-color: #000; 113 | } 114 | 115 | &.cached { 116 | color: #000; 117 | font-weight: bold; 118 | } 119 | } 120 | } 121 | } 122 | 123 | .bookshelf { 124 | 125 | .book-list { 126 | font-size: 0; 127 | .book-item { 128 | display: inline-block; 129 | width: 50%; 130 | padding: 10px; 131 | min-height: 200px; 132 | vertical-align: middle; 133 | font-size: 24px; 134 | line-height: 40px; 135 | margin: 0; 136 | 137 | .book-cover { 138 | display: inline-block; 139 | height: 100%; 140 | border: 1px solid #ccc; 141 | background-size: 100%; 142 | background-position: center; 143 | vertical-align: middle; 144 | 145 | img { 146 | width: 100%; 147 | height: 100%; 148 | } 149 | } 150 | 151 | .book-info { 152 | display: inline-block; 153 | height: 100%; 154 | padding: 10px; 155 | vertical-align: middle; 156 | 157 | >div { 158 | overflow: hidden; 159 | text-overflow: ellipsis; 160 | white-space: nowrap; 161 | 162 | &:first-child { 163 | font-size: 32px; 164 | font-weight: bold; 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | 172 | .config { 173 | .content { 174 | padding: 20px 40px; 175 | font-size: 30px; 176 | line-height: 40px; 177 | height: 100%; 178 | 179 | .button { 180 | margin-left: 10px; 181 | } 182 | 183 | .right { 184 | float: right; 185 | } 186 | } 187 | .form-item { 188 | padding: 10px 0; 189 | 190 | .label { 191 | 192 | 193 | &::after { 194 | content: ":"; 195 | } 196 | } 197 | 198 | .text { 199 | line-height: 60px; 200 | 201 | input { 202 | font-size: 50px; 203 | line-height: 60px; 204 | 205 | width: 70%; 206 | } 207 | } 208 | 209 | &.display { 210 | .text { 211 | height: 500px; 212 | border: 1px solid #333; 213 | padding: 10px; 214 | line-height: 20px; 215 | font-size: 16px; 216 | overflow: hidden; 217 | 218 | p { 219 | margin: 0; 220 | text-indent: 2em; 221 | } 222 | } 223 | } 224 | 225 | } 226 | } 227 | 228 | .modal-box { 229 | @import url(common/modal/modal.less); 230 | } 231 | 232 | .message-box { 233 | position: fixed; 234 | top: 0; 235 | left: 0; 236 | right: 0; 237 | @import url(common/message/message.less); 238 | } -------------------------------------------------------------------------------- /temp/style.css: -------------------------------------------------------------------------------- 1 | .page,body,html{position:relative;height:100%;width:100%}body,html{overflow:hidden;margin:0;padding:0;background-color:#fff;font-size:20px;line-height:24px}body *,html *{box-sizing:border-box}body .button,html .button{min-width:150px;padding:0 10px;font-size:60px;display:inline-block;border:1px solid #333;text-align:center;justify-content:center}.page{display:none}.page.show{display:block}.page .content{padding:10px 15px 10px 10px}.page .content .pagination-box{height:100%;overflow:hidden}.page .content .pagination-box .pagination-body{min-height:100%}.page .current-info{height:20px;padding:0 20px;line-height:20px}.page .bar{height:50px;position:absolute;bottom:180px;width:100%;background-color:#666;text-align:center}.page .bar .bar-progress{display:inline-block;height:100%;background-color:#000;float:left}.page .bar .bar-text{position:absolute;width:100%;height:100%;line-height:50px;color:#fff}.page .control{height:180px;background-color:#eee;position:absolute;bottom:0;width:100%;padding-top:20px}.page .control .left-box{float:left}.config .content .right,.page .control .right-box{float:right}.page .control .button{line-height:160px;height:100%;margin:0 10px}.article .content .content-inner>p{margin:0;text-indent:2em}.catalogue .article-list{font-size:0}.catalogue .article-list .article-item{display:inline-block;width:50%;font-size:40px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;vertical-align:middle;color:#999;padding:0 10px}.catalogue .article-list .article-item.current{color:#fff!important;background-color:#000}.catalogue .article-list .article-item.cached{color:#000;font-weight:700}.bookshelf .book-list{font-size:0}.bookshelf .book-list .book-item{display:inline-block;width:50%;padding:10px;min-height:200px;vertical-align:middle;font-size:24px;line-height:40px;margin:0}.bookshelf .book-list .book-item .book-cover{display:inline-block;height:100%;border:1px solid #ccc;background-size:100%;background-position:center;vertical-align:middle}.bookshelf .book-list .book-item .book-cover img{width:100%;height:100%}.bookshelf .book-list .book-item .book-info{display:inline-block;height:100%;padding:10px;vertical-align:middle}.bookshelf .book-list .book-item .book-info>div{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bookshelf .book-list .book-item .book-info>div:first-child{font-size:32px;font-weight:700}.config .content{padding:20px 40px;font-size:30px;line-height:40px;height:100%}.config .content .button{margin-left:10px}.config .form-item{padding:10px 0}.config .form-item .label::after{content:":"}.config .form-item .text{line-height:60px}.config .form-item .text input{font-size:50px;line-height:60px;width:70%}.config .form-item.display .text{height:500px;border:1px solid #333;padding:10px;line-height:20px;font-size:16px;overflow:hidden}.config .form-item.display .text p{margin:0;text-indent:2em}.modal-box .modal{position:absolute;min-width:600px;min-height:600px;left:50px;right:50px;top:40%;margin-top:-400px;border:1px solid #ccc;background-color:#fff}.modal-box .modal .modal-content{min-height:500px;padding:10px;word-break:break-all;font-size:20px;line-height:40px}.modal-box .modal .modal-content *{word-break:break-all}.modal-box .modal .modal-footer{height:100px;text-align:center;border-top:1px solid #ccc;line-height:100px}.modal-box .modal .modal-footer .button{height:90px;margin:0 20px;vertical-align:middle;line-height:90px}.message-box{position:fixed;top:0;left:0;right:0}.message-box .message{text-align:center;padding:5px}.message-box .message .message-content{display:inline-block;height:60px;background-color:#666;font-size:50px;line-height:60px;color:#fff;border-radius:8px} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/main.ts" 4 | ], 5 | "compilerOptions": { 6 | "noImplicitAny": true, 7 | "target": "es5" 8 | }, 9 | "include": [ 10 | "src/*.d.ts" 11 | ] 12 | } --------------------------------------------------------------------------------