mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 09:16:58 +08:00
feat(plugin): expose host toast to federated views
This commit is contained in:
@@ -6,7 +6,6 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
|
||||
|
||||
关联阅读后端插件开发文档:[第三方插件开发说明](https://github.com/jxxghp/MoviePilot-Plugins/blob/main/README.md)
|
||||
|
||||
|
||||
## 2. 技术要求
|
||||
|
||||
- Node.js 20+
|
||||
@@ -19,7 +18,7 @@ MoviePilot前端采用模块联邦(Module Federation)技术实现插件的动态
|
||||
每个 Vue 联邦插件需要提供下列标准组件(`AppPage` 为可选,用于主界面侧栏全页入口):
|
||||
|
||||
| 组件名称 | 暴露名 | 文件名 | 用途 |
|
||||
|---------|--------|--------|------|
|
||||
| --------- | ---------------- | ---------------------- | --------------------------------------------- |
|
||||
| Page | `./Page` | Page.vue | 插件管理中的详情弹窗 |
|
||||
| Config | `./Config` | Config.vue | 插件配置页面 |
|
||||
| Dashboard | `./Dashboard` | Dashboard.vue | 仪表盘小组件 |
|
||||
@@ -79,8 +78,8 @@ export default defineConfig({
|
||||
singleton: true,
|
||||
},
|
||||
},
|
||||
format: 'esm'
|
||||
})
|
||||
format: 'esm',
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
target: 'esnext', // 必须设置为esnext以支持顶层await
|
||||
@@ -91,43 +90,40 @@ export default defineConfig({
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '/* 覆盖vuetify样式 */',
|
||||
}
|
||||
},
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
{
|
||||
postcssPlugin: 'internal:charset-removal',
|
||||
AtRule: {
|
||||
charset: (atRule) => {
|
||||
charset: atRule => {
|
||||
if (atRule.name === 'charset') {
|
||||
atRule.remove();
|
||||
}
|
||||
}
|
||||
atRule.remove()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
postcssPlugin: 'vuetify-filter',
|
||||
Root(root) {
|
||||
// 过滤掉所有vuetify相关的CSS
|
||||
root.walkRules(rule => {
|
||||
if (rule.selector && (
|
||||
rule.selector.includes('.v-') ||
|
||||
rule.selector.includes('.mdi-'))) {
|
||||
rule.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
if (rule.selector && (rule.selector.includes('.v-') || rule.selector.includes('.mdi-'))) {
|
||||
rule.remove()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5001, // 使用不同于主应用的端口
|
||||
cors: true, // 启用CORS
|
||||
origin: 'http://localhost:5001'
|
||||
origin: 'http://localhost:5001',
|
||||
},
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
## 5. 组件开发规范
|
||||
@@ -143,8 +139,8 @@ const emit = defineEmits(['action', 'switch', 'close'])
|
||||
const props = defineProps({
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
// 页面逻辑代码...
|
||||
@@ -183,16 +179,16 @@ function notifyClose() {
|
||||
const props = defineProps({
|
||||
initialConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
default: () => ({}),
|
||||
},
|
||||
api: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
|
||||
// 配置数据
|
||||
const config = ref({...props.initialConfig})
|
||||
const config = ref({ ...props.initialConfig })
|
||||
|
||||
// 自定义事件,用于保存配置
|
||||
const emit = defineEmits(['save', 'close', 'switch'])
|
||||
@@ -238,12 +234,12 @@ function notifyClose() {
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
default: () => ({}),
|
||||
},
|
||||
allowRefresh: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 仪表板逻辑...
|
||||
@@ -277,7 +273,7 @@ const props = defineProps({
|
||||
主应用传入的 props:
|
||||
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| ---------- | ----------------------------------------------------- |
|
||||
| `api` | 与 `Page` 相同,用于 `bear` 认证的插件 HTTP 调用 |
|
||||
| `navKey` | 与侧栏声明的 `nav_key` 一致,同一插件多入口时用于区分 |
|
||||
| `pluginId` | 当前插件 ID |
|
||||
@@ -300,12 +296,31 @@ const emit = defineEmits(['action'])
|
||||
</template>
|
||||
```
|
||||
|
||||
### 5.5 调用主应用 Toast
|
||||
|
||||
`Page`、`Config`、`Dashboard` 与 `AppPage` 的宿主容器会通过固定键提供主应用 Toast。远程组件应复用该实例,不要自行渲染 `VSnackbar` 或创建另一套 Toast 容器:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
const toast = inject<any>('moviepilot:toast', null)
|
||||
|
||||
// 保存完成后调用主应用的统一通知。
|
||||
function saveComplete() {
|
||||
toast?.success('保存成功')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
可用方法与主项目 `vue-toastification` 一致,包括 `success`、`info`、`warning` 和 `error`。注入不存在时应静默降级,关键错误仍需保留页面内状态提示。
|
||||
|
||||
#### 后端:注册侧栏入口
|
||||
|
||||
插件需为 **Vue** 渲染模式(`get_render_mode` 返回 `vue`),并实现 `get_sidebar_nav`,返回列表项字段与主应用 `GET /api/v1/plugin/sidebar_nav` 一致:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| ------------ | ------------------------------------------------------------------------------------- |
|
||||
| `nav_key` | URL 路径段,唯一标识本入口(同一插件可多入口) |
|
||||
| `title` | 侧栏显示标题 |
|
||||
| `icon` | MDI 图标名,如 `mdi-rss` |
|
||||
@@ -334,7 +349,7 @@ def get_sidebar_nav(self) -> List[Dict[str, Any]]:
|
||||
前端加载远程组件的顺序为:
|
||||
|
||||
| `nav_key` | 依次尝试的联邦暴露名 |
|
||||
|-----------|----------------------|
|
||||
| -------------------------------- | ------------------------------------------------ |
|
||||
| `main` 或省略 | `./AppPage` → `./Page` |
|
||||
| 其它(如 `settings`、`my_tool`) | `./AppPage{PascalCase}` → `./AppPage` → `./Page` |
|
||||
|
||||
@@ -365,8 +380,7 @@ yarn build
|
||||
|
||||
- 将生成的dist文件夹上传到插件后端目录下(默认为`dist/assets`)
|
||||
|
||||
**注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*`、`_plugin-vue_export-helper-*`、`remoteEntry.js`
|
||||
|
||||
**注意: `__federation_shared_vuetify` 目录以及 `index-`、`date-`、`runtime-` 开头的文件不需要上传**,只需要上传以下命名格式文件:`__federation_*`、`_plugin-vue_export-helper-*`、`remoteEntry.js`
|
||||
|
||||
- 在插件的后端python代码中,实现以下方法来集成远程组件:
|
||||
|
||||
@@ -381,6 +395,7 @@ def get_render_mode() -> Tuple[str, str]:
|
||||
```
|
||||
|
||||
- 需要在插件前端页面调用后端接口时,通过传入的api模块发起调用,后端api接口声明认证类型为:`bear`
|
||||
|
||||
```typescript
|
||||
// 演示使用api模块调用插件接口
|
||||
recentItems.value = await props.api.get(`plugin/MyPlugin/history`)
|
||||
@@ -402,7 +417,6 @@ def get_api(self) -> List[Dict[str, Any]]:
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## 7. 调试与排错
|
||||
|
||||
### 常见问题
|
||||
|
||||
@@ -40,6 +40,9 @@ const progressText = ref('')
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 向联邦插件提供主应用 Toast,避免远程组件自行创建通知容器。
|
||||
provide('moviepilot:toast', $toast)
|
||||
|
||||
// 是否刷新
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import PageRender from '@/components/render/PageRender.vue'
|
||||
import api from '@/api'
|
||||
import { loadRemoteComponent } from '@/utils/federationLoader'
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -26,6 +27,10 @@ const display = useDisplay()
|
||||
// PWA模式检测
|
||||
const { appMode } = usePWA()
|
||||
|
||||
// 向联邦插件提供主应用 Toast,确保通知沿用统一主题和路由逻辑。
|
||||
const $toast = useToast()
|
||||
provide('moviepilot:toast', $toast)
|
||||
|
||||
// 是否刷新
|
||||
const isRefreshed = ref(false)
|
||||
// 组件是否已加载成功
|
||||
|
||||
@@ -5,9 +5,14 @@ import { DashboardItem } from '@/api/types'
|
||||
import DashboardRender from '@/components/render/DashboardRender.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { loadRemoteComponent } from '@/utils/federationLoader'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
type DashboardComponentLoader = () => Promise<any>
|
||||
|
||||
// 仪表板联邦组件复用主应用 Toast 实例。
|
||||
const $toast = useToast()
|
||||
provide('moviepilot:toast', $toast)
|
||||
|
||||
const DashboardSkeleton = {
|
||||
setup() {
|
||||
const SkeletonLoader = resolveComponent('VSkeletonLoader')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Component } from 'vue'
|
||||
import api from '@/api'
|
||||
import { loadRemoteAppPageComponent } from '@/utils/federationLoader'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -11,6 +12,10 @@ const navKey = computed(() => (route.params.navKey as string) || 'main')
|
||||
const RemoteView = shallowRef<Component | null>(null)
|
||||
const loadError = ref(false)
|
||||
|
||||
// 侧栏联邦页面复用主应用 Toast 实例。
|
||||
const $toast = useToast()
|
||||
provide('moviepilot:toast', $toast)
|
||||
|
||||
watch(
|
||||
[pluginId, navKey],
|
||||
async ([pid, nk]) => {
|
||||
|
||||
Reference in New Issue
Block a user