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,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 = { |