├── src ├── static │ ├── README.md │ ├── guanyu.png │ ├── lianxi.png │ ├── lishi.png │ ├── logo.png │ ├── weixin.png │ ├── yejian.png │ ├── yijian.png │ ├── fengmian.png │ ├── fenxiang.png │ ├── home_off.png │ ├── home_on.png │ ├── logo_wx.png │ ├── logo_zfb.png │ ├── nothing.png │ ├── nothing1.png │ ├── nothing2.png │ ├── nothing3.png │ ├── qingchu.png │ ├── shuyuan.png │ ├── user_img.png │ ├── yejian1.png │ ├── arrow-right.png │ ├── none_data.png │ ├── pengyouquan.png │ ├── personal_on.png │ ├── user_female.png │ ├── user_male.png │ ├── default_head.png │ ├── personal_off.png │ └── iconfont │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.css │ │ └── iconfont.json ├── main.ts ├── types │ ├── user.ts │ └── index.ts ├── parser │ ├── index.ts │ ├── top.ts │ ├── content.ts │ ├── catalog.ts │ └── search.ts ├── env.d.ts ├── components │ ├── painter │ │ ├── components │ │ │ ├── l-painter-qrcode │ │ │ │ └── l-painter-qrcode.vue │ │ │ ├── l-painter-image │ │ │ │ └── l-painter-image.vue │ │ │ ├── l-painter-view │ │ │ │ └── l-painter-view.vue │ │ │ ├── l-painter-text │ │ │ │ └── l-painter-text.vue │ │ │ ├── l-painter │ │ │ │ ├── props.js │ │ │ │ └── nvue.js │ │ │ └── common │ │ │ │ └── relation.js │ │ ├── package.json │ │ ├── hybrid │ │ │ └── html │ │ │ │ ├── index.html │ │ │ │ └── uni.webview.1.5.3.js │ │ └── changelog.md │ ├── Upload │ │ ├── README.md │ │ └── Image.vue │ ├── InconFonts.vue │ ├── global │ │ ├── g-icon-fonts.vue │ │ ├── g-statusbar.vue │ │ ├── g-popup.vue │ │ ├── g-page.vue │ │ └── g-confirm.vue │ ├── BookTip.vue │ ├── TabBar.vue │ ├── Expand.vue │ └── share.vue ├── utils │ ├── Config.ts │ ├── Control.ts │ └── request.ts ├── App.vue ├── directives │ └── index.ts ├── store │ ├── User.ts │ ├── AppOption.ts │ └── index.ts ├── uni.scss ├── api │ └── common.ts ├── pages │ ├── blank │ │ ├── policy.vue │ │ ├── about.vue │ │ ├── feedback.vue │ │ ├── agreement.vue │ │ └── origin.vue │ ├── tabBar │ │ ├── components │ │ │ ├── groupItem.vue │ │ │ ├── addGroup.vue │ │ │ └── bookDetail.vue │ │ ├── export.vue │ │ └── personal.vue │ └── reader │ │ └── components │ │ ├── Renderjs.vue │ │ └── Origin.vue ├── pages.json ├── styles │ ├── loading.scss │ └── index.scss ├── manifest.json └── type.d.ts ├── .prettierignore ├── README.md ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── .prettierrc.js ├── index.html └── package.json /src/static/README.md: -------------------------------------------------------------------------------- 1 | # 静态文件存放目录 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /node_modules/** 3 | /public/* 4 | **/*.svg 5 | **/*.sh -------------------------------------------------------------------------------- /src/static/guanyu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/guanyu.png -------------------------------------------------------------------------------- /src/static/lianxi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/lianxi.png -------------------------------------------------------------------------------- /src/static/lishi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/lishi.png -------------------------------------------------------------------------------- /src/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/logo.png -------------------------------------------------------------------------------- /src/static/weixin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/weixin.png -------------------------------------------------------------------------------- /src/static/yejian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/yejian.png -------------------------------------------------------------------------------- /src/static/yijian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/yijian.png -------------------------------------------------------------------------------- /src/static/fengmian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/fengmian.png -------------------------------------------------------------------------------- /src/static/fenxiang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/fenxiang.png -------------------------------------------------------------------------------- /src/static/home_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/home_off.png -------------------------------------------------------------------------------- /src/static/home_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/home_on.png -------------------------------------------------------------------------------- /src/static/logo_wx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/logo_wx.png -------------------------------------------------------------------------------- /src/static/logo_zfb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/logo_zfb.png -------------------------------------------------------------------------------- /src/static/nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/nothing.png -------------------------------------------------------------------------------- /src/static/nothing1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/nothing1.png -------------------------------------------------------------------------------- /src/static/nothing2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/nothing2.png -------------------------------------------------------------------------------- /src/static/nothing3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/nothing3.png -------------------------------------------------------------------------------- /src/static/qingchu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/qingchu.png -------------------------------------------------------------------------------- /src/static/shuyuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/shuyuan.png -------------------------------------------------------------------------------- /src/static/user_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/user_img.png -------------------------------------------------------------------------------- /src/static/yejian1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/yejian1.png -------------------------------------------------------------------------------- /src/static/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/arrow-right.png -------------------------------------------------------------------------------- /src/static/none_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/none_data.png -------------------------------------------------------------------------------- /src/static/pengyouquan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/pengyouquan.png -------------------------------------------------------------------------------- /src/static/personal_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/personal_on.png -------------------------------------------------------------------------------- /src/static/user_female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/user_female.png -------------------------------------------------------------------------------- /src/static/user_male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/user_male.png -------------------------------------------------------------------------------- /src/static/default_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/default_head.png -------------------------------------------------------------------------------- /src/static/personal_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/personal_off.png -------------------------------------------------------------------------------- /src/static/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/static/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/iconfont/iconfont.woff -------------------------------------------------------------------------------- /src/static/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herozse/eReader/HEAD/src/static/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | export function createApp() { 5 | const app = createSSRApp(App); 6 | return { 7 | app, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | /** 用户信息类型 */ 2 | export interface UserInfo { 3 | /** 登录凭据 */ 4 | token: string; 5 | /** 用户手机号 */ 6 | phone: number | ""; 7 | /** 用户`id` */ 8 | id: number | ""; 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eReader 2 | 3 | ## 介绍 4 | 5 | 1. h5 6 | 7 | ``` 8 | npm install ( 若node版本冲突,使用 npm install --force ) 9 | npm run dev 10 | ``` 11 | 12 | 2. ios 13 | 14 | [ipa自签教程](https://blog.csdn.net/m0_66504310/article/details/129842809) 15 | 16 | 3. Android 17 | -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import { getTopBook } from "./top"; 2 | import { searchBook } from "./search"; 3 | import { getCatalogs } from "./catalog"; 4 | import { getPageContent } from "./content"; 5 | 6 | export default { 7 | getTopBook, 8 | searchBook, 9 | getCatalogs, 10 | getPageContent, 11 | }; 12 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import { DefineComponent } from "vue" 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | unpackage/ 4 | dist/ 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .project 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw* 24 | 25 | package-lock.json 26 | yarn.lock 27 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { defineConfig } from "vite"; 3 | import uni from "@dcloudio/vite-plugin-uni"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [uni()], 8 | base: "./", 9 | resolve: { 10 | alias: { 11 | "@": path.resolve(__dirname, "src") 12 | } 13 | }, 14 | server: { 15 | port: 2023, 16 | host: "0.0.0.0" 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/painter/components/l-painter-qrcode/l-painter-qrcode.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 25 | 26 | 28 | -------------------------------------------------------------------------------- /src/components/painter/components/l-painter-image/l-painter-image.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 26 | 27 | 29 | -------------------------------------------------------------------------------- /src/components/Upload/README.md: -------------------------------------------------------------------------------- 1 | # 上传组件 2 | 3 | ## 上传图片组件 4 | 5 | 使用示例 6 | 7 | ```html 8 | 14 | 27 | ``` 28 | -------------------------------------------------------------------------------- /src/components/painter/components/l-painter-view/l-painter-view.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "lib": ["esnext", "dom"], 13 | "baseUrl": "./", 14 | "paths": { 15 | "@/*": ["src/*"] 16 | }, 17 | "types": ["@dcloudio/types", "@types/node"] 18 | }, 19 | "include": [ 20 | "node_modules/@types/lodash", 21 | "src/**/*.ts", 22 | "src/**/*.d.ts", 23 | "src/**/*.tsx", 24 | "src/**/*.vue", 25 | "src/**/**/*.vue" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/InconFonts.vue: -------------------------------------------------------------------------------- 1 | 8 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/painter/components/l-painter-text/l-painter-text.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /src/utils/Config.ts: -------------------------------------------------------------------------------- 1 | function moduleConfig() { 2 | const env = process.env.NODE_ENV === "development" ? "dev" : "prod"; 3 | 4 | const url = { 5 | dev: `http://127.0.0.1:3000`, 6 | prod: "http://127.0.0.1:3000", 7 | }; 8 | 9 | return { 10 | /** 请求超时毫秒 */ 11 | get requestOvertime() { 12 | return 60000; 13 | }, 14 | /** `api`请求域名 */ 15 | get apiUrl() { 16 | return url[env]; 17 | }, 18 | /** 当前环境模式 */ 19 | get env() { 20 | return env; 21 | }, 22 | /** 上传图片地址 */ 23 | get uploadUrl() { 24 | return "http://xxx.com/upload"; 25 | }, 26 | }; 27 | } 28 | 29 | /** 配置模块 */ 30 | const config = moduleConfig(); 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, //单行长度 3 | tabWidth: 2, //缩进长度 4 | useTabs: false, //使用空格代替tab缩进 5 | semi: true, //句末使用分号 6 | singleQuote: false, //使用单引号 7 | quoteProps: "as-needed", //仅在必需时为对象的key添加引号 8 | trailingComma: "all", //多行时尽可能打印尾随逗号 9 | bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar } 10 | jsxBracketSameLine: true, //多属性html标签的‘>’折行放置 11 | requirePragma: false, //无需顶部注释即可格式化 12 | insertPragma: false, //在已被preitter格式化的文件顶部加上标注 13 | proseWrap: "preserve", //markdown字符数超过保持原样 14 | htmlWhitespaceSensitivity: "ignore", //对HTML全局空白不敏感 15 | vueIndentScriptAndStyle: false, //不对vue中的script及style标签缩进 16 | endOfLine: "lf", //结束行形式 17 | embeddedLanguageFormatting: "auto", //对引用代码进行格式化 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/global/g-icon-fonts.vue: -------------------------------------------------------------------------------- 1 | 8 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 26 | 41 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from "vue"; 2 | 3 | // 声明模块扩展 4 | declare module "vue" { 5 | interface DirectiveBindings { 6 | value: () => void; 7 | } 8 | } 9 | 10 | export const vLongPress: Directive = { 11 | mounted(el, binding) { 12 | let timer: NodeJS.Timeout | null = null; 13 | 14 | const start = () => { 15 | timer = setTimeout(() => { 16 | binding.value(); // 触发长按逻辑 17 | }, 350); // 长按时间设为 350 毫秒 18 | }; 19 | 20 | const cancel = () => { 21 | if (timer) { 22 | clearTimeout(timer); 23 | timer = null; 24 | } 25 | }; 26 | 27 | el.addEventListener("touchstart", start); 28 | el.addEventListener("touchend", cancel); 29 | el.addEventListener("touchmove", cancel); 30 | }, 31 | 32 | unmounted(el) { 33 | const cancel = () => {}; // 避免卸载时使用未定义的函数 34 | el.removeEventListener("touchstart", cancel); 35 | el.removeEventListener("touchend", cancel); 36 | el.removeEventListener("touchmove", cancel); 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/store/User.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { modifyData } from "@/utils"; 3 | import { UserInfo } from "@/types/user"; 4 | 5 | const cacheName = "user-info"; 6 | 7 | function useUserInfo(): UserInfo { 8 | return { 9 | id: "", 10 | token: "", 11 | phone: "" 12 | } 13 | } 14 | 15 | /** 16 | * 用户状态管理模块 17 | */ 18 | export default class ModuleUser { 19 | constructor() { 20 | this.init(); 21 | } 22 | 23 | /** 初始化用户数据(从本地获取) */ 24 | private init() { 25 | const data = uni.getStorageSync(cacheName); 26 | if (data) { 27 | this.update(JSON.parse(data)); 28 | } 29 | } 30 | 31 | /** 用户信息 */ 32 | readonly info = reactive>(useUserInfo()); 33 | 34 | /** 35 | * 更新用户信息字段 36 | * @param value 修改的值 37 | */ 38 | update(value: Partial) { 39 | modifyData(this.info, value); 40 | uni.setStorageSync(cacheName, JSON.stringify(this.info)); 41 | } 42 | 43 | /** 重置用户信息 */ 44 | reset() { 45 | modifyData(this.info, useUserInfo()); 46 | uni.setStorageSync(cacheName, JSON.stringify(this.info)); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/uni.scss: -------------------------------------------------------------------------------- 1 | // 每个页面需要引入的文件,注意:vue.config.js 中配置的 css.loaderOptions 是无法在当前项目生效的, 2 | // 可能是uni-app项目设定和标准vue-cli项目设定不一样导致的,需要在当前文件全局引入即可 3 | // 全局样式写在`App.vue`里面,一些预处理工具样式可以写这里,因为每个页面都会注入一遍 4 | // 全局样式不写在这里的原因是因为每个页面都注入一遍的话会导致`html-style`有多个相同的代码标签 5 | 6 | $white: #fff; 7 | $black: #010101; 8 | $dark: #222; 9 | $red: #fa2d2d; 10 | $pink: #f04e7d; 11 | $blue: #2c72f3; 12 | 13 | // 溢出...显示 14 | @mixin ellipsis($number) { 15 | display: -webkit-box; 16 | -webkit-box-orient: vertical; 17 | -webkit-line-clamp: $number; 18 | overflow: hidden; 19 | } 20 | 21 | @mixin button($color, $bg) { 22 | font-size: 30rpx; 23 | min-width: 160rpx; 24 | padding: 0 20rpx; 25 | border-radius: 2px; 26 | height: 80rpx; 27 | background-color: $bg; 28 | border-color: $bg; 29 | color: $color; 30 | line-height: 1; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | 35 | &::after, 36 | &::before { 37 | border: none; 38 | background-color: transparent; 39 | } 40 | 41 | &:hover { 42 | background-color: $bg; 43 | border-color: $bg; 44 | color: $color; 45 | } 46 | 47 | &:active { 48 | opacity: 0.85; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/painter/components/l-painter/props.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | board: Object, 4 | pathType: String, // 'base64'、'url' 5 | fileType: { 6 | type: String, 7 | default: 'png' 8 | }, 9 | hidden: Boolean, 10 | quality: { 11 | type: Number, 12 | default: 1 13 | }, 14 | css: [String, Object], 15 | // styles: [String, Object], 16 | width: [Number, String], 17 | height: [Number, String], 18 | pixelRatio: Number, 19 | customStyle: String, 20 | isCanvasToTempFilePath: Boolean, 21 | // useCanvasToTempFilePath: Boolean, 22 | sleep: { 23 | type: Number, 24 | default: 1000 / 30 25 | }, 26 | beforeDelay: { 27 | type: Number, 28 | default: 100 29 | }, 30 | afterDelay: { 31 | type: Number, 32 | default: 100 33 | }, 34 | performance: Boolean, 35 | // #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY 36 | type: { 37 | type: String, 38 | default: '2d' 39 | }, 40 | // #endif 41 | // #ifdef APP-NVUE 42 | hybrid: Boolean, 43 | timeout: { 44 | type: Number, 45 | default: 2000 46 | }, 47 | // #endif 48 | // #ifdef H5 || APP-PLUS 49 | useCORS: Boolean, 50 | hidpi: { 51 | type: Boolean, 52 | default: true 53 | } 54 | // #endif 55 | } 56 | } -------------------------------------------------------------------------------- /src/api/common.ts: -------------------------------------------------------------------------------- 1 | import { requestService, requestCrawlerHtml } from "@/utils/request"; 2 | 3 | export function parseBookHtml(params: { origin: string }) { 4 | return requestCrawlerHtml(params.origin); 5 | } 6 | 7 | export function searchBook(params: { 8 | fuzzy: string; 9 | limit?: number; 10 | pageSize: number; 11 | pageNo: number; 12 | origins?: Array; 13 | }) { 14 | return requestService<{ data: searchBookList[]; complete: boolean }>("POST", "/books/search", params); 15 | } 16 | export function getCatalogs(params: { url: string }) { 17 | return requestService("POST", "/books/getCatalogs", params); 18 | } 19 | 20 | export function getContent(params: { url: string }) { 21 | return requestService<{ title: string; text: string }>("POST", "/books/getContent", params); 22 | } 23 | 24 | export function getTopBooks(params: { gender?: string; cate?: string; rank?: string }) { 25 | return requestService("POST", "/books/getTopBooks", params); 26 | } 27 | 28 | // APP端 29 | export async function getTopBooksByApp(url: string) { 30 | return requestCrawlerHtml(url); 31 | } 32 | 33 | export function searchBookByApp(url: string, method?: RequestMethod, data?: any, options?: object) { 34 | return requestCrawlerHtml(url, method, data, options); 35 | } 36 | 37 | export function getCatalogsByApp(url: string) { 38 | return requestCrawlerHtml(url); 39 | } 40 | 41 | export function getContentByApp(url: string) { 42 | return requestCrawlerHtml(url); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/BookTip.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 38 | 39 | 78 | -------------------------------------------------------------------------------- /src/pages/blank/policy.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 48 | 49 | 68 | -------------------------------------------------------------------------------- /src/components/global/g-statusbar.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 57 | 58 | 88 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // import { ComponentInternalInstance } from "vue"; 2 | 3 | // /** 4 | // * `vue3`类型扩充 5 | // * - 弥补官方的类型不足问题 6 | // */ 7 | // export declare namespace Vue3 { 8 | // /** 9 | // * `vue`实例上下文 10 | // * - `vue3`类型文件中貌似没有暴露可以使用的类型,所以自定义补充一个接口代替,和`vue2`中一样的类型一致 11 | // */ 12 | // interface Ctx { 13 | // /** 当前组件节点 */ 14 | // readonly $el: HTMLElement 15 | // /** 父节点 */ 16 | // readonly $parent?: Ctx 17 | // /** 当前组件实例 */ 18 | // readonly $root: Ctx 19 | // /** 选项配置 */ 20 | // readonly $options: { 21 | // name: string 22 | // } 23 | // } 24 | // /** 25 | // * `hooks: getCurrentInstance()`钩子函数返回的类型扩充 26 | // */ 27 | // interface Instance extends ComponentInternalInstance { 28 | // /** 实例上下文对象 */ 29 | // ctx: Ctx 30 | // } 31 | // } 32 | 33 | /** 上传图片返回结果 */ 34 | export interface UploadChange { 35 | /** 和当前上传组件绑定的`id` */ 36 | id: string | number; 37 | /** 图片路径 */ 38 | src: string; 39 | } 40 | 41 | /** 表单规则类型 */ 42 | export interface TheFormRulesItem { 43 | /** 是否必填项 */ 44 | required?: boolean; 45 | /** 提示字段 */ 46 | message?: string; 47 | /** 指定类型 */ 48 | type?: "number" | "array"; 49 | /** 50 | * 自定义的校验规则(正则) 51 | * - 考虑到微信一些特殊的抽风机制,在微信小程序中,除`number|string|object|undefined|null`这几个基础类型外,其他类型是会被过滤掉,所以这里在写正则的时候,在末尾加上`.toString()`即可 52 | */ 53 | reg?: string; // | RegExp 54 | } 55 | 56 | /** 表单规则类型 */ 57 | export type TheFormRules = { [key: string]: Array }; 58 | 59 | /** `label`布局位置 */ 60 | export type LabelPosition = "left" | "right" | "top"; 61 | 62 | /** 表单验证回调类型 */ 63 | export interface TheFormValidateCallback { 64 | ( 65 | /** 是否验证通过 */ 66 | isValid: boolean, 67 | /** 验证不通过的规则列表 */ 68 | rules: { [key: string]: Array }, 69 | ): void; 70 | } 71 | 72 | /** 选择器`item`数据 */ 73 | export interface PickerSelectItem { 74 | /** 展示字段 */ 75 | label: string; 76 | /** 对应的值 */ 77 | value: T; 78 | /** 79 | * 下级数据 80 | * @description 最多三层,选择器栏目数根据当前下级动态显示 81 | */ 82 | children?: Array>; 83 | /** 其他携带的值 */ 84 | [key: string]: any; 85 | } 86 | -------------------------------------------------------------------------------- /src/components/painter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "lime-painter", 3 | "displayName": "海报画板", 4 | "version": "1.9.6.6", 5 | "description": "一款canvas海报组件,更优雅的海报生成方案,有限的支持富文本", 6 | "keywords": [ 7 | "海报", 8 | "富文本", 9 | "生成海报", 10 | "生成二维码", 11 | "JSON" 12 | ], 13 | "repository": "https://gitee.com/liangei/lime-painter", 14 | "engines": { 15 | "HBuilderX": "^3.4.14" 16 | }, 17 | "dcloudext": { 18 | "sale": { 19 | "regular": { 20 | "price": "0.00" 21 | }, 22 | "sourcecode": { 23 | "price": "0.00" 24 | } 25 | }, 26 | "contact": { 27 | "qq": "305716444" 28 | }, 29 | "declaration": { 30 | "ads": "无", 31 | "data": "无", 32 | "permissions": "无" 33 | }, 34 | "npmurl": "", 35 | "type": "component-vue" 36 | }, 37 | "uni_modules": { 38 | "dependencies": [], 39 | "encrypt": [], 40 | "platforms": { 41 | "cloud": { 42 | "tcb": "y", 43 | "aliyun": "y", 44 | "alipay": "n" 45 | }, 46 | "client": { 47 | "App": { 48 | "app-vue": "y", 49 | "app-nvue": "y" 50 | }, 51 | "H5-mobile": { 52 | "Safari": "y", 53 | "Android Browser": "y", 54 | "微信浏览器(Android)": "y", 55 | "QQ浏览器(Android)": "y" 56 | }, 57 | "H5-pc": { 58 | "Chrome": "y", 59 | "IE": "u", 60 | "Edge": "u", 61 | "Firefox": "u", 62 | "Safari": "y" 63 | }, 64 | "小程序": { 65 | "微信": "y", 66 | "阿里": "y", 67 | "百度": "y", 68 | "字节跳动": "y", 69 | "QQ": "y", 70 | "钉钉": "u", 71 | "快手": "u", 72 | "飞书": "u", 73 | "京东": "u" 74 | }, 75 | "快应用": { 76 | "华为": "u", 77 | "联盟": "u" 78 | }, 79 | "Vue": { 80 | "vue2": "y", 81 | "vue3": "y" 82 | } 83 | } 84 | } 85 | }, 86 | "name": "lime-painter", 87 | "main": "index.js", 88 | "scripts": { 89 | "test": "echo \"Error: no test specified\" && exit 1" 90 | }, 91 | "author": "", 92 | "license": "ISC" 93 | } 94 | -------------------------------------------------------------------------------- /src/pages/blank/about.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | 34 | 86 | -------------------------------------------------------------------------------- /src/pages/tabBar/components/groupItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 43 | 44 | 90 | -------------------------------------------------------------------------------- /src/components/global/g-popup.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 85 | 86 | 100 | -------------------------------------------------------------------------------- /src/parser/top.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | import * as api from "@/api/common"; 3 | 4 | export function getTopBook(params: { gender: string; cate: string; rank: string }) { 5 | return new Promise((resolve, reject) => { 6 | const url = formatUrl("https://vt.quark.cn/blm/novel-rank-627/index", params); 7 | api 8 | .getTopBooksByApp(url) 9 | .then((res) => { 10 | const value = paraseSearchContent(res as string); 11 | const result: TopBookInfo[] = handlerSearch(value); 12 | resolve(result); 13 | }) 14 | .catch((e) => { 15 | reject(e); 16 | }); 17 | }); 18 | } 19 | 20 | function paraseSearchContent(res: string) { 21 | const $ = cheerio.load(res); 22 | let targetScript = ""; 23 | let initialPropsStr = null; 24 | $("script").each((index, element) => { 25 | const scriptText = $(element).text(); 26 | if (scriptText.includes("window.__INITIAL_PROPS__")) { 27 | targetScript = scriptText; 28 | return false; // 找到后就停止遍历 29 | } 30 | }); 31 | 32 | // 找到window.__INITIAL_PROPS__在脚本中的起始位置 33 | const startIndex = targetScript.indexOf("window.__INITIAL_PROPS__ ="); 34 | if (startIndex !== -1) { 35 | // 提取等号后面的JSON字符串部分(去除末尾可能多余的分号等) 36 | let jsonStr = targetScript.substring(startIndex + "window.__INITIAL_PROPS__ =".length).trim(); 37 | if (jsonStr.endsWith(";")) { 38 | jsonStr = jsonStr.slice(0, -1).trim(); 39 | } 40 | try { 41 | initialPropsStr = JSON.parse(jsonStr); 42 | } catch (error) { 43 | initialPropsStr = {}; 44 | console.error("解析JSON字符串失败:", error); 45 | } 46 | } 47 | return initialPropsStr; 48 | } 49 | 50 | const handlerSearch = (value: any) => { 51 | let bookList = value?.initialData?.data?.item?.novel_list?.novel_item || []; 52 | bookList = bookList.map((i: any) => { 53 | return { 54 | bookName: i.title, 55 | author: i.author, 56 | image: i.icon, 57 | categories: i.second_category, 58 | description: i.description, 59 | status: i.status, 60 | tag: i.tag, 61 | }; 62 | }); 63 | 64 | return bookList; 65 | }; 66 | 67 | const formatUrl = (origin: string, params: { gender: string; cate: string; rank: string }) => { 68 | let str = `${origin}`; 69 | 70 | Object.entries(params).forEach(([key, value], index) => { 71 | if (index === 0) { 72 | str += `?${key}=${value}`; 73 | } else { 74 | str += `&${key}=${value}`; 75 | } 76 | }); 77 | 78 | return str; 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/global/g-page.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 66 | -------------------------------------------------------------------------------- /src/pages/blank/feedback.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 62 | 63 | 99 | -------------------------------------------------------------------------------- /src/components/TabBar.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 86 | 87 | 102 | -------------------------------------------------------------------------------- /src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "path": "pages/tabBar/home", 5 | "style": { 6 | "navigationBarTitleText": "首页" 7 | } 8 | }, 9 | { 10 | "path": "pages/tabBar/personal", 11 | "style": { 12 | "navigationBarTitleText": "个人页" 13 | } 14 | }, 15 | { 16 | "path": "pages/tabBar/export", 17 | "style": { 18 | "navigationBarTitleText": "导入" 19 | } 20 | }, 21 | { 22 | "path": "pages/tabBar/book", 23 | "style": { 24 | "navigationBarTitleText": "搜书" 25 | } 26 | }, 27 | { 28 | "path": "pages/search/index", 29 | "style": { 30 | "navigationBarTitleText": "搜书" 31 | } 32 | }, 33 | { 34 | "path": "pages/groupDetail/index", 35 | "style": { 36 | "navigationBarTitleText": "分组详情" 37 | } 38 | }, 39 | { 40 | "path": "pages/bookDetail/index", 41 | "style": { 42 | "navigationBarTitleText": "书籍详情" 43 | } 44 | }, 45 | { 46 | "path": "pages/catalogs/index", 47 | "style": { 48 | "navigationBarTitleText": "目录" 49 | } 50 | }, 51 | { 52 | "path": "pages/reader/index", 53 | "style": { 54 | "navigationBarTitleText": "阅读页", 55 | "disableSwipeBack": true 56 | } 57 | }, 58 | { 59 | "path": "pages/blank/about", 60 | "style": { 61 | "navigationBarTitleText": "关于我们" 62 | } 63 | }, 64 | { 65 | "path": "pages/blank/agreement", 66 | "style": { 67 | "navigationBarTitleText": "用户协议" 68 | } 69 | }, 70 | { 71 | "path": "pages/blank/policy", 72 | "style": { 73 | "navigationBarTitleText": "隐私政策" 74 | } 75 | }, 76 | { 77 | "path": "pages/blank/origin", 78 | "style": { 79 | "navigationBarTitleText": "书源管理" 80 | } 81 | }, 82 | { 83 | "path": "pages/blank/history", 84 | "style": { 85 | "navigationBarTitleText": "浏览历史" 86 | } 87 | }, 88 | { 89 | "path": "pages/blank/feedback", 90 | "style": { 91 | "navigationBarTitleText": "意见反馈" 92 | } 93 | } 94 | ], 95 | "globalStyle": { 96 | "navigationBarTextStyle": "black", 97 | "navigationBarTitleText": "uni-app", 98 | "navigationBarBackgroundColor": "#FEF7F1", 99 | "backgroundColor": "#FEF7F1", 100 | "app-plus": { 101 | "bounce": "none" 102 | }, 103 | "navigationStyle": "custom" 104 | }, 105 | "easycom": { 106 | "autoscan": true, 107 | "custom": { 108 | "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue", 109 | "^g-(.*)": "@/components/global/g-$1.vue", 110 | "^l-(.*)": "@/components/painter/components/l-$1/l-$1.vue" 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/store/AppOption.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { modifyData } from "@/utils"; 3 | export default class ModuleAppOption { 4 | /** `APP`操作信息 */ 5 | readonly appOption = reactive({ 6 | theme: "light", 7 | /** `小程序`导航栏高度 */ 8 | navBarHeight: 0, 9 | /** `小程序`胶囊距右方间距(方保持左、右间距一致) */ 10 | menuRight: 0, 11 | /** `小程序`胶囊距底部间距(保持底部间距一致) */ 12 | menuBottom: 0, 13 | /** `小程序`胶囊高度(自定义内容可与胶囊高度保证一致) */ 14 | menuHeight: 0, 15 | /** `小程序`胶囊宽度 */ 16 | menuWidth: 0, 17 | /** 状态栏高度 */ 18 | statusBarHeight: 0, 19 | /** 原生底部`tabbar`高度 */ 20 | tabBarHeight: 0, 21 | /** 可使用窗口高度 */ 22 | windowHeight: 0, 23 | /** 可使用窗口宽度 */ 24 | windowWidth: 0, 25 | /** 屏幕宽度 */ 26 | screenWidth: 0, 27 | /** 屏幕高度 */ 28 | screenHeight: 0, 29 | /** 是否为`iPhoneX`系列(做底部`UI`判断) */ 30 | isIPhoneX: false, 31 | }); 32 | 33 | /** 34 | * 初始化`APP`操作信息 35 | * @description 最好放在`App.onLaunch`执行,因为这时才是页页面初始化完成,各个尺寸值会比较准确 36 | * @learn 条件编译 https://uniapp.dcloud.io/platform 37 | */ 38 | initAppOption() { 39 | const systemInfo = uni.getSystemInfoSync(); 40 | this.appOption.statusBarHeight = systemInfo.statusBarHeight!; 41 | this.appOption.tabBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - systemInfo.statusBarHeight!; 42 | this.appOption.windowHeight = systemInfo.windowHeight; 43 | this.appOption.windowWidth = systemInfo.windowWidth; 44 | this.appOption.screenWidth = systemInfo.screenWidth; 45 | this.appOption.screenHeight = systemInfo.screenHeight; 46 | 47 | const isIos = systemInfo.system.toLocaleLowerCase().includes("ios"); 48 | const vaule = systemInfo.screenWidth / systemInfo.screenHeight < 0.5; 49 | this.appOption.isIPhoneX = isIos && vaule; 50 | 51 | // #ifdef H5 52 | this.appOption.tabBarHeight = 50; 53 | this.appOption.isIPhoneX = false; // 网页端不需要判断底部UI判断 54 | // #endif 55 | 56 | // #ifdef MP 57 | const menuButtonInfo = uni.getMenuButtonBoundingClientRect(); 58 | // 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度 59 | this.appOption.navBarHeight = 60 | (menuButtonInfo.top - systemInfo.statusBarHeight!) * 2 + menuButtonInfo.height + systemInfo.statusBarHeight!; 61 | this.appOption.menuRight = systemInfo.screenWidth - menuButtonInfo.right; 62 | this.appOption.menuBottom = menuButtonInfo.top - systemInfo.statusBarHeight!; 63 | this.appOption.menuHeight = menuButtonInfo.height; 64 | this.appOption.menuWidth = menuButtonInfo.width; 65 | // #endif 66 | 67 | this.initAppStorage(); 68 | } 69 | 70 | initAppStorage() { 71 | const data = uni.getStorageSync("app-option"); 72 | if (data) { 73 | modifyData(this.appOption, JSON.parse(data)); 74 | } else { 75 | this.saveAppOption(); 76 | } 77 | } 78 | 79 | saveAppOption() { 80 | uni.setStorageSync("app-option", JSON.stringify(this.appOption)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/pages/reader/components/Renderjs.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | 92 | 93 | -------------------------------------------------------------------------------- /src/styles/loading.scss: -------------------------------------------------------------------------------- 1 | .preloader-inner { 2 | position: relative; 3 | display: block; 4 | width: 100%; 5 | height: 100%; 6 | animation: md-preloader-inner-rotate 5.25s cubic-bezier(0.35, 0, 0.25, 1) infinite; 7 | 8 | .preloader-inner-gap { 9 | position: absolute; 10 | width: 2px; 11 | left: 50%; 12 | margin-left: -1px; 13 | top: 0; 14 | bottom: 0; 15 | box-sizing: border-box; 16 | border-top: 3px solid #010101; 17 | } 18 | 19 | .preloader-inner-left, 20 | .preloader-inner-right { 21 | position: absolute; 22 | top: 0; 23 | height: 100%; 24 | width: 50%; 25 | overflow: hidden; 26 | } 27 | 28 | .preloader-inner-half-circle { 29 | position: absolute; 30 | top: 0; 31 | height: 100%; 32 | width: 200%; 33 | box-sizing: border-box; 34 | border: 3px solid #010101; 35 | border-bottom-color: transparent !important; 36 | border-radius: 50%; 37 | animation-iteration-count: infinite; 38 | animation-duration: 1.3125s; 39 | animation-timing-function: cubic-bezier(0.35, 0, 0.25, 1); 40 | } 41 | 42 | .preloader-inner-left { 43 | left: 0; 44 | 45 | .preloader-inner-half-circle { 46 | left: 0; 47 | border-right-color: transparent !important; 48 | animation-name: md-preloader-left-rotate; 49 | } 50 | } 51 | 52 | .preloader-inner-right { 53 | right: 0; 54 | 55 | .preloader-inner-half-circle { 56 | right: 0; 57 | border-left-color: transparent !important; 58 | animation-name: md-preloader-right-rotate; 59 | } 60 | } 61 | } 62 | 63 | @keyframes md-preloader-left-rotate { 64 | 0%, 65 | 100% { 66 | transform: rotate(130deg); 67 | } 68 | 69 | 50% { 70 | transform: rotate(-5deg); 71 | } 72 | } 73 | 74 | @keyframes md-preloader-right-rotate { 75 | 0%, 76 | 100% { 77 | transform: rotate(-130deg); 78 | } 79 | 80 | 50% { 81 | transform: rotate(5deg); 82 | } 83 | } 84 | 85 | @keyframes md-preloader-inner-rotate { 86 | 12.5% { 87 | transform: rotate(135deg); 88 | } 89 | 90 | 25% { 91 | transform: rotate(270deg); 92 | } 93 | 94 | 37.5% { 95 | transform: rotate(405deg); 96 | } 97 | 98 | 50% { 99 | transform: rotate(540deg); 100 | } 101 | 102 | 62.5% { 103 | transform: rotate(675deg); 104 | } 105 | 106 | 75% { 107 | transform: rotate(810deg); 108 | } 109 | 110 | 87.5% { 111 | transform: rotate(945deg); 112 | } 113 | 114 | 100% { 115 | transform: rotate(1080deg); 116 | } 117 | } 118 | 119 | @keyframes md-preloader-outer { 120 | 0% { 121 | transform: rotate(0); 122 | } 123 | 124 | 100% { 125 | transform: rotate(360deg); 126 | } 127 | } 128 | 129 | .preloader { 130 | display: block; 131 | width: 40px; 132 | height: 40px; 133 | animation: md-preloader-outer 3.3s linear infinite; 134 | } 135 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "e-reader", 3 | "appid" : "__UNI__5E3CD06", 4 | "description" : "", 5 | "versionName" : "1.0.0", 6 | "versionCode" : "100", 7 | "transformPx" : false, 8 | "app-plus" : { 9 | "usingComponents" : true, 10 | "nvueStyleCompiler" : "uni-app", 11 | "compilerVersion" : 3, 12 | "splashscreen" : { 13 | "alwaysShowBeforeRender" : true, 14 | "waiting" : true, 15 | "autoclose" : true, 16 | "delay" : 0 17 | }, 18 | "modules" : { 19 | "Share" : {} 20 | }, 21 | "distribute" : { 22 | "android" : { 23 | "permissions" : [ 24 | "", 25 | "", 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "" 39 | ] 40 | }, 41 | "ios" : { 42 | "idfa" : true, 43 | "dSYMs" : false 44 | }, 45 | "sdkConfigs" : { 46 | "share" : {} 47 | }, 48 | "splashscreen" : { 49 | "androidStyle" : "common" 50 | } 51 | }, 52 | "nvueLaunchMode" : "" 53 | }, 54 | "quickapp" : {}, 55 | "mp-weixin" : { 56 | "appid" : "", 57 | "setting" : { 58 | "urlCheck" : false 59 | }, 60 | "usingComponents" : true 61 | }, 62 | "mp-alipay" : { 63 | "usingComponents" : true 64 | }, 65 | "mp-baidu" : { 66 | "usingComponents" : true 67 | }, 68 | "mp-toutiao" : { 69 | "usingComponents" : true 70 | }, 71 | "uniStatistics" : { 72 | "enable" : false 73 | }, 74 | "vueVersion" : "3" 75 | } 76 | -------------------------------------------------------------------------------- /src/parser/content.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | import * as api from "@/api/common"; 3 | import sourceJson from "./source"; 4 | import { URLPolyfill } from "@/utils"; 5 | 6 | const source = sourceJson.local; 7 | type ContentParser = { title: string; text: string }; 8 | 9 | export const getPageContent = async (params: { url: string }) => { 10 | const r = await getDeepthPage(params.url) as ContentParser; 11 | let content: ContentParser = { 12 | title: "Not Foud", 13 | text: "找不到资源", 14 | }; 15 | if (r) { 16 | content = r; 17 | } 18 | const result: ContentParser = handlerPage(content, params.url); 19 | 20 | return Promise.resolve(result); 21 | }; 22 | 23 | const getDeepthPage = async (url: string, content: AnyObject = {}) => { 24 | try { 25 | const res = (await api.getContentByApp(url)) as string; 26 | if (res) { 27 | const parse = URLPolyfill(url); 28 | const targetSource = source.find((i) => i.origin === parse.origin); 29 | if (targetSource) { 30 | const $ = cheerio.load(res); 31 | const { wrapContainer, titleContainer, pagination, lineBreak } = targetSource.content.rules; 32 | 33 | if (!content.isParse) { 34 | content = { 35 | isParse: true, 36 | text: "", 37 | title: $(titleContainer.selector).text() || "", 38 | }; 39 | } 40 | if (lineBreak === "br") { 41 | const txtElement = $(wrapContainer.selector); 42 | // 将
标签替换为 \n 43 | const modifiedHtml = txtElement.html()?.replace(//g, "\n"); 44 | // 获取修改后的文本内容 45 | const text = cheerio.load(modifiedHtml || "").text(); 46 | content.text += text; 47 | } else { 48 | $(wrapContainer.selector).each((index, element) => { 49 | const text = $(element).text(); 50 | content.text += `${text}\n`; 51 | }); 52 | } 53 | 54 | if (pagination) { 55 | const nextPagination = $(pagination.selector).last(); 56 | const nextPath = nextPagination.attr("href") || ""; 57 | const nextPathText = nextPagination.text(); 58 | 59 | if (nextPath.startsWith("/") && nextPathText === "下一页") { 60 | content = await getDeepthPage(`${parse.origin}${nextPath}`, content); 61 | } 62 | } 63 | } 64 | } 65 | } catch (error) { 66 | console.log(error, "error"); 67 | return content; 68 | } 69 | 70 | return content as ContentParser; 71 | }; 72 | 73 | const handlerPage = (content: ContentParser, url: string) => { 74 | const parse = URLPolyfill(url); 75 | const targetSource = source.find((i) => i.origin === parse.origin); 76 | let handler = targetSource?.content?.handler; 77 | 78 | let { text, title } = content; 79 | text = text.replace(/\n\s*\n/g, "\n"); 80 | const p = text 81 | .split(/\n/) 82 | .map((i) => i.trim()) 83 | .filter((i) => i); 84 | handler?.replace?.forEach((r) => { 85 | if (r.type === "Array") { 86 | p.splice(r.value, 1); 87 | } 88 | }); 89 | 90 | text = p.join("\n"); 91 | 92 | return { 93 | text, 94 | title, 95 | }; 96 | }; 97 | -------------------------------------------------------------------------------- /src/pages/blank/agreement.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 79 | 80 | 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e-reader", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "uni", 6 | "dev:custom": "uni -p", 7 | "dev:h5": "uni", 8 | "dev:h5:ssr": "uni --ssr", 9 | "dev:mp-alipay": "uni -p mp-alipay", 10 | "dev:mp-baidu": "uni -p mp-baidu", 11 | "dev:mp-jd": "uni -p mp-jd", 12 | "dev:mp-kuaishou": "uni -p mp-kuaishou", 13 | "dev:mp-lark": "uni -p mp-lark", 14 | "dev:mp-qq": "uni -p mp-qq", 15 | "dev:mp-toutiao": "uni -p mp-toutiao", 16 | "dev:mp-weixin": "uni -p mp-weixin", 17 | "dev:mp-xhs": "uni -p mp-xhs", 18 | "dev:quickapp-webview": "uni -p quickapp-webview", 19 | "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei", 20 | "dev:quickapp-webview-union": "uni -p quickapp-webview-union", 21 | "build:custom": "uni build -p", 22 | "build:h5": "uni build", 23 | "build:h5:ssr": "uni build --ssr", 24 | "build:mp-alipay": "uni build -p mp-alipay", 25 | "build:mp-baidu": "uni build -p mp-baidu", 26 | "build:mp-jd": "uni build -p mp-jd", 27 | "build:mp-kuaishou": "uni build -p mp-kuaishou", 28 | "build:mp-lark": "uni build -p mp-lark", 29 | "build:mp-qq": "uni build -p mp-qq", 30 | "build:mp-toutiao": "uni build -p mp-toutiao", 31 | "build:mp-weixin": "uni build -p mp-weixin", 32 | "build:mp-xhs": "uni build -p mp-xhs", 33 | "build:quickapp-webview": "uni build -p quickapp-webview", 34 | "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", 35 | "build:quickapp-webview-union": "uni build -p quickapp-webview-union" 36 | }, 37 | "dependencies": { 38 | "@dcloudio/uni-app": "3.0.0-4030620241128001", 39 | "@dcloudio/uni-app-harmony": "3.0.0-4030620241128001", 40 | "@dcloudio/uni-app-plus": "3.0.0-4030620241128001", 41 | "@dcloudio/uni-components": "3.0.0-4030620241128001", 42 | "@dcloudio/uni-h5": "3.0.0-4030620241128001", 43 | "@dcloudio/uni-mp-alipay": "3.0.0-4030620241128001", 44 | "@dcloudio/uni-mp-baidu": "3.0.0-4030620241128001", 45 | "@dcloudio/uni-mp-jd": "3.0.0-4030620241128001", 46 | "@dcloudio/uni-mp-kuaishou": "3.0.0-4030620241128001", 47 | "@dcloudio/uni-mp-lark": "3.0.0-4030620241128001", 48 | "@dcloudio/uni-mp-qq": "3.0.0-4030620241128001", 49 | "@dcloudio/uni-mp-toutiao": "3.0.0-4030620241128001", 50 | "@dcloudio/uni-mp-weixin": "3.0.0-4030620241128001", 51 | "@dcloudio/uni-mp-xhs": "3.0.0-4030620241128001", 52 | "@dcloudio/uni-quickapp-webview": "3.0.0-4030620241128001", 53 | "@dcloudio/uni-ui": "^1.5.7", 54 | "cheerio": "^1.0.0", 55 | "css-doodle": "^0.40.0", 56 | "lodash": "^4.17.21", 57 | "prettier": "^3.3.3", 58 | "uuid": "^11.0.2", 59 | "vue": "^3.4.21", 60 | "vue-i18n": "^9.1.9" 61 | }, 62 | "devDependencies": { 63 | "@dcloudio/types": "^3.4.8", 64 | "@dcloudio/uni-automator": "3.0.0-4030620241128001", 65 | "@dcloudio/uni-cli-shared": "3.0.0-4030620241128001", 66 | "@dcloudio/uni-stacktracey": "3.0.0-4030620241128001", 67 | "@dcloudio/vite-plugin-uni": "3.0.0-4030620241128001", 68 | "@types/lodash": "^4.17.13", 69 | "@types/node": "17.0.40", 70 | "@vue/runtime-core": "^3.4.21", 71 | "sass": "1.52.2", 72 | "typescript": "4.8.3", 73 | "vite": "5.2.8" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/painter/hybrid/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/components/global/g-confirm.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 80 | 81 | 120 | -------------------------------------------------------------------------------- /src/static/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 4659366 */ 3 | src: url('iconfont.woff2?t=1736477956755') format('woff2'), 4 | url('iconfont.woff?t=1736477956755') format('woff'), 5 | url('iconfont.ttf?t=1736477956755') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-jinyong:before { 17 | content: "\e66f"; 18 | } 19 | 20 | .icon-qiyong:before { 21 | content: "\e75e"; 22 | } 23 | 24 | .icon-fuzhilianjie:before { 25 | content: "\e62c"; 26 | } 27 | 28 | .icon-haibao:before { 29 | content: "\e674"; 30 | } 31 | 32 | .icon-fenzu1:before { 33 | content: "\e602"; 34 | } 35 | 36 | .icon-xinzeng:before { 37 | content: "\e759"; 38 | } 39 | 40 | .icon-xinzengfenzu:before { 41 | content: "\e746"; 42 | } 43 | 44 | .icon-huidaodingbu:before { 45 | content: "\e622"; 46 | } 47 | 48 | .icon-shujia:before { 49 | content: "\e63c"; 50 | } 51 | 52 | .icon-tianjiashujia:before { 53 | content: "\e634"; 54 | } 55 | 56 | .icon-zaishujia:before { 57 | content: "\e636"; 58 | } 59 | 60 | .icon-zhiding:before { 61 | content: "\e718"; 62 | } 63 | 64 | .icon-fenzu:before { 65 | content: "\e726"; 66 | } 67 | 68 | .icon-fenxiang1:before { 69 | content: "\e64c"; 70 | } 71 | 72 | .icon-yichushujia:before { 73 | content: "\e601"; 74 | } 75 | 76 | .icon-huo:before { 77 | content: "\e6c9"; 78 | } 79 | 80 | .icon-xia:before { 81 | content: "\e61b"; 82 | } 83 | 84 | .icon-shuaxin:before { 85 | content: "\e610"; 86 | } 87 | 88 | .icon-shanchu:before { 89 | content: "\e642"; 90 | } 91 | 92 | .icon-shang-:before { 93 | content: "\e63f"; 94 | } 95 | 96 | .icon-expand:before { 97 | content: "\e61e"; 98 | } 99 | 100 | .icon-arrowleft:before { 101 | content: "\e61a"; 102 | } 103 | 104 | .icon-circle-filled:before { 105 | content: "\e611"; 106 | } 107 | 108 | .icon-arrowright:before { 109 | content: "\e633"; 110 | } 111 | 112 | .icon-circle1:before { 113 | content: "\e621"; 114 | } 115 | 116 | .icon-baitian:before { 117 | content: "\e632"; 118 | } 119 | 120 | .icon-night:before { 121 | content: "\e6fd"; 122 | } 123 | 124 | .icon-shuqian:before { 125 | content: "\e603"; 126 | } 127 | 128 | .icon-jiarushujia:before { 129 | content: "\e62a"; 130 | } 131 | 132 | .icon-mulu:before { 133 | content: "\e620"; 134 | } 135 | 136 | .icon-shezhi:before { 137 | content: "\e635"; 138 | } 139 | 140 | .icon-huancun:before { 141 | content: "\e6b0"; 142 | } 143 | 144 | .icon-qudangqian:before { 145 | content: "\e6eb"; 146 | } 147 | 148 | .icon-xiazaiyun:before { 149 | content: "\e679"; 150 | } 151 | 152 | .icon-qudingbu:before { 153 | content: "\e641"; 154 | } 155 | 156 | .icon-qudibu:before { 157 | content: "\e70f"; 158 | } 159 | 160 | .icon-dingyue:before { 161 | content: "\e6bc"; 162 | } 163 | 164 | .icon-search:before { 165 | content: "\e600"; 166 | } 167 | 168 | .icon-more:before { 169 | content: "\e6ad"; 170 | } 171 | 172 | .icon-sousuo:before { 173 | content: "\e8b9"; 174 | } 175 | 176 | .icon-icon-test:before { 177 | content: "\e618"; 178 | } 179 | 180 | .icon-export:before { 181 | content: "\e669"; 182 | } 183 | 184 | .icon-wode:before { 185 | content: "\101a9"; 186 | } 187 | 188 | -------------------------------------------------------------------------------- /src/components/painter/components/common/relation.js: -------------------------------------------------------------------------------- 1 | const styles = (v ='') => v.split(';').filter(v => v && !/^[\n\s]+$/.test(v)).map(v => { 2 | const key = v.slice(0, v.indexOf(':')) 3 | const value = v.slice(v.indexOf(':')+1) 4 | return { 5 | [key 6 | .replace(/-([a-z])/g, function() { return arguments[1].toUpperCase()}) 7 | .replace(/\s+/g, '') 8 | ]: value.replace(/^\s+/, '').replace(/\s+$/, '') || '' 9 | } 10 | }) 11 | export function parent(parent) { 12 | return { 13 | provide() { 14 | return { 15 | [parent]: this 16 | } 17 | }, 18 | data() { 19 | return { 20 | el: { 21 | id: null, 22 | css: {}, 23 | views: [] 24 | }, 25 | } 26 | }, 27 | watch: { 28 | css: { 29 | handler(v) { 30 | if(this.canvasId) { 31 | this.el.css = (typeof v == 'object' ? v : v && Object.assign(...styles(v))) || {} 32 | this.canvasWidth = this.el.css && this.el.css.width || this.canvasWidth 33 | this.canvasHeight = this.el.css && this.el.css.height || this.canvasHeight 34 | } 35 | }, 36 | immediate: true 37 | } 38 | } 39 | } 40 | } 41 | export function children(parent, options = {}) { 42 | const indexKey = options.indexKey || 'index' 43 | return { 44 | inject: { 45 | [parent]: { 46 | default: null 47 | } 48 | }, 49 | watch: { 50 | el: { 51 | handler(v, o) { 52 | if(JSON.stringify(v) != JSON.stringify(o)) 53 | this.bindRelation() 54 | }, 55 | deep: true, 56 | immediate: true 57 | }, 58 | src: { 59 | handler(v, o) { 60 | if(v != o) 61 | this.bindRelation() 62 | }, 63 | immediate: true 64 | }, 65 | text: { 66 | handler(v, o) { 67 | if(v != o) this.bindRelation() 68 | }, 69 | immediate: true 70 | }, 71 | css: { 72 | handler(v, o) { 73 | if(v != o) 74 | this.el.css = (typeof v == 'object' ? v : v && Object.assign(...styles(v))) || {} 75 | }, 76 | immediate: true 77 | }, 78 | replace: { 79 | handler(v, o) { 80 | if(JSON.stringify(v) != JSON.stringify(o)) 81 | this.bindRelation() 82 | }, 83 | deep: true, 84 | immediate: true 85 | } 86 | }, 87 | created() { 88 | if(!this._uid) { 89 | this._uid = this._.uid 90 | } 91 | Object.defineProperty(this, 'parent', { 92 | get: () => this[parent] || [], 93 | }) 94 | Object.defineProperty(this, 'index', { 95 | get: () => { 96 | this.bindRelation(); 97 | const {parent: {el: {views=[]}={}}={}} = this 98 | return views.indexOf(this.el) 99 | }, 100 | }); 101 | this.el.type = this.type 102 | if(this.uid) { 103 | this.el.uid = this.uid 104 | } 105 | this.bindRelation() 106 | }, 107 | // #ifdef VUE3 108 | beforeUnmount() { 109 | this.removeEl() 110 | }, 111 | // #endif 112 | // #ifdef VUE2 113 | beforeDestroy() { 114 | this.removeEl() 115 | }, 116 | // #endif 117 | methods: { 118 | removeEl() { 119 | if (this.parent) { 120 | this.parent.el.views = this.parent.el.views.filter( 121 | (item) => item._uid !== this._uid 122 | ); 123 | } 124 | }, 125 | bindRelation() { 126 | if(!this.el._uid) { 127 | this.el._uid = this._uid 128 | } 129 | if(['text','qrcode'].includes(this.type)) { 130 | this.el.text = this.$slots && this.$slots.default && this.$slots.default[0].text || `${this.text || ''}`.replace(/\\n/g, '\n') 131 | } 132 | if(this.type == 'image') { 133 | this.el.src = this.src 134 | } 135 | if (!this.parent) { 136 | return; 137 | } 138 | let views = this.parent.el.views || []; 139 | if(views.indexOf(this.el) !== -1) { 140 | this.parent.el.views = views.map(v => v._uid == this._uid ? this.el : v) 141 | } else { 142 | this.parent.el.views = [...views, this.el]; 143 | } 144 | } 145 | }, 146 | mounted() { 147 | // this.bindRelation() 148 | }, 149 | } 150 | } -------------------------------------------------------------------------------- /src/components/Expand.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 101 | 102 | 137 | -------------------------------------------------------------------------------- /src/pages/tabBar/export.vue: -------------------------------------------------------------------------------- 1 | 16 | 135 | 165 | -------------------------------------------------------------------------------- /src/components/Upload/Image.vue: -------------------------------------------------------------------------------- 1 | 29 | 34 | 117 | -------------------------------------------------------------------------------- /src/components/painter/hybrid/html/uni.webview.1.5.3.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).uni=n()}(this,(function(){"use strict";try{var e={};Object.defineProperty(e,"passive",{get:function(){!0}}),window.addEventListener("test-passive",null,e)}catch(e){}var n=Object.prototype.hasOwnProperty;function i(e,i){return n.call(e,i)}var t=[];function o(){return window.__dcloud_weex_postMessage||window.__dcloud_weex_}var r=function(e,n){var i={options:{timestamp:+new Date},name:e,arg:n};if(o()){if("postMessage"===e){var r={data:[n]};return window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(r):window.__dcloud_weex_.postMessage(JSON.stringify(r))}var a={type:"WEB_INVOKE_APPSERVICE",args:{data:i,webviewIds:t}};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessageToService(a):window.__dcloud_weex_.postMessageToService(JSON.stringify(a))}if(!window.plus)return window.parent.postMessage({type:"WEB_INVOKE_APPSERVICE",data:i,pageId:""},"*");if(0===t.length){var d=plus.webview.currentWebview();if(!d)throw new Error("plus.webview.currentWebview() is undefined");var s=d.parent(),w="";w=s?s.id:d.id,t.push(w)}if(plus.webview.getWebviewById("__uniapp__service"))plus.webview.postMessageToUniNView({type:"WEB_INVOKE_APPSERVICE",args:{data:i,webviewIds:t}},"__uniapp__service");else{var u=JSON.stringify(i);plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE",'",').concat(u,",").concat(JSON.stringify(t),");"))}},a={navigateTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;r("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("redirectTo",{url:encodeURI(n)})},getEnv:function(e){o()?e({nvue:!0}):window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};r("postMessage",e.data||{})}},d=/uni-app/i.test(navigator.userAgent),s=/Html5Plus/i.test(navigator.userAgent),w=/complete|loaded|interactive/;var u=window.my&&navigator.userAgent.indexOf("AlipayClient")>-1;var g=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var c=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var v=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var m=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.qa&&/quickapp/i.test(navigator.userAgent);var f=window.ks&&window.ks.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var l=window.tt&&window.tt.miniProgram&&/Lark|Feishu/i.test(navigator.userAgent);var _=window.jd&&window.jd.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);for(var E,b=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},h=[function(e){if(d||s)return window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&w.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),a},function(e){if(m)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(c)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(u){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(g)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(v)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(p){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(f)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.ks.miniProgram},function(e){if(l)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(_)return window.JDJSBridgeReady&&window.JDJSBridgeReady.invoke?setTimeout(e,0):document.addEventListener("JDJSBridgeReady",e),window.jd.miniProgram},function(e){return document.addEventListener("DOMContentLoaded",e),a}],y=0;y void) { 11 | // #ifdef APP-PLUS 12 | // learn: https://ask.dcloud.net.cn/article/35621 13 | const data = { 14 | qq: "mqq://", 15 | wx: "weixin://", 16 | zfb: "alipay://", 17 | sina: "sinaweibo://", 18 | taobao: "taobao://", 19 | }; 20 | plus.runtime.openURL(data[name], callback); 21 | // #endif 22 | } 23 | 24 | /** 25 | * 显示加载提示 26 | * @param text 提示文字 27 | */ 28 | export function showLoading(text: string = "加载中..") { 29 | uni.hideLoading(); 30 | uni.showLoading({ 31 | title: text, 32 | mask: false, 33 | }); 34 | } 35 | 36 | /** 37 | * 显示提示条 38 | * @param tip 提示文字 39 | * @param duration 持续时间 40 | */ 41 | export function showToast(tip: string, duration = 2000) { 42 | uni.showToast({ 43 | title: tip, 44 | // position: "bottom", 45 | icon: "none", 46 | duration: duration, 47 | // image: src 48 | }); 49 | } 50 | 51 | /** 52 | * 显示提示框 53 | * @param content 提示的内容 54 | * @param success 确认回调 55 | * @param title 提示标题 56 | */ 57 | export function showAlert(content: string, success?: (res: UniApp.ShowModalRes) => void, title: string = "操作提示") { 58 | uni.showModal({ 59 | title: title, 60 | content: content, 61 | showCancel: false, 62 | success: success, 63 | }); 64 | } 65 | 66 | interface ShowConfirmOptions { 67 | /** 内容 */ 68 | content: string; 69 | /** 标题 */ 70 | title?: string; 71 | /** 确认回调 */ 72 | confirm?(): void; 73 | /** 取消回调 */ 74 | cancel?(): void; 75 | /** 确认按钮文字 */ 76 | confirmText?: string; 77 | /** 取消按钮文字,超过4个中文会报错 */ 78 | cancelText: string; 79 | } 80 | 81 | /** 82 | * 确认弹窗 83 | * @param options 传参对象 84 | */ 85 | export function showConfirm(options: ShowConfirmOptions) { 86 | uni.showModal({ 87 | title: options.title || "操作提示", 88 | content: options.content, 89 | showCancel: true, 90 | confirmText: options.confirmText || "确认", 91 | cancelText: options.cancelText || "取消", 92 | success(res) { 93 | if (res.confirm) { 94 | options.confirm && options.confirm(); 95 | } else if (res.cancel) { 96 | options.cancel && options.cancel(); 97 | } 98 | }, 99 | }); 100 | } 101 | 102 | /** 103 | * 复制文本 104 | * @param value 复制的内容 105 | * @param success 成功回调 106 | */ 107 | export function copyText(value: string, success?: () => void) { 108 | value = value.replace(/(^\s*)|(\s*$)/g, ""); 109 | if (!value) { 110 | return showToast("复制的内容不能为空!"); 111 | } 112 | 113 | // #ifndef H5 114 | uni.setClipboardData({ 115 | data: value, 116 | success() { 117 | showToast("复制成功"); 118 | success && success(); 119 | }, 120 | }); 121 | // #endif 122 | 123 | // #ifdef H5 124 | const id = "the-clipboard"; 125 | let clipboard = document.getElementById(id) as HTMLTextAreaElement; 126 | if (!clipboard) { 127 | clipboard = document.createElement("textarea"); 128 | clipboard.id = id; 129 | clipboard.readOnly = true; 130 | clipboard.style.cssText = "font-size: 15px; position: fixed; top: -1000%; left: -1000%;"; 131 | document.body.appendChild(clipboard); 132 | } 133 | clipboard.value = value; 134 | clipboard.select(); 135 | clipboard.setSelectionRange(0, clipboard.value.length); 136 | const state = document.execCommand("copy"); 137 | if (state) { 138 | showToast("复制成功"); 139 | success && success(); 140 | } else { 141 | showToast("复制失败"); 142 | } 143 | // #endif 144 | } 145 | 146 | interface ScrollviewCenterOptions { 147 | /** 148 | * 当前实例 149 | * ```js 150 | * import { getCurrentInstance } from "vue"; 151 | * getCurrentInstance(); 152 | * ``` 153 | */ 154 | ctx: T; 155 | /** 要滚动的目标节点`id` */ 156 | id: string; 157 | /** ``的宽度,默认是屏幕宽度 */ 158 | wrapWidth?: number; 159 | /** 点击事件 */ 160 | event?: Event; 161 | /** 是否主动设置偏移到中心位置,设置值时,`event`不需要传入 */ 162 | scrollValue?: number; 163 | /** 回调 */ 164 | callback?: (left: number, info: UniApp.NodeInfo) => void; 165 | } 166 | 167 | /** 168 | * 监听``组件指定元素滚动到视图中心的偏移值 169 | * @param option 配置参数 170 | */ 171 | export function onScrollviewCenter(option: ScrollviewCenterOptions) { 172 | const width = option.wrapWidth || uni.getSystemInfoSync().windowWidth; 173 | nextTick(function () { 174 | const node = uni.createSelectorQuery().in(option.ctx).select(`#${option.id}`); 175 | const left = option.event ? (option.event.currentTarget as any).offsetLeft : 0; 176 | node 177 | .boundingClientRect(function (nodeInfo) { 178 | let result = 0; 179 | if (nodeInfo && !Array.isArray(nodeInfo)) { 180 | if (typeof option.scrollValue === "number") { 181 | result = option.scrollValue + nodeInfo.left! + nodeInfo.width! / 2 - width / 2; 182 | } else { 183 | result = left + nodeInfo.width! / 2 - width / 2; 184 | } 185 | } 186 | typeof option.callback === "function" && option.callback(result, nodeInfo as any); 187 | }) 188 | .exec(); 189 | }); 190 | } 191 | -------------------------------------------------------------------------------- /src/pages/tabBar/components/addGroup.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 133 | 134 | 183 | -------------------------------------------------------------------------------- /src/type.d.ts: -------------------------------------------------------------------------------- 1 | // 全局声明文件,局部声明文件不要写在这里 2 | interface PageListItem { 3 | center: boolean; 4 | isTitle: boolean; 5 | lineIndex: number; 6 | pFirst: boolean; 7 | pIndex: number; 8 | text: string; 9 | textIndex: number; 10 | } 11 | 12 | type PageList = PageListItem[]; 13 | type ChapterList = PageList[]; 14 | interface PopupInstance { 15 | open: () => void; 16 | close: () => void; 17 | } 18 | 19 | interface PainterInstance { 20 | render: (options: any) => void; 21 | canvasToTempFilePathSync: (options: any) => void; 22 | } 23 | 24 | // confirm 25 | type ConfirmOptions = { 26 | type: "success" | "warn" | "error" | "info"; 27 | cancelText: string; 28 | confirmText: string; 29 | title: string; 30 | content: string; 31 | beforeClose: boolean; 32 | mode: "base" | "input"; 33 | maxlength: number; 34 | showClose: boolean; 35 | value: string; 36 | placeholder: string; 37 | key: string; 38 | }; 39 | 40 | interface ConfirmInstance { 41 | show: (params: Partial) => void; 42 | close: () => void; 43 | } 44 | 45 | /** 深层递归所有属性为可选 */ 46 | type DeepPartial = { 47 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 48 | }; 49 | 50 | /** 深层递归所有属性为只读 */ 51 | type DeepReadonly = { 52 | readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; 53 | }; 54 | 55 | /** 深层递归所有属性为必选选(貌似不生效) */ 56 | type DeepRequired = { 57 | [P in keyof T]-?: T[P] extends object ? Required : T[P]; 58 | }; 59 | 60 | /** 运算符号 */ 61 | type NumberSymbols = "+" | "-" | "*" | "/"; 62 | 63 | /** 64 | * `JavaScript`类型 65 | * - 这里只枚举一些常见类型,后续根据使用场景自行添加即可 66 | */ 67 | type JavaScriptTypes = 68 | | "string" 69 | | "number" 70 | | "array" 71 | | "object" 72 | | "boolean" 73 | | "function" 74 | | "null" 75 | | "undefined" 76 | | "regexp" 77 | | "promise" 78 | | "formdata"; 79 | 80 | /** 基础对象 */ 81 | interface BaseObj { 82 | [key: string]: T; 83 | } 84 | 85 | /** 接口请求基础响应数据 */ 86 | interface ApiResult { 87 | /** 接口状态`code === 1`为成功 */ 88 | code: number; 89 | /** 接口响应数据 */ 90 | data: T; 91 | /** 接口响应信息 */ 92 | msg: string; 93 | } 94 | 95 | /** 接口请求列表响应数据 */ 96 | interface ApiResultList extends PageInfo { 97 | /** 列表数据 */ 98 | list: Array; 99 | } 100 | 101 | interface requestResultList { 102 | code: number | string; 103 | message: string; 104 | data: T; 105 | } 106 | 107 | /** 请求配置 */ 108 | interface RequestOptions { 109 | /** 110 | * 请求头配置(header 中不能设置 Referer。),会覆盖默认设置 111 | * - 需要表单形式就`headers: { "codeMode": "form" }`; 112 | * - 其他头部设置自行定义 113 | */ 114 | headers: BaseObj & { 115 | codeMode?: "json" | "form"; 116 | }; 117 | /** 请求数据类型 */ 118 | dataType: "json" | "text" | ""; 119 | /** 接口数据响应类型 */ 120 | responseType: "json" | "arraybuffer" | "blob" | "stream" | "document" | "text"; 121 | /** 是否在`res.code !== 1`的时候显示提示,默认`false`,传入`true`则用`res.msg`作为提示,也可以传入字符串作为提示内容 */ 122 | showTip: string | boolean; 123 | } 124 | 125 | type RequestMethod = "GET" | "POST" | "DELETE" | "PUT"; 126 | 127 | /** 页码信息 */ 128 | interface PageInfo { 129 | /** 一页多少条 */ 130 | pageSize: number; 131 | /** 当前页,从`1`开始 */ 132 | currentPage: number; 133 | /** 总数 */ 134 | total?: number; 135 | } 136 | 137 | interface parseRequest { 138 | origin: String; 139 | href?: String; 140 | } 141 | 142 | type OriginItem = { origin: string; pathname: string }; 143 | type Origins = Array; 144 | 145 | interface bookInfo { 146 | author: string; 147 | bookName: string; 148 | origin: string; 149 | pathname: string; 150 | image: string; 151 | latestChapter: string; 152 | latestUpdateTime: string; 153 | description: string; 154 | categories: string; 155 | origins: Origins; 156 | links: { 157 | href: string; 158 | text: string; 159 | }[]; 160 | } 161 | interface BooksShelvesData extends bookInfo { 162 | uid: string; 163 | latestReadeTimestamp?: number; // 上一次阅读时间 164 | chapter: number; 165 | pageIndex: number; 166 | stickyIndex?: number; // 是否置顶 167 | group?: string; // 所属分组 168 | isInBookShelves?: boolean; // 是否在书架 169 | show?: boolean; // 是否展示 -> 有分组的情况 170 | } 171 | interface groupBook { 172 | index: number; 173 | group: string; 174 | data: BooksShelvesData[]; 175 | type: string; 176 | } 177 | 178 | interface BookGrop { 179 | name: string; 180 | stickyIndex?: number; 181 | } 182 | 183 | interface SourceMap { 184 | origin: string; 185 | name: string; 186 | enable: boolean; 187 | } 188 | 189 | interface BookShelves { 190 | sort: "default" | "join" | "chart"; 191 | data: BooksShelvesData[]; 192 | } 193 | 194 | interface TempDataReflect { 195 | author: string; 196 | bookName: string; 197 | uid: string; 198 | } 199 | 200 | interface searchBookList { 201 | author: string; 202 | bookName: string; 203 | origin: string; 204 | pathname: string; 205 | image: string; 206 | latestChapter: string; 207 | categories: string; 208 | } 209 | 210 | interface TopBookInfo { 211 | bookName: string; 212 | author: string; 213 | image: string; 214 | categories: string; 215 | description: string; 216 | status: string; 217 | tag: string; 218 | } 219 | 220 | interface cacheInfo { 221 | content: string; 222 | title: string; 223 | } 224 | 225 | interface chapterInfo { 226 | pageIndex: number; 227 | chapter: number; 228 | cache: cacheInfo[]; 229 | mark: BookMark[]; 230 | bookInfo: bookInfo; 231 | uid: string; 232 | } 233 | 234 | interface BookMark { 235 | pageIndex: number; 236 | pageCount: number; 237 | chapter: number; 238 | title: string; 239 | time: string; 240 | } 241 | 242 | /** `uni-app`change事件参数 */ 243 | interface UniAppChangeEvent { 244 | detail: { 245 | /** `@change`事件触发的值 */ 246 | value: T; 247 | }; 248 | } 249 | 250 | interface Window { 251 | /** 252 | * 当前版本,方便在控制台查看调试用 253 | * @description 引用的是`package.json`中的`version` 254 | */ 255 | version: string; 256 | } 257 | 258 | /** .nvue文件专用模块 */ 259 | declare const weex: any; 260 | -------------------------------------------------------------------------------- /src/parser/catalog.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | import * as api from "@/api/common"; 3 | import sourceJson from "./source"; 4 | import { URLPolyfill } from "@/utils"; 5 | const source = sourceJson.local; 6 | 7 | export const getCatalogs = async (params: { url: string }) => { 8 | let content = (await getDeepthHtml(params.url)) as bookInfo; 9 | 10 | const result = handlerCatelogs(content); 11 | return Promise.resolve(result); 12 | }; 13 | 14 | const getDeepthHtml = async (url: string, content: AnyObject = {}) => { 15 | try { 16 | let res = (await api.getCatalogsByApp(url)) as string; 17 | if (res) { 18 | const parse = URLPolyfill(url); 19 | const targetSource = source.find((i) => i.origin === parse.origin); 20 | if (targetSource) { 21 | const $ = cheerio.load(res); 22 | const { wrapContainer, infomation, pagination, redirect } = targetSource.catalogs.rules; 23 | 24 | let info = { 25 | bookName: "", 26 | author: "", 27 | image: "", 28 | categories: "", 29 | latestChapter: "", 30 | latestUpdateTime: "", 31 | description: "", 32 | }; 33 | 34 | const reflect = [ 35 | ["bookName", infomation.bookName], 36 | ["author", infomation.author], 37 | ["image", infomation.image], 38 | ["categories", infomation.categories], 39 | ["latestChapter", infomation.latestChapter], 40 | ["latestUpdateTime", infomation.latestUpdateTime], 41 | ["description", infomation.description], 42 | ]; 43 | 44 | type HanderKeys = 45 | | "bookName" 46 | | "author" 47 | | "image" 48 | | "categories" 49 | | "latestChapter" 50 | | "latestUpdateTime" 51 | | "description"; 52 | 53 | reflect.forEach((arr: any) => { 54 | const key: HanderKeys = arr[0]; 55 | const rule = arr[1]; 56 | let node = $(rule.selector); 57 | if (rule?.hasOwnProperty("nthchild")) { 58 | const num = rule.nthchild; 59 | if (node?.length >= num + 1) { 60 | node = node.eq(num); 61 | } 62 | } 63 | if (rule.type === "element") { 64 | if (rule.attr) { 65 | info[key] = node.attr(rule.attr) || ""; 66 | } else { 67 | info[key] = node.text() || ""; 68 | } 69 | 70 | if (rule.subPath) { 71 | info[key] = `${parse.origin}${info[key]}`; 72 | } 73 | } else { 74 | info[key] = node.attr("content") || ""; 75 | } 76 | 77 | // 处理文字信息 78 | if (rule?.handler) { 79 | Object.entries(rule.handler).forEach(([type, value]) => { 80 | switch (type) { 81 | case "replace": 82 | info[key] = info[key].replace(new RegExp(value as string, "g"), rule.handler?.replaceValue || ""); 83 | break; 84 | default: 85 | break; 86 | } 87 | }); 88 | } 89 | }); 90 | 91 | if (!content.isParse) { 92 | content = { 93 | isParse: true, 94 | origin: parse.origin, 95 | pathname: parse.pathname, 96 | links: [], 97 | ...info, 98 | }; 99 | } 100 | 101 | const wrapperContainer = $(wrapContainer.selector); 102 | wrapperContainer.each((index, element) => { 103 | let href = $(element).attr("href"); 104 | if (redirect) { 105 | href = `${parse.pathname}${href}`; 106 | } 107 | 108 | content.links.push({ 109 | href, 110 | text: $(element).text(), 111 | }); 112 | }); 113 | if (redirect) { 114 | let node = $(redirect.selector); 115 | if (redirect?.hasOwnProperty("nthchild")) { 116 | const num = redirect.nthchild; 117 | if (node?.length >= num + 1) { 118 | node = node.eq(num); 119 | } 120 | } 121 | const redirectUrl = node.attr("href"); 122 | if (redirectUrl) { 123 | const nextUrl = `${parse.origin}${redirectUrl}`; 124 | 125 | content = await getDeepthHtml(nextUrl, content); 126 | } 127 | } 128 | if (pagination) { 129 | const nextPagination = $(pagination.selector).last(); 130 | 131 | let nextPath = nextPagination.attr("href"); 132 | let nextButtonName = nextPagination.text(); 133 | 134 | if (nextPath && nextButtonName === "下一页") { 135 | const nextUrl = pagination.fullpath 136 | ? `${parse.origin}${parse.pathname}${nextPath}` 137 | : `${parse.origin}${nextPath}`; 138 | 139 | content = await getDeepthHtml(nextUrl, content); 140 | } 141 | } 142 | } 143 | } 144 | } catch (error) { 145 | console.log(error); 146 | return content; 147 | } 148 | 149 | return content as bookInfo; 150 | }; 151 | 152 | const handlerCatelogs = (content: bookInfo) => { 153 | const array = content.links || []; 154 | 155 | type LinkType = { 156 | href: string; 157 | text: string; 158 | }; 159 | // 过滤重复的 href 160 | const uniqueArray = Array.from(new Set(array.map((item) => item.href))).map((href) => 161 | array.find((item) => item.href === href), 162 | ) as LinkType[]; 163 | 164 | // 按照 xxxx.html 从小到大排序 165 | uniqueArray.sort((a, b) => { 166 | const numA = parseInt(a?.href?.split("/")?.pop()?.split(".html")[0] || "0"); 167 | const numB = parseInt(b?.href?.split("/")?.pop()?.split(".html")[0] || "0"); 168 | return numA - numB; 169 | }); 170 | 171 | content.links = uniqueArray; 172 | content.origins = [ 173 | { 174 | origin: content.origin, 175 | pathname: content.pathname, 176 | }, 177 | ]; 178 | 179 | return content; 180 | }; 181 | -------------------------------------------------------------------------------- /src/components/painter/components/l-painter/nvue.js: -------------------------------------------------------------------------------- 1 | // #ifdef APP-NVUE 2 | import { 3 | sleep, 4 | getImageInfo, 5 | isBase64, 6 | networkReg 7 | } from './utils'; 8 | const dom = weex.requireModule('dom') 9 | import { 10 | version 11 | } from '../../package.json' 12 | 13 | export default { 14 | data() { 15 | return { 16 | tempFilePath: [], 17 | isInitFile: false, 18 | osName: uni.getSystemInfoSync().osName 19 | } 20 | }, 21 | methods: { 22 | getParentWeith() { 23 | return new Promise(resolve => { 24 | dom.getComponentRect(this.$refs.limepainter, (res) => { 25 | this.parentWidth = Math.ceil(res.size.width) 26 | this.canvasWidth = this.canvasWidth || this.parentWidth || 300 27 | this.canvasHeight = res.size.height || this.canvasHeight || 150 28 | resolve(res.size) 29 | }) 30 | }) 31 | }, 32 | onPageFinish() { 33 | this.webview = this.$refs.webview 34 | this.webview.evalJS(`init(${this.dpr})`) 35 | }, 36 | onMessage(e) { 37 | const res = e.detail.data[0] || null; 38 | if (res.event) { 39 | if (res.event == 'inited') { 40 | this.inited = true 41 | } 42 | if (res.event == 'fail') { 43 | this.$emit('fail', res) 44 | } 45 | if (res.event == 'layoutChange') { 46 | const data = typeof res.data == 'string' ? JSON.parse(res.data) : res.data 47 | this.canvasWidth = Math.ceil(data.width); 48 | this.canvasHeight = Math.ceil(data.height); 49 | } 50 | if (res.event == 'progressChange') { 51 | this.progress = res.data * 1 52 | } 53 | if (res.event == 'file') { 54 | this.tempFilePath.push(res.data) 55 | if (this.tempFilePath.length > 7) { 56 | this.tempFilePath.shift() 57 | } 58 | return 59 | } 60 | if (res.event == 'success') { 61 | if (res.data) { 62 | this.tempFilePath.push(res.data) 63 | if (this.tempFilePath.length > 8) { 64 | this.tempFilePath.shift() 65 | } 66 | if (this.isCanvasToTempFilePath) { 67 | this.setFilePath(this.tempFilePath.join(''), { 68 | isEmit: true 69 | }) 70 | } 71 | } else { 72 | this.$emit('fail', 'canvas no data') 73 | } 74 | return 75 | } 76 | this.$emit(res.event, JSON.parse(res.data)); 77 | } else if (res.file) { 78 | this.file = res.data; 79 | } else { 80 | console.info(res[0]) 81 | } 82 | }, 83 | getWebViewInited() { 84 | if (this.inited) return Promise.resolve(this.inited); 85 | return new Promise((resolve) => { 86 | this.$watch( 87 | 'inited', 88 | async val => { 89 | if (val) { 90 | resolve(val) 91 | } 92 | }, { 93 | immediate: true 94 | } 95 | ); 96 | }) 97 | }, 98 | getTempFilePath() { 99 | if (this.tempFilePath.length == 8) return Promise.resolve(this.tempFilePath) 100 | return new Promise((resolve) => { 101 | this.$watch( 102 | 'tempFilePath', 103 | async val => { 104 | if (val.length == 8) { 105 | resolve(val.join('')) 106 | } 107 | }, { 108 | deep: true 109 | } 110 | ); 111 | }) 112 | }, 113 | getWebViewDone() { 114 | if (this.progress == 1) return Promise.resolve(this.progress); 115 | return new Promise((resolve) => { 116 | this.$watch( 117 | 'progress', 118 | async val => { 119 | if (val == 1) { 120 | this.$emit('done') 121 | this.done = true 122 | this.runTask() 123 | resolve(val) 124 | } 125 | }, { 126 | immediate: true 127 | } 128 | ); 129 | }) 130 | }, 131 | async render(args) { 132 | try { 133 | await this.getSize(args) 134 | const { 135 | width 136 | } = args.css || args 137 | if (!width && this.parentWidth) { 138 | Object.assign(args, { 139 | width: this.parentWidth 140 | }) 141 | } 142 | const newNode = await this.calcImage(args); 143 | await this.getWebViewInited() 144 | this.webview.evalJS(`source(${JSON.stringify(newNode)})`) 145 | await this.getWebViewDone() 146 | await sleep(this.afterDelay) 147 | if (this.isCanvasToTempFilePath) { 148 | const params = { 149 | fileType: this.fileType, 150 | quality: this.quality 151 | } 152 | this.webview.evalJS(`save(${JSON.stringify(params)})`) 153 | } 154 | return Promise.resolve() 155 | } catch (e) { 156 | this.$emit('fail', e) 157 | } 158 | }, 159 | async calcImage(args) { 160 | let node = JSON.parse(JSON.stringify(args)) 161 | const urlReg = /url\((.+)\)/ 162 | const { 163 | backgroundImage 164 | } = node.css || {} 165 | const isBG = backgroundImage && urlReg.exec(backgroundImage)[1] 166 | const url = node.url || node.src || isBG 167 | if (['text', 'qrcode'].includes(node.type)) { 168 | return node 169 | } 170 | if ((node.type === "image" || isBG) && url && !isBase64(url) && (this.osName == 'ios' || !networkReg 171 | .test(url))) { 172 | let { 173 | path 174 | } = await getImageInfo(url, true) 175 | if (isBG) { 176 | node.css.backgroundImage = `url(${path})` 177 | } else { 178 | node.src = path 179 | } 180 | } else if (node.views && node.views.length) { 181 | for (let i = 0; i < node.views.length; i++) { 182 | node.views[i] = await this.calcImage(node.views[i]) 183 | } 184 | } 185 | return node 186 | }, 187 | async canvasToTempFilePath(args = {}) { 188 | if (!this.inited) { 189 | return this.$emit('fail', 'no init') 190 | } 191 | this.tempFilePath = [] 192 | if (args.fileType == 'jpg') { 193 | args.fileType = 'jpeg' 194 | } 195 | 196 | this.webview.evalJS(`save(${JSON.stringify(args)})`) 197 | try { 198 | let tempFilePath = await this.getTempFilePath() 199 | 200 | tempFilePath = await this.setFilePath(tempFilePath, args) 201 | args.success({ 202 | errMsg: "canvasToTempFilePath:ok", 203 | tempFilePath 204 | }) 205 | } catch (e) { 206 | console.log('e', e) 207 | args.fail({ 208 | error: e 209 | }) 210 | } 211 | } 212 | } 213 | } 214 | // #endif -------------------------------------------------------------------------------- /src/pages/tabBar/personal.vue: -------------------------------------------------------------------------------- 1 | 37 | 142 | 217 | -------------------------------------------------------------------------------- /src/parser/search.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | import * as api from "@/api/common"; 3 | import sourceJson from "./source"; 4 | const source = sourceJson.local; 5 | 6 | export async function searchBook(params: { 7 | fuzzy: string; 8 | limit?: number; 9 | pageSize: number; 10 | pageNo: number; 11 | origins?: Array; 12 | }) { 13 | let bookList: searchBookList[] = []; 14 | 15 | const { pageSize, pageNo, origins } = params; 16 | let allowSource = source; 17 | // 从指定源搜索 18 | if (origins?.length) { 19 | allowSource = source.filter((i) => origins.includes(i.origin)); 20 | } 21 | const start = (pageNo - 1) * pageSize; 22 | const end = pageSize * pageNo; 23 | const requestList = allowSource.slice(start, end); 24 | 25 | for (let index = 0; index < requestList.length; index++) { 26 | try { 27 | const element = requestList[index]; 28 | let res: string; 29 | if (element.search.method === "POST") { 30 | const options: AnyObject = {}; 31 | if (element.search.queryType === "FormData") { 32 | options.sendFormData = true; 33 | } 34 | const [url, query] = formatPostUrl(element.origin, element.search.pathname, params, element.search.query); 35 | res = (await api.searchBookByApp(url, element.search.method, query, options)) as string; 36 | } else { 37 | const url = formatUrl(element.origin, element.search.pathname, params, element.search.query); 38 | res = (await api.searchBookByApp(url)) as string; 39 | } 40 | 41 | if (res) { 42 | let value = paraseSearchContent(res, element); 43 | if (params.limit) { 44 | value = value.slice(0, params.limit); 45 | } 46 | 47 | bookList = bookList.concat(value); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | } 53 | 54 | const result: searchBookList[] = handlerSearch(bookList); 55 | const value = { data: result, complete: end >= allowSource.length - 1 }; 56 | return Promise.resolve(value); 57 | } 58 | 59 | function paraseSearchContent(res: string, item: any) { 60 | const bookList: searchBookList[] = []; 61 | const { rules } = item.search; 62 | const $ = cheerio.load(res); 63 | 64 | // 第一步:获取类 65 | const wrapperContainer = $(rules.wrapContainer.selector); 66 | // 第二步:遍历每个元素,提取其内部具体内容 67 | wrapperContainer.each((index, element) => { 68 | // 封面 书名 作者 目录地址 分类 最新章节 最近更新时间 69 | const { image, bookName, author, pathname, categories, latestChapter } = rules.infomation; 70 | 71 | const bookInfo = { 72 | origin: item.origin, 73 | bookName: "", 74 | author: "", 75 | image: "", 76 | pathname: "", 77 | categories: "", 78 | latestChapter: "", 79 | }; 80 | 81 | const handlers = [ 82 | ["image", image], 83 | ["bookName", bookName], 84 | ["author", author], 85 | ["pathname", pathname], 86 | ["categories", categories], 87 | ["latestChapter", latestChapter], 88 | ]; 89 | 90 | type HanderKeys = "image" | "bookName" | "author" | "pathname" | "categories" | "latestChapter"; 91 | handlers.forEach((arr) => { 92 | const key: HanderKeys = arr[0]; 93 | const rule = arr[1]; 94 | if (!rule) return; 95 | 96 | const ele = $(element).find(rule.selector); 97 | 98 | if (ele) { 99 | if (key === "image") { 100 | bookInfo[key] = ele.attr(rule.attr || "src") || ""; 101 | } else if (key === "pathname") { 102 | bookInfo[key] = ele.attr(rule.attr || "href") || ""; 103 | } else { 104 | bookInfo[key] = ele.text() || ""; 105 | } 106 | 107 | if (rule.subPath) { 108 | bookInfo[key] = `${item.origin}${bookInfo[key]}`; 109 | } 110 | } 111 | 112 | if (rule?.hasOwnProperty("nthchild")) { 113 | const num = rule.nthchild; 114 | if (ele?.length >= num + 1) { 115 | const text = ele.eq(num).text() || ""; 116 | bookInfo[key] = text; 117 | } 118 | } 119 | if (rule?.handler) { 120 | Object.entries(rule.handler).forEach(([type, value]) => { 121 | switch (type) { 122 | case "replace": 123 | bookInfo[key] = bookInfo[key].replace(new RegExp(value as string, "g"), rule.handler?.replaceValue || ""); 124 | break; 125 | default: 126 | break; 127 | } 128 | }); 129 | } 130 | 131 | bookInfo[key] = bookInfo[key].trim(); 132 | }); 133 | bookList.push(bookInfo); 134 | }); 135 | 136 | return bookList; 137 | } 138 | 139 | const handlerSearch = (bookList: searchBookList[]) => { 140 | bookList = bookList.filter((i) => i.bookName && i.pathname); 141 | 142 | return bookList; 143 | }; 144 | 145 | const formatUrl = (origin: string, path: string, params: { fuzzy: string; limit?: number }, query: AnyObject) => { 146 | origin = origin.endsWith("/") ? origin.slice(0, origin.length - 1) : origin; 147 | path = path.startsWith("/") ? path : `/${path}`; 148 | let str = `${origin}${path}?`; 149 | let searchParams = ``; 150 | 151 | Object.entries(query).forEach(([key, value]) => { 152 | if (typeof value === "string") { 153 | searchParams += `&${key}=${encodeURIComponent(value)}`; 154 | } 155 | if (value instanceof Object) { 156 | switch (value.type) { 157 | case "date": 158 | searchParams += `&${key}=${new Date().getTime()}`; 159 | break; 160 | case "query": 161 | searchParams += `&${key}=${encodeURIComponent(params.fuzzy)}`; 162 | break; 163 | default: 164 | break; 165 | } 166 | } 167 | }); 168 | 169 | str += searchParams.slice(1, searchParams.length); 170 | return str; 171 | }; 172 | 173 | const formatPostUrl = (origin: string, path: string, params: { fuzzy: string; limit?: number }, query: AnyObject) => { 174 | origin = origin.endsWith("/") ? origin.slice(0, origin.length - 1) : origin; 175 | path = path.startsWith("/") ? path : `/${path}`; 176 | let str = `${origin}${path}`; 177 | let searchParams: AnyObject = {}; 178 | 179 | Object.entries(query).forEach(([key, value]) => { 180 | if (typeof value === "string") { 181 | searchParams[key] = value; 182 | } 183 | if (value instanceof Object) { 184 | switch (value.type) { 185 | case "date": 186 | searchParams[key] = new Date().getTime(); 187 | break; 188 | case "query": 189 | searchParams[key] = params.fuzzy; 190 | break; 191 | default: 192 | break; 193 | } 194 | } 195 | }); 196 | const res = [str, searchParams]; 197 | 198 | return res as [string, AnyObject]; 199 | }; 200 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import config from "./Config"; 2 | import store from "@/store"; 3 | import { showLoading, showToast } from "@/utils/Control"; 4 | import { setUserAgent } from "./RequestHeader"; 5 | 6 | uni.addInterceptor("request", { 7 | invoke(args) { 8 | // console.log(args.header); 9 | }, 10 | success(args) { 11 | //成功回调拦截 12 | if (!args || !args.statusCode) { 13 | return Promise.reject("错误的消息内容。"); 14 | } 15 | //中文显示 16 | switch (args.statusCode) { 17 | case 100: 18 | args.message = "客户端应继续其请求"; 19 | break; 20 | case 101: 21 | args.message = "服务器根据客户端的请求切换协议。"; 22 | break; 23 | case 200: 24 | args.message = "请求成功。"; 25 | break; 26 | case 201: 27 | args.message = "成功请求并创建了新的资源。"; 28 | break; 29 | case 202: 30 | args.message = "已经接受请求,但未处理完成。"; 31 | break; 32 | case 203: 33 | args.message = "非授权信息。"; 34 | break; 35 | case 204: 36 | args.message = "服务器成功处理,但未返回内容。"; 37 | break; 38 | case 205: 39 | args.message = "服务器处理成功,用户终端(例如:浏览器)应重置文档视图。"; 40 | break; 41 | case 206: 42 | args.message = "服务器成功处理了部分GET请求。"; 43 | break; 44 | case 300: 45 | args.message = "请求的资源可包括多个位置。"; 46 | break; 47 | case 301: 48 | args.message = "请求的资源已被永久的移动到新URI。"; 49 | break; 50 | case 302: 51 | args.message = "临时移动。"; 52 | break; 53 | case 303: 54 | args.message = "查看其它地址。"; 55 | break; 56 | case 304: 57 | args.message = "所请求的资源未修改。"; 58 | break; 59 | case 305: 60 | args.message = "所请求的资源必须通过代理访问。"; 61 | break; 62 | case 306: 63 | args.message = "已经被废弃的HTTP状态码。"; 64 | break; 65 | case 307: 66 | args.message = "临时重定向。"; 67 | break; 68 | case 400: 69 | args.message = "客户端请求的语法错误,服务器无法理解。"; 70 | break; 71 | case 401: 72 | args.message = "请先登录系统。"; 73 | //跳转到登录页面 74 | break; 75 | case 402: 76 | args.message = "暂未定义。"; 77 | break; 78 | case 403: 79 | args.message = "服务器理解请求客户端的请求,但是拒绝执行此请求。"; 80 | break; 81 | case 404: 82 | args.message = "服务器无法根据客户端的请求找到资源(网页)。"; 83 | break; 84 | case 405: 85 | args.message = "客户端请求中的方法被禁止。"; 86 | break; 87 | case 406: 88 | args.message = "服务器无法根据客户端请求的内容特性完成请求。"; 89 | break; 90 | case 407: 91 | args.message = "请求要求代理的身份认证。"; 92 | break; 93 | case 408: 94 | args.message = "服务器等待客户端发送的请求时间过长。"; 95 | break; 96 | case 409: 97 | args.message = "服务器处理请求时发生了冲突。"; 98 | break; 99 | case 410: 100 | args.message = "客户端请求的资源已经不存在。"; 101 | break; 102 | case 411: 103 | args.message = "服务器无法处理客户端发送的不带Content-Length的请求信息。"; 104 | break; 105 | case 412: 106 | args.message = "客户端请求信息的先决条件错误。"; 107 | break; 108 | case 413: 109 | args.message = "由于请求的实体过大,服务器无法处理,因此拒绝请求。"; 110 | break; 111 | case 414: 112 | args.message = "请求的URI过长(URI通常为网址),服务器无法处理。"; 113 | break; 114 | case 415: 115 | args.message = "服务器无法处理请求附带的媒体格式。"; 116 | break; 117 | case 416: 118 | args.message = "客户端请求的范围无效。"; 119 | break; 120 | case 417: 121 | args.message = "服务器无法满足Expect的请求头信息。"; 122 | break; 123 | case 500: 124 | args.message = "服务器内部错误,无法完成请求。"; 125 | break; 126 | case 501: 127 | args.message = "服务器不支持请求的功能,无法完成请求。"; 128 | break; 129 | case 502: 130 | args.message = "作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应。"; 131 | break; 132 | case 503: 133 | args.message = "由于超载或系统维护,服务器暂时的无法处理客户端的请求。"; 134 | break; 135 | case 504: 136 | args.message = "充当网关或代理的服务器,未及时从远端服务器获取请求。"; 137 | break; 138 | case 505: 139 | args.message = "服务器不支持请求的HTTP协议的版本,无法完成处理。"; 140 | break; 141 | default: 142 | args.message = "状态错误(" + args.statusCode + ")"; 143 | break; 144 | } 145 | 146 | //处理状态码 147 | if (args.statusCode !== 200) { 148 | // showToast(args.message); 149 | return Promise.resolve({ data: "" }); 150 | // return Promise.reject(args.message); 151 | } 152 | 153 | return Promise.resolve(args); 154 | }, 155 | fail(err) { 156 | //失败回调拦截 157 | // showToast("无法发起请求"); 158 | return Promise.resolve({ data: "" }); 159 | }, 160 | }); 161 | /** 162 | * 基础请求 163 | * @param method 请求方法 164 | * @param path 请求路径 165 | * @param data 请求参数 166 | * @param options 其他配置参数 167 | */ 168 | export function requestService( 169 | method: "GET" | "POST" | "DELETE" | "PUT", 170 | path: string, 171 | data?: any, 172 | options: Partial = {}, 173 | ) { 174 | const headers = options.headers || {}; 175 | return new Promise>(function (resolve, reject) { 176 | uni.request({ 177 | method: method, 178 | header: { 179 | Authorization: store.user.info.token, 180 | ...headers, 181 | }, 182 | url: config.apiUrl + path, 183 | data: data, 184 | timeout: config.requestOvertime, 185 | success(res) { 186 | const data = res.data as requestResultList; 187 | if (data.code === 200) { 188 | resolve(data); 189 | } else { 190 | showToast(data.message || "操作失败"); 191 | reject(data.message || "操作失败"); 192 | } 193 | }, 194 | fail(err) { 195 | showToast("操作失败"); 196 | reject(err); 197 | }, 198 | }); 199 | }); 200 | } 201 | 202 | export function requestCrawlerHtml(url: string, method: RequestMethod = "GET", data?: any, options?: AnyObject) { 203 | let header: AnyObject = {}; 204 | 205 | const ua = setUserAgent(); 206 | header = Object.assign(header, { 207 | Accept: "*/*", 208 | "accept-language": "zh-CN,zh;q=0.9", 209 | "Content-Type": "text/html; charset=utf-8", 210 | ...ua, 211 | }); 212 | if (options?.sendFormData) { 213 | header["Content-Type"] = "application/x-www-form-urlencoded"; 214 | } 215 | // #ifdef APP-PLUS 216 | //设置userAgent代理 217 | plus.navigator.setUserAgent(ua["User-Agent"]); 218 | // #endif 219 | 220 | const promise = new Promise((resolve, reject) => { 221 | uni.request({ 222 | method, 223 | url, 224 | data, 225 | header, 226 | timeout: config.requestOvertime, 227 | success: (res: any) => { 228 | resolve(res.data as any); 229 | }, 230 | fail: (err) => { 231 | reject(err); 232 | }, 233 | }); 234 | }); 235 | return Promise.resolve(promise); 236 | } 237 | -------------------------------------------------------------------------------- /src/components/painter/changelog.md: -------------------------------------------------------------------------------- 1 | ## 1.9.6.6(2024-09-25) 2 | - fix: 修复background-position无效的问题 3 | ## 1.9.6.5(2024-04-14) 4 | - fix: 修复`nvue`无法生图的问题 5 | ## 1.9.6.4(2024-03-10) 6 | - fix: 修复代理ctx导致H5不能使用ctx.save 7 | ## 1.9.6.3(2024-03-08) 8 | - fix: 修复支付宝真机无法使用的问题 9 | ## 1.9.6.2(2024-02-22) 10 | - fix: 修复使用render函数报错的问题 11 | ## 1.9.6.1(2023-12-22) 12 | - fix: 修复字节小程序非2d字体偏移 13 | - fix: 修复`canvasToTempFilePathSync`会触发两次的问题 14 | - fix: 修复`parser`图片没有宽度的问题 15 | ## 1.9.6(2023-12-06) 16 | - fix: 修复背景图受padding影响 17 | - fix: 修复因字节报错改了代理实现导致微信报错 18 | - 1.9.5.8(2023-11-16) 19 | - fix: 修复margin问题 20 | - fix: 修复borderWidth问题 21 | - fix: 修复textBox问题 22 | - fix: 修复字节开发工具报`could not be cloned.`问题 23 | ## 1.9.5.7(2023-07-27) 24 | - fix: 去掉多余的方法 25 | - chore: 更新文档,增加自定义字体说明 26 | ## 1.9.5.6(2023-07-21) 27 | - feat: 有限的支持富文本 28 | - feat: H5和APP 增加 `hidpi` prop,主要用于大尺寸无法生成图片时用 29 | - fix: 修复 钉钉小程序 缺少 `measureText` 方法 30 | - chore: 由于微信小程序 pc 端的 canvas 2d 时不时抽风,故不使用canvas 2d 31 | ## 1.9.5.5(2023-06-27) 32 | - fix: 修复把`emoji`表情字符拆分成多个字符的情况 33 | ## 1.9.5.4(2023-06-05) 34 | - fix: 修复因`canvasToTempFilePathSync`监听导致重复调用 35 | ## 1.9.5.3(2023-05-23) 36 | - fix: 因isPc错写成了isPC导致小程序PC不能生成图片 37 | ## 1.9.5.2(2023-05-22) 38 | - feat: 删除多余文件 39 | ## 1.9.5.1(2023-05-22) 40 | - fix: 修复 文字行数与`line-clamp`相同但不满一行时也加了省略号的问题 41 | ## 1.9.5(2023-05-14) 42 | - feat: 增加 `text-indent` 和 `calc` 方法 43 | - feat: 优化 布局时间 44 | ## 1.9.4.4(2023-04-15) 45 | - fix: 修复无法匹配负值 46 | - fix: 修复 Nvue IOS getImageInfo `useCORS` 为 undefined 47 | ## 1.9.4.3(2023-04-01) 48 | - feat: 增加支持文字描边 `text-stroke: '5rpx #fff'` 49 | ## 1.9.4.2(2023-03-30) 50 | - fix: 修复 支付宝小程序 isPC 在手机也为true的问题 51 | - feat: 由 微信开发工具 3060 版 无法获取图片尺寸,现 微信开发工具 3220 版 修复该问题,故还原上一版的获取图片方式。 52 | ## 1.9.4.1(2023-03-28) 53 | - fix: 修复固定高度不正确问题 54 | ## 1.9.4(2023-03-17) 55 | - fix: nvue ios getImageInfo缺少this报错 56 | - fix: pathType 非2d无效问题 57 | - fix: 修复 小米9se 可能会存在多次init 导致画面多次放大 58 | - fix: 修复 border 分开写 width style无效问题 59 | - fix: 修复 支付宝小程序IOS 再次进入不渲染的问题 60 | - fix: 修复 支付宝小程序安卓Zindex排序错乱问题 61 | - fix: 修复 微信开发工具 3060 版 无法获取图片的问题 62 | - feat: 把 for in 改为 forEach 63 | - feat: 增加 hidden 64 | - feat: 根节点 box-sizing 默认 `border-box` 65 | - feat: 增加支持 `vw` `wh` 66 | - chore: pathType 取消 默认值,因为字节开发工具不能显示 67 | - chore: 支付宝小程序开发工具不支持 生成图片 请以真机调试为准 68 | - bug: 企业微信 2.20.3无法使用 69 | ## 1.9.3.5(2022-06-29) 70 | - feat: justifyContent 增加 `space-around`、`space-between` 71 | - feat: canvas 2d 也使用`getImageInfo` 72 | - fix: 修复 `text`的 `text-decoration`错位 73 | ## 1.9.3.4(2022-06-20) 74 | - fix: 修复 因创建节点速度问题导致顺序出错。 75 | - fix: 修复 微信小程序 PC 无法显示本地图片 76 | - fix: 修复 flex-box 对齐问题 77 | - feat: 增加 `text-shadow` 78 | - feat: 重写 `text` 对齐方式 79 | - chore: 更新文档 80 | ## 1.9.3.3(2022-06-17) 81 | - fix: 修复 支付宝小程序 canvas 2d 存在ctx.draw问题导致报错 82 | - fix: 修复 支付宝小程序 toDataURL 存在权限问题改用 `toTempFilePath` 83 | - fix: 修复 支付宝小程序 image size 问题导致 `objectFit` 无效 84 | ## 1.9.3.2(2022-06-14) 85 | - fix: 修复 image 设置背景色不生效问题 86 | - fix: 修复 nvue 环境判断缺少参数问题 87 | ## 1.9.3.1(2022-06-14) 88 | - fix: 修复 bottom 定位不对问题 89 | - fix: 修复 因小数导致计算出错换行问题 90 | - feat: 增加 `useCORS` h5端图片跨域 在设置请求头无效果后试一下设置这个值 91 | - chore: 更新文档 92 | ## 1.9.3(2022-06-13) 93 | - feat: 增加 `zIndex` 94 | - feat: 增加 `flex-box` 该功能处于原始阶段,非常简陋。 95 | - tips: QQ小程序 vue3 不支持, 为 uni 官方BUG 96 | ## 1.9.2.9(2022-06-10) 97 | - fix: 修复`text-align`及`margin`居中问题 98 | ## 1.9.2.8(2022-06-10) 99 | - fix: 修复 Nvue `canvasToTempFilePathSync` 不生效问题 100 | ## 1.9.2.7(2022-06-10) 101 | - fix: 修复 margin及padding的bug 102 | - fix: 修复 Nvue `isCanvasToTempFilePath` 不生效问题 103 | ## 1.9.2.6(2022-06-09) 104 | - fix: 修复 Nvue 不显示 105 | - feat: 增加支持字体渐变 106 | ```html 107 | 110 | ``` 111 | ## 1.9.2.5(2022-06-09) 112 | - chore: 更变获取父级宽度的设定 113 | - chore: `pathType` 在canvas 2d 默认为 `url` 114 | ## 1.9.2.4(2022-06-08) 115 | - fix: 修复 `pathType` 不生效问题 116 | ## 1.9.2.3(2022-06-08) 117 | - fix: 修复 `canvasToTempFilePath` 漏写 `success` 参数 118 | ## 1.9.2.2(2022-06-07) 119 | - chore: 更新文档 120 | ## 1.9.2.1(2022-06-07) 121 | - fix: 修复 vue3 赋值给this再传入导致image无法绘制 122 | - fix: 修复 `canvasToTempFilePathSync` 时机问题 123 | - feat: canvas 2d 更改图片生成方式 `toDataURL` 124 | ## 1.9.2(2022-05-30) 125 | - fix: 修复 `canvasToTempFilePathSync` 在 vue3 下只生成一次 126 | ## 1.9.1.7(2022-05-28) 127 | - fix: 修复 `qrcode`显示不全问题 128 | ## 1.9.1.6(2022-05-28) 129 | - fix: 修复 `canvasToTempFilePathSync` 会重复多次问题 130 | - fix: 修复 `view` css `backgroundImage` 图片下载失败导致 子节点不渲染 131 | ## 1.9.1.5(2022-05-27) 132 | - fix: 修正支付宝小程序 canvas 2d版本号 2.7.15 133 | ## 1.9.1.4(2022-05-22) 134 | - fix: 修复字节小程序无法使用xml方式 135 | - fix: 修复字节小程序无法使用base64(非2D情况下工具上无法显示) 136 | - fix: 修复支付宝小程序 `canvasToTempFilePath` 报错 137 | ## 1.9.1.3(2022-04-29) 138 | - fix: 修复vue3打包后uni对象为空后的报错 139 | ## 1.9.1.2(2022-04-25) 140 | - fix: 删除多余文件 141 | ## 1.9.1.1(2022-04-25) 142 | - fix: 修复图片不显示问题 143 | ## 1.9.1(2022-04-12) 144 | - fix: 因四舍五入导致有些机型错位 145 | - fix: 修复无views报错 146 | - chore: nvue下因ios无法读取插件内static文件,改由下载方式 147 | ## 1.9.0(2022-03-20) 148 | - fix: 因无法固定尺寸导致生成图片不全 149 | - fix: 特定情况下text判断无效 150 | - chore: 本地化APP Nvue webview 151 | ## 1.8.9(2022-02-20) 152 | - fix: 修复 小程序下载最多10次并发的问题 153 | - fix: 修复 APP端无法获取本地图片 154 | - fix: 修复 APP Nvue端不执行问题 155 | - chore: 增加图片缓存机制 156 | ## 1.8.8.8(2022-01-27) 157 | - fix: 修复 主动调用尺寸问题 158 | ## 1.8.8.6(2022-01-26) 159 | - fix: 修复 nvue 下无宽度时获取父级宽度 160 | - fix: 修复 ios app 无法渲染问题 161 | ## 1.8.8(2022-01-23) 162 | - fix: 修复 主动调用时无节点问题 163 | - fix: 修复 `box-shadow` 颜色问题 164 | - fix: 修复 `transform:rotate` 角度位置问题 165 | - feat: 增加 `overflow:hidden` 166 | ## 1.8.7(2022-01-07) 167 | - fix: 修复 image 方向为 `right` 时原始宽高问题 168 | - feat: 支持 view 设置背景图 `background-image: url(xxx)` 169 | - chore: 去掉可选链 170 | ## 1.8.6(2021-11-28) 171 | - feat: 支持`view`对`inline-block`的子集使用`text-align` 172 | ## 1.8.5.5(2021-08-17) 173 | - chore: 更新文档,删除 replace 174 | - fix: 修复 text 值为 number时报错 175 | ## 1.8.5.4(2021-08-16) 176 | - fix: 字节小程序兼容 177 | ## 1.8.5.3(2021-08-15) 178 | - fix: 修复线性渐变与css现实效果不一致的问题 179 | - chore: 更新文档 180 | ## 1.8.5.2(2021-08-13) 181 | - chore: 增加`background-image`、`background-repeat` 能力,主要用于背景纹理的绘制,并不是代替`image`。例如:大面积的重复平铺的水印 182 | - 注意:这个功能H5暂时无法使用,因为[官方的API有BUG](https://ask.dcloud.net.cn/question/128793),待官方修复!!! 183 | ## 1.8.5.1(2021-08-10) 184 | - fix: 修复因`margin`报错问题 185 | ## 1.8.5(2021-08-09) 186 | - chore: 增加margin支持`auto`,以达到居中效果 187 | ## 1.8.4(2021-08-06) 188 | - chore: 增加判断缓存文件条件 189 | - fix: 修复css 多余空格报错问题 190 | ## 1.8.3(2021-08-04) 191 | - tips: 1.6.x 以下的版本升级到1.8.x后要为每个元素都加上定位:position: 'absolute' 192 | - fix: 修复只有一个view子元素时不计算高度的问题 193 | ## 1.8.2(2021-08-03) 194 | - fix: 修复 path-type 为 `url` 无效问题 195 | - fix: 修复 qrcode `text` 为空时报错问题 196 | - fix: 修复 image `src` 动态设置时不生效问题 197 | - feat: 增加 css 属性 `min-width` `max-width` 198 | ## 1.8.1(2021-08-02) 199 | - fix: 修复无法加载本地图片 200 | ## 1.8.0(2021-08-02) 201 | - chore 文档更新 202 | - 使用旧版的同学不要升级! 203 | ## 1.8.0-beta(2021-07-30) 204 | - ## 全新布局方式 不兼容旧版! 205 | - chore: 布局方式变更 206 | - tips: 微信canvas 2d 不支持真机调试 207 | ## 1.6.6(2021-07-09) 208 | - chore: 统一命名规范,无须主动引入组件 209 | ## 1.6.5(2021-06-08) 210 | - chore: 去掉console 211 | ## 1.6.4(2021-06-07) 212 | - fix: 修复 数字 为纯字符串时不转换的BUG 213 | ## 1.6.3(2021-06-06) 214 | - fix: 修复 PC 端放大的BUG 215 | ## 1.6.2(2021-05-31) 216 | - fix: 修复 报`adaptor is not a function`错误 217 | - fix: 修复 text 多行高度 218 | - fix: 优化 默认文字的基准线 219 | - feat: `@progress`事件,监听绘制进度 220 | ## 1.6.1(2021-02-28) 221 | - 删除多余节点 222 | ## 1.6.0(2021-02-26) 223 | - 调整为uni_modules目录规范 224 | - 修复:transform的rotate不能为负数问题 225 | - 新增:`pathType` 指定生成图片返回的路径类型,可选值有 `base64`、`url` 226 | -------------------------------------------------------------------------------- /src/pages/reader/components/Origin.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 213 | 214 | 267 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "../uni.scss"; 2 | 3 | @mixin reset { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | page { 10 | background-color: #fff; 11 | width: 100%; 12 | min-height: 100vh; 13 | @include reset(); 14 | } 15 | view, 16 | button, 17 | text, 18 | input, 19 | textarea { 20 | font-family: "Arial", "Helvetica Neue", "Helvetica", "sans-serif"; 21 | @include reset(); 22 | } 23 | image, 24 | input, 25 | textarea, 26 | picker { 27 | display: block; 28 | width: 100%; 29 | @include reset(); 30 | } 31 | input::after, 32 | input::before, 33 | button::after, 34 | button::before { 35 | border: none; 36 | @include reset(); 37 | } 38 | 39 | .loader { 40 | transform: scale(0.5); 41 | width: 50px; 42 | aspect-ratio: 1; 43 | display: grid; 44 | } 45 | .loader::before, 46 | .loader::after { 47 | content: ""; 48 | grid-area: 1/1; 49 | --c: no-repeat radial-gradient(farthest-side, #fef8f2 90%, #0000); 50 | background: 51 | var(--c) 50% 0, 52 | var(--c) 50% 100%, 53 | var(--c) 100% 50%, 54 | var(--c) 0 50%; 55 | background-size: 12px 12px; 56 | animation: l12 1s infinite; 57 | } 58 | .loader::before { 59 | margin: 4px; 60 | filter: hue-rotate(45deg); 61 | background-size: 8px 8px; 62 | animation-timing-function: linear; 63 | } 64 | 65 | .loader-dark { 66 | transform: scale(0.5); 67 | width: 50px; 68 | aspect-ratio: 1; 69 | display: grid; 70 | } 71 | .loader-dark::before, 72 | .loader-dark::after { 73 | content: ""; 74 | grid-area: 1/1; 75 | --c: no-repeat radial-gradient(farthest-side, #453f39 90%, #0000); 76 | background: 77 | var(--c) 50% 0, 78 | var(--c) 50% 100%, 79 | var(--c) 100% 50%, 80 | var(--c) 0 50%; 81 | background-size: 12px 12px; 82 | animation: l12 1s infinite; 83 | } 84 | .loader-dark::before { 85 | margin: 4px; 86 | filter: hue-rotate(45deg); 87 | background-size: 8px 8px; 88 | animation-timing-function: linear; 89 | } 90 | 91 | @keyframes l12 { 92 | 100% { 93 | transform: rotate(0.5turn); 94 | } 95 | } 96 | 97 | .button-loading { 98 | display: flex; 99 | align-content: center; 100 | justify-content: center; 101 | } 102 | .uni-easyinput { 103 | .is-focused { 104 | .content-clear-icon { 105 | color: #d44842 !important; 106 | } 107 | } 108 | } 109 | 110 | .uni-searchbar__cancel { 111 | color: var(--theme-primary-color-light-1) !important; 112 | } 113 | 114 | .hz-button-primary { 115 | margin-top: 24rpx; 116 | width: 100%; 117 | height: 100rpx; 118 | border-radius: 50rpx; 119 | line-height: 100rpx; 120 | text-align: center; 121 | background-color: #d44842; 122 | color: #fef8f2; 123 | } 124 | 125 | .hz-button-plain { 126 | margin-top: 24rpx; 127 | width: 100%; 128 | height: 100rpx; 129 | border-radius: 50rpx; 130 | line-height: 100rpx; 131 | text-align: center; 132 | background-color: #fae0da; 133 | color: #d44842; 134 | } 135 | 136 | .hz-button-cancel { 137 | margin-top: 24rpx; 138 | width: 100%; 139 | height: 100rpx; 140 | border-radius: 50rpx; 141 | line-height: 100rpx; 142 | text-align: center; 143 | background-color: var(--theme-bg-color-deep); 144 | color: var(--theme-grey-color); 145 | } 146 | 147 | uni-checkbox { 148 | margin-right: 16rpx; 149 | transform: scale(0.7); 150 | .uni-checkbox-wrapper { 151 | .uni-checkbox-input { 152 | border: 2px solid var(--theme-border-color); 153 | border-radius: 80rpx; 154 | background-color: var(--theme-bg-color); 155 | } 156 | } 157 | } 158 | uni-checkbox.checked { 159 | .uni-checkbox-wrapper { 160 | .uni-checkbox-input { 161 | border-color: var(--theme-icon-active-color); 162 | background-color: var(--theme-icon-active-color); 163 | svg { 164 | color: #f1edea; 165 | path { 166 | fill: #f1edea; 167 | } 168 | } 169 | } 170 | } 171 | } 172 | .theme-dark { 173 | // .hz-button-plain { 174 | // background-color: #d44842; 175 | // color: #fef8f2; 176 | // } 177 | // .hz-button-primary { 178 | // background-color: #fae0da; 179 | // color: #d44842; 180 | // } 181 | .uni-easyinput__content { 182 | background-color: var(--theme-bg-white-color) !important; 183 | } 184 | .uni-input-input { 185 | color: var(--theme-primary-color) !important; 186 | } 187 | .uni-textarea-textarea { 188 | color: var(--theme-primary-color) !important; 189 | } 190 | } 191 | 192 | // h5默认是100vh,即使页面没有内容也会滚动,这里取消这个设定 193 | // 注意:scss 条件编译必须以 css 注释为标准才能条件编译 194 | /* #ifdef H5 */ 195 | uni-page-body { 196 | min-height: auto !important; 197 | } 198 | /* #endif */ 199 | 200 | /* flex布局 */ 201 | .flex { 202 | display: flex; 203 | flex-wrap: nowrap; 204 | } 205 | .fwrap { 206 | display: flex; 207 | flex-wrap: wrap; 208 | } 209 | .f1 { 210 | flex: 1; 211 | } 212 | .f2 { 213 | flex: 2; 214 | } 215 | .f3 { 216 | flex: 3; 217 | } 218 | /* 垂直居中 */ 219 | .fvertical { 220 | display: flex; 221 | align-items: center; 222 | } 223 | /* 水平居中 */ 224 | .fcenter { 225 | display: flex; 226 | justify-content: center; 227 | } 228 | /* 水平+垂直居中 */ 229 | .fvc { 230 | display: flex; 231 | align-items: center; 232 | justify-content: center; 233 | } 234 | /* 右对齐 */ 235 | .fright { 236 | display: flex; 237 | justify-content: flex-end; 238 | } 239 | /* 两端对齐 */ 240 | .fbetween { 241 | display: flex; 242 | justify-content: space-between; 243 | } 244 | /* 靠底部对齐 */ 245 | .fbottom { 246 | display: flex; 247 | align-items: flex-end; 248 | } 249 | 250 | /* 溢出...显示 当前节点生效 */ 251 | .ellipsis { 252 | overflow: hidden; 253 | text-overflow: ellipsis; 254 | white-space: nowrap; 255 | } 256 | 257 | /* 溢出...显示 子节点生效 */ 258 | .ellipsis_1 { 259 | @include ellipsis(1); 260 | } 261 | .ellipsis_2 { 262 | @include ellipsis(2); 263 | } 264 | .ellipsis_3 { 265 | @include ellipsis(3); 266 | } 267 | 268 | /* 卡片 */ 269 | .card { 270 | border-radius: 2px; 271 | box-shadow: 272 | 0px 1px 5px 0px rgba(0, 0, 0, 0.2), 273 | 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 274 | 0px 3px 1px -2px rgba(0, 0, 0, 0.12); 275 | background-color: #fff; 276 | overflow: hidden; 277 | } 278 | 279 | .button { 280 | @include button($dark, #eee); 281 | } 282 | .button-dark { 283 | @include button(#e3b480, $dark); 284 | } 285 | .button-pink { 286 | @include button($white, $pink); 287 | } 288 | .button-red { 289 | @include button($white, $red); 290 | } 291 | 292 | .line { 293 | width: 100%; 294 | padding-top: 40rpx; 295 | margin-bottom: 40rpx; 296 | border-bottom: solid 1px #eee; 297 | } 298 | 299 | /* 上下左右边距 */ 300 | @for $index from 1 through 5 { 301 | .mgl_#{$index}0 { 302 | margin-left: 10rpx * $index; 303 | } 304 | .mgr_#{$index}0 { 305 | margin-right: 10rpx * $index; 306 | } 307 | .mgt_#{$index}0 { 308 | margin-top: 10rpx * $index; 309 | } 310 | .mgb_#{$index}0 { 311 | margin-bottom: 10rpx * $index; 312 | } 313 | .pdl_#{$index}0 { 314 | padding-left: 10rpx * $index; 315 | } 316 | .pdr_#{$index}0 { 317 | padding-right: 10rpx * $index; 318 | } 319 | .pdt_#{$index}0 { 320 | padding-top: 10rpx * $index; 321 | } 322 | .pdb_#{$index}0 { 323 | padding-bottom: 10rpx * $index; 324 | } 325 | } 326 | 327 | .uni-scroll-view-refresh { 328 | transform: scale(0.65); 329 | .uni-scroll-view-refresh-inner { 330 | background-color: #fdf6f0; 331 | } 332 | .uni-scroll-view-refresh__icon { 333 | fill: #d44842; 334 | } 335 | .uni-scroll-view-refresh__spinner { 336 | circle { 337 | stroke: #d44842; 338 | color: #d44842; 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/pages/tabBar/components/bookDetail.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 160 | 161 | 252 | -------------------------------------------------------------------------------- /src/static/iconfont/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4659366", 3 | "name": "reader", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "20168979", 10 | "name": "禁用", 11 | "font_class": "jinyong", 12 | "unicode": "e66f", 13 | "unicode_decimal": 58991 14 | }, 15 | { 16 | "icon_id": "33307514", 17 | "name": "启用", 18 | "font_class": "qiyong", 19 | "unicode": "e75e", 20 | "unicode_decimal": 59230 21 | }, 22 | { 23 | "icon_id": "1817762", 24 | "name": "复制链接", 25 | "font_class": "fuzhilianjie", 26 | "unicode": "e62c", 27 | "unicode_decimal": 58924 28 | }, 29 | { 30 | "icon_id": "7579868", 31 | "name": "分享海报", 32 | "font_class": "haibao", 33 | "unicode": "e674", 34 | "unicode_decimal": 58996 35 | }, 36 | { 37 | "icon_id": "7610933", 38 | "name": "分组", 39 | "font_class": "fenzu1", 40 | "unicode": "e602", 41 | "unicode_decimal": 58882 42 | }, 43 | { 44 | "icon_id": "20754787", 45 | "name": "新增", 46 | "font_class": "xinzeng", 47 | "unicode": "e759", 48 | "unicode_decimal": 59225 49 | }, 50 | { 51 | "icon_id": "37291154", 52 | "name": "新增分组", 53 | "font_class": "xinzengfenzu", 54 | "unicode": "e746", 55 | "unicode_decimal": 59206 56 | }, 57 | { 58 | "icon_id": "8867799", 59 | "name": "回到顶部", 60 | "font_class": "huidaodingbu", 61 | "unicode": "e622", 62 | "unicode_decimal": 58914 63 | }, 64 | { 65 | "icon_id": "21935586", 66 | "name": "我的书架", 67 | "font_class": "shujia", 68 | "unicode": "e63c", 69 | "unicode_decimal": 58940 70 | }, 71 | { 72 | "icon_id": "25202491", 73 | "name": "添加书架", 74 | "font_class": "tianjiashujia", 75 | "unicode": "e634", 76 | "unicode_decimal": 58932 77 | }, 78 | { 79 | "icon_id": "25273026", 80 | "name": "已添加书架", 81 | "font_class": "zaishujia", 82 | "unicode": "e636", 83 | "unicode_decimal": 58934 84 | }, 85 | { 86 | "icon_id": "3437096", 87 | "name": "置顶", 88 | "font_class": "zhiding", 89 | "unicode": "e718", 90 | "unicode_decimal": 59160 91 | }, 92 | { 93 | "icon_id": "4933315", 94 | "name": "分组", 95 | "font_class": "fenzu", 96 | "unicode": "e726", 97 | "unicode_decimal": 59174 98 | }, 99 | { 100 | "icon_id": "37288742", 101 | "name": "分享", 102 | "font_class": "fenxiang1", 103 | "unicode": "e64c", 104 | "unicode_decimal": 58956 105 | }, 106 | { 107 | "icon_id": "40183285", 108 | "name": "移除书架", 109 | "font_class": "yichushujia", 110 | "unicode": "e601", 111 | "unicode_decimal": 58881 112 | }, 113 | { 114 | "icon_id": "1223993", 115 | "name": "火", 116 | "font_class": "huo", 117 | "unicode": "e6c9", 118 | "unicode_decimal": 59081 119 | }, 120 | { 121 | "icon_id": "5485668", 122 | "name": "下", 123 | "font_class": "xia", 124 | "unicode": "e61b", 125 | "unicode_decimal": 58907 126 | }, 127 | { 128 | "icon_id": "5835478", 129 | "name": "刷新", 130 | "font_class": "shuaxin", 131 | "unicode": "e610", 132 | "unicode_decimal": 58896 133 | }, 134 | { 135 | "icon_id": "6582362", 136 | "name": "删除", 137 | "font_class": "shanchu", 138 | "unicode": "e642", 139 | "unicode_decimal": 58946 140 | }, 141 | { 142 | "icon_id": "6629848", 143 | "name": "上", 144 | "font_class": "shang-", 145 | "unicode": "e63f", 146 | "unicode_decimal": 58943 147 | }, 148 | { 149 | "icon_id": "17361895", 150 | "name": "展开", 151 | "font_class": "expand", 152 | "unicode": "e61e", 153 | "unicode_decimal": 58910 154 | }, 155 | { 156 | "icon_id": "716229", 157 | "name": "左", 158 | "font_class": "arrowleft", 159 | "unicode": "e61a", 160 | "unicode_decimal": 58906 161 | }, 162 | { 163 | "icon_id": "1404747", 164 | "name": "圆-填充", 165 | "font_class": "circle-filled", 166 | "unicode": "e611", 167 | "unicode_decimal": 58897 168 | }, 169 | { 170 | "icon_id": "3315448", 171 | "name": "右", 172 | "font_class": "arrowright", 173 | "unicode": "e633", 174 | "unicode_decimal": 58931 175 | }, 176 | { 177 | "icon_id": "27188531", 178 | "name": "圆", 179 | "font_class": "circle1", 180 | "unicode": "e621", 181 | "unicode_decimal": 58913 182 | }, 183 | { 184 | "icon_id": "398280", 185 | "name": "白天", 186 | "font_class": "baitian", 187 | "unicode": "e632", 188 | "unicode_decimal": 58930 189 | }, 190 | { 191 | "icon_id": "1621098", 192 | "name": "夜间", 193 | "font_class": "night", 194 | "unicode": "e6fd", 195 | "unicode_decimal": 59133 196 | }, 197 | { 198 | "icon_id": "4171100", 199 | "name": "书签", 200 | "font_class": "shuqian", 201 | "unicode": "e603", 202 | "unicode_decimal": 58883 203 | }, 204 | { 205 | "icon_id": "4895492", 206 | "name": "加入书架", 207 | "font_class": "jiarushujia", 208 | "unicode": "e62a", 209 | "unicode_decimal": 58922 210 | }, 211 | { 212 | "icon_id": "9097247", 213 | "name": "目录", 214 | "font_class": "mulu", 215 | "unicode": "e620", 216 | "unicode_decimal": 58912 217 | }, 218 | { 219 | "icon_id": "10715436", 220 | "name": "设置", 221 | "font_class": "shezhi", 222 | "unicode": "e635", 223 | "unicode_decimal": 58933 224 | }, 225 | { 226 | "icon_id": "15705447", 227 | "name": "缓存", 228 | "font_class": "huancun", 229 | "unicode": "e6b0", 230 | "unicode_decimal": 59056 231 | }, 232 | { 233 | "icon_id": "16322570", 234 | "name": "去当前", 235 | "font_class": "qudangqian", 236 | "unicode": "e6eb", 237 | "unicode_decimal": 59115 238 | }, 239 | { 240 | "icon_id": "24516455", 241 | "name": "下载云", 242 | "font_class": "xiazaiyun", 243 | "unicode": "e679", 244 | "unicode_decimal": 59001 245 | }, 246 | { 247 | "icon_id": "33680200", 248 | "name": "去顶部", 249 | "font_class": "qudingbu", 250 | "unicode": "e641", 251 | "unicode_decimal": 58945 252 | }, 253 | { 254 | "icon_id": "40646857", 255 | "name": "去底部", 256 | "font_class": "qudibu", 257 | "unicode": "e70f", 258 | "unicode_decimal": 59151 259 | }, 260 | { 261 | "icon_id": "41894611", 262 | "name": "订阅", 263 | "font_class": "dingyue", 264 | "unicode": "e6bc", 265 | "unicode_decimal": 59068 266 | }, 267 | { 268 | "icon_id": "160206", 269 | "name": "search", 270 | "font_class": "search", 271 | "unicode": "e600", 272 | "unicode_decimal": 58880 273 | }, 274 | { 275 | "icon_id": "269810", 276 | "name": "more", 277 | "font_class": "more", 278 | "unicode": "e6ad", 279 | "unicode_decimal": 59053 280 | }, 281 | { 282 | "icon_id": "11372706", 283 | "name": "搜索", 284 | "font_class": "sousuo", 285 | "unicode": "e8b9", 286 | "unicode_decimal": 59577 287 | }, 288 | { 289 | "icon_id": "3755552", 290 | "name": "设置", 291 | "font_class": "icon-test", 292 | "unicode": "e618", 293 | "unicode_decimal": 58904 294 | }, 295 | { 296 | "icon_id": "6360795", 297 | "name": "export", 298 | "font_class": "export", 299 | "unicode": "e669", 300 | "unicode_decimal": 58985 301 | }, 302 | { 303 | "icon_id": "39973232", 304 | "name": "我的", 305 | "font_class": "wode", 306 | "unicode": "101a9", 307 | "unicode_decimal": 65961 308 | } 309 | ] 310 | } 311 | -------------------------------------------------------------------------------- /src/pages/blank/origin.vue: -------------------------------------------------------------------------------- 1 | 82 | 202 | 287 | -------------------------------------------------------------------------------- /src/components/share.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 201 | 202 | 251 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { modifyData } from "@/utils"; 2 | import { reactive, ref } from "vue"; 3 | import ModuleAppOption from "./AppOption"; 4 | import ModuleUser from "./User"; 5 | import { cloneDeep } from "lodash"; 6 | import { v4 as uuidv4 } from "uuid"; 7 | 8 | // import { getIamgeByName } from "@/utils"; 9 | 10 | const BOOK_OPTION = { 11 | /** 是否首次打开 */ 12 | first: true, 13 | /** 主题 */ 14 | theme: "eyeProtect", 15 | themeLight: "eyeProtect", 16 | themeIndex: 1, 17 | turnPageType: "cover", // 'cover' | 'transition' | 'none' 18 | fontFamilyList: [ 19 | { 20 | name: "黑体", 21 | value: "Arial", 22 | }, 23 | { 24 | name: "草书", 25 | value: "Helvetica Neue", 26 | }, 27 | { 28 | name: "等宽", 29 | value: "Helvetica", 30 | }, 31 | { 32 | name: "仿宋", 33 | value: "sans-serif", 34 | }, 35 | ], 36 | fontFamilyIndex: 0, 37 | /** 阅读器字体信息 */ 38 | sizeInfo: { 39 | /** 标题字体大小 */ 40 | title: 24, 41 | /** 段落字体大小 */ 42 | p: 16, 43 | /** 标题行高 */ 44 | tLineHeight: 24 * 1.5, 45 | /** 段落行高 */ 46 | pLineHeight: 18 * 1.5, 47 | /** 标题边距 */ 48 | titleMargin: 16, 49 | /** 左右边距 */ 50 | lrPadding: 24, 51 | /** 上边距 */ 52 | tPadding: 12, 53 | /** 下边距 */ 54 | bPadding: 32, 55 | margin: 12, 56 | // 小说信息高度 57 | infoHeight: 30, 58 | // 小说信息边距 59 | infoMarginBottom: 10, 60 | // 页码高度 61 | pagginationHeight: 20, 62 | }, 63 | }; 64 | 65 | export class ModuleStore extends ModuleAppOption { 66 | constructor() { 67 | super(); 68 | this.initBookOption(); 69 | this.initBookShelves(); 70 | this.initHistoryBookShelves(); 71 | this.initHistorySearchList(); 72 | this.initBookGroups(); 73 | this.initTempData(); 74 | this.initSourceMap(); 75 | } 76 | 77 | /** 图片对象集 */ 78 | get imageInfo() { 79 | // 需要用作背景图的可以用`import`引入 80 | return { 81 | iconWx: "/static/logo_wx.png", 82 | iconZfb: "/static/logo_zfb.png", 83 | logo: "/static/logo.png", 84 | defaultHead: "/static/default_head.png", 85 | noneData: "/static/none_data.png", 86 | iconArrowRight: "/static/arrow-right.png", 87 | }; 88 | } 89 | 90 | /** 用户状态 */ 91 | readonly user = new ModuleUser(); 92 | 93 | /** 小说操作信息 */ 94 | readonly bookOption = reactive(cloneDeep(BOOK_OPTION)); 95 | /** 书架信息 */ 96 | readonly bookShelves = reactive({ 97 | sort: "default", 98 | data: [], 99 | }); 100 | /** 浏览历史 */ 101 | readonly historyBookShelves = ref([]); 102 | /** 临时缓存 */ 103 | readonly tempData = ref([]); 104 | /** 搜索历史 */ 105 | readonly historySearchList = ref>([]); 106 | // 分组列表 107 | readonly bookGroups = ref>([]); 108 | readonly sourceMap = ref>([]); 109 | 110 | /** 保存小说操作信息 */ 111 | saveBookOption() { 112 | uni.setStorageSync("book-app-option", JSON.stringify(this.bookOption)); 113 | } 114 | 115 | /** 获取小说操作信息 */ 116 | private initBookOption() { 117 | const data = uni.getStorageSync("book-app-option"); 118 | if (data) { 119 | modifyData(this.bookOption, JSON.parse(data)); 120 | } else { 121 | uni.setStorageSync("book-app-option", JSON.stringify(this.bookOption)); 122 | } 123 | } 124 | 125 | /** 清除小说操作信息 */ 126 | removeBookOption() { 127 | modifyData(this.bookOption, cloneDeep(BOOK_OPTION)); 128 | uni.removeStorageSync("book-app-option"); 129 | } 130 | 131 | saveBookShelves() { 132 | uni.setStorageSync("__User_Bookshelves_Data__", JSON.stringify(this.bookShelves)); 133 | } 134 | 135 | private initBookShelves() { 136 | const data = uni.getStorageSync("__User_Bookshelves_Data__"); 137 | if (data) { 138 | const values: BookShelves = JSON.parse(data); 139 | values.data.forEach((book) => { 140 | if (book.uid.length !== 36) { 141 | // 矫正uid, 一些中途或者意外的退出可能导致uid错误 142 | book.uid = uuidv4(); 143 | } 144 | }); 145 | 146 | modifyData(this.bookShelves, values); 147 | } 148 | } 149 | 150 | removeBookShelves() { 151 | uni.removeStorageSync("__User_Bookshelves_Data__"); 152 | } 153 | 154 | saveHistoryBookShelves() { 155 | uni.setStorageSync("__User_History_BookShelves_Data__", JSON.stringify(this.historyBookShelves.value)); 156 | } 157 | modifyHistoryBookShelves(data: BooksShelvesData) { 158 | const idx = this.historyBookShelves.value.findIndex( 159 | (i) => i.bookName === data.bookName && i.author === data.author, 160 | ); 161 | if (idx > -1) { 162 | this.historyBookShelves.value.splice(idx, 1); 163 | } 164 | this.historyBookShelves.value.unshift(data); 165 | 166 | if (this.historyBookShelves.value.length > 100) { 167 | this.historyBookShelves.value.splice(100); 168 | } 169 | this.saveHistoryBookShelves(); 170 | } 171 | private initHistoryBookShelves() { 172 | const data = uni.getStorageSync("__User_History_BookShelves_Data__"); 173 | if (data) { 174 | this.historyBookShelves.value = JSON.parse(data); 175 | } 176 | } 177 | 178 | removeHistoryBookShelves() { 179 | this.historyBookShelves.value = []; 180 | uni.removeStorageSync("__User_History_BookShelves_Data__"); 181 | } 182 | 183 | saveBookGroups() { 184 | uni.setStorageSync("__User_Book_Groups__", JSON.stringify(this.bookGroups.value)); 185 | } 186 | 187 | private initBookGroups() { 188 | const data = uni.getStorageSync("__User_Book_Groups__"); 189 | if (data) { 190 | this.bookGroups.value = JSON.parse(data); 191 | } 192 | } 193 | removeBookGroups() { 194 | this.bookGroups.value = []; 195 | uni.removeStorageSync("__User_Book_Groups__"); 196 | } 197 | 198 | saveSourceMap() { 199 | uni.setStorageSync("__User_Source_Map__", JSON.stringify(this.sourceMap.value)); 200 | } 201 | 202 | private initSourceMap() { 203 | const data = uni.getStorageSync("__User_Source_Map__"); 204 | if (data) { 205 | this.sourceMap.value = JSON.parse(data); 206 | } 207 | } 208 | removeSourceMap() { 209 | this.sourceMap.value = []; 210 | uni.removeStorageSync("__User_Source_Map__"); 211 | } 212 | 213 | saveHistorySearchList() { 214 | uni.setStorageSync("__User_History_Search__", JSON.stringify(this.historySearchList.value)); 215 | } 216 | 217 | private initHistorySearchList() { 218 | const data = uni.getStorageSync("__User_History_Search__"); 219 | if (data) { 220 | this.historySearchList.value = JSON.parse(data); 221 | } 222 | } 223 | removeHistorySearchList() { 224 | this.historySearchList.value = []; 225 | uni.removeStorageSync("__User_History_Search__"); 226 | } 227 | 228 | modifyTempData(params: { author: string; bookName: string; uid: string }) { 229 | const idx = this.bookShelves.data.findIndex((i) => i.uid === params.uid); 230 | // 如果书架里面存在书籍信息,移除映射 231 | if (idx > -1) { 232 | const ix = this.tempData.value.findIndex((i) => i.author === params.author && i.bookName === params.bookName); 233 | if (ix > -1) { 234 | this.tempData.value.splice(ix, 1); 235 | } 236 | } else { 237 | this.tempData.value.push(params); 238 | } 239 | this.saveTempData(); 240 | } 241 | saveTempData() { 242 | uni.setStorageSync("__User_Temp_Data__", JSON.stringify(this.tempData.value)); 243 | } 244 | private initTempData() { 245 | const data = uni.getStorageSync("__User_Temp_Data__"); 246 | if (data) { 247 | const cache: TempDataReflect[] = JSON.parse(data); 248 | cache.forEach((i) => { 249 | if (i.uid) { 250 | uni.removeStorage({ 251 | key: i.uid, 252 | }); 253 | } 254 | }); 255 | } 256 | this.removeTempData(); 257 | } 258 | removeTempData() { 259 | this.tempData.value = []; 260 | uni.removeStorageSync("__User_Temp_Data__"); 261 | } 262 | 263 | clearAllCache() { 264 | this.bookShelves.data.forEach((i) => { 265 | uni.removeStorage({ 266 | key: i.uid, 267 | }); 268 | }); 269 | this.removeBookShelves(); 270 | this.removeHistorySearchList(); 271 | this.removeHistoryBookShelves(); 272 | this.removeTempData(); 273 | this.removeBookOption(); 274 | this.removeSourceMap(); 275 | } 276 | } 277 | 278 | /** 279 | * 状态管理模块 280 | * - `OOP`单例设计模式 281 | * - 参考 [你不需要`Vuex`](https://juejin.cn/post/6844903904023429128) 282 | */ 283 | const store = new ModuleStore(); 284 | 285 | export default store; 286 | --------------------------------------------------------------------------------