Эх сурвалжийг харах

fix(modules/web): 重构阅读API;封面图片优先使用源站资源

- 添加图片代理接口API;API地址测试(3s超时)
Xwite 2 жил өмнө
parent
commit
d532608b66

+ 3 - 4
modules/web/.eslintrc-auto-import.json

@@ -5,6 +5,7 @@
     "ComputedRef": true,
     "EffectScope": true,
     "ElMessage": true,
+    "ElMessageBox": true,
     "InjectionKey": true,
     "PropType": true,
     "Ref": true,
@@ -78,8 +79,6 @@
     "watch": true,
     "watchEffect": true,
     "watchPostEffect": true,
-    "watchSyncEffect": true,
-    "toValue": true,
-    "WritableComputedRef": true
+    "watchSyncEffect": true
   }
-}
+}

+ 1 - 16
modules/web/src/api/axios.js

@@ -1,25 +1,10 @@
 import axios from "axios";
 
 const SECOND = 1000;
-const remoteIp = ref(localStorage.getItem("remoteIp"));
 
 const ajax = axios.create({
-  // baseURL: import.meta.env.VITE_API || location.origin,
+  baseURL: import.meta.env.VITE_API || localStorage.getItem("remoteIp") || location.origin,
   timeout: 120 * SECOND,
 });
 
-ajax.interceptors.request.use((config) => {
-  config.baseURL = baseUrl();
-  return config;
-});
-
 export default ajax;
-
-export const setRemoteIp = (ip) => {
-  remoteIp.value = ip;
-  localStorage.setItem("remoteIp", ip);
-};
-
-export const baseUrl = () => {
-  return remoteIp.value || location.origin;
-};

+ 86 - 14
modules/web/src/api/index.js

@@ -1,14 +1,48 @@
-import ajax, { baseUrl } from "./axios";
+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 */
 
-const getUrl = () => {
-  const { hostname, port } = new URL(baseUrl());
-  return `${hostname}:${Number(port) + 1}`;
+let legado_http_origin
+let legado_webSocket_origin
+
+const setLeagdoHttpUrl = (http_url) => {
+  let legado_webSocket_port;
+  const { protocol, hostname, port } = new URL(http_url);
+  if (!protocol.startsWith("http"))
+    throw new Error("unexpect protocol:" + http_url);
+  ajax.defaults.baseURL = http_url;
+  legado_http_origin = http_url;
+  if (port !== "") {
+    legado_webSocket_port = Number(port) + 1;
+  } else {
+    legado_webSocket_port = protocol.startsWith("https:") ? "444" : "81";
+  }
+  legado_webSocket_origin = 
+    `${protocol.startsWith("https:") ? "wss://" : "ws://"}${hostname}:${legado_webSocket_port}`;
+
+  console.info("legado_server_config:");
+  console.table({legado_http_origin, legado_webSocket_origin});
 };
 
+// 手动初始化 阅读web服务地址
+setLeagdoHttpUrl(ajax.defaults.baseURL);
+
+const testLeagdoHttpUrlConnection = async (http_url) => {
+  const {data = {}} = await ajax.get("/getReadConfig", {
+    baseURL: http_url,
+    timeout: 3000
+  })
+  // 返回结果应该是JSON 并有键值isSuccess
+  try {
+    if ("isSuccess" in data) return
+    throw new Error("ReadConfig后端返回格式错误" )
+  } catch {
+    throw new Error("ReadConfig后端返回格式错误" )
+  }
+}
+
 const isSourecEditor = /source/i.test(location.href);
 const APIExceptionHandler = (error) => {
   if (isSourecEditor) {
@@ -19,8 +53,10 @@ const APIExceptionHandler = (error) => {
   }
   throw error;
 };
+
 ajax.interceptors.response.use((response) => response, APIExceptionHandler);
 
+// 书架API
 // Http
 const getReadConfig = () => ajax.get("/getReadConfig");
 const saveReadConfig = (config) => ajax.post("/saveReadConfig", config);
@@ -32,8 +68,8 @@ const saveBookProgressWithBeacon = (bookProgress) => {
   if (!bookProgress) return;
   // 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
   navigator.sendBeacon(
-    `${baseUrl()}/saveBookProgress`,
-    JSON.stringify(bookProgress),
+    `${legado_http_origin}/saveBookProgress`,
+    JSON.stringify(bookProgress)
   );
 };
 
@@ -44,22 +80,23 @@ const getChapterList = (/** @type {string} */ bookUrl) =>
 
 const getBookContent = (
   /** @type {string} */ bookUrl,
-  /** @type {number} */ chapterIndex,
+  /** @type {number} */ chapterIndex
 ) =>
   ajax.get(
     "/getBookContent?url=" +
       encodeURIComponent(bookUrl) +
       "&index=" +
-      chapterIndex,
+      chapterIndex
   );
 
+// webSocket
 const search = (
   /** @type {string} */ searchKey,
   /** @type {(data: string) => void} */ onReceive,
-  /** @type {() => void} */ onFinish,
+  /** @type {() => void} */ onFinish
 ) => {
-  // webSocket
-  const url = `ws://${getUrl()}/searchBook`;
+
+  const url = `${legado_webSocket_origin}/searchBook`;
   const socket = new WebSocket(url);
 
   socket.onopen = () => {
@@ -77,6 +114,7 @@ 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");
@@ -96,14 +134,15 @@ const deleteSource = (data) =>
     ? 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,
+  /** @type {() => void} */ onFinish
 ) => {
-  // webSocket
-  const url = `ws://${getUrl()}/${
+
+  const url = `${legado_webSocket_origin}/${
     isBookSource ? "bookSource" : "rssSource"
   }Debug`;
 
@@ -123,6 +162,32 @@ const debug = (
   };
 };
 
+/**
+ * 从阅读获取需要特定处理的书籍封面
+ * @param {string} coverUrl
+ */
+const getProxyCoverUrl = (coverUrl) => {
+  if(coverUrl.startsWith(legado_http_origin)) return coverUrl
+  return legado_http_origin + "/cover?path=" + encodeURIComponent(coverUrl);
+}
+/**
+ * 从阅读获取需要特定处理的图片
+ * @param {string} src
+ * @param {`{number}`} width
+ */
+const getProxyImageUrl = (src, width) => {
+  if (src.startsWith(legado_http_origin)) return src
+  return (
+    legado_http_origin +
+    "/image?path=" +
+    encodeURIComponent(src) +
+    "&url=" +
+    encodeURIComponent(sessionStorage.getItem("bookUrl")) +
+    "&width=" +
+    width
+  );
+}
+
 export default {
   getReadConfig,
   saveReadConfig,
@@ -140,4 +205,11 @@ export default {
   saveSource,
   deleteSource,
   debug,
+
+  getProxyCoverUrl,
+  getProxyImageUrl,
+
+  testLeagdoHttpUrlConnection,
+  setLeagdoHttpUrl,
+  legado_http_origin,
 };

+ 2 - 4
modules/web/src/auto-imports.d.ts

@@ -1,12 +1,12 @@
 /* eslint-disable */
 /* prettier-ignore */
 // @ts-nocheck
-// noinspection JSUnusedGlobalSymbols
 // Generated by unplugin-auto-import
 export {}
 declare global {
   const EffectScope: typeof import('vue')['EffectScope']
   const ElMessage: typeof import('element-plus/es')['ElMessage']
+  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
   const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
   const computed: typeof import('vue')['computed']
   const createApp: typeof import('vue')['createApp']
@@ -62,7 +62,6 @@ declare global {
   const toRaw: typeof import('vue')['toRaw']
   const toRef: typeof import('vue')['toRef']
   const toRefs: typeof import('vue')['toRefs']
-  const toValue: typeof import('vue')['toValue']
   const triggerRef: typeof import('vue')['triggerRef']
   const unref: typeof import('vue')['unref']
   const useAttrs: typeof import('vue')['useAttrs']
@@ -82,6 +81,5 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
-  import('vue')
+  export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
 }

+ 10 - 5
modules/web/src/components/BookItems.vue

@@ -12,6 +12,7 @@
             class="cover"
             :src="getCover(book.coverUrl)"
             :key="book.coverUrl"
+            @error.once="proxyImage"
             alt=""
             loading="lazy"
           />
@@ -49,17 +50,21 @@
   </div>
 </template>
 <script setup>
-import { dateFormat } from "../utils/utils";
-import { baseUrl } from "@/api/axios.js";
+import { dateFormat, isLegadoUrl } from "../utils/utils";
+import API from "@api";
+
 const props = defineProps(["books", "isSearch"]);
 const emit = defineEmits(["bookClick"]);
 const handleClick = (book) => emit("bookClick", book);
 const getCover = (coverUrl) => {
-  return /^data:/.test(coverUrl)
-    ? coverUrl
-    : baseUrl() + "/cover?path=" + encodeURIComponent(coverUrl);
+  return isLegadoUrl(coverUrl)
+    ? API.getProxyCoverUrl(coverUrl) : coverUrl
+};
+const proxyImage = (event) => {
+  event.target.src = API.getProxyCoverUrl(event.target.src);
 };
 
+
 const subJustify = computed(() =>
   props.isSearch ? "space-between" : "flex-start",
 );

+ 4 - 3
modules/web/src/components/ChapterContent.vue

@@ -18,7 +18,8 @@
 </template>
 
 <script setup>
-import { getImageFromLegado, isLegadoUrl } from "@/utils/utils";
+import { isLegadoUrl } from "@/utils/utils";
+import API from "@api";
 import jump from "@/plugins/jump";
 
 const props = defineProps({
@@ -33,11 +34,11 @@ const props = defineProps({
 const getImageSrc = (content) => {
   const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/;
   const src = content.match(imgPattern)[1];
-  if (isLegadoUrl(src)) return getImageFromLegado(src);
+  if (isLegadoUrl(src)) return API.getProxyImageUrl(src, useBookStore().config.readWidth)
   return src;
 };
 const proxyImage = (event) => {
-  event.target.src = getImageFromLegado(event.target.src);
+  event.target.src = API.getProxyImageUrl(event.target.src, useBookStore().config.readWidth);
 };
 
 const calculateWordCount = (paragraph) => {

+ 0 - 2
modules/web/src/main.js

@@ -4,5 +4,3 @@ import router from "@/router";
 import store from "@/store";
 
 createApp(App).use(store).use(router).mount("#app");
-
-import("./pages/bookshelf/config");

+ 0 - 17
modules/web/src/pages/bookshelf/config.js

@@ -1,17 +0,0 @@
-import API from "@api";
-import { useBookStore } from "@/store";
-import "@/assets/bookshelf.css";
-
-/**
- * 加载配置
- */
-API.getReadConfig().then((res) => {
-  var data = res.data.data;
-  if (data) {
-    const bookStore = useBookStore();
-    let config = JSON.parse(data);
-    let defaultConfig = bookStore.config;
-    config = Object.assign(defaultConfig, config);
-    bookStore.setConfig(config);
-  }
-});

+ 0 - 2
modules/web/src/pages/bookshelf/main.js

@@ -4,5 +4,3 @@ import bookRouter from "@/router";
 import store from "@/store";
 
 createApp(App).use(store).use(bookRouter).mount("#app");
-
-import("./config");

+ 4 - 6
modules/web/src/store/bookStore.js

@@ -49,6 +49,9 @@ export const useBookStore = defineStore("book", {
         durChapterTitle: title,
       };
     },
+    theme: (state) => {
+      return state.config.theme
+    },
   },
   actions: {
     setConnectStatus(connectStatus) {
@@ -57,11 +60,6 @@ export const useBookStore = defineStore("book", {
     setConnectType(connectType) {
       this.connectType = connectType;
     },
-    resetConnect() {
-      this.connectStatus = "正在连接后端服务器……";
-      this.connectType = "";
-      this.clearBooks();
-    },
     setNewConnect(newConnect) {
       this.newConnect = newConnect;
     },
@@ -84,7 +82,7 @@ export const useBookStore = defineStore("book", {
       this.readingBook = readingBook;
     },
     setConfig(config) {
-      Object.assign(this.config, config);
+      this.config = Object.assign({}, this.config, config);
     },
     setReadSettingsVisible(visible) {
       this.readSettingsVisible = visible;

+ 1 - 15
modules/web/src/utils/utils.js

@@ -1,5 +1,4 @@
 import { formatDate } from "@vueuse/shared";
-import { baseUrl } from "@/api/axios.js";
 
 export const isLegadoUrl = (/** @type {string} */ url) =>
   /,\s*\{/.test(url) ||
@@ -8,20 +7,7 @@ export const isLegadoUrl = (/** @type {string} */ url) =>
     url.startsWith("data:") ||
     url.startsWith("blob:")
   );
-/**
- * @param {string} src
- */
-export function getImageFromLegado(src) {
-  return (
-    baseUrl() +
-    "/image?path=" +
-    encodeURIComponent(src) +
-    "&url=" +
-    encodeURIComponent(sessionStorage.getItem("bookUrl")) +
-    "&width=" +
-    useBookStore().config.readWidth
-  );
-}
+
 // @ts-ignore
 export const dateFormat = (/** @type {number} */ t) => {
   let time = new Date().getTime();

+ 111 - 88
modules/web/src/views/BookShelf.vue

@@ -29,7 +29,7 @@
                   readingRecent.name,
                   readingRecent.author,
                   readingRecent.chapterIndex,
-                  readingRecent.chapterPos,
+                  readingRecent.chapterPos
                 )
               "
               :class="{ 'no-point': readingRecent.url == '' }"
@@ -40,65 +40,52 @@
         </div>
         <div class="setting-wrapper">
           <div class="setting-title">基本设定</div>
-          <div class="setting-ip">
-            <el-input
-              class="setting-input"
-              size="small"
-              :disabled="ipInput.disable"
-              v-model="ipInput.ip"
-              @keydown.enter="setIP"
-            />
-            <el-tag
-              type="primary"
-              class="setting-toggle"
-              @click="toggleIpConfig"
-            >
-              {{ ipInput.disable ? "修改" : "取消" }}
-            </el-tag>
-          </div>
-          <div class="setting-item">
-            <el-tag :type="connectType" size="large" class="setting-connect">
-              {{ connectStatus }}
-            </el-tag>
+            <div class="setting-item">
+              <el-tag
+                :type="connectType"
+                size="large"
+                class="setting-connect"
+                :class="{ 'no-point': newConnect }"
+                @click="setIP"
+              >
+                {{ connectStatus }}
+              </el-tag>
+            </div>
           </div>
         </div>
+        <div class="bottom-icons">
+          <a
+            href="https://github.com/gedoor/legado_web_bookshelf"
+            target="_blank"
+          >
+            <div class="bottom-icon">
+              <img :src="githubUrl" alt="" />
+            </div>
+          </a>
+        </div>
       </div>
-      <div class="bottom-icons">
-        <a
-          href="https://github.com/gedoor/legado_web_bookshelf"
-          target="_blank"
-        >
-          <div class="bottom-icon">
-            <img :src="githubUrl" alt="" />
-          </div>
-        </a>
+      <div class="shelf-wrapper" ref="shelfWrapper">
+        <book-items
+          :books="books"
+          @bookClick="handleBookClick"
+          :isSearch="isSearching"
+        ></book-items>
       </div>
-    </div>
-    <div class="shelf-wrapper" ref="shelfWrapper">
-      <book-items
-        :books="books"
-        @bookClick="handleBookClick"
-        :isSearch="isSearching"
-      ></book-items>
-    </div>
   </div>
 </template>
 
 <script setup>
+import "@/assets/bookshelf.css";
 import "@/assets/fonts/shelffont.css";
 import { useBookStore } from "@/store";
 import githubUrl from "@/assets/imgs/github.png";
 import { useLoading } from "@/hooks/loading";
 import { Search } from "@element-plus/icons-vue";
 import API from "@api";
-import { baseUrl, setRemoteIp } from "@/api/axios.js";
 
 const store = useBookStore();
-const { connectStatus, connectType, newConnect, shelf } = storeToRefs(store);
+const { connectStatus, connectType, newConnect, shelf, theme } = storeToRefs(store);
 
-const theme = computed(() => {
-  return store.config.theme;
-});
 const isNight = computed(() => theme.value == 6);
 
 const readingRecent = ref({
@@ -111,7 +98,7 @@ const readingRecent = ref({
 const shelfWrapper = ref(null);
 const { showLoading, closeLoading, loadingWrapper, isLoading } = useLoading(
   shelfWrapper,
-  "正在获取书籍信息",
+  "正在获取书籍信息"
 );
 
 const books = shallowRef([]);
@@ -159,22 +146,59 @@ const searchBook = () => {
       if (books.value.length == 0) {
         ElMessage.info("搜索结果为空");
       }
-    },
+    }
   );
 };
 
-const ipInput = reactive({
-  ip: baseUrl(),
-  disable: true,
-});
-const toggleIpConfig = () => {
-  ipInput.ip = baseUrl();
-  ipInput.disable = !ipInput.disable;
-};
 const setIP = () => {
-  setRemoteIp(ipInput.ip);
-  ipInput.disable = true;
-  loadShelf();
+  ElMessageBox.prompt(
+    "请输入 IP 和端口 ( 如:127.0.0.1:9527 或者通过内网穿透的地址)",
+    "提示",
+    {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      inputPattern:
+        /^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?:([1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-6][0-5][0-5][0-3][0-5])$/,
+      inputErrorMessage: "url 形式不正确",
+      beforeClose: (action, instance, done) => {
+        if (action === "confirm") {
+          store.setNewConnect(true);
+          instance.confirmButtonLoading = true;
+          instance.confirmButtonText = "校验中……";
+          // instance.inputValue
+          API.testLeagdoHttpUrlConnection("http://" + instance.inputValue)
+          //API.getBookShelf()
+            .then(function (response) {
+              instance.confirmButtonLoading = false;
+              // that.$store.commit(
+              // "increaseBookNum",
+              // response.data.data.length
+              // );
+              //store.addBooks(response.data.data);
+              const ip = instance.inputValue;
+              store.setConnectType("success");
+              store.setConnectStatus("已连接 " + ip);
+              store.clearSearchBooks();
+              store.setNewConnect(false);
+              API.setLeagdoHttpUrl("http://" + ip);
+              //持久化
+              localStorage.setItem("remoteIp", ip);
+              fetchBookShelfData()
+              done();
+            })
+            .catch(function (error) {
+              instance.confirmButtonLoading = false;
+              instance.confirmButtonText = "确定";
+              ElMessage.error("访问失败,请检查您输入的 url");
+              store.setNewConnect(false);
+              throw error;
+            });
+        } else {
+          done();
+        }
+      },
+    }
+  );
 };
 
 const router = useRouter();
@@ -221,16 +245,24 @@ onMounted(() => {
       readingRecent.value.chapterIndex = 0;
     }
   }
-  loadShelf();
+  API.testLeagdoHttpUrlConnection()
+  .then(loadReadConfig)
+  .then(loadShelf)
+  .catch(function (error) {
+      store.setConnectType("danger");
+      store.setConnectStatus("连接异常");
+      ElMessage.error("后端连接失败异常,请检查阅读WEB服务或者设置其它可用IP")
+      store.setNewConnect(false);
+      throw error;
+  });
 });
 
 const loadShelf = () => {
-  store.resetConnect();
   loadingWrapper(
     store
       .saveBookProgress()
       //确保各种网络情况下同步请求先完成
-      .finally(fetchBookShelfData),
+      .finally(fetchBookShelfData)
   );
 };
 
@@ -245,22 +277,29 @@ const fetchBookShelfData = () => {
             var x = a["durChapterTime"] || 0;
             var y = b["durChapterTime"] || 0;
             return y - x;
-          }),
+          })
         );
       } else {
-        ElMessage.error(response.data.errorMsg);
+        ElMessage.error(response.data.errorMsg ?? "后端返回格式错误!");
       }
-      store.setConnectStatus("已连接 ");
+      store.setConnectStatus("已连接 " + API.legado_http_origin);
       store.setNewConnect(false);
     })
-    .catch(function (error) {
-      store.setConnectType("danger");
-      store.setConnectStatus("连接失败");
-      ElMessage.error("后端连接失败");
-      store.setNewConnect(false);
-      throw error;
-    });
 };
+
+/**
+ * 加载阅读配置
+ */
+const loadReadConfig = () => {
+  return API.getReadConfig().then((res) => {
+    var data = res.data.data;
+    if (data) {
+      let config = JSON.parse(data);
+      store.setConfig(config);
+    }
+  });
+}
+
 </script>
 
 <style lang="scss" scoped>
@@ -340,31 +379,15 @@ const fetchBookShelfData = () => {
         font-family: FZZCYSK;
       }
 
-      .setting-ip {
-        margin-top: 16px;
-        white-space: nowrap;
-      }
-
-      .setting-input {
-        width: 216px;
-        margin-right: 4px;
-      }
-
       .no-point {
         pointer-events: none;
       }
 
-      .setting-toggle {
-        font-size: 10px;
-        cursor: pointer;
-        //margin-top: 4px;
-      }
-
       .setting-connect {
-        font-size: 10px;
-        margin-top: 4px;
+        font-size: 8px;
+        margin-top: 16px;
         // color: #6B7C87;
-        //cursor: pointer;
+        cursor: pointer;
       }
     }