周末修订
build / build (push) Successful in 4m47s

This commit is contained in:
chenxing
2026-09-15 23:41:11 +08:00
parent a05b0ded55
commit 6f1f666479
14 changed files with 45 additions and 1576 deletions
+37 -10
View File
@@ -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="栏目名称" />