From 5a5ccab2e9b5fad1ed92d9d0797a9bff30f0e964 Mon Sep 17 00:00:00 2001 From: "jlj05024111@163.com" Date: Fri, 7 Nov 2025 22:42:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=90=9C=E7=B4=A2=E6=A0=8F=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=90=9C=E7=B4=A2=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=B8=8E=E5=BF=AB=E9=80=9F=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/control/search-bar/search-bar.scss | 206 +++++++++- src/control/search-bar/search-bar.tsx | 514 +++++++++++++++++++++++-- src/locale/en/index.ts | 6 + src/locale/zh-CN/index.ts | 6 + 4 files changed, 708 insertions(+), 24 deletions(-) diff --git a/src/control/search-bar/search-bar.scss b/src/control/search-bar/search-bar.scss index 1fb0981bfe3..2ed5226c2f0 100644 --- a/src/control/search-bar/search-bar.scss +++ b/src/control/search-bar/search-bar.scss @@ -7,6 +7,28 @@ $control-searchbar: ( filter-btn-height: getCssVar('height-control', 'default'), enable-filter-padding: getCssVar(spacing, tight) getCssVar(spacing, tight) getCssVar(spacing, tight) getCssVar(spacing, base), + // 快速分组 + + color-group-item-text: getCssVar(color, text, 1), + color-group-item-bg: getCssVar(color, fill, 1), + color-group-item-selected-bg: getCssVar(color, primary), + color-group-item-selected-text: getCssVar(color, white), + spacing-group-item-padding: getCssVar(spacing, tight) getCssVar(spacing, base), + spacing-group-item-gap: getCssVar(spacing, tight), + spacing-group-more-gap: getCssVar(spacing, extra-tight), + spacing-group-item-margin:getCssVar(spacing, extra-tight) getCssVar(spacing, tight) 0 0, + spacing-group-container-padding: getCssVar(spacing, base), + radius-group-item: getCssVar(border-radius, full), + footer-item-padding: getCssVar(spacing, base), + footer-item-gap: getCssVar(spacing, base, tight), + footer-group-padding: 0 getCssVar(spacing, base), + footer-more-color: getCssVar(color, link), + footer-item-shadow: getCssVar(shadow, elevated), + footer-item-border-radius: getCssVar(border, radius, small), + footer-reset-color: getCssVar(color, text), + footer-reset-bg-color: transparent, + footer-confirm-color: getCssVar(color, default), + footer-confirm-bg-color: getCssVar(color, primary), ); $control-searchbar-quick-search: ( @@ -15,9 +37,28 @@ $control-searchbar-quick-search: ( left-icon: 0 getCssVar('spacing', 'tight') 0 0, ); +$dropdown-popover: ( + color-popover-bg: getCssVar(color, bg, 1), + // 历史搜索 + color-history-item-bg: getCssVar(color, fill, 1), + spacing-dropdown-popover-padding: getCssVar(spacing, base), + spacing-history-header-padding: getCssVar(spacing, tight), + spacing-history-header-clear-gap: getCssVar(spacing, extra-tight), + spacing-history-footer-padding: getCssVar(spacing, tight), + spacing-history-item-padding: getCssVar(spacing, extra-tight) + getCssVar(spacing, base), + spacing-history-item-margin: getCssVar(spacing, extra-tight), + spacing-history-item-gap: getCssVar(spacing, tight), + font-history-item-line-height: getCssVar(spacing, loose), + radius-history-item: getCssVar(border-radius, full), +); + // 搜索输入框样式 @include b(control-searchbar-quick-search) { - @include set-component-css-var(control-searchbar-quick-search, $control-searchbar-quick-search); + @include set-component-css-var( + control-searchbar-quick-search, + $control-searchbar-quick-search + ); width: 100%; background-color: transparent; height: getCssVar(control-searchbar-quick-search, height); @@ -57,3 +98,166 @@ $control-searchbar-quick-search: ( padding: getCssVar(control-searchbar, enable-filter-padding); } } + +@include b('control-searchbar-container') { + width: 100%; + height: 100%; + display: block; + padding: 0; +} + +@include b('control-searchbar-dropdown') { + @include set-component-css-var(dropdown-popover, $dropdown-popover); + background-color: getCssVar(dropdown-popover, color-popover-bg); + flex-direction: column; + position: absolute; + left: 0; + gap: 8px; + width: 100vw; + z-index: 999; + padding: getCssVar(dropdown-popover, spacing-dropdown-popover-padding); + box-shadow: + inset 0 4px 4px 0px getCssVar(color, fill, 0), + inset 0 -4px 4px 0px getCssVar(color, fill, 0); +} + +@include b('control-searchbar-history') { + flex: 1; + display: flex; + flex-direction: column; + width: 100%; + overflow: hidden; + @include e('header') { + flex-shrink: 0; + display: flex; + justify-content: space-between; + padding: getCssVar(dropdown-popover, spacing-history-header-padding) 0; + @include m('title') { + font-weight: bold; + } + @include m('clear') { + display: flex; + align-items: center; + gap: getCssVar(dropdown-popover, spacing-history-header-clear-gap); + } + } + @include e('content') { + flex: 1; + + display: flex; + flex-flow: wrap; + overflow: auto; + gap: getCssVar(dropdown-popover, spacing-history-item-gap); + @include m('item') { + flex-shrink: 0; + display: inline-flex; + max-width: 100%; + position: relative; + } + @include m('item-text') { + max-width: 100%; + background-color: getCssVar(dropdown-popover, color-history-item-bg); + border-radius: getCssVar(dropdown-popover, radius-history-item); + padding: getCssVar(dropdown-popover, spacing-history-item-padding); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: getCssVar(dropdown-popover, spacing-history-item-margin); + line-height: getCssVar(dropdown-popover, font-history-item-line-height); + } + @include m('item-remove') { + position: absolute; + top: 0; + right: 0; + } + } + @include e('footer') { + --van-divider-margin: 0; + flex-shrink: 0; + text-align: center; + padding: getCssVar(dropdown-popover, spacing-history-footer-padding) + getCssVar(dropdown-popover, spacing-history-footer-padding) 0 + getCssVar(dropdown-popover, spacing-history-footer-padding); + } +} + +@include b('control-searchbar-quick-group') { + padding: getCssVar(control-searchbar, padding); + display: flex; + gap: getCssVar(control-searchbar, spacing-group-item-gap); + flex-flow: wrap; + @include e('item') { + user-select: none; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; + height: max-content; + margin-top: getCssVar(control-searchbar, margin) ; + color: getCssVar(control-searchbar, color-group-item-text); + background-color: getCssVar(control-searchbar, color-group-item-bg); + padding: getCssVar(control-searchbar, spacing-group-item-padding); + border-radius: getCssVar(control-searchbar, radius-group-item); + ion-icon{ + vertical-align: text-top; + margin-right: getCssVar(control-searchbar,spacing-group-more-gap); + } + @include when('selected') { + color: getCssVar(control-searchbar, color-group-item-selected-text); + background-color: getCssVar( + 'control-searchbar', + 'color-group-item-selected-bg' + ); + } + } +} + +@include b('control-searchbar-quick-group-popup') { + width: 100%; + height: 100%; + max-width: 100%; + padding: getCssVar(control-searchbar, spacing-group-container-padding); +} + +@include b('control-searchbar-quick-group-container') { + @include set-component-css-var(control-searchbar, $control-searchbar); + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + @include e('content') { + flex: 1; + overflow: auto; + @include m('item'){ + margin: getCssVar(control-searchbar, spacing-group-item-margin); + } + } + @include e('footer') { + @include flex(row, flex-start, center); + display: flex; + flex-shrink: 0; + gap: getCssVar(control-searchbar, footer-item-gap); + padding: getCssVar(control-searchbar, footer-padding); + @include m('reset') { + flex: 1; + padding: getCssVar(control-searchbar, footer-item-padding); + box-shadow: getCssVar(control-searchbar, footer-item-shadow); + background-color: getCssVar(control-searchbar, footer-reset-bg-color); + color: getCssVar(control-searchbar, text-color); + width: 100%; + text-align: center; + border-radius: getCssVar(control-searchbar, footer-item-border-radius); + } + @include m('search') { + flex: 1; + padding: getCssVar(control-searchbar, footer-item-padding); + box-shadow: getCssVar(control-searchbar, footer-item-shadow); + background-color: getCssVar(control-searchbar, footer-confirm-bg-color); + color: getCssVar(control-searchbar, footer-confirm-color); + width: 100%; + text-align: center; + opacity: 0.9; + border-radius: getCssVar(control-searchbar, footer-item-border-radius); + } + } +} diff --git a/src/control/search-bar/search-bar.tsx b/src/control/search-bar/search-bar.tsx index 8e09b147a6e..e58304dff0d 100644 --- a/src/control/search-bar/search-bar.tsx +++ b/src/control/search-bar/search-bar.tsx @@ -1,13 +1,25 @@ -import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; -import { computed, defineComponent, PropType, ref } from 'vue'; -import { ISearchBar } from '@ibiz/model-core'; +import { + useClickOutside, + useControlController, + useNamespace, +} from '@ibiz-template/vue3-util'; +import { + computed, + defineComponent, + onMounted, + onUnmounted, + PropType, + Ref, + ref, +} from 'vue'; +import { ISearchBar, ISearchBarGroup } from '@ibiz/model-core'; import { debounce } from 'lodash-es'; import { IControlProvider, IOverlayPopoverContainer, SearchBarController, } from '@ibiz-template/runtime'; -import { showTitle } from '@ibiz-template/core'; +import { OnClickOutsideResult, showTitle } from '@ibiz-template/core'; import './search-bar.scss'; export const SearchBarControl = defineComponent({ @@ -40,8 +52,83 @@ export const SearchBarControl = defineComponent({ ); const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + // 搜索栏 + const searchRef = ref(); + + // 下拉弹框距离顶部位置 + const dropdownTop = ref(0); + + // 下拉弹框 + const dropdownRef = ref(); + + // 显示下拉弹框 + const showDropdown = ref(false); + + // 历史搜索记录 + const historyItems: Ref = ref([]); + + // 是否收缩折叠 + const isCollapse = ref(true); + + // 搜索项移除按钮显示 + const showItemRemove = ref(false); + + // 记录搜索历史项按下时间 + const timer = ref(); + + // 点击外部 + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + let funcs: OnClickOutsideResult; + + // 是否显示所有快速分组 + const showAllGroups = ref(false); + + // 是否启用历史搜索记录功能 + const enableStoredQuery = computed(() => { + if (Object.hasOwnProperty.call(c.controlParams, 'enablestoredquery')) { + return ( + c.controlParams.enablestoredquery === 'true' || + c.controlParams.enablestoredquery === true + ); + } + return ibiz.config.mob.mobEnableStoredQuery; + }); + + // 是否启用快速分组 + const enableQuickGroup = computed(() => { + return c.model.searchBarGroups.length > 0; + }); + + // 是否出下拉弹框 + const enableDropDown = computed(() => { + return enableStoredQuery.value; + }); + + // 计数器数据 + const counterData: Ref = ref({}); + + const fn = (counter: IData) => { + counterData.value = counter; + }; + + c.evt.on('onCreated', () => { + if (c.counter) { + c.counter.onChange(fn, true); + } + }); + const onSearch = () => { c.onSearch(); + if (enableStoredQuery.value && c.state.query) { + const index = historyItems.value.indexOf(c.state.query); + if (index === -1) { + historyItems.value.unshift(c.state.query); + localStorage.setItem( + `searchbar_${c.model.id}`, + JSON.stringify(historyItems.value), + ); + } + } }; const debounceSearch = debounce(() => { @@ -116,41 +203,422 @@ export const SearchBarControl = defineComponent({ } }; + // 聚焦 + const onFocus = (_event: PointerEvent) => { + if (!enableDropDown.value) { + return; + } + showDropdown.value = true; + if (searchRef.value) { + // 每次聚焦出现时重新获取历史搜索记录 + if (enableStoredQuery.value) { + const tmepHistory = localStorage.getItem(`searchbar_${c.model.id}`); + if (tmepHistory) { + historyItems.value = JSON.parse(tmepHistory); + } + } + const rect = ( + searchRef.value.$el as HTMLElement + ).getBoundingClientRect(); + const { height, top } = rect; + dropdownTop.value = top + height + 8; + } + }; + // 搜索栏清除 + const onClear = (_event: Event) => { + if (!enableDropDown.value) { + return; + } + _event.preventDefault(); + _event.stopPropagation(); + showDropdown.value = true; + }; + + // 下拉弹框样式 + const style = computed(() => { + return { + display: showDropdown.value ? 'flex' : 'none', + top: `${dropdownTop.value}px`, + maxHeight: `calc(100vh - ${dropdownTop.value}px - 10px)`, + }; + }); + + // 搜索历史内容样式 + const contentStyle = computed(() => { + return { + maxHeight: isCollapse.value ? '150px' : '100%', + }; + }); + + // 选择历史搜索项 + const onSelectItem = (item: string) => { + c.handleInput(item); + onSearch(); + showDropdown.value = false; + }; + + // 按下 + const onPointerDown = () => { + timer.value = setTimeout(() => { + showItemRemove.value = true; + }, 1000); + }; + + // 放起 + const onPointerUp = () => { + clearTimeout(timer.value); + }; + + // 触摸取消 + const onpointerCancel = () => { + clearTimeout(timer.value); + }; + + // 删除历史搜索项 + const onRemoveItem = (item: string, event: PointerEvent) => { + event.stopPropagation(); + event.preventDefault(); + const index = historyItems.value.indexOf(item); + historyItems.value.splice(index, 1); + }; + + // 绘制历史搜索项 + const renderSearchHistory = () => { + return historyItems.value + .filter(item => { + return item.includes(c.state.query); + }) + .map((item: string) => { + return ( +
onSelectItem(item)} + onPointerdown={onPointerDown} + onPointerup={onPointerUp} + onPointercancel={onpointerCancel} + tabindex={0} + class={ns.bem('history', 'content', 'item')} + > +
+ {item} + {showItemRemove.value && ( + { + onRemoveItem(item, e); + }} + class={ns.bem('history', 'content', 'item-remove')} + name='close-circle-outline' + > + )} +
+
+ ); + }); + }; + + // 清除历史 + const clearHistory = () => { + historyItems.value = []; + localStorage.removeItem(`searchbar_${c.model.id}`); + }; + + // 分组点击 + const onGroupClick = ( + item: ISearchBarGroup, + immediatelySearch: boolean = true, + ) => { + c.state.selectedGroupItem = item; + if (immediatelySearch) { + c.evt.emit('onTabChange', { data: [item] }); + onSearch(); + } + }; + + // 点击折叠 + const onCollapse = () => { + isCollapse.value = !isCollapse.value; + }; + + // 取消单个删除 + const onCancelItemRemove = () => { + showItemRemove.value = false; + }; + + // 绘制历史记录 + const renderHistory = () => { + if (!enableStoredQuery.value) return null; + return ( +
+
+
+ {ibiz.i18n.t('control.searchBar.history')} +
+ {showItemRemove.value ? ( + + {ibiz.i18n.t('control.searchBar.cancel')} + + ) : ( +
+ + {ibiz.i18n.t('control.searchBar.clear')} +
+ )} +
+
+ {renderSearchHistory()} +
+ {historyItems.value.length > 0 ? ( +
+ + {!isCollapse.value + ? ibiz.i18n.t('control.searchBar.collapse') + : ibiz.i18n.t('control.searchBar.expand')} + {!isCollapse.value ? ( + + ) : ( + + )} + +
+ ) : null} +
+ ); + }; + + // 打开全部分组 + const onOpenAllGroups = () => { + showAllGroups.value = true; + }; + + // 重置分组选择 + const onResetGrupSelection = () => { + c.state.selectedGroupItem = null; + showAllGroups.value = false; + onSearch(); + }; + // 绘制快速分组 + const renderQuickGroup = () => { + if (!enableQuickGroup.value) return null; + const count = c.model.quickGroupCount || c.model.searchBarGroups.length; + return ( +
+ {c.model.searchBarGroups + .slice(0, count) + ?.map((groupItem: ISearchBarGroup) => { + const visible = c.calcCountVisible(groupItem); + if (!visible) { + return null; + } + return ( + onGroupClick(groupItem)} + > + {groupItem.caption} + {groupItem.counterId && ( + + )} + + ); + })} + {count > 0 && count < c.model.searchBarGroups.length ? ( +
+ + {c.model.groupMoreText || ibiz.i18n.t('control.searchBar.more')} +
+ ) : null} +
+ ); + }; + + // 关闭所有分组 + const onCloseAllGroups = () => { + showAllGroups.value = false; + onSearch(); + }; + + // 搜索按钮 + const renderSearch = () => { + return ( +
+ {ibiz.i18n.t('control.searchBar.confirm')} +
+ ); + }; + + // 重置按钮 + const renderReset = () => { + return ( +
onResetGrupSelection()} + > + {ibiz.i18n.t('control.searchBar.reset')} +
+ ); + }; + + // 绘制所有分组 + const renderAllGroups = () => { + return ( + +
+
+ {[ + ...c.model.searchBarGroups, + ...c.model.searchBarGroups, + ...c.model.searchBarGroups, + ...c.model.searchBarGroups, + ...c.model.searchBarGroups, + ]?.map((groupItem: ISearchBarGroup) => { + const visible = c.calcCountVisible(groupItem); + if (!visible) { + return null; + } + return ( +
onGroupClick(groupItem, false)} + > + {groupItem.caption} + {groupItem.counterId && ( + + )} +
+ ); + })} +
+
+ {renderReset()} + {renderSearch()} +
+
+
+ ); + }; + + onMounted(() => { + if (enableDropDown.value && dropdownRef.value) { + if (enableStoredQuery.value) { + // 初始化,从本地拿一次历史记录 + const tmepHistory = localStorage.getItem(`searchbar_${c.model.id}`); + if (tmepHistory) { + historyItems.value = JSON.parse(tmepHistory); + } + } + funcs = useClickOutside( + dropdownRef, + async () => { + showDropdown.value = false; + // 关闭时,如果当前启用了记录历史搜索功能,会将当前的所有搜索记录放入localStorage中 + if (enableStoredQuery.value) { + localStorage.setItem( + `searchbar_${c.model.id}`, + JSON.stringify(historyItems.value), + ); + } + }, + { + ignore: [searchRef.value.$el], + }, + ); + } + }); + + onUnmounted(() => { + if (funcs && funcs.stop) { + funcs.stop(); + } + }); + return { c, ns, filterButtonRef, + searchRef, onInput, onSearch, cssVars, triggerFilter, + onFocus, + onClear, + style, + contentStyle, + dropdownRef, + enableStoredQuery, + enableDropDown, + renderHistory, + renderQuickGroup, + renderAllGroups, }; }, render() { return ( - {this.c.model.enableQuickSearch && ( - - )} - {this.c.enableFilter && ( - this.triggerFilter()} - > - - - )} +
+ {this.c.model.enableQuickSearch && ( + + )} + {!this.c.enableFilter && ( + this.triggerFilter()} + > + + + )} +
+ {this.renderQuickGroup()} +
+ {this.renderHistory()} +
+ {this.renderAllGroups()}
); }, diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 5a251d2be16..5054b610791 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -141,6 +141,12 @@ export default { property: 'property', and: 'AND', or: 'OR', + expand: 'Expand', + collapse: 'Collapse', + history: 'History', + clear: 'Clear', + more: 'More', + cancel: 'Cancel', }, toolbar: { noSupportType: 'Toolbar item type: {itemType} is not supported', diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index e9cc478d344..162e4dfa023 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -122,6 +122,12 @@ export default { property: '属性', and: '且', or: '或', + expand: '展开', + collapse: '收起', + history: '历史记录', + clear: '清除', + more: '更多', + cancel: '取消', }, toolbar: { noSupportType: '工具栏项类型:{itemType}暂不支持', -- Gitee