diff --git a/CHANGELOG.md b/CHANGELOG.md
index a01e2637fee8208337f94b8883bf6f073496d403..180346edd5886b663558d2a894f262b2018fefbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
- 数据选择、地址栏编辑器参数增强相关
- 支持实体多项数据选择视图(左右关系)
+- 支持分割面板容器
### Changed
diff --git a/src/common/index.ts b/src/common/index.ts
index ea67b2fa301718aefb3c5f7860979474c1448f9a..0dd78d96823e9336a7f7ce79eb227f89dcb88c5c 100644
--- a/src/common/index.ts
+++ b/src/common/index.ts
@@ -10,6 +10,8 @@ import { IBizViewShell } from './view-shell/view-shell';
import { IBizRawItem } from './rawitem/rawitem';
import { IBizCodeList } from './code-list/code-list';
import { IBizNoData } from './no-data/no-data';
+import { IBizSplit } from './split/split';
+import { IBizSplitTrigger } from './split-trigger/split-trigger';
export * from './icon/icon';
export * from './router-view/router-view';
@@ -23,6 +25,8 @@ export * from './rawitem/rawitem';
export * from './code-list/code-list';
export * from './code-list/code-list';
export * from './control-loading-placeholder/control-loading-placeholder';
+export * from './split/split';
+export * from './split-trigger/split-trigger';
export const IBizCommonComponents = {
install: (v: App): void => {
@@ -37,6 +41,8 @@ export const IBizCommonComponents = {
v.component(IBizRawItem.name, IBizRawItem);
v.component(IBizCodeList.name, IBizCodeList);
v.component(IBizNoData.name, IBizNoData);
+ v.component(IBizSplit.name, IBizSplit);
+ v.component(IBizSplitTrigger.name, IBizSplitTrigger);
},
};
diff --git a/src/common/split-trigger/split-trigger.scss b/src/common/split-trigger/split-trigger.scss
new file mode 100644
index 0000000000000000000000000000000000000000..76e6781ab887103e2a307c4f68aceb106f6d4f1e
--- /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: ($trigger-width - $trigger-bar-width) / 2;
+$trigger-bar-interval: 3px;
+$trigger-bar-weight: 1px;
+$trigger-bar-con-height: ($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..5f51282a6ae5d30dd6177334b36ee8d9e2deddf1
--- /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..335cd7c8d1a5ee3ecd00ee6ec36f4151c745f837
--- /dev/null
+++ b/src/common/split/split.scss
@@ -0,0 +1,66 @@
+@include b(split-wrapper) {
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ @include when(no-select) {
+ -webkit-touch-callout: none;
+ user-select: none;
+ }
+}
+
+@include b(split) {
+ @include m(horizontal) {
+ @include b(split-trigger-con) {
+ top: 50%;
+ width: 0;
+ height: 100%;
+ }
+ }
+
+ @include m(vertical) {
+ @include b(split-trigger-con) {
+ left: 50%;
+ width: 100%;
+ height: 0;
+ }
+ }
+}
+
+@include b(split-pane) {
+ position: absolute;
+
+ @include m(left) {
+ top: 0;
+ bottom: 0;
+ left: 0;
+ }
+
+ @include m(right) {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ @include m(top) {
+ top: 0;
+ right: 0;
+ left: 0;
+ }
+
+ @include m(bottom) {
+ right: 0;
+ bottom: 0;
+ left: 0;
+ }
+
+ @include m(moving) {
+ user-select: none;
+ }
+}
+
+@include b(split-trigger-con) {
+ position: absolute;
+ z-index: 10;
+ transform: translate(-50%, -50%);
+}
\ 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..c4028a1e3c0c896f6b3926907f53d43606481d95
--- /dev/null
+++ b/src/common/split/split.tsx
@@ -0,0 +1,252 @@
+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: MouseEvent) => {
+ 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));
+ }
+ Object.assign(e, {
+ atMin: props.modelValue === computedMin.value,
+ atMax: valueIsPx.value
+ ? getAnotherOffset(props.modelValue) === computedMax.value
+ : (getAnotherOffset(props.modelValue) as number).toFixed(5) ===
+ (computedMax.value as number).toFixed(5),
+ });
+ emit('update:modelValue', value);
+ emit('on-moving', e);
+ };
+
+ const handleUp = () => {
+ isMoving.value = false;
+ document.removeEventListener('mousemove', handleMove);
+ document.removeEventListener('mouseup', handleUp);
+ emit('on-move-end');
+ };
+
+ const handleMousedown = (e: MouseEvent) => {
+ initOffset.value = isHorizontal.value ? e.pageX : e.pageY;
+ oldOffset.value = props.modelValue;
+ isMoving.value = true;
+ document.addEventListener('mousemove', handleMove);
+ document.addEventListener('mouseup', handleUp);
+ 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();
+ });
+
+ window.addEventListener('resize', computeOffset);
+ });
+
+ onBeforeUnmount(() => {
+ window.removeEventListener('resize', computeOffset);
+ });
+
+ 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/panel-component/index.ts b/src/panel-component/index.ts
index 961585f57cc0fecd70fbcc8c65f66a3c189079d7..42f566a4d7f610a4f8cd61dfd51fb11cccca4aa6 100644
--- a/src/panel-component/index.ts
+++ b/src/panel-component/index.ts
@@ -17,6 +17,7 @@ import IBizGridContainer from './grid-container';
import IBizPanelViewContent from './panel-view-content';
import IBizPanelTabPanel from './panel-tab-panel';
import IBizPanelTabPage from './panel-tab-page';
+import IBizSplitContainer from './split-container';
export * from './panel-container';
export * from './panel-ctrl-pos';
@@ -31,6 +32,7 @@ export * from './panel-field';
export * from './panel-rawitem';
export * from './panel-tab-panel';
export * from './panel-tab-page';
+export * from './split-container';
export const IBizPanelComponents = {
install: (v: App): void => {
@@ -52,6 +54,7 @@ export const IBizPanelComponents = {
v.use(IBizGridContainer);
v.use(IBizPanelTabPanel);
v.use(IBizPanelTabPage);
+ v.use(IBizSplitContainer);
},
};
diff --git a/src/panel-component/split-container/index.ts b/src/panel-component/split-container/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd6e4cd571fb599ffc654f5865ac4cde62c26dd1
--- /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 { SplitContainerProvider, 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..048fe4c1903076b57460f5df19a59074ac7ff597
--- /dev/null
+++ b/src/panel-component/split-container/split-container.controller.ts
@@ -0,0 +1,73 @@
+import { IPanelContainer } from '@ibiz/model-core';
+import { PanelItemController } from '../../control';
+
+/**
+ * 分割面板容器控制器
+ *
+ * @author zhanghengfeng
+ * @date 2023-08-22 17:08:37
+ * @export
+ * @class SplitContainerController
+ * @extends {PanelItemController}
+ */
+export class SplitContainerController extends PanelItemController {
+ /**
+ * 分割面板模式
+ *
+ * @author zhanghengfeng
+ * @date 2023-08-22 17:08:24
+ * @type {('horizontal' | 'vertical')}
+ */
+ splitMode: 'horizontal' | 'vertical' = 'horizontal';
+
+ /**
+ * 默认分割值
+ *
+ * @author zhanghengfeng
+ * @date 2023-08-22 17:08:38
+ * @type {(number | string)}
+ */
+ splitValue: number | string = 0.5;
+
+ /**
+ * 初始化默认分割值
+ *
+ * @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;
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+ }
+}
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..1c6934cd904024f866338577ca866a2ff471feb9
--- /dev/null
+++ b/src/panel-component/split-container/split-container.provider.ts
@@ -0,0 +1,27 @@
+import { IPanelItemProvider } from '@ibiz-template/runtime';
+import { IPanelContainer } from '@ibiz/model-core';
+import { PanelController, PanelItemController } from '../../control';
+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..fe035b2ed28f5042e0e2e19bd06458d81fb192f6
--- /dev/null
+++ b/src/panel-component/split-container/split-container.scss
@@ -0,0 +1,9 @@
+@include b(split-container) {
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+
+ @include when(hidden) {
+ display: none;
+ }
+}
\ No newline at end of file
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..594409de0ca7853e80206b98ef8fd818d126f846
--- /dev/null
+++ b/src/panel-component/split-container/split-container.tsx
@@ -0,0 +1,56 @@
+import { useNamespace } from '@ibiz-template/vue3-util';
+import { PropType, VNode, computed, defineComponent, ref } from 'vue';
+import { IPanelContainer } from '@ibiz/model-core';
+import { SplitContainerController } from './split-container.controller';
+import './split-container.scss';
+
+export const SplitContainer = defineComponent({
+ name: 'IBizSplitContainer',
+ props: {
+ modelData: {
+ type: Object as PropType,
+ required: true,
+ },
+ 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),
+ ];
+ return result;
+ });
+
+ const splitValue = ref(props.controller.splitValue);
+
+ return {
+ ns,
+ classArr,
+ splitValue,
+ };
+ },
+ render() {
+ const defaultSlots: VNode[] = this.$slots.default?.() || [];
+ return (
+
+
+ {{
+ left: () => defaultSlots[0],
+ right: () => defaultSlots[1],
+ top: () => defaultSlots[0],
+ bottom: () => defaultSlots[1],
+ }}
+
+
+ );
+ },
+});