fix(web): connectStatus not change when connected backend abort connection; fix relative URL parse; fix loadWebCatalog ElMessage bug

This commit is contained in:
Xwite
2024-10-19 19:22:23 +08:00
parent a0c307dc3f
commit c9a7e3aba8
7 changed files with 95 additions and 79 deletions
+34 -29
View File
@@ -22,9 +22,10 @@ export let legado_http_entry_point = ''
export let legado_webSocket_entry_point = '' export let legado_webSocket_entry_point = ''
let wsOnError: typeof WebSocket.prototype.onerror = () => {} let wsOnError: typeof WebSocket.prototype.onerror = () => {}
export const setWebsocketOnError = ( let wsOnMessage: typeof WebSocket.prototype.onmessage = () => {}
callback: typeof WebSocket.prototype.onerror, export const setWebsocketOnMessage = (callback: typeof wsOnMessage) =>
) => { (wsOnMessage = callback)
export const setWebsocketOnError = (callback: typeof wsOnError) => {
//WebSocket.prototype.onerror = callback //WebSocket.prototype.onerror = callback
wsOnError = callback wsOnError = callback
} }
@@ -33,15 +34,15 @@ export const setApiEntryPoint = (
http_entry_point: string, http_entry_point: string,
webSocket_entry_point: string, webSocket_entry_point: string,
) => { ) => {
legado_http_entry_point = http_entry_point legado_http_entry_point = new URL(http_entry_point).toString()
legado_webSocket_entry_point = webSocket_entry_point legado_webSocket_entry_point = new URL(webSocket_entry_point).toString()
ajax.defaults.baseURL = http_entry_point.toString() ajax.defaults.baseURL = legado_http_entry_point
} }
// 书架API // 书架API
// Http // Http
const getReadConfig = async (http_url = legado_http_entry_point) => { const getReadConfig = async (http_url = legado_http_entry_point) => {
const { data } = await ajax.get<LeagdoApiResponse<string>>('/getReadConfig', { const { data } = await ajax.get<LeagdoApiResponse<string>>('getReadConfig', {
baseURL: http_url.toString(), baseURL: http_url.toString(),
timeout: 3000, timeout: 3000,
}) })
@@ -52,27 +53,27 @@ const getReadConfig = async (http_url = legado_http_entry_point) => {
} }
} }
const saveReadConfig = (config: webReadConfig) => const saveReadConfig = (config: webReadConfig) =>
ajax.post<LeagdoApiResponse<string>>('/saveReadConfig', config) ajax.post<LeagdoApiResponse<string>>('saveReadConfig', config)
/** @deprecated: 使用`API.saveBookProgressWithBeacon`以确保在页面或者直接关闭的情况下保存进度 */ /** @deprecated: 使用`API.saveBookProgressWithBeacon`以确保在页面或者直接关闭的情况下保存进度 */
const saveBookProgress = (bookProgress: BookProgress) => const saveBookProgress = (bookProgress: BookProgress) =>
ajax.post('/saveBookProgress', bookProgress) ajax.post('saveBookProgress', bookProgress)
/**主要在直接关闭浏览器情况下可靠发送书籍进度 */ /**主要在直接关闭浏览器情况下可靠发送书籍进度 */
const saveBookProgressWithBeacon = (bookProgress: BookProgress) => { const saveBookProgressWithBeacon = (bookProgress: BookProgress) => {
if (!bookProgress) return if (!bookProgress) return
// 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon // 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
navigator.sendBeacon( navigator.sendBeacon(
new URL('/saveBookProgress', legado_http_entry_point), new URL('saveBookProgress', legado_http_entry_point),
JSON.stringify(bookProgress), JSON.stringify(bookProgress),
) )
} }
const getBookShelf = () => ajax.get<LeagdoApiResponse<Book[]>>('/getBookshelf') const getBookShelf = () => ajax.get<LeagdoApiResponse<Book[]>>('getBookshelf')
const getChapterList = (/** @type {string} */ bookUrl: string) => const getChapterList = (/** @type {string} */ bookUrl: string) =>
ajax.get<LeagdoApiResponse<BookChapter[]>>( ajax.get<LeagdoApiResponse<BookChapter[]>>(
'/getChapterList?url=' + encodeURIComponent(bookUrl), 'getChapterList?url=' + encodeURIComponent(bookUrl),
) )
const getBookContent = ( const getBookContent = (
@@ -80,7 +81,7 @@ const getBookContent = (
/** @type {number} */ chapterIndex: number, /** @type {number} */ chapterIndex: number,
) => ) =>
ajax.get<LeagdoApiResponse<string>>( ajax.get<LeagdoApiResponse<string>>(
'/getBookContent?url=' + 'getBookContent?url=' +
encodeURIComponent(bookUrl) + encodeURIComponent(bookUrl) +
'&index=' + '&index=' +
chapterIndex, chapterIndex,
@@ -93,16 +94,17 @@ const search = (
onFinish: () => void, onFinish: () => void,
) => { ) => {
const socket = new WebSocket( const socket = new WebSocket(
new URL('/searchBook', legado_webSocket_entry_point), new URL('searchBook', legado_webSocket_entry_point),
) )
socket.onerror = wsOnError socket.onerror = wsOnError
socket.onopen = () => { socket.onopen = () => {
socket.send(`{"key":"${searchKey}"}`) socket.send(`{"key":"${searchKey}"}`)
} }
socket.onmessage = ({ data }) => { socket.onmessage = event => {
try { try {
onReceive(JSON.parse(data)) onReceive(JSON.parse(event.data))
wsOnMessage?.call(socket, event)
} catch { } catch {
onFinish() onFinish()
} }
@@ -114,31 +116,31 @@ const search = (
} }
const saveBook = (book: BaseBook) => const saveBook = (book: BaseBook) =>
ajax.post<LeagdoApiResponse<string>>('/saveBook', book) ajax.post<LeagdoApiResponse<string>>('saveBook', book)
const deleteBook = (book: BaseBook) => const deleteBook = (book: BaseBook) =>
ajax.post<LeagdoApiResponse<string>>('/deleteBook', book) ajax.post<LeagdoApiResponse<string>>('deleteBook', book)
const isBookSource = /bookSource/i.test(location.href) const isBookSource = /bookSource/i.test(location.href)
// 源编辑API // 源编辑API
// Http // Http
const getSources = () => const getSources = () =>
isBookSource ? ajax.get('/getBookSources') : ajax.get('/getRssSources') isBookSource ? ajax.get('getBookSources') : ajax.get('getRssSources')
const saveSource = (data: Source) => const saveSource = (data: Source) =>
isBookSource isBookSource
? ajax.post<LeagdoApiResponse<string>>('/saveBookSource', data) ? ajax.post<LeagdoApiResponse<string>>('saveBookSource', data)
: ajax.post<LeagdoApiResponse<string>>('/saveRssSource', data) : ajax.post<LeagdoApiResponse<string>>('saveRssSource', data)
const saveSources = (data: Source[]) => const saveSources = (data: Source[]) =>
isBookSource isBookSource
? ajax.post<LeagdoApiResponse<Source[]>>('/saveBookSources', data) ? ajax.post<LeagdoApiResponse<Source[]>>('saveBookSources', data)
: ajax.post<LeagdoApiResponse<Source[]>>('/saveRssSources', data) : ajax.post<LeagdoApiResponse<Source[]>>('saveRssSources', data)
const deleteSource = (data: Source[]) => const deleteSource = (data: Source[]) =>
isBookSource isBookSource
? ajax.post<LeagdoApiResponse<string>>('/deleteBookSources', data) ? ajax.post<LeagdoApiResponse<string>>('deleteBookSources', data)
: ajax.post<LeagdoApiResponse<string>>('/deleteRssSources', data) : ajax.post<LeagdoApiResponse<string>>('deleteRssSources', data)
// webSocket // webSocket
const debug = ( const debug = (
@@ -148,7 +150,7 @@ const debug = (
/** @type {() => void} */ onFinish: () => void, /** @type {() => void} */ onFinish: () => void,
) => { ) => {
const url = new URL( const url = new URL(
`/${isBookSource ? 'bookSource' : 'rssSource'}Debug`, `${isBookSource ? 'bookSource' : 'rssSource'}Debug`,
legado_webSocket_entry_point, legado_webSocket_entry_point,
) )
@@ -157,7 +159,10 @@ const debug = (
socket.onopen = () => { socket.onopen = () => {
socket.send(JSON.stringify({ tag: sourceUrl, key: searchKey })) socket.send(JSON.stringify({ tag: sourceUrl, key: searchKey }))
} }
socket.onmessage = ({ data }) => onReceive(data) socket.onmessage = event => {
onReceive(event.data)
wsOnMessage?.call(socket, event)
}
socket.onclose = () => { socket.onclose = () => {
onFinish() onFinish()
@@ -171,7 +176,7 @@ const debug = (
const getProxyCoverUrl = (coverUrl: string) => { const getProxyCoverUrl = (coverUrl: string) => {
if (coverUrl.startsWith(legado_http_entry_point)) return coverUrl if (coverUrl.startsWith(legado_http_entry_point)) return coverUrl
return new URL( return new URL(
'/cover?path=' + encodeURIComponent(coverUrl), 'cover?path=' + encodeURIComponent(coverUrl),
legado_http_entry_point, legado_http_entry_point,
).toString() ).toString()
} }
@@ -188,7 +193,7 @@ const getProxyImageUrl = (
) => { ) => {
if (src.startsWith(legado_http_entry_point)) return src if (src.startsWith(legado_http_entry_point)) return src
return new URL( return new URL(
'/image?path=' + 'image?path=' +
encodeURIComponent(src) + encodeURIComponent(src) +
'&url=' + '&url=' +
encodeURIComponent(bookUrl) + encodeURIComponent(bookUrl) +
+21 -2
View File
@@ -1,9 +1,21 @@
import type { AxiosResponse } from 'axios' import type { AxiosResponse } from 'axios'
import type { LeagdoApiResponse } from './api' import type { LeagdoApiResponse } from './api'
import API, { setWebsocketOnError, setApiEntryPoint } from './api' import API, {
setWebsocketOnError,
setApiEntryPoint,
legado_http_entry_point,
setWebsocketOnMessage,
} from './api'
import ajax from './axios' import ajax from './axios'
import { validatorHttpUrl } from '@/utils/utils' import { validatorHttpUrl } from '@/utils/utils'
import { createApp } from 'vue'
import App from '@/App.vue'
import store, { useConnectionStore } from '@/store'
createApp(App).use(store)
const connectionStore = useConnectionStore()
const LeagdoApiResponseKeys: string[] = Array.of('isSuccess', 'errorMsg') const LeagdoApiResponseKeys: string[] = Array.of('isSuccess', 'errorMsg')
const notification = ElMessage const notification = ElMessage
@@ -31,6 +43,8 @@ const responseCheckInterceptor = (resp: AxiosResponse) => {
notification.warning({ message: '后端返回内容格式错误', grouping: true }) notification.warning({ message: '后端返回内容格式错误', grouping: true })
throw new Error() throw new Error()
} }
connectionStore.setConnectType('primary')
connectionStore.setConnectStatus('已连接 ' + legado_http_entry_point)
return resp return resp
} }
@@ -39,13 +53,18 @@ const axiosErrorInterceptor = (err: unknown) => {
message: '后端连接失败,请检查阅读WEB服务或者设置其它可用链接', message: '后端连接失败,请检查阅读WEB服务或者设置其它可用链接',
grouping: true, grouping: true,
}) })
connectionStore.setConnectType('danger')
connectionStore.setConnectStatus('连接异常')
throw err throw err
} }
// http全局 // http全局
ajax.interceptors.response.use(responseCheckInterceptor, axiosErrorInterceptor) ajax.interceptors.response.use(responseCheckInterceptor, axiosErrorInterceptor)
// websocket // websocket
setWebsocketOnError(axiosErrorInterceptor) setWebsocketOnError(axiosErrorInterceptor)
setWebsocketOnMessage(() => {
connectionStore.setConnectType('primary')
connectionStore.setConnectStatus('已连接 ' + legado_http_entry_point)
})
/** /**
* 按照阅读的默认规则 解析阅读HTTP WebSocket API入口地址 * 按照阅读的默认规则 解析阅读HTTP WebSocket API入口地址
* @returns [http_url, webSocekt_url] * @returns [http_url, webSocekt_url]
+1
View File
@@ -70,6 +70,7 @@ declare global {
const unref: typeof import('vue')['unref'] const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs'] const useAttrs: typeof import('vue')['useAttrs']
const useBookStore: typeof import('./store/bookStore')['useBookStore'] const useBookStore: typeof import('./store/bookStore')['useBookStore']
const useConnectionStore: typeof import('./store/connectionStore')['useConnectionStore']
const useCssModule: typeof import('vue')['useCssModule'] const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars'] const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId'] const useId: typeof import('vue')['useId']
+3 -20
View File
@@ -29,9 +29,6 @@ let webReadConfigLoadedDate: Date | undefined
export const useBookStore = defineStore('book', { export const useBookStore = defineStore('book', {
state: () => { state: () => {
return { return {
connectStatus: '正在连接后端服务器……',
connectType: 'primary' as 'primary' | 'success' | 'danger',
newConnect: true,
searchBooks: [] as SeachBook[], searchBooks: [] as SeachBook[],
shelf: [] as Book[], shelf: [] as Book[],
catalog: [] as BookChapter[], catalog: [] as BookChapter[],
@@ -69,15 +66,6 @@ export const useBookStore = defineStore('book', {
isNight: state => state.config.theme == 6, isNight: state => state.config.theme == 6,
}, },
actions: { actions: {
setConnectStatus(connectStatus: string) {
this.connectStatus = connectStatus
},
setConnectType(connectType: 'primary' | 'success' | 'danger') {
this.connectType = connectType
},
setNewConnect(newConnect: boolean) {
this.newConnect = newConnect
},
/** 从后端加载书架书籍,优先返回内存缓存 */ /** 从后端加载书架书籍,优先返回内存缓存 */
async loadBookShelf(): Promise<Book[]> { async loadBookShelf(): Promise<Book[]> {
const fetchBookshellf_promise = API.getBookShelf().then(resp => { const fetchBookshellf_promise = API.getBookShelf().then(resp => {
@@ -116,16 +104,13 @@ export const useBookStore = defineStore('book', {
return await fetchBookshellf_promise return await fetchBookshellf_promise
} }
}, },
clearBooks() {
this.shelf = []
},
/** 从后端加载书籍目录,优先返回内存缓存 */ /** 从后端加载书籍目录,优先返回内存缓存 */
async loadWebCatalog( async loadWebCatalog(
book: typeof this.readingBook, book: typeof this.readingBook,
): Promise<BookChapter[]> { ): Promise<BookChapter[]> {
const { bookUrl, name, chapterIndex } = book const { bookUrl, name, chapterIndex } = book
const fetchChapterList_promise = API.getChapterList( const fetchChapterList_promise = API.getChapterList(
book.bookUrl as string, bookUrl as string,
).then(res => { ).then(res => {
const { isSuccess, data, errorMsg } = res.data const { isSuccess, data, errorMsg } = res.data
if (isSuccess === false) { if (isSuccess === false) {
@@ -133,13 +118,14 @@ export const useBookStore = defineStore('book', {
throw new Error() throw new Error()
} }
if ( if (
bookUrl === this.readingBook.bookUrl &&
data.length !== this.catalog.length && data.length !== this.catalog.length &&
data.length > 0 && data.length > 0 &&
this.catalog.length > 0 this.catalog.length > 0
) { ) {
ElMessage.info(`书籍${name}: 章节目录已更新`) ElMessage.info(`书籍${name}: 章节目录已更新`)
} }
this.setCatalog(data) this.catalog = data
console.log(`书籍${name}: 章节目录已更新`) console.log(`书籍${name}: 章节目录已更新`)
return this.catalog return this.catalog
}) })
@@ -155,9 +141,6 @@ export const useBookStore = defineStore('book', {
return await fetchChapterList_promise return await fetchChapterList_promise
} }
}, },
setCatalog(catalog: BookChapter[]) {
this.catalog = catalog
},
setPopCataVisible(visible: boolean) { setPopCataVisible(visible: boolean) {
this.popCataVisible = visible this.popCataVisible = visible
}, },
+24
View File
@@ -0,0 +1,24 @@
import { defineStore } from 'pinia'
export const useConnectionStore = defineStore('connection', {
state: () => {
return {
connectStatus: '正在连接后端服务器……',
connectType: 'primary' as 'primary' | 'success' | 'danger',
newConnect: false,
}
},
actions: {
setConnectStatus(connectStatus: string) {
if (this.newConnect === true) return
this.connectStatus = connectStatus
},
setConnectType(connectType: 'primary' | 'success' | 'danger') {
if (this.newConnect === true) return
this.connectType = connectType
},
setNewConnect(newConnect: boolean) {
this.newConnect = newConnect
},
},
})
+1
View File
@@ -2,4 +2,5 @@ import { createPinia } from 'pinia'
export * from './bookStore' export * from './bookStore'
export * from './sourceStore' export * from './sourceStore'
export * from './connectionStore'
export default createPinia() export default createPinia()
+8 -25
View File
@@ -175,9 +175,9 @@ const searchBook = () => {
} }
//连接状态 //连接状态
const connectStatus = computed(() => store.connectStatus) const connectionStore = useConnectionStore()
const connectType = computed(() => store.connectType) const { connectStatus, connectType, newConnect } = storeToRefs(connectionStore)
const newConnect = computed(() => store.newConnect)
const setLegadoRetmoteUrl = () => { const setLegadoRetmoteUrl = () => {
ElMessageBox.prompt( ElMessageBox.prompt(
'请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)', '请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)',
@@ -190,34 +190,30 @@ const setLegadoRetmoteUrl = () => {
inputErrorMessage: '输入的格式不对', inputErrorMessage: '输入的格式不对',
beforeClose: (action, instance, done) => { beforeClose: (action, instance, done) => {
if (action === 'confirm') { if (action === 'confirm') {
store.setNewConnect(true) connectionStore.setNewConnect(true)
instance.confirmButtonLoading = true instance.confirmButtonLoading = true
instance.confirmButtonText = '校验中……' instance.confirmButtonText = '校验中……'
// instance.inputValue // instance.inputValue
const url = new URL(instance.inputValue).toString() const url = new URL(instance.inputValue).toString()
API.getReadConfig(url) API.getReadConfig(url)
//API.getBookShelf()
.then(function (config) { .then(function (config) {
connectionStore.setNewConnect(false)
applyReadConfig(config) applyReadConfig(config)
instance.confirmButtonLoading = false instance.confirmButtonLoading = false
store.setConnectType('success')
store.clearSearchBooks() store.clearSearchBooks()
store.setNewConnect(false)
setApiEntryPoint(...parseLeagdoHttpUrlWithDefault(url)) setApiEntryPoint(...parseLeagdoHttpUrlWithDefault(url))
if (url === location.origin) { if (url === location.origin) {
localStorage.removeItem(baseURL_localStorage_key) localStorage.removeItem(baseURL_localStorage_key)
} else { } else {
localStorage.setItem(baseURL_localStorage_key, url) localStorage.setItem(baseURL_localStorage_key, url)
} }
store.setConnectStatus('已连接 ' + url.toString()) store.loadBookShelf()
fetchBookShelfData()
done() done()
}) })
.catch(function (error) { .catch(function (error) {
connectionStore.setNewConnect(false)
instance.confirmButtonLoading = false instance.confirmButtonLoading = false
instance.confirmButtonText = '确定' instance.confirmButtonText = '确定'
ElMessage.error('访问失败,请检查您输入的 url')
store.setNewConnect(false)
throw error throw error
}) })
} else { } else {
@@ -287,25 +283,12 @@ const toDetail = (
} }
const loadShelf = async () => { const loadShelf = async () => {
try {
await store.loadWebConfig() await store.loadWebConfig()
await store.saveBookProgress() await store.saveBookProgress()
//确保各种网络情况下同步请求先完成 //确保各种网络情况下同步请求先完成
await fetchBookShelfData() await store.loadBookShelf()
} catch (error: unknown) {
store.setConnectType('danger')
store.setConnectStatus('连接异常')
store.setNewConnect(false)
throw error
}
} }
const fetchBookShelfData = async () => {
await store.loadBookShelf()
store.setConnectType('primary')
store.setConnectStatus('已连接 ' + legado_http_entry_point)
store.setNewConnect(false)
}
onMounted(() => { onMounted(() => {
//获取最近阅读书籍 //获取最近阅读书籍
const readingRecentStr = localStorage.getItem('readingRecent') const readingRecentStr = localStorage.getItem('readingRecent')