diff --git a/components/cascader-panel/cascaderPanel.vue b/components/cascader-panel/cascaderPanel.vue
index dfd23f8d..b070c0e2 100644
--- a/components/cascader-panel/cascaderPanel.vue
+++ b/components/cascader-panel/cascaderPanel.vue
@@ -19,6 +19,7 @@ import {
} from './const';
import usePanel from './usePanel';
import CascaderMenu from './menu';
+import PROPS from './props';
const prefixCls = getPrefixCls('cascader-panel');
@@ -28,21 +29,7 @@ export default defineComponent({
CascaderMenu,
},
props: {
- currentValue: [Number, String, Array, Object],
- options: {
- type: Array,
- default: () => [],
- },
- multiple: Boolean,
- nodeConfig: {
- type: Object,
- default: () => {},
- },
- renderLabel: Function,
- handleUpdateSelectedNodes: {
- type: Function,
- required: true,
- },
+ ...PROPS,
},
emits: ['expandChange', 'checkChange', 'close'],
setup(props, { emit, slots }) {
diff --git a/components/cascader-panel/const.js b/components/cascader-panel/const.js
index 1f563334..20f029b3 100644
--- a/components/cascader-panel/const.js
+++ b/components/cascader-panel/const.js
@@ -23,3 +23,9 @@ export const EVENT_CODE = {
DOWN: 'ArrowDown', // 40
ESC: 'Escape',
};
+
+export const CHECK_STRATEGY = {
+ ALL: 'all',
+ PARENT: 'parent',
+ CHILD: 'child',
+};
diff --git a/components/cascader-panel/nodeContent.jsx b/components/cascader-panel/nodeContent.jsx
index 3238917d..a05fa5f1 100644
--- a/components/cascader-panel/nodeContent.jsx
+++ b/components/cascader-panel/nodeContent.jsx
@@ -1,4 +1,4 @@
-import { defineComponent, h, inject } from 'vue';
+import { computed, defineComponent, inject } from 'vue';
import getPrefixCls from '../_util/getPrefixCls';
import { CASCADER_PANEL_INJECTION_KEY } from './const';
@@ -18,16 +18,15 @@ export default defineComponent({
const { data, label } = node;
const { renderLabelFn } = panel;
- return () =>
- h(
- 'span',
- {
- class: {
- [prefixCls]: true,
- 'is-multiple': panel.multiple,
- },
- },
- renderLabelFn ? renderLabelFn({ node, data }) : label,
- );
+ const classes = computed(() => ({
+ [prefixCls]: true,
+ 'is-multiple': panel.multiple,
+ }));
+
+ return () => (
+
+ {renderLabelFn ? renderLabelFn({ node, data }) : label}
+
+ );
},
});
diff --git a/components/cascader-panel/props.js b/components/cascader-panel/props.js
new file mode 100644
index 00000000..6c0c6c6f
--- /dev/null
+++ b/components/cascader-panel/props.js
@@ -0,0 +1,33 @@
+import { CHECK_STRATEGY } from './const';
+
+const PROPS = {
+ currentValue: [Number, String, Array, Object],
+ options: {
+ type: Array,
+ default: () => [],
+ },
+ multiple: Boolean,
+ nodeConfig: {
+ type: Object,
+ default: () => {},
+ },
+ renderLabel: Function,
+ handleUpdateSelectedNodes: Function,
+ showAllLevels: {
+ type: Boolean,
+ default: true,
+ },
+ separator: {
+ type: String,
+ default: ' / ',
+ },
+ checkStrictly: {
+ type: String,
+ default: CHECK_STRATEGY.CHILD,
+ validator(value) {
+ return Object.values(CHECK_STRATEGY).includes(value);
+ },
+ },
+};
+
+export default PROPS;
diff --git a/components/cascader-panel/useNode.js b/components/cascader-panel/useNode.js
index 60001b39..76ee3ceb 100644
--- a/components/cascader-panel/useNode.js
+++ b/components/cascader-panel/useNode.js
@@ -51,7 +51,7 @@ function setNodeElem(node, elem) {
node.elem = elem;
}
-function useSelectedNodes(config, props, nodes) {
+function useSelectedNodes(config, props, allNodes) {
const selectedNodes = computed(() => {
const { emitPath } = config.value;
const { currentValue, multiple } = props;
@@ -66,7 +66,7 @@ function useSelectedNodes(config, props, nodes) {
currentValue,
);
- const node = getNodeByValue(nodes.value, nodeValue);
+ const node = getNodeByValue(allNodes.value, nodeValue);
if (node) {
currentSelectedNodes.push(node);
@@ -79,10 +79,9 @@ function useSelectedNodes(config, props, nodes) {
emitPath,
currentValue,
);
- nodeValues.forEach((nodeValue) => {
- const node = getNodeByValue(nodes.value, nodeValue);
- if (node) {
- currentSelectedNodes.push(node);
+ allNodes.value.forEach((item) => {
+ if (nodeValues.includes(item.value)) {
+ currentSelectedNodes.push(item);
}
});
}
@@ -101,7 +100,7 @@ export default (config, props) => {
props,
);
- const { selectedNodes } = useSelectedNodes(config, props, nodes);
+ const { selectedNodes } = useSelectedNodes(config, props, allNodes);
return {
nodes,
diff --git a/components/cascader-panel/usePanel.js b/components/cascader-panel/usePanel.js
index 3ffbbbd4..2155e089 100644
--- a/components/cascader-panel/usePanel.js
+++ b/components/cascader-panel/usePanel.js
@@ -1,9 +1,10 @@
import { ref, watch } from 'vue';
import { flatNodes } from '../_util/utils';
-import { EVENT_CODE, EXPAND_TRIGGER } from './const';
+import { CHECK_STRATEGY, EVENT_CODE, EXPAND_TRIGGER } from './const';
import useNode from './useNode';
import {
updateParentNodesCheckState,
+ updateChildNodesCheckState,
getValueByOption,
getNodeSibling,
focusNodeElem,
@@ -11,6 +12,7 @@ import {
getMenuIndexByElem,
checkNodeElem,
generateId,
+ getCheckNodesByLeafCheckNodes,
} from './utils';
function useUpdateNodes(props, selectedNodes, allNodes) {
@@ -25,15 +27,22 @@ function useUpdateNodes(props, selectedNodes, allNodes) {
const doUpdateNodes = () => {
clearCheckedNodes();
- const { multiple, handleUpdateSelectedNodes } = props;
+ const { multiple, handleUpdateSelectedNodes, checkStrictly } = props;
selectedNodes.value.forEach((node) => {
node.checked = true;
});
+ /**
+ * 多选情况,更新节点选中状态
+ * 1. 根据选中节点,更新父节点的中间状态和选中状态
+ * 1. 若为 checkStrictly = parent 情况,则需要更新子节点的选中状态
+ */
if (multiple) {
- // 更新中间状态
updateParentNodesCheckState(selectedNodes.value);
+ if (checkStrictly === CHECK_STRATEGY.PARENT) {
+ updateChildNodesCheckState(selectedNodes.value);
+ }
}
handleUpdateSelectedNodes(selectedNodes.value);
@@ -92,7 +101,14 @@ function useExpandNode(menus, emit, updateMenus) {
};
}
-function useCheckChange(config, props, emit, selectedNodes, leafNodes) {
+function useCheckChange(
+ config,
+ props,
+ emit,
+ selectedNodes,
+ leafNodes,
+ allNodes,
+) {
const handleCheckChange = (node, checked) => {
if (node.checked === checked) return;
const { multiple } = props;
@@ -104,33 +120,58 @@ function useCheckChange(config, props, emit, selectedNodes, leafNodes) {
} else {
/**
* 多选
- * 1. 解析得到节点值的列表(目前为最后一级节点的值的列表)
- * 2. 解析得到当前节点下所有子孙节点的值的列表
- * 3. 若为选中情况,则遍历子孙节点值,判断当前值是否存在,若不存在,则插入
- * 4. 若为取消选中情况,则遍历子孙节点值,判断当前值是否存在,若存在,则删除
+ * 解析得到叶子节点列表
+ * 1. 解析得到当前节点下所有叶子节点的值的列表
+ * 2. 若为选中情况,则遍历子孙节点值,判断当前值是否存在,若不存在,则插入
+ * 3. 若为取消选中情况,则遍历子孙节点值,判断当前值是否存在,若存在,则删除
*/
- const nodeValues = selectedNodes.value.map((item) => item.value);
-
+ // 因为 selectedNodes 可能父子节点都有的情况,所以需要做下去重处理
+ const leafNodeValues = flatNodes(selectedNodes.value, true).reduce(
+ (prev, cur) =>
+ prev.includes(cur.value) ? prev : [...prev, cur.value],
+ [],
+ );
// 获取当前节点的子孙节点信息
const checkNodes = flatNodes([node], true);
checkNodes.forEach((item) => {
if (checked) {
- if (!nodeValues.includes(item.value)) {
- nodeValues.push(item.value);
+ if (!leafNodeValues.includes(item.value)) {
+ leafNodeValues.push(item.value);
}
- } else if (nodeValues.includes(item.value)) {
- nodeValues.splice(nodeValues.indexOf(item.value), 1);
+ } else if (leafNodeValues.includes(item.value)) {
+ leafNodeValues.splice(
+ leafNodeValues.indexOf(item.value),
+ 1,
+ );
}
});
// 目前值为最后一级节点值,所以仅需过滤子节点即可
- const filterNodes = leafNodes.value.filter((item) =>
- nodeValues.includes(item.value),
+ const sortLeafNodes = leafNodes.value.filter((item) =>
+ leafNodeValues.includes(item.value),
);
- const sortValues = filterNodes.map((item) =>
- getValueByOption(config.value, item),
- );
- emit('checkChange', sortValues);
+
+ let checkValues = [];
+
+ if (props.checkStrictly === CHECK_STRATEGY.CHILD) {
+ const sortLeafValues = sortLeafNodes.map((item) =>
+ getValueByOption(config.value, item),
+ );
+ checkValues = sortLeafValues;
+ } else {
+ const checkParentNodes = getCheckNodesByLeafCheckNodes(
+ sortLeafNodes,
+ allNodes.value,
+ props.checkStrictly,
+ );
+ const checkParentValus = checkParentNodes.map((item) =>
+ getValueByOption(config.value, item),
+ );
+
+ checkValues = checkParentValus;
+ }
+
+ emit('checkChange', checkValues);
}
};
@@ -239,6 +280,7 @@ export default (config, props, emit) => {
emit,
selectedNodes,
leafNodes,
+ allNodes,
);
const { handleKeyDown } = useKeyDown(config, emit, menus);
diff --git a/components/cascader-panel/utils.js b/components/cascader-panel/utils.js
index b19c7f70..7c2b0ae7 100644
--- a/components/cascader-panel/utils.js
+++ b/components/cascader-panel/utils.js
@@ -1,4 +1,5 @@
import getPrefixCls from '../_util/getPrefixCls';
+import { CHECK_STRATEGY } from './const';
import { flatNodes } from '../_util/utils';
/**
@@ -22,7 +23,7 @@ export const calculatePathNodes = (node) => {
/**
* 多选的时候,更新选中节点的父级节点的选中和半选中状态
- * 1. 遍历选中节点,level从下到上更新父节点的选中状态
+ * 1. 遍历选中节点,level 从下到上更新父节点的选中状态
*/
export const updateParentNodesCheckState = (selectedNodes = []) => {
selectedNodes.forEach((node) => {
@@ -44,6 +45,61 @@ export const updateParentNodesCheckState = (selectedNodes = []) => {
});
};
+/**
+ * 多选情况,更新选中节点子级节点的选中状态
+ * 1. 遍历选中节点,获取所有的子孙节点,全部置为选中状态
+ */
+export const updateChildNodesCheckState = (selectedNodes = []) => {
+ selectedNodes.forEach((node) => {
+ const childAndLeafNodes = flatNodes(node.children);
+ childAndLeafNodes.forEach((item) => {
+ item.checked = true;
+ });
+ });
+};
+
+/**
+ * 根据叶子节点列表获取包含关联选中父节点的值列表
+ * 1. 遍历叶子节点,level 从下到上判断父节点是否选中
+ * 2. 若父节点为选中状态(所有子节点值都在值列表中),则值列表中插入父节点
+ * 3. 遍历全部节点列表,顺序返回
+ * 4. 若为 checkStrictly = parent 情况,则若当前节点的父节点值不在值列表中,则插入当前节点
+ */
+export const getCheckNodesByLeafCheckNodes = (
+ checkLeafNodes = [],
+ allNodes = [],
+ checkStrictly,
+) => {
+ const checkNodeValues = [];
+
+ checkLeafNodes.forEach((node) => {
+ checkNodeValues.push(node.value);
+
+ const parentNodes = node.pathNodes.slice(0, node.pathNodes.length - 1);
+ for (let i = parentNodes.length - 1; i >= 0; i--) {
+ const parentNode = parentNodes[i];
+ const parentChecked = parentNode.children.every((child) =>
+ checkNodeValues.includes(child.value),
+ );
+ if (parentChecked) {
+ checkNodeValues.push(parentNode.value);
+ }
+ }
+ });
+
+ return allNodes.filter((item) => {
+ if (!checkNodeValues.includes(item.value)) {
+ return false;
+ }
+ if (checkStrictly === CHECK_STRATEGY.PARENT) {
+ if (item.parent) {
+ return !checkNodeValues.includes(item.parent.value);
+ }
+ }
+ return true;
+ });
+};
+
export const getNode = (data = [], config = {}, parent = null) => {
const node = {};
@@ -79,11 +135,8 @@ export const getNode = (data = [], config = {}, parent = null) => {
return node;
};
-export const getNodeByValue = (nodes, value) => {
- const filterNodes = flatNodes(nodes).filter((node) => node.value === value);
-
- return filterNodes[0] || null;
-};
+export const getNodeByValue = (allNodes, value) =>
+ allNodes.find((node) => node.value === value) || null;
export const getValueByOption = (config, node) =>
config.emitPath ? node.pathValues : node.value;
@@ -93,7 +146,7 @@ export const getNodeValueByCurrentValue = (emitPath, value) => {
let nodeValue = '';
if (emitPath) {
if (Array.isArray(value)) {
- nodeValue = value[value.length - 1];
+ nodeValue = (value.length && value[value.length - 1]) || '';
} else {
console.warn(
'value类型不符预期,emitPath为true的情况下,value应该为数组格式',
@@ -124,10 +177,10 @@ export const getMultiNodeValuesByCurrentValue = (emitPath, currentValue) => {
};
export const getMenuIndexByElem = (el, menus) =>
- menus.findIndex((menu) => menu.find((node) => node.elem === el));
+ menus.findIndex((menu) => menu.nodes.find((node) => node.elem === el));
export const getMenuNodeByElem = (el, menus) => {
const currentMenu = menus[getMenuIndexByElem(el, menus)] || null;
- return currentMenu?.find((node) => node.elem === el) || null;
+ return currentMenu?.nodes.find((node) => node.elem === el) || null;
};
// 获取元素兄弟节点
@@ -137,21 +190,22 @@ export const getNodeSibling = (el, distance = 0, menus) => {
let currentNodeIndex = -1;
const currentMenu = menus[getMenuIndexByElem(el, menus)] || null;
- currentNodeIndex = currentMenu?.findIndex((node) => node.elem === el);
+ currentNodeIndex = currentMenu?.nodes.findIndex((node) => node.elem === el);
siblingNode =
currentNodeIndex > -1
- ? currentMenu?.[currentNodeIndex + distance] || null
+ ? currentMenu?.nodes[currentNodeIndex + distance] || null
: null;
while (siblingNode && siblingNode.isDisabled) {
const currentElem = siblingNode.elem;
- currentNodeIndex = currentMenu.findIndex(
+ currentNodeIndex = currentMenu.nodes.findIndex(
(node) => node.elem === currentElem,
);
siblingNode =
currentNodeIndex > -1
- ? currentMenu[currentNodeIndex + (distance > 0 ? 1 : -1)] ||
- null
+ ? currentMenu.nodes[
+ currentNodeIndex + (distance > 0 ? 1 : -1)
+ ] || null
: null;
}
diff --git a/components/cascader/cascader.vue b/components/cascader/cascader.vue
index 14beb461..6a92cc56 100644
--- a/components/cascader/cascader.vue
+++ b/components/cascader/cascader.vue
@@ -28,6 +28,7 @@
[],
- },
- nodeConfig: {
- type: Object,
- default: () => {},
- },
- placeholder: {
- type: String,
- default: () => '请选择',
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- clearable: {
- type: Boolean,
- default: false,
- },
- multiple: {
- type: Boolean,
- default: false,
- },
- showAllLevels: {
- type: Boolean,
- default: true,
- },
- separator: {
- type: String,
- default: ' / ',
- },
- appendToContainer: {
- type: Boolean,
- default: true,
- },
- getContainer: {
- type: Function,
- },
- collapseTags: {
- type: Boolean,
- default: false,
- },
- collapseTagsLimit: {
- type: Number,
- default: 1,
- },
+ ...SELECT_PROPS,
+ ...CASCADER_PANEL_PROPS,
},
emits: [
UPDATE_MODEL_EVENT,
@@ -133,28 +91,47 @@ export default defineComponent({
emit(CHANGE_EVENT, unref(currentValue));
});
const handleClear = () => {
- const value = props.multiple ? [] : null;
+ const value =
+ props.multiple || props?.nodeConfig?.emitPath ? [] : null;
updateCurrentValue(value);
emit('clear');
};
- // 多选才会有删除事件,所有仅考虑数组情况即可
+ /**
+ * 多选才会有删除事件,所有仅考虑数组情况即可
+ *
+ * 若为 checkStrictly = all 情况,则:
+ * 1. 若删除的为父节点,需要把子节点的值一起删除
+ * 2. 若删除的为子节点,需要把父节点的值一起删除
+ */
const handleRemove = (value) => {
if (props.disabled) return;
const { emitPath } = props.nodeConfig || {};
- const copyValue = cloneDeep(currentValue.value);
+ let copyValue = cloneDeep(currentValue.value);
+ const updateValues = [];
+
if (emitPath) {
- copyValue.splice(
- copyValue.findIndex((item) => item.includes(value)),
- 1,
- );
- } else {
- copyValue.splice(
- copyValue.findIndex((item) => item === value),
- 1,
- );
+ copyValue = copyValue.map((item) => item[item.length - 1]);
}
- updateCurrentValue(copyValue);
+
+ const currentNode = selectedNodes.value.find(
+ (node) => node.value === value,
+ );
+ const removeValues = []
+ .concat(currentNode.pathNodes, flatNodes(currentNode.children))
+ .map((node) => node.value);
+
+ copyValue.forEach((item) => {
+ let itemValue = item;
+ if (emitPath) {
+ itemValue = item[item.length - 1];
+ }
+ if (!removeValues.includes(itemValue)) {
+ updateValues.push(item);
+ }
+ });
+
+ updateCurrentValue(updateValues);
emit('removeTag', value);
};
const handleExpandChange = (value) => {
diff --git a/docs/.vitepress/components/cascader/checkStrictly.vue b/docs/.vitepress/components/cascader/checkStrictly.vue
new file mode 100644
index 00000000..f0d688ec
--- /dev/null
+++ b/docs/.vitepress/components/cascader/checkStrictly.vue
@@ -0,0 +1,243 @@
+
+ 默认(child):
+
+
+ parent:
+
+
+ all:
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/.vitepress/components/cascader/index.md b/docs/.vitepress/components/cascader/index.md
index ec7aa2b6..281f86c3 100644
--- a/docs/.vitepress/components/cascader/index.md
+++ b/docs/.vitepress/components/cascader/index.md
@@ -20,6 +20,10 @@ app.use(FCascader);
--MULTIPLE
+### 关联多选
+
+--CHECKSTRICTLY
+
### 空选项
--EMPTY
@@ -36,21 +40,22 @@ app.use(FCascader);
## Cascader Props
-| 参数 | 说明 | 类型 | 默认值 |
-| -------------------- | ----------------------------------------------- | ------------------- | --------------------- |
-| modelValue / v-model | 选中项绑定值 | - | - |
-| options | 可选项数据源,键名可通过 nodeConfig 属性配置 | Array\ | - |
-| nodeConfig | 菜单选择配置选项,具体见下表 `NodeConfig Props` | object | - |
-| placeholder | 输入框占位文本 | string | `请选择` |
-| disabled | 是否禁用 | boolean | `false` |
-| clearable | 是否支持清空选项 | boolean | `false` |
-| collapseTags | 多选时选中项是否折叠展示 | boolean | `false` |
-| collapseTagsLimit | 多选时选中项超出限制个数后才会折叠 | number | 1 |
-| multiple | 是否多选 | boolean | `false` |
-| showAllLevels | 输入框中是否显示选中值的完整路径 | boolean | `true` |
-| separator | 选项分隔符 | string | `/` |
-| appendToContainer | 弹窗内容是否添加到指定的 DOM 元素 | boolean | `true` |
-| getContainer | 指定下拉选项挂载的 HTML 节点 | () => HTMLElement | `() => document.body` |
+| 参数 | 说明 | 类型 | 默认值 |
+| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | --------------------- |
+| modelValue / v-model | 选中项绑定值 | - | - |
+| options | 可选项数据源,键名可通过 nodeConfig 属性配置 | Array\ | - |
+| nodeConfig | 菜单选择配置选项,具体见下表 `NodeConfig Props` | object | - |
+| placeholder | 输入框占位文本 | string | `请选择` |
+| disabled | 是否禁用 | boolean | `false` |
+| clearable | 是否支持清空选项 | boolean | `false` |
+| collapseTags | 多选时选中项是否折叠展示 | boolean | `false` |
+| collapseTagsLimit | 多选时选中项超出限制个数后才会折叠 | number | 1 |
+| multiple | 是否多选 | boolean | `false` |
+| showAllLevels | 输入框中是否显示选中值的完整路径 | boolean | `true` |
+| separator | 选项分隔符 | string | `/` |
+| appendToContainer | 弹窗内容是否添加到指定的 DOM 元素 | boolean | `true` |
+| getContainer | 指定下拉选项挂载的 HTML 节点 | () => HTMLElement | `() => document.body` |
+| checkStrictly | 设置勾选策略来指定勾选回调返回的值,`all` 表示回调函数值为全部选中节点;`parent` 表示回调函数值为父节点(当父节点下所有子节点都选中时);`child` 表示回调函数值为子节点 | string | `child` |
## Cascader Events