refactor(modules/web): Mirgrat to typescript; fix bugs

This commit is contained in:
Xwite
2024-10-14 21:46:45 +08:00
parent 3eded53041
commit 30b31b9c96
81 changed files with 6396 additions and 3166 deletions
+6
View File
@@ -0,0 +1,6 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
-96
View File
@@ -1,96 +0,0 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"ElMessage": true,
"ElMessageBox": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"acceptHMRUpdate": true,
"computed": true,
"createApp": true,
"createPinia": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"store": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useBookStore": true,
"useCssModule": true,
"useCssVars": true,
"useLink": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"useSourceStore": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"DirectiveBinding": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"WritableComputedRef": true,
"onWatcherCleanup": true,
"toValue": true,
"useId": true,
"useModel": true,
"useTemplateRef": true
}
}
-20
View File
@@ -1,20 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"plugin:prettier/recommended",
"./.eslintrc-auto-import.json",
],
parserOptions: {
ecmaVersion: "latest",
},
rules: {
"no-unused-vars": "warn",
},
};
+18 -16
View File
@@ -1,28 +1,30 @@
.DS_Store
node_modules
/dist
/doc
# local env files
.env.local
.env.*.local
# Log files
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vim
.vscode/*
!.vscode/extensions.json
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/package-lock.json
/yarn.lock
pnpm-lock.yaml
*.tsbuildinfo
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"arrowParens": "avoid"
}
+1 -1
View File
@@ -9,7 +9,7 @@
| ![IE](https://cdn.jsdelivr.net/npm/@browser-logos/edge/edge_32x32.png) | ![Firefox](https://cdn.jsdelivr.net/npm/@browser-logos/firefox/firefox_32x32.png) | ![Chrome](https://cdn.jsdelivr.net/npm/@browser-logos/chrome/chrome_32x32.png) | ![Safari](https://cdn.jsdelivr.net/npm/@browser-logos/safari/safari_32x32.png) |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
| Edge ≥ 85 | Firefox ≥ 79 | Chrome ≥ 85 | Safari ≥ 14.1 |
## 开发
> 需要阅读app提供后端服务
+3
View File
@@ -0,0 +1,3 @@
/// <reference types="vite/client" />
declare module "vue3-virtual-scroll-list";
+19
View File
@@ -0,0 +1,19 @@
import pluginVue from 'eslint-plugin-vue'
import vueTsEslintConfig from '@vue/eslint-config-typescript'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', "src/plugins/jump.js"],
},
...pluginVue.configs['flat/essential'],
...vueTsEslintConfig(),
skipFormatting,
]
+1 -1
View File
@@ -8,6 +8,6 @@
<body>
<div id="app"></div>
<script type="module" src="./src/main.js"></script>
<script type="module" src="./src/main.ts"></script>
</body>
</html>
-20
View File
@@ -1,20 +0,0 @@
{
"compilerOptions": {
"types": ["@element-plus/icons-vue", "@vueuse/shared", "vite/client"],
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler",
"checkJs": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"],
"@api": ["./src/api"],
"@utils/*": ["./src/utils/*"]
}
},
//"exclude": ["node_modules", "dist"],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.js", "src/**/*.vue"]
}
+20 -10
View File
@@ -9,9 +9,12 @@
},
"scripts": {
"dev": "vite",
"build": "vite build && node scripts/sync.js",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint:fix": "eslint . --fix",
"format": "prettier --write src/"
},
"dependencies": {
"@element-plus/icons-svg": "^2.3.1",
@@ -19,24 +22,31 @@
"@vueuse/core": "^11.1.0",
"@vueuse/shared": "^11.1.0",
"axios": "^1.7.7",
"element-plus": "^2.8.4",
"element-plus": "^2.8.5",
"hotkeys-js": "^3.13.7",
"pinia": "^2.2.4",
"vue": "^3.5.11",
"vue": "^3.5.12",
"vue-router": "^4.4.5",
"vue3-virtual-scroll-list": "^0.2.1"
},
"devDependencies": {
"@eslint/compat": "^1.2.0",
"@eslint/js": "^9.12.0",
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.16.11",
"@vitejs/plugin-vue": "^5.1.4",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.28.0",
"@vue/eslint-config-prettier": "^10.0.0",
"@vue/eslint-config-typescript": "^14.0.1",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.12.0",
"eslint-plugin-vue": "^9.29.0",
"npm-run-all2": "^6.2.3",
"prettier": "^3.3.3",
"sass": "^1.79.4",
"typescript": "~5.5.4",
"unplugin-auto-import": "^0.18.3",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.8"
"vite": "^5.4.8",
"vue-tsc": "^2.1.6"
}
}
+2844
View File
File diff suppressed because it is too large Load Diff
+219
View File
@@ -0,0 +1,219 @@
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/api */
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/web */
import type { webReadConfig } from '@/web'
import ajax from './axios'
import type {
BaseBook,
Book,
BookChapter,
BookProgress,
SeachBook,
} from '@/book'
import type { Source } from '@/source'
export type LeagdoApiResponse<T> = {
isSuccess: boolean
errorMsg: string
data: T
}
export let legado_http_entry_point = ''
export let legado_webSocket_entry_point = ''
let wsOnError: typeof WebSocket.prototype.onerror = () => {}
export const setWebsocketOnError = (
callback: typeof WebSocket.prototype.onerror,
) => {
//WebSocket.prototype.onerror = callback
wsOnError = callback
}
export const setApiEntryPoint = (
http_entry_point: string,
webSocket_entry_point: string,
) => {
legado_http_entry_point = http_entry_point
legado_webSocket_entry_point = webSocket_entry_point
ajax.defaults.baseURL = http_entry_point.toString()
}
// 书架API
// Http
const getReadConfig = async (http_url = legado_http_entry_point) => {
const { data } = await ajax.get<LeagdoApiResponse<string>>('/getReadConfig', {
baseURL: http_url.toString(),
timeout: 3000,
})
if (data.isSuccess) {
try {
return JSON.parse(data.data) as webReadConfig
} catch {}
}
}
const saveReadConfig = (config: webReadConfig) =>
ajax.post<LeagdoApiResponse<string>>('/saveReadConfig', config)
const saveBookProgress = (bookProgress: BookProgress) =>
ajax.post('/saveBookProgress', bookProgress)
const saveBookProgressWithBeacon = (bookProgress: BookProgress) => {
if (!bookProgress) return
// 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
navigator.sendBeacon(
new URL('/saveBookProgress', legado_http_entry_point),
JSON.stringify(bookProgress),
)
}
const getBookShelf = () => ajax.get<LeagdoApiResponse<Book[]>>('/getBookshelf')
const getChapterList = (/** @type {string} */ bookUrl: string) =>
ajax.get<LeagdoApiResponse<BookChapter[]>>(
'/getChapterList?url=' + encodeURIComponent(bookUrl),
)
const getBookContent = (
/** @type {string} */ bookUrl: string,
/** @type {number} */ chapterIndex: number,
) =>
ajax.get<LeagdoApiResponse<string>>(
'/getBookContent?url=' +
encodeURIComponent(bookUrl) +
'&index=' +
chapterIndex,
)
// webSocket
const search = (
searchKey: string,
onReceive: (data: SeachBook[]) => void,
onFinish: () => void,
) => {
const socket = new WebSocket(
new URL('/searchBook', legado_webSocket_entry_point),
)
socket.onerror = wsOnError
socket.onopen = () => {
socket.send(`{"key":"${searchKey}"}`)
}
socket.onmessage = ({ data }) => {
try {
onReceive(JSON.parse(data))
} catch {
onFinish()
}
}
socket.onclose = () => {
onFinish()
}
}
const saveBook = (book: BaseBook) =>
ajax.post<LeagdoApiResponse<string>>('/saveBook', book)
const deleteBook = (book: BaseBook) =>
ajax.post<LeagdoApiResponse<string>>('/deleteBook', book)
const isBookSource = /bookSource/i.test(location.href)
// 源编辑API
// Http
const getSources = () =>
isBookSource ? ajax.get('/getBookSources') : ajax.get('/getRssSources')
const saveSource = (data: Source) =>
isBookSource
? ajax.post<LeagdoApiResponse<string>>('/saveBookSource', data)
: ajax.post<LeagdoApiResponse<string>>('/saveRssSource', data)
const saveSources = (data: Source[]) =>
isBookSource
? ajax.post<LeagdoApiResponse<Source[]>>('/saveBookSources', data)
: ajax.post<LeagdoApiResponse<Source[]>>('/saveRssSources', data)
const deleteSource = (data: Source[]) =>
isBookSource
? ajax.post<LeagdoApiResponse<string>>('/deleteBookSources', data)
: ajax.post<LeagdoApiResponse<string>>('/deleteRssSources', data)
// webSocket
const debug = (
/** @type {string} */ sourceUrl: string,
/** @type {string} */ searchKey: string,
/** @type {(data: string) => void} */ onReceive: (data: string) => void,
/** @type {() => void} */ onFinish: () => void,
) => {
const url = new URL(
`/${isBookSource ? 'bookSource' : 'rssSource'}Debug`,
legado_webSocket_entry_point,
)
const socket = new WebSocket(url)
socket.onerror = wsOnError
socket.onopen = () => {
socket.send(JSON.stringify({ tag: sourceUrl, key: searchKey }))
}
socket.onmessage = ({ data }) => onReceive(data)
socket.onclose = () => {
onFinish()
}
}
/**
* 从阅读获取需要特定处理的书籍封面
* @param {string} coverUrl
*/
const getProxyCoverUrl = (coverUrl: string) => {
if (coverUrl.startsWith(legado_http_entry_point)) return coverUrl
return new URL(
'/cover?path=' + encodeURIComponent(coverUrl),
legado_http_entry_point,
).toString()
}
/**
* 从阅读获取需要特定处理的图片
* @param {string} bookUrl
* @param {string} src
* @param {number|`${number}`} width
*/
const getProxyImageUrl = (
bookUrl: string,
src: string,
width: number | `${number}`,
) => {
if (src.startsWith(legado_http_entry_point)) return src
return new URL(
'/image?path=' +
encodeURIComponent(src) +
'&url=' +
encodeURIComponent(bookUrl) +
'&width=' +
width,
legado_http_entry_point,
).toString()
}
export default {
getReadConfig,
saveReadConfig,
saveBookProgress,
saveBookProgressWithBeacon,
getBookShelf,
getChapterList,
getBookContent,
search,
saveBook,
deleteBook,
getSources,
saveSources,
saveSource,
deleteSource,
debug,
getProxyCoverUrl,
getProxyImageUrl,
}
@@ -1,8 +1,8 @@
import axios from "axios";
import axios from 'axios'
/** @type {string} localStorage保存自定义阅读http服务接口的键值 */
export const baseURL_localStorage_key = "remoteUrl"
const SECOND = 1000;
export const baseURL_localStorage_key = 'remoteUrl'
const SECOND = 1000
const ajax = axios.create({
baseURL:
@@ -10,6 +10,6 @@ const ajax = axios.create({
localStorage.getItem(baseURL_localStorage_key) ||
location.origin,
timeout: 120 * SECOND,
});
})
export default ajax;
export default ajax
-266
View File
@@ -1,266 +0,0 @@
import ajax from "./axios";
import { ElMessage } from "element-plus/es";
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/api */
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/web */
/**@type string */
export let legado_http_entry_point = "";
/**@type string */
export let legado_webSocket_entry_point = "";
/**
* @param {string|URL} http_url
* @returns {URL}
* @throws {Error}
*/
export const validatorHttpUrl = (http_url) => {
try {
const url = new URL(http_url);
if (url.toString() === legado_http_entry_point)
throw new Error("Please input different url: " + legado_http_entry_point);
const { protocol } = url;
if (!protocol.startsWith("http"))
throw new Error("Expect http:/https: protocol but " + protocol);
return url;
} catch (e) {
if (localStorage.getItem("remoteUrl") == http_url) {
localStorage.removeItem("remoteUrl");
console.warn("Remove remoteUrl from localStorage");
}
throw new Error("Fail to parse Leagdo remoteUrl " + http_url, { cause: e });
}
};
/**
* @param {string|URL} http_url
* @returns
*/
export const setLeagdoHttpUrl = (http_url) => {
let url = new URL(location.origin); //默认当前网址的origin部分
try {
url = validatorHttpUrl(http_url);
} catch (e) {
console.warn(e);
console.info(
"setLeagdoHttpUrl: FallBack to location.origin: " + location.origin,
);
}
const { protocol, port } = url;
// websocket服务端口 为http服务端口 + 1
let legado_webSocket_port, legado_webSocket_protocol;
if (port !== "") {
legado_webSocket_port = String(Number(port) + 1);
} else {
legado_webSocket_port = protocol.startsWith("https:") ? "444" : "81";
}
// websocket协议是否为加密版本
legado_webSocket_protocol = protocol.startsWith("https:")
? "wss://"
: "ws://";
ajax.defaults.baseURL = url.toString();
legado_http_entry_point = url.toString();
url.protocol = legado_webSocket_protocol;
url.port = legado_webSocket_port;
legado_webSocket_entry_point = url.toString();
console.info("legado_api_config:");
console.table({
"http API入口": legado_http_entry_point,
"webSocket API入口": legado_webSocket_entry_point,
});
};
// 手动初始化 阅读web服务地址
setLeagdoHttpUrl(ajax.defaults.baseURL);
/**
* @param {string|URL|undefined} http_url 不传为当前阅读HTTP服务接口
* @returns
*/
const testLeagdoHttpUrlConnection = async (http_url = legado_http_entry_point) => {
const { data = {} } = await ajax.get("/getReadConfig", {
baseURL: http_url.toString(),
timeout: 3000,
});
// 返回结果应该是JSON 并有键值isSuccess
try {
if ("isSuccess" in data) return data.data;
throw new Error("ReadConfig后端返回格式错误");
} catch {
throw new Error("ReadConfig后端返回格式错误");
}
};
const isSourecEditor = /source/i.test(location.href);
const APIExceptionHandler = (error) => {
if (isSourecEditor) {
ElMessage({
message: "后端错误,检查网络或者阅读app",
type: "error",
});
}
throw error;
};
ajax.interceptors.response.use((response) => response, APIExceptionHandler);
// 书架API
// Http
/** @returns {Promise<import("axios").AxiosResponse<{isSuccess: boolean, data: string, errorMsg:string}>>} */
const getReadConfig = () => ajax.get("/getReadConfig", { timeout: 3000 });
const saveReadConfig = (config) => ajax.post("/saveReadConfig", config);
const saveBookProgress = (bookProgress) =>
ajax.post("/saveBookProgress", bookProgress);
const saveBookProgressWithBeacon = (bookProgress) => {
if (!bookProgress) return;
// 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
navigator.sendBeacon(
new URL("/saveBookProgress", legado_http_entry_point),
JSON.stringify(bookProgress),
);
};
const getBookShelf = () => ajax.get("/getBookshelf");
const getChapterList = (/** @type {string} */ bookUrl) =>
ajax.get("/getChapterList?url=" + encodeURIComponent(bookUrl));
const getBookContent = (
/** @type {string} */ bookUrl,
/** @type {number} */ chapterIndex,
) =>
ajax.get(
"/getBookContent?url=" +
encodeURIComponent(bookUrl) +
"&index=" +
chapterIndex,
);
// webSocket
const search = (
/** @type {string} */ searchKey,
/** @type {(data: string) => void} */ onReceive,
/** @type {() => void} */ onFinish,
) => {
const socket = new WebSocket(
new URL("/searchBook", legado_webSocket_entry_point),
);
socket.onopen = () => {
socket.send(`{"key":"${searchKey}"}`);
};
socket.onmessage = ({ data }) => onReceive(data);
socket.onclose = () => {
onFinish();
};
};
const saveBook = (book) => ajax.post("/saveBook", book);
const deleteBook = (book) => ajax.post("/deleteBook", book);
const isBookSource = /bookSource/i.test(location.href);
// 源编辑API
// Http
const getSources = () =>
isBookSource ? ajax.get("/getBookSources") : ajax.get("/getRssSources");
const saveSource = (data) =>
isBookSource
? ajax.post("/saveBookSource", data)
: ajax.post("/saveRssSource", data);
const saveSources = (data) =>
isBookSource
? ajax.post("/saveBookSources", data)
: ajax.post("/saveRssSources", data);
const deleteSource = (data) =>
isBookSource
? ajax.post("/deleteBookSources", data)
: ajax.post("/deleteRssSources", data);
// webSocket
const debug = (
/** @type {string} */ sourceUrl,
/** @type {string} */ searchKey,
/** @type {(data: string) => void} */ onReceive,
/** @type {() => void} */ onFinish,
) => {
const url = new URL(
`/${isBookSource ? "bookSource" : "rssSource"}Debug`,
legado_webSocket_entry_point,
);
const socket = new WebSocket(url);
socket.onopen = () => {
socket.send(JSON.stringify({ tag: sourceUrl, key: searchKey }));
};
socket.onmessage = ({ data }) => onReceive(data);
socket.onclose = () => {
ElMessage({
message: "调试已关闭!",
type: "info",
});
onFinish();
};
};
/**
* 从阅读获取需要特定处理的书籍封面
* @param {string} coverUrl
*/
const getProxyCoverUrl = (coverUrl) => {
if (coverUrl.startsWith(legado_http_entry_point)) return coverUrl;
return new URL(
"/cover?path=" + encodeURIComponent(coverUrl),
legado_http_entry_point,
).toString();
};
/**
* 从阅读获取需要特定处理的图片
* @param {string} src
* @param {number|`${number}`} width
*/
const getProxyImageUrl = (src, width) => {
if (src.startsWith(legado_http_entry_point)) return src;
return new URL(
"/image?path=" +
encodeURIComponent(src) +
"&url=" +
encodeURIComponent(sessionStorage.getItem("bookUrl")) +
"&width=" +
width,
legado_http_entry_point,
).toString();
};
export default {
getReadConfig,
saveReadConfig,
saveBookProgress,
saveBookProgressWithBeacon,
getBookShelf,
getChapterList,
getBookContent,
search,
saveBook,
deleteBook,
getSources,
saveSources,
saveSource,
deleteSource,
debug,
getProxyCoverUrl,
getProxyImageUrl,
testLeagdoHttpUrlConnection,
};
+96
View File
@@ -0,0 +1,96 @@
import type { AxiosResponse } from 'axios'
import type { LeagdoApiResponse } from './api'
import API, {
setWebsocketOnError,
legado_http_entry_point,
legado_webSocket_entry_point,
setApiEntryPoint,
} from './api'
import ajax from './axios'
import { validatorHttpUrl } from '@/utils/utils'
const LeagdoApiResponseKeys: string[] = Array.of('isSuccess', 'errorMsg')
/** Axios.Interceptor: check if resp is LeagaoLeagdoApiResponse*/
const responseCheckInterceptor = (resp: AxiosResponse) => {
let isLeagdoApiResponse = true
try {
const data = resp.data
for (const key of LeagdoApiResponseKeys) {
if (!(key in data)) {
isLeagdoApiResponse = false
LeagdoApiResponseKeys.length = 0
}
}
if ((data as LeagdoApiResponse<unknown>).isSuccess === true) {
if (!('data' in data)) {
isLeagdoApiResponse = false
LeagdoApiResponseKeys.length = 0
}
}
} catch {
isLeagdoApiResponse = false
}
if (isLeagdoApiResponse === false) {
ElNotification.warning('后端返回内容格式错误')
throw new Error()
}
return resp
}
const axiosErrorInterceptor = (err: unknown) => {
ElNotification.error('后端连接失败,请检查阅读WEB服务或者设置其它可用链接')
throw err
}
// http全局
ajax.interceptors.response.use(responseCheckInterceptor, axiosErrorInterceptor)
// websocket
setWebsocketOnError(axiosErrorInterceptor)
/**
* 按照阅读的默认规则 解析阅读HTTP WebSocket API入口地址
* @returns [http_url, webSocekt_url]
*/
export const parseLeagdoHttpUrlWithDefault = (
http_url: string | URL,
): [string, string] => {
let url = new URL(location.origin) //默认当前网址的origin部分
if (validatorHttpUrl(http_url)) {
url = new URL(http_url)
}
const { protocol, port } = url
// websocket服务端口 为http服务端口 + 1
let legado_webSocket_port
if (port !== '') {
legado_webSocket_port = String(Number(port) + 1)
} else {
legado_webSocket_port = protocol.startsWith('https:') ? '444' : '81'
}
// websocket协议是否为加密版本
const legado_webSocket_protocol = protocol.startsWith('https:')
? 'wss://'
: 'ws://'
const http_entry_point = url.toString()
url.protocol = legado_webSocket_protocol
url.port = legado_webSocket_port
const webSocket_entry_point = url.toString()
console.info('legado_api_config:')
console.table({
'http API入口': legado_http_entry_point,
'webSocket API入口': legado_webSocket_entry_point,
})
return [http_entry_point, webSocket_entry_point]
}
//export const useLeagdoRemoteUrlDialog = () => { }
setApiEntryPoint(
...parseLeagdoHttpUrlWithDefault(ajax.defaults.baseURL as string),
)
export default API
export * from './api'
+1 -1
View File
@@ -5,7 +5,7 @@ body {
}
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
+4 -2
View File
@@ -1,7 +1,9 @@
code {
border-radius: 4px;
padding: .15rem .5rem;
padding: 0.15rem 0.5rem;
background-color: var(--el-fill-color-light);
transition: color .25s, background-color .5s;
transition:
color 0.25s,
background-color 0.5s;
font-size: 14px;
}
+2 -2
View File
@@ -1,5 +1,5 @@
@charset "UTF-8";
@font-face {
font-family: "iconfont";
src: url("./iconfont.woff") format("woff");
font-family: 'iconfont';
src: url('./iconfont.woff') format('woff');
}
+2 -2
View File
@@ -1,7 +1,7 @@
@charset "UTF-8";
@font-face {
font-family: "FZZCYSK";
src: local("☺"), url("./popfont.ttf");
font-family: 'FZZCYSK';
src: local('☺'), url('./popfont.ttf');
font-style: normal;
font-weight: normal;
}
+2 -2
View File
@@ -1,7 +1,7 @@
@charset "UTF-8";
@font-face {
font-family: "FZZCYSK";
src: local("☺"), url("./shelffont.ttf");
font-family: 'FZZCYSK';
src: local('☺'), url('./shelffont.ttf');
font-style: normal;
font-weight: normal;
}
+5 -2
View File
@@ -1,9 +1,12 @@
kbd {
align-items: center;
background: rgba(125, 125, 125, .1);
background: rgba(125, 125, 125, 0.1);
border-radius: 3px;
border: 0;
padding: 4px 5px;
font-weight: bold;
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, .4);
box-shadow:
inset 0 -2px 0 0 #cdcde6,
inset 0 0 1px 1px #fff,
0 1px 2px 1px rgba(30, 35, 90, 0.4);
}
+95 -79
View File
@@ -6,88 +6,104 @@
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const store: typeof import('./store/index.js')['default']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useBookStore: typeof import('./store/bookStore.js')['useBookStore']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useSourceStore: typeof import('./store/sourceStore.js')['useSourceStore']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const EffectScope: (typeof import('vue'))['EffectScope']
const ElMessage: (typeof import('element-plus/es'))['ElMessage']
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
const acceptHMRUpdate: (typeof import('pinia'))['acceptHMRUpdate']
const computed: (typeof import('vue'))['computed']
const createApp: (typeof import('vue'))['createApp']
const createPinia: (typeof import('pinia'))['createPinia']
const customRef: (typeof import('vue'))['customRef']
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
const defineComponent: (typeof import('vue'))['defineComponent']
const defineStore: (typeof import('pinia'))['defineStore']
const effectScope: (typeof import('vue'))['effectScope']
const getActivePinia: (typeof import('pinia'))['getActivePinia']
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
const h: (typeof import('vue'))['h']
const inject: (typeof import('vue'))['inject']
const isProxy: (typeof import('vue'))['isProxy']
const isReactive: (typeof import('vue'))['isReactive']
const isReadonly: (typeof import('vue'))['isReadonly']
const isRef: (typeof import('vue'))['isRef']
const mapActions: (typeof import('pinia'))['mapActions']
const mapGetters: (typeof import('pinia'))['mapGetters']
const mapState: (typeof import('pinia'))['mapState']
const mapStores: (typeof import('pinia'))['mapStores']
const mapWritableState: (typeof import('pinia'))['mapWritableState']
const markRaw: (typeof import('vue'))['markRaw']
const nextTick: (typeof import('vue'))['nextTick']
const onActivated: (typeof import('vue'))['onActivated']
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave']
const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate']
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
const onDeactivated: (typeof import('vue'))['onDeactivated']
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
const onMounted: (typeof import('vue'))['onMounted']
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
const onUnmounted: (typeof import('vue'))['onUnmounted']
const onUpdated: (typeof import('vue'))['onUpdated']
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
const provide: (typeof import('vue'))['provide']
const reactive: (typeof import('vue'))['reactive']
const readonly: (typeof import('vue'))['readonly']
const ref: (typeof import('vue'))['ref']
const resolveComponent: (typeof import('vue'))['resolveComponent']
const setActivePinia: (typeof import('pinia'))['setActivePinia']
const setMapStoreSuffix: (typeof import('pinia'))['setMapStoreSuffix']
const shallowReactive: (typeof import('vue'))['shallowReactive']
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
const shallowRef: (typeof import('vue'))['shallowRef']
const store: (typeof import('./store/index'))['default']
const storeToRefs: (typeof import('pinia'))['storeToRefs']
const toRaw: (typeof import('vue'))['toRaw']
const toRef: (typeof import('vue'))['toRef']
const toRefs: (typeof import('vue'))['toRefs']
const toValue: (typeof import('vue'))['toValue']
const triggerRef: (typeof import('vue'))['triggerRef']
const unref: (typeof import('vue'))['unref']
const useAttrs: (typeof import('vue'))['useAttrs']
const useBookStore: (typeof import('./store/bookStore'))['useBookStore']
const useCssModule: (typeof import('vue'))['useCssModule']
const useCssVars: (typeof import('vue'))['useCssVars']
const useId: (typeof import('vue'))['useId']
const useLink: (typeof import('vue-router'))['useLink']
const useModel: (typeof import('vue'))['useModel']
const useRoute: (typeof import('vue-router'))['useRoute']
const useRouter: (typeof import('vue-router'))['useRouter']
const useSlots: (typeof import('vue'))['useSlots']
const useSourceStore: (typeof import('./store/sourceStore'))['useSourceStore']
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
const watch: (typeof import('vue'))['watch']
const watchEffect: (typeof import('vue'))['watchEffect']
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type {
Component,
ComponentPublicInstance,
ComputedRef,
DirectiveBinding,
ExtractDefaultPropTypes,
ExtractPropTypes,
ExtractPublicPropTypes,
InjectionKey,
PropType,
Ref,
MaybeRef,
MaybeRefOrGetter,
VNode,
WritableComputedRef,
} from 'vue'
import('vue')
}
+109
View File
@@ -0,0 +1,109 @@
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/data/entities */
export type BaseBook = {
name: string
author: string
bookUrl: string
kind?: string
wordCount?: string
variable?: string
/** 忽略序列化
infoHtml?: string
tocHtml?: string
*/
}
export type Book = BaseBook & {
// 目录页Url (toc=table of Contents)
tocUrl: string
// 书源URL(默认BookType.local)
origin: string
//书源名称 or 本地书籍文件名
originName: string
// 分类信息(用户修改)
customTag?: string
// 封面Url(书源获取)
coverUrl?: string
// 封面Url(用户修改)
customCoverUrl?: string
// 简介内容(书源获取)
intro?: string
// 简介内容(用户修改)
customnumberro?: string
// 自定义字符集名称(仅适用于本地书籍)
charset?: string
// 类型详见BookType
type: number
// 自定义分组索引号
group: number
// 最新章节标题
latestChapterTitle?: string
// 最新章节标题更新时间
latestChapterTime: number
// 最近一次更新书籍信息的时间
lastCheckTime: number
// 最近一次发现新章节的数量
lastCheckCount: number
// 书籍目录总数
totalChapterNum: number
// 当前章节名称
durChapterTitle?: string
// 当前章节索引
durChapterIndex: number
// 当前阅读的进度(首行字符的索引位置)
durChapterPos: number
// 最近一次阅读书籍的时间(打开正文的时间)
durChapterTime: number
// 刷新书架时更新书籍信息
canUpdate: boolean
// 手动排序
order: number
//书源排序
originOrder: number
//阅读设置
readConfig?: ReadConfig
//同步时间
syncTime: number
}
export type SeachBook = BaseBook & {
/** 书源 */
origin: string
originName: string
/** BookType */
type: number
coverUrl?: string
intro?: string
latestChapterTitle?: string
/** 目录页Url (toc=table of Contents) */
tocUrl: string
time: number
originOrder: number
chapterWordCountText?: string
chapterWordCount: number0
respondTime: number
}
export type BookProgress = Pick<
Book,
| 'name'
| 'author'
| 'durChapterIndex'
| 'durChapterPos'
| 'durChapterTime'
| 'durChapterTitle'
>
export type BookChapter = {
url: string // 章节地址
title: string // 章节标题
isVolume: boolean // 是否是卷名
baseUrl: string // 用来拼接相对url
bookUrl: string // 书籍地址
index: number // 章节序号
isVip: boolean // 是否VIP
isPay: boolean // 是否已购买
resourceUrl?: string // 音频真实URL
tag?: string // 更新时间或其他章节附加信息
start?: number // 章节起始位置
end?: number // 章节终止位置
startFragmentId?: string //EPUB书籍当前章节的fragmentId
endFragmentId?: string //EPUB书籍下一章节的fragmentId
variable?: string //变量
}
+29 -20
View File
@@ -10,7 +10,7 @@
<div class="cover-img">
<img
class="cover"
:src="getCover(book.coverUrl)"
:src="getCover(book)"
:key="book.coverUrl"
@error.once="proxyImage"
alt=""
@@ -33,15 +33,17 @@
</div>
<div class="update-info" v-if="!isSearch">
<div class="dot"></div>
<div class="size">{{ book.totalChapterNum }}</div>
<div class="size">{{ (book as Book).totalChapterNum }}</div>
<div class="dot"></div>
<div class="date">{{ dateFormat(book.lastCheckTime) }}</div>
<div class="date">
{{ dateFormat((book as Book).lastCheckTime) }}
</div>
</div>
</div>
<div class="intro" v-if="isSearch">{{ book.intro }}</div>
<div class="dur-chapter" v-if="!isSearch">
已读{{ book.durChapterTitle }}
已读{{ (book as Book).durChapterTitle }}
</div>
<div class="last-chapter">最新{{ book.latestChapterTitle }}</div>
</div>
@@ -49,26 +51,32 @@
</div>
</div>
</template>
<script setup>
import { dateFormat, isLegadoUrl } from "../utils/utils";
import API from "@api";
<script setup lang="ts">
import type { Book, SeachBook } from '@/book'
import { dateFormat, isLegadoUrl } from '../utils/utils'
import API from '@api'
const props = defineProps<{
books: Array<Book | SeachBook>
isSearch: boolean
}>()
const props = defineProps(["books", "isSearch"]);
const emit = defineEmits(["bookClick"]);
const handleClick = (book) => emit("bookClick", book);
const getCover = (coverUrl) => {
return isLegadoUrl(coverUrl) ? API.getProxyCoverUrl(coverUrl) : coverUrl;
};
const proxyImage = (event) => {
event.target.src = API.getProxyCoverUrl(event.target.src);
};
const emit = defineEmits(['bookClick'])
const handleClick = (book: Book | SeachBook) => emit('bookClick', book)
const getCover = ({ bookUrl, coverUrl }: Book | SeachBook) => {
if (coverUrl === undefined) return API.getProxyCoverUrl(bookUrl)
return isLegadoUrl(coverUrl) ? API.getProxyCoverUrl(coverUrl) : coverUrl
}
const proxyImage = (evt: Event) => {
const target = evt.target as HTMLImageElement
target.src = API.getProxyCoverUrl(target.src)
}
const subJustify = computed(() =>
props.isSearch ? "space-between" : "flex-start",
);
props.isSearch ? 'space-between' : 'flex-start',
)
</script>
<style lang="scss" scoped>
<style scoped>
.books-wrapper {
overflow: auto;
@@ -119,7 +127,7 @@ const subJustify = computed(() =>
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: v-bind("subJustify");
justify-content: v-bind('subJustify');
font-size: 12px;
font-weight: 600;
color: #6b6b6b;
@@ -149,6 +157,7 @@ const subJustify = computed(() =>
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
text-align: left;
}
}
+17 -13
View File
@@ -11,25 +11,29 @@
</div>
</div>
</template>
<script setup>
const props = defineProps([
"index",
"source",
"gotoChapter",
"currentChapterIndex",
]);
<script setup lang="ts">
import type { BookChapter } from '@/book'
const isSelected = (idx) => {
return idx == props.currentChapterIndex;
};
const props = defineProps<{
index: number
source: BookChapter | { index: number; catas: BookChapter[] }
gotoChapter: (chapter: BookChapter) => void
currentChapterIndex: number
}>()
const isSelected = (idx: number) => {
return idx == props.currentChapterIndex
}
// PC端 一个虚拟列表中有两个章节
const catas = computed(() => {
return props.source?.catas ?? [props.source];
});
const source = props.source
if ('catas' in source) return source.catas
return [props.source as BookChapter]
})
</script>
<style lang="scss" scoped>
<style scoped>
.selected {
color: #eb4259;
}
+79 -72
View File
@@ -17,116 +17,123 @@
</div>
</template>
<script setup>
import { isLegadoUrl } from "@/utils/utils";
import API from "@api";
import jump from "@/plugins/jump";
<script setup lang="ts">
import { isLegadoUrl } from '@/utils/utils'
import API from '@api'
import jump from '@/plugins/jump'
import type { webReadConfig } from '@/web'
const props = defineProps({
chapterIndex: { type: Number, required: true },
contents: { type: Array, required: true },
title: { type: String, required: true },
spacing: { type: Object, required: true },
fontFamily: { type: String, required: true },
fontSize: { type: String, required: true },
});
const store = useBookStore()
const readWidth = computed(() => store.config.readWidth)
const bookUrl = computed(() => store.readingBook.bookUrl)
const props = defineProps<{
chapterIndex: number
contents: Array<string>
title: string
spacing: webReadConfig['spacing']
fontFamily: string
fontSize: string
}>()
const getImageSrc = (content) => {
const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/;
const src = content.match(imgPattern)[1];
const getImageSrc = (content: string) => {
const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/
const src = content.match(imgPattern)![1] //reg tested in template
if (isLegadoUrl(src))
return API.getProxyImageUrl(src, useBookStore().config.readWidth);
return src;
};
const proxyImage = (event) => {
event.target.src = API.getProxyImageUrl(
event.target.src,
return API.getProxyImageUrl(
bookUrl.value,
src,
useBookStore().config.readWidth,
);
};
)
return src
}
const proxyImage = (event: Event) => {
;(event.target as HTMLImageElement).src = API.getProxyImageUrl(
bookUrl.value,
(event.target as HTMLImageElement).src,
readWidth.value,
)
}
const calculateWordCount = (paragraph) => {
const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g;
const calculateWordCount = (paragraph: string) => {
const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g
//内嵌图片文字为1
const imagePlaceHolder = " ";
return paragraph.replaceAll(imgPattern, imagePlaceHolder).length;
};
const imagePlaceHolder = ' '
return paragraph.replaceAll(imgPattern, imagePlaceHolder).length
}
const chapterPos = computed(() => {
let pos = -1;
return Array.from(props.contents, (content) => {
pos += calculateWordCount(content) + 1; //计算上一段的换行符
return pos;
});
});
let pos = -1
return Array.from(props.contents, content => {
pos += calculateWordCount(content) + 1 //计算上一段的换行符
return pos
})
})
const titleRef = ref();
const paragraphRef = ref();
const scrollToReadedLength = (length) => {
if (length === 0) return;
let paragraphIndex = chapterPos.value.findIndex(
(wordCount) => wordCount >= length,
);
if (paragraphIndex === -1) return;
const titleRef = ref<HTMLElement>()
const paragraphRef = ref<HTMLParagraphElement[]>()
const scrollToReadedLength = (length: number) => {
if (length === 0) return
const paragraphIndex = chapterPos.value.findIndex(
wordCount => wordCount >= length,
)
if (paragraphIndex === -1) return
nextTick(() => {
jump(paragraphRef.value[paragraphIndex], {
jump(paragraphRef.value![paragraphIndex], {
duration: 0,
});
});
};
})
})
}
defineExpose({
scrollToReadedLength,
});
let intersectionObserver = null;
const emit = defineEmits(["readedLengthChange"]);
})
let intersectionObserver: IntersectionObserver | null = null
const emit = defineEmits(['readedLengthChange'])
onMounted(() => {
intersectionObserver = new IntersectionObserver(
(entries) => {
for (let { target, isIntersecting } of entries) {
entries => {
for (const { target, isIntersecting } of entries) {
if (isIntersecting) {
emit(
"readedLengthChange",
'readedLengthChange',
props.chapterIndex,
// @ts-ignore
parseInt(target.dataset.chapterpos),
);
parseInt((target as HTMLElement).dataset.chapterpos as string),
)
}
}
},
{
rootMargin: `0px 0px -${window.innerHeight - 24}px 0px`,
},
);
intersectionObserver.observe(titleRef.value);
paragraphRef.value.forEach((element) => {
intersectionObserver.observe(element);
});
});
)
intersectionObserver.observe(titleRef.value!)
paragraphRef.value!.forEach(element => {
intersectionObserver!.observe(element)
})
})
onUnmounted(() => {
intersectionObserver?.disconnect();
intersectionObserver = null;
});
intersectionObserver?.disconnect()
intersectionObserver = null
})
</script>
<style lang="scss" scoped>
<style scoped>
.title {
margin-bottom: 57px;
font:
24px / 32px PingFangSC-Regular,
HelveticaNeue-Light,
"Helvetica Neue Light",
"Microsoft YaHei",
'Helvetica Neue Light',
'Microsoft YaHei',
sans-serif;
}
p {
display: block;
word-wrap: break-word;
// word-break: break-all;
letter-spacing: calc(v-bind("props.spacing.letter") * 1em);
line-height: calc(1 + v-bind("props.spacing.line"));
margin: calc(v-bind("props.spacing.paragraph") * 1em) 0;
/* word-break: break-all; */
letter-spacing: calc(v-bind('props.spacing.letter') * 1em);
line-height: calc(1 + v-bind('props.spacing.line'));
margin: calc(v-bind('props.spacing.paragraph') * 1em) 0;
:deep(img) {
height: 1em;
+49 -45
View File
@@ -19,81 +19,85 @@
</div>
</template>
<script setup>
import VirtualList from "vue3-virtual-scroll-list";
import settings from "../config/themeConfig";
import "../assets/fonts/popfont.css";
import CatalogItem from "./CatalogItem.vue";
<script setup lang="ts">
import VirtualList from 'vue3-virtual-scroll-list'
import settings from '../config/themeConfig'
import '../assets/fonts/popfont.css'
import CatalogItem from './CatalogItem.vue'
import type { BookChapter } from '@/book'
const store = useBookStore();
const store = useBookStore()
const { catalog, popCataVisible, miniInterface } = storeToRefs(store);
const { catalog, popCataVisible, miniInterface } = storeToRefs(store)
//主题
const isNight = computed(() => store.theme);
const theme = computed(() => store.theme);
const isNight = computed(() => store.theme)
const theme = computed(() => store.theme)
const popupTheme = computed(() => {
return {
background: settings.themes[theme.value].popup,
};
});
}
})
//虚拟列表 数据源
const virtualListdata = computed(() => {
let catalogValue = catalog.value;
if (miniInterface.value) return catalogValue;
const catalogValue = catalog.value
if (miniInterface.value) return catalogValue
// pc端 virtualListIitem有2个章节
let length = Math.ceil(catalogValue.length / 2);
let virtualListDataSource = new Array(length);
const length = Math.ceil(catalogValue.length / 2)
const virtualListDataSource = new Array<{
index: number
catas: BookChapter[]
}>(length)
let i = 0;
let i = 0
while (i < length) {
virtualListDataSource[i] = {
index: i,
catas: catalogValue.slice(2 * i, 2 * i + 2),
};
i++;
}
return virtualListDataSource;
});
i++
}
return virtualListDataSource
})
//打开目录 计算当前章节对应的虚拟列表位置
const virtualListRef = ref();
const virtualListRef = ref()
const currentChapterIndex = computed({
get: () => store.readingBook.index,
set: (value) => (store.readingBook.index = value),
});
get: () => store.readingBook.chapterIndex,
set: value => (store.readingBook.chapterIndex = value),
})
const virtualListIndex = computed(() => {
let index = currentChapterIndex.value;
if (miniInterface.value) return index;
const index = currentChapterIndex.value
if (miniInterface.value) return index
// pc端 virtualListIitem有2个章节
return Math.floor(index / 2);
});
return Math.floor(index / 2)
})
onUpdated(() => {
// dom更新触发ResizeObserver,更新虚拟列表内部的sizes Map
if (!popCataVisible.value) return;
virtualListRef.value.scrollToIndex(virtualListIndex.value);
});
if (!popCataVisible.value) return
virtualListRef.value.scrollToIndex(virtualListIndex.value)
})
// 点击加载对应章节内容
const emit = defineEmits(["getContent"]);
const gotoChapter = (note) => {
const chapterIndex = catalog.value.indexOf(note);
currentChapterIndex.value = chapterIndex;
store.setPopCataVisible(false);
store.setContentLoading(true);
store.saveBookProgress();
emit("getContent", chapterIndex);
};
const emit = defineEmits(['getContent'])
const gotoChapter = (chapter: BookChapter) => {
const chapterIndex = catalog.value.indexOf(chapter)
currentChapterIndex.value = chapterIndex
store.setPopCataVisible(false)
store.setContentLoading(true)
store.saveBookProgress()
emit('getContent', chapterIndex)
}
</script>
<style lang="scss" scoped>
<style scoped>
.cata-wrapper {
margin: -16px;
padding: 18px 0 24px 25px;
// background: #ede7da url('../assets/imgs/themes/popup_1.png') repeat;
/* background: #ede7da url('../assets/imgs/themes/popup_1.png') repeat; */
.title {
font-size: 18px;
font-weight: 400;
@@ -105,14 +109,14 @@ const gotoChapter = (note) => {
}
:deep(.data-wrapper) {
.cata {
//width: 50%;
/*width: 50%;*/
height: 40px;
cursor: pointer;
font:
16px / 40px PingFangSC-Regular,
HelveticaNeue-Light,
"Helvetica Neue Light",
"Microsoft YaHei",
'Helvetica Neue Light',
'Microsoft YaHei',
sans-serif;
}
}
+116 -130
View File
@@ -62,19 +62,10 @@
<el-button
type="primary"
size="small"
@click="
setCustomFont();
customFontSavePopVisible = false;
"
@click="setCustomFont(), (customFontSavePopVisible = false)"
>确定</el-button
>
<el-button
type="primary"
size="small"
@click="
loadFontFromURL();
customFontSavePopVisible = false;
"
<el-button type="primary" size="small" @click="loadFontFromURL()"
>网络下载</el-button
>
</div>
@@ -183,197 +174,192 @@
</div>
</template>
<script setup>
import "../assets/fonts/popfont.css";
import "../assets/fonts/iconfont.css";
import settings from "../config/themeConfig";
import API from "@api";
const store = useBookStore();
<script setup lang="ts">
import '../assets/fonts/popfont.css'
import '../assets/fonts/iconfont.css'
import settings from '../config/themeConfig'
import API from '@api'
import { useDebounceFn } from '@vueuse/shared'
const store = useBookStore()
const saveConfigDebounce = useDebounceFn(
() => API.saveReadConfig(store.config),
500,
)
//阅读界面设置改变时保存同步配置
let configChanged = false;
watch(
() => store.config,
(newValue) => {
localStorage.setItem("config", JSON.stringify(newValue));
configChanged = true;
() => {
saveConfigDebounce()
},
{
deep: 2, //深度为2
},
);
// 设置页面关闭时同步设置到阅读APP
watch(
() => store.readSettingsVisible,
(visbile) => {
if (!visbile && configChanged)
API.saveReadConfig(store.config).then(() => (configChanged = false));
},
);
)
//主题颜色
const theme = computed(() => store.theme);
const isNight = computed(() => store.isNight);
const moonIcon = computed(() => (theme.value == 6 ? "" : ""));
const theme = computed(() => store.theme)
const isNight = computed(() => store.isNight)
const moonIcon = computed(() => (theme.value == 6 ? '' : ''))
const themeColors = [
{
background: "rgba(250, 245, 235, 0.8)",
background: 'rgba(250, 245, 235, 0.8)',
},
{
background: "rgba(245, 234, 204, 0.8)",
background: 'rgba(245, 234, 204, 0.8)',
},
{
background: "rgba(230, 242, 230, 0.8)",
background: 'rgba(230, 242, 230, 0.8)',
},
{
background: "rgba(228, 241, 245, 0.8)",
background: 'rgba(228, 241, 245, 0.8)',
},
{
background: "rgba(245, 228, 228, 0.8)",
background: 'rgba(245, 228, 228, 0.8)',
},
{
background: "rgba(224, 224, 224, 0.8)",
background: 'rgba(224, 224, 224, 0.8)',
},
{
background: "rgba(0, 0, 0, 0.5)",
background: 'rgba(0, 0, 0, 0.5)',
},
];
]
const popupTheme = computed(() => {
return {
background: settings.themes[theme.value].popup,
};
});
const setTheme = (theme) => {
store.config.theme = theme;
};
}
})
const setTheme = (theme: number) => {
store.config.theme = theme
}
//预置字体
const fonts = ref(["雅黑", "宋体", "楷书"]);
const setFont = (font) => {
store.config.font = font;
};
const fonts = ref(['雅黑', '宋体', '楷书'])
const setFont = (font: number) => {
store.config.font = font
}
const selectedFont = computed(() => {
return store.config.font;
});
return store.config.font
})
//自定义字体
const customFontName = ref(store.config.customFontName);
const customFontSavePopVisible = ref(false);
const customFontName = ref(store.config.customFontName)
const customFontSavePopVisible = ref(false)
const setCustomFont = () => {
store.config.font = -1;
store.config.customFontName = customFontName.value;
};
customFontSavePopVisible.value = false
store.config.font = -1
store.config.customFontName = customFontName.value
}
// 加载网络字体
const loadFontFromURL = () => {
ElMessageBox.prompt("请输入 字体网络链接", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
customFontSavePopVisible.value = false
ElMessageBox.prompt('请输入 字体网络链接', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^https?:.+$/,
inputErrorMessage: "url 形式不正确",
inputErrorMessage: 'url 形式不正确',
beforeClose: (action, instance, done) => {
if (action === "confirm") {
instance.confirmButtonLoading = true;
instance.confirmButtonText = "下载中……";
if (action === 'confirm') {
instance.confirmButtonLoading = true
instance.confirmButtonText = '下载中……'
// instance.inputValue
const url = instance.inputValue;
if (typeof FontFace !== "function") {
ElMessage.error("浏览器不支持FontFace");
return done();
const url = instance.inputValue
if (typeof FontFace !== 'function') {
ElMessage.error('浏览器不支持FontFace')
return done()
}
const fontface = new FontFace(customFontName.value, `url("${url}")`);
//@ts-ignore
document.fonts.add(fontface);
const fontface = new FontFace(customFontName.value, `url("${url}")`)
document.fonts.add(fontface)
fontface
.load()
//API.getBookShelf()
.then(function () {
instance.confirmButtonLoading = false;
ElMessage.info("字体加载成功!");
setCustomFont();
done();
instance.confirmButtonLoading = false
ElMessage.info('字体加载成功!')
setCustomFont()
done()
})
.catch(function (error) {
instance.confirmButtonLoading = false;
instance.confirmButtonText = "确定";
ElMessage.error("下载失败,请检查您输入的 url");
throw error;
});
instance.confirmButtonLoading = false
instance.confirmButtonText = '确定'
ElMessage.error('下载失败,请检查您输入的 url')
throw error
})
} else {
done();
done()
}
},
});
};
})
}
//字体大小
const fontSize = computed(() => {
return store.config.fontSize;
});
return store.config.fontSize
})
const moreFontSize = () => {
if (store.config.fontSize < 48) store.config.fontSize += 2;
};
if (store.config.fontSize < 48) store.config.fontSize += 2
}
const lessFontSize = () => {
if (store.config.fontSize > 12) store.config.fontSize -= 2;
};
if (store.config.fontSize > 12) store.config.fontSize -= 2
}
//字 行 段落间距
const spacing = computed(() => {
return store.config.spacing;
});
return store.config.spacing
})
const lessLetterSpacing = () => {
store.config.spacing.letter -= 0.01;
};
store.config.spacing.letter -= 0.01
}
const moreLetterSpacing = () => {
store.config.spacing.letter += 0.01;
};
store.config.spacing.letter += 0.01
}
const lessLineSpacing = () => {
store.config.spacing.line -= 0.1;
};
store.config.spacing.line -= 0.1
}
const moreLineSpacing = () => {
store.config.spacing.line += 0.1;
};
store.config.spacing.line += 0.1
}
const lessParagraphSpacing = () => {
store.config.spacing.paragraph -= 0.1;
};
store.config.spacing.paragraph -= 0.1
}
const moreParagraphSpacing = () => {
store.config.spacing.paragraph += 0.1;
};
store.config.spacing.paragraph += 0.1
}
//页面宽度
const readWidth = computed(() => {
return store.config.readWidth;
});
return store.config.readWidth
})
const moreReadWidth = () => {
// 此时会截断页面
if (store.config.readWidth + 160 + 2 * 68 > window.innerWidth) return;
store.config.readWidth += 160;
};
if (store.config.readWidth + 160 + 2 * 68 > window.innerWidth) return
store.config.readWidth += 160
}
const lessReadWidth = () => {
if (store.config.readWidth > 640) store.config.readWidth -= 160;
};
if (store.config.readWidth > 640) store.config.readWidth -= 160
}
//翻页速度
const jumpDuration = computed(() => {
return store.config.jumpDuration;
});
return store.config.jumpDuration
})
const moreJumpDuration = () => {
store.config.jumpDuration += 100;
};
store.config.jumpDuration += 100
}
const lessJumpDuration = () => {
if (store.config.jumpDuration === 0) return;
store.config.jumpDuration -= 100;
};
if (store.config.jumpDuration === 0) return
store.config.jumpDuration -= 100
}
//无限加载
const infiniteLoading = computed(() => {
return store.config.infiniteLoading;
});
const setInfiniteLoading = (loading) => {
store.config.infiniteLoading = loading;
};
return store.config.infiniteLoading
})
const setInfiniteLoading = (loading: boolean) => {
store.config.infiniteLoading = loading
}
</script>
<style lang="scss" scoped>
<style scoped>
:deep(.iconfont) {
font-family: iconfont;
font-style: normal;
@@ -387,11 +373,11 @@ const setInfiniteLoading = (loading) => {
.settings-wrapper {
user-select: none;
margin: -13px;
// width: 478px;
// height: 350px;
/* width: 478px;
height: 350px; */
text-align: left;
padding: 40px 0 40px 24px;
background: #ede7da url("../assets/imgs/themes/popup_1.png") repeat;
background: #ede7da url('../assets/imgs/themes/popup_1.png') repeat;
.settings-title {
font-size: 18px;
@@ -416,7 +402,7 @@ const setInfiniteLoading = (loading) => {
i {
font:
12px / 16px PingFangSC-Regular,
"-apple-system",
'-apple-system',
Simsun;
display: inline-block;
min-width: 48px;
@@ -468,8 +454,8 @@ const setInfiniteLoading = (loading) => {
font:
14px / 34px PingFangSC-Regular,
HelveticaNeue-Light,
"Helvetica Neue Light",
"Microsoft YaHei",
'Helvetica Neue Light',
'Microsoft YaHei',
sans-serif;
}
.font-item-input {
+22 -22
View File
@@ -18,49 +18,49 @@
/>
</template>
<script setup>
import API from "@api";
import { Search } from "@element-plus/icons-vue";
<script setup lang="ts">
import API from '@api'
import { Search } from '@element-plus/icons-vue'
const store = useSourceStore();
const store = useSourceStore()
const printDebug = ref("");
const searchKey = ref("");
const printDebug = ref('')
const searchKey = ref('')
watch(
() => store.isDebuging,
() => {
if (store.isDebuging) startDebug();
if (store.isDebuging) startDebug()
},
);
)
const appendDebugMsg = (msg) => {
let debugDom = document.querySelector("#debug-text");
debugDom.scrollTop = debugDom.scrollHeight;
printDebug.value += msg + "\n";
};
const appendDebugMsg = (msg: string) => {
const debugDom = document.querySelector('#debug-text')
debugDom!.scrollTop = debugDom!.scrollHeight
printDebug.value += msg + '\n'
}
const startDebug = async () => {
printDebug.value = "";
printDebug.value = ''
try {
await API.saveSource(store.currentSource);
await API.saveSource(store.currentSource)
} catch (e) {
store.debugFinish();
throw e;
store.debugFinish()
throw e
}
API.debug(
store.currentSourceUrl,
searchKey.value || store.searchKey,
appendDebugMsg,
store.debugFinish,
);
};
)
}
const isBookSource = computed(() => {
return /bookSource/i.test(window.location.href);
});
return /bookSource/i.test(window.location.href)
})
</script>
<style lang="scss" scoped>
<style scoped>
:deep(#debug-text) {
height: calc(100vh - 45px - 36px - 5px);
}
+3 -3
View File
@@ -1,5 +1,5 @@
<script setup>
import { Link } from "@element-plus/icons-vue";
<script setup lang="ts">
import { Link } from '@element-plus/icons-vue'
</script>
<template>
<el-link :icon="Link" href="/help/#appHelp" target="_blank"
@@ -53,7 +53,7 @@ import { Link } from "@element-plus/icons-vue";
</div>
</template>
<style lang="scss" scoped>
<style scoped>
.el-link {
padding: 4px;
}
+19 -16
View File
@@ -8,32 +8,35 @@
edit: sourceUrl == currentSourceUrl,
}"
>
{{ source.bookSourceName || source.sourceName }}
{{ getSourceName(source) }}
<el-button text :icon="Edit" @click="handleSourceClick(source)" />
</el-checkbox>
</template>
<script setup>
import { Edit } from "@element-plus/icons-vue";
import { getSourceUniqueKey } from "@/utils/souce";
<script setup lang="ts">
import { Edit } from '@element-plus/icons-vue'
import { getSourceUniqueKey, getSourceName } from '@/utils/souce'
import type { Source } from '@/source'
const props = defineProps(["source"]);
const props = defineProps<{
source: Source
}>()
const store = useSourceStore();
const store = useSourceStore()
const currentSourceUrl = computed(() => store.currentSourceUrl);
const sourceUrl = computed(() => getSourceUniqueKey(props.source));
const currentSourceUrl = computed(() => store.currentSourceUrl)
const sourceUrl = computed(() => getSourceUniqueKey(props.source))
const handleSourceClick = (source) => {
store.changeCurrentSource(source);
};
const handleSourceClick = (source: Source) => {
store.changeCurrentSource(source)
}
const isSaveError = computed(() => {
const map = store.savedSourcesMap;
if (map.size == 0) return false;
return !map.has(sourceUrl.value);
});
const map = store.savedSourcesMap
if (map.size == 0) return false
return !map.has(sourceUrl.value)
})
</script>
<style lang="scss" scoped>
<style scoped>
:deep(.el-checkbox__label) {
flex: 1;
display: flex;
+14 -14
View File
@@ -9,30 +9,30 @@
style="margin-bottom: 4px"
></el-input>
</template>
<script setup>
import { useSourceStore } from "@/store";
<script setup lang="ts">
import { useSourceStore } from '@/store'
const store = useSourceStore();
const sourceString = ref("");
const update = async (string) => {
const store = useSourceStore()
const sourceString = ref('')
const update = async (string: string) => {
try {
store.changeEditTabSource(JSON.parse(string));
store.changeEditTabSource(JSON.parse(string))
} catch {
ElMessage({
message: "粘贴的源格式错误",
type: "error",
});
message: '粘贴的源格式错误',
type: 'error',
})
}
}
};
watchEffect(async () => {
let source = store.editTabSource;
const source = store.editTabSource
if (Object.keys(source).length > 0) {
sourceString.value = JSON.stringify(source, null, 4);
sourceString.value = JSON.stringify(source, null, 4)
} else {
sourceString.value = "";
sourceString.value = ''
}
});
})
</script>
<style scoped>
:deep(.el-input) {
+75 -81
View File
@@ -32,7 +32,7 @@
<el-checkbox-group id="source-list" v-model="sourceUrlSelect">
<virtual-list
style="height: 100%; overflow-y: auto; overflow-x: hidden"
:data-key="(source) => source.bookSourceUrl || source.sourceUrl"
:data-key="(source: Source) => getSourceName(source)"
:data-sources="sourcesFiltered"
:data-component="SourceItem"
:estimate-size="45"
@@ -40,116 +40,110 @@
</el-checkbox-group>
</template>
<script setup>
import API from "@api";
import { Folder, Delete, Download, Search } from "@element-plus/icons-vue";
<script setup lang="ts">
import API from '@api'
import { Folder, Delete, Download, Search } from '@element-plus/icons-vue'
import {
isSourceMatches,
getSourceUniqueKey,
getSourceName,
convertSourcesToMap,
} from "@utils/souce";
import VirtualList from "vue3-virtual-scroll-list";
import SourceItem from "./SourceItem.vue";
} from '@utils/souce'
import VirtualList from 'vue3-virtual-scroll-list'
import SourceItem from './SourceItem.vue'
import type { Source } from '@/source'
const store = useSourceStore();
const sourceUrlSelect = ref([]);
const searchKey = ref("");
const sources = computed(() => store.sources);
const store = useSourceStore()
const sourceUrlSelect = ref<string[]>([])
const searchKey = ref('')
const sources = computed(() => store.sources)
// 筛选源
/** @type Ref<import('@/source').Source[]> */
const sourcesFiltered = computed(() => {
const key = searchKey.value;
if (key === "") return sources.value;
return (
sources.value
// @ts-ignore
.filter((source) => isSourceMatches(source, key))
);
});
/* 筛选源 */
const sourcesFiltered = computed<Source[]>(() => {
const key = searchKey.value
if (key === '') return sources.value
return sources.value.filter(source => isSourceMatches(source, key))
})
// 计算当前筛选关键词下的选中源
/** @type Ref<import('@/source').Source[]> */
const sourceSelect = computed(() => {
const urls = sourceUrlSelect.value;
if (urls.length == 0) return [];
const sourceSelect = computed<Source[]>(() => {
const urls = sourceUrlSelect.value
if (urls.length == 0) return []
const sourcesFilteredMap =
searchKey.value == ""
searchKey.value == ''
? store.sourcesMap
: convertSourcesToMap(sourcesFiltered.value);
: convertSourcesToMap(sourcesFiltered.value)
return urls.reduce((sources, sourceUrl) => {
const source = sourcesFilteredMap.get(sourceUrl);
if (source) sources.push(source);
return sources;
}, []);
});
const source = sourcesFilteredMap.get(sourceUrl)
if (source) sources.push(source)
return sources
}, [] as Source[])
})
const deleteSelectSources = () => {
const sourceSelectValue = sourceSelect.value;
const sourceSelectValue = sourceSelect.value
API.deleteSource(sourceSelectValue).then(({ data }) => {
if (!data.isSuccess) return ElMessage.error(data.errorMsg);
store.deleteSources(sourceSelectValue);
const sourceUrlSelectRawValue = toRaw(sourceUrlSelect.value);
sourceSelectValue.forEach((source) => {
const index = sourceUrlSelectRawValue.indexOf(getSourceUniqueKey(source));
if (index > -1) sourceUrlSelectRawValue.splice(index, 1);
});
sourceUrlSelect.value = sourceUrlSelectRawValue;
});
};
if (!data.isSuccess) return ElMessage.error(data.errorMsg)
store.deleteSources(sourceSelectValue)
const sourceUrlSelectRawValue = toRaw(sourceUrlSelect.value)
sourceSelectValue.forEach(source => {
const index = sourceUrlSelectRawValue.indexOf(getSourceUniqueKey(source))
if (index > -1) sourceUrlSelectRawValue.splice(index, 1)
})
sourceUrlSelect.value = sourceUrlSelectRawValue
})
}
const clearAllSources = () => {
store.clearAllSource();
sourceUrlSelect.value = [];
};
store.clearAllSource()
sourceUrlSelect.value = []
}
//导入本地文件
const importSourceFile = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json,.txt";
input.addEventListener("change", (e) => {
// @ts-ignore
const file = e.target.files[0];
const reader = new FileReader();
reader.readAsText(file);
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json,.txt'
input.addEventListener('change', () => {
const files = input.files
if (files === null) {
return ElMessage.info('未选择文件')
}
const reader = new FileReader()
reader.readAsText(files[0])
reader.onload = () => {
try {
// @ts-ignore
const jsonData = JSON.parse(reader.result);
store.saveSources(jsonData);
} catch {
ElMessage({
message: "上传的源格式错误",
type: "error",
});
const jsonData = JSON.parse(reader.result as string)
store.saveSources(jsonData)
} catch (e: unknown) {
ElMessage.error('上传的源格式错误: ' + (e as Error).message)
}
}
})
input.click()
}
};
});
input.click();
};
const isBookSource = /bookSource/i.test(window.location.href);
const isBookSource = /bookSource/i.test(window.location.href)
const outExport = () => {
const exportFile = document.createElement("a");
let sources =
const exportFile = document.createElement('a')
const sources =
sourceUrlSelect.value.length === 0
? sourcesFiltered.value
: sourceSelect.value,
sourceType = isBookSource ? "BookSource" : "RssSource";
sourceType = isBookSource ? 'BookSource' : 'RssSource'
exportFile.download = `${sourceType}_${Date()
.replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, "$2$1$3")
.replace(/:/g, "")}.json`;
.replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3')
.replace(/:/g, '')}.json`
let myBlob = new Blob([JSON.stringify(sources, null, 4)], {
type: "application/json",
});
exportFile.href = window.URL.createObjectURL(myBlob);
exportFile.click();
window.URL.revokeObjectURL(exportFile.href); //avoid memory leak
};
const myBlob = new Blob([JSON.stringify(sources, null, 4)], {
type: 'application/json',
})
exportFile.href = window.URL.createObjectURL(myBlob)
exportFile.click()
window.URL.revokeObjectURL(exportFile.href) //avoid memory leak
}
</script>
<style lang="scss" scoped>
<style scoped>
.tool {
display: flex;
margin: 4px 0;
+19 -11
View File
@@ -14,7 +14,7 @@
id,
array,
hint,
required,
required = false,
} in children"
:label="title"
:key="title"
@@ -35,20 +35,26 @@
autosize
/>
<el-switch v-if="type == 'Boolean'" v-model="currentSource[id]" />
<el-switch
v-if="(type as string) === 'Boolean'"
v-model="currentSource[id]"
/>
<el-input-number
v-if="type == 'Number'"
v-if="(type as string) === 'Number'"
v-model="currentSource[id]"
:min="0"
/>
<el-select v-if="type == 'Array'" v-model="currentSource[id]">
<el-select
v-if="(type as string) === 'Array'"
v-model="currentSource[id]"
>
<el-option
v-for="(name, index) in array"
v-for="(optionName, index) in array"
:value="index"
:key="name"
:label="name"
:key="optionName"
:label="optionName"
/>
</el-select>
</el-form-item>
@@ -57,11 +63,13 @@
</el-tabs>
</template>
<script setup>
const store = useSourceStore();
defineProps(["config"]);
<script setup lang="ts">
import type { SourceConfig } from '@/config/sourceConfig'
const currentSource = computed(() => store.currentSource);
const store = useSourceStore()
defineProps<{ config: SourceConfig }>()
const currentSource = computed(() => store.currentSource)
/*
修改currentSource的属性 没有直接修改本身
const { currentSource } = storeToRefs(store);
+10 -10
View File
@@ -14,22 +14,22 @@
</el-tabs>
</template>
<script setup>
import { useSourceStore } from "@/store";
<script setup lang="ts">
import { useSourceStore } from '@/store'
const store = useSourceStore();
const store = useSourceStore()
const current_tab = computed({
get: () => store.currentTab,
set: (val) => (store.currentTab = val),
});
set: val => (store.currentTab = val),
})
const tabData = ref([
["editTab", "编辑源"],
["editDebug", "调试源"],
["editList", "源列表"],
["editHelp", "帮助信息"],
]);
['editTab', '编辑源'],
['editDebug', '调试源'],
['editList', '源列表'],
['editHelp', '帮助信息'],
])
</script>
<style lang="scss" scoped>
+142 -145
View File
@@ -36,7 +36,7 @@
<div class="hotkeys-settings flex-column-center">
<div
v-for="(button, index) in buttons"
v-for="(button, buttonIndex) in buttons"
:key="button.name"
class="hotkeys-item flex-space-between"
>
@@ -44,9 +44,9 @@
><el-text>{{ button.name }}</el-text></span
>
<div class="hotkeys-item__content">
<div v-for="(key, index) in button.hotKeys" :key="key">
<div v-for="(key, hotKeysIndex) in button.hotKeys" :key="key">
<kbd>{{ key }}</kbd>
<span v-if="index + 1 < button.hotKeys.length">
<span v-if="hotKeysIndex + 1 < button.hotKeys.length">
<el-text>+</el-text>
</span>
</div>
@@ -56,7 +56,7 @@
:disabled="recordKeyDowning"
text
:icon="Edit"
@click="recordKeyDown(index)"
@click="recordKeyDown(buttonIndex)"
>编辑</el-button
>
</div>
@@ -64,59 +64,59 @@
</el-dialog>
</template>
<script setup>
import API from "@api";
import { CircleCheckFilled, Edit } from "@element-plus/icons-vue";
import hotkeys from "hotkeys-js";
import { isInvaildSource } from "../utils/souce";
<script setup lang="ts">
import API from '@api'
import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
import hotkeys from 'hotkeys-js'
import { getSourceName, isInvaildSource } from '../utils/souce'
const store = useSourceStore();
const store = useSourceStore()
const pull = () => {
const loadingMsg = ElMessage({
message: "加载中……",
message: '加载中……',
showClose: true,
duration: 0,
});
})
API.getSources()
.then(({ data }) => {
if (data.isSuccess) {
store.changeTabName("editList");
store.saveSources(data.data);
store.changeTabName('editList')
store.saveSources(data.data)
ElMessage({
message: `成功拉取${data.data.length}条源`,
type: "success",
});
type: 'success',
})
} else {
ElMessage({
message: data.errorMsg ?? "后端错误",
type: "error",
});
message: data.errorMsg ?? '后端错误',
type: 'error',
})
}
})
.finally(() => loadingMsg.close());
};
.finally(() => loadingMsg.close())
}
const push = () => {
let sources = store.sources;
store.changeTabName("editList");
const sources = store.sources
store.changeTabName('editList')
if (sources.length === 0) {
return ElMessage({
message: "空空如也",
type: "info",
});
message: '空空如也',
type: 'info',
})
}
ElMessage({
message: "正在推送中",
type: "info",
});
message: '正在推送中',
type: 'info',
})
API.saveSources(sources).then(({ data }) => {
if (data.isSuccess) {
let okData = data.data;
const okData = data.data
if (Array.isArray(okData)) {
let failMsg = ``;
let failMsg = ``
if (sources.length > okData.length) {
failMsg = "\n推送失败的源将用红色字体标注!";
store.setPushReturnSources(okData);
failMsg = '\n推送失败的源将用红色字体标注!'
store.setPushReturnSources(okData)
}
ElMessage({
message: `批量推送源到「阅读3.0APP」\n共计: ${
@@ -124,168 +124,163 @@ const push = () => {
}\n成功: ${okData.length}\n失败: ${
sources.length - okData.length
}${failMsg}`,
type: "success",
});
type: 'success',
})
}
} else {
ElMessage({
message: `批量推送源失败!\nErrorMsg: ${data.errorMsg}`,
type: "error",
});
type: 'error',
})
}
})
}
});
};
const conver2Tab = () => {
store.changeTabName("editTab");
store.changeEditTabSource(store.currentSource);
};
store.changeTabName('editTab')
store.changeEditTabSource(store.currentSource)
}
const conver2Source = () => {
store.changeCurrentSource(store.editTabSource);
};
store.changeCurrentSource(store.editTabSource)
}
const undo = () => {
store.editHistoryUndo();
};
store.editHistoryUndo()
}
const clearEdit = () => {
store.clearEdit();
store.clearEdit()
ElMessage({
message: "已清除",
type: "success",
});
};
message: '已清除',
type: 'success',
})
}
const redo = () => {
store.clearEdit();
store.clearAllHistory();
store.clearEdit()
store.clearAllHistory()
ElMessage({
message: "已清除所有历史记录",
type: "success",
});
};
message: '已清除所有历史记录',
type: 'success',
})
}
const saveSource = () => {
let isBookSource = /bookSource/i.test(location.href),
/** @type {import("@/source.js").Source} */
source = store.currentSource;
const source = store.currentSource
if (isInvaildSource(source)) {
API.saveSource(source).then(({ data }) => {
const sourceName = getSourceName(source)
if (data.isSuccess) {
ElMessage({
message: `源《${
isBookSource ? source.bookSourceName : source.sourceName
}》已成功保存到「阅读3.0APP」`,
type: "success",
});
message: `源《${sourceName}》已成功保存到「阅读3.0APP」`,
type: 'success',
})
//save to store
store.saveCurrentSource();
store.saveCurrentSource()
} else {
ElMessage({
message: `源《${
isBookSource ? source.bookSourceName : source.sourceName
}》保存失败!\nErrorMsg: ${data.errorMsg}`,
type: "error",
});
message: `源《${sourceName}》保存失败!\nErrorMsg: ${data.errorMsg}`,
type: 'error',
})
}
});
})
} else {
ElMessage({
message: `请检查<必填>项是否全部填写`,
type: "error",
});
type: 'error',
})
}
}
};
const debug = () => {
store.startDebug();
};
store.startDebug()
}
const buttons = ref(
const buttons = ref<{ name: string; hotKeys: string[]; action: () => void }[]>(
Array.of(
{ name: "⇈推送源", hotKeys: [], action: push },
{ name: "⇊拉取源", hotKeys: [], action: pull },
{ name: "⋙生成源", hotKeys: [], action: conver2Tab },
{ name: "⋘编辑源", hotKeys: [], action: conver2Source },
{ name: "✗清空表单", hotKeys: [], action: clearEdit },
{ name: "↶撤销操作", hotKeys: [], action: undo },
{ name: "↷重做操作", hotKeys: [], action: redo },
{ name: "⇏调试源", hotKeys: [], action: debug },
{ name: "✓保存源", hotKeys: [], action: saveSource },
{ name: '⇈推送源', hotKeys: [], action: push },
{ name: '⇊拉取源', hotKeys: [], action: pull },
{ name: '⋙生成源', hotKeys: [], action: conver2Tab },
{ name: '⋘编辑源', hotKeys: [], action: conver2Source },
{ name: '✗清空表单', hotKeys: [], action: clearEdit },
{ name: '↶撤销操作', hotKeys: [], action: undo },
{ name: '↷重做操作', hotKeys: [], action: redo },
{ name: '⇏调试源', hotKeys: [], action: debug },
{ name: '✓保存源', hotKeys: [], action: saveSource },
),
);
const hotkeysDialogVisible = ref(true);
)
const hotkeysDialogVisible = ref(true)
const recordKeyDowning = ref(false);
const recordKeyDowning = ref(false)
const recordKeyDownIndex = ref(-1);
const recordKeyDownIndex = ref(-1)
const stopRecordKeyDown = () => {
if (!recordKeyDowning.value) {
hotkeysDialogVisible.value = false;
hotkeysDialogVisible.value = false
}
recordKeyDowning.value = false
}
recordKeyDowning.value = false;
};
watch(
hotkeysDialogVisible,
(visibale) => {
visibale => {
if (!visibale) {
hotkeys.unbind("*");
readHotkeysConfig();
bindHotKeys();
return;
hotkeys.unbind('*')
readHotkeysConfig()
bindHotKeys()
return
}
readHotkeysConfig();
hotkeys.unbind();
readHotkeysConfig()
hotkeys.unbind()
/**监听按键 */
hotkeys("*", (event) => {
event.preventDefault();
let pressedKeys = hotkeys.getPressedKeyString();
if (pressedKeys.length == 1 && pressedKeys[0] == "esc") {
hotkeys('*', event => {
event.preventDefault()
const pressedKeys = hotkeys.getPressedKeyString()
if (pressedKeys.length == 1 && pressedKeys[0] == 'esc') {
//单独按下esc 不录入
return;
return
}
if (recordKeyDowning.value && recordKeyDownIndex.value > -1)
buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys;
});
buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys
})
},
{ immediate: true },
);
)
const recordKeyDown = (index) => {
recordKeyDowning.value = true;
const recordKeyDown = (index: number) => {
recordKeyDowning.value = true
ElMessage({
message: "按ESC键或者点击空白处结束录入",
type: "info",
});
buttons.value[index].hotKeys = [];
recordKeyDownIndex.value = index;
};
message: '按ESC键或者点击空白处结束录入',
type: 'info',
})
buttons.value[index].hotKeys = []
recordKeyDownIndex.value = index
}
const saveHotKeys = () => {
const hotKeysConfig = [];
const hotKeysConfig: string[][] = []
buttons.value.forEach(({ hotKeys }) => {
hotKeysConfig.push(hotKeys);
});
saveHotkeysConfig(hotKeysConfig);
hotkeysDialogVisible.value = false;
};
hotKeysConfig.push(hotKeys)
})
saveHotkeysConfig(hotKeysConfig)
hotkeysDialogVisible.value = false
}
const bindHotKeys = () => {
// hotkeys默认过滤INPUT SELECT TEXTAREA
hotkeys.filter = () => true;
hotkeys.filter = () => true
buttons.value.forEach(({ hotKeys, action }) => {
if (hotKeys.length == 0) return;
hotkeys(hotKeys.join("+"), (event) => {
event.preventDefault();
action.call(null);
});
});
};
const saveHotkeysConfig = (config) => {
localStorage.setItem("legado_web_hotkeys", JSON.stringify(config));
};
if (hotKeys.length == 0) return
hotkeys(hotKeys.join('+'), event => {
event.preventDefault()
action.call(null)
})
})
}
const saveHotkeysConfig = (config: string[][]) => {
localStorage.setItem('legado_web_hotkeys', JSON.stringify(config))
}
/**
* 读取快捷键配置
@@ -293,26 +288,28 @@ const saveHotkeysConfig = (config) => {
*/
function readHotkeysConfig() {
try {
const config = JSON.parse(localStorage.getItem("legado_web_hotkeys"));
if (!Array.isArray(config) || config.length == 0) return false;
buttons.value.forEach((button, index) => (button.hotKeys = config[index]));
return true;
const localStorageConfig = localStorage.getItem('legado_web_hotkeys')
if (localStorageConfig === null) return false
const config = JSON.parse(localStorageConfig)
if (!Array.isArray(config) || config.length == 0) return false
buttons.value.forEach((button, index) => (button.hotKeys = config[index]))
return true
} catch {
ElMessage({ message: "快捷键配置错误", type: "error" });
localStorage.removeItem("legado_web_hotkeys");
ElMessage({ message: '快捷键配置错误', type: 'error' })
localStorage.removeItem('legado_web_hotkeys')
}
return false;
return false
}
onMounted(() => {
/**读取热键配置 */
if (readHotkeysConfig()) {
hotkeysDialogVisible.value = false;
hotkeysDialogVisible.value = false
}
});
})
</script>
<style lang="scss" scoped>
<style scoped>
.flex-space-between {
display: flex;
justify-content: space-between;
@@ -337,7 +334,7 @@ onMounted(() => {
justify-content: flex-end;
margin-right: 1em;
}
&__content {
.hotkeys-item__content {
display: flex;
flex-wrap: wrap;
flex: 1;
@@ -1,585 +0,0 @@
export default {
base: {
name: "基础",
children: [
{
title: "源类型",
id: "bookSourceType",
type: "Array",
array: ["文本", "音频", "图片", "文件"],
required: true,
},
{
title: "源域名",
id: "bookSourceUrl",
type: "String",
hint: "通常填写网站主页,例: https://www.qidian.com",
required: true,
},
{
title: "源名称",
id: "bookSourceName",
type: "String",
hint: "会显示在源列表",
required: true,
},
{
title: "源分组",
id: "bookSourceGroup",
type: "String",
hint: "描述源的特征信息",
},
{
title: "源注释",
id: "bookSourceComment",
type: "String",
hint: "描述源作者和状态",
},
{
title: "登录地址",
id: "loginUrl",
type: "String",
hint: "填写网站登录网址,仅在需要登录的源有用",
},
{
title: "登录界面",
id: "loginUi",
type: "String",
hint: "自定义登录界面",
},
{
title: "登录检测",
id: "loginCheckJs",
type: "String",
hint: "登录检测js",
},
{
title: "封面解密",
id: "coverDecodeJs",
type: "String",
hint: "封面解密js",
},
{
title: "链接验证",
id: "bookUrlPattern",
type: "String",
hint: "书籍URL正则,当详情页URL与源URL的域名不一致时有效,用于添加网址",
},
{
title: "请求头",
id: "header",
type: "String",
hint: "客户端标识",
},
{
title: "变量说明",
id: "variableComment",
type: "String",
hint: "书源变量说明",
},
{
title: "并发率",
id: "concurrentRate",
type: "String",
hint: "并发率,如1000(访问间隔1000ms)或者1/1000(1000ms内访问1次)",
},
{
title: "js库",
id: "jsLib",
type: "String",
hint: "js库, 可填写js或者key-value object获取在线js文件",
},
],
},
search: {
name: "搜索",
children: [
{
title: "搜索地址",
id: "searchUrl",
type: "String",
hint: "[域名可省略]/search.php@kw={{key}}",
},
{
title: "校验文字",
namespace: "ruleSearch",
id: "checkKeyWord",
type: "String",
hint: "校验关键字,强烈建议填写",
},
{
title: "列表规则",
namespace: "ruleSearch",
id: "bookList",
type: "String",
hint: "选择书籍节点 (规则结果为List<Element>)",
},
{
title: "书名规则",
namespace: "ruleSearch",
id: "name",
type: "String",
hint: "选择节点书名 (规则结果为String)",
},
{
title: "作者规则",
namespace: "ruleSearch",
id: "author",
type: "String",
hint: "选择节点作者 (规则结果为String)",
},
{
title: "分类规则",
namespace: "ruleSearch",
id: "kind",
type: "String",
hint: "选择节点分类信息 (规则结果为String)",
},
{
title: "字数规则",
namespace: "ruleSearch",
id: "wordCount",
type: "String",
hint: "选择节点字数信息 (规则结果为String)",
},
{
title: "最新章节",
namespace: "ruleSearch",
id: "lastChapter",
type: "String",
hint: "选择节点最新章节 (规则结果为String)",
},
{
title: "简介规则",
namespace: "ruleSearch",
id: "intro",
type: "String",
hint: "选择节点书籍简介 (规则结果为String)",
},
{
title: "封面规则",
namespace: "ruleSearch",
id: "coverUrl",
type: "String",
hint: "选择节点书籍封面 (规则结果为String类型的url)",
},
{
title: "详情地址",
namespace: "ruleSearch",
id: "bookUrl",
type: "String",
hint: "选择书籍详情页网址 (规则结果为String类型的url)",
},
],
},
find: {
name: "发现",
children: [
{
title: "发现地址",
id: "exploreUrl",
type: "String",
hint: "单个发现格式<name>::<url>或者{url:<url>,title:<name>,style:...};前者用换行符或者&&连接,后者放在数组内;可用js动态生成",
},
{
title: "列表规则",
namespace: "ruleExplore",
id: "bookList",
type: "String",
hint: "选择书籍节点 (规则结果为List<Element>)",
},
{
title: "书名规则",
namespace: "ruleExplore",
id: "name",
type: "String",
hint: "选择节点书名 (规则结果为String)",
},
{
title: "作者规则",
namespace: "ruleExplore",
id: "author",
type: "String",
hint: "选择节点作者 (规则结果为String)",
},
{
title: "分类规则",
namespace: "ruleExplore",
id: "kind",
type: "String",
hint: "选择节点分类信息 (规则结果为String)",
},
{
title: "字数规则",
namespace: "ruleExplore",
id: "wordCount",
type: "String",
hint: "选择节点字数信息 (规则结果为String)",
},
{
title: "最新章节",
namespace: "ruleExplore",
id: "lastChapter",
type: "String",
hint: "选择节点最新章节 (规则结果为String)",
},
{
title: "简介规则",
namespace: "ruleExplore",
id: "intro",
type: "String",
hint: "选择节点书籍简介 (规则结果为String)",
},
{
title: "封面规则",
namespace: "ruleExplore",
id: "coverUrl",
type: "String",
hint: "选择节点书籍封面 (规则结果为String类型的url)",
},
{
title: "详情地址",
namespace: "ruleExplore",
id: "bookUrl",
type: "String",
hint: "选择书籍详情页网址 (规则结果为String类型的url)",
},
],
},
detail: {
name: "详情",
children: [
{
title: "预处理",
namespace: "ruleBookInfo",
id: "init",
type: "String",
hint: "用于加速详情信息检索,只支持AllInOne规则",
},
{
title: "书名规则",
namespace: "ruleBookInfo",
id: "name",
type: "String",
hint: "选择节点书名 (规则结果为String)",
},
{
title: "作者规则",
namespace: "ruleBookInfo",
id: "author",
type: "String",
hint: "选择节点作者 (规则结果为String)",
},
{
title: "分类规则",
namespace: "ruleBookInfo",
id: "kind",
type: "String",
hint: "选择节点分类信息 (规则结果为String)",
},
{
title: "字数规则",
namespace: "ruleBookInfo",
id: "wordCount",
type: "String",
hint: "选择节点字数信息 (规则结果为String)",
},
{
title: "最新章节",
namespace: "ruleBookInfo",
id: "lastChapter",
type: "String",
hint: "选择节点最新章节 (规则结果为String)",
},
{
title: "简介规则",
namespace: "ruleBookInfo",
id: "intro",
type: "String",
hint: "选择节点书籍简介 (规则结果为String)",
},
{
title: "封面规则",
namespace: "ruleBookInfo",
id: "coverUrl",
type: "String",
hint: "选择节点书籍封面 (规则结果为String类型的url)",
},
{
title: "目录地址",
namespace: "ruleBookInfo",
id: "tocUrl",
type: "String",
hint: "选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)",
},
{
title: "修改书籍",
namespace: "ruleBookInfo",
id: "canReName",
type: "String",
hint: "允许修改书名作者(规则结果为String类型, 默认不允许)",
},
{
title: "下载URL",
namespace: "ruleBookInfo",
id: "downloadUrls",
type: "String",
hint: "文件类书源下载地址 (规则结果为String类型的url, 多个链接返回数组)",
},
],
},
directory: {
name: "目录",
children: [
{
title: "更新前JS",
namespace: "ruleToc",
id: "preUpdateJs",
type: "String",
hint: "更新目录前调用JS 动态更新目录链接",
},
{
title: "列表规则",
namespace: "ruleToc",
id: "chapterList",
type: "String",
hint: "选择目录列表的章节节点 (规则结果为List<Element>)",
},
{
title: "章节名称",
namespace: "ruleToc",
id: "chapterName",
type: "String",
hint: "选择章节名称 (规则结果为String)",
},
{
title: "章节地址",
namespace: "ruleToc",
id: "chapterUrl",
type: "String",
hint: "选择章节链接 (规则结果为String类型的Url)",
},
{
title: "标题处理",
namespace: "ruleToc",
id: "formatJs",
type: "String",
hint: "遍历去重后的章节列表的回调,提供index(章节序号从1开始)、title(章节标题)变量,额外提供gInt(初始值0),返回值作为新的标题",
},
{
title: "卷名标识",
namespace: "ruleToc",
id: "isVolume",
type: "String",
hint: "章节名称是否是卷名 (规则结果为Bool)",
},
{
title: "章节信息",
namespace: "ruleToc",
id: "updateTime",
type: "String",
hint: "选择章节信息(如更新时间) (规则结果为String)",
},
{
title: "收费标识",
namespace: "ruleToc",
id: "isVip",
type: "String",
hint: "章节是否为VIP章节 (规则结果为Bool)",
},
{
title: "购买标识",
namespace: "ruleToc",
id: "isPay",
type: "String",
hint: "章节是否为已购买 (规则结果为Bool)",
},
{
title: "翻页规则",
namespace: "ruleToc",
id: "nextTocUrl",
type: "String",
hint: "选择目录下一页链接 (规则结果为List<String>或String)",
},
],
},
content: {
name: "正文",
children: [
{
title: "正文规则",
namespace: "ruleContent",
id: "content",
type: "String",
hint: "选择正文内容 (规则结果为String)",
},
{
title: "标题规则",
namespace: "ruleContent",
id: "title",
type: "String",
hint: "获取结果将会覆盖章节标题 (规则结果为String)",
},
{
title: "翻页规则",
namespace: "ruleContent",
id: "nextContentUrl",
type: "String",
hint: "选择下一分页(不是下一章)链接 (规则结果为String类型的Url)",
},
{
title: "脚本注入",
namespace: "ruleContent",
id: "webJs",
type: "String",
hint: "注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型",
},
{
title: "资源正则",
namespace: "ruleContent",
id: "sourceRegex",
type: "String",
hint: "匹配资源的url特征,用于嗅探",
},
{
title: "替换规则",
namespace: "ruleContent",
id: "replaceRegex",
type: "String",
hint: "多页内容合并后替换,用于正文净化",
},
{
title: "图片样式",
namespace: "ruleContent",
id: "imageStyle",
type: "String",
hint: "FULL:铺满 不填:默认样式",
},
{
title: "图片解密",
namespace: "ruleContent",
id: "imageDecode",
type: "String",
hint: "填写JavaScript 返回解密图片的bytes ",
},
{
title: "购买操作",
namespace: "ruleContent",
id: "payAction",
type: "String",
hint: "填写JavaScript 返回购买链接或者调用购买接口",
},
],
},
/*
review: {
name: "段评",
children: [
{
title: "段评URL",
namespace: "ruleReview",
id: "reviewUrl",
type: "String",
hint: "段评URL",
},
{
title: "发布头像",
namespace: "ruleReview",
id: "avatarRule",
type: "String",
hint: "段评发布者头像",
},
{
title: "段评内容",
namespace: "ruleReview",
id: "contentRule",
type: "String",
hint: "段评内容",
},
{
title: "发布时间",
namespace: "ruleReview",
id: "postTimeRule",
type: "String",
hint: "段评发布时间",
},
{
title: "回复URL",
namespace: "ruleReview",
id: "reviewQuoteUrl",
type: "String",
hint: "获取段评回复URL",
},
{
title: "点赞URL",
namespace: "ruleReview",
id: "voteUpUrl",
type: "String",
hint: "点赞URL",
},
{
title: "点踩URL",
namespace: "ruleReview",
id: "voteDownUrl",
type: "String",
hint: "点踩URL",
},
{
title: "发送回复",
namespace: "ruleReview",
id: "postReviewUrl",
type: "String",
hint: "发送回复URL",
},
{
title: "回复段评",
namespace: "ruleReview",
id: "postQuoteUrl",
type: "String",
hint: "发送回复段评URL",
},
{
title: "删除段评",
namespace: "ruleReview",
id: "deleteUrl",
type: "String",
hint: "删除段评URL",
},
],
},*/
other: {
name: "其他",
children: [
{
title: "启用搜索",
id: "enabled",
type: "Boolean",
},
{
title: "启用发现",
id: "enabledExplore",
type: "Boolean",
},
// {
// title: "启用段评",
// id: "enabledReview",
// type: "Boolean",
// },
{
title: "CookieJar",
id: "enabledCookieJar",
type: "Boolean",
},
{
title: "搜索权重",
id: "weight",
type: "Number",
},
{
title: "排序编号",
id: "customOrder",
type: "Number",
},
],
},
};
@@ -0,0 +1,585 @@
export default {
base: {
name: '基础',
children: [
{
title: '源类型',
id: 'bookSourceType',
type: 'Array',
array: ['文本', '音频', '图片', '文件'],
required: true,
},
{
title: '源域名',
id: 'bookSourceUrl',
type: 'String',
hint: '通常填写网站主页,例: https://www.qidian.com',
required: true,
},
{
title: '源名称',
id: 'bookSourceName',
type: 'String',
hint: '会显示在源列表',
required: true,
},
{
title: '源分组',
id: 'bookSourceGroup',
type: 'String',
hint: '描述源的特征信息',
},
{
title: '源注释',
id: 'bookSourceComment',
type: 'String',
hint: '描述源作者和状态',
},
{
title: '登录地址',
id: 'loginUrl',
type: 'String',
hint: '填写网站登录网址,仅在需要登录的源有用',
},
{
title: '登录界面',
id: 'loginUi',
type: 'String',
hint: '自定义登录界面',
},
{
title: '登录检测',
id: 'loginCheckJs',
type: 'String',
hint: '登录检测js',
},
{
title: '封面解密',
id: 'coverDecodeJs',
type: 'String',
hint: '封面解密js',
},
{
title: '链接验证',
id: 'bookUrlPattern',
type: 'String',
hint: '书籍URL正则,当详情页URL与源URL的域名不一致时有效,用于添加网址',
},
{
title: '请求头',
id: 'header',
type: 'String',
hint: '客户端标识',
},
{
title: '变量说明',
id: 'variableComment',
type: 'String',
hint: '书源变量说明',
},
{
title: '并发率',
id: 'concurrentRate',
type: 'String',
hint: '并发率,如1000(访问间隔1000ms)或者1/1000(1000ms内访问1次)',
},
{
title: 'js库',
id: 'jsLib',
type: 'String',
hint: 'js库, 可填写js或者key-value object获取在线js文件',
},
],
},
search: {
name: '搜索',
children: [
{
title: '搜索地址',
id: 'searchUrl',
type: 'String',
hint: '[域名可省略]/search.php@kw={{key}}',
},
{
title: '校验文字',
namespace: 'ruleSearch',
id: 'checkKeyWord',
type: 'String',
hint: '校验关键字,强烈建议填写',
},
{
title: '列表规则',
namespace: 'ruleSearch',
id: 'bookList',
type: 'String',
hint: '选择书籍节点 (规则结果为List<Element>)',
},
{
title: '书名规则',
namespace: 'ruleSearch',
id: 'name',
type: 'String',
hint: '选择节点书名 (规则结果为String)',
},
{
title: '作者规则',
namespace: 'ruleSearch',
id: 'author',
type: 'String',
hint: '选择节点作者 (规则结果为String)',
},
{
title: '分类规则',
namespace: 'ruleSearch',
id: 'kind',
type: 'String',
hint: '选择节点分类信息 (规则结果为String)',
},
{
title: '字数规则',
namespace: 'ruleSearch',
id: 'wordCount',
type: 'String',
hint: '选择节点字数信息 (规则结果为String)',
},
{
title: '最新章节',
namespace: 'ruleSearch',
id: 'lastChapter',
type: 'String',
hint: '选择节点最新章节 (规则结果为String)',
},
{
title: '简介规则',
namespace: 'ruleSearch',
id: 'intro',
type: 'String',
hint: '选择节点书籍简介 (规则结果为String)',
},
{
title: '封面规则',
namespace: 'ruleSearch',
id: 'coverUrl',
type: 'String',
hint: '选择节点书籍封面 (规则结果为String类型的url)',
},
{
title: '详情地址',
namespace: 'ruleSearch',
id: 'bookUrl',
type: 'String',
hint: '选择书籍详情页网址 (规则结果为String类型的url)',
},
],
},
find: {
name: '发现',
children: [
{
title: '发现地址',
id: 'exploreUrl',
type: 'String',
hint: '单个发现格式<name>::<url>或者{url:<url>,title:<name>,style:...};前者用换行符或者&&连接,后者放在数组内;可用js动态生成',
},
{
title: '列表规则',
namespace: 'ruleExplore',
id: 'bookList',
type: 'String',
hint: '选择书籍节点 (规则结果为List<Element>)',
},
{
title: '书名规则',
namespace: 'ruleExplore',
id: 'name',
type: 'String',
hint: '选择节点书名 (规则结果为String)',
},
{
title: '作者规则',
namespace: 'ruleExplore',
id: 'author',
type: 'String',
hint: '选择节点作者 (规则结果为String)',
},
{
title: '分类规则',
namespace: 'ruleExplore',
id: 'kind',
type: 'String',
hint: '选择节点分类信息 (规则结果为String)',
},
{
title: '字数规则',
namespace: 'ruleExplore',
id: 'wordCount',
type: 'String',
hint: '选择节点字数信息 (规则结果为String)',
},
{
title: '最新章节',
namespace: 'ruleExplore',
id: 'lastChapter',
type: 'String',
hint: '选择节点最新章节 (规则结果为String)',
},
{
title: '简介规则',
namespace: 'ruleExplore',
id: 'intro',
type: 'String',
hint: '选择节点书籍简介 (规则结果为String)',
},
{
title: '封面规则',
namespace: 'ruleExplore',
id: 'coverUrl',
type: 'String',
hint: '选择节点书籍封面 (规则结果为String类型的url)',
},
{
title: '详情地址',
namespace: 'ruleExplore',
id: 'bookUrl',
type: 'String',
hint: '选择书籍详情页网址 (规则结果为String类型的url)',
},
],
},
detail: {
name: '详情',
children: [
{
title: '预处理',
namespace: 'ruleBookInfo',
id: 'init',
type: 'String',
hint: '用于加速详情信息检索,只支持AllInOne规则',
},
{
title: '书名规则',
namespace: 'ruleBookInfo',
id: 'name',
type: 'String',
hint: '选择节点书名 (规则结果为String)',
},
{
title: '作者规则',
namespace: 'ruleBookInfo',
id: 'author',
type: 'String',
hint: '选择节点作者 (规则结果为String)',
},
{
title: '分类规则',
namespace: 'ruleBookInfo',
id: 'kind',
type: 'String',
hint: '选择节点分类信息 (规则结果为String)',
},
{
title: '字数规则',
namespace: 'ruleBookInfo',
id: 'wordCount',
type: 'String',
hint: '选择节点字数信息 (规则结果为String)',
},
{
title: '最新章节',
namespace: 'ruleBookInfo',
id: 'lastChapter',
type: 'String',
hint: '选择节点最新章节 (规则结果为String)',
},
{
title: '简介规则',
namespace: 'ruleBookInfo',
id: 'intro',
type: 'String',
hint: '选择节点书籍简介 (规则结果为String)',
},
{
title: '封面规则',
namespace: 'ruleBookInfo',
id: 'coverUrl',
type: 'String',
hint: '选择节点书籍封面 (规则结果为String类型的url)',
},
{
title: '目录地址',
namespace: 'ruleBookInfo',
id: 'tocUrl',
type: 'String',
hint: '选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)',
},
{
title: '修改书籍',
namespace: 'ruleBookInfo',
id: 'canReName',
type: 'String',
hint: '允许修改书名作者(规则结果为String类型, 默认不允许)',
},
{
title: '下载URL',
namespace: 'ruleBookInfo',
id: 'downloadUrls',
type: 'String',
hint: '文件类书源下载地址 (规则结果为String类型的url, 多个链接返回数组)',
},
],
},
directory: {
name: '目录',
children: [
{
title: '更新前JS',
namespace: 'ruleToc',
id: 'preUpdateJs',
type: 'String',
hint: '更新目录前调用JS 动态更新目录链接',
},
{
title: '列表规则',
namespace: 'ruleToc',
id: 'chapterList',
type: 'String',
hint: '选择目录列表的章节节点 (规则结果为List<Element>)',
},
{
title: '章节名称',
namespace: 'ruleToc',
id: 'chapterName',
type: 'String',
hint: '选择章节名称 (规则结果为String)',
},
{
title: '章节地址',
namespace: 'ruleToc',
id: 'chapterUrl',
type: 'String',
hint: '选择章节链接 (规则结果为String类型的Url)',
},
{
title: '标题处理',
namespace: 'ruleToc',
id: 'formatJs',
type: 'String',
hint: '遍历去重后的章节列表的回调,提供index(章节序号从1开始)、title(章节标题)变量,额外提供gInt(初始值0),返回值作为新的标题',
},
{
title: '卷名标识',
namespace: 'ruleToc',
id: 'isVolume',
type: 'String',
hint: '章节名称是否是卷名 (规则结果为Bool)',
},
{
title: '章节信息',
namespace: 'ruleToc',
id: 'updateTime',
type: 'String',
hint: '选择章节信息(如更新时间) (规则结果为String)',
},
{
title: '收费标识',
namespace: 'ruleToc',
id: 'isVip',
type: 'String',
hint: '章节是否为VIP章节 (规则结果为Bool)',
},
{
title: '购买标识',
namespace: 'ruleToc',
id: 'isPay',
type: 'String',
hint: '章节是否为已购买 (规则结果为Bool)',
},
{
title: '翻页规则',
namespace: 'ruleToc',
id: 'nextTocUrl',
type: 'String',
hint: '选择目录下一页链接 (规则结果为List<String>或String)',
},
],
},
content: {
name: '正文',
children: [
{
title: '正文规则',
namespace: 'ruleContent',
id: 'content',
type: 'String',
hint: '选择正文内容 (规则结果为String)',
},
{
title: '标题规则',
namespace: 'ruleContent',
id: 'title',
type: 'String',
hint: '获取结果将会覆盖章节标题 (规则结果为String)',
},
{
title: '翻页规则',
namespace: 'ruleContent',
id: 'nextContentUrl',
type: 'String',
hint: '选择下一分页(不是下一章)链接 (规则结果为String类型的Url)',
},
{
title: '脚本注入',
namespace: 'ruleContent',
id: 'webJs',
type: 'String',
hint: '注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型',
},
{
title: '资源正则',
namespace: 'ruleContent',
id: 'sourceRegex',
type: 'String',
hint: '匹配资源的url特征,用于嗅探',
},
{
title: '替换规则',
namespace: 'ruleContent',
id: 'replaceRegex',
type: 'String',
hint: '多页内容合并后替换,用于正文净化',
},
{
title: '图片样式',
namespace: 'ruleContent',
id: 'imageStyle',
type: 'String',
hint: 'FULL:铺满 不填:默认样式',
},
{
title: '图片解密',
namespace: 'ruleContent',
id: 'imageDecode',
type: 'String',
hint: '填写JavaScript 返回解密图片的bytes ',
},
{
title: '购买操作',
namespace: 'ruleContent',
id: 'payAction',
type: 'String',
hint: '填写JavaScript 返回购买链接或者调用购买接口',
},
],
},
/*
review: {
name: "段评",
children: [
{
title: "段评URL",
namespace: "ruleReview",
id: "reviewUrl",
type: "String",
hint: "段评URL",
},
{
title: "发布头像",
namespace: "ruleReview",
id: "avatarRule",
type: "String",
hint: "段评发布者头像",
},
{
title: "段评内容",
namespace: "ruleReview",
id: "contentRule",
type: "String",
hint: "段评内容",
},
{
title: "发布时间",
namespace: "ruleReview",
id: "postTimeRule",
type: "String",
hint: "段评发布时间",
},
{
title: "回复URL",
namespace: "ruleReview",
id: "reviewQuoteUrl",
type: "String",
hint: "获取段评回复URL",
},
{
title: "点赞URL",
namespace: "ruleReview",
id: "voteUpUrl",
type: "String",
hint: "点赞URL",
},
{
title: "点踩URL",
namespace: "ruleReview",
id: "voteDownUrl",
type: "String",
hint: "点踩URL",
},
{
title: "发送回复",
namespace: "ruleReview",
id: "postReviewUrl",
type: "String",
hint: "发送回复URL",
},
{
title: "回复段评",
namespace: "ruleReview",
id: "postQuoteUrl",
type: "String",
hint: "发送回复段评URL",
},
{
title: "删除段评",
namespace: "ruleReview",
id: "deleteUrl",
type: "String",
hint: "删除段评URL",
},
],
},*/
other: {
name: '其他',
children: [
{
title: '启用搜索',
id: 'enabled',
type: 'Boolean',
},
{
title: '启用发现',
id: 'enabledExplore',
type: 'Boolean',
},
// {
// title: "启用段评",
// id: "enabledReview",
// type: "Boolean",
// },
{
title: 'CookieJar',
id: 'enabledCookieJar',
type: 'Boolean',
},
{
title: '搜索权重',
id: 'weight',
type: 'Number',
},
{
title: '排序编号',
id: 'customOrder',
type: 'Number',
},
],
},
}
@@ -1,216 +0,0 @@
export default {
base: {
name: "基础",
children: [
{
title: "源域名",
id: "sourceUrl",
type: "String",
hint: "通常填写网站主页,例: https://www.qidian.com",
required: true,
},
{
title: "图标",
id: "sourceIcon",
type: "String",
hint: "填写图片网络链接",
},
{
title: "源名称",
id: "sourceName",
type: "String",
hint: "会显示在源列表",
required: true,
},
{
title: "源分组",
id: "sourceGroup",
type: "String",
hint: "描述源的特征信息",
},
{
title: "源注释",
id: "sourceComment",
type: "String",
hint: "描述源作者和状态",
},
{
title: "分类地址",
id: "sortUrl",
type: "String",
hint: "名称1::链接1\n名称2::链接2",
},
{
title: "登录地址",
id: "loginUrl",
type: "String",
hint: "填写网站登录网址,仅在需要登录的源有用",
},
{
title: "登录界面",
id: "loginUi",
type: "String",
hint: "自定义登录界面",
},
{
title: "登录检测",
id: "loginCheckJs",
type: "String",
hint: "登录检测js",
},
{
title: "封面解密",
id: "coverDecodeJs",
type: "String",
hint: "封面解密js",
},
{
title: "请求头",
id: "header",
type: "String",
hint: "客户端标识",
},
{
title: "变量说明",
id: "variableComment",
type: "String",
hint: "源变量说明",
},
{
title: "并发率",
id: "concurrentRate",
type: "String",
hint: "并发率",
},
],
},
list: {
name: "列表",
children: [
{
title: "列表规则",
id: "ruleArticles",
type: "String",
hint: "规则结果为List<Element>",
},
{
title: "翻页规则",
id: "ruleNextPage",
type: "String",
hint: "下一页链接 规则结果为List<String>或String",
},
{
title: "标题规则",
id: "ruleTitle",
type: "String",
hint: "文章标题 规则结果为String",
},
{
title: "时间规则",
id: "rulePubDate",
type: "String",
hint: "文章发布时间 规则结果为String",
},
{
title: "描述规则",
id: "ruleDescription",
type: "String",
hint: "文章简要描述 规则结果为String",
},
{
title: "图片规则",
id: "ruleImage",
type: "String",
hint: "文章图片链接 规则结果为String",
},
{
title: "链接规则",
id: "ruleLink",
type: "String",
hint: "文章链接 规则结果为String",
},
],
},
webView: {
name: "WebView",
children: [
{
title: "内容规则",
id: "ruleContent",
type: "String",
hint: "文章正文",
},
{
title: "样式规则",
id: "style",
type: "String",
hint: "文章正文样式 填写css",
},
{
title: "注入规则",
id: "injectJs",
type: "String",
hint: "注入网页的JavaScript",
},
{
title: "黑名单",
id: "contentBlacklist",
type: "String",
hint: "webView链接加载黑名单,英文逗号隔开",
},
{
title: "白名单",
id: "contentWhitelist",
type: "String",
hint: "webView链接加载白名单,英文逗号隔开",
},
{
title: "链接拦截",
id: "shouldOverrideUrlLoading",
type: "String",
hint: "填写js,变量url为当前资源链接,返回true拦截",
},
],
},
other: {
name: "其他",
children: [
{
title: "列表样式",
id: "articleStyle",
type: "Array",
array: ["默认", "大图", "双列"],
},
{
title: "加载地址",
id: "loadWithBaseUrl",
type: "Boolean",
},
{
title: "启用JS",
id: "enableJs",
type: "Boolean",
},
{
title: "启用",
id: "enabled",
type: "Boolean",
},
{
title: "Cookie",
id: "enabledCookieJar",
type: "Boolean",
},
{
title: "单URL",
id: "singleUrl",
type: "Boolean",
},
{
title: "排序编号",
id: "customOrder",
type: "Number",
},
],
},
};
@@ -0,0 +1,216 @@
export default {
base: {
name: '基础',
children: [
{
title: '源域名',
id: 'sourceUrl',
type: 'String',
hint: '通常填写网站主页,例: https://www.qidian.com',
required: true,
},
{
title: '图标',
id: 'sourceIcon',
type: 'String',
hint: '填写图片网络链接',
},
{
title: '源名称',
id: 'sourceName',
type: 'String',
hint: '会显示在源列表',
required: true,
},
{
title: '源分组',
id: 'sourceGroup',
type: 'String',
hint: '描述源的特征信息',
},
{
title: '源注释',
id: 'sourceComment',
type: 'String',
hint: '描述源作者和状态',
},
{
title: '分类地址',
id: 'sortUrl',
type: 'String',
hint: '名称1::链接1\n名称2::链接2',
},
{
title: '登录地址',
id: 'loginUrl',
type: 'String',
hint: '填写网站登录网址,仅在需要登录的源有用',
},
{
title: '登录界面',
id: 'loginUi',
type: 'String',
hint: '自定义登录界面',
},
{
title: '登录检测',
id: 'loginCheckJs',
type: 'String',
hint: '登录检测js',
},
{
title: '封面解密',
id: 'coverDecodeJs',
type: 'String',
hint: '封面解密js',
},
{
title: '请求头',
id: 'header',
type: 'String',
hint: '客户端标识',
},
{
title: '变量说明',
id: 'variableComment',
type: 'String',
hint: '源变量说明',
},
{
title: '并发率',
id: 'concurrentRate',
type: 'String',
hint: '并发率',
},
],
},
list: {
name: '列表',
children: [
{
title: '列表规则',
id: 'ruleArticles',
type: 'String',
hint: '规则结果为List<Element>',
},
{
title: '翻页规则',
id: 'ruleNextPage',
type: 'String',
hint: '下一页链接 规则结果为List<String>或String',
},
{
title: '标题规则',
id: 'ruleTitle',
type: 'String',
hint: '文章标题 规则结果为String',
},
{
title: '时间规则',
id: 'rulePubDate',
type: 'String',
hint: '文章发布时间 规则结果为String',
},
{
title: '描述规则',
id: 'ruleDescription',
type: 'String',
hint: '文章简要描述 规则结果为String',
},
{
title: '图片规则',
id: 'ruleImage',
type: 'String',
hint: '文章图片链接 规则结果为String',
},
{
title: '链接规则',
id: 'ruleLink',
type: 'String',
hint: '文章链接 规则结果为String',
},
],
},
webView: {
name: 'WebView',
children: [
{
title: '内容规则',
id: 'ruleContent',
type: 'String',
hint: '文章正文',
},
{
title: '样式规则',
id: 'style',
type: 'String',
hint: '文章正文样式 填写css',
},
{
title: '注入规则',
id: 'injectJs',
type: 'String',
hint: '注入网页的JavaScript',
},
{
title: '黑名单',
id: 'contentBlacklist',
type: 'String',
hint: 'webView链接加载黑名单,英文逗号隔开',
},
{
title: '白名单',
id: 'contentWhitelist',
type: 'String',
hint: 'webView链接加载白名单,英文逗号隔开',
},
{
title: '链接拦截',
id: 'shouldOverrideUrlLoading',
type: 'String',
hint: '填写js,变量url为当前资源链接,返回true拦截',
},
],
},
other: {
name: '其他',
children: [
{
title: '列表样式',
id: 'articleStyle',
type: 'Array',
array: ['默认', '大图', '双列'],
},
{
title: '加载地址',
id: 'loadWithBaseUrl',
type: 'Boolean',
},
{
title: '启用JS',
id: 'enableJs',
type: 'Boolean',
},
{
title: '启用',
id: 'enabled',
type: 'Boolean',
},
{
title: 'Cookie',
id: 'enabledCookieJar',
type: 'Boolean',
},
{
title: '单URL',
id: 'singleUrl',
type: 'Boolean',
},
{
title: '排序编号',
id: 'customOrder',
type: 'Number',
},
],
},
}
+23
View File
@@ -0,0 +1,23 @@
import type { Source } from '@/source'
import bookSourceEditConfig from './bookSourceEditConfig'
import rssSourceEditConfig from './rssSourceEditConfig'
type PickAnyValueKey<T> = {
[K in keyof T]: T[K] extends { [prop: string]: string } ? K : never
}
type b = keyof PickAnyValueKey<BookSoure>
type SourceConfigKey =
| keyof typeof bookSourceEditConfig
| keyof typeof rssSourceEditConfig
type SourceConfigRecord = {
title: string
type: string //"array" | "String" | "Boolean"
array?: string[]
hint?: string
required?: boolean
namespace?: Partial<keyof Source>
id: Partial<keyof Source>
}
type SourceConfigValue = { name: string; children: SourceConfigRecord[] }
export type SourceConfig = Partial<Record<SourceConfigKey, SourceConfigValue>>
-65
View File
@@ -1,65 +0,0 @@
import body_0 from "../assets/imgs/themes/body_0.png";
import content_0 from "../assets/imgs/themes/content_0.png";
import popup_0 from "../assets/imgs/themes/popup_0.png";
import body_1 from "../assets/imgs/themes/body_1.png";
import content_1 from "../assets/imgs/themes/content_1.png";
import popup_1 from "../assets/imgs/themes/popup_1.png";
import body_2 from "../assets/imgs/themes/body_2.png";
import content_2 from "../assets/imgs/themes/content_2.png";
import popup_2 from "../assets/imgs/themes/popup_2.png";
import body_3 from "../assets/imgs/themes/body_3.png";
import content_3 from "../assets/imgs/themes/content_3.png";
import popup_3 from "../assets/imgs/themes/popup_3.png";
import body_5 from "../assets/imgs/themes/body_5.png";
import content_5 from "../assets/imgs/themes/content_5.png";
import popup_5 from "../assets/imgs/themes/popup_5.png";
import body_6 from "../assets/imgs/themes/body_6.png";
import content_6 from "../assets/imgs/themes/content_6.png";
import popup_6 from "../assets/imgs/themes/popup_6.png";
var settings = {
themes: [
{
body: "#ede7da url(" + body_0 + ") repeat",
content: "#ede7da url(" + content_0 + ") repeat",
popup: "#ede7da url(" + popup_0 + ") repeat",
},
{
body: "#ede7da url(" + body_1 + ") repeat",
content: "#ede7da url(" + content_1 + ") repeat",
popup: "#ede7da url(" + popup_1 + ") repeat",
},
{
body: "#ede7da url(" + body_2 + ") repeat",
content: "#ede7da url(" + content_2 + ") repeat",
popup: "#ede7da url(" + popup_2 + ") repeat",
},
{
body: "#ede7da url(" + body_3 + ") repeat",
content: "#ede7da url(" + content_3 + ") repeat",
popup: "#ede7da url(" + popup_3 + ") repeat",
},
{
body: "#ebcece repeat",
content: "#f5e4e4 repeat",
popup: "#faeceb repeat",
},
{
body: "#ede7da url(" + body_5 + ") repeat",
content: "#ede7da url(" + content_5 + ") repeat",
popup: "#ede7da url(" + popup_5 + ") repeat",
},
{
body: "#ede7da url(" + body_6 + ") repeat",
content: "#ede7da url(" + content_6 + ") repeat",
popup: "#ede7da url(" + popup_6 + ") repeat",
},
],
fonts: [
"Microsoft YaHei, PingFangSC-Regular, HelveticaNeue-Light, Helvetica Neue Light, sans-serif",
"PingFangSC-Regular, -apple-system, Simsun",
"Kaiti",
],
};
export default settings;
+65
View File
@@ -0,0 +1,65 @@
import body_0 from '../assets/imgs/themes/body_0.png'
import content_0 from '../assets/imgs/themes/content_0.png'
import popup_0 from '../assets/imgs/themes/popup_0.png'
import body_1 from '../assets/imgs/themes/body_1.png'
import content_1 from '../assets/imgs/themes/content_1.png'
import popup_1 from '../assets/imgs/themes/popup_1.png'
import body_2 from '../assets/imgs/themes/body_2.png'
import content_2 from '../assets/imgs/themes/content_2.png'
import popup_2 from '../assets/imgs/themes/popup_2.png'
import body_3 from '../assets/imgs/themes/body_3.png'
import content_3 from '../assets/imgs/themes/content_3.png'
import popup_3 from '../assets/imgs/themes/popup_3.png'
import body_5 from '../assets/imgs/themes/body_5.png'
import content_5 from '../assets/imgs/themes/content_5.png'
import popup_5 from '../assets/imgs/themes/popup_5.png'
import body_6 from '../assets/imgs/themes/body_6.png'
import content_6 from '../assets/imgs/themes/content_6.png'
import popup_6 from '../assets/imgs/themes/popup_6.png'
const settings = {
themes: [
{
body: '#ede7da url(' + body_0 + ') repeat',
content: '#ede7da url(' + content_0 + ') repeat',
popup: '#ede7da url(' + popup_0 + ') repeat',
},
{
body: '#ede7da url(' + body_1 + ') repeat',
content: '#ede7da url(' + content_1 + ') repeat',
popup: '#ede7da url(' + popup_1 + ') repeat',
},
{
body: '#ede7da url(' + body_2 + ') repeat',
content: '#ede7da url(' + content_2 + ') repeat',
popup: '#ede7da url(' + popup_2 + ') repeat',
},
{
body: '#ede7da url(' + body_3 + ') repeat',
content: '#ede7da url(' + content_3 + ') repeat',
popup: '#ede7da url(' + popup_3 + ') repeat',
},
{
body: '#ebcece repeat',
content: '#f5e4e4 repeat',
popup: '#faeceb repeat',
},
{
body: '#ede7da url(' + body_5 + ') repeat',
content: '#ede7da url(' + content_5 + ') repeat',
popup: '#ede7da url(' + popup_5 + ') repeat',
},
{
body: '#ede7da url(' + body_6 + ') repeat',
content: '#ede7da url(' + content_6 + ') repeat',
popup: '#ede7da url(' + popup_6 + ') repeat',
},
],
fonts: [
'Microsoft YaHei, PingFangSC-Regular, HelveticaNeue-Light, Helvetica Neue Light, sans-serif',
'PingFangSC-Regular, -apple-system, Simsun',
'Kaiti',
],
}
export default settings
-36
View File
@@ -1,36 +0,0 @@
import { watch, unref, onUnmounted } from "vue";
import { ElLoading } from "element-plus";
import loadingSvg from "@element-plus/icons-svg/loading.svg?raw";
import "element-plus/theme-chalk/el-loading.css";
import "./loading.css";
export const useLoading = (target, text, spinner = loadingSvg) => {
// loading spinner
const isLoading = ref(false);
let loadingInstance = null;
const closeLoading = () => (isLoading.value = false);
const showLoading = () => (isLoading.value = true);
watch(isLoading, (loading) => {
if (!loading) return loadingInstance?.close();
loadingInstance = ElLoading.service({
target: unref(target),
spinner: spinner,
text: text,
lock: true,
background: "rgba(0, 0, 0, 0)",
});
});
const loadingWrapper = (promise) => {
if (!(promise instanceof Promise))
throw TypeError("loadingWrapper argument must be Promise");
showLoading();
return promise.finally(closeLoading);
};
onUnmounted(() => {
closeLoading();
});
return { isLoading, showLoading, closeLoading, loadingWrapper };
};
+40
View File
@@ -0,0 +1,40 @@
import { watch, unref, onUnmounted } from 'vue'
import { ElLoading } from 'element-plus'
import loadingSvg from '@element-plus/icons-svg/loading.svg?raw'
import 'element-plus/theme-chalk/el-loading.css'
import './loading.css'
export const useLoading = (
target: MaybeRef<string | HTMLElement | undefined>,
text: string,
spinner = loadingSvg,
) => {
// loading spinner
const isLoading = ref(false)
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null
const closeLoading = () => (isLoading.value = false)
const showLoading = () => (isLoading.value = true)
watch(isLoading, loading => {
if (!loading) return loadingInstance?.close()
loadingInstance = ElLoading.service({
target: unref(target),
spinner: spinner,
text: text,
lock: true,
background: 'rgba(0, 0, 0, 0)',
})
})
const loadingWrapper = (promise: Promise<unknown>) => {
if (!(promise instanceof Promise))
throw TypeError('loadingWrapper argument must be Promise')
showLoading()
return promise.finally(closeLoading)
}
onUnmounted(() => {
closeLoading()
})
return { isLoading, showLoading, closeLoading, loadingWrapper }
}
-18
View File
@@ -1,18 +0,0 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "@/router";
import store from "@/store";
import "element-plus/theme-chalk/dark/css-vars.css";
createApp(App).use(store).use(router).mount("#app");
// 书架 同步Element PLUS 夜间模式
watch(
() => useBookStore().isNight,
(isNight) => {
if (isNight) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
},
);
+18
View File
@@ -0,0 +1,18 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/router'
import store from '@/store'
import 'element-plus/theme-chalk/dark/css-vars.css'
createApp(App).use(store).use(router).mount('#app')
// 书架 同步Element PLUS 夜间模式
watch(
() => useBookStore().isNight,
isNight => {
if (isNight) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
},
)
@@ -29,4 +29,5 @@ pnpm build
pnpm lint:fix
#格式化代码
```
- 调试的时候可以修改.env.development里面的地址连接手机端调试
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="zh" class="">
<head>
<meta charset="UTF-8" />
+10 -10
View File
@@ -1,19 +1,19 @@
import { createApp } from "vue";
import App from "@/App.vue";
import bookRouter from "@/router/bookRouter";
import store from "@/store";
import "element-plus/theme-chalk/dark/css-vars.css";
import { createApp } from 'vue'
import App from '@/App.vue'
import bookRouter from '@/router/bookRouter'
import store from '@/store'
import 'element-plus/theme-chalk/dark/css-vars.css'
createApp(App).use(store).use(bookRouter).mount("#app");
createApp(App).use(store).use(bookRouter).mount('#app')
// 同步Element PLUS 夜间模式
watch(
() => useBookStore().isNight,
(isNight) => {
isNight => {
if (isNight) {
document.documentElement.classList.add("dark");
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove('dark')
}
},
);
)
+3 -2
View File
@@ -1,6 +1,5 @@
# legado_web_editor
## 🚧开发注意
如果你想要调试项目 请修改文件`.env.development``VITE_API`为阅读web服务ip
@@ -17,17 +16,19 @@ pnpm i
```
### Compiles and hot-reloads for development
```
pnpm dev
```
### Compiles and minifies for production
```
pnpm build
```
### Lints and fixes files
```
pnpm lint:fix
```
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="zh" class="">
<head>
<meta charset="UTF-8" />
+6 -6
View File
@@ -1,7 +1,7 @@
import { createApp } from "vue";
import App from "@/App.vue";
import sourceRouter from "@/router/sourceRouter";
import store from "@/store";
import "element-plus/theme-chalk/dark/css-vars.css";
import { createApp } from 'vue'
import App from '@/App.vue'
import sourceRouter from '@/router/sourceRouter'
import store from '@/store'
import 'element-plus/theme-chalk/dark/css-vars.css'
createApp(App).use(store).use(sourceRouter).mount("#app");
createApp(App).use(store).use(sourceRouter).mount('#app')
+18
View File
@@ -0,0 +1,18 @@
export {}
export type Options = {
duration?: number | [(distance: number) => number]
offset?: number
callback?: () => void // "undefined" is a suitable default, and won't be called
easing?: (
timeElapsed: number,
start: number,
distance: number,
duration: number,
) => number
a11y?: boolean
container?: HTMLElement | string
}
export default function (
target: number | string | HTMLElement,
options: Options = {},
): void
+74 -74
View File
@@ -1,51 +1,51 @@
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2;
if (t < 1) return (c / 2) * t * t + b;
t--;
return (-c / 2) * (t * (t - 2) - 1) + b;
};
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const jumper = () => {
// private variable cache
// no variables are created during a jump, preventing memory leaks
let container; // container element to be scrolled (node)
let element; // element to scroll to (node)
let container // container element to be scrolled (node)
let element // element to scroll to (node)
let start; // where scroll starts (px)
let stop; // where scroll stops (px)
let start // where scroll starts (px)
let stop // where scroll stops (px)
let offset; // adjustment from the stop position (px)
let easing; // easing function (function)
let a11y; // accessibility support flag (boolean)
let offset // adjustment from the stop position (px)
let easing // easing function (function)
let a11y // accessibility support flag (boolean)
let distance; // distance of scroll (px)
let duration; // scroll duration (ms)
let distance // distance of scroll (px)
let duration // scroll duration (ms)
let timeStart; // time scroll started (ms)
let timeElapsed; // time spent scrolling thus far (ms)
let timeStart // time scroll started (ms)
let timeElapsed // time spent scrolling thus far (ms)
let next; // next scroll position (px)
let next // next scroll position (px)
let callback; // to call when done scrolling (function)
let callback // to call when done scrolling (function)
// scroll position helper
function location() {
let top = container.scrollTop || container.scrollY || container.pageYOffset;
top = typeof top === "undefined" ? 0 : top;
return top;
let top = container.scrollTop || container.scrollY || container.pageYOffset
top = typeof top === 'undefined' ? 0 : top
return top
}
// element offset helper
function top(element) {
const elementTop = element.getBoundingClientRect().top;
const elementTop = element.getBoundingClientRect().top
const containerTop = container.getBoundingClientRect
? container.getBoundingClientRect().top
: 0;
: 0
return elementTop - containerTop + start;
return elementTop - containerTop + start
}
// scrollTo helper
@@ -53,7 +53,7 @@ const jumper = () => {
function scrollTo(top) {
container.scrollTo
? container.scrollTo(0, top) // window
: (container.scrollTop = top); // custom container
: (container.scrollTop = top) // custom container
}
// rAF loop helper
@@ -61,126 +61,126 @@ const jumper = () => {
function loop(timeCurrent) {
// store time scroll started, if not started already
if (!timeStart) {
timeStart = timeCurrent;
timeStart = timeCurrent
}
// determine time spent scrolling so far
timeElapsed = timeCurrent - timeStart;
timeElapsed = timeCurrent - timeStart
// calculate next scroll position
next = easing(timeElapsed, start, distance, duration);
next = easing(timeElapsed, start, distance, duration)
// scroll to it
scrollTo(next);
scrollTo(next)
// check progress
timeElapsed < duration
? requestAnimationFrame(loop) // continue scroll loop
: done(); // scrolling is done
: done() // scrolling is done
}
// scroll finished helper
function done() {
// account for rAF time rounding inaccuracies
scrollTo(start + distance);
scrollTo(start + distance)
// if scrolling to an element, and accessibility is enabled
if (element && a11y) {
// add tabindex indicating programmatic focus
element.setAttribute("tabindex", "-1");
element.setAttribute('tabindex', '-1')
// focus the element
element.focus();
element.focus()
}
// if it exists, fire the callback
if (typeof callback === "function") {
callback();
if (typeof callback === 'function') {
callback()
}
// reset time for next jump
timeStart = false;
timeStart = false
}
// API
function jump(target, options = {}) {
// resolve options, or use defaults
duration = options.duration || 1000;
offset = options.offset || 0;
callback = options.callback; // "undefined" is a suitable default, and won't be called
easing = options.easing || easeInOutQuad;
a11y = options.a11y || false;
duration = options.duration || 1000
offset = options.offset || 0
callback = options.callback // "undefined" is a suitable default, and won't be called
easing = options.easing || easeInOutQuad
a11y = options.a11y || false
// resolve container
switch (typeof options.container) {
case "object":
case 'object':
// we assume container is an HTML element (Node)
container = options.container;
break;
container = options.container
break
case "string":
container = document.querySelector(options.container);
break;
case 'string':
container = document.querySelector(options.container)
break
default:
container = window;
container = window
}
// cache starting position
start = location();
start = location()
// resolve target
switch (typeof target) {
// scroll from current position
case "number":
element = undefined; // no element to scroll to
a11y = false; // make sure accessibility is off
stop = start + target;
break;
case 'number':
element = undefined // no element to scroll to
a11y = false // make sure accessibility is off
stop = start + target
break
// scroll to element (node)
// bounding rect is relative to the viewport
case "object":
element = target;
stop = top(element);
break;
case 'object':
element = target
stop = top(element)
break
// scroll to element (selector)
// bounding rect is relative to the viewport
case "string":
element = document.querySelector(target);
stop = top(element);
break;
case 'string':
element = document.querySelector(target)
stop = top(element)
break
}
// resolve scroll distance, accounting for offset
distance = stop - start + offset;
distance = stop - start + offset
// resolve duration
switch (typeof options.duration) {
// number in ms
case "number":
duration = options.duration;
break;
case 'number':
duration = options.duration
break
// function passed the distance of the scroll
case "function":
duration = options.duration(distance);
break;
case 'function':
duration = options.duration(distance)
break
}
// start the loop
requestAnimationFrame(loop);
requestAnimationFrame(loop)
}
// expose only the jump method
return jump;
};
return jump
}
// export singleton
const singleton = jumper();
const singleton = jumper()
export default singleton;
export default singleton
-23
View File
@@ -1,23 +0,0 @@
import { createWebHashHistory, createRouter } from "vue-router";
export const bookRoutes = [
{
path: "/",
name: "shelf",
component: () => import("../views/BookShelf.vue"),
},
{
path: "/chapter",
name: "chapter",
component: () => import("../views/BookChapter.vue"),
},
];
const router = createRouter({
// mode: "history",
history: createWebHashHistory(),
// @ts-ignore
routes: bookRoutes,
});
export default router;
+22
View File
@@ -0,0 +1,22 @@
import { createWebHashHistory, createRouter } from 'vue-router'
export const bookRoutes = [
{
path: '/',
name: 'shelf',
component: () => import('../views/BookShelf.vue'),
},
{
path: '/chapter',
name: 'chapter',
component: () => import('../views/BookChapter.vue'),
},
]
const router = createRouter({
// mode: "history",
history: createWebHashHistory(),
routes: bookRoutes,
})
export default router
-15
View File
@@ -1,15 +0,0 @@
import { createWebHashHistory, createRouter } from "vue-router";
import { bookRoutes } from "./bookRouter";
import { sourceRoutes } from "./sourceRouter";
const router = createRouter({
// history: createWebHistory(process.env.BASE_URL),
history: createWebHashHistory(),
// @ts-ignore
routes: bookRoutes.concat(sourceRoutes),
});
router.afterEach((to) => {
if (to.name == "shelf") document.title = "书架";
});
export default router;
+14
View File
@@ -0,0 +1,14 @@
import { createWebHashHistory, createRouter } from 'vue-router'
import { bookRoutes } from './bookRouter'
import { sourceRoutes } from './sourceRouter'
const router = createRouter({
// history: createWebHistory(process.env.BASE_URL),
history: createWebHashHistory(),
routes: [bookRoutes, sourceRoutes].flat(),
})
router.afterEach(to => {
if (to.name == 'shelf') document.title = '书架'
})
export default router
@@ -1,23 +1,23 @@
import sourceEditor from "../views/SourceEditor.vue";
import { createWebHashHistory, createRouter } from "vue-router";
import sourceEditor from '../views/SourceEditor.vue'
import { createWebHashHistory, createRouter } from 'vue-router'
export const sourceRoutes = [
{
path: "/bookSource",
name: "book-home",
path: '/bookSource',
name: 'book-home',
component: sourceEditor,
},
{
path: "/rssSource",
name: "rss-home",
path: '/rssSource',
name: 'rss-home',
component: sourceEditor,
},
];
]
const router = createRouter({
// history: createWebHistory(process.env.BASE_URL),
history: createWebHashHistory(),
routes: sourceRoutes,
});
})
export default router;
export default router
+158 -22
View File
@@ -1,29 +1,165 @@
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/data/entities */
interface BaseSource {
lastUpdateTime?: number | undefined
type BaseSource = {
/**
* 并发率
*/
concurrentRate?: string
/**
* 登录地址
*/
loginUrl?: string
/**
* 登录UI
*/
loginUi?: string
/**
* 请求头
*/
header?: string
/**
* 启用cookieJar
*/
enabledCookieJar?: boolean
/**
* js库
*/
jsLib?: string
}
interface BookSoure extends BaseSource {
bookSourceUrl?: string | undefined
bookSourceName?: string | undefined
bookSourceType?: number | undefined
bookSourceGroup?: string | undefined
bookSourceComment?: string | undefined
ruleSearch?: RuleSearch | undefined
ruleBookInfo?: RuleBookInfo | undefined
ruleToc?: RuleToc | undefined
ruleContent?: RuleContent | undefined
ruleReview?: RuleReview | undefined
ruleExplore?: ruleExplore | undefined
type BookSoure = BaseSource & {
// 地址,包括 http/https
bookSourceUrl: string
// 名称
bookSourceName: string
// 分组
bookSourceGroup?: string
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
bookSourceType: number
// 详情页url正则
bookUrlPattern?: string
// 手动排序编号
customOrder: number
// 是否启用
enabled: boolean
// 启用发现
enabledExplore: boolean
// 登录检测js
loginCheckJs?: string
// 封面解密js
coverDecodeJs?: string
// 注释
bookSourceComment?: string
// 自定义变量说明
variableComment?: string
// 最后更新时间,用于排序
lastUpdateTime: number
// 响应时间,用于排序
respondTime: number
// 智能排序的权重
weight: number
// 发现url
exploreUrl?: string
// 发现筛选规则
exploreScreen?: string
// 发现规则
ruleExplore?: ExploreRule
// 搜索url
searchUrl?: string
// 搜索规则
ruleSearch?: SearchRule
// 书籍信息页规则
ruleBookInfo?: BookInfoRule
// 目录页规则
ruleToc?: TocRule
// 正文页规则
ruleContent?: ContentRule
// 段评规则
ruleReview?: ReviewRule
}
interface RuleSearch {
checkKeyWord?: string | undefined
type RuleSearch = {
checkKeyWord?: string
[prop: string]: string
}
interface RssSource extends BaseSource {
sourceUrl?: string | undefined
sourceName?: string | undefined
sourceGroup?: string | undefined
sourceComment?: string | undefined
/* type ExploreRule = {
[prop:string]: string
}
type Source = BookSoure & RssSource
type BookInfoRule = {
[prop:string]: string
}
type TocRule = {
[prop:string]: string
}
type ContentRule = {
[prop:string]: string
}
type ReviewRule = {
[prop:string]: string
} */
type RssSource = BaseSource & {
sourceUrl: string
// 名称
sourceName: string
// 图标
sourceIcon: string
// 分组
sourceGroup?: string
// 注释
sourceComment?: string
// 是否启用
enabled: boolean
// 自定义变量说明
variableComment?: string
/**登录检测js**/
loginCheckJs?: string
/**封面解密js**/
coverDecodeJs?: string
/**分类Url**/
sortUrl?: string
/**是否单url源**/
singleUrl: boolean
/*列表规则*/
/**列表样式,0,1,2**/
articleStyle: number
/**列表规则**/
ruleArticles?: string
/**下一页规则**/
ruleNextPage?: string
/**标题规则**/
ruleTitle?: string
/**发布日期规则**/
rulePubDate?: string
/*webView规则*/
/**描述规则**/
ruleDescription?: string
/**图片规则**/
ruleImage?: string
/**链接规则**/
ruleLink?: string
/**正文规则**/
ruleContent?: string
/**正文url白名单**/
contentWhitelist?: string
/**正文url黑名单**/
contentBlacklist?: string
/**
* 跳转url拦截,
* js, 返回true拦截,js变量url,可以通过js打开url,比如调用阅读搜索,添加书架等,简化规则写法,不用webView js注入
* **/
shouldOverrideUrlLoading?: string
/**webView样式**/
style?: string
enableJs: boolean
loadWithBaseUrl: boolean
/**注入js**/
injectJs?: string
/*其它规则*/
/**最后更新时间,用于排序**/
lastUpdateTime: number
customOrder: number
}
type Source = BookSoure | RssSource
export { Source, BookSoure, RssSource }
-116
View File
@@ -1,116 +0,0 @@
import { defineStore } from "pinia";
import API from "@api";
export const useBookStore = defineStore("book", {
state: () => {
return {
connectStatus: "正在连接后端服务器……",
/**@type {"primary" | "success" |"danger"} */
connectType: "primary",
newConnect: true,
/**@type {Array<{respondTime:number}>} */
searchBooks: [],
shelf: [],
catalog: [],
/**@type {{index: number,chapterPos:number}} */
readingBook: { index: 0, chapterPos: 0 },
popCataVisible: false,
contentLoading: true,
showContent: false,
config: {
theme: 0,
font: 0,
fontSize: 18,
readWidth: 800,
infiniteLoading: false,
customFontName: "",
jumpDuration: 1000,
spacing: {
paragraph: 1,
line: 0.8,
letter: 0,
},
},
miniInterface: false,
readSettingsVisible: false,
};
},
getters: {
bookProgress: (state) => {
if (state.catalog.length == 0) return;
// @ts-ignore
const { index, chapterPos, bookName, bookAuthor } = state.readingBook;
let title = state.catalog[index]?.title;
if (!title) return;
return {
name: bookName,
author: bookAuthor,
durChapterIndex: index,
durChapterPos: chapterPos,
durChapterTime: new Date().getTime(),
durChapterTitle: title,
};
},
theme: (state) => {
return state.config.theme;
},
isNight: (state) => state.config.theme == 6,
},
actions: {
setConnectStatus(connectStatus) {
this.connectStatus = connectStatus;
},
setConnectType(connectType) {
this.connectType = connectType;
},
setNewConnect(newConnect) {
this.newConnect = newConnect;
},
addBooks(books) {
this.shelf = books;
},
clearBooks() {
this.shelf = [];
},
setCatalog(catalog) {
this.catalog = catalog;
},
setPopCataVisible(visible) {
this.popCataVisible = visible;
},
setContentLoading(loading) {
this.contentLoading = loading;
},
setReadingBook(readingBook) {
this.readingBook = readingBook;
},
setConfig(config) {
this.config = Object.assign({}, this.config, config);
},
setReadSettingsVisible(visible) {
this.readSettingsVisible = visible;
},
setShowContent(visible) {
this.showContent = visible;
},
setMiniInterface(mini) {
this.miniInterface = mini;
},
async setSearchBooks(books) {
books.forEach((book) => {
let findBook = this.shelf.find((item) => item.bookUrl == book.bookUrl);
if (findBook === undefined) {
this.searchBooks.push(book);
}
});
},
clearSearchBooks() {
this.searchBooks = [];
},
//保存进度到app
async saveBookProgress() {
if (!this.bookProgress) return Promise.resolve();
return API.saveBookProgress(this.bookProgress);
},
},
});
+127
View File
@@ -0,0 +1,127 @@
import { defineStore } from 'pinia'
import API from '@api'
import type {
BaseBook,
Book,
BookChapter,
BookProgress,
SeachBook,
} from '@/book'
import type { webReadConfig } from '@/web'
export const useBookStore = defineStore('book', {
state: () => {
return {
connectStatus: '正在连接后端服务器……',
connectType: 'primary' as 'primary' | 'success' | 'danger',
newConnect: true,
searchBooks: [] as SeachBook[],
shelf: [] as Book[],
catalog: [] as BookChapter[],
readingBook: {} as BaseBook & {
chapterPos: number
chapterIndex: number
isSeachBook?: boolean
},
popCataVisible: false,
contentLoading: true,
showContent: false,
config: {
theme: -1,
font: 0,
fontSize: 18,
readWidth: 800,
infiniteLoading: false,
customFontName: '',
jumpDuration: 1000,
spacing: {
paragraph: 1,
line: 0.8,
letter: 0,
},
} as webReadConfig,
miniInterface: false,
readSettingsVisible: false,
}
},
getters: {
bookProgress: (state): BookProgress | undefined => {
if (state.catalog.length == 0) return
const { chapterIndex, chapterPos, name, author } = state.readingBook
const title = state.catalog[chapterIndex]?.title
if (!title) return
return {
name,
author,
durChapterIndex: chapterIndex,
durChapterPos: chapterPos,
durChapterTime: new Date().getTime(),
durChapterTitle: title,
}
},
theme: state => {
return state.config.theme
},
configInited: state => state.config.theme !== -1,
isNight: state => state.config.theme == 6,
},
actions: {
setConnectStatus(connectStatus: string) {
this.connectStatus = connectStatus
},
setConnectType(connectType: 'primary' | 'success' | 'danger') {
this.connectType = connectType
},
setNewConnect(newConnect: boolean) {
this.newConnect = newConnect
},
addBooks(books: Book[]) {
this.shelf = books
},
clearBooks() {
this.shelf = []
},
setCatalog(catalog: BookChapter[]) {
this.catalog = catalog
},
setPopCataVisible(visible: boolean) {
this.popCataVisible = visible
},
setContentLoading(loading: boolean) {
this.contentLoading = loading
},
setReadingBook(readingBook: typeof this.readingBook) {
this.readingBook = readingBook
},
setConfig(config: webReadConfig) {
this.config = Object.assign({}, this.config, config)
},
setReadSettingsVisible(visible: boolean) {
this.readSettingsVisible = visible
},
setShowContent(visible: boolean) {
this.showContent = visible
},
setMiniInterface(mini: boolean) {
this.miniInterface = mini
},
async setSearchBooks(books: SeachBook[]) {
books.forEach(book => {
const isSeachBook = this.shelf.every(
item => item.bookUrl !== book.bookUrl,
)
if (isSeachBook === true) {
this.searchBooks.push(book)
}
})
},
clearSearchBooks() {
this.searchBooks = []
},
//保存进度到app
async saveBookProgress() {
if (!this.bookProgress) return Promise.resolve()
return API.saveBookProgress(this.bookProgress)
},
},
})
-5
View File
@@ -1,5 +0,0 @@
import { createPinia } from "pinia";
export * from "./bookStore";
export * from "./sourceStore";
export default createPinia();
+5
View File
@@ -0,0 +1,5 @@
import { createPinia } from 'pinia'
export * from './bookStore'
export * from './sourceStore'
export default createPinia()
-132
View File
@@ -1,132 +0,0 @@
import { defineStore } from "pinia";
import {
emptyBookSource,
emptyRssSource,
getSourceUniqueKey,
convertSourcesToMap,
} from "@utils/souce";
const isBookSource = /bookSource/i.test(location.href);
const emptySource = isBookSource ? emptyBookSource : emptyRssSource;
export const useSourceStore = defineStore("source", {
state: () => {
return {
/** @type {import("@/source").BookSoure[]} */
bookSources: [], // 临时存放所有书源,
/** @type {import("@/source").RssSource[]} */
rssSources: [], // 临时存放所有订阅源
/** @type {import("@/source").Source[]} */
savedSources: [], // 批量保存到阅读app成功的源
/** @type {import("@/source").Source} */
currentSource: JSON.parse(JSON.stringify(emptySource)), // 当前编辑的源
currentTab: localStorage.getItem("tabName") || "editTab",
editTabSource: {}, // 生成序列化的json数据
isDebuging: false,
};
},
getters: {
sources: (state) => (isBookSource ? state.bookSources : state.rssSources),
// @ts-ignore
sourcesMap: (state) => convertSourcesToMap(state.sources),
savedSourcesMap: (state) => convertSourcesToMap(state.savedSources),
currentSourceUrl: (state) =>
isBookSource
? state.currentSource.bookSourceUrl
: state.currentSource.sourceUrl,
searchKey: (state) =>
isBookSource
? state.currentSource.ruleSearch.checkKeyWord || "我的"
: null,
},
actions: {
startDebug() {
this.currentTab = "editDebug";
this.isDebuging = true;
},
debugFinish() {
this.isDebuging = false;
},
//拉取源后保存
saveSources(data) {
if (isBookSource) {
this.bookSources = data;
} else {
this.rssSources = data;
}
},
//批量推送
setPushReturnSources(returnSoures) {
this.savedSources = returnSoures;
},
//删除源
deleteSources(data) {
let sources = isBookSource ? this.bookSources : this.rssSources;
data.forEach((source) => {
let index = sources.indexOf(source);
if (index > -1) sources.splice(index, 1);
});
},
//保存当前编辑源
saveCurrentSource() {
let source = this.currentSource,
map = this.sourcesMap;
map.set(getSourceUniqueKey(source), JSON.parse(JSON.stringify(source)));
this.saveSources(Array.from(map.values()));
},
// 更改当前编辑的源qq
changeCurrentSource(source) {
this.currentSource = JSON.parse(JSON.stringify(source));
},
// update editTab tabName and editTab info
changeTabName(tabName) {
this.currentTab = tabName;
localStorage.setItem("tabName", tabName);
},
changeEditTabSource(source) {
this.editTabSource = JSON.parse(JSON.stringify(source));
},
editHistory(history) {
let historyObj;
if (localStorage.getItem("history")) {
historyObj = JSON.parse(localStorage.getItem("history"));
historyObj.new.push(history);
if (historyObj.new.length > 50) {
historyObj.new.shift();
}
if (historyObj.old.length > 50) {
historyObj.old.shift();
}
localStorage.setItem("history", JSON.stringify(historyObj));
} else {
const arr = { new: [history], old: [] };
localStorage.setItem("history", JSON.stringify(arr));
}
},
editHistoryUndo() {
if (localStorage.getItem("history")) {
let historyObj = JSON.parse(localStorage.getItem("history"));
historyObj.old.push(this.currentSource);
if (historyObj.new.length) {
this.currentSource = historyObj.new.pop();
}
localStorage.setItem("history", JSON.stringify(historyObj));
}
},
clearAllHistory() {
localStorage.setItem("history", JSON.stringify({ new: [], old: [] }));
},
clearEdit() {
this.editTabSource = {};
this.currentSource = JSON.parse(JSON.stringify(emptySource)); //复制一份新对象
},
// clear all source
clearAllSource() {
this.bookSources = [];
this.rssSources = [];
this.savedSources = [];
},
},
});
+134
View File
@@ -0,0 +1,134 @@
import { defineStore } from 'pinia'
import {
emptyBookSource,
emptyRssSource,
getSourceUniqueKey,
convertSourcesToMap,
} from '@utils/souce'
import type { BookSoure, RssSource, Source } from '@/source'
const isBookSource = /bookSource/i.test(location.href)
const emptySource = isBookSource ? emptyBookSource : emptyRssSource
export const useSourceStore = defineStore('source', {
state: () => {
return {
bookSources: [] as BookSoure[], // 临时存放所有书源,
rssSources: [] as RssSource[], // 临时存放所有订阅源
savedSources: [] as Source[], // 批量保存到阅读app成功的源
currentSource: JSON.parse(JSON.stringify(emptySource)) as Source, // 当前编辑的源
currentTab: localStorage.getItem('tabName') || 'editTab',
editTabSource: {} as Source, // 生成序列化的json数据
isDebuging: false,
}
},
getters: {
sources: (state): Source[] =>
isBookSource ? state.bookSources : state.rssSources,
sourcesMap: function (): Map<string, Source> {
return convertSourcesToMap(this.sources)
},
savedSourcesMap: (state): Map<string, Source> =>
convertSourcesToMap(state.savedSources),
currentSourceUrl: state =>
isBookSource
? (state.currentSource as BookSoure).bookSourceUrl
: (state.currentSource as RssSource).sourceUrl,
searchKey: (state): string =>
isBookSource
? (state.currentSource as BookSoure)?.ruleSearch?.checkKeyWord || '我的'
: '',
},
actions: {
startDebug() {
this.currentTab = 'editDebug'
this.isDebuging = true
},
debugFinish() {
this.isDebuging = false
},
//拉取源后保存
saveSources(data: Source[]) {
if (isBookSource) {
this.bookSources = data as BookSoure[]
} else {
this.rssSources = data as RssSource[]
}
},
//批量推送
setPushReturnSources(returnSoures: Source[]) {
this.savedSources = returnSoures
},
//删除源
deleteSources(data: Source[]) {
const sources: Source[] = isBookSource
? this.bookSources
: this.rssSources
data.forEach(source => {
const index = sources.indexOf(source)
if (index > -1) sources.splice(index, 1)
})
},
//保存当前编辑源
saveCurrentSource() {
const source = this.currentSource,
map = this.sourcesMap
map.set(getSourceUniqueKey(source), JSON.parse(JSON.stringify(source)))
this.saveSources(Array.from(map.values()))
},
// 更改当前编辑的源qq
changeCurrentSource(source: Source) {
this.currentSource = JSON.parse(JSON.stringify(source))
},
// update editTab tabName and editTab info
changeTabName(tabName: string) {
this.currentTab = tabName
localStorage.setItem('tabName', tabName)
},
changeEditTabSource(source: Source) {
this.editTabSource = JSON.parse(JSON.stringify(source))
},
editHistory(history: Source) {
let historyObj
if (localStorage.getItem('history')) {
historyObj = JSON.parse(localStorage.getItem('history')!)
historyObj.new.push(history)
if (historyObj.new.length > 50) {
historyObj.new.shift()
}
if (historyObj.old.length > 50) {
historyObj.old.shift()
}
localStorage.setItem('history', JSON.stringify(historyObj))
} else {
const arr = { new: [history], old: [] }
localStorage.setItem('history', JSON.stringify(arr))
}
},
editHistoryUndo() {
if (localStorage.getItem('history')) {
const historyObj = JSON.parse(localStorage.getItem('history')!)
historyObj.old.push(this.currentSource)
if (historyObj.new.length) {
this.currentSource = historyObj.new.pop()
}
localStorage.setItem('history', JSON.stringify(historyObj))
}
},
clearAllHistory() {
localStorage.setItem('history', JSON.stringify({ new: [], old: [] }))
},
clearEdit() {
this.editTabSource = {} as Source
this.currentSource = JSON.parse(JSON.stringify(emptySource)) //复制一份新对象
},
// clear all source
clearAllSource() {
this.bookSources = []
this.rssSources = []
this.savedSources = []
},
},
})
+35 -24
View File
@@ -1,41 +1,52 @@
import { Source } from '../source'
import type { BookSoure, RssSource, Source } from '../source'
import { isNullOrBlank } from './utils'
const isBookSource = (source: Source): source is BookSoure =>
'bookSourceName' in source
const isNullOrBlank = (string: string | null | undefined | number) => string == null || (string as string).length === 0 || /^\s+$/.test(string as string)
const isBookSource = (source: Source) => "bookSourceName" in source
export const isInvaildSource: (source: Source) => boolean = (source) => {
export const isInvaildSource: (source: Source) => boolean = source => {
if (isBookSource(source)) {
return !isNullOrBlank(source.bookSourceName) &&
return (
!isNullOrBlank(source.bookSourceName) &&
!isNullOrBlank(source.bookSourceUrl) &&
!isNullOrBlank(source.bookSourceType)
)
}
return !isNullOrBlank(source.sourceName) &&
!isNullOrBlank(source.sourceUrl)
return !isNullOrBlank(source.sourceName) && !isNullOrBlank(source.sourceUrl)
}
export const getSourceUniqueKey = (source: Source) => isBookSource(source) ? source.bookSourceUrl : source.sourceUrl;
export const getSourceUniqueKey = (source: Source) =>
isBookSource(source) ? source.bookSourceUrl : source.sourceUrl
export const getSourceName = (source: Source) =>
isBookSource(source) ? source.bookSourceName : source.sourceName
export const isSourceMatches: (source: Source, searchKey: string) => boolean = (source, searchKey) => {
export const isSourceMatches: (source: Source, searchKey: string) => boolean = (
source,
searchKey,
) => {
// TODO: 正则和普通字符串识别 识别 * . \ [ ] <= <! != = ?: () \d\w\s\...
if (isBookSource(source)) {
return (source.bookSourceName?.includes(searchKey) ||
source.bookSourceUrl?.includes(searchKey) ||
return (
(source.bookSourceName.includes(searchKey) ||
source.bookSourceUrl.includes(searchKey) ||
source.bookSourceGroup?.includes(searchKey) ||
source.bookSourceComment?.includes(searchKey)) ?? false
source.bookSourceComment?.includes(searchKey)) ??
false
)
}
return (source.sourceName?.includes(searchKey) ||
source.sourceUrl?.includes(searchKey) ||
return (
(source.sourceName.includes(searchKey) ||
source.sourceUrl.includes(searchKey) ||
source.sourceGroup?.includes(searchKey) ||
source.sourceComment?.includes(searchKey)) ?? false
source.sourceComment?.includes(searchKey)) ??
false
)
}
export const convertSourcesToMap = (sources: Source[]): Map<string, Source> => {
const map = new Map();
sources.forEach((source) =>
map.set(getSourceUniqueKey(source), source)
);
return map;
const map = new Map()
sources.forEach(source => map.set(getSourceUniqueKey(source), source))
return map
}
export const emptyBookSource = {
@@ -44,6 +55,6 @@ export const emptyBookSource = {
ruleToc: {},
ruleContent: {},
ruleReview: {},
ruleExplore: {}
}
export const emptyRssSource = {}
ruleExplore: {},
} as BookSoure
export const emptyRssSource = {} as RssSource
-31
View File
@@ -1,31 +0,0 @@
import { formatDate } from "@vueuse/shared";
export const isLegadoUrl = (/** @type {string} */ url) =>
/,\s*\{/.test(url) ||
!(
url.startsWith("http") ||
url.startsWith("data:") ||
url.startsWith("blob:")
);
// @ts-ignore
export const dateFormat = (/** @type {number} */ t) => {
let time = new Date().getTime();
let offset = Math.floor((time - t) / 1000);
let str = "";
if (offset <= 30) {
str = "刚刚";
} else if (offset < 60) {
str = offset + "秒前";
} else if (offset < 3600) {
str = Math.floor(offset / 60) + "分钟前";
} else if (offset < 86400) {
str = Math.floor(offset / 3600) + "小时前";
} else if (offset < 2592000) {
str = Math.floor(offset / 86400) + "天前";
} else {
str = formatDate(new Date(t), "YYYY-MM-DD");
}
return str;
};
+55
View File
@@ -0,0 +1,55 @@
import { formatDate } from '@vueuse/shared'
export const isNullOrBlank = (string: string | null | undefined | number) =>
string == null ||
(string as string).length === 0 ||
/^\s+$/.test(string as string)
export const isLegadoUrl = (/** @type {string} */ url: string) =>
/,\s*\{/.test(url) ||
!(
url.startsWith('http') ||
url.startsWith('data:') ||
url.startsWith('blob:')
)
/**
* 验证输入的URL是否符合阅读后端地址规则
* @param allowedProtocols 允许的协议,默认`["https:", "http:"]`
*/
export const validatorHttpUrl = (
http_url: string | URL,
allowedProtocols: string[] = ['https:', 'http:'],
) => {
try {
const url = new URL(http_url)
const { protocol } = url
if (!allowedProtocols.includes(protocol))
throw new Error(
`Expected protocol ${allowedProtocols.join('/')}, but ${protocol}`,
)
return true
} catch {
return false
}
}
export const dateFormat = (/** @type {number} */ t: number) => {
const time = new Date().getTime()
const offset = Math.floor((time - t) / 1000)
let str = ''
if (offset <= 30) {
str = '刚刚'
} else if (offset < 60) {
str = offset + '秒前'
} else if (offset < 3600) {
str = Math.floor(offset / 60) + '分钟前'
} else if (offset < 86400) {
str = Math.floor(offset / 3600) + '小时前'
} else if (offset < 2592000) {
str = Math.floor(offset / 86400) + '天前'
} else {
str = formatDate(new Date(t), 'YYYY-MM-DD')
}
return str
}
+299 -274
View File
@@ -106,25 +106,18 @@
</div>
</template>
<script setup>
import jump from "@/plugins/jump";
import settings from "@/config/themeConfig";
import API from "@api";
import { useLoading } from "@/hooks/loading";
import { useThrottleFn } from "@vueuse/shared";
<script setup lang="ts">
import jump from '@/plugins/jump'
import settings from '@/config/themeConfig'
import API from '@api'
import { useLoading } from '@/hooks/loading'
import { useThrottleFn } from '@vueuse/shared'
import { isNullOrBlank } from '@/utils/utils'
const content = ref();
const content = ref()
// loading spinner
const { isLoading, loadingWrapper } = useLoading(content, "正在获取信息");
const store = useBookStore();
// 读取阅读配置
try {
const browerConfig = JSON.parse(localStorage.getItem("config"));
if (browerConfig != null) store.setConfig(browerConfig);
} catch {
localStorage.removeItem("config");
}
const { isLoading, loadingWrapper } = useLoading(content, '正在获取信息')
const store = useBookStore()
const {
catalog,
@@ -135,232 +128,238 @@ const {
bookProgress,
theme,
isNight,
} = storeToRefs(store);
} = storeToRefs(store)
const chapterPos = computed({
get: () => store.readingBook.chapterPos,
set: (value) => (store.readingBook.chapterPos = value),
});
set: value => (store.readingBook.chapterPos = value),
})
const chapterIndex = computed({
get: () => store.readingBook.index,
set: (value) => (store.readingBook.index = value),
});
get: () => store.readingBook.chapterIndex,
set: value => (store.readingBook.chapterIndex = value),
})
const isSeachBook = computed({
get: () => store.readingBook.isSeachBook,
set: value => (store.readingBook.isSeachBook = value),
})
// 当前阅读书籍readingBook持久化
watch(
() => store.readingBook,
book => {
// 保存localStorage
// localStorage.setItem(book.bookUrl, JSON.stringify(book));
// 最近阅读
localStorage.setItem('readingRecent', JSON.stringify(book))
//保存 sessionStorage
sessionStorage.setItem('chapterIndex', book.chapterIndex.toString())
sessionStorage.setItem('chapterPos', book.chapterPos.toString())
},
)
// 无限滚动
const infiniteLoading = computed(() => store.config.infiniteLoading);
let scrollObserver;
const loading = ref();
const infiniteLoading = computed(() => store.config.infiniteLoading)
let scrollObserver: IntersectionObserver | null
const loading = ref()
watchEffect(() => {
if (!infiniteLoading.value) {
scrollObserver?.disconnect();
scrollObserver?.disconnect()
} else {
scrollObserver?.observe(loading.value);
scrollObserver?.observe(loading.value)
}
});
})
const loadMore = () => {
let index = chapterData.value.slice(-1)[0].index;
const index = chapterData.value.slice(-1)[0].index
if (catalog.value.length - 1 > index) {
getContent(index + 1, false);
store.saveBookProgress(); // 保存的是上一章的进度,不是预载的本章进度
getContent(index + 1, false)
store.saveBookProgress() // 保存的是上一章的进度,不是预载的本章进度
}
}
};
// IntersectionObserver回调 底部加载
const onReachBottom = (entries) => {
if (isLoading.value) return;
for (let { isIntersecting } of entries) {
if (!isIntersecting) return;
loadMore();
const onReachBottom = (entries: IntersectionObserverEntry[]) => {
if (isLoading.value) return
for (const { isIntersecting } of entries) {
if (!isIntersecting) return
loadMore()
}
}
};
// 字体
const fontFamily = computed(() => {
if (store.config.font >= 0) {
return settings.fonts[store.config.font];
return settings.fonts[store.config.font]
}
return store.config.customFontName;
});
return store.config.customFontName
})
const fontSize = computed(() => {
return store.config.fontSize + "px";
});
return store.config.fontSize + 'px'
})
// 主题部分
const bodyColor = computed(() => settings.themes[theme.value].body);
const chapterColor = computed(() => settings.themes[theme.value].content);
const popupColor = computed(() => settings.themes[theme.value].popup);
const bodyColor = computed(() => settings.themes[theme.value].body)
const chapterColor = computed(() => settings.themes[theme.value].content)
const popupColor = computed(() => settings.themes[theme.value].popup)
const readWidth = computed(() => {
if (!miniInterface.value) {
return store.config.readWidth - 130 + "px";
return store.config.readWidth - 130 + 'px'
} else {
return window.innerWidth + "px";
return window.innerWidth + 'px'
}
});
})
const popupWidth = computed(() => {
if (!miniInterface.value) {
return store.config.readWidth - 33;
return store.config.readWidth - 33
} else {
return window.innerWidth - 33;
return window.innerWidth - 33
}
});
})
const bodyTheme = computed(() => {
return {
background: bodyColor.value,
};
});
}
})
const chapterTheme = computed(() => {
return {
background: chapterColor.value,
width: readWidth.value,
};
});
const showToolBar = ref(false);
}
})
const showToolBar = ref(false)
const leftBarTheme = computed(() => {
return {
background: popupColor.value,
marginLeft: miniInterface.value
? 0
: -(store.config.readWidth / 2 + 68) + "px",
display: miniInterface.value && !showToolBar.value ? "none" : "block",
};
});
: -(store.config.readWidth / 2 + 68) + 'px',
display: miniInterface.value && !showToolBar.value ? 'none' : 'block',
}
})
const rightBarTheme = computed(() => {
return {
background: popupColor.value,
marginRight: miniInterface.value
? 0
: -(store.config.readWidth / 2 + 52) + "px",
display: miniInterface.value && !showToolBar.value ? "none" : "block",
};
});
: -(store.config.readWidth / 2 + 52) + 'px',
display: miniInterface.value && !showToolBar.value ? 'none' : 'block',
}
})
/**
* pc移动端判断 最大阅读宽度修正
* 阅读宽度最小为640px 加上工具栏 68px 52px 取较大值 为 776px
*/
const onResize = () => {
store.setMiniInterface(window.innerWidth < 776);
const width = store.config.readWidth; /**包含padding */
checkPageWidth(width);
};
store.setMiniInterface(window.innerWidth < 776)
const width = store.config.readWidth /**包含padding */
checkPageWidth(width)
}
/** 判断阅读宽度是否超出页面 */
const checkPageWidth = (readWidth) => {
if (store.miniInterface) return;
if (readWidth + 2 * 68 > window.innerWidth) store.config.readWidth -= 160;
};
const checkPageWidth = (readWidth: number) => {
if (store.miniInterface) return
if (readWidth + 2 * 68 > window.innerWidth) store.config.readWidth -= 160
}
watch(
() => store.config.readWidth,
(width) => checkPageWidth(width),
);
width => checkPageWidth(width),
)
// 顶部底部跳转
const top = ref();
const bottom = ref();
const top = ref()
const bottom = ref()
const toTop = () => {
jump(top.value);
};
jump(top.value)
}
const toBottom = () => {
jump(bottom.value);
};
jump(bottom.value)
}
// 书架路由切换
const router = useRouter();
const router = useRouter()
const toShelf = () => {
router.push("/");
};
router.push('/')
}
// 获取章节内容
const chapterData = ref([]);
const noPoint = ref(true);
const getContent = (index, reloadChapter = true, chapterPos = 0) => {
const chapterData = ref<{ index: number; content: string[]; title: string }[]>(
[],
)
const noPoint = ref(true)
const getContent = (index: number, reloadChapter = true, chapterPos = 0) => {
if (reloadChapter) {
//展示进度条
store.setShowContent(false);
store.setShowContent(false)
//强制滚回顶层
jump(top.value, { duration: 0 });
jump(top.value, { duration: 0 })
//从目录,按钮切换章节时保存进度 预加载时不保存
saveReadingBookProgressToBrowser(index, chapterPos);
chapterData.value = [];
saveReadingBookProgressToBrowser(index, chapterPos)
chapterData.value = []
}
let bookUrl = sessionStorage.getItem("bookUrl");
let { title, index: chapterIndex } = catalog.value[index];
const bookUrl = store.readingBook.bookUrl
const { title, index: chapterIndex } = catalog.value[index]
loadingWrapper(
API.getBookContent(bookUrl, chapterIndex).then(
(res) => {
res => {
if (res.data.isSuccess) {
let data = res.data.data;
let content = data.split(/\n+/);
chapterData.value.push({ index, content, title });
if (reloadChapter) toChapterPos(chapterPos);
const data = res.data.data
const content = data.split(/\n+/)
chapterData.value.push({ index, content, title })
if (reloadChapter) toChapterPos(chapterPos)
} else {
ElMessage({ message: res.data.errorMsg, type: "error" });
let content = [res.data.errorMsg];
chapterData.value.push({ index, content, title });
ElMessage({ message: res.data.errorMsg, type: 'error' })
const content = [res.data.errorMsg]
chapterData.value.push({ index, content, title })
}
store.setContentLoading(true);
noPoint.value = false;
store.setShowContent(true);
store.setContentLoading(true)
noPoint.value = false
store.setShowContent(true)
if (!res.data.isSuccess) {
throw res.data;
throw res.data
}
},
(err) => {
ElMessage({ message: "获取章节内容失败", type: "error" });
let content = ["获取章节内容失败!"];
chapterData.value.push({ index, content, title });
store.setShowContent(true);
throw err;
err => {
ElMessage({ message: '获取章节内容失败', type: 'error' })
const content = ['获取章节内容失败!']
chapterData.value.push({ index, content, title })
store.setShowContent(true)
throw err
},
),
);
};
)
}
// 章节进度跳转和计算
const chapter = ref();
const chapterRef = ref();
const toChapterPos = (pos) => {
const chapter = ref()
const chapterRef = ref()
const toChapterPos = (pos: number) => {
nextTick(() => {
if (chapterRef.value.length === 1)
chapterRef.value[0].scrollToReadedLength(pos);
});
};
chapterRef.value[0].scrollToReadedLength(pos)
})
}
// 60秒保存一次进度
const saveBookProgressThrottle = useThrottleFn(
() => store.saveBookProgress(),
60000,
);
)
const onReadedLengthChange = (index, pos) => {
saveReadingBookProgressToBrowser(index, pos);
saveBookProgressThrottle();
};
const onReadedLengthChange = (index: number, pos: number) => {
saveReadingBookProgressToBrowser(index, pos)
saveBookProgressThrottle()
}
// 文档标题
watchEffect(() => {
document.title = catalog.value[chapterIndex.value]?.title || document.title;
});
document.title = catalog.value[chapterIndex.value]?.title || document.title
})
// 阅读记录保存浏览器
const saveReadingBookProgressToBrowser = (index, pos) => {
//保存localStorage
let bookUrl = sessionStorage.getItem("bookUrl");
var book = JSON.parse(localStorage.getItem(bookUrl));
book.index = index;
book.chapterPos = pos;
localStorage.setItem(bookUrl, JSON.stringify(book));
//最近阅读
book = JSON.parse(localStorage.getItem("readingRecent"));
book.chapterIndex = index;
book.chapterPos = pos;
localStorage.setItem("readingRecent", JSON.stringify(book));
//保存vuex
chapterIndex.value = index;
chapterPos.value = pos;
//保存sessionStorage
sessionStorage.setItem("chapterIndex", index);
sessionStorage.setItem("chapterPos", String(pos));
};
const saveReadingBookProgressToBrowser = (index: number, pos: number) => {
// 保存pinia
chapterIndex.value = index
chapterPos.value = pos
}
// 进度同步
// 返回导航变化 同步请求会在获取书架前完成
@@ -371,186 +370,210 @@ const saveReadingBookProgressToBrowser = (index, pos) => {
* 注意不用监听点击链接导航变化 不对Safari<14.5兼容处理
**/
const onVisibilityChange = () => {
if (document.visibilityState == "hidden") {
API.saveBookProgressWithBeacon(bookProgress.value);
const _bookProgress = bookProgress.value
if (document.visibilityState == 'hidden' && _bookProgress) {
API.saveBookProgressWithBeacon(_bookProgress)
}
}
};
// 定时同步
// 章节切换
const toNextChapter = () => {
store.setContentLoading(true);
let index = chapterIndex.value + 1;
if (typeof catalog.value[index] !== "undefined") {
store.setContentLoading(true)
const index = chapterIndex.value + 1
if (typeof catalog.value[index] !== 'undefined') {
ElMessage({
message: "下一章",
type: "info",
});
getContent(index);
store.saveBookProgress();
message: '下一章',
type: 'info',
})
getContent(index)
store.saveBookProgress()
} else {
ElMessage({
message: "本章是最后一章",
type: "error",
});
message: '本章是最后一章',
type: 'error',
})
}
}
};
const toPreChapter = () => {
store.setContentLoading(true);
let index = chapterIndex.value - 1;
if (typeof catalog.value[index] !== "undefined") {
store.setContentLoading(true)
const index = chapterIndex.value - 1
if (typeof catalog.value[index] !== 'undefined') {
ElMessage({
message: "上一章",
type: "info",
});
getContent(index);
store.saveBookProgress();
message: '上一章',
type: 'info',
})
getContent(index)
store.saveBookProgress()
} else {
ElMessage({
message: "本章是第一章",
type: "error",
});
message: '本章是第一章',
type: 'error',
})
}
}
};
let canJump = true;
let canJump = true
// 监听方向键
const handleKeyPress = (event) => {
if (!canJump) return;
const handleKeyPress = (event: KeyboardEvent) => {
if (!canJump) return
switch (event.key) {
case "ArrowLeft":
event.stopPropagation();
event.preventDefault();
toPreChapter();
break;
case "ArrowRight":
event.stopPropagation();
event.preventDefault();
toNextChapter();
break;
case "ArrowUp":
event.stopPropagation();
event.preventDefault();
case 'ArrowLeft':
event.stopPropagation()
event.preventDefault()
toPreChapter()
break
case 'ArrowRight':
event.stopPropagation()
event.preventDefault()
toNextChapter()
break
case 'ArrowUp':
event.stopPropagation()
event.preventDefault()
if (document.documentElement.scrollTop === 0) {
ElMessage.warning("已到达页面顶部");
ElMessage.warning('已到达页面顶部')
} else {
canJump = false;
canJump = false
jump(0 - document.documentElement.clientHeight + 100, {
duration: store.config.jumpDuration,
callback: () => (canJump = true),
});
})
}
break;
case "ArrowDown":
event.stopPropagation();
event.preventDefault();
break
case 'ArrowDown':
event.stopPropagation()
event.preventDefault()
if (
document.documentElement.clientHeight +
document.documentElement.scrollTop ===
document.documentElement.scrollHeight
) {
ElMessage.warning("已到达页面底部");
ElMessage.warning('已到达页面底部')
} else {
canJump = false;
canJump = false
jump(document.documentElement.clientHeight - 100, {
duration: store.config.jumpDuration,
callback: () => (canJump = true),
});
})
}
break
}
break;
}
};
// 阻止默认滚动事件
const ignoreKeyPress = (event) => {
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
event.preventDefault();
event.stopPropagation();
const ignoreKeyPress = (event: {
key: string
preventDefault: () => void
stopPropagation: () => void
}) => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault()
event.stopPropagation()
}
}
};
onMounted(() => {
//获取书籍数据
let bookUrl = sessionStorage.getItem("bookUrl");
let bookName = sessionStorage.getItem("bookName");
let bookAuthor = sessionStorage.getItem("bookAuthor");
let chapterIndex = Number(sessionStorage.getItem("chapterIndex") || 0);
let chapterPos = Number(sessionStorage.getItem("chapterPos") || 0);
var book = JSON.parse(localStorage.getItem(bookUrl));
const bookUrl = sessionStorage.getItem('bookUrl')
const name = sessionStorage.getItem('bookName')
const author = sessionStorage.getItem('bookAuthor')
const chapterIndex = Number(sessionStorage.getItem('chapterIndex') || 0)
const chapterPos = Number(sessionStorage.getItem('chapterPos') || 0)
const isSeachBook = sessionStorage.getItem('isSeachBook') === 'true'
if (isNullOrBlank(bookUrl) || isNullOrBlank(name) || isNullOrBlank(author)) {
ElMessage.warning('书籍信息为空,即将自动返回书架页面...')
return setTimeout(toShelf, 500)
}
const book: typeof store.readingBook = {
// @ts-expect-error: bookUrl name author is NON_Blank string here
bookUrl,
// @ts-expect-error: bookUrl name author is NON_Blank string here
name,
// @ts-expect-error: bookUrl name author is NON_Blank string here
author,
chapterIndex,
chapterPos,
isSeachBook,
}
/* const bookStr = localStorage.getItem(bookUrl);
if (isNullOrBlank(bookStr)) {
return setTimeout(toShelf, 500);
}
book = JSON.parse(bookStr as string);
if (
book == null ||
chapterIndex != book.index ||
chapterIndex != book.chapterIndex ||
chapterPos != book.chapterPos
) {
book = {
bookName: bookName,
bookAuthor: bookAuthor,
bookUrl: bookUrl,
index: chapterIndex,
chapterPos: chapterPos,
name: bookName!!,
author: bookAuthor!!,
bookUrl,
chapterIndex,
chapterPos,
isSeachBook
};
localStorage.setItem(bookUrl, JSON.stringify(book));
}
onResize();
window.addEventListener("resize", onResize);
} */
onResize()
window.addEventListener('resize', onResize)
loadingWrapper(
API.getChapterList(bookUrl).then(
(res) => {
API.getChapterList(bookUrl as string).then(
res => {
if (!res.data.isSuccess) {
ElMessage({ message: res.data.errorMsg, type: "error" });
setTimeout(toShelf, 500);
return;
ElMessage({ message: res.data.errorMsg, type: 'error' })
setTimeout(toShelf, 500)
return
}
let data = res.data.data;
store.setCatalog(data);
store.setReadingBook(book);
const data = res.data.data
store.setCatalog(data)
store.setReadingBook(book)
getContent(chapterIndex, true, chapterPos);
window.addEventListener("keyup", handleKeyPress);
window.addEventListener("keydown", ignoreKeyPress);
getContent(chapterIndex, true, chapterPos)
window.addEventListener('keyup', handleKeyPress)
window.addEventListener('keydown', ignoreKeyPress)
// 兼容Safari < 14
document.addEventListener("visibilitychange", onVisibilityChange);
document.addEventListener('visibilitychange', onVisibilityChange)
//监听底部加载
scrollObserver = new IntersectionObserver(onReachBottom, {
rootMargin: "-100% 0% 20% 0%",
});
infiniteLoading.value && scrollObserver.observe(loading.value);
rootMargin: '-100% 0% 20% 0%',
})
if (infiniteLoading.value === true)
scrollObserver.observe(loading.value)
//第二次点击同一本书 页面标题不会变化
document.title = null;
document.title = bookName + " | " + catalog.value[chapterIndex].title;
document.title = '...'
document.title =
(name as string) + ' | ' + catalog.value[chapterIndex].title
},
(err) => {
ElMessage({ message: "获取书籍目录失败", type: "error" });
throw err;
err => {
ElMessage({ message: '获取书籍目录失败', type: 'error' })
throw err
},
),
);
});
)
})
onUnmounted(() => {
window.removeEventListener("keyup", handleKeyPress);
window.removeEventListener("keydown", ignoreKeyPress);
window.removeEventListener("resize", onResize);
window.removeEventListener('keyup', handleKeyPress)
window.removeEventListener('keydown', ignoreKeyPress)
window.removeEventListener('resize', onResize)
// 兼容Safari < 14
document.removeEventListener("visibilitychange", onVisibilityChange);
readSettingsVisible.value = false;
popCataVisible.value = false;
scrollObserver?.disconnect();
scrollObserver = null;
});
document.removeEventListener('visibilitychange', onVisibilityChange)
readSettingsVisible.value = false
popCataVisible.value = false
scrollObserver?.disconnect()
scrollObserver = null
})
const addToBookShelfConfirm = async () => {
const bookUrl = sessionStorage.getItem("bookUrl");
const bookName = sessionStorage.getItem("bookName");
const isSeachBook = sessionStorage.getItem("isSeachBook");
const book = JSON.parse(localStorage.getItem(bookUrl));
sessionStorage.removeItem("isSeachBook");
const book = store.readingBook
// 阅读的是搜索的书籍 并未在书架
if (isSeachBook === "true") {
await ElMessageBox.confirm(`是否将《${bookName}》放入书架?`, "放入书架", {
confirmButtonText: "确认",
cancelButtonText: "否",
type: "info",
if (book.isSeachBook === true) {
await ElMessageBox.confirm(`是否将《${book.name}》放入书架?`, '放入书架', {
confirmButtonText: '确认',
cancelButtonText: '否',
type: 'info',
/*
ElMessageBox.confirm默认在触发hashChange事件时自动关闭
按下物理返回键时触发hashChange事件
@@ -560,20 +583,22 @@ const addToBookShelfConfirm = async () => {
})
.then(() => {
//选择是,无动作
isSeachBook.value = false
})
.catch(async () => {
//选择否,删除书籍
await API.deleteBook(book);
});
await API.deleteBook(book)
})
.finally(() => sessionStorage.removeItem('isSeachBook'))
}
}
};
onBeforeRouteLeave(async (to, from, next) => {
console.log("onBeforeRouteLeave");
console.log('onBeforeRouteLeave')
// 弹窗时停止响应按键翻页
window.removeEventListener("keyup", handleKeyPress);
await addToBookShelfConfirm();
next();
});
window.removeEventListener('keyup', handleKeyPress)
await addToBookShelfConfirm()
next()
})
</script>
<style lang="scss" scoped>
@@ -662,8 +687,8 @@ onBeforeRouteLeave(async (to, from, next) => {
}
.chapter {
font-family: "Microsoft YaHei", PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", sans-serif;
font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light,
'Helvetica Neue Light', sans-serif;
text-align: left;
padding: 0 65px;
min-height: 100vh;
@@ -673,8 +698,8 @@ onBeforeRouteLeave(async (to, from, next) => {
.content {
font-size: 18px;
line-height: 1.8;
font-family: "Microsoft YaHei", PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", sans-serif;
font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light,
'Helvetica Neue Light', sans-serif;
.bottom-bar,
.top-bar {
+194 -226
View File
@@ -27,7 +27,7 @@
size="large"
@click="
toDetail(
readingRecent.url,
readingRecent.bookUrl,
readingRecent.name,
readingRecent.author,
readingRecent.chapterIndex,
@@ -36,7 +36,7 @@
true,
)
"
:class="{ 'no-point': readingRecent.url == '' }"
:class="{ 'no-point': readingRecent.bookUrl == '' }"
>
{{ readingRecent.name }}
</el-tag>
@@ -78,309 +78,277 @@
</div>
</template>
<script>
import "@/assets/bookshelf.css";
import "@/assets/fonts/shelffont.css";
import { useBookStore } from "@/store";
import githubUrl from "@/assets/imgs/github.png";
import { useLoading } from "@/hooks/loading";
import { Search as SearchIcon } from "@element-plus/icons-vue";
import {baseURL_localStorage_key} from "@/api/axios"
<script setup lang="ts">
import '@/assets/bookshelf.css'
import '@/assets/fonts/shelffont.css'
import { useBookStore } from '@/store'
import githubUrl from '@/assets/imgs/github.png'
import { useLoading } from '@/hooks/loading'
import { Search as SearchIcon } from '@element-plus/icons-vue'
import { baseURL_localStorage_key } from '@/api/axios'
import API, {
legado_http_entry_point,
validatorHttpUrl,
setLeagdoHttpUrl,
} from "@api";
parseLeagdoHttpUrlWithDefault,
setApiEntryPoint,
} from '@api'
import { validatorHttpUrl } from '@/utils/utils'
import type { Book, SeachBook } from '@/book'
import type { webReadConfig } from '@/web'
export default defineComponent({
beforeRouteEnter: (to, from, next) => {
API.getReadConfig()
.then((response) => response.data)
.then(({ isSuccess, data }) => {
if (isSuccess) {
next((vm) => {
console.log("初始化加载阅读界面配置成功");
// @ts-ignore
vm.saveReadConfig(data);
});
} else {
next();
const store = useBookStore()
const isNight = computed(() => store.isNight)
/** shortcuts of `store.setConfig` */
const applyReadConfig = (config?: webReadConfig) => {
try {
if (config !== undefined) store.setConfig(config)
} catch {
ElMessage.info('阅读界面配置解析错误')
}
}
})
.catch(() => next());
},
setup: () => {
const store = useBookStore();
const isNight = computed(() => store.isNight);
const readingRecent = ref({
name: "尚无阅读记录",
author: "",
url: "",
const readingRecent = ref<typeof store.readingBook>({
name: '尚无阅读记录',
author: '',
bookUrl: '',
chapterIndex: 0,
chapterPos: 0,
isSeachBook: false,
});
const shelfWrapper = ref(null);
})
const shelfWrapper = ref<HTMLElement>()
//const shelfWrapper = useTemplateRef<HTMLElement>("shelfWrapper")
const { showLoading, closeLoading, loadingWrapper, isLoading } = useLoading(
shelfWrapper,
"正在获取书籍信息",
);
'正在获取书籍信息',
)
// 书架书籍和在线书籍搜索
const books = shallowRef([]);
const shelf = computed(() => store.shelf);
const searchWord = ref("");
const isSearching = ref(false);
const books = shallowRef<Book[] | SeachBook[]>([])
const shelf = computed(() => store.shelf)
const searchWord = ref('')
const isSearching = ref(false)
watchEffect(() => {
if (isSearching.value && searchWord.value != "") return;
isSearching.value = false;
books.value = [];
if (searchWord.value == "") {
books.value = shelf.value;
return;
if (isSearching.value && searchWord.value != '') return
isSearching.value = false
books.value = []
if (searchWord.value == '') {
books.value = shelf.value
return
}
books.value = shelf.value.filter((book) => {
books.value = shelf.value.filter(book => {
return (
book.name.includes(searchWord.value) ||
book.author.includes(searchWord.value)
);
});
});
)
})
})
//搜索在线书籍
const searchBook = () => {
if (searchWord.value == "") return;
books.value = [];
store.clearSearchBooks();
showLoading();
isSearching.value = true;
if (searchWord.value == '') return
books.value = []
store.clearSearchBooks()
showLoading()
isSearching.value = true
API.search(
searchWord.value,
(data) => {
searcBooks => {
if (isLoading) {
closeLoading();
closeLoading()
}
try {
store.setSearchBooks(JSON.parse(data));
books.value = store.searchBooks;
store.setSearchBooks(searcBooks)
books.value = store.searchBooks
//store.searchBooks.forEach((item) => books.value.push(item));
} catch (e) {
ElMessage.error("后端数据错误");
throw e;
ElMessage.error('后端数据错误')
throw e
}
},
() => {
closeLoading();
closeLoading()
if (books.value.length == 0) {
ElMessage.info("搜索结果为空");
ElMessage.info('搜索结果为空')
}
},
);
};
)
}
//连接状态
const connectStatus = computed(() => store.connectStatus);
const connectType = computed(() => store.connectType);
const newConnect = computed(() => store.newConnect);
const connectStatus = computed(() => store.connectStatus)
const connectType = computed(() => store.connectType)
const newConnect = computed(() => store.newConnect)
const setLegadoRetmoteUrl = () => {
ElMessageBox.prompt(
"请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)",
"提示",
'请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)',
'提示',
{
confirmButtonText: "确定",
cancelButtonText: "取消",
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: legado_http_entry_point,
inputValidator: (value) => {
try {
validatorHttpUrl(value);
} catch (e) {
return e?.cause?.message ?? e.message;
}
return true;
},
inputValidator: value => validatorHttpUrl(value),
inputErrorMessage: '输入的格式不对',
beforeClose: (action, instance, done) => {
if (action === "confirm") {
store.setNewConnect(true);
instance.confirmButtonLoading = true;
instance.confirmButtonText = "校验中……";
if (action === 'confirm') {
store.setNewConnect(true)
instance.confirmButtonLoading = true
instance.confirmButtonText = '校验中……'
// instance.inputValue
const url = new URL(instance.inputValue).toString();
API.testLeagdoHttpUrlConnection(url)
const url = new URL(instance.inputValue).toString()
API.getReadConfig(url)
//API.getBookShelf()
.then(function (configStr) {
saveReadConfig(configStr);
instance.confirmButtonLoading = false;
store.setConnectType("success");
store.clearSearchBooks();
store.setNewConnect(false);
setLeagdoHttpUrl(url);
.then(function (config) {
applyReadConfig(config)
instance.confirmButtonLoading = false
store.setConnectType('success')
store.clearSearchBooks()
store.setNewConnect(false)
setApiEntryPoint(...parseLeagdoHttpUrlWithDefault(url))
if (url === location.origin) {
localStorage.removeItem(baseURL_localStorage_key);
localStorage.removeItem(baseURL_localStorage_key)
} else {
localStorage.setItem(baseURL_localStorage_key, url);
localStorage.setItem(baseURL_localStorage_key, url)
}
store.setConnectStatus("已连接 " + url.toString());
fetchBookShelfData();
done();
store.setConnectStatus('已连接 ' + url.toString())
fetchBookShelfData()
done()
})
.catch(function (error) {
instance.confirmButtonLoading = false;
instance.confirmButtonText = "确定";
ElMessage.error("访问失败,请检查您输入的 url");
store.setNewConnect(false);
throw error;
});
instance.confirmButtonLoading = false
instance.confirmButtonText = '确定'
ElMessage.error('访问失败,请检查您输入的 url')
store.setNewConnect(false)
throw error
})
} else {
done();
done()
}
},
},
);
};
)
}
const router = useRouter();
const handleBookClick = async (book) => {
const router = useRouter()
const handleBookClick = async (book: SeachBook | Book) => {
// 判断是否为 searchBook
const isSeachBook = 'respondTime' in book
if (isSeachBook) {
await API.saveBook(book)
}
const {
bookUrl,
name,
author,
// @ts-expect-error: descruct with default value
durChapterIndex = 0,
// @ts-expect-error: descruct with default value
durChapterPos = 0,
} = book;
// 判断是否为 searchBook
const isSeachBook = "respondTime" in book;
if (isSeachBook) {
await API.saveBook(book);
} = book
toDetail(bookUrl, name, author, durChapterIndex, durChapterPos, isSeachBook)
}
toDetail(
bookUrl,
name,
author,
durChapterIndex,
durChapterPos,
isSeachBook,
);
};
const toDetail = (
bookUrl,
bookName,
bookAuthor,
chapterIndex,
chapterPos,
isSeachBook,
bookUrl: string,
bookName: string,
bookAuthor: string,
chapterIndex: number,
chapterPos: number,
isSeachBook: boolean | undefined = false,
fromReadRecentClick = false,
) => {
if (bookName === "尚无阅读记录") return;
if (bookName === '尚无阅读记录') return
// 最近书籍不再书架上 自动搜索
if (isSeachBook === true && fromReadRecentClick) {
searchWord.value = bookName;
searchBook();
return;
if (
fromReadRecentClick &&
shelf.value.every(book => book.bookUrl !== bookUrl)
) {
searchWord.value = bookName
searchBook()
return
}
sessionStorage.setItem("bookUrl", bookUrl);
sessionStorage.setItem("bookName", bookName);
sessionStorage.setItem("bookAuthor", bookAuthor);
sessionStorage.setItem("chapterIndex", chapterIndex);
sessionStorage.setItem("chapterPos", chapterPos);
sessionStorage.setItem("isSeachBook", String(isSeachBook));
sessionStorage.setItem('bookUrl', bookUrl)
sessionStorage.setItem('bookName', bookName)
sessionStorage.setItem('bookAuthor', bookAuthor)
sessionStorage.setItem('chapterIndex', String(chapterIndex))
sessionStorage.setItem('chapterPos', String(chapterPos))
sessionStorage.setItem('isSeachBook', String(isSeachBook))
readingRecent.value = {
name: bookName,
author: bookAuthor,
url: bookUrl,
chapterIndex: chapterIndex,
chapterPos: chapterPos,
bookUrl,
chapterIndex,
chapterPos,
isSeachBook,
};
localStorage.setItem(
"readingRecent",
JSON.stringify(readingRecent.value),
);
router.push({
path: "/chapter",
});
};
const loadShelf = () => {
loadingWrapper(
store
.saveBookProgress()
//确保各种网络情况下同步请求先完成
.finally(fetchBookShelfData),
);
};
const saveReadConfig = (configStr) => {
try {
store.setConfig(JSON.parse(configStr));
} catch {
ElMessage.info("阅读界面配置解析错误");
}
};
localStorage.setItem('readingRecent', JSON.stringify(readingRecent.value))
router.push({
path: '/chapter',
})
}
const loadShelf = async () => {
try {
if (store.configInited === false) {
const config = await API.getReadConfig()
applyReadConfig(config)
} else {
}
await store.saveBookProgress()
//确保各种网络情况下同步请求先完成
await fetchBookShelfData()
} catch (error: unknown) {
store.setConnectType('danger')
store.setConnectStatus('连接异常')
store.setNewConnect(false)
throw error
}
}
const fetchBookShelfData = () => {
return API.getBookShelf().then((response) => {
store.setConnectType("success");
return API.getBookShelf().then(response => {
store.setConnectType('success')
if (response.data.isSuccess) {
//store.increaseBookNum(response.data.data.length);
store.addBooks(
response.data.data.sort(function (a, b) {
var x = a["durChapterTime"] || 0;
var y = b["durChapterTime"] || 0;
return y - x;
const x = a['durChapterTime'] || 0
const y = b['durChapterTime'] || 0
return y - x
}),
);
)
} else {
ElMessage.error(response.data.errorMsg ?? "后端返回格式错误!");
if (
response.data.errorMsg.includes('还没有添加小说') &&
shelf.value.length > 0
) {
ElNotification.warning({
title: '提示',
message: '当前书架上的书籍已经被删除',
position: 'bottom-right',
})
return store.clearBooks()
}
ElMessage.error(response.data.errorMsg ?? '后端返回格式错误!')
}
store.setConnectStatus('已连接 ' + legado_http_entry_point)
store.setNewConnect(false)
})
}
store.setConnectStatus("已连接 " + legado_http_entry_point);
store.setNewConnect(false);
});
};
onMounted(() => {
//获取最近阅读书籍
let readingRecentStr = localStorage.getItem("readingRecent");
const readingRecentStr = localStorage.getItem('readingRecent')
if (readingRecentStr != null) {
readingRecent.value = JSON.parse(readingRecentStr);
if (typeof readingRecent.value.chapterIndex == "undefined") {
readingRecent.value.chapterIndex = 0;
readingRecent.value = JSON.parse(readingRecentStr)
if (typeof readingRecent.value.chapterIndex == 'undefined') {
readingRecent.value.chapterIndex = 0
}
}
console.log("bookshelf mounted");
API.testLeagdoHttpUrlConnection()
//.then(saveReadConfig) 应该在组件挂载前读取阅读配置
.then(loadShelf)
.catch(function (error) {
store.setConnectType("danger");
store.setConnectStatus("连接异常");
ElMessage.error(
"后端连接失败异常,请检查阅读WEB服务或者设置其它可用链接",
);
store.setNewConnect(false);
throw error;
});
});
return {
setLegadoRetmoteUrl,
isNight,
connectStatus,
connectType,
newConnect,
saveReadConfig, //expose it so beforeRouteEnter next can access it
readingRecent,
searchBook,
books,
handleBookClick,
toDetail,
isSearching,
SearchIcon,
githubUrl,
searchWord,
};
},
});
console.log('bookshelf mounted')
loadingWrapper(loadShelf())
})
</script>
<style lang="scss" scoped>
<style scoped>
.index-wrapper {
height: 100%;
width: 100%;
@@ -438,12 +406,12 @@ export default defineComponent({
.recent-book {
font-size: 10px;
// font-weight: 400;
/* // font-weight: 400;
// margin: 12px 0;
// font-weight: 500;
// color: #6B7C87;
// color: #6B7C87; */
cursor: pointer;
// padding: 6px 18px;
/* // padding: 6px 18px; */
}
}
}
@@ -464,7 +432,7 @@ export default defineComponent({
.setting-connect {
font-size: 8px;
margin-top: 16px;
// color: #6B7C87;
/* // color: #6B7C87; */
cursor: pointer;
}
}
@@ -538,7 +506,7 @@ export default defineComponent({
}
.night {
:deep(.navigation-wrapper) {
.navigation-wrapper {
background-color: #454545;
.navigation-title {
+12 -11
View File
@@ -5,22 +5,23 @@
<source-tab-tools class="right" />
</div>
</template>
<script setup>
import bookSourceConfig from "@/config/bookSourceEditConfig";
import rssSourceConfig from "@/config/rssSourceEditConfig";
import "@/assets/sourceeditor.css";
import { useDark } from "@vueuse/core";
<script setup lang="ts">
import bookSourceConfig from '@/config/bookSourceEditConfig'
import rssSourceConfig from '@/config/rssSourceEditConfig'
import '@/assets/sourceeditor.css'
import { useDark } from '@vueuse/core'
import type { SourceConfig } from '@/config/sourceConfig'
useDark();
useDark()
let config;
let config: SourceConfig
if (/bookSource/i.test(location.href)) {
config = bookSourceConfig;
document.title = "书源管理";
config = bookSourceConfig as SourceConfig
document.title = '书源管理'
} else {
config = rssSourceConfig;
document.title = "订阅源管理";
config = rssSourceConfig as SourceConfig
document.title = '订阅源管理'
}
</script>
<style lang="scss" scoped>
+14
View File
@@ -0,0 +1,14 @@
export type webReadConfig = {
theme: number
font: number
fontSize: number
readWidth: number
infiniteLoading: boolean
customFontName: string
jumpDuration: number
spacing: {
paragraph: number
line: number
letter: number
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@api": ["./src/api"],
"@utils/*": ["./src/utils/*"]
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
}
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}
@@ -8,16 +8,16 @@ import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/
export default ({ mode }) =>
defineConfig({
export default defineConfig(({ mode }) => {
return {
plugins: [
vue(),
AutoImport({
imports: ["vue", "vue-router", "pinia"],
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
dirs: ["src/components", "src/store"],
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/],
dirs: ["src/components", "src/store", "*.d.ts"],
eslintrc: {
enabled: true,
//enabled: true,
},
resolvers: [
ElementPlusResolver(),
@@ -72,4 +72,5 @@ export default ({ mode }) =>
},
},
},
}
});