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