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 # Logs
node_modules logs
/dist *.log
/doc
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files # Editor directories and files
.vim .vscode/*
!.vscode/extensions.json
.idea .idea
.vscode
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
/package-lock.json
/yarn.lock *.tsbuildinfo
pnpm-lock.yaml
+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) | | ![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提供后端服务 > 需要阅读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,
]
+12 -12
View File
@@ -1,13 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh" class=""> <html lang="zh" class="">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./src/main.js"></script> <script type="module" src="./src/main.ts"></script>
</body> </body>
</html> </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": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build && node scripts/sync.js", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "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": { "dependencies": {
"@element-plus/icons-svg": "^2.3.1", "@element-plus/icons-svg": "^2.3.1",
@@ -19,24 +22,31 @@
"@vueuse/core": "^11.1.0", "@vueuse/core": "^11.1.0",
"@vueuse/shared": "^11.1.0", "@vueuse/shared": "^11.1.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"element-plus": "^2.8.4", "element-plus": "^2.8.5",
"hotkeys-js": "^3.13.7", "hotkeys-js": "^3.13.7",
"pinia": "^2.2.4", "pinia": "^2.2.4",
"vue": "^3.5.11", "vue": "^3.5.12",
"vue-router": "^4.4.5", "vue-router": "^4.4.5",
"vue3-virtual-scroll-list": "^0.2.1" "vue3-virtual-scroll-list": "^0.2.1"
}, },
"devDependencies": { "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", "@vitejs/plugin-vue": "^5.1.4",
"eslint": "^8.57.1", "@vue/eslint-config-prettier": "^10.0.0",
"eslint-config-prettier": "^9.1.0", "@vue/eslint-config-typescript": "^14.0.1",
"eslint-plugin-prettier": "^5.2.1", "@vue/tsconfig": "^0.5.1",
"eslint-plugin-vue": "^9.28.0", "eslint": "^9.12.0",
"eslint-plugin-vue": "^9.29.0",
"npm-run-all2": "^6.2.3",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"sass": "^1.79.4", "typescript": "~5.5.4",
"unplugin-auto-import": "^0.18.3", "unplugin-auto-import": "^0.18.3",
"unplugin-icons": "^0.19.3", "unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4", "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服务接口的键值 */ /** @type {string} localStorage保存自定义阅读http服务接口的键值 */
export const baseURL_localStorage_key = "remoteUrl" export const baseURL_localStorage_key = 'remoteUrl'
const SECOND = 1000; const SECOND = 1000
const ajax = axios.create({ const ajax = axios.create({
baseURL: baseURL:
@@ -10,6 +10,6 @@ const ajax = axios.create({
localStorage.getItem(baseURL_localStorage_key) || localStorage.getItem(baseURL_localStorage_key) ||
location.origin, location.origin,
timeout: 120 * SECOND, 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'
+10 -10
View File
@@ -1,14 +1,14 @@
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
height: 100vh; height: 100vh;
} }
#app { #app {
font-family: "Avenir", Helvetica, Arial, sans-serif; font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
color: #2c3e50; color: #2c3e50;
margin: 0; margin: 0;
height: 100%; height: 100%;
} }
+9 -7
View File
@@ -1,7 +1,9 @@
code { code {
border-radius: 4px; border-radius: 4px;
padding: .15rem .5rem; padding: 0.15rem 0.5rem;
background-color: var(--el-fill-color-light); background-color: var(--el-fill-color-light);
transition: color .25s, background-color .5s; transition:
font-size: 14px; color 0.25s,
} background-color 0.5s;
font-size: 14px;
}
+2 -2
View File
@@ -1,5 +1,5 @@
@charset "UTF-8"; @charset "UTF-8";
@font-face { @font-face {
font-family: "iconfont"; font-family: 'iconfont';
src: url("./iconfont.woff") format("woff"); src: url('./iconfont.woff') format('woff');
} }
+2 -2
View File
@@ -1,7 +1,7 @@
@charset "UTF-8"; @charset "UTF-8";
@font-face { @font-face {
font-family: "FZZCYSK"; font-family: 'FZZCYSK';
src: local("☺"), url("./popfont.ttf"); src: local('☺'), url('./popfont.ttf');
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
} }
+2 -2
View File
@@ -1,7 +1,7 @@
@charset "UTF-8"; @charset "UTF-8";
@font-face { @font-face {
font-family: "FZZCYSK"; font-family: 'FZZCYSK';
src: local("☺"), url("./shelffont.ttf"); src: local('☺'), url('./shelffont.ttf');
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
} }
+12 -9
View File
@@ -1,9 +1,12 @@
kbd { kbd {
align-items: center; align-items: center;
background: rgba(125, 125, 125, .1); background: rgba(125, 125, 125, 0.1);
border-radius: 3px; border-radius: 3px;
border: 0; border: 0;
padding: 4px 5px; padding: 4px 5px;
font-weight: bold; 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);
}
+13 -13
View File
@@ -1,13 +1,13 @@
@import './kbd.css'; @import './kbd.css';
@import './code.css'; @import './code.css';
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.el-tabs__header { .el-tabs__header {
position: sticky; position: sticky;
top: 0px; top: 0px;
z-index: 2; z-index: 2;
} }
+95 -79
View File
@@ -6,88 +6,104 @@
// biome-ignore lint: disable // biome-ignore lint: disable
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: (typeof import('vue'))['EffectScope']
const ElMessage: typeof import('element-plus/es')['ElMessage'] const ElMessage: (typeof import('element-plus/es'))['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox'] const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const ElNotification: (typeof import('element-plus/es'))['ElNotification']
const computed: typeof import('vue')['computed'] const acceptHMRUpdate: (typeof import('pinia'))['acceptHMRUpdate']
const createApp: typeof import('vue')['createApp'] const computed: (typeof import('vue'))['computed']
const createPinia: typeof import('pinia')['createPinia'] const createApp: (typeof import('vue'))['createApp']
const customRef: typeof import('vue')['customRef'] const createPinia: (typeof import('pinia'))['createPinia']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] const customRef: (typeof import('vue'))['customRef']
const defineComponent: typeof import('vue')['defineComponent'] const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
const defineStore: typeof import('pinia')['defineStore'] const defineComponent: (typeof import('vue'))['defineComponent']
const effectScope: typeof import('vue')['effectScope'] const defineStore: (typeof import('pinia'))['defineStore']
const getActivePinia: typeof import('pinia')['getActivePinia'] const effectScope: (typeof import('vue'))['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getActivePinia: (typeof import('pinia'))['getActivePinia']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
const h: typeof import('vue')['h'] const getCurrentScope: (typeof import('vue'))['getCurrentScope']
const inject: typeof import('vue')['inject'] const h: (typeof import('vue'))['h']
const isProxy: typeof import('vue')['isProxy'] const inject: (typeof import('vue'))['inject']
const isReactive: typeof import('vue')['isReactive'] const isProxy: (typeof import('vue'))['isProxy']
const isReadonly: typeof import('vue')['isReadonly'] const isReactive: (typeof import('vue'))['isReactive']
const isRef: typeof import('vue')['isRef'] const isReadonly: (typeof import('vue'))['isReadonly']
const mapActions: typeof import('pinia')['mapActions'] const isRef: (typeof import('vue'))['isRef']
const mapGetters: typeof import('pinia')['mapGetters'] const mapActions: (typeof import('pinia'))['mapActions']
const mapState: typeof import('pinia')['mapState'] const mapGetters: (typeof import('pinia'))['mapGetters']
const mapStores: typeof import('pinia')['mapStores'] const mapState: (typeof import('pinia'))['mapState']
const mapWritableState: typeof import('pinia')['mapWritableState'] const mapStores: (typeof import('pinia'))['mapStores']
const markRaw: typeof import('vue')['markRaw'] const mapWritableState: (typeof import('pinia'))['mapWritableState']
const nextTick: typeof import('vue')['nextTick'] const markRaw: (typeof import('vue'))['markRaw']
const onActivated: typeof import('vue')['onActivated'] const nextTick: (typeof import('vue'))['nextTick']
const onBeforeMount: typeof import('vue')['onBeforeMount'] const onActivated: (typeof import('vue'))['onActivated']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] const onBeforeMount: (typeof import('vue'))['onBeforeMount']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
const onDeactivated: typeof import('vue')['onDeactivated'] const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
const onErrorCaptured: typeof import('vue')['onErrorCaptured'] const onDeactivated: (typeof import('vue'))['onDeactivated']
const onMounted: typeof import('vue')['onMounted'] const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
const onRenderTracked: typeof import('vue')['onRenderTracked'] const onMounted: (typeof import('vue'))['onMounted']
const onRenderTriggered: typeof import('vue')['onRenderTriggered'] const onRenderTracked: (typeof import('vue'))['onRenderTracked']
const onScopeDispose: typeof import('vue')['onScopeDispose'] const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
const onServerPrefetch: typeof import('vue')['onServerPrefetch'] const onScopeDispose: (typeof import('vue'))['onScopeDispose']
const onUnmounted: typeof import('vue')['onUnmounted'] const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
const onUpdated: typeof import('vue')['onUpdated'] const onUnmounted: (typeof import('vue'))['onUnmounted']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] const onUpdated: (typeof import('vue'))['onUpdated']
const provide: typeof import('vue')['provide'] const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
const reactive: typeof import('vue')['reactive'] const provide: (typeof import('vue'))['provide']
const readonly: typeof import('vue')['readonly'] const reactive: (typeof import('vue'))['reactive']
const ref: typeof import('vue')['ref'] const readonly: (typeof import('vue'))['readonly']
const resolveComponent: typeof import('vue')['resolveComponent'] const ref: (typeof import('vue'))['ref']
const setActivePinia: typeof import('pinia')['setActivePinia'] const resolveComponent: (typeof import('vue'))['resolveComponent']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const setActivePinia: (typeof import('pinia'))['setActivePinia']
const shallowReactive: typeof import('vue')['shallowReactive'] const setMapStoreSuffix: (typeof import('pinia'))['setMapStoreSuffix']
const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowReactive: (typeof import('vue'))['shallowReactive']
const shallowRef: typeof import('vue')['shallowRef'] const shallowReadonly: (typeof import('vue'))['shallowReadonly']
const store: typeof import('./store/index.js')['default'] const shallowRef: (typeof import('vue'))['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs'] const store: (typeof import('./store/index'))['default']
const toRaw: typeof import('vue')['toRaw'] const storeToRefs: (typeof import('pinia'))['storeToRefs']
const toRef: typeof import('vue')['toRef'] const toRaw: (typeof import('vue'))['toRaw']
const toRefs: typeof import('vue')['toRefs'] const toRef: (typeof import('vue'))['toRef']
const toValue: typeof import('vue')['toValue'] const toRefs: (typeof import('vue'))['toRefs']
const triggerRef: typeof import('vue')['triggerRef'] const toValue: (typeof import('vue'))['toValue']
const unref: typeof import('vue')['unref'] const triggerRef: (typeof import('vue'))['triggerRef']
const useAttrs: typeof import('vue')['useAttrs'] const unref: (typeof import('vue'))['unref']
const useBookStore: typeof import('./store/bookStore.js')['useBookStore'] const useAttrs: (typeof import('vue'))['useAttrs']
const useCssModule: typeof import('vue')['useCssModule'] const useBookStore: (typeof import('./store/bookStore'))['useBookStore']
const useCssVars: typeof import('vue')['useCssVars'] const useCssModule: (typeof import('vue'))['useCssModule']
const useId: typeof import('vue')['useId'] const useCssVars: (typeof import('vue'))['useCssVars']
const useLink: typeof import('vue-router')['useLink'] const useId: (typeof import('vue'))['useId']
const useModel: typeof import('vue')['useModel'] const useLink: (typeof import('vue-router'))['useLink']
const useRoute: typeof import('vue-router')['useRoute'] const useModel: (typeof import('vue'))['useModel']
const useRouter: typeof import('vue-router')['useRouter'] const useRoute: (typeof import('vue-router'))['useRoute']
const useSlots: typeof import('vue')['useSlots'] const useRouter: (typeof import('vue-router'))['useRouter']
const useSourceStore: typeof import('./store/sourceStore.js')['useSourceStore'] const useSlots: (typeof import('vue'))['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef'] const useSourceStore: (typeof import('./store/sourceStore'))['useSourceStore']
const watch: typeof import('vue')['watch'] const useTemplateRef: (typeof import('vue'))['useTemplateRef']
const watchEffect: typeof import('vue')['watchEffect'] const watch: (typeof import('vue'))['watch']
const watchPostEffect: typeof import('vue')['watchPostEffect'] const watchEffect: (typeof import('vue'))['watchEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect'] const watchPostEffect: (typeof import('vue'))['watchPostEffect']
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
} }
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @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') 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"> <div class="cover-img">
<img <img
class="cover" class="cover"
:src="getCover(book.coverUrl)" :src="getCover(book)"
:key="book.coverUrl" :key="book.coverUrl"
@error.once="proxyImage" @error.once="proxyImage"
alt="" alt=""
@@ -33,15 +33,17 @@
</div> </div>
<div class="update-info" v-if="!isSearch"> <div class="update-info" v-if="!isSearch">
<div class="dot"></div> <div class="dot"></div>
<div class="size">{{ book.totalChapterNum }}</div> <div class="size">{{ (book as Book).totalChapterNum }}</div>
<div class="dot"></div> <div class="dot"></div>
<div class="date">{{ dateFormat(book.lastCheckTime) }}</div> <div class="date">
{{ dateFormat((book as Book).lastCheckTime) }}
</div>
</div> </div>
</div> </div>
<div class="intro" v-if="isSearch">{{ book.intro }}</div> <div class="intro" v-if="isSearch">{{ book.intro }}</div>
<div class="dur-chapter" v-if="!isSearch"> <div class="dur-chapter" v-if="!isSearch">
已读{{ book.durChapterTitle }} 已读{{ (book as Book).durChapterTitle }}
</div> </div>
<div class="last-chapter">最新{{ book.latestChapterTitle }}</div> <div class="last-chapter">最新{{ book.latestChapterTitle }}</div>
</div> </div>
@@ -49,26 +51,32 @@
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { dateFormat, isLegadoUrl } from "../utils/utils"; import type { Book, SeachBook } from '@/book'
import API from "@api"; 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 emit = defineEmits(["bookClick"]); const handleClick = (book: Book | SeachBook) => emit('bookClick', book)
const handleClick = (book) => emit("bookClick", book); const getCover = ({ bookUrl, coverUrl }: Book | SeachBook) => {
const getCover = (coverUrl) => { if (coverUrl === undefined) return API.getProxyCoverUrl(bookUrl)
return isLegadoUrl(coverUrl) ? API.getProxyCoverUrl(coverUrl) : coverUrl; return isLegadoUrl(coverUrl) ? API.getProxyCoverUrl(coverUrl) : coverUrl
}; }
const proxyImage = (event) => { const proxyImage = (evt: Event) => {
event.target.src = API.getProxyCoverUrl(event.target.src); const target = evt.target as HTMLImageElement
}; target.src = API.getProxyCoverUrl(target.src)
}
const subJustify = computed(() => const subJustify = computed(() =>
props.isSearch ? "space-between" : "flex-start", props.isSearch ? 'space-between' : 'flex-start',
); )
</script> </script>
<style lang="scss" scoped> <style scoped>
.books-wrapper { .books-wrapper {
overflow: auto; overflow: auto;
@@ -119,7 +127,7 @@ const subJustify = computed(() =>
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: baseline;
justify-content: v-bind("subJustify"); justify-content: v-bind('subJustify');
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: #6b6b6b; color: #6b6b6b;
@@ -149,6 +157,7 @@ const subJustify = computed(() =>
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
line-clamp: 1;
text-align: left; text-align: left;
} }
} }
+17 -13
View File
@@ -11,25 +11,29 @@
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
const props = defineProps([ import type { BookChapter } from '@/book'
"index",
"source",
"gotoChapter",
"currentChapterIndex",
]);
const isSelected = (idx) => { const props = defineProps<{
return idx == props.currentChapterIndex; index: number
}; source: BookChapter | { index: number; catas: BookChapter[] }
gotoChapter: (chapter: BookChapter) => void
currentChapterIndex: number
}>()
const isSelected = (idx: number) => {
return idx == props.currentChapterIndex
}
// PC端 一个虚拟列表中有两个章节 // PC端 一个虚拟列表中有两个章节
const catas = computed(() => { 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> </script>
<style lang="scss" scoped> <style scoped>
.selected { .selected {
color: #eb4259; color: #eb4259;
} }
+80 -73
View File
@@ -17,116 +17,123 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { isLegadoUrl } from "@/utils/utils"; import { isLegadoUrl } from '@/utils/utils'
import API from "@api"; import API from '@api'
import jump from "@/plugins/jump"; import jump from '@/plugins/jump'
import type { webReadConfig } from '@/web'
const props = defineProps({ const store = useBookStore()
chapterIndex: { type: Number, required: true }, const readWidth = computed(() => store.config.readWidth)
contents: { type: Array, required: true }, const bookUrl = computed(() => store.readingBook.bookUrl)
title: { type: String, required: true }, const props = defineProps<{
spacing: { type: Object, required: true }, chapterIndex: number
fontFamily: { type: String, required: true }, contents: Array<string>
fontSize: { type: String, required: true }, title: string
}); spacing: webReadConfig['spacing']
fontFamily: string
fontSize: string
}>()
const getImageSrc = (content) => { const getImageSrc = (content: string) => {
const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/; const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/
const src = content.match(imgPattern)[1]; const src = content.match(imgPattern)![1] //reg tested in template
if (isLegadoUrl(src)) if (isLegadoUrl(src))
return API.getProxyImageUrl(src, useBookStore().config.readWidth); return API.getProxyImageUrl(
return src; bookUrl.value,
}; src,
const proxyImage = (event) => { useBookStore().config.readWidth,
event.target.src = API.getProxyImageUrl( )
event.target.src, return src
useBookStore().config.readWidth, }
); const proxyImage = (event: Event) => {
}; ;(event.target as HTMLImageElement).src = API.getProxyImageUrl(
bookUrl.value,
(event.target as HTMLImageElement).src,
readWidth.value,
)
}
const calculateWordCount = (paragraph) => { const calculateWordCount = (paragraph: string) => {
const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g; const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g
//内嵌图片文字为1 //内嵌图片文字为1
const imagePlaceHolder = " "; const imagePlaceHolder = ' '
return paragraph.replaceAll(imgPattern, imagePlaceHolder).length; return paragraph.replaceAll(imgPattern, imagePlaceHolder).length
}; }
const chapterPos = computed(() => { const chapterPos = computed(() => {
let pos = -1; let pos = -1
return Array.from(props.contents, (content) => { return Array.from(props.contents, content => {
pos += calculateWordCount(content) + 1; //计算上一段的换行符 pos += calculateWordCount(content) + 1 //计算上一段的换行符
return pos; return pos
}); })
}); })
const titleRef = ref(); const titleRef = ref<HTMLElement>()
const paragraphRef = ref(); const paragraphRef = ref<HTMLParagraphElement[]>()
const scrollToReadedLength = (length) => { const scrollToReadedLength = (length: number) => {
if (length === 0) return; if (length === 0) return
let paragraphIndex = chapterPos.value.findIndex( const paragraphIndex = chapterPos.value.findIndex(
(wordCount) => wordCount >= length, wordCount => wordCount >= length,
); )
if (paragraphIndex === -1) return; if (paragraphIndex === -1) return
nextTick(() => { nextTick(() => {
jump(paragraphRef.value[paragraphIndex], { jump(paragraphRef.value![paragraphIndex], {
duration: 0, duration: 0,
}); })
}); })
}; }
defineExpose({ defineExpose({
scrollToReadedLength, scrollToReadedLength,
}); })
let intersectionObserver = null; let intersectionObserver: IntersectionObserver | null = null
const emit = defineEmits(["readedLengthChange"]); const emit = defineEmits(['readedLengthChange'])
onMounted(() => { onMounted(() => {
intersectionObserver = new IntersectionObserver( intersectionObserver = new IntersectionObserver(
(entries) => { entries => {
for (let { target, isIntersecting } of entries) { for (const { target, isIntersecting } of entries) {
if (isIntersecting) { if (isIntersecting) {
emit( emit(
"readedLengthChange", 'readedLengthChange',
props.chapterIndex, props.chapterIndex,
// @ts-ignore parseInt((target as HTMLElement).dataset.chapterpos as string),
parseInt(target.dataset.chapterpos), )
);
} }
} }
}, },
{ {
rootMargin: `0px 0px -${window.innerHeight - 24}px 0px`, rootMargin: `0px 0px -${window.innerHeight - 24}px 0px`,
}, },
); )
intersectionObserver.observe(titleRef.value); intersectionObserver.observe(titleRef.value!)
paragraphRef.value.forEach((element) => { paragraphRef.value!.forEach(element => {
intersectionObserver.observe(element); intersectionObserver!.observe(element)
}); })
}); })
onUnmounted(() => { onUnmounted(() => {
intersectionObserver?.disconnect(); intersectionObserver?.disconnect()
intersectionObserver = null; intersectionObserver = null
}); })
</script> </script>
<style lang="scss" scoped> <style scoped>
.title { .title {
margin-bottom: 57px; margin-bottom: 57px;
font: font:
24px / 32px PingFangSC-Regular, 24px / 32px PingFangSC-Regular,
HelveticaNeue-Light, HelveticaNeue-Light,
"Helvetica Neue Light", 'Helvetica Neue Light',
"Microsoft YaHei", 'Microsoft YaHei',
sans-serif; sans-serif;
} }
p { p {
display: block; display: block;
word-wrap: break-word; word-wrap: break-word;
// word-break: break-all; /* word-break: break-all; */
letter-spacing: calc(v-bind('props.spacing.letter') * 1em);
letter-spacing: calc(v-bind("props.spacing.letter") * 1em); line-height: calc(1 + v-bind('props.spacing.line'));
line-height: calc(1 + v-bind("props.spacing.line")); margin: calc(v-bind('props.spacing.paragraph') * 1em) 0;
margin: calc(v-bind("props.spacing.paragraph") * 1em) 0;
:deep(img) { :deep(img) {
height: 1em; height: 1em;
+49 -45
View File
@@ -19,81 +19,85 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import VirtualList from "vue3-virtual-scroll-list"; import VirtualList from 'vue3-virtual-scroll-list'
import settings from "../config/themeConfig"; import settings from '../config/themeConfig'
import "../assets/fonts/popfont.css"; import '../assets/fonts/popfont.css'
import CatalogItem from "./CatalogItem.vue"; 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 isNight = computed(() => store.theme)
const theme = computed(() => store.theme); const theme = computed(() => store.theme)
const popupTheme = computed(() => { const popupTheme = computed(() => {
return { return {
background: settings.themes[theme.value].popup, background: settings.themes[theme.value].popup,
}; }
}); })
//虚拟列表 数据源 //虚拟列表 数据源
const virtualListdata = computed(() => { const virtualListdata = computed(() => {
let catalogValue = catalog.value; const catalogValue = catalog.value
if (miniInterface.value) return catalogValue; if (miniInterface.value) return catalogValue
// pc端 virtualListIitem有2个章节 // pc端 virtualListIitem有2个章节
let length = Math.ceil(catalogValue.length / 2); const length = Math.ceil(catalogValue.length / 2)
let virtualListDataSource = new Array(length); const virtualListDataSource = new Array<{
index: number
catas: BookChapter[]
}>(length)
let i = 0; let i = 0
while (i < length) { while (i < length) {
virtualListDataSource[i] = { virtualListDataSource[i] = {
index: i, index: i,
catas: catalogValue.slice(2 * i, 2 * i + 2), catas: catalogValue.slice(2 * i, 2 * i + 2),
}; }
i++; i++
} }
return virtualListDataSource; return virtualListDataSource
}); })
//打开目录 计算当前章节对应的虚拟列表位置 //打开目录 计算当前章节对应的虚拟列表位置
const virtualListRef = ref(); const virtualListRef = ref()
const currentChapterIndex = computed({ const currentChapterIndex = computed({
get: () => store.readingBook.index, get: () => store.readingBook.chapterIndex,
set: (value) => (store.readingBook.index = value), set: value => (store.readingBook.chapterIndex = value),
}); })
const virtualListIndex = computed(() => { const virtualListIndex = computed(() => {
let index = currentChapterIndex.value; const index = currentChapterIndex.value
if (miniInterface.value) return index; if (miniInterface.value) return index
// pc端 virtualListIitem有2个章节 // pc端 virtualListIitem有2个章节
return Math.floor(index / 2); return Math.floor(index / 2)
}); })
onUpdated(() => { onUpdated(() => {
// dom更新触发ResizeObserver,更新虚拟列表内部的sizes Map // dom更新触发ResizeObserver,更新虚拟列表内部的sizes Map
if (!popCataVisible.value) return; if (!popCataVisible.value) return
virtualListRef.value.scrollToIndex(virtualListIndex.value); virtualListRef.value.scrollToIndex(virtualListIndex.value)
}); })
// 点击加载对应章节内容 // 点击加载对应章节内容
const emit = defineEmits(["getContent"]); const emit = defineEmits(['getContent'])
const gotoChapter = (note) => { const gotoChapter = (chapter: BookChapter) => {
const chapterIndex = catalog.value.indexOf(note); const chapterIndex = catalog.value.indexOf(chapter)
currentChapterIndex.value = chapterIndex; currentChapterIndex.value = chapterIndex
store.setPopCataVisible(false); store.setPopCataVisible(false)
store.setContentLoading(true); store.setContentLoading(true)
store.saveBookProgress(); store.saveBookProgress()
emit("getContent", chapterIndex); emit('getContent', chapterIndex)
}; }
</script> </script>
<style lang="scss" scoped> <style scoped>
.cata-wrapper { .cata-wrapper {
margin: -16px; margin: -16px;
padding: 18px 0 24px 25px; 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 { .title {
font-size: 18px; font-size: 18px;
font-weight: 400; font-weight: 400;
@@ -105,14 +109,14 @@ const gotoChapter = (note) => {
} }
:deep(.data-wrapper) { :deep(.data-wrapper) {
.cata { .cata {
//width: 50%; /*width: 50%;*/
height: 40px; height: 40px;
cursor: pointer; cursor: pointer;
font: font:
16px / 40px PingFangSC-Regular, 16px / 40px PingFangSC-Regular,
HelveticaNeue-Light, HelveticaNeue-Light,
"Helvetica Neue Light", 'Helvetica Neue Light',
"Microsoft YaHei", 'Microsoft YaHei',
sans-serif; sans-serif;
} }
} }
+116 -130
View File
@@ -62,19 +62,10 @@
<el-button <el-button
type="primary" type="primary"
size="small" size="small"
@click=" @click="setCustomFont(), (customFontSavePopVisible = false)"
setCustomFont();
customFontSavePopVisible = false;
"
>确定</el-button >确定</el-button
> >
<el-button <el-button type="primary" size="small" @click="loadFontFromURL()"
type="primary"
size="small"
@click="
loadFontFromURL();
customFontSavePopVisible = false;
"
>网络下载</el-button >网络下载</el-button
> >
</div> </div>
@@ -183,197 +174,192 @@
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import "../assets/fonts/popfont.css"; import '../assets/fonts/popfont.css'
import "../assets/fonts/iconfont.css"; import '../assets/fonts/iconfont.css'
import settings from "../config/themeConfig"; import settings from '../config/themeConfig'
import API from "@api"; import API from '@api'
import { useDebounceFn } from '@vueuse/shared'
const store = useBookStore();
const store = useBookStore()
const saveConfigDebounce = useDebounceFn(
() => API.saveReadConfig(store.config),
500,
)
//阅读界面设置改变时保存同步配置 //阅读界面设置改变时保存同步配置
let configChanged = false;
watch( watch(
() => store.config, () => store.config,
(newValue) => { () => {
localStorage.setItem("config", JSON.stringify(newValue)); saveConfigDebounce()
configChanged = true;
}, },
{ {
deep: 2, //深度为2 deep: 2, //深度为2
}, },
); )
// 设置页面关闭时同步设置到阅读APP
watch(
() => store.readSettingsVisible,
(visbile) => {
if (!visbile && configChanged)
API.saveReadConfig(store.config).then(() => (configChanged = false));
},
);
//主题颜色 //主题颜色
const theme = computed(() => store.theme); const theme = computed(() => store.theme)
const isNight = computed(() => store.isNight); const isNight = computed(() => store.isNight)
const moonIcon = computed(() => (theme.value == 6 ? "" : "")); const moonIcon = computed(() => (theme.value == 6 ? '' : ''))
const themeColors = [ 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(() => { const popupTheme = computed(() => {
return { return {
background: settings.themes[theme.value].popup, background: settings.themes[theme.value].popup,
}; }
}); })
const setTheme = (theme) => { const setTheme = (theme: number) => {
store.config.theme = theme; store.config.theme = theme
}; }
//预置字体 //预置字体
const fonts = ref(["雅黑", "宋体", "楷书"]); const fonts = ref(['雅黑', '宋体', '楷书'])
const setFont = (font) => { const setFont = (font: number) => {
store.config.font = font; store.config.font = font
}; }
const selectedFont = computed(() => { const selectedFont = computed(() => {
return store.config.font; return store.config.font
}); })
//自定义字体 //自定义字体
const customFontName = ref(store.config.customFontName); const customFontName = ref(store.config.customFontName)
const customFontSavePopVisible = ref(false); const customFontSavePopVisible = ref(false)
const setCustomFont = () => { const setCustomFont = () => {
store.config.font = -1; customFontSavePopVisible.value = false
store.config.customFontName = customFontName.value; store.config.font = -1
}; store.config.customFontName = customFontName.value
}
// 加载网络字体 // 加载网络字体
const loadFontFromURL = () => { const loadFontFromURL = () => {
ElMessageBox.prompt("请输入 字体网络链接", "提示", { customFontSavePopVisible.value = false
confirmButtonText: "确定", ElMessageBox.prompt('请输入 字体网络链接', '提示', {
cancelButtonText: "取消", confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^https?:.+$/, inputPattern: /^https?:.+$/,
inputErrorMessage: "url 形式不正确", inputErrorMessage: 'url 形式不正确',
beforeClose: (action, instance, done) => { beforeClose: (action, instance, done) => {
if (action === "confirm") { if (action === 'confirm') {
instance.confirmButtonLoading = true; instance.confirmButtonLoading = true
instance.confirmButtonText = "下载中……"; instance.confirmButtonText = '下载中……'
// instance.inputValue // instance.inputValue
const url = instance.inputValue; const url = instance.inputValue
if (typeof FontFace !== "function") { if (typeof FontFace !== 'function') {
ElMessage.error("浏览器不支持FontFace"); ElMessage.error('浏览器不支持FontFace')
return done(); return done()
} }
const fontface = new FontFace(customFontName.value, `url("${url}")`); const fontface = new FontFace(customFontName.value, `url("${url}")`)
//@ts-ignore document.fonts.add(fontface)
document.fonts.add(fontface);
fontface fontface
.load() .load()
//API.getBookShelf() //API.getBookShelf()
.then(function () { .then(function () {
instance.confirmButtonLoading = false; instance.confirmButtonLoading = false
ElMessage.info("字体加载成功!"); ElMessage.info('字体加载成功!')
setCustomFont(); setCustomFont()
done(); done()
}) })
.catch(function (error) { .catch(function (error) {
instance.confirmButtonLoading = false; instance.confirmButtonLoading = false
instance.confirmButtonText = "确定"; instance.confirmButtonText = '确定'
ElMessage.error("下载失败,请检查您输入的 url"); ElMessage.error('下载失败,请检查您输入的 url')
throw error; throw error
}); })
} else { } else {
done(); done()
} }
}, },
}); })
}; }
//字体大小 //字体大小
const fontSize = computed(() => { const fontSize = computed(() => {
return store.config.fontSize; return store.config.fontSize
}); })
const moreFontSize = () => { const moreFontSize = () => {
if (store.config.fontSize < 48) store.config.fontSize += 2; if (store.config.fontSize < 48) store.config.fontSize += 2
}; }
const lessFontSize = () => { const lessFontSize = () => {
if (store.config.fontSize > 12) store.config.fontSize -= 2; if (store.config.fontSize > 12) store.config.fontSize -= 2
}; }
//字 行 段落间距 //字 行 段落间距
const spacing = computed(() => { const spacing = computed(() => {
return store.config.spacing; return store.config.spacing
}); })
const lessLetterSpacing = () => { const lessLetterSpacing = () => {
store.config.spacing.letter -= 0.01; store.config.spacing.letter -= 0.01
}; }
const moreLetterSpacing = () => { const moreLetterSpacing = () => {
store.config.spacing.letter += 0.01; store.config.spacing.letter += 0.01
}; }
const lessLineSpacing = () => { const lessLineSpacing = () => {
store.config.spacing.line -= 0.1; store.config.spacing.line -= 0.1
}; }
const moreLineSpacing = () => { const moreLineSpacing = () => {
store.config.spacing.line += 0.1; store.config.spacing.line += 0.1
}; }
const lessParagraphSpacing = () => { const lessParagraphSpacing = () => {
store.config.spacing.paragraph -= 0.1; store.config.spacing.paragraph -= 0.1
}; }
const moreParagraphSpacing = () => { const moreParagraphSpacing = () => {
store.config.spacing.paragraph += 0.1; store.config.spacing.paragraph += 0.1
}; }
//页面宽度 //页面宽度
const readWidth = computed(() => { const readWidth = computed(() => {
return store.config.readWidth; return store.config.readWidth
}); })
const moreReadWidth = () => { const moreReadWidth = () => {
// 此时会截断页面 // 此时会截断页面
if (store.config.readWidth + 160 + 2 * 68 > window.innerWidth) return; if (store.config.readWidth + 160 + 2 * 68 > window.innerWidth) return
store.config.readWidth += 160; store.config.readWidth += 160
}; }
const lessReadWidth = () => { const lessReadWidth = () => {
if (store.config.readWidth > 640) store.config.readWidth -= 160; if (store.config.readWidth > 640) store.config.readWidth -= 160
}; }
//翻页速度 //翻页速度
const jumpDuration = computed(() => { const jumpDuration = computed(() => {
return store.config.jumpDuration; return store.config.jumpDuration
}); })
const moreJumpDuration = () => { const moreJumpDuration = () => {
store.config.jumpDuration += 100; store.config.jumpDuration += 100
}; }
const lessJumpDuration = () => { const lessJumpDuration = () => {
if (store.config.jumpDuration === 0) return; if (store.config.jumpDuration === 0) return
store.config.jumpDuration -= 100; store.config.jumpDuration -= 100
}; }
//无限加载 //无限加载
const infiniteLoading = computed(() => { const infiniteLoading = computed(() => {
return store.config.infiniteLoading; return store.config.infiniteLoading
}); })
const setInfiniteLoading = (loading) => { const setInfiniteLoading = (loading: boolean) => {
store.config.infiniteLoading = loading; store.config.infiniteLoading = loading
}; }
</script> </script>
<style lang="scss" scoped> <style scoped>
:deep(.iconfont) { :deep(.iconfont) {
font-family: iconfont; font-family: iconfont;
font-style: normal; font-style: normal;
@@ -387,11 +373,11 @@ const setInfiniteLoading = (loading) => {
.settings-wrapper { .settings-wrapper {
user-select: none; user-select: none;
margin: -13px; margin: -13px;
// width: 478px; /* width: 478px;
// height: 350px; height: 350px; */
text-align: left; text-align: left;
padding: 40px 0 40px 24px; 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 { .settings-title {
font-size: 18px; font-size: 18px;
@@ -416,7 +402,7 @@ const setInfiniteLoading = (loading) => {
i { i {
font: font:
12px / 16px PingFangSC-Regular, 12px / 16px PingFangSC-Regular,
"-apple-system", '-apple-system',
Simsun; Simsun;
display: inline-block; display: inline-block;
min-width: 48px; min-width: 48px;
@@ -468,8 +454,8 @@ const setInfiniteLoading = (loading) => {
font: font:
14px / 34px PingFangSC-Regular, 14px / 34px PingFangSC-Regular,
HelveticaNeue-Light, HelveticaNeue-Light,
"Helvetica Neue Light", 'Helvetica Neue Light',
"Microsoft YaHei", 'Microsoft YaHei',
sans-serif; sans-serif;
} }
.font-item-input { .font-item-input {
+22 -22
View File
@@ -18,49 +18,49 @@
/> />
</template> </template>
<script setup> <script setup lang="ts">
import API from "@api"; import API from '@api'
import { Search } from "@element-plus/icons-vue"; import { Search } from '@element-plus/icons-vue'
const store = useSourceStore(); const store = useSourceStore()
const printDebug = ref(""); const printDebug = ref('')
const searchKey = ref(""); const searchKey = ref('')
watch( watch(
() => store.isDebuging, () => store.isDebuging,
() => { () => {
if (store.isDebuging) startDebug(); if (store.isDebuging) startDebug()
}, },
); )
const appendDebugMsg = (msg) => { const appendDebugMsg = (msg: string) => {
let debugDom = document.querySelector("#debug-text"); const debugDom = document.querySelector('#debug-text')
debugDom.scrollTop = debugDom.scrollHeight; debugDom!.scrollTop = debugDom!.scrollHeight
printDebug.value += msg + "\n"; printDebug.value += msg + '\n'
}; }
const startDebug = async () => { const startDebug = async () => {
printDebug.value = ""; printDebug.value = ''
try { try {
await API.saveSource(store.currentSource); await API.saveSource(store.currentSource)
} catch (e) { } catch (e) {
store.debugFinish(); store.debugFinish()
throw e; throw e
} }
API.debug( API.debug(
store.currentSourceUrl, store.currentSourceUrl,
searchKey.value || store.searchKey, searchKey.value || store.searchKey,
appendDebugMsg, appendDebugMsg,
store.debugFinish, store.debugFinish,
); )
}; }
const isBookSource = computed(() => { const isBookSource = computed(() => {
return /bookSource/i.test(window.location.href); return /bookSource/i.test(window.location.href)
}); })
</script> </script>
<style lang="scss" scoped> <style scoped>
:deep(#debug-text) { :deep(#debug-text) {
height: calc(100vh - 45px - 36px - 5px); height: calc(100vh - 45px - 36px - 5px);
} }
+3 -3
View File
@@ -1,5 +1,5 @@
<script setup> <script setup lang="ts">
import { Link } from "@element-plus/icons-vue"; import { Link } from '@element-plus/icons-vue'
</script> </script>
<template> <template>
<el-link :icon="Link" href="/help/#appHelp" target="_blank" <el-link :icon="Link" href="/help/#appHelp" target="_blank"
@@ -53,7 +53,7 @@ import { Link } from "@element-plus/icons-vue";
</div> </div>
</template> </template>
<style lang="scss" scoped> <style scoped>
.el-link { .el-link {
padding: 4px; padding: 4px;
} }
+19 -16
View File
@@ -8,32 +8,35 @@
edit: sourceUrl == currentSourceUrl, edit: sourceUrl == currentSourceUrl,
}" }"
> >
{{ source.bookSourceName || source.sourceName }} {{ getSourceName(source) }}
<el-button text :icon="Edit" @click="handleSourceClick(source)" /> <el-button text :icon="Edit" @click="handleSourceClick(source)" />
</el-checkbox> </el-checkbox>
</template> </template>
<script setup> <script setup lang="ts">
import { Edit } from "@element-plus/icons-vue"; import { Edit } from '@element-plus/icons-vue'
import { getSourceUniqueKey } from "@/utils/souce"; 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 currentSourceUrl = computed(() => store.currentSourceUrl)
const sourceUrl = computed(() => getSourceUniqueKey(props.source)); const sourceUrl = computed(() => getSourceUniqueKey(props.source))
const handleSourceClick = (source) => { const handleSourceClick = (source: Source) => {
store.changeCurrentSource(source); store.changeCurrentSource(source)
}; }
const isSaveError = computed(() => { const isSaveError = computed(() => {
const map = store.savedSourcesMap; const map = store.savedSourcesMap
if (map.size == 0) return false; if (map.size == 0) return false
return !map.has(sourceUrl.value); return !map.has(sourceUrl.value)
}); })
</script> </script>
<style lang="scss" scoped> <style scoped>
:deep(.el-checkbox__label) { :deep(.el-checkbox__label) {
flex: 1; flex: 1;
display: flex; display: flex;
+14 -14
View File
@@ -9,30 +9,30 @@
style="margin-bottom: 4px" style="margin-bottom: 4px"
></el-input> ></el-input>
</template> </template>
<script setup> <script setup lang="ts">
import { useSourceStore } from "@/store"; import { useSourceStore } from '@/store'
const store = useSourceStore(); const store = useSourceStore()
const sourceString = ref(""); const sourceString = ref('')
const update = async (string) => { const update = async (string: string) => {
try { try {
store.changeEditTabSource(JSON.parse(string)); store.changeEditTabSource(JSON.parse(string))
} catch { } catch {
ElMessage({ ElMessage({
message: "粘贴的源格式错误", message: '粘贴的源格式错误',
type: "error", type: 'error',
}); })
} }
}; }
watchEffect(async () => { watchEffect(async () => {
let source = store.editTabSource; const source = store.editTabSource
if (Object.keys(source).length > 0) { if (Object.keys(source).length > 0) {
sourceString.value = JSON.stringify(source, null, 4); sourceString.value = JSON.stringify(source, null, 4)
} else { } else {
sourceString.value = ""; sourceString.value = ''
} }
}); })
</script> </script>
<style scoped> <style scoped>
:deep(.el-input) { :deep(.el-input) {
+75 -81
View File
@@ -32,7 +32,7 @@
<el-checkbox-group id="source-list" v-model="sourceUrlSelect"> <el-checkbox-group id="source-list" v-model="sourceUrlSelect">
<virtual-list <virtual-list
style="height: 100%; overflow-y: auto; overflow-x: hidden" 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-sources="sourcesFiltered"
:data-component="SourceItem" :data-component="SourceItem"
:estimate-size="45" :estimate-size="45"
@@ -40,116 +40,110 @@
</el-checkbox-group> </el-checkbox-group>
</template> </template>
<script setup> <script setup lang="ts">
import API from "@api"; import API from '@api'
import { Folder, Delete, Download, Search } from "@element-plus/icons-vue"; import { Folder, Delete, Download, Search } from '@element-plus/icons-vue'
import { import {
isSourceMatches, isSourceMatches,
getSourceUniqueKey, getSourceUniqueKey,
getSourceName,
convertSourcesToMap, convertSourcesToMap,
} from "@utils/souce"; } from '@utils/souce'
import VirtualList from "vue3-virtual-scroll-list"; import VirtualList from 'vue3-virtual-scroll-list'
import SourceItem from "./SourceItem.vue"; import SourceItem from './SourceItem.vue'
import type { Source } from '@/source'
const store = useSourceStore(); const store = useSourceStore()
const sourceUrlSelect = ref([]); const sourceUrlSelect = ref<string[]>([])
const searchKey = ref(""); const searchKey = ref('')
const sources = computed(() => store.sources); const sources = computed(() => store.sources)
// 筛选源 /* 筛选源 */
/** @type Ref<import('@/source').Source[]> */ const sourcesFiltered = computed<Source[]>(() => {
const sourcesFiltered = computed(() => { const key = searchKey.value
const key = searchKey.value; if (key === '') return sources.value
if (key === "") return sources.value; return sources.value.filter(source => isSourceMatches(source, key))
return ( })
sources.value
// @ts-ignore
.filter((source) => isSourceMatches(source, key))
);
});
// 计算当前筛选关键词下的选中源 // 计算当前筛选关键词下的选中源
/** @type Ref<import('@/source').Source[]> */ const sourceSelect = computed<Source[]>(() => {
const sourceSelect = computed(() => { const urls = sourceUrlSelect.value
const urls = sourceUrlSelect.value; if (urls.length == 0) return []
if (urls.length == 0) return [];
const sourcesFilteredMap = const sourcesFilteredMap =
searchKey.value == "" searchKey.value == ''
? store.sourcesMap ? store.sourcesMap
: convertSourcesToMap(sourcesFiltered.value); : convertSourcesToMap(sourcesFiltered.value)
return urls.reduce((sources, sourceUrl) => { return urls.reduce((sources, sourceUrl) => {
const source = sourcesFilteredMap.get(sourceUrl); const source = sourcesFilteredMap.get(sourceUrl)
if (source) sources.push(source); if (source) sources.push(source)
return sources; return sources
}, []); }, [] as Source[])
}); })
const deleteSelectSources = () => { const deleteSelectSources = () => {
const sourceSelectValue = sourceSelect.value; const sourceSelectValue = sourceSelect.value
API.deleteSource(sourceSelectValue).then(({ data }) => { API.deleteSource(sourceSelectValue).then(({ data }) => {
if (!data.isSuccess) return ElMessage.error(data.errorMsg); if (!data.isSuccess) return ElMessage.error(data.errorMsg)
store.deleteSources(sourceSelectValue); store.deleteSources(sourceSelectValue)
const sourceUrlSelectRawValue = toRaw(sourceUrlSelect.value); const sourceUrlSelectRawValue = toRaw(sourceUrlSelect.value)
sourceSelectValue.forEach((source) => { sourceSelectValue.forEach(source => {
const index = sourceUrlSelectRawValue.indexOf(getSourceUniqueKey(source)); const index = sourceUrlSelectRawValue.indexOf(getSourceUniqueKey(source))
if (index > -1) sourceUrlSelectRawValue.splice(index, 1); if (index > -1) sourceUrlSelectRawValue.splice(index, 1)
}); })
sourceUrlSelect.value = sourceUrlSelectRawValue; sourceUrlSelect.value = sourceUrlSelectRawValue
}); })
}; }
const clearAllSources = () => { const clearAllSources = () => {
store.clearAllSource(); store.clearAllSource()
sourceUrlSelect.value = []; sourceUrlSelect.value = []
}; }
//导入本地文件 //导入本地文件
const importSourceFile = () => { const importSourceFile = () => {
const input = document.createElement("input"); const input = document.createElement('input')
input.type = "file"; input.type = 'file'
input.accept = ".json,.txt"; input.accept = '.json,.txt'
input.addEventListener("change", (e) => { input.addEventListener('change', () => {
// @ts-ignore const files = input.files
const file = e.target.files[0]; if (files === null) {
const reader = new FileReader(); return ElMessage.info('未选择文件')
reader.readAsText(file); }
const reader = new FileReader()
reader.readAsText(files[0])
reader.onload = () => { reader.onload = () => {
try { try {
// @ts-ignore const jsonData = JSON.parse(reader.result as string)
const jsonData = JSON.parse(reader.result); store.saveSources(jsonData)
store.saveSources(jsonData); } catch (e: unknown) {
} catch { ElMessage.error('上传的源格式错误: ' + (e as Error).message)
ElMessage({
message: "上传的源格式错误",
type: "error",
});
} }
}; }
}); })
input.click(); input.click()
}; }
const isBookSource = /bookSource/i.test(window.location.href); const isBookSource = /bookSource/i.test(window.location.href)
const outExport = () => { const outExport = () => {
const exportFile = document.createElement("a"); const exportFile = document.createElement('a')
let sources = const sources =
sourceUrlSelect.value.length === 0 sourceUrlSelect.value.length === 0
? sourcesFiltered.value ? sourcesFiltered.value
: sourceSelect.value, : sourceSelect.value,
sourceType = isBookSource ? "BookSource" : "RssSource"; sourceType = isBookSource ? 'BookSource' : 'RssSource'
exportFile.download = `${sourceType}_${Date() exportFile.download = `${sourceType}_${Date()
.replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, "$2$1$3") .replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3')
.replace(/:/g, "")}.json`; .replace(/:/g, '')}.json`
let myBlob = new Blob([JSON.stringify(sources, null, 4)], { const myBlob = new Blob([JSON.stringify(sources, null, 4)], {
type: "application/json", type: 'application/json',
}); })
exportFile.href = window.URL.createObjectURL(myBlob); exportFile.href = window.URL.createObjectURL(myBlob)
exportFile.click(); exportFile.click()
window.URL.revokeObjectURL(exportFile.href); //avoid memory leak window.URL.revokeObjectURL(exportFile.href) //avoid memory leak
}; }
</script> </script>
<style lang="scss" scoped> <style scoped>
.tool { .tool {
display: flex; display: flex;
margin: 4px 0; margin: 4px 0;
+19 -11
View File
@@ -14,7 +14,7 @@
id, id,
array, array,
hint, hint,
required, required = false,
} in children" } in children"
:label="title" :label="title"
:key="title" :key="title"
@@ -35,20 +35,26 @@
autosize 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 <el-input-number
v-if="type == 'Number'" v-if="(type as string) === 'Number'"
v-model="currentSource[id]" v-model="currentSource[id]"
:min="0" :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 <el-option
v-for="(name, index) in array" v-for="(optionName, index) in array"
:value="index" :value="index"
:key="name" :key="optionName"
:label="name" :label="optionName"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -57,11 +63,13 @@
</el-tabs> </el-tabs>
</template> </template>
<script setup> <script setup lang="ts">
const store = useSourceStore(); import type { SourceConfig } from '@/config/sourceConfig'
defineProps(["config"]);
const currentSource = computed(() => store.currentSource); const store = useSourceStore()
defineProps<{ config: SourceConfig }>()
const currentSource = computed(() => store.currentSource)
/* /*
修改currentSource的属性 没有直接修改本身 修改currentSource的属性 没有直接修改本身
const { currentSource } = storeToRefs(store); const { currentSource } = storeToRefs(store);
+10 -10
View File
@@ -14,22 +14,22 @@
</el-tabs> </el-tabs>
</template> </template>
<script setup> <script setup lang="ts">
import { useSourceStore } from "@/store"; import { useSourceStore } from '@/store'
const store = useSourceStore(); const store = useSourceStore()
const current_tab = computed({ const current_tab = computed({
get: () => store.currentTab, get: () => store.currentTab,
set: (val) => (store.currentTab = val), set: val => (store.currentTab = val),
}); })
const tabData = ref([ const tabData = ref([
["editTab", "编辑源"], ['editTab', '编辑源'],
["editDebug", "调试源"], ['editDebug', '调试源'],
["editList", "源列表"], ['editList', '源列表'],
["editHelp", "帮助信息"], ['editHelp', '帮助信息'],
]); ])
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+142 -145
View File
@@ -36,7 +36,7 @@
<div class="hotkeys-settings flex-column-center"> <div class="hotkeys-settings flex-column-center">
<div <div
v-for="(button, index) in buttons" v-for="(button, buttonIndex) in buttons"
:key="button.name" :key="button.name"
class="hotkeys-item flex-space-between" class="hotkeys-item flex-space-between"
> >
@@ -44,9 +44,9 @@
><el-text>{{ button.name }}</el-text></span ><el-text>{{ button.name }}</el-text></span
> >
<div class="hotkeys-item__content"> <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> <kbd>{{ key }}</kbd>
<span v-if="index + 1 < button.hotKeys.length"> <span v-if="hotKeysIndex + 1 < button.hotKeys.length">
<el-text>+</el-text> <el-text>+</el-text>
</span> </span>
</div> </div>
@@ -56,7 +56,7 @@
:disabled="recordKeyDowning" :disabled="recordKeyDowning"
text text
:icon="Edit" :icon="Edit"
@click="recordKeyDown(index)" @click="recordKeyDown(buttonIndex)"
>编辑</el-button >编辑</el-button
> >
</div> </div>
@@ -64,59 +64,59 @@
</el-dialog> </el-dialog>
</template> </template>
<script setup> <script setup lang="ts">
import API from "@api"; import API from '@api'
import { CircleCheckFilled, Edit } from "@element-plus/icons-vue"; import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
import hotkeys from "hotkeys-js"; import hotkeys from 'hotkeys-js'
import { isInvaildSource } from "../utils/souce"; import { getSourceName, isInvaildSource } from '../utils/souce'
const store = useSourceStore(); const store = useSourceStore()
const pull = () => { const pull = () => {
const loadingMsg = ElMessage({ const loadingMsg = ElMessage({
message: "加载中……", message: '加载中……',
showClose: true, showClose: true,
duration: 0, duration: 0,
}); })
API.getSources() API.getSources()
.then(({ data }) => { .then(({ data }) => {
if (data.isSuccess) { if (data.isSuccess) {
store.changeTabName("editList"); store.changeTabName('editList')
store.saveSources(data.data); store.saveSources(data.data)
ElMessage({ ElMessage({
message: `成功拉取${data.data.length}条源`, message: `成功拉取${data.data.length}条源`,
type: "success", type: 'success',
}); })
} else { } else {
ElMessage({ ElMessage({
message: data.errorMsg ?? "后端错误", message: data.errorMsg ?? '后端错误',
type: "error", type: 'error',
}); })
} }
}) })
.finally(() => loadingMsg.close()); .finally(() => loadingMsg.close())
}; }
const push = () => { const push = () => {
let sources = store.sources; const sources = store.sources
store.changeTabName("editList"); store.changeTabName('editList')
if (sources.length === 0) { if (sources.length === 0) {
return ElMessage({ return ElMessage({
message: "空空如也", message: '空空如也',
type: "info", type: 'info',
}); })
} }
ElMessage({ ElMessage({
message: "正在推送中", message: '正在推送中',
type: "info", type: 'info',
}); })
API.saveSources(sources).then(({ data }) => { API.saveSources(sources).then(({ data }) => {
if (data.isSuccess) { if (data.isSuccess) {
let okData = data.data; const okData = data.data
if (Array.isArray(okData)) { if (Array.isArray(okData)) {
let failMsg = ``; let failMsg = ``
if (sources.length > okData.length) { if (sources.length > okData.length) {
failMsg = "\n推送失败的源将用红色字体标注!"; failMsg = '\n推送失败的源将用红色字体标注!'
store.setPushReturnSources(okData); store.setPushReturnSources(okData)
} }
ElMessage({ ElMessage({
message: `批量推送源到「阅读3.0APP」\n共计: ${ message: `批量推送源到「阅读3.0APP」\n共计: ${
@@ -124,168 +124,163 @@ const push = () => {
}\n成功: ${okData.length}\n失败: ${ }\n成功: ${okData.length}\n失败: ${
sources.length - okData.length sources.length - okData.length
}${failMsg}`, }${failMsg}`,
type: "success", type: 'success',
}); })
} }
} else { } else {
ElMessage({ ElMessage({
message: `批量推送源失败!\nErrorMsg: ${data.errorMsg}`, message: `批量推送源失败!\nErrorMsg: ${data.errorMsg}`,
type: "error", type: 'error',
}); })
} }
}); })
}; }
const conver2Tab = () => { const conver2Tab = () => {
store.changeTabName("editTab"); store.changeTabName('editTab')
store.changeEditTabSource(store.currentSource); store.changeEditTabSource(store.currentSource)
}; }
const conver2Source = () => { const conver2Source = () => {
store.changeCurrentSource(store.editTabSource); store.changeCurrentSource(store.editTabSource)
}; }
const undo = () => { const undo = () => {
store.editHistoryUndo(); store.editHistoryUndo()
}; }
const clearEdit = () => { const clearEdit = () => {
store.clearEdit(); store.clearEdit()
ElMessage({ ElMessage({
message: "已清除", message: '已清除',
type: "success", type: 'success',
}); })
}; }
const redo = () => { const redo = () => {
store.clearEdit(); store.clearEdit()
store.clearAllHistory(); store.clearAllHistory()
ElMessage({ ElMessage({
message: "已清除所有历史记录", message: '已清除所有历史记录',
type: "success", type: 'success',
}); })
}; }
const saveSource = () => { const saveSource = () => {
let isBookSource = /bookSource/i.test(location.href), const source = store.currentSource
/** @type {import("@/source.js").Source} */
source = store.currentSource;
if (isInvaildSource(source)) { if (isInvaildSource(source)) {
API.saveSource(source).then(({ data }) => { API.saveSource(source).then(({ data }) => {
const sourceName = getSourceName(source)
if (data.isSuccess) { if (data.isSuccess) {
ElMessage({ ElMessage({
message: `源《${ message: `源《${sourceName}》已成功保存到「阅读3.0APP」`,
isBookSource ? source.bookSourceName : source.sourceName type: 'success',
}》已成功保存到「阅读3.0APP」`, })
type: "success",
});
//save to store //save to store
store.saveCurrentSource(); store.saveCurrentSource()
} else { } else {
ElMessage({ ElMessage({
message: `源《${ message: `源《${sourceName}》保存失败!\nErrorMsg: ${data.errorMsg}`,
isBookSource ? source.bookSourceName : source.sourceName type: 'error',
}》保存失败!\nErrorMsg: ${data.errorMsg}`, })
type: "error",
});
} }
}); })
} else { } else {
ElMessage({ ElMessage({
message: `请检查<必填>项是否全部填写`, message: `请检查<必填>项是否全部填写`,
type: "error", type: 'error',
}); })
} }
}; }
const debug = () => { const debug = () => {
store.startDebug(); store.startDebug()
}; }
const buttons = ref( const buttons = ref<{ name: string; hotKeys: string[]; action: () => void }[]>(
Array.of( Array.of(
{ name: "⇈推送源", hotKeys: [], action: push }, { name: '⇈推送源', hotKeys: [], action: push },
{ name: "⇊拉取源", hotKeys: [], action: pull }, { name: '⇊拉取源', hotKeys: [], action: pull },
{ name: "⋙生成源", hotKeys: [], action: conver2Tab }, { name: '⋙生成源', hotKeys: [], action: conver2Tab },
{ name: "⋘编辑源", hotKeys: [], action: conver2Source }, { name: '⋘编辑源', hotKeys: [], action: conver2Source },
{ name: "✗清空表单", hotKeys: [], action: clearEdit }, { name: '✗清空表单', hotKeys: [], action: clearEdit },
{ name: "↶撤销操作", hotKeys: [], action: undo }, { name: '↶撤销操作', hotKeys: [], action: undo },
{ name: "↷重做操作", hotKeys: [], action: redo }, { name: '↷重做操作', hotKeys: [], action: redo },
{ name: "⇏调试源", hotKeys: [], action: debug }, { name: '⇏调试源', hotKeys: [], action: debug },
{ name: "✓保存源", hotKeys: [], action: saveSource }, { 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 = () => { const stopRecordKeyDown = () => {
if (!recordKeyDowning.value) { if (!recordKeyDowning.value) {
hotkeysDialogVisible.value = false; hotkeysDialogVisible.value = false
} }
recordKeyDowning.value = false; recordKeyDowning.value = false
}; }
watch( watch(
hotkeysDialogVisible, hotkeysDialogVisible,
(visibale) => { visibale => {
if (!visibale) { if (!visibale) {
hotkeys.unbind("*"); hotkeys.unbind('*')
readHotkeysConfig(); readHotkeysConfig()
bindHotKeys(); bindHotKeys()
return; return
} }
readHotkeysConfig(); readHotkeysConfig()
hotkeys.unbind(); hotkeys.unbind()
/**监听按键 */ /**监听按键 */
hotkeys("*", (event) => { hotkeys('*', event => {
event.preventDefault(); event.preventDefault()
let pressedKeys = hotkeys.getPressedKeyString(); const pressedKeys = hotkeys.getPressedKeyString()
if (pressedKeys.length == 1 && pressedKeys[0] == "esc") { if (pressedKeys.length == 1 && pressedKeys[0] == 'esc') {
//单独按下esc 不录入 //单独按下esc 不录入
return; return
} }
if (recordKeyDowning.value && recordKeyDownIndex.value > -1) if (recordKeyDowning.value && recordKeyDownIndex.value > -1)
buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys; buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys
}); })
}, },
{ immediate: true }, { immediate: true },
); )
const recordKeyDown = (index) => { const recordKeyDown = (index: number) => {
recordKeyDowning.value = true; recordKeyDowning.value = true
ElMessage({ ElMessage({
message: "按ESC键或者点击空白处结束录入", message: '按ESC键或者点击空白处结束录入',
type: "info", type: 'info',
}); })
buttons.value[index].hotKeys = []; buttons.value[index].hotKeys = []
recordKeyDownIndex.value = index; recordKeyDownIndex.value = index
}; }
const saveHotKeys = () => { const saveHotKeys = () => {
const hotKeysConfig = []; const hotKeysConfig: string[][] = []
buttons.value.forEach(({ hotKeys }) => { buttons.value.forEach(({ hotKeys }) => {
hotKeysConfig.push(hotKeys); hotKeysConfig.push(hotKeys)
}); })
saveHotkeysConfig(hotKeysConfig); saveHotkeysConfig(hotKeysConfig)
hotkeysDialogVisible.value = false; hotkeysDialogVisible.value = false
}; }
const bindHotKeys = () => { const bindHotKeys = () => {
// hotkeys默认过滤INPUT SELECT TEXTAREA // hotkeys默认过滤INPUT SELECT TEXTAREA
hotkeys.filter = () => true; hotkeys.filter = () => true
buttons.value.forEach(({ hotKeys, action }) => { buttons.value.forEach(({ hotKeys, action }) => {
if (hotKeys.length == 0) return; if (hotKeys.length == 0) return
hotkeys(hotKeys.join("+"), (event) => { hotkeys(hotKeys.join('+'), event => {
event.preventDefault(); event.preventDefault()
action.call(null); action.call(null)
}); })
}); })
}; }
const saveHotkeysConfig = (config) => { const saveHotkeysConfig = (config: string[][]) => {
localStorage.setItem("legado_web_hotkeys", JSON.stringify(config)); localStorage.setItem('legado_web_hotkeys', JSON.stringify(config))
}; }
/** /**
* 读取快捷键配置 * 读取快捷键配置
@@ -293,26 +288,28 @@ const saveHotkeysConfig = (config) => {
*/ */
function readHotkeysConfig() { function readHotkeysConfig() {
try { try {
const config = JSON.parse(localStorage.getItem("legado_web_hotkeys")); const localStorageConfig = localStorage.getItem('legado_web_hotkeys')
if (!Array.isArray(config) || config.length == 0) return false; if (localStorageConfig === null) return false
buttons.value.forEach((button, index) => (button.hotKeys = config[index])); const config = JSON.parse(localStorageConfig)
return true; if (!Array.isArray(config) || config.length == 0) return false
buttons.value.forEach((button, index) => (button.hotKeys = config[index]))
return true
} catch { } catch {
ElMessage({ message: "快捷键配置错误", type: "error" }); ElMessage({ message: '快捷键配置错误', type: 'error' })
localStorage.removeItem("legado_web_hotkeys"); localStorage.removeItem('legado_web_hotkeys')
} }
return false; return false
} }
onMounted(() => { onMounted(() => {
/**读取热键配置 */ /**读取热键配置 */
if (readHotkeysConfig()) { if (readHotkeysConfig()) {
hotkeysDialogVisible.value = false; hotkeysDialogVisible.value = false
} }
}); })
</script> </script>
<style lang="scss" scoped> <style scoped>
.flex-space-between { .flex-space-between {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -337,7 +334,7 @@ onMounted(() => {
justify-content: flex-end; justify-content: flex-end;
margin-right: 1em; margin-right: 1em;
} }
&__content { .hotkeys-item__content {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex: 1; 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
+2 -2
View File
@@ -3,6 +3,6 @@
color: #b5b5b5; color: #b5b5b5;
} }
.el-loading-text { .el-loading-text {
font-weight: 500; font-weight: 500;
color: #b5b5b5 !important; color: #b5b5b5 !important;
} }
-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')
}
},
)
+2 -1
View File
@@ -29,4 +29,5 @@ pnpm build
pnpm lint:fix pnpm lint:fix
#格式化代码 #格式化代码
``` ```
- 调试的时候可以修改.env.development里面的地址连接手机端调试
- 调试的时候可以修改.env.development里面的地址连接手机端调试
+10 -10
View File
@@ -1,13 +1,13 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh" class=""> <html lang="zh" class="">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script>
</body> </body>
</html> </html>
+10 -10
View File
@@ -1,19 +1,19 @@
import { createApp } from "vue"; import { createApp } from 'vue'
import App from "@/App.vue"; import App from '@/App.vue'
import bookRouter from "@/router/bookRouter"; import bookRouter from '@/router/bookRouter'
import store from "@/store"; import store from '@/store'
import "element-plus/theme-chalk/dark/css-vars.css"; 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 夜间模式 // 同步Element PLUS 夜间模式
watch( watch(
() => useBookStore().isNight, () => useBookStore().isNight,
(isNight) => { isNight => {
if (isNight) { if (isNight) {
document.documentElement.classList.add("dark"); document.documentElement.classList.add('dark')
} else { } else {
document.documentElement.classList.remove("dark"); document.documentElement.classList.remove('dark')
} }
}, },
); )
+3 -2
View File
@@ -1,6 +1,5 @@
# legado_web_editor # legado_web_editor
## 🚧开发注意 ## 🚧开发注意
如果你想要调试项目 请修改文件`.env.development``VITE_API`为阅读web服务ip 如果你想要调试项目 请修改文件`.env.development``VITE_API`为阅读web服务ip
@@ -17,17 +16,19 @@ pnpm i
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
pnpm dev pnpm dev
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
``` ```
pnpm build pnpm build
``` ```
### Lints and fixes files ### Lints and fixes files
``` ```
pnpm lint:fix pnpm lint:fix
``` ```
+10 -10
View File
@@ -1,13 +1,13 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh" class=""> <html lang="zh" class="">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="./main.js"></script> <script type="module" src="./main.js"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -1,7 +1,7 @@
import { createApp } from "vue"; import { createApp } from 'vue'
import App from "@/App.vue"; import App from '@/App.vue'
import sourceRouter from "@/router/sourceRouter"; import sourceRouter from '@/router/sourceRouter'
import store from "@/store"; import store from '@/store'
import "element-plus/theme-chalk/dark/css-vars.css"; 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) => { const easeInOutQuad = (t, b, c, d) => {
t /= d / 2; t /= d / 2
if (t < 1) return (c / 2) * t * t + b; if (t < 1) return (c / 2) * t * t + b
t--; t--
return (-c / 2) * (t * (t - 2) - 1) + b; return (-c / 2) * (t * (t - 2) - 1) + b
}; }
const jumper = () => { const jumper = () => {
// private variable cache // private variable cache
// no variables are created during a jump, preventing memory leaks // no variables are created during a jump, preventing memory leaks
let container; // container element to be scrolled (node) let container // container element to be scrolled (node)
let element; // element to scroll to (node) let element // element to scroll to (node)
let start; // where scroll starts (px) let start // where scroll starts (px)
let stop; // where scroll stops (px) let stop // where scroll stops (px)
let offset; // adjustment from the stop position (px) let offset // adjustment from the stop position (px)
let easing; // easing function (function) let easing // easing function (function)
let a11y; // accessibility support flag (boolean) let a11y // accessibility support flag (boolean)
let distance; // distance of scroll (px) let distance // distance of scroll (px)
let duration; // scroll duration (ms) let duration // scroll duration (ms)
let timeStart; // time scroll started (ms) let timeStart // time scroll started (ms)
let timeElapsed; // time spent scrolling thus far (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 // scroll position helper
function location() { function location() {
let top = container.scrollTop || container.scrollY || container.pageYOffset; let top = container.scrollTop || container.scrollY || container.pageYOffset
top = typeof top === "undefined" ? 0 : top; top = typeof top === 'undefined' ? 0 : top
return top; return top
} }
// element offset helper // element offset helper
function top(element) { function top(element) {
const elementTop = element.getBoundingClientRect().top; const elementTop = element.getBoundingClientRect().top
const containerTop = container.getBoundingClientRect const containerTop = container.getBoundingClientRect
? container.getBoundingClientRect().top ? container.getBoundingClientRect().top
: 0; : 0
return elementTop - containerTop + start; return elementTop - containerTop + start
} }
// scrollTo helper // scrollTo helper
@@ -53,7 +53,7 @@ const jumper = () => {
function scrollTo(top) { function scrollTo(top) {
container.scrollTo container.scrollTo
? container.scrollTo(0, top) // window ? container.scrollTo(0, top) // window
: (container.scrollTop = top); // custom container : (container.scrollTop = top) // custom container
} }
// rAF loop helper // rAF loop helper
@@ -61,126 +61,126 @@ const jumper = () => {
function loop(timeCurrent) { function loop(timeCurrent) {
// store time scroll started, if not started already // store time scroll started, if not started already
if (!timeStart) { if (!timeStart) {
timeStart = timeCurrent; timeStart = timeCurrent
} }
// determine time spent scrolling so far // determine time spent scrolling so far
timeElapsed = timeCurrent - timeStart; timeElapsed = timeCurrent - timeStart
// calculate next scroll position // calculate next scroll position
next = easing(timeElapsed, start, distance, duration); next = easing(timeElapsed, start, distance, duration)
// scroll to it // scroll to it
scrollTo(next); scrollTo(next)
// check progress // check progress
timeElapsed < duration timeElapsed < duration
? requestAnimationFrame(loop) // continue scroll loop ? requestAnimationFrame(loop) // continue scroll loop
: done(); // scrolling is done : done() // scrolling is done
} }
// scroll finished helper // scroll finished helper
function done() { function done() {
// account for rAF time rounding inaccuracies // account for rAF time rounding inaccuracies
scrollTo(start + distance); scrollTo(start + distance)
// if scrolling to an element, and accessibility is enabled // if scrolling to an element, and accessibility is enabled
if (element && a11y) { if (element && a11y) {
// add tabindex indicating programmatic focus // add tabindex indicating programmatic focus
element.setAttribute("tabindex", "-1"); element.setAttribute('tabindex', '-1')
// focus the element // focus the element
element.focus(); element.focus()
} }
// if it exists, fire the callback // if it exists, fire the callback
if (typeof callback === "function") { if (typeof callback === 'function') {
callback(); callback()
} }
// reset time for next jump // reset time for next jump
timeStart = false; timeStart = false
} }
// API // API
function jump(target, options = {}) { function jump(target, options = {}) {
// resolve options, or use defaults // resolve options, or use defaults
duration = options.duration || 1000; duration = options.duration || 1000
offset = options.offset || 0; offset = options.offset || 0
callback = options.callback; // "undefined" is a suitable default, and won't be called callback = options.callback // "undefined" is a suitable default, and won't be called
easing = options.easing || easeInOutQuad; easing = options.easing || easeInOutQuad
a11y = options.a11y || false; a11y = options.a11y || false
// resolve container // resolve container
switch (typeof options.container) { switch (typeof options.container) {
case "object": case 'object':
// we assume container is an HTML element (Node) // we assume container is an HTML element (Node)
container = options.container; container = options.container
break; break
case "string": case 'string':
container = document.querySelector(options.container); container = document.querySelector(options.container)
break; break
default: default:
container = window; container = window
} }
// cache starting position // cache starting position
start = location(); start = location()
// resolve target // resolve target
switch (typeof target) { switch (typeof target) {
// scroll from current position // scroll from current position
case "number": case 'number':
element = undefined; // no element to scroll to element = undefined // no element to scroll to
a11y = false; // make sure accessibility is off a11y = false // make sure accessibility is off
stop = start + target; stop = start + target
break; break
// scroll to element (node) // scroll to element (node)
// bounding rect is relative to the viewport // bounding rect is relative to the viewport
case "object": case 'object':
element = target; element = target
stop = top(element); stop = top(element)
break; break
// scroll to element (selector) // scroll to element (selector)
// bounding rect is relative to the viewport // bounding rect is relative to the viewport
case "string": case 'string':
element = document.querySelector(target); element = document.querySelector(target)
stop = top(element); stop = top(element)
break; break
} }
// resolve scroll distance, accounting for offset // resolve scroll distance, accounting for offset
distance = stop - start + offset; distance = stop - start + offset
// resolve duration // resolve duration
switch (typeof options.duration) { switch (typeof options.duration) {
// number in ms // number in ms
case "number": case 'number':
duration = options.duration; duration = options.duration
break; break
// function passed the distance of the scroll // function passed the distance of the scroll
case "function": case 'function':
duration = options.duration(distance); duration = options.duration(distance)
break; break
} }
// start the loop // start the loop
requestAnimationFrame(loop); requestAnimationFrame(loop)
} }
// expose only the jump method // expose only the jump method
return jump; return jump
}; }
// export singleton // 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 sourceEditor from '../views/SourceEditor.vue'
import { createWebHashHistory, createRouter } from "vue-router"; import { createWebHashHistory, createRouter } from 'vue-router'
export const sourceRoutes = [ export const sourceRoutes = [
{ {
path: "/bookSource", path: '/bookSource',
name: "book-home", name: 'book-home',
component: sourceEditor, component: sourceEditor,
}, },
{ {
path: "/rssSource", path: '/rssSource',
name: "rss-home", name: 'rss-home',
component: sourceEditor, component: sourceEditor,
}, },
]; ]
const router = createRouter({ const router = createRouter({
// history: createWebHistory(process.env.BASE_URL), // history: createWebHistory(process.env.BASE_URL),
history: createWebHashHistory(), history: createWebHashHistory(),
routes: sourceRoutes, 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 */ /** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/data/entities */
interface BaseSource { type BaseSource = {
lastUpdateTime?: number | undefined /**
* 并发率
*/
concurrentRate?: string
/**
* 登录地址
*/
loginUrl?: string
/**
* 登录UI
*/
loginUi?: string
/**
* 请求头
*/
header?: string
/**
* 启用cookieJar
*/
enabledCookieJar?: boolean
/**
* js库
*/
jsLib?: string
} }
interface BookSoure extends BaseSource { type BookSoure = BaseSource & {
bookSourceUrl?: string | undefined // 地址,包括 http/https
bookSourceName?: string | undefined bookSourceUrl: string
bookSourceType?: number | undefined // 名称
bookSourceGroup?: string | undefined bookSourceName: string
bookSourceComment?: string | undefined // 分组
ruleSearch?: RuleSearch | undefined bookSourceGroup?: string
ruleBookInfo?: RuleBookInfo | undefined // 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
ruleToc?: RuleToc | undefined bookSourceType: number
ruleContent?: RuleContent | undefined // 详情页url正则
ruleReview?: RuleReview | undefined bookUrlPattern?: string
ruleExplore?: ruleExplore | undefined // 手动排序编号
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 { type RuleSearch = {
checkKeyWord?: string | undefined checkKeyWord?: string
[prop: string]: string
} }
interface RssSource extends BaseSource { /* type ExploreRule = {
sourceUrl?: string | undefined [prop:string]: string
sourceName?: string | undefined
sourceGroup?: string | undefined
sourceComment?: string | undefined
} }
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 } 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 = []
},
},
})
+49 -38
View File
@@ -1,49 +1,60 @@
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) export const isInvaildSource: (source: Source) => boolean = source => {
const isBookSource = (source: Source) => "bookSourceName" in source if (isBookSource(source)) {
return (
export const isInvaildSource: (source: Source) => boolean = (source) => { !isNullOrBlank(source.bookSourceName) &&
if (isBookSource(source)) { !isNullOrBlank(source.bookSourceUrl) &&
return !isNullOrBlank(source.bookSourceName) && !isNullOrBlank(source.bookSourceType)
!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 = (
// TODO: 正则和普通字符串识别 识别 * . \ [ ] <= <! != = ?: () \d\w\s\... source,
if (isBookSource(source)) { searchKey,
return (source.bookSourceName?.includes(searchKey) || ) => {
source.bookSourceUrl?.includes(searchKey) || // TODO: 正则和普通字符串识别 识别 * . \ [ ] <= <! != = ?: () \d\w\s\...
source.bookSourceGroup?.includes(searchKey) || if (isBookSource(source)) {
source.bookSourceComment?.includes(searchKey)) ?? false return (
} (source.bookSourceName.includes(searchKey) ||
return (source.sourceName?.includes(searchKey) || source.bookSourceUrl.includes(searchKey) ||
source.sourceUrl?.includes(searchKey) || source.bookSourceGroup?.includes(searchKey) ||
source.sourceGroup?.includes(searchKey) || source.bookSourceComment?.includes(searchKey)) ??
source.sourceComment?.includes(searchKey)) ?? false false
)
}
return (
(source.sourceName.includes(searchKey) ||
source.sourceUrl.includes(searchKey) ||
source.sourceGroup?.includes(searchKey) ||
source.sourceComment?.includes(searchKey)) ??
false
)
} }
export const convertSourcesToMap = (sources: Source[]): Map<string, Source> => { export const convertSourcesToMap = (sources: Source[]): Map<string, Source> => {
const map = new Map(); const map = new Map()
sources.forEach((source) => sources.forEach(source => map.set(getSourceUniqueKey(source), source))
map.set(getSourceUniqueKey(source), source) return map
);
return map;
} }
export const emptyBookSource = { export const emptyBookSource = {
ruleSearch: {}, ruleSearch: {},
ruleBookInfo: {}, ruleBookInfo: {},
ruleToc: {}, ruleToc: {},
ruleContent: {}, ruleContent: {},
ruleReview: {}, ruleReview: {},
ruleExplore: {} ruleExplore: {},
} } as BookSoure
export const emptyRssSource = {} 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> </div>
</template> </template>
<script setup> <script setup lang="ts">
import jump from "@/plugins/jump"; import jump from '@/plugins/jump'
import settings from "@/config/themeConfig"; import settings from '@/config/themeConfig'
import API from "@api"; import API from '@api'
import { useLoading } from "@/hooks/loading"; import { useLoading } from '@/hooks/loading'
import { useThrottleFn } from "@vueuse/shared"; import { useThrottleFn } from '@vueuse/shared'
import { isNullOrBlank } from '@/utils/utils'
const content = ref(); const content = ref()
// loading spinner // loading spinner
const { isLoading, loadingWrapper } = useLoading(content, "正在获取信息"); const { isLoading, loadingWrapper } = useLoading(content, '正在获取信息')
const store = useBookStore(); const store = useBookStore()
// 读取阅读配置
try {
const browerConfig = JSON.parse(localStorage.getItem("config"));
if (browerConfig != null) store.setConfig(browerConfig);
} catch {
localStorage.removeItem("config");
}
const { const {
catalog, catalog,
@@ -135,232 +128,238 @@ const {
bookProgress, bookProgress,
theme, theme,
isNight, isNight,
} = storeToRefs(store); } = storeToRefs(store)
const chapterPos = computed({ const chapterPos = computed({
get: () => store.readingBook.chapterPos, get: () => store.readingBook.chapterPos,
set: (value) => (store.readingBook.chapterPos = value), set: value => (store.readingBook.chapterPos = value),
}); })
const chapterIndex = computed({ const chapterIndex = computed({
get: () => store.readingBook.index, get: () => store.readingBook.chapterIndex,
set: (value) => (store.readingBook.index = value), 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); const infiniteLoading = computed(() => store.config.infiniteLoading)
let scrollObserver; let scrollObserver: IntersectionObserver | null
const loading = ref(); const loading = ref()
watchEffect(() => { watchEffect(() => {
if (!infiniteLoading.value) { if (!infiniteLoading.value) {
scrollObserver?.disconnect(); scrollObserver?.disconnect()
} else { } else {
scrollObserver?.observe(loading.value); scrollObserver?.observe(loading.value)
} }
}); })
const loadMore = () => { const loadMore = () => {
let index = chapterData.value.slice(-1)[0].index; const index = chapterData.value.slice(-1)[0].index
if (catalog.value.length - 1 > index) { if (catalog.value.length - 1 > index) {
getContent(index + 1, false); getContent(index + 1, false)
store.saveBookProgress(); // 保存的是上一章的进度,不是预载的本章进度 store.saveBookProgress() // 保存的是上一章的进度,不是预载的本章进度
} }
}; }
// IntersectionObserver回调 底部加载 // IntersectionObserver回调 底部加载
const onReachBottom = (entries) => { const onReachBottom = (entries: IntersectionObserverEntry[]) => {
if (isLoading.value) return; if (isLoading.value) return
for (let { isIntersecting } of entries) { for (const { isIntersecting } of entries) {
if (!isIntersecting) return; if (!isIntersecting) return
loadMore(); loadMore()
} }
}; }
// 字体 // 字体
const fontFamily = computed(() => { const fontFamily = computed(() => {
if (store.config.font >= 0) { 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(() => { const fontSize = computed(() => {
return store.config.fontSize + "px"; return store.config.fontSize + 'px'
}); })
// 主题部分 // 主题部分
const bodyColor = computed(() => settings.themes[theme.value].body); const bodyColor = computed(() => settings.themes[theme.value].body)
const chapterColor = computed(() => settings.themes[theme.value].content); const chapterColor = computed(() => settings.themes[theme.value].content)
const popupColor = computed(() => settings.themes[theme.value].popup); const popupColor = computed(() => settings.themes[theme.value].popup)
const readWidth = computed(() => { const readWidth = computed(() => {
if (!miniInterface.value) { if (!miniInterface.value) {
return store.config.readWidth - 130 + "px"; return store.config.readWidth - 130 + 'px'
} else { } else {
return window.innerWidth + "px"; return window.innerWidth + 'px'
} }
}); })
const popupWidth = computed(() => { const popupWidth = computed(() => {
if (!miniInterface.value) { if (!miniInterface.value) {
return store.config.readWidth - 33; return store.config.readWidth - 33
} else { } else {
return window.innerWidth - 33; return window.innerWidth - 33
} }
}); })
const bodyTheme = computed(() => { const bodyTheme = computed(() => {
return { return {
background: bodyColor.value, background: bodyColor.value,
}; }
}); })
const chapterTheme = computed(() => { const chapterTheme = computed(() => {
return { return {
background: chapterColor.value, background: chapterColor.value,
width: readWidth.value, width: readWidth.value,
}; }
}); })
const showToolBar = ref(false); const showToolBar = ref(false)
const leftBarTheme = computed(() => { const leftBarTheme = computed(() => {
return { return {
background: popupColor.value, background: popupColor.value,
marginLeft: miniInterface.value marginLeft: miniInterface.value
? 0 ? 0
: -(store.config.readWidth / 2 + 68) + "px", : -(store.config.readWidth / 2 + 68) + 'px',
display: miniInterface.value && !showToolBar.value ? "none" : "block", display: miniInterface.value && !showToolBar.value ? 'none' : 'block',
}; }
}); })
const rightBarTheme = computed(() => { const rightBarTheme = computed(() => {
return { return {
background: popupColor.value, background: popupColor.value,
marginRight: miniInterface.value marginRight: miniInterface.value
? 0 ? 0
: -(store.config.readWidth / 2 + 52) + "px", : -(store.config.readWidth / 2 + 52) + 'px',
display: miniInterface.value && !showToolBar.value ? "none" : "block", display: miniInterface.value && !showToolBar.value ? 'none' : 'block',
}; }
}); })
/** /**
* pc移动端判断 最大阅读宽度修正 * pc移动端判断 最大阅读宽度修正
* 阅读宽度最小为640px 加上工具栏 68px 52px 取较大值 为 776px * 阅读宽度最小为640px 加上工具栏 68px 52px 取较大值 为 776px
*/ */
const onResize = () => { const onResize = () => {
store.setMiniInterface(window.innerWidth < 776); store.setMiniInterface(window.innerWidth < 776)
const width = store.config.readWidth; /**包含padding */ const width = store.config.readWidth /**包含padding */
checkPageWidth(width); checkPageWidth(width)
}; }
/** 判断阅读宽度是否超出页面 */ /** 判断阅读宽度是否超出页面 */
const checkPageWidth = (readWidth) => { const checkPageWidth = (readWidth: number) => {
if (store.miniInterface) return; if (store.miniInterface) return
if (readWidth + 2 * 68 > window.innerWidth) store.config.readWidth -= 160; if (readWidth + 2 * 68 > window.innerWidth) store.config.readWidth -= 160
}; }
watch( watch(
() => store.config.readWidth, () => store.config.readWidth,
(width) => checkPageWidth(width), width => checkPageWidth(width),
); )
// 顶部底部跳转 // 顶部底部跳转
const top = ref(); const top = ref()
const bottom = ref(); const bottom = ref()
const toTop = () => { const toTop = () => {
jump(top.value); jump(top.value)
}; }
const toBottom = () => { const toBottom = () => {
jump(bottom.value); jump(bottom.value)
}; }
// 书架路由切换 // 书架路由切换
const router = useRouter(); const router = useRouter()
const toShelf = () => { const toShelf = () => {
router.push("/"); router.push('/')
}; }
// 获取章节内容 // 获取章节内容
const chapterData = ref([]); const chapterData = ref<{ index: number; content: string[]; title: string }[]>(
const noPoint = ref(true); [],
const getContent = (index, reloadChapter = true, chapterPos = 0) => { )
const noPoint = ref(true)
const getContent = (index: number, reloadChapter = true, chapterPos = 0) => {
if (reloadChapter) { if (reloadChapter) {
//展示进度条 //展示进度条
store.setShowContent(false); store.setShowContent(false)
//强制滚回顶层 //强制滚回顶层
jump(top.value, { duration: 0 }); jump(top.value, { duration: 0 })
//从目录,按钮切换章节时保存进度 预加载时不保存 //从目录,按钮切换章节时保存进度 预加载时不保存
saveReadingBookProgressToBrowser(index, chapterPos); saveReadingBookProgressToBrowser(index, chapterPos)
chapterData.value = []; chapterData.value = []
} }
let bookUrl = sessionStorage.getItem("bookUrl"); const bookUrl = store.readingBook.bookUrl
let { title, index: chapterIndex } = catalog.value[index]; const { title, index: chapterIndex } = catalog.value[index]
loadingWrapper( loadingWrapper(
API.getBookContent(bookUrl, chapterIndex).then( API.getBookContent(bookUrl, chapterIndex).then(
(res) => { res => {
if (res.data.isSuccess) { if (res.data.isSuccess) {
let data = res.data.data; const data = res.data.data
let content = data.split(/\n+/); const content = data.split(/\n+/)
chapterData.value.push({ index, content, title }); chapterData.value.push({ index, content, title })
if (reloadChapter) toChapterPos(chapterPos); if (reloadChapter) toChapterPos(chapterPos)
} else { } else {
ElMessage({ message: res.data.errorMsg, type: "error" }); ElMessage({ message: res.data.errorMsg, type: 'error' })
let content = [res.data.errorMsg]; const content = [res.data.errorMsg]
chapterData.value.push({ index, content, title }); chapterData.value.push({ index, content, title })
} }
store.setContentLoading(true); store.setContentLoading(true)
noPoint.value = false; noPoint.value = false
store.setShowContent(true); store.setShowContent(true)
if (!res.data.isSuccess) { if (!res.data.isSuccess) {
throw res.data; throw res.data
} }
}, },
(err) => { err => {
ElMessage({ message: "获取章节内容失败", type: "error" }); ElMessage({ message: '获取章节内容失败', type: 'error' })
let content = ["获取章节内容失败!"]; const content = ['获取章节内容失败!']
chapterData.value.push({ index, content, title }); chapterData.value.push({ index, content, title })
store.setShowContent(true); store.setShowContent(true)
throw err; throw err
}, },
), ),
); )
}; }
// 章节进度跳转和计算 // 章节进度跳转和计算
const chapter = ref(); const chapter = ref()
const chapterRef = ref(); const chapterRef = ref()
const toChapterPos = (pos) => { const toChapterPos = (pos: number) => {
nextTick(() => { nextTick(() => {
if (chapterRef.value.length === 1) if (chapterRef.value.length === 1)
chapterRef.value[0].scrollToReadedLength(pos); chapterRef.value[0].scrollToReadedLength(pos)
}); })
}; }
// 60秒保存一次进度 // 60秒保存一次进度
const saveBookProgressThrottle = useThrottleFn( const saveBookProgressThrottle = useThrottleFn(
() => store.saveBookProgress(), () => store.saveBookProgress(),
60000, 60000,
); )
const onReadedLengthChange = (index, pos) => { const onReadedLengthChange = (index: number, pos: number) => {
saveReadingBookProgressToBrowser(index, pos); saveReadingBookProgressToBrowser(index, pos)
saveBookProgressThrottle(); saveBookProgressThrottle()
}; }
// 文档标题 // 文档标题
watchEffect(() => { watchEffect(() => {
document.title = catalog.value[chapterIndex.value]?.title || document.title; document.title = catalog.value[chapterIndex.value]?.title || document.title
}); })
// 阅读记录保存浏览器 // 阅读记录保存浏览器
const saveReadingBookProgressToBrowser = (index, pos) => { const saveReadingBookProgressToBrowser = (index: number, pos: number) => {
//保存localStorage // 保存pinia
let bookUrl = sessionStorage.getItem("bookUrl"); chapterIndex.value = index
var book = JSON.parse(localStorage.getItem(bookUrl)); chapterPos.value = pos
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));
};
// 进度同步 // 进度同步
// 返回导航变化 同步请求会在获取书架前完成 // 返回导航变化 同步请求会在获取书架前完成
@@ -371,186 +370,210 @@ const saveReadingBookProgressToBrowser = (index, pos) => {
* 注意不用监听点击链接导航变化 不对Safari<14.5兼容处理 * 注意不用监听点击链接导航变化 不对Safari<14.5兼容处理
**/ **/
const onVisibilityChange = () => { const onVisibilityChange = () => {
if (document.visibilityState == "hidden") { const _bookProgress = bookProgress.value
API.saveBookProgressWithBeacon(bookProgress.value); if (document.visibilityState == 'hidden' && _bookProgress) {
API.saveBookProgressWithBeacon(_bookProgress)
} }
}; }
// 定时同步 // 定时同步
// 章节切换 // 章节切换
const toNextChapter = () => { const toNextChapter = () => {
store.setContentLoading(true); store.setContentLoading(true)
let index = chapterIndex.value + 1; const index = chapterIndex.value + 1
if (typeof catalog.value[index] !== "undefined") { if (typeof catalog.value[index] !== 'undefined') {
ElMessage({ ElMessage({
message: "下一章", message: '下一章',
type: "info", type: 'info',
}); })
getContent(index); getContent(index)
store.saveBookProgress(); store.saveBookProgress()
} else { } else {
ElMessage({ ElMessage({
message: "本章是最后一章", message: '本章是最后一章',
type: "error", type: 'error',
}); })
} }
}; }
const toPreChapter = () => { const toPreChapter = () => {
store.setContentLoading(true); store.setContentLoading(true)
let index = chapterIndex.value - 1; const index = chapterIndex.value - 1
if (typeof catalog.value[index] !== "undefined") { if (typeof catalog.value[index] !== 'undefined') {
ElMessage({ ElMessage({
message: "上一章", message: '上一章',
type: "info", type: 'info',
}); })
getContent(index); getContent(index)
store.saveBookProgress(); store.saveBookProgress()
} else { } else {
ElMessage({ ElMessage({
message: "本章是第一章", message: '本章是第一章',
type: "error", type: 'error',
}); })
} }
}; }
let canJump = true; let canJump = true
// 监听方向键 // 监听方向键
const handleKeyPress = (event) => { const handleKeyPress = (event: KeyboardEvent) => {
if (!canJump) return; if (!canJump) return
switch (event.key) { switch (event.key) {
case "ArrowLeft": case 'ArrowLeft':
event.stopPropagation(); event.stopPropagation()
event.preventDefault(); event.preventDefault()
toPreChapter(); toPreChapter()
break; break
case "ArrowRight": case 'ArrowRight':
event.stopPropagation(); event.stopPropagation()
event.preventDefault(); event.preventDefault()
toNextChapter(); toNextChapter()
break; break
case "ArrowUp": case 'ArrowUp':
event.stopPropagation(); event.stopPropagation()
event.preventDefault(); event.preventDefault()
if (document.documentElement.scrollTop === 0) { if (document.documentElement.scrollTop === 0) {
ElMessage.warning("已到达页面顶部"); ElMessage.warning('已到达页面顶部')
} else { } else {
canJump = false; canJump = false
jump(0 - document.documentElement.clientHeight + 100, { jump(0 - document.documentElement.clientHeight + 100, {
duration: store.config.jumpDuration, duration: store.config.jumpDuration,
callback: () => (canJump = true), callback: () => (canJump = true),
}); })
} }
break; break
case "ArrowDown": case 'ArrowDown':
event.stopPropagation(); event.stopPropagation()
event.preventDefault(); event.preventDefault()
if ( if (
document.documentElement.clientHeight + document.documentElement.clientHeight +
document.documentElement.scrollTop === document.documentElement.scrollTop ===
document.documentElement.scrollHeight document.documentElement.scrollHeight
) { ) {
ElMessage.warning("已到达页面底部"); ElMessage.warning('已到达页面底部')
} else { } else {
canJump = false; canJump = false
jump(document.documentElement.clientHeight - 100, { jump(document.documentElement.clientHeight - 100, {
duration: store.config.jumpDuration, duration: store.config.jumpDuration,
callback: () => (canJump = true), callback: () => (canJump = true),
}); })
} }
break; break
} }
}; }
// 阻止默认滚动事件 // 阻止默认滚动事件
const ignoreKeyPress = (event) => { const ignoreKeyPress = (event: {
if (event.key === "ArrowUp" || event.key === "ArrowDown") { key: string
event.preventDefault(); preventDefault: () => void
event.stopPropagation(); stopPropagation: () => void
}) => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault()
event.stopPropagation()
} }
}; }
onMounted(() => { onMounted(() => {
//获取书籍数据 //获取书籍数据
let bookUrl = sessionStorage.getItem("bookUrl"); const bookUrl = sessionStorage.getItem('bookUrl')
let bookName = sessionStorage.getItem("bookName"); const name = sessionStorage.getItem('bookName')
let bookAuthor = sessionStorage.getItem("bookAuthor"); const author = sessionStorage.getItem('bookAuthor')
let chapterIndex = Number(sessionStorage.getItem("chapterIndex") || 0); const chapterIndex = Number(sessionStorage.getItem('chapterIndex') || 0)
let chapterPos = Number(sessionStorage.getItem("chapterPos") || 0); const chapterPos = Number(sessionStorage.getItem('chapterPos') || 0)
var book = JSON.parse(localStorage.getItem(bookUrl)); 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 ( if (
book == null || book == null ||
chapterIndex != book.index || chapterIndex != book.chapterIndex ||
chapterPos != book.chapterPos chapterPos != book.chapterPos
) { ) {
book = { book = {
bookName: bookName, name: bookName!!,
bookAuthor: bookAuthor, author: bookAuthor!!,
bookUrl: bookUrl, bookUrl,
index: chapterIndex, chapterIndex,
chapterPos: chapterPos, chapterPos,
isSeachBook
}; };
localStorage.setItem(bookUrl, JSON.stringify(book)); localStorage.setItem(bookUrl, JSON.stringify(book));
} } */
onResize(); onResize()
window.addEventListener("resize", onResize); window.addEventListener('resize', onResize)
loadingWrapper( loadingWrapper(
API.getChapterList(bookUrl).then( API.getChapterList(bookUrl as string).then(
(res) => { res => {
if (!res.data.isSuccess) { if (!res.data.isSuccess) {
ElMessage({ message: res.data.errorMsg, type: "error" }); ElMessage({ message: res.data.errorMsg, type: 'error' })
setTimeout(toShelf, 500); setTimeout(toShelf, 500)
return; return
} }
let data = res.data.data; const data = res.data.data
store.setCatalog(data); store.setCatalog(data)
store.setReadingBook(book); store.setReadingBook(book)
getContent(chapterIndex, true, chapterPos); getContent(chapterIndex, true, chapterPos)
window.addEventListener("keyup", handleKeyPress); window.addEventListener('keyup', handleKeyPress)
window.addEventListener("keydown", ignoreKeyPress); window.addEventListener('keydown', ignoreKeyPress)
// 兼容Safari < 14 // 兼容Safari < 14
document.addEventListener("visibilitychange", onVisibilityChange); document.addEventListener('visibilitychange', onVisibilityChange)
//监听底部加载 //监听底部加载
scrollObserver = new IntersectionObserver(onReachBottom, { scrollObserver = new IntersectionObserver(onReachBottom, {
rootMargin: "-100% 0% 20% 0%", rootMargin: '-100% 0% 20% 0%',
}); })
infiniteLoading.value && scrollObserver.observe(loading.value); if (infiniteLoading.value === true)
scrollObserver.observe(loading.value)
//第二次点击同一本书 页面标题不会变化 //第二次点击同一本书 页面标题不会变化
document.title = null; document.title = '...'
document.title = bookName + " | " + catalog.value[chapterIndex].title; document.title =
(name as string) + ' | ' + catalog.value[chapterIndex].title
}, },
(err) => { err => {
ElMessage({ message: "获取书籍目录失败", type: "error" }); ElMessage({ message: '获取书籍目录失败', type: 'error' })
throw err; throw err
}, },
), ),
); )
}); })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("keyup", handleKeyPress); window.removeEventListener('keyup', handleKeyPress)
window.removeEventListener("keydown", ignoreKeyPress); window.removeEventListener('keydown', ignoreKeyPress)
window.removeEventListener("resize", onResize); window.removeEventListener('resize', onResize)
// 兼容Safari < 14 // 兼容Safari < 14
document.removeEventListener("visibilitychange", onVisibilityChange); document.removeEventListener('visibilitychange', onVisibilityChange)
readSettingsVisible.value = false; readSettingsVisible.value = false
popCataVisible.value = false; popCataVisible.value = false
scrollObserver?.disconnect(); scrollObserver?.disconnect()
scrollObserver = null; scrollObserver = null
}); })
const addToBookShelfConfirm = async () => { const addToBookShelfConfirm = async () => {
const bookUrl = sessionStorage.getItem("bookUrl"); const book = store.readingBook
const bookName = sessionStorage.getItem("bookName");
const isSeachBook = sessionStorage.getItem("isSeachBook");
const book = JSON.parse(localStorage.getItem(bookUrl));
sessionStorage.removeItem("isSeachBook");
// 阅读的是搜索的书籍 并未在书架 // 阅读的是搜索的书籍 并未在书架
if (isSeachBook === "true") { if (book.isSeachBook === true) {
await ElMessageBox.confirm(`是否将《${bookName}》放入书架?`, "放入书架", { await ElMessageBox.confirm(`是否将《${book.name}》放入书架?`, '放入书架', {
confirmButtonText: "确认", confirmButtonText: '确认',
cancelButtonText: "否", cancelButtonText: '否',
type: "info", type: 'info',
/* /*
ElMessageBox.confirm默认在触发hashChange事件时自动关闭 ElMessageBox.confirm默认在触发hashChange事件时自动关闭
按下物理返回键时触发hashChange事件 按下物理返回键时触发hashChange事件
@@ -560,20 +583,22 @@ const addToBookShelfConfirm = async () => {
}) })
.then(() => { .then(() => {
//选择是,无动作 //选择是,无动作
isSeachBook.value = false
}) })
.catch(async () => { .catch(async () => {
//选择否,删除书籍 //选择否,删除书籍
await API.deleteBook(book); await API.deleteBook(book)
}); })
.finally(() => sessionStorage.removeItem('isSeachBook'))
} }
}; }
onBeforeRouteLeave(async (to, from, next) => { onBeforeRouteLeave(async (to, from, next) => {
console.log("onBeforeRouteLeave"); console.log('onBeforeRouteLeave')
// 弹窗时停止响应按键翻页 // 弹窗时停止响应按键翻页
window.removeEventListener("keyup", handleKeyPress); window.removeEventListener('keyup', handleKeyPress)
await addToBookShelfConfirm(); await addToBookShelfConfirm()
next(); next()
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -662,8 +687,8 @@ onBeforeRouteLeave(async (to, from, next) => {
} }
.chapter { .chapter {
font-family: "Microsoft YaHei", PingFangSC-Regular, HelveticaNeue-Light, font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", sans-serif; 'Helvetica Neue Light', sans-serif;
text-align: left; text-align: left;
padding: 0 65px; padding: 0 65px;
min-height: 100vh; min-height: 100vh;
@@ -673,8 +698,8 @@ onBeforeRouteLeave(async (to, from, next) => {
.content { .content {
font-size: 18px; font-size: 18px;
line-height: 1.8; line-height: 1.8;
font-family: "Microsoft YaHei", PingFangSC-Regular, HelveticaNeue-Light, font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", sans-serif; 'Helvetica Neue Light', sans-serif;
.bottom-bar, .bottom-bar,
.top-bar { .top-bar {
+264 -296
View File
@@ -27,7 +27,7 @@
size="large" size="large"
@click=" @click="
toDetail( toDetail(
readingRecent.url, readingRecent.bookUrl,
readingRecent.name, readingRecent.name,
readingRecent.author, readingRecent.author,
readingRecent.chapterIndex, readingRecent.chapterIndex,
@@ -36,7 +36,7 @@
true, true,
) )
" "
:class="{ 'no-point': readingRecent.url == '' }" :class="{ 'no-point': readingRecent.bookUrl == '' }"
> >
{{ readingRecent.name }} {{ readingRecent.name }}
</el-tag> </el-tag>
@@ -78,309 +78,277 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import "@/assets/bookshelf.css"; import '@/assets/bookshelf.css'
import "@/assets/fonts/shelffont.css"; import '@/assets/fonts/shelffont.css'
import { useBookStore } from "@/store"; import { useBookStore } from '@/store'
import githubUrl from "@/assets/imgs/github.png"; import githubUrl from '@/assets/imgs/github.png'
import { useLoading } from "@/hooks/loading"; import { useLoading } from '@/hooks/loading'
import { Search as SearchIcon } from "@element-plus/icons-vue"; import { Search as SearchIcon } from '@element-plus/icons-vue'
import {baseURL_localStorage_key} from "@/api/axios" import { baseURL_localStorage_key } from '@/api/axios'
import API, { import API, {
legado_http_entry_point, legado_http_entry_point,
validatorHttpUrl, parseLeagdoHttpUrlWithDefault,
setLeagdoHttpUrl, setApiEntryPoint,
} from "@api"; } from '@api'
import { validatorHttpUrl } from '@/utils/utils'
import type { Book, SeachBook } from '@/book'
import type { webReadConfig } from '@/web'
export default defineComponent({ const store = useBookStore()
beforeRouteEnter: (to, from, next) => { const isNight = computed(() => store.isNight)
API.getReadConfig()
.then((response) => response.data)
.then(({ isSuccess, data }) => {
if (isSuccess) {
next((vm) => {
console.log("初始化加载阅读界面配置成功");
// @ts-ignore
vm.saveReadConfig(data);
});
} else {
next();
}
})
.catch(() => next());
},
setup: () => {
const store = useBookStore();
const isNight = computed(() => store.isNight);
const readingRecent = ref({ /** shortcuts of `store.setConfig` */
name: "尚无阅读记录", const applyReadConfig = (config?: webReadConfig) => {
author: "", try {
url: "", if (config !== undefined) store.setConfig(config)
chapterIndex: 0, } catch {
chapterPos: 0, ElMessage.info('阅读界面配置解析错误')
isSeachBook: false, }
}); }
const shelfWrapper = ref(null);
const { showLoading, closeLoading, loadingWrapper, isLoading } = useLoading(
shelfWrapper,
"正在获取书籍信息",
);
// 书架书籍和在线书籍搜索 const readingRecent = ref<typeof store.readingBook>({
const books = shallowRef([]); name: '尚无阅读记录',
const shelf = computed(() => store.shelf); author: '',
const searchWord = ref(""); bookUrl: '',
const isSearching = ref(false); chapterIndex: 0,
watchEffect(() => { chapterPos: 0,
if (isSearching.value && searchWord.value != "") return; isSeachBook: false,
isSearching.value = false; })
books.value = [];
if (searchWord.value == "") { const shelfWrapper = ref<HTMLElement>()
books.value = shelf.value; //const shelfWrapper = useTemplateRef<HTMLElement>("shelfWrapper")
return; const { showLoading, closeLoading, loadingWrapper, isLoading } = useLoading(
shelfWrapper,
'正在获取书籍信息',
)
// 书架书籍和在线书籍搜索
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
}
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
API.search(
searchWord.value,
searcBooks => {
if (isLoading) {
closeLoading()
} }
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;
API.search(
searchWord.value,
(data) => {
if (isLoading) {
closeLoading();
}
try {
store.setSearchBooks(JSON.parse(data));
books.value = store.searchBooks;
//store.searchBooks.forEach((item) => books.value.push(item));
} catch (e) {
ElMessage.error("后端数据错误");
throw e;
}
},
() => {
closeLoading();
if (books.value.length == 0) {
ElMessage.info("搜索结果为空");
}
},
);
};
//连接状态
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 或者通过内网穿透的地址)",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: legado_http_entry_point,
inputValidator: (value) => {
try {
validatorHttpUrl(value);
} catch (e) {
return e?.cause?.message ?? e.message;
}
return true;
},
beforeClose: (action, instance, done) => {
if (action === "confirm") {
store.setNewConnect(true);
instance.confirmButtonLoading = true;
instance.confirmButtonText = "校验中……";
// instance.inputValue
const url = new URL(instance.inputValue).toString();
API.testLeagdoHttpUrlConnection(url)
//API.getBookShelf()
.then(function (configStr) {
saveReadConfig(configStr);
instance.confirmButtonLoading = false;
store.setConnectType("success");
store.clearSearchBooks();
store.setNewConnect(false);
setLeagdoHttpUrl(url);
if (url === location.origin) {
localStorage.removeItem(baseURL_localStorage_key);
} else {
localStorage.setItem(baseURL_localStorage_key, url);
}
store.setConnectStatus("已连接 " + url.toString());
fetchBookShelfData();
done();
})
.catch(function (error) {
instance.confirmButtonLoading = false;
instance.confirmButtonText = "确定";
ElMessage.error("访问失败,请检查您输入的 url");
store.setNewConnect(false);
throw error;
});
} else {
done();
}
},
},
);
};
const router = useRouter();
const handleBookClick = async (book) => {
const {
bookUrl,
name,
author,
durChapterIndex = 0,
durChapterPos = 0,
} = book;
// 判断是否为 searchBook
const isSeachBook = "respondTime" in book;
if (isSeachBook) {
await API.saveBook(book);
}
toDetail(
bookUrl,
name,
author,
durChapterIndex,
durChapterPos,
isSeachBook,
);
};
const toDetail = (
bookUrl,
bookName,
bookAuthor,
chapterIndex,
chapterPos,
isSeachBook,
fromReadRecentClick = false,
) => {
if (bookName === "尚无阅读记录") return;
// 最近书籍不再书架上 自动搜索
if (isSeachBook === true && fromReadRecentClick) {
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));
readingRecent.value = {
name: bookName,
author: bookAuthor,
url: bookUrl,
chapterIndex: chapterIndex,
chapterPos: chapterPos,
isSeachBook,
};
localStorage.setItem(
"readingRecent",
JSON.stringify(readingRecent.value),
);
router.push({
path: "/chapter",
});
};
const loadShelf = () => {
loadingWrapper(
store
.saveBookProgress()
//确保各种网络情况下同步请求先完成
.finally(fetchBookShelfData),
);
};
const saveReadConfig = (configStr) => {
try { try {
store.setConfig(JSON.parse(configStr)); store.setSearchBooks(searcBooks)
} catch { books.value = store.searchBooks
ElMessage.info("阅读界面配置解析错误"); //store.searchBooks.forEach((item) => books.value.push(item));
} catch (e) {
ElMessage.error('后端数据错误')
throw e
} }
}; },
() => {
closeLoading()
if (books.value.length == 0) {
ElMessage.info('搜索结果为空')
}
},
)
}
const fetchBookShelfData = () => { //连接状态
return API.getBookShelf().then((response) => { const connectStatus = computed(() => store.connectStatus)
store.setConnectType("success"); const connectType = computed(() => store.connectType)
if (response.data.isSuccess) { const newConnect = computed(() => store.newConnect)
//store.increaseBookNum(response.data.data.length); const setLegadoRetmoteUrl = () => {
store.addBooks( ElMessageBox.prompt(
response.data.data.sort(function (a, b) { '请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)',
var x = a["durChapterTime"] || 0; '提示',
var y = b["durChapterTime"] || 0; {
return y - x; confirmButtonText: '确定',
}), cancelButtonText: '取消',
); inputPlaceholder: legado_http_entry_point,
inputValidator: value => validatorHttpUrl(value),
inputErrorMessage: '输入的格式不对',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
store.setNewConnect(true)
instance.confirmButtonLoading = true
instance.confirmButtonText = '校验中……'
// instance.inputValue
const url = new URL(instance.inputValue).toString()
API.getReadConfig(url)
//API.getBookShelf()
.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)
} else {
localStorage.setItem(baseURL_localStorage_key, url)
}
store.setConnectStatus('已连接 ' + url.toString())
fetchBookShelfData()
done()
})
.catch(function (error) {
instance.confirmButtonLoading = false
instance.confirmButtonText = '确定'
ElMessage.error('访问失败,请检查您输入的 url')
store.setNewConnect(false)
throw error
})
} else { } else {
ElMessage.error(response.data.errorMsg ?? "后端返回格式错误!"); done()
}
store.setConnectStatus("已连接 " + legado_http_entry_point);
store.setNewConnect(false);
});
};
onMounted(() => {
//获取最近阅读书籍
let readingRecentStr = localStorage.getItem("readingRecent");
if (readingRecentStr != null) {
readingRecent.value = JSON.parse(readingRecentStr);
if (typeof readingRecent.value.chapterIndex == "undefined") {
readingRecent.value.chapterIndex = 0;
} }
},
},
)
}
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
toDetail(bookUrl, name, author, durChapterIndex, durChapterPos, isSeachBook)
}
const toDetail = (
bookUrl: string,
bookName: string,
bookAuthor: string,
chapterIndex: number,
chapterPos: number,
isSeachBook: boolean | undefined = false,
fromReadRecentClick = false,
) => {
if (bookName === '尚无阅读记录') 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', String(chapterIndex))
sessionStorage.setItem('chapterPos', String(chapterPos))
sessionStorage.setItem('isSeachBook', String(isSeachBook))
readingRecent.value = {
name: bookName,
author: bookAuthor,
bookUrl,
chapterIndex,
chapterPos,
isSeachBook,
}
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')
if (response.data.isSuccess) {
//store.increaseBookNum(response.data.data.length);
store.addBooks(
response.data.data.sort(function (a, b) {
const x = a['durChapterTime'] || 0
const y = b['durChapterTime'] || 0
return y - x
}),
)
} else {
if (
response.data.errorMsg.includes('还没有添加小说') &&
shelf.value.length > 0
) {
ElNotification.warning({
title: '提示',
message: '当前书架上的书籍已经被删除',
position: 'bottom-right',
})
return store.clearBooks()
} }
console.log("bookshelf mounted"); ElMessage.error(response.data.errorMsg ?? '后端返回格式错误!')
API.testLeagdoHttpUrlConnection() }
//.then(saveReadConfig) 应该在组件挂载前读取阅读配置 store.setConnectStatus('已连接 ' + legado_http_entry_point)
.then(loadShelf) store.setNewConnect(false)
.catch(function (error) { })
store.setConnectType("danger"); }
store.setConnectStatus("连接异常"); onMounted(() => {
ElMessage.error( //获取最近阅读书籍
"后端连接失败异常,请检查阅读WEB服务或者设置其它可用链接", const readingRecentStr = localStorage.getItem('readingRecent')
); if (readingRecentStr != null) {
store.setNewConnect(false); readingRecent.value = JSON.parse(readingRecentStr)
throw error; if (typeof readingRecent.value.chapterIndex == 'undefined') {
}); readingRecent.value.chapterIndex = 0
}); }
return { }
setLegadoRetmoteUrl, console.log('bookshelf mounted')
isNight, loadingWrapper(loadShelf())
connectStatus, })
connectType,
newConnect,
saveReadConfig, //expose it so beforeRouteEnter next can access it
readingRecent,
searchBook,
books,
handleBookClick,
toDetail,
isSearching,
SearchIcon,
githubUrl,
searchWord,
};
},
});
</script> </script>
<style lang="scss" scoped> <style scoped>
.index-wrapper { .index-wrapper {
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -438,12 +406,12 @@ export default defineComponent({
.recent-book { .recent-book {
font-size: 10px; font-size: 10px;
// font-weight: 400; /* // font-weight: 400;
// margin: 12px 0; // margin: 12px 0;
// font-weight: 500; // font-weight: 500;
// color: #6B7C87; // color: #6B7C87; */
cursor: pointer; cursor: pointer;
// padding: 6px 18px; /* // padding: 6px 18px; */
} }
} }
} }
@@ -464,7 +432,7 @@ export default defineComponent({
.setting-connect { .setting-connect {
font-size: 8px; font-size: 8px;
margin-top: 16px; margin-top: 16px;
// color: #6B7C87; /* // color: #6B7C87; */
cursor: pointer; cursor: pointer;
} }
} }
@@ -538,7 +506,7 @@ export default defineComponent({
} }
.night { .night {
:deep(.navigation-wrapper) { .navigation-wrapper {
background-color: #454545; background-color: #454545;
.navigation-title { .navigation-title {
+12 -11
View File
@@ -5,22 +5,23 @@
<source-tab-tools class="right" /> <source-tab-tools class="right" />
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import bookSourceConfig from "@/config/bookSourceEditConfig"; import bookSourceConfig from '@/config/bookSourceEditConfig'
import rssSourceConfig from "@/config/rssSourceEditConfig"; import rssSourceConfig from '@/config/rssSourceEditConfig'
import "@/assets/sourceeditor.css"; import '@/assets/sourceeditor.css'
import { useDark } from "@vueuse/core"; import { useDark } from '@vueuse/core'
import type { SourceConfig } from '@/config/sourceConfig'
useDark(); useDark()
let config; let config: SourceConfig
if (/bookSource/i.test(location.href)) { if (/bookSource/i.test(location.href)) {
config = bookSourceConfig; config = bookSourceConfig as SourceConfig
document.title = "书源管理"; document.title = '书源管理'
} else { } else {
config = rssSourceConfig; config = rssSourceConfig as SourceConfig
document.title = "订阅源管理"; document.title = '订阅源管理'
} }
</script> </script>
<style lang="scss" scoped> <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"; import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default ({ mode }) => export default defineConfig(({ mode }) => {
defineConfig({ return {
plugins: [ plugins: [
vue(), vue(),
AutoImport({ AutoImport({
imports: ["vue", "vue-router", "pinia"], imports: ["vue", "vue-router", "pinia"],
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/], include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/],
dirs: ["src/components", "src/store"], dirs: ["src/components", "src/store", "*.d.ts"],
eslintrc: { eslintrc: {
enabled: true, //enabled: true,
}, },
resolvers: [ resolvers: [
ElementPlusResolver(), ElementPlusResolver(),
@@ -72,4 +72,5 @@ export default ({ mode }) =>
}, },
}, },
}, },
}); }
});