mirror of https://github.com/halo-dev/halo
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
parent
6bb7af1762
commit
55b6d77c80
|
@ -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",
|
||||
|
|
|
@ -40,7 +40,7 @@ const classes = computed(() => {
|
|||
}
|
||||
|
||||
.status-dot-text {
|
||||
@apply text-gray-500;
|
||||
@apply text-gray-500 text-xs;
|
||||
}
|
||||
|
||||
&.status-dot-animate {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)">
|
||||
关闭
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"template": "",
|
||||
"cover": "",
|
||||
"deleted": false,
|
||||
"published": false,
|
||||
"publish": true,
|
||||
"publishTime": "",
|
||||
"pinned": false,
|
||||
"allowComment": true,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"template": "",
|
||||
"cover": "",
|
||||
"deleted": false,
|
||||
"published": false,
|
||||
"publish": true,
|
||||
"publishTime": "",
|
||||
"pinned": false,
|
||||
"allowComment": true,
|
||||
|
|
Loading…
Reference in New Issue