mirror of https://github.com/halo-dev/halo-admin
refactor: post and singlePage publishing (#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/690/head
parent
f3e44717ee
commit
fb446d382f
|
@ -33,7 +33,7 @@
|
||||||
"@formkit/inputs": "^1.0.0-beta.11",
|
"@formkit/inputs": "^1.0.0-beta.11",
|
||||||
"@formkit/themes": "^1.0.0-beta.11",
|
"@formkit/themes": "^1.0.0-beta.11",
|
||||||
"@formkit/vue": "^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/components": "workspace:*",
|
||||||
"@halo-dev/console-shared": "workspace:*",
|
"@halo-dev/console-shared": "workspace:*",
|
||||||
"@halo-dev/richtext-editor": "^0.0.0-alpha.11",
|
"@halo-dev/richtext-editor": "^0.0.0-alpha.11",
|
||||||
|
|
|
@ -40,7 +40,7 @@ const classes = computed(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot-text {
|
.status-dot-text {
|
||||||
@apply text-gray-500;
|
@apply text-gray-500 text-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.status-dot-animate {
|
&.status-dot-animate {
|
||||||
|
|
|
@ -52,6 +52,8 @@ import IconReplyLine from "~icons/ri/reply-line";
|
||||||
import IconExternalLinkLine from "~icons/ri/external-link-line";
|
import IconExternalLinkLine from "~icons/ri/external-link-line";
|
||||||
import IconRefreshLine from "~icons/ri/refresh-line";
|
import IconRefreshLine from "~icons/ri/refresh-line";
|
||||||
import IconWindowLine from "~icons/ri/window-line";
|
import IconWindowLine from "~icons/ri/window-line";
|
||||||
|
import IconSendPlaneFill from "~icons/ri/send-plane-fill";
|
||||||
|
import IconRocketLine from "~icons/ri/rocket-line";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
|
@ -108,4 +110,6 @@ export {
|
||||||
IconExternalLinkLine,
|
IconExternalLinkLine,
|
||||||
IconRefreshLine,
|
IconRefreshLine,
|
||||||
IconWindowLine,
|
IconWindowLine,
|
||||||
|
IconSendPlaneFill,
|
||||||
|
IconRocketLine,
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,7 +13,7 @@ importers:
|
||||||
'@formkit/inputs': ^1.0.0-beta.11
|
'@formkit/inputs': ^1.0.0-beta.11
|
||||||
'@formkit/themes': ^1.0.0-beta.11
|
'@formkit/themes': ^1.0.0-beta.11
|
||||||
'@formkit/vue': ^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/components': workspace:*
|
||||||
'@halo-dev/console-shared': workspace:*
|
'@halo-dev/console-shared': workspace:*
|
||||||
'@halo-dev/richtext-editor': ^0.0.0-alpha.11
|
'@halo-dev/richtext-editor': ^0.0.0-alpha.11
|
||||||
|
@ -108,7 +108,7 @@ importers:
|
||||||
'@formkit/inputs': 1.0.0-beta.11
|
'@formkit/inputs': 1.0.0-beta.11
|
||||||
'@formkit/themes': 1.0.0-beta.11_tailwindcss@3.2.1
|
'@formkit/themes': 1.0.0-beta.11_tailwindcss@3.2.1
|
||||||
'@formkit/vue': 1.0.0-beta.11_vjnbgdptsk6bkj7ab5a6mk2cwm
|
'@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/components': link:packages/components
|
||||||
'@halo-dev/console-shared': link:packages/shared
|
'@halo-dev/console-shared': link:packages/shared
|
||||||
'@halo-dev/richtext-editor': 0.0.0-alpha.11_vue@3.2.41
|
'@halo-dev/richtext-editor': 0.0.0-alpha.11_vue@3.2.41
|
||||||
|
@ -1953,8 +1953,8 @@ packages:
|
||||||
- windicss
|
- windicss
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@halo-dev/api-client/0.0.41:
|
/@halo-dev/api-client/0.0.43:
|
||||||
resolution: {integrity: sha512-YpwoIyT+6BjNEfhQqZPSG7dewmC9AE7wxc/uaIRcVZdKr0C4GLmbiBKGOFFnVWIYr42islhMWjqYCGaiJSzkUg==}
|
resolution: {integrity: sha512-bCh5P7AYCYA/nVbAB/t62acrtOaDs8UItFe4JmK1qfeb5vOmFT2FT9jeJFj4ABqa4t4xJxy9hnoxNm6H+iB3IQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@halo-dev/richtext-editor/0.0.0-alpha.11_vue@3.2.41:
|
/@halo-dev/richtext-editor/0.0.0-alpha.11_vue@3.2.41:
|
||||||
|
|
|
@ -11,6 +11,16 @@ export enum roleLabels {
|
||||||
// post
|
// post
|
||||||
export enum postLabels {
|
export enum postLabels {
|
||||||
DELETED = "content.halo.run/deleted",
|
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",
|
OWNER = "content.halo.run/owner",
|
||||||
VISIBLE = "content.halo.run/visible",
|
VISIBLE = "content.halo.run/visible",
|
||||||
PHASE = "content.halo.run/phase",
|
PHASE = "content.halo.run/phase",
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import {
|
import {
|
||||||
VPageHeader,
|
VPageHeader,
|
||||||
IconPages,
|
IconPages,
|
||||||
|
IconSettings,
|
||||||
|
IconSendPlaneFill,
|
||||||
VSpace,
|
VSpace,
|
||||||
VButton,
|
VButton,
|
||||||
IconSave,
|
IconSave,
|
||||||
|
@ -9,12 +11,15 @@ import {
|
||||||
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||||
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||||
import PostPreviewModal from "../posts/components/PostPreviewModal.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 { v4 as uuid } from "uuid";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const initialFormState: SinglePageRequest = {
|
const initialFormState: SinglePageRequest = {
|
||||||
page: {
|
page: {
|
||||||
|
@ -24,7 +29,7 @@ const initialFormState: SinglePageRequest = {
|
||||||
template: "",
|
template: "",
|
||||||
cover: "",
|
cover: "",
|
||||||
deleted: false,
|
deleted: false,
|
||||||
published: false,
|
publish: false,
|
||||||
publishTime: "",
|
publishTime: "",
|
||||||
pinned: false,
|
pinned: false,
|
||||||
allowComment: true,
|
allowComment: true,
|
||||||
|
@ -52,6 +57,7 @@ const initialFormState: SinglePageRequest = {
|
||||||
|
|
||||||
const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
|
const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
const publishing = ref(false);
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const previewModal = ref(false);
|
const previewModal = ref(false);
|
||||||
|
|
||||||
|
@ -77,10 +83,21 @@ const handleSave = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUpdateMode.value) {
|
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({
|
const { data } = await apiClient.singlePage.updateDraftSinglePage({
|
||||||
name: formState.value.page.metadata.name,
|
name: formState.value.page.metadata.name,
|
||||||
singlePageRequest: formState.value,
|
singlePageRequest: formState.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
formState.value.page = data;
|
formState.value.page = data;
|
||||||
} else {
|
} else {
|
||||||
const { data } = await apiClient.singlePage.draftSinglePage({
|
const { data } = await apiClient.singlePage.draftSinglePage({
|
||||||
|
@ -89,6 +106,7 @@ const handleSave = async () => {
|
||||||
formState.value.page = data;
|
formState.value.page = data;
|
||||||
routeQueryName.value = data.metadata.name;
|
routeQueryName.value = data.metadata.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save single page", 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 () => {
|
const handleFetchContent = async () => {
|
||||||
if (!formState.value.page.spec.headSnapshot) {
|
if (!formState.value.page.spec.headSnapshot) {
|
||||||
return;
|
return;
|
||||||
|
@ -108,14 +196,33 @@ const handleFetchContent = async () => {
|
||||||
formState.value.content = data;
|
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
|
// Set route query parameter
|
||||||
if (!isUpdateMode.value) {
|
if (!isUpdateMode.value) {
|
||||||
routeQueryName.value = page.page.metadata.name;
|
routeQueryName.value = page.metadata.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
formState.value = page;
|
formState.value.page = page;
|
||||||
settingModal.value = false;
|
settingModal.value = false;
|
||||||
|
|
||||||
|
if (!isUpdateMode.value) {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSettingPublished = (singlePage: SinglePage) => {
|
||||||
|
formState.value.page = singlePage;
|
||||||
|
settingModal.value = false;
|
||||||
|
handlePublish();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -135,8 +242,11 @@ onMounted(async () => {
|
||||||
<template>
|
<template>
|
||||||
<SinglePageSettingModal
|
<SinglePageSettingModal
|
||||||
v-model:visible="settingModal"
|
v-model:visible="settingModal"
|
||||||
:single-page="formState"
|
:single-page="formState.page"
|
||||||
|
:publish-support="!isUpdateMode"
|
||||||
|
:only-emit="!isUpdateMode"
|
||||||
@saved="onSettingSaved"
|
@saved="onSettingSaved"
|
||||||
|
@published="onSettingPublished"
|
||||||
/>
|
/>
|
||||||
<PostPreviewModal v-model:visible="previewModal" />
|
<PostPreviewModal v-model:visible="previewModal" />
|
||||||
<VPageHeader title="自定义页面">
|
<VPageHeader title="自定义页面">
|
||||||
|
@ -155,12 +265,30 @@ onMounted(async () => {
|
||||||
预览
|
预览
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
||||||
保存
|
|
||||||
</VButton>
|
|
||||||
<VButton type="secondary" @click="settingModal = true">
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconSave class="h-full w-full" />
|
<IconSave class="h-full w-full" />
|
||||||
</template>
|
</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>
|
</VButton>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
|
|
|
@ -22,11 +22,10 @@ import {
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||||
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
|
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
|
||||||
import { onMounted, ref, watch, watchEffect } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import type {
|
import type {
|
||||||
ListedSinglePageList,
|
ListedSinglePageList,
|
||||||
SinglePage,
|
SinglePage,
|
||||||
SinglePageRequest,
|
|
||||||
User,
|
User,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
@ -34,15 +33,10 @@ import { formatDatetime } from "@/utils/date";
|
||||||
import { onBeforeRouteLeave, RouterLink } from "vue-router";
|
import { onBeforeRouteLeave, RouterLink } from "vue-router";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
import { singlePageLabels } from "@/constants/labels";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
enum SinglePagePhase {
|
|
||||||
DRAFT = "未发布",
|
|
||||||
PENDING_APPROVAL = "待审核",
|
|
||||||
PUBLISHED = "已发布",
|
|
||||||
}
|
|
||||||
|
|
||||||
const singlePages = ref<ListedSinglePageList>({
|
const singlePages = ref<ListedSinglePageList>({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
@ -56,7 +50,6 @@ const singlePages = ref<ListedSinglePageList>({
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const selectedSinglePage = ref<SinglePage>();
|
const selectedSinglePage = ref<SinglePage>();
|
||||||
const selectedSinglePageWithContent = ref<SinglePageRequest>();
|
|
||||||
const selectedPageNames = ref<string[]>([]);
|
const selectedPageNames = ref<string[]>([]);
|
||||||
const checkedAll = ref(false);
|
const checkedAll = ref(false);
|
||||||
const refreshInterval = ref();
|
const refreshInterval = ref();
|
||||||
|
@ -68,32 +61,44 @@ const handleFetchSinglePages = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
let contributors: string[] | undefined;
|
let contributors: string[] | undefined;
|
||||||
|
const labelSelector: string[] = ["content.halo.run/deleted=false"];
|
||||||
|
|
||||||
if (selectedContributor.value) {
|
if (selectedContributor.value) {
|
||||||
contributors = [selectedContributor.value.metadata.name];
|
contributors = [selectedContributor.value.metadata.name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedPublishStatusItem.value.value !== undefined) {
|
||||||
|
labelSelector.push(
|
||||||
|
`${singlePageLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await apiClient.singlePage.listSinglePages({
|
const { data } = await apiClient.singlePage.listSinglePages({
|
||||||
labelSelector: [`content.halo.run/deleted=false`],
|
labelSelector,
|
||||||
page: singlePages.value.page,
|
page: singlePages.value.page,
|
||||||
size: singlePages.value.size,
|
size: singlePages.value.size,
|
||||||
visible: selectedVisibleItem.value.value,
|
visible: selectedVisibleItem.value.value,
|
||||||
sort: selectedSortItem.value?.sort,
|
sort: selectedSortItem.value?.sort,
|
||||||
publishPhase: selectedPublishPhaseItem.value.value,
|
|
||||||
sortOrder: selectedSortItem.value?.sortOrder,
|
sortOrder: selectedSortItem.value?.sortOrder,
|
||||||
keyword: keyword.value,
|
keyword: keyword.value,
|
||||||
contributor: contributors,
|
contributor: contributors,
|
||||||
});
|
});
|
||||||
singlePages.value = data;
|
singlePages.value = data;
|
||||||
|
|
||||||
const deletedSinglePages = singlePages.value.items.filter(
|
const abnormalSinglePages = singlePages.value.items.filter((singlePage) => {
|
||||||
(singlePage) => singlePage.page.spec.deleted
|
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(() => {
|
refreshInterval.value = setInterval(() => {
|
||||||
handleFetchSinglePages();
|
handleFetchSinglePages();
|
||||||
}, 3000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch single pages", error);
|
console.error("Failed to fetch single pages", error);
|
||||||
|
@ -129,28 +134,9 @@ const handleOpenSettingModal = async (singlePage: SinglePage) => {
|
||||||
|
|
||||||
const onSettingModalClose = () => {
|
const onSettingModalClose = () => {
|
||||||
selectedSinglePage.value = undefined;
|
selectedSinglePage.value = undefined;
|
||||||
selectedSinglePageWithContent.value = undefined;
|
|
||||||
handleFetchSinglePages();
|
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 handleSelectPrevious = async () => {
|
||||||
const { items, hasPrevious } = singlePages.value;
|
const { items, hasPrevious } = singlePages.value;
|
||||||
const index = items.findIndex(
|
const index = items.findIndex(
|
||||||
|
@ -264,35 +250,24 @@ const handleDeleteInBatch = async () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalStatus = (singlePage: SinglePage) => {
|
const getPublishStatus = (singlePage: SinglePage) => {
|
||||||
if (singlePage.status?.phase) {
|
const { labels } = singlePage.metadata;
|
||||||
return SinglePagePhase[singlePage.status.phase];
|
return labels?.[singlePageLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
|
||||||
}
|
};
|
||||||
return "";
|
|
||||||
|
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) => {
|
watch(selectedPageNames, (newValue) => {
|
||||||
checkedAll.value = newValue.length === singlePages.value.items?.length;
|
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);
|
onMounted(handleFetchSinglePages);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
|
@ -302,9 +277,9 @@ interface VisibleItem {
|
||||||
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublishPhaseItem {
|
interface PublishStatusItem {
|
||||||
label: string;
|
label: string;
|
||||||
value?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED";
|
value?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortItem {
|
interface SortItem {
|
||||||
|
@ -332,22 +307,18 @@ const VisibleItems: VisibleItem[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const PublishPhaseItems: PublishPhaseItem[] = [
|
const PublishStatusItems: PublishStatusItem[] = [
|
||||||
{
|
{
|
||||||
label: "全部",
|
label: "全部",
|
||||||
value: undefined,
|
value: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "已发布",
|
label: "已发布",
|
||||||
value: "PUBLISHED",
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "未发布",
|
label: "未发布",
|
||||||
value: "DRAFT",
|
value: false,
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "待审核",
|
|
||||||
value: "PENDING_APPROVAL",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -376,7 +347,7 @@ const SortItems: SortItem[] = [
|
||||||
|
|
||||||
const selectedContributor = ref<User>();
|
const selectedContributor = ref<User>();
|
||||||
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
|
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
|
||||||
const selectedPublishPhaseItem = ref<PublishPhaseItem>(PublishPhaseItems[0]);
|
const selectedPublishStatusItem = ref<PublishStatusItem>(PublishStatusItems[0]);
|
||||||
const selectedSortItem = ref<SortItem>();
|
const selectedSortItem = ref<SortItem>();
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
|
|
||||||
|
@ -390,8 +361,8 @@ const handleSelectUser = (user?: User) => {
|
||||||
handleFetchSinglePages();
|
handleFetchSinglePages();
|
||||||
};
|
};
|
||||||
|
|
||||||
function handlePublishPhaseItemChange(publishPhaseItem: PublishPhaseItem) {
|
function handlePublishStatusItemChange(publishStatusItem: PublishStatusItem) {
|
||||||
selectedPublishPhaseItem.value = publishPhaseItem;
|
selectedPublishStatusItem.value = publishStatusItem;
|
||||||
handleFetchSinglePages();
|
handleFetchSinglePages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,7 +375,7 @@ function handleSortItemChange(sortItem?: SortItem) {
|
||||||
<template>
|
<template>
|
||||||
<SinglePageSettingModal
|
<SinglePageSettingModal
|
||||||
v-model:visible="settingModal"
|
v-model:visible="settingModal"
|
||||||
:single-page="selectedSinglePageWithContent"
|
:single-page="selectedSinglePage"
|
||||||
@close="onSettingModalClose"
|
@close="onSettingModalClose"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
@ -446,15 +417,15 @@ function handleSortItemChange(sortItem?: SortItem) {
|
||||||
@keyup.enter="handleFetchSinglePages"
|
@keyup.enter="handleFetchSinglePages"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<div
|
<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"
|
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">
|
<span class="text-xs text-gray-600 group-hover:text-gray-900">
|
||||||
状态:{{ selectedPublishPhaseItem.label }}
|
状态:{{ selectedPublishStatusItem.label }}
|
||||||
</span>
|
</span>
|
||||||
<IconCloseCircle
|
<IconCloseCircle
|
||||||
class="h-4 w-4 text-gray-600"
|
class="h-4 w-4 text-gray-600"
|
||||||
@click="handlePublishPhaseItemChange(PublishPhaseItems[0])"
|
@click="handlePublishStatusItemChange(PublishStatusItems[0])"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -513,15 +484,16 @@ function handleSortItemChange(sortItem?: SortItem) {
|
||||||
<div class="w-72 p-4">
|
<div class="w-72 p-4">
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li
|
<li
|
||||||
v-for="(filterItem, index) in PublishPhaseItems"
|
v-for="(filterItem, index) in PublishStatusItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-close-popper
|
v-close-popper
|
||||||
:class="{
|
:class="{
|
||||||
'bg-gray-100':
|
'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"
|
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>
|
<span class="truncate">{{ filterItem.label }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -713,7 +685,11 @@ function handleSortItemChange(sortItem?: SortItem) {
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField :description="finalStatus(singlePage.page)" />
|
<VEntityField :description="getPublishStatus(singlePage.page)">
|
||||||
|
<template v-if="isPublishing(singlePage.page)" #description>
|
||||||
|
<VStatusDot text="发布中" animate />
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<IconEye
|
<IconEye
|
||||||
|
|
|
@ -1,71 +1,70 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
|
import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
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 cloneDeep from "lodash.clonedeep";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
||||||
|
import { singlePageLabels } from "@/constants/labels";
|
||||||
|
|
||||||
const initialFormState: SinglePageRequest = {
|
const initialFormState: SinglePage = {
|
||||||
page: {
|
spec: {
|
||||||
spec: {
|
title: "",
|
||||||
title: "",
|
slug: "",
|
||||||
slug: "",
|
template: "",
|
||||||
template: "",
|
cover: "",
|
||||||
cover: "",
|
deleted: false,
|
||||||
deleted: false,
|
publish: false,
|
||||||
published: false,
|
publishTime: "",
|
||||||
publishTime: "",
|
pinned: false,
|
||||||
pinned: false,
|
allowComment: true,
|
||||||
allowComment: true,
|
visible: "PUBLIC",
|
||||||
visible: "PUBLIC",
|
version: 1,
|
||||||
version: 1,
|
priority: 0,
|
||||||
priority: 0,
|
excerpt: {
|
||||||
excerpt: {
|
autoGenerate: true,
|
||||||
autoGenerate: true,
|
raw: "",
|
||||||
raw: "",
|
|
||||||
},
|
|
||||||
htmlMetas: [],
|
|
||||||
},
|
|
||||||
apiVersion: "content.halo.run/v1alpha1",
|
|
||||||
kind: "SinglePage",
|
|
||||||
metadata: {
|
|
||||||
name: uuid(),
|
|
||||||
},
|
},
|
||||||
|
htmlMetas: [],
|
||||||
},
|
},
|
||||||
content: {
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
raw: "",
|
kind: "SinglePage",
|
||||||
content: "",
|
metadata: {
|
||||||
rawType: "HTML",
|
name: uuid(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
singlePage?: SinglePageRequest;
|
singlePage?: SinglePage;
|
||||||
|
publishSupport?: boolean;
|
||||||
|
onlyEmit?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
singlePage: undefined,
|
singlePage: undefined,
|
||||||
|
publishSupport: true,
|
||||||
|
onlyEmit: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "update:visible", visible: boolean): void;
|
(event: "update:visible", visible: boolean): void;
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
(event: "saved", singlePage: SinglePageRequest): void;
|
(event: "saved", singlePage: SinglePage): void;
|
||||||
|
(event: "published", singlePage: SinglePage): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activeTab = ref("general");
|
const activeTab = ref("general");
|
||||||
const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
|
const formState = ref<SinglePage>(cloneDeep(initialFormState));
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
const publishCanceling = ref(false);
|
const publishCanceling = ref(false);
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.page.metadata.creationTimestamp;
|
return !!formState.value.metadata.creationTimestamp;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
const onVisibleChange = (visible: boolean) => {
|
||||||
|
@ -76,26 +75,33 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
if (props.onlyEmit) {
|
||||||
|
emit("saved", formState.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
|
||||||
// Set rendered content
|
saving.value = true;
|
||||||
formState.value.content.content = formState.value.content.raw;
|
|
||||||
|
|
||||||
if (isUpdateMode.value) {
|
const { data } = isUpdateMode.value
|
||||||
const { data } = await apiClient.singlePage.updateDraftSinglePage({
|
? await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
|
||||||
name: formState.value.page.metadata.name,
|
{
|
||||||
singlePageRequest: formState.value,
|
name: formState.value.metadata.name,
|
||||||
});
|
singlePage: formState.value,
|
||||||
formState.value.page = data;
|
}
|
||||||
emit("saved", formState.value);
|
)
|
||||||
} else {
|
: await apiClient.extension.singlePage.createcontentHaloRunV1alpha1SinglePage(
|
||||||
const { data } = await apiClient.singlePage.draftSinglePage({
|
{
|
||||||
singlePageRequest: formState.value,
|
singlePage: formState.value,
|
||||||
});
|
}
|
||||||
formState.value.page = data;
|
);
|
||||||
emit("saved", formState.value);
|
|
||||||
}
|
formState.value = data;
|
||||||
|
emit("saved", data);
|
||||||
|
|
||||||
|
onVisibleChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save single page", error);
|
console.error("Failed to save single page", error);
|
||||||
} finally {
|
} 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 {
|
try {
|
||||||
publishing.value = true;
|
if (publish) {
|
||||||
|
publishing.value = true;
|
||||||
|
} else {
|
||||||
|
publishCanceling.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Save single page
|
if (publish) {
|
||||||
await handleSave();
|
formState.value.spec.releaseSnapshot = formState.value.spec.headSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
// Get latest version single page
|
const { data } =
|
||||||
const { data: latestData } =
|
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
|
||||||
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
|
{
|
||||||
name: formState.value.page.metadata.name,
|
name: formState.value.metadata.name,
|
||||||
});
|
singlePage: {
|
||||||
formState.value.page = latestData;
|
...formState.value,
|
||||||
|
spec: {
|
||||||
|
...formState.value.spec,
|
||||||
|
publish: publish,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Publish single page
|
formState.value = data;
|
||||||
const { data } = await apiClient.singlePage.publishSinglePage({
|
|
||||||
name: formState.value.page.metadata.name,
|
if (publish) {
|
||||||
});
|
emit("published", data);
|
||||||
formState.value.page = data;
|
}
|
||||||
emit("saved", formState.value);
|
|
||||||
|
onVisibleChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to publish single page", error);
|
console.error("Failed to publish single page", error);
|
||||||
} finally {
|
} finally {
|
||||||
publishing.value = false;
|
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;
|
publishCanceling.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -188,21 +187,21 @@ const { templates } = useThemeCustomTemplates("page");
|
||||||
type="form"
|
type="form"
|
||||||
>
|
>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.title"
|
v-model="formState.spec.title"
|
||||||
label="标题"
|
label="标题"
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
validation="required"
|
validation="required"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.slug"
|
v-model="formState.spec.slug"
|
||||||
label="别名"
|
label="别名"
|
||||||
name="slug"
|
name="slug"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required"
|
validation="required"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.excerpt.autoGenerate"
|
v-model="formState.spec.excerpt.autoGenerate"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '是', value: true },
|
{ label: '是', value: true },
|
||||||
{ label: '否', value: false },
|
{ label: '否', value: false },
|
||||||
|
@ -213,8 +212,8 @@ const { templates } = useThemeCustomTemplates("page");
|
||||||
>
|
>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-if="!formState.page.spec.excerpt.autoGenerate"
|
v-if="!formState.spec.excerpt.autoGenerate"
|
||||||
v-model="formState.page.spec.excerpt.raw"
|
v-model="formState.spec.excerpt.raw"
|
||||||
name="raw"
|
name="raw"
|
||||||
label="自定义摘要"
|
label="自定义摘要"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
@ -231,7 +230,7 @@ const { templates } = useThemeCustomTemplates("page");
|
||||||
type="form"
|
type="form"
|
||||||
>
|
>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.allowComment"
|
v-model="formState.spec.allowComment"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '是', value: true },
|
{ label: '是', value: true },
|
||||||
{ label: '否', value: false },
|
{ label: '否', value: false },
|
||||||
|
@ -241,7 +240,7 @@ const { templates } = useThemeCustomTemplates("page");
|
||||||
type="radio"
|
type="radio"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.pinned"
|
v-model="formState.spec.pinned"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '是', value: true },
|
{ label: '是', value: true },
|
||||||
{ label: '否', value: false },
|
{ label: '否', value: false },
|
||||||
|
@ -251,7 +250,7 @@ const { templates } = useThemeCustomTemplates("page");
|
||||||
type="radio"
|
type="radio"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.visible"
|
v-model="formState.spec.visible"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '公开', value: 'PUBLIC' },
|
{ label: '公开', value: 'PUBLIC' },
|
||||||
{ label: '内部成员可访问', value: 'INTERNAL' },
|
{ label: '内部成员可访问', value: 'INTERNAL' },
|
||||||
|
@ -262,20 +261,20 @@ const { templates } = useThemeCustomTemplates("page");
|
||||||
type="select"
|
type="select"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.publishTime"
|
v-model="formState.spec.publishTime"
|
||||||
label="发表时间"
|
label="发表时间"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
name="publishTime"
|
name="publishTime"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.template"
|
v-model="formState.spec.template"
|
||||||
:options="templates"
|
:options="templates"
|
||||||
label="自定义模板"
|
label="自定义模板"
|
||||||
type="select"
|
type="select"
|
||||||
name="template"
|
name="template"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.page.spec.cover"
|
v-model="formState.spec.cover"
|
||||||
label="封面图"
|
label="封面图"
|
||||||
type="attachment"
|
type="attachment"
|
||||||
name="cover"
|
name="cover"
|
||||||
|
@ -287,27 +286,28 @@ const { templates } = useThemeCustomTemplates("page");
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton :loading="publishing" type="secondary" @click="handlePublish">
|
<template v-if="publishSupport">
|
||||||
{{
|
<VButton
|
||||||
formState.page.status?.phase === "PUBLISHED" ? "重新发布" : "发布"
|
v-if="
|
||||||
}}
|
formState.metadata.labels?.[singlePageLabels.PUBLISHED] !== 'true'
|
||||||
</VButton>
|
"
|
||||||
<VButton
|
:loading="publishing"
|
||||||
:loading="saving"
|
type="secondary"
|
||||||
size="sm"
|
@click="handleSwitchPublish(true)"
|
||||||
type="secondary"
|
>
|
||||||
@click="handleSave"
|
发布
|
||||||
>
|
</VButton>
|
||||||
仅保存
|
<VButton
|
||||||
</VButton>
|
v-else
|
||||||
<VButton
|
:loading="publishCanceling"
|
||||||
v-if="formState.page.status?.phase === 'PUBLISHED'"
|
type="danger"
|
||||||
:loading="publishCanceling"
|
@click="handleSwitchPublish(false)"
|
||||||
type="danger"
|
>
|
||||||
size="sm"
|
取消发布
|
||||||
@click="handleCancelPublish"
|
</VButton>
|
||||||
>
|
</template>
|
||||||
取消发布
|
<VButton :loading="saving" type="secondary" @click="handleSave">
|
||||||
|
保存
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton size="sm" type="default" @click="onVisibleChange(false)">
|
<VButton size="sm" type="default" @click="onVisibleChange(false)">
|
||||||
关闭
|
关闭
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import {
|
import {
|
||||||
IconBookRead,
|
IconBookRead,
|
||||||
IconSave,
|
IconSave,
|
||||||
|
IconSettings,
|
||||||
|
IconSendPlaneFill,
|
||||||
VButton,
|
VButton,
|
||||||
VPageHeader,
|
VPageHeader,
|
||||||
VSpace,
|
VSpace,
|
||||||
|
@ -9,12 +11,15 @@ import {
|
||||||
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||||
import PostPreviewModal from "./components/PostPreviewModal.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 { computed, onMounted, ref } from "vue";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const initialFormState: PostRequest = {
|
const initialFormState: PostRequest = {
|
||||||
post: {
|
post: {
|
||||||
|
@ -24,7 +29,7 @@ const initialFormState: PostRequest = {
|
||||||
template: "",
|
template: "",
|
||||||
cover: "",
|
cover: "",
|
||||||
deleted: false,
|
deleted: false,
|
||||||
published: false,
|
publish: false,
|
||||||
publishTime: "",
|
publishTime: "",
|
||||||
pinned: false,
|
pinned: false,
|
||||||
allowComment: true,
|
allowComment: true,
|
||||||
|
@ -56,6 +61,7 @@ const formState = ref<PostRequest>(cloneDeep(initialFormState));
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const previewModal = ref(false);
|
const previewModal = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
const publishing = ref(false);
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.post.metadata.creationTimestamp;
|
return !!formState.value.post.metadata.creationTimestamp;
|
||||||
|
@ -72,15 +78,25 @@ const handleSave = async () => {
|
||||||
if (!formState.value.post.spec.title) {
|
if (!formState.value.post.spec.title) {
|
||||||
formState.value.post.spec.title = "无标题文章";
|
formState.value.post.spec.title = "无标题文章";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formState.value.post.spec.slug) {
|
if (!formState.value.post.spec.slug) {
|
||||||
formState.value.post.spec.slug = uuid();
|
formState.value.post.spec.slug = uuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUpdateMode.value) {
|
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({
|
const { data } = await apiClient.post.updateDraftPost({
|
||||||
name: formState.value.post.metadata.name,
|
name: formState.value.post.metadata.name,
|
||||||
postRequest: formState.value,
|
postRequest: formState.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
formState.value.post = data;
|
formState.value.post = data;
|
||||||
} else {
|
} else {
|
||||||
const { data } = await apiClient.post.draftPost({
|
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 () => {
|
const handleFetchContent = async () => {
|
||||||
if (!formState.value.post.spec.headSnapshot) {
|
if (!formState.value.post.spec.headSnapshot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await apiClient.content.obtainSnapshotContent({
|
const { data } = await apiClient.content.obtainSnapshotContent({
|
||||||
snapshotName: formState.value.post.spec.headSnapshot,
|
snapshotName: formState.value.post.spec.headSnapshot,
|
||||||
});
|
});
|
||||||
|
@ -109,14 +192,33 @@ const handleFetchContent = async () => {
|
||||||
formState.value.content = data;
|
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
|
// Set route query parameter
|
||||||
if (!isUpdateMode.value) {
|
if (!isUpdateMode.value) {
|
||||||
name.value = post.post.metadata.name;
|
name.value = post.metadata.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
formState.value = post;
|
formState.value.post = post;
|
||||||
settingModal.value = false;
|
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
|
// Get post data when the route contains the name parameter
|
||||||
|
@ -139,8 +241,11 @@ onMounted(async () => {
|
||||||
<template>
|
<template>
|
||||||
<PostSettingModal
|
<PostSettingModal
|
||||||
v-model:visible="settingModal"
|
v-model:visible="settingModal"
|
||||||
:post="formState"
|
:post="formState.post"
|
||||||
|
:publish-support="!isUpdateMode"
|
||||||
|
:only-emit="!isUpdateMode"
|
||||||
@saved="onSettingSaved"
|
@saved="onSettingSaved"
|
||||||
|
@published="onSettingPublished"
|
||||||
/>
|
/>
|
||||||
<PostPreviewModal v-model:visible="previewModal" :post="formState.post" />
|
<PostPreviewModal v-model:visible="previewModal" :post="formState.post" />
|
||||||
<VPageHeader title="文章">
|
<VPageHeader title="文章">
|
||||||
|
@ -159,12 +264,30 @@ onMounted(async () => {
|
||||||
预览
|
预览
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
||||||
保存
|
|
||||||
</VButton>
|
|
||||||
<VButton type="secondary" @click="settingModal = true">
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconSave class="h-full w-full" />
|
<IconSave class="h-full w-full" />
|
||||||
</template>
|
</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>
|
</VButton>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
|
|
|
@ -25,13 +25,12 @@ import {
|
||||||
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
|
import UserDropdownSelector from "@/components/dropdown-selector/UserDropdownSelector.vue";
|
||||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||||
import PostTag from "../posts/tags/components/PostTag.vue";
|
import PostTag from "../posts/tags/components/PostTag.vue";
|
||||||
import { onMounted, ref, watch, watchEffect } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import type {
|
import type {
|
||||||
User,
|
User,
|
||||||
Category,
|
Category,
|
||||||
ListedPostList,
|
ListedPostList,
|
||||||
Post,
|
Post,
|
||||||
PostRequest,
|
|
||||||
Tag,
|
Tag,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/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 { usePermission } from "@/utils/permission";
|
||||||
import { onBeforeRouteLeave } from "vue-router";
|
import { onBeforeRouteLeave } from "vue-router";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import { postLabels } from "@/constants/labels";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
enum PostPhase {
|
|
||||||
DRAFT = "未发布",
|
|
||||||
PENDING_APPROVAL = "待审核",
|
|
||||||
PUBLISHED = "已发布",
|
|
||||||
}
|
|
||||||
|
|
||||||
const posts = ref<ListedPostList>({
|
const posts = ref<ListedPostList>({
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 20,
|
size: 20,
|
||||||
|
@ -62,8 +56,7 @@ const posts = ref<ListedPostList>({
|
||||||
});
|
});
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const selectedPost = ref<Post | null>(null);
|
const selectedPost = ref<Post>();
|
||||||
const selectedPostWithContent = ref<PostRequest | null>(null);
|
|
||||||
const checkedAll = ref(false);
|
const checkedAll = ref(false);
|
||||||
const selectedPostNames = ref<string[]>([]);
|
const selectedPostNames = ref<string[]>([]);
|
||||||
const refreshInterval = ref();
|
const refreshInterval = ref();
|
||||||
|
@ -77,6 +70,7 @@ const handleFetchPosts = async () => {
|
||||||
let categories: string[] | undefined;
|
let categories: string[] | undefined;
|
||||||
let tags: string[] | undefined;
|
let tags: string[] | undefined;
|
||||||
let contributors: string[] | undefined;
|
let contributors: string[] | undefined;
|
||||||
|
const labelSelector: string[] = ["content.halo.run/deleted=false"];
|
||||||
|
|
||||||
if (selectedCategory.value) {
|
if (selectedCategory.value) {
|
||||||
categories = [
|
categories = [
|
||||||
|
@ -93,12 +87,17 @@ const handleFetchPosts = async () => {
|
||||||
contributors = [selectedContributor.value.metadata.name];
|
contributors = [selectedContributor.value.metadata.name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedPublishStatusItem.value.value !== undefined) {
|
||||||
|
labelSelector.push(
|
||||||
|
`${postLabels.PUBLISHED}=${selectedPublishStatusItem.value.value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await apiClient.post.listPosts({
|
const { data } = await apiClient.post.listPosts({
|
||||||
labelSelector: [`content.halo.run/deleted=false`],
|
labelSelector,
|
||||||
page: posts.value.page,
|
page: posts.value.page,
|
||||||
size: posts.value.size,
|
size: posts.value.size,
|
||||||
visible: selectedVisibleItem.value?.value,
|
visible: selectedVisibleItem.value?.value,
|
||||||
publishPhase: selectedPublishPhaseItem.value?.value,
|
|
||||||
sort: selectedSortItem.value?.sort,
|
sort: selectedSortItem.value?.sort,
|
||||||
sortOrder: selectedSortItem.value?.sortOrder,
|
sortOrder: selectedSortItem.value?.sortOrder,
|
||||||
keyword: keyword.value,
|
keyword: keyword.value,
|
||||||
|
@ -108,14 +107,20 @@ const handleFetchPosts = async () => {
|
||||||
});
|
});
|
||||||
posts.value = data;
|
posts.value = data;
|
||||||
|
|
||||||
const deletedPosts = posts.value.items.filter(
|
// When an post is in the process of deleting or publishing, the list needs to be refreshed regularly
|
||||||
(post) => post.post.spec.deleted
|
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(() => {
|
refreshInterval.value = setInterval(() => {
|
||||||
handleFetchPosts();
|
handleFetchPosts();
|
||||||
}, 3000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to fetch posts", e);
|
console.error("Failed to fetch posts", e);
|
||||||
|
@ -151,8 +156,7 @@ const handleOpenSettingModal = async (post: Post) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSettingModalClose = () => {
|
const onSettingModalClose = () => {
|
||||||
selectedPost.value = null;
|
selectedPost.value = undefined;
|
||||||
selectedPostWithContent.value = null;
|
|
||||||
handleFetchPosts();
|
handleFetchPosts();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -203,11 +207,17 @@ const checkSelection = (post: Post) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalStatus = (post: Post) => {
|
const getPublishStatus = (post: Post) => {
|
||||||
if (post.status?.phase) {
|
const { labels } = post.metadata;
|
||||||
return PostPhase[post.status.phase];
|
return labels?.[postLabels.PUBLISHED] === "true" ? "已发布" : "未发布";
|
||||||
}
|
};
|
||||||
return "";
|
|
||||||
|
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) => {
|
const handleCheckAllChange = (e: Event) => {
|
||||||
|
@ -273,21 +283,6 @@ watch(selectedPostNames, (newValue) => {
|
||||||
checkedAll.value = newValue.length === posts.value.items?.length;
|
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(() => {
|
onMounted(() => {
|
||||||
handleFetchPosts();
|
handleFetchPosts();
|
||||||
});
|
});
|
||||||
|
@ -299,9 +294,9 @@ interface VisibleItem {
|
||||||
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
value?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublishPhaseItem {
|
interface PublishStatuItem {
|
||||||
label: string;
|
label: string;
|
||||||
value?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED";
|
value?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortItem {
|
interface SortItem {
|
||||||
|
@ -329,22 +324,18 @@ const VisibleItems: VisibleItem[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const PublishPhaseItems: PublishPhaseItem[] = [
|
const PublishStatuItems: PublishStatuItem[] = [
|
||||||
{
|
{
|
||||||
label: "全部",
|
label: "全部",
|
||||||
value: undefined,
|
value: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "已发布",
|
label: "已发布",
|
||||||
value: "PUBLISHED",
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "未发布",
|
label: "未发布",
|
||||||
value: "DRAFT",
|
value: false,
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "待审核",
|
|
||||||
value: "PENDING_APPROVAL",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -375,7 +366,7 @@ const { categories } = usePostCategory({ fetchOnMounted: true });
|
||||||
const { tags } = usePostTag({ fetchOnMounted: true });
|
const { tags } = usePostTag({ fetchOnMounted: true });
|
||||||
|
|
||||||
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
|
const selectedVisibleItem = ref<VisibleItem>(VisibleItems[0]);
|
||||||
const selectedPublishPhaseItem = ref<PublishPhaseItem>(PublishPhaseItems[0]);
|
const selectedPublishStatusItem = ref<PublishStatuItem>(PublishStatuItems[0]);
|
||||||
const selectedSortItem = ref<SortItem>();
|
const selectedSortItem = ref<SortItem>();
|
||||||
const selectedCategory = ref<Category>();
|
const selectedCategory = ref<Category>();
|
||||||
const selectedTag = ref<Tag>();
|
const selectedTag = ref<Tag>();
|
||||||
|
@ -387,8 +378,8 @@ function handleVisibleItemChange(visibleItem: VisibleItem) {
|
||||||
handleFetchPosts();
|
handleFetchPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePublishPhaseItemChange(publishPhaseItem: PublishPhaseItem) {
|
function handlePublishStatusItemChange(publishStatusItem: PublishStatuItem) {
|
||||||
selectedPublishPhaseItem.value = publishPhaseItem;
|
selectedPublishStatusItem.value = publishStatusItem;
|
||||||
handleFetchPosts();
|
handleFetchPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -415,7 +406,7 @@ function handleContributorChange(user?: User) {
|
||||||
<template>
|
<template>
|
||||||
<PostSettingModal
|
<PostSettingModal
|
||||||
v-model:visible="settingModal"
|
v-model:visible="settingModal"
|
||||||
:post="selectedPostWithContent"
|
:post="selectedPost"
|
||||||
@close="onSettingModalClose"
|
@close="onSettingModalClose"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
@ -482,15 +473,15 @@ function handleContributorChange(user?: User) {
|
||||||
></FormKit>
|
></FormKit>
|
||||||
|
|
||||||
<div
|
<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"
|
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">
|
<span class="text-xs text-gray-600 group-hover:text-gray-900">
|
||||||
状态:{{ selectedPublishPhaseItem.label }}
|
状态:{{ selectedPublishStatusItem.label }}
|
||||||
</span>
|
</span>
|
||||||
<IconCloseCircle
|
<IconCloseCircle
|
||||||
class="h-4 w-4 text-gray-600"
|
class="h-4 w-4 text-gray-600"
|
||||||
@click="handlePublishPhaseItemChange(PublishPhaseItems[0])"
|
@click="handlePublishStatusItemChange(PublishStatuItems[0])"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -576,16 +567,16 @@ function handleContributorChange(user?: User) {
|
||||||
<div class="w-72 p-4">
|
<div class="w-72 p-4">
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li
|
<li
|
||||||
v-for="(filterItem, index) in PublishPhaseItems"
|
v-for="(filterItem, index) in PublishStatuItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-close-popper
|
v-close-popper
|
||||||
:class="{
|
:class="{
|
||||||
'bg-gray-100':
|
'bg-gray-100':
|
||||||
selectedPublishPhaseItem.value ===
|
selectedPublishStatusItem.value ===
|
||||||
filterItem.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"
|
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>
|
<span class="truncate">{{ filterItem.label }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -933,9 +924,11 @@ function handleContributorChange(user?: User) {
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
<VEntityField
|
<VEntityField :description="getPublishStatus(post.post)">
|
||||||
:description="finalStatus(post.post)"
|
<template v-if="isPublishing(post.post)" #description>
|
||||||
></VEntityField>
|
<VStatusDot text="发布中" animate />
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
<VEntityField>
|
<VEntityField>
|
||||||
<template #description>
|
<template #description>
|
||||||
<IconEye
|
<IconEye
|
||||||
|
|
|
@ -1,73 +1,72 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
|
import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
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 cloneDeep from "lodash.clonedeep";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
|
||||||
|
import { postLabels } from "@/constants/labels";
|
||||||
|
|
||||||
const initialFormState: PostRequest = {
|
const initialFormState: Post = {
|
||||||
post: {
|
spec: {
|
||||||
spec: {
|
title: "",
|
||||||
title: "",
|
slug: "",
|
||||||
slug: "",
|
template: "",
|
||||||
template: "",
|
cover: "",
|
||||||
cover: "",
|
deleted: false,
|
||||||
deleted: false,
|
publish: false,
|
||||||
published: false,
|
publishTime: "",
|
||||||
publishTime: "",
|
pinned: false,
|
||||||
pinned: false,
|
allowComment: true,
|
||||||
allowComment: true,
|
visible: "PUBLIC",
|
||||||
visible: "PUBLIC",
|
version: 1,
|
||||||
version: 1,
|
priority: 0,
|
||||||
priority: 0,
|
excerpt: {
|
||||||
excerpt: {
|
autoGenerate: true,
|
||||||
autoGenerate: true,
|
raw: "",
|
||||||
raw: "",
|
|
||||||
},
|
|
||||||
categories: [],
|
|
||||||
tags: [],
|
|
||||||
htmlMetas: [],
|
|
||||||
},
|
|
||||||
apiVersion: "content.halo.run/v1alpha1",
|
|
||||||
kind: "Post",
|
|
||||||
metadata: {
|
|
||||||
name: uuid(),
|
|
||||||
},
|
},
|
||||||
|
categories: [],
|
||||||
|
tags: [],
|
||||||
|
htmlMetas: [],
|
||||||
},
|
},
|
||||||
content: {
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
raw: "",
|
kind: "Post",
|
||||||
content: "",
|
metadata: {
|
||||||
rawType: "HTML",
|
name: uuid(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
post?: PostRequest | null;
|
post?: Post;
|
||||||
|
publishSupport?: boolean;
|
||||||
|
onlyEmit?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
post: null,
|
post: undefined,
|
||||||
|
publishSupport: true,
|
||||||
|
onlyEmit: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "update:visible", visible: boolean): void;
|
(event: "update:visible", visible: boolean): void;
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
(event: "saved", post: PostRequest): void;
|
(event: "saved", post: Post): void;
|
||||||
|
(event: "published", post: Post): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activeTab = ref("general");
|
const activeTab = ref("general");
|
||||||
const formState = ref<PostRequest>(cloneDeep(initialFormState));
|
const formState = ref<Post>(cloneDeep(initialFormState));
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
const publishCanceling = ref(false);
|
const publishCanceling = ref(false);
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.post.metadata.creationTimestamp;
|
return !!formState.value.metadata.creationTimestamp;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVisibleChange = (visible: boolean) => {
|
const handleVisibleChange = (visible: boolean) => {
|
||||||
|
@ -78,26 +77,27 @@ const handleVisibleChange = (visible: boolean) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
if (props.onlyEmit) {
|
||||||
|
emit("saved", formState.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
|
||||||
// Set rendered content
|
const { data } = isUpdateMode.value
|
||||||
formState.value.content.content = formState.value.content.raw;
|
? await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
|
||||||
|
name: formState.value.metadata.name,
|
||||||
|
post: formState.value,
|
||||||
|
})
|
||||||
|
: await apiClient.extension.post.createcontentHaloRunV1alpha1Post({
|
||||||
|
post: formState.value,
|
||||||
|
});
|
||||||
|
|
||||||
if (isUpdateMode.value) {
|
formState.value = data;
|
||||||
const { data } = await apiClient.post.updateDraftPost({
|
emit("saved", data);
|
||||||
name: formState.value.post.metadata.name,
|
|
||||||
postRequest: formState.value,
|
handleVisibleChange(false);
|
||||||
});
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to save post", e);
|
console.error("Failed to save post", e);
|
||||||
} finally {
|
} 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 {
|
try {
|
||||||
publishing.value = true;
|
if (publish) {
|
||||||
|
publishing.value = true;
|
||||||
|
} else {
|
||||||
|
publishCanceling.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Save post
|
if (publish) {
|
||||||
await handleSave();
|
formState.value.spec.releaseSnapshot = formState.value.spec.headSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
// Get latest version post
|
const { data } =
|
||||||
const { data: latestData } =
|
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
|
||||||
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
name: formState.value.metadata.name,
|
||||||
name: formState.value.post.metadata.name,
|
post: {
|
||||||
|
...formState.value,
|
||||||
|
spec: {
|
||||||
|
...formState.value.spec,
|
||||||
|
publish: publish,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
formState.value.post = latestData;
|
|
||||||
|
|
||||||
// Publish post
|
formState.value = data;
|
||||||
const { data } = await apiClient.post.publishPost({
|
|
||||||
name: formState.value.post.metadata.name,
|
if (publish) {
|
||||||
});
|
emit("published", data);
|
||||||
formState.value.post = data;
|
}
|
||||||
emit("saved", formState.value);
|
|
||||||
|
handleVisibleChange(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to publish post", e);
|
console.error("Failed to publish post", e);
|
||||||
} finally {
|
} finally {
|
||||||
publishing.value = false;
|
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;
|
publishCanceling.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -190,33 +180,33 @@ const { templates } = useThemeCustomTemplates("post");
|
||||||
type="form"
|
type="form"
|
||||||
>
|
>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.title"
|
v-model="formState.spec.title"
|
||||||
label="标题"
|
label="标题"
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
validation="required"
|
validation="required"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.slug"
|
v-model="formState.spec.slug"
|
||||||
label="别名"
|
label="别名"
|
||||||
name="slug"
|
name="slug"
|
||||||
type="text"
|
type="text"
|
||||||
validation="required"
|
validation="required"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.categories"
|
v-model="formState.spec.categories"
|
||||||
label="分类目录"
|
label="分类目录"
|
||||||
name="categories"
|
name="categories"
|
||||||
type="categoryCheckbox"
|
type="categoryCheckbox"
|
||||||
/>
|
/>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.tags"
|
v-model="formState.spec.tags"
|
||||||
label="标签"
|
label="标签"
|
||||||
name="tags"
|
name="tags"
|
||||||
type="tagCheckbox"
|
type="tagCheckbox"
|
||||||
/>
|
/>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.excerpt.autoGenerate"
|
v-model="formState.spec.excerpt.autoGenerate"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '是', value: true },
|
{ label: '是', value: true },
|
||||||
{ label: '否', value: false },
|
{ label: '否', value: false },
|
||||||
|
@ -227,8 +217,8 @@ const { templates } = useThemeCustomTemplates("post");
|
||||||
>
|
>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-if="!formState.post.spec.excerpt.autoGenerate"
|
v-if="!formState.spec.excerpt.autoGenerate"
|
||||||
v-model="formState.post.spec.excerpt.raw"
|
v-model="formState.spec.excerpt.raw"
|
||||||
label="自定义摘要"
|
label="自定义摘要"
|
||||||
name="raw"
|
name="raw"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
@ -245,7 +235,7 @@ const { templates } = useThemeCustomTemplates("post");
|
||||||
type="form"
|
type="form"
|
||||||
>
|
>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.allowComment"
|
v-model="formState.spec.allowComment"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '是', value: true },
|
{ label: '是', value: true },
|
||||||
{ label: '否', value: false },
|
{ label: '否', value: false },
|
||||||
|
@ -254,7 +244,7 @@ const { templates } = useThemeCustomTemplates("post");
|
||||||
type="radio"
|
type="radio"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.pinned"
|
v-model="formState.spec.pinned"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '是', value: true },
|
{ label: '是', value: true },
|
||||||
{ label: '否', value: false },
|
{ label: '否', value: false },
|
||||||
|
@ -264,7 +254,7 @@ const { templates } = useThemeCustomTemplates("post");
|
||||||
type="radio"
|
type="radio"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.visible"
|
v-model="formState.spec.visible"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '公开', value: 'PUBLIC' },
|
{ label: '公开', value: 'PUBLIC' },
|
||||||
{ label: '内部成员可访问', value: 'INTERNAL' },
|
{ label: '内部成员可访问', value: 'INTERNAL' },
|
||||||
|
@ -275,19 +265,19 @@ const { templates } = useThemeCustomTemplates("post");
|
||||||
type="select"
|
type="select"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.publishTime"
|
v-model="formState.spec.publishTime"
|
||||||
label="发表时间"
|
label="发表时间"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.template"
|
v-model="formState.spec.template"
|
||||||
:options="templates"
|
:options="templates"
|
||||||
label="自定义模板"
|
label="自定义模板"
|
||||||
name="template"
|
name="template"
|
||||||
type="select"
|
type="select"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.post.spec.cover"
|
v-model="formState.spec.cover"
|
||||||
name="cover"
|
name="cover"
|
||||||
label="封面图"
|
label="封面图"
|
||||||
type="attachment"
|
type="attachment"
|
||||||
|
@ -299,29 +289,28 @@ const { templates } = useThemeCustomTemplates("post");
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<VButton :loading="publishing" type="secondary" @click="handlePublish">
|
<template v-if="publishSupport">
|
||||||
{{
|
<VButton
|
||||||
formState.post.status?.phase === "PUBLISHED" ? "重新发布" : "发布"
|
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>
|
||||||
<VButton
|
<VButton type="default" @click="handleVisibleChange(false)">
|
||||||
: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>
|
</VButton>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
|
|
|
@ -4,14 +4,16 @@ import { onMounted, ref } from "vue";
|
||||||
import type { ListedPost } from "@halo-dev/api-client";
|
import type { ListedPost } from "@halo-dev/api-client";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import { postLabels } from "@/constants/labels";
|
||||||
|
|
||||||
const posts = ref<ListedPost[]>([] as ListedPost[]);
|
const posts = ref<ListedPost[]>([] as ListedPost[]);
|
||||||
|
|
||||||
const handleFetchPosts = async () => {
|
const handleFetchPosts = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await apiClient.post.listPosts({
|
const { data } = await apiClient.post.listPosts({
|
||||||
|
labelSelector: [`${postLabels.PUBLISHED}=true`],
|
||||||
sort: "PUBLISH_TIME",
|
sort: "PUBLISH_TIME",
|
||||||
publishPhase: "PUBLISHED",
|
sortOrder: false,
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 10,
|
size: 10,
|
||||||
});
|
});
|
||||||
|
|
|
@ -50,25 +50,11 @@ const handleSubmit = async () => {
|
||||||
});
|
});
|
||||||
await apiClient.post.draftPost({ postRequest: post as PostRequest });
|
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
|
// Create singlePage
|
||||||
await apiClient.singlePage.draftSinglePage({
|
await apiClient.singlePage.draftSinglePage({
|
||||||
singlePageRequest: singlePage as SinglePageRequest,
|
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
|
// Create menu and menu items
|
||||||
const menuItemPromises = menuItems.map((item) => {
|
const menuItemPromises = menuItems.map((item) => {
|
||||||
return apiClient.extension.menuItem.createv1alpha1MenuItem({
|
return apiClient.extension.menuItem.createv1alpha1MenuItem({
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"template": "",
|
"template": "",
|
||||||
"cover": "",
|
"cover": "",
|
||||||
"deleted": false,
|
"deleted": false,
|
||||||
"published": false,
|
"publish": true,
|
||||||
"publishTime": "",
|
"publishTime": "",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"allowComment": true,
|
"allowComment": true,
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"template": "",
|
"template": "",
|
||||||
"cover": "",
|
"cover": "",
|
||||||
"deleted": false,
|
"deleted": false,
|
||||||
"published": false,
|
"publish": true,
|
||||||
"publishTime": "",
|
"publishTime": "",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"allowComment": true,
|
"allowComment": true,
|
||||||
|
|
Loading…
Reference in New Issue