mirror of https://github.com/halo-dev/halo-admin
feat: add editor extension point (#781)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 添加编辑器的扩展点,用于扩展集成其他编辑器。 定义一个扩展点的方式: ```ts export default definePlugin({ extensionPoints: { "editor:create": () => { return [ { name: "stackedit", displayName: "StackEdit", component: markRaw(StackEdit), rawType: "markdown", }, ]; }, }, }); ``` 其中 `component` 字段即编辑器组件对象,需要包含 `raw`、`content` 的 prop。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2881 #### Screenshots: <img width="1664" alt="image" src="https://user-images.githubusercontent.com/21301288/208406097-60258cba-cff6-436f-bd50-6d8c27ea9a53.png"> <img width="1662" alt="image" src="https://user-images.githubusercontent.com/21301288/208406174-d4649365-3448-4581-a452-f9781502eac6.png"> <img width="1920" alt="image" src="https://user-images.githubusercontent.com/21301288/208407570-db10e956-cd6a-4e0d-801e-b794ad0261bc.png"> <img width="1920" alt="image" src="https://user-images.githubusercontent.com/21301288/208407607-fd595957-5278-40c2-a3b5-fb73c1de429c.png"> #### Special notes for your reviewer: 目前可用于测试的插件: 1. [plugin-stackedit-1.0.0-SNAPSHOT.jar.zip](https://github.com/halo-dev/console/files/10258488/plugin-stackedit-1.0.0-SNAPSHOT.jar.zip) 2. [plugin-bytemd-1.0.0-SNAPSHOT.jar.zip](https://github.com/halo-dev/console/files/10258490/plugin-bytemd-1.0.0-SNAPSHOT.jar.zip) 测试方式: 1. Console 需要 `pnpm build:packages`。 2. 在 Console 的插件管理上传以上插件。 3. 新建若干文章,使用不同的编辑器。 4. 检查是否能够正常发布和编辑。 5. 检查编辑的时候,是否正确使用了之前的编辑器。 6. 检查主题端是否渲染正常。 一些实现细节: 1. 为了支持更新文章时能够选择发布时的编辑器,会在 post 的 `metadata.annotations` 添加一条 `content.halo.run/preferred-editor` 用于标记使用的什么编辑器。如果编辑器不存在,会使用 content 的 `rawType` 来匹配。 2. 目前没有全局默认编辑器设置,只能在新建文章的时候选择。 #### Does this PR introduce a user-facing change? ```release-note Console 端支持扩展集成其他编辑器 ```pull/789/head^2
parent
d575b6698b
commit
a396aad87f
|
@ -0,0 +1,73 @@
|
|||
# 编辑器集成扩展点
|
||||
|
||||
## 定义方式
|
||||
|
||||
```ts
|
||||
import MarkdownEditor from "./components/MarkdownEditor.vue"
|
||||
|
||||
export default definePlugin({
|
||||
extensionPoints: {
|
||||
"editor:create": () => {
|
||||
return [
|
||||
{
|
||||
name: "markdown-editor",
|
||||
displayName: "Markdown",
|
||||
component: markRaw(MarkdownEditor),
|
||||
rawType: "markdown",
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- name: 编辑器名称,用于标识编辑器
|
||||
- displayName: 编辑器显示名称
|
||||
- component: 编辑器组件
|
||||
- rawType: 编辑器支持的原始类型,可以完全由插件定义。但必须保证最终能够将渲染后的 html 设置到 content 中。
|
||||
|
||||
## 组件
|
||||
|
||||
组件必须设置两个 `v-model` 绑定。即 `v-model:raw` 和 `v-model:content`,以下是示例:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<textarea :value="raw" @input="onRawUpdate" />
|
||||
<div v-html="content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from "vue";
|
||||
import marked from "marked";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
raw?: string;
|
||||
content: string;
|
||||
}>(),
|
||||
{
|
||||
raw: "",
|
||||
content: "",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:raw", value: string): void;
|
||||
(event: "update:content", value: string): void;
|
||||
}>();
|
||||
|
||||
function onRawUpdate(e: Event) {
|
||||
const raw = (e.target as HTMLTextAreaElement).value;
|
||||
emit("update:raw", raw);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.raw,
|
||||
() => {
|
||||
emit("update:content", marked(props.raw));
|
||||
}
|
||||
);
|
||||
</script>
|
||||
```
|
|
@ -3,3 +3,4 @@ export * from "./types/menus";
|
|||
export * from "./core/plugins";
|
||||
export * from "./states/pages";
|
||||
export * from "./states/attachment-selector";
|
||||
export * from "./states/editor";
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import type { Component } from "vue";
|
||||
|
||||
export interface EditorProvider {
|
||||
name: string;
|
||||
displayName: string;
|
||||
component: Component;
|
||||
rawType: string;
|
||||
}
|
|
@ -2,6 +2,7 @@ import type { Component } from "vue";
|
|||
import type { RouteRecordRaw, RouteRecordName } from "vue-router";
|
||||
import type { FunctionalPage } from "../states/pages";
|
||||
import type { AttachmentSelectProvider } from "../states/attachment-selector";
|
||||
import type { EditorProvider } from "..";
|
||||
|
||||
export interface RouteRecordAppend {
|
||||
parentName: RouteRecordName;
|
||||
|
@ -14,6 +15,8 @@ export interface ExtensionPoint {
|
|||
"attachment:selector:create"?: () =>
|
||||
| AttachmentSelectProvider[]
|
||||
| Promise<AttachmentSelectProvider[]>;
|
||||
|
||||
"editor:create"?: () => EditorProvider[] | Promise<EditorProvider[]>;
|
||||
}
|
||||
|
||||
export interface PluginModule {
|
||||
|
|
|
@ -89,30 +89,39 @@ import MdiFormatHeader3 from "~icons/mdi/format-header-3";
|
|||
import MdiFormatHeader4 from "~icons/mdi/format-header-4";
|
||||
import MdiFormatHeader5 from "~icons/mdi/format-header-5";
|
||||
import MdiFormatHeader6 from "~icons/mdi/format-header-6";
|
||||
import { computed, markRaw, nextTick, ref, watch } from "vue";
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
markRaw,
|
||||
nextTick,
|
||||
ref,
|
||||
watch,
|
||||
type ComputedRef,
|
||||
} from "vue";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { useAttachmentSelect } from "@/modules/contents/attachments/composables/use-attachment";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string;
|
||||
owner?: string;
|
||||
permalink?: string;
|
||||
publishTime?: string | null;
|
||||
raw?: string;
|
||||
content: string;
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
owner: undefined,
|
||||
permalink: undefined,
|
||||
publishTime: undefined,
|
||||
raw: "",
|
||||
content: "",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: string): void;
|
||||
(event: "update:raw", value: string): void;
|
||||
(event: "update:content", value: string): void;
|
||||
(event: "update", value: string): void;
|
||||
}>();
|
||||
|
||||
const owner = inject<ComputedRef<string | undefined>>("owner");
|
||||
const publishTime = inject<ComputedRef<string | undefined>>("publishTime");
|
||||
const permalink = inject<ComputedRef<string | undefined>>("permalink");
|
||||
|
||||
interface HeadingNode {
|
||||
id: string;
|
||||
level: number;
|
||||
|
@ -134,7 +143,7 @@ const extraActiveId = ref("toc");
|
|||
const attachmentSelectorModal = ref(false);
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue,
|
||||
content: props.raw,
|
||||
extensions: [
|
||||
ExtensionBlockquote,
|
||||
ExtensionBold,
|
||||
|
@ -226,7 +235,8 @@ const editor = useEditor({
|
|||
],
|
||||
autofocus: "start",
|
||||
onUpdate: () => {
|
||||
emit("update:modelValue", editor.value?.getHTML() + "");
|
||||
emit("update:raw", editor.value?.getHTML() + "");
|
||||
emit("update:content", editor.value?.getHTML() + "");
|
||||
emit("update", editor.value?.getHTML() + "");
|
||||
nextTick(() => {
|
||||
handleGenerateTableOfContent();
|
||||
|
@ -340,10 +350,10 @@ const handleSelectHeadingNode = (node: HeadingNode) => {
|
|||
const { onAttachmentSelect } = useAttachmentSelect(editor);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => props.raw,
|
||||
() => {
|
||||
if (props.modelValue !== editor.value?.getHTML()) {
|
||||
editor.value?.commands.setContent(props.modelValue);
|
||||
if (props.raw !== editor.value?.getHTML()) {
|
||||
editor.value?.commands.setContent(props.raw);
|
||||
nextTick(() => {
|
||||
handleGenerateTableOfContent();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
||||
import { markRaw, onMounted, ref, type Ref } from "vue";
|
||||
|
||||
interface useEditorExtensionPointsReturn {
|
||||
editorProviders: Ref<EditorProvider[]>;
|
||||
}
|
||||
|
||||
export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
||||
// resolve plugin extension points
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
const editorProviders = ref<EditorProvider[]>([
|
||||
{
|
||||
name: "default",
|
||||
displayName: "默认编辑器",
|
||||
component: markRaw(DefaultEditor),
|
||||
rawType: "HTML",
|
||||
},
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||
const { extensionPoints } = pluginModule;
|
||||
if (!extensionPoints?.["editor:create"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providers = extensionPoints["editor:create"]() as EditorProvider[];
|
||||
|
||||
if (providers) {
|
||||
providers.forEach((provider) => {
|
||||
editorProviders.value.push(provider);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
editorProviders,
|
||||
};
|
||||
}
|
|
@ -8,21 +8,34 @@ import {
|
|||
VButton,
|
||||
IconSave,
|
||||
Toast,
|
||||
Dialog,
|
||||
} from "@halo-dev/components";
|
||||
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||
import PostPreviewModal from "../posts/components/PostPreviewModal.vue";
|
||||
import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client";
|
||||
import { computed, onMounted, ref, toRef } from "vue";
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
toRef,
|
||||
type ComputedRef,
|
||||
} from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { useRouter } from "vue-router";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { useContentCache } from "@/composables/use-content-cache";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
const currentEditorProvider = ref<EditorProvider>();
|
||||
|
||||
const initialFormState: SinglePageRequest = {
|
||||
page: {
|
||||
spec: {
|
||||
|
@ -47,6 +60,7 @@ const initialFormState: SinglePageRequest = {
|
|||
kind: "SinglePage",
|
||||
metadata: {
|
||||
name: randomUUID(),
|
||||
annotations: {},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
|
@ -66,15 +80,26 @@ const isUpdateMode = computed(() => {
|
|||
return !!formState.value.page.metadata.creationTimestamp;
|
||||
});
|
||||
|
||||
// provide some data to editor
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"owner",
|
||||
computed(() => formState.value.page.spec.owner)
|
||||
);
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"publishTime",
|
||||
computed(() => formState.value.page.spec.publishTime)
|
||||
);
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"permalink",
|
||||
computed(() => formState.value.page.status?.permalink)
|
||||
);
|
||||
|
||||
const routeQueryName = useRouteQuery<string>("name");
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
saving.value = true;
|
||||
|
||||
// Set rendered content
|
||||
formState.value.content.content = formState.value.content.raw;
|
||||
|
||||
//Set default title and slug
|
||||
if (!formState.value.page.spec.title) {
|
||||
formState.value.page.spec.title = "无标题页面";
|
||||
|
@ -116,9 +141,6 @@ const handlePublish = async () => {
|
|||
try {
|
||||
publishing.value = true;
|
||||
|
||||
// Set rendered content
|
||||
formState.value.content.content = formState.value.content.raw;
|
||||
|
||||
if (isUpdateMode.value) {
|
||||
const { name: singlePageName } = formState.value.page.metadata;
|
||||
const { permalink } = formState.value.page.status || {};
|
||||
|
@ -171,6 +193,38 @@ const handleFetchContent = async () => {
|
|||
snapshotName: formState.value.page.spec.headSnapshot,
|
||||
});
|
||||
|
||||
// get editor provider
|
||||
if (!currentEditorProvider.value) {
|
||||
const preferredEditor = editorProviders.value.find(
|
||||
(provider) =>
|
||||
provider.name ===
|
||||
formState.value.page.metadata.annotations?.[
|
||||
"content.halo.run/preferred-editor"
|
||||
]
|
||||
);
|
||||
const provider =
|
||||
preferredEditor ||
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.rawType === data.rawType
|
||||
);
|
||||
if (provider) {
|
||||
currentEditorProvider.value = provider;
|
||||
formState.value.page.metadata.annotations = {
|
||||
...formState.value.page.metadata.annotations,
|
||||
"content.halo.run/preferred-editor": provider.name,
|
||||
};
|
||||
} else {
|
||||
Dialog.warning({
|
||||
title: "警告",
|
||||
description: `未找到符合 ${data.rawType} 格式的编辑器,请检查是否已安装编辑器插件`,
|
||||
onConfirm: () => {
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
}
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
formState.value.content = Object.assign(formState.value.content, data);
|
||||
};
|
||||
|
||||
|
@ -203,6 +257,7 @@ const onSettingPublished = (singlePage: SinglePage) => {
|
|||
handlePublish();
|
||||
};
|
||||
|
||||
const editor = useRouteQuery("editor");
|
||||
onMounted(async () => {
|
||||
if (routeQueryName.value) {
|
||||
const { data: singlePage } =
|
||||
|
@ -213,7 +268,21 @@ onMounted(async () => {
|
|||
|
||||
// fetch single page content
|
||||
await handleFetchContent();
|
||||
} else {
|
||||
// Set default editor
|
||||
const provider =
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.name === editor.value
|
||||
) || editorProviders.value[0];
|
||||
if (provider) {
|
||||
currentEditorProvider.value = provider;
|
||||
formState.value.content.rawType = provider.rawType;
|
||||
}
|
||||
formState.value.page.metadata.annotations = {
|
||||
"content.halo.run/preferred-editor": provider.name,
|
||||
};
|
||||
}
|
||||
|
||||
handleResetCache();
|
||||
});
|
||||
|
||||
|
@ -281,11 +350,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
</template>
|
||||
</VPageHeader>
|
||||
<div class="editor border-t" style="height: calc(100vh - 3.5rem)">
|
||||
<DefaultEditor
|
||||
v-model="formState.content.raw"
|
||||
:owner="formState.page.spec.owner"
|
||||
:permalink="formState.page.status?.permalink"
|
||||
:publish-time="formState.page.spec.publishTime"
|
||||
<component
|
||||
:is="currentEditorProvider.component"
|
||||
v-if="currentEditorProvider"
|
||||
v-model:raw="formState.content.raw"
|
||||
v-model:content="formState.content.content"
|
||||
class="h-full"
|
||||
@update="handleSetContentCache"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -11,10 +11,13 @@ import {
|
|||
} from "@halo-dev/components";
|
||||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
|
||||
interface PageTab {
|
||||
id: string;
|
||||
label: string;
|
||||
|
@ -75,7 +78,38 @@ watchEffect(() => {
|
|||
<VButton :route="{ name: 'DeletedSinglePages' }" size="sm">
|
||||
回收站
|
||||
</VButton>
|
||||
<FloatingDropdown
|
||||
v-if="editorProviders.length > 1"
|
||||
v-permission="['system:singlepages:manage']"
|
||||
>
|
||||
<VButton type="secondary">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
</VButton>
|
||||
<template #popper>
|
||||
<div class="w-48 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
<VButton
|
||||
v-for="(editorProvider, index) in editorProviders"
|
||||
:key="index"
|
||||
v-close-popper
|
||||
block
|
||||
type="default"
|
||||
:route="{
|
||||
name: 'SinglePageEditor',
|
||||
query: { editor: editorProvider.name },
|
||||
}"
|
||||
>
|
||||
{{ editorProvider.displayName }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
</template>
|
||||
</FloatingDropdown>
|
||||
<VButton
|
||||
v-else
|
||||
v-permission="['system:singlepages:manage']"
|
||||
:route="{ name: 'SinglePageEditor' }"
|
||||
type="secondary"
|
||||
|
|
|
@ -8,21 +8,34 @@ import {
|
|||
VPageHeader,
|
||||
VSpace,
|
||||
Toast,
|
||||
Dialog,
|
||||
} from "@halo-dev/components";
|
||||
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||
import PostPreviewModal from "./components/PostPreviewModal.vue";
|
||||
import type { Post, PostRequest } from "@halo-dev/api-client";
|
||||
import { computed, onMounted, ref, toRef } from "vue";
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
toRef,
|
||||
type ComputedRef,
|
||||
} from "vue";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useRouter } from "vue-router";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { useContentCache } from "@/composables/use-content-cache";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
const currentEditorProvider = ref<EditorProvider>();
|
||||
|
||||
const initialFormState: PostRequest = {
|
||||
post: {
|
||||
spec: {
|
||||
|
@ -49,6 +62,7 @@ const initialFormState: PostRequest = {
|
|||
kind: "Post",
|
||||
metadata: {
|
||||
name: randomUUID(),
|
||||
annotations: {},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
|
@ -68,13 +82,24 @@ const isUpdateMode = computed(() => {
|
|||
return !!formState.value.post.metadata.creationTimestamp;
|
||||
});
|
||||
|
||||
// provide some data to editor
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"owner",
|
||||
computed(() => formState.value.post.spec.owner)
|
||||
);
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"publishTime",
|
||||
computed(() => formState.value.post.spec.publishTime)
|
||||
);
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"permalink",
|
||||
computed(() => formState.value.post.status?.permalink)
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
saving.value = true;
|
||||
|
||||
// Set rendered content
|
||||
formState.value.content.content = formState.value.content.raw;
|
||||
|
||||
// Set default title and slug
|
||||
if (!formState.value.post.spec.title) {
|
||||
formState.value.post.spec.title = "无标题文章";
|
||||
|
@ -116,9 +141,6 @@ const handlePublish = async () => {
|
|||
try {
|
||||
publishing.value = true;
|
||||
|
||||
// Set rendered content
|
||||
formState.value.content.content = formState.value.content.raw;
|
||||
|
||||
if (isUpdateMode.value) {
|
||||
const { name: postName } = formState.value.post.metadata;
|
||||
const { permalink } = formState.value.post.status || {};
|
||||
|
@ -176,6 +198,42 @@ const handleFetchContent = async () => {
|
|||
snapshotName: formState.value.post.spec.headSnapshot,
|
||||
});
|
||||
|
||||
// get editor provider
|
||||
if (!currentEditorProvider.value) {
|
||||
const preferredEditor = editorProviders.value.find(
|
||||
(provider) =>
|
||||
provider.name ===
|
||||
formState.value.post.metadata.annotations?.[
|
||||
"content.halo.run/preferred-editor"
|
||||
]
|
||||
);
|
||||
|
||||
const provider =
|
||||
preferredEditor ||
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.rawType === data.rawType
|
||||
);
|
||||
|
||||
if (provider) {
|
||||
currentEditorProvider.value = provider;
|
||||
|
||||
formState.value.post.metadata.annotations = {
|
||||
...formState.value.post.metadata.annotations,
|
||||
"content.halo.run/preferred-editor": provider.name,
|
||||
};
|
||||
} else {
|
||||
Dialog.warning({
|
||||
title: "警告",
|
||||
description: `未找到符合 ${data.rawType} 格式的编辑器,请检查是否已安装编辑器插件`,
|
||||
onConfirm: () => {
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
formState.value.content = Object.assign(formState.value.content, data);
|
||||
};
|
||||
|
||||
|
@ -210,6 +268,7 @@ const onSettingPublished = (post: Post) => {
|
|||
|
||||
// Get post data when the route contains the name parameter
|
||||
const name = useRouteQuery("name");
|
||||
const editor = useRouteQuery("editor");
|
||||
onMounted(async () => {
|
||||
if (name.value) {
|
||||
// fetch post
|
||||
|
@ -221,6 +280,21 @@ onMounted(async () => {
|
|||
|
||||
// fetch post content
|
||||
await handleFetchContent();
|
||||
} else {
|
||||
// Set default editor
|
||||
const provider =
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.name === editor.value
|
||||
) || editorProviders.value[0];
|
||||
|
||||
if (provider) {
|
||||
currentEditorProvider.value = provider;
|
||||
formState.value.content.rawType = provider.rawType;
|
||||
}
|
||||
|
||||
formState.value.post.metadata.annotations = {
|
||||
"content.halo.run/preferred-editor": provider.name,
|
||||
};
|
||||
}
|
||||
handleResetCache();
|
||||
});
|
||||
|
@ -289,11 +363,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
</template>
|
||||
</VPageHeader>
|
||||
<div class="editor border-t" style="height: calc(100vh - 3.5rem)">
|
||||
<DefaultEditor
|
||||
v-model="formState.content.raw"
|
||||
:owner="formState.post.spec.owner"
|
||||
:permalink="formState.post.status?.permalink"
|
||||
:publish-time="formState.post.spec.publishTime"
|
||||
<component
|
||||
:is="currentEditorProvider.component"
|
||||
v-if="currentEditorProvider"
|
||||
v-model:raw="formState.content.raw"
|
||||
v-model:content="formState.content.content"
|
||||
class="h-full"
|
||||
@update="handleSetContentCache"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -45,9 +45,12 @@ import FilterTag from "@/components/filter/FilterTag.vue";
|
|||
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import { getNode } from "@formkit/core";
|
||||
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
|
||||
const posts = ref<ListedPostList>({
|
||||
page: 1,
|
||||
size: 20,
|
||||
|
@ -476,7 +479,40 @@ const hasFilters = computed(() => {
|
|||
<VButton :route="{ name: 'Categories' }" size="sm">分类</VButton>
|
||||
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
|
||||
<VButton :route="{ name: 'DeletedPosts' }" size="sm">回收站</VButton>
|
||||
|
||||
<FloatingDropdown
|
||||
v-if="editorProviders.length > 1"
|
||||
v-permission="['system:posts:manage']"
|
||||
>
|
||||
<VButton type="secondary">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
新建
|
||||
</VButton>
|
||||
<template #popper>
|
||||
<div class="w-48 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
<VButton
|
||||
v-for="(editorProvider, index) in editorProviders"
|
||||
:key="index"
|
||||
v-close-popper
|
||||
block
|
||||
type="default"
|
||||
:route="{
|
||||
name: 'PostEditor',
|
||||
query: { editor: editorProvider.name },
|
||||
}"
|
||||
>
|
||||
{{ editorProvider.displayName }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</div>
|
||||
</template>
|
||||
</FloatingDropdown>
|
||||
|
||||
<VButton
|
||||
v-else
|
||||
v-permission="['system:posts:manage']"
|
||||
:route="{ name: 'PostEditor' }"
|
||||
type="secondary"
|
||||
|
|
Loading…
Reference in New Issue