123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- <script lang="tsx">
- import type { CSSProperties } from 'vue';
- import type {
- FieldNames,
- TreeState,
- TreeItem,
- KeyType,
- CheckKeys,
- TreeActionType,
- } from './types/tree';
- import {
- defineComponent,
- reactive,
- computed,
- unref,
- ref,
- watchEffect,
- toRaw,
- watch,
- onMounted,
- } from 'vue';
- import TreeHeader from './components/TreeHeader.vue';
- import { Tree, Spin, Empty } from 'ant-design-vue';
- import { TreeIcon } from './TreeIcon';
- import { ScrollContainer } from '/@/components/Container';
- import { omit, get, difference, cloneDeep } from 'lodash-es';
- import { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
- import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
- import { filter, treeToList, eachTree } from '/@/utils/helper/treeHelper';
- import { useTree } from './hooks/useTree';
- import { useContextMenu } from '/@/hooks/web/useContextMenu';
- import { CreateContextOptions } from '/@/components/ContextMenu';
- import { treeEmits, treeProps } from './types/tree';
- import { createBEM } from '/@/utils/bem';
- import type { TreeProps } from 'ant-design-vue/es/tree/Tree';
- export default defineComponent({
- name: 'BasicTree',
- inheritAttrs: false,
- props: treeProps,
- emits: treeEmits,
- setup(props, { attrs, slots, emit, expose }) {
- const [bem] = createBEM('tree');
- const state = reactive<TreeState>({
- checkStrictly: props.checkStrictly,
- expandedKeys: props.expandedKeys || [],
- selectedKeys: props.selectedKeys || [],
- checkedKeys: props.checkedKeys || [],
- });
- const searchState = reactive({
- startSearch: false,
- searchText: '',
- searchData: [] as TreeItem[],
- });
- const treeDataRef = ref<TreeItem[]>([]);
- const [createContextMenu] = useContextMenu();
- const getFieldNames = computed((): Required<FieldNames> => {
- const { fieldNames } = props;
- return {
- children: 'children',
- title: 'title',
- key: 'key',
- ...fieldNames,
- };
- });
- const getBindValues = computed(() => {
- let propsData = {
- blockNode: true,
- ...attrs,
- ...props,
- expandedKeys: state.expandedKeys,
- selectedKeys: state.selectedKeys,
- checkedKeys: state.checkedKeys,
- checkStrictly: state.checkStrictly,
- fieldNames: unref(getFieldNames),
- 'onUpdate:expandedKeys': (v: KeyType[]) => {
- state.expandedKeys = v;
- emit('update:expandedKeys', v);
- },
- 'onUpdate:selectedKeys': (v: KeyType[]) => {
- state.selectedKeys = v;
- emit('update:selectedKeys', v);
- },
- onCheck: (v: CheckKeys, e) => {
- let currentValue = toRaw(state.checkedKeys) as KeyType[];
- if (isArray(currentValue) && searchState.startSearch) {
- const value = e.node.eventKey;
- currentValue = difference(currentValue, getChildrenKeys(value));
- if (e.checked) {
- currentValue.push(value);
- }
- state.checkedKeys = currentValue;
- } else {
- state.checkedKeys = v;
- }
- const rawVal = toRaw(state.checkedKeys);
- emit('update:value', rawVal);
- emit('check', rawVal, e);
- },
- onRightClick: handleRightClick,
- };
- return omit(propsData, 'treeData', 'class') as TreeProps;
- });
- const getTreeData = computed((): TreeItem[] =>
- searchState.startSearch ? searchState.searchData : unref(treeDataRef),
- );
- const getNotFound = computed((): boolean => {
- return !getTreeData.value || getTreeData.value.length === 0;
- });
- const {
- deleteNodeByKey,
- insertNodeByKey,
- insertNodesByKey,
- filterByLevel,
- updateNodeByKey,
- getAllKeys,
- getChildrenKeys,
- getEnabledKeys,
- getSelectedNode,
- } = useTree(treeDataRef, getFieldNames);
- function getIcon(params: TreeItem, icon?: string) {
- if (!icon) {
- if (props.renderIcon && isFunction(props.renderIcon)) {
- return props.renderIcon(params);
- }
- }
- return icon;
- }
- async function handleRightClick({ event, node }: Recordable) {
- const { rightMenuList: menuList = [], beforeRightClick } = props;
- let contextMenuOptions: CreateContextOptions = { event, items: [] };
- if (beforeRightClick && isFunction(beforeRightClick)) {
- let result = await beforeRightClick(node, event);
- if (Array.isArray(result)) {
- contextMenuOptions.items = result;
- } else {
- Object.assign(contextMenuOptions, result);
- }
- } else {
- contextMenuOptions.items = menuList;
- }
- if (!contextMenuOptions.items?.length) return;
- contextMenuOptions.items = contextMenuOptions.items.filter((item) => !item.hidden);
- createContextMenu(contextMenuOptions);
- }
- function setExpandedKeys(keys: KeyType[]) {
- state.expandedKeys = keys;
- }
- function getExpandedKeys() {
- return state.expandedKeys;
- }
- function setSelectedKeys(keys: KeyType[]) {
- state.selectedKeys = keys;
- }
- function getSelectedKeys() {
- return state.selectedKeys;
- }
- function setCheckedKeys(keys: CheckKeys) {
- state.checkedKeys = keys;
- }
- function getCheckedKeys() {
- return state.checkedKeys;
- }
- function checkAll(checkAll: boolean) {
- state.checkedKeys = checkAll ? getEnabledKeys() : ([] as KeyType[]);
- }
- function expandAll(expandAll: boolean) {
- state.expandedKeys = expandAll ? getAllKeys() : ([] as KeyType[]);
- }
- function onStrictlyChange(strictly: boolean) {
- state.checkStrictly = strictly;
- }
- watch(
- () => props.searchValue,
- (val) => {
- if (val !== searchState.searchText) {
- searchState.searchText = val;
- }
- },
- {
- immediate: true,
- },
- );
- watch(
- () => props.treeData,
- (val) => {
- if (val) {
- handleSearch(searchState.searchText);
- }
- },
- );
- function handleSearch(searchValue: string) {
- if (searchValue !== searchState.searchText) searchState.searchText = searchValue;
- emit('update:searchValue', searchValue);
- if (!searchValue) {
- searchState.startSearch = false;
- return;
- }
- const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
- unref(props);
- searchState.startSearch = true;
- const { title: titleField, key: keyField } = unref(getFieldNames);
- const matchedKeys: string[] = [];
- searchState.searchData = filter(
- unref(treeDataRef),
- (node) => {
- const result = filterFn
- ? filterFn(searchValue, node, unref(getFieldNames))
- : node[titleField]?.includes(searchValue) ?? false;
- if (result) {
- matchedKeys.push(node[keyField]);
- }
- return result;
- },
- unref(getFieldNames),
- );
- if (expandOnSearch) {
- const expandKeys = treeToList(searchState.searchData).map((val) => {
- return val[keyField];
- });
- if (expandKeys && expandKeys.length) {
- setExpandedKeys(expandKeys);
- }
- }
- if (checkOnSearch && checkable && matchedKeys.length) {
- setCheckedKeys(matchedKeys);
- }
- if (selectedOnSearch && matchedKeys.length) {
- setSelectedKeys(matchedKeys);
- }
- }
- function handleClickNode(key: string, children: TreeItem[]) {
- if (!props.clickRowToExpand || !children || children.length === 0) return;
- if (!state.expandedKeys.includes(key)) {
- setExpandedKeys([...state.expandedKeys, key]);
- } else {
- const keys = [...state.expandedKeys];
- const index = keys.findIndex((item) => item === key);
- if (index !== -1) {
- keys.splice(index, 1);
- }
- setExpandedKeys(keys);
- }
- }
- watchEffect(() => {
- treeDataRef.value = props.treeData as TreeItem[];
- });
- onMounted(() => {
- const level = parseInt(props.defaultExpandLevel);
- if (level > 0) {
- state.expandedKeys = filterByLevel(level);
- } else if (props.defaultExpandAll) {
- expandAll(true);
- }
- });
- watchEffect(() => {
- state.expandedKeys = props.expandedKeys;
- });
- watchEffect(() => {
- state.selectedKeys = props.selectedKeys;
- });
- watchEffect(() => {
- state.checkedKeys = props.checkedKeys;
- });
- watch(
- () => props.value,
- () => {
- state.checkedKeys = toRaw(props.value || []);
- },
- { immediate: true },
- );
- watch(
- () => state.checkedKeys,
- () => {
- const v = toRaw(state.checkedKeys);
- emit('update:value', v);
- emit('change', v);
- },
- );
- watchEffect(() => {
- state.checkStrictly = props.checkStrictly;
- });
- const instance: TreeActionType = {
- getTreeData: () => treeDataRef,
- setExpandedKeys,
- getExpandedKeys,
- setSelectedKeys,
- getSelectedKeys,
- setCheckedKeys,
- getCheckedKeys,
- insertNodeByKey,
- insertNodesByKey,
- deleteNodeByKey,
- updateNodeByKey,
- getSelectedNode,
- checkAll,
- expandAll,
- filterByLevel: (level: number) => {
- state.expandedKeys = filterByLevel(level);
- },
- setSearchValue: (value: string) => {
- handleSearch(value);
- },
- getSearchValue: () => {
- return searchState.searchText;
- },
- };
- function renderAction(node: TreeItem) {
- const { actionList } = props;
- if (!actionList || actionList.length === 0) return;
- return actionList.map((item, index) => {
- let nodeShow = true;
- if (isFunction(item.show)) {
- nodeShow = item.show?.(node);
- } else if (isBoolean(item.show)) {
- nodeShow = item.show;
- }
- if (!nodeShow) return null;
- return (
- <span key={index} class={bem('action')}>
- {item.render(node)}
- </span>
- );
- });
- }
- const treeData = computed(() => {
- const data = cloneDeep(getTreeData.value);
- eachTree(data, (item, _parent) => {
- const searchText = searchState.searchText;
- const { highlight } = unref(props);
- const {
- title: titleField,
- key: keyField,
- children: childrenField,
- } = unref(getFieldNames);
- const icon = getIcon(item, item.icon);
- const title = get(item, titleField);
- const searchIdx = searchText ? title.indexOf(searchText) : -1;
- const isHighlight =
- searchState.startSearch && !isEmpty(searchText) && highlight && searchIdx !== -1;
- const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
- const titleDom = isHighlight ? (
- <span class={unref(getBindValues)?.blockNode ? `${bem('content')}` : ''}>
- <span>{title.substr(0, searchIdx)}</span>
- <span style={highlightStyle}>{searchText}</span>
- <span>{title.substr(searchIdx + (searchText as string).length)}</span>
- </span>
- ) : (
- title
- );
- const iconDom = icon ? (
- <TreeIcon icon={icon} />
- ) : slots.icon ? (
- <span class="mr-1">{getSlot(slots, 'icon')}</span>
- ) : null;
- item[titleField] = (
- <span
- class={`${bem('title')} pl-2`}
- onClick={handleClickNode.bind(null, item[keyField], item[childrenField])}
- >
- {slots?.title ? (
- <>
- {iconDom}
- {getSlot(slots, 'title', item)}
- </>
- ) : (
- <>
- {iconDom}
- {titleDom}
- <span class={bem('actions')}>{renderAction(item)}</span>
- </>
- )}
- </span>
- );
- return item;
- });
- return data;
- });
- expose(instance);
- return () => {
- const { title, helpMessage, toolbar, search, checkable } = props;
- const showTitle = title || toolbar || search || slots.headerTitle;
- const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
- return (
- <div class={[bem(), 'h-full', attrs.class]}>
- {showTitle && (
- <TreeHeader
- checkable={checkable}
- checkAll={checkAll}
- expandAll={expandAll}
- title={title}
- search={search}
- toolbar={toolbar}
- helpMessage={helpMessage}
- onStrictlyChange={onStrictlyChange}
- onSearch={handleSearch}
- searchText={searchState.searchText}
- >
- {extendSlots(slots)}
- </TreeHeader>
- )}
- <Spin
- wrapperClassName={unref(props.treeWrapperClassName)}
- spinning={unref(props.loading)}
- tip="加载中..."
- >
- <ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
- <Tree {...unref(getBindValues)} showIcon={false} treeData={treeData.value}>
- {extendSlots(slots, ['title'])}
- </Tree>
- </ScrollContainer>
- <Empty
- v-show={unref(getNotFound)}
- image={Empty.PRESENTED_IMAGE_SIMPLE}
- class="!mt-4"
- />
- </Spin>
- </div>
- );
- };
- },
- });
- </script>
|