mirror of https://github.com/halo-dev/halo-admin
refactor: editor provider switching (#803)
#### What type of PR is this? /kind improvement #### What this PR does / why we need it: 重构编辑器的选择方式,改为在编辑页面选择,并支持缓存上一次的选择。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3060 #### Screenshots: <img width="1662" alt="image" src="https://user-images.githubusercontent.com/21301288/209756212-b14725da-3d89-4416-8860-e75cdb270b75.png"> #### Special notes for your reviewer: 测试方式: 1. 此改动针对文章和自定义页面的编辑。 2. 测试新建页面选择编辑器是否正常。 3. 测试重新进入新建页面,编辑器是否自动选择为了上一次的编辑器。 4. 测试发布文章或者自定义页面之后,重新编辑时,是否选择了正确的编辑器。 编辑器插件可使用: 1. https://github.com/halo-sigs/plugin-bytemd 2. https://github.com/halo-sigs/plugin-stackedit #### Does this PR introduce a user-facing change? ```release-note 重构 Console 端选择编辑器的方式,支持记住上一次的选择。 ```pull/811/head
parent
89ee700f9d
commit
565efcb3ac
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
useEditorExtensionPoints,
|
||||
type EditorProvider,
|
||||
} from "@/composables/use-editor-extension-points";
|
||||
import { VAvatar, VSpace, IconExchange } from "@halo-dev/components";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
provider?: EditorProvider;
|
||||
}>(),
|
||||
{
|
||||
provider: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", provider: EditorProvider): void;
|
||||
}>();
|
||||
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FloatingDropdown>
|
||||
<div
|
||||
class="group flex w-full cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100"
|
||||
>
|
||||
<VAvatar v-if="provider?.logo" :src="provider.logo" size="xs"></VAvatar>
|
||||
<div class="select-none text-sm text-gray-600 group-hover:text-gray-900">
|
||||
{{ provider?.displayName }}
|
||||
</div>
|
||||
<IconExchange class="h-4 w-4 text-gray-600 group-hover:text-gray-900" />
|
||||
</div>
|
||||
<template #popper>
|
||||
<div class="w-48 p-2">
|
||||
<VSpace class="w-full" direction="column">
|
||||
<div
|
||||
v-for="(editorProvider, index) in editorProviders"
|
||||
:key="index"
|
||||
v-close-popper
|
||||
class="group flex w-full cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100"
|
||||
:class="{
|
||||
'bg-gray-100': editorProvider.name === provider?.name,
|
||||
}"
|
||||
@click="emit('select', editorProvider)"
|
||||
>
|
||||
<VAvatar :src="editorProvider.logo" size="xs"></VAvatar>
|
||||
<div
|
||||
class="text-sm text-gray-600 group-hover:text-gray-900"
|
||||
:class="{
|
||||
'text-gray-900': editorProvider.name === provider?.name,
|
||||
}"
|
||||
>
|
||||
{{ editorProvider.displayName }}
|
||||
</div>
|
||||
</div>
|
||||
</VSpace>
|
||||
</div>
|
||||
</template>
|
||||
</FloatingDropdown>
|
||||
</template>
|
|
@ -1,7 +1,13 @@
|
|||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
||||
import type { EditorProvider as EditorProviderRaw } from "@halo-dev/console-shared";
|
||||
import type { PluginModule } from "@/stores/plugin";
|
||||
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
||||
import { VLoading } from "@halo-dev/components";
|
||||
import Logo from "@/assets/logo.png";
|
||||
|
||||
export interface EditorProvider extends EditorProviderRaw {
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
interface useEditorExtensionPointsReturn {
|
||||
editorProviders: Ref<EditorProvider[]>;
|
||||
|
@ -21,6 +27,7 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
|||
delay: 200,
|
||||
}),
|
||||
rawType: "HTML",
|
||||
logo: Logo,
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -35,7 +42,10 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
|||
|
||||
if (providers) {
|
||||
providers.forEach((provider) => {
|
||||
editorProviders.value.push(provider);
|
||||
editorProviders.value.push({
|
||||
...provider,
|
||||
logo: pluginModule.extension.status?.logo,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -145,7 +145,10 @@ async function loadPluginModules() {
|
|||
|
||||
if (pluginModule) {
|
||||
registerModule(pluginModule, false);
|
||||
pluginModuleStore.registerPluginModule(pluginModule);
|
||||
pluginModuleStore.registerPluginModule({
|
||||
...pluginModule,
|
||||
extension: plugin,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `${plugin.metadata.name}: 加载插件入口文件失败`;
|
||||
|
|
|
@ -28,14 +28,29 @@ 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";
|
||||
import {
|
||||
useEditorExtensionPoints,
|
||||
type EditorProvider,
|
||||
} from "@/composables/use-editor-extension-points";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Editor providers
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
const currentEditorProvider = ref<EditorProvider>();
|
||||
const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
|
||||
|
||||
const handleChangeEditorProvider = (provider: EditorProvider) => {
|
||||
currentEditorProvider.value = provider;
|
||||
storedEditorProviderName.value = provider.name;
|
||||
formState.value.page.metadata.annotations = {
|
||||
"content.halo.run/preferred-editor": provider.name,
|
||||
};
|
||||
};
|
||||
|
||||
// SinglePage form
|
||||
const initialFormState: SinglePageRequest = {
|
||||
page: {
|
||||
spec: {
|
||||
|
@ -238,6 +253,7 @@ const handleFetchContent = async () => {
|
|||
formState.value.content = Object.assign(formState.value.content, data);
|
||||
};
|
||||
|
||||
// SinglePage settings
|
||||
const handleOpenSettingModal = async () => {
|
||||
const { data: latestSinglePage } =
|
||||
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
|
||||
|
@ -267,7 +283,6 @@ const onSettingPublished = (singlePage: SinglePage) => {
|
|||
handlePublish();
|
||||
};
|
||||
|
||||
const editor = useRouteQuery("editor");
|
||||
onMounted(async () => {
|
||||
if (routeQueryName.value) {
|
||||
const { data: singlePage } =
|
||||
|
@ -282,7 +297,7 @@ onMounted(async () => {
|
|||
// Set default editor
|
||||
const provider =
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.name === editor.value
|
||||
(provider) => provider.name === storedEditorProviderName.value
|
||||
) || editorProviders.value[0];
|
||||
if (provider) {
|
||||
currentEditorProvider.value = provider;
|
||||
|
@ -296,6 +311,7 @@ onMounted(async () => {
|
|||
handleResetCache();
|
||||
});
|
||||
|
||||
// SinglePage content cache
|
||||
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||
useContentCache(
|
||||
"singlePage-content-cache",
|
||||
|
@ -320,6 +336,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<EditorProviderSelector
|
||||
v-if="editorProviders.length > 1 && !isUpdateMode"
|
||||
:provider="currentEditorProvider"
|
||||
@select="handleChangeEditorProvider"
|
||||
/>
|
||||
|
||||
<!-- TODO: add preview single page support -->
|
||||
<VButton
|
||||
v-if="false"
|
||||
|
|
|
@ -11,13 +11,10 @@ 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;
|
||||
|
@ -78,38 +75,7 @@ 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"
|
||||
|
|
|
@ -22,20 +22,35 @@ import {
|
|||
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";
|
||||
import {
|
||||
useEditorExtensionPoints,
|
||||
type EditorProvider,
|
||||
} from "@/composables/use-editor-extension-points";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Editor providers
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
const currentEditorProvider = ref<EditorProvider>();
|
||||
const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
|
||||
|
||||
const handleChangeEditorProvider = (provider: EditorProvider) => {
|
||||
currentEditorProvider.value = provider;
|
||||
storedEditorProviderName.value = provider.name;
|
||||
formState.value.post.metadata.annotations = {
|
||||
"content.halo.run/preferred-editor": provider.name,
|
||||
};
|
||||
};
|
||||
|
||||
// Post form
|
||||
const initialFormState: PostRequest = {
|
||||
post: {
|
||||
spec: {
|
||||
|
@ -254,6 +269,7 @@ const handleOpenSettingModal = async () => {
|
|||
settingModal.value = true;
|
||||
};
|
||||
|
||||
// Post settings
|
||||
const onSettingSaved = (post: Post) => {
|
||||
// Set route query parameter
|
||||
if (!isUpdateMode.value) {
|
||||
|
@ -276,7 +292,6 @@ 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
|
||||
|
@ -292,7 +307,7 @@ onMounted(async () => {
|
|||
// Set default editor
|
||||
const provider =
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.name === editor.value
|
||||
(provider) => provider.name === storedEditorProviderName.value
|
||||
) || editorProviders.value[0];
|
||||
|
||||
if (provider) {
|
||||
|
@ -307,6 +322,7 @@ onMounted(async () => {
|
|||
handleResetCache();
|
||||
});
|
||||
|
||||
// Post content cache
|
||||
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||
useContentCache(
|
||||
"post-content-cache",
|
||||
|
@ -331,6 +347,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<EditorProviderSelector
|
||||
v-if="editorProviders.length > 1 && !isUpdateMode"
|
||||
:provider="currentEditorProvider"
|
||||
@select="handleChangeEditorProvider"
|
||||
/>
|
||||
|
||||
<!-- TODO: add preview post support -->
|
||||
<VButton
|
||||
v-if="false"
|
||||
|
|
|
@ -45,12 +45,9 @@ 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,
|
||||
|
@ -480,39 +477,7 @@ const hasFilters = computed(() => {
|
|||
<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"
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type { PluginModule } from "@halo-dev/console-shared";
|
||||
import type { PluginModule as PluginModuleRaw } from "@halo-dev/console-shared";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
|
||||
export interface PluginModule extends PluginModuleRaw {
|
||||
extension: Plugin;
|
||||
}
|
||||
|
||||
interface PluginStoreState {
|
||||
pluginModules: PluginModule[];
|
||||
|
|
Loading…
Reference in New Issue