fix(cms): 分类树防环——上级候选剔除自身子树,列表跳过自指向脏数据

与后端 checkParentValid 配套的前端侧防御(分类是自关联结构,成环会让分类树整个渲染不出来):

- category-edit:新增 parentOptions 计算属性,修改分类时把「自己及自己的整棵子树」
  从上级候选中剔除,避免在界面上就能选出环。
- index:parseData 先过滤掉 categoryId === parentId 的记录(toTreeData 遇到这类数据
  会直接 throw,拖垮整个列表),并把被跳过的分类 id 提示给用户,让其去库里修正。
This commit is contained in:
2026-10-13 20:00:37 +08:00
parent 8ca7a00afb
commit da14a1b45c
2 changed files with 38 additions and 4 deletions
@@ -25,7 +25,7 @@
<a-form-item label="上级分类" name="parentId">
<a-tree-select
allow-clear
:tree-data="categoryList"
:tree-data="parentOptions"
tree-default-expand-all
placeholder="请选择上级分类"
:value="form.parentId || undefined"
@@ -84,13 +84,12 @@
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
@@ -141,6 +140,26 @@
sortNumber: 100
});
/*
* 上级分类候选:修改时要把自己及自己的整棵子树剔除,
* 否则可以把上级选成自己或自己的下级,形成环导致分类树无法渲染。
*/
const parentOptions = computed(() => {
const currentId = form.categoryId;
if (currentId == null) {
return props.categoryList ?? [];
}
const filterSelf = (list: ArticleCategory[]): ArticleCategory[] =>
(list ?? [])
.filter((d) => d.categoryId !== currentId)
.map((d) =>
d.children?.length
? { ...d, children: filterSelf(d.children) }
: { ...d }
);
return filterSelf(props.categoryList);
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
title: [
+16 -1
View File
@@ -195,8 +195,23 @@
/* 数据转为树形结构 */
const parseData = (data: ArticleCategory[]) => {
/*
* 防御性过滤:分类是自关联结构,如果库里存在「自己指向自己」的脏数据,
* toTreeData 会直接 throw,导致整个页面无法渲染(报 data error: {categoryId: x, parentId: x})。
* 这里先剔除这类记录,只影响异常的那一行,不拖垮整个列表。
*/
const validData = (data ?? []).filter((d) => d.categoryId !== d.parentId);
if (validData.length !== (data ?? []).length) {
const badIds = (data ?? [])
.filter((d) => d.categoryId === d.parentId)
.map((d) => d.categoryId)
.join('、');
message.error(
`分类 ${badIds} 的上级分类是它自己,已跳过显示,请在数据库中修正`
);
}
return toTreeData({
data: data.map((d) => {
data: validData.map((d) => {
return { ...d, key: d.categoryId, value: d.categoryId };
}),
idField: 'categoryId',