diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ac9164039f75c6eb2a033daa73f440af5630a3..06bf6ec5b45e2988a8b75e1c0ffc6a9b1e10de9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ - 新增树部件加载更多和节点绘制器 - 标签编辑器支持转化为代码项文本 - 表单视图消息支持直接内容展示,无效配置为对象格式 +- 新增加载更多按钮,用于多数据、卡片加载更多模式使用 +- 编辑表单支持分组锚点、表单项锚点,锚点位置支持右侧中间、右上角、右下角 ### Change @@ -85,6 +87,8 @@ - 优化搜索栏组件样式,不直接使用基础css变量,组件定义专属变量 - 统一处理界面行为按钮按钮类型、按钮行为级别、按钮样式 - 优化多数据选择编辑器的呈现样式 +- 多数据、卡片通用逻辑提取,支持滚动加载loading、加载完成提示、分组锚点 +- 多数据、卡片变量样式规范调整,部件统一使用useListRender ### Fixed diff --git a/src/common/add-more/add-more.scss b/src/common/add-more/add-more.scss new file mode 100644 index 0000000000000000000000000000000000000000..eee6a2efc7dc37320f9ddb931078e37bdb008c89 --- /dev/null +++ b/src/common/add-more/add-more.scss @@ -0,0 +1,14 @@ +$add-more: ( + color-bg: getCssVar(color, fill, 0), + color-text: getCssVar(color, primary), +); +@include b('add-more') { + @include set-component-css-var('add-more', $add-more); + width: 100%; + .van-button { + width: 100%; + border: none; + background-color: getCssVar(add-more, color-bg); + color: getCssVar(add-more, color-text); + } +} diff --git a/src/common/add-more/add-more.tsx b/src/common/add-more/add-more.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9721ea58c3fb76a510ea37f2b170fe1104684134 --- /dev/null +++ b/src/common/add-more/add-more.tsx @@ -0,0 +1,20 @@ +import { defineComponent } from 'vue'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import './add-more.scss'; + +export const IBizAddMore = defineComponent({ + name: 'IBizAddMore', + setup() { + const ns = useNamespace('add-more'); + return { ns }; + }, + render() { + return ( +
+ + {ibiz.i18n.t('control.common.loadMore')} + +
+ ); + }, +}); diff --git a/src/common/index.ts b/src/common/index.ts index 6a116581efc03bba0af63e7865641aec59a02ca9..dfd6b5ddf8c42a75614cd89363c5d178cc253f21 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -31,6 +31,7 @@ import { IBizSplit } from './split/split'; import { IBizSplitTrigger } from './split-trigger/split-trigger'; import { IBizMdAdvanedSearchfrom } from './md-advaned-searchform/md-advaned-searchform'; import { IBizAddBtn } from './add-btn/add-btn'; +import { IBizAddMore } from './add-more/add-more'; export * from './col/col'; export * from './row/row'; @@ -38,10 +39,12 @@ export * from './keep-alive/keep-alive'; export * from './md-sort-setting/md-sort-setting'; export * from './md-advaned-searchform/md-advaned-searchform'; export * from './add-btn/add-btn'; +export * from './add-more/add-more'; export const IBizCommonComponents = { install: (v: App): void => { v.component(IBizAddBtn.name!, IBizAddBtn); + v.component(IBizAddMore.name!, IBizAddMore); v.component(IBizMdAdvanedSearchfrom.name!, IBizMdAdvanedSearchfrom); v.component(IBizSplit.name!, IBizSplit); v.component(IBizSplitTrigger.name!, IBizSplitTrigger); diff --git a/src/control/data-view/data-view.scss b/src/control/data-view/data-view.scss index 2faf3ee74be8792e4e2193718eb212fdb1e60c63..5d99d07eb8a4a467edf1ad13028a81beeb1a5efa 100644 --- a/src/control/data-view/data-view.scss +++ b/src/control/data-view/data-view.scss @@ -1,26 +1,20 @@ $control-dataview: ( - text-color: getCssVar(color, text, 0), - item-padding: getCssVar(spacing, base-tight) getCssVar(spacing, base), - padding: getCssVar(spacing, tight), - item-gap: getCssVar(spacing, base, tight), - item-bg-color: transparent, - active-text-color: getCssVar(color, primary), - active-bg-color: getCssVar(color, primary, light, default), - group-font-size: getCssVar(font-size, header-6), - group-bg-color: getCssVar(color, bg, 0), - group-text-color: getCssVar(color, text, 1), - group-header-padding: getCssVar(spacing, base), - group-padding: 0 getCssVar(spacing, tight), - group-anchor-bg-color: getCssVar(color, bg, 1), - group-anchor-border-radius: getCssVar(border, radius, small), - group-anchor-right: getCssVar('spacing', 'tight'), - group-anchor-item-padding: getCssVar('spacing', 'tight') getCssVar('spacing', 'base'), - item-shadow: getCssVar(shadow, elevated), - button-padding: getCssVar(spacing, tight) 0, - button-bg: getCssVar(color, fill, 0), - button-active-bg: getCssVar(color, fill, 2), - button-color: getCssVar(color, primary), - add-border: 2px dashed getCssVar(color, border), + // Color + color-text: getCssVar(color, text, 0), + color-bg: getCssVar(color, bg, 0), + color-item-active: getCssVar(color, primary, light, default), + color-group-bg: getCssVar(color, bg, 0), + color-group-text: getCssVar(color, text, 1), + // Spacing + spacing-item-padding: getCssVar(spacing, base-tight) getCssVar(spacing, base), + spacing-padding: getCssVar(spacing, tight) getCssVar(spacing, base), + spacing-item-gap: getCssVar(spacing, base, tight), + spacing-group-padding: getCssVar(spacing, tight) getCssVar(spacing, base), + // Font + font-group-fontSize: getCssVar(font-size, header-6), + font-group-lineHeight: getCssVar(height-control, default), + // Other + shadow: getCssVar(shadow, elevated), ); @include b(control-dataview) { @@ -32,73 +26,79 @@ $control-dataview: ( @include e(content-container) { height: 100%; width: 100%; - padding: getCssVar(control-dataview, padding); - overflow-y: auto; } @include e(content) { display: flex; flex-wrap: wrap; - height: 100%; - gap: getCssVar(control-dataview, item-gap); + max-height: 100%; + gap: getCssVar(control-dataview, spacing-item-gap); + overflow-y: auto; + padding: getCssVar(control-dataview, spacing-padding); - @include when(enable-anchor) { - height: 100%; + .van-list__loading,.van-list__finished-text,.van-list__error-text { + width: 100%; } } @include e(row) { gap: 0; + margin: calc(-1 * getCssVar(control-dataview, spacing-item-gap)/ 2); .#{bem(control-dataview, item-col)} { - padding: calc(getCssVar(control-dataview, item-gap) / 2); + padding: calc(getCssVar(control-dataview, spacing-item-gap) / 2); } } - @include when(enable-page) { + @include when(enable-pagination) { display: flex; flex-direction: column; .#{bem(control-dataview, content-container)} { flex: auto; + height: 0; } .#{bem(control-dataview, pagination)} { flex: none; } } - @include when(enable-pagination) { + @include when(hidden-finished) { + // 启用分页时隐藏列表加载完成提示 + .van-list__finished-text { + display: none; + } + } + + @include when(enable-group) { .#{bem(control-dataview, content)} { - height: auto; + padding: 0; + } + .#{bem(control-dataview-group, item)} { + padding: getCssVar(control-dataview, spacing-padding); } } - @include e(load-more) { - padding: getCssVar(control-dataview, button-padding); - .van-button { - width: 100%; - border: none; - background-color: getCssVar(control-dataview, button-bg); - color: getCssVar(control-dataview, button-color); - &:active { - background-color: getCssVar(control-dataview, button-active-bg); - } + @include e(anchor) { + width: 100%; + .van-index-anchor { + padding: 0; } } } @include b(control-dataview-item) { flex: none; - padding: getCssVar(control-dataview, item-padding); - box-shadow: getCssVar(control-dataview, item-shadow); - background-color: getCssVar(control-dataview, item-bg-color); - color: getCssVar(control-dataview, text-color); + padding: getCssVar(control-dataview, spacing-item-padding); + box-shadow: getCssVar(control-dataview, shadow); + color: getCssVar(control-dataview, color-text); + background-color: getCssVar(control-dataview, color-bg); width: 100%; @include when(active) { - background-color: getCssVar(control-dataview, - item-active-color - ); + background-color: getCssVar(control-dataview, color-item-active); } >.van-card { - padding: 0 + padding: 0; + background-color: transparent; + color: inherit; } } @@ -109,77 +109,21 @@ $control-dataview: ( @include e('container') { width: 100%; height: 100%; - padding: getCssVar(control-dataview, group-padding); - overflow-y: auto; } @include e(item) { display: flex; flex-direction: column; - gap: getCssVar(control-dataview, item-gap); + gap: getCssVar(control-dataview, spacing-item-gap); } // 分组标题样式 @include e('caption') { - padding: getCssVar(control-dataview, group-header-padding); - font-size: getCssVar(control-dataview, group-font-size); - color: getCssVar(control-dataview, group-text-color); - background-color: getCssVar(control-dataview, group-bg-color); - } - - // 分组锚点容器样式 - @include e('anchor-container') { - position: absolute; - right: getCssVar(control-dataview, group-anchor-right); - top: 45%; - transform: translateY(-50%); - border-radius: getCssVar(control-dataview, group-anchor-border-radius); - overflow: hidden; - display: flex; - flex-direction: column; - background-color: getCssVar(control-dataview, group-anchor-bg-color); - box-shadow: - getCssVar(control-dataview, box-shadow-inner), - getCssVar(control-dataview, box-shadow-outer); - } - - // 分组锚点项样式 - @include e('anchor-item') { - width: 100%; - text-align: center; - padding: getCssVar(control-dataview, group-anchor-item-padding); - - @include when(active) { - color: getCssVar(control-dataview, active-text-color); - background-color: getCssVar(control-dataview, active-bg-color); - } - } -} - -// 分组锚点容器样式 -@include e('anchor-container') { - position: absolute; - right: getCssVar(control-dataview, group-anchor-right); - top: 45%; - transform: translateY(-50%); - border-radius: getCssVar(control-dataview, group-anchor-border-radius); - overflow: hidden; - display: flex; - flex-direction: column; - background-color: getCssVar(control-dataview, group-anchor-bg-color); - box-shadow: - getCssVar(control-dataview, item-shadow); -} - -// 分组锚点项样式 -@include e('anchor-item') { - width: 100%; - text-align: center; - padding: getCssVar(control-dataview, group-anchor-item-padding); - - @include when(active) { - color: getCssVar(control-dataview, active-text-color); - background-color: getCssVar(control-dataview, active-bg-color); + padding: getCssVar(control-dataview, spacing-group-padding); + font-size: getCssVar(control-dataview, font-group-fontSize); + line-height: getCssVar(control-dataview, font-group-lineHeight); + color: getCssVar(control-dataview, color-group-text); + background-color: getCssVar(control-dataview, color-group-bg); } } diff --git a/src/control/data-view/data-view.tsx b/src/control/data-view/data-view.tsx index 1d28739e80d57f3f0311dd390719e6625d31fa5d..3e0cb60b1edadfd69d7c7ce1882f9f0d49e941f7 100644 --- a/src/control/data-view/data-view.tsx +++ b/src/control/data-view/data-view.tsx @@ -1,17 +1,15 @@ import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; -import { computed, defineComponent, PropType, ref, VNode, watch } from 'vue'; +import { defineComponent, PropType, VNode } from 'vue'; import { IDEDataView, ILayoutPanel, IUIActionGroupDetail, } from '@ibiz/model-core'; import { - ControlVO, DataViewControlController, IControlProvider, } from '@ibiz-template/runtime'; -import { createUUID } from 'qx-util'; -import { usePagination } from '../../util'; +import { useListRender, usePagination } from '../../util'; import './data-view.scss'; export const DataViewControl = defineComponent({ @@ -58,106 +56,16 @@ export const DataViewControl = defineComponent({ (...args) => new DataViewControlController(...args), ); const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); - - const isUpdating = ref(false); - - const scrollContainer = ref(); - - // 是否可以加载更多 - const isLodeMoreDisabled = computed(() => { - if (c.model.enablePagingBar === true) { - return true; - } - if (c.model.pagingMode !== 2) { - return true; - } - return ( - c.state.items.length >= c.state.total || - c.state.isLoading || - c.state.total <= c.state.size - ); - }); - - const scrollKey = createUUID(); - const selectScrollKey = ref(); - - // 处理分组锚点项点击 - const handleGroupAnchorClick = (_id: string) => { - // 获取目标元素和滚动容器 - const targetElement = document.getElementById(_id); - const el = scrollContainer.value; - - if (targetElement && el) { - const targetTop = targetElement.offsetTop; - const containerTop = el.offsetTop; - const relativePosition = targetTop - containerTop; - - // 基于滚动容器进行滚动 - el.scrollTo({ - top: relativePosition, - behavior: 'smooth', - }); - selectScrollKey.value = _id; - } - }; - - // 本地数据模式 - const initSimpleData = (): void => { - if (!props.data) { - return; - } - c.state.items = (props.data as IData[]).map(item => new ControlVO(item)); - c.afterLoad({}, c.state.items); - }; - - // 添加动画帧,反正加载多次 - c.evt.on('onLoadSuccess', () => { - isUpdating.value = true; - window.requestAnimationFrame(() => { - isUpdating.value = false; - }); - selectScrollKey.value = ''; - }); - - c.evt.on('onCreated', async () => { - if (props.isSimple) { - initSimpleData(); - c.state.isSimple = true; - c.state.isLoaded = true; - } - }); - - watch( - () => props.data, - () => { - if (props.isSimple) { - initSimpleData(); - } - }, - { - deep: true, - }, - ); + const { + enableLoadMore, + renderNoData, + renderAddItem, + renderScrollList, + renderGroup, + } = useListRender(props, c, ns); const { onPageChange } = usePagination(c); - // 是否显示数据伸缩图标 - // 如果未开启分组,并且加载模式为【加载更多】,并且已经加载过一次更多,则为 true - const showCollapseOrExpandIcon = computed(() => { - return !c.model.enableGroup && c.model.pagingMode === 3; - }); - - const renderAddBtn = (group?: IData) => { - if (!c.enableNew) { - return; - } - return ( - c.onClickNew(event, group?.key)} - > - ); - }; - // 绘制项布局面板 const renderPanelItem = (item: IData, modelData: ILayoutPanel): VNode => { const { context, params } = c; @@ -198,35 +106,6 @@ export const DataViewControl = defineComponent({ ); }; - const renderNoData = (): VNode | undefined => { - // 未加载不显示无数据 - const { isLoaded } = c.state; - if (!isLoaded) { - return; - } - const ctrlModel = c.model.controls?.find(item => { - return item.name === `${c.model.name!}_quicktoolbar`; - }); - if (ctrlModel) { - return ( - - ); - } - return ( - isLoaded && ( - - ) - ); - }; - const renderDefaultItem = (item: IData) => { return ( @@ -265,10 +144,9 @@ export const DataViewControl = defineComponent({ Object.assign( cardStyle, ns.cssVarBlock({ - 'item-bg-color': `${item.bgcolor || ''}`, - 'item-font-color': `${item.fontcolor || ''}`, - 'item-hover-color': `${item.hovercolor || ''}`, - 'item-active-color': `${item.activecolor || ''}`, + 'color-bg': `${item.bgcolor || ''}`, + 'color-text': `${item.fontcolor || ''}`, + 'color-item-active': `${item.activecolor || ''}`, }), ); return ( @@ -282,13 +160,10 @@ export const DataViewControl = defineComponent({ ); }; - const renderContent = (items: IData[], group?: IData) => { - if (!items.length) { - return renderNoData(); - } + const renderContent = (items: IData[]) => { const { cardColMD } = c.model; if (cardColMD) { - return [ + return ( {items.map(item => { return ( @@ -297,110 +172,40 @@ export const DataViewControl = defineComponent({ ); })} - , - renderAddBtn(group), - ]; + + ); } return [ ...items.map(item => { return renderCard(item); }), - renderAddBtn(group), ]; }; - const renderGroup = () => { - const showGroupAnchor = c.state.groups.length > 1 && c.showGroupAnchor; - return [ -
-
- {c.state.groups.map((group, index) => { - const _id = `group-${scrollKey}-${index}`; - return [ -
- {group.caption} -
, -
- {renderContent(group.children, group)} -
, - ]; - })} -
- {showGroupAnchor ? ( -
- {c.state.groups.map((group, index) => { - const _id = `group-${scrollKey}-${index}`; - return ( -
handleGroupAnchorClick(_id)} - class={[ - ns.be('group', 'anchor-item'), - ns.is('active', selectScrollKey.value === _id), - ]} - > - {group.caption} -
- ); - })} -
- ) : null} -
, - ]; + const renderDefault = () => { + const result = []; + result.push(renderContent(c.state.items)); + if (c.enableNew) { + result.push(renderAddItem()); + } + return result; }; - // 绘制卡片内容 + // 绘制列表内容 const renderMDContent = () => { - const showGroupAnchor = c.state.groups.length > 1 && c.showGroupAnchor; - return ( - c.loadMore()} - > - {c.enableGroup ? renderGroup() : renderContent(c.state.items)} - - ); - }; - - // 加载更多 - const loadMoreIcon = () => { - return ( -
- c.loadMore()}> - {ibiz.i18n.t('control.common.loadMore')} - -
- ); - }; - - // 分页模式为点击加载时并且当前数量小于总数 - const renderLoadMore = () => { - let icon = null; - const loadMore = - c.state.items.length < c.state.total && c.state.total > c.state.size; - if (showCollapseOrExpandIcon.value && loadMore) { - icon = loadMoreIcon(); - } - return icon; + const slots = c.enableGroup + ? renderGroup({ children: renderContent }) + : renderDefault(); + return renderScrollList(slots); }; return { c, ns, - scrollContainer, - showCollapseOrExpandIcon, + enableLoadMore, onPageChange, renderNoData, renderMDContent, - renderLoadMore, }; }, render() { @@ -409,17 +214,20 @@ export const DataViewControl = defineComponent({ return (
- {this.c.state.isCreated && this.renderMDContent()} - {this.renderLoadMore()} + {this.c.state.isCreated && + (this.c.state.items.length > 0 + ? this.renderMDContent() + : this.renderNoData())}
{enablePagingBar ? ( = @@ -63,6 +63,9 @@ export const EditFormControl: ReturnType = const filter = ref(undefined); + // 所有启用了锚点的表单项 + const anchorList: Ref = ref([]); + if (props.isSimple) { if (props.simpleDataIndex || props.simpleDataIndex === 0) { c.setSimpleDataIndex(props.simpleDataIndex); @@ -99,6 +102,7 @@ export const EditFormControl: ReturnType = const detail = c.details[key]; detail.state = reactive(detail.state); }); + anchorList.value = c.anchorData; }); const handleInput = debounce( @@ -110,12 +114,18 @@ export const EditFormControl: ReturnType = { leading: true }, ); - return { c, ns, filter, handleInput }; + return { + c, + ns, + filter, + handleInput, + anchorList, + }; }, render() { - const { enableItemFilter } = this.c.model; - return ( + const { enableItemFilter, showFormNavBar } = this.c.model; + const content = (
{enableItemFilter && ( =
); + if (showFormNavBar) { + const { navBarSysCss, navBarPos } = this.c.model; + const items = this.anchorList.filter( + (item: IData) => item.pageId === this.c.state.activeTab, + ); + return ( + x.title)} + sticky={false} + class={[ + this.ns.e('anchor'), + navBarSysCss, + this.ns.em('anchor', navBarPos?.toLowerCase()), + ]} + > + {content} + + ); + } + return content; }, }); diff --git a/src/control/form/form-detail/form-group-panel/form-group-panel.tsx b/src/control/form/form-detail/form-group-panel/form-group-panel.tsx index 08abbd07c7bf29edb7d9bf62bed974f5c9d9330a..6df6fdaa3ad58b0b34a2d348747c47f1a70b4d8e 100644 --- a/src/control/form/form-detail/form-group-panel/form-group-panel.tsx +++ b/src/control/form/form-detail/form-group-panel/form-group-panel.tsx @@ -46,6 +46,7 @@ export const FormGroupPanel = defineComponent({ }, render() { const { state } = this.controller; + const { enableAnchor } = this.modelData; const defaultSlots: VNode[] = this.$slots.default?.() || []; const content = ( @@ -147,7 +148,11 @@ export const FormGroupPanel = defineComponent({ class={[classArr, this.ns.is('loading', this.controller.state.loading)]} onClick={(event: MouseEvent) => this.controller.onClick(event)} > - {header} + {enableAnchor ? ( + {header} + ) : ( + header + )}
{content}
{footer} {this.controller.state.loading ? ( diff --git a/src/control/form/form-detail/form-item/form-item-container/form-item-container.tsx b/src/control/form/form-detail/form-item/form-item-container/form-item-container.tsx index 019a5f615b211966a487b93e20c155f5ed3e997c..5e1818362569e15fb04619ff070e5fb1a6986540 100644 --- a/src/control/form/form-detail/form-item/form-item-container/form-item-container.tsx +++ b/src/control/form/form-detail/form-item/form-item-container/form-item-container.tsx @@ -99,6 +99,7 @@ export const IBizFormItemContainer = defineComponent({ }; const renderLabel = () => { + const { enableAnchor } = c.model; return ( )} - {c.labelCaption} + {enableAnchor ? ( + + {c.labelCaption} + + ) : ( + {c.labelCaption} + )} ); }, diff --git a/src/control/list/list/list.tsx b/src/control/list/list/list.tsx index 92d8cea0e223d7ad1f4195d7dcb83edce6cd6b97..c36814657118b1842653bb024f75a994cb6b00f5 100644 --- a/src/control/list/list/list.tsx +++ b/src/control/list/list/list.tsx @@ -2,8 +2,8 @@ import { IDEList } from '@ibiz/model-core'; import { defineComponent, PropType, ref } from 'vue'; import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; import { IControlProvider, ListController } from '@ibiz-template/runtime'; -import { useListRender } from '../list-render-util'; import './list.scss'; +import { useListRender } from '../../../util'; export const ListControl = defineComponent({ name: 'IBizListControl', diff --git a/src/control/list/md-ctrl/md-ctrl.scss b/src/control/list/md-ctrl/md-ctrl.scss index 8e21dfb3caaf99cff9f6cd93007672fad0d9e724..9a01c116c29881021fe5c6141add21b77450b2ea 100644 --- a/src/control/list/md-ctrl/md-ctrl.scss +++ b/src/control/list/md-ctrl/md-ctrl.scss @@ -1,139 +1,108 @@ $control-mobmdctrl: ( - font-size: getCssVar(font-size, regular), - text-color: getCssVar(color, text, 0), - border-color: getCssVar(color, border), - active-text-color: getCssVar(color, primary), - active-bg-color: getCssVar(color, primary, light, default), - padding: getCssVar(spacing, tight) getCssVar(spacing, base), - overflow: getCssVar(control, overflow), - box-shadow-inner: 0 0 0 rgba(0 0 0 / 30%), - box-shadow-outer: 0 rem(4px) rem(14px) rgba(0 0 0 / 10%), - img-width: getCssVar(width-icon, extra-large), - img-height: getCssVar(width-icon, extra-large), - img-radius: getCssVar(border-radius, extra-small), - img-padding: getCssVar(spacing, tight), - item-font-color: getCssVar(color, text, 0), - item-bg-color: getCssVar(color, bg, 2), - item-under-line-right: getCssVar(spacing, base), - item-under-line-left: getCssVar(spacing, base), - item-right-padding: 0 getCssVar('spacing', 'tight') 0 0 , - group-font-size: getCssVar(font-size, header-6), - group-bg-color: getCssVar(color, bg, 0), - group-text-color: getCssVar(color, text, 1), - group-padding: getCssVar(spacing, base), - group-anchor-bg-color: getCssVar(color, bg, 1), - group-anchor-active-bg-color: getCssVar(color, primary, light, default), - group-anchor-color: getCssVar(color, primary), - group-anchor-border-radius: getCssVar(border, radius, small), - group-anchor-right: getCssVar('spacing', 'tight'), - group-anchor-item-padding: getCssVar(spacing, base-tight) getCssVar(spacing, base), - group-anchor-max-width: 120px, - more-padding: getCssVar(spacing, tight) getCssVar(spacing, base), - more-bg: getCssVar(color, fill, 0), - more-active-bg: getCssVar(color, fill, 2), - more-color: getCssVar(color, primary), - pagination-height: getCssVar('height-control', 'large'), - simplelist-height: 250px, + // Color + color-text: getCssVar(color, text, 0), + color-active-bg: getCssVar(color, primary, light, default), + color-item-bg: getCssVar(color, bg, 2), + color-group-bg: getCssVar(color, bg, 0), + color-group-text: getCssVar(color, text, 1), + // Width/Height + width-img: getCssVar(width-icon, extra-large), + height-img: getCssVar(width-icon, extra-large), + // Spacing + spacing-item-padding: getCssVar(spacing, tight) getCssVar(spacing, base), + spacing-group-padding: getCssVar(spacing, tight) getCssVar(spacing, base), + spacing-img-padding: getCssVar(spacing, tight), + spacing-checkbox-gap: getCssVar('spacing', 'tight'), + spacing-border-padding: getCssVar(spacing, base), + // Radius + radius-img: getCssVar(border-radius, extra-small), + // Font + font-fontSize: getCssVar(font-size, regular), + font-group-fontSize: getCssVar(font-size, header-6), + font-group-lineHeight: getCssVar(height-control, default), ); @include b(control-mobmdctrl) { @include set-component-css-var(control-mobmdctrl, $control-mobmdctrl); height: 100%; - overflow: getCssVar(control-mobmdctrl, overflow); position: relative; // 列表容器样式 @include e(content) { width: 100%; - height: 99%; + height: 100%; + overflow-y: auto; @include when(show-under-line) { - // 列表项下划线样式 - .#{bem(control-mobmdctrl-item)}, - .#{bem(control-mobmdctrl-select-item)} { - position: relative; - &::after { - position: absolute; - box-sizing: border-box; - content: ''; - pointer-events: none; - right: getCssVar(control-mobmdctrl, item-under-line-right); - bottom: 0; - left: getCssVar(control-mobmdctrl, item-under-line-left); - border-bottom: 1px solid getCssVar(control-mobmdctrl, border-color); - transform: scaleY(0.5); - display: block; - } + .van-hairline--bottom:after { + left: calc(-50% + getCssVar(control-mobmdctrl, spacing-border-padding)*2); + right: calc(-50% + getCssVar(control-mobmdctrl, spacing-border-padding)*2); } } + .#{bem(add-more)} { + padding: getCssVar(control-mobmdctrl, spacing-item-padding); + } } // 启用分页,列表内容区需减去分页高度。分页时内容区出滚动条,避免滚动时页码偏移 @include when(enable-page) { + display: flex; + flex-direction: column; .#{bem(control-mobmdctrl, content)} { - height: calc(100% - getCssVar(control-mobmdctrl, pagination-height)); - overflow-y: auto; + flex: auto; + height: 0; + } + .#{bem(control-mobmdctrl, pagination)} { + flex: none; } } - // 启用加载更多时,部件整体高度应该由列表内容加上加载更多高度撑起来,继承父元素高度会导致滚动异常 - @include when(load-more) { - height: auto; - } - - // 启用分组时,应该由列表内容区出滚动条,避免滚动时锚点偏移 - @include when(enable-anchor) { - .#{bem(control-mobmdctrl, content)} { - overflow-y: auto; + @include when(hidden-finished) { + // 启用分页时隐藏列表加载完成提示 + .van-list__finished-text { + display: none; } } - // 加载更多样式 -@include e(load-more) { - padding: getCssVar(control-mobmdctrl, more-padding); - .van-button { - width: 100%; - border: none; - background-color: getCssVar(control-mobmdctrl, more-bg); - color: getCssVar(control-mobmdctrl, more-color); - &:active { - background-color: getCssVar(control-mobmdctrl, more-active-bg); + // 分组锚点容器样式 + @include e('anchor') { + .van-index-anchor { + padding: 0; } } } -} // 列表项样式 @include b(control-mobmdctrl-item) { // 部件为选择模式时,右侧复选框间距 @include e(right) { - padding: getCssVar(control-mobmdctrl, item-right-padding); + margin-right: getCssVar(control-mobmdctrl, spacing-checkbox-gap); } // 增加权重,避免ui组件本身样式影响 &.#{bem(control-mobmdctrl-item)} { - background-color: getCssVar(control-mobmdctrl, item-bg-color); - color: getCssVar(control-mobmdctrl, item-font-color); + background-color: getCssVar(control-mobmdctrl, color-item-bg); + color: getCssVar(control-mobmdctrl, color-text); height: auto; - font-size: getCssVar(control-mobmdctrl, font-size); - padding: getCssVar(control-mobmdctrl, padding); + font-size: getCssVar(control-mobmdctrl, font-fontSize); + padding: getCssVar(control-mobmdctrl, spacing-item-padding); } // 列表项选中样式 @include when(active) { &.#{bem(control-mobmdctrl-item)} { - background-color: getCssVar(control-mobmdctrl, item-active-color); + background-color: getCssVar(control-mobmdctrl, color-active-bg); } } } // 列表项左侧图片样式,默认绘制并且项数据image存在路径值时生效 @include b(control-mobmdctrl-image) { - width: getCssVar(control-mobmdctrl, img-width); - height: getCssVar(control-mobmdctrl, img-height); - margin-right: getCssVar(control-mobmdctrl, img-padding); - border-radius: getCssVar(control-mobmdctrl, img-radius); + width: getCssVar(control-mobmdctrl, width-img); + height: getCssVar(control-mobmdctrl, height-img); + margin-right: getCssVar(control-mobmdctrl, spacing-img-padding); + border-radius: getCssVar(control-mobmdctrl, radius-img); } // 分组样式 @@ -145,40 +114,11 @@ $control-mobmdctrl: ( // 分组标题样式 @include e('caption') { - padding: getCssVar(control-mobmdctrl, group-padding); - font-size: getCssVar(control-mobmdctrl, group-font-size); - color: getCssVar(control-mobmdctrl, group-text-color); - background-color: getCssVar(control-mobmdctrl, group-bg-color); - } - - // 分组锚点容器样式 - @include e('anchor-container') { - position: absolute; - right: getCssVar(control-mobmdctrl, group-anchor-right); - top: 45%; - transform: translateY(-50%); - border-radius: getCssVar(control-mobmdctrl, group-anchor-border-radius); - overflow: hidden; - font-size: getCssVar(control-mobmdctrl, font-size); - max-width: getCssVar(control-mobmdctrl, group-anchor-max-width); - display: flex; - flex-direction: column; - background-color: getCssVar(control-mobmdctrl, group-anchor-bg-color); - box-shadow: - getCssVar(control-mobmdctrl, box-shadow-inner), - getCssVar(control-mobmdctrl, box-shadow-outer); - } - - // 分组锚点项样式 - @include e('anchor-item') { - width: 100%; - text-align: center; - padding: getCssVar(control-mobmdctrl, group-anchor-item-padding); - - @include when(active) { - color: getCssVar(control-mobmdctrl, group-anchor-color); - background-color: getCssVar(control-mobmdctrl, group-anchor-active-bg-color); - } + padding: getCssVar(control-mobmdctrl, spacing-group-padding); + font-size: getCssVar(control-mobmdctrl, font-group-fontSize); + line-height: getCssVar(control-mobmdctrl, font-group-lineHeight); + color: getCssVar(control-mobmdctrl, color-group-text); + background-color: getCssVar(control-mobmdctrl, color-group-bg); } } @@ -187,18 +127,20 @@ $control-mobmdctrl: ( display: flex; align-items: center; justify-content: space-between; - color: getCssVar(control-mobmdctrl, item-font-color); - background-color: getCssVar(control-mobmdctrl, item-bg-color); - padding: getCssVar(control-mobmdctrl, padding); + padding: getCssVar(control-mobmdctrl, spacing-item-padding); @include e(left) { - padding: getCssVar(control-mobmdctrl, item-right-padding); + margin-right: getCssVar(control-mobmdctrl, spacing-checkbox-gap); + } + @include when(active) { + background-color: getCssVar(control-mobmdctrl, color-active-bg); } .#{bem(control-mobmdctrl-item)} { flex: 1; padding: 0; width: auto; + // 隐藏边框 &.#{bem(control-mobmdctrl-item)}::after { display: none; } diff --git a/src/control/list/md-ctrl/md-ctrl.tsx b/src/control/list/md-ctrl/md-ctrl.tsx index b3b5a7776de5d54edd6e2c40cd6c9e2ca3e139b8..b291d2b07ddd9fda554b47f1efbbce7929034b94 100644 --- a/src/control/list/md-ctrl/md-ctrl.tsx +++ b/src/control/list/md-ctrl/md-ctrl.tsx @@ -1,6 +1,5 @@ import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; -import { computed, defineComponent, PropType, ref, watch } from 'vue'; -import { debounce } from 'lodash-es'; +import { defineComponent, PropType } from 'vue'; import { IDEMobMDCtrl, IDETBUIActionItem, @@ -11,11 +10,8 @@ import { MDCtrlController, IMobMDCtrlRowState, getAllUIActionItems, - ControlVO, } from '@ibiz-template/runtime'; -import { createUUID } from 'qx-util'; -import { useListRender } from '../list-render-util'; -import { convertBtnType, usePagination } from '../../../util'; +import { convertBtnType, useListRender, usePagination } from '../../../util'; import './md-ctrl.scss'; export const MDCtrlControl = defineComponent({ @@ -74,122 +70,17 @@ export const MDCtrlControl = defineComponent({ setup(props) { const c = useControlController((...args) => new MDCtrlController(...args)); const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); - const { renderItem, renderNoData, renderLoadMore, renderAddItem } = - useListRender(props, c, ns); - - const listRef = ref(); - - const isUpdating = ref(false); - - // 不分页 0 分页栏 1 滚动加载 2 加载更多 3 - // 是否可以加载更多 - const isLodeMoreDisabled = computed(() => { - if (c.model.enablePagingBar === true) { - return true; - } - if (c.model.pagingMode !== 2) { - return true; - } - return ( - c.state.items.length >= c.state.total || - c.state.isLoading || - c.state.total <= c.state.size - ); - }); - - // 本地数据模式 - const initSimpleData = (): void => { - if (!props.data) { - return; - } - c.state.items = (props.data as IData[]).map(item => new ControlVO(item)); - c.afterLoad({}, c.state.items as ControlVO[]); - }; - - c.evt.on('onCreated', async () => { - if (props.isSimple) { - initSimpleData(); - c.state.isSimple = true; - c.state.isLoaded = true; - } - }); - - watch( - () => props.data, - () => { - if (props.isSimple) { - initSimpleData(); - } - }, - { - deep: true, - }, - ); + const { + enableLoadMore, + renderItem, + renderNoData, + renderAddItem, + renderScrollList, + renderGroup, + } = useListRender(props, c, ns); const { onPageChange } = usePagination(c); - // 排序值 - const sortVal = computed(() => { - if (c.state.sortQuery) { - const [key, order] = c.state.sortQuery.split(','); - return { key, order }; - } - return null; - }); - - // 处理排序配置回调 - const onSortChange = (sort: { key: string; order: 'asc' | 'desc' }) => { - c.setSort(sort.key, sort.order); - c.load({ isInitialLoad: true }); - }; - - // 加载更多 - const debounceLoadMore = debounce(async () => { - c.loadMore(); - }, 500); - - const scrollKey = createUUID(); - const selectScrollKey = ref(); - - // 处理分组锚点项点击 - const handleGroupAnchorClick = (_id: string) => { - // 获取目标元素和滚动容器 - const targetElement = document.getElementById(_id); - const listDom = listRef.value?.$el; - - if (targetElement && listDom) { - const targetTop = targetElement.offsetTop; - const containerTop = listDom.offsetTop; - const relativePosition = targetTop - containerTop; - - // 基于滚动容器进行滚动 - listDom.scrollTo({ - top: relativePosition, - behavior: 'smooth', - }); - selectScrollKey.value = _id; - } - }; - - const onLoadMore = () => { - debounceLoadMore(); - }; - - // 添加动画帧,反正加载多次 - c.evt.on('onLoadSuccess', () => { - isUpdating.value = true; - window.requestAnimationFrame(() => { - isUpdating.value = false; - }); - selectScrollKey.value = ''; - }); - - // 是否显示数据伸缩图标 - // 如果未开启分组,并且加载模式为【加载更多】,并且已经加载过一次更多,则为 true - const showCollapseOrExpandIcon = computed(() => { - return !c.model.enableGroup && c.model.pagingMode === 3; - }); - // 左滑界面行为组 const leftSlidingActionGroup = c.model.deuiactionGroup; // 右滑界面行为组 @@ -254,7 +145,7 @@ export const MDCtrlControl = defineComponent({ const renderDefault = () => { const result = []; result.push( - c.state.items.map((item: IData) => { + ...c.state.items.map((item: IData) => { return renderDefaultItem(item); }), ); @@ -264,91 +155,44 @@ export const MDCtrlControl = defineComponent({ return result; }; - const renderGroup = () => { - const showGroupAnchor = c.state.groups.length > 1 && c.showGroupAnchor; - return [ -
-
- {c.state.groups.map((group, index) => { - const _id = `group-${scrollKey}-${index}`; - return ( -
-
{group.caption}
- {group.children.map(item => { - return renderDefaultItem(item.data); - })} - {c.enableNew ? renderAddItem(group) : null} -
- ); - })} -
- {showGroupAnchor ? ( -
- {c.state.groups.map((group, index) => { - const _id = `group-${scrollKey}-${index}`; - return ( -
handleGroupAnchorClick(_id)} - class={[ - ns.be('group', 'anchor-item'), - ns.is('active', selectScrollKey.value === _id), - ]} - > - {group.caption} -
- ); - })} -
- ) : null} -
, - ]; + const renderGroupChildren = (children: IData[]) => { + return children.map(item => { + return renderDefaultItem(item.data); + }); }; // 绘制列表内容 const renderMDContent = () => { - return ( - onLoadMore()} - > - {c.enableGroup ? renderGroup() : renderDefault()} - - ); + const slots = c.enableGroup + ? renderGroup({ children: renderGroupChildren }) + : renderDefault(); + return renderScrollList(slots); }; return { c, ns, - listRef, + enableLoadMore, renderMDContent, renderNoData, - showCollapseOrExpandIcon, onPageChange, - renderLoadMore, - sortVal, - onSortChange, }; }, render() { - const enablePagingBar = - this.c.model.enablePagingBar && this.c.model.pagingMode === 1; + const enablePagingBar = this.c.model.enablePagingBar; return ( {this.c.state.isCreated && @@ -366,7 +210,6 @@ export const MDCtrlControl = defineComponent({ onChange={this.onPageChange} >
) : null} - {this.showCollapseOrExpandIcon && this.renderLoadMore()}
); }, diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index f1155eaf4a094fdb490d6f12e79c93e7c6cb3d64..b585af818759d78f33d7b72e91e541f69ae96ab8 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -79,7 +79,9 @@ export default { // 部件 control: { common: { - loadMore: 'Load more', + loadMore: 'Load more...', + loadFinish: 'I have made it to the end', + loadError: 'Loading failed. Click to reload', addbtn: 'Add', }, appmenu: { @@ -88,7 +90,6 @@ export default { customNav: 'Customize Navigation', save: 'Save', }, - dataView: { end: 'The end~' }, form: { noSupportDetailType: 'Form detail type not supported: {detailType} or corresponding provider cannot be found', diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index c3f0a3cc236e44b064ff4c80b2b0fcb5528e3a8d..1196bf57c993efdbf7406e17c784f449b9a7a507 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -63,10 +63,11 @@ export default { // 部件 control: { common: { - loadMore: '加载更多', + loadMore: '加载更多...', + loadFinish: '我已经到底啦', + loadError: '加载失败,点击重新加载', addbtn: '新增', }, - dataView: { end: '我已经到底啦~' }, appmenu: { more: '更多', bottomNav: '底部导航', @@ -111,7 +112,6 @@ export default { list: { expand: '展开', selectedData: '选中数据', - end: '我已经到底啦~', }, searchBar: { confirm: '确认', diff --git a/src/util/index.ts b/src/util/index.ts index 4fc48e9be04b00be0308ea430c7a0eb14c143b30..f5a33d7d79703a4f9920115e8925e1021bc91b4a 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -13,3 +13,4 @@ export * from './store'; export { usePopstateListener } from './use-popstate-util/use-popstate-util'; export { QrcodeUtil } from './qrcode-util/qrcode-util'; export { convertBtnType } from './button-util/button-util'; +export { useListRender } from './list-util/list-render-util'; diff --git a/src/control/list/list-render-util.tsx b/src/util/list-util/list-render-util.tsx similarity index 40% rename from src/control/list/list-render-util.tsx rename to src/util/list-util/list-render-util.tsx index 37e584e55e9f05e0a6d2ff5f0789e2ed578727fc..bdc82f78fbbe369cbaef929331aa85101d7b8e16 100644 --- a/src/control/list/list-render-util.tsx +++ b/src/util/list-util/list-render-util.tsx @@ -1,34 +1,91 @@ import { Namespace } from '@ibiz-template/core'; -import { ListController, MDCtrlController } from '@ibiz-template/runtime'; -import { VNode } from 'vue'; -import { ILayoutPanel } from '@ibiz/model-core'; +import { + ControlVO, + MDControlController, + MDCtrlController, +} from '@ibiz-template/runtime'; +import { computed, Ref, ref, VNode, watch } from 'vue'; +import { IDEMobMDCtrl, ILayoutPanel } from '@ibiz/model-core'; import { JSX } from 'vue/jsx-runtime'; -/** - * 列表绘制工具 - * - * @author zk - * @date 2023-12-06 03:12:00 - * @export - * @param {IData} props - * @param {(IMobMDCtrlController | IListController)} c - * @param {Namespace} ns - * @return {*} {({ - * renderItem: (row: IData) => VNode | undefined; - * render: () => VNode | null; - * renderNoData: () => VNode | undefined; - * })} - */ export function useListRender( props: IData, - c: MDCtrlController | ListController, + c: MDControlController, ns: Namespace, ): { + enableLoadMore: Ref; renderItem: (row: IData) => VNode | undefined; renderNoData: () => VNode | undefined; renderLoadMore: () => JSX.Element | null; renderAddItem: (group?: IData) => JSX.Element | null; + renderScrollList: (slots: IData) => JSX.Element | null; + renderGroup: (slots: IData) => JSX.Element; } { + const { + name, + enablePagingBar, + pagingMode, + controlStyle, + itemLayoutPanel, + controls = [], + emptyText, + emptyTextLanguageRes, + } = c.model as IDEMobMDCtrl; + + // 是否加载失败,用于列表下拉加载失败时点击重新加载 + const isLoadError = ref(false); + + // 是否加载完成,用于判断数据加载(启用分页栏或加载更多时禁用滚动加载) + const isLodeFinished = ref(enablePagingBar === true || pagingMode === 3); + + // 是否加载更多,根据已加载数据与总数据条数判断 + const enableLoadMore = computed(() => { + return c.state.items.length < c.state.total && c.state.total > c.state.size; + }); + + // 加载完成后计算是否存在剩余数据 + c.evt.on('onLoadSuccess', () => { + if (!enablePagingBar && pagingMode !== 3) { + isLodeFinished.value = + c.state.items.length >= c.state.total || c.state.total <= c.state.size; + } + }); + + c.evt.on('onLoadError', () => { + isLoadError.value = true; + // 加载失败时重新加载当前页 + c.state.curPage -= 1; + }); + + // 本地数据模式 + const initSimpleData = (): void => { + if (!props.data) { + return; + } + c.state.items = (props.data as IData[]).map(item => new ControlVO(item)); + c.afterLoad({}, c.state.items as ControlVO[]); + }; + + c.evt.on('onCreated', async () => { + if (props.isSimple) { + initSimpleData(); + c.state.isSimple = true; + c.state.isLoaded = true; + } + }); + + watch( + () => props.data, + () => { + if (props.isSimple) { + initSimpleData(); + } + }, + { + deep: true, + }, + ); + const isSelect = (row: IData) => { const findIndex = c.state.selectedData.findIndex(data => { return data.srfkey === row.srfkey; @@ -47,10 +104,9 @@ export function useListRender( Object.assign( cardStyle, ns.cssVarBlock({ - 'item-bg-color': `${row.bgcolor || ''}`, - 'item-font-color': `${row.fontcolor || ''}`, - 'item-hover-color': `${row.hovercolor || ''}`, - 'item-active-color': `${row.activecolor || ''}`, + 'color-bg': `${row.bgcolor || ''}`, + 'color-text': `${row.fontcolor || ''}`, + 'color-item-active': `${row.activecolor || ''}`, }), ); return cardStyle; @@ -94,6 +150,9 @@ export function useListRender( const { context, params } = c; const itemClass = calcItemClass(item); const itemStyle = calcItemStyle(item); + if (controlStyle !== 'EXTVIEW1') { + itemClass.push('van-hairline--bottom'); + } const content = ( +
{ - const panel = c.model.itemLayoutPanel; - return props.modelData.name !== 'simplelist' && panel - ? renderPanelItem(row, panel) + return props.modelData.name !== 'simplelist' && itemLayoutPanel + ? renderPanelItem(row, itemLayoutPanel) : renderItemContent(row); }; @@ -137,8 +195,8 @@ export function useListRender( if (!isLoaded) { return; } - const ctrlModel = c.model.controls?.find(item => { - return item.name === `${c.model.name!}_quicktoolbar`; + const ctrlModel = controls.find(item => { + return item.name === `${name!}_quicktoolbar`; }); if (ctrlModel) { return ( @@ -153,47 +211,114 @@ export function useListRender( return ( isLoaded && ( ) ); }; - // 加载更多 - const loadMoreIcon = () => { - return ( -
- c.loadMore()}> - {ibiz.i18n.t('control.common.loadMore')} - -
- ); - }; - - // 分页模式为点击加载时并且当前数量小于总数 + // 分页模式为加载更多时并且当前数量小于总数 const renderLoadMore = () => { let icon = null; - const loadMore = - c.state.items.length < c.state.total && c.state.total > c.state.size; - if (loadMore) { - icon = loadMoreIcon(); + if (pagingMode === 3 && enableLoadMore.value) { + icon = c.loadMore()}>; } return icon; }; // 添加项 const renderAddItem = (group?: IData) => { + if (!(c as MDCtrlController).enableNew) { + return null; + } + return ( + { + (c as MDCtrlController).onClickNew(event, group?.key); + }} + > + ); + }; + + const renderScrollList = (slots: IData) => { + if (!c.state.isLoaded) { + return null; + } return ( -
- { - c.onClickNew(event, group?.key); - }} - > + c.loadMore()} + > + {slots} + {renderLoadMore()} + + ); + }; + + const renderGroup = (slots: IData) => { + const showGroupAnchor = + c.state.groups.length > 1 && (c as MDCtrlController).showGroupAnchor; + const content = ( +
+
+ {c.state.groups.map(group => { + let header = ( +
{group.caption}
+ ); + if (showGroupAnchor) { + header = ( + + {header} + + ); + } + return ( +
+ {header} +
+ {slots.children && slots.children(group.children)} + {renderAddItem(group)} +
+
+ ); + })} +
); + if (showGroupAnchor) { + const indexList = c.state.groups.map(x => x.caption); + return ( + + {content} + + ); + } + return content; }; - return { renderNoData, renderItem, renderLoadMore, renderAddItem }; + return { + enableLoadMore, + renderNoData, + renderItem, + renderLoadMore, + renderAddItem, + renderScrollList, + renderGroup, + }; }