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,9 +19,9 @@
19 import { ScrollContainer } from '/@/components/Container'; 19 import { ScrollContainer } from '/@/components/Container';
20 20
21 import { omit, get, difference } from 'lodash-es'; 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 import { extendSlots, getSlot } from '/@/utils/helper/tsxHelper'; 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 import { useTree } from './useTree'; 26 import { useTree } from './useTree';
27 import { useContextMenu } from '/@/hooks/web/useContextMenu'; 27 import { useContextMenu } from '/@/hooks/web/useContextMenu';
@@ -60,6 +60,7 @@ @@ -60,6 +60,7 @@
60 60
61 const searchState = reactive({ 61 const searchState = reactive({
62 startSearch: false, 62 startSearch: false,
  63 + searchText: '',
63 searchData: [] as TreeItem[], 64 searchData: [] as TreeItem[],
64 }); 65 });
65 66
@@ -199,23 +200,40 @@ @@ -199,23 +200,40 @@
199 state.checkStrictly = strictly; 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 function handleSearch(searchValue: string) { 224 function handleSearch(searchValue: string) {
208 - if (searchValue !== searchText.value) searchText.value = searchValue; 225 + if (searchValue !== searchState.searchText) searchState.searchText = searchValue;
209 emit('update:searchValue', searchValue); 226 emit('update:searchValue', searchValue);
210 if (!searchValue) { 227 if (!searchValue) {
211 searchState.startSearch = false; 228 searchState.startSearch = false;
212 return; 229 return;
213 } 230 }
214 - const { filterFn, checkable, expandOnSearch, checkOnSearch } = unref(props); 231 + const { filterFn, checkable, expandOnSearch, checkOnSearch, selectedOnSearch } =
  232 + unref(props);
215 searchState.startSearch = true; 233 searchState.startSearch = true;
216 const { title: titleField, key: keyField } = unref(getReplaceFields); 234 const { title: titleField, key: keyField } = unref(getReplaceFields);
217 235
218 - const searchKeys: string[] = []; 236 + const matchedKeys: string[] = [];
219 searchState.searchData = filter( 237 searchState.searchData = filter(
220 unref(treeDataRef), 238 unref(treeDataRef),
221 (node) => { 239 (node) => {
@@ -223,19 +241,28 @@ @@ -223,19 +241,28 @@
223 ? filterFn(searchValue, node, unref(getReplaceFields)) 241 ? filterFn(searchValue, node, unref(getReplaceFields))
224 : node[titleField]?.includes(searchValue) ?? false; 242 : node[titleField]?.includes(searchValue) ?? false;
225 if (result) { 243 if (result) {
226 - searchKeys.push(node[keyField]); 244 + matchedKeys.push(node[keyField]);
227 } 245 }
228 return result; 246 return result;
229 }, 247 },
230 unref(getReplaceFields), 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,7 +282,6 @@
255 282
256 watchEffect(() => { 283 watchEffect(() => {
257 treeDataRef.value = props.treeData as TreeItem[]; 284 treeDataRef.value = props.treeData as TreeItem[];
258 - handleSearch(unref(searchText));  
259 }); 285 });
260 286
261 onMounted(() => { 287 onMounted(() => {
@@ -328,7 +354,7 @@ @@ -328,7 +354,7 @@
328 handleSearch(value); 354 handleSearch(value);
329 }, 355 },
330 getSearchValue: () => { 356 getSearchValue: () => {
331 - return searchText.value; 357 + return searchState.searchText;
332 }, 358 },
333 }; 359 };
334 360
@@ -359,6 +385,8 @@ @@ -359,6 +385,8 @@
359 if (!data) { 385 if (!data) {
360 return null; 386 return null;
361 } 387 }
  388 + const searchText = searchState.searchText;
  389 + const { highlight } = unref(props);
362 return data.map((item) => { 390 return data.map((item) => {
363 const { 391 const {
364 title: titleField, 392 title: titleField,
@@ -369,6 +397,23 @@ @@ -369,6 +397,23 @@
369 const propsData = omit(item, 'title'); 397 const propsData = omit(item, 'title');
370 const icon = getIcon({ ...item, level }, item.icon); 398 const icon = getIcon({ ...item, level }, item.icon);
371 const children = get(item, childrenField) || []; 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 return ( 417 return (
373 <Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}> 418 <Tree.TreeNode {...propsData} node={toRaw(item)} key={get(item, keyField)}>
374 {{ 419 {{
@@ -382,11 +427,8 @@ @@ -382,11 +427,8 @@
382 ) : ( 427 ) : (
383 <> 428 <>
384 {icon && <TreeIcon icon={icon} />} 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 <span class={`${prefixCls}__actions`}> 432 <span class={`${prefixCls}__actions`}>
391 {renderAction({ ...item, level })} 433 {renderAction({ ...item, level })}
392 </span> 434 </span>
@@ -417,7 +459,7 @@ @@ -417,7 +459,7 @@
417 helpMessage={helpMessage} 459 helpMessage={helpMessage}
418 onStrictlyChange={onStrictlyChange} 460 onStrictlyChange={onStrictlyChange}
419 onSearch={handleSearch} 461 onSearch={handleSearch}
420 - searchText={unref(searchText)} 462 + searchText={searchState.searchText}
421 > 463 >
422 {extendSlots(slots)} 464 {extendSlots(slots)}
423 </TreeHeader> 465 </TreeHeader>
src/components/Tree/src/TreeHeader.vue
@@ -5,8 +5,11 @@ @@ -5,8 +5,11 @@
5 {{ title }} 5 {{ title }}
6 </BasicTitle> 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 <InputSearch 13 <InputSearch
11 :placeholder="t('common.searchText')" 14 :placeholder="t('common.searchText')"
12 size="small" 15 size="small"
@@ -31,7 +34,7 @@ @@ -31,7 +34,7 @@
31 </div> 34 </div>
32 </template> 35 </template>
33 <script lang="ts"> 36 <script lang="ts">
34 - import type { PropType } from 'vue'; 37 + import { PropType } from 'vue';
35 import { defineComponent, computed, ref, watch } from 'vue'; 38 import { defineComponent, computed, ref, watch } from 'vue';
36 39
37 import { Dropdown, Menu, Input } from 'ant-design-vue'; 40 import { Dropdown, Menu, Input } from 'ant-design-vue';
@@ -80,10 +83,22 @@ @@ -80,10 +83,22 @@
80 searchText: propTypes.string, 83 searchText: propTypes.string,
81 }, 84 },
82 emits: ['strictly-change', 'search'], 85 emits: ['strictly-change', 'search'],
83 - setup(props, { emit }) { 86 + setup(props, { emit, slots }) {
84 const { t } = useI18n(); 87 const { t } = useI18n();
85 const searchValue = ref(''); 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 const toolbarList = computed(() => { 102 const toolbarList = computed(() => {
88 const { checkable } = props; 103 const { checkable } = props;
89 const defaultToolbarList = [ 104 const defaultToolbarList = [
@@ -157,7 +172,7 @@ @@ -157,7 +172,7 @@
157 // debounceEmitChange(e.target.value); 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 </script> 178 </script>
src/components/Tree/src/props.ts
@@ -80,10 +80,17 @@ export const basicProps = { @@ -80,10 +80,17 @@ export const basicProps = {
80 >, 80 >,
81 default: null, 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 expandOnSearch: propTypes.bool.def(false), 89 expandOnSearch: propTypes.bool.def(false),
85 // 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效 90 // 搜索完成自动选中所有结果,当且仅当 checkable===true 时生效
86 checkOnSearch: propTypes.bool.def(false), 91 checkOnSearch: propTypes.bool.def(false),
  92 + // 搜索完成自动select所有结果
  93 + selectedOnSearch: propTypes.bool.def(false),
87 }; 94 };
88 95
89 export const treeNodeProps = { 96 export const treeNodeProps = {