mirror of https://github.com/halo-dev/halo-admin
refactor: plugin extension points of console plugin (#738)
#### What type of PR is this? /kind improvement /milestone 2.0 #### What this PR does / why we need it: 重构 Console 端插件扩展点的定义方式。现在需要如下定义: ```ts extensionPoints: { "page:functional:create": () => { return [ { name: "链接", url: "/links", path: "/pages/functional/links", permissions: ["plugin:links:view"], }, ]; }, }, ``` #### Special notes for your reviewer: 可以用以下插件进行测试: - https://github.com/halo-sigs/plugin-links - https://github.com/halo-sigs/plugin-unsplash #### Does this PR introduce a user-facing change? ```release-note None ```pull/741/head
parent
7e2a2852ea
commit
8b24cee0f6
|
@ -1,5 +1,5 @@
|
|||
import type { Plugin } from "../types/plugin";
|
||||
import type { PluginModule } from "../types/plugin";
|
||||
|
||||
export function definePlugin(plugin: Plugin): Plugin {
|
||||
export function definePlugin(plugin: PluginModule): PluginModule {
|
||||
return plugin;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import type { Attachment } from "@halo-dev/api-client";
|
||||
import type { Component } from "vue";
|
||||
|
||||
export interface AttachmentSelectorPublicState {
|
||||
providers: AttachmentProvider[];
|
||||
}
|
||||
|
||||
export type AttachmentLike =
|
||||
| Attachment
|
||||
| string
|
||||
|
@ -13,7 +9,7 @@ export type AttachmentLike =
|
|||
type: string;
|
||||
};
|
||||
|
||||
export interface AttachmentProvider {
|
||||
export interface AttachmentSelectProvider {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Component | string;
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
export interface PagesPublicState {
|
||||
functionalPages: FunctionalPagesState[];
|
||||
}
|
||||
|
||||
export interface FunctionalPagesState {
|
||||
export interface FunctionalPage {
|
||||
name: string;
|
||||
path: string;
|
||||
url?: string;
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import type { Component, Ref } from "vue";
|
||||
import type { Component } from "vue";
|
||||
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
|
||||
import type { PagesPublicState } from "../states/pages";
|
||||
import type { AttachmentSelectorPublicState } from "../states/attachment-selector";
|
||||
|
||||
export type ExtensionPointName = "PAGES" | "POSTS" | "ATTACHMENT_SELECTOR";
|
||||
|
||||
export type ExtensionPointState =
|
||||
| PagesPublicState
|
||||
| AttachmentSelectorPublicState;
|
||||
import type { FunctionalPage } from "../states/pages";
|
||||
import type { AttachmentSelectProvider } from "../states/attachment-selector";
|
||||
|
||||
export interface RouteRecordAppend {
|
||||
parentName: RouteRecordName;
|
||||
route: RouteRecordRaw;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
export interface ExtensionPoint {
|
||||
"page:functional:create"?: () => FunctionalPage[] | Promise<FunctionalPage[]>;
|
||||
|
||||
"attachment:selector:create"?: () =>
|
||||
| AttachmentSelectProvider[]
|
||||
| Promise<AttachmentSelectProvider[]>;
|
||||
}
|
||||
|
||||
export interface PluginModule {
|
||||
/**
|
||||
* These components will be registered when plugin is activated.
|
||||
*/
|
||||
|
@ -34,7 +34,5 @@ export interface Plugin {
|
|||
|
||||
routes?: RouteRecordRaw[] | RouteRecordAppend[];
|
||||
|
||||
extensionPoints?: {
|
||||
[key in ExtensionPointName]?: (state: Ref<ExtensionPointState>) => void;
|
||||
};
|
||||
extensionPoints?: ExtensionPoint;
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { usePluginStore } from "@/stores/plugin";
|
||||
import type {
|
||||
ExtensionPointName,
|
||||
ExtensionPointState,
|
||||
} from "@halo-dev/console-shared";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
export function useExtensionPointsState(
|
||||
point: ExtensionPointName,
|
||||
state: Ref<ExtensionPointState>
|
||||
) {
|
||||
const { plugins } = usePluginStore();
|
||||
|
||||
plugins.forEach((plugin: Plugin) => {
|
||||
// @ts-ignore
|
||||
if (!plugin.spec.module?.extensionPoints?.[point]) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
plugin.spec.module.extensionPoints[point]?.(state);
|
||||
});
|
||||
}
|
14
src/main.ts
14
src/main.ts
|
@ -3,7 +3,7 @@ import type { DirectiveBinding } from "vue";
|
|||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import type { Plugin, RouteRecordAppend } from "@halo-dev/console-shared";
|
||||
import type { PluginModule, RouteRecordAppend } from "@halo-dev/console-shared";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
// setup
|
||||
|
@ -12,7 +12,7 @@ import { setupComponents } from "./setup/setupComponents";
|
|||
// core modules
|
||||
import { coreModules } from "./modules";
|
||||
import { useScriptTag } from "@vueuse/core";
|
||||
import { usePluginStore } from "@/stores/plugin";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
@ -26,7 +26,7 @@ setupComponents(app);
|
|||
|
||||
app.use(createPinia());
|
||||
|
||||
function registerModule(pluginModule: Plugin, core: boolean) {
|
||||
function registerModule(pluginModule: PluginModule, core: boolean) {
|
||||
if (pluginModule.components) {
|
||||
Object.keys(pluginModule.components).forEach((key) => {
|
||||
const component = pluginModule.components?.[key];
|
||||
|
@ -38,7 +38,6 @@ function registerModule(pluginModule: Plugin, core: boolean) {
|
|||
|
||||
if (pluginModule.routes) {
|
||||
if (!Array.isArray(pluginModule.routes)) {
|
||||
console.error(`${pluginModule.name}: Plugin routes must be an array`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -86,7 +85,7 @@ function loadCoreModules() {
|
|||
});
|
||||
}
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const pluginModuleStore = usePluginModuleStore();
|
||||
|
||||
function loadStyle(href: string) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
|
@ -144,9 +143,8 @@ async function loadPluginModules() {
|
|||
const pluginModule = window[plugin.metadata.name];
|
||||
|
||||
if (pluginModule) {
|
||||
// @ts-ignore
|
||||
plugin.spec.module = pluginModule;
|
||||
registerModule(pluginModule, false);
|
||||
pluginModuleStore.registerPluginModule(pluginModule);
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `${plugin.metadata.name}: 加载插件入口文件失败`;
|
||||
|
@ -164,8 +162,6 @@ async function loadPluginModules() {
|
|||
pluginErrorMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
pluginStore.registerPlugin(plugin);
|
||||
}
|
||||
|
||||
if (pluginErrorMessages.length > 0) {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import { VButton, VModal, VTabbar } from "@halo-dev/components";
|
||||
import { ref, markRaw } from "vue";
|
||||
import { ref, markRaw, onMounted } from "vue";
|
||||
import CoreSelectorProvider from "./selector-providers/CoreSelectorProvider.vue";
|
||||
import UploadSelectorProvider from "./selector-providers/UploadSelectorProvider.vue";
|
||||
import type {
|
||||
AttachmentLike,
|
||||
AttachmentSelectorPublicState,
|
||||
AttachmentSelectProvider,
|
||||
PluginModule,
|
||||
} from "@halo-dev/console-shared";
|
||||
import { useExtensionPointsState } from "@/composables/usePlugins";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
@ -26,24 +27,42 @@ const emit = defineEmits<{
|
|||
|
||||
const selected = ref<AttachmentLike[]>([] as AttachmentLike[]);
|
||||
|
||||
const attachmentSelectorPublicState = ref<AttachmentSelectorPublicState>({
|
||||
providers: [
|
||||
{
|
||||
id: "core",
|
||||
label: "附件库",
|
||||
component: markRaw(CoreSelectorProvider),
|
||||
},
|
||||
{
|
||||
id: "upload",
|
||||
label: "上传",
|
||||
component: markRaw(UploadSelectorProvider),
|
||||
},
|
||||
],
|
||||
const attachmentSelectProviders = ref<AttachmentSelectProvider[]>([
|
||||
{
|
||||
id: "core",
|
||||
label: "附件库",
|
||||
component: markRaw(CoreSelectorProvider),
|
||||
},
|
||||
{
|
||||
id: "upload",
|
||||
label: "上传",
|
||||
component: markRaw(UploadSelectorProvider),
|
||||
},
|
||||
]);
|
||||
|
||||
// resolve plugin extension points
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
onMounted(() => {
|
||||
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||
const { extensionPoints } = pluginModule;
|
||||
if (!extensionPoints?.["attachment:selector:create"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providers = extensionPoints[
|
||||
"attachment:selector:create"
|
||||
]() as AttachmentSelectProvider[];
|
||||
|
||||
if (providers) {
|
||||
providers.forEach((provider) => {
|
||||
attachmentSelectProviders.value.push(provider);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
useExtensionPointsState("ATTACHMENT_SELECTOR", attachmentSelectorPublicState);
|
||||
|
||||
const activeId = ref(attachmentSelectorPublicState.value.providers[0].id);
|
||||
const activeId = ref(attachmentSelectProviders.value[0].id);
|
||||
|
||||
const onVisibleChange = (visible: boolean) => {
|
||||
emit("update:visible", visible);
|
||||
|
@ -53,7 +72,7 @@ const onVisibleChange = (visible: boolean) => {
|
|||
};
|
||||
|
||||
const onChangeProvider = (providerId: string) => {
|
||||
const provider = attachmentSelectorPublicState.value.providers.find(
|
||||
const provider = attachmentSelectProviders.value.find(
|
||||
(provider) => provider.id === providerId
|
||||
);
|
||||
|
||||
|
@ -80,14 +99,14 @@ const handleConfirm = () => {
|
|||
>
|
||||
<VTabbar
|
||||
v-model:active-id="activeId"
|
||||
:items="attachmentSelectorPublicState.providers"
|
||||
:items="attachmentSelectProviders"
|
||||
class="w-full"
|
||||
type="outline"
|
||||
></VTabbar>
|
||||
|
||||
<div v-if="visible" class="mt-2">
|
||||
<template
|
||||
v-for="(provider, index) in attachmentSelectorPublicState.providers"
|
||||
v-for="(provider, index) in attachmentSelectProviders"
|
||||
:key="index"
|
||||
>
|
||||
<Suspense>
|
||||
|
|
|
@ -6,7 +6,6 @@ import { IconFolder } from "@halo-dev/components";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "attachmentModule",
|
||||
components: {
|
||||
AttachmentSelectorModal,
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@ import CommentStatsWidget from "./widgets/CommentStatsWidget.vue";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "commentModule",
|
||||
components: {
|
||||
CommentStatsWidget,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import {
|
||||
VEmpty,
|
||||
VSpace,
|
||||
|
@ -8,19 +8,37 @@ import {
|
|||
VEntity,
|
||||
VEntityField,
|
||||
} from "@halo-dev/components";
|
||||
import type { PagesPublicState } from "@halo-dev/console-shared";
|
||||
import { useExtensionPointsState } from "@/composables/usePlugins";
|
||||
import type { FunctionalPage, PluginModule } from "@halo-dev/console-shared";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
|
||||
const pagesPublicState = ref<PagesPublicState>({
|
||||
functionalPages: [],
|
||||
const functionalPages = ref<FunctionalPage[]>([] as FunctionalPage[]);
|
||||
|
||||
// resolve plugin extension points
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
onMounted(() => {
|
||||
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||
const { extensionPoints } = pluginModule;
|
||||
if (!extensionPoints?.["page:functional:create"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pages = extensionPoints[
|
||||
"page:functional:create"
|
||||
]() as FunctionalPage[];
|
||||
|
||||
if (pages) {
|
||||
pages.forEach((page) => {
|
||||
functionalPages.value.push(page);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
useExtensionPointsState("PAGES", pagesPublicState);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VEmpty
|
||||
v-if="!pagesPublicState.functionalPages.length"
|
||||
v-if="!functionalPages.length"
|
||||
message="当前没有功能页面,功能页面通常由各个插件提供,你可以尝试安装新插件以获得支持"
|
||||
title="当前没有功能页面"
|
||||
>
|
||||
|
@ -41,7 +59,7 @@ useExtensionPointsState("PAGES", pagesPublicState);
|
|||
role="list"
|
||||
>
|
||||
<li
|
||||
v-for="(page, index) in pagesPublicState.functionalPages"
|
||||
v-for="(page, index) in functionalPages"
|
||||
:key="index"
|
||||
v-permission="page.permissions"
|
||||
>
|
||||
|
|
|
@ -11,7 +11,6 @@ import { IconPages } from "@halo-dev/components";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "pageModule",
|
||||
components: {
|
||||
SinglePageStatsWidget,
|
||||
},
|
||||
|
|
|
@ -12,7 +12,6 @@ import RecentPublishedWidget from "./widgets/RecentPublishedWidget.vue";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "postModule",
|
||||
components: {
|
||||
PostStatsWidget,
|
||||
RecentPublishedWidget,
|
||||
|
|
|
@ -8,7 +8,6 @@ import ViewsStatsWidget from "./widgets/ViewsStatsWidget.vue";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "dashboardModule",
|
||||
components: {
|
||||
QuickLinkWidget,
|
||||
ViewsStatsWidget,
|
||||
|
|
|
@ -5,7 +5,6 @@ import { IconListSettings } from "@halo-dev/components";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "menuModule",
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
|
|
|
@ -6,7 +6,6 @@ import { IconPalette } from "@halo-dev/components";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "themeModule",
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
|
|
|
@ -9,7 +9,6 @@ import { IconPlug } from "@halo-dev/components";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "pluginModule",
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
|
|
|
@ -4,7 +4,6 @@ import RoleList from "./RoleList.vue";
|
|||
import RoleDetail from "./RoleDetail.vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "roleModule",
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
|
|
|
@ -5,7 +5,6 @@ import { IconSettings } from "@halo-dev/components";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "settingModule",
|
||||
components: {},
|
||||
routes: [
|
||||
{
|
||||
|
|
|
@ -11,7 +11,6 @@ import { IconUserSettings } from "@halo-dev/components";
|
|||
import { markRaw } from "vue";
|
||||
|
||||
export default definePlugin({
|
||||
name: "userModule",
|
||||
components: {
|
||||
UserStatsWidget,
|
||||
},
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
import type { PluginModule } from "@halo-dev/console-shared";
|
||||
|
||||
interface PluginStoreState {
|
||||
plugins: Plugin[];
|
||||
pluginModules: PluginModule[];
|
||||
}
|
||||
|
||||
export const usePluginStore = defineStore("plugin", {
|
||||
export const usePluginModuleStore = defineStore("plugin", {
|
||||
state: (): PluginStoreState => ({
|
||||
plugins: [],
|
||||
pluginModules: [],
|
||||
}),
|
||||
actions: {
|
||||
registerPlugin(plugin: Plugin) {
|
||||
this.plugins.push(plugin);
|
||||
registerPluginModule(pluginModule: PluginModule) {
|
||||
this.pluginModules.push(pluginModule);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue