mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
更新多个组件以支持新的事件通知机制,添加切换到配置页面的功能,调整文档以反映组件文件名的变化,提升用户交互体验。
This commit is contained in:
@@ -17,9 +17,9 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
|
|||||||
|
|
||||||
| 组件名称 | 文件名 | 用途 |
|
| 组件名称 | 文件名 | 用途 |
|
||||||
|---------|-------|------|
|
|---------|-------|------|
|
||||||
| Page | Page.js | 插件详情页面 |
|
| Page | Page.vue | 插件详情页面 |
|
||||||
| Config | Config.js | 插件配置页面 |
|
| Config | Config.vue | 插件配置页面 |
|
||||||
| Dashboard | Dashboard.js | 仪表板组件 |
|
| Dashboard | Dashboard.vue | 仪表板组件 |
|
||||||
|
|
||||||
## 4. 快速开始
|
## 4. 快速开始
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ export default defineConfig({
|
|||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 自定义事件,用于通知主应用刷新数据
|
// 自定义事件,用于通知主应用刷新数据
|
||||||
const emit = defineEmits(['action'])
|
const emit = defineEmits(['action', 'switch'])
|
||||||
|
|
||||||
// 页面逻辑代码...
|
// 页面逻辑代码...
|
||||||
|
|
||||||
@@ -96,12 +96,18 @@ const emit = defineEmits(['action'])
|
|||||||
function notifyRefresh() {
|
function notifyRefresh() {
|
||||||
emit('action')
|
emit('action')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知主应用切换到配置页面
|
||||||
|
function notifySwitch() {
|
||||||
|
emit('switch')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="plugin-page">
|
<div class="plugin-page">
|
||||||
<!-- 插件详情页面内容 -->
|
<!-- 插件详情页面内容 -->
|
||||||
<v-btn @click="notifyRefresh">刷新数据</v-btn>
|
<v-btn @click="notifyRefresh">刷新数据</v-btn>
|
||||||
|
<v-btn @click="notifySwitch">配置插件</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
@@ -181,39 +187,9 @@ const props = defineProps({
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. 后端API要求
|
将生成的dist文件夹上传到插件后端,并配置插件后端API路径。
|
||||||
|
|
||||||
### 7.1 注册远程组件API
|
## 7. 调试与排错
|
||||||
|
|
||||||
后端需要实现以下API用于注册远程组件(已公共实现,插件后端按第三方插件开发要求实现即可):
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/plugins/remotes
|
|
||||||
```
|
|
||||||
|
|
||||||
返回结构:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "my-plugin", // 插件ID,必需
|
|
||||||
"url": "/custom/path/to/plugin" // 自定义组件路径,可选
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "another-plugin" // 使用默认路径
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 组件访问路径
|
|
||||||
|
|
||||||
指定了`url`后使用:
|
|
||||||
|
|
||||||
- `{url}/remoteEntry.js`
|
|
||||||
- `{url}/Page.js`
|
|
||||||
- `{url}/Config.js`
|
|
||||||
- `{url}/Dashboard.js`
|
|
||||||
|
|
||||||
## 8. 调试与排错
|
|
||||||
|
|
||||||
### 常见问题
|
### 常见问题
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,10 @@
|
|||||||
<v-icon left>mdi-refresh</v-icon>
|
<v-icon left>mdi-refresh</v-icon>
|
||||||
刷新数据
|
刷新数据
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn color="primary" @click="notifySwitch">
|
||||||
|
<v-icon left>mdi-cog</v-icon>
|
||||||
|
配置
|
||||||
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +77,7 @@ const status = ref('running')
|
|||||||
const lastUpdated = ref('')
|
const lastUpdated = ref('')
|
||||||
|
|
||||||
// 自定义事件,用于通知主应用刷新数据
|
// 自定义事件,用于通知主应用刷新数据
|
||||||
const emit = defineEmits(['action'])
|
const emit = defineEmits(['action', 'switch'])
|
||||||
|
|
||||||
// 获取状态图标
|
// 获取状态图标
|
||||||
function getItemIcon(type) {
|
function getItemIcon(type) {
|
||||||
@@ -138,6 +142,11 @@ async function refreshData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知主应用切换到配置页面
|
||||||
|
function notifySwitch() {
|
||||||
|
emit('switch')
|
||||||
|
}
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
// 组件挂载时加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshData()
|
refreshData()
|
||||||
|
|||||||
@@ -149,20 +149,20 @@ onBeforeMount(async () => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
<VDialog scrollable max-width="60rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard :title="`${props.plugin?.plugin_name} - ${t('dialog.pluginConfig.title')}`" class="rounded-t">
|
<!-- Vuetify 渲染模式 -->
|
||||||
|
<VCard
|
||||||
|
v-if="renderMode === 'vuetify'"
|
||||||
|
:title="`${props.plugin?.plugin_name} - ${t('dialog.pluginConfig.title')}`"
|
||||||
|
class="rounded-t"
|
||||||
|
>
|
||||||
<VDialogCloseBtn @click="emit('close')" />
|
<VDialogCloseBtn @click="emit('close')" />
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
||||||
<VCardText v-else="isRefreshed">
|
<VCardText v-else="isRefreshed">
|
||||||
<!-- Vuetify 渲染模式 -->
|
<div>
|
||||||
<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" />
|
||||||
<div v-if="!pluginFormItems || pluginFormItems.length === 0">此插件没有可配置项</div>
|
<div v-if="!pluginFormItems || pluginFormItems.length === 0">此插件没有可配置项</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Vue 渲染模式 -->
|
|
||||||
<div v-else-if="renderMode === 'vue'">
|
|
||||||
<component :is="dynamicComponent" :initial-config="pluginConfigForm" @save="handleVueComponentSave" />
|
|
||||||
</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">
|
||||||
@@ -181,6 +181,10 @@ onBeforeMount(async () => {
|
|||||||
</VBtn>
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
<!-- Vue 渲染模式 -->
|
||||||
|
<div v-else-if="renderMode === 'vue'">
|
||||||
|
<component :is="dynamicComponent" :initial-config="pluginConfigForm" @save="handleVueComponentSave" />
|
||||||
|
</div>
|
||||||
<!-- 进度框 -->
|
<!-- 进度框 -->
|
||||||
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
<ProgressDialog v-if="progressDialog" v-model="progressDialog" :text="progressText" />
|
||||||
</VDialog>
|
</VDialog>
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ const appMode = inject('pwaMode') && display.mdAndDown.value
|
|||||||
|
|
||||||
// 是否刷新
|
// 是否刷新
|
||||||
const isRefreshed = ref(false)
|
const isRefreshed = ref(false)
|
||||||
|
// 组件是否已加载成功
|
||||||
|
const componentLoaded = ref(false)
|
||||||
|
// 是否正在加载数据
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
// 渲染模式: 'vuetify' 或 'vue'
|
// 渲染模式: 'vuetify' 或 'vue'
|
||||||
const renderMode = ref('vuetify')
|
const renderMode = ref('vuetify')
|
||||||
@@ -40,10 +44,11 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
|
|
||||||
// 动态加载远程组件
|
// 动态加载远程组件
|
||||||
const module = await loadRemoteComponent(props.plugin.id, 'Page')
|
const module = await loadRemoteComponent(props.plugin.id, 'Page')
|
||||||
|
componentLoaded.value = true
|
||||||
return module
|
return module
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载远程组件失败:', error)
|
console.error('加载远程组件失败:', error)
|
||||||
|
componentLoaded.value = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 加载中显示的组件
|
// 加载中显示的组件
|
||||||
@@ -66,11 +71,21 @@ const dynamicComponent = defineAsyncComponent({
|
|||||||
|
|
||||||
// 调用API读取数据页面UI
|
// 调用API读取数据页面UI
|
||||||
async function loadPluginUIData() {
|
async function loadPluginUIData() {
|
||||||
|
// 如果正在加载,则不重复加载
|
||||||
|
if (isLoading.value) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
isRefreshed.value = false
|
isRefreshed.value = false
|
||||||
pluginPageItems.value = []
|
pluginPageItems.value = []
|
||||||
renderMode.value = 'vuetify'
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 如果已经是vue模式且组件已加载成功,不需要再请求模式
|
||||||
|
if (renderMode.value === 'vue' && componentLoaded.value) {
|
||||||
|
isRefreshed.value = true
|
||||||
|
isLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const result: { [key: string]: any } = await api.get(`plugin/page/${props.plugin?.id}`)
|
const result: { [key: string]: any } = await api.get(`plugin/page/${props.plugin?.id}`)
|
||||||
if (!result || !result.render_mode) {
|
if (!result || !result.render_mode) {
|
||||||
console.error(`插件 ${props.plugin?.plugin_name} UI数据加载失败:无效的响应`)
|
console.error(`插件 ${props.plugin?.plugin_name} UI数据加载失败:无效的响应`)
|
||||||
@@ -85,11 +100,16 @@ async function loadPluginUIData() {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
} finally {
|
} finally {
|
||||||
isRefreshed.value = true
|
isRefreshed.value = true
|
||||||
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载数据(可由 PageRender 或 Vue component 触发)
|
// 重新加载数据(可由 PageRender 或 Vue component 触发)
|
||||||
function handleAction() {
|
function handleAction(event: any) {
|
||||||
|
// 避免在组件已加载的情况下重复调用loadPluginUIData
|
||||||
|
if (renderMode.value === 'vue' && componentLoaded.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
loadPluginUIData()
|
loadPluginUIData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,19 +119,15 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
<VDialog scrollable max-width="80rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
<!-- Vuetify 渲染模式 -->
|
||||||
|
<VCard v-if="renderMode === 'vuetify'" :title="`${props.plugin?.plugin_name}`" class="rounded-t">
|
||||||
<VDialogCloseBtn @click="emit('close')" />
|
<VDialogCloseBtn @click="emit('close')" />
|
||||||
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
||||||
<VCardText v-else class="min-h-40">
|
<VCardText v-else class="min-h-40">
|
||||||
<!-- Vuetify 渲染模式 -->
|
<div>
|
||||||
<div v-if="renderMode === 'vuetify'">
|
|
||||||
<PageRender @action="handleAction" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
<PageRender @action="handleAction" v-for="(item, index) in pluginPageItems" :key="index" :config="item" />
|
||||||
<div v-if="!pluginPageItems || pluginPageItems.length === 0">此插件没有详情页面</div>
|
<div v-if="!pluginPageItems || pluginPageItems.length === 0">此插件没有详情页面</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Vue 渲染模式 -->
|
|
||||||
<div v-else-if="renderMode === 'vue'">
|
|
||||||
<component :is="dynamicComponent" @action="handleAction" />
|
|
||||||
</div>
|
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VFab
|
<VFab
|
||||||
icon="mdi-cog"
|
icon="mdi-cog"
|
||||||
@@ -124,5 +140,9 @@ onMounted(() => {
|
|||||||
:class="{ 'mb-10': appMode }"
|
:class="{ 'mb-10': appMode }"
|
||||||
/>
|
/>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
<!-- Vue 渲染模式 -->
|
||||||
|
<div v-else-if="renderMode === 'vue'">
|
||||||
|
<component :is="dynamicComponent" @action="handleAction" @switch="emit('switch')" />
|
||||||
|
</div>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user