From 4823b341c57ed641e6349565c90fb46fb9a76ef8 Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Sun, 28 Sep 2025 20:45:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=8E=92=E7=A8=8B=E6=A0=91=E5=8F=8A=E8=B5=84=E6=BA=90=E6=8E=92?= =?UTF-8?q?=E7=A8=8B=E6=8B=96=E6=8B=BD=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/panel-bottom-tab-panel/src/index.ts | 23 + .../src/panel-bottom-tab-panel.controller.ts | 42 + .../src/panel-bottom-tab-panel.provider.ts | 27 + .../src/panel-bottom-tab-panel.scss | 30 + .../src/panel-bottom-tab-panel.state.ts | 16 + .../src/panel-bottom-tab-panel.tsx | 78 ++ .../resource-schedule-table.tsx | 10 +- .../src/el-tree-util.ts | 370 +++++ src/resource-schedule-tree/src/index.ts | 23 + .../src/resource-schedule-tree.controller.ts | 194 +++ .../src/resource-schedule-tree.privoder.ts | 11 + .../src/resource-schedule-tree.scss | 60 + .../src/resource-schedule-tree.tsx | 1222 +++++++++++++++++ .../schedule-table/schedule-table.tsx | 348 ++--- src/resource-scheduler/controller/index.ts | 4 +- .../controller/render-layer.controller.ts | 460 ++++--- .../hooks/use-schedule-table.ts | 439 +++--- src/resource-scheduler/index.ts | 24 +- .../interface/i-drag-item.ts | 25 + src/resource-scheduler/interface/index.ts | 3 +- src/resource-scheduler/store/index.ts | 80 +- src/user-register.ts | 6 +- 22 files changed, 2885 insertions(+), 610 deletions(-) create mode 100644 src/panel-bottom-tab-panel/src/index.ts create mode 100644 src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.controller.ts create mode 100644 src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.provider.ts create mode 100644 src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.scss create mode 100644 src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.state.ts create mode 100644 src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.tsx create mode 100644 src/resource-schedule-tree/src/el-tree-util.ts create mode 100644 src/resource-schedule-tree/src/index.ts create mode 100644 src/resource-schedule-tree/src/resource-schedule-tree.controller.ts create mode 100644 src/resource-schedule-tree/src/resource-schedule-tree.privoder.ts create mode 100644 src/resource-schedule-tree/src/resource-schedule-tree.scss create mode 100644 src/resource-schedule-tree/src/resource-schedule-tree.tsx create mode 100644 src/resource-scheduler/interface/i-drag-item.ts diff --git a/src/panel-bottom-tab-panel/src/index.ts b/src/panel-bottom-tab-panel/src/index.ts new file mode 100644 index 0000000..e5d1d31 --- /dev/null +++ b/src/panel-bottom-tab-panel/src/index.ts @@ -0,0 +1,23 @@ +import { App } from 'vue'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { registerPanelItemProvider } from '@ibiz-template/runtime'; +import { PanelBottomTabPanel } from './panel-bottom-tab-panel'; +import { PanelBottomTabPanelProvider } from './panel-bottom-tab-panel.provider'; + +const IBizPanelBottomTabPanel = withInstall( + PanelBottomTabPanel, + function (v: App) { + v.component(PanelBottomTabPanel.name!, PanelBottomTabPanel); + registerPanelItemProvider( + 'CUSTOM_PANEL_BOTTOM_TABPANEL', + () => new PanelBottomTabPanelProvider(), + ); + }, +); + +export default { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type + install(app: App) { + app.use(IBizPanelBottomTabPanel); + }, +}; diff --git a/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.controller.ts b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.controller.ts new file mode 100644 index 0000000..c354b31 --- /dev/null +++ b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.controller.ts @@ -0,0 +1,42 @@ +import { IPanelTabPanel } from '@ibiz/model-core'; +import { PanelContainerController } from '@ibiz-template/runtime'; +import { PanelBottomTabPanelState } from './panel-bottom-tab-panel.state'; + +/** + * @description 面板底部分页部件控制器 + * @export + * @class PanelBottomTabPanelController + * @extends {PanelContainerController} + */ +export class PanelBottomTabPanelController extends PanelContainerController { + declare state: PanelBottomTabPanelState; + + /** + * @description 新建状态 + * @protected + * @returns {*} {PanelBottomTabPanelState} + * @memberof PanelBottomTabPanelController + */ + protected createState(): PanelBottomTabPanelState { + return new PanelBottomTabPanelState(this.parent?.state); + } + + /** + * @description 初始化 + * @returns {*} {Promise} + * @memberof PanelBottomTabPanelController + */ + async onInit(): Promise { + await super.onInit(); + this.state.activeTab = this.model.panelTabPages?.[0].id || ''; + } + + /** + * @description 分页点击切换处理 + * @param {string} tabId + * @memberof PanelBottomTabPanelController + */ + onTabChange(tabId: string): void { + this.state.activeTab = tabId; + } +} diff --git a/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.provider.ts b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.provider.ts new file mode 100644 index 0000000..cc40795 --- /dev/null +++ b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.provider.ts @@ -0,0 +1,27 @@ +import { + PanelController, + IPanelItemProvider, + PanelItemController, +} from '@ibiz-template/runtime'; +import { IPanelItem } from '@ibiz/model-core'; +import { PanelBottomTabPanelController } from './panel-bottom-tab-panel.controller'; + +/** + * @description 面板底部分页部件适配器 + * @export + * @class PanelBottomTabPanelProvider + * @implements {IPanelItemProvider} + */ +export class PanelBottomTabPanelProvider implements IPanelItemProvider { + component: string = 'IBizPanelBottomTabPanel'; + + async createController( + panelItem: IPanelItem, + panel: PanelController, + parent: PanelItemController | undefined, + ): Promise { + const c = new PanelBottomTabPanelController(panelItem, panel, parent); + await c.init(); + return c; + } +} diff --git a/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.scss b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.scss new file mode 100644 index 0000000..866a8c7 --- /dev/null +++ b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.scss @@ -0,0 +1,30 @@ +@include b(panel-bottom-tab-panel) { + width: 100%; + height: 100%; + + > .el-tabs__header { + height: var(--el-tabs-header-height); + margin-bottom: 0; + border-bottom: none; + + .el-tabs__item.is-active { + color: getCssVar(color, primary); + } + } + + > .el-tabs__content { + height: calc(100% - 51px); + overflow: auto; + + >.el-tab-pane { + height: 100%; + + > .#{bem(panel-tab-page)} { + height: 100%; + > .#{bem(col,'',flex)} { + height: 100%; + } + } + } + } +} \ No newline at end of file diff --git a/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.state.ts b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.state.ts new file mode 100644 index 0000000..7c71ecf --- /dev/null +++ b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.state.ts @@ -0,0 +1,16 @@ +import { PanelContainerState } from '@ibiz-template/runtime'; + +/** + * @description 面板底部分页部件状态 + * @export + * @class PanelBottomTabPanelState + * @extends {PanelContainerState} + */ +export class PanelBottomTabPanelState extends PanelContainerState { + /** + * @description 当前激活分页 + * @type {string} + * @memberof PanelBottomTabPanelState + */ + activeTab: string = ''; +} diff --git a/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.tsx b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.tsx new file mode 100644 index 0000000..6fd01f4 --- /dev/null +++ b/src/panel-bottom-tab-panel/src/panel-bottom-tab-panel.tsx @@ -0,0 +1,78 @@ +import { useNamespace } from '@ibiz-template/vue3-util'; +import { IPanelTabPanel } from '@ibiz/model-core'; +import { computed, defineComponent, PropType, VNode } from 'vue'; +import { PanelBottomTabPanelController } from './panel-bottom-tab-panel.controller'; +import './panel-bottom-tab-panel.scss'; + +export const PanelBottomTabPanel = defineComponent({ + name: 'IBizPanelBottomTabPanel', + props: { + modelData: { + type: Object as PropType, + required: true, + }, + controller: { + type: Object as PropType, + required: true, + }, + }, + setup(props) { + const ns = useNamespace('panel-bottom-tab-panel'); + const { state } = props.controller; + // 类名控制 + const classArr = computed(() => { + const { id } = props.modelData; + const result: Array = [ns.b(), ns.m(id)]; + result.push(...props.controller.containerClass); + return result; + }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars + const onTabClick = (tabIns: IData, event: MouseEvent) => { + props.controller.onTabChange(tabIns.props.name); + }; + + return { + ns, + state, + classArr, + onTabClick, + }; + }, + render() { + // 动态控制显示 + if (!this.controller.state.visible) return; + // 内容区默认插槽处理,封装app-col + const defaultSlots: VNode[] = this.$slots.default?.() || []; + return ( + + {defaultSlots!.map(slot => { + const props = slot.props as IData; + if (!props || !props.controller) return slot; + const c = props.controller; + // 不显示且不用保活时直接不绘制 + if (!c.state.visible && !c.state.keepAlive) return null; + return ( + + {this.state.activeTab === c.model.id && slot} + + ); + })} + + ); + }, +}); diff --git a/src/plugins/resource-schedule-table/resource-schedule-table.tsx b/src/plugins/resource-schedule-table/resource-schedule-table.tsx index c3678f9..9ab168c 100644 --- a/src/plugins/resource-schedule-table/resource-schedule-table.tsx +++ b/src/plugins/resource-schedule-table/resource-schedule-table.tsx @@ -156,14 +156,20 @@ export const ResourceScheduleTable = defineComponent({ } } ]); + + const verifyDrag = async (data: IData) => { + return { ok: true, data } + } + return { c, ns, + tasks, resources, - tasks + verifyDrag }; }, render() { - return
; + return
; }, }); diff --git a/src/resource-schedule-tree/src/el-tree-util.ts b/src/resource-schedule-tree/src/el-tree-util.ts new file mode 100644 index 0000000..baf9176 --- /dev/null +++ b/src/resource-schedule-tree/src/el-tree-util.ts @@ -0,0 +1,370 @@ +import { RuntimeError } from '@ibiz-template/core'; +import { + getControlPanel, + IControlProvider, + ITreeController, + ITreeNodeData, + ScriptFactory, + TreeController, +} from '@ibiz-template/runtime'; +import { IControlRender, IDETree, IPanel } from '@ibiz/model-core'; +import { ElTree } from 'element-plus'; +import { createUUID } from 'qx-util'; +import { NodeDropType } from 'element-plus/es/components/tree/src/tree.type'; +import { cloneDeep, debounce } from 'lodash-es'; +import { Ref, watch } from 'vue'; + +/** + * 树props接口 + * + * @export + * @interface IGridProps + */ +export interface ITreeProps { + // 模型 + modelData: IDETree; + // 上下文 + context: IContext; + // 视图参数 + params: IParams; + // 适配器 + provider?: IControlProvider; + // 部件行数据默认激活模式 + mdctrlActiveMode?: number; + // 是否单选 + singleSelect?: boolean; + // 是否是导航的 + navigational?: boolean; + // 默认展开节点 + defaultExpandedKeys?: string[]; + // 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法 + checkStrictly?: boolean; + // 是否是本地数据模式 + isSimple?: boolean; + // 本地数据模式data + data?: Array; + // 默认加载 + loadDefault: boolean; +} + +/** + * 根据id查找树节点数据对象 + * @author lxm + * @date 2023-09-28 03:45:35 + * @export + * @param {string} key (数据的_uuid 或者 _id) + * @param {ITreeController} c + * @return {*} {(ITreeNodeData | undefined)} + */ +export function findNodeData( + key: string, + c: ITreeController, +): ITreeNodeData | undefined { + const find = c.state.items.find(item => item._id === key); + if (find) { + return find; + } + return c.state.items.find(item => item._uuid === key); +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function useElTreeUtil( + treeRef: Ref | null>, + c: ITreeController, +) { + const getTreeInstance = () => { + const elTree = treeRef.value; + if (!elTree) { + throw new RuntimeError(ibiz.i18n.t('control.tree.noFoundInstance')); + } + return elTree; + }; + + const _updateUI = () => { + const elTree = treeRef.value; + if (!elTree) { + setTimeout(() => { + _updateUI(); + }, 200); + return; + } + // 设置节点展开 + Object.values(elTree.store.nodesMap).forEach(node => { + const shouldExpanded = c.state.expandedKeys.includes(node.data._id); + if (shouldExpanded !== node.expanded) { + if (shouldExpanded) { + node.expand(); + } else { + node.collapse(); + } + } + }); + // 设置选中状态。 + if (c.state.singleSelect) { + treeRef.value!.setCurrentKey(c.state.selectedData[0]?._id || undefined); + } else { + // el-tree,会把没选中的反选,且不触发check事件 + elTree.setCheckedKeys(c.state.selectedData.map(item => item._id)); + } + }; + + /** + * 更新树的UI状态,如选中状态和展开状态。 + * 在树组件的loadData和调用updateKeyChildren之后去更新 + * 加了防抖,所以尽可能多的调用,确保状态 todo + * @author lxm + * @date 2023-09-28 03:36:44 + */ + const updateUI = debounce(_updateUI, 500) as typeof _updateUI; + + /** + * 切换节点的展开 + * @author lxm + * @date 2023-11-08 03:57:27 + * @param {string} id + */ + const triggerNodeExpand = (id: string): boolean | undefined => { + const elTree = getTreeInstance(); + const target = elTree.store.nodesMap[id]; + if (target) { + if (target.expanded) { + target.collapse(); + return false; + } + target.expand(); + return true; + } + }; + + return { getTreeInstance, updateUI, triggerNodeExpand }; +} + +/** + * 格式化dropType + * @author lxm + * @date 2023-12-14 02:13:10 + * @export + * @param {NodeDropType} dropType + * @return {*} {('inner' | 'prev' | 'next')} + */ +export function formatNodeDropType( + dropType: NodeDropType, +): 'inner' | 'prev' | 'next' { + switch (dropType) { + case 'inner': + return 'inner'; + case 'before': + return 'prev'; + case 'after': + return 'next'; + default: + throw new RuntimeError( + ibiz.i18n.t('control.tree.noSupported', { dropType }), + ); + } +} + +/** + * 初始化树数据 + * + * @export + * @param {TreeController} c + * @param {ITreeProps} props + */ +export function useAppTreeBase(c: TreeController, props: ITreeProps): void { + // 设置默认展开 + if (props.defaultExpandedKeys) { + c.state.defaultExpandedKeys = props.defaultExpandedKeys; + } + + // 初始化数据 + const initSimpleData = (): void => { + if (!props.data) { + return; + } + const root = props.data.find((item: ITreeNodeData) => { + return (item as IData).isRoot === true; + }); + if (root) { + c.state.rootNodes = props.data; + c.state.items = props.data; + } + }; + + c.evt.on('onCreated', async () => { + if (props.isSimple) { + initSimpleData(); + c.state.isLoaded = true; + } + }); + + watch( + () => props.data, + () => { + if (props.isSimple) { + initSimpleData(); + } + }, + { + deep: true, + }, + ); +} + +/** + * 根据关系获取当前项的子节点 + * + * @export + * @param {TreeController} c + * @param {IData} modelData + * @param {IData} curItem + */ +export function findChildItems( + c: TreeController, + modelData: IData, + curItem: IData, +): IData[] { + const { detreeNodeRSs } = modelData; + const children: IData[] = []; + if (!detreeNodeRSs || detreeNodeRSs.length === 0) { + return children; + } + const rss = detreeNodeRSs.filter((_rss: IData) => { + const temp = c.state.items.find((_item: IData) => { + return _item._uuid === curItem.data._uuid; + }); + + if (temp) { + return _rss.parentDETreeNodeId === temp._nodeId?.toLowerCase(); + } + return false; + }); + rss.sort((a: IData, b: IData) => { + return a.ordervalue - b.ordervalue; + }); + rss.forEach((_rss: IData) => { + const _children = c.state.items.filter((_item: IData) => { + return _item._nodeId?.toLowerCase() === _rss.childDETreeNodeId; + }); + if (_children.length) { + _children.forEach(child => { + const temp = cloneDeep(child); + temp._id = createUUID(); + children.push(temp); + }); + } + }); + return children; +} + +/** + * 获取树节点布局面板 + * + * @param {{ + * controlRenders?: IControlRender[]; + * }} control + * @return {*} {(IPanel | undefined)} + */ +export function getNodeControlPanel(control: { + controlRenders?: IControlRender[]; +}): IPanel | undefined { + if (control.controlRenders) { + // 排除新建节点内容绘制器 + const controlRenders = control.controlRenders.filter( + item => (item.id || '').split('_')[0] !== 'newnoderender', + ); + return getControlPanel({ controlRenders }); + } +} + +/** + * 获取新建树节点布局面板 + * + * @param {{ + * controlRenders?: IControlRender[]; + * }} control + * @return {*} {(IPanel | undefined)} + */ +export function getNewNodeControlPanel(control: { + controlRenders?: IControlRender[]; +}): IPanel | undefined { + if (control.controlRenders) { + // 排除新建节点内容绘制器 + const controlRender = control.controlRenders.find( + item => (item.id || '').split('_')[0] === 'newnoderender', + ); + if (!controlRender) return; + + if ( + controlRender.renderType === 'LAYOUTPANEL_MODEL' && + controlRender.layoutPanelModel + ) { + const layoutPanelModel = ScriptFactory.execScriptFn( + {}, + controlRender.layoutPanelModel, + { isAsync: false }, + ) as IPanel; + return layoutPanelModel; + } + + if ( + controlRender.renderType === 'LAYOUTPANEL' && + controlRender.layoutPanel + ) { + return controlRender.layoutPanel; + } + } +} + +/** + * @description 树加载更多工具 + * @export + * @param {(Ref | null>)} treeRef + * @param {TreeController} c + * @returns {*} + */ +export function useLoadMoreUtil( + treeRef: Ref | null>, + c: TreeController, +): { + toLoadMoreNode: (parentId: string) => IData; + addLoadMoreNode: (parentId: string, parentNode: IData) => void; +} { + // 获取加载更多节点 + const toLoadMoreNode = (parentId: string) => { + const _loadMoreNodeData = { + _id: `${parentId}_load_more`, + _text: ibiz.i18n.t('control.common.loadMore'), + _leaf: true, + _disableSelect: true, + _load_more: true, + }; + return _loadMoreNodeData; + }; + + // 添加加载更多节点 + const addLoadMoreNode = (parentId: string, parentNode: IData) => { + if (!treeRef.value || !parentId || !parentNode) { + return; + } + const _loadMoreNodeData = toLoadMoreNode(parentId); + if (treeRef.value.getNode(_loadMoreNodeData)) { + treeRef.value.remove(_loadMoreNodeData); + } + const infoItems = c.getLoadMoreInfoItems(parentId); + if (infoItems) { + // 是否有分页数据没有到达最后一页 + const result = infoItems.some(infoItem => { + return infoItem.curPage < infoItem.totalPage - 1; + }); + if (result) { + treeRef.value.append(_loadMoreNodeData, parentNode); + } + } + }; + + return { + toLoadMoreNode, + addLoadMoreNode, + }; +} diff --git a/src/resource-schedule-tree/src/index.ts b/src/resource-schedule-tree/src/index.ts new file mode 100644 index 0000000..1bc8b4b --- /dev/null +++ b/src/resource-schedule-tree/src/index.ts @@ -0,0 +1,23 @@ +import { App } from 'vue'; +import { registerControlProvider } from '@ibiz-template/runtime'; +import { withInstall } from '@ibiz-template/vue3-util'; +import { ResourceScheduleTreeProvider } from './resource-schedule-tree.privoder'; +import { ResourceScheduleTree } from './resource-schedule-tree'; + +const IBizResourceScheduleTree = withInstall( + ResourceScheduleTree, + function (v: App) { + v.component(ResourceScheduleTree.name!, ResourceScheduleTree); + registerControlProvider( + 'TREE_RENDER_RESOURCE_SCHEDULER_TREE', + () => new ResourceScheduleTreeProvider(), + ); + }, +); + +export default { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type + install(app: App) { + app.use(IBizResourceScheduleTree); + }, +}; diff --git a/src/resource-schedule-tree/src/resource-schedule-tree.controller.ts b/src/resource-schedule-tree/src/resource-schedule-tree.controller.ts new file mode 100644 index 0000000..d104eb7 --- /dev/null +++ b/src/resource-schedule-tree/src/resource-schedule-tree.controller.ts @@ -0,0 +1,194 @@ +import { + ValueOP, + ISearchCond, + ITreeNodeData, + TreeController, + ISearchCondField, + ToolbarController, + SearchBarController, + SearchFormController, +} from '@ibiz-template/runtime'; +import { IDESearchForm, IDEToolbar, ISearchBar } from '@ibiz/model-core'; + +/** + * @description 资源排程树控制器 + * @export + * @class ResourceScheduleTreeController + * @extends {TreeController} + */ +export class ResourceScheduleTreeController extends TreeController { + /** + * @description 工具栏模型 + * @readonly + * @type {(IDEToolbar | undefined)} + * @memberof ResourceScheduleTreeController + */ + get toolbarModel(): IDEToolbar | undefined { + const toolbarModel = this.view.model.viewLayoutPanel?.controls?.find( + ctrl => ctrl.name === `${this.model.name}toolbar`, + ); + if (toolbarModel) + return { + ...toolbarModel, + xdataControlName: this.model.name, + }; + return; + } + + /** + * @description 搜索栏模型 + * @readonly + * @type {(ISearchBar | undefined)} + * @memberof ResourceScheduleTreeController + */ + get searchBarModel(): ISearchBar | undefined { + return this.view.model.viewLayoutPanel?.controls?.find( + ctrl => ctrl.name === `${this.model.name}searchbar`, + ); + } + + /** + * @description 搜索表单模型 + * @readonly + * @type {(IDESearchForm | undefined)} + * @memberof ResourceScheduleTreeController + */ + get searchFormModel(): IDESearchForm | undefined { + return this.view.model.viewLayoutPanel?.controls?.find( + ctrl => ctrl.name === `${this.model.name}searchform`, + ); + } + + /** + * @description 工具栏 + * @readonly + * @type {(ToolbarController | undefined)} + * @memberof ResourceScheduleTreeController + */ + get toolbar(): ToolbarController | undefined { + if (this.toolbarModel) + return this.view.getController( + this.toolbarModel.name!, + ) as ToolbarController; + return; + } + + /** + * @description 搜索栏 + * @readonly + * @type {(SearchBarController | undefined)} + * @memberof ResourceScheduleTreeController + */ + get searchBar(): SearchBarController | undefined { + if (this.searchBarModel) + return this.view.getController( + this.searchBarModel.name!, + ) as SearchBarController; + return; + } + + /** + * @description 搜索表单 + * @readonly + * @type {(SearchFormController | undefined)} + * @memberof ResourceScheduleTreeController + */ + get searchForm(): SearchFormController | undefined { + if (this.searchFormModel) + return this.view.getController( + this.searchFormModel.name!, + ) as SearchFormController; + return; + } + + /** + * @description 获取搜索表单参数 + * @returns {*} {(ISearchCond[] | undefined)} + * @memberof ResourceScheduleTreeController + */ + getSearchFormParams(): ISearchCond[] | undefined { + if (!this.searchForm) return; + const params = this.searchForm.getFilterParams(); + const valueOPs: string[] = []; + for (const key in ValueOP) { + if (key !== ValueOP.EXISTS && key !== ValueOP.NOT_EXISTS) { + const value = ValueOP[key as keyof typeof ValueOP]; + valueOPs.push(value); + } + } + const searchconds: ISearchCondField[] = []; + Object.keys(params).forEach(key => { + const value = params[key]; + const tempKey = key.toLocaleUpperCase(); + let fieldname = ''; + + // 检查键是否以'N_'开头 + const condop = valueOPs.find(filter => tempKey.endsWith(`_${filter}`)); + if (tempKey.startsWith('N_') && condop) { + // 去除'N_'前缀与条件后缀 + fieldname = key.slice(2).slice(0, -`_${condop}`.length); + if (fieldname && condop) + searchconds.push({ + condtype: 'DEFIELD', + fieldname, + value, + condop: condop.toLocaleUpperCase() as ValueOP, + }); + } + }); + if (searchconds.length) + return [ + { + searchconds, + condop: 'AND', + condtype: 'GROUP', + }, + ]; + return; + } + + /** + * @description 获取过滤参数 + * @param {IParams} [extraParams] + * @returns {*} {Promise} + * @memberof ResourceScheduleTreeController + */ + async getFetchParams(extraParams?: IParams): Promise { + const params = await super.getFetchParams(extraParams); + if (this.searchBar) { + if (this.searchBar.state.query && !params.query) + params.query = this.searchBar.state.query; + Object.assign(params, this.searchBar.getFilterParams()); + } + const searchconds = this.getSearchFormParams(); + if (searchconds) { + if (params.searchconds && params.searchconds.length > 0) { + params.searchconds = [ + { + condop: 'AND', + condtype: 'GROUP', + searchconds: [...params.searchconds, ...searchconds], + }, + ] + } else { + params.searchconds = searchconds; + } + } + return params; + } + + /** + * @description 设置选中改变 + * @param {ITreeNodeData[]} selection + * @memberof ResourceScheduleTreeController + */ + setSelection(selection: ITreeNodeData[]): void { + super.setSelection(selection); + const node = selection?.[0]; + const nodeModel = this.getNodeModel(node._nodeId); + this.toolbar?.calcButtonState(node, nodeModel?.appDataEntityId, { + view: this.view, + ctrl: this, + }); + } +} diff --git a/src/resource-schedule-tree/src/resource-schedule-tree.privoder.ts b/src/resource-schedule-tree/src/resource-schedule-tree.privoder.ts new file mode 100644 index 0000000..8d7a53f --- /dev/null +++ b/src/resource-schedule-tree/src/resource-schedule-tree.privoder.ts @@ -0,0 +1,11 @@ +import { IControlProvider } from '@ibiz-template/runtime'; + +/** + * @description 资源排程树适配器 + * @export + * @class ResourceScheduleTreeProvider + * @implements {IControlProvider} + */ +export class ResourceScheduleTreeProvider implements IControlProvider { + component: string = 'IBizResourceScheduleTree'; +} diff --git a/src/resource-schedule-tree/src/resource-schedule-tree.scss b/src/resource-schedule-tree/src/resource-schedule-tree.scss new file mode 100644 index 0000000..efe4468 --- /dev/null +++ b/src/resource-schedule-tree/src/resource-schedule-tree.scss @@ -0,0 +1,60 @@ +@include b(resource-schedule-tree) { + display: flex; + flex-direction: column; + + @include e(hearder) { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + padding-bottom: getCssVar(spacing, tight); + border-bottom: 1px solid getCssVar(color, border); + + @include m(left) { + min-width: 0; + padding-left: getCssVar(spacing, base); + overflow: hidden; + font-size: getCssVar(font-size, header-5); + text-overflow: ellipsis; + white-space: nowrap; + } + + @include m(right) { + display: flex; + flex-shrink: 0; + } + } + + @include e(search-bar) { + display: flex; + justify-content: end; + .#{bem(control-searchbar-quick-search)} { + width: auto; + min-width: 160px; + max-width: 220px; + } + } + + @include e(tree) { + &.el-tree { + flex-grow: 1; + padding: getCssVar(spacing, tight) getCssVar(spacing, base); + overflow: auto; + } + } + + @include e(search-form) { + flex-shrink: 0; + height: auto; + padding: 0 getCssVar(spacing, base-tight); + } + + @include e(tree-node) { + @include when(draggable) { + + } + @include when(dragging) { + cursor: grabbing; + } + } +} diff --git a/src/resource-schedule-tree/src/resource-schedule-tree.tsx b/src/resource-schedule-tree/src/resource-schedule-tree.tsx new file mode 100644 index 0000000..4304773 --- /dev/null +++ b/src/resource-schedule-tree/src/resource-schedule-tree.tsx @@ -0,0 +1,1222 @@ +/* eslint-disable no-nested-ternary */ +import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; +import { + ref, + Ref, + watch, + computed, + nextTick, + PropType, + onMounted, + onUnmounted, + defineComponent, + resolveComponent, +} from 'vue'; +import { MenuItem } from '@imengyu/vue3-context-menu'; +import { createUUID } from 'qx-util'; +import { cloneDeep, debounce } from 'lodash-es'; +import { + IPanel, + IDETree, + IDETreeNode, + IDETBRawItem, + IDEToolbarItem, + IDETBGroupItem, + IDETBUIActionItem, +} from '@ibiz/model-core'; +import { + IButtonState, + ITreeNodeData, + AppDataEntity, + IControlProvider, + PanelItemEventName, + IButtonContainerState, +} from '@ibiz-template/runtime'; +import { RuntimeError } from '@ibiz-template/core'; +import { ElTree } from 'element-plus'; +import { + NodeDropType, + AllowDropType, +} from 'element-plus/es/components/tree/src/tree.type'; +import { isNil } from 'ramda'; +import { + findNodeData, + useElTreeUtil, + useAppTreeBase, + findChildItems, + useLoadMoreUtil, + formatNodeDropType, + getNodeControlPanel, + getNewNodeControlPanel, +} from './el-tree-util'; +import { ResourceScheduleTreeController } from './resource-schedule-tree.controller'; +import './resource-schedule-tree.scss'; + +export const ResourceScheduleTree = defineComponent({ + name: 'IBizResourceScheduleTree', + props: { + /** + * @description 树模型数据 + */ + modelData: { type: Object as PropType, required: true }, + /** + * @description 应用上下文对象 + */ + context: { type: Object as PropType, required: true }, + /** + * @description 视图参数对象 + * @default {} + */ + params: { type: Object as PropType, default: () => ({}) }, + /** + * @description 部件适配器 + */ + provider: { type: Object as PropType }, + /** + * @description 树节点默认激活模式,值为0:不激活,值为1:单击激活,值为2:双击激活 + */ + mdctrlActiveMode: { type: Number, default: undefined }, + /** + * @description 是否单选 + */ + singleSelect: { type: Boolean, default: undefined }, + /** + * @description 是否是导航的 + */ + navigational: { type: Boolean, default: undefined }, + /** + * @description 默认展开节点集合 + */ + defaultExpandedKeys: { type: Array as PropType }, + /** + * @description 是否默认加载数据 + * @default true + */ + loadDefault: { type: Boolean, default: true }, + /** + * @description 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法 + * @default true + */ + checkStrictly: { type: Boolean, default: true }, + /** + * @description 是否是简单模式 + * @default false + */ + isSimple: { type: Boolean, default: false }, + /** + * @description 简单模式下传入的数据 + */ + data: { type: Array, required: false }, + }, + setup(props) { + const c: ResourceScheduleTreeController = + useControlController( + (...args) => new ResourceScheduleTreeController(...args), + ); + + useAppTreeBase(c, props); + + const cascadeSelect = ref(false); + const counterData: Ref = ref({}); + + // 上下文分组图标显示模式,值为hover时默认隐藏,hover时显示 + const menuShowMode: Ref<'default' | 'hover'> = ref('default'); + const fn = (counter: IData) => { + counterData.value = counter; + }; + c.evt.on('onCreated', () => { + if (c.counter) { + c.counter.onChange(fn, true); + } + if (c.controlParams.cascadeselect) { + cascadeSelect.value = true; + } + if (c.controlParams.menushowmode) { + menuShowMode.value = c.controlParams.menushowmode; + } + }); + + onUnmounted(() => { + c.counter?.offChange(fn); + }); + + const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + const ns2 = useNamespace(`resource-schedule-tree`); + + const treeRef = ref | null>(null); + const treeviewRef = ref(null); + + const treeRefreshKey = ref(''); + + // 节点名称编辑相关 + const treeNodeTextInputRef = ref(null); + const editingNodeKey = ref(null); + const editingNodeText = ref(null); + + // 新建树节点相关 + const newNodePanelRef = ref(); + const newTreeNodeText = ref(null); + const newNodeModel = ref(null); + const newDefaultValue = ref(null); + const newNodeKey = ref(`${createUUID()}-${createUUID()}`); + const newNodeData = ref(null); + const newNodeDeData = ref(null); + const newNodeControlPanel = ref(null); + c.evt.on('onNewTreeNode', async args => { + const { nodeModel, defaultValue, parentNodeData } = args; + const { appId, appDataEntityId } = nodeModel; + const entityModel = await ibiz.hub.getAppDataEntity( + appDataEntityId!, + appId, + ); + // 当新建节点存在布局面板时 + const layoutPanel = getNewNodeControlPanel(nodeModel); + if (layoutPanel) { + newNodeDeData.value = new AppDataEntity(entityModel, {}); + newNodeControlPanel.value = layoutPanel || null; + } + + newDefaultValue.value = defaultValue; + editingNodeKey.value = null; + editingNodeText.value = null; + + // 先销毁,避免新建多个节点 + if (newNodeData.value) { + treeRef.value?.remove(newNodeData.value); + newNodeData.value = null; + } + if (parentNodeData && treeRef.value) { + const _newNodeData = {}; + Object.assign(_newNodeData, { + _id: newNodeKey.value, + _text: '', + _leaf: true, + }); + treeRef.value.append(_newNodeData, parentNodeData); + newNodeData.value = _newNodeData; + } + newNodeModel.value = nodeModel; + }); + + watch( + () => treeNodeTextInputRef.value, + newVal => { + if (newVal) { + newVal.$el.getElementsByTagName('input')[0].focus(); + } + }, + ); + + c.evt.on('onSelectionChange', async () => { + if (!treeRef.value) return; + if (c.state.singleSelect) { + treeRef.value.setCurrentKey(c.state.selectedData[0]?._id); + } else { + treeRef.value.setCheckedKeys( + c.state.selectedData.map(item => item._id), + ); + } + }); + + /** + * 编辑当前节点的文本 + * @author lxm + * @date 2023-12-15 05:46:02 + * @return {*} {void} + */ + const editCurrentNodeText = (): void => { + const currentkey = treeRef.value?.getCurrentKey(); + if (!currentkey || currentkey === editingNodeKey.value) { + return; + } + const nodeData = findNodeData(currentkey, c)!; + const model = c.getNodeModel(nodeData._nodeId); + if (model?.allowEditText) { + editingNodeKey.value = currentkey; + } + }; + + /** + * 处理节点文本编辑/新建事件 + * @author lxm + * @date 2023-12-15 05:37:03 + */ + const onNodeTextEditBlur = async () => { + // 树节点编辑 + if (editingNodeKey.value) { + if (editingNodeText.value) { + const nodeData = findNodeData(editingNodeKey.value, c)!; + await c.modifyNodeText(nodeData, editingNodeText.value); + editingNodeKey.value = null; + editingNodeText.value = null; + } else { + // 取消编辑 + editingNodeKey.value = null; + } + } + // 树节点新建 + if (newNodeModel.value) { + // 检查 newNodeDeData.value 是否存在且包含至少一个非空值 + const hasNonNullValue = + newNodeDeData.value && + Object.values(newNodeDeData.value).some(_val => !!_val); + if (newNodeControlPanel.value && hasNonNullValue) { + const { textAppDEFieldId, id } = newNodeModel.value as IParams; + const nodeData = { + _nodeId: id, + _text: [newNodeDeData.value![textAppDEFieldId]], + _deData: {} as IData, + }; + Object.assign(nodeData._deData, newNodeDeData.value); + + if (newDefaultValue.value) { + Object.keys(newDefaultValue.value).forEach(_key => { + if (!nodeData._deData[_key]) + Object.assign(nodeData._deData, { + [_key]: newDefaultValue.value![_key], + }); + }); + } + await c.createDeNodeData([nodeData]); + } else if (newTreeNodeText.value) { + const { textAppDEFieldId, id } = newNodeModel.value as IParams; + const _text = newTreeNodeText.value; + const nodeData = { _deData: {} }; + Object.assign(nodeData, { _nodeId: id, _text }); + if (newDefaultValue.value) { + Object.assign(nodeData._deData, newDefaultValue.value); + } + Object.assign(nodeData._deData, { [textAppDEFieldId]: _text }); + await c.createDeNodeData([nodeData]); + } + newNodeModel.value = null; + newTreeNodeText.value = null; + newDefaultValue.value = null; + newNodeDeData.value = null; + newNodeControlPanel.value = null; + newNodePanelRef.value = null; + if (newNodeData.value) treeRef.value?.remove(newNodeData.value); + newNodeData.value = null; + } + }; + + /** + * 处理键盘ESC事件 + * @author tony001 + * @date 2023-12-15 05:37:03 + */ + const onNodeTextEditEsc = async () => { + // 树节点编辑框 + if (editingNodeKey.value) { + editingNodeKey.value = null; + editingNodeText.value = null; + } + // 新建树节点编辑框 + if (newNodeModel.value) { + newNodeModel.value = null; + newTreeNodeText.value = null; + newDefaultValue.value = null; + newNodeDeData.value = null; + newNodeControlPanel.value = null; + newNodePanelRef.value = null; + if (newNodeData.value) treeRef.value?.remove(newNodeData.value); + newNodeData.value = null; + } + }; + + /** + * 处理新建节点键盘事件 + * @author tony001 + * @date 2023-12-15 05:37:03 + */ + const handleEditKeyDown = (e: KeyboardEvent) => { + e.stopPropagation(); + if (e.code === 'Enter' || e.keyCode === 13) { + onNodeTextEditBlur(); + } + if (e.code === 'Escape' || e.keyCode === 27) { + onNodeTextEditEsc(); + } + }; + + /** + * 处理面板项事件 + * + * @param {IData} _args + * @return {*} + */ + const onPanelItemEvent = (_args: IData) => { + if (!_args) return; + const { panelItemEventName } = _args; + if (panelItemEventName === PanelItemEventName.ENTER) { + onNodeTextEditBlur(); + } + }; + const onPanelItemKeydown = (e: KeyboardEvent) => { + if (e.code === 'Escape' || e.keyCode === 27) onNodeTextEditEsc(); + }; + + const { updateUI, triggerNodeExpand } = useElTreeUtil(treeRef, c); + + const { addLoadMoreNode } = useLoadMoreUtil(treeRef, c); + + /** + * 创建新的节点对象,隔离组件数据和controller数据 + * @author lxm + * @date 2023-08-30 09:35:25 + * @param {ITreeNodeData[]} nodes + * @return {*} {IData[]} + */ + const toElNodes = (nodes: ITreeNodeData[]): IData[] => { + return nodes.map(node => ({ + _id: node._id, + _uuid: node._uuid, + _leaf: node._leaf, + _text: node._text, + _disableSelect: node._disableSelect, + })); + }; + + // el-treeUI更新子节点 + c.evt.on('onAfterRefreshParent', event => { + if (treeRef.value) { + const { parentNode, children } = event; + const elNodes = toElNodes(children); + treeRef.value!.updateKeyChildren(parentNode._id, elNodes); + updateUI(); + nextTick(() => { + addLoadMoreNode(parentNode._id, parentNode); + }); + } + }); + + c.evt.on('onAfterNodeDrop', event => { + if (event.isChangedParent) { + // 变更父节点的时候强刷 + treeRefreshKey.value = createUUID(); + } + }); + + // 更新UI + c.evt.on('onUpdateUI', () => { + updateUI(); + }); + + /** 树展示数据 */ + const treeData = computed(() => { + if (!c.state.isLoaded) { + return []; + } + return c.model.rootVisible + ? c.state.rootNodes + : c.state.rootNodes.reduce((result, nodeData) => { + if (nodeData._children) { + return result.concat(nodeData._children as ITreeNodeData[]); + } + return result; + }, []); + }); + + // 根节点数据变更时重绘tree + watch(treeData, (newVal, oldVal) => { + if (newVal !== oldVal) { + treeRefreshKey.value = createUUID(); + } + }); + + watch( + () => c.state.expandedKeys, + () => { + updateUI(); + }, + { deep: true }, + ); + + /** + * 触发节点加载数据 + * @author lxm + * @date 2023-05-29 09:16:07 + * @param {IData} item + * @param {(nodes: IData[]) => void} callback + */ + const loadData = async ( + item: IData, + callback: (nodes: IData[]) => void, + ) => { + if (props.isSimple) { + // simple模式 + let children: ITreeNodeData[] = []; + if (item.level === 0) { + const tempNodes = c.state.items.find((_item: IData) => { + return _item.isRoot; + }); + if (tempNodes) { + children = [tempNodes]; + } + } else { + children = findChildItems( + c, + props.modelData, + item, + ) as ITreeNodeData[]; + } + item._children = children; + callback(toElNodes(children)); + updateUI(); + return; + } + let nodes: ITreeNodeData[]; + if (item.level === 0) { + nodes = treeData.value as ITreeNodeData[]; + ibiz.log.debug('初始加载'); + } else { + const nodeData = findNodeData(item.data._uuid, c)!; + if (nodeData._children) { + ibiz.log.debug('节点展开加载-本地', nodeData); + nodes = nodeData._children; + } else { + ibiz.log.debug('节点展开加载-远程', nodeData); + nodes = await c.loadNodes(nodeData); + } + } + ibiz.log.debug('给树返回值', nodes); + + callback(toElNodes(nodes)); + updateUI(); + nextTick(() => { + addLoadMoreNode( + item.level === 0 ? c.state.rootNodes?.[0]?._id : item.data?._id, + item, + ); + }); + }; + + /** + * 多选时选中节点变更 + */ + const onCheck = ( + nodeData: ITreeNodeData, + opts: { checkedNodes: ITreeNodeData[] }, + ) => { + const { checkedNodes } = opts; + c.setSelection(checkedNodes); + }; + + let forbidClick: boolean = false; + + const readonly = computed(() => { + return !!( + c.context.srfreadonly === true || c.context.srfreadonly === 'true' + ); + }); + + /** + * 节点单击事件 + */ + const onNodeClick = ( + nodeData: ITreeNodeData, + data: IData, + evt: MouseEvent, + ) => { + evt.stopPropagation(); + if (nodeData._disableSelect || forbidClick) return; + + // 已经是当前节点,则进入编辑模式 + if (treeRef.value?.getCurrentKey() === nodeData._id && !readonly.value) { + editCurrentNodeText(); + } + + // 设置节点的当前节点 + treeRef.value?.setCurrentKey(nodeData._id); + + // 导航树节点不配置导航视图的时候,只切换展开状态 + if (c.state.navigational) { + const nodeModel = c.getNodeModel(nodeData._nodeId); + if (!nodeModel?.navAppViewId) { + const expanded = triggerNodeExpand(nodeData._id); + c.onExpandChange(nodeData, expanded!); + } + } + if (props.isSimple) { + treeRef.value!.setCurrentKey(data?._id); + } else { + c.onTreeNodeClick(nodeData, evt); + } + forbidClick = true; + setTimeout(() => { + forbidClick = false; + }, 200); + }; + + /** + * 节点双击事件 + */ + const onNodeDbClick = (nodeData: ITreeNodeData, evt: MouseEvent) => { + evt.stopPropagation(); + if (nodeData._disableSelect) return; + c.onDbTreeNodeClick(nodeData); + }; + + // *上下文菜单相关 / + + 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; + } + }); + } + }); + + const iBizRawItem = resolveComponent('IBizRawItem'); + const iBizIcon = resolveComponent('IBizIcon'); + + /** + * 计算上下文菜单组件配置项集合 + */ + const calcContextMenuItems = ( + toolbarItems: IDEToolbarItem[], + nodeData: ITreeNodeData, + 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; + + // 除分隔符之外的公共部分 + let menuItem: MenuItem | undefined = {}; + 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') { + // 分组项绘制子菜单 + const group = item as IDETBGroupItem; + if (group.detoolbarItems?.length) { + menuItem.children = calcContextMenuItems( + group.detoolbarItems!, + nodeData, + evt, + menuState, + ); + } + // 分组项配置界面行为组 + if (group.uiactionGroup && group.groupExtractMode) { + const menuItems = group.uiactionGroup.uiactionGroupDetails + ?.filter(detail => { + const detailState: IButtonState = menuState[detail.id!]; + return detailState.visible; + }) + .map(detail => { + const detailState: IButtonState = menuState[detail.id!]; + const { sysImage } = detail as IData; + return { + label: detail.showCaption ? detail.caption : undefined, + icon: detail.showIcon ? ( + + ) : undefined, + disabled: detailState.disabled, + clickableWhenHasChildren: true, + onClick: () => { + ContextMenu.closeContextMenu(); + c.doUIAction( + detail.uiactionId!, + nodeData, + evt, + detail.appId, + ); + }, + }; + }); + switch (group.groupExtractMode) { + case 'ITEMS': + menuItem.children = menuItems; + break; + case 'ITEMX': + if (menuItems) { + menuItem = menuItems[0]; + menuItem.children = menuItems.slice(1); + } + break; + case 'ITEM': + default: + menuItem = undefined; + if (menuItems) result.push(...menuItems); + break; + } + } + } + if (menuItem) result.push(menuItem); + }); + + return result; + }; + + /** + * 节点右键菜单点击事件 + */ + const onNodeContextmenu = async ( + nodeData: ITreeNodeData, + evt: MouseEvent, + ) => { + // 阻止原生浏览器右键菜单打开 + evt.preventDefault(); + evt.stopPropagation(); + let stopClickTag = ibiz.config.tree.contextMenuRightClickInvoke; + if (c.controlParams?.contextmenurightclickinvoke) { + stopClickTag = Object.is( + c.controlParams.contextmenurightclickinvoke, + 'true', + ); + } + + if (!stopClickTag) return; + 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, + { view: c.view, ctrl: c }, + ); + const menuState = contextMenuC.state.buttonsState; + + const menus: MenuItem[] = calcContextMenuItems( + contextMenuC.model.detoolbarItems, + nodeData, + evt, + menuState as IButtonContainerState, + ); + if (!menus.length) return; + + ContextMenu.showContextMenu({ + x: evt.x, + y: evt.y, + customClass: ns.b('context-menu'), + items: menus, + }); + }; + + /** + * 绘制上下文菜单触发图标 + * @param nodeModel + * @param nodeData + * @returns + */ + const renderContextMenu = ( + nodeModel: IDETreeNode, + nodeData: ITreeNodeData, + ) => { + if (!nodeModel?.decontextMenu?.detoolbarItems?.length) return; + + // 只有一个界面行为项时,且是点击触发的界面行为时,不绘制。。。 + const menuInfo = c.contextMenuInfos[nodeModel.id!]; + if (menuInfo.clickTBUIActionItem && menuInfo.onlyOneActionItem) + return null; + + return ( + + c.doUIAction(detail.uiactionId!, nodeData, e, detail.appId) + } + > + ); + }; + + const updateNodeExpand = (data: IData, expanded: boolean) => { + const nodeData = findNodeData(data._uuid, c); + if (!nodeData) { + throw new RuntimeError( + ibiz.i18n.t('control.common.noFoundNode', { id: data._uuid }), + ); + } + if (props.isSimple) { + // 之前处理过节点数据的_id,避免循环嵌套进入死循环,这里需要对_id在再次进行处理保证选中展开效果 + const tempData = cloneDeep(nodeData); + tempData._id = data._id; + c.onExpandChange(tempData as ITreeNodeData, expanded); + } else { + c.onExpandChange(nodeData as ITreeNodeData, expanded); + } + }; + + const debounceSearch = debounce(() => { + c.load(); + }, 500); + + const onInput = (value: string): void => { + c.state.query = value; + debounceSearch(); + }; + + // 拖拽相关 + const allowDrop = ( + draggingNode: IData, + dropNode: IData, + type: AllowDropType, + ) => { + if (dropNode.data?._load_more) return false; + const draggingNodeData = findNodeData(draggingNode.data._uuid, c)!; + const dropNodeData = findNodeData(dropNode.data._uuid, c)!; + const result = c.calcAllowDrop(draggingNodeData, dropNodeData, type); + return result; + }; + + const allowDrag = (draggingNode: IData) => { + if (draggingNode.data?._load_more) return false; + const nodeData = findNodeData(draggingNode.data._uuid, c)!; + return c.calcAllowDrag(nodeData); + }; + + /** + * 处理拖入完成事件 + * @author lxm + * @date 2023-12-15 05:37:10 + * @param {IData} draggingNode + * @param {IData} dropNode + * @param {NodeDropType} dropType + */ + const handleDrop = ( + draggingNode: IData, + dropNode: IData, + dropType: NodeDropType, + ) => { + const type = formatNodeDropType(dropType); + const draggingNodeData = findNodeData(draggingNode.data._uuid, c)!; + const dropNodeData = findNodeData(dropNode.data._uuid, c)!; + c.onNodeDrop(draggingNodeData, dropNodeData, type); + }; + + /** + * 处理按键事件 + * @author lxm + * @date 2023-12-15 05:39:00 + * @param {KeyboardEvent} e + */ + const keydownHandle = (e: KeyboardEvent) => { + if (e.code === 'F2' || e.code === 'Enter') editCurrentNodeText(); + }; + + /** + * 处理全局鼠标抬起事件 + */ + const handleMouseup = () => { + // 布局面板内是否存在聚焦,如果存在,则不销毁新建节点。适配下拉框选择 + const isFoucs = newNodePanelRef.value?.$el.querySelector('.is-focus'); + // 处理新建节点布局面板绘制后的关闭逻辑 + if (newNodeControlPanel.value && !isFoucs) onNodeTextEditBlur(); + }; + + onMounted(() => { + document.addEventListener('mouseup', handleMouseup.bind(this)); + treeviewRef.value?.$el?.addEventListener('keydown', keydownHandle); + }); + + onUnmounted(() => { + document.removeEventListener('mouseup', handleMouseup.bind(this)); + treeviewRef.value?.$el.removeEventListener('keydown', keydownHandle); + }); + + const renderCounter = (nodeModel: IDETreeNode) => { + if (nodeModel.counterId) { + const value = counterData.value[nodeModel.counterId]; + if (isNil(value)) return null; + if (nodeModel.counterMode === 1 && value === 0) return null; + return ; + } + }; + + // 绘制新增节点 + const renderNewNode = () => { + if (!newNodeModel.value) return null; + if (newNodeControlPanel.value) { + return ( + _e.stopPropagation()} + onPanelItemEvent={onPanelItemEvent} + onKeydown={onPanelItemKeydown} + > + ); + } + return ( +
{ + _e.preventDefault(); + _e.stopPropagation(); + }} + > + {newNodeModel.value?.sysImage ? ( + + ) : null} + { + onNodeTextEditBlur(); + }} + onKeydown={(e: KeyboardEvent) => { + handleEditKeyDown(e); + }} + > +
+ ); + }; + + // 处理加载更多 + const handleLoadMore = async (e: MouseEvent, item: IData) => { + e.stopPropagation(); + if (!item) return; + if (item.level === 0) { + await c.loadNodes(undefined, true); + return; + } + const nodeData = findNodeData(item.data?._uuid, c); + if (!nodeData) return; + await c.loadNodes(nodeData, true); + const elNodes = toElNodes(nodeData._children || []); + treeRef.value?.updateKeyChildren(nodeData._id, elNodes); + updateUI(); + nextTick(() => { + addLoadMoreNode(nodeData._id, nodeData); + }); + }; + + /** + * @description 处理开始拖拽 + * @param {MouseEvent} payload + * @param {ITreeNodeData} node + */ + const handleDragStart = (payload: DragEvent, node: ITreeNodeData) => { + payload.dataTransfer!.effectAllowed = 'copy'; + const item = { + dragType: 'add', + data: node._deData, + scheduleType: node._nodeId === 'resourses' ? 'resource' : 'task', + } + payload.dataTransfer!.setData('text/plain', JSON.stringify(item)); + }; + + /** + * @description 绘制工具栏 + * @returns {*} + */ + const renderToolber = () => { + if (c.toolbarModel) + return ( + + ); + }; + + /** + * @description 绘制搜索栏 + * @returns {*} + */ + const renderSearchBar = () => { + if (c.searchBarModel) + return ( + c.load()} + /> + ); + }; + + /** + * @description 绘制搜索表单 + * @returns {*} + */ + const renderSearchFrom = () => { + if (c.searchFormModel) + return ( + c.load()} + /> + ); + }; + + /** + * @description 绘制头部内容 + * @returns {*} + */ + const renderHearder = () => { + return ( +
+
+ {c.model.logicName} +
+
+ {renderSearchBar()} + {renderToolber()} +
+
+ ); + }; + + return { + c, + ns, + ns2, + treeRef, + treeData, + newNodeKey, + treeviewRef, + newNodeData, + cascadeSelect, + treeRefreshKey, + editingNodeKey, + editingNodeText, + treeNodeTextInputRef, + onCheck, + onInput, + loadData, + allowDrop, + allowDrag, + handleDrop, + onNodeClick, + findNodeData, + onNodeDbClick, + renderHearder, + renderCounter, + renderNewNode, + handleLoadMore, + handleDragStart, + updateNodeExpand, + renderSearchFrom, + handleEditKeyDown, + onNodeContextmenu, + renderContextMenu, + onNodeTextEditBlur, + }; + }, + render() { + return ( + +
+ {this.renderHearder()} + {this.renderSearchFrom()} + { + if (data?._load_more) return this.ns.is('load-more', true); + return ''; + }, + }} + lazy + load={this.loadData} + onCheck={this.onCheck} + onNodeExpand={(data: IData) => { + this.updateNodeExpand(data, true); + }} + onNodeCollapse={(data: IData) => { + this.updateNodeExpand(data, false); + }} + allow-drop={this.allowDrop} + allow-drag={this.allowDrag} + onNodeDrop={this.handleDrop} + {...this.$attrs} + > + {{ + default: ({ data, node }: { node: IData; data: IData }) => { + if (data._load_more) { + return ( +
{ + this.handleLoadMore(e, node?.parent); + }} + > + {data._text} +
+ ); + } + // 绘制新建项 + if (this.newNodeKey === data._id) return this.renderNewNode(); + + const nodeData = this.findNodeData(data._uuid, this.c)!; + if (!nodeData) { + return null; + } + const nodeModel = this.c.getNodeModel(nodeData._nodeId)!; + + // 绘制编辑项 + if (this.editingNodeKey === nodeData._id) { + return ( +
+ { + this.onNodeTextEditBlur(); + }} + onKeydown={(e: KeyboardEvent) => { + this.handleEditKeyDown(e); + }} + > +
+ ); + } + + const layoutPanel = getNodeControlPanel(nodeModel); + + let content; + if (layoutPanel) { + content = ( + + ); + } else { + content = [ + nodeData._icon ? ( + + ) : null, + nodeData._textHtml ? ( + + ) : ( + + {nodeData._text} + + ), + ]; + } + + return ( +
this.onNodeDbClick(nodeData, evt)} + onClick={evt => this.onNodeClick(nodeData, data, evt)} + onDragstart={evt => this.handleDragStart(evt, nodeData)} + onContextmenu={evt => this.onNodeContextmenu(nodeData, evt)} + > + {content} + {this.renderCounter(nodeModel)} + {this.renderContextMenu(nodeModel, nodeData)} +
+ ); + }, + }} +
+ {!this.newNodeData && this.renderNewNode()} + {this.c.state.enableNavView && this.c.state.showNavIcon ? ( + !this.c.state.showNavView ? ( + this.c.onShowNavViewChange()} + > + ) : ( + this.c.onShowNavViewChange()} + > + ) + ) : null} +
+
+ ); + }, +}); diff --git a/src/resource-scheduler/components/schedule-table/schedule-table.tsx b/src/resource-scheduler/components/schedule-table/schedule-table.tsx index 5964503..1d168de 100644 --- a/src/resource-scheduler/components/schedule-table/schedule-table.tsx +++ b/src/resource-scheduler/components/schedule-table/schedule-table.tsx @@ -1,170 +1,180 @@ -import { computed, defineComponent, onMounted, PropType, ref } from "vue"; -import { IResourceViewModel, IScheduleResource, IScheduleTask, ITaskViewModel } from "../../interface"; -import Variables from "../../constant/vars"; -import { initStore } from "../../store"; -import { useInitScheduleTable, useScheduleTableReSize, useScheduleTableStyle, useVirtualScroll } from "../../hooks/use-schedule-table"; -import './schedule-table.scss'; - - -export const ScheduleTable = defineComponent({ - name: 'ScheduleTable', - props: { - resources: { - type: Array as PropType, - default: () => [], - }, - tasks: { - type: Array as PropType, - default: () => [], - }, - startTime: { - type: Object as PropType, - default: Variables.default.startTime, - }, - endTime: { - type: Object as PropType, - default: Variables.default.endTime, - }, - headerRowHeight: { - type: Number, - default: Variables.default.headerRowHeight, - }, - headerBgColor: { - type: String, - default: Variables.default.headerBgColor, - }, - headerTextColor: { - type: String, - default: Variables.default.headerTextColor, - }, - resourceColumnWidth: { - type: Number, - default: Variables.default.resourceColumnWidth, - }, - resourceBodyRowHeight: { - type: Number, - default: Variables.default.resourceBodyRowHeight, - }, - resourceBodyBg: { - type: String, - default: Variables.default.resourceBodyBg, - }, - resourceDriverColor: { - type: String, - default: Variables.default.resourceDriverColor, - }, - scaleColumnWidth: { - type: Number, - default: Variables.default.scaleColumnWidth, - }, - scaleRange: { - type: Array as PropType, - default: Variables.default.scaleRange, - }, - scaleValue: { - type: Number, - default: Variables.default.scaleValue, - }, - scaleBodyBg: { - type: String, - default: Variables.default.scaleBodyBg, - }, - scaleTextColor: { - type: String, - default: Variables.default.scaleTextColor, - }, - gridLineColor: { - type: String, - default: Variables.default.gridLineColor, - }, - }, - emits: [ - ], - setup(props, { emit }) { - - // 头部canvas实例 - const headerCanvas = ref(null); - - // 内容canvas实例 - const bodyCanvas = ref(null); - - // 坐标元素 - const coordinateElement = ref(null); - - // 初始化store - initStore(props, emit); - - // 初始化表格 - const { resourceViewModels, taskViewModels } = useInitScheduleTable(headerCanvas, bodyCanvas, coordinateElement); - - // 计算表格样式 - const { headerStyle, bodyStyle } = useScheduleTableStyle(props); - - // 监听视图大小变化 - useScheduleTableReSize(props, headerCanvas, bodyCanvas, coordinateElement); - - // 虚拟滚动 - const { visibleRange, handleScroll } = useVirtualScroll(coordinateElement); - - // 过滤可见的资源 - const visibleResourceViewModels = computed(() => { - if (!resourceViewModels.value) return []; - return resourceViewModels.value.slice(visibleRange.value.start, visibleRange.value.end); - }); - - // 过滤可见的任务 - const visibleTaskViewModels = computed(() => { - if (!taskViewModels.value) return []; - return taskViewModels.value.filter((task:ITaskViewModel) => { - const resourceIndex = props.resources.findIndex(res => res.id === task.resourceId); - return resourceIndex >= visibleRange.value.start && resourceIndex < visibleRange.value.end; - }); - }); - - return { - headerCanvas, - bodyCanvas, - coordinateElement, - headerStyle, - bodyStyle, - visibleResourceViewModels, - visibleTaskViewModels, - handleScroll, - visibleRange - }; - }, - render() { - return
-
- -
-
- -
- { - this.visibleResourceViewModels.length > 0 ? <> - { - this.visibleResourceViewModels.map((resourceViewModel: IResourceViewModel) => { - return
- {resourceViewModel.data.name} -
- }) - } - : null - } - { - this.visibleTaskViewModels.length > 0 ? <> - { - this.visibleTaskViewModels.map((taskViewModel: ITaskViewModel) => { - return
- {taskViewModel.data.name} -
- }) - } - : null - } -
-
-
- }, +import { computed, defineComponent, PropType, ref } from "vue"; +import { IDragItem, IResourceViewModel, IScheduleResource, IScheduleTask, ITaskViewModel } from "../../interface"; +import Variables from "../../constant/vars"; +import { initStore } from "../../store"; +import { useInitScheduleTable, useScheduleDrop, useScheduleTableReSize, useScheduleTableStyle, useVirtualScroll } from "../../hooks/use-schedule-table"; +import './schedule-table.scss'; + + +export const ScheduleTable = defineComponent({ + name: 'ScheduleTable', + props: { + resources: { + type: Array as PropType, + default: () => [], + }, + tasks: { + type: Array as PropType, + default: () => [], + }, + verifyDrag: { + type: Function as PropType<(data: IDragItem) => Promise<{ + ok: boolean; + data: IDragItem; + }>>, + }, + startTime: { + type: Object as PropType, + default: Variables.default.startTime, + }, + endTime: { + type: Object as PropType, + default: Variables.default.endTime, + }, + headerRowHeight: { + type: Number, + default: Variables.default.headerRowHeight, + }, + headerBgColor: { + type: String, + default: Variables.default.headerBgColor, + }, + headerTextColor: { + type: String, + default: Variables.default.headerTextColor, + }, + resourceColumnWidth: { + type: Number, + default: Variables.default.resourceColumnWidth, + }, + resourceBodyRowHeight: { + type: Number, + default: Variables.default.resourceBodyRowHeight, + }, + resourceBodyBg: { + type: String, + default: Variables.default.resourceBodyBg, + }, + resourceDriverColor: { + type: String, + default: Variables.default.resourceDriverColor, + }, + scaleColumnWidth: { + type: Number, + default: Variables.default.scaleColumnWidth, + }, + scaleRange: { + type: Array as PropType, + default: Variables.default.scaleRange, + }, + scaleValue: { + type: Number, + default: Variables.default.scaleValue, + }, + scaleBodyBg: { + type: String, + default: Variables.default.scaleBodyBg, + }, + scaleTextColor: { + type: String, + default: Variables.default.scaleTextColor, + }, + gridLineColor: { + type: String, + default: Variables.default.gridLineColor, + }, + }, + emits: [ + ], + setup(props, { emit }) { + + // 头部canvas实例 + const headerCanvas = ref(null); + + // 内容canvas实例 + const bodyCanvas = ref(null); + + // 坐标元素 + const coordinateElement = ref(null); + + // 初始化store + initStore(props, emit); + + // 初始化表格 + const { resourceViewModels, taskViewModels } = useInitScheduleTable(headerCanvas, bodyCanvas, coordinateElement); + + // 计算表格样式 + const { headerStyle, bodyStyle } = useScheduleTableStyle(props); + + // 监听视图大小变化 + useScheduleTableReSize(props, headerCanvas, bodyCanvas, coordinateElement); + + // 虚拟滚动 + const { visibleRange, handleScroll } = useVirtualScroll(coordinateElement); + + const { handleDrop, handleDragOver } = useScheduleDrop(props, coordinateElement, bodyCanvas); + + // 过滤可见的资源 + const visibleResourceViewModels = computed(() => { + if (!resourceViewModels.value) return []; + return resourceViewModels.value.slice(visibleRange.value.start, visibleRange.value.end); + }); + + // 过滤可见的任务 + const visibleTaskViewModels = computed(() => { + if (!taskViewModels.value) return []; + return taskViewModels.value.filter((task:ITaskViewModel) => { + const resourceIndex = props.resources.findIndex(res => res.id === task.resourceId); + return resourceIndex >= visibleRange.value.start && resourceIndex < visibleRange.value.end; + }); + }); + + return { + headerCanvas, + bodyCanvas, + coordinateElement, + headerStyle, + bodyStyle, + visibleResourceViewModels, + visibleTaskViewModels, + handleScroll, + visibleRange, + handleDrop, + handleDragOver + }; + }, + render() { + return
+
+ +
+
+ +
+ { + this.visibleResourceViewModels.length > 0 ? <> + { + this.visibleResourceViewModels.map((resourceViewModel: IResourceViewModel) => { + return
+ {resourceViewModel.data.name} +
+ }) + } + : null + } + { + this.visibleTaskViewModels.length > 0 ? <> + { + this.visibleTaskViewModels.map((taskViewModel: ITaskViewModel) => { + return
+ {taskViewModel.data.name} +
+ }) + } + : null + } +
+
+
+ }, }); \ No newline at end of file diff --git a/src/resource-scheduler/controller/index.ts b/src/resource-scheduler/controller/index.ts index bdc0c95..b1ca2e7 100644 --- a/src/resource-scheduler/controller/index.ts +++ b/src/resource-scheduler/controller/index.ts @@ -1,3 +1,3 @@ -export { BgLayerController } from './bg-layer.controller'; -export { RenderLayerController } from './render-layer.controller'; +export { BgLayerController } from './bg-layer.controller'; +export { RenderLayerController } from './render-layer.controller'; export { GlobalConfigController } from './global-config.controller'; \ No newline at end of file diff --git a/src/resource-scheduler/controller/render-layer.controller.ts b/src/resource-scheduler/controller/render-layer.controller.ts index 09d96fa..cce0c8f 100644 --- a/src/resource-scheduler/controller/render-layer.controller.ts +++ b/src/resource-scheduler/controller/render-layer.controller.ts @@ -1,217 +1,245 @@ -import { get } from "http" -import { IGlobalConfig, IResourceViewModel, IScheduleResource, IScheduleTask, ITaskViewModel } from "../interface" -import { set } from "nprogress" -import Variables from "../constant/vars"; -import { UICoordinateController } from "./ui-coordinate.controller"; -import { GlobalConfigController } from "./global-config.controller"; - -/** - * @description 绘制图层控制器 - * @export - * @class RenderLayerController - */ -export class RenderLayerController { - - /** - * @description 资源 - * @type {IScheduleResource[]} - * @memberof RenderLayerController - */ - private resources: IScheduleResource[] = []; - - /** - * @description 任务 - * @private - * @type {IScheduleTask[]} - * @memberof RenderLayerController - */ - private tasks: IScheduleTask[] = []; - - /** - * @description 全局配置 - * @private - * @type {GlobalConfigController} - * @memberof RenderLayerController - */ - private configController!: GlobalConfigController; - - /** - * @description 资源视图模型 - * @type {IResourceViewModel[]} - * @memberof RenderLayerController - */ - resourceViewModels: IResourceViewModel[] = []; - - /** - * @description 任务视图模型 - * @type {ITaskViewModel[]} - * @memberof RenderLayerController - */ - taskViewModels: ITaskViewModel[] = []; - - /** - * Creates an instance of RenderLayerController. - * @param {GlobalConfigController} configController - * @param {IScheduleResource[]} resources - * @param {IScheduleTask[]} tasks - * @memberof RenderLayerController - */ - constructor(configController: GlobalConfigController, resources: IScheduleResource[], tasks: IScheduleTask[]) { - this.configController = configController; - this.resources = resources; - this.tasks = this.filterValidTasks(tasks); - } - - /** - * @description 获取资源 - * @readonly - * @type {IScheduleResource[]} - * @memberof RenderLayerController - */ - getResources(): IScheduleResource[] { return this.resources } - - /** - * @description 设置资源 - * @param {IScheduleResource[]} resources - * @memberof RenderLayerController - */ - setResources(resources: IScheduleResource[]) { this.resources = resources } - - /** - * @description 获取任务 - * @returns {*} {IScheduleTask[]} - * @memberof RenderLayerController - */ - getTasks(): IScheduleTask[] { return this.tasks } - - /** - * @description 设置任务 - * @param {IScheduleTask[]} tasks - * @memberof RenderLayerController - */ - setTasks(tasks: IScheduleTask[]) { this.tasks = this.filterValidTasks(tasks); } - - - /** - * @description 过滤有效任务 - * @param {IScheduleTask[]} tasks 任务列表 - * @param {IGlobalConfig} config 全局配置 - * @returns {IScheduleTask[]} 有效任务列表 - * @memberof RenderLayerController - */ - filterValidTasks(tasks: IScheduleTask[]): IScheduleTask[] { - const config = this.configController.getConfig(); - return tasks.filter(task => { - // 检查任务的开始时间和结束时间是否有效 - if (task.start >= task.end) { - return false; - } - // 如果任务的结束时间小于全局开始时间,则任务无效 - if (task.end < config.startTime) { - return false; - } - // 如果任务的开始时间大于全局结束时间,则任务无效 - if (task.start > config.endTime) { - return false; - } - return true; - }); - } - - /** - * @description 构建资源视图模型 - * @private - * @param {IGlobalConfig} config - * @memberof RenderLayerController - */ - private buildResourceViewModels(config: IGlobalConfig): void { - const resourceViewModels: IResourceViewModel[] = []; - if (this.resources.length > 0) { - for (let i = 0; i < this.resources.length; i++) { - const resource = this.resources[i]; - resourceViewModels.push({ - id: resource.id, - width: config.resourceColumnWidth, - height: config.resourceBodyRowHeight, - top: i * config.resourceBodyRowHeight, - left: 0, - data: resource, - }) - } - } - this.resourceViewModels = resourceViewModels; - } - - /** - * @description 构建任务视图模型 - * @private - * @param {IGlobalConfig} config - * @param {UICoordinateController} uiCoordinate - * @returns {*} {void} - * @memberof RenderLayerController - */ - private buildTaskViewModels(config: IGlobalConfig, uiCoordinate: UICoordinateController): void { - if (!uiCoordinate) { - return; - } - const taskViewModels: ITaskViewModel[] = []; - if (this.tasks.length > 0) { - // 计算时间范围内的天数 - const startDate = new Date(config.startTime); - startDate.setHours(0, 0, 0, 0); - const endDate = new Date(config.endTime); - endDate.setHours(0, 0, 0, 0); - for (let i = 0; i < this.tasks.length; i++) { - const task = this.tasks[i]; - const resourceIndex = this.resources.findIndex(resource => resource.id === task.resourceId); - if (resourceIndex === -1) { - console.warn(`Task ${task.id} references non-existent resource ${task.resourceId}`); - continue; - } - // 计算任务日期属性 - const taskStartDate = new Date(task.start); - taskStartDate.setHours(0, 0, 0, 0); - const taskEndDate = new Date(task.end); - taskEndDate.setHours(0, 0, 0, 0); - - // 计算任务在日期轴上的位置 - let daysFromStart = Math.ceil((taskStartDate.getTime() - startDate.getTime()) / Variables.time.millisecondOf.day); - const left = config.resourceColumnWidth + config.scaleColumnWidth + (daysFromStart * uiCoordinate.cellWidth); - - // 如果任务跨多天,计算宽度 - const taskDurationDays = Math.ceil((taskEndDate.getTime() - taskStartDate.getTime()) / Variables.time.millisecondOf.day) + 1; - const width = taskDurationDays * uiCoordinate.cellWidth; - - // 计算任务在时间轴上的位置(垂直方向) - const taskStartHour = task.start.getHours() + task.start.getMinutes() / 60; - const top = resourceIndex * config.resourceBodyRowHeight + (taskStartHour - config.scaleRange[0]) * (uiCoordinate.cellHeight / config.scaleValue); - - // 计算任务高度 - const taskDurationHours = (task.end.getTime() - task.start.getTime()) / Variables.time.millisecondOf.hour; - const height = taskDurationHours * (uiCoordinate.cellHeight / config.scaleValue); - - taskViewModels.push({ - id: task.id, - resourceId: task.resourceId, - width, - height, - top, - left, - data: task, - }) - } - } - this.taskViewModels = taskViewModels; - } - - /** - * @description 构建视图模型 - * @param {UICoordinateController} uiCoordinate - * @memberof RenderLayerController - */ - buildViewModels(uiCoordinate: UICoordinateController): void { - const config = this.configController.getConfig(); - this.buildResourceViewModels(config); - this.buildTaskViewModels(config, uiCoordinate); - } +import { IGlobalConfig, IResourceViewModel, IScheduleResource, IScheduleTask, ITaskViewModel } from "../interface" +import Variables from "../constant/vars"; +import { UICoordinateController } from "./ui-coordinate.controller"; +import { GlobalConfigController } from "./global-config.controller"; + +/** + * @description 绘制图层控制器 + * @export + * @class RenderLayerController + */ +export class RenderLayerController { + + /** + * @description 资源 + * @type {IScheduleResource[]} + * @memberof RenderLayerController + */ + private resources: IScheduleResource[] = []; + + /** + * @description 任务 + * @private + * @type {IScheduleTask[]} + * @memberof RenderLayerController + */ + private tasks: IScheduleTask[] = []; + + /** + * @description 全局配置 + * @private + * @type {GlobalConfigController} + * @memberof RenderLayerController + */ + private configController!: GlobalConfigController; + + /** + * @description 资源视图模型 + * @type {IResourceViewModel[]} + * @memberof RenderLayerController + */ + resourceViewModels: IResourceViewModel[] = []; + + /** + * @description 任务视图模型 + * @type {ITaskViewModel[]} + * @memberof RenderLayerController + */ + taskViewModels: ITaskViewModel[] = []; + + /** + * Creates an instance of RenderLayerController. + * @param {GlobalConfigController} configController + * @param {IScheduleResource[]} resources + * @param {IScheduleTask[]} tasks + * @memberof RenderLayerController + */ + constructor(configController: GlobalConfigController, resources: IScheduleResource[], tasks: IScheduleTask[]) { + this.configController = configController; + this.resources = resources; + this.tasks = this.filterValidTasks(tasks); + } + + /** + * @description 获取资源 + * @readonly + * @type {IScheduleResource[]} + * @memberof RenderLayerController + */ + getResources(): IScheduleResource[] { return this.resources } + + /** + * @description 设置资源 + * @param {IScheduleResource[]} resources + * @memberof RenderLayerController + */ + setResources(resources: IScheduleResource[]) { this.resources = resources } + + /** + * @description 添加资源 + * @param {IScheduleResource} resource 资源 + * @param {number} [index=-1] 添加位置,如果为-1则添加到最后 + * @memberof RenderLayerController + */ + addResource(resource: IScheduleResource, index: number = -1) { + if (index === -1 ) { + this.resources.push(resource); + } else { + this.resources.splice(index, 0, resource); + } + } + + /** + * @description 获取任务 + * @returns {*} {IScheduleTask[]} + * @memberof RenderLayerController + */ + getTasks(): IScheduleTask[] { return this.tasks } + + /** + * @description 设置任务 + * @param {IScheduleTask[]} tasks + * @memberof RenderLayerController + */ + setTasks(tasks: IScheduleTask[]) { this.tasks = this.filterValidTasks(tasks); } + + /** + * @description 添加任务 + * @param {IScheduleTask} task + * @memberof RenderLayerController + */ + addTask(task: IScheduleTask) { + if (this.verifyTask(task)) this.tasks.push(task) + } + + /** + * @description 更新任务 + * @param {IScheduleTask} task + * @memberof RenderLayerController + */ + updateTask(task: IScheduleTask) { + const index = this.tasks.findIndex(_task => _task.id === task.id); + if (index !== -1 && this.verifyTask(task)) { + this.tasks.splice(index, 1, task); + } + } + + verifyTask(task: IScheduleTask) { + const config = this.configController.getConfig(); + // 检查任务的开始时间和结束时间是否有效 + if (task.start >= task.end) return false; + // 如果任务的结束时间小于全局开始时间,则任务无效 + if (task.end < config.startTime) return false; + // 如果任务的开始时间大于全局结束时间,则任务无效 + if (task.start > config.endTime) return false; + return true; + } + + /** + * @description 过滤有效任务 + * @param {IScheduleTask[]} tasks 任务列表 + * @param {IGlobalConfig} config 全局配置 + * @returns {IScheduleTask[]} 有效任务列表 + * @memberof RenderLayerController + */ + filterValidTasks(tasks: IScheduleTask[]): IScheduleTask[] { + return tasks.filter(task => this.verifyTask(task)); + } + + /** + * @description 构建资源视图模型 + * @private + * @param {IGlobalConfig} config + * @memberof RenderLayerController + */ + private buildResourceViewModels(config: IGlobalConfig): void { + const resourceViewModels: IResourceViewModel[] = []; + if (this.resources.length > 0) { + for (let i = 0; i < this.resources.length; i++) { + const resource = this.resources[i]; + resourceViewModels.push({ + id: resource.id, + width: config.resourceColumnWidth, + height: config.resourceBodyRowHeight, + top: i * config.resourceBodyRowHeight, + left: 0, + data: resource, + }) + } + } + this.resourceViewModels = resourceViewModels; + } + + /** + * @description 构建任务视图模型 + * @private + * @param {IGlobalConfig} config + * @param {UICoordinateController} uiCoordinate + * @returns {*} {void} + * @memberof RenderLayerController + */ + private buildTaskViewModels(config: IGlobalConfig, uiCoordinate: UICoordinateController): void { + if (!uiCoordinate) { + return; + } + const taskViewModels: ITaskViewModel[] = []; + if (this.tasks.length > 0) { + // 计算时间范围内的天数 + const startDate = new Date(config.startTime); + startDate.setHours(0, 0, 0, 0); + const endDate = new Date(config.endTime); + endDate.setHours(0, 0, 0, 0); + for (let i = 0; i < this.tasks.length; i++) { + const task = this.tasks[i]; + const resourceIndex = this.resources.findIndex(resource => resource.id === task.resourceId); + if (resourceIndex === -1) { + console.warn(`Task ${task.id} references non-existent resource ${task.resourceId}`); + continue; + } + // 计算任务日期属性 + const taskStartDate = new Date(task.start); + taskStartDate.setHours(0, 0, 0, 0); + const taskEndDate = new Date(task.end); + taskEndDate.setHours(0, 0, 0, 0); + + // 计算任务在日期轴上的位置 + let daysFromStart = Math.ceil((taskStartDate.getTime() - startDate.getTime()) / Variables.time.millisecondOf.day); + const left = config.resourceColumnWidth + config.scaleColumnWidth + (daysFromStart * uiCoordinate.cellWidth); + + // 如果任务跨多天,计算宽度 + const taskDurationDays = Math.ceil((taskEndDate.getTime() - taskStartDate.getTime()) / Variables.time.millisecondOf.day) + 1; + const width = taskDurationDays * uiCoordinate.cellWidth; + + // 计算任务在时间轴上的位置(垂直方向) + const taskStartHour = task.start.getHours() + task.start.getMinutes() / 60; + const top = resourceIndex * config.resourceBodyRowHeight + (taskStartHour - config.scaleRange[0]) * (uiCoordinate.cellHeight / config.scaleValue); + + // 计算任务高度 + const taskDurationHours = (task.end.getTime() - task.start.getTime()) / Variables.time.millisecondOf.hour; + const height = taskDurationHours * (uiCoordinate.cellHeight / config.scaleValue); + + taskViewModels.push({ + id: task.id, + resourceId: task.resourceId, + width, + height, + top, + left, + data: task, + }) + } + } + this.taskViewModels = taskViewModels; + } + + /** + * @description 构建视图模型 + * @param {UICoordinateController} uiCoordinate + * @memberof RenderLayerController + */ + buildViewModels(uiCoordinate: UICoordinateController): void { + const config = this.configController.getConfig(); + this.buildResourceViewModels(config); + this.buildTaskViewModels(config, uiCoordinate); + } } \ No newline at end of file diff --git a/src/resource-scheduler/hooks/use-schedule-table.ts b/src/resource-scheduler/hooks/use-schedule-table.ts index ffb7786..3d0b520 100644 --- a/src/resource-scheduler/hooks/use-schedule-table.ts +++ b/src/resource-scheduler/hooks/use-schedule-table.ts @@ -1,167 +1,272 @@ -import { computed, onMounted, onUnmounted, ref, Ref } from "vue"; -import useStore from "../store"; -import { debounce } from "../utils"; - -// 组件样式 -export function useScheduleTableStyle(props: any) { - // 头部样式 - const headerStyle = computed(() => { - return { - width: '100%', - height: `${props.headerRowHeight * 2}px`, - }; - }); - - // 内容样式 - const bodyStyle = computed(() => { - return { - width: '100%', - height: `calc(100% - ${props.headerRowHeight * 2}px)`, - }; - }); - - return { headerStyle, bodyStyle }; -} - -// 初始化调度表格 -export function useInitScheduleTable(headerCanvas: Ref, bodyCanvas: Ref, coordinateElement: Ref) { - - const { $config, $bgLayer, $renderLayer, $uiCoordinate } = useStore(); - - // 资源视图模型 - const resourceViewModels = computed(() => $renderLayer.resourceViewModels); - - // 任务视图模型 - const taskViewModels = computed(() => $renderLayer.taskViewModels); - - // 挂载 - onMounted(() => { - if (headerCanvas.value && bodyCanvas.value) { - $bgLayer.init(headerCanvas.value, bodyCanvas.value); - } - if (coordinateElement.value) { - $uiCoordinate.buildCoordinate(coordinateElement.value, $renderLayer.getResources()); - $bgLayer.draw($uiCoordinate, $renderLayer.getResources(), $renderLayer.getTasks()); - $renderLayer.buildViewModels($uiCoordinate); - } - }); - return { resourceViewModels, taskViewModels }; -} - -// 处理视图大小变更 -export function useScheduleTableReSize(props: any, headerCanvas: Ref, bodyCanvas: Ref, coordinateElement: Ref) { - const { $config, $bgLayer, $renderLayer, $uiCoordinate } = useStore(); - // 重绘函数 - const redraw = (props: any) => { - if (coordinateElement.value && headerCanvas.value && bodyCanvas.value) { - // 已经完成布局不需要重绘 - if (coordinateElement.value.clientWidth === $uiCoordinate.canvasWidth) return; - // 更新数据 - $config.setConfig(props); - $renderLayer.setResources(props.resources); - $renderLayer.setTasks(props.tasks); - // 更新绘制 - $uiCoordinate.buildCoordinate(coordinateElement.value, $renderLayer.getResources()); - $bgLayer.draw($uiCoordinate, $renderLayer.getResources(), $renderLayer.getTasks()); - $renderLayer.buildViewModels($uiCoordinate); - } - }; - - // 防抖重绘函数 - const debouncedRedraw = debounce(redraw, 100); - - // 窗口resize防抖 - const handleResize = () => { - debouncedRedraw(props); - }; - - // 监听 coordinateElement 的尺寸变化 - let resizeObserver: ResizeObserver | null = null; - - // 挂载后绑定事件监听 - onMounted(() => { - if (window.ResizeObserver && coordinateElement.value) { - resizeObserver = new ResizeObserver(handleResize); - resizeObserver.observe(coordinateElement.value); - } - }); - - // 移除事件监听器 - onUnmounted(() => { - if (resizeObserver && coordinateElement.value) { - resizeObserver.unobserve(coordinateElement.value); - resizeObserver = null; - } - }); -} - -// 虚拟滚动相关hook -export function useVirtualScroll(coordinateElement: Ref) { - // 可视区域状态 - const visibleRange = ref({ - start: 0, - end: 0, - offsetY: 0 - }); - - const { $config, $renderLayer } = useStore(); - - // 初始化时计算可见范围 - const initializeVisibleRange = () => { - if (!coordinateElement.value) return; - - const config = $config.getConfig(); - const resources = $renderLayer.getResources(); - - const clientHeight = coordinateElement.value.clientHeight; - const itemHeight = config.resourceBodyRowHeight; - - // 计算初始可见范围 - const visibleCount = Math.ceil(clientHeight / itemHeight); - const end = Math.min(resources.length, visibleCount + 2); // 添加缓冲区 - - visibleRange.value = { - start: 0, - end, - offsetY: 0 - }; - }; - - onMounted(() => { - // 使用 setTimeout 确保 DOM 已经渲染完成 - setTimeout(() => { - initializeVisibleRange(); - }, 0); - }); - - // 滚动事件处理 - const handleScroll = () => { - if (!coordinateElement.value) return; - - const config = $config.getConfig(); - const resources = $renderLayer.getResources(); - - const scrollTop = coordinateElement.value.scrollTop; - const clientHeight = coordinateElement.value.clientHeight; - const itemHeight = config.resourceBodyRowHeight; - - // 计算可见范围 - const start = Math.max(0, Math.floor(scrollTop / itemHeight)); - const visibleCount = Math.ceil(clientHeight / itemHeight); - const end = Math.min( - resources.length, - start + visibleCount + 2 // 添加缓冲区 - ); - - visibleRange.value = { - start, - end, - offsetY: scrollTop - }; - }; - - return { - visibleRange, - handleScroll - }; -} \ No newline at end of file +import { computed, onMounted, onUnmounted, ref, Ref } from 'vue'; +import dayjs from 'dayjs'; +import { createUUID } from 'qx-util'; +import useStore from '../store'; +import { debounce } from '../utils'; +import { IDragItem } from '../interface'; + +// 组件样式 +export function useScheduleTableStyle(props: any) { + // 头部样式 + const headerStyle = computed(() => { + return { + width: '100%', + height: `${props.headerRowHeight * 2}px`, + }; + }); + + // 内容样式 + const bodyStyle = computed(() => { + return { + width: '100%', + height: `calc(100% - ${props.headerRowHeight * 2}px)`, + }; + }); + + return { headerStyle, bodyStyle }; +} + +// 初始化调度表格 +export function useInitScheduleTable( + headerCanvas: Ref, + bodyCanvas: Ref, + coordinateElement: Ref, +) { + const { $config, $bgLayer, $renderLayer, $uiCoordinate } = useStore(); + + // 资源视图模型 + const resourceViewModels = computed(() => $renderLayer.resourceViewModels); + + // 任务视图模型 + const taskViewModels = computed(() => $renderLayer.taskViewModels); + + // 挂载 + onMounted(() => { + if (headerCanvas.value && bodyCanvas.value) { + $bgLayer.init(headerCanvas.value, bodyCanvas.value); + } + if (coordinateElement.value) { + $uiCoordinate.buildCoordinate( + coordinateElement.value, + $renderLayer.getResources(), + ); + $bgLayer.draw( + $uiCoordinate, + $renderLayer.getResources(), + ); + $renderLayer.buildViewModels($uiCoordinate); + } + }); + return { resourceViewModels, taskViewModels }; +} + +// 处理视图大小变更 +export function useScheduleTableReSize( + props: any, + headerCanvas: Ref, + bodyCanvas: Ref, + coordinateElement: Ref, +) { + const { $config, $bgLayer, $renderLayer, $uiCoordinate } = useStore(); + // 重绘函数 + const redraw = (props: any) => { + if (coordinateElement.value && headerCanvas.value && bodyCanvas.value) { + // 已经完成布局不需要重绘 + if (coordinateElement.value.clientWidth === $uiCoordinate.canvasWidth) + return; + // 更新数据 + $config.setConfig(props); + $renderLayer.setResources(props.resources); + $renderLayer.setTasks(props.tasks); + // 更新绘制 + $uiCoordinate.buildCoordinate( + coordinateElement.value, + $renderLayer.getResources(), + ); + $bgLayer.draw( + $uiCoordinate, + $renderLayer.getResources(), + ); + $renderLayer.buildViewModels($uiCoordinate); + } + }; + + // 防抖重绘函数 + const debouncedRedraw = debounce(redraw, 100); + + // 窗口resize防抖 + const handleResize = () => { + debouncedRedraw(props); + }; + + // 监听 coordinateElement 的尺寸变化 + let resizeObserver: ResizeObserver | null = null; + + // 挂载后绑定事件监听 + onMounted(() => { + if (window.ResizeObserver && coordinateElement.value) { + resizeObserver = new ResizeObserver(handleResize); + resizeObserver.observe(coordinateElement.value); + } + }); + + // 移除事件监听器 + onUnmounted(() => { + if (resizeObserver && coordinateElement.value) { + resizeObserver.unobserve(coordinateElement.value); + resizeObserver = null; + } + }); +} + +// 虚拟滚动相关hook +export function useVirtualScroll( + coordinateElement: Ref, +) { + // 可视区域状态 + const visibleRange = ref({ + start: 0, + end: 0, + offsetY: 0, + }); + + const { $config, $renderLayer } = useStore(); + + // 初始化时计算可见范围 + const initializeVisibleRange = () => { + if (!coordinateElement.value) return; + + const config = $config.getConfig(); + const resources = $renderLayer.getResources(); + + const clientHeight = coordinateElement.value.clientHeight; + const itemHeight = config.resourceBodyRowHeight; + + // 计算初始可见范围 + const visibleCount = Math.ceil(clientHeight / itemHeight); + const end = Math.min(resources.length, visibleCount + 2); // 添加缓冲区 + + visibleRange.value = { + start: 0, + end, + offsetY: 0, + }; + }; + + onMounted(() => { + // 使用 setTimeout 确保 DOM 已经渲染完成 + setTimeout(() => { + initializeVisibleRange(); + }, 0); + }); + + // 滚动事件处理 + const handleScroll = () => { + if (!coordinateElement.value) return; + + const config = $config.getConfig(); + const resources = $renderLayer.getResources(); + + const scrollTop = coordinateElement.value.scrollTop; + const clientHeight = coordinateElement.value.clientHeight; + const itemHeight = config.resourceBodyRowHeight; + + // 计算可见范围 + const start = Math.max(0, Math.floor(scrollTop / itemHeight)); + const visibleCount = Math.ceil(clientHeight / itemHeight); + const end = Math.min( + resources.length, + start + visibleCount + 2, // 添加缓冲区 + ); + + visibleRange.value = { + start, + end, + offsetY: scrollTop, + }; + }; + + return { + visibleRange, + handleScroll, + }; +} + +// 资源拖拽 +export function useScheduleDrop(props: any, coordinateElement: Ref, bodyCanvas: Ref,): { + handleDrop: (payload: DragEvent) => void; + handleDragOver: (payload: DragEvent) => void; +} { + const { $bgLayer, $renderLayer, $uiCoordinate, $config } = useStore(); + + /** + * @description 处理添加时的拖拽经过 + * @param {DragEvent} payload + */ + const handleDragOver = (payload: DragEvent) => { + payload.preventDefault(); + payload.dataTransfer!.dropEffect = 'copy'; + }; + + /** + * @description 处理添加时的拖拽放置 + * @param {DragEvent} payload + * @returns {*} + */ + const handleDrop = async (payload: DragEvent) => { + payload.preventDefault(); + if (!coordinateElement.value || !bodyCanvas.value) return; + try { + const str = payload.dataTransfer?.getData('text/plain'); + if (!str) return; + const dragItem: IDragItem = JSON.parse(str); + const { dragType, data, scheduleType } = dragItem; + if (!dragType || !['resource', 'task'].includes(scheduleType)) return; + + // 拖拽校验 + if (props.verifyDrag && props.verifyDrag instanceof Function) { + const result = await props.verifyDrag(dragItem); + if (!result.ok) return; + } + + const config = $config.getConfig(); + const rect = bodyCanvas.value.getBoundingClientRect(); + const x = payload.clientX - rect.left - config.resourceColumnWidth - config.scaleColumnWidth; + const y = payload.clientY - rect.top; + const id = dragType === 'add' ? createUUID() : data.id; + + // 资源索引位置 + const resourceIndex = Math.ceil(y / config.resourceBodyRowHeight) - 1; + if (scheduleType === 'resource') { + $renderLayer.addResource({ id: id, name: data.name, tasks: [], data }, resourceIndex); + } else { + // 任务未拖拽到任务区时不处理 + if (x < 0) return; + const resourceId = $renderLayer.resourceViewModels[resourceIndex].id; + // 计算日期 + const dateIndex = Math.ceil(x / $uiCoordinate.cellWidth); + const date = dayjs(config.startTime).add(dateIndex - 1, 'day').startOf('day'); + const timeIndex = Math.ceil((y - (resourceIndex * config.resourceBodyRowHeight)) / $uiCoordinate.cellHeight) - 1; + const startHour = config.scaleRange[0] + config.scaleValue * timeIndex; + const start = new Date(date.hour(startHour).format('YYYY-MM-DD HH:mm:ss')); + const end = new Date(date.hour(startHour + config.scaleValue).format('YYYY-MM-DD HH:mm:ss')); + $renderLayer.addTask({ id: id, name: data.name, data, start, end, resourceId }); + } + + // 更新绘制 + $uiCoordinate.buildCoordinate( + coordinateElement.value, + $renderLayer.getResources(), + ); + $bgLayer.draw( + $uiCoordinate, + $renderLayer.getResources(), + ); + $renderLayer.buildViewModels($uiCoordinate); + } catch (error) { + ibiz.log.error(error); + } + }; + + return { handleDrop, handleDragOver }; +} diff --git a/src/resource-scheduler/index.ts b/src/resource-scheduler/index.ts index 6e4bb3f..6e9d54d 100644 --- a/src/resource-scheduler/index.ts +++ b/src/resource-scheduler/index.ts @@ -1,13 +1,13 @@ -import { App } from "vue"; -import { ScheduleTable } from "./components"; -import type { IGlobalConfig, IScheduleResource, IScheduleTask } from "./interface"; - -export type { IGlobalConfig, IScheduleResource, IScheduleTask }; -export { ScheduleTable }; - -export default { - install(a: App): void { - // 自定义插件注入 - a.component(ScheduleTable.name!, ScheduleTable); - }, +import { App } from "vue"; +import { ScheduleTable } from "./components"; +import type { IGlobalConfig, IScheduleResource, IScheduleTask } from "./interface"; + +export type { IGlobalConfig, IScheduleResource, IScheduleTask }; +export { ScheduleTable }; + +export default { + install(a: App): void { + // 自定义插件注入 + a.component(ScheduleTable.name!, ScheduleTable); + }, }; \ No newline at end of file diff --git a/src/resource-scheduler/interface/i-drag-item.ts b/src/resource-scheduler/interface/i-drag-item.ts new file mode 100644 index 0000000..f81674f --- /dev/null +++ b/src/resource-scheduler/interface/i-drag-item.ts @@ -0,0 +1,25 @@ +/** + * @description 拖拽项 + * @export + * @interface IDragItem + */ +export interface IDragItem { + /** + * @description 拖拽类型 + * @type {('add' | 'update')}(新增|更新) + * @memberof IDragData + */ + dragType: 'add' | 'update'; + /** + * @description 排程类型 + * @type {('resource' | 'task')}(资源|任务) + * @memberof IDragData + */ + scheduleType: 'resource' | 'task'; + /** + * @description 拖拽数据 + * @type {IData} + * @memberof IDragData + */ + data: IData; +} \ No newline at end of file diff --git a/src/resource-scheduler/interface/index.ts b/src/resource-scheduler/interface/index.ts index a9cecfb..123b460 100644 --- a/src/resource-scheduler/interface/index.ts +++ b/src/resource-scheduler/interface/index.ts @@ -1,2 +1,3 @@ -export type { IGlobalConfig } from './i-global-config'; +export type { IGlobalConfig } from './i-global-config'; +export type { IDragItem } from './i-drag-item'; export type { IScheduleResource, IScheduleTask, IResourceViewModel, ITaskViewModel } from './i-ui-data'; \ No newline at end of file diff --git a/src/resource-scheduler/store/index.ts b/src/resource-scheduler/store/index.ts index 175cb2c..e50f786 100644 --- a/src/resource-scheduler/store/index.ts +++ b/src/resource-scheduler/store/index.ts @@ -1,40 +1,40 @@ -import { reactive } from "vue"; -import { BgLayerController, GlobalConfigController, RenderLayerController } from "../controller"; -import { UICoordinateController } from "../controller/ui-coordinate.controller"; - -/** - * @description 全局数据 - */ -const Store: Record = {}; - -/** - * @description 初始化状态数据 - */ -export const initStore = (props: any, emit: any) => { - const config = new GlobalConfigController(props); - Store.$config = reactive(config); - const bgLayer = new BgLayerController(config); - Store.$bgLayer = reactive(bgLayer); - const renderLayer = new RenderLayerController(config, props.resources, props.tasks); - Store.$renderLayer = reactive(renderLayer); - const uiCoordinate = new UICoordinateController(config); - Store.$uiCoordinate = reactive(uiCoordinate); -}; - -/** - * @description 状态hook - */ -export const useStore = () => { - return { - // 配置 - $config: Store.$config, - // 背景层 - $bgLayer: Store.$bgLayer, - // 渲染层 - $renderLayer: Store.$renderLayer, - // 坐标层 - $uiCoordinate: Store.$uiCoordinate - }; -}; - -export default useStore; +import { reactive } from "vue"; +import { BgLayerController, GlobalConfigController, RenderLayerController } from "../controller"; +import { UICoordinateController } from "../controller/ui-coordinate.controller"; + +/** + * @description 全局数据 + */ +const Store: Record = {}; + +/** + * @description 初始化状态数据 + */ +export const initStore = (props: any, emit: any) => { + const config = new GlobalConfigController(props); + Store.$config = reactive(config); + const bgLayer = new BgLayerController(config); + Store.$bgLayer = reactive(bgLayer); + const renderLayer = new RenderLayerController(config, props.resources, props.tasks); + Store.$renderLayer = reactive(renderLayer); + const uiCoordinate = new UICoordinateController(config); + Store.$uiCoordinate = reactive(uiCoordinate); +}; + +/** + * @description 状态hook + */ +export const useStore = () => { + return { + // 配置 + $config: Store.$config as GlobalConfigController, + // 背景层 + $bgLayer: Store.$bgLayer as BgLayerController, + // 渲染层 + $renderLayer: Store.$renderLayer as RenderLayerController, + // 坐标层 + $uiCoordinate: Store.$uiCoordinate as UICoordinateController + }; +}; + +export default useStore; diff --git a/src/user-register.ts b/src/user-register.ts index cdafd1c..5b37f07 100644 --- a/src/user-register.ts +++ b/src/user-register.ts @@ -1,11 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable import/no-extraneous-dependencies */ import { App } from 'vue'; -import ResourceScheduleTable from './plugins/resource-schedule-table' +import ResourceScheduleTable from './plugins/resource-schedule-table'; +import IBizPanelBottomTabPanel from './panel-bottom-tab-panel/src'; +import IBizResourceScheduleTree from './resource-schedule-tree/src'; export default { install(V:App): void { // 自定义插件注入 V.use(ResourceScheduleTable); + V.use(IBizPanelBottomTabPanel); + V.use(IBizResourceScheduleTree); }, }; -- Gitee