@@ -112,25 +112,28 @@ function findNode(list: any[], id: number): any {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** target 是否 node 自身或其子孙(上级下拉排除,防把栏目挂到自己子树下成环) */
|
||||
/** target 是否 node 自身或其子孙(成环判定:把栏目挂到自己子树下会形成环) */
|
||||
function isSelfOrDescendant(node: any, targetId: number | undefined): boolean {
|
||||
if (!node || targetId == null) return false;
|
||||
if (node.id === targetId) return true;
|
||||
return (node.children || []).some((c: any) => isSelfOrDescendant(c, targetId));
|
||||
}
|
||||
|
||||
/** 上级栏目选项:虚拟根(0=rootLabel)+ 真实树拍平缩进;编辑态禁选自身及后代(后端仅防一级成环) */
|
||||
/**
|
||||
* 上级栏目选项:虚拟根(0=rootLabel)+ 真实树拍平缩进。
|
||||
*
|
||||
* 2026-09-11 口径调整:**不再置灰任何选项**(含外链型栏目、含自身及其子栏目)——
|
||||
* 类型与"能否作父"无关,而且灰色项只显示一个无解释的锁,用户看到的往往是
|
||||
* "办事大厅被禁用了"这类误解。成环改为保存前校验(见 save())并给出明确原因,
|
||||
* 后端 CategoryService.validateParent 同步升级为沿祖先链防任意层级成环。
|
||||
*/
|
||||
const parentOptions = computed(() => {
|
||||
const list: { id: number; label: string; disabled: boolean }[] = [
|
||||
{ id: 0, label: rootLabel.value || "根栏目", disabled: false },
|
||||
const list: { id: number; label: string }[] = [
|
||||
{ id: 0, label: rootLabel.value || "根栏目" },
|
||||
];
|
||||
const walk = (nodes: any[], depth: number) => {
|
||||
(nodes || []).forEach((n: any) => {
|
||||
list.push({
|
||||
id: n.id,
|
||||
label: `${" ".repeat(depth)}${n.name}`,
|
||||
disabled: isSelfOrDescendant(n, form.id),
|
||||
});
|
||||
list.push({ id: n.id, label: `${" ".repeat(depth)}${n.name}` });
|
||||
walk(n.children || [], depth + 1);
|
||||
});
|
||||
};
|
||||
@@ -217,9 +220,32 @@ function showDeleteNode() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前成环校验:上级栏目不能是自身或自己的子栏目。
|
||||
* 返回提示文案(命中)或空串(通过)。
|
||||
*/
|
||||
function findCycleParent(): string {
|
||||
const parentId = Number(form.parentId);
|
||||
if (!parentId) return "";
|
||||
if (form.id != null && parentId === Number(form.id)) {
|
||||
return "上级栏目不能是自身,请另选一个上级栏目";
|
||||
}
|
||||
const parentNode = findNode(treeData.value, parentId);
|
||||
if (parentNode && isSelfOrDescendant(parentNode, form.id)) {
|
||||
return `「${parentNode.name}」是当前栏目的子栏目,挂到它下面会形成循环,请另选上级栏目`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const valid = await refFormData.value?.validate();
|
||||
if (!valid) return;
|
||||
// 成环校验(下拉已不再置灰,这里给出可读原因;后端 CategoryService.validateParent 同口径兜底)
|
||||
const cycleHit = findCycleParent();
|
||||
if (cycleHit) {
|
||||
Message.warning(cycleHit);
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await categoryApi
|
||||
@@ -474,10 +500,11 @@ function openLink(url: string) {
|
||||
<Divider size="small" orientation="left">基本信息</Divider>
|
||||
<FormItem label="上级栏目" prop="parentId">
|
||||
<Select v-model="form.parentId" transfer>
|
||||
<Option v-for="opt in parentOptions" :key="opt.id" :value="opt.id" :disabled="opt.disabled">
|
||||
<Option v-for="opt in parentOptions" :key="opt.id" :value="opt.id">
|
||||
{{ opt.label }}
|
||||
</Option>
|
||||
</Select>
|
||||
<p class="form-tip">任何类型的栏目都可作为上级(外链型也可以);不能选自身或自己的子栏目(会成环)。</p>
|
||||
</FormItem>
|
||||
<FormItem label="栏目名称" prop="name">
|
||||
<Input ref="refNodeNameInput" v-model="form.name" :maxlength="64" placeholder="栏目名称" />
|
||||
|
||||
Reference in New Issue
Block a user