Commit 3b6b4f73033e8757fd3a032f0910dfcc30dee151

Authored by Lan
Committed by GitHub
1 parent 5fa730c4

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

1. 修复expandOnSearch与checkOnSearch功能
2. 添加selectOnSearch功能
3. 添加搜索高亮title功能
4. 优化TreeHeader的样式表现: searchInput自动扩充
src/components/Tree/src/Tree.vue
... ... @@ -19,9 +19,9 @@
19 19 import { ScrollContainer } from '/@/components/Container';
20 20  
21 21 import { omit, get, difference } from 'lodash-es';
22   - import { isArray, isBoolean, isFunction } from '/@/utils/is';
  22 + import { isArray, isBoolean, isEmpty, isFunction } from '/@/utils/is';
23 23 import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper';
24   - import { filter } from '/@/utils/helper/treeHelper';
  24 + import { filter, treeToList } from '/@/utils/helper/treeHelper';
25 25  
26 26 import { useTree } from './useTree';
27 27 import { useContextMenu } from '/@/hooks/web/useContextMenu';
... ... @@ -60,6 +60,7 @@
60 60  
61 61 const searchState = reactive({
62 62 startSearch: false,
  63 + searchText: '',
63 64 searchData: [] as TreeItem[],
64 65 });
65 66  
... ... @@ -199,23 +200,40 @@
199 200 state.checkStrictly = strictly;
200 201 }
201 202  
202   - const searchText = ref('');
203   - watchEffect(() => {
204   - if (props.searchValue !== searchText.value) searchText.value = props.searchValue;
205   - });
  203 + watch(
  204 + () => props.searchValue,
  205 + (val) => {
  206 + if (val !== searchState.searchText) {
  207 + searchState.searchText = val;
  208 + }
  209 + },
  210 + {
  211 + immediate: true,
  212 + },
  213 + );
  214 +
  215 + watch(
  216 + () => props.treeData,
  217 + (val) => {
  218 + if (val) {
  219 + handleSearch(searchState.searchText);
  220 + }
  221 + },
  222 + );
206 223  
207 224 function handleSearch(searchValue: string) {
208   - if (searchValue !== searchText.value) searchText.value = searchValue;
  225 + if (searchValue !== searchState.searchText) searchState.searchText = searchValue;
209 226 emit('update:searchValue', searchValue);
210 227 if (!searchValue) {
211 228 searchState.startSearch = false;
212 229 return;
213 230 }
214   - const { filterFn, checkable, expandOnSearch, checkOnSearch } = unref(props);
  231 + const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
  232 + unref(props);
215 233 searchState.startSearch = true;
216 234 const { title: titleField, key: keyField } = unref(getReplaceFields);
217 235  
218   - const searchKeys: string[] = [];
  236 + const matchedKeys: string[] = [];
219 237 searchState.searchData = filter(
220 238 unref(treeDataRef),
221 239 (node) => {
... ... @@ -223,19 +241,28 @@
223 241 ? filterFn(searchValue, node, unref(getReplaceFields))
224 242 : node[titleField]?.includes(searchValue) ?? false;
225 243 if (result) {
226   - searchKeys.push(node[keyField]);
  244 + matchedKeys.push(node[keyField]);
227 245 }
228 246 return result;
229 247 },
230 248 unref(getReplaceFields),
231 249 );
232 250  
233   - if (expandOnSearch && searchKeys.length > 0) {
234   - setExpandedKeys(searchKeys);
  251 + if (expandOnSearch) {
  252 + const expandKeys = treeToList(searchState.searchData).map((val) => {
  253 + return val[keyField];
  254 + });
  255 + if (expandKeys && expandKeys.length) {
  256 + setExpandedKeys(expandKeys);
  257 + }
  258 + }
  259 +
  260 + if (checkOnSearch && checkable && matchedKeys.length) {
  261 + setCheckedKeys(matchedKeys);
235 262 }
236 263  
237   - if (checkOnSearch && checkable && searchKeys.length > 0) {
238   - setCheckedKeys(searchKeys);
  264 + if (selectedOnSearch && matchedKeys.length) {
  265 + setSelectedKeys(matchedKeys);
239 266 }
240 267 }
241 268  
... ... @@ -255,7 +282,6 @@
255 282  
256 283 watchEffect(() => {
257 284 treeDataRef.value = props.treeData as TreeItem[];
258   - handleSearch(unref(searchText));
259 285 });
260 286  
261 287 onMounted(() => {
... ... @@ -328,7 +354,7 @@
328 354 handleSearch(value);
329 355 },
330 356 getSearchValue: () => {
331   - return searchText.value;
  357 + return searchState.searchText;
332 358 },
333 359 };
334 360  
... ... @@ -359,6 +385,8 @@
359 385 if (!data) {
360 386 return null;
361 387 }
  388 + const searchText = searchState.searchText;
  389 + const { highlight } = unref(props);
362 390 return data.map((item) => {
363 391 const {
364 392 title: titleField,
... ... @@ -369,6 +397,23 @@
369 397 const propsData = omit(item, 'title');
370 398 const icon = getIcon({ ...item, level }, item.icon);
371 399 const children = get(item, childrenField) || [];
  400 + const title = get(item, titleField);
  401 +
  402 + const searchIdx = title.indexOf(searchText);
  403 + const isHighlight =
  404 + searchState.startSearch && !isEmpty(searchText) && highlight && searchIdx !== -1;
  405 + const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;
  406 +
  407 + const titleDom = isHighlight ? (
  408 + <span class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}>
  409 + <span>{title.substr(0, searchIdx)}</span>
  410 + <span style={highlightStyle}>{searchText}</span>
  411 + <span>{title.substr(searchIdx + searchText.length)}</span>
  412 + </span>
  413 + ) : (
  414 + title
  415 + );
  416 +
372 417 return (
373 418 <Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
374 419 {{
... ... @@ -382,11 +427,8 @@
382 427 ) : (
383 428 <>
384 429 {icon && <TreeIcon icon={icon} />}
385   - <span
386   - class={unref(getBindValues)?.blockNode ? `${prefixCls}__content` : ''}
387   - >
388   - {get(item, titleField)}
389   - </span>
  430 + {titleDom}
  431 + {/*{get(item, titleField)}*/}
390 432 <span class={`${prefixCls}__actions`}>
391 433 {renderAction({ ...item, level })}
392 434 </span>
... ... @@ -417,7 +459,7 @@
417 459 helpMessage={helpMessage}
418 460 onStrictlyChange={onStrictlyChange}
419 461 onSearch={handleSearch}
420   - searchText={unref(searchText)}
  462 + searchText={searchState.searchText}
421 463 >
422 464 {extendSlots(slots)}
423 465 </TreeHeader>
... ...
src/components/Tree/src/TreeHeader.vue
... ... @@ -5,8 +5,11 @@
5 5 {{ title }}
6 6 </BasicTitle>
7 7  
8   - <div class="flex flex-1 justify-end items-center cursor-pointer" v-if="search || toolbar">
9   - <div class="mr-1 w-2/3" v-if="search">
  8 + <div
  9 + class="flex flex-1 justify-self-stretch items-center cursor-pointer"
  10 + v-if="search || toolbar"
  11 + >
  12 + <div :class="getInputSearchCls" v-if="search">
10 13 <InputSearch
11 14 :placeholder="t('common.searchText')"
12 15 size="small"
... ... @@ -31,7 +34,7 @@
31 34 </div>
32 35 </template>
33 36 <script lang="ts">
34   - import type { PropType } from 'vue';
  37 + import { PropType } from 'vue';
35 38 import { defineComponent, computed, ref, watch } from 'vue';
36 39  
37 40 import { Dropdown, Menu, Input } from 'ant-design-vue';
... ... @@ -80,10 +83,22 @@
80 83 searchText: propTypes.string,
81 84 },
82 85 emits: ['strictly-change', 'search'],
83   - setup(props, { emit }) {
  86 + setup(props, { emit, slots }) {
84 87 const { t } = useI18n();
85 88 const searchValue = ref('');
86 89  
  90 + const getInputSearchCls = computed(() => {
  91 + const titleExists = slots.headerTitle || props.title;
  92 + return [
  93 + 'mr-1',
  94 + 'w-full',
  95 + // titleExists ? 'w-2/3' : 'w-full',
  96 + {
  97 + ['ml-5']: titleExists,
  98 + },
  99 + ];
  100 + });
  101 +
87 102 const toolbarList = computed(() => {
88 103 const { checkable } = props;
89 104 const defaultToolbarList = [
... ... @@ -157,7 +172,7 @@
157 172 // debounceEmitChange(e.target.value);
158 173 // }
159 174  
160   - return { t, toolbarList, handleMenuClick, searchValue };
  175 + return { t, toolbarList, handleMenuClick, searchValue, getInputSearchCls };
161 176 },
162 177 });
163 178 </script>
... ...
src/components/Tree/src/props.ts
... ... @@ -80,10 +80,17 @@ export const basicProps = {
80 80 >,
81 81 default: null,
82 82 },
  83 + // 高亮搜索值,仅高亮具体匹配值(通过title)值为true时使用默认色值,值为#xxx时使用此值替代且高亮开启
  84 + highlight: {
  85 + type: [Boolean, String] as PropType<Boolean | String>,
  86 + default: false,
  87 + },
83 88 // 搜索完成时自动展开结果
84 89 expandOnSearch: propTypes.bool.def(false),
85 90 // 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
86 91 checkOnSearch: propTypes.bool.def(false),
  92 + // 搜索完成自动select所有结果
  93 + selectedOnSearch: propTypes.bool.def(false),
87 94 };
88 95  
89 96 export const treeNodeProps = {
... ...