diff --git a/CHANGELOG.md b/CHANGELOG.md index 79f25f77ef62f54f5db8434c3e17985c6cb5609e..50a03e7bfc88b244359d8faf2d60ed6f651414f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ ### Added +- 新增移动端分割容器组件 +- 新增数组编辑器 - 多数据、卡片支持simple模式 - 分页导航面板支持计数器、支持左侧、右侧、下方布局 - 新增注册多数据容器(仅数据)、传送占位组件 @@ -25,6 +27,7 @@ ### Change +- 更新视图头预置返回按钮和视图内容区返回顶部按钮显隐逻辑 - 更新所有编辑器在进行属性透传时,class和style属性不往下透传 - 卡片部件样式调整,高度撑满视图内容 - 数据关系分页布局激活样式适配主题颜色 diff --git a/src/common/index.ts b/src/common/index.ts index 156c3cecf0ab3ad62f3812dc57fe05339cb7d3b5..1b77d9b027e43504519740e6ade9f369d682cfa2 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -27,6 +27,8 @@ import { IBizCropping } from './cropping/cropping'; import { IBizActionGroup } from './action-group/action-group'; import { ViewMessage } from './view-message/view-message'; import { IBizMdSortSetting } from './md-sort-setting/md-sort-setting'; +import { IBizSplit } from './split/split'; +import { IBizSplitTrigger } from './split-trigger/split-trigger'; export * from './col/col'; export * from './row/row'; @@ -35,6 +37,8 @@ export * from './md-sort-setting/md-sort-setting'; export const IBizCommonComponents = { install: (v: App): void => { + v.component(IBizSplit.name!, IBizSplit); + v.component(IBizSplitTrigger.name!, IBizSplitTrigger); v.component(IBizActionGroup.name!, IBizActionGroup); v.component(IBizDateRangeCalendar.name!, IBizDateRangeCalendar); v.component(IBizViewShell.name!, IBizViewShell); diff --git a/src/common/preset-view-back/preset-view-back.tsx b/src/common/preset-view-back/preset-view-back.tsx index e6eb4cc57751ee0aa97429ce8a3bb7c69b8535e2..390b1567df89a580d51287726b2af290801d2dec 100644 --- a/src/common/preset-view-back/preset-view-back.tsx +++ b/src/common/preset-view-back/preset-view-back.tsx @@ -29,7 +29,16 @@ export const IBizPresetViewBack = defineComponent({ } }; - if (ibiz.config.view.mobShowPresetBack) { + if ( + Object.prototype.hasOwnProperty.call( + props.view.params, + 'srfmobshowpresetback', + ) + ) { + if (props.view.params.srfmobshowpresetback === true) { + initButtonVisible(); + } + } else if (ibiz.config.view.mobShowPresetBack) { initButtonVisible(); } diff --git a/src/common/split-trigger/split-trigger.scss b/src/common/split-trigger/split-trigger.scss new file mode 100644 index 0000000000000000000000000000000000000000..91caeef5625f749c617dc0ff5f7ec93af13378ec --- /dev/null +++ b/src/common/split-trigger/split-trigger.scss @@ -0,0 +1,62 @@ +$trigger-bar-background: getCssVar(color, border); +$trigger-width: 6px; +$trigger-bar-width: 4px; +$trigger-bar-offset: calc(($trigger-width - $trigger-bar-width) / 2); +$trigger-bar-interval: 3px; +$trigger-bar-weight: 1px; +$trigger-bar-con-height: calc(($trigger-bar-weight + $trigger-bar-interval) * 8); + +@include b(split-trigger) { + border: 1px solid #{getCssVar(color, border)}; + + @include m(vertical) { + width: $trigger-width; + height: 100%; + cursor: col-resize; + border-top: none; + border-bottom: none; + + @include b(split-trigger-bar) { + float: left; + width: $trigger-bar-width; + height: 1px; + margin-top: $trigger-bar-interval; + background: $trigger-bar-background; + } + } + + @include m(horizontal) { + width: 100%; + height: $trigger-width; + cursor: row-resize; + border-right: none; + border-left: none; + + @include b(split-trigger-bar) { + float: left; + width: 1px; + height: $trigger-bar-width; + margin-right: $trigger-bar-interval; + background: $trigger-bar-background; + } + } +} + +@include b(split-trigger-bar-con) { + position: absolute; + overflow: hidden; + + @include m(vertical) { + top: 50%; + left: $trigger-bar-offset; + height: $trigger-bar-con-height; + transform: translate(0, -50%); + } + + @include m(horizontal) { + top: $trigger-bar-offset; + left: 50%; + width: $trigger-bar-con-height; + transform: translate(-50%, 0); + } +} \ No newline at end of file diff --git a/src/common/split-trigger/split-trigger.tsx b/src/common/split-trigger/split-trigger.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c40ae49b7976596b6b47d48589aff9bb73a87b2f --- /dev/null +++ b/src/common/split-trigger/split-trigger.tsx @@ -0,0 +1,42 @@ +import { useNamespace } from '@ibiz-template/vue3-util'; +import { computed, defineComponent } from 'vue'; +import './split-trigger.scss'; + +export const IBizSplitTrigger = defineComponent({ + name: 'IBizSplitTrigger', + props: { + mode: String, + }, + setup(prop) { + const ns = useNamespace('split-trigger'); + const isVertical = computed(() => prop.mode === 'vertical'); + const classes = computed(() => [ + ns.b(), + isVertical.value ? ns.m('vertical') : ns.m('horizontal'), + ]); + const barConClasses = computed(() => [ + ns.b('bar-con'), + isVertical.value + ? ns.bm('bar-con', 'vertical') + : ns.bm('bar-con', 'horizontal'), + ]); + const items = Array(8).fill(0); + return { + ns, + classes, + barConClasses, + items, + }; + }, + render() { + return ( +
+
+ {this.items.map((_item, i) => ( + + ))} +
+
+ ); + }, +}); diff --git a/src/common/split/split.scss b/src/common/split/split.scss new file mode 100644 index 0000000000000000000000000000000000000000..b8f4896a289308f5d412013be134f38652964134 --- /dev/null +++ b/src/common/split/split.scss @@ -0,0 +1,104 @@ +$trigger-width: 6px; + +@include b(split-wrapper) { + position: relative; + width: 100%; + height: 100%; + + @include when(no-select) { + -webkit-touch-callout: none; + user-select: none; + } +} +@include b(split-pane) { + position: absolute; + + @include m(left) { + top: 0; + bottom: 0; + left: 0; + overflow: hidden; + } + + @include m(right) { + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + } + + @include m(top) { + top: 0; + right: 0; + left: 0; + overflow: hidden; + } + + @include m(bottom) { + right: 0; + bottom: 0; + left: 0; + overflow: hidden; + } + + @include m(moving) { + user-select: none; + } +} + +@include b(split-trigger-con) { + position: absolute; + z-index: 10; + width: 6px; + transform: translate(-50%, -50%); + touch-action: 'none'; +} + +@mixin horizontal-trigger-con-and-pane { + @include b(split-trigger-con) { + top: 50%; + height: 100%; + } + + @include b(split-pane) { + position: unset; + @include m(right){ + padding-left: $trigger-width; + } + } +} + +@mixin vertical-trigger-con-and-pane { + @include b(split-trigger-con) { + left: 50%; + width: 100%; + height: 0; + } + + @include b(split-pane) { + position: absolute; + @include m(bottom){ + padding-top: $trigger-width; + } + } +} + +@include b(split) { + @include m(horizontal) { + display: flex; + height: 100%; + @include horizontal-trigger-con-and-pane; + } + + @include m(vertical) { + @include vertical-trigger-con-and-pane; + } +} + +// 修复分割容器内存在其它分割容器时 分割容器(上下) 与 分割容器(左右) 样式互相影响,在此加权重 +.#{bem('split--vertical')} .#{bem('split--horizontal')} { + @include horizontal-trigger-con-and-pane; +} +.#{bem('split--horizontal')} .#{bem('split--vertical')} { + @include vertical-trigger-con-and-pane; +} \ No newline at end of file diff --git a/src/common/split/split.tsx b/src/common/split/split.tsx new file mode 100644 index 0000000000000000000000000000000000000000..801f83ac9abd17f4fb305c108345b96c63057657 --- /dev/null +++ b/src/common/split/split.tsx @@ -0,0 +1,250 @@ +import { useNamespace } from '@ibiz-template/vue3-util'; +import { + Ref, + computed, + defineComponent, + nextTick, + onBeforeUnmount, + onMounted, + ref, + watch, +} from 'vue'; +import './split.scss'; + +export const IBizSplit = defineComponent({ + name: 'IBizSplit', + props: { + modelValue: { + type: [Number, String], + default: 0.5, + }, + mode: { + validator: (value: string) => { + return ['horizontal', 'vertical'].includes(value); + }, + default: 'horizontal', + }, + min: { + type: [Number, String], + default: '30px', + }, + max: { + type: [Number, String], + default: '30px', + }, + }, + emits: ['update:modelValue', 'on-move-start', 'on-moving', 'on-move-end'], + setup(props, { emit }) { + const ns = useNamespace('split'); + + const outerWrapper: Ref = ref(null); + + const offset = ref(0); + const oldOffset: Ref = ref(0); + const isMoving = ref(false); + const computedMin: Ref = ref(0); + const computedMax: Ref = ref(0); + const currentValue: Ref = ref(0.5); + const initOffset = ref(0); + + const wrapperClasses = computed(() => [ + ns.b('wrapper'), + ns.is('no-select', isMoving.value), + ]); + const paneClasses = computed(() => [ + ns.b('pane'), + isMoving.value ? ns.bm('pane', 'moving') : '', + ]); + const isHorizontal = computed(() => props.mode === 'horizontal'); + const anotherOffset = computed(() => 100 - offset.value); + const valueIsPx = computed(() => typeof props.modelValue === 'string'); + const offsetSize = computed(() => + isHorizontal.value ? 'offsetWidth' : 'offsetHeight', + ); + + const px2percent = (numerator: string, denominator: string) => { + return parseFloat(numerator) / parseFloat(denominator); + }; + + const getComputedThresholdValue = (type: 'min' | 'max') => { + const size = outerWrapper.value![offsetSize.value]; + if (valueIsPx.value) { + return typeof props[type] === 'string' + ? props[type] + : size * (props[type] as number); + } + return typeof props[type] === 'string' + ? px2percent(props[type] as string, size as unknown as string) + : props[type]; + }; + + const getMax = (value1: string | number, value2: string | number) => { + if (valueIsPx.value) + return `${Math.max( + parseFloat(value1 as string), + parseFloat(value2 as string), + )}px`; + return Math.max(value1 as number, value2 as number); + }; + + const getAnotherOffset = (value: string | number) => { + let res: string | number = 0; + if (valueIsPx.value) + res = `${ + outerWrapper.value![offsetSize.value] - parseFloat(value as string) + }px`; + else res = 1 - (value as number); + return res; + }; + + const handleMove = (e: PointerEvent) => { + if (!isMoving.value) return false; + const pageOffset = isHorizontal.value ? e.pageX : e.pageY; + const moveOffset = pageOffset - initOffset.value; + const outerWidth = outerWrapper.value![offsetSize.value]; + let value = valueIsPx.value + ? `${parseFloat(oldOffset.value as string) + moveOffset}px` + : px2percent( + (outerWidth * (oldOffset.value as number) + + moveOffset) as unknown as string, + outerWidth as unknown as string, + ); + const anotherValue = getAnotherOffset(value); + if ( + parseFloat(value as string) <= parseFloat(computedMin.value as string) + ) { + value = getMax(value, computedMin.value); + } + if ( + parseFloat(anotherValue as string) <= + parseFloat(computedMax.value as string) + ) { + value = getAnotherOffset(getMax(anotherValue, computedMax.value)); + } + emit('update:modelValue', value); + emit('on-moving', e); + }; + + const handleUp = () => { + isMoving.value = false; + emit('on-move-end'); + }; + + const handleMousedown = (e: PointerEvent) => { + initOffset.value = isHorizontal.value ? e.pageX : e.pageY; + oldOffset.value = props.modelValue; + isMoving.value = true; + + emit('on-move-start'); + }; + + const computeOffset = () => { + nextTick(() => { + computedMin.value = getComputedThresholdValue('min'); + computedMax.value = getComputedThresholdValue('max'); + offset.value = + (((valueIsPx.value + ? px2percent( + props.modelValue as string, + outerWrapper.value![offsetSize.value] as unknown as string, + ) + : props.modelValue) as number) * + 10000) / + 100; + }); + }; + + watch( + () => props.modelValue, + (val: string | number) => { + if (val !== currentValue.value) { + currentValue.value = val; + computeOffset(); + } + }, + ); + + onMounted(() => { + nextTick(() => { + computeOffset(); + }); + + document.addEventListener('pointermove', handleMove); + document.addEventListener('pointerup', handleUp); + document.addEventListener('pointercancel', handleUp); + + window.addEventListener('resize', computeOffset); + }); + + onBeforeUnmount(() => { + window.removeEventListener('resize', computeOffset); + document.removeEventListener('pointermove', handleMove); + document.removeEventListener('pointerup', handleUp); + document.removeEventListener('pointercancel', handleUp); + }); + + return { + ns, + outerWrapper, + offset, + wrapperClasses, + paneClasses, + isHorizontal, + anotherOffset, + handleMousedown, + }; + }, + render() { + return ( +
+ {this.isHorizontal ? ( +
+
+ {this.$slots.left?.()} +
+
this.handleMousedown(e)} + > + {this.$slots.trigger?.() || } +
+
+ {this.$slots.right?.()} +
+
+ ) : ( +
+
+ {this.$slots.top?.()} +
+
this.handleMousedown(e)} + > + {this.$slots.trigger?.() || ( + + )} +
+
+ {this.$slots.bottom?.()} +
+
+ )} +
+ ); + }, +}); diff --git a/src/editor/array/array-editor.controller.ts b/src/editor/array/array-editor.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d19f0bc073a4858c975e10b452966b9f0d8457a --- /dev/null +++ b/src/editor/array/array-editor.controller.ts @@ -0,0 +1,10 @@ +import { EditorController } from '@ibiz-template/runtime'; +import { IArray } from '@ibiz/model-core'; + +/** + * 数组编辑器控制器 + * @return {*} + * @author: zhujiamin + * @Date: 2022-08-25 10:57:58 + */ +export class ArrayEditorController extends EditorController {} diff --git a/src/editor/array/array-editor.provider.ts b/src/editor/array/array-editor.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..14f519cb565ec898acb42b283bebbba9c63442a4 --- /dev/null +++ b/src/editor/array/array-editor.provider.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + IEditorContainerController, + IEditorProvider, +} from '@ibiz-template/runtime'; +import { IArray } from '@ibiz/model-core'; +import { ArrayEditorController } from './array-editor.controller'; + +/** + * 数组编辑器适配器 + * + * @author lxm + * @date 2022-09-19 22:09:03 + * @export + * @class ArrayEditorProvider + * @implements {EditorProvider} + */ +export class ArrayEditorProvider implements IEditorProvider { + formEditor: string = 'IBizArray'; + + gridEditor: string = 'IBizArray'; + + async createController( + editorModel: IArray, + parentController: IEditorContainerController, + ): Promise { + const c = new ArrayEditorController(editorModel, parentController); + await c.init(); + return c; + } +} diff --git a/src/editor/array/ibiz-array/ibiz-array.scss b/src/editor/array/ibiz-array/ibiz-array.scss new file mode 100644 index 0000000000000000000000000000000000000000..f69584f1a899df55a5327d966bc1653787bba44e --- /dev/null +++ b/src/editor/array/ibiz-array/ibiz-array.scss @@ -0,0 +1,12 @@ +@include b(array) { + height: 100%; + line-height: getCssVar(editor, default, line-height); + text-align: getCssVar(form-item-container, editor-align); + .van-field{ + line-height: getCssVar(editor, default, line-height); + } + + ion-icon { + cursor: pointer; + } +} diff --git a/src/editor/array/ibiz-array/ibiz-array.tsx b/src/editor/array/ibiz-array/ibiz-array.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f4df74611527ea48e3a71b8bf2fd97b90f9d4e3c --- /dev/null +++ b/src/editor/array/ibiz-array/ibiz-array.tsx @@ -0,0 +1,223 @@ +import { computed, defineComponent, Ref, ref, watch } from 'vue'; +import { + getArrayProps, + getEditorEmits, + useNamespace, + useFilterAttribute, +} from '@ibiz-template/vue3-util'; +import { createUUID } from 'qx-util'; +import { toNumber } from 'lodash-es'; +import { ArrayEditorController } from '../array-editor.controller'; +import './ibiz-array.scss'; + +/** + * 数组数据编辑 + * + * @description 使用van-field组件封装,提供数组数据的输入能力,其呈现样式为多个携带自增自减按钮的输入框。支持编辑器类型包含:`数组编辑器` + * @primary + * @editorparams {"name":"size","parameterType":"'large' | 'normal'","defaultvalue":"'normal'","description":"van-field组件的size属性"} + * @editorparams {"name":"limit","parameterType":"number","defaultvalue":0,"description":"默认不限制输入项数量,若设置了非零的限制数,当输入项数量超出该限制时,自增按钮将隐藏"} + * @editorparams {"name":"maxlength","parameterType":"number","description":"设置单个输入框可输入内容的最大长度"} + * @editorparams {"name":"showwordlimit","parameterType":"boolean","defaultvalue":false,"description":"是否显示字数限制统计,仅在设置了maxlength属性时生效"} + * @editorparams {"name":"triggermode","parameterType":"'blur' |' input'","defaultvalue":"'blur'","description":"指定编辑器触发 `change` 值变更事件的模式,input: 输入框输入时触发事件,blur:输入框blur时触发事件"} + * @editorparams {"name":"readonly","parameterType":"boolean","defaultvalue":false,"description":"设置编辑器是否为只读态"} + * @ignoreprops autoFocus | overflowMode + * @ignoreemits infoTextChange + */ +export const IBizArray = defineComponent({ + name: 'IBizArray', + props: getArrayProps(), + emits: getEditorEmits(), + setup(props, { emit }) { + const ns = useNamespace('array'); + + const c = props.controller!; + + const editorModel = c.model; + + // 输入框大小 + let size = 'default'; + // 数组大小限制 + let limit = 0; + // 输入内容最长长度 + let maxLength; + // 是否显示输入内容长度 + let showWordLimit = false; + // 输入内容项集合 + const items: Ref = ref([]); + + if (editorModel.editorParams) { + if (editorModel.editorParams.size) { + size = editorModel.editorParams.size; + } + if (editorModel.editorParams.limit) { + limit = toNumber(editorModel.editorParams.limit); + } + if (editorModel.editorParams.maxLength) { + maxLength = toNumber(editorModel.editorParams.maxLength); + } + if (editorModel.editorParams.maxlength) { + maxLength = toNumber(editorModel.editorParams.maxlength); + } + if (editorModel.editorParams.showWordLimit) { + showWordLimit = c.toBoolean(editorModel.editorParams.showWordLimit); + } + if (editorModel.editorParams.showwordlimit) { + showWordLimit = c.toBoolean(editorModel.editorParams.showwordlimit); + } + } + + // 输入框类型 + const dataType = editorModel.dataType; + const type = + Object.is(dataType, 'NUMBER') || Object.is(dataType, 'INTEGER') + ? 'number' + : 'text'; + + // 是否显示表单默认内容 + const showFormDefaultContent = computed(() => { + if ( + props.controlParams && + props.controlParams.editmode === 'hover' && + !props.readonly + ) { + return true; + } + return false; + }); + + watch( + () => props.value, + (newVal, oldVal) => { + if (newVal && newVal !== oldVal) { + if (items.value.length === 0) { + const tempItems = newVal.map((value: string | number) => { + return { value, key: createUUID() }; + }); + items.value = tempItems; + } + } + }, + { immediate: true }, + ); + + // 抛值 + const onEmit = (eventName: string = 'blur'): void => { + const result = items.value.map(item => item.value); + if (eventName === c.triggerMode) { + emit('change', result); + } + }; + + // 新增项 + const addItem = (index?: number): void => { + if (props.disabled || props.readonly) { + return; + } + const tempLink = { + key: createUUID(), + value: null, + }; + if (index) { + items.value.splice(index, 0, tempLink); + } else { + items.value.push(tempLink); + } + onEmit(); + }; + + // 删除项 + const removeItem = (index: number): void => { + items.value.splice(index, 1); + onEmit(); + }; + + // 处理值改变 + const handleInput = (): void => { + onEmit('input'); + }; + + const onBlur = (e: IData): void => { + onEmit('blur'); + emit('blur', e); + }; + + const onFocus = (e: IData): void => { + emit('focus', e); + }; + + return { + ns, + c, + type, + size, + limit, + maxLength, + showWordLimit, + items, + addItem, + removeItem, + handleInput, + onBlur, + onFocus, + showFormDefaultContent, + }; + }, + render() { + return ( +
+ {this.items.length === 0 ? ( + + ) : ( + this.items.map((item: IData, index: number) => { + return ( +
+ this.handleInput()} + {...useFilterAttribute(this.$attrs)} + > + {!(this.disabled || this.readonly) && ( +
+ {(!this.limit || this.items.length < this.limit) && ( + this.addItem(index + 1)} + /> + )} + this.removeItem(index)} + /> +
+ )} +
+ ); + }) + )} +
+ ); + }, +}); diff --git a/src/editor/array/index.ts b/src/editor/array/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8dfac96d5dca124003e115df1e2ab625391422bd --- /dev/null +++ b/src/editor/array/index.ts @@ -0,0 +1,3 @@ +export { IBizArray } from './ibiz-array/ibiz-array'; +export * from './array-editor.controller'; +export * from './array-editor.provider'; diff --git a/src/editor/index.ts b/src/editor/index.ts index 4999e3e2008108ae7dcd804cb15c59174e513994..e568c42ea7641300f66aaa1d7828e8f2a020938f 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -49,10 +49,12 @@ import { IBizDropdownList } from './dropdown-list/ibiz-dropdown-list/ibiz-dropdo import { IBizQrcode, QrcodeEditorProvider } from './qrcode'; import { IBizCheckbox, CheckBoxEditorProvider } from './check-box'; import { IBizMapPicker, MapPickerEditorProvider } from './map-picker'; +import { IBizArray, ArrayEditorProvider } from './array'; export const IBizEditor = { install: (v: App): void => { // 组件注册 + v.component(IBizArray.name, IBizArray); v.component(NotSupportedEditor.name, NotSupportedEditor); v.component(IBizInput.name, IBizInput); v.component(IBizInputNumber.name, IBizInputNumber); @@ -104,6 +106,10 @@ export const IBizEditor = { ), ); + // 数组编辑器 + registerEditorProvider('ARRAY', () => new ArrayEditorProvider()); + registerEditorProvider('MOBARRAY', () => new ArrayEditorProvider()); + // 标签 registerEditorProvider('SPAN', () => new SpanEditorProvider()); registerEditorProvider( diff --git a/src/panel-component/index.ts b/src/panel-component/index.ts index 86001d7db4f4eb2c0d433b406f1ebb73e500eacf..5b7163aaa8dacd7aefb4b8faf2704a77eefcd5b4 100644 --- a/src/panel-component/index.ts +++ b/src/panel-component/index.ts @@ -38,9 +38,11 @@ import IBizIndexBlankPlaceholder from './index-blank-placeholder'; import IBizViewMessage from './view-message'; import IBizViewMsgPos from './view-msg-pos'; import IBizSettingContainer from './setting-container'; +import IBizSplitContainer from './split-container'; export const IBizPanelComponents = { install: (v: App): void => { + v.use(IBizSplitContainer); v.use(IBizPanelCtrlViewPageCaption); v.use(IBizPanelContainer); v.use(IBizPanelCtrlPos); diff --git a/src/panel-component/split-container/index.ts b/src/panel-component/split-container/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..21f285a598232ac5037b5c320054b338f342c6dd --- /dev/null +++ b/src/panel-component/split-container/index.ts @@ -0,0 +1,25 @@ +import { App } from 'vue'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { registerPanelItemProvider } from '@ibiz-template/runtime'; +import { SplitContainer } from './split-container'; +import { SplitContainerProvider } from './split-container.provider'; +import { SplitContainerController } from './split-container.controller'; + +export { SplitContainerController }; + +export const IBizSplitContainer = withInstall( + SplitContainer, + function (v: App) { + v.component(SplitContainer.name, SplitContainer); + registerPanelItemProvider( + 'CONTAINER_CONTAINER_H_SPLIT', + () => new SplitContainerProvider(), + ); + registerPanelItemProvider( + 'CONTAINER_CONTAINER_V_SPLIT', + () => new SplitContainerProvider(), + ); + }, +); + +export default IBizSplitContainer; diff --git a/src/panel-component/split-container/split-container.controller.ts b/src/panel-component/split-container/split-container.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..a41ed963aa58265ac7132b825ac11acec4904268 --- /dev/null +++ b/src/panel-component/split-container/split-container.controller.ts @@ -0,0 +1,125 @@ +import { IPanelContainer } from '@ibiz/model-core'; +import { PanelContainerController } from '@ibiz-template/runtime'; +import { SplitContainerState } from './split-container.state'; + +/** + * 分割面板容器控制器 + * + * @author zhanghengfeng + * @date 2023-08-22 17:08:37 + * @export + * @class SplitContainerController + * @extends {PanelContainerController} + */ +export class SplitContainerController extends PanelContainerController { + /** + * @description 分割面板容器状态 + * @exposedoc + * @author zhanghengfeng + * @date 2023-10-08 17:10:25 + * @type {SplitContainerState} + */ + declare state: SplitContainerState; + + /** + * @description 分割面板模式 + * @exposedoc + * @author zhanghengfeng + * @date 2023-08-22 17:08:24 + * @type {('horizontal' | 'vertical')} + */ + splitMode: 'horizontal' | 'vertical' = 'horizontal'; + + /** + * @description 默认分割值 + * @exposedoc + * @author zhanghengfeng + * @date 2023-08-22 17:08:38 + * @type {(number | string)} + */ + splitValue: number | string = 0.5; + + /** + * @description 面板隐藏前分割值 + * @author zhanghengfeng + * @date 2023-10-08 17:10:58 + * @type {(number | string | null)} + */ + lastSplitValue: number | string | null = null; + + /** + * 初始化默认分割值 + * + * @author zhanghengfeng + * @date 2023-08-22 17:08:13 + * @param {number} value + * @param {string} mode + */ + initSplitValue(value: number, mode: string): void { + if (mode === 'PX') { + this.splitValue = `${value}px`; + } + if (mode === 'PERCENTAGE') { + this.splitValue = value / 100; + } + this.state.splitValue = this.splitValue; + } + + protected async onInit(): Promise { + await super.onInit(); + const { predefinedType, panelItems } = this.model; + this.splitMode = + predefinedType === 'CONTAINER_V_SPLIT' ? 'vertical' : 'horizontal'; + if (Array.isArray(panelItems) && panelItems.length) { + const panelItem = panelItems[0]; + const layoutPos = panelItem.layoutPos; + if (layoutPos) { + if (this.splitMode === 'horizontal') { + const { width, widthMode } = layoutPos; + if (width != null && widthMode != null) { + this.initSplitValue(width, widthMode); + } + } + if (this.splitMode === 'vertical') { + const { height, heightMode } = layoutPos; + if (height != null && heightMode != null) { + this.initSplitValue(height, heightMode); + } + } + } + } + } + + /** + * @description 隐藏面板,left:左侧面板隐藏,right:右侧面板隐藏,top:上方面板隐藏,bottom:底部面板隐藏 + * @exposedoc + * @author zhanghengfeng + * @date 2023-10-08 17:10:35 + * @param {('left' | 'right' | 'top' | 'bottom')} position + */ + hiddenPanel(position: 'left' | 'right' | 'top' | 'bottom'): void { + if (!this.state.isHiddenTrigger) + this.lastSplitValue = this.state.splitValue; + if (position === 'left' || position === 'top') { + this.state.splitValue = 0; + } + if (position === 'right' || position === 'bottom') { + this.state.splitValue = 1; + } + this.state.isHiddenTrigger = true; + } + + /** + * @description 显示面板,恢复上一次的分割比例 + * @exposedoc + * @author zhanghengfeng + * @date 2023-10-08 17:10:31 + */ + showPanel(): void { + if (this.lastSplitValue != null) { + this.state.splitValue = this.lastSplitValue; + this.state.isHiddenTrigger = false; + this.lastSplitValue = null; + } + } +} diff --git a/src/panel-component/split-container/split-container.provider.ts b/src/panel-component/split-container/split-container.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..6996a218fa3ab1d36eeb0830a8be38cf8703fa6a --- /dev/null +++ b/src/panel-component/split-container/split-container.provider.ts @@ -0,0 +1,30 @@ +import { + IPanelItemProvider, + PanelController, + PanelItemController, +} from '@ibiz-template/runtime'; +import { IPanelContainer } from '@ibiz/model-core'; +import { SplitContainerController } from './split-container.controller'; + +/** + * 分割面板容器适配器 + * + * @author zhanghengfeng + * @date 2023-08-22 17:08:20 + * @export + * @class SplitContainerProvider + * @implements {IPanelItemProvider} + */ +export class SplitContainerProvider implements IPanelItemProvider { + component: string = 'IBizSplitContainer'; + + async createController( + panelItem: IPanelContainer, + panel: PanelController, + parent: PanelItemController | undefined, + ): Promise { + const c = new SplitContainerController(panelItem, panel, parent); + await c.init(); + return c; + } +} diff --git a/src/panel-component/split-container/split-container.scss b/src/panel-component/split-container/split-container.scss new file mode 100644 index 0000000000000000000000000000000000000000..18f09bc775b6ed7d3772cebcd7897e9ef1dfe8d7 --- /dev/null +++ b/src/panel-component/split-container/split-container.scss @@ -0,0 +1,22 @@ +@include b(split-container) { + width: 100%; + height: 100%; + overflow: auto; + + @include when(hidden) { + display: none; + } + + @include when(hidden-trigger) { + >.#{bem('split-wrapper')} { + >.#{bem('split', '', 'horizontal')}, >.#{bem('split', '', 'vertical')} { + >.#{bem(split-trigger-con)} { + display: none; + +.#{bem('split-pane')} { + padding: 0; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/panel-component/split-container/split-container.state.ts b/src/panel-component/split-container/split-container.state.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c1b14c4ee5d74f9751fe35f9e87a0d970f5c5c2 --- /dev/null +++ b/src/panel-component/split-container/split-container.state.ts @@ -0,0 +1,26 @@ +import { PanelContainerState } from '@ibiz-template/runtime'; + +/** + * 分割面板容器状态 + * + * @author zhanghengfeng + * @date 2023-10-08 17:10:22 + * @export + * @class SplitContainerState + * @extends {PanelContainerState} + */ +export class SplitContainerState extends PanelContainerState { + /** + * @description 分割值 + * @exposedoc + * @type {(number | string)} + */ + splitValue: number | string = 0.5; + + /** + * @description 是否隐藏拖拽触发器,即不允许拖拽 + * @exposedoc + * @type {boolean} + */ + isHiddenTrigger: boolean = false; +} diff --git a/src/panel-component/split-container/split-container.tsx b/src/panel-component/split-container/split-container.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3b7babfda29daa91eda16ca74add994bfbb85c10 --- /dev/null +++ b/src/panel-component/split-container/split-container.tsx @@ -0,0 +1,68 @@ +import { useNamespace } from '@ibiz-template/vue3-util'; +import { PropType, VNode, computed, defineComponent } from 'vue'; +import { IPanelContainer } from '@ibiz/model-core'; +import { SplitContainerController } from './split-container.controller'; +import './split-container.scss'; + +/** + * 分割容器 + * @primary + * @description 将容器根据固定比例或像素分割为左右或上下两个区域,两个区域的大小可以通过拖拽改变。 + */ +export const SplitContainer = defineComponent({ + name: 'IBizSplitContainer', + props: { + /** + * @description 分割容器容器模型 + */ + modelData: { + type: Object as PropType, + required: true, + }, + /** + * @description 分割容器控制器 + */ + controller: { + type: SplitContainerController, + required: true, + }, + }, + setup(props) { + const ns = useNamespace('split-container'); + const { id } = props.modelData; + + const classArr = computed(() => { + let result: Array = [ns.b(), ns.m(id)]; + result = [ + ...result, + ...props.controller.containerClass, + ns.is('hidden', !props.controller.state.visible), + ns.is('hidden-trigger', props.controller.state.isHiddenTrigger), + ]; + return result; + }); + + return { + ns, + classArr, + }; + }, + render() { + const defaultSlots: VNode[] = this.$slots.default?.() || []; + return ( +
+ + {{ + left: () => defaultSlots[0], + right: () => defaultSlots[1], + top: () => defaultSlots[0], + bottom: () => defaultSlots[1], + }} + +
+ ); + }, +}); diff --git a/src/panel-component/view-content-panel-container/view-content-panel-container.tsx b/src/panel-component/view-content-panel-container/view-content-panel-container.tsx index 9dd2fe394abc89d1d76cb724f7fbb9102a326160..3505ac168ee0455eb9a74d3a0e57cd19e06c40cc 100644 --- a/src/panel-component/view-content-panel-container/view-content-panel-container.tsx +++ b/src/panel-component/view-content-panel-container/view-content-panel-container.tsx @@ -85,6 +85,23 @@ export const ViewContentPanelContainer: Component = defineComponent({ return result; }); const contentRef: Ref = ref(); + // 是否显示返回顶部按钮,如果视图参数中配置了srfshowbacktop为true,则显示返回顶部按钮, + // 否则按照全局配置判断,如果全局配置了显示返回按钮,则如果该视图在首页或者home下,则允许出返回按钮 + const showBackTop = computed(() => { + if ( + Object.prototype.hasOwnProperty.call( + props.controller.params, + 'srfshowbacktop', + ) + ) { + return props.controller.params.srfshowbacktop === true; + } + if (ibiz.config.mob.mobShowBackTop) { + return isScrollable; + } + return false; + }); + return { ns, viewModel, @@ -92,6 +109,7 @@ export const ViewContentPanelContainer: Component = defineComponent({ isScrollable, classArr, contentRef, + showBackTop, refresh, }; }, @@ -104,7 +122,7 @@ export const ViewContentPanelContainer: Component = defineComponent({ layout={this.modelData.layout} ref='contentRef' > - {this.isScrollable && this.contentRef && ( + {this.contentRef && this.showBackTop && ( )} {defaultSlots.map((slot: { props: IData }) => {