diff --git a/CHANGELOG.md b/CHANGELOG.md index 045e92408a1ec5ea3555d88fb661922d8133e6d5..3038757a7eeee61eb96a605beac25f1a0f40ff2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - 识别全局参数mobFormItemAlignMode - 新增多数据部件排序设置通用组件 - 更新多数据部件适配排序配置 +- 新增关系栏部件并支持默认布局和上方(下拉列表)布局 ### Change diff --git a/src/control/drbar/drbar.controller.ts b/src/control/drbar/drbar.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2f9b30650cc9aeeb08b06f2e3661b20ad440794 --- /dev/null +++ b/src/control/drbar/drbar.controller.ts @@ -0,0 +1,576 @@ +/* eslint-disable no-param-reassign */ +import { + ControlController, + IDRBarController, + IDRBarEvent, + IDRBarState, + IDRBarItemsState, + IEditFormController, + calcNavParams, + Srfuf, + IPanelItemNavPosController, + hasSubRoute, + AppCounter, + CounterService, + IPanelItemController, + calcItemVisibleByCounter, + calcItemVisible, +} from '@ibiz-template/runtime'; +import { IDEDRBar, IDEDRBarItem } from '@ibiz/model-core'; +import { Router } from 'vue-router'; + +/** + * 数据关系栏控制器 + * + * @export + * @class DRBarController + * @extends {ControlController} + * @implements {IDRBarController} + */ +export class DRBarController + extends ControlController + implements IDRBarController +{ + /** + * 计数器对象 + * @author lxm + * @date 2024-01-18 05:12:35 + * @type {AppCounter} + */ + counter?: AppCounter; + + /** + * 导航占位控制器 + * + * @readonly + * @memberof DRBarController + */ + get navPos(): IPanelItemNavPosController { + return this.view.layoutPanel?.panelItems + .nav_pos as IPanelItemNavPosController; + } + + /** + * 导航视图容器控制器 + * @return {*} + * @author: zhujiamin + * @Date: 2024-01-25 16:03:00 + */ + get viewNavPos(): IPanelItemController | undefined { + return this.view.layoutPanel?.panelItems.view_nav_pos; + } + + /** + * 表单部件 + * + * @readonly + * @memberof DRBarController + */ + get form(): IEditFormController { + return this.view.getController('form') as IEditFormController; + } + + /** + * 是否是新建 + * @author lxm + * @date 2023-12-11 06:32:04 + * @readonly + * @type {boolean} + */ + get isCreate(): boolean { + return this.getData()[0].srfuf !== Srfuf.UPDATE; + } + + /** + * 获取数据 + * + * @return {*} {IData[]} + * @memberof DRBarController + */ + getData(): IData[] { + return this.form?.getData() || [{}]; + } + + /** + * Router 对象 + * + * @type {Router} + * @memberof DRTabController + */ + router!: Router; + + /** + * 设置 Router 对象 + * + * @param {Router} router + * @memberof DRTabController + */ + setRouter(router: Router): void { + this.router = router; + } + + /** + * 路由层级 + * + * @readonly + * @type {(number | undefined)} + * @memberof DRBarController + */ + get routeDepth(): number | undefined { + return this.view.modal.routeDepth; + } + + /** + * 初始化state的属性 + * + * @protected + * @memberof DRBarController + */ + protected initState(): void { + super.initState(); + this.state.drBarItems = []; + this.state.srfnav = ''; + this.state.isCalculatedPermission = false; + this.state.hideEditItem = !Object.is(this.model.hideEditItem, false); + } + + /** + * 创建完成 + * + * @return {*} {Promise} + * @memberof DRBarController + */ + async onCreated(): Promise { + await super.onCreated(); + await this.initCounter(); + } + + /** + * 通过计数器数据,计算项状态 + * + * @memberof DRBarController + */ + calcItemStateByCounter(): void { + this.state.drBarItems.forEach(item => { + if (item.children?.length) { + item.children.forEach(childItem => { + const visible = calcItemVisibleByCounter(childItem, this.counter); + if (visible !== undefined) { + childItem.visible = visible; + } + }); + // 有一个子显示的时候分组就是显示的 + item.visible = item.children.some(childItem => childItem.visible); + } else { + // 不是分组的时候直接计算 + const visible = calcItemVisibleByCounter(item, this.counter); + if (visible !== undefined) { + item.visible = visible; + } + } + }); + if (this.state.selectedItem) { + const { visible, defaultVisibleItem } = this.getItemVisibleState( + this.state.selectedItem, + ); + if (!visible && defaultVisibleItem) { + this.handleSelectChange(defaultVisibleItem.tag); + } + } + } + + /** + * 获取对应项的显示状态 + * + * @author zhanghengfeng + * @date 2024-05-16 17:05:15 + * @param {string} key + * @return {*} {{ + * visible: boolean; + * defaultVisibleItem?: IDRBarItemsState; + * }} + */ + getItemVisibleState(key: string): { + visible: boolean; + defaultVisibleItem?: IDRBarItemsState; + } { + let visible = true; + let defaultVisibleItem: IDRBarItemsState | undefined; + this.state.drBarItems.forEach(item => { + if (item.children) { + if (!defaultVisibleItem) { + defaultVisibleItem = item.children.find(child => child.visible); + } + const drBarItem = item.children.find(child => child.tag === key); + if (drBarItem) { + visible = !!drBarItem.visible; + } + } else { + if (!defaultVisibleItem && item.visible) { + defaultVisibleItem = item; + } + if (item.tag === key) { + visible = !!item.visible; + } + } + }); + + return { + visible, + defaultVisibleItem, + }; + } + + /** + * 计算关系界面组权限 + * + * @param {IDRBarItemsState} item 关系组成员 + * @memberof DRBarController + */ + async calcPermitted(item: IDRBarItemsState): Promise { + let permitted = true; + const data = this.getData()?.length ? this.getData()[0] : undefined; + const visible = await calcItemVisible( + item, + this.context, + this.params, + this.model.appDataEntityId!, + this.model.appId, + data, + ); + if (visible !== undefined) { + permitted = visible; + } + item.visible = permitted; + } + + /** + * 计算是否展示 + * + * @param {IData} item 关系组成员 + * @memberof DRBarController + */ + async calcDrBarItemsState(): Promise { + await Promise.all( + this.state.drBarItems.map(async item => { + if (item.children?.length) { + await Promise.all( + item.children.map(async childItem => { + await this.calcPermitted(childItem); + }), + ); + // 有一个子显示的时候分组就是显示的 + item.visible = item.children.some(childItem => childItem.visible); + } else { + // 不是分组的时候直接计算权限 + await this.calcPermitted(item); + } + }), + ); + this.calcItemStateByCounter(); + this.state.isCalculatedPermission = true; + } + + /** + * 加载完成 + * + * @return {*} {Promise} + * @memberof DRBarController + */ + async onMounted(): Promise { + await super.onMounted(); + if (this.form) { + this.form.evt.on('onLoadSuccess', async event => { + // 更新视图作用域数据和srfreadonly数据 + const data = event.data[0]; + this.view.state.srfactiveviewdata = data; + if (Object.prototype.hasOwnProperty.call(data, 'srfreadonly')) { + this.view.context.srfreadonly = data.srfreadonly; + } + await this.calcDrBarItemsState(); + this.handleFormChange(); + }); + this.form.evt.on('onLoadDraftSuccess', () => { + this.handleFormChange(); + }); + this.form.evt.on('onSaveSuccess', () => { + this.handleFormChange(); + }); + } + this.initDRBarItems(); + if (!this.form) { + await this.calcDrBarItemsState(); + } + } + + /** + * 处理表单数据变更 + * + * @memberof DRBarController + */ + handleFormChange(): void { + const disabled = this.isCreate; + this.setDRBarItemsState(this.state.drBarItems, disabled); + } + + /** + * 设置关系项状态 + * + * @param {IDRBarItemsState[]} drBarItems 关系项 + * @param {boolean} disabled 禁用状态 + * @memberof DRBarController + */ + setDRBarItemsState(drBarItems: IDRBarItemsState[], disabled: boolean): void { + drBarItems.forEach(item => { + // 排除首项 + if (item.tag !== this.model.uniqueTag) { + item.disabled = disabled; + } + if (item.children) { + this.setDRBarItemsState(item.children, disabled); + } + }); + } + + /** + * 初始化关系项数据 + * + * @memberof DRBarController + */ + initDRBarItems(): void { + const { dedrctrlItems, dedrbarGroups } = this.model; + const drBarItems: IDRBarItemsState[] = []; + + // 绘制编辑项 + if (!this.state.hideEditItem) { + const { + editItemCaption, + editItemCapLanguageRes, + editItemSysImage, + uniqueTag, + } = this.model; + let caption = editItemCaption; + if (editItemCapLanguageRes) { + caption = ibiz.i18n.t( + editItemCapLanguageRes.lanResTag!, + editItemCaption, + ); + } + // 编辑项 + drBarItems.push({ + tag: uniqueTag!, + caption, + disabled: false, + visible: !this.state.hideEditItem, + sysImage: editItemSysImage, + fullPath: this.router.currentRoute.value.fullPath, + }); + // 默认显示编辑项 + this.state.defaultItem = uniqueTag!; + } + + // 分组和关系项 + if (dedrbarGroups && dedrctrlItems) { + dedrbarGroups.forEach(group => { + const children = dedrctrlItems.filter( + item => (item as IDEDRBarItem).dedrbarGroupId === group.id, + ); + let groupCaption = group.caption; + if (group.capLanguageRes) { + groupCaption = ibiz.i18n.t( + group.capLanguageRes.lanResTag!, + group.caption, + ); + } + drBarItems.push({ + tag: group.id!, + visible: false, + caption: groupCaption, + sysImage: group.sysImage, + children: children.map(child => { + let itemCaption = child.caption; + if (child.capLanguageRes) { + itemCaption = ibiz.i18n.t( + child.capLanguageRes.lanResTag!, + child.caption, + ); + } + const { + sysImage, + counterId, + enableMode, + counterMode, + testScriptCode, + dataAccessAction, + testAppDELogicId, + } = child; + return { + tag: child.id!, + caption: itemCaption, + sysImage, + visible: false, // 默认不显示 + disabled: false, + counterId, + dataAccessAction, + enableMode, + testAppDELogicId, + testScriptCode, + counterMode, + }; + }), + }); + }); + } + + this.state.drBarItems = drBarItems; + // 路由模式下,且有子路由的时候不需要navpos跳转路由,只要做呈现 + const isRoutePushed = !!this.routeDepth && hasSubRoute(this.routeDepth); + const defaultSelect = (this.view.state as IData).srfnav + ? (this.view.state as IData).srfnav + : drBarItems[0].children![0].tag; + this.handleSelectChange(defaultSelect, isRoutePushed); + } + + /** + * 处理选中改变 + * + * @param {boolean} [isRoutePushed=false] + * @memberof DRBarController + */ + handleSelectChange(key: string, isRoutePushed: boolean = false): void { + const { selectedItem } = this.state; + if (key !== selectedItem) { + this.state.selectedItem = key; + const drBarItem = this.model.dedrctrlItems?.find(item => item.id === key); + if (drBarItem) { + this.setVisible('navPos'); + this.openNavPosView(drBarItem, isRoutePushed); + } else { + this.setVisible('form'); + if (this.routeDepth) { + this.router.push(this.state.drBarItems[0].fullPath!); + } + } + } + } + + /** + * 设置显示状态 + * + * @param {('form' | 'navPos')} ctrlName 显示的部件名称 + * @memberof DRBarController + */ + setVisible(ctrlName: 'form' | 'navPos'): void { + if (this.state.hideEditItem) { + // 不显示编辑项的时候不需要控制显示隐藏 + return; + } + const viewForm = this.view.layoutPanel?.panelItems.view_form; + if (ctrlName === 'form') { + if (viewForm) { + viewForm.state.visible = true; + viewForm.state.keepAlive = true; + } + if (this.viewNavPos) { + this.viewNavPos.state.visible = false; + this.viewNavPos.state.keepAlive = true; + } + } else { + if (viewForm) { + viewForm.state.visible = false; + viewForm.state.keepAlive = true; + } + if (this.viewNavPos) { + this.viewNavPos.state.visible = true; + this.viewNavPos.state.keepAlive = true; + } + } + } + + /** + * 准备参数 + * + * @param {IDEDRBarItem} drBarItem 关系项 + * @return {*} + * @memberof DRBarController + */ + prepareParams(drBarItem: IDEDRBarItem): { + context: IContext; + params: IParams; + } { + const { navigateContexts, navigateParams } = drBarItem; + const model = { + navContexts: navigateContexts, + navParams: navigateParams, + }; + const originParams = { + context: this.context, + params: this.params, + data: this.getData()[0], + }; + const { resultContext, resultParams } = calcNavParams(model, originParams); + const context = Object.assign(this.context.clone(), resultContext); + const params = { ...this.params, ...resultParams }; + return { context, params }; + } + + /** + * 打开导航占位视图 + * + * @param {IDEDRBarItem} drBarItem 关系项 + * @memberof DRBarController + */ + async openNavPosView( + drBarItem: IDEDRBarItem, + isRoutePushed = false, + ): Promise { + const { context, params } = this.prepareParams(drBarItem); + // 合并SrfNav + context.currentSrfNav = drBarItem.id!; + this.state.srfnav = drBarItem.id!; + this.navPos?.openView({ + key: drBarItem.id!, + context, + params, + viewId: drBarItem.appViewId, + isRoutePushed, + }); + } + + /** + * 初始化计数器 + * @author lxm + * @date 2024-01-18 05:12:02 + * @protected + * @return {*} {Promise} + */ + protected async initCounter(): Promise { + const { appCounterRefs } = this.model; + const appCounterRef = appCounterRefs?.[0]; + if (appCounterRef) { + this.counter = await CounterService.getCounterByRef( + appCounterRef, + this.context, + { ...this.params }, + ); + this.calcItemStateByCounter = this.calcItemStateByCounter.bind(this); + this.counter.onChange(this.calcItemStateByCounter); + } + } + + /** + * 监听组件销毁 + * + * @author zhanghengfeng + * @date 2024-04-10 19:04:43 + * @protected + * @return {*} {Promise} + */ + protected async onDestroyed(): Promise { + await super.onDestroyed(); + if (this.counter) { + this.counter.offChange(this.calcItemStateByCounter); + this.counter.destroy(); + } + } +} diff --git a/src/control/drbar/drbar.provider.ts b/src/control/drbar/drbar.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4c711351bc27b6d67648780fb9f05ca59bcda6e --- /dev/null +++ b/src/control/drbar/drbar.provider.ts @@ -0,0 +1,12 @@ +import { IControlProvider } from '@ibiz-template/runtime'; + +/** + * 数据关系栏适配器 + * + * @export + * @class DRBarProvider + * @implements {IControlProvider} + */ +export class DRBarProvider implements IControlProvider { + component: string = 'IBizDrBarControl'; +} diff --git a/src/control/drbar/drbar.scss b/src/control/drbar/drbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..994b3bbf5edf52ae24be78307093152f84a0a090 --- /dev/null +++ b/src/control/drbar/drbar.scss @@ -0,0 +1,96 @@ +@include b(control-drbar) { + padding: getCssVar(spacing, tight); + background-color: getCssVar(view, bg-color); + + .van-button { + border: none; + height: 100%; + background-color: transparent; + + .van-button__text { + display: flex; + align-items: center; + line-height: getCssVar(font-size, header-4); + font-size: getCssVar(font-size, header-4); + gap: getCssVar(spacing, extra-tight); + } + } + + @include e(default) { + width: auto; + max-width: rem(100px); + .#{bem(icon)} { + @include flex(row, center, center); + + width: getCssVar(width-icon, medium); + height: getCssVar(width-icon, medium); + font-size: getCssVar(width-icon, medium); + } + @include m(group) { + color: getCssVar(color, text-2); + padding: getCssVar(spacing, tight); + } + @include m(group-title) { + display: flex; + align-items: center; + gap: getCssVar(spacing, tight); + } + @include m(item-title) { + display: flex; + align-items: center; + gap: getCssVar(spacing, tight); + } + } + + @include e(popover) { + @include m(group) { + display: flex; + cursor: pointer; + position: relative; + align-items: center; + box-sizing: border-box; + height: var(--van-popover-action-height); + padding: 0 getCssVar(spacing, base-loose); + font-size: getCssVar(font-size, header-5); + gap: getCssVar(spacing, tight); + color: getCssVar(color, text-2); + } + @include m(item) { + display: flex; + cursor: pointer; + position: relative; + align-items: center; + box-sizing: border-box; + height: var(--van-popover-action-height); + padding: 0 getCssVar(spacing, base-loose); + font-size: getCssVar(font-size, header-5); + gap: getCssVar(spacing, tight); + + .#{bem(icon)} { + @include flex(row, center, center); + + width: getCssVar(width-icon, medium); + height: getCssVar(width-icon, medium); + font-size: getCssVar(width-icon, medium); + } + + .caption { + flex-grow: 1; + display: flex; + align-items: center; + gap: getCssVar(spacing, tight); + } + + ion-icon { + color: getCssVar(color, primary); + } + + @include when(disabled) { + cursor: not-allowed; + pointer-events: none; + color: getCssVar(color, disabled-text); + background-color: getCssVar(color, disabled-bg); + } + } + } +} diff --git a/src/control/drbar/drbar.tsx b/src/control/drbar/drbar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e4ed04ef51b8f6f55b142bc16a41585f725c2568 --- /dev/null +++ b/src/control/drbar/drbar.tsx @@ -0,0 +1,260 @@ +import { useRouter } from 'vue-router'; +import { IAppDETabExplorerView, IDEDRBar } from '@ibiz/model-core'; +import { + Ref, + ref, + computed, + PropType, + reactive, + onUnmounted, + defineComponent, +} from 'vue'; +import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; +import { IControlProvider, IDRBarItemsState } from '@ibiz-template/runtime'; +import { DRBarController } from './drbar.controller'; +import './drbar.scss'; + +export const DRBarControl = defineComponent({ + name: 'IBizDrBarControl', + props: { + modelData: { type: Object as PropType, required: true }, + context: { type: Object as PropType, required: true }, + params: { type: Object as PropType, default: () => ({}) }, + provider: { type: Object as PropType }, + srfnav: { type: String, required: false }, + showMode: { type: String, default: 'vertical' }, + hideEditItem: { type: Boolean, default: undefined }, + }, + setup() { + const c: DRBarController = useControlController( + (...args) => new DRBarController(...args), + ); + const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + const router = useRouter(); + + c.setRouter(router); + + const counterData = reactive({}); + + const showPopover: Ref = ref(false); + + const tabPosition = + (c.view.model as IAppDETabExplorerView).tabLayout?.toLowerCase() || 'top'; + + const drBarItems = computed(() => { + const items: IDRBarItemsState[] = []; + c.state.drBarItems.forEach(drBar => { + if (drBar.children) { + items.push(...drBar.children); + } + }); + return items; + }); + + const activeBar = computed(() => { + return drBarItems.value.find(tab => tab.tag === c.state.selectedItem); + }); + + const sidebar: Ref = ref(); + + const sidebarIndex = computed({ + get: () => { + if (sidebar.value === undefined) { + return drBarItems.value.findIndex( + tab => tab.tag === c.state.selectedItem, + ); + } + return sidebar.value; + }, + set: (val: number) => { + sidebar.value = val; + }, + }); + + const fn = (counter: IData) => { + Object.assign(counterData, counter); + }; + + c.evt.on('onCreated', () => { + if (c.counter) { + c.counter.onChange(fn, true); + } + }); + + /** + * 选中改变 + * @param name + */ + const onSelectChange = (name: string) => { + showPopover.value = false; + c.handleSelectChange(name); + }; + + /** + * 打开/关闭 Popover + * @param e + */ + const onChangePopover = (e: MouseEvent) => { + e.stopPropagation(); + showPopover.value = !showPopover.value; + }; + + onUnmounted(() => { + c.counter?.offChange(fn); + }); + + const renderDropdownList = () => { + return ( + + {{ + default: () => { + return ( +
+ {c.state.drBarItems.map(bar => { + if (bar.visible) { + return [ +
+ {bar.sysImage && } +
+ {bar.caption} +
+
, + bar.children?.map(child => { + if (child.visible) { + return ( +
onSelectChange(child.tag)} + > + {child.sysImage && ( + + )} +
+ {child.caption} + {child.counterId && ( + + )} +
+ {child.tag === c.state.selectedItem && ( + + )} +
+ ); + } + return null; + }), + ]; + } + return null; + })} +
+ ); + }, + reference: () => { + return ( + + {activeBar.value?.sysImage && ( + + )} + + {activeBar.value?.caption} + {activeBar.value?.counterId && ( + + )} + + + + ); + }, + }} +
+ ); + }; + + const renderDefault = () => { + return ( + + {c.state.drBarItems.map(bar => { + if (bar.visible) { + return [ +
+
+ {bar.sysImage && } +
{bar.caption}
+
+
, + bar.children?.map(child => { + if (child.visible) { + return ( + + {{ + title: () => { + return ( +
onSelectChange(child.tag)} + > + {child.sysImage && ( + + )} +
{child.caption}
+
+ ); + }, + }} +
+ ); + } + return null; + }), + ]; + } + return null; + })} +
+ ); + }; + + return { + c, + ns, + tabPosition, + renderDefault, + renderDropdownList, + }; + }, + render() { + const { isCreated, isCalculatedPermission } = this.c.state; + return ( + + {isCreated && + isCalculatedPermission && + this.tabPosition === 'top_dropdownlist' + ? this.renderDropdownList() + : this.renderDefault()} + + ); + }, +}); diff --git a/src/control/drbar/index.ts b/src/control/drbar/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..76b8c0511c5e5e4ee5217139a3a48a8dfeb482f9 --- /dev/null +++ b/src/control/drbar/index.ts @@ -0,0 +1,12 @@ +import { App } from 'vue'; +import { registerControlProvider, ControlType } from '@ibiz-template/runtime'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { DRBarControl } from './drbar'; +import { DRBarProvider } from './drbar.provider'; + +export const IBizDRBarControl = withInstall(DRBarControl, function (v: App) { + v.component(DRBarControl.name!, DRBarControl); + registerControlProvider(ControlType.DRBAR, () => new DRBarProvider()); +}); + +export default IBizDRBarControl; diff --git a/src/control/index.ts b/src/control/index.ts index f5ee577136bf1445de761695f060a44bcad31a5a..17431d818880875d2589de3f2335edf0eb3c192f 100644 --- a/src/control/index.ts +++ b/src/control/index.ts @@ -17,3 +17,4 @@ export * from './chart'; export * from './calendar'; export * from './wizard-panel'; export * from './tree-exp-bar'; +export * from './drbar'; diff --git a/src/ibiz-vue3.ts b/src/ibiz-vue3.ts index 4593890050ccac9587d81be4bd141b01a7bfd0b5..b424109b1435283853266089e228b9b3a82ebe90 100644 --- a/src/ibiz-vue3.ts +++ b/src/ibiz-vue3.ts @@ -29,14 +29,15 @@ import { IBizAppMenuIconViewControl, IBizAppMenuListViewControl, IBizTreeExpBarControl, + IBizDRBarControl, } from './control'; import { iBizI18n } from './locale'; import IBizPanelComponents from './panel-component'; import { VueBrowserPlatformProvider } from './platform'; import { IBizPortalView } from './view/portal-view'; -import './style/index.scss'; import { IBizViewEngine } from './view-engine'; import IBizEditor from './editor'; +import './style/index.scss'; export default { install: (v: App): void => { @@ -79,6 +80,7 @@ export default { v.use(IBizCalendarControl); v.use(IBizWizardPanelControl); v.use(IBizTreeExpBarControl); + v.use(IBizDRBarControl); // 编辑器 v.use(IBizEditor); },