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 { 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 { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
||||||
import { VLoading } from "@halo-dev/components";
|
import { VLoading } from "@halo-dev/components";
|
||||||
|
import Logo from "@/assets/logo.png";
|
||||||
|
|
||||||
|
export interface EditorProvider extends EditorProviderRaw {
|
||||||
|
logo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface useEditorExtensionPointsReturn {
|
interface useEditorExtensionPointsReturn {
|
||||||
editorProviders: Ref<EditorProvider[]>;
|
editorProviders: Ref<EditorProvider[]>;
|
||||||
|
@ -21,6 +27,7 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
||||||
delay: 200,
|
delay: 200,
|
||||||
}),
|
}),
|
||||||
rawType: "HTML",
|
rawType: "HTML",
|
||||||
|
logo: Logo,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -35,7 +42,10 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
||||||
|
|
||||||
if (providers) {
|
if (providers) {
|
||||||
providers.forEach((provider) => {
|
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) {
|
if (pluginModule) {
|
||||||
registerModule(pluginModule, false);
|
registerModule(pluginModule, false);
|
||||||
pluginModuleStore.registerPluginModule(pluginModule);
|
pluginModuleStore.registerPluginModule({
|
||||||
|
...pluginModule,
|
||||||
|
extension: plugin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = `${plugin.metadata.name}: 加载插件入口文件失败`;
|
const message = `${plugin.metadata.name}: 加载插件入口文件失败`;
|
||||||
|
|
|
@ -28,14 +28,29 @@ import cloneDeep from "lodash.clonedeep";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { useContentCache } from "@/composables/use-content-cache";
|
import { useContentCache } from "@/composables/use-content-cache";
|
||||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
import {
|
||||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
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();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Editor providers
|
||||||
const { editorProviders } = useEditorExtensionPoints();
|
const { editorProviders } = useEditorExtensionPoints();
|
||||||
const currentEditorProvider = ref<EditorProvider>();
|
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 = {
|
const initialFormState: SinglePageRequest = {
|
||||||
page: {
|
page: {
|
||||||
spec: {
|
spec: {
|
||||||
|
@ -238,6 +253,7 @@ const handleFetchContent = async () => {
|
||||||
formState.value.content = Object.assign(formState.value.content, data);
|
formState.value.content = Object.assign(formState.value.content, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SinglePage settings
|
||||||
const handleOpenSettingModal = async () => {
|
const handleOpenSettingModal = async () => {
|
||||||
const { data: latestSinglePage } =
|
const { data: latestSinglePage } =
|
||||||
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
|
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
|
||||||
|
@ -267,7 +283,6 @@ const onSettingPublished = (singlePage: SinglePage) => {
|
||||||
handlePublish();
|
handlePublish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const editor = useRouteQuery("editor");
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (routeQueryName.value) {
|
if (routeQueryName.value) {
|
||||||
const { data: singlePage } =
|
const { data: singlePage } =
|
||||||
|
@ -282,7 +297,7 @@ onMounted(async () => {
|
||||||
// Set default editor
|
// Set default editor
|
||||||
const provider =
|
const provider =
|
||||||
editorProviders.value.find(
|
editorProviders.value.find(
|
||||||
(provider) => provider.name === editor.value
|
(provider) => provider.name === storedEditorProviderName.value
|
||||||
) || editorProviders.value[0];
|
) || editorProviders.value[0];
|
||||||
if (provider) {
|
if (provider) {
|
||||||
currentEditorProvider.value = provider;
|
currentEditorProvider.value = provider;
|
||||||
|
@ -296,6 +311,7 @@ onMounted(async () => {
|
||||||
handleResetCache();
|
handleResetCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SinglePage content cache
|
||||||
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
useContentCache(
|
useContentCache(
|
||||||
"singlePage-content-cache",
|
"singlePage-content-cache",
|
||||||
|
@ -320,6 +336,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
<EditorProviderSelector
|
||||||
|
v-if="editorProviders.length > 1 && !isUpdateMode"
|
||||||
|
:provider="currentEditorProvider"
|
||||||
|
@select="handleChangeEditorProvider"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- TODO: add preview single page support -->
|
<!-- TODO: add preview single page support -->
|
||||||
<VButton
|
<VButton
|
||||||
v-if="false"
|
v-if="false"
|
||||||
|
|
|
@ -11,13 +11,10 @@ import {
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import BasicLayout from "@/layouts/BasicLayout.vue";
|
import BasicLayout from "@/layouts/BasicLayout.vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { editorProviders } = useEditorExtensionPoints();
|
|
||||||
|
|
||||||
interface PageTab {
|
interface PageTab {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -78,38 +75,7 @@ watchEffect(() => {
|
||||||
<VButton :route="{ name: 'DeletedSinglePages' }" size="sm">
|
<VButton :route="{ name: 'DeletedSinglePages' }" size="sm">
|
||||||
回收站
|
回收站
|
||||||
</VButton>
|
</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
|
<VButton
|
||||||
v-else
|
|
||||||
v-permission="['system:singlepages:manage']"
|
v-permission="['system:singlepages:manage']"
|
||||||
:route="{ name: 'SinglePageEditor' }"
|
:route="{ name: 'SinglePageEditor' }"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
|
|
|
@ -22,20 +22,35 @@ import {
|
||||||
toRef,
|
toRef,
|
||||||
type ComputedRef,
|
type ComputedRef,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { useContentCache } from "@/composables/use-content-cache";
|
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();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Editor providers
|
||||||
const { editorProviders } = useEditorExtensionPoints();
|
const { editorProviders } = useEditorExtensionPoints();
|
||||||
const currentEditorProvider = ref<EditorProvider>();
|
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 = {
|
const initialFormState: PostRequest = {
|
||||||
post: {
|
post: {
|
||||||
spec: {
|
spec: {
|
||||||
|
@ -254,6 +269,7 @@ const handleOpenSettingModal = async () => {
|
||||||
settingModal.value = true;
|
settingModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Post settings
|
||||||
const onSettingSaved = (post: Post) => {
|
const onSettingSaved = (post: Post) => {
|
||||||
// Set route query parameter
|
// Set route query parameter
|
||||||
if (!isUpdateMode.value) {
|
if (!isUpdateMode.value) {
|
||||||
|
@ -276,7 +292,6 @@ const onSettingPublished = (post: Post) => {
|
||||||
|
|
||||||
// Get post data when the route contains the name parameter
|
// Get post data when the route contains the name parameter
|
||||||
const name = useRouteQuery("name");
|
const name = useRouteQuery("name");
|
||||||
const editor = useRouteQuery("editor");
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (name.value) {
|
if (name.value) {
|
||||||
// fetch post
|
// fetch post
|
||||||
|
@ -292,7 +307,7 @@ onMounted(async () => {
|
||||||
// Set default editor
|
// Set default editor
|
||||||
const provider =
|
const provider =
|
||||||
editorProviders.value.find(
|
editorProviders.value.find(
|
||||||
(provider) => provider.name === editor.value
|
(provider) => provider.name === storedEditorProviderName.value
|
||||||
) || editorProviders.value[0];
|
) || editorProviders.value[0];
|
||||||
|
|
||||||
if (provider) {
|
if (provider) {
|
||||||
|
@ -307,6 +322,7 @@ onMounted(async () => {
|
||||||
handleResetCache();
|
handleResetCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Post content cache
|
||||||
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
useContentCache(
|
useContentCache(
|
||||||
"post-content-cache",
|
"post-content-cache",
|
||||||
|
@ -331,6 +347,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
<EditorProviderSelector
|
||||||
|
v-if="editorProviders.length > 1 && !isUpdateMode"
|
||||||
|
:provider="currentEditorProvider"
|
||||||
|
@select="handleChangeEditorProvider"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- TODO: add preview post support -->
|
<!-- TODO: add preview post support -->
|
||||||
<VButton
|
<VButton
|
||||||
v-if="false"
|
v-if="false"
|
||||||
|
|
|
@ -45,12 +45,9 @@ import FilterTag from "@/components/filter/FilterTag.vue";
|
||||||
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||||
import { getNode } from "@formkit/core";
|
import { getNode } from "@formkit/core";
|
||||||
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
|
||||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const { editorProviders } = useEditorExtensionPoints();
|
|
||||||
|
|
||||||
const posts = ref<ListedPostList>({
|
const posts = ref<ListedPostList>({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
@ -480,39 +477,7 @@ const hasFilters = computed(() => {
|
||||||
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
|
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
|
||||||
<VButton :route="{ name: 'DeletedPosts' }" 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
|
<VButton
|
||||||
v-else
|
|
||||||
v-permission="['system:posts:manage']"
|
v-permission="['system:posts:manage']"
|
||||||
:route="{ name: 'PostEditor' }"
|
:route="{ name: 'PostEditor' }"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { defineStore } from "pinia";
|
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 {
|
interface PluginStoreState {
|
||||||
pluginModules: PluginModule[];
|
pluginModules: PluginModule[];
|
||||||
|
|
Loading…
Reference in New Issue