refactor: post and singlePage publishing (halo-dev/console#685)

#### What type of PR is this?

/kind improvement
/milestone 2.0

#### What this PR does / why we need it:

重构文章和自定义页面的发布流程。

Ref https://github.com/halo-dev/halo/pull/2659

1. 修改文章、自定义页面标识发布状态的字段名。
2. 修改初始化页面中创建文章和自定义页面的逻辑,取消使用发布接口,改为直接将 `spec.publish` 设置为 `true`
3. 列表支持检测发布状态。

#### Special notes for your reviewer:

测试方式:

1. Halo 需要切换到 https://github.com/halo-dev/halo/pull/2659 分支。
2. Console 需要 `pnpm install && pnpm build:packages`
3. 测试文章和自定义页面的发布、保存等流程。需要完整测试整个流程。

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/3445/head
Ryan Wang 2022-11-11 00:24:10 +08:00 committed by GitHub
parent 6bb7af1762
commit 55b6d77c80
15 changed files with 643 additions and 432 deletions

View File

@ -33,7 +33,7 @@
"@formkit/inputs": "^1.0.0-beta.11",
"@formkit/themes": "^1.0.0-beta.11",
"@formkit/vue": "^1.0.0-beta.11",
"@halo-dev/api-client": "^0.0.41",
"@halo-dev/api-client": "^0.0.43",
"@halo-dev/components": "workspace:*",
"@halo-dev/console-shared": "workspace:*",
"@halo-dev/richtext-editor": "^0.0.0-alpha.11",

View File

@ -40,7 +40,7 @@ const classes = computed(() => {
}
.status-dot-text {
@apply text-gray-500;
@apply text-gray-500 text-xs;
}
&.status-dot-animate {

View File

@ -52,6 +52,8 @@ import IconReplyLine from "~icons/ri/reply-line";
import IconExternalLinkLine from "~icons/ri/external-link-line";
import IconRefreshLine from "~icons/ri/refresh-line";
import IconWindowLine from "~icons/ri/window-line";
import IconSendPlaneFill from "~icons/ri/send-plane-fill";
import IconRocketLine from "~icons/ri/rocket-line";
export {
IconDashboard,
@ -108,4 +110,6 @@ export {
IconExternalLinkLine,
IconRefreshLine,
IconWindowLine,
IconSendPlaneFill,
IconRocketLine,
};

View File

@ -13,7 +13,7 @@ importers:
'@formkit/inputs': ^1.0.0-beta.11
'@formkit/themes': ^1.0.0-beta.11
'@formkit/vue': ^1.0.0-beta.11
'@halo-dev/api-client': ^0.0.41
'@halo-dev/api-client': ^0.0.43
'@halo-dev/components': workspace:*
'@halo-dev/console-shared': workspace:*
'@halo-dev/richtext-editor': ^0.0.0-alpha.11
@ -108,7 +108,7 @@ importers:
'@formkit/inputs': 1.0.0-beta.11
'@formkit/themes': 1.0.0-beta.11_tailwindcss@3.2.1
'@formkit/vue': 1.0.0-beta.11_vjnbgdptsk6bkj7ab5a6mk2cwm
'@halo-dev/api-client': 0.0.41
'@halo-dev/api-client': 0.0.43
'@halo-dev/components': link:packages/components
'@halo-dev/console-shared': link:packages/shared
'@halo-dev/richtext-editor': 0.0.0-alpha.11_vue@3.2.41
@ -1953,8 +1953,8 @@ packages:
- windicss
dev: false
/@halo-dev/api-client/0.0.41:
resolution: {integrity: sha512-YpwoIyT+6BjNEfhQqZPSG7dewmC9AE7wxc/uaIRcVZdKr0C4GLmbiBKGOFFnVWIYr42islhMWjqYCGaiJSzkUg==}
/@halo-dev/api-client/0.0.43:
resolution: {integrity: sha512-bCh5P7AYCYA/nVbAB/t62acrtOaDs8UItFe4JmK1qfeb5vOmFT2FT9jeJFj4ABqa4t4xJxy9hnoxNm6H+iB3IQ==}
dev: false
/@halo-dev/richtext-editor/0.0.0-alpha.11_vue@3.2.41:

View File

@ -11,6 +11,16 @@ export enum roleLabels {
// post
export enum postLabels {
DELETED = "content.halo.run/deleted",
PUBLISHED = "content.halo.run/published",
OWNER = "content.halo.run/owner",
VISIBLE = "content.halo.run/visible",
PHASE = "content.halo.run/phase",
}
// singlePage
export enum singlePageLabels {
DELETED = "content.halo.run/deleted",
PUBLISHED = "content.halo.run/published",
OWNER = "content.halo.run/owner",
VISIBLE = "content.halo.run/visible",
PHASE = "content.halo.run/phase",

View File

@ -2,6 +2,8 @@
import {
VPageHeader,
IconPages,
IconSettings,
IconSendPlaneFill,
VSpace,
VButton,
IconSave,
@ -9,12 +11,15 @@ import {
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
import PostPreviewModal from "../posts/components/PostPreviewModal.vue";
import type { SinglePageRequest } from "@halo-dev/api-client";
import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client";
import { v4 as uuid } from "uuid";
import { computed, onMounted, ref } from "vue";
import { apiClient } from "@/utils/api-client";
import { useRouteQuery } from "@vueuse/router";
import cloneDeep from "lodash.clonedeep";
import { useRouter } from "vue-router";
const router = useRouter();
const initialFormState: SinglePageRequest = {
page: {
@ -24,7 +29,7 @@ const initialFormState: SinglePageRequest = {
template: "",
cover: "",
deleted: false,
published: false,
publish: false,
publishTime: "",
pinned: false,
allowComment: true,
@ -52,6 +57,7 @@ const initialFormState: SinglePageRequest = {
const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
const saving = ref(false);
const publishing = ref(false);
const settingModal = ref(false);
const previewModal = ref(false);
@ -77,10 +83,21 @@ const handleSave = async () => {
}
if (isUpdateMode.value) {
// Get latest single page
const { data: latestSinglePage } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage(
{
name: formState.value.page.metadata.name,
}
);
formState.value.page = latestSinglePage;
const { data } = await apiClient.singlePage.updateDraftSinglePage({
name: formState.value.page.metadata.name,
singlePageRequest: formState.value,
});
formState.value.page = data;
} else {
const { data } = await apiClient.singlePage.draftSinglePage({
@ -89,6 +106,7 @@ const handleSave = async () => {
formState.value.page = data;
routeQueryName.value = data.metadata.name;
}
await handleFetchContent();
} catch (error) {
console.error("Failed to save single page", error);
@ -97,6 +115,76 @@ const handleSave = async () => {
}
};
const handlePublish = async () => {
try {
publishing.value = true;
// Set rendered content
formState.value.content.content = formState.value.content.raw;
if (isUpdateMode.value) {
const { headSnapshot } = formState.value.page.spec;
const { name: singlePageName } = formState.value.page.metadata;
const { data: latestContent } =
await apiClient.content.updateSnapshotContent({
snapshotName: headSnapshot as string,
contentRequest: {
raw: formState.value.content.raw as string,
content: formState.value.content.content as string,
rawType: formState.value.content.rawType as string,
headSnapshotName: headSnapshot,
subjectRef: {
kind: "SinglePage",
version: "v1alpha1",
group: "content.halo.run",
name: singlePageName,
},
},
});
// Get latest single page
const { data: latestSinglePage } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage(
{
name: formState.value.page.metadata.name,
}
);
formState.value.page = latestSinglePage;
formState.value.page.spec.publish = true;
formState.value.page.spec.headSnapshot = latestContent.snapshotName;
formState.value.page.spec.releaseSnapshot =
formState.value.page.spec.headSnapshot;
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{
name: singlePageName,
singlePage: formState.value.page,
}
);
} else {
formState.value.page.spec.publish = true;
await apiClient.singlePage.draftSinglePage({
singlePageRequest: formState.value,
});
}
router.push({ name: "SinglePages" });
} catch (error) {
console.error("Failed to publish single page", error);
} finally {
publishing.value = false;
}
};
const handlePublishClick = () => {
if (isUpdateMode.value) {
handlePublish();
} else {
settingModal.value = true;
}
};
const handleFetchContent = async () => {
if (!formState.value.page.spec.headSnapshot) {
return;
@ -108,14 +196,33 @@ const handleFetchContent = async () => {
formState.value.content = data;
};
const onSettingSaved = (page: SinglePageRequest) => {
const handleOpenSettingModal = async () => {
const { data: latestSinglePage } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
name: formState.value.page.metadata.name,
});
formState.value.page = latestSinglePage;
settingModal.value = true;
};
const onSettingSaved = (page: SinglePage) => {
// Set route query parameter
if (!isUpdateMode.value) {
routeQueryName.value = page.page.metadata.name;
routeQueryName.value = page.metadata.name;
}
formState.value = page;
formState.value.page = page;
settingModal.value = false;
if (!isUpdateMode.value) {
handleSave();
}
};
const onSettingPublished = (singlePage: SinglePage) => {
formState.value.page = singlePage;
settingModal.value = false;
handlePublish();
};
onMounted(async () => {
@ -135,8 +242,11 @@ onMounted(async () => {
<template>
<SinglePageSettingModal
v-model:visible="settingModal"
:single-page="formState"
:single-page="formState.page"
:publish-support="!isUpdateMode"
:only-emit="!isUpdateMode"
@saved="onSettingSaved"
@published="onSettingPublished"
/>
<PostPreviewModal v-model:visible="previewModal" />
<VPageHeader title="自定义页面">
@ -155,12 +265,30 @@ onMounted(async () => {
预览
</VButton>
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
保存
</VButton>
<VButton type="secondary" @click="settingModal = true">
<template #icon>
<IconSave class="h-full w-full" />
</template>
保存
</VButton>
<VButton
v-if="isUpdateMode"
size="sm"
type="default"
@click="handleOpenSettingModal"
>
<template #icon>
<IconSettings class="h-full w-full" />
</template>
设置
</VButton>
<VButton
type="secondary"
:loading="publishing"
@click="handlePublishClick"
>
<template #icon>
<IconSendPlaneFill class="h-full w-full" />
</template>
发布
</VButton>
</VSpace>

View File

@ -22,11 +22,10 @@ import {
} from "@halo-dev/components";
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import { onMounted, ref, watch, watchEffect } from "vue";
import { onMounted, ref, watch } from "vue";
import type {
ListedSinglePageList,
SinglePage,
SinglePageRequest,
User,
} from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
@ -34,15 +33,10 @@ import { formatDatetime } from "@/utils/date";
import { onBeforeRouteLeave, RouterLink } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { usePermission } from "@/utils/permission";
import { singlePageLabels } from "@/constants/labels";
const { currentUserHasPermission } = usePermission();
enum SinglePagePhase {
DRAFT = "未发布",
PENDING_APPROVAL = "待审核",
PUBLISHED = "已发布",
}
const singlePages = ref<ListedSinglePageList>({
page: 1,
size: 20,
@ -56,7 +50,6 @@ const singlePages = ref<ListedSinglePageList>({
const loading = ref(false);
const settingModal = ref(false);
const selectedSinglePage = ref<SinglePage>();
const selectedSinglePageWithContent = ref<SinglePageRequest>();
const selectedPageNames = ref<string[]>([]);
const checkedAll = ref(false);
const refreshInterval = ref();
@ -68,32 +61,44 @@ const handleFetchSinglePages = async () => {
loading.value = true;
let contributors: string[] | undefined;
const labelSelector: string[] = ["content.halo.run/deleted=false"];
if (selectedContributor.value) {
contributors = [selectedContributor.value.metadata.name];
}
if (selectedPublishStatusItem.value.value !== undefined) {
labelSelector.push(
`${singlePageLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
);
}
const { data } = await apiClient.singlePage.listSinglePages({
labelSelector: [`content.halo.run/deleted=false`],
labelSelector,
page: singlePages.value.page,
size: singlePages.value.size,
visible: selectedVisibleItem.value.value,
sort: selectedSortItem.value?.sort,
publishPhase: selectedPublishPhaseItem.value.value,
sortOrder: selectedSortItem.value?.sortOrder,
keyword: keyword.value,
contributor: contributors,
});
singlePages.value = data;
const deletedSinglePages = singlePages.value.items.filter(
(singlePage) => singlePage.page.spec.deleted
);
const abnormalSinglePages = singlePages.value.items.filter((singlePage) => {
const { spec, metadata, status } = singlePage.page;
return (
spec.deleted ||
(spec.publish &&
metadata.labels?.[singlePageLabels.PUBLISHED] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
});
if (deletedSinglePages.length) {
if (abnormalSinglePages.length) {
refreshInterval.value = setInterval(() => {
handleFetchSinglePages();
}, 3000);
}, 1000);
}
} catch (error) {
console.error("Failed to fetch single pages", error);
@ -129,28 +134,9 @@ const handleOpenSettingModal = async (singlePage: SinglePage) => {
const onSettingModalClose = () => {
selectedSinglePage.value = undefined;
selectedSinglePageWithContent.value = undefined;
handleFetchSinglePages();
};
watchEffect(async () => {
if (
!selectedSinglePage.value ||
!selectedSinglePage.value.spec.headSnapshot
) {
return;
}
const { data: content } = await apiClient.content.obtainSnapshotContent({
snapshotName: selectedSinglePage.value.spec.headSnapshot,
});
selectedSinglePageWithContent.value = {
page: selectedSinglePage.value,
content: content,
};
});
const handleSelectPrevious = async () => {
const { items, hasPrevious } = singlePages.value;
const index = items.findIndex(
@ -264,35 +250,24 @@ const handleDeleteInBatch = async () => {
});
};
const finalStatus = (singlePage: SinglePage) => {
if (singlePage.status?.phase) {
return SinglePagePhase[singlePage.status.phase];
}
return "";
const getPublishStatus = (singlePage: SinglePage) => {
const { labels } = singlePage.metadata;
return labels?.[singlePageLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
};
const isPublishing = (singlePage: SinglePage) => {
const { spec, status, metadata } = singlePage;
return (
(spec.publish &&
metadata.labels?.[singlePageLabels.PUBLISHED] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
};
watch(selectedPageNames, (newValue) => {
checkedAll.value = newValue.length === singlePages.value.items?.length;
});
watchEffect(async () => {
if (
!selectedSinglePage.value ||
!selectedSinglePage.value.spec.headSnapshot
) {
return;
}
const { data: content } = await apiClient.content.obtainSnapshotContent({
snapshotName: selectedSinglePage.value.spec.headSnapshot,
});
selectedSinglePageWithContent.value = {
page: selectedSinglePage.value,
content: content,
};
});
onMounted(handleFetchSinglePages);
// Filters
@ -302,9 +277,9 @@ interface VisibleItem {
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
}
interface PublishPhaseItem {
interface PublishStatusItem {
label: string;
value?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED";
value?: boolean;
}
interface SortItem {
@ -332,22 +307,18 @@ const VisibleItems: VisibleItem[] = [
},
];
const PublishPhaseItems: PublishPhaseItem[] = [
const PublishStatusItems: PublishStatusItem[] = [
{
label: "全部",
value: undefined,
},
{
label: "已发布",
value: "PUBLISHED",
value: true,
},
{
label: "未发布",
value: "DRAFT",
},
{
label: "待审核",
value: "PENDING_APPROVAL",
value: false,
},
];
@ -376,7 +347,7 @@ const SortItems: SortItem[] = [
const selectedContributor = ref<User>();
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
const selectedPublishPhaseItem = ref<PublishPhaseItem>(PublishPhaseItems[0]);
const selectedPublishStatusItem = ref<PublishStatusItem>(PublishStatusItems[0]);
const selectedSortItem = ref<SortItem>();
const keyword = ref("");
@ -390,8 +361,8 @@ const handleSelectUser = (user?: User) => {
handleFetchSinglePages();
};
function handlePublishPhaseItemChange(publishPhaseItem: PublishPhaseItem) {
selectedPublishPhaseItem.value = publishPhaseItem;
function handlePublishStatusItemChange(publishStatusItem: PublishStatusItem) {
selectedPublishStatusItem.value = publishStatusItem;
handleFetchSinglePages();
}
@ -404,7 +375,7 @@ function handleSortItemChange(sortItem?: SortItem) {
<template>
<SinglePageSettingModal
v-model:visible="settingModal"
:single-page="selectedSinglePageWithContent"
:single-page="selectedSinglePage"
@close="onSettingModalClose"
>
<template #actions>
@ -446,15 +417,15 @@ function handleSortItemChange(sortItem?: SortItem) {
@keyup.enter="handleFetchSinglePages"
></FormKit>
<div
v-if="selectedPublishPhaseItem.value"
v-if="selectedPublishStatusItem.value"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
状态{{ selectedPublishPhaseItem.label }}
状态{{ selectedPublishStatusItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handlePublishPhaseItemChange(PublishPhaseItems[0])"
@click="handlePublishStatusItemChange(PublishStatusItems[0])"
/>
</div>
<div
@ -513,15 +484,16 @@ function handleSortItemChange(sortItem?: SortItem) {
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(filterItem, index) in PublishPhaseItems"
v-for="(filterItem, index) in PublishStatusItems"
:key="index"
v-close-popper
:class="{
'bg-gray-100':
selectedPublishPhaseItem.value === filterItem.value,
selectedPublishStatusItem.value ===
filterItem.value,
}"
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handlePublishPhaseItemChange(filterItem)"
@click="handlePublishStatusItemChange(filterItem)"
>
<span class="truncate">{{ filterItem.label }}</span>
</li>
@ -713,7 +685,11 @@ function handleSortItemChange(sortItem?: SortItem) {
</RouterLink>
</template>
</VEntityField>
<VEntityField :description="finalStatus(singlePage.page)" />
<VEntityField :description="getPublishStatus(singlePage.page)">
<template v-if="isPublishing(singlePage.page)" #description>
<VStatusDot text="发布中" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<IconEye

View File

@ -1,71 +1,70 @@
<script lang="ts" setup>
import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
import { computed, ref, watchEffect } from "vue";
import type { SinglePageRequest } from "@halo-dev/api-client";
import type { SinglePage } from "@halo-dev/api-client";
import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client";
import { v4 as uuid } from "uuid";
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
import { singlePageLabels } from "@/constants/labels";
const initialFormState: SinglePageRequest = {
page: {
spec: {
title: "",
slug: "",
template: "",
cover: "",
deleted: false,
published: false,
publishTime: "",
pinned: false,
allowComment: true,
visible: "PUBLIC",
version: 1,
priority: 0,
excerpt: {
autoGenerate: true,
raw: "",
},
htmlMetas: [],
},
apiVersion: "content.halo.run/v1alpha1",
kind: "SinglePage",
metadata: {
name: uuid(),
const initialFormState: SinglePage = {
spec: {
title: "",
slug: "",
template: "",
cover: "",
deleted: false,
publish: false,
publishTime: "",
pinned: false,
allowComment: true,
visible: "PUBLIC",
version: 1,
priority: 0,
excerpt: {
autoGenerate: true,
raw: "",
},
htmlMetas: [],
},
content: {
raw: "",
content: "",
rawType: "HTML",
apiVersion: "content.halo.run/v1alpha1",
kind: "SinglePage",
metadata: {
name: uuid(),
},
};
const props = withDefaults(
defineProps<{
visible: boolean;
singlePage?: SinglePageRequest;
singlePage?: SinglePage;
publishSupport?: boolean;
onlyEmit?: boolean;
}>(),
{
visible: false,
singlePage: undefined,
publishSupport: true,
onlyEmit: false,
}
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
(event: "saved", singlePage: SinglePageRequest): void;
(event: "saved", singlePage: SinglePage): void;
(event: "published", singlePage: SinglePage): void;
}>();
const activeTab = ref("general");
const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
const formState = ref<SinglePage>(cloneDeep(initialFormState));
const saving = ref(false);
const publishing = ref(false);
const publishCanceling = ref(false);
const isUpdateMode = computed(() => {
return !!formState.value.page.metadata.creationTimestamp;
return !!formState.value.metadata.creationTimestamp;
});
const onVisibleChange = (visible: boolean) => {
@ -76,26 +75,33 @@ const onVisibleChange = (visible: boolean) => {
};
const handleSave = async () => {
if (props.onlyEmit) {
emit("saved", formState.value);
return;
}
try {
saving.value = true;
// Set rendered content
formState.value.content.content = formState.value.content.raw;
saving.value = true;
if (isUpdateMode.value) {
const { data } = await apiClient.singlePage.updateDraftSinglePage({
name: formState.value.page.metadata.name,
singlePageRequest: formState.value,
});
formState.value.page = data;
emit("saved", formState.value);
} else {
const { data } = await apiClient.singlePage.draftSinglePage({
singlePageRequest: formState.value,
});
formState.value.page = data;
emit("saved", formState.value);
}
const { data } = isUpdateMode.value
? await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{
name: formState.value.metadata.name,
singlePage: formState.value,
}
)
: await apiClient.extension.singlePage.createcontentHaloRunV1alpha1SinglePage(
{
singlePage: formState.value,
}
);
formState.value = data;
emit("saved", data);
onVisibleChange(false);
} catch (error) {
console.error("Failed to save single page", error);
} finally {
@ -103,55 +109,48 @@ const handleSave = async () => {
}
};
const handlePublish = async () => {
const handleSwitchPublish = async (publish: boolean) => {
if (props.onlyEmit) {
emit("published", formState.value);
return;
}
try {
publishing.value = true;
if (publish) {
publishing.value = true;
} else {
publishCanceling.value = true;
}
// Save single page
await handleSave();
if (publish) {
formState.value.spec.releaseSnapshot = formState.value.spec.headSnapshot;
}
// Get latest version single page
const { data: latestData } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
name: formState.value.page.metadata.name,
});
formState.value.page = latestData;
const { data } =
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{
name: formState.value.metadata.name,
singlePage: {
...formState.value,
spec: {
...formState.value.spec,
publish: publish,
},
},
}
);
// Publish single page
const { data } = await apiClient.singlePage.publishSinglePage({
name: formState.value.page.metadata.name,
});
formState.value.page = data;
emit("saved", formState.value);
formState.value = data;
if (publish) {
emit("published", data);
}
onVisibleChange(false);
} catch (error) {
console.error("Failed to publish single page", error);
} finally {
publishing.value = false;
}
};
const handleCancelPublish = async () => {
try {
publishCanceling.value = true;
// Update published spec = false
const singlePageToUpdate = cloneDeep(formState.value);
singlePageToUpdate.page.spec.published = false;
await apiClient.singlePage.updateDraftSinglePage({
name: singlePageToUpdate.page.metadata.name,
singlePageRequest: singlePageToUpdate,
});
// Get latest version single page
const { data: latestData } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
name: formState.value.page.metadata.name,
});
formState.value.page = latestData;
emit("saved", formState.value);
} catch (error) {
console.error("Failed to cancel publish single page", error);
} finally {
publishCanceling.value = false;
}
};
@ -188,21 +187,21 @@ const { templates } = useThemeCustomTemplates("page");
type="form"
>
<FormKit
v-model="formState.page.spec.title"
v-model="formState.spec.title"
label="标题"
type="text"
name="title"
validation="required"
></FormKit>
<FormKit
v-model="formState.page.spec.slug"
v-model="formState.spec.slug"
label="别名"
name="slug"
type="text"
validation="required"
></FormKit>
<FormKit
v-model="formState.page.spec.excerpt.autoGenerate"
v-model="formState.spec.excerpt.autoGenerate"
:options="[
{ label: '是', value: true },
{ label: '否', value: false },
@ -213,8 +212,8 @@ const { templates } = useThemeCustomTemplates("page");
>
</FormKit>
<FormKit
v-if="!formState.page.spec.excerpt.autoGenerate"
v-model="formState.page.spec.excerpt.raw"
v-if="!formState.spec.excerpt.autoGenerate"
v-model="formState.spec.excerpt.raw"
name="raw"
label="自定义摘要"
type="textarea"
@ -231,7 +230,7 @@ const { templates } = useThemeCustomTemplates("page");
type="form"
>
<FormKit
v-model="formState.page.spec.allowComment"
v-model="formState.spec.allowComment"
:options="[
{ label: '是', value: true },
{ label: '否', value: false },
@ -241,7 +240,7 @@ const { templates } = useThemeCustomTemplates("page");
type="radio"
></FormKit>
<FormKit
v-model="formState.page.spec.pinned"
v-model="formState.spec.pinned"
:options="[
{ label: '是', value: true },
{ label: '否', value: false },
@ -251,7 +250,7 @@ const { templates } = useThemeCustomTemplates("page");
type="radio"
></FormKit>
<FormKit
v-model="formState.page.spec.visible"
v-model="formState.spec.visible"
:options="[
{ label: '公开', value: 'PUBLIC' },
{ label: '内部成员可访问', value: 'INTERNAL' },
@ -262,20 +261,20 @@ const { templates } = useThemeCustomTemplates("page");
type="select"
></FormKit>
<FormKit
v-model="formState.page.spec.publishTime"
v-model="formState.spec.publishTime"
label="发表时间"
type="datetime-local"
name="publishTime"
></FormKit>
<FormKit
v-model="formState.page.spec.template"
v-model="formState.spec.template"
:options="templates"
label="自定义模板"
type="select"
name="template"
></FormKit>
<FormKit
v-model="formState.page.spec.cover"
v-model="formState.spec.cover"
label="封面图"
type="attachment"
name="cover"
@ -287,27 +286,28 @@ const { templates } = useThemeCustomTemplates("page");
<template #footer>
<VSpace>
<VButton :loading="publishing" type="secondary" @click="handlePublish">
{{
formState.page.status?.phase === "PUBLISHED" ? "重新发布" : "发布"
}}
</VButton>
<VButton
:loading="saving"
size="sm"
type="secondary"
@click="handleSave"
>
仅保存
</VButton>
<VButton
v-if="formState.page.status?.phase === 'PUBLISHED'"
:loading="publishCanceling"
type="danger"
size="sm"
@click="handleCancelPublish"
>
取消发布
<template v-if="publishSupport">
<VButton
v-if="
formState.metadata.labels?.[singlePageLabels.PUBLISHED] !== 'true'
"
:loading="publishing"
type="secondary"
@click="handleSwitchPublish(true)"
>
发布
</VButton>
<VButton
v-else
:loading="publishCanceling"
type="danger"
@click="handleSwitchPublish(false)"
>
取消发布
</VButton>
</template>
<VButton :loading="saving" type="secondary" @click="handleSave">
保存
</VButton>
<VButton size="sm" type="default" @click="onVisibleChange(false)">
关闭

View File

@ -2,6 +2,8 @@
import {
IconBookRead,
IconSave,
IconSettings,
IconSendPlaneFill,
VButton,
VPageHeader,
VSpace,
@ -9,12 +11,15 @@ import {
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
import PostSettingModal from "./components/PostSettingModal.vue";
import PostPreviewModal from "./components/PostPreviewModal.vue";
import type { PostRequest } from "@halo-dev/api-client";
import type { Post, PostRequest } from "@halo-dev/api-client";
import { computed, onMounted, ref } from "vue";
import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client";
import { useRouteQuery } from "@vueuse/router";
import { v4 as uuid } from "uuid";
import { useRouter } from "vue-router";
const router = useRouter();
const initialFormState: PostRequest = {
post: {
@ -24,7 +29,7 @@ const initialFormState: PostRequest = {
template: "",
cover: "",
deleted: false,
published: false,
publish: false,
publishTime: "",
pinned: false,
allowComment: true,
@ -56,6 +61,7 @@ const formState = ref<PostRequest>(cloneDeep(initialFormState));
const settingModal = ref(false);
const previewModal = ref(false);
const saving = ref(false);
const publishing = ref(false);
const isUpdateMode = computed(() => {
return !!formState.value.post.metadata.creationTimestamp;
@ -72,15 +78,25 @@ const handleSave = async () => {
if (!formState.value.post.spec.title) {
formState.value.post.spec.title = "无标题文章";
}
if (!formState.value.post.spec.slug) {
formState.value.post.spec.slug = uuid();
}
if (isUpdateMode.value) {
// Get latest post
const { data: latestPost } =
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
name: formState.value.post.metadata.name,
});
formState.value.post = latestPost;
const { data } = await apiClient.post.updateDraftPost({
name: formState.value.post.metadata.name,
postRequest: formState.value,
});
formState.value.post = data;
} else {
const { data } = await apiClient.post.draftPost({
@ -98,10 +114,77 @@ const handleSave = async () => {
}
};
const handlePublish = async () => {
try {
publishing.value = true;
// Set rendered content
formState.value.content.content = formState.value.content.raw;
if (isUpdateMode.value) {
const { headSnapshot } = formState.value.post.spec;
const { name: postName } = formState.value.post.metadata;
const { data: latestContent } =
await apiClient.content.updateSnapshotContent({
snapshotName: headSnapshot as string,
contentRequest: {
raw: formState.value.content.raw as string,
content: formState.value.content.content as string,
rawType: formState.value.content.rawType as string,
headSnapshotName: headSnapshot,
subjectRef: {
kind: "Post",
version: "v1alpha1",
group: "content.halo.run",
name: postName,
},
},
});
// Get latest post
const { data: latestPost } =
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
name: postName,
});
formState.value.post = latestPost;
formState.value.post.spec.publish = true;
formState.value.post.spec.headSnapshot = latestContent.snapshotName;
formState.value.post.spec.releaseSnapshot =
formState.value.post.spec.headSnapshot;
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
name: postName,
post: formState.value.post,
});
} else {
formState.value.post.spec.publish = true;
await apiClient.post.draftPost({
postRequest: formState.value,
});
}
router.push({ name: "Posts" });
} catch (error) {
console.error("Failed to publish post", error);
} finally {
publishing.value = false;
}
};
const handlePublishClick = () => {
if (isUpdateMode.value) {
handlePublish();
} else {
settingModal.value = true;
}
};
const handleFetchContent = async () => {
if (!formState.value.post.spec.headSnapshot) {
return;
}
const { data } = await apiClient.content.obtainSnapshotContent({
snapshotName: formState.value.post.spec.headSnapshot,
});
@ -109,14 +192,33 @@ const handleFetchContent = async () => {
formState.value.content = data;
};
const onSettingSaved = (post: PostRequest) => {
const handleOpenSettingModal = async () => {
const { data: latestPost } =
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
name: formState.value.post.metadata.name,
});
formState.value.post = latestPost;
settingModal.value = true;
};
const onSettingSaved = (post: Post) => {
// Set route query parameter
if (!isUpdateMode.value) {
name.value = post.post.metadata.name;
name.value = post.metadata.name;
}
formState.value = post;
formState.value.post = post;
settingModal.value = false;
if (!isUpdateMode.value) {
handleSave();
}
};
const onSettingPublished = (post: Post) => {
formState.value.post = post;
settingModal.value = false;
handlePublish();
};
// Get post data when the route contains the name parameter
@ -139,8 +241,11 @@ onMounted(async () => {
<template>
<PostSettingModal
v-model:visible="settingModal"
:post="formState"
:post="formState.post"
:publish-support="!isUpdateMode"
:only-emit="!isUpdateMode"
@saved="onSettingSaved"
@published="onSettingPublished"
/>
<PostPreviewModal v-model:visible="previewModal" :post="formState.post" />
<VPageHeader title="文章">
@ -159,12 +264,30 @@ onMounted(async () => {
预览
</VButton>
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
保存
</VButton>
<VButton type="secondary" @click="settingModal = true">
<template #icon>
<IconSave class="h-full w-full" />
</template>
保存
</VButton>
<VButton
v-if="isUpdateMode"
size="sm"
type="default"
@click="handleOpenSettingModal"
>
<template #icon>
<IconSettings class="h-full w-full" />
</template>
设置
</VButton>
<VButton
type="secondary"
:loading="publishing"
@click="handlePublishClick"
>
<template #icon>
<IconSendPlaneFill class="h-full w-full" />
</template>
发布
</VButton>
</VSpace>

View File

@ -25,13 +25,12 @@ import {
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
import PostSettingModal from "./components/PostSettingModal.vue";
import PostTag from "../posts/tags/components/PostTag.vue";
import { onMounted, ref, watch, watchEffect } from "vue";
import { onMounted, ref, watch } from "vue";
import type {
User,
Category,
ListedPostList,
Post,
PostRequest,
Tag,
} from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
@ -41,15 +40,10 @@ import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-t
import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
import cloneDeep from "lodash.clonedeep";
import { postLabels } from "@/constants/labels";
const { currentUserHasPermission } = usePermission();
enum PostPhase {
DRAFT = "未发布",
PENDING_APPROVAL = "待审核",
PUBLISHED = "已发布",
}
const posts = ref<ListedPostList>({
page: 1,
size: 20,
@ -62,8 +56,7 @@ const posts = ref<ListedPostList>({
});
const loading = ref(false);
const settingModal = ref(false);
const selectedPost = ref<Post | null>(null);
const selectedPostWithContent = ref<PostRequest | null>(null);
const selectedPost = ref<Post>();
const checkedAll = ref(false);
const selectedPostNames = ref<string[]>([]);
const refreshInterval = ref();
@ -77,6 +70,7 @@ const handleFetchPosts = async () => {
let categories: string[] | undefined;
let tags: string[] | undefined;
let contributors: string[] | undefined;
const labelSelector: string[] = ["content.halo.run/deleted=false"];
if (selectedCategory.value) {
categories = [
@ -93,12 +87,17 @@ const handleFetchPosts = async () => {
contributors = [selectedContributor.value.metadata.name];
}
if (selectedPublishStatusItem.value.value !== undefined) {
labelSelector.push(
`${postLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
);
}
const { data } = await apiClient.post.listPosts({
labelSelector: [`content.halo.run/deleted=false`],
labelSelector,
page: posts.value.page,
size: posts.value.size,
visible: selectedVisibleItem.value?.value,
publishPhase: selectedPublishPhaseItem.value?.value,
sort: selectedSortItem.value?.sort,
sortOrder: selectedSortItem.value?.sortOrder,
keyword: keyword.value,
@ -108,14 +107,20 @@ const handleFetchPosts = async () => {
});
posts.value = data;
const deletedPosts = posts.value.items.filter(
(post) => post.post.spec.deleted
);
// When an post is in the process of deleting or publishing, the list needs to be refreshed regularly
const abnormalPosts = posts.value.items.filter((post) => {
const { spec, metadata, status } = post.post;
return (
spec.deleted ||
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
});
if (deletedPosts.length) {
if (abnormalPosts.length) {
refreshInterval.value = setInterval(() => {
handleFetchPosts();
}, 3000);
}, 1000);
}
} catch (e) {
console.error("Failed to fetch posts", e);
@ -151,8 +156,7 @@ const handleOpenSettingModal = async (post: Post) => {
};
const onSettingModalClose = () => {
selectedPost.value = null;
selectedPostWithContent.value = null;
selectedPost.value = undefined;
handleFetchPosts();
};
@ -203,11 +207,17 @@ const checkSelection = (post: Post) => {
);
};
const finalStatus = (post: Post) => {
if (post.status?.phase) {
return PostPhase[post.status.phase];
}
return "";
const getPublishStatus = (post: Post) => {
const { labels } = post.metadata;
return labels?.[postLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
};
const isPublishing = (post: Post) => {
const { spec, status, metadata } = post;
return (
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
};
const handleCheckAllChange = (e: Event) => {
@ -273,21 +283,6 @@ watch(selectedPostNames, (newValue) => {
checkedAll.value = newValue.length === posts.value.items?.length;
});
watchEffect(async () => {
if (!selectedPost.value || !selectedPost.value.spec.headSnapshot) {
return;
}
const { data: content } = await apiClient.content.obtainSnapshotContent({
snapshotName: selectedPost.value.spec.headSnapshot,
});
selectedPostWithContent.value = {
post: selectedPost.value,
content: content,
};
});
onMounted(() => {
handleFetchPosts();
});
@ -299,9 +294,9 @@ interface VisibleItem {
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
}
interface PublishPhaseItem {
interface PublishStatuItem {
label: string;
value?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED";
value?: boolean;
}
interface SortItem {
@ -329,22 +324,18 @@ const VisibleItems: VisibleItem[] = [
},
];
const PublishPhaseItems: PublishPhaseItem[] = [
const PublishStatuItems: PublishStatuItem[] = [
{
label: "全部",
value: undefined,
},
{
label: "已发布",
value: "PUBLISHED",
value: true,
},
{
label: "未发布",
value: "DRAFT",
},
{
label: "待审核",
value: "PENDING_APPROVAL",
value: false,
},
];
@ -375,7 +366,7 @@ const { categories } = usePostCategory({ fetchOnMounted: true });
const { tags } = usePostTag({ fetchOnMounted: true });
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
const selectedPublishPhaseItem = ref<PublishPhaseItem>(PublishPhaseItems[0]);
const selectedPublishStatusItem = ref<PublishStatuItem>(PublishStatuItems[0]);
const selectedSortItem = ref<SortItem>();
const selectedCategory = ref<Category>();
const selectedTag = ref<Tag>();
@ -387,8 +378,8 @@ function handleVisibleItemChange(visibleItem: VisibleItem) {
handleFetchPosts();
}
function handlePublishPhaseItemChange(publishPhaseItem: PublishPhaseItem) {
selectedPublishPhaseItem.value = publishPhaseItem;
function handlePublishStatusItemChange(publishStatusItem: PublishStatuItem) {
selectedPublishStatusItem.value = publishStatusItem;
handleFetchPosts();
}
@ -415,7 +406,7 @@ function handleContributorChange(user?: User) {
<template>
<PostSettingModal
v-model:visible="settingModal"
:post="selectedPostWithContent"
:post="selectedPost"
@close="onSettingModalClose"
>
<template #actions>
@ -482,15 +473,15 @@ function handleContributorChange(user?: User) {
></FormKit>
<div
v-if="selectedPublishPhaseItem.value"
v-if="selectedPublishStatusItem.value"
class="group flex cursor-pointer items-center justify-center gap-1 rounded-full bg-gray-200 px-2 py-1 hover:bg-gray-300"
>
<span class="text-xs text-gray-600 group-hover:text-gray-900">
状态{{ selectedPublishPhaseItem.label }}
状态{{ selectedPublishStatusItem.label }}
</span>
<IconCloseCircle
class="h-4 w-4 text-gray-600"
@click="handlePublishPhaseItemChange(PublishPhaseItems[0])"
@click="handlePublishStatusItemChange(PublishStatuItems[0])"
/>
</div>
<div
@ -576,16 +567,16 @@ function handleContributorChange(user?: User) {
<div class="w-72 p-4">
<ul class="space-y-1">
<li
v-for="(filterItem, index) in PublishPhaseItems"
v-for="(filterItem, index) in PublishStatuItems"
:key="index"
v-close-popper
:class="{
'bg-gray-100':
selectedPublishPhaseItem.value ===
selectedPublishStatusItem.value ===
filterItem.value,
}"
class="flex cursor-pointer items-center rounded px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
@click="handlePublishPhaseItemChange(filterItem)"
@click="handlePublishStatusItemChange(filterItem)"
>
<span class="truncate">{{ filterItem.label }}</span>
</li>
@ -933,9 +924,11 @@ function handleContributorChange(user?: User) {
</RouterLink>
</template>
</VEntityField>
<VEntityField
:description="finalStatus(post.post)"
></VEntityField>
<VEntityField :description="getPublishStatus(post.post)">
<template v-if="isPublishing(post.post)" #description>
<VStatusDot text="发布中" animate />
</template>
</VEntityField>
<VEntityField>
<template #description>
<IconEye

View File

@ -1,73 +1,72 @@
<script lang="ts" setup>
import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
import { computed, ref, watchEffect } from "vue";
import type { PostRequest } from "@halo-dev/api-client";
import type { Post } from "@halo-dev/api-client";
import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client";
import { v4 as uuid } from "uuid";
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
import { postLabels } from "@/constants/labels";
const initialFormState: PostRequest = {
post: {
spec: {
title: "",
slug: "",
template: "",
cover: "",
deleted: false,
published: false,
publishTime: "",
pinned: false,
allowComment: true,
visible: "PUBLIC",
version: 1,
priority: 0,
excerpt: {
autoGenerate: true,
raw: "",
},
categories: [],
tags: [],
htmlMetas: [],
},
apiVersion: "content.halo.run/v1alpha1",
kind: "Post",
metadata: {
name: uuid(),
const initialFormState: Post = {
spec: {
title: "",
slug: "",
template: "",
cover: "",
deleted: false,
publish: false,
publishTime: "",
pinned: false,
allowComment: true,
visible: "PUBLIC",
version: 1,
priority: 0,
excerpt: {
autoGenerate: true,
raw: "",
},
categories: [],
tags: [],
htmlMetas: [],
},
content: {
raw: "",
content: "",
rawType: "HTML",
apiVersion: "content.halo.run/v1alpha1",
kind: "Post",
metadata: {
name: uuid(),
},
};
const props = withDefaults(
defineProps<{
visible: boolean;
post?: PostRequest | null;
post?: Post;
publishSupport?: boolean;
onlyEmit?: boolean;
}>(),
{
visible: false,
post: null,
post: undefined,
publishSupport: true,
onlyEmit: false,
}
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
(event: "saved", post: PostRequest): void;
(event: "saved", post: Post): void;
(event: "published", post: Post): void;
}>();
const activeTab = ref("general");
const formState = ref<PostRequest>(cloneDeep(initialFormState));
const formState = ref<Post>(cloneDeep(initialFormState));
const saving = ref(false);
const publishing = ref(false);
const publishCanceling = ref(false);
const isUpdateMode = computed(() => {
return !!formState.value.post.metadata.creationTimestamp;
return !!formState.value.metadata.creationTimestamp;
});
const handleVisibleChange = (visible: boolean) => {
@ -78,26 +77,27 @@ const handleVisibleChange = (visible: boolean) => {
};
const handleSave = async () => {
if (props.onlyEmit) {
emit("saved", formState.value);
return;
}
try {
saving.value = true;
// Set rendered content
formState.value.content.content = formState.value.content.raw;
const { data } = isUpdateMode.value
? await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
name: formState.value.metadata.name,
post: formState.value,
})
: await apiClient.extension.post.createcontentHaloRunV1alpha1Post({
post: formState.value,
});
if (isUpdateMode.value) {
const { data } = await apiClient.post.updateDraftPost({
name: formState.value.post.metadata.name,
postRequest: formState.value,
});
formState.value.post = data;
emit("saved", formState.value);
} else {
const { data } = await apiClient.post.draftPost({
postRequest: formState.value,
});
formState.value.post = data;
emit("saved", formState.value);
}
formState.value = data;
emit("saved", data);
handleVisibleChange(false);
} catch (e) {
console.error("Failed to save post", e);
} finally {
@ -105,56 +105,46 @@ const handleSave = async () => {
}
};
const handlePublish = async () => {
const handleSwitchPublish = async (publish: boolean) => {
if (props.onlyEmit) {
emit("published", formState.value);
return;
}
try {
publishing.value = true;
if (publish) {
publishing.value = true;
} else {
publishCanceling.value = true;
}
// Save post
await handleSave();
if (publish) {
formState.value.spec.releaseSnapshot = formState.value.spec.headSnapshot;
}
// Get latest version post
const { data: latestData } =
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
name: formState.value.post.metadata.name,
const { data } =
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
name: formState.value.metadata.name,
post: {
...formState.value,
spec: {
...formState.value.spec,
publish: publish,
},
},
});
formState.value.post = latestData;
// Publish post
const { data } = await apiClient.post.publishPost({
name: formState.value.post.metadata.name,
});
formState.value.post = data;
emit("saved", formState.value);
formState.value = data;
if (publish) {
emit("published", data);
}
handleVisibleChange(false);
} catch (e) {
console.error("Failed to publish post", e);
} finally {
publishing.value = false;
}
};
const handlePublishCanceling = async () => {
try {
publishCanceling.value = true;
// Update published spec = false
const postToUpdate = cloneDeep(formState.value);
postToUpdate.post.spec.published = false;
await apiClient.post.updateDraftPost({
name: postToUpdate.post.metadata.name,
postRequest: postToUpdate,
});
// Get latest version post
const { data: latestData } =
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
name: formState.value.post.metadata.name,
});
formState.value.post = latestData;
emit("saved", formState.value);
} catch (e) {
console.log("Failed to cancel publish", e);
} finally {
publishCanceling.value = false;
}
};
@ -190,33 +180,33 @@ const { templates } = useThemeCustomTemplates("post");
type="form"
>
<FormKit
v-model="formState.post.spec.title"
v-model="formState.spec.title"
label="标题"
type="text"
name="title"
validation="required"
></FormKit>
<FormKit
v-model="formState.post.spec.slug"
v-model="formState.spec.slug"
label="别名"
name="slug"
type="text"
validation="required"
></FormKit>
<FormKit
v-model="formState.post.spec.categories"
v-model="formState.spec.categories"
label="分类目录"
name="categories"
type="categoryCheckbox"
/>
<FormKit
v-model="formState.post.spec.tags"
v-model="formState.spec.tags"
label="标签"
name="tags"
type="tagCheckbox"
/>
<FormKit
v-model="formState.post.spec.excerpt.autoGenerate"
v-model="formState.spec.excerpt.autoGenerate"
:options="[
{ label: '是', value: true },
{ label: '否', value: false },
@ -227,8 +217,8 @@ const { templates } = useThemeCustomTemplates("post");
>
</FormKit>
<FormKit
v-if="!formState.post.spec.excerpt.autoGenerate"
v-model="formState.post.spec.excerpt.raw"
v-if="!formState.spec.excerpt.autoGenerate"
v-model="formState.spec.excerpt.raw"
label="自定义摘要"
name="raw"
type="textarea"
@ -245,7 +235,7 @@ const { templates } = useThemeCustomTemplates("post");
type="form"
>
<FormKit
v-model="formState.post.spec.allowComment"
v-model="formState.spec.allowComment"
:options="[
{ label: '是', value: true },
{ label: '否', value: false },
@ -254,7 +244,7 @@ const { templates } = useThemeCustomTemplates("post");
type="radio"
></FormKit>
<FormKit
v-model="formState.post.spec.pinned"
v-model="formState.spec.pinned"
:options="[
{ label: '是', value: true },
{ label: '否', value: false },
@ -264,7 +254,7 @@ const { templates } = useThemeCustomTemplates("post");
type="radio"
></FormKit>
<FormKit
v-model="formState.post.spec.visible"
v-model="formState.spec.visible"
:options="[
{ label: '公开', value: 'PUBLIC' },
{ label: '内部成员可访问', value: 'INTERNAL' },
@ -275,19 +265,19 @@ const { templates } = useThemeCustomTemplates("post");
type="select"
></FormKit>
<FormKit
v-model="formState.post.spec.publishTime"
v-model="formState.spec.publishTime"
label="发表时间"
type="datetime-local"
></FormKit>
<FormKit
v-model="formState.post.spec.template"
v-model="formState.spec.template"
:options="templates"
label="自定义模板"
name="template"
type="select"
></FormKit>
<FormKit
v-model="formState.post.spec.cover"
v-model="formState.spec.cover"
name="cover"
label="封面图"
type="attachment"
@ -299,29 +289,28 @@ const { templates } = useThemeCustomTemplates("post");
<template #footer>
<VSpace>
<VButton :loading="publishing" type="secondary" @click="handlePublish">
{{
formState.post.status?.phase === "PUBLISHED" ? "重新发布" : "发布"
}}
<template v-if="publishSupport">
<VButton
v-if="formState.metadata.labels?.[postLabels.PUBLISHED] !== 'true'"
:loading="publishing"
type="secondary"
@click="handleSwitchPublish(true)"
>
发布
</VButton>
<VButton
v-else
:loading="publishCanceling"
type="danger"
@click="handleSwitchPublish(false)"
>
取消发布
</VButton>
</template>
<VButton :loading="saving" type="secondary" @click="handleSave">
保存
</VButton>
<VButton
:loading="saving"
size="sm"
type="secondary"
@click="handleSave"
>
仅保存
</VButton>
<VButton
v-if="formState.post.status?.phase === 'PUBLISHED'"
:loading="publishCanceling"
type="danger"
size="sm"
@click="handlePublishCanceling"
>
取消发布
</VButton>
<VButton size="sm" type="default" @click="handleVisibleChange(false)">
<VButton type="default" @click="handleVisibleChange(false)">
关闭
</VButton>
</VSpace>

View File

@ -4,14 +4,16 @@ import { onMounted, ref } from "vue";
import type { ListedPost } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { formatDatetime } from "@/utils/date";
import { postLabels } from "@/constants/labels";
const posts = ref<ListedPost[]>([] as ListedPost[]);
const handleFetchPosts = async () => {
try {
const { data } = await apiClient.post.listPosts({
labelSelector: [`${postLabels.PUBLISHED}=true`],
sort: "PUBLISH_TIME",
publishPhase: "PUBLISHED",
sortOrder: false,
page: 1,
size: 10,
});

View File

@ -50,25 +50,11 @@ const handleSubmit = async () => {
});
await apiClient.post.draftPost({ postRequest: post as PostRequest });
try {
await apiClient.post.publishPost({ name: post.post.metadata.name });
} catch (error) {
console.error("Failed to publish post", error);
}
// Create singlePage
await apiClient.singlePage.draftSinglePage({
singlePageRequest: singlePage as SinglePageRequest,
});
try {
await apiClient.singlePage.publishSinglePage({
name: singlePage.page.metadata.name,
});
} catch (error) {
console.error("Failed to publish singlePage", error);
}
// Create menu and menu items
const menuItemPromises = menuItems.map((item) => {
return apiClient.extension.menuItem.createv1alpha1MenuItem({

View File

@ -6,7 +6,7 @@
"template": "",
"cover": "",
"deleted": false,
"published": false,
"publish": true,
"publishTime": "",
"pinned": false,
"allowComment": true,

View File

@ -6,7 +6,7 @@
"template": "",
"cover": "",
"deleted": false,
"published": false,
"publish": true,
"publishTime": "",
"pinned": false,
"allowComment": true,