mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
更新模块联邦文档,调整远程组件API路径格式,优化组件加载逻辑,移除不必要的注册步骤,增强代码可读性。
This commit is contained in:
@@ -194,10 +194,10 @@ npm run build
|
|||||||
|
|
||||||
这些文件需要部署到后端,并通过以下URL可访问:
|
这些文件需要部署到后端,并通过以下URL可访问:
|
||||||
|
|
||||||
- `/api/plugin/component/{插件ID}/remoteEntry.js`
|
- `/api/v1/plugin/file/{插件ID}/dist/remoteEntry.js`
|
||||||
- `/api/plugin/component/{插件ID}/Page.js`
|
- `/api/v1/plugin/file/{插件ID}/dist/Page.js`
|
||||||
- `/api/plugin/component/{插件ID}/Config.js`
|
- `/api/v1/plugin/file/{插件ID}/dist/Config.js`
|
||||||
- `/api/plugin/component/{插件ID}/Dashboard.js`
|
- `/api/v1/plugin/file/{插件ID}/dist/Dashboard.js`
|
||||||
|
|
||||||
## 7. 后端API要求
|
## 7. 后端API要求
|
||||||
|
|
||||||
|
|||||||
@@ -51,17 +51,3 @@ api.interceptors.response.use(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|
||||||
export async function fetchGlobalSettings() {
|
|
||||||
try {
|
|
||||||
const result: { [key: string]: any } = await api.get('system/global', {
|
|
||||||
params: {
|
|
||||||
token: 'moviepilot',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return result.data || {}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch global settings', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ import FormRender from '../render/FormRender.vue'
|
|||||||
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
import ProgressDialog from '../dialog/ProgressDialog.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import {
|
import { loadRemoteComponent, clearRemoteComponentCache, ComponentType } from '@/utils/federationLoader'
|
||||||
loadRemoteComponent,
|
|
||||||
clearRemoteComponentCache,
|
|
||||||
registerRemotePlugin,
|
|
||||||
isRemoteComponentLoaded,
|
|
||||||
ComponentType,
|
|
||||||
} from '@/utils/federationLoader'
|
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -64,14 +58,8 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
componentMounted.value = false
|
|
||||||
|
|
||||||
// 确保插件已注册
|
|
||||||
if (!isRemoteComponentLoaded(props.plugin.id, ComponentType.CONFIG)) {
|
|
||||||
await registerRemotePlugin(props.plugin.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载配置组件
|
// 加载配置组件
|
||||||
|
componentMounted.value = false
|
||||||
const component = await loadRemoteComponent(props.plugin.id, ComponentType.CONFIG)
|
const component = await loadRemoteComponent(props.plugin.id, ComponentType.CONFIG)
|
||||||
componentMounted.value = true
|
componentMounted.value = true
|
||||||
|
|
||||||
@@ -82,7 +70,6 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
return component
|
return component
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`加载插件配置组件失败: ${props.plugin.id}`, error)
|
console.error(`加载插件配置组件失败: ${props.plugin.id}`, error)
|
||||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
|
||||||
return {
|
return {
|
||||||
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
|
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
|
||||||
}
|
}
|
||||||
@@ -91,7 +78,7 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
loadingComponent: {
|
loadingComponent: {
|
||||||
render: () =>
|
render: () =>
|
||||||
h('div', { class: 'text-center pa-4' }, [
|
h('div', { class: 'text-center pa-4' }, [
|
||||||
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
|
h('VProgressCircular', { indeterminate: true, class: 'mr-2' }),
|
||||||
'加载组件中...',
|
'加载组件中...',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@@ -110,7 +97,6 @@ async function loadPluginUIData() {
|
|||||||
pluginFormItems = []
|
pluginFormItems = []
|
||||||
pluginConfigForm.value = {}
|
pluginConfigForm.value = {}
|
||||||
renderMode.value = 'vuetify'
|
renderMode.value = 'vuetify'
|
||||||
componentMounted.value = false
|
|
||||||
|
|
||||||
// 清除组件缓存
|
// 清除组件缓存
|
||||||
if (props.plugin?.id) {
|
if (props.plugin?.id) {
|
||||||
@@ -126,11 +112,6 @@ async function loadPluginUIData() {
|
|||||||
}
|
}
|
||||||
renderMode.value = result.render_mode
|
renderMode.value = result.render_mode
|
||||||
if (renderMode.value === 'vue') {
|
if (renderMode.value === 'vue') {
|
||||||
// 注册远程插件 (如果提供了组件URL,则使用它)
|
|
||||||
if (props.plugin?.id) {
|
|
||||||
registerRemotePlugin(props.plugin.id, result.component_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vue模式下,初始配置在同一个API返回
|
// Vue模式下,初始配置在同一个API返回
|
||||||
if (!isNullOrEmptyObject(result.model)) {
|
if (!isNullOrEmptyObject(result.model)) {
|
||||||
pluginConfigForm.value = result.model
|
pluginConfigForm.value = result.model
|
||||||
@@ -191,7 +172,8 @@ onUnmounted(() => {
|
|||||||
<VCard :title="`${props.plugin?.plugin_name} - ${t('dialog.pluginConfig.title')}`" class="rounded-t">
|
<VCard :title="`${props.plugin?.plugin_name} - ${t('dialog.pluginConfig.title')}`" class="rounded-t">
|
||||||
<VDialogCloseBtn @click="emit('close')" />
|
<VDialogCloseBtn @click="emit('close')" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText v-if="isRefreshed">
|
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
||||||
|
<VCardText v-else="isRefreshed">
|
||||||
<!-- Vuetify 渲染模式 -->
|
<!-- Vuetify 渲染模式 -->
|
||||||
<div v-if="renderMode === 'vuetify'">
|
<div v-if="renderMode === 'vuetify'">
|
||||||
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :model="pluginConfigForm" />
|
<FormRender v-for="(item, index) in pluginFormItems" :key="index" :config="item" :model="pluginConfigForm" />
|
||||||
@@ -201,8 +183,6 @@ onUnmounted(() => {
|
|||||||
<div v-else-if="renderMode === 'vue'">
|
<div v-else-if="renderMode === 'vue'">
|
||||||
<component :is="dynamicComponent" :initial-config="pluginConfigForm" @save="handleVueComponentSave" />
|
<component :is="dynamicComponent" :initial-config="pluginConfigForm" @save="handleVueComponentSave" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 加载中或错误 -->
|
|
||||||
<div v-else><VProgressCircular indeterminate /> 加载中...</div>
|
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions class="pt-3">
|
<VCardActions class="pt-3">
|
||||||
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" variant="outlined" color="info">
|
<VBtn v-if="props.plugin?.has_page" @click="emit('switch')" variant="outlined" color="info">
|
||||||
|
|||||||
@@ -5,13 +5,7 @@ import PageRender from '@/components/render/PageRender.vue'
|
|||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useToast } from 'vue-toast-notification'
|
import { useToast } from 'vue-toast-notification'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import {
|
import { loadRemoteComponent, clearRemoteComponentCache, ComponentType } from '@/utils/federationLoader'
|
||||||
loadRemoteComponent,
|
|
||||||
clearRemoteComponentCache,
|
|
||||||
registerRemotePlugin,
|
|
||||||
isRemoteComponentLoaded,
|
|
||||||
ComponentType,
|
|
||||||
} from '@/utils/federationLoader'
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -51,14 +45,8 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
componentMounted.value = false
|
|
||||||
|
|
||||||
// 确保插件已注册
|
|
||||||
if (!isRemoteComponentLoaded(props.plugin.id, ComponentType.PAGE)) {
|
|
||||||
await registerRemotePlugin(props.plugin.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载页面组件
|
// 加载页面组件
|
||||||
|
componentMounted.value = false
|
||||||
const component = await loadRemoteComponent(props.plugin.id, ComponentType.PAGE)
|
const component = await loadRemoteComponent(props.plugin.id, ComponentType.PAGE)
|
||||||
componentMounted.value = true
|
componentMounted.value = true
|
||||||
|
|
||||||
@@ -69,7 +57,6 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
return component
|
return component
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`加载插件页面组件失败: ${props.plugin.id}`, error)
|
console.error(`加载插件页面组件失败: ${props.plugin.id}`, error)
|
||||||
$toast.error(`加载插件组件失败: ${error.message || '未知错误'}`)
|
|
||||||
return {
|
return {
|
||||||
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
|
render: () => h('div', { class: 'text-error pa-4' }, `加载失败: ${error.message || '未知错误'}`),
|
||||||
}
|
}
|
||||||
@@ -78,7 +65,7 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
loadingComponent: {
|
loadingComponent: {
|
||||||
render: () =>
|
render: () =>
|
||||||
h('div', { class: 'text-center pa-4' }, [
|
h('div', { class: 'text-center pa-4' }, [
|
||||||
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
|
h('VProgressCircular', { indeterminate: true, class: 'mr-2' }),
|
||||||
'加载组件中...',
|
'加载组件中...',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@@ -95,7 +82,6 @@ async function loadPluginUIData() {
|
|||||||
isRefreshed.value = false
|
isRefreshed.value = false
|
||||||
pluginPageItems.value = []
|
pluginPageItems.value = []
|
||||||
renderMode.value = 'vuetify'
|
renderMode.value = 'vuetify'
|
||||||
componentMounted.value = false
|
|
||||||
|
|
||||||
// 清除组件缓存
|
// 清除组件缓存
|
||||||
if (props.plugin?.id) {
|
if (props.plugin?.id) {
|
||||||
@@ -108,14 +94,8 @@ async function loadPluginUIData() {
|
|||||||
console.error(`插件 ${props.plugin?.plugin_name} UI数据加载失败:无效的响应`)
|
console.error(`插件 ${props.plugin?.plugin_name} UI数据加载失败:无效的响应`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMode.value = result.render_mode
|
renderMode.value = result.render_mode
|
||||||
if (renderMode.value === 'vue') {
|
if (renderMode.value === 'vuetify') {
|
||||||
// 注册远程插件 (如果提供了组件URL,则使用它)
|
|
||||||
if (props.plugin?.id) {
|
|
||||||
registerRemotePlugin(props.plugin.id, result.component_url)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Vuetify模式
|
// Vuetify模式
|
||||||
pluginPageItems.value = result.page || []
|
pluginPageItems.value = result.page || []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,7 @@ import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
|||||||
import DashboardRender from '@/components/render/DashboardRender.vue'
|
import DashboardRender from '@/components/render/DashboardRender.vue'
|
||||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import {
|
import { loadRemoteComponent, clearRemoteComponentCache, ComponentType } from '@/utils/federationLoader'
|
||||||
loadRemoteComponent,
|
|
||||||
clearRemoteComponentCache,
|
|
||||||
registerRemotePlugin,
|
|
||||||
isRemoteComponentLoaded,
|
|
||||||
ComponentType,
|
|
||||||
} from '@/utils/federationLoader'
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -50,14 +44,8 @@ const dynamicPluginComponent = defineAsyncComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
componentMounted.value = false
|
|
||||||
|
|
||||||
// 确保插件已注册
|
|
||||||
if (!isRemoteComponentLoaded(props.config.id, ComponentType.DASHBOARD)) {
|
|
||||||
await registerRemotePlugin(props.config.id, props.config.component_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载仪表板组件
|
// 加载仪表板组件
|
||||||
|
componentMounted.value = false
|
||||||
const component = await loadRemoteComponent(props.config.id, ComponentType.DASHBOARD)
|
const component = await loadRemoteComponent(props.config.id, ComponentType.DASHBOARD)
|
||||||
componentMounted.value = true
|
componentMounted.value = true
|
||||||
|
|
||||||
@@ -76,7 +64,7 @@ const dynamicPluginComponent = defineAsyncComponent({
|
|||||||
loadingComponent: {
|
loadingComponent: {
|
||||||
render: () =>
|
render: () =>
|
||||||
h('div', { class: 'text-center pa-4' }, [
|
h('div', { class: 'text-center pa-4' }, [
|
||||||
h('v-progress-circular', { indeterminate: true, class: 'mr-2' }),
|
h('VProgressCircular', { indeterminate: true, class: 'mr-2' }),
|
||||||
'加载组件中...',
|
'加载组件中...',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -18,9 +18,9 @@ import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
|
|||||||
import { CronVuetify } from '@vue-js-cron/vuetify'
|
import { CronVuetify } from '@vue-js-cron/vuetify'
|
||||||
|
|
||||||
// 4. 工具函数和其他辅助模块
|
// 4. 工具函数和其他辅助模块
|
||||||
import { fetchGlobalSettings } from './api'
|
|
||||||
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'
|
||||||
|
|
||||||
// 5. 其他插件和功能模块
|
// 5. 其他插件和功能模块
|
||||||
import ToastPlugin from 'vue-toast-notification'
|
import ToastPlugin from 'vue-toast-notification'
|
||||||
|
|||||||
@@ -54,13 +54,12 @@ async function loadRemoteEntry(url: string): Promise<void> {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 创建script标签
|
// 创建script标签
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.src = url
|
script.src = `${import.meta.env.VITE_API_BASE_URL}${url}`
|
||||||
script.type = 'text/javascript'
|
script.type = 'text/javascript'
|
||||||
script.async = true
|
script.async = true
|
||||||
|
|
||||||
// 加载成功
|
// 加载成功
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
console.log(`远程模块加载成功: ${url}`)
|
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,11 +83,11 @@ async function loadRemoteEntry(url: string): Promise<void> {
|
|||||||
function getRemoteEntryUrl(pluginId: string, url?: string): string {
|
function getRemoteEntryUrl(pluginId: string, url?: string): string {
|
||||||
// 如果提供了完整URL则直接使用
|
// 如果提供了完整URL则直接使用
|
||||||
if (url && (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'))) {
|
if (url && (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'))) {
|
||||||
return url.endsWith('/remoteEntry.js') ? url : `${url}/remoteEntry.js`
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
// 否则使用默认路径格式
|
// 否则使用默认路径格式
|
||||||
return `/api/plugin/component/${pluginId}/remoteEntry.js`
|
return `/api/v1/plugin/file/${pluginId.toLowerCase()}/dist/remoteEntry.js`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +115,7 @@ function getComponentModule(componentType: ComponentType): string {
|
|||||||
export async function loadRemoteComponents(): Promise<boolean> {
|
export async function loadRemoteComponents(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// 调用后端API获取远程组件列表
|
// 调用后端API获取远程组件列表
|
||||||
const result = await api.get('plugins/remotes')
|
const result = await api.get('plugin/remotes?token=moviepilot')
|
||||||
|
|
||||||
if (!result || !Array.isArray(result)) {
|
if (!result || !Array.isArray(result)) {
|
||||||
console.error('获取远程组件列表失败:无效的响应格式')
|
console.error('获取远程组件列表失败:无效的响应格式')
|
||||||
@@ -175,16 +174,16 @@ export async function registerRemotePlugin(pluginId: string, url?: string): Prom
|
|||||||
// 动态导入远程模块
|
// 动态导入远程模块
|
||||||
return async () => {
|
return async () => {
|
||||||
// 这里可能需要根据实际情况调整
|
// 这里可能需要根据实际情况调整
|
||||||
|
const moduleName = module.replace('./', '')
|
||||||
try {
|
try {
|
||||||
// 理论上这里应该使用模块联邦的import机制
|
// 理论上这里应该使用模块联邦的import机制
|
||||||
// 但由于我们是运行时加载,需要用一种变通方式
|
// 但由于我们是运行时加载,需要用一种变通方式
|
||||||
const moduleUrl = `${remoteEntryUrl.replace('remoteEntry.js', '')}${module.replace('./', '')}.js`
|
const moduleUrl = `${remoteEntryUrl.replace('remoteEntry.js', '')}${moduleName}.js`
|
||||||
|
|
||||||
// 使用动态导入
|
// 使用动态导入
|
||||||
const moduleExports = await import(/* @vite-ignore */ moduleUrl)
|
const moduleExports = await import(/* @vite-ignore */ moduleUrl)
|
||||||
return { default: moduleExports.default }
|
return { default: moduleExports.default }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`加载远程模块失败: ${remoteId}/${module}`, error)
|
console.error(`加载远程模块失败: ${remoteId}/${moduleName}`, error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,8 +253,6 @@ export async function loadRemoteComponent(pluginId: string, componentType: Compo
|
|||||||
error: error,
|
error: error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`加载远程组件失败: ${pluginId}/${componentType}`, error)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,7 +278,6 @@ async function loadRemoteComponentModule(pluginId: string, componentType: Compon
|
|||||||
// 返回组件
|
// 返回组件
|
||||||
return Module.default
|
return Module.default
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`加载远程组件模块失败: ${remoteId}/${moduleName}`, error)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import api from '@/api'
|
||||||
|
export async function fetchGlobalSettings() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/global', {
|
||||||
|
params: {
|
||||||
|
token: 'moviepilot',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return result.data || {}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch global settings', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user