diff --git a/package.json b/package.json index 299e483..2d9318c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-fast-compare": "^3.2.0", "react-media": "^1.10.0", "react-sortable-hoc": "^2.0.0", + "relation-graph": "^2.1.19", "simple-query-string": "^1.3.2", "solarlunar": "^2.0.7", "store": "^2.0.12", diff --git a/src/pages/salaryItemFlowGraph/index.less b/src/pages/salaryItemFlowGraph/index.less index 3d6223f..3156b89 100644 --- a/src/pages/salaryItemFlowGraph/index.less +++ b/src/pages/salaryItemFlowGraph/index.less @@ -15,3 +15,63 @@ } } } + +.rootNode { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgb(0, 154, 73); + color: #FFF; + font-size: 14px; + font-weight: bold; + border-radius: 25px; + padding: 10px; +} + +.levelNode { + background: rgb(170, 212, 179) !important; +} + +.commonNode { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + font-size: 14px; + font-weight: bold; + color: #333; + + & > span { + display: inline-block; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + text-align: center; + } +} + +.c-right-menu-panel { + z-index: 999; + background-color: rgb(255, 255, 255); + border: 1px solid rgb(238, 238, 238); + box-shadow: rgb(204, 204, 204) 0px 0px 8px; + position: absolute; + border-radius: 10px; + + .c-node-menu-item { + line-height: 30px; + padding: 5px 8px; + cursor: pointer; + color: rgb(68, 68, 68); + font-size: 12px; + } + + .c-node-menu-item:first-child { + border-bottom: 1px solid rgb(239, 239, 239); + } +} diff --git a/src/pages/salaryItemFlowGraph/index.tsx b/src/pages/salaryItemFlowGraph/index.tsx index 2941fb0..e9f584e 100644 --- a/src/pages/salaryItemFlowGraph/index.tsx +++ b/src/pages/salaryItemFlowGraph/index.tsx @@ -1,25 +1,30 @@ -/* - * Author: 黎永顺 - * name: 中江-薪资账套薪资项目流向图 - * Description: - * Date: 2023/11/14 - */ -import React, { FunctionComponent, useEffect, useMemo, useState } from "react"; -import { Menu } from "antd"; -import { FundFlowGraph } from "@ant-design/graphs"; +import type { MutableRefObject } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import type { RelationGraphExpose, RGLink, RGNode, RGNodeSlotProps, RGOptions } from "relation-graph/react"; +import RelationGraph from "relation-graph/react"; import { exceptStr } from "@/utils/common"; import { extractTree } from "@/pages/salaryItemDiagram"; +import { RGEventTargetType, RGUserEvent } from "relation-graph/types/types/types"; +import cs from "classnames"; import styles from "./index.less"; -interface OwnProps { -} - -type Props = OwnProps; - -const index: FunctionComponent = (props) => { - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); +const NodeSlot: React.FC = ({ node }) => { + if (node.id === "0") { + return
{node.text}
; + } + // @ts-ignore + if (node.id !== "0" && !node.data.parentId) { + return
{node.text}
; + } + return
+ {node.text} +
; +}; +const Index: React.FC = () => { + const [itemsTree, setItemsTree] = useState([]); const [i18n, setI18n] = useState({}); + const [menuXY, setMenuXY] = useState({ left: "", top: "", data: {} }); + const graphRef = useRef() as MutableRefObject; useEffect(() => { window.parent.postMessage({ type: "init" }, "*"); window.addEventListener("message", receiveMessageFromIndex, false); @@ -27,139 +32,114 @@ const index: FunctionComponent = (props) => { window.removeEventListener("message", receiveMessageFromIndex, false); }; }, []); - const openNotification = (data: any) => { - const { formulaId } = data; - window.parent.postMessage({ type: "turn", payload: { id: "VIEW", params: { formulaId } } }, "*"); - }; const receiveMessageFromIndex = (event: any) => { const data: any = exceptStr(event.data); if (!_.isEmpty(data)) { const { itemsTree, i18n } = data; setI18n(i18n); - setNodes([{ - id: "0", - salaryItemName: i18n["薪资项目"], - value: { text: i18n["薪资项目"] } - }, ..._.map(extractTree(itemsTree), o => ({ - ...o, + setItemsTree(itemsTree); + } + }; + const dataSource: any = useMemo(() => { + setMenuXY({ left: "", top: "", data: {} }); + return { + rootId: "0", + nodes: [{ id: "0", text: i18n["薪资项目"] }, ..._.map(extractTree(itemsTree), o => ({ id: o.salaryItemId.toString(), - value: { text: o.salaryItemName.length >= 8 ? o.salaryItemName.slice(0, 8) + "..." : o.salaryItemName } - }))]); - const edges: any = _.map(extractTree(itemsTree), o => ({ - source: o.parentId ? o.parentId.toString() : "0", - target: o.salaryItemId.toString() - })); - setEdges(edges); + text: o.salaryItemName, data: { ...o } + }))], + lines: _.map(extractTree(itemsTree), o => ({ + from: o.parentId ? o.parentId.toString() : "0", + to: o.salaryItemId.toString() + })) + }; + }, [itemsTree, i18n]); + useEffect(() => { + if (!_.isEmpty(dataSource.nodes) && !_.isEmpty(dataSource.lines)) { + const init = showGraph(); } + }, [dataSource]); + const showGraph = async () => { + await graphRef.current.setJsonData(dataSource, (graphInstance) => { + }); }; - const config: any = useMemo(() => { - const data = { nodes, edges }; + const options: RGOptions = useMemo(() => { return { - data, - menuCfg: { - className: styles.salaryItemOptWrapper, - shouldBegin: (evt: any) => { - const { item: { _cfg: { model } } } = evt; - return model.canAddItem || (!_.isNil(model.formulaId) && model.formulaId !== 0); - }, - handleMenuClick: (__: any, evt: any) => { - const { _cfg: { model } } = evt; - const id = __.innerText.indexOf(i18n["添加薪资项目"]) !== -1 ? "ADD" : "EDIT"; - window.parent.postMessage({ - type: "turn", - payload: { - id, params: { - visible: true, id: model?.groupId, dataType: model?.dateType, - formulaId: model?.formulaId, valueType: model?.valueType, - salaryItemId: model?.salaryItemId - } - } - }, "*"); - }, - customContent: (evt: any) => { - const { item: { _cfg: { model } } } = evt; - let items: any = [ - { label: i18n["添加薪资项目"], key: "add" }, - { label: i18n["编辑公式"], key: "edit" } - ]; - (_.isNil(model.formulaId) || model.formulaId === 0) && (items = _.filter(items, o => o.key !== "edit")); - (!model.canAddItem) && (items = _.filter(items, o => o.key !== "add")); - return ( - - ); - } - }, - edgeCfg: { - endArrow: { fill: "#333" }, - style: { stroke: "#333" }, - edgeStateStyles: { - hover: { - stroke: "#1890ff", - lineWidth: 2, - endArrow: { - fill: "#1890ff" - } - } - } - }, - nodeCfg: { - style: (item: any, group: any) => { - const { id, parentId } = item; - let styleObj = {}; - switch (id) { - case "0": - styleObj = { ...styleObj, fill: "rgb(0,154,73)", stroke: "rgb(0,154,73)" }; - break; - default: - styleObj = { ...styleObj, fill: "#FFF", stroke: "#666", radius: [2, 2, 2, 2] }; - if (_.isNil(parentId)) styleObj = { - ...styleObj, fill: "rgb(170,212,179)", - stroke: "#000", radius: [2, 2, 2, 2] - }; - break; - } - - return styleObj; - }, - label: { - style: (item: any) => { - const { id } = item; - return { fontWeight: "bold", fill: id === "0" ? "#FFF" : "#333", fontSize: id === "0" ? 18 : 12 }; - } - } - }, - markerCfg: (cfg: any) => { - const { edges } = data; - return { - position: "right", - show: edges.find((item) => item.source === cfg.id) - }; - }, + debug: false, layout: { - rankdir: "LR", - nodesep: 5, - ranksep: 20 - }, - tooltipCfg: { - show: true, - shouldBegin: (evt: any) => { - const { item: { _cfg: { model } } } = evt; - return model.salaryItemName.length >= 8; - }, - customContent: (item: any) => (

{item.salaryItemName}

) - }, - onReady: (graph: any) => { - graph.on("node:click", (evt: any) => { - if (_.isEmpty(evt.target.attrs) || JSON.stringify(evt.target.attrs).indexOf("cursor") !== -1) return; - openNotification(evt.item._cfg.model); - }); - graph.on("node:contextmenu", (evt: any) => { - }); + label: "树", + layoutName: "tree", + layoutClassName: "seeks-layout-center", + from: "left", + // 通过这4个属性来调整 tree-层级距离&节点距离 + min_per_width: undefined, + max_per_width: 300, + min_per_height: 60, + max_per_height: undefined, + levelDistance: "" // 如果此选项有值,则优先级高于上面那4个选项 }, - behaviors: ["drag-canvas", "zoom-canvas"] + defaultExpandHolderPosition: "right", + defaultNodeWidth: 180, + defaultNodeHeight: 50, + defaultLineShape: 4, + defaultNodeBorderWidth: 1, + defaultLineColor: "#333", + defaultExpandHolderColor: "rgba(0, 206, 209, 1)", + defaultNodeColor: "transparent", + defaultNodeBorderColor: "#333", + allowShowMiniToolBar: false //工具栏展示与否 }; - }, [nodes, edges]); - return ((!_.isEmpty(nodes) && !_.isEmpty(edges)) ? : null); + }, []); + const onNodeRightClick = (e: RGUserEvent & { pageX: number, pageY: number }, objectType: RGEventTargetType, object: RGNode | RGLink | undefined) => { + if (objectType === "node") { + // @ts-ignore + setMenuXY({ left: e.pageX, top: e.pageY, data: object?.data }); + } else { + setMenuXY({ left: "", top: "", data: {} }); + } + return true; + }; + const onNodeClick = (node: RGNode) => { + setMenuXY({ left: "", top: "", data: {} }); + if (node.type === "node") { + // @ts-ignore + const { formulaId } = node?.data; + formulaId && + window.parent.postMessage({ type: "turn", payload: { id: "VIEW", params: { formulaId } } }, "*"); + } + return true; + }; + const handleNodeOpt = (id: string) => { + const { data } = menuXY; + window.parent.postMessage({ + type: "turn", + payload: { + id, params: { + visible: true, id: data?.groupId, dataType: data?.dateType, + formulaId: data?.formulaId, valueType: data?.valueType, + salaryItemId: data?.salaryItemId + } + } + }, "*"); + }; + return
+ + { + !_.isEmpty(menuXY.data) && +
+ { + menuXY.data.canAddItem && +
handleNodeOpt("ADD")}>{i18n["添加薪资项目"]}
+ } + { + !_.isNil(menuXY.data.formulaId) && +
handleNodeOpt("EDIT")}>{i18n["编辑公式"]}
+ } +
+ } +
; }; - -export default index; +export default Index; diff --git a/src/pages/salaryItemFlowGraph/index_antdestgnCharts.tsx b/src/pages/salaryItemFlowGraph/index_antdestgnCharts.tsx new file mode 100644 index 0000000..6d49e00 --- /dev/null +++ b/src/pages/salaryItemFlowGraph/index_antdestgnCharts.tsx @@ -0,0 +1,167 @@ +/* + * Author: 黎永顺 + * name: 中江-薪资账套薪资项目流向图 + * Description: + * Date: 2023/11/14 + */ +import React, { FunctionComponent, useEffect, useMemo, useState } from "react"; +import { Menu } from "antd"; +import { FundFlowGraph } from "@ant-design/graphs"; +import { exceptStr } from "@/utils/common"; +import { extractTree } from "@/pages/salaryItemDiagram"; +import styles from "./index.less"; + +interface OwnProps { +} + +type Props = OwnProps; + +const index: FunctionComponent = (props) => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [i18n, setI18n] = useState({}); + useEffect(() => { + window.parent.postMessage({ type: "init" }, "*"); + window.addEventListener("message", receiveMessageFromIndex, false); + return () => { + window.removeEventListener("message", receiveMessageFromIndex, false); + }; + }, []); + const openNotification = (data: any) => { + const { formulaId } = data; + window.parent.postMessage({ type: "turn", payload: { id: "VIEW", params: { formulaId } } }, "*"); + }; + const receiveMessageFromIndex = (event: any) => { + const data: any = exceptStr(event.data); + if (!_.isEmpty(data)) { + const { itemsTree, i18n } = data; + setI18n(i18n); + setNodes([{ + id: "0", + salaryItemName: i18n["薪资项目"], + value: { text: i18n["薪资项目"] } + }, ..._.map(extractTree(itemsTree), o => ({ + ...o, + id: o.salaryItemId.toString(), + value: { text: o.salaryItemName.length >= 8 ? o.salaryItemName.slice(0, 8) + "..." : o.salaryItemName } + }))]); + const edges: any = _.map(extractTree(itemsTree), o => ({ + source: o.parentId ? o.parentId.toString() : "0", + target: o.salaryItemId.toString() + })); + setEdges(edges); + } + }; + const config: any = useMemo(() => { + const data = { nodes, edges }; + return { + data, + menuCfg: { + className: styles.salaryItemOptWrapper, + shouldBegin: (evt: any) => { + const { item: { _cfg: { model } } } = evt; + return model.canAddItem || (!_.isNil(model.formulaId) && model.formulaId !== 0); + }, + handleMenuClick: (__: any, evt: any) => { + const { _cfg: { model } } = evt; + const id = __.innerText.indexOf(i18n["添加薪资项目"]) !== -1 ? "ADD" : "EDIT"; + window.parent.postMessage({ + type: "turn", + payload: { + id, params: { + visible: true, id: model?.groupId, dataType: model?.dateType, + formulaId: model?.formulaId, valueType: model?.valueType, + salaryItemId: model?.salaryItemId + } + } + }, "*"); + }, + customContent: (evt: any) => { + const { item: { _cfg: { model } } } = evt; + let items: any = [ + { label: i18n["添加薪资项目"], key: "add" }, + { label: i18n["编辑公式"], key: "edit" } + ]; + (_.isNil(model.formulaId) || model.formulaId === 0) && (items = _.filter(items, o => o.key !== "edit")); + (!model.canAddItem) && (items = _.filter(items, o => o.key !== "add")); + return ( + + ); + } + }, + edgeCfg: { + endArrow: { fill: "#333" }, + style: { stroke: "#333" }, + edgeStateStyles: { + hover: { + stroke: "#1890ff", + lineWidth: 2, + endArrow: { + fill: "#1890ff" + } + } + } + }, + nodeCfg: { + style: (item: any, group: any) => { + const { id, parentId } = item; + let styleObj = {}; + switch (id) { + case "0": + styleObj = { ...styleObj, fill: "rgb(0,154,73)", stroke: "rgb(0,154,73)" }; + break; + default: + styleObj = { ...styleObj, fill: "#FFF", stroke: "#666", radius: [2, 2, 2, 2] }; + if (_.isNil(parentId)) styleObj = { + ...styleObj, fill: "rgb(170,212,179)", + stroke: "#000", radius: [2, 2, 2, 2] + }; + break; + } + + return styleObj; + }, + label: { + style: (item: any) => { + const { id } = item; + return { fontWeight: "bold", fill: id === "0" ? "#FFF" : "#333", fontSize: id === "0" ? 18 : 12 }; + } + } + }, + markerCfg: (cfg: any) => { + const { edges } = data; + return { + position: "right", + show: edges.find((item) => item.source === cfg.id) + }; + }, + layout: { + rankdir: "LR", + // nodesep: 5, + // ranksep: 20 + }, + tooltipCfg: { + show: true, + shouldBegin: (evt: any) => { + const { item: { _cfg: { model } } } = evt; + return model.salaryItemName.length >= 8; + }, + customContent: (item: any) => (

{item.salaryItemName}

) + }, + onReady: (graph: any) => { + graph.on("node:click", (evt: any) => { + if (_.isEmpty(evt.target.attrs) || JSON.stringify(evt.target.attrs).indexOf("cursor") !== -1) return; + openNotification(evt.item._cfg.model); + }); + graph.on("node:contextmenu", (evt: any) => { + }); + }, + behaviors: ["drag-canvas", "zoom-canvas", 'drag-node'], + // autoFit: false, + // fitCenter: true + }; + }, [nodes, edges]); + return ((!_.isEmpty(nodes) && !_.isEmpty(edges)) ? : null); +}; + +export default index;