在网上找到了一篇文章https://www.jb51.net/javascript/302754q75.htm (可以先看下这个文章,确实给出了很不错的思路)拿到右侧面板id值就能知道对应选项的id值,再触发li标签的孩子标签中的checkbox的点击事件就可以了。但是忽略了一个事情,就是三级的时候面板id都是一样的所以根据索引根本找不到对应的dom。只有二级的时候是有效的看下图对比


都是同一个一级下面 面板id虽然不一样,但是之前的9279你通过代码获取到后 这个dom是没有了换成了新的8964所以开头那篇文章不能作为最终的解决方法。
最后我准备获取不到dom的时候就去修改值下面是代码
<el-cascader ref="prouductSysRef" :key="cascaderKey" v-model="formData.productSysteminfo" :options="productOptions" :props="productSystemProps" @change="handleProductChange" > <template slot-scope="{ node, data }"> <el-popover placement="top" trigger="hover" :open-delay="1000" > <div class="popover-content">{{ data.atlasName }}</div> <div slot="reference" class="cascader-node-label"> {{ data.atlasName }} </div> </el-popover> </template> </el-cascader> data(){ return { cascaderKey:0, productOptions: [], productSystemProps: { value: 'atlasId', label: 'atlasName', children: 'children', lazy: false, multiple: true, checkStrictly: true, leaf: 'leaf' }, } } // 同一父级下只选一个子级 handleProductChange(value) { const selectedPaths = this.formData.productSysteminfo || []; // 单班型模式:只能选一个,选中新的取消之前的 if (this.singleClassTypeInfo) { if (value && value.length > 1) { const prev = this.prevValue || []; const added = value.find(v => !prev.some(p => JSON.stringify(p) === JSON.stringify(v))); this.prevValue = value; if (added) { this.formData.productSysteminfo = [added]; } } else { this.prevValue = [selectedPaths[selectedPaths.length - 1]]; } return; } // 多班型模式:每个一级节点下只能选一个,选中新的通过 JS 点击取消同一一级下之前的 // 如果是取消勾选触发的二次 change,直接更新 prevValue 并返回 if (this.isCancellingItem) { this.isCancellingItem = false; this.prevValue = [...value.filter(p => p.length > 1)]; return; } // 过滤掉一级节点的选中项 if (!value || !value.length) { this.prevValue = []; return; } const validValue = value.filter(p => p.length > 1); const validPrev = (this.prevValue || []).filter(p => p.length > 1); // 按一级分组 const groupByRoot = arr => { const groups = {}; for (const path of arr) { const rootId = path[0]; if (!groups[rootId]) groups[rootId] = []; groups[rootId].push(path); } return groups; }; const currentGroups = groupByRoot(validValue); const prevGroups = groupByRoot(validPrev); console.log(currentGroups, 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'); console.log(prevGroups, 'pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp'); // 遍历当前每个一级,找新增项和需要取消的项 for (const rootId in currentGroups) { const currentPaths = currentGroups[rootId]; const prevPaths = prevGroups[rootId] || []; // 找出新增的项 const added = currentPaths.find(v => !prevPaths.some(p => this.isSamePath(p, v))); console.log(added, '111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111', prevPaths); if (added && prevPaths.length > 0) { // 有新增项,且之前该一级下有选中项,需要取消之前的 const toCancel = prevPaths[0]; // 通过 JS 点击取消勾选 this.cancelCascaderItem(toCancel); // 只处理第一个有变化的,因为取消后会再次触发 change return; } } // 没有需要取消的,直接更新 prevValue this.prevValue = [...validValue]; }, // 通过 JS 点击取消级联选择器中的勾选 cancelCascaderItem(path) { this.$nextTick(() => { try { // 获取要取消的项的 atlasId(路径最后一项) const targetId = path[path.length - 1]; // 直接通过 atlasId 查找对应的 DOM 元素 const labelElement = document.getElementById(targetId); if (labelElement) { // 找到了,向上查找最近的 li,然后找 el-checkbox const liElement = labelElement.closest('li'); if (liElement) { const checkbox = liElement.querySelector('.el-checkbox'); if (checkbox) { // 设置标志位,跳过取消勾选触发的二次 change 处理 this.isCancellingItem = true; checkbox.click(); return; } } } // 找不到 DOM 元素,直接修改值 this.cancelByValue(path); } catch (e) { console.error('取消级联选择器勾选失败:', e); this.cancelByValue(path); } }); }, // 通过直接修改值来取消勾选(当 DOM 操作不可用时) cancelByValue(path) { const currentValue = this.formData.productSysteminfo || []; const newValue = currentValue.filter(p => !this.isSamePath(p, path)); this.prevValue = [...newValue.filter(p => p.length > 1)]; this.formData.productSysteminfo = newValue; // 强制级联选择器重新渲染 // ++this.cascaderKey; }, isSamePath(a, b) { return JSON.stringify(a) === JSON.stringify(b); } 代码层面确实可以准确的每级只选一个。但是还是有个致命bug,级联面板中右侧的部分(即子节点部分),会自动刷新跳转到第一个父级下的子级面板,就是赋值完自动刷新到第一个赋值的节点下面。
因为js取消checkbox就能避免这个问题,但是三级js就解决不了了。无解只能自己再手搓代码改变了。所以只用联级面板自己写el-tag回显内容,可以尝试一下.
然后我自己写个组件,自己调用联级面板el-cascader-panel不用el-cascader。发现确实通过input name属性可以完美做到每个一级节点下只能选择一个。
<template>
<div v-click-outside="closePanel" class="custom-cascader-wrap">
<!-- 触发器:显示已选标签 -->
<div class="trigger" :class="{ 'is-focus': panelVisible }" @click="togglePanel">
<template v-if="selectedItems.length">
<el-tag
v-for="item in selectedItems"
:key="item.key"
closable
size="small"
@close.stop="removeTag(item)"
>
{{ item.label }}
</el-tag>
</template>
<span v-else class="placeholder">请选择产品体系</span>
<i class="el-icon-arrow-down trigger-arrow" :class="{ 'is-reverse': panelVisible }" />
</div>
<!-- 级联面板 -->
<transition name="el-zoom-in-top">
<el-cascader-panel
v-show="panelVisible"
ref="cascaderPanel"
v-model="innerValue"
:options="productOptions"
:props="panelProps"
class="cascader-panel-dropdown"
>
<!-- 自定义节点内容 -->
<template slot-scope="{ node, data }">
<!-- 叶子节点:一个 radio-group 里只有一个 radio,但分组由外部 radioSelected 对象控制 -->
<input
:id="data.atlasId"
type="radio"
:name="getRadioName(node)"
:value="data.atlasId"
:checked="isRadioSelected(node, data)"
@click.stop="radioClick(node, data)"
>
{{ data.atlasName }}
</template>
</el-cascader-panel>
</transition>
</div>
</template>
<script>
export default {
name: 'cascader-panel',
directives: {
clickOutside: {
bind(el, binding) {
el._clickOutside = e => {
if (!el.contains(e.target)) {
binding.value();
}
};
document.addEventListener('click', el._clickOutside);
},
unbind(el) {
document.removeEventListener('click', el._clickOutside);
}
}
},
props: {
value: {
type: Array,
default: () => []
},
productOptions: {
type: Array,
default: () => []
},
singleClassTypeInfo: {
type: Object,
default: null
},
panelProps: {
type: Object,
default: ()=>({})
}
},
data() {
return {
rootId: 'sing',
panelVisible: false,
innerValue: [],
// 存储每个一级节点下的选中路径 { rootId: pathArray }
selectionsByRoot: {}
};
},
computed: {
// 已选中的项目,用于显示标签
selectedItems() {
const items = [];
for (const rootId in this.selectionsByRoot) {
const path = this.selectionsByRoot[rootId];
if (path && path.length > 0) {
const label = this.getPathLabel(path);
items.push({
key: JSON.stringify(path),
label,
path,
rootId
});
}
}
return items;
}
},
watch: {
value: {
handler(val) {
this.initSelections(val);
},
immediate: true,
deep: true
},
productOptions: {
handler() {
this.initSelections(this.value);
},
deep: true
}
},
methods: {
togglePanel() {
this.panelVisible = !this.panelVisible;
if (this.panelVisible) {
this.initSelections(this.value);
}
},
closePanel() {
this.panelVisible = false;
},
// 判断是否是一级节点
isFirstLevel(node) {
return node.level === 1;
},
// 判断节点是否被选中
isSelected(data) {
const { atlasId } = data;
for (const rootId in this.selectionsByRoot) {
const path = this.selectionsByRoot[rootId];
if (path && path.includes(atlasId)) {
return true;
}
}
return false;
},
// 处理节点点击
handleNodeClick(node, data) {
if (data.disabled) return;
// 一级节点不可选,只能展开
if (this.isFirstLevel(node)) {
return;
}
// 获取完整路径
const { path } = node;
// 获取一级节点 ID
const rootId = path[0];
// 单班型模式:只能选一个
if (this.singleClassTypeInfo) {
this.selectionsByRoot = {
single: path
};
} else {
// 多班型模式:每个一级节点下只能选一个
// 检查是否已经选中了这个路径
const currentPath = this.selectionsByRoot[rootId];
if (currentPath && JSON.stringify(currentPath) === JSON.stringify(path)) {
// 取消选中
this.$delete(this.selectionsByRoot, rootId);
} else {
// 选中新路径
this.$set(this.selectionsByRoot, rootId, path);
}
}
this.emitChange();
},
// 移除标签
removeTag(item) {
const { rootId } = item;
this.$delete(this.selectionsByRoot, rootId);
this.emitChange();
},
// 发送变更事件
emitChange() {
const paths = [];
for (const rootId in this.selectionsByRoot) {
const path = this.selectionsByRoot[rootId];
if (path && path.length > 0) {
paths.push(path);
}
}
this.$emit('input', paths);
this.$emit('change', paths);
},
// 初始化选中状态
initSelections(val) {
if (!val || val.length === 0) {
this.selectionsByRoot = {};
this.innerValue = [];
return;
}
const selections = {};
// 单班型模式
if (this.singleClassTypeInfo) {
if (val.length > 0) {
selections.single = val[val.length - 1];
}
} else {
// 多班型模式:按一级节点分组
val.forEach(path => {
if (path && path.length > 0) {
const rootId = path[0];
selections[rootId] = path;
}
});
}
this.selectionsByRoot = selections;
this.innerValue = [...val];
},
// 获取路径的完整标签
getPathLabel(path) {
if (!path || path.length === 0) return '';
const labels = [];
let nodes = this.productOptions;
// 单班型模式:productOptions 没有 disabled 一级
if (this.singleClassTypeInfo) {
for (const atlasId of path) {
const node = this.findNodeById(nodes, atlasId);
if (node) {
labels.push(node.atlasName);
nodes = node.children || [];
}
}
} else {
// 多班型模式:path[0] 是一级节点
for (let i = 0; i < path.length; i++) {
const atlasId = path[i];
const node = this.findNodeById(nodes, atlasId);
if (node) {
labels.push(node.atlasName);
nodes = node.children || [];
}
}
}
return labels.join(' / ');
},
// 根据 ID 查找节点
findNodeById(nodes, id) {
for (const node of nodes) {
if (node.atlasId === id) {
return node;
}
if (node.children && node.children.length > 0) {
const found = this.findNodeById(node.children, id);
if (found) return found;
}
}
return null;
},
// 获取 radio 的 name(按一级节点分组)
getRadioName(node) {
// 单班型模式:所有节点共用同一个 name
if (this.singleClassTypeInfo) {
return this.rootId;
}
// 多班型模式:按一级节点分组
const {path} = node;
return path && path.length > 0 ? String(path[0]) : 'default';
},
// 判断 radio 是否选中
isRadioSelected(node, data) {
const {path} = node;
if (!path || path.length === 0) return false;
// 单班型模式
if (this.singleClassTypeInfo) {
const selectedPath = this.selectionsByRoot.single;
if (!selectedPath) return false;
// 比较最后一级(叶子节点)
return selectedPath[selectedPath.length - 1] === data.atlasId;
}
// 多班型模式:按一级节点分组
const rootId = path[0];
const selectedPath = this.selectionsByRoot[rootId];
if (!selectedPath) return false;
// 比较最后一级(叶子节点)
return selectedPath[selectedPath.length - 1] === data.atlasId;
},
// 处理 radio 点击
radioClick(node, data) {
if (data.disabled) return;
const {path} = node;
if (!path || path.length === 0) return;
// 单班型模式:只能选一个
if (this.singleClassTypeInfo) {
// 如果点击的是已选中的,取消选中
const currentPath = this.selectionsByRoot.single;
if (currentPath && currentPath[currentPath.length - 1] === data.atlasId) {
this.$delete(this.selectionsByRoot, 'single');
this.innerValue = [];
} else {
this.$set(this.selectionsByRoot, 'single', [...path]);
this.innerValue = [[...path]];
}
} else {
// 多班型模式:每个一级节点下只能选一个
const rootId = path[0];
const currentPath = this.selectionsByRoot[rootId];
// 如果点击的是已选中的,取消选中
if (currentPath && currentPath[currentPath.length - 1] === data.atlasId) {
this.$delete(this.selectionsByRoot, rootId);
} else {
this.$set(this.selectionsByRoot, rootId, [...path]);
}
// 更新 innerValue 用于 el-cascader-panel 的选中状态
this.updateInnerValue();
}
this.emitChange();
},
// 更新 innerValue
updateInnerValue() {
const paths = [];
for (const rootId in this.selectionsByRoot) {
const path = this.selectionsByRoot[rootId];
if (path && path.length > 0) {
paths.push([...path]);
}
}
this.innerValue = paths;
}
}
};
</script>
<style lang="less" scoped>
.custom-cascader-wrap {
position: relative;
width: 100%;
.trigger {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 30px 0 10px;
min-height: 36px;
cursor: pointer;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
background: #fff;
transition: border-color 0.2s;
&:hover {
border-color: #c0c4cc;
}
&.is-focus {
border-color: #409eff;
}
.trigger-arrow {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
transition: transform 0.3s;
color: #c0c4cc;
&.is-reverse {
transform: translateY(-50%) rotate(180deg);
}
}
.placeholder {
color: #c0c4cc;
font-size: 14px;
}
}
}
.cascader-panel-dropdown {
position: absolute;
top: 100%;
left: 0;
z-index: 2001;
margin-top: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
background: #fff;
border: 1px solid #e4e7ed;
// 隐藏 el-cascader-panel 默认的 radio/checkbox
// ::v-deep .el-cascader-node__prefix,
::v-deep .el-radio{
margin: 0;
}
::v-deep .el-checkbox {
display: none;
}
// 让自定义节点占满整个行
::v-deep .el-cascader-node__label {
padding: 0;
}
}
.custom-node {
// display: inline-flex;
// align-items: center;
// padding: 0 10px;
// height: 34px;
cursor: pointer;
// &.is-disabled {
// cursor: not-allowed;
// opacity: 0.5;
// }
// &:hover:not(.is-disabled) {
// background: #f5f7fa;
// }
}
// .node-label {
// margin-left: 8px;
// }
// 自定义 radio 样式
// .custom-radio {
// position: relative;
// display: inline-block;
// width: 14px;
// height: 14px;
// cursor: pointer;
// &.is-disabled {
// cursor: not-allowed;
// }
// .custom-radio__inner {
// position: absolute;
// top: 0;
// left: 0;
// width: 14px;
// height: 14px;
// border: 1px solid #dcdfe6;
// border-radius: 50%;
// background: #fff;
// box-sizing: border-box;
// &::after {
// content: '';
// position: absolute;
// top: 50%;
// left: 50%;
// width: 6px;
// height: 6px;
// border-radius: 50%;
// background: transparent;
// transform: translate(-50%, -50%);
// transition: background 0.2s;
// }
// }
// &.is-checked .custom-radio__inner {
// border-color: #409eff;
// &::after {
// background: #409eff;
// }
// }
// &.is-disabled .custom-radio__inner {
// background: #f5f7fa;
// border-color: #e4e7ed;
// }
// }
</style>
父组件调用
<cascaderPanel
v-if="formData.productSysteminfo"
v-model="formData.productSysteminfo"
:productOptions="productOptions"
:panelProps="productSystemProps"
:singleClassTypeInfo="singleClassTypeInfo"
@change="handleProductChange"
/>
// 单班型模式下的班型信息(用于取值时补充一级节点信息)
singleClassTypeInfo: null,
最后发现联级面板el-cascader-panel也是个坑。只要是选择三级节点,数据更新后还是默认回到第一个节点,又会自动刷新跳转到第一个父级下的子级面板。服了 真的要手搓一个联级面板吗
最后网上找了个项目https://github.com/Charming2015/el-cascader-multi# 这个可以copy到项目 按需求自己实现就行了这个很不错
最后还是同事做出来了,比较高级的做法还是同事厉害自己得多学习了
// 同一父级下只选一个子级 handleProductChange(values) { // 之前选中的路径 const oldSelectedPaths = this.formData.productSysteminfo || []; // 新选中的路径 const selectedPaths = values || []; // 删除 const oldKeys = new Set(oldSelectedPaths.map(v => JSON.stringify(v))); if (oldSelectedPaths.length >= selectedPaths.length) { this.formData.productSysteminfo = selectedPaths; return; } // 多模式 const newValues = selectedPaths.filter(v => !oldKeys.has(JSON.stringify(v))); const changeKey = newValues[0][0]; oldSelectedPaths.forEach(v => { if(v[0] !== changeKey) { newValues.push(v); } }); this.formData.productSysteminfo = newValues; }, <el-cascader ref="prouductSysRef" :key="cascaderKey" popper-class="newTextbookDialog_cascader" :value="formData.productSysteminfo" :options="productOptions" :props="productSystemProps" @change="handleProductChange" > <template slot-scope="{ node, data }"> <el-popover placement="top" trigger="hover" :open-delay="1000" > <div class="popover-content">{{ data.atlasName }}</div> <div slot="reference" class="cascader-node-label"> {{ data.atlasName }} </div> </el-popover> </template> </el-cascader>
把v-model改成value就能手动去更新不去触发双向绑定 这个思路是我一直考虑不到的。把之前的其他调用
handleProductChange都不用了。就靠change事件来触发。至此这个需求就完事了。
右侧赞助一下 代码改变世界一块二块也是爱
浙公网安备 33010602011771号