mirror of https://github.com/halo-dev/halo
feat: add support for selecting the parent category when creating a new category (#4056)
#### What type of PR is this? /area console /kind feature /milestone 2.7.x #### What this PR does / why we need it: 支持在创建分类的时候选择上级分类。 <img width="805" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/02912f70-4de4-4b8e-bbbe-f973fbd3e684"> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/4040 #### Special notes for your reviewer: 测试方式: 1. 测试新建的时候指定上级分类的功能是否正常。 2. 点击某个分类的操作按钮,选择添加子分类的菜单项,检查在新建分类的表单中上级分类是否选中了此分类。 #### Does this PR introduce a user-facing change? ```release-note Console 端的文章分类支持在新建时指定上级分类。 ```pull/4092/head^2
parent
4a1fe8dd1e
commit
2fd9cbde33
|
@ -276,6 +276,8 @@ core:
|
||||||
delete:
|
delete:
|
||||||
title: Are you sure you want to delete this category?
|
title: Are you sure you want to delete this category?
|
||||||
description: After deleting this category, the association with corresponding articles will be removed. This operation cannot be undone.
|
description: After deleting this category, the association with corresponding articles will be removed. This operation cannot be undone.
|
||||||
|
add_sub_category:
|
||||||
|
button: Add sub category
|
||||||
editing_modal:
|
editing_modal:
|
||||||
titles:
|
titles:
|
||||||
update: Update post category
|
update: Update post category
|
||||||
|
@ -284,6 +286,8 @@ core:
|
||||||
general: General
|
general: General
|
||||||
annotations: Annotations
|
annotations: Annotations
|
||||||
fields:
|
fields:
|
||||||
|
parent:
|
||||||
|
label: Parent
|
||||||
display_name:
|
display_name:
|
||||||
label: Display name
|
label: Display name
|
||||||
slug:
|
slug:
|
||||||
|
|
|
@ -276,6 +276,8 @@ core:
|
||||||
delete:
|
delete:
|
||||||
title: 确定要删除该分类吗?
|
title: 确定要删除该分类吗?
|
||||||
description: 删除此分类之后,对应文章的关联将被解除。该操作不可恢复。
|
description: 删除此分类之后,对应文章的关联将被解除。该操作不可恢复。
|
||||||
|
add_sub_category:
|
||||||
|
button: 新增子分类
|
||||||
editing_modal:
|
editing_modal:
|
||||||
titles:
|
titles:
|
||||||
update: 编辑文章分类
|
update: 编辑文章分类
|
||||||
|
@ -284,6 +286,8 @@ core:
|
||||||
general: 常规
|
general: 常规
|
||||||
annotations: 元数据
|
annotations: 元数据
|
||||||
fields:
|
fields:
|
||||||
|
parent:
|
||||||
|
label: 上级分类
|
||||||
display_name:
|
display_name:
|
||||||
label: 名称
|
label: 名称
|
||||||
slug:
|
slug:
|
||||||
|
|
|
@ -276,6 +276,8 @@ core:
|
||||||
delete:
|
delete:
|
||||||
title: 確定要刪除該分類嗎?
|
title: 確定要刪除該分類嗎?
|
||||||
description: 刪除此分類之後,對應文章的關聯將被解除。該操作不可恢復。
|
description: 刪除此分類之後,對應文章的關聯將被解除。該操作不可恢復。
|
||||||
|
add_sub_category:
|
||||||
|
button: 新增子分類
|
||||||
editing_modal:
|
editing_modal:
|
||||||
titles:
|
titles:
|
||||||
update: 編輯文章分類
|
update: 編輯文章分類
|
||||||
|
@ -284,6 +286,8 @@ core:
|
||||||
general: 常規
|
general: 常規
|
||||||
annotations: 元數據
|
annotations: 元數據
|
||||||
fields:
|
fields:
|
||||||
|
parent:
|
||||||
|
label: 上級分類
|
||||||
display_name:
|
display_name:
|
||||||
label: 名稱
|
label: 名稱
|
||||||
slug:
|
slug:
|
||||||
|
|
|
@ -33,7 +33,8 @@ import { useDebounceFn } from "@vueuse/core";
|
||||||
import { usePostCategory } from "./composables/use-post-category";
|
import { usePostCategory } from "./composables/use-post-category";
|
||||||
|
|
||||||
const editingModal = ref(false);
|
const editingModal = ref(false);
|
||||||
const selectedCategory = ref<Category | null>(null);
|
const selectedCategory = ref<Category>();
|
||||||
|
const selectedParentCategory = ref<Category>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
categories,
|
categories,
|
||||||
|
@ -68,8 +69,14 @@ const handleOpenEditingModal = (category: CategoryTree) => {
|
||||||
editingModal.value = true;
|
editingModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenCreateByParentModal = (category: CategoryTree) => {
|
||||||
|
selectedParentCategory.value = convertCategoryTreeToCategory(category);
|
||||||
|
editingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
const onEditingModalClose = () => {
|
const onEditingModalClose = () => {
|
||||||
selectedCategory.value = null;
|
selectedCategory.value = undefined;
|
||||||
|
selectedParentCategory.value = undefined;
|
||||||
handleFetchCategories();
|
handleFetchCategories();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -77,6 +84,7 @@ const onEditingModalClose = () => {
|
||||||
<CategoryEditingModal
|
<CategoryEditingModal
|
||||||
v-model:visible="editingModal"
|
v-model:visible="editingModal"
|
||||||
:category="selectedCategory"
|
:category="selectedCategory"
|
||||||
|
:parent-category="selectedParentCategory"
|
||||||
@close="onEditingModalClose"
|
@close="onEditingModalClose"
|
||||||
/>
|
/>
|
||||||
<VPageHeader :title="$t('core.post_category.title')">
|
<VPageHeader :title="$t('core.post_category.title')">
|
||||||
|
@ -147,6 +155,7 @@ const onEditingModalClose = () => {
|
||||||
@change="handleUpdateInBatch"
|
@change="handleUpdateInBatch"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
@open-editing="handleOpenEditingModal"
|
@open-editing="handleOpenEditingModal"
|
||||||
|
@open-create-by-parent="handleOpenCreateByParentModal"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
|
@ -28,11 +28,13 @@ import { useI18n } from "vue-i18n";
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
category: Category | null;
|
category?: Category;
|
||||||
|
parentCategory?: Category;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
visible: false,
|
||||||
category: null,
|
category: undefined,
|
||||||
|
parentCategory: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -63,6 +65,7 @@ const initialFormState: Category = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formState = ref<Category>(cloneDeep(initialFormState));
|
const formState = ref<Category>(cloneDeep(initialFormState));
|
||||||
|
const selectedParentCategory = ref("");
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
|
@ -100,9 +103,45 @@ const handleSaveCategory = async () => {
|
||||||
category: formState.value,
|
category: formState.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await apiClient.extension.category.createcontentHaloRunV1alpha1Category({
|
// Gets parent category, calculates priority and updates it.
|
||||||
category: formState.value,
|
let parentCategory: Category | undefined = undefined;
|
||||||
|
|
||||||
|
if (selectedParentCategory.value) {
|
||||||
|
const { data } =
|
||||||
|
await apiClient.extension.category.getcontentHaloRunV1alpha1Category({
|
||||||
|
name: selectedParentCategory.value,
|
||||||
});
|
});
|
||||||
|
parentCategory = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const priority = parentCategory?.spec.children
|
||||||
|
? parentCategory.spec.children.length + 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
formState.value.spec.priority = priority;
|
||||||
|
|
||||||
|
const { data: createdCategory } =
|
||||||
|
await apiClient.extension.category.createcontentHaloRunV1alpha1Category(
|
||||||
|
{
|
||||||
|
category: formState.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parentCategory) {
|
||||||
|
parentCategory.spec.children = Array.from(
|
||||||
|
new Set([
|
||||||
|
...(parentCategory.spec.children || []),
|
||||||
|
createdCategory.metadata.name,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
await apiClient.extension.category.updatecontentHaloRunV1alpha1Category(
|
||||||
|
{
|
||||||
|
name: selectedParentCategory.value,
|
||||||
|
category: parentCategory,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onVisibleChange(false);
|
onVisibleChange(false);
|
||||||
|
|
||||||
|
@ -122,6 +161,7 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetForm = () => {
|
const handleResetForm = () => {
|
||||||
|
selectedParentCategory.value = "";
|
||||||
formState.value = cloneDeep(initialFormState);
|
formState.value = cloneDeep(initialFormState);
|
||||||
reset("category-form");
|
reset("category-form");
|
||||||
};
|
};
|
||||||
|
@ -130,18 +170,15 @@ watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
(visible) => {
|
(visible) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setFocus("displayNameInput");
|
if (props.parentCategory) {
|
||||||
} else {
|
selectedParentCategory.value = props.parentCategory.metadata.name;
|
||||||
handleResetForm();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
if (props.category) {
|
||||||
() => props.category,
|
formState.value = cloneDeep(props.category);
|
||||||
(category) => {
|
}
|
||||||
if (category) {
|
|
||||||
formState.value = cloneDeep(category);
|
setFocus("displayNameInput");
|
||||||
} else {
|
} else {
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
}
|
}
|
||||||
|
@ -189,6 +226,14 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||||
|
<FormKit
|
||||||
|
v-if="!isUpdateMode"
|
||||||
|
v-model="selectedParentCategory"
|
||||||
|
type="categorySelect"
|
||||||
|
:label="
|
||||||
|
$t('core.post_category.editing_modal.fields.parent.label')
|
||||||
|
"
|
||||||
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
id="displayNameInput"
|
id="displayNameInput"
|
||||||
v-model="formState.spec.displayName"
|
v-model="formState.spec.displayName"
|
||||||
|
|
|
@ -26,6 +26,7 @@ withDefaults(
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "change"): void;
|
(event: "change"): void;
|
||||||
(event: "open-editing", category: CategoryTree): void;
|
(event: "open-editing", category: CategoryTree): void;
|
||||||
|
(event: "open-create-by-parent", category: CategoryTree): void;
|
||||||
(event: "delete", category: CategoryTree): void;
|
(event: "delete", category: CategoryTree): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -39,6 +40,10 @@ function onOpenEditingModal(category: CategoryTree) {
|
||||||
emit("open-editing", category);
|
emit("open-editing", category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onOpenCreateByParentModal(category: CategoryTree) {
|
||||||
|
emit("open-create-by-parent", category);
|
||||||
|
}
|
||||||
|
|
||||||
function onDelete(category: CategoryTree) {
|
function onDelete(category: CategoryTree) {
|
||||||
emit("delete", category);
|
emit("delete", category);
|
||||||
}
|
}
|
||||||
|
@ -117,6 +122,9 @@ function onDelete(category: CategoryTree) {
|
||||||
>
|
>
|
||||||
{{ $t("core.common.buttons.edit") }}
|
{{ $t("core.common.buttons.edit") }}
|
||||||
</VDropdownItem>
|
</VDropdownItem>
|
||||||
|
<VDropdownItem @click="onOpenCreateByParentModal(category)">
|
||||||
|
{{ $t("core.post_category.operations.add_sub_category.button") }}
|
||||||
|
</VDropdownItem>
|
||||||
<VDropdownItem
|
<VDropdownItem
|
||||||
v-permission="['system:posts:manage']"
|
v-permission="['system:posts:manage']"
|
||||||
type="danger"
|
type="danger"
|
||||||
|
@ -132,6 +140,7 @@ function onDelete(category: CategoryTree) {
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
@open-editing="onOpenEditingModal"
|
@open-editing="onOpenEditingModal"
|
||||||
|
@open-create-by-parent="onOpenCreateByParentModal"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue