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 }) => {