2021-03-02 23:14:36 +08:00
|
|
|
<script lang="tsx">
|
|
|
|
|
import type { ReplaceFields, Keys, CheckKeys, TreeActionType, TreeItem } from './types';
|
|
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
import { defineComponent, reactive, computed, unref, ref, watchEffect, toRaw } from 'vue';
|
2021-03-02 23:14:36 +08:00
|
|
|
import { Tree } from 'ant-design-vue';
|
|
|
|
|
import { TreeIcon } from './TreeIcon';
|
2021-03-03 23:35:30 +08:00
|
|
|
import TreeHeader from './TreeHeader.vue';
|
2021-03-02 23:14:36 +08:00
|
|
|
// import { DownOutlined } from '@ant-design/icons-vue';
|
|
|
|
|
|
|
|
|
|
import { omit, get } from 'lodash-es';
|
2021-03-02 23:57:35 +08:00
|
|
|
import { isBoolean, isFunction } from '/@/utils/is';
|
2021-03-02 23:14:36 +08:00
|
|
|
import { extendSlots } from '/@/utils/helper/tsxHelper';
|
2021-03-03 23:35:30 +08:00
|
|
|
import { filter } from '/@/utils/helper/treeHelper';
|
2021-03-02 23:14:36 +08:00
|
|
|
|
|
|
|
|
import { useTree } from './useTree';
|
|
|
|
|
import { useContextMenu, ContextMenuItem } from '/@/hooks/web/useContextMenu';
|
|
|
|
|
import { useExpose } from '/@/hooks/core/useExpose';
|
|
|
|
|
import { useDesign } from '/@/hooks/web/useDesign';
|
|
|
|
|
|
|
|
|
|
import { basicProps } from './props';
|
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
expandedKeys: Keys;
|
|
|
|
|
selectedKeys: Keys;
|
|
|
|
|
checkedKeys: CheckKeys;
|
2021-03-03 23:35:30 +08:00
|
|
|
checkStrictly: boolean;
|
2021-03-02 23:14:36 +08:00
|
|
|
}
|
|
|
|
|
export default defineComponent({
|
|
|
|
|
name: 'BasicTree',
|
|
|
|
|
props: basicProps,
|
2021-03-03 23:35:30 +08:00
|
|
|
emits: ['update:expandedKeys', 'update:selectedKeys', 'update:value', 'change'],
|
2021-03-02 23:14:36 +08:00
|
|
|
setup(props, { attrs, slots, emit }) {
|
|
|
|
|
const state = reactive<State>({
|
2021-03-03 23:35:30 +08:00
|
|
|
checkStrictly: props.checkStrictly,
|
2021-03-02 23:14:36 +08:00
|
|
|
expandedKeys: props.expandedKeys || [],
|
|
|
|
|
selectedKeys: props.selectedKeys || [],
|
|
|
|
|
checkedKeys: props.checkedKeys || [],
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
const searchState = reactive({
|
|
|
|
|
startSearch: false,
|
|
|
|
|
searchData: [] as TreeItem[],
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-02 23:14:36 +08:00
|
|
|
const treeDataRef = ref<TreeItem[]>([]);
|
|
|
|
|
|
|
|
|
|
const [createContextMenu] = useContextMenu();
|
|
|
|
|
const { prefixCls } = useDesign('basic-tree');
|
|
|
|
|
|
|
|
|
|
const getReplaceFields = computed(
|
|
|
|
|
(): Required<ReplaceFields> => {
|
|
|
|
|
const { replaceFields } = props;
|
|
|
|
|
return {
|
|
|
|
|
children: 'children',
|
|
|
|
|
title: 'title',
|
|
|
|
|
key: 'key',
|
|
|
|
|
...replaceFields,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// const getContentStyle = computed(
|
|
|
|
|
// (): CSSProperties => {
|
|
|
|
|
// const { actionList } = props;
|
|
|
|
|
// const width = actionList.length * 18;
|
|
|
|
|
// return {
|
|
|
|
|
// width: `calc(100% - ${width}px)`,
|
|
|
|
|
// };
|
|
|
|
|
// }
|
|
|
|
|
// );
|
|
|
|
|
|
|
|
|
|
const getBindValues = computed(() => {
|
|
|
|
|
let propsData = {
|
|
|
|
|
blockNode: true,
|
|
|
|
|
...attrs,
|
|
|
|
|
...props,
|
|
|
|
|
expandedKeys: state.expandedKeys,
|
|
|
|
|
selectedKeys: state.selectedKeys,
|
|
|
|
|
checkedKeys: state.checkedKeys,
|
2021-03-03 23:35:30 +08:00
|
|
|
checkStrictly: state.checkStrictly,
|
2021-03-02 23:14:36 +08:00
|
|
|
replaceFields: unref(getReplaceFields),
|
|
|
|
|
'onUpdate:expandedKeys': (v: Keys) => {
|
|
|
|
|
state.expandedKeys = v;
|
|
|
|
|
emit('update:expandedKeys', v);
|
|
|
|
|
},
|
|
|
|
|
'onUpdate:selectedKeys': (v: Keys) => {
|
|
|
|
|
state.selectedKeys = v;
|
|
|
|
|
emit('update:selectedKeys', v);
|
|
|
|
|
},
|
2021-03-02 23:26:11 +08:00
|
|
|
onCheck: (v: CheckKeys) => {
|
2021-03-02 23:14:36 +08:00
|
|
|
state.checkedKeys = v;
|
2021-03-03 23:35:30 +08:00
|
|
|
emit('change', v);
|
2021-03-02 23:14:36 +08:00
|
|
|
emit('update:value', v);
|
|
|
|
|
},
|
|
|
|
|
onRightClick: handleRightClick,
|
|
|
|
|
};
|
2021-03-03 23:35:30 +08:00
|
|
|
propsData = omit(propsData, 'treeData', 'class');
|
2021-03-02 23:14:36 +08:00
|
|
|
return propsData;
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
const getTreeData = computed((): TreeItem[] =>
|
|
|
|
|
searchState.startSearch ? searchState.searchData : unref(treeDataRef)
|
2021-03-02 23:14:36 +08:00
|
|
|
);
|
|
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
const {
|
|
|
|
|
deleteNodeByKey,
|
|
|
|
|
insertNodeByKey,
|
|
|
|
|
filterByLevel,
|
|
|
|
|
updateNodeByKey,
|
|
|
|
|
getAllKeys,
|
|
|
|
|
} = useTree(treeDataRef, getReplaceFields);
|
|
|
|
|
|
2021-03-02 23:14:36 +08:00
|
|
|
function getIcon(params: Recordable, icon?: string) {
|
|
|
|
|
if (!icon) {
|
|
|
|
|
if (props.renderIcon && isFunction(props.renderIcon)) {
|
|
|
|
|
return props.renderIcon(params);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return icon;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleRightClick({ event, node }: any) {
|
|
|
|
|
const { rightMenuList: menuList = [], beforeRightClick } = props;
|
|
|
|
|
let rightMenuList: ContextMenuItem[] = [];
|
|
|
|
|
|
|
|
|
|
if (beforeRightClick && isFunction(beforeRightClick)) {
|
|
|
|
|
rightMenuList = await beforeRightClick(node);
|
|
|
|
|
} else {
|
|
|
|
|
rightMenuList = menuList;
|
|
|
|
|
}
|
|
|
|
|
if (!rightMenuList.length) return;
|
|
|
|
|
createContextMenu({
|
|
|
|
|
event,
|
|
|
|
|
items: rightMenuList,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setExpandedKeys(keys: string[]) {
|
|
|
|
|
state.expandedKeys = keys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getExpandedKeys() {
|
|
|
|
|
return state.expandedKeys;
|
|
|
|
|
}
|
|
|
|
|
function setSelectedKeys(keys: string[]) {
|
|
|
|
|
state.selectedKeys = keys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSelectedKeys() {
|
|
|
|
|
return state.selectedKeys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setCheckedKeys(keys: CheckKeys) {
|
|
|
|
|
state.checkedKeys = keys;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCheckedKeys() {
|
|
|
|
|
return state.checkedKeys;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
function checkAll(checkAll: boolean) {
|
|
|
|
|
state.checkedKeys = checkAll ? getAllKeys() : ([] as Keys);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function expandAll(expandAll: boolean) {
|
|
|
|
|
state.expandedKeys = expandAll ? getAllKeys() : ([] as Keys);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onStrictlyChange(strictly: boolean) {
|
|
|
|
|
state.checkStrictly = strictly;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSearch(searchValue: string) {
|
|
|
|
|
if (!searchValue) {
|
|
|
|
|
searchState.startSearch = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
searchState.startSearch = true;
|
|
|
|
|
|
|
|
|
|
searchState.searchData = filter(unref(treeDataRef), (node) => {
|
|
|
|
|
const { title } = node;
|
|
|
|
|
return title?.includes(searchValue) ?? false;
|
|
|
|
|
// || key?.includes(searchValue);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-02 23:14:36 +08:00
|
|
|
watchEffect(() => {
|
|
|
|
|
treeDataRef.value = props.treeData as TreeItem[];
|
|
|
|
|
state.expandedKeys = props.expandedKeys;
|
|
|
|
|
state.selectedKeys = props.selectedKeys;
|
|
|
|
|
state.checkedKeys = props.checkedKeys;
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
watchEffect(() => {
|
|
|
|
|
if (props.value) {
|
|
|
|
|
state.checkedKeys = props.value;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
|
state.checkStrictly = props.checkStrictly;
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-02 23:14:36 +08:00
|
|
|
const instance: TreeActionType = {
|
|
|
|
|
setExpandedKeys,
|
|
|
|
|
getExpandedKeys,
|
|
|
|
|
setSelectedKeys,
|
|
|
|
|
getSelectedKeys,
|
|
|
|
|
setCheckedKeys,
|
|
|
|
|
getCheckedKeys,
|
|
|
|
|
insertNodeByKey,
|
|
|
|
|
deleteNodeByKey,
|
|
|
|
|
updateNodeByKey,
|
2021-03-03 23:35:30 +08:00
|
|
|
checkAll,
|
|
|
|
|
expandAll,
|
2021-03-02 23:14:36 +08:00
|
|
|
filterByLevel: (level: number) => {
|
|
|
|
|
state.expandedKeys = filterByLevel(level);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useExpose<TreeActionType>(instance);
|
|
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
function renderAction(node: TreeItem) {
|
|
|
|
|
const { actionList } = props;
|
|
|
|
|
if (!actionList || actionList.length === 0) return;
|
|
|
|
|
return actionList.map((item, index) => {
|
|
|
|
|
if (isFunction(item.show)) {
|
|
|
|
|
return item.show?.(node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isBoolean(item.show)) {
|
|
|
|
|
return item.show;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<span key={index} class={`${prefixCls}__action`}>
|
|
|
|
|
{item.render(node)}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderTreeNode({ data, level }: { data: TreeItem[] | undefined; level: number }) {
|
|
|
|
|
if (!data) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return data.map((item) => {
|
|
|
|
|
const { title: titleField, key: keyField, children: childrenField } = unref(
|
|
|
|
|
getReplaceFields
|
|
|
|
|
);
|
2021-03-02 23:14:36 +08:00
|
|
|
|
2021-03-03 23:35:30 +08:00
|
|
|
const propsData = omit(item, 'title');
|
|
|
|
|
const icon = getIcon({ ...item, level }, item.icon);
|
|
|
|
|
return (
|
|
|
|
|
<Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
|
|
|
|
|
{{
|
|
|
|
|
title: () => (
|
|
|
|
|
<span class={`${prefixCls}-title pl-2`}>
|
|
|
|
|
{icon && <TreeIcon icon={icon} />}
|
|
|
|
|
<span
|
|
|
|
|
class={`${prefixCls}__content`}
|
|
|
|
|
// style={unref(getContentStyle)}
|
|
|
|
|
>
|
|
|
|
|
{get(item, titleField)}
|
|
|
|
|
</span>
|
|
|
|
|
<span class={`${prefixCls}__actions`}> {renderAction({ ...item, level })}</span>
|
|
|
|
|
</span>
|
|
|
|
|
),
|
|
|
|
|
default: () =>
|
|
|
|
|
renderTreeNode({ data: get(item, childrenField) || [], level: level + 1 }),
|
|
|
|
|
}}
|
|
|
|
|
</Tree.TreeNode>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
2021-03-02 23:14:36 +08:00
|
|
|
return () => {
|
2021-03-03 23:35:30 +08:00
|
|
|
const { title, helpMessage, toolbar, search } = props;
|
2021-03-02 23:14:36 +08:00
|
|
|
return (
|
2021-03-03 23:35:30 +08:00
|
|
|
<div class={[prefixCls, 'h-full bg-white']}>
|
|
|
|
|
{(title || toolbar || search) && (
|
|
|
|
|
<TreeHeader
|
|
|
|
|
checkAll={checkAll}
|
|
|
|
|
expandAll={expandAll}
|
|
|
|
|
title={title}
|
|
|
|
|
search={search}
|
|
|
|
|
toolbar={toolbar}
|
|
|
|
|
helpMessage={helpMessage}
|
|
|
|
|
onStrictlyChange={onStrictlyChange}
|
|
|
|
|
onSearch={handleSearch}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<Tree {...unref(getBindValues)} showIcon={false}>
|
|
|
|
|
{{
|
|
|
|
|
// switcherIcon: () => <DownOutlined />,
|
|
|
|
|
default: () => renderTreeNode({ data: unref(getTreeData), level: 1 }),
|
|
|
|
|
...extendSlots(slots),
|
|
|
|
|
}}
|
|
|
|
|
</Tree>
|
|
|
|
|
</div>
|
2021-03-02 23:14:36 +08:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
<style lang="less">
|
|
|
|
|
@prefix-cls: ~'@{namespace}-basic-tree';
|
|
|
|
|
|
|
|
|
|
.@{prefix-cls} {
|
|
|
|
|
.ant-tree-node-content-wrapper {
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
.ant-tree-title {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&-title {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding-right: 10px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
.@{prefix-cls}__action {
|
|
|
|
|
visibility: visible;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__content {
|
2021-03-03 23:35:30 +08:00
|
|
|
// display: inline-block;
|
2021-03-02 23:14:36 +08:00
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__actions {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 2px;
|
2021-03-03 23:35:30 +08:00
|
|
|
right: 3px;
|
2021-03-02 23:14:36 +08:00
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__action {
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|