mirror of https://github.com/halo-dev/halo
feat: add a title input box in the editor (#5465)
#### What type of PR is this? /area editor /area ui /kind feature #### What this PR does / why we need it: 为默认编辑器添加标题输入框。 <img width="1665" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/df903e02-76b0-45fe-89d9-6ac81af8f041"> #### Which issue(s) this PR fixes: Fixes #5427 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 为默认编辑器添加标题输入框。 ```pull/5523/head
parent
5e073bfe3c
commit
581a738423
|
@ -32,15 +32,6 @@ export default function useSlugify(
|
||||||
auto: Ref<boolean>,
|
auto: Ref<boolean>,
|
||||||
formType: FormType
|
formType: FormType
|
||||||
) {
|
) {
|
||||||
watch(
|
|
||||||
() => source.value,
|
|
||||||
() => {
|
|
||||||
if (auto.value) {
|
|
||||||
handleGenerateSlug(false, formType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleGenerateSlug = (forceUpdate = false, formType: FormType) => {
|
const handleGenerateSlug = (forceUpdate = false, formType: FormType) => {
|
||||||
const globalInfoStore = useGlobalInfoStore();
|
const globalInfoStore = useGlobalInfoStore();
|
||||||
const mode = globalInfoStore.globalInfo?.postSlugGenerationStrategy;
|
const mode = globalInfoStore.globalInfo?.postSlugGenerationStrategy;
|
||||||
|
@ -60,6 +51,18 @@ export default function useSlugify(
|
||||||
target.value = Strategy[mode](source.value);
|
target.value = Strategy[mode](source.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => source.value,
|
||||||
|
() => {
|
||||||
|
if (auto.value) {
|
||||||
|
handleGenerateSlug(false, formType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleGenerateSlug,
|
handleGenerateSlug,
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,10 +21,10 @@ import {
|
||||||
ref,
|
ref,
|
||||||
toRef,
|
toRef,
|
||||||
type ComputedRef,
|
type ComputedRef,
|
||||||
|
watch,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
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";
|
||||||
|
@ -69,7 +69,7 @@ const handleChangeEditorProvider = async (provider: EditorProvider) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// SinglePage form
|
// SinglePage form
|
||||||
const initialFormState: SinglePageRequest = {
|
const formState = ref<SinglePageRequest>({
|
||||||
page: {
|
page: {
|
||||||
spec: {
|
spec: {
|
||||||
title: "",
|
title: "",
|
||||||
|
@ -101,13 +101,19 @@ const initialFormState: SinglePageRequest = {
|
||||||
content: "",
|
content: "",
|
||||||
rawType: "HTML",
|
rawType: "HTML",
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
|
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
|
|
||||||
|
const isTitleChanged = ref(false);
|
||||||
|
watch(
|
||||||
|
() => formState.value.page.spec.title,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
isTitleChanged.value = newValue !== oldValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.page.metadata.creationTimestamp;
|
return !!formState.value.page.metadata.creationTimestamp;
|
||||||
});
|
});
|
||||||
|
@ -143,15 +149,23 @@ const handleSave = async (options?: { mute?: boolean }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode.value) {
|
||||||
|
if (isTitleChanged.value) {
|
||||||
|
formState.value.page = (
|
||||||
|
await singlePageUpdateMutate(formState.value.page)
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await apiClient.singlePage.updateSinglePageContent({
|
const { data } = await apiClient.singlePage.updateSinglePageContent({
|
||||||
name: formState.value.page.metadata.name,
|
name: formState.value.page.metadata.name,
|
||||||
content: formState.value.content,
|
content: formState.value.content,
|
||||||
});
|
});
|
||||||
|
|
||||||
formState.value.page = data;
|
formState.value.page = data;
|
||||||
|
isTitleChanged.value = false;
|
||||||
} else {
|
} else {
|
||||||
// Clear new page content cache
|
// Clear new page content cache
|
||||||
handleClearCache();
|
handleClearCache();
|
||||||
|
|
||||||
const { data } = await apiClient.singlePage.draftSinglePage({
|
const { data } = await apiClient.singlePage.draftSinglePage({
|
||||||
singlePageRequest: formState.value,
|
singlePageRequest: formState.value,
|
||||||
});
|
});
|
||||||
|
@ -184,6 +198,12 @@ const handlePublish = async () => {
|
||||||
const { name: singlePageName } = formState.value.page.metadata;
|
const { name: singlePageName } = formState.value.page.metadata;
|
||||||
const { permalink } = formState.value.page.status || {};
|
const { permalink } = formState.value.page.status || {};
|
||||||
|
|
||||||
|
if (isTitleChanged.value) {
|
||||||
|
formState.value.page = (
|
||||||
|
await singlePageUpdateMutate(formState.value.page)
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
await apiClient.singlePage.updateSinglePageContent({
|
await apiClient.singlePage.updateSinglePageContent({
|
||||||
name: singlePageName,
|
name: singlePageName,
|
||||||
content: formState.value.content,
|
content: formState.value.content,
|
||||||
|
@ -224,6 +244,7 @@ const handlePublishClick = () => {
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode.value) {
|
||||||
handlePublish();
|
handlePublish();
|
||||||
} else {
|
} else {
|
||||||
|
// Set editor title to page
|
||||||
settingModal.value = true;
|
settingModal.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -477,6 +498,7 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
|
||||||
v-if="currentEditorProvider"
|
v-if="currentEditorProvider"
|
||||||
v-model:raw="formState.content.raw"
|
v-model:raw="formState.content.raw"
|
||||||
v-model:content="formState.content.content"
|
v-model:content="formState.content.content"
|
||||||
|
v-model:title="formState.page.spec.title"
|
||||||
:upload-image="handleUploadImage"
|
:upload-image="handleUploadImage"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
@update="handleSetContentCache"
|
@update="handleSetContentCache"
|
||||||
|
|
|
@ -255,6 +255,9 @@ watch(
|
||||||
formState.value = toRaw(value);
|
formState.value = toRaw(value);
|
||||||
publishTime.value = toDatetimeLocal(formState.value.spec.publishTime);
|
publishTime.value = toDatetimeLocal(formState.value.spec.publishTime);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,8 @@ import {
|
||||||
ref,
|
ref,
|
||||||
toRef,
|
toRef,
|
||||||
type ComputedRef,
|
type ComputedRef,
|
||||||
|
watch,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
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";
|
||||||
|
@ -80,7 +80,7 @@ interface PostRequestWithContent extends PostRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post form
|
// Post form
|
||||||
const initialFormState: PostRequestWithContent = {
|
const formState = ref<PostRequestWithContent>({
|
||||||
post: {
|
post: {
|
||||||
spec: {
|
spec: {
|
||||||
title: "",
|
title: "",
|
||||||
|
@ -114,13 +114,19 @@ const initialFormState: PostRequestWithContent = {
|
||||||
content: "",
|
content: "",
|
||||||
rawType: "HTML",
|
rawType: "HTML",
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const formState = ref<PostRequestWithContent>(cloneDeep(initialFormState));
|
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
|
|
||||||
|
const isTitleChanged = ref(false);
|
||||||
|
watch(
|
||||||
|
() => formState.value.post.spec.title,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
isTitleChanged.value = newValue !== oldValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.post.metadata.creationTimestamp;
|
return !!formState.value.post.metadata.creationTimestamp;
|
||||||
});
|
});
|
||||||
|
@ -155,15 +161,25 @@ const handleSave = async (options?: { mute?: boolean }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode.value) {
|
||||||
|
// Save post title
|
||||||
|
if (isTitleChanged.value) {
|
||||||
|
formState.value.post = (
|
||||||
|
await postUpdateMutate(formState.value.post)
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await apiClient.post.updatePostContent({
|
const { data } = await apiClient.post.updatePostContent({
|
||||||
name: formState.value.post.metadata.name,
|
name: formState.value.post.metadata.name,
|
||||||
content: formState.value.content,
|
content: formState.value.content,
|
||||||
});
|
});
|
||||||
|
|
||||||
formState.value.post = data;
|
formState.value.post = data;
|
||||||
|
|
||||||
|
isTitleChanged.value = false;
|
||||||
} else {
|
} else {
|
||||||
// Clear new post content cache
|
// Clear new post content cache
|
||||||
handleClearCache();
|
handleClearCache();
|
||||||
|
|
||||||
const { data } = await apiClient.post.draftPost({
|
const { data } = await apiClient.post.draftPost({
|
||||||
postRequest: formState.value,
|
postRequest: formState.value,
|
||||||
});
|
});
|
||||||
|
@ -195,6 +211,12 @@ const handlePublish = async () => {
|
||||||
const { name: postName } = formState.value.post.metadata;
|
const { name: postName } = formState.value.post.metadata;
|
||||||
const { permalink } = formState.value.post.status || {};
|
const { permalink } = formState.value.post.status || {};
|
||||||
|
|
||||||
|
if (isTitleChanged.value) {
|
||||||
|
formState.value.post = (
|
||||||
|
await postUpdateMutate(formState.value.post)
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
await apiClient.post.updatePostContent({
|
await apiClient.post.updatePostContent({
|
||||||
name: postName,
|
name: postName,
|
||||||
content: formState.value.content,
|
content: formState.value.content,
|
||||||
|
@ -240,6 +262,7 @@ const handlePublishClick = () => {
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode.value) {
|
||||||
handlePublish();
|
handlePublish();
|
||||||
} else {
|
} else {
|
||||||
|
// Set editor title to post
|
||||||
settingModal.value = true;
|
settingModal.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -503,6 +526,7 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
|
||||||
v-if="currentEditorProvider"
|
v-if="currentEditorProvider"
|
||||||
v-model:raw="formState.content.raw"
|
v-model:raw="formState.content.raw"
|
||||||
v-model:content="formState.content.content"
|
v-model:content="formState.content.content"
|
||||||
|
v-model:title="formState.post.spec.title"
|
||||||
:upload-image="handleUploadImage"
|
:upload-image="handleUploadImage"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
@update="handleSetContentCache"
|
@update="handleSetContentCache"
|
||||||
|
|
|
@ -216,6 +216,9 @@ watch(
|
||||||
formState.value = toRaw(value);
|
formState.value = toRaw(value);
|
||||||
publishTime.value = toDatetimeLocal(formState.value.spec.publishTime);
|
publishTime.value = toDatetimeLocal(formState.value.spec.publishTime);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -37,14 +37,20 @@ watch(
|
||||||
<editor-bubble-menu :editor="editor" />
|
<editor-bubble-menu :editor="editor" />
|
||||||
<editor-header :editor="editor" />
|
<editor-header :editor="editor" />
|
||||||
<div class="h-full flex flex-row w-full overflow-hidden">
|
<div class="h-full flex flex-row w-full overflow-hidden">
|
||||||
<editor-content
|
<div class="overflow-y-auto flex-1 bg-white">
|
||||||
:editor="editor"
|
<div v-if="$slots.content" class="editor-header-extra">
|
||||||
:style="contentStyles"
|
<slot name="content" />
|
||||||
class="editor-content markdown-body flex-1 relative bg-white overflow-y-auto"
|
</div>
|
||||||
/>
|
|
||||||
|
<editor-content
|
||||||
|
:editor="editor"
|
||||||
|
:style="contentStyles"
|
||||||
|
class="editor-content markdown-body relative"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$slots.extra"
|
v-if="$slots.extra"
|
||||||
class="h-full hidden sm:!block w-72 flex-shrink-0"
|
class="h-full hidden sm:!block w-72 flex-shrink-0 flex-none"
|
||||||
>
|
>
|
||||||
<slot name="extra"></slot>
|
<slot name="extra"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,15 +30,24 @@ export class SearchAndReplacePluginView {
|
||||||
|
|
||||||
public containerElement: HTMLElement;
|
public containerElement: HTMLElement;
|
||||||
|
|
||||||
|
public init: boolean;
|
||||||
|
|
||||||
constructor({ view, editor, element }: SearchAndReplacePluginViewProps) {
|
constructor({ view, editor, element }: SearchAndReplacePluginViewProps) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.view = view;
|
this.view = view;
|
||||||
this.containerElement = element;
|
this.containerElement = element;
|
||||||
const { element: editorElement } = this.editor.options;
|
this.init = false;
|
||||||
editorElement.insertAdjacentElement("afterbegin", this.containerElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
const { parentElement: editorParentElement } = this.editor.options.element;
|
||||||
|
if (!this.init && editorParentElement) {
|
||||||
|
editorParentElement.insertAdjacentElement(
|
||||||
|
"afterbegin",
|
||||||
|
this.containerElement
|
||||||
|
);
|
||||||
|
this.init = true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,11 @@
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-header-extra {
|
||||||
|
width: 100%;
|
||||||
|
padding: $editorVerticalPadding 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-content {
|
.editor-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -78,7 +83,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen {
|
@media screen {
|
||||||
.ProseMirror {
|
.ProseMirror,
|
||||||
|
.editor-header-extra {
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
padding: $editorVerticalPadding min($editorHorizontalPadding, 10%) !important;
|
padding: $editorVerticalPadding min($editorHorizontalPadding, 10%) !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,12 +98,14 @@ import { onBeforeUnmount } from "vue";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import type { AxiosRequestConfig } from "axios";
|
import type { AxiosRequestConfig } from "axios";
|
||||||
import { getContents } from "./utils/attachment";
|
import { getContents } from "./utils/attachment";
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
raw?: string;
|
raw?: string;
|
||||||
content: string;
|
content: string;
|
||||||
uploadImage?: (
|
uploadImage?: (
|
||||||
|
@ -112,6 +114,7 @@ const props = withDefaults(
|
||||||
) => Promise<Attachment>;
|
) => Promise<Attachment>;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
title: "",
|
||||||
raw: "",
|
raw: "",
|
||||||
content: "",
|
content: "",
|
||||||
uploadImage: undefined,
|
uploadImage: undefined,
|
||||||
|
@ -119,6 +122,7 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
(event: "update:title", value: string): void;
|
||||||
(event: "update:raw", value: string): void;
|
(event: "update:raw", value: string): void;
|
||||||
(event: "update:content", value: string): void;
|
(event: "update:content", value: string): void;
|
||||||
(event: "update", value: string): void;
|
(event: "update", value: string): void;
|
||||||
|
@ -404,7 +408,6 @@ onMounted(() => {
|
||||||
UiExtensionUpload,
|
UiExtensionUpload,
|
||||||
ExtensionSearchAndReplace,
|
ExtensionSearchAndReplace,
|
||||||
],
|
],
|
||||||
autofocus: "start",
|
|
||||||
parseOptions: {
|
parseOptions: {
|
||||||
preserveWhitespace: true,
|
preserveWhitespace: true,
|
||||||
},
|
},
|
||||||
|
@ -441,6 +444,26 @@ const currentLocale = i18n.global.locale.value as
|
||||||
| "en"
|
| "en"
|
||||||
| "zh"
|
| "zh"
|
||||||
| "en-US";
|
| "en-US";
|
||||||
|
|
||||||
|
function onTitleInput(event: Event) {
|
||||||
|
emit("update:title", (event.target as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set focus
|
||||||
|
const editorTitleRef = ref();
|
||||||
|
onMounted(() => {
|
||||||
|
// if name is empty, it means the editor is in the creation mode
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const name = urlParams.get("name");
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
nextTick(() => {
|
||||||
|
editorTitleRef.value.focus();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
editor.value?.commands.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -452,6 +475,17 @@ const currentLocale = i18n.global.locale.value as
|
||||||
@close="handleCloseAttachmentSelectorModal"
|
@close="handleCloseAttachmentSelectorModal"
|
||||||
/>
|
/>
|
||||||
<RichTextEditor v-if="editor" :editor="editor" :locale="currentLocale">
|
<RichTextEditor v-if="editor" :editor="editor" :locale="currentLocale">
|
||||||
|
<template #content>
|
||||||
|
<input
|
||||||
|
ref="editorTitleRef"
|
||||||
|
:value="title"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('core.components.default_editor.title_placeholder')"
|
||||||
|
class="w-full border-x-0 !border-b border-t-0 !border-solid !border-gray-100 p-0 !py-2 text-4xl font-semibold placeholder:text-gray-300"
|
||||||
|
@input="onTitleInput"
|
||||||
|
@keydown.enter="() => editor?.commands.focus('start')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template v-if="showSidebar" #extra>
|
<template v-if="showSidebar" #extra>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
element="div"
|
element="div"
|
||||||
|
|
|
@ -1414,6 +1414,7 @@ core:
|
||||||
toolbox:
|
toolbox:
|
||||||
attachment: Attachment
|
attachment: Attachment
|
||||||
show_hide_sidebar: Show/Hide Sidebar
|
show_hide_sidebar: Show/Hide Sidebar
|
||||||
|
title_placeholder: Please enter the title
|
||||||
global_search:
|
global_search:
|
||||||
placeholder: Enter keywords to search
|
placeholder: Enter keywords to search
|
||||||
no_results: No search results
|
no_results: No search results
|
||||||
|
|
|
@ -436,7 +436,7 @@ core:
|
||||||
description: 该操作会将自定义页面恢复到被删除之前的状态。
|
description: 该操作会将自定义页面恢复到被删除之前的状态。
|
||||||
page_editor:
|
page_editor:
|
||||||
title: 页面编辑
|
title: 页面编辑
|
||||||
untitled: Untitled page
|
untitled: 未命名页面
|
||||||
comment:
|
comment:
|
||||||
title: 评论
|
title: 评论
|
||||||
empty:
|
empty:
|
||||||
|
@ -1362,6 +1362,7 @@ core:
|
||||||
toolbox:
|
toolbox:
|
||||||
attachment: 选择附件
|
attachment: 选择附件
|
||||||
show_hide_sidebar: 显示 / 隐藏侧边栏
|
show_hide_sidebar: 显示 / 隐藏侧边栏
|
||||||
|
title_placeholder: 请输入标题
|
||||||
global_search:
|
global_search:
|
||||||
placeholder: 输入关键词以搜索
|
placeholder: 输入关键词以搜索
|
||||||
no_results: 没有搜索结果
|
no_results: 没有搜索结果
|
||||||
|
|
|
@ -1328,6 +1328,7 @@ core:
|
||||||
toolbox:
|
toolbox:
|
||||||
attachment: 選擇附件
|
attachment: 選擇附件
|
||||||
show_hide_sidebar: 顯示 / 隱藏側邊欄
|
show_hide_sidebar: 顯示 / 隱藏側邊欄
|
||||||
|
title_placeholder: 請輸入標題
|
||||||
global_search:
|
global_search:
|
||||||
placeholder: 輸入關鍵字以搜尋
|
placeholder: 輸入關鍵字以搜尋
|
||||||
no_results: 沒有搜尋結果
|
no_results: 沒有搜尋結果
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||||
import { ref, toRef } from "vue";
|
import { ref, toRef, watch } from "vue";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import type { Post, Content, Snapshot } from "@halo-dev/api-client";
|
import type { Post, Content, Snapshot } from "@halo-dev/api-client";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
|
@ -82,6 +82,14 @@ const content = ref<Content>({
|
||||||
});
|
});
|
||||||
const snapshot = ref<Snapshot>();
|
const snapshot = ref<Snapshot>();
|
||||||
|
|
||||||
|
const isTitleChanged = ref(false);
|
||||||
|
watch(
|
||||||
|
() => formState.value.spec.title,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
isTitleChanged.value = newValue !== oldValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// provide some data to editor
|
// provide some data to editor
|
||||||
provide<ComputedRef<string | undefined>>(
|
provide<ComputedRef<string | undefined>>(
|
||||||
"owner",
|
"owner",
|
||||||
|
@ -166,21 +174,7 @@ useAutoSaveContent(currentCache, toRef(content.value, "raw"), async () => {
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode.value) {
|
||||||
handleSave({ mute: true });
|
handleSave({ mute: true });
|
||||||
} else {
|
} else {
|
||||||
formState.value.metadata.annotations = {
|
handleCreate();
|
||||||
...formState.value.metadata.annotations,
|
|
||||||
[contentAnnotations.CONTENT_JSON]: JSON.stringify(content.value),
|
|
||||||
};
|
|
||||||
// Set default title and slug
|
|
||||||
if (!formState.value.spec.title) {
|
|
||||||
formState.value.spec.title = t("core.post_editor.untitled");
|
|
||||||
}
|
|
||||||
if (!formState.value.spec.slug) {
|
|
||||||
formState.value.spec.slug = new Date().getTime().toString();
|
|
||||||
}
|
|
||||||
const { data: createdPost } = await apiClient.uc.post.createMyPost({
|
|
||||||
post: formState.value,
|
|
||||||
});
|
|
||||||
onCreatePostSuccess(createdPost);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -271,16 +265,34 @@ async function handleSetEditorProviderFromRemote() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create post
|
// Create post
|
||||||
const postCreationModal = ref(false);
|
|
||||||
|
|
||||||
function handleSaveClick() {
|
function handleSaveClick() {
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode.value) {
|
||||||
handleSave({ mute: false });
|
handleSave({ mute: false });
|
||||||
} else {
|
} else {
|
||||||
postCreationModal.value = true;
|
handleCreate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCreate() {
|
||||||
|
formState.value.metadata.annotations = {
|
||||||
|
...formState.value.metadata.annotations,
|
||||||
|
[contentAnnotations.CONTENT_JSON]: JSON.stringify(content.value),
|
||||||
|
};
|
||||||
|
// Set default title and slug
|
||||||
|
if (!formState.value.spec.title) {
|
||||||
|
formState.value.spec.title = t("core.post_editor.untitled");
|
||||||
|
}
|
||||||
|
if (!formState.value.spec.slug) {
|
||||||
|
formState.value.spec.slug = new Date().getTime().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: createdPost } = await apiClient.uc.post.createMyPost({
|
||||||
|
post: formState.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
await onCreatePostSuccess(createdPost);
|
||||||
|
}
|
||||||
|
|
||||||
async function onCreatePostSuccess(data: Post) {
|
async function onCreatePostSuccess(data: Post) {
|
||||||
formState.value = data;
|
formState.value = data;
|
||||||
// Update route query params
|
// Update route query params
|
||||||
|
@ -300,6 +312,17 @@ const { mutateAsync: handleSave, isLoading: isSaving } = useMutation({
|
||||||
mute: false,
|
mute: false,
|
||||||
},
|
},
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
// Update title
|
||||||
|
// TODO: needs retry
|
||||||
|
if (isTitleChanged.value) {
|
||||||
|
const { data: updatedPost } = await apiClient.uc.post.updateMyPost({
|
||||||
|
name: formState.value.metadata.name,
|
||||||
|
post: formState.value,
|
||||||
|
});
|
||||||
|
formState.value = updatedPost;
|
||||||
|
isTitleChanged.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot always exists in update mode
|
// Snapshot always exists in update mode
|
||||||
if (!snapshot.value) {
|
if (!snapshot.value) {
|
||||||
return;
|
return;
|
||||||
|
@ -345,6 +368,7 @@ function handlePublishClick() {
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode.value) {
|
||||||
handlePublish();
|
handlePublish();
|
||||||
} else {
|
} else {
|
||||||
|
// Set editor title to post
|
||||||
postPublishModal.value = true;
|
postPublishModal.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -396,24 +420,7 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isUpdateMode.value) {
|
if (!isUpdateMode.value) {
|
||||||
formState.value.metadata.annotations = {
|
await handleCreate();
|
||||||
...formState.value.metadata.annotations,
|
|
||||||
[contentAnnotations.CONTENT_JSON]: JSON.stringify(content.value),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!formState.value.spec.title) {
|
|
||||||
formState.value.spec.title = t("core.post_editor.untitled");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formState.value.spec.slug) {
|
|
||||||
formState.value.spec.slug = new Date().getTime().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await apiClient.uc.post.createMyPost({
|
|
||||||
post: formState.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
await onCreatePostSuccess(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await apiClient.uc.attachment.createAttachmentForPost(
|
const { data } = await apiClient.uc.attachment.createAttachmentForPost(
|
||||||
|
@ -487,23 +494,17 @@ useSessionKeepAlive();
|
||||||
v-if="currentEditorProvider"
|
v-if="currentEditorProvider"
|
||||||
v-model:raw="content.raw"
|
v-model:raw="content.raw"
|
||||||
v-model:content="content.content"
|
v-model:content="content.content"
|
||||||
|
v-model:title="formState.spec.title"
|
||||||
:upload-image="handleUploadImage"
|
:upload-image="handleUploadImage"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
@update="handleSetContentCache"
|
@update="handleSetContentCache"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PostCreationModal
|
|
||||||
v-if="postCreationModal"
|
|
||||||
:title="$t('core.uc_post.creation_modal.title')"
|
|
||||||
:content="content"
|
|
||||||
@close="postCreationModal = false"
|
|
||||||
@success="onCreatePostSuccess"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PostCreationModal
|
<PostCreationModal
|
||||||
v-if="postPublishModal"
|
v-if="postPublishModal"
|
||||||
:title="$t('core.uc_post.publish_modal.title')"
|
:title="$t('core.uc_post.publish_modal.title')"
|
||||||
|
:post="formState"
|
||||||
:content="content"
|
:content="content"
|
||||||
publish
|
publish
|
||||||
@close="postPublishModal = false"
|
@close="postPublishModal = false"
|
||||||
|
|
|
@ -17,6 +17,7 @@ const props = withDefaults(
|
||||||
title: string;
|
title: string;
|
||||||
content: Content;
|
content: Content;
|
||||||
publish?: boolean;
|
publish?: boolean;
|
||||||
|
post: Post;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
publish: false,
|
publish: false,
|
||||||
|
@ -107,7 +108,17 @@ function onSubmit(data: PostFormState) {
|
||||||
centered
|
centered
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
>
|
>
|
||||||
<PostSettingForm @submit="onSubmit" />
|
<PostSettingForm
|
||||||
|
:form-state="{
|
||||||
|
title: props.post.spec.title,
|
||||||
|
slug: props.post.spec.slug,
|
||||||
|
allowComment: props.post.spec.allowComment,
|
||||||
|
visible: props.post.spec.visible,
|
||||||
|
pinned: props.post.spec.pinned,
|
||||||
|
excerptAutoGenerate: props.post.spec.excerpt.autoGenerate,
|
||||||
|
}"
|
||||||
|
@submit="onSubmit"
|
||||||
|
/>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
|
Loading…
Reference in New Issue