Commit 3b6b4f73033e8757fd3a032f0910dfcc30dee151
Committed by
GitHub
1 parent
5fa730c4
perf(tree): 优化Tree搜索功能,添加搜索高亮功能,优化样式表现 (#1153)
1. 修复expandOnSearch与checkOnSearch功能 2. 添加selectOnSearch功能 3. 添加搜索高亮title功能 4. 优化TreeHeader的样式表现: searchInput自动扩充
Showing
3 changed files
with
91 additions
and
27 deletions
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 = { | ... | ... |