mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-24 01:39:42 +08:00
增强模块联邦支持,添加动态导入远程模块的声明,更新示例项目以展示新组件结构和配置,调整 Vite 配置以支持更灵活的远程组件加载。
This commit is contained in:
@@ -7,12 +7,14 @@ import { useToast } from 'vue-toast-notification'
|
||||
import FormRender from '../render/FormRender.vue'
|
||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import {
|
||||
loadRemoteComponent,
|
||||
clearRemoteComponentCache,
|
||||
registerRemoteComponent,
|
||||
getRemoteComponent,
|
||||
} from '@/utils/remoteFederationLoader'
|
||||
registerRemotePlugin,
|
||||
isRemoteComponentLoaded,
|
||||
ComponentType,
|
||||
} from '@/utils/federationLoader'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -51,28 +53,54 @@ const isRefreshed = ref(false)
|
||||
// 渲染模式: 'vuetify' 或 'vue'
|
||||
const renderMode = ref('vuetify')
|
||||
|
||||
//Vue 模式:组件 URL
|
||||
const vueComponentUrl = ref<string | null>(null)
|
||||
// 挂载状态
|
||||
const componentMounted = ref(false)
|
||||
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicComponent = computed(() => {
|
||||
if (renderMode.value === 'vue' && vueComponentUrl.value) {
|
||||
// 检查是否已经注册,如果没有则进行注册
|
||||
const remoteInfo = props.plugin?.id ? getRemoteComponent(props.plugin.id) : null
|
||||
if (!remoteInfo && props.plugin?.id) {
|
||||
// 动态注册远程组件
|
||||
registerRemoteComponent(props.plugin.id, vueComponentUrl.value)
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicComponent = defineAsyncComponent({
|
||||
loader: async () => {
|
||||
if (renderMode.value !== 'vue' || !props.plugin?.id) {
|
||||
return { render: () => null }
|
||||
}
|
||||
|
||||
// 加载远程组件
|
||||
return loadRemoteComponent(vueComponentUrl.value, {
|
||||
onError: error => {
|
||||
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)
|
||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
return null
|
||||
try {
|
||||
componentMounted.value = false
|
||||
|
||||
// 确保插件已注册
|
||||
if (!isRemoteComponentLoaded(props.plugin.id, ComponentType.CONFIG)) {
|
||||
await registerRemotePlugin(props.plugin.id)
|
||||
}
|
||||
|
||||
// 加载配置组件
|
||||
const component = await loadRemoteComponent(props.plugin.id, ComponentType.CONFIG)
|
||||
componentMounted.value = true
|
||||
|
||||
if (!component) {
|
||||
throw new Error('组件加载失败')
|
||||
}
|
||||
|
||||
return component
|
||||
} catch (error: any) {
|
||||
console.error(`加载插件配置组件失败: ${props.plugin.id}`, error)
|
||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
||||
return {
|
||||
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
|
||||
}
|
||||
}
|
||||
},
|
||||
loadingComponent: {
|
||||
render: () =>
|
||||
h('div', { class: 'text-center pa-4' }, [
|
||||
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
|
||||
'加载组件中...',
|
||||
]),
|
||||
},
|
||||
errorComponent: {
|
||||
render: () => h('div', { class: 'text-error pa-4 text-center' }, '组件加载失败'),
|
||||
},
|
||||
onError: error => {
|
||||
console.error('加载插件组件出错', error)
|
||||
},
|
||||
})
|
||||
|
||||
//调用API读取UI和配置数据
|
||||
@@ -82,12 +110,12 @@ async function loadPluginUIData() {
|
||||
pluginFormItems = []
|
||||
pluginConfigForm.value = {}
|
||||
renderMode.value = 'vuetify'
|
||||
componentMounted.value = false
|
||||
|
||||
// 如果存在旧的组件URL,清除其缓存
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
// 清除组件缓存
|
||||
if (props.plugin?.id) {
|
||||
clearRemoteComponentCache(props.plugin.id)
|
||||
}
|
||||
vueComponentUrl.value = null
|
||||
|
||||
try {
|
||||
// 获取UI定义
|
||||
@@ -98,15 +126,15 @@ async function loadPluginUIData() {
|
||||
}
|
||||
renderMode.value = result.render_mode
|
||||
if (renderMode.value === 'vue') {
|
||||
// 使用 component_url
|
||||
vueComponentUrl.value = result.component_url
|
||||
// 注册远程插件 (如果提供了组件URL,则使用它)
|
||||
if (props.plugin?.id) {
|
||||
registerRemotePlugin(props.plugin.id, result.component_url)
|
||||
}
|
||||
|
||||
// Vue模式下,初始配置在同一个API返回
|
||||
if (!isNullOrEmptyObject(result.model)) {
|
||||
pluginConfigForm.value = result.model
|
||||
}
|
||||
if (!vueComponentUrl.value) {
|
||||
console.error(`插件 ${props.plugin?.plugin_name} 配置错误:未提供Vue组件URL`)
|
||||
}
|
||||
} else {
|
||||
// Vuetify模式
|
||||
pluginFormItems = result.conf || []
|
||||
@@ -153,8 +181,8 @@ onBeforeMount(async () => {
|
||||
|
||||
// 组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
if (props.plugin?.id) {
|
||||
clearRemoteComponentCache(props.plugin.id, ComponentType.CONFIG)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -170,7 +198,7 @@ onUnmounted(() => {
|
||||
<div v-if="!pluginFormItems || pluginFormItems.length === 0">此插件没有可配置项</div>
|
||||
</div>
|
||||
<!-- Vue 渲染模式 -->
|
||||
<div v-else-if="renderMode === 'vue' && dynamicComponent">
|
||||
<div v-else-if="renderMode === 'vue'">
|
||||
<component :is="dynamicComponent" :initial-config="pluginConfigForm" @save="handleVueComponentSave" />
|
||||
</div>
|
||||
<!-- 加载中或错误 -->
|
||||
|
||||
@@ -4,12 +4,14 @@ import type { Plugin } from '@/api/types'
|
||||
import PageRender from '@/components/render/PageRender.vue'
|
||||
import api from '@/api'
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import {
|
||||
loadRemoteComponent,
|
||||
clearRemoteComponentCache,
|
||||
registerRemoteComponent,
|
||||
getRemoteComponent,
|
||||
} from '@/utils/remoteFederationLoader'
|
||||
registerRemotePlugin,
|
||||
isRemoteComponentLoaded,
|
||||
ComponentType,
|
||||
} from '@/utils/federationLoader'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -35,31 +37,57 @@ const isRefreshed = ref(false)
|
||||
// 渲染模式: 'vuetify' 或 'vue'
|
||||
const renderMode = ref('vuetify')
|
||||
|
||||
// Vue 模式:组件 URL
|
||||
const vueComponentUrl = ref<string | null>(null)
|
||||
// 挂载状态
|
||||
const componentMounted = ref(false)
|
||||
|
||||
// 插件数据页面配置项
|
||||
let pluginPageItems = ref([])
|
||||
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicComponent = computed(() => {
|
||||
if (renderMode.value === 'vue' && vueComponentUrl.value) {
|
||||
// 检查是否已经注册,如果没有则进行注册
|
||||
const remoteInfo = props.plugin?.id ? getRemoteComponent(props.plugin.id) : null
|
||||
if (!remoteInfo && props.plugin?.id) {
|
||||
// 动态注册远程组件
|
||||
registerRemoteComponent(props.plugin.id, vueComponentUrl.value)
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicComponent = defineAsyncComponent({
|
||||
loader: async () => {
|
||||
if (renderMode.value !== 'vue' || !props.plugin?.id) {
|
||||
return { render: () => null }
|
||||
}
|
||||
|
||||
// 加载远程组件
|
||||
return loadRemoteComponent(vueComponentUrl.value, {
|
||||
onError: error => {
|
||||
console.error(`加载插件组件失败: ${vueComponentUrl.value}`, error)
|
||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
return null
|
||||
try {
|
||||
componentMounted.value = false
|
||||
|
||||
// 确保插件已注册
|
||||
if (!isRemoteComponentLoaded(props.plugin.id, ComponentType.PAGE)) {
|
||||
await registerRemotePlugin(props.plugin.id)
|
||||
}
|
||||
|
||||
// 加载页面组件
|
||||
const component = await loadRemoteComponent(props.plugin.id, ComponentType.PAGE)
|
||||
componentMounted.value = true
|
||||
|
||||
if (!component) {
|
||||
throw new Error('组件加载失败')
|
||||
}
|
||||
|
||||
return component
|
||||
} catch (error: any) {
|
||||
console.error(`加载插件页面组件失败: ${props.plugin.id}`, error)
|
||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
||||
return {
|
||||
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
|
||||
}
|
||||
}
|
||||
},
|
||||
loadingComponent: {
|
||||
render: () =>
|
||||
h('div', { class: 'text-center pa-4' }, [
|
||||
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
|
||||
'加载组件中...',
|
||||
]),
|
||||
},
|
||||
errorComponent: {
|
||||
render: () => h('div', { class: 'text-error pa-4 text-center' }, '组件加载失败'),
|
||||
},
|
||||
onError: error => {
|
||||
console.error('加载插件组件出错', error)
|
||||
},
|
||||
})
|
||||
|
||||
// 调用API读取数据页面UI
|
||||
@@ -67,12 +95,12 @@ async function loadPluginUIData() {
|
||||
isRefreshed.value = false
|
||||
pluginPageItems.value = []
|
||||
renderMode.value = 'vuetify'
|
||||
componentMounted.value = false
|
||||
|
||||
// 如果存在旧的组件URL,清除其缓存
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
// 清除组件缓存
|
||||
if (props.plugin?.id) {
|
||||
clearRemoteComponentCache(props.plugin.id)
|
||||
}
|
||||
vueComponentUrl.value = null
|
||||
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||
@@ -80,14 +108,15 @@ async function loadPluginUIData() {
|
||||
console.error(`插件 ${props.plugin?.plugin_name} UI数据加载失败:无效的响应`)
|
||||
return
|
||||
}
|
||||
|
||||
renderMode.value = result.render_mode
|
||||
if (renderMode.value === 'vue') {
|
||||
vueComponentUrl.value = result.component_url
|
||||
if (!vueComponentUrl.value) {
|
||||
console.error(`插件 ${props.plugin?.plugin_name} 配置错误:未提供Vue组件URL`)
|
||||
renderMode.value = 'vuetify'
|
||||
// 注册远程插件 (如果提供了组件URL,则使用它)
|
||||
if (props.plugin?.id) {
|
||||
registerRemotePlugin(props.plugin.id, result.component_url)
|
||||
}
|
||||
} else {
|
||||
// Vuetify模式
|
||||
pluginPageItems.value = result.page || []
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -96,6 +125,7 @@ async function loadPluginUIData() {
|
||||
isRefreshed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载数据(可由 PageRender 或 Vue component 触发)
|
||||
function handleAction() {
|
||||
loadPluginUIData()
|
||||
@@ -107,8 +137,8 @@ onMounted(() => {
|
||||
|
||||
// 组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
if (vueComponentUrl.value) {
|
||||
clearRemoteComponentCache(vueComponentUrl.value)
|
||||
if (props.plugin?.id) {
|
||||
clearRemoteComponentCache(props.plugin.id, ComponentType.PAGE)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -124,7 +154,7 @@ onUnmounted(() => {
|
||||
<div v-if="!pluginPageItems || pluginPageItems.length === 0">此插件没有详情页面</div>
|
||||
</div>
|
||||
<!-- Vue 渲染模式 -->
|
||||
<div v-else-if="renderMode === 'vue' && dynamicComponent">
|
||||
<div v-else-if="renderMode === 'vue'">
|
||||
<component :is="dynamicComponent" @action="handleAction" />
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
@@ -12,12 +12,14 @@ import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||
import DashboardRender from '@/components/render/DashboardRender.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import {
|
||||
loadRemoteComponent,
|
||||
clearRemoteComponentCache,
|
||||
registerRemoteComponent,
|
||||
getRemoteComponent,
|
||||
} from '@/utils/remoteFederationLoader'
|
||||
registerRemotePlugin,
|
||||
isRemoteComponentLoaded,
|
||||
ComponentType,
|
||||
} from '@/utils/federationLoader'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -37,27 +39,53 @@ const emit = defineEmits(['update:refreshStatus'])
|
||||
// 插件UI渲染模式 ('vuetify' 或 'vue')
|
||||
const pluginRenderMode = computed(() => props.config?.render_mode || 'vuetify')
|
||||
|
||||
// 挂载状态
|
||||
const componentMounted = ref(false)
|
||||
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicPluginComponent = computed(() => {
|
||||
// 确保 config 存在并且 component_url 也存在
|
||||
if (pluginRenderMode.value === 'vue' && props.config?.component_url) {
|
||||
// 如果有插件ID,尝试注册远程组件
|
||||
if (props.config.id) {
|
||||
const remoteInfo = getRemoteComponent(props.config.id)
|
||||
if (!remoteInfo) {
|
||||
// 动态注册远程组件
|
||||
registerRemoteComponent(props.config.id, props.config.component_url)
|
||||
}
|
||||
const dynamicPluginComponent = defineAsyncComponent({
|
||||
loader: async () => {
|
||||
if (pluginRenderMode.value !== 'vue' || !props.config?.id) {
|
||||
return { render: () => null }
|
||||
}
|
||||
|
||||
// 加载远程组件
|
||||
return loadRemoteComponent(props.config.component_url, {
|
||||
onError: error => {
|
||||
console.error(`加载插件组件失败: ${props.config?.component_url}`, error)
|
||||
},
|
||||
})
|
||||
}
|
||||
return null
|
||||
try {
|
||||
componentMounted.value = false
|
||||
|
||||
// 确保插件已注册
|
||||
if (!isRemoteComponentLoaded(props.config.id, ComponentType.DASHBOARD)) {
|
||||
await registerRemotePlugin(props.config.id, props.config.component_url)
|
||||
}
|
||||
|
||||
// 加载仪表板组件
|
||||
const component = await loadRemoteComponent(props.config.id, ComponentType.DASHBOARD)
|
||||
componentMounted.value = true
|
||||
|
||||
if (!component) {
|
||||
throw new Error('组件加载失败')
|
||||
}
|
||||
|
||||
return component
|
||||
} catch (error: any) {
|
||||
console.error(`加载插件仪表板组件失败: ${props.config.id}`, error)
|
||||
return {
|
||||
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
|
||||
}
|
||||
}
|
||||
},
|
||||
loadingComponent: {
|
||||
render: () =>
|
||||
h('div', { class: 'text-center pa-4' }, [
|
||||
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
|
||||
'加载组件中...',
|
||||
]),
|
||||
},
|
||||
errorComponent: {
|
||||
render: () => h('div', { class: 'text-error pa-4 text-center' }, '组件加载失败'),
|
||||
},
|
||||
onError: error => {
|
||||
console.error('加载插件组件出错', error)
|
||||
},
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -65,8 +93,8 @@ onUnmounted(() => {
|
||||
emit('update:refreshStatus', false)
|
||||
|
||||
// 清理远程组件缓存
|
||||
if (pluginRenderMode.value === 'vue' && props.config?.component_url) {
|
||||
clearRemoteComponentCache(props.config.component_url)
|
||||
if (props.config?.id) {
|
||||
clearRemoteComponentCache(props.config.id, ComponentType.DASHBOARD)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -85,7 +113,7 @@ onUnmounted(() => {
|
||||
<!-- 插件仪表板 -->
|
||||
<template v-else-if="!isNullOrEmptyObject(props.config)">
|
||||
<!-- Vue 渲染模式 -->
|
||||
<div v-if="pluginRenderMode === 'vue' && dynamicPluginComponent">
|
||||
<div v-if="pluginRenderMode === 'vue'">
|
||||
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" />
|
||||
<!-- Vue 模式下也可以显示拖拽句柄 -->
|
||||
<div class="absolute right-5 top-5">
|
||||
|
||||
Reference in New Issue
Block a user