mirror of https://github.com/halo-dev/halo-admin
feat: add full-functional post category select component for formkit (#818)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 添加功能更为全面的文章分类选择器,支持以下特性: 1. 按层级展示分类 2. 支持搜索 3. 选中结果支持显示层级 todo list: - [x] 样式整理 - [x] 支持创建新分类 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2670 Fixes https://github.com/halo-dev/halo/issues/2485 #### Screenshots: <img width="832" alt="image" src="https://user-images.githubusercontent.com/21301288/211768419-087d9727-1468-41a1-868a-62d90eef9cca.png"> <img width="858" alt="image" src="https://user-images.githubusercontent.com/21301288/211768478-dcc70d79-127b-42b0-ae44-e48a6a22273a.png"> #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 重构 Console 端的文章分类选择器。 ```pull/826/head
parent
b132a950a8
commit
9d5529b827
|
@ -20,6 +20,8 @@
|
|||
- `postSelect`:选择文章
|
||||
- `singlePageSelect`:选择自定义页面
|
||||
- `categorySelect`:选择分类
|
||||
- 参数
|
||||
1. multiple: 是否多选,默认为 `false`
|
||||
- `categoryCheckbox`:选择多个分类
|
||||
- `tagSelect`:选择标签
|
||||
- 参数
|
||||
|
|
|
@ -11,10 +11,10 @@ import { menuRadio } from "./inputs/menu-radio";
|
|||
import { menuItemSelect } from "./inputs/menu-item-select";
|
||||
import { postSelect } from "./inputs/post-select";
|
||||
import { singlePageSelect } from "./inputs/singlePage-select";
|
||||
import { tagSelect } from "./inputs/tag-select";
|
||||
import { categorySelect } from "./inputs/category-select";
|
||||
import { categoryCheckbox } from "./inputs/category-checkbox";
|
||||
import { tagCheckbox } from "./inputs/tag-checkbox";
|
||||
import { tagSelect } from "./inputs/tag-select/index";
|
||||
|
||||
import radioAlt from "./plugins/radio-alt";
|
||||
import stopImplicitSubmission from "./plugins/stop-implicit-submission";
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
|
||||
import { select, selects, defaultIcon } from "@formkit/inputs";
|
||||
|
||||
function optionsHandler(node: FormKitNode) {
|
||||
node.on("created", async () => {
|
||||
const { data } =
|
||||
await apiClient.extension.category.listcontentHaloRunV1alpha1Category();
|
||||
|
||||
node.props.options = data.items.map((category) => {
|
||||
return {
|
||||
value: category.metadata.name,
|
||||
label: category.spec.displayName,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const categorySelect: FormKitTypeDefinition = {
|
||||
...select,
|
||||
props: ["placeholder"],
|
||||
forceTypeProp: "select",
|
||||
features: [optionsHandler, selects, defaultIcon("select", "select")],
|
||||
};
|
|
@ -0,0 +1,315 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FormKitFrameworkContext } from "@formkit/core";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { computed, provide, ref, watch, type PropType, type Ref } from "vue";
|
||||
import { IconArrowRight } from "@halo-dev/components";
|
||||
import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category";
|
||||
import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
|
||||
import { convertTreeToCategories } from "@/modules/contents/posts/categories/utils";
|
||||
import CategoryListItem from "./components/CategoryListItem.vue";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import Fuse from "fuse.js";
|
||||
import CategoryTag from "./components/CategoryTag.vue";
|
||||
import SearchResultListItem from "./components/SearchResultListItem.vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const props = defineProps({
|
||||
context: {
|
||||
type: Object as PropType<FormKitFrameworkContext>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const multiple = computed(() => {
|
||||
const { multiple } = props.context;
|
||||
if (multiple === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof multiple === "boolean") {
|
||||
return multiple;
|
||||
}
|
||||
return multiple === "true";
|
||||
});
|
||||
|
||||
const { categories, categoriesTree, handleFetchCategories } = usePostCategory({
|
||||
fetchOnMounted: true,
|
||||
});
|
||||
|
||||
provide<Ref<CategoryTree[]>>("categoriesTree", categoriesTree);
|
||||
|
||||
const selectedCategory = ref<Category | CategoryTree>();
|
||||
|
||||
provide<Ref<Category | CategoryTree | undefined>>(
|
||||
"selectedCategory",
|
||||
selectedCategory
|
||||
);
|
||||
|
||||
const dropdownVisible = ref(false);
|
||||
const text = ref("");
|
||||
const wrapperRef = ref<HTMLElement>();
|
||||
|
||||
onClickOutside(wrapperRef, () => {
|
||||
dropdownVisible.value = false;
|
||||
});
|
||||
|
||||
// search
|
||||
let fuse: Fuse<Category> | undefined = undefined;
|
||||
|
||||
const searchResults = computed(() => {
|
||||
if (!text.value) {
|
||||
return categories.value;
|
||||
}
|
||||
return fuse?.search(text.value).map((item) => item.item) || [];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => searchResults.value,
|
||||
(value) => {
|
||||
if (value?.length > 0 && text.value) {
|
||||
selectedCategory.value = value[0];
|
||||
scrollToSelected();
|
||||
} else {
|
||||
selectedCategory.value = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => categories.value,
|
||||
() => {
|
||||
fuse = new Fuse(categories.value, {
|
||||
keys: ["spec.displayName", "spec.slug"],
|
||||
useExtendedSearch: true,
|
||||
threshold: 0.2,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const selectedCategories = computed(() => {
|
||||
if (multiple.value) {
|
||||
const currentValue = props.context._value || [];
|
||||
return currentValue
|
||||
.map((categoryName): Category | undefined => {
|
||||
return categories.value.find(
|
||||
(category) => category.metadata.name === categoryName
|
||||
);
|
||||
})
|
||||
.filter(Boolean) as Category[];
|
||||
}
|
||||
|
||||
const category = categories.value.find(
|
||||
(category) => category.metadata.name === props.context._value
|
||||
);
|
||||
return [category].filter(Boolean) as Category[];
|
||||
});
|
||||
|
||||
const isSelected = (category: CategoryTree | Category) => {
|
||||
if (multiple.value) {
|
||||
return (props.context._value || []).includes(category.metadata.name);
|
||||
}
|
||||
return props.context._value === category.metadata.name;
|
||||
};
|
||||
|
||||
provide<(category: CategoryTree | Category) => boolean>(
|
||||
"isSelected",
|
||||
isSelected
|
||||
);
|
||||
|
||||
const handleSelect = (category: CategoryTree | Category) => {
|
||||
if (multiple.value) {
|
||||
const currentValue = props.context._value || [];
|
||||
if (currentValue.includes(category.metadata.name)) {
|
||||
props.context.node.input(
|
||||
currentValue.filter((name: string) => name !== category.metadata.name)
|
||||
);
|
||||
} else {
|
||||
props.context.node.input([...currentValue, category.metadata.name]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
props.context.node.input(
|
||||
category.metadata.name === props.context._value
|
||||
? ""
|
||||
: category.metadata.name
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
|
||||
const categoryIndices = text.value
|
||||
? searchResults.value
|
||||
: convertTreeToCategories(categoriesTree.value);
|
||||
|
||||
const index = categoryIndices.findIndex(
|
||||
(category) =>
|
||||
category.metadata.name === selectedCategory.value?.metadata.name
|
||||
);
|
||||
|
||||
if (index < searchResults.value.length - 1) {
|
||||
selectedCategory.value = categoryIndices[index + 1];
|
||||
}
|
||||
scrollToSelected();
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
|
||||
const categoryIndices = text.value
|
||||
? searchResults.value
|
||||
: convertTreeToCategories(categoriesTree.value);
|
||||
|
||||
const index = categoryIndices.findIndex(
|
||||
(tag) => tag.metadata.name === selectedCategory.value?.metadata.name
|
||||
);
|
||||
if (index > 0) {
|
||||
selectedCategory.value = categoryIndices[index - 1];
|
||||
}
|
||||
scrollToSelected();
|
||||
}
|
||||
|
||||
if (e.key === "Enter") {
|
||||
if (searchResults.value.length === 0 && text.value) {
|
||||
handleCreateCategory();
|
||||
return;
|
||||
}
|
||||
if (selectedCategory.value) {
|
||||
handleSelect(selectedCategory.value);
|
||||
text.value = "";
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToSelected = () => {
|
||||
if (!selectedCategory.value) {
|
||||
return;
|
||||
}
|
||||
const selectedNode = document.getElementById(
|
||||
`category-${selectedCategory.value?.metadata.name}`
|
||||
);
|
||||
if (selectedNode) {
|
||||
selectedNode.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "start",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCategory = async () => {
|
||||
if (!currentUserHasPermission(["system:posts:manage"])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } =
|
||||
await apiClient.extension.category.createcontentHaloRunV1alpha1Category({
|
||||
category: {
|
||||
spec: {
|
||||
displayName: text.value,
|
||||
slug: text.value,
|
||||
description: "",
|
||||
cover: "",
|
||||
template: "",
|
||||
priority: categories.value.length + 1,
|
||||
children: [],
|
||||
},
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Category",
|
||||
metadata: {
|
||||
name: "",
|
||||
generateName: "category-",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
handleFetchCategories();
|
||||
handleSelect(data);
|
||||
text.value = "";
|
||||
};
|
||||
|
||||
// update value immediately during IME composition
|
||||
// please see https://vuejs.org//guide/essentials/forms.html#text
|
||||
const onTextInput = (e: Event) => {
|
||||
text.value = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
|
||||
// delete last category when text input is empty
|
||||
const handleDelete = () => {
|
||||
if (!text.value) {
|
||||
if (multiple.value) {
|
||||
const selectedTagNames = (props.context._value as string[]) || [];
|
||||
props.context.node.input(selectedTagNames.slice(0, -1));
|
||||
return;
|
||||
}
|
||||
props.context.node.input("");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="wrapperRef"
|
||||
:class="context.classes['post-categories-wrapper']"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<div :class="context.classes['post-categories']">
|
||||
<CategoryTag
|
||||
v-for="(category, index) in selectedCategories"
|
||||
:key="index"
|
||||
:category="category"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
<input
|
||||
:value="text"
|
||||
:class="context.classes.input"
|
||||
type="text"
|
||||
@input="onTextInput"
|
||||
@focus="dropdownVisible = true"
|
||||
@keydown.delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="context.classes['post-categories-button']"
|
||||
@click="dropdownVisible = !dropdownVisible"
|
||||
>
|
||||
<IconArrowRight class="rotate-90 text-gray-500 hover:text-gray-700" />
|
||||
</div>
|
||||
|
||||
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
||||
<ul class="p-1">
|
||||
<li
|
||||
v-if="text.trim() && searchResults.length <= 0"
|
||||
v-permission="['system:posts:manage']"
|
||||
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||
>
|
||||
<span class="text-xs text-gray-700 group-hover:text-gray-900">
|
||||
创建 {{ text }} 分类
|
||||
</span>
|
||||
</li>
|
||||
<template v-if="text">
|
||||
<SearchResultListItem
|
||||
v-for="category in searchResults"
|
||||
:key="category.metadata.name"
|
||||
:category="category"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<CategoryListItem
|
||||
v-for="category in categoriesTree"
|
||||
:key="category.metadata.name"
|
||||
:category="category"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { IconCheckboxCircle } from "@halo-dev/components";
|
||||
import { inject, ref, type Ref } from "vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
category: CategoryTree;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const isSelected = inject<(category: CategoryTree) => boolean>("isSelected");
|
||||
const selectedCategory = inject<Ref<Category | CategoryTree | undefined>>(
|
||||
"selectedCategory",
|
||||
ref(undefined)
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", category: CategoryTree): void;
|
||||
}>();
|
||||
|
||||
const onSelect = (childCategory: CategoryTree) => {
|
||||
emit("select", childCategory);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li :id="`category-${category.metadata.name}`">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between rounded p-2 hover:bg-gray-100"
|
||||
:class="{
|
||||
'bg-gray-100':
|
||||
selectedCategory?.metadata.name === category.metadata.name,
|
||||
}"
|
||||
@click="emit('select', category)"
|
||||
>
|
||||
<span
|
||||
class="flex-1 truncate text-xs text-gray-700 group-hover:text-gray-900"
|
||||
:class="{
|
||||
'text-gray-900':
|
||||
isSelected?.(category) &&
|
||||
selectedCategory?.metadata.name === category.metadata.name,
|
||||
}"
|
||||
>
|
||||
{{ category.spec.displayName }}
|
||||
</span>
|
||||
|
||||
<IconCheckboxCircle
|
||||
class="text-primary opacity-0"
|
||||
:class="{ 'opacity-100': isSelected?.(category) }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-if="category.spec.children.length > 0"
|
||||
class="my-2.5 ml-2.5 border-l pl-1.5"
|
||||
>
|
||||
<CategoryListItem
|
||||
v-for="(childCategory, index) in category.spec.children"
|
||||
:key="index"
|
||||
:category="childCategory"
|
||||
@select="onSelect"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CategoryTree } from "@/modules/contents/posts/categories/utils";
|
||||
import { getCategoryPath } from "@/modules/contents/posts/categories/utils";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { computed, inject, ref, type Ref } from "vue";
|
||||
import { IconClose } from "@halo-dev/components";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
category: Category;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", category: CategoryTree | Category): void;
|
||||
}>();
|
||||
|
||||
const categoriesTree = inject<Ref<CategoryTree[]>>("categoriesTree", ref([]));
|
||||
|
||||
const label = computed(() => {
|
||||
const categories = getCategoryPath(
|
||||
categoriesTree.value,
|
||||
props.category.metadata.name
|
||||
);
|
||||
return categories
|
||||
.map((category: CategoryTree) => category.spec.displayName)
|
||||
.join(" / ");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center p-1">
|
||||
<div
|
||||
class="box-border inline-flex min-h-[1.25rem] items-center gap-1 rounded-full border border-solid border-[#d9d9d9] bg-white px-1 align-middle"
|
||||
>
|
||||
<span class="flex-1 text-xs">
|
||||
{{ label }}
|
||||
</span>
|
||||
<IconClose
|
||||
class="h-4 w-4 cursor-pointer text-gray-600 hover:text-gray-900"
|
||||
@click="emit('select', category)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
getCategoryPath,
|
||||
type CategoryTree,
|
||||
} from "@/modules/contents/posts/categories/utils";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { IconCheckboxCircle } from "@halo-dev/components";
|
||||
import { computed, inject, ref, type Ref } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
category: Category;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const categoriesTree = inject<Ref<CategoryTree[]>>("categoriesTree", ref([]));
|
||||
const selectedCategory = inject<Ref<Category | CategoryTree | undefined>>(
|
||||
"selectedCategory",
|
||||
ref(undefined)
|
||||
);
|
||||
|
||||
const isSelected =
|
||||
inject<(category: Category | CategoryTree) => boolean>("isSelected");
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", category: CategoryTree | Category): void;
|
||||
}>();
|
||||
|
||||
const label = computed(() => {
|
||||
const categories = getCategoryPath(
|
||||
categoriesTree.value,
|
||||
props.category.metadata.name
|
||||
);
|
||||
return categories
|
||||
.map((category: CategoryTree) => category.spec.displayName)
|
||||
.join(" / ");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li :id="`category-${category.metadata.name}`">
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between rounded p-2 hover:bg-gray-100"
|
||||
:class="{
|
||||
'bg-gray-100':
|
||||
selectedCategory?.metadata.name === category.metadata.name,
|
||||
}"
|
||||
@click="emit('select', category)"
|
||||
>
|
||||
<span
|
||||
class="flex-1 truncate text-xs text-gray-700 group-hover:text-gray-900"
|
||||
:class="{
|
||||
'text-gray-900':
|
||||
isSelected?.(category) &&
|
||||
selectedCategory?.metadata.name === category.metadata.name,
|
||||
}"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<IconCheckboxCircle
|
||||
class="h-5 w-5 text-primary opacity-0"
|
||||
:class="{ 'opacity-100': isSelected?.(category) }"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
|
@ -0,0 +1,37 @@
|
|||
import type { FormKitTypeDefinition } from "@formkit/core";
|
||||
import {
|
||||
help,
|
||||
icon,
|
||||
inner,
|
||||
label,
|
||||
message,
|
||||
messages,
|
||||
outer,
|
||||
prefix,
|
||||
suffix,
|
||||
wrapper,
|
||||
} from "@formkit/inputs";
|
||||
import CategorySelect from "./CategorySelect.vue";
|
||||
import { CategorySelectSection } from "./sections";
|
||||
|
||||
export const categorySelect: FormKitTypeDefinition = {
|
||||
schema: outer(
|
||||
wrapper(
|
||||
label("$label"),
|
||||
inner(
|
||||
icon("prefix"),
|
||||
prefix(),
|
||||
CategorySelectSection(),
|
||||
suffix(),
|
||||
icon("suffix")
|
||||
)
|
||||
),
|
||||
help("$help"),
|
||||
messages(message("$message.value"))
|
||||
),
|
||||
type: "input",
|
||||
props: ["multiple"],
|
||||
library: {
|
||||
CategorySelect: CategorySelect,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
import { createSection } from "@formkit/inputs";
|
||||
|
||||
export const CategorySelectSection = createSection(
|
||||
"CategorySelectSection",
|
||||
() => ({
|
||||
$cmp: "CategorySelect",
|
||||
props: {
|
||||
context: "$node.context",
|
||||
},
|
||||
})
|
||||
);
|
|
@ -104,6 +104,17 @@ const theme: Record<string, Record<string, string>> = {
|
|||
"dropdown-wrapper":
|
||||
"absolute ring-1 ring-white top-full bottom-auto right-0 z-10 mt-1 max-h-96 w-full overflow-auto rounded bg-white shadow-lg",
|
||||
},
|
||||
categorySelect: {
|
||||
...textClassification,
|
||||
inner: `${textClassification.inner} !overflow-visible !h-auto min-h-[2.25rem]`,
|
||||
input: `w-0 flex-grow outline-0 bg-transparent py-1 px-3 block transition-all appearance-none text-sm antialiased`,
|
||||
"post-categories-wrapper": "flex w-full items-center",
|
||||
"post-categories": "flex w-full flex-wrap items-center",
|
||||
"post-categories-button":
|
||||
"inline-flex h-full cursor-pointer items-center px-1",
|
||||
"dropdown-wrapper":
|
||||
"absolute ring-1 ring-white top-full bottom-auto right-0 z-10 mt-1 max-h-96 w-full overflow-auto rounded bg-white shadow-lg",
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
|
|
@ -121,3 +121,26 @@ export function convertCategoryTreeToCategory(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const getCategoryPath = (
|
||||
categories: CategoryTree[],
|
||||
name: string,
|
||||
path: CategoryTree[] = []
|
||||
) => {
|
||||
for (const category of categories) {
|
||||
if (category.metadata && category.metadata.name === name) {
|
||||
return path.concat([category]);
|
||||
}
|
||||
|
||||
if (category.spec && category.spec.children) {
|
||||
const found = getCategoryPath(
|
||||
category.spec.children,
|
||||
name,
|
||||
path.concat([category])
|
||||
);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -270,7 +270,8 @@ const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
|||
v-model="formState.spec.categories"
|
||||
label="分类目录"
|
||||
name="categories"
|
||||
type="categoryCheckbox"
|
||||
type="categorySelect"
|
||||
:multiple="true"
|
||||
/>
|
||||
<FormKit
|
||||
v-model="formState.spec.tags"
|
||||
|
|
Loading…
Reference in New Issue