mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 08:46:40 +08:00
添加全局请求和图片优化器
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { useAuthStore } from '@/stores'
|
import { useAuthStore } from '@/stores'
|
||||||
|
import { initializeRequestOptimizer } from '@/utils/requestOptimizer'
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
@@ -17,6 +18,9 @@ declare global {
|
|||||||
// 将 API 实例暴露到全局,供插件使用
|
// 将 API 实例暴露到全局,供插件使用
|
||||||
window.MoviePilotAPI = api
|
window.MoviePilotAPI = api
|
||||||
|
|
||||||
|
// 初始化请求优化器(必须在其他拦截器之前)
|
||||||
|
initializeRequestOptimizer(api)
|
||||||
|
|
||||||
// 添加请求拦截器
|
// 添加请求拦截器
|
||||||
api.interceptors.request.use(config => {
|
api.interceptors.request.use(config => {
|
||||||
// 认证 Store
|
// 认证 Store
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useToast } from 'vue-toastification'
|
|||||||
import { formatSeason, formatRating } from '@/@core/utils/formatters'
|
import { formatSeason, formatRating } from '@/@core/utils/formatters'
|
||||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||||
import type { MediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
|
import type { MediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
|
||||||
import router, { registerAbortController } from '@/router'
|
import router from '@/router'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
|
||||||
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
|
||||||
@@ -232,9 +232,6 @@ async function handleCheckSubscribe() {
|
|||||||
// 查询当前媒体是否已入库
|
// 查询当前媒体是否已入库
|
||||||
async function handleCheckExists() {
|
async function handleCheckExists() {
|
||||||
try {
|
try {
|
||||||
const abortController = new AbortController()
|
|
||||||
registerAbortController(abortController)
|
|
||||||
const { signal } = abortController
|
|
||||||
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||||
params: {
|
params: {
|
||||||
tmdbid: props.media?.tmdb_id,
|
tmdbid: props.media?.tmdb_id,
|
||||||
@@ -243,7 +240,6 @@ async function handleCheckExists() {
|
|||||||
season: props.media?.season,
|
season: props.media?.season,
|
||||||
mtype: props.media?.type,
|
mtype: props.media?.type,
|
||||||
},
|
},
|
||||||
signal,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) isExists.value = true
|
if (result.success) isExists.value = true
|
||||||
@@ -255,16 +251,13 @@ async function handleCheckExists() {
|
|||||||
// 调用API检查是否已订阅,电视剧需要指定季
|
// 调用API检查是否已订阅,电视剧需要指定季
|
||||||
async function checkSubscribe(season = 0) {
|
async function checkSubscribe(season = 0) {
|
||||||
try {
|
try {
|
||||||
const abortController = new AbortController()
|
// AbortController 现在由全局请求优化器自动管理
|
||||||
registerAbortController(abortController)
|
|
||||||
const { signal } = abortController
|
|
||||||
const mediaid = getMediaId()
|
const mediaid = getMediaId()
|
||||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||||
params: {
|
params: {
|
||||||
season,
|
season,
|
||||||
title: props.media?.title,
|
title: props.media?.title,
|
||||||
},
|
},
|
||||||
signal,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.id || null
|
return result.id || null
|
||||||
|
|||||||
+6
-1
@@ -21,6 +21,7 @@ import { CronVuetify } from '@vue-js-cron/vuetify'
|
|||||||
import { isPWA } from './@core/utils/navigator'
|
import { isPWA } from './@core/utils/navigator'
|
||||||
import { loadRemoteComponents } from './utils/federationLoader'
|
import { loadRemoteComponents } from './utils/federationLoader'
|
||||||
import { fetchGlobalSettings } from './utils/globalSetting'
|
import { fetchGlobalSettings } from './utils/globalSetting'
|
||||||
|
import { initializeImageOptimizer } from '@/utils/imageOptimizer'
|
||||||
|
|
||||||
// 5. 其他插件和功能模块
|
// 5. 其他插件和功能模块
|
||||||
import Toast from 'vue-toastification'
|
import Toast from 'vue-toastification'
|
||||||
@@ -105,5 +106,9 @@ initializeApp().then(() => {
|
|||||||
})
|
})
|
||||||
.use(ConfirmDialog)
|
.use(ConfirmDialog)
|
||||||
.use(i18n)
|
.use(i18n)
|
||||||
.mount('#app')
|
|
||||||
|
// 初始化全局图片优化器
|
||||||
|
initializeImageOptimizer()
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
})
|
})
|
||||||
|
|||||||
+17
-19
@@ -1,6 +1,8 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import { configureNProgress } from '@/api/nprogress'
|
import { configureNProgress } from '@/api/nprogress'
|
||||||
import { useAuthStore } from '@/stores'
|
import { useAuthStore } from '@/stores'
|
||||||
|
import { setNavigatingState as setImageNavigatingState } from '@/utils/imageOptimizer'
|
||||||
|
import { setNavigatingState as setRequestNavigatingState } from '@/utils/requestOptimizer'
|
||||||
|
|
||||||
// Nprogress
|
// Nprogress
|
||||||
configureNProgress()
|
configureNProgress()
|
||||||
@@ -208,23 +210,12 @@ const router = createRouter({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const abortControllers = new Set<AbortController>()
|
|
||||||
|
|
||||||
// 注册中止控制器
|
|
||||||
function registerAbortController(controller: AbortController) {
|
|
||||||
abortControllers.add(controller)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 中止所有组件的任务
|
|
||||||
function abortAllControllers() {
|
|
||||||
for (const controller of abortControllers) {
|
|
||||||
controller.abort()
|
|
||||||
}
|
|
||||||
abortControllers.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 路由导航守卫
|
// 路由导航守卫
|
||||||
router.beforeEach(async (to: any, from: any, next: any) => {
|
router.beforeEach(async (to: any, from: any, next: any) => {
|
||||||
|
// 设置导航状态 - 同时暂停图片加载和中断API请求
|
||||||
|
setImageNavigatingState(true)
|
||||||
|
setRequestNavigatingState(true)
|
||||||
|
|
||||||
// 认证 Store
|
// 认证 Store
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
// 总是记录非login路由
|
// 总是记录非login路由
|
||||||
@@ -233,15 +224,22 @@ router.beforeEach(async (to: any, from: any, next: any) => {
|
|||||||
|
|
||||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||||
// 用户未登录,重定向到登录页
|
// 用户未登录,重定向到登录页
|
||||||
|
setImageNavigatingState(false)
|
||||||
|
setRequestNavigatingState(false)
|
||||||
next('/login')
|
next('/login')
|
||||||
} else {
|
} else {
|
||||||
// 清理所有中止控制器
|
|
||||||
abortAllControllers()
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 路由导航完成后
|
||||||
|
router.afterEach(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setImageNavigatingState(false)
|
||||||
|
setRequestNavigatingState(false)
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
|
||||||
// 导出默认对象
|
// 导出默认对象
|
||||||
export default router
|
export default router
|
||||||
// 另行导出其他功能
|
// 延迟恢复图片加载,给页面一些初始化时间
|
||||||
export { registerAbortController }
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
let isNavigating = false
|
||||||
|
const MAX_CONCURRENT_IMAGES = 10 // 并发数
|
||||||
|
let currentLoadingCount = 0
|
||||||
|
const imageQueue: { img: HTMLImageElement; src: string }[] = []
|
||||||
|
|
||||||
|
// 监听路由状态
|
||||||
|
export function setNavigatingState(navigating: boolean) {
|
||||||
|
isNavigating = navigating
|
||||||
|
|
||||||
|
if (navigating) {
|
||||||
|
// 路由切换时只是标记状态,不强制中断图片加载
|
||||||
|
console.log('Navigation started - pausing new image loads')
|
||||||
|
} else {
|
||||||
|
// 路由切换完成后,处理队列中的图片
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Navigation ended - resuming image loads')
|
||||||
|
processImageQueue()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图片队列
|
||||||
|
function processImageQueue() {
|
||||||
|
while (imageQueue.length > 0 && currentLoadingCount < MAX_CONCURRENT_IMAGES && !isNavigating) {
|
||||||
|
const { img, src } = imageQueue.shift()!
|
||||||
|
startImageLoad(img, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始加载图片
|
||||||
|
function startImageLoad(img: HTMLImageElement, src: string) {
|
||||||
|
currentLoadingCount++
|
||||||
|
|
||||||
|
const originalOnLoad = img.onload
|
||||||
|
const originalOnError = img.onerror
|
||||||
|
|
||||||
|
img.onload = function (event) {
|
||||||
|
currentLoadingCount--
|
||||||
|
processImageQueue() // 加载完成后处理队列
|
||||||
|
if (originalOnLoad) originalOnLoad.call(this, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = function (event) {
|
||||||
|
currentLoadingCount--
|
||||||
|
processImageQueue() // 出错时也要处理队列
|
||||||
|
if (originalOnError) originalOnError.call(this, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = src
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能图片加载函数
|
||||||
|
function smartImageLoad(img: HTMLImageElement, src: string) {
|
||||||
|
if (isNavigating) {
|
||||||
|
// 路由切换时,加入队列等待
|
||||||
|
imageQueue.push({ img, src })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLoadingCount < MAX_CONCURRENT_IMAGES) {
|
||||||
|
// 直接加载
|
||||||
|
startImageLoad(img, src)
|
||||||
|
} else {
|
||||||
|
// 加入队列
|
||||||
|
imageQueue.push({ img, src })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化图片优化器
|
||||||
|
export function initializeImageOptimizer() {
|
||||||
|
// 只对新创建的img元素进行温和的优化
|
||||||
|
const originalCreateElement = document.createElement
|
||||||
|
document.createElement = function (tagName: string, options?: ElementCreationOptions) {
|
||||||
|
const element = originalCreateElement.call(this, tagName, options)
|
||||||
|
|
||||||
|
if (tagName.toLowerCase() === 'img') {
|
||||||
|
const img = element as HTMLImageElement
|
||||||
|
|
||||||
|
// 只拦截src的设置,使用更温和的方式
|
||||||
|
const originalSetAttribute = img.setAttribute
|
||||||
|
img.setAttribute = function (name: string, value: string) {
|
||||||
|
if (name === 'src' && value) {
|
||||||
|
// 使用智能加载
|
||||||
|
smartImageLoad(this, value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return originalSetAttribute.call(this, name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为现有图片添加懒加载属性
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const images = document.querySelectorAll('img:not([loading])')
|
||||||
|
images.forEach(img => {
|
||||||
|
img.setAttribute('loading', 'lazy')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听新添加的图片
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const element = node as Element
|
||||||
|
|
||||||
|
// 为新添加的img标签设置懒加载
|
||||||
|
if (element.tagName === 'IMG' && !element.getAttribute('loading')) {
|
||||||
|
element.setAttribute('loading', 'lazy')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为子元素中的img设置懒加载
|
||||||
|
const imgs = element.querySelectorAll('img:not([loading])')
|
||||||
|
imgs.forEach(img => {
|
||||||
|
img.setAttribute('loading', 'lazy')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// 全局请求优化器
|
||||||
|
// 自动管理所有API请求的中断,无需手动注册
|
||||||
|
|
||||||
|
let isNavigating = false
|
||||||
|
const activeRequests = new Set<AbortController>()
|
||||||
|
|
||||||
|
// 监听路由状态
|
||||||
|
export function setNavigatingState(navigating: boolean) {
|
||||||
|
isNavigating = navigating
|
||||||
|
|
||||||
|
if (navigating) {
|
||||||
|
// 路由切换时,中断所有未完成的请求
|
||||||
|
console.log('Navigation started - aborting active requests')
|
||||||
|
abortAllActiveRequests()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中断所有活跃的请求
|
||||||
|
function abortAllActiveRequests() {
|
||||||
|
for (const controller of activeRequests) {
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeRequests.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理已完成的请求控制器
|
||||||
|
function cleanupController(controller: AbortController) {
|
||||||
|
activeRequests.delete(controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化请求优化器
|
||||||
|
export function initializeRequestOptimizer(axiosInstance: any) {
|
||||||
|
// 拦截请求,自动添加 AbortController
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
(config: any) => {
|
||||||
|
// 如果请求已经有 signal,跳过(避免覆盖手动设置的)
|
||||||
|
if (config.signal) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的 AbortController
|
||||||
|
const controller = new AbortController()
|
||||||
|
config.signal = controller.signal
|
||||||
|
|
||||||
|
// 将控制器添加到活跃列表
|
||||||
|
activeRequests.add(controller)
|
||||||
|
|
||||||
|
// 监听请求完成事件来清理控制器
|
||||||
|
const cleanup = () => cleanupController(controller)
|
||||||
|
|
||||||
|
// 监听中断事件
|
||||||
|
controller.signal.addEventListener('abort', cleanup, { once: true })
|
||||||
|
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 拦截响应,清理对应的控制器
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response: any) => {
|
||||||
|
// 从配置中获取 signal 对应的控制器并清理
|
||||||
|
if (response.config?.signal) {
|
||||||
|
const controller = Array.from(activeRequests).find(ctrl => ctrl.signal === response.config.signal)
|
||||||
|
if (controller) {
|
||||||
|
cleanupController(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
// 错误时也要清理控制器
|
||||||
|
if (error.config?.signal) {
|
||||||
|
const controller = Array.from(activeRequests).find(ctrl => ctrl.signal === error.config.signal)
|
||||||
|
if (controller) {
|
||||||
|
cleanupController(controller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('Request optimizer initialized - all requests will be auto-managed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前活跃请求数量(调试用)
|
||||||
|
export function getActiveRequestsCount() {
|
||||||
|
return activeRequests.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动中断所有请求(备用方法)
|
||||||
|
export function abortAllRequests() {
|
||||||
|
abortAllActiveRequests()
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import api from '@/api'
|
|||||||
import type { MediaInfo } from '@/api/types'
|
import type { MediaInfo } from '@/api/types'
|
||||||
import MediaCard from '@/components/cards/MediaCard.vue'
|
import MediaCard from '@/components/cards/MediaCard.vue'
|
||||||
import SlideView from '@/components/slide/SlideView.vue'
|
import SlideView from '@/components/slide/SlideView.vue'
|
||||||
import { registerAbortController } from '@/router'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -28,10 +27,7 @@ const dataList = ref<MediaInfo[]>([])
|
|||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
if (!props.apipath) return
|
if (!props.apipath) return
|
||||||
const abortController = new AbortController()
|
dataList.value = await api.get(props.apipath)
|
||||||
registerAbortController(abortController)
|
|
||||||
const { signal } = abortController
|
|
||||||
dataList.value = await api.get(props.apipath, { signal })
|
|
||||||
if (dataList.value.length > 0) componentLoaded.value = true
|
if (dataList.value.length > 0) componentLoaded.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|||||||
Reference in New Issue
Block a user