diff --git a/CHANGELOG.md b/CHANGELOG.md index a03adf7153ba90ee4171e78fcae745f87e0dbc3e..c38b1390e2da0f1b4fb8854052c67c57c54ee35c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ - 表单分页计数器添加data-value属性,可通过data-value属性设置0值隐藏 - 新增文本框编辑器参数autoquestion(AI历史数据最后一个项是用户消息时是否自动提问,默认开启)和autofill(AI回答完成之后是否触发回填,默认关闭) +- 数据选择编辑器支持自填模式配置的加载更多、滚动加载 ### Fixed - 修复菜单图标样式显示不正确异常 +- 修复数据选择激活数据的回显逻辑计算异常 ## [0.7.41-alpha.27] - 2025-09-19 diff --git a/src/editor/data-picker/ibiz-picker/ibiz-picker.scss b/src/editor/data-picker/ibiz-picker/ibiz-picker.scss index e12ddbade568406d91632567d931bd51b4cf4281..0c14ed81d7131fbeeff83780883c266926582ea3 100644 --- a/src/editor/data-picker/ibiz-picker/ibiz-picker.scss +++ b/src/editor/data-picker/ibiz-picker/ibiz-picker.scss @@ -128,6 +128,16 @@ $picker: ( background-color: getCssVar(picker, empty-bg-color); } } + li:has(.#{bem(picker__loadmore)}) { + padding: 0; + } + } + + @include e(loadmore) { + display: flex; + align-items: center; + justify-content: center; + color: getCssVar(color, link); } @include e(suffix) { diff --git a/src/editor/data-picker/ibiz-picker/ibiz-picker.tsx b/src/editor/data-picker/ibiz-picker/ibiz-picker.tsx index cae251cee585f597b783c574d73f01609a79209c..363317b0faaf87dd2687b7e3416fcb633d0a9d91 100644 --- a/src/editor/data-picker/ibiz-picker/ibiz-picker.tsx +++ b/src/editor/data-picker/ibiz-picker/ibiz-picker.tsx @@ -8,6 +8,7 @@ import { onMounted, defineComponent, resolveComponent, + onBeforeUnmount, } from 'vue'; import { renderString, @@ -16,6 +17,7 @@ import { getDataPickerProps, } from '@ibiz-template/vue3-util'; import { isEmpty, isNil } from 'ramda'; +import { debounce } from 'lodash-es'; import { showTitle } from '@ibiz-template/core'; import { IAppDEUIActionGroupDetail } from '@ibiz/model-core'; import { PickerEditorController } from '../picker-editor.controller'; @@ -193,6 +195,25 @@ export const IBizPicker = defineComponent({ } }; + // 更新下拉列表数据回调方法 + let listCallback: (_items: IData[]) => void | void; + // 搜索值 + let searchQuery = ''; + + // 更新下拉列表数据 + const handleCallback = (cb: (_items: IData[]) => void) => { + const callbackItems: IData[] = items.value.length + ? [...items.value] + : [{ srftype: 'empty' }]; + actionPostion === 'top' + ? callbackItems.unshift(...c.actionDetails) + : callbackItems.push(...c.actionDetails); + + if (c.isShowLoadMore) { + callbackItems.push({ srftype: 'loadmore' }); + } + cb(callbackItems); + }; // 搜索 const onSearch = async (query: string, cb?: (_items: IData[]) => void) => { if (c.model.appDataEntityId) { @@ -205,13 +226,27 @@ export const IBizPicker = defineComponent({ items.value = res.data as IData[]; isLoaded.value = true; if (cb && cb instanceof Function) { - const callbackItems: IData[] = items.value.length - ? [...items.value] - : [{ srftype: 'empty' }]; - actionPostion === 'top' - ? callbackItems.unshift(...c.actionDetails) - : callbackItems.push(...c.actionDetails); - cb(callbackItems); + searchQuery = trimQuery; + listCallback = cb; + handleCallback(cb); + } + } + } + }; + + /** + * 加载更多 + * @return {*} {Promise} + */ + const loadMore = async (): Promise => { + if (c.total > items.value.length && c.model.appDataEntityId) { + const res = await c.getServiceData(searchQuery, props.data, { + isLoadMore: true, + }); + if (res) { + items.value = [...items.value, ...(res.data as IData[])]; + if (listCallback) { + handleCallback(listCallback); } } } @@ -222,7 +257,11 @@ export const IBizPicker = defineComponent({ isShowAll.value = true; setEditable(false); // 回车选中空白 - if (item.srftype === 'empty' || item.detailType === 'DEUIACTION') + if ( + item.srftype === 'empty' || + item.detailType === 'DEUIACTION' || + item.srftype === 'loadmore' + ) return resetCurValue(); await handleDataSelect(item); }; @@ -290,6 +329,74 @@ export const IBizPicker = defineComponent({ { immediate: true }, ); + // 加载更多Ref + const loadmoreRef = ref(); + + // 是否关闭弹窗 + const isClosePopper = ref(false); + // 处理加载更多的点击事件 + const onLoadMoreClick = async (_event: MouseEvent) => { + _event.preventDefault(); + _event.stopPropagation(); + loadMore(); + editorRef.value?.popperRef?.onOpen(); + isClosePopper.value = true; + }; + + // 处理弹窗关闭逻辑 + const handlePopperClose = (_event: MouseEvent) => { + // 点击加载更多 + const isClickInside = loadmoreRef.value?.contains(_event.target); + // 点击输入框 + const isFocus = editorRef.value?.inputRef?.input.contains(_event.target); + if (!isClickInside && !isFocus && isClosePopper.value) { + editorRef.value?.popperRef?.onClose(); + isClosePopper.value = false; + } + }; + + // 获取弹窗中的滚动容器元素 + const getPopperScroll = (): IParams | void => { + return editorRef.value?.popperRef?.popperRef.contentRef.querySelector( + '.el-scrollbar__wrap', + ); + }; + + // 处理滚动加载 + const handleScrollLoad = async () => { + const infiniteScroll = getPopperScroll(); + + // 确保滚动容器存在 + if (!infiniteScroll) return; + + // 获取滚动容器的相关属性 + const { scrollTop, scrollHeight, clientHeight } = infiniteScroll; + + // 计算当前滚动位置与底部的距离(预留20px的缓冲) + const distanceToBottom = scrollHeight - scrollTop - clientHeight; + + // 当滚动到接近底部(距离小于等于20px)且不在加载中时,触发加载 + if (distanceToBottom <= 20) { + await loadMore(); + } + }; + + const debScrollLoad = debounce(handleScrollLoad, 300); + + // 初始化懒加载逻辑 + const initLazyLoad = () => { + switch (c.pagingMode) { + case 3: + document.addEventListener('mouseup', handlePopperClose); + break; + case 2: + getPopperScroll()?.addEventListener('scroll', debScrollLoad); + break; + default: + break; + } + }; + onMounted(() => { watch( () => props.data[c.valueItem], @@ -314,11 +421,26 @@ export const IBizPicker = defineComponent({ emit('change', null, c.model.id, true); } } + + initLazyLoad(); }, { immediate: true }, ); }); + onBeforeUnmount(() => { + switch (c.pagingMode) { + case 3: + document.removeEventListener('mouseup', handlePopperClose); + break; + case 2: + getPopperScroll()?.removeEventListener('scroll', debScrollLoad); + break; + default: + break; + } + }); + const renderActionItem = (detail: IAppDEUIActionGroupDetail) => { if (!c.groupActionState[detail.id!].visible) return; return ( @@ -355,6 +477,18 @@ export const IBizPicker = defineComponent({ ); }; + const renderLoadMore = () => { + return ( +
onLoadMoreClick(_event)} + > + {ibiz.i18n.t('editor.common.loadMore')} +
+ ); + }; + return { ns, c, @@ -374,6 +508,7 @@ export const IBizPicker = defineComponent({ handleKeyUp, setEditable, renderEmpty, + renderLoadMore, openLinkView, openPickUpView, renderActionItem, @@ -391,7 +526,7 @@ export const IBizPicker = defineComponent({ const panel = this.c.deACMode?.itemLayoutPanel; const { context, params } = this.c; let selected = - item[this.c.textName] || item.srfmajortext === this.curValue; + (item[this.c.textName] || item.srfmajortext) === this.curValue; if (this.c.valueItem) { selected = (item[this.c.keyName] || item.srfkey) === this.data[this.c.valueItem]; @@ -491,6 +626,7 @@ export const IBizPicker = defineComponent({ clearable popper-class={[ this.ns.e('transfer'), + this.ns.bm('popper', `${this.c.model.id}`), this.ns.is('empty', !this.items.length), ]} fetch-suggestions={this.onSearch} @@ -507,6 +643,7 @@ export const IBizPicker = defineComponent({ default: ({ item }: { item: IData }) => { if (this.$slots.append) return this.$slots.append({}); if (item.srftype === 'empty') return this.renderEmpty(); + if (item.srftype === 'loadmore') return this.renderLoadMore(); if (item.detailType === 'DEUIACTION') return this.renderActionItem(item as IAppDEUIActionGroupDetail); return itemContent(item); diff --git a/src/editor/data-picker/picker-editor.controller.ts b/src/editor/data-picker/picker-editor.controller.ts index 1509f8f2153a396729cc423c2dc24e89ac5383e3..e83b0c5356b67f5c75db4a4b3634c99191aeb7dd 100644 --- a/src/editor/data-picker/picker-editor.controller.ts +++ b/src/editor/data-picker/picker-editor.controller.ts @@ -19,6 +19,7 @@ import { IUIActionGroupDetail, IAppDEUIActionGroupDetail, } from '@ibiz/model-core'; +import { isNumber } from 'lodash-es'; import { mergeDeepLeft } from 'ramda'; /** @@ -118,6 +119,75 @@ export class PickerEditorController extends EditorController { */ public actionDetails: IAppDEUIActionGroupDetail[] = []; + /** + * 当前页 + * + * @type {number} + * @default 1 + * @memberof PickerEditorController + */ + curPage: number = 1; + + /** + * 总条数 + * + * @type {number} + * @default 0 + * @memberof PickerEditorController + */ + public total: number = 0; + + /** + * 总页数 + * + * @type {number} + * @memberof PickerEditorController + */ + public totalPages?: number; + + /** + * 自填模式分页大小 + * + * @type {number} + * @memberof PickerEditorController + */ + public pagingSize?: number; + + /** + * 自填模式分页模式配置:0:不分页,1:分页栏,2:滚动加载,3:加载更多 + * + * @type {number} + * @memberof PickerEditorController + */ + public pagingMode?: number; + + /** + * 是否懒加载 + * + * @readonly + * @type {boolean} + * @memberof PickerEditorController + */ + get isLazyLoad(): boolean { + return isNumber(this.pagingMode) && this.pagingMode !== 0; + } + + /** + * 是否显示加载更多 + * + * @readonly + * @type {boolean} + * @memberof PickerEditorController + */ + get isShowLoadMore(): boolean { + return !!( + this.isLazyLoad && + this.pagingMode === 3 && + this.totalPages && + this.curPage < this.totalPages + ); + } + protected async onInit(): Promise { super.onInit(); this.initParams(); @@ -134,7 +204,14 @@ export class PickerEditorController extends EditorController { ); if (this.deACMode) { // 自填模式相关 - const { minorSortAppDEFieldId, minorSortDir } = this.deACMode; + const { + minorSortAppDEFieldId, + minorSortDir, + pagingMode, + pagingSize, + } = this.deACMode; + this.pagingMode = pagingMode; + this.pagingSize = pagingSize; if (minorSortAppDEFieldId && minorSortDir) { this.sort = `${minorSortAppDEFieldId.toLowerCase()},${minorSortDir.toLowerCase()}`; } @@ -262,6 +339,9 @@ export class PickerEditorController extends EditorController { public async getServiceData( query: string, data: IData, + options?: { + isLoadMore?: boolean; + }, ): Promise> { const { context, params } = this.handlePublicParams( data, @@ -280,6 +360,23 @@ export class PickerEditorController extends EditorController { Object.assign(fixedParams, { query }); } Object.assign(fixedParams, { size: 1000 }); + if (this.isLazyLoad) { + // 初始加载需要重置分页 + const isLoadMore = options?.isLoadMore === true; + if (isLoadMore) { + this.curPage += 1; + } else { + this.curPage = 1; + } + } + + // 是懒加载并且有size才给page和size。size默认值给0就不传分页和大小 + if (this.isLazyLoad && this.pagingSize) { + Object.assign(fixedParams, { + size: this.pagingSize, + page: this.curPage - 1, + }); + } // 合并计算出来的参数和固定参数,以计算参数为准 const tempParams = mergeDeepLeft(params, fixedParams); if (this.interfaceName) { @@ -290,6 +387,15 @@ export class PickerEditorController extends EditorController { context, tempParams, ); + + if (res.headers) { + if (res.headers['x-total']) { + this.total = Number(res.headers['x-total']); + } + if (res.headers['x-total-pages']) { + this.totalPages = Number(res.headers['x-total-pages']); + } + } return res as IHttpResponse; } throw new RuntimeModelError( diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 60e3ef06e6ccc50a0b3a4d63724e48267f627cac..e967364cb40ca75f041fc51140fb67801db2af0e 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -657,6 +657,7 @@ export default { cancel: 'Cancel', fullscreen: 'Fullscreen', minimize: 'Minimize', + loadMore: 'Load more', }, cascader: { ibizCascader: { diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 51772b6f1c5a5bc29c934b9f20fda870b2243e91..6ad49fbd3281a759e2b696fa211d17bbbeb2d496 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -613,6 +613,7 @@ export default { cancel: '取消', fullscreen: '全屏', minimize: '最小化', + loadMore: '加载更多', }, cascader: { ibizCascader: {