perf(tree): 优化Tree搜索功能,添加搜索高亮功能,优化样式表现 (#1153)

1. 修复expandOnSearch与checkOnSearch功能
2. 添加selectOnSearch功能
3. 添加搜索高亮title功能
4. 优化TreeHeader的样式表现: searchInput自动扩充
3个文件已修改
118 ■■■■ 已修改文件
src/components/Tree/src/Tree.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Tree/src/TreeHeader.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Tree/src/props.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Tree/src/Tree.vue
@@ -19,9 +19,9 @@
  import { ScrollContainer } from '/@/components/Container';
  import { omit, get, difference } from 'lodash-es';
  import { isArray, isBoolean, isFunction } from '/@/utils/is';
  import { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
  import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
  import { filter } from '/@/utils/helper/treeHelper';
  import { filter, treeToList } from '/@/utils/helper/treeHelper';
  import { useTree } from './useTree';
  import { useContextMenu } from '/@/hooks/web/useContextMenu';
@@ -60,6 +60,7 @@
      const searchState = reactive({
        startSearch: false,
        searchText: '',
        searchData: [] as TreeItem[],
      });
@@ -199,23 +200,40 @@
        state.checkStrictly = strictly;
      }
      const searchText = ref('');
      watchEffect(() => {
        if (props.searchValue !== searchText.value) searchText.value = props.searchValue;
      });
      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 !== searchText.value) searchText.value = searchValue;
        if (searchValue !== searchState.searchText) searchState.searchText = searchValue;
        emit('update:searchValue', searchValue);
        if (!searchValue) {
          searchState.startSearch = false;
          return;
        }
        const { filterFn, checkable, expandOnSearch, checkOnSearch } = unref(props);
        const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
          unref(props);
        searchState.startSearch = true;
        const { title: titleField, key: keyField } = unref(getReplaceFields);
        const searchKeys: string[] = [];
        const matchedKeys: string[] = [];
        searchState.searchData = filter(
          unref(treeDataRef),
          (node) => {
@@ -223,19 +241,28 @@
              ? filterFn(searchValue, node, unref(getReplaceFields))
              : node[titleField]?.includes(searchValue) ?? false;
            if (result) {
              searchKeys.push(node[keyField]);
              matchedKeys.push(node[keyField]);
            }
            return result;
          },
          unref(getReplaceFields),
        );
        if (expandOnSearch && searchKeys.length > 0) {
          setExpandedKeys(searchKeys);
        if (expandOnSearch) {
          const expandKeys = treeToList(searchState.searchData).map((val) => {
            return val[keyField];
          });
          if (expandKeys && expandKeys.length) {
            setExpandedKeys(expandKeys);
          }
        }
        if (checkOnSearch && checkable && searchKeys.length > 0) {
          setCheckedKeys(searchKeys);
        if (checkOnSearch && checkable && matchedKeys.length) {
          setCheckedKeys(matchedKeys);
        }
        if (selectedOnSearch && matchedKeys.length) {
          setSelectedKeys(matchedKeys);
        }
      }
@@ -255,7 +282,6 @@
      watchEffect(() => {
        treeDataRef.value = props.treeData as TreeItem[];
        handleSearch(unref(searchText));
      });
      onMounted(() => {
@@ -328,7 +354,7 @@
          handleSearch(value);
        },
        getSearchValue: () => {
          return searchText.value;
          return searchState.searchText;
        },
      };
@@ -359,6 +385,8 @@
        if (!data) {
          return null;
        }
        const searchText = searchState.searchText;
        const { highlight } = unref(props);
        return data.map((item) => {
          const {
            title: titleField,
@@ -369,6 +397,23 @@
          const propsData = omit(item, 'title');
          const icon = getIcon({ ...item, level }, item.icon);
          const children = get(item, childrenField) || [];
          const title = get(item, titleField);
          const searchIdx = title.indexOf(searchText);
          const isHighlight =
            searchState.startSearch && !isEmpty(searchText) && highlight && searchIdx !== -1;
          const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
          const titleDom = isHighlight ? (
            <span class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}>
              <span>{title.substr(0, searchIdx)}</span>
              <span style={highlightStyle}>{searchText}</span>
              <span>{title.substr(searchIdx + searchText.length)}</span>
            </span>
          ) : (
            title
          );
          return (
            <Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
              {{
@@ -382,11 +427,8 @@
                    ) : (
                      <>
                        {icon && <TreeIcon icon={icon} />}
                        <span
                          class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}
                        >
                          {get(item, titleField)}
                        </span>
                        {titleDom}
                        {/*{get(item, titleField)}*/}
                        <span class={`${prefixCls}__actions`}>
                          {renderAction({ ...item, level })}
                        </span>
@@ -417,7 +459,7 @@
                helpMessage={helpMessage}
                onStrictlyChange={onStrictlyChange}
                onSearch={handleSearch}
                searchText={unref(searchText)}
                searchText={searchState.searchText}
              >
                {extendSlots(slots)}
              </TreeHeader>
src/components/Tree/src/TreeHeader.vue
@@ -5,8 +5,11 @@
      {{ title }}
    </BasicTitle>
    <div class="flex flex-1 justify-end items-center cursor-pointer" v-if="search || toolbar">
      <div class="mr-1 w-2/3" v-if="search">
    <div
      class="flex flex-1 justify-self-stretch items-center cursor-pointer"
      v-if="search || toolbar"
    >
      <div :class="getInputSearchCls" v-if="search">
        <InputSearch
          :placeholder="t('common.searchText')"
          size="small"
@@ -31,7 +34,7 @@
  </div>
</template>
<script lang="ts">
  import type { PropType } from 'vue';
  import { PropType } from 'vue';
  import { defineComponent, computed, ref, watch } from 'vue';
  import { Dropdown, Menu, Input } from 'ant-design-vue';
@@ -80,9 +83,21 @@
      searchText: propTypes.string,
    },
    emits: ['strictly-change', 'search'],
    setup(props, { emit }) {
    setup(props, { emit, slots }) {
      const { t } = useI18n();
      const searchValue = ref('');
      const getInputSearchCls = computed(() => {
        const titleExists = slots.headerTitle || props.title;
        return [
          'mr-1',
          'w-full',
          // titleExists ? 'w-2/3' : 'w-full',
          {
            ['ml-5']: titleExists,
          },
        ];
      });
      const toolbarList = computed(() => {
        const { checkable } = props;
@@ -157,7 +172,7 @@
      //   debounceEmitChange(e.target.value);
      // }
      return { t, toolbarList, handleMenuClick, searchValue };
      return { t, toolbarList, handleMenuClick, searchValue, getInputSearchCls };
    },
  });
</script>
src/components/Tree/src/props.ts
@@ -80,10 +80,17 @@
    >,
    default: null,
  },
  // 高亮搜索值,仅高亮具体匹配值(通过title)值为true时使用默认色值,值为#xxx时使用此值替代且高亮开启
  highlight: {
    type: [Boolean, String] as PropType<Boolean | String>,
    default: false,
  },
  // 搜索完成时自动展开结果
  expandOnSearch: propTypes.bool.def(false),
  // 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
  checkOnSearch: propTypes.bool.def(false),
  // 搜索完成自动select所有结果
  selectedOnSearch: propTypes.bool.def(false),
};
export const treeNodeProps = {