diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fddc5ca0237a98b6bcc67faa3fad0afd6ecae9d..21c8604dd3ddb7b5c565b41bc055f6cc7b3fd2dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### Changed - 修改全局配置里首页禁用分页导航参数名称,从noNavTabs改成disableHomeTabs +- 看板部件增强自动分组、代码表分组逻辑及UI呈现 +- 新增甘特图部件 ## [0.4.12] - 2023-12-14 diff --git a/ibiz.config.ts b/ibiz.config.ts index be43aa926f58d5424d7f99cacbe0afdcc250bf0d..2624a8a57b38347f9007942c8dd7519e37a06c13 100644 --- a/ibiz.config.ts +++ b/ibiz.config.ts @@ -59,5 +59,6 @@ export default defineConfig({ '@wangeditor/editor-for-vue', '@imengyu/vue3-context-menu', '@ibiz-template-plugin/ai-chat', + '@ibiz-template-plugin/gantt', ], }); diff --git a/package.json b/package.json index b5047789d6cc4ea90ec55d0f44432ef251a3a9fa..78db11f7f69502d4495712893dc3295d84ba7977 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dependencies": { "@floating-ui/dom": "^1.5.3", "@ibiz-template-plugin/ai-chat": "^0.0.1", + "@ibiz-template-plugin/gantt":"0.0.10-alpha.14", "@ibiz-template/core": "^0.4.12", "@ibiz-template/model-helper": "^0.4.12", "@ibiz-template/runtime": "^0.4.12", diff --git a/src/control/gantt/gantt.tsx b/src/control/gantt/gantt.tsx index b7b98626afa04c7474492f033e5f860a665980aa..62c3caae1d0c04c99653c525f2960b2777152a14 100644 --- a/src/control/gantt/gantt.tsx +++ b/src/control/gantt/gantt.tsx @@ -1,7 +1,38 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-restricted-syntax */ import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; -import { defineComponent, PropType } from 'vue'; -import { IDEGantt } from '@ibiz/model-core'; -import { IControlProvider, GanttController } from '@ibiz-template/runtime'; +import { + computed, + defineComponent, + PropType, + resolveComponent, + VNode, + h, +} from 'vue'; +import { + IDEGantt, + IDETBGroupItem, + IDETBRawItem, + IDETBUIActionItem, + IDEToolbarItem, + IDETreeColumn, + IPanel, +} from '@ibiz/model-core'; +import { + IControlProvider, + GanttController, + IGanttNodeData, + IButtonState, + IButtonContainerState, +} from '@ibiz-template/runtime'; +import { MenuItem } from '@imengyu/vue3-context-menu'; +import { + XGanttColumn, + XGantt, + XGanttSlider, +} from '@ibiz-template-plugin/gantt'; +import '@ibiz-template-plugin/gantt/dist/style.css'; export const GanttControl = defineComponent({ name: 'IBizGanttControl', @@ -11,20 +42,417 @@ export const GanttControl = defineComponent({ params: { type: Object as PropType, default: () => ({}) }, provider: { type: Object as PropType }, loadDefault: { type: Boolean, default: true }, + mdctrlActiveMode: { type: Number, default: undefined }, + singleSelect: { type: Boolean, default: undefined }, }, - setup() { - const c = useControlController((...args) => new GanttController(...args)); + setup(props) { + const c: GanttController = useControlController( + (...args) => new GanttController(...args), + ); const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + const iBizRawItem = resolveComponent('IBizRawItem'); + const iBizIcon = resolveComponent('IBizIcon'); + + let forbidClick: boolean = false; + + const selection: IData[] = []; + + /** + * 实际呈现的数据 + */ + const data = computed(() => { + return props.modelData.rootVisible + ? c.state.rootNodes + : c.state.rootNodes[0]?.children; + }); + + /** + * 实际绘制的表格列 + */ + const columns = computed(() => { + const columnsModel: IDETreeColumn[] = []; + c.state.columnStates.forEach(item => { + const columnModel = c.columns[item.key]?.model; + if (!item.hidden && columnModel) { + columnsModel.push(columnModel); + } + }); + return columnsModel; + }); + + const locale = computed(() => { + return ibiz.i18n.getLang().toLowerCase(); + }); + + /** + * 根据id查找树节点数据 + * + * @param {string} id + * @param {IGanttNodeData[]} nodes + * @return {*} {(IGanttNodeData | null)} + */ + const findDataById = ( + id: string, + nodes: IGanttNodeData[], + ): IGanttNodeData | null => { + for (const node of nodes) { + if (node.id === id) { + return node; + } + if (node.children) { + const result = findDataById(id, node.children); + if (result) { + return result; + } + } + } + return null; + }; + + /** + * 查找对应节点的布局面板 + * + * @param {string} id + * @return {*} {(IPanel | undefined)} + */ + const findNodeLayoutPanel = (id: string): IPanel | undefined => { + let layoutPanel: IPanel | undefined; + const nodeModel = c.getNodeModel(id); + nodeModel?.controlRenders?.forEach(renderItem => { + if (renderItem.renderType === 'LAYOUTPANEL') { + layoutPanel = renderItem.layoutPanel; + } + }); + return layoutPanel; + }; + + /** + * 多选时选中节点变更 + * + * @param {boolean} state 当前复选框状态 + * @param {IGanttNodeData} item 当前数据 + */ + const onCheck = (state: boolean, item: IGanttNodeData) => { + if (state) { + selection.push(item); + } else { + const index = selection.findIndex(selected => selected.id === item.id); + if (index > -1) { + selection.splice(index, 1); + } + } + c.setSelection(selection); + }; + + /** + * 节点单击事件 + * + * @param {IGanttNodeData} nodeData + * @return {*} + */ + const onNodeClick = (nodeData: IGanttNodeData) => { + if (forbidClick) { + return; + } + + c.onGanttNodeClick(nodeData); + forbidClick = true; + setTimeout(() => { + forbidClick = false; + }, 200); + }; + + /** + * 节点双击事件 + * + * @param {IGanttNodeData} nodeData + */ + const onNodeDbClick = (nodeData: IGanttNodeData) => { + c.onDbGanttNodeClick(nodeData); + }; + + /** + * 处理节点展开 + * + * @param {IGanttNodeData} node + */ + const handleNodeExpand = (node: IGanttNodeData) => { + if (node && !node.children) { + c.refreshNodeChildren(node); + } + }; + + /** + * 计算上下文菜单组件配置项集合 + * + * @param {IDEToolbarItem[]} toolbarItems + * @param {IGanttNodeData} nodeData + * @param {MouseEvent} evt + * @param {IButtonContainerState} menuState + * @return {*} {MenuItem[]} + */ + const calcContextMenuItems = ( + toolbarItems: IDEToolbarItem[], + nodeData: IGanttNodeData, + evt: MouseEvent, + menuState: IButtonContainerState, + ): MenuItem[] => { + const result: MenuItem[] = []; + toolbarItems.forEach(item => { + if (item.itemType === 'SEPERATOR') { + result.push({ + divided: 'self', + }); + return; + } + + const buttonState = menuState[item.id!] as IButtonState; + if (buttonState && !buttonState.visible) { + return; + } + + // 除分隔符之外的公共部分 + const menuItem: MenuItem = {}; + if (item.showCaption && item.caption) { + menuItem.label = item.caption; + } + if (item.sysImage && item.showIcon) { + menuItem.icon = ; + } + + // 界面行为项 + if (item.itemType === 'DEUIACTION') { + menuItem.disabled = buttonState.disabled; + menuItem.clickClose = true; + const { uiactionId } = item as IDETBUIActionItem; + if (uiactionId) { + menuItem.onClick = () => { + c.doUIAction(uiactionId, nodeData, evt, item.appId); + }; + } + } else if (item.itemType === 'RAWITEM') { + const { rawItem } = item as IDETBRawItem; + if (rawItem) { + menuItem.label = ( + + ); + } + } else if (item.itemType === 'ITEMS') { + // 分组项绘制子菜单 + if ((item as IDETBGroupItem).detoolbarItems?.length) { + menuItem.children = calcContextMenuItems( + (item as IDETBGroupItem).detoolbarItems!, + nodeData, + evt, + menuState, + ); + } + } + result.push(menuItem); + }); + + return result; + }; + + /** + * 上下文菜单相关 + */ + let ContextMenu: IData; + c.evt.on('onMounted', () => { + // 有上下文菜单时加载组件 + if (Object.values(c.contextMenus).length > 0) { + const importMenu = () => import('@imengyu/vue3-context-menu'); + importMenu().then(value => { + ContextMenu = value.default; + if (ContextMenu.default && !ContextMenu.showContextMenu) { + ContextMenu = ContextMenu.default; + } + }); + } + }); + + /** + * 节点右键菜单点击事件 + * + * @param {IGanttNodeData} nodeData + * @param {MouseEvent} evt + */ + const onNodeContextmenu = async ( + nodeData: IGanttNodeData, + evt: MouseEvent, + ) => { + evt.stopPropagation(); + evt.preventDefault(); + const nodeModel = c.getNodeModel(nodeData.nodeId); + if (!nodeModel?.decontextMenu) { + return; + } + const contextMenuC = c.contextMenus[nodeModel.decontextMenu.id!]; + + if (!contextMenuC.model.detoolbarItems) { + return; + } + + // 更新菜单的权限状态 + await contextMenuC.calcButtonState( + nodeData.deData || (nodeData.srfkey ? nodeData : undefined), + nodeModel.appDataEntityId, + ); + const menuState = contextMenuC.state.buttonsState; + + const menus: MenuItem[] = calcContextMenuItems( + contextMenuC.model.detoolbarItems, + nodeData, + evt, + menuState, + ); + if (!menus.length) { + return; + } + + ContextMenu.showContextMenu({ + x: evt.x, + y: evt.y, + customClass: ns.b('context-menu'), + items: menus, + }); + }; + + /** + * 绘制表格列 + * @param model + * @param index + * @returns + */ + const renderColumn = (model: IDETreeColumn, index: number): VNode => { + const { caption, codeName, width, headerSysCss, align } = model; + const columnC = c.columns[codeName!]; + return ( + 30 ? width : 30} + ellipsis={true} + center={align?.toLowerCase() === 'center'} + > + {{ + title: ({ label }: IData) => { + return
{label}
; + }, + default: ({ row }: IData): VNode | null => { + const elRow = row; + const rowState = c.state.items.find( + item => item.srfkey === elRow.srfkey, + ); + if (rowState) { + Object.assign(rowState, { data: rowState.nodeDataItem || {} }); + const comp = resolveComponent(c.providers[codeName!].component); + return h(comp, { + controller: columnC, + row: rowState, + key: elRow.srfkey + codeName, + }); + } + return null; + }, + }} +
+ ); + }; + + /** + * 绘制节点面板 + * + * @param {IPanel} modelData + * @param {IData} item + * @return {*} + */ + const renderNodePanel = (modelData: IPanel, item: IData) => { + return ( + + ); + }; + + /** + * 绘制滑块 + * + * @return {*} + */ + const renderSlider = () => { + return ( + + {{ + default: ({ row }: IData): VNode => { + const panel = findNodeLayoutPanel(row.nodeId); + return ( +
onNodeContextmenu(row, evt)} + > + {panel ? renderNodePanel(panel, row.deData) : null} +
+ ); + }, + }} +
+ ); + }; + + /** + * 绘制内容 + * @returns + */ + const renderContent = () => { + const _columns = columns.value.map((model, index) => { + return renderColumn(model, index); + }); + const slider = renderSlider(); + return [..._columns, slider]; + }; + return { c, ns, + data, + locale, + columns, + onCheck, + onNodeClick, + onNodeDbClick, + handleNodeExpand, + renderContent, }; }, render() { return ( - 甘特图 + + {{ + default: () => { + return this.renderContent(); + }, + }} + ); }, diff --git a/src/control/kanban/kanban.scss b/src/control/kanban/kanban.scss index 4910cdb9aa4b29b0ec70473d76876cb82db407af..f267fc5bd2ff5c52e0aa34e8870fadec3a6e15da 100644 --- a/src/control/kanban/kanban.scss +++ b/src/control/kanban/kanban.scss @@ -5,34 +5,110 @@ $control-kanban: ( ); @include b(control-kanban) { - @include set-component-css-var(control-kanban, $control-kanban);; + @include set-component-css-var(control-kanban, $control-kanban); width: 100%; height: 100%; - @include m(row){ - @include flex(row) + @include m(row) { + @include flex(row); + gap: 16px; + overflow: auto; + padding: 0 8px 8px; + @include b(control-kanban-group) { + @include when(collapse) { + width: 32px; + @include e(header) { + position: relative; + height: 100%; + padding: 0; + align-items: start; + flex-direction: column; + ion-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + @include e(header-caption) { + align-self: center; + transform: rotate(90deg) translate(35px, 0); + } + } + } + } + + @include m(column) { + @include flex(row); + flex-direction: column; + gap: 16px; + + @include b(control-kanban-group) { + @include when(collapse) { + width: 100%; + ion-icon { + transform: rotate(-90deg); + } + } + } } } -@include b(control-kanban-group){ +@include b(control-kanban-group) { flex-shrink: 0; - padding: 0 8px; - @include e(header){ + border: 1px solid getCssVar(color, border); + border-radius: 4px; + transition: all 0.3s; + &::before { + content: ''; + display: block; + border-top: 2px solid; + border-color: inherit; + } + @include e(header) { height: 48px; + font-size: 14px; font-weight: bold; + padding: 0 16px; + border-bottom: 1px solid getCssVar(color, border); @include flex(row, space-between, center); } - @include e(list){ + @include e(header-left) { + @include flex(row, center, center); + ion-icon { + margin-right: 8px; + transition: transform 0.3s; + } + } + @include e(header-caption) { + padding: 0 8px; + border-radius: 100px; + white-space: nowrap; + @include when(badge) { + color: getCssVar(color, primary, active, text); + } + } + @include e(list) { + padding: 0 8px; height: calc(100% - 48px); overflow: auto; } - @include e(header-actions){ - padding: 5px 8px; + @include e(header-actions) { + margin-left: 4px; + height: 32px; + width: 32px; + border-radius: 50%; + padding: 8px; + font-size: 14px; + color: getCssVar(color, primary); + &:hover { + background-color: getCssVar(color, fill, 0); + } } } -@include b(control-kanban-item){ +@include b(control-kanban-item) { margin: 16px 0; cursor: pointer; diff --git a/src/control/kanban/kanban.tsx b/src/control/kanban/kanban.tsx index fb4ca6dd829aa0f1e59fdfc282a72c29e3bb9402..a626419292b4264c8d1e9422b3d6d1af62371312 100644 --- a/src/control/kanban/kanban.tsx +++ b/src/control/kanban/kanban.tsx @@ -1,6 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; -import { computed, defineComponent, PropType, StyleValue, VNode } from 'vue'; +import { + computed, + defineComponent, + PropType, + ref, + Ref, + StyleValue, + VNode, +} from 'vue'; import draggable from 'vuedraggable'; import { IDEKanban, @@ -9,7 +17,7 @@ import { } from '@ibiz/model-core'; import { KanbanController, - IMDControlGroupState, + IKanbanGroupState, IDragChangeInfo, IControlProvider, } from '@ibiz-template/runtime'; @@ -38,6 +46,8 @@ export const KanbanControl = defineComponent({ const groupClass = c.model.groupSysCss?.cssName || ''; // 分组样式表 + const collapseMap: Ref = ref({}); + // 分组的行内样式 const groupStyle: StyleValue = {}; switch (c.model.groupLayout) { @@ -47,23 +57,39 @@ export const KanbanControl = defineComponent({ break; case 'COLUMN': groupStyle.width = '100%'; - groupStyle.height = `${c.model.height || 500}px`; + groupStyle.height = `${c.model.groupHeight || 500}px`; break; default: } + const stopPropagation = (event: MouseEvent) => { + event.stopPropagation(); + }; + + const onCollapse = (group: IKanbanGroupState, event: MouseEvent) => { + stopPropagation(event); + const key = String(group.key); + collapseMap.value[key] = !collapseMap.value[key]; + }; + // 行单击事件 const onRowClick = (item: IData, event: MouseEvent): Promise => { - event.stopPropagation(); + stopPropagation(event); return c.onRowClick(item); }; // 行双击事件 const onDbRowClick = (item: IData, event: MouseEvent): Promise => { - event.stopPropagation(); + stopPropagation(event); return c.onDbRowClick(item); }; + // 新建 + const onClickNew = (event: MouseEvent, group: string | number) => { + stopPropagation(event); + return c.onClickNew(event, group); + }; + // 绘制项布局面板 const renderPanelItemLayout = ( item: IData, @@ -97,6 +123,23 @@ export const KanbanControl = defineComponent({ ); }; + // 绘制快速工具栏行为 + const renderQuickToolBar = (): VNode | undefined => { + const ctrlModel = c.model.controls?.find(item => { + return item.name === `${c.model.name!}_quicktoolbar`; + }); + if (!ctrlModel) { + return; + } + return ( + + ); + }; + // 绘制默认项 const renderDefaultItem = (item: IData) => { return [ @@ -164,7 +207,9 @@ export const KanbanControl = defineComponent({ + > + {renderQuickToolBar()} + ); }; @@ -196,67 +241,115 @@ export const KanbanControl = defineComponent({ }; // 绘制分组 - const renderGroup = (group: IMDControlGroupState) => { - return ( -
-
- - {group.caption} - - - {c.enableNew && ( - { - c.onClickNew(event, group.key); - }} + const renderGroup = (group: IKanbanGroupState) => { + const collapse = collapseMap.value[String(group.key)]; + const isColumn = c.model.groupLayout === 'COLUMN'; + const tempGroupStyle = { borderTopColor: group.color, ...groupStyle }; + if (collapse) { + tempGroupStyle.height = '50px'; + } + if (!collapse || isColumn) { + return ( +
+
onCollapse(group, event)} + > +
+ + - - - )} - {c.model.groupUIActionGroup && group.groupActionGroupState && ( - +
+ stopPropagation(event)} + > + {c.enableNew && ( + { + onClickNew(event, group.key); + }} + > + + + )} + {c.model.groupUIActionGroup && group.groupActionGroupState && ( + { + c.onGroupToolbarClick(detail, event, group); + }} + > + )} + +
+ onChange(evt, group.key)} + > + {{ + item: ({ element }: { element: IData }) => { + return renderCard(element); + }, + header: () => { + if (group.children.length) { + return null; } - caption='...' - mode='dropdown' - actions-state={c.state.groupActionGroupState} - onActionClick={( - detail: IUIActionGroupDetail, - event: MouseEvent, - ) => { - c.onGroupToolbarClick(detail, event, group); - }} - > - )} - + return ( +
{renderNoData()}
+ ); + }, + }} +
- onChange(evt, group.key)} + ); + } + return ( +
+
onCollapse(group, event)} > - {{ - item: ({ element }: { element: IData }) => { - return renderCard(element); - }, - header: () => { - if (group.children.length) { - return null; - } - return ( -
{renderNoData()}
- ); - }, - }} - + + {group.caption} + + +
); }; diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts index 021589d072498dfe696c240b2561f604b5b2352d..0ce62c39b7eb46ec5aaaad1b9b9e1e4b224b6ce6 100644 --- a/src/shims-vue.d.ts +++ b/src/shims-vue.d.ts @@ -6,3 +6,4 @@ declare module '*.vue' { } declare module 'element-plus/dist/locale/zh-cn.mjs'; +declare module '@ibiz-template-plugin/gantt';