1210 lines
36 KiB
Vue
1210 lines
36 KiB
Vue
<template>
|
||
<view class="da-tree" :style="{ '--theme-color': themeColor }">
|
||
<scroll-view class="da-tree-scroll" :scroll-y="true" :scroll-x="false">
|
||
<view
|
||
v-for="item in datalist"
|
||
:key="item.key"
|
||
class="da-tree-item"
|
||
:class="{ 'is-show': item.show }"
|
||
:style="{ paddingLeft: item.level * indent + 'rpx' }"
|
||
>
|
||
<view
|
||
v-if="item.showArrow && !filterValue"
|
||
class="da-tree-item__icon"
|
||
@click="handleExpandedChange(item)"
|
||
>
|
||
<view
|
||
v-if="loadLoading && item.loading"
|
||
:class="['da-tree-item__icon--arr', 'is-loading']"
|
||
/>
|
||
<view
|
||
v-else
|
||
:class="['da-tree-item__icon--arr', 'is-expand', { 'is-right': !item.expand }]"
|
||
/>
|
||
</view>
|
||
<view v-else class="da-tree-item__icon" />
|
||
<view
|
||
v-if="showCheckbox"
|
||
class="da-tree-item__checkbox"
|
||
:class="[
|
||
`da-tree-item__checkbox--${checkboxPlacement}`,
|
||
{ 'is--disabled': item.disabled },
|
||
]"
|
||
@click="handleCheckChange(item)"
|
||
>
|
||
<view
|
||
v-if="item.checkedStatus === isCheckedStatus"
|
||
class="da-tree-item__checkbox--icon da-tree-checkbox-checked"
|
||
/>
|
||
<view
|
||
v-else-if="item.checkedStatus === halfCheckedStatus"
|
||
class="da-tree-item__checkbox--icon da-tree-checkbox-indeterminate"
|
||
/>
|
||
<view v-else class="da-tree-item__checkbox--icon da-tree-checkbox-outline" />
|
||
</view>
|
||
<view
|
||
v-if="!showCheckbox && showRadioIcon"
|
||
class="da-tree-item__checkbox"
|
||
:class="[
|
||
`da-tree-item__checkbox--${checkboxPlacement}`,
|
||
{ 'is--disabled': item.disabled },
|
||
]"
|
||
@click="handleRadioChange(item)"
|
||
>
|
||
<view
|
||
v-if="item.checkedStatus === isCheckedStatus"
|
||
class="da-tree-item__checkbox--icon da-tree-radio-checked"
|
||
/>
|
||
<view
|
||
v-else-if="item.checkedStatus === halfCheckedStatus"
|
||
class="da-tree-item__checkbox--icon da-tree-radio-indeterminate"
|
||
/>
|
||
<view v-else class="da-tree-item__checkbox--icon da-tree-radio-outline" />
|
||
</view>
|
||
<view
|
||
class="da-tree-item__label"
|
||
:class="'da-tree-item__label--' + item.checkedStatus"
|
||
@click="handleLabelClick(item)"
|
||
>
|
||
{{ item.label }}
|
||
<text v-if="item.append" class="da-tree-item__label--append">{{ item.append }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { defineComponent, ref, unref, watch } from "vue";
|
||
|
||
import {
|
||
unCheckedStatus,
|
||
halfCheckedStatus,
|
||
isCheckedStatus,
|
||
deepClone,
|
||
getAllNodeKeys,
|
||
getAllNodes,
|
||
logError,
|
||
isArray,
|
||
isString,
|
||
isNumber,
|
||
isFunction,
|
||
} from "./utils";
|
||
import basicProps from "./props";
|
||
|
||
export default defineComponent({
|
||
name: "DaTree",
|
||
props: basicProps,
|
||
emits: ["change", "expand"],
|
||
setup(props, { emit }) {
|
||
/** 原始的树数据 */
|
||
const dataRef = ref([]);
|
||
/** 处理后的一维树项数据 */
|
||
const datalist = ref([]);
|
||
/** 处理后的以key为键值的树项数据 */
|
||
const datamap = ref({});
|
||
/** 默认的展开数据 */
|
||
const expandedKeys = ref([]);
|
||
/** 默认的已选数据 */
|
||
const checkedKeys = ref(null);
|
||
/** 加载状态 */
|
||
const loadLoading = ref(false);
|
||
let fieldMap = {
|
||
value: "value",
|
||
label: "label",
|
||
children: "children",
|
||
disabled: "disabled",
|
||
append: "append",
|
||
leaf: "leaf",
|
||
sort: "sort",
|
||
};
|
||
|
||
/**
|
||
* 初始化数据结构
|
||
*/
|
||
function initData() {
|
||
fieldMap = {
|
||
value: props.field?.key || props.field?.value || props.valueField || "value",
|
||
label: props.field?.label || props.labelField || "label",
|
||
children: props.field?.children || props.childrenField || "children",
|
||
disabled: props.field?.disabled || props.disabledField || "disabled",
|
||
append: props.field?.append || props.appendField || "append",
|
||
leaf: props.field?.leaf || props.leafField || "leaf",
|
||
sort: props.field?.sort || props.sortField || "sort",
|
||
};
|
||
|
||
const data = deepClone(dataRef.value);
|
||
datalist.value = [];
|
||
datamap.value = {};
|
||
|
||
// clean tree
|
||
handleTreeData(data);
|
||
// flat tree
|
||
datalist.value = checkInitData(datalist.value);
|
||
// console.log('init datalist', datalist.value)
|
||
// console.log('init datamap', datamap.value)
|
||
}
|
||
|
||
/**
|
||
* 转换为节点数据
|
||
* @param data
|
||
* @param parent
|
||
* @param level
|
||
*/
|
||
function handleTreeData(data = [], parent = null, level = 0, insertIndex = -1) {
|
||
return data.reduce((prev, cur, index) => {
|
||
const key = cur[fieldMap.value];
|
||
const children = cur[fieldMap.children] || null;
|
||
const newItem = createNewItem(cur, index, parent, level);
|
||
if (insertIndex > -1) {
|
||
// 插入子项尾部
|
||
const index = (parent.childrenKeys?.length || 0) + insertIndex + 1;
|
||
if (!parent?.childrenKeys?.includes(key)) {
|
||
datamap.value[key] = newItem;
|
||
datalist.value.splice(index, 0, newItem);
|
||
parent.children.push(newItem);
|
||
if (newItem.parentKeys?.length) {
|
||
newItem.parentKeys.forEach((k) => {
|
||
datamap.value[k].childrenKeys = [...datamap.value[k].childrenKeys, newItem.key];
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
datamap.value[key] = newItem;
|
||
datalist.value.push(newItem);
|
||
}
|
||
|
||
const hasChildren = children && children.length > 0;
|
||
if (hasChildren) {
|
||
const childrenData = handleTreeData(children, newItem, level + 1);
|
||
// childrenData.sort((a, b) => a.sort - b.sort)
|
||
newItem.children = childrenData;
|
||
const childrenKeys = childrenData.reduce((p, k) => {
|
||
const keys = k.childrenKeys;
|
||
p.push(...keys, k.key);
|
||
return p;
|
||
}, []);
|
||
newItem.childrenKeys = childrenKeys;
|
||
}
|
||
prev.push(newItem);
|
||
return prev;
|
||
}, []);
|
||
}
|
||
|
||
/**
|
||
* 创建节点
|
||
* @param item
|
||
* @param index
|
||
* @param parent
|
||
* @param level
|
||
*/
|
||
function createNewItem(item, index, parent, level) {
|
||
const key = item[fieldMap.value];
|
||
const label = item[fieldMap.label];
|
||
const sort = item[fieldMap.sort] || 0;
|
||
const children = item[fieldMap.children] || null;
|
||
const append = item[fieldMap.append] || null;
|
||
let disabled = item[fieldMap.disabled] || false;
|
||
// 优先继承父级禁用属性
|
||
disabled = parent?.disabled || disabled;
|
||
let isLeaf = isFunction(props.isLeafFn) ? props.isLeafFn(item) : item[fieldMap.leaf] || false;
|
||
// const hasChildren = children && children.length > 0
|
||
const isEmptyChildren = children && children.length === 0;
|
||
let showArrow = true;
|
||
// let isLeaf = !hasChildren
|
||
let expand = props.defaultExpandAll || false;
|
||
// 是否异步加载模式
|
||
const isLoadMode = props.loadMode && isFunction(props.loadApi);
|
||
|
||
if (!children) {
|
||
expand = false;
|
||
if (isLoadMode) {
|
||
showArrow = true;
|
||
} else {
|
||
isLeaf = true;
|
||
showArrow = false;
|
||
}
|
||
}
|
||
|
||
if (isEmptyChildren) {
|
||
expand = false;
|
||
if (isLoadMode) {
|
||
showArrow = true;
|
||
} else {
|
||
isLeaf = true;
|
||
showArrow = false;
|
||
}
|
||
}
|
||
|
||
if (isLeaf) {
|
||
showArrow = false;
|
||
expand = false;
|
||
} else {
|
||
showArrow = true;
|
||
}
|
||
|
||
// onlyRadioLeaf 单选只能选择末级节点
|
||
if (!props.showCheckbox) {
|
||
if (props.onlyRadioLeaf) {
|
||
if (!isLeaf) {
|
||
disabled = true;
|
||
} else {
|
||
// 仍旧继承父类原始禁用状态
|
||
disabled = parent?.originItem?.disabled || false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (disabled) {
|
||
if (isLeaf || !children || isEmptyChildren) {
|
||
expand = false;
|
||
showArrow = false;
|
||
}
|
||
}
|
||
|
||
const parentKey = parent ? parent.key : null;
|
||
const show = props.defaultExpandAll || level === 0;
|
||
|
||
const newItem = {
|
||
key,
|
||
parentKey,
|
||
label,
|
||
append,
|
||
isLeaf,
|
||
showArrow,
|
||
level,
|
||
expand,
|
||
show,
|
||
sort,
|
||
disabled,
|
||
loaded: false,
|
||
loading: false,
|
||
indexs: [index],
|
||
checkedStatus: unCheckedStatus,
|
||
parentKeys: [],
|
||
childrenKeys: [],
|
||
children: [],
|
||
originItem: item,
|
||
};
|
||
|
||
if (parent) {
|
||
newItem.parentKeys = [parent.key, ...parent.parentKeys];
|
||
newItem.indexs = [...parent.indexs, index];
|
||
}
|
||
|
||
return newItem;
|
||
}
|
||
|
||
/**
|
||
* 处理初始化内容
|
||
* @param list
|
||
*/
|
||
function checkInitData(list) {
|
||
let checkedKeyList = null;
|
||
let expandedKeyList = [];
|
||
if (props.showCheckbox) {
|
||
checkedKeyList = [...new Set(checkedKeys.value || [])];
|
||
expandedKeyList = props.expandChecked
|
||
? [...(checkedKeys.value || []), ...(expandedKeys.value || [])]
|
||
: expandedKeys.value;
|
||
} else {
|
||
checkedKeyList = checkedKeys.value || null;
|
||
expandedKeyList =
|
||
props.expandChecked && checkedKeys.value
|
||
? [checkedKeys.value, ...(expandedKeys.value || [])]
|
||
: expandedKeys.value;
|
||
}
|
||
|
||
handleCheckState(list, checkedKeyList, true);
|
||
|
||
// 处理初始展开
|
||
expandedKeyList = [...new Set(expandedKeyList)];
|
||
if (!props.defaultExpandAll) {
|
||
handleExpandState(list, expandedKeyList, true);
|
||
}
|
||
|
||
list.sort((a, b) => {
|
||
if (a.sort === 0 && b.sort === 0) {
|
||
return 0;
|
||
}
|
||
|
||
if (a.parentKey === b.parentKey) {
|
||
if (a.sort - b.sort > 0) {
|
||
return 1;
|
||
} else {
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
});
|
||
|
||
return list;
|
||
}
|
||
|
||
/**
|
||
* 处理选中
|
||
* @param list
|
||
* @param checkedKeyList
|
||
*/
|
||
function handleCheckState(list, checkedKeyList, checked = true) {
|
||
// 多选
|
||
if (props.showCheckbox) {
|
||
if (checkedKeyList?.length) {
|
||
checkedKeyList.forEach((k) => {
|
||
const item = datamap.value[k];
|
||
if (item) {
|
||
checkTheChecked(item, checked);
|
||
}
|
||
});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
// 单选
|
||
for (let i = 0; i < list.length; i++) {
|
||
const item = list[i];
|
||
if (item.key === checkedKeyList) {
|
||
checkTheRadio(item, checked);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 校验多选节点
|
||
* @param item
|
||
* @param checked
|
||
*/
|
||
function checkTheChecked(item, checked = true) {
|
||
const { childrenKeys, parentKeys, disabled = false } = item;
|
||
if (!props.checkedDisabled && disabled) return;
|
||
|
||
// 当前
|
||
item.checkedStatus = checked ? isCheckedStatus : unCheckedStatus;
|
||
|
||
if (!props.checkStrictly) {
|
||
// 子类
|
||
childrenKeys.forEach((k) => {
|
||
const childrenItem = unref(datamap)[k];
|
||
childrenItem.checkedStatus =
|
||
!props.checkedDisabled && childrenItem.disabled
|
||
? childrenItem.checkedStatus
|
||
: item.checkedStatus;
|
||
});
|
||
|
||
// 父类
|
||
parentKeys.forEach((k) => {
|
||
const parentItem = datamap.value[k];
|
||
parentItem.checkedStatus = getParentCheckedStatus(parentItem);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 校验单选节点
|
||
* @param item
|
||
*/
|
||
function checkTheRadio(item, checked) {
|
||
const { parentKeys, isLeaf, disabled = false } = item;
|
||
if (!props.checkedDisabled && disabled) return;
|
||
|
||
// 限制末节点选中,但当前非末节点
|
||
if (props.onlyRadioLeaf && !isLeaf) {
|
||
logError(`限制了末节点选中,当前[${item.label}]非末节点`);
|
||
return;
|
||
}
|
||
|
||
if (datalist.value?.length) {
|
||
datalist.value.forEach((k) => {
|
||
k.checkedStatus = unCheckedStatus;
|
||
});
|
||
}
|
||
|
||
parentKeys.forEach((k) => {
|
||
const parentItem = datamap.value[k];
|
||
parentItem.checkedStatus = checked ? getParentCheckedStatus(parentItem) : unCheckedStatus;
|
||
});
|
||
|
||
// 当前
|
||
item.checkedStatus = checked ? isCheckedStatus : unCheckedStatus;
|
||
}
|
||
|
||
/**
|
||
* 处理父节点展开
|
||
* @param item
|
||
* @param expand
|
||
*/
|
||
// function handleExpandParentNode(item, expand = true) {
|
||
// if (!expand) return
|
||
|
||
// if (item?.parentKeys?.length) {
|
||
// item.parentKeys.forEach(pk => {
|
||
// if (!datamap.value[pk].expand) {
|
||
// datamap.value[pk].expand = true
|
||
// }
|
||
// })
|
||
// }
|
||
// }
|
||
|
||
/**
|
||
* 处理节点展开
|
||
* @param list
|
||
* @param expandedKeyList
|
||
* @param expand
|
||
*/
|
||
function handleExpandState(list, expandedKeyList, expand = true) {
|
||
// 收起
|
||
if (expand === false) {
|
||
for (let i = 0; i < list.length; i++) {
|
||
const item = list[i];
|
||
if (expandedKeyList?.includes(item.key)) {
|
||
item.expand = false;
|
||
if (item.childrenKeys?.length) {
|
||
item.childrenKeys.forEach((ck) => {
|
||
datamap.value[ck].expand = false;
|
||
datamap.value[ck].show = false;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
// 展开
|
||
for (let i = 0; i < list.length; i++) {
|
||
const item = list[i];
|
||
// 处理展开
|
||
if (expandedKeyList?.includes(item.key)) {
|
||
// 父子
|
||
item.expand = true;
|
||
if (item.children?.length) {
|
||
item.children.forEach((k) => {
|
||
const kItem = unref(datamap)[k.key];
|
||
kItem.show = true;
|
||
});
|
||
}
|
||
|
||
// 族系
|
||
if (item.parentKeys?.length) {
|
||
item.parentKeys.forEach((k) => {
|
||
const kItem = unref(datamap)[k];
|
||
kItem.expand = true;
|
||
if (kItem.children?.length) {
|
||
kItem.children.forEach((k) => {
|
||
const skItem = unref(datamap)[k.key];
|
||
skItem.show = true;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 点击选框
|
||
* @param item
|
||
*/
|
||
function handleCheckChange(item) {
|
||
const { childrenKeys, parentKeys, checkedStatus, isLeaf, disabled = false } = item;
|
||
if (!props.showCheckbox) return;
|
||
if (disabled) return;
|
||
|
||
// 当前
|
||
item.checkedStatus = checkedStatus === isCheckedStatus ? unCheckedStatus : isCheckedStatus;
|
||
|
||
// 子类
|
||
if (!props.checkStrictly) {
|
||
if (props.expandChecked) {
|
||
item.show = true;
|
||
item.expand = childrenKeys?.length > 0 || isLeaf;
|
||
}
|
||
|
||
childrenKeys.forEach((k) => {
|
||
const childrenItem = unref(datamap)[k];
|
||
childrenItem.checkedStatus = childrenItem.disabled
|
||
? childrenItem.checkedStatus
|
||
: item.checkedStatus;
|
||
|
||
if (props.expandChecked) {
|
||
childrenItem.show = true;
|
||
childrenItem.expand = childrenItem?.childrenKeys?.length > 0 || childrenItem.isLeaf;
|
||
}
|
||
});
|
||
} else {
|
||
if (props.expandChecked) {
|
||
logError(
|
||
`多选时,当 checkStrictly 为 true 时,不支持选择自动展开子节点属性(expandChecked)`
|
||
);
|
||
}
|
||
}
|
||
|
||
// 父类
|
||
if (!props.checkStrictly) {
|
||
parentKeys.forEach((k) => {
|
||
const parentItem = datamap.value[k];
|
||
parentItem.checkedStatus = getParentCheckedStatus(parentItem);
|
||
});
|
||
}
|
||
|
||
const hasCheckedKeys = [];
|
||
for (let i = 0; i < datalist.value.length; i++) {
|
||
const k = datalist.value[i];
|
||
if (k.checkedStatus === isCheckedStatus) {
|
||
if ((props.packDisabledkey && k.disabled) || !k.disabled) {
|
||
hasCheckedKeys.push(k.key);
|
||
}
|
||
}
|
||
}
|
||
|
||
checkedKeys.value = [...hasCheckedKeys];
|
||
|
||
emit("change", hasCheckedKeys, item);
|
||
}
|
||
|
||
/**
|
||
* 点击单选
|
||
* @param item
|
||
*/
|
||
function handleRadioChange(item) {
|
||
const { parentKeys, checkedStatus, key, disabled = false, isLeaf } = item;
|
||
if (props.showCheckbox) return;
|
||
if (props.onlyRadioLeaf && !isLeaf) handleExpandedChange(item);
|
||
|
||
if (disabled) return;
|
||
|
||
// 重置所有选择
|
||
if (datalist.value?.length) {
|
||
for (let i = 0; i < datalist.value.length; i++) {
|
||
const k = datalist.value[i];
|
||
k.checkedStatus = unCheckedStatus;
|
||
}
|
||
}
|
||
|
||
parentKeys.forEach((k) => {
|
||
const parentItem = datamap.value[k];
|
||
parentItem.checkedStatus = getParentCheckedStatus(parentItem);
|
||
});
|
||
|
||
// 当前
|
||
item.checkedStatus = checkedStatus === isCheckedStatus ? unCheckedStatus : isCheckedStatus;
|
||
|
||
checkedKeys.value = key;
|
||
emit("change", key, item);
|
||
}
|
||
|
||
/**
|
||
* 点击标签
|
||
*/
|
||
function handleLabelClick(item) {
|
||
if (props.showCheckbox) {
|
||
handleCheckChange(item);
|
||
} else {
|
||
handleRadioChange(item);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 点击展开收起
|
||
* @param item
|
||
*/
|
||
async function handleExpandedChange(item) {
|
||
if (props.filterValue) return;
|
||
|
||
const { expand, loading = false, disabled } = item;
|
||
if (loadLoading.value && loading) return;
|
||
|
||
checkExpandedChange(item);
|
||
|
||
// 异步
|
||
item.expand = !expand;
|
||
|
||
let currentItem = null;
|
||
if (!disabled) {
|
||
if (!props.showCheckbox && props.onlyRadioLeaf && props.loadMode) {
|
||
logError(`单选时,当 onlyRadioLeaf 为 true 时不支持动态数据`);
|
||
} else {
|
||
currentItem = await loadExpandNode(item);
|
||
}
|
||
}
|
||
|
||
emit("expand", !expand, currentItem || item || null);
|
||
}
|
||
|
||
/**
|
||
* 检查展开状态
|
||
* @param item
|
||
*/
|
||
function checkExpandedChange(item) {
|
||
const { expand, childrenKeys, children = null } = item;
|
||
|
||
if (expand) {
|
||
if (childrenKeys?.length) {
|
||
childrenKeys.forEach((k) => {
|
||
if (unref(datamap)[k]) {
|
||
unref(datamap)[k].show = false;
|
||
unref(datamap)[k].expand = false;
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
if (children?.length) {
|
||
const childrenKeys = children.map((k) => k.key);
|
||
childrenKeys.forEach((k) => {
|
||
if (unref(datamap)[k]) {
|
||
unref(datamap)[k].show = true;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载异步数据
|
||
* @param item
|
||
*/
|
||
async function loadExpandNode(item) {
|
||
const { expand, key, loaded, children } = item;
|
||
if (children?.length && !props.alwaysFirstLoad) {
|
||
return item;
|
||
}
|
||
|
||
if (expand && props.loadMode && !loaded) {
|
||
if (isFunction(props.loadApi)) {
|
||
expandedKeys.value.push(key);
|
||
loadLoading.value = true;
|
||
item.loading = true;
|
||
|
||
const currentNode = deepClone(item);
|
||
const apiRes = await props.loadApi(currentNode);
|
||
|
||
// 新增子项
|
||
let newChildren = [...(item.originItem?.children || []), ...(apiRes || [])];
|
||
const newChildrenObj = {};
|
||
newChildren = newChildren.reduce((total, next) => {
|
||
if (newChildrenObj[next[fieldMap.value]]) {
|
||
return total;
|
||
} else {
|
||
newChildrenObj[next[fieldMap.value]] = true;
|
||
total.push(next);
|
||
return total;
|
||
}
|
||
}, []);
|
||
|
||
item.originItem.children = newChildren || null;
|
||
if (apiRes?.length) {
|
||
const insertIndex = datalist.value.findIndex((k) => k.key === item.key);
|
||
handleTreeData(apiRes, item, item.level + 1, insertIndex);
|
||
datalist.value = checkInitData(datalist.value);
|
||
} else {
|
||
// 加载后无数据就移除展开图标
|
||
item.expand = false;
|
||
item.isLeaf = true;
|
||
item.showArrow = false;
|
||
}
|
||
|
||
loadLoading.value = false;
|
||
item.loading = false;
|
||
item.loaded = true;
|
||
}
|
||
} else {
|
||
const eki = expandedKeys.value.findIndex((k) => k === key);
|
||
if (eki >= 0) {
|
||
expandedKeys.value.splice(eki, 1);
|
||
}
|
||
}
|
||
|
||
return item;
|
||
}
|
||
|
||
/**
|
||
* 获取父类的选中状态
|
||
* @param item
|
||
*/
|
||
function getParentCheckedStatus(item) {
|
||
if (!item) {
|
||
return unCheckedStatus;
|
||
}
|
||
|
||
if (!props.checkedDisabled && item.disabled) {
|
||
return item.checkedStatus || unCheckedStatus;
|
||
}
|
||
|
||
// 单选时,父类永远为半选
|
||
if (!props.showCheckbox) {
|
||
return halfCheckedStatus;
|
||
}
|
||
|
||
const { children } = item;
|
||
// 子类全选中
|
||
const childrenCheckedAll = children.every((k) => k.checkedStatus === isCheckedStatus);
|
||
if (childrenCheckedAll) {
|
||
return isCheckedStatus;
|
||
}
|
||
|
||
// 子类全不选中
|
||
const childrenUncheckedAll = children.every((k) => k.checkedStatus === unCheckedStatus);
|
||
if (childrenUncheckedAll) {
|
||
return unCheckedStatus;
|
||
}
|
||
|
||
return halfCheckedStatus;
|
||
}
|
||
|
||
function filterData() {
|
||
if (props.filterValue === "") {
|
||
datalist.value.forEach((k) => {
|
||
k.show = true;
|
||
});
|
||
return;
|
||
}
|
||
|
||
datalist.value.forEach((k) => {
|
||
if (k.label.indexOf(props.filterValue) > -1) {
|
||
k.show = true;
|
||
k.parentKeys.forEach((k) => {
|
||
datamap.value[k].show = true;
|
||
});
|
||
} else {
|
||
k.show = false;
|
||
}
|
||
});
|
||
|
||
datalist.value.forEach((k) => {
|
||
if (k.show) {
|
||
k.parentKeys.forEach((k) => {
|
||
datamap.value[k].show = true;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 返回已选的 key
|
||
*/
|
||
const getCheckedKeys = () =>
|
||
getAllNodeKeys(datalist.value, "checkedStatus", isCheckedStatus, props.packDisabledkey);
|
||
/**
|
||
* 根据key设置已选
|
||
* @param keys 单选时为数字或者字符串,多选时为数组
|
||
* @param checked 多选时为key的数组,单选时为key
|
||
*/
|
||
function setCheckedKeys(keys, checked = true) {
|
||
// 多选
|
||
if (props.showCheckbox) {
|
||
if (!isArray(keys)) {
|
||
logError(`setCheckedKeys 第一个参数非数组,传入的是[${keys}]`);
|
||
return;
|
||
}
|
||
|
||
const list = datalist.value;
|
||
|
||
// 取消选择
|
||
if (checked === false) {
|
||
let newCheckedKeys = [];
|
||
for (let i = 0; i < checkedKeys.value.length; i++) {
|
||
const ck = checkedKeys.value[i];
|
||
if (!keys.includes(ck)) {
|
||
newCheckedKeys.push(ck);
|
||
}
|
||
}
|
||
newCheckedKeys = [...new Set(newCheckedKeys)];
|
||
checkedKeys.value = newCheckedKeys;
|
||
handleCheckState(list, keys, false);
|
||
|
||
return;
|
||
}
|
||
|
||
// 选择
|
||
const newCheckedKeys = [...checkedKeys.value, ...keys];
|
||
checkedKeys.value = [...new Set(newCheckedKeys)];
|
||
handleCheckState(list, checkedKeys.value, true);
|
||
|
||
if (props.expandChecked && checked) {
|
||
expandedKeys.value = [...new Set([...(checkedKeys.value || []), ...(keys || [])])];
|
||
handleExpandState(list, keys, true);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 单选
|
||
// 如果为数组则拿第一个
|
||
if (isArray(keys)) {
|
||
keys = keys[0];
|
||
}
|
||
|
||
if (!isString(keys) && !isNumber(keys)) {
|
||
logError("setCheckedKeys 第一个参数字符串或数字,传入的是==>", keys);
|
||
return;
|
||
}
|
||
|
||
const list = datalist.value;
|
||
checkedKeys.value = checked ? keys : null;
|
||
|
||
if (props.expandChecked && checked) {
|
||
handleExpandState(list, [keys], true);
|
||
}
|
||
|
||
handleCheckState(list, keys, !!checked);
|
||
}
|
||
/**
|
||
* 返回半选的 key
|
||
*/
|
||
const getHalfCheckedKeys = () =>
|
||
getAllNodeKeys(datalist.value, "checkedStatus", halfCheckedStatus, props.packDisabledkey);
|
||
/**
|
||
* 返回未选的 key
|
||
*/
|
||
const getUncheckedKeys = () =>
|
||
getAllNodeKeys(datalist.value, "checkedStatus", unCheckedStatus, props.packDisabledkey);
|
||
/**
|
||
* 返回已展开的 key
|
||
*/
|
||
const getExpandedKeys = () => getAllNodeKeys(datalist.value, "expand", true);
|
||
/**
|
||
* 返回未展开的 key
|
||
*/
|
||
const getUnexpandedKeys = () => getAllNodeKeys(datalist.value, "expand", false);
|
||
/**
|
||
* 根据key展开/收起
|
||
*
|
||
* @param keys 数组,或字符串 all
|
||
* @param expand true为展开/false为收起
|
||
*/
|
||
function setExpandedKeys(keys, expand = true) {
|
||
if (!Array.isArray(keys) && keys !== "all") {
|
||
logError("setExpandedKeys 第一个参数非数组,传入的是===>", keys);
|
||
return;
|
||
}
|
||
|
||
const list = datalist.value;
|
||
|
||
// 展开/收起全部
|
||
if (keys === "all") {
|
||
list.forEach((k) => {
|
||
k.expand = expand;
|
||
if (k.level > 0) {
|
||
k.show = expand;
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 收起
|
||
if (expand === false) {
|
||
const newExpandedKeys = [];
|
||
for (let i = 0; i < expandedKeys.value.length; i++) {
|
||
const ek = expandedKeys.value[i];
|
||
if (!keys.includes(ek)) {
|
||
newExpandedKeys.push(ek);
|
||
}
|
||
}
|
||
expandedKeys.value = [...new Set(newExpandedKeys)];
|
||
handleExpandState(list, keys, false);
|
||
|
||
return;
|
||
}
|
||
|
||
// 展开
|
||
const newExpandedKeys = [];
|
||
for (let i = 0; i < list.length; i++) {
|
||
if (keys.includes(list[i].key)) {
|
||
newExpandedKeys.push(list[i].key);
|
||
}
|
||
}
|
||
expandedKeys.value = [...new Set(newExpandedKeys)];
|
||
handleExpandState(list, newExpandedKeys, true);
|
||
}
|
||
/**
|
||
* 返回已选的节点
|
||
*/
|
||
const getCheckedNodes = () =>
|
||
getAllNodes(datalist.value, "checkedStatus", isCheckedStatus, props.packDisabledkey);
|
||
/**
|
||
* 返回半选的节点
|
||
*/
|
||
const getHalfCheckedNodes = () =>
|
||
getAllNodes(datalist.value, "checkedStatus", halfCheckedStatus, props.packDisabledkey);
|
||
/**
|
||
* 返回未选的节点
|
||
*/
|
||
const getUncheckedNodes = () =>
|
||
getAllNodes(datalist.value, "checkedStatus", unCheckedStatus, props.packDisabledkey);
|
||
/**
|
||
* 返回已展开的节点
|
||
*/
|
||
const getExpandedNodes = () => getAllNodes(datalist.value, "expand", true);
|
||
/**
|
||
* 返回未展开的节点
|
||
*/
|
||
const getUnexpandedNodes = () => getAllNodes(datalist.value, "expand", false);
|
||
|
||
watch(
|
||
() => props.defaultExpandedKeys,
|
||
(v) => {
|
||
if (v?.length) {
|
||
expandedKeys.value = v;
|
||
} else {
|
||
expandedKeys.value = [];
|
||
}
|
||
|
||
// if (v) checkInitData(datalist.value)
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
watch(
|
||
() => props.defaultCheckedKeys,
|
||
(v) => {
|
||
if (props.showCheckbox) {
|
||
if (v?.length) {
|
||
checkedKeys.value = v;
|
||
} else {
|
||
checkedKeys.value = [];
|
||
}
|
||
} else {
|
||
if (v || v === 0) {
|
||
checkedKeys.value = v;
|
||
} else {
|
||
checkedKeys.value = null;
|
||
}
|
||
}
|
||
// checkInitData(datalist.value)
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
watch(
|
||
() => props.data,
|
||
(v) => {
|
||
dataRef.value = deepClone(v);
|
||
setTimeout(() => {
|
||
initData();
|
||
}, 36);
|
||
},
|
||
{ immediate: true, deep: true }
|
||
);
|
||
|
||
watch(
|
||
() => props.filterValue,
|
||
() => {
|
||
filterData();
|
||
}
|
||
);
|
||
|
||
return {
|
||
datalist,
|
||
unCheckedStatus,
|
||
halfCheckedStatus,
|
||
isCheckedStatus,
|
||
handleCheckChange,
|
||
handleRadioChange,
|
||
handleLabelClick,
|
||
handleExpandedChange,
|
||
loadLoading,
|
||
|
||
// updateChildrenByKey: () => {},
|
||
// insertBeforeByKey: () => {},
|
||
// insertAfterByKey: () => {},
|
||
getCheckedKeys,
|
||
setCheckedKeys,
|
||
getHalfCheckedKeys,
|
||
getUncheckedKeys,
|
||
getExpandedKeys,
|
||
getUnexpandedKeys,
|
||
setExpandedKeys,
|
||
getCheckedNodes,
|
||
getHalfCheckedNodes,
|
||
getUncheckedNodes,
|
||
getExpandedNodes,
|
||
getUnexpandedNodes,
|
||
};
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@font-face {
|
||
font-family: "da-tree-iconfont"; /* Project id */
|
||
src: url("data:application/octet-stream;base64,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")
|
||
format("truetype");
|
||
}
|
||
|
||
.da-tree {
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
&-scroll {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
&-item {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 0;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
font-size: 36rpx;
|
||
line-height: 1;
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
transition: opacity 0.2s linear;
|
||
|
||
&.is-show {
|
||
height: auto;
|
||
padding: 12rpx 24rpx;
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
&__icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 50rpx;
|
||
height: 50rpx;
|
||
overflow: hidden;
|
||
|
||
&--arr {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
|
||
&::after {
|
||
position: relative;
|
||
z-index: 1;
|
||
overflow: hidden;
|
||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||
font-family: "da-tree-iconfont" !important;
|
||
font-size: 40rpx;
|
||
font-style: normal;
|
||
color: #999;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
&.is-expand {
|
||
&::after {
|
||
content: "\e604";
|
||
}
|
||
}
|
||
|
||
&.is-right {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
&.is-loading {
|
||
animation: IconLoading 1s linear 0s infinite;
|
||
|
||
&::after {
|
||
content: "\e7f1";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
&__checkbox {
|
||
width: 50rpx;
|
||
height: 50rpx;
|
||
overflow: hidden;
|
||
|
||
&--left {
|
||
order: 0;
|
||
}
|
||
|
||
&--right {
|
||
order: 1;
|
||
}
|
||
|
||
&--icon {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 50rpx;
|
||
height: 50rpx;
|
||
|
||
&::after {
|
||
position: relative;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 1;
|
||
overflow: hidden;
|
||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||
font-family: "da-tree-iconfont" !important;
|
||
font-size: 40rpx;
|
||
font-style: normal;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
&.da-tree-checkbox-outline::after {
|
||
color: #bbb;
|
||
content: "\ead5";
|
||
}
|
||
|
||
&.da-tree-checkbox-checked::after {
|
||
color: var(--theme-color, #007aff);
|
||
content: "\ead4";
|
||
}
|
||
|
||
&.da-tree-checkbox-indeterminate::after {
|
||
color: var(--theme-color, #007aff);
|
||
content: "\ebce";
|
||
}
|
||
|
||
&.da-tree-radio-outline::after {
|
||
color: #bbb;
|
||
content: "\ecc5";
|
||
}
|
||
|
||
&.da-tree-radio-checked::after {
|
||
color: var(--theme-color, #007aff);
|
||
content: "\ecc4";
|
||
}
|
||
|
||
&.da-tree-radio-indeterminate::after {
|
||
color: var(--theme-color, #007aff);
|
||
content: "\ea4f";
|
||
}
|
||
}
|
||
|
||
&.is--disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.35;
|
||
}
|
||
}
|
||
|
||
&__label {
|
||
flex: 1;
|
||
margin-left: 4rpx;
|
||
color: #555;
|
||
|
||
&--2 {
|
||
color: var(--theme-color, #007aff);
|
||
}
|
||
|
||
&--append {
|
||
font-size: 60%;
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes IconLoading {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
</style>
|