refactor(modules/web): Mirgrat to typescript; fix bugs
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
|  |  |  |  |
|
|  |  |  |  |
|
||||||
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
|
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
|
||||||
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
|
| Edge ≥ 85 | Firefox ≥ 79 | Chrome ≥ 85 | Safari ≥ 14.1 |
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
> 需要阅读app提供后端服务
|
> 需要阅读app提供后端服务
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module "vue3-virtual-scroll-list";
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2844
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { AxiosResponse } from 'axios'
|
||||||
|
import type { LeagdoApiResponse } from './api'
|
||||||
|
import API, {
|
||||||
|
setWebsocketOnError,
|
||||||
|
legado_http_entry_point,
|
||||||
|
legado_webSocket_entry_point,
|
||||||
|
setApiEntryPoint,
|
||||||
|
} from './api'
|
||||||
|
import ajax from './axios'
|
||||||
|
import { validatorHttpUrl } from '@/utils/utils'
|
||||||
|
|
||||||
|
const LeagdoApiResponseKeys: string[] = Array.of('isSuccess', 'errorMsg')
|
||||||
|
|
||||||
|
/** Axios.Interceptor: check if resp is LeagaoLeagdoApiResponse*/
|
||||||
|
const responseCheckInterceptor = (resp: AxiosResponse) => {
|
||||||
|
let isLeagdoApiResponse = true
|
||||||
|
try {
|
||||||
|
const data = resp.data
|
||||||
|
|
||||||
|
for (const key of LeagdoApiResponseKeys) {
|
||||||
|
if (!(key in data)) {
|
||||||
|
isLeagdoApiResponse = false
|
||||||
|
LeagdoApiResponseKeys.length = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((data as LeagdoApiResponse<unknown>).isSuccess === true) {
|
||||||
|
if (!('data' in data)) {
|
||||||
|
isLeagdoApiResponse = false
|
||||||
|
LeagdoApiResponseKeys.length = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
isLeagdoApiResponse = false
|
||||||
|
}
|
||||||
|
if (isLeagdoApiResponse === false) {
|
||||||
|
ElNotification.warning('后端返回内容格式错误')
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosErrorInterceptor = (err: unknown) => {
|
||||||
|
ElNotification.error('后端连接失败,请检查阅读WEB服务或者设置其它可用链接')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
// http全局
|
||||||
|
ajax.interceptors.response.use(responseCheckInterceptor, axiosErrorInterceptor)
|
||||||
|
// websocket
|
||||||
|
setWebsocketOnError(axiosErrorInterceptor)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按照阅读的默认规则 解析阅读HTTP WebSocket API入口地址
|
||||||
|
* @returns [http_url, webSocekt_url]
|
||||||
|
*/
|
||||||
|
export const parseLeagdoHttpUrlWithDefault = (
|
||||||
|
http_url: string | URL,
|
||||||
|
): [string, string] => {
|
||||||
|
let url = new URL(location.origin) //默认当前网址的origin部分
|
||||||
|
if (validatorHttpUrl(http_url)) {
|
||||||
|
url = new URL(http_url)
|
||||||
|
}
|
||||||
|
const { protocol, port } = url
|
||||||
|
// websocket服务端口 为http服务端口 + 1
|
||||||
|
let legado_webSocket_port
|
||||||
|
if (port !== '') {
|
||||||
|
legado_webSocket_port = String(Number(port) + 1)
|
||||||
|
} else {
|
||||||
|
legado_webSocket_port = protocol.startsWith('https:') ? '444' : '81'
|
||||||
|
}
|
||||||
|
// websocket协议是否为加密版本
|
||||||
|
const legado_webSocket_protocol = protocol.startsWith('https:')
|
||||||
|
? 'wss://'
|
||||||
|
: 'ws://'
|
||||||
|
|
||||||
|
const http_entry_point = url.toString()
|
||||||
|
|
||||||
|
url.protocol = legado_webSocket_protocol
|
||||||
|
url.port = legado_webSocket_port
|
||||||
|
const webSocket_entry_point = url.toString()
|
||||||
|
|
||||||
|
console.info('legado_api_config:')
|
||||||
|
console.table({
|
||||||
|
'http API入口': legado_http_entry_point,
|
||||||
|
'webSocket API入口': legado_webSocket_entry_point,
|
||||||
|
})
|
||||||
|
return [http_entry_point, webSocket_entry_point]
|
||||||
|
}
|
||||||
|
|
||||||
|
//export const useLeagdoRemoteUrlDialog = () => { }
|
||||||
|
|
||||||
|
setApiEntryPoint(
|
||||||
|
...parseLeagdoHttpUrlWithDefault(ajax.defaults.baseURL as string),
|
||||||
|
)
|
||||||
|
|
||||||
|
export default API
|
||||||
|
export * from './api'
|
||||||
@@ -1,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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+95
-79
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+109
@@ -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 //变量
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>>
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
};
|
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import store from '@/store'
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
|
||||||
|
createApp(App).use(store).use(router).mount('#app')
|
||||||
|
// 书架 同步Element PLUS 夜间模式
|
||||||
|
watch(
|
||||||
|
() => useBookStore().isNight,
|
||||||
|
isNight => {
|
||||||
|
if (isNight) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -29,4 +29,5 @@ pnpm build
|
|||||||
pnpm lint:fix
|
pnpm lint:fix
|
||||||
#格式化代码
|
#格式化代码
|
||||||
```
|
```
|
||||||
- 调试的时候可以修改.env.development里面的地址连接手机端调试
|
|
||||||
|
- 调试的时候可以修改.env.development里面的地址连接手机端调试
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Vendored
+18
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
Vendored
+158
-22
@@ -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 }
|
||||||
|
|||||||
@@ -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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createPinia } from "pinia";
|
|
||||||
|
|
||||||
export * from "./bookStore";
|
|
||||||
export * from "./sourceStore";
|
|
||||||
export default createPinia();
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
export * from './bookStore'
|
||||||
|
export * from './sourceStore'
|
||||||
|
export default createPinia()
|
||||||
@@ -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 = [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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 = []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Vendored
+14
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -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 }) =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user