mirror of https://github.com/halo-dev/halo
refactor: improve code base of post category-related (#5958)
#### What type of PR is this? /area ui /kind improvement /milestone 2.16.x #### What this PR does / why we need it: 优化文章分类管理相关的 UI 代码。 1. 使用 vue-draggable-plus 库代替 vuedraggable 库实现拖拽排序。vue-draggable-plus 是在 https://github.com/halo-dev/halo/pull/5914 中引入,替换的原因是 vuedraggable 库已经不再积极维护。 2. 改进分类编辑表单的逻辑,清理无用代码。 #### Special notes for your reviewer: 需要测试: 1. 测试文章分类拖拽排序功能是否表现正常。 2. 测试新增/编辑文章分类功能是否表现正常。 #### Does this PR introduce a user-facing change? ```release-note None ```pull/5965/head
parent
d29da319e7
commit
9bfe3a66d5
|
@ -10,21 +10,14 @@ import {
|
||||||
VButton,
|
VButton,
|
||||||
VCard,
|
VCard,
|
||||||
VEmpty,
|
VEmpty,
|
||||||
|
VLoading,
|
||||||
VPageHeader,
|
VPageHeader,
|
||||||
VSpace,
|
VSpace,
|
||||||
VLoading,
|
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import CategoryEditingModal from "./components/CategoryEditingModal.vue";
|
import CategoryEditingModal from "./components/CategoryEditingModal.vue";
|
||||||
import CategoryListItem from "./components/CategoryListItem.vue";
|
import CategoryListItem from "./components/CategoryListItem.vue";
|
||||||
|
|
||||||
// types
|
import { convertTreeToCategories, resetCategoriesTreePriority } from "./utils";
|
||||||
import type { Category } from "@halo-dev/api-client";
|
|
||||||
import type { CategoryTree } from "./utils";
|
|
||||||
import {
|
|
||||||
convertCategoryTreeToCategory,
|
|
||||||
convertTreeToCategories,
|
|
||||||
resetCategoriesTreePriority,
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
// libs
|
// libs
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
|
@ -32,17 +25,12 @@ import { useDebounceFn } from "@vueuse/core";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePostCategory } from "./composables/use-post-category";
|
import { usePostCategory } from "./composables/use-post-category";
|
||||||
|
|
||||||
const editingModal = ref(false);
|
const creationModal = ref(false);
|
||||||
const selectedCategory = ref<Category>();
|
|
||||||
const selectedParentCategory = ref<Category>();
|
|
||||||
|
|
||||||
const {
|
const { categories, categoriesTree, isLoading, handleFetchCategories } =
|
||||||
categories,
|
usePostCategory();
|
||||||
categoriesTree,
|
|
||||||
isLoading,
|
const batchUpdating = ref(false);
|
||||||
handleFetchCategories,
|
|
||||||
handleDelete,
|
|
||||||
} = usePostCategory();
|
|
||||||
|
|
||||||
const handleUpdateInBatch = useDebounceFn(async () => {
|
const handleUpdateInBatch = useDebounceFn(async () => {
|
||||||
const categoriesTreeToUpdate = resetCategoriesTreePriority(
|
const categoriesTreeToUpdate = resetCategoriesTreePriority(
|
||||||
|
@ -50,6 +38,7 @@ const handleUpdateInBatch = useDebounceFn(async () => {
|
||||||
);
|
);
|
||||||
const categoriesToUpdate = convertTreeToCategories(categoriesTreeToUpdate);
|
const categoriesToUpdate = convertTreeToCategories(categoriesTreeToUpdate);
|
||||||
try {
|
try {
|
||||||
|
batchUpdating.value = true;
|
||||||
const promises = categoriesToUpdate.map((category) =>
|
const promises = categoriesToUpdate.map((category) =>
|
||||||
apiClient.extension.category.updatecontentHaloRunV1alpha1Category({
|
apiClient.extension.category.updatecontentHaloRunV1alpha1Category({
|
||||||
name: category.metadata.name,
|
name: category.metadata.name,
|
||||||
|
@ -61,32 +50,12 @@ const handleUpdateInBatch = useDebounceFn(async () => {
|
||||||
console.error("Failed to update categories", e);
|
console.error("Failed to update categories", e);
|
||||||
} finally {
|
} finally {
|
||||||
await handleFetchCategories();
|
await handleFetchCategories();
|
||||||
|
batchUpdating.value = false;
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 300);
|
||||||
|
|
||||||
const handleOpenEditingModal = (category: CategoryTree) => {
|
|
||||||
selectedCategory.value = convertCategoryTreeToCategory(category);
|
|
||||||
editingModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenCreateByParentModal = (category: CategoryTree) => {
|
|
||||||
selectedParentCategory.value = convertCategoryTreeToCategory(category);
|
|
||||||
editingModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEditingModalClose = () => {
|
|
||||||
selectedCategory.value = undefined;
|
|
||||||
selectedParentCategory.value = undefined;
|
|
||||||
handleFetchCategories();
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<CategoryEditingModal
|
<CategoryEditingModal v-if="creationModal" @close="creationModal = false" />
|
||||||
v-model:visible="editingModal"
|
|
||||||
:category="selectedCategory"
|
|
||||||
:parent-category="selectedParentCategory"
|
|
||||||
@close="onEditingModalClose"
|
|
||||||
/>
|
|
||||||
<VPageHeader :title="$t('core.post_category.title')">
|
<VPageHeader :title="$t('core.post_category.title')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconBookRead class="mr-2 self-center" />
|
<IconBookRead class="mr-2 self-center" />
|
||||||
|
@ -96,7 +65,7 @@ const onEditingModalClose = () => {
|
||||||
<VButton
|
<VButton
|
||||||
v-permission="['system:posts:manage']"
|
v-permission="['system:posts:manage']"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@click="editingModal = true"
|
@click="creationModal = true"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconAddCircle class="h-full w-full" />
|
<IconAddCircle class="h-full w-full" />
|
||||||
|
@ -138,7 +107,7 @@ const onEditingModalClose = () => {
|
||||||
<VButton
|
<VButton
|
||||||
v-permission="['system:posts:manage']"
|
v-permission="['system:posts:manage']"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="editingModal = true"
|
@click="creationModal = true"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconAddCircle class="h-full w-full" />
|
<IconAddCircle class="h-full w-full" />
|
||||||
|
@ -151,11 +120,11 @@ const onEditingModalClose = () => {
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition v-else appear name="fade">
|
<Transition v-else appear name="fade">
|
||||||
<CategoryListItem
|
<CategoryListItem
|
||||||
:categories="categoriesTree"
|
v-model="categoriesTree"
|
||||||
|
:class="{
|
||||||
|
'cursor-progress opacity-60': batchUpdating,
|
||||||
|
}"
|
||||||
@change="handleUpdateInBatch"
|
@change="handleUpdateInBatch"
|
||||||
@delete="handleDelete"
|
|
||||||
@open-editing="handleOpenEditingModal"
|
|
||||||
@open-create-by-parent="handleOpenCreateByParentModal"
|
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// core libs
|
// core libs
|
||||||
import { computed, nextTick, ref, watch } from "vue";
|
import { computed, nextTick, onMounted, ref, toRaw } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
|
@ -17,36 +17,33 @@ import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||||
import type { Category } from "@halo-dev/api-client";
|
import type { Category } from "@halo-dev/api-client";
|
||||||
|
|
||||||
// libs
|
// libs
|
||||||
import { cloneDeep } from "lodash-es";
|
|
||||||
import { reset } from "@formkit/core";
|
|
||||||
import { setFocus } from "@/formkit/utils/focus";
|
import { setFocus } from "@/formkit/utils/focus";
|
||||||
import { useThemeCustomTemplates } from "@console/modules/interface/themes/composables/use-theme";
|
import { useThemeCustomTemplates } from "@console/modules/interface/themes/composables/use-theme";
|
||||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||||
import useSlugify from "@console/composables/use-slugify";
|
import useSlugify from "@console/composables/use-slugify";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { FormType } from "@/types/slug";
|
import { FormType } from "@/types/slug";
|
||||||
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
|
||||||
category?: Category;
|
category?: Category;
|
||||||
parentCategory?: Category;
|
parentCategory?: Category;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
visible: false,
|
|
||||||
category: undefined,
|
category: undefined,
|
||||||
parentCategory: undefined,
|
parentCategory: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "update:visible", visible: boolean): void;
|
|
||||||
(event: "close"): void;
|
(event: "close"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const initialFormState: Category = {
|
const formState = ref<Category>({
|
||||||
spec: {
|
spec: {
|
||||||
displayName: "",
|
displayName: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
|
@ -63,21 +60,16 @@ const initialFormState: Category = {
|
||||||
name: "",
|
name: "",
|
||||||
generateName: "category-",
|
generateName: "category-",
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
const selectedParentCategory = ref();
|
||||||
const formState = ref<Category>(cloneDeep(initialFormState));
|
|
||||||
const selectedParentCategory = ref("");
|
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
const modal = ref();
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = !!props.category;
|
||||||
return !!formState.value.metadata.creationTimestamp;
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalTitle = computed(() => {
|
const modalTitle = props.category
|
||||||
return isUpdateMode.value
|
? t("core.post_category.editing_modal.titles.update")
|
||||||
? t("core.post_category.editing_modal.titles.update")
|
: t("core.post_category.editing_modal.titles.create");
|
||||||
: t("core.post_category.editing_modal.titles.create");
|
|
||||||
});
|
|
||||||
|
|
||||||
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
||||||
|
|
||||||
|
@ -98,7 +90,7 @@ const handleSaveCategory = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
if (isUpdateMode.value) {
|
if (isUpdateMode) {
|
||||||
await apiClient.extension.category.updatecontentHaloRunV1alpha1Category({
|
await apiClient.extension.category.updatecontentHaloRunV1alpha1Category({
|
||||||
name: formState.value.metadata.name,
|
name: formState.value.metadata.name,
|
||||||
category: formState.value,
|
category: formState.value,
|
||||||
|
@ -144,7 +136,10 @@ const handleSaveCategory = async () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onVisibleChange(false);
|
|
||||||
|
modal.value.close();
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["post-categories"] });
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.save_success"));
|
Toast.success(t("core.common.toast.save_success"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -154,37 +149,13 @@ const handleSaveCategory = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onVisibleChange = (visible: boolean) => {
|
onMounted(() => {
|
||||||
emit("update:visible", visible);
|
if (props.category) {
|
||||||
if (!visible) {
|
formState.value = toRaw(props.category);
|
||||||
emit("close");
|
|
||||||
}
|
}
|
||||||
};
|
selectedParentCategory.value = props.parentCategory?.metadata.name;
|
||||||
|
setFocus("displayNameInput");
|
||||||
const handleResetForm = () => {
|
});
|
||||||
selectedParentCategory.value = "";
|
|
||||||
formState.value = cloneDeep(initialFormState);
|
|
||||||
reset("category-form");
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.visible,
|
|
||||||
(visible) => {
|
|
||||||
if (visible) {
|
|
||||||
if (props.parentCategory) {
|
|
||||||
selectedParentCategory.value = props.parentCategory.metadata.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.category) {
|
|
||||||
formState.value = cloneDeep(props.category);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFocus("displayNameInput");
|
|
||||||
} else {
|
|
||||||
handleResetForm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// custom templates
|
// custom templates
|
||||||
const { templates } = useThemeCustomTemplates("category");
|
const { templates } = useThemeCustomTemplates("category");
|
||||||
|
@ -200,17 +171,12 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
formState.value.spec.slug = value;
|
formState.value.spec.slug = value;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
computed(() => !isUpdateMode.value),
|
computed(() => !isUpdateMode),
|
||||||
FormType.CATEGORY
|
FormType.CATEGORY
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal ref="modal" :title="modalTitle" :width="700" @close="emit('close')">
|
||||||
:title="modalTitle"
|
|
||||||
:visible="visible"
|
|
||||||
:width="700"
|
|
||||||
@update:visible="onVisibleChange"
|
|
||||||
>
|
|
||||||
<FormKit
|
<FormKit
|
||||||
id="category-form"
|
id="category-form"
|
||||||
type="form"
|
type="form"
|
||||||
|
@ -331,14 +297,13 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<VSpace>
|
<VSpace>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
v-if="visible"
|
|
||||||
:loading="saving"
|
:loading="saving"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
:text="$t('core.common.buttons.submit')"
|
:text="$t('core.common.buttons.submit')"
|
||||||
@submit="$formkit.submit('category-form')"
|
@submit="$formkit.submit('category-form')"
|
||||||
>
|
>
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
<VButton @click="onVisibleChange(false)">
|
<VButton @click="modal.close()">
|
||||||
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
{{ $t("core.common.buttons.cancel_and_shortcut") }}
|
||||||
</VButton>
|
</VButton>
|
||||||
</VSpace>
|
</VSpace>
|
||||||
|
|
|
@ -1,148 +1,182 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
|
Dialog,
|
||||||
IconList,
|
IconList,
|
||||||
VStatusDot,
|
Toast,
|
||||||
|
VDropdownItem,
|
||||||
VEntity,
|
VEntity,
|
||||||
VEntityField,
|
VEntityField,
|
||||||
VDropdownItem,
|
VStatusDot,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import Draggable from "vuedraggable";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import type { CategoryTree } from "../utils";
|
import { type CategoryTree, convertCategoryTreeToCategory } from "../utils";
|
||||||
import { ref } from "vue";
|
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import CategoryEditingModal from "./CategoryEditingModal.vue";
|
||||||
|
import type { Category } from "@halo-dev/api-client";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
withDefaults(
|
const categories = defineModel({
|
||||||
defineProps<{
|
type: Array as PropType<CategoryTree[]>,
|
||||||
categories: CategoryTree[];
|
default: [],
|
||||||
}>(),
|
});
|
||||||
{
|
|
||||||
categories: () => [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "change"): void;
|
(event: "change"): void;
|
||||||
(event: "open-editing", category: CategoryTree): void;
|
|
||||||
(event: "open-create-by-parent", category: CategoryTree): void;
|
|
||||||
(event: "delete", category: CategoryTree): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isDragging = ref(false);
|
const queryClient = useQueryClient();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
function onChange() {
|
function onChange() {
|
||||||
emit("change");
|
emit("change");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpenEditingModal(category: CategoryTree) {
|
// Editing category
|
||||||
emit("open-editing", category);
|
const editingModal = ref(false);
|
||||||
|
const selectedCategory = ref<Category>();
|
||||||
|
const selectedParentCategory = ref<Category>();
|
||||||
|
|
||||||
|
function onEditingModalClose() {
|
||||||
|
selectedCategory.value = undefined;
|
||||||
|
selectedParentCategory.value = undefined;
|
||||||
|
editingModal.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpenCreateByParentModal(category: CategoryTree) {
|
const handleOpenEditingModal = (category: CategoryTree) => {
|
||||||
emit("open-create-by-parent", category);
|
selectedCategory.value = convertCategoryTreeToCategory(category);
|
||||||
}
|
editingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
function onDelete(category: CategoryTree) {
|
const handleOpenCreateByParentModal = (category: CategoryTree) => {
|
||||||
emit("delete", category);
|
selectedParentCategory.value = convertCategoryTreeToCategory(category);
|
||||||
}
|
editingModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (category: CategoryTree) => {
|
||||||
|
Dialog.warning({
|
||||||
|
title: t("core.post_category.operations.delete.title"),
|
||||||
|
description: t("core.post_category.operations.delete.description"),
|
||||||
|
confirmType: "danger",
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.extension.category.deletecontentHaloRunV1alpha1Category(
|
||||||
|
{
|
||||||
|
name: category.metadata.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Toast.success(t("core.common.toast.delete_success"));
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["post-categories"] });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to delete tag", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<draggable
|
<VueDraggable
|
||||||
:list="categories"
|
v-model="categories"
|
||||||
class="box-border h-full w-full divide-y divide-gray-100"
|
class="box-border h-full w-full divide-y divide-gray-100"
|
||||||
ghost-class="opacity-50"
|
ghost-class="opacity-50"
|
||||||
group="category-item"
|
group="category-item"
|
||||||
handle=".drag-element"
|
handle=".drag-element"
|
||||||
item-key="metadata.name"
|
|
||||||
tag="ul"
|
tag="ul"
|
||||||
@change="onChange"
|
@sort="onChange"
|
||||||
@end="isDragging = false"
|
|
||||||
@start="isDragging = true"
|
|
||||||
>
|
>
|
||||||
<template #item="{ element: category }">
|
<CategoryEditingModal
|
||||||
<li>
|
v-if="editingModal"
|
||||||
<VEntity>
|
:category="selectedCategory"
|
||||||
<template #prepend>
|
:parent-category="selectedParentCategory"
|
||||||
<div
|
@close="onEditingModalClose"
|
||||||
v-permission="['system:posts:manage']"
|
/>
|
||||||
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
|
<li v-for="category in categories" :key="category.metadata.name">
|
||||||
>
|
<VEntity>
|
||||||
<IconList class="h-3.5 w-3.5" />
|
<template #prepend>
|
||||||
</div>
|
<div
|
||||||
</template>
|
v-permission="['system:posts:manage']"
|
||||||
<template #start>
|
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
|
||||||
<VEntityField :title="category.spec.displayName">
|
|
||||||
<template #description>
|
|
||||||
<a
|
|
||||||
v-if="category.status.permalink"
|
|
||||||
:href="category.status.permalink"
|
|
||||||
:title="category.status.permalink"
|
|
||||||
target="_blank"
|
|
||||||
class="truncate text-xs text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
{{ category.status.permalink }}
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</VEntityField>
|
|
||||||
</template>
|
|
||||||
<template #end>
|
|
||||||
<VEntityField v-if="category.metadata.deletionTimestamp">
|
|
||||||
<template #description>
|
|
||||||
<VStatusDot
|
|
||||||
v-tooltip="$t('core.common.status.deleting')"
|
|
||||||
state="warning"
|
|
||||||
animate
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</VEntityField>
|
|
||||||
<VEntityField
|
|
||||||
:description="
|
|
||||||
$t('core.common.fields.post_count', {
|
|
||||||
count: category.status?.postCount || 0,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<VEntityField>
|
|
||||||
<template #description>
|
|
||||||
<span class="truncate text-xs tabular-nums text-gray-500">
|
|
||||||
{{ formatDatetime(category.metadata.creationTimestamp) }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</VEntityField>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-if="currentUserHasPermission(['system:posts:manage'])"
|
|
||||||
#dropdownItems
|
|
||||||
>
|
>
|
||||||
<VDropdownItem
|
<IconList class="h-3.5 w-3.5" />
|
||||||
v-permission="['system:posts:manage']"
|
</div>
|
||||||
@click="onOpenEditingModal(category)"
|
</template>
|
||||||
>
|
<template #start>
|
||||||
{{ $t("core.common.buttons.edit") }}
|
<VEntityField :title="category.spec.displayName">
|
||||||
</VDropdownItem>
|
<template #description>
|
||||||
<VDropdownItem @click="onOpenCreateByParentModal(category)">
|
<a
|
||||||
{{ $t("core.post_category.operations.add_sub_category.button") }}
|
v-if="category.status?.permalink"
|
||||||
</VDropdownItem>
|
:href="category.status.permalink"
|
||||||
<VDropdownItem
|
:title="category.status.permalink"
|
||||||
v-permission="['system:posts:manage']"
|
target="_blank"
|
||||||
type="danger"
|
class="truncate text-xs text-gray-500 group-hover:text-gray-900"
|
||||||
@click="onDelete(category)"
|
>
|
||||||
>
|
{{ category.status.permalink }}
|
||||||
{{ $t("core.common.buttons.delete") }}
|
</a>
|
||||||
</VDropdownItem>
|
</template>
|
||||||
</template>
|
</VEntityField>
|
||||||
</VEntity>
|
</template>
|
||||||
<CategoryListItem
|
<template #end>
|
||||||
:categories="category.spec.children"
|
<VEntityField v-if="category.metadata.deletionTimestamp">
|
||||||
class="pl-10 transition-all duration-300"
|
<template #description>
|
||||||
@change="onChange"
|
<VStatusDot
|
||||||
@delete="onDelete"
|
v-tooltip="$t('core.common.status.deleting')"
|
||||||
@open-editing="onOpenEditingModal"
|
state="warning"
|
||||||
@open-create-by-parent="onOpenCreateByParentModal"
|
animate
|
||||||
/>
|
/>
|
||||||
</li>
|
</template>
|
||||||
</template>
|
</VEntityField>
|
||||||
</draggable>
|
<VEntityField
|
||||||
|
:description="
|
||||||
|
$t('core.common.fields.post_count', {
|
||||||
|
count: category.status?.postCount || 0,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<VEntityField>
|
||||||
|
<template #description>
|
||||||
|
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||||
|
{{ formatDatetime(category.metadata.creationTimestamp) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="currentUserHasPermission(['system:posts:manage'])"
|
||||||
|
#dropdownItems
|
||||||
|
>
|
||||||
|
<VDropdownItem
|
||||||
|
v-permission="['system:posts:manage']"
|
||||||
|
@click="handleOpenEditingModal(category)"
|
||||||
|
>
|
||||||
|
{{ $t("core.common.buttons.edit") }}
|
||||||
|
</VDropdownItem>
|
||||||
|
<VDropdownItem @click="handleOpenCreateByParentModal(category)">
|
||||||
|
{{ $t("core.post_category.operations.add_sub_category.button") }}
|
||||||
|
</VDropdownItem>
|
||||||
|
<VDropdownItem
|
||||||
|
v-permission="['system:posts:manage']"
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(category)"
|
||||||
|
>
|
||||||
|
{{ $t("core.common.buttons.delete") }}
|
||||||
|
</VDropdownItem>
|
||||||
|
</template>
|
||||||
|
</VEntity>
|
||||||
|
<CategoryListItem
|
||||||
|
v-model="category.spec.children"
|
||||||
|
class="pl-10 transition-all duration-300"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</VueDraggable>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,23 +2,18 @@ import { apiClient } from "@/utils/api-client";
|
||||||
import type { Category } from "@halo-dev/api-client";
|
import type { Category } from "@halo-dev/api-client";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { CategoryTree } from "@console/modules/contents/posts/categories/utils";
|
import type { CategoryTree } from "../utils";
|
||||||
import { buildCategoriesTree } from "@console/modules/contents/posts/categories/utils";
|
import { buildCategoriesTree } from "../utils";
|
||||||
import { Dialog, Toast } from "@halo-dev/components";
|
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
|
|
||||||
interface usePostCategoryReturn {
|
interface usePostCategoryReturn {
|
||||||
categories: Ref<Category[] | undefined>;
|
categories: Ref<Category[] | undefined>;
|
||||||
categoriesTree: Ref<CategoryTree[]>;
|
categoriesTree: Ref<CategoryTree[]>;
|
||||||
isLoading: Ref<boolean>;
|
isLoading: Ref<boolean>;
|
||||||
handleFetchCategories: () => void;
|
handleFetchCategories: () => void;
|
||||||
handleDelete: (category: CategoryTree) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePostCategory(): usePostCategoryReturn {
|
export function usePostCategory(): usePostCategoryReturn {
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
|
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -38,47 +33,21 @@ export function usePostCategory(): usePostCategoryReturn {
|
||||||
return data.items;
|
return data.items;
|
||||||
},
|
},
|
||||||
refetchInterval(data) {
|
refetchInterval(data) {
|
||||||
const abnormalCategories = data?.filter(
|
const hasAbnormalCategory = data?.some(
|
||||||
(category) =>
|
(category) =>
|
||||||
!!category.metadata.deletionTimestamp || !category.status?.permalink
|
!!category.metadata.deletionTimestamp || !category.status?.permalink
|
||||||
);
|
);
|
||||||
return abnormalCategories?.length ? 1000 : false;
|
return hasAbnormalCategory ? 1000 : false;
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
categoriesTree.value = buildCategoriesTree(data);
|
categoriesTree.value = buildCategoriesTree(data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = async (category: CategoryTree) => {
|
|
||||||
Dialog.warning({
|
|
||||||
title: t("core.post_category.operations.delete.title"),
|
|
||||||
description: t("core.post_category.operations.delete.description"),
|
|
||||||
confirmType: "danger",
|
|
||||||
confirmText: t("core.common.buttons.confirm"),
|
|
||||||
cancelText: t("core.common.buttons.cancel"),
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await apiClient.extension.category.deletecontentHaloRunV1alpha1Category(
|
|
||||||
{
|
|
||||||
name: category.metadata.name,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.delete_success"));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to delete tag", e);
|
|
||||||
} finally {
|
|
||||||
await refetch();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
categoriesTree,
|
categoriesTree,
|
||||||
isLoading,
|
isLoading,
|
||||||
handleFetchCategories: refetch,
|
handleFetchCategories: refetch,
|
||||||
handleDelete,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,8 +100,7 @@
|
||||||
"vue-draggable-plus": "^0.4.1",
|
"vue-draggable-plus": "^0.4.1",
|
||||||
"vue-grid-layout": "3.0.0-beta1",
|
"vue-grid-layout": "3.0.0-beta1",
|
||||||
"vue-i18n": "^9.9.1",
|
"vue-i18n": "^9.9.1",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5"
|
||||||
"vuedraggable": "^4.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.25.2",
|
"@changesets/cli": "^2.25.2",
|
||||||
|
|
2281
ui/pnpm-lock.yaml
2281
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -87,7 +87,7 @@ export function createViteConfig(options: Options) {
|
||||||
"axios",
|
"axios",
|
||||||
"vue-grid-layout",
|
"vue-grid-layout",
|
||||||
"transliteration",
|
"transliteration",
|
||||||
"vuedraggable",
|
"vue-draggable-plus",
|
||||||
"emoji-mart",
|
"emoji-mart",
|
||||||
"colorjs.io",
|
"colorjs.io",
|
||||||
"jsencrypt",
|
"jsencrypt",
|
||||||
|
|
Loading…
Reference in New Issue