mirror of https://github.com/halo-dev/halo-admin
refactor: add validation for post and singlePage settings form (#791)
#### What type of PR is this? /kind improvement #### What this PR does / why we need it: 重构文章和自定义页面的设置表单,支持提交时验证表单。 > 因为之前的多选项卡设计导致无法同时验证所有表单,所以这个 PR 重构了表单的布局。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2859 #### Screenshots:  #### Special notes for your reviewer: 测试方式: 1. 测试在内容编辑页面和列表打开文章和自定义页面的设置表单。 2. 检查表单验证是否有效。 #### Does this PR introduce a user-facing change? ```release-note 重构 Console 端文章和自定义页面的设置表单布局,支持提交时验证表单。 ```pull/797/head^2
parent
9c29538c7a
commit
f7ece9135a
|
@ -1,13 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||||
Toast,
|
import { computed, nextTick, ref, watchEffect } from "vue";
|
||||||
VButton,
|
|
||||||
VModal,
|
|
||||||
VSpace,
|
|
||||||
VTabItem,
|
|
||||||
VTabs,
|
|
||||||
} from "@halo-dev/components";
|
|
||||||
import { computed, ref, watchEffect } from "vue";
|
|
||||||
import type { SinglePage } 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";
|
||||||
|
@ -15,6 +8,7 @@ import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/
|
||||||
import { singlePageLabels } from "@/constants/labels";
|
import { singlePageLabels } from "@/constants/labels";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
||||||
|
import { submitForm } from "@formkit/core";
|
||||||
|
|
||||||
const initialFormState: SinglePage = {
|
const initialFormState: SinglePage = {
|
||||||
spec: {
|
spec: {
|
||||||
|
@ -64,11 +58,11 @@ const emit = defineEmits<{
|
||||||
(event: "published", singlePage: SinglePage): void;
|
(event: "published", singlePage: SinglePage): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activeTab = ref("general");
|
|
||||||
const formState = ref<SinglePage>(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 submitType = ref<"publish" | "save">();
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.metadata.creationTimestamp;
|
return !!formState.value.metadata.creationTimestamp;
|
||||||
|
@ -77,13 +71,35 @@ const isUpdateMode = computed(() => {
|
||||||
const onVisibleChange = (visible: boolean) => {
|
const onVisibleChange = (visible: boolean) => {
|
||||||
emit("update:visible", visible);
|
emit("update:visible", visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
setTimeout(() => {
|
|
||||||
activeTab.value = "general";
|
|
||||||
}, 200);
|
|
||||||
emit("close");
|
emit("close");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (submitType.value === "publish") {
|
||||||
|
handlePublish();
|
||||||
|
}
|
||||||
|
if (submitType.value === "save") {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
submitType.value = "save";
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
submitForm("singlePage-setting-form");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublishClick = () => {
|
||||||
|
submitType.value = "publish";
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
submitForm("singlePage-setting-form");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (props.onlyEmit) {
|
if (props.onlyEmit) {
|
||||||
emit("saved", formState.value);
|
emit("saved", formState.value);
|
||||||
|
@ -121,50 +137,71 @@ const handleSave = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitchPublish = async (publish: boolean) => {
|
const handlePublish = async () => {
|
||||||
if (props.onlyEmit) {
|
if (props.onlyEmit) {
|
||||||
emit("published", formState.value);
|
emit("published", formState.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (publish) {
|
|
||||||
publishing.value = true;
|
publishing.value = true;
|
||||||
} else {
|
|
||||||
publishCanceling.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publish) {
|
const singlePageToUpdate = cloneDeep(formState.value);
|
||||||
formState.value.spec.releaseSnapshot = formState.value.spec.headSnapshot;
|
|
||||||
}
|
singlePageToUpdate.spec.releaseSnapshot =
|
||||||
|
singlePageToUpdate.spec.headSnapshot;
|
||||||
|
singlePageToUpdate.spec.publish = true;
|
||||||
|
|
||||||
const { data } =
|
const { data } =
|
||||||
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
|
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
|
||||||
{
|
{
|
||||||
name: formState.value.metadata.name,
|
name: formState.value.metadata.name,
|
||||||
singlePage: {
|
singlePage: singlePageToUpdate,
|
||||||
...formState.value,
|
|
||||||
spec: {
|
|
||||||
...formState.value.spec,
|
|
||||||
publish: publish,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
formState.value = data;
|
formState.value = data;
|
||||||
|
|
||||||
if (publish) {
|
|
||||||
emit("published", data);
|
emit("published", data);
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChange(false);
|
onVisibleChange(false);
|
||||||
|
|
||||||
Toast.success(`${publish ? "发布" : "取消发布"}成功`);
|
Toast.success("发布成功");
|
||||||
} 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 handleUnpublish = async () => {
|
||||||
|
try {
|
||||||
|
publishCanceling.value = true;
|
||||||
|
|
||||||
|
const { data: singlePage } =
|
||||||
|
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
|
||||||
|
name: formState.value.metadata.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const singlePageToUpdate = cloneDeep(singlePage);
|
||||||
|
singlePageToUpdate.spec.publish = false;
|
||||||
|
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
|
||||||
|
{
|
||||||
|
name: formState.value.metadata.name,
|
||||||
|
singlePage: singlePageToUpdate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
formState.value = data;
|
||||||
|
|
||||||
|
onVisibleChange(false);
|
||||||
|
|
||||||
|
Toast.success("取消发布成功");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to unpublish single page", error);
|
||||||
|
} finally {
|
||||||
publishCanceling.value = false;
|
publishCanceling.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -204,15 +241,23 @@ const onPublishTimeChange = (value: string) => {
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<VTabs v-model:active-id="activeTab" type="outline">
|
|
||||||
<VTabItem id="general" label="常规">
|
|
||||||
<FormKit
|
<FormKit
|
||||||
id="basic"
|
id="singlePage-setting-form"
|
||||||
name="basic"
|
|
||||||
:actions="false"
|
|
||||||
:preserve="true"
|
|
||||||
type="form"
|
type="form"
|
||||||
|
name="singlePage-setting-form"
|
||||||
|
:config="{ validationVisibility: 'submit' }"
|
||||||
|
@submit="handleSubmit"
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||||
|
<div class="md:col-span-1">
|
||||||
|
<div class="sticky top-0">
|
||||||
|
<span class="text-base font-medium text-gray-900">
|
||||||
|
常规设置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.title"
|
v-model="formState.spec.title"
|
||||||
label="标题"
|
label="标题"
|
||||||
|
@ -247,16 +292,22 @@ const onPublishTimeChange = (value: string) => {
|
||||||
validation="length:0,1024"
|
validation="length:0,1024"
|
||||||
:rows="5"
|
:rows="5"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
</FormKit>
|
</div>
|
||||||
</VTabItem>
|
</div>
|
||||||
<VTabItem id="advanced" label="高级">
|
|
||||||
<FormKit
|
<div class="py-5">
|
||||||
id="advanced"
|
<div class="border-t border-gray-200"></div>
|
||||||
name="advanced"
|
</div>
|
||||||
:actions="false"
|
|
||||||
:preserve="true"
|
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||||
type="form"
|
<div class="md:col-span-1">
|
||||||
>
|
<div class="sticky top-0">
|
||||||
|
<span class="text-base font-medium text-gray-900">
|
||||||
|
高级设置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.allowComment"
|
v-model="formState.spec.allowComment"
|
||||||
:options="[
|
:options="[
|
||||||
|
@ -308,10 +359,10 @@ const onPublishTimeChange = (value: string) => {
|
||||||
name="cover"
|
name="cover"
|
||||||
validation="length:0,1024"
|
validation="length:0,1024"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<!--TODO: add SEO/Metas/Inject Code form-->
|
|
||||||
</VTabItem>
|
|
||||||
</VTabs>
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
@ -322,7 +373,7 @@ const onPublishTimeChange = (value: string) => {
|
||||||
"
|
"
|
||||||
:loading="publishing"
|
:loading="publishing"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@click="handleSwitchPublish(true)"
|
@click="handlePublishClick()"
|
||||||
>
|
>
|
||||||
发布
|
发布
|
||||||
</VButton>
|
</VButton>
|
||||||
|
@ -330,17 +381,15 @@ const onPublishTimeChange = (value: string) => {
|
||||||
v-else
|
v-else
|
||||||
:loading="publishCanceling"
|
:loading="publishCanceling"
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleSwitchPublish(false)"
|
@click="handleUnpublish()"
|
||||||
>
|
>
|
||||||
取消发布
|
取消发布
|
||||||
</VButton>
|
</VButton>
|
||||||
</template>
|
</template>
|
||||||
<VButton :loading="saving" type="secondary" @click="handleSave">
|
<VButton :loading="saving" type="secondary" @click="handleSaveClick">
|
||||||
保存
|
保存
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton size="sm" type="default" @click="onVisibleChange(false)">
|
<VButton type="default" @click="onVisibleChange(false)"> 关闭 </VButton>
|
||||||
关闭
|
|
||||||
</VButton>
|
|
||||||
</VSpace>
|
</VSpace>
|
||||||
</template>
|
</template>
|
||||||
</VModal>
|
</VModal>
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||||
Toast,
|
import { computed, nextTick, ref, watchEffect } from "vue";
|
||||||
VButton,
|
|
||||||
VModal,
|
|
||||||
VSpace,
|
|
||||||
VTabItem,
|
|
||||||
VTabs,
|
|
||||||
} from "@halo-dev/components";
|
|
||||||
import { computed, ref, watchEffect } from "vue";
|
|
||||||
import type { Post } 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";
|
||||||
|
@ -15,6 +8,7 @@ import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
||||||
|
import { submitForm } from "@formkit/core";
|
||||||
|
|
||||||
const initialFormState: Post = {
|
const initialFormState: Post = {
|
||||||
spec: {
|
spec: {
|
||||||
|
@ -66,11 +60,11 @@ const emit = defineEmits<{
|
||||||
(event: "published", post: Post): void;
|
(event: "published", post: Post): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activeTab = ref("general");
|
|
||||||
const formState = ref<Post>(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 submitType = ref<"publish" | "save">();
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.metadata.creationTimestamp;
|
return !!formState.value.metadata.creationTimestamp;
|
||||||
|
@ -79,13 +73,39 @@ const isUpdateMode = computed(() => {
|
||||||
const handleVisibleChange = (visible: boolean) => {
|
const handleVisibleChange = (visible: boolean) => {
|
||||||
emit("update:visible", visible);
|
emit("update:visible", visible);
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
setTimeout(() => {
|
|
||||||
activeTab.value = "general";
|
|
||||||
}, 200);
|
|
||||||
emit("close");
|
emit("close");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (submitType.value === "publish") {
|
||||||
|
handlePublish();
|
||||||
|
}
|
||||||
|
if (submitType.value === "save") {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
submitType.value = "save";
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
submitForm("post-setting-form");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublishClick = () => {
|
||||||
|
if (!props.onlyEmit) {
|
||||||
|
handlePublish();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitType.value = "publish";
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
submitForm("post-setting-form");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (props.onlyEmit) {
|
if (props.onlyEmit) {
|
||||||
emit("saved", formState.value);
|
emit("saved", formState.value);
|
||||||
|
@ -197,15 +217,23 @@ const onPublishTimeChange = (value: string) => {
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<VTabs v-model:active-id="activeTab" type="outline">
|
|
||||||
<VTabItem id="general" label="常规">
|
|
||||||
<FormKit
|
<FormKit
|
||||||
id="basic"
|
id="post-setting-form"
|
||||||
name="basic"
|
|
||||||
:actions="false"
|
|
||||||
:preserve="true"
|
|
||||||
type="form"
|
type="form"
|
||||||
|
name="post-setting-form"
|
||||||
|
:config="{ validationVisibility: 'submit' }"
|
||||||
|
@submit="handleSubmit"
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||||
|
<div class="md:col-span-1">
|
||||||
|
<div class="sticky top-0">
|
||||||
|
<span class="text-base font-medium text-gray-900">
|
||||||
|
常规设置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.title"
|
v-model="formState.spec.title"
|
||||||
label="标题"
|
label="标题"
|
||||||
|
@ -252,16 +280,22 @@ const onPublishTimeChange = (value: string) => {
|
||||||
:rows="5"
|
:rows="5"
|
||||||
validation="length:0,1024"
|
validation="length:0,1024"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
</FormKit>
|
</div>
|
||||||
</VTabItem>
|
</div>
|
||||||
<VTabItem id="advanced" label="高级">
|
|
||||||
<FormKit
|
<div class="py-5">
|
||||||
id="advanced"
|
<div class="border-t border-gray-200"></div>
|
||||||
name="advanced"
|
</div>
|
||||||
:actions="false"
|
|
||||||
:preserve="true"
|
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||||
type="form"
|
<div class="md:col-span-1">
|
||||||
>
|
<div class="sticky top-0">
|
||||||
|
<span class="text-base font-medium text-gray-900">
|
||||||
|
高级设置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.allowComment"
|
v-model="formState.spec.allowComment"
|
||||||
:options="[
|
:options="[
|
||||||
|
@ -311,10 +345,10 @@ const onPublishTimeChange = (value: string) => {
|
||||||
type="attachment"
|
type="attachment"
|
||||||
validation="length:0,1024"
|
validation="length:0,1024"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</FormKit>
|
</FormKit>
|
||||||
<!--TODO: add SEO/Metas/Inject Code form-->
|
|
||||||
</VTabItem>
|
|
||||||
</VTabs>
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
|
@ -323,7 +357,7 @@ const onPublishTimeChange = (value: string) => {
|
||||||
v-if="formState.metadata.labels?.[postLabels.PUBLISHED] !== 'true'"
|
v-if="formState.metadata.labels?.[postLabels.PUBLISHED] !== 'true'"
|
||||||
:loading="publishing"
|
:loading="publishing"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@click="handlePublish()"
|
@click="handlePublishClick()"
|
||||||
>
|
>
|
||||||
发布
|
发布
|
||||||
</VButton>
|
</VButton>
|
||||||
|
@ -336,7 +370,7 @@ const onPublishTimeChange = (value: string) => {
|
||||||
取消发布
|
取消发布
|
||||||
</VButton>
|
</VButton>
|
||||||
</template>
|
</template>
|
||||||
<VButton :loading="saving" type="secondary" @click="handleSave">
|
<VButton :loading="saving" type="secondary" @click="handleSaveClick()">
|
||||||
保存
|
保存
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton type="default" @click="handleVisibleChange(false)">
|
<VButton type="default" @click="handleVisibleChange(false)">
|
||||||
|
|
Loading…
Reference in New Issue