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 0000000000000000000000000000000000000000..e5d1d31706be1d46d96c6a0bdf42679cdcb17b33 --- /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 0000000000000000000000000000000000000000..c354b314cf273fdeae18ec62b4c9150fc592541c --- /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 0000000000000000000000000000000000000000..cc407952658662929c068547dc6739e8fed6e09f --- /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 0000000000000000000000000000000000000000..866a8c700891c27828c8492952ed3ddefcbceae7 --- /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 0000000000000000000000000000000000000000..7c71ecf6604c3a566b71cca38fe0290c25fafd54 --- /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 0000000000000000000000000000000000000000..6fd01f4df2827f3c25a2e759f94f748a8ba9e2aa --- /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/resource-schedule-tree/src/el-tree-util.ts b/src/resource-schedule-tree/src/el-tree-util.ts new file mode 100644 index 0000000000000000000000000000000000000000..baf9176f65bd7a2e0b12438b207ad51ff7833c57 --- /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 0000000000000000000000000000000000000000..1bc8b4b0ae2f46088fb674695f2d4071ae492399 --- /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 0000000000000000000000000000000000000000..d104eb76b354cb278e6e4a953084a4d3069a4442 --- /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 0000000000000000000000000000000000000000..8d7a53fec6be273cb46f102ee09ee41cf8c5534f --- /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 0000000000000000000000000000000000000000..3310bcee4544da3771e90b445adccab294fd8d13 --- /dev/null +++ b/src/resource-schedule-tree/src/resource-schedule-tree.scss @@ -0,0 +1,77 @@ +@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(drag-ghost) { + position: fixed; + z-index: 10000; + display: flex; + flex-direction: column; + max-width: 250px; + padding: 12px 16px; + font-size: 14px; + font-weight: 600; + color: white; + white-space: nowrap; + pointer-events: none; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: 2px solid white; + border-radius: 8px; + box-shadow: 0 8px 25px rgb(0 0 0 / 30%); + opacity: 0.9; + transition: + transform 0.05s linear, + background 0.2s ease, + opacity 0.2s ease; + transform: translate3d(0, 0, 0); + will-change: transform; + backface-visibility: hidden; + } +} 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 0000000000000000000000000000000000000000..d2d876c902a02c6fd5a82598e0e25f4dd0336c4b --- /dev/null +++ b/src/resource-schedule-tree/src/resource-schedule-tree.tsx @@ -0,0 +1,1296 @@ +/* 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); + }); + }; + + /** + * 排程类型 + */ + const scheduleType = c.controlParams.scheduletype; + + /** + * ghost状态 + */ + const ghostState = ref({ + show: false, + text: '', + style: { + left: '-1000px', + top: '-1000px', + }, + }); + + /** + * @description 更新ghost + * @param {DragEvent} payload + */ + const updateGhost = (payload: DragEvent) => { + ghostState.value.style = { + left: payload.clientX + 20 + 'px', + top: payload.clientY + 20 + 'px', + }; + ghostState.value.show = true; + }; + + /** + * @description 处理开始拖拽 + * @param {MouseEvent} payload + * @param {ITreeNodeData} node + */ + const handleDragStart = (payload: DragEvent, node: ITreeNodeData) => { + payload.dataTransfer!.effectAllowed = 'copy'; + const item = { + scheduleType, + dragType: 'add', + data: node._deData, + }; + payload.dataTransfer?.setData('data', JSON.stringify(item)); + ghostState.value.text = node._text; + + // 更新自定义ghost + updateGhost(payload); + + // 将默认 ghost 覆盖 + const defaultGhost = document.createElement('div'); + defaultGhost.id = 'default-ghost'; + document.body.appendChild(defaultGhost); + payload.dataTransfer?.setDragImage(defaultGhost, 0, 0); + setTimeout(() => { + if (document.body.contains(defaultGhost)) + document.body.removeChild(defaultGhost); + }, 0); + }; + + /** + * @description 拖拽结束 + */ + const handleDragEnd = () => { + ghostState.value.show = false; + }; + + /** + * @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, + ghostState, + newNodeKey, + treeviewRef, + newNodeData, + cascadeSelect, + treeRefreshKey, + editingNodeKey, + editingNodeText, + treeNodeTextInputRef, + onCheck, + onInput, + loadData, + allowDrop, + allowDrag, + handleDrop, + updateGhost, + onNodeClick, + findNodeData, + onNodeDbClick, + renderHearder, + renderCounter, + handleDragEnd, + 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} +
+ {this.ghostState.show && ( +
+ + + {this.ghostState.text} + +
+ )} +
+ ); + }, +}); diff --git a/src/resource-scheduler/components/schedule-table/schedule-table.tsx b/src/resource-scheduler/components/schedule-table/schedule-table.tsx index 5eb2cb3858c4783f045c265554d784cb650b1988..240c9bc788d0763929c0a4e9342dba4141421562 100644 --- a/src/resource-scheduler/components/schedule-table/schedule-table.tsx +++ b/src/resource-scheduler/components/schedule-table/schedule-table.tsx @@ -1,20 +1,21 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { computed, defineComponent, ref } from 'vue'; -import { IResourceViewModel, ITaskViewModel } from '../../interface'; -import { initStore } from '../../store'; +import { computed, defineComponent, ref } from "vue"; +import { IResourceViewModel, ITaskViewModel } from "../../interface"; +import { initStore } from "../../store"; import { + useDrag, + useVirtualScroll, useInitScheduleTable, useScheduleTableReSize, useScheduleTableStyle, - useVirtualScroll, -} from '../../hooks/use-schedule-table'; -import { ScheduleTableEvent, ScheduleTableProps } from './schedule-table-type'; -import './schedule-table.scss'; +} from "../../hooks"; +import { ScheduleTableEvent, ScheduleTableProps } from "./schedule-table-type"; +import "./schedule-table.scss"; export const ScheduleTable = defineComponent({ - name: 'ScheduleTable', + name: "ScheduleTable", props: ScheduleTableProps, emits: ScheduleTableEvent, setup(props, { emit }) { @@ -34,7 +35,7 @@ export const ScheduleTable = defineComponent({ const { resourceViewModels, taskViewModels } = useInitScheduleTable( headerCanvas, bodyCanvas, - coordinateElement, + coordinateElement ); // 计算表格样式 @@ -46,12 +47,16 @@ export const ScheduleTable = defineComponent({ // 虚拟滚动 const { visibleRange, handleScroll } = useVirtualScroll(coordinateElement); + // 拖拽 + const { handleDrop, handleDragOver, handleDragStart, handleDragLeave } = + useDrag(props, coordinateElement, bodyCanvas); + // 过滤可见的资源 const visibleResourceViewModels = computed(() => { if (!resourceViewModels.value) return []; return resourceViewModels.value.slice( visibleRange.value.start, - visibleRange.value.end, + visibleRange.value.end ); }); @@ -60,7 +65,7 @@ export const ScheduleTable = defineComponent({ if (!taskViewModels.value) return []; return taskViewModels.value.filter((task: ITaskViewModel) => { const resourceIndex = props.resources.findIndex( - res => res.id === task.resourceId, + (res) => res.id === task.resourceId ); return ( resourceIndex >= visibleRange.value.start && @@ -70,44 +75,51 @@ export const ScheduleTable = defineComponent({ }); return { - headerCanvas, + bodyStyle, bodyCanvas, - coordinateElement, headerStyle, - bodyStyle, - visibleResourceViewModels, + headerCanvas, + visibleRange, + coordinateElement, visibleTaskViewModels, + visibleResourceViewModels, + handleDrop, handleScroll, - visibleRange, + handleDragOver, + handleDragStart, + handleDragLeave, }; }, render() { return ( -
-
+
+
-
+
{this.visibleResourceViewModels.length > 0 ? ( <> {this.visibleResourceViewModels.map( (resourceViewModel: IResourceViewModel) => { return (
); - }, + } )} ) : null} @@ -129,7 +141,7 @@ export const ScheduleTable = defineComponent({ (taskViewModel: ITaskViewModel) => { return (
); - }, + } )} ) : null} diff --git a/src/resource-scheduler/controller/render-layer.controller.ts b/src/resource-scheduler/controller/render-layer.controller.ts index 544b24df39bd9a69ff9c5ac383ce515e452a9a4e..35fbe9ac134c70a7f93847ced6527bccbace8882 100644 --- a/src/resource-scheduler/controller/render-layer.controller.ts +++ b/src/resource-scheduler/controller/render-layer.controller.ts @@ -10,10 +10,10 @@ import { IScheduleResource, IScheduleTask, ITaskViewModel, -} from '../interface'; -import Variables from '../constant/vars'; -import { UICoordinateController } from './ui-coordinate.controller'; -import { GlobalConfigController } from './global-config.controller'; +} from "../interface"; +import Variables from "../constant/vars"; +import { UICoordinateController } from "./ui-coordinate.controller"; +import { GlobalConfigController } from "./global-config.controller"; /** * @description 绘制图层控制器 @@ -77,7 +77,7 @@ export class RenderLayerController { configController: GlobalConfigController, evt: any, resources: IScheduleResource[], - tasks: IScheduleTask[], + tasks: IScheduleTask[] ) { this.configController = configController; this.evt = evt; @@ -104,6 +104,19 @@ export class RenderLayerController { this.resources = resources; } + /** + * @description 更新资源 + * - 如果已经存在则更新,不存在则新增 + * @param {IScheduleResource} resource + * @memberof RenderLayerController + */ + updateResource(resource: IScheduleResource): void { + const index = this.resources.findIndex((res) => res.id === resource.id); + index !== -1 + ? this.resources.splice(index, 1, resource) + : this.resources.push(resource); + } + /** * @description 获取任务 * @returns {*} {IScheduleTask[]} @@ -123,35 +136,48 @@ export class RenderLayerController { } /** - * @description 过滤有效任务 - * @param {IScheduleTask[]} tasks 任务列表 - * @param {IGlobalConfig} config 全局配置 - * @returns {IScheduleTask[]} 有效任务列表 + * @description 更新任务 + * - 如果已经存在则更新,不存在则新增 + * @param {IScheduleTask} task * @memberof RenderLayerController */ - filterValidTasks(tasks: IScheduleTask[]): IScheduleTask[] { + updateTask(task: IScheduleTask): void { + if (this.verifyTask(task)) { + const index = this.tasks.findIndex((_task) => _task.id === task.id); + index !== -1 ? this.tasks.splice(index, 1, task) : this.tasks.push(task); + } + } + + /** + * @description 校验任务 + * @param {IScheduleTask} task + * @returns {*} {boolean} + * @memberof RenderLayerController + */ + verifyTask(task: IScheduleTask): boolean { const config = this.configController.getConfig(); // 全局起止时间 const startDate = new Date(config.startTime); startDate.setHours(0, 0, 0, 0); const endDate = new Date(config.endTime); endDate.setHours(23, 59, 59, 59); + // 检查任务的开始时间和结束时间是否有效 + if (task.start >= task.end) return false; + // 如果任务的结束时间小于全局开始时间,则任务无效 + if (task.end < startDate) return false; + // 如果任务的开始时间大于全局结束时间,则任务无效 + if (task.start > endDate) return false; + return true; + } - return tasks.filter(task => { - // 检查任务的开始时间和结束时间是否有效 - if (task.start >= task.end) { - return false; - } - // 如果任务的结束时间小于全局开始时间,则任务无效 - if (task.end < startDate) { - return false; - } - // 如果任务的开始时间大于全局结束时间,则任务无效 - if (task.start > endDate) { - return false; - } - return true; - }); + /** + * @description 过滤有效任务 + * @param {IScheduleTask[]} tasks + * @returns {*} {IScheduleTask[]} + * @memberof RenderLayerController + */ + filterValidTasks(tasks: IScheduleTask[]): IScheduleTask[] { + return tasks.filter(task => this.verifyTask(task)); } /** @@ -186,18 +212,18 @@ export class RenderLayerController { * @memberof RenderLayerController */ private groupConflictingTasks( - taskViewModels: ITaskViewModel[], + taskViewModels: ITaskViewModel[] ): ITaskViewModel[][] { const groups: ITaskViewModel[][] = []; const visited = new Set(); - taskViewModels.forEach(task => { + taskViewModels.forEach((task) => { if (visited.has(task.id)) return; const group: ITaskViewModel[] = [task]; visited.add(task.id); - taskViewModels.forEach(otherTask => { + taskViewModels.forEach((otherTask) => { if (visited.has(otherTask.id) || task.id === otherTask.id) return; // 检查是否为同一资源 @@ -226,7 +252,7 @@ export class RenderLayerController { */ private isTimeOverlapping( task1: ITaskViewModel, - task2: ITaskViewModel, + task2: ITaskViewModel ): boolean { // 如果是同一原始任务的不同部分,则不认为是冲突 if (task1.originalId === task2.originalId) { @@ -248,7 +274,7 @@ export class RenderLayerController { * @memberof RenderLayerController */ private processConflictingTasks(conflictGroups: ITaskViewModel[][]): void { - conflictGroups.forEach(group => { + conflictGroups.forEach((group) => { if (group.length <= 1) return; // 按照top位置排序 @@ -275,7 +301,7 @@ export class RenderLayerController { */ private splitTaskByDays( task: IScheduleTask, - config: IGlobalConfig, + config: IGlobalConfig ): IScheduleTask[] { const splitTasks: IScheduleTask[] = []; @@ -331,7 +357,7 @@ export class RenderLayerController { */ private buildTaskViewModels( config: IGlobalConfig, - uiCoordinate: UICoordinateController, + uiCoordinate: UICoordinateController ): void { if (!uiCoordinate) { return; @@ -346,11 +372,11 @@ export class RenderLayerController { for (let i = 0; i < this.tasks.length; i++) { const task = this.tasks[i]; const resourceIndex = this.resources.findIndex( - resource => resource.id === task.resourceId, + (resource) => resource.id === task.resourceId ); if (resourceIndex === -1) { console.warn( - `Task ${task.id} references non-existent resource ${task.resourceId}`, + `Task ${task.id} references non-existent resource ${task.resourceId}` ); continue; } @@ -358,7 +384,7 @@ export class RenderLayerController { // 处理跨天任务,将其分割成多个部分 const splitTasks = this.splitTaskByDays(task, config); - splitTasks.forEach(splitTask => { + splitTasks.forEach((splitTask) => { // 计算任务日期属性 const taskStartDate = new Date(splitTask.start); taskStartDate.setHours(0, 0, 0, 0); @@ -368,7 +394,7 @@ export class RenderLayerController { // 计算任务在日期轴上的位置 const daysFromStart = Math.ceil( (taskStartDate.getTime() - startDate.getTime()) / - Variables.time.millisecondOf.day, + Variables.time.millisecondOf.day ); const left = config.resourceColumnWidth + @@ -379,7 +405,7 @@ export class RenderLayerController { const taskDurationDays = Math.ceil( (taskEndDate.getTime() - taskStartDate.getTime()) / - Variables.time.millisecondOf.day, + Variables.time.millisecondOf.day ) + 1; const width = taskDurationDays * uiCoordinate.cellWidth; diff --git a/src/resource-scheduler/hooks/index.ts b/src/resource-scheduler/hooks/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7a7c352a0850ab603b5263facc845529ed4228c --- /dev/null +++ b/src/resource-scheduler/hooks/index.ts @@ -0,0 +1,2 @@ +export { useDrag } from './use-drag'; +export { useInitScheduleTable, useScheduleTableReSize, useScheduleTableStyle, useVirtualScroll } from './use-schedule-table'; \ No newline at end of file diff --git a/src/resource-scheduler/hooks/use-drag.ts b/src/resource-scheduler/hooks/use-drag.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9623e5b3718ff7705a612338a5d46687ff2163b --- /dev/null +++ b/src/resource-scheduler/hooks/use-drag.ts @@ -0,0 +1,173 @@ +import { Ref } from 'vue'; +import dayjs from 'dayjs'; +import { createUUID } from 'qx-util'; +import { IDragData, IScheduleTask } from '../interface'; +import useStore from '../store'; + +export function useDrag( + props: any, + coordinateElement: Ref, + bodyCanvas: Ref, +): { + handleDragOver: (payload: DragEvent) => void; + handleDragLeave: (payload: DragEvent) => void; + handleDrop: (payload: DragEvent) => Promise; + handleDragStart: (payload: DragEvent, task: IScheduleTask) => void; +} { + const { $bgLayer, $renderLayer, $uiCoordinate, $config } = useStore(); + + const config = $config.getConfig(); + + /** + * @description 根据拖拽事件源计算位置 + * @param {DragEvent} payload + * @returns {*} {{ + * date: Date; + * resourceId: string; + * position: { x: number; y: number }; + * }} + */ + const calcPositionByEvent = ( + payload: DragEvent, + ): { + date: dayjs.Dayjs; + resourceId: string; + position: { x: number; y: number }; + } => { + const rect = bodyCanvas.value!.getBoundingClientRect(); + const { + startTime, + scaleRange, + dragInterval, + scaleColumnWidth, + resourceColumnWidth, + resourceBodyRowHeight, + } = config; + const x = + payload.clientX - rect.left - resourceColumnWidth - scaleColumnWidth; + const y = payload.clientY - rect.top; + const resourceIndex = Math.ceil(y / resourceBodyRowHeight) - 1; + // 资源标识 + const resourceId = $renderLayer.resourceViewModels[resourceIndex].id; + // 天索引 + const dayIndex = Math.ceil(x / $uiCoordinate.cellWidth) - 1; + // 刻度开始时间 + const startDate = dayjs(startTime) + .add(dayIndex, 'day') + .startOf('day') + .hour(scaleRange[0]); + const range = scaleRange[1] - scaleRange[0]; + // 拖拽精度的高度(使用分钟来计算) + const intervalHeight = + resourceBodyRowHeight / ((range * 60) / (dragInterval * 60)); + const height = y - resourceIndex * config.resourceBodyRowHeight; + const date = startDate.add(Math.round(height / intervalHeight), 'hour'); + return { position: { x, y }, date, resourceId }; + }; + + /** + * @description 处理拖拽经过 + * @param {DragEvent} payload + */ + const handleDragOver = (payload: DragEvent): void => { + payload.preventDefault(); + const element = document.querySelector('#drag-ghost-date'); + if (element) { + const { date, position } = calcPositionByEvent(payload); + element.innerHTML = position.x > 0 ? date.format('YYYY-MM-DD HH:mm') : ''; + } + }; + + /** + * @description 处理拖拽离开 + * @param {DragEvent} payload + */ + const handleDragLeave = (payload: DragEvent): void => { + const element = document.querySelector('#drag-ghost-date'); + if (element) element.innerHTML = ''; + }; + + /** + * @description 处理拖拽放置 + * @param {DragEvent} payload + * @returns {*} + */ + const handleDrop = async (payload: DragEvent): Promise => { + payload.preventDefault(); + if (!coordinateElement.value || !bodyCanvas.value) return; + try { + const str = payload.dataTransfer?.getData('data'); + if (!str) return; + const dragData: IDragData = JSON.parse(str); + const { dragType, data, scheduleType } = dragData; + if (!dragType || !['resource', 'task'].includes(scheduleType)) return; + + // 拖拽校验 + const { position, date, resourceId } = calcPositionByEvent(payload); + const start = new Date(date.format('YYYY-MM-DD HH:mm')); + if (props.dragVerify && props.dragVerify instanceof Function) { + const result = await props.dragVerify(dragData, start); + if (!result) return; + } + + // TODO 资源标识 + const id = dragType === 'add' ? createUUID() : data.id; + + if (scheduleType === 'resource') { + $renderLayer.updateResource({ + id: id, + name: data.name, + tasks: [], + data, + }); + } else { + // 任务未拖拽到任务区或时长为零时不做处理 + // TODO: 有些任务又时长,有些任务没有时长,有开始时间和结束时间, + let duration = data.planned_duration + ? Number(data.planned_duration) + : 0; + // TODO 临时测试时长默认为2小时 + duration = 120; + if (position.x < 0 || duration === 0) return; + const end = new Date( + date.add(duration, 'minute').format('YYYY-MM-DD HH:mm'), + ); + const task = { + end, + start, + id: id, + resourceId, + name: data.name, + data: dragType === 'add' ? data : data.data, + }; + $renderLayer.updateTask(task); + } + // 更新绘制 + $uiCoordinate.buildCoordinate( + coordinateElement.value, + $renderLayer.getResources(), + ); + $bgLayer.draw($uiCoordinate, $renderLayer.getResources()); + $renderLayer.buildViewModels($uiCoordinate); + } catch (error) { + ibiz.log.error(error); + } + }; + + /** + * @description 处理开始拖拽 + * @param {DragEvent} payload + * @param {IScheduleTask} task + */ + const handleDragStart = (payload: DragEvent, task: IScheduleTask): void => { + payload.dataTransfer!.effectAllowed = 'copy'; + const item = { + data: task, + dragType: 'update', + scheduleType: 'task', + }; + payload.dataTransfer!.setData('data', JSON.stringify(item)); + }; + + return { handleDrop, handleDragOver, handleDragStart, handleDragLeave }; +} diff --git a/src/resource-scheduler/interface/i-drag-data.ts b/src/resource-scheduler/interface/i-drag-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5f459d87e6a5794c6040784c293f4ebc115b3ea --- /dev/null +++ b/src/resource-scheduler/interface/i-drag-data.ts @@ -0,0 +1,25 @@ +/** + * @description 拖拽数据 + * @export + * @interface IDragData + */ +export interface IDragData { + /** + * @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 91f7af39e1187e7805e55943949b2b321a1115d0..571330bf6fc0d074fb7225ed504a386232e0c83f 100644 --- a/src/resource-scheduler/interface/index.ts +++ b/src/resource-scheduler/interface/index.ts @@ -1,7 +1,8 @@ export type { IGlobalConfig } from './i-global-config'; export type { - IScheduleResource, IScheduleTask, - IResourceViewModel, ITaskViewModel, + IScheduleResource, + IResourceViewModel, } from './i-ui-data'; +export type { IDragData } from './i-drag-data'; diff --git a/src/user-register.ts b/src/user-register.ts index d38a4bbdb4a326625b989514b86b031103be06d4..f9f7b741ed699ffcf56e8e371c44a7f97a29a975 100644 --- a/src/user-register.ts +++ b/src/user-register.ts @@ -1,9 +1,13 @@ import { App } from 'vue'; 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(IBizPanelBottomTabPanel); + V.use(IBizResourceScheduleTree); V.use(ResourceScheduleTable); }, };