国外得组织架构图

不常见得组织架构图,用了G6和自己一些写法再里面。此篇为自己得记录篇-----------------------------

js 复制代码
let data = [
  {
    "id": 10009070,
    "shortNameEn": "MAINT",
    "shortNameZh": null,
    "shortNameAr": null,
    "nameEn": "Maintenance",
    "nameZh": "Maintenance",
    "nameAr": "Maintenance",
    "sectionCount": 0,
    "localCount": 38,
    "expatCount": 68,
    "mixedCount": 0,
    "headcount": 106,
    "employeeCount": 0,
    "leaderList": null,
    "positionList": [
      {
        "id": 10074621,
        "unicode": "AOSIMTNSPOS111",
        "enterpriseId": 10000504,
        "buId": 10009070,
        "name": "Metering Engineer",
        "shortName": "Metering Engineer",
        "reportTo": 10074531,
        "reportBuId": 10009153,
        "personalType": 0,
        "positionType": 1,
        "workPatternId": 10000042,
        "workPatternName": "14/14",
        "headCount": 2,
        "sort": 1,
        "employeeCount": 1,
        "employeeList": [{
            "id": 10087744,
            "unicode": null,
            "fullName": "HASAN LAZIM MOHSIN",
            "workPatternId": 10000042,
            "workPatternName": "14/14",
          }
        ],
        "children": null
      }
    ],
    "children": [
      {
        "id": 10009109,
        "tenantId": 10000000,
        "appId": 10000000,
        "unicode": "AOSIMAINTSEC003",
        "enterpriseId": 10000504,
        "parentId": 10009070,
        "unitTypeId": 10001520,
        "shortNameEn": "Diesel.",
        "shortNameZh": null,
        "shortNameAr": null,
        "nameEn": "Diesel Workshop",
        "nameZh": "Diesel Workshop",
        "nameAr": "Diesel Workshop",
        "sectionCount": 0,
        "localCount": 0,
        "expatCount": 4,
        "mixedCount": 0,
        "headcount": 4,
        "employeeCount": 0,
        "unitTypeIcon": "section",
        "leaderList": null,
        "positionList": [{
            "id": 10076160,
            "uuid": "a858b0a8b57e47f7adbaeda46e67bbd9",
            "deleted": 0,
            "createTime": "2024-12-30 11:07:25",
            "updateTime": "2024-12-30 11:07:25",
            "createBy": "levi.liang@itforce-tech.com",
            "updateBy": "levi.liang@itforce-tech.com",
            "tenantId": 10000000,
            "appId": 10000000,
            "unicode": "AOSIDIESEL.POS002",
            "enterpriseId": 10000504,
            "buId": 10009109,
            "name": "WPB",
            "shortName": "WPB",
            "reportTo": 10074592,
            "reportBuId": 10009161,
            "personalType": 1,
            "positionType": 0,
            "headCount": 1,
            "sort": 1,
            "employeeCount": 2,
            "employeeList": [{
                "id": 10087898,
                "unicode": null,
                "fullName": "WALEED KAMIL JASIM",
                "workPatternId": 10000042,
                "workPatternName": "14/14",
              },
            ],
            "children": null
          },
        ],
        "children": null
      }
    ]
  }
]

这是组织树需要得数据结构 view-org-chart.vue 父级

js 复制代码
<script setup>
import { h } from 'vue';
import { CloseOutlined } from '@ant-design/icons-vue';
import { useLanguage } from '@/hooks/index';
import { GetBusinessUnitTree } from '@/api/organization/index';
const { isCn, isArabic } = useLanguage();
const TreeChart = defineAsyncComponent(() =>
	import('./components/TreeChart.vue'),
);
const route = useRoute();
const router = useRouter();
const TreeChartRef = ref(null);
// 使用watch来监听对象
const stringTree = ref([]);
const convertValuesToString = (tree) => {
	return tree.map((node) => {
		const convertedNode = { ...node };
		if (convertedNode.children) {
			convertedNode.children = convertValuesToString(convertedNode.children);
		}
		convertedNode.id = String(convertedNode.id);
		convertedNode.nameZh =
			isCn && convertedNode.nameZh
				? convertedNode.nameZh
				: convertedNode.nameEn;
		convertedNode.nameAr =
			isArabic && convertedNode.nameAr
				? convertedNode.nameAr
				: convertedNode.nameEn;
		return convertedNode;
	});
};

// 我给了每个层级加了标识好区分
const addLevels = (nodes, level2 = 0) => {
	return nodes.map((node) => {
		node.level2 = level2; // 添加层级标记
		if (node.children && node.children.length > 0) {
			node.children = addLevels(node.children, level2 + 1);
		}
		return node;
	});
};
// 这个是树形访问接口,可以替换
const getSelectTree = async () => {
	await GetBusinessUnitTree({
		id: route.query.buId,
	}).then((res) => {
		stringTree.value = addLevels(convertValuesToString(res.data));
	});
};
onMounted(() => {
	getSelectTree();
});

const loading = ref(false);
const loading2 = ref(false);
// 下载svg 图片
const handleDownIamges = debounce(() => {
	loading.value = true;
	TreeChartRef.value.downImage();
	loading.value = false;
}, 1000);
// 下载PDF
const handleDownPdf = debounce(() => {
	loading2.value = true;
	TreeChartRef.value.downPdf();
	loading2.value = false;
}, 1000);
const onClose = () => {
	router.back();
};
</script>
<template>
	<div style="background-color: #fff; border-radius: 6px">
		<a-row style="padding: 16px 16px 0 16px">
			<a-col :span="24" style="display: flex; justify-content: flex-end">
				<a-button
					style="margin-right: 8px"
					:loading="loading"
					@click="handleDownIamges"
				>
					<i class="fa fa-download" />
					<span style="margin-left: 4px; margin-right: 4px">SVG</span>
				</a-button>
				<a-button
					style="margin-right: 8px"
					:loading="loading2"
					@click="handleDownPdf"
				>
					<i class="fa fa-download" />
					<span style="margin-left: 4px; margin-right: 4px">PDF</span>
				</a-button>
				<a-button :icon="h(CloseOutlined)" @click="onClose"></a-button>
			</a-col>
		</a-row>

		<TreeChart
			v-if="stringTree.length > 0"
			ref="TreeChartRef"
			:tree-data="stringTree"
		/>
	</div>
</template>

TreeChart.vue 子级 渲染图

js 复制代码
<script setup>
import G6 from '@antv/g6';
import { useLanguage } from '@/hooks/index';
import jsPDF from 'jspdf';
import { Session } from '@/utils/storage';
const router = useRouter();
const { isCn, isArabic } = useLanguage();

const props = defineProps({
	treeData: { type: Array },
});

const graphContainer = ref(null);
let graph = null;

const isCategory = (cfg) => {
	if (cfg.personalType === 0) {
		return `<div class="square-style-right-top positionGreen">
      ${cfg.name}
    </div>`;
	} else if (cfg.personalType === 1) {
		return `<div class="square-style-right-top positionOrange">
      ${cfg.name}
    </div>`;
	} else {
		return `<div class="square-style-right-top gradualChange">
      ${cfg.name}
    </div>`;
	}
};
const generateSonHtml = (children, hasOuterLine = false) => {
	if (!children) return '';

	let level = 0;

	let html = '';

	function generaHtml(kids, level) {
		if (!kids) return '';
		level++;

		let kidHtml = '';
		kids.forEach((cfg, idx) => {
			kidHtml += `
              <div class="outermost">
                ${
									level === 1 && hasOuterLine
										? `
                    <div class="outermost-line">
                      <div class="top-line"></div>
                      ${
												idx === kids.length - 1
													? ''
													: `<div class="bottom-line"></div>`
											}
                    </div>
                  `
										: ''
								}
                <li class="hierarchy" data-level='${level}' last-index='${
				kids.length - 1
			}' index='${idx}'>
                  <div class="square-wrap">
                    ${
											level > 1
												? `
                        <div class="line">
                          <div class="top-line"></div>
                          ${
														idx === kids.length - 1
															? ''
															: `<div class="bottom-line"></div>`
													}
                        </div>
                      `
												: ''
										}
                    <div class="node square-style">
                      <div class="square-style-left">
                        <div class="square-style-left-top"> ${
													cfg.employeeCount || 0
												} / ${
				(cfg.employeeList && cfg.employeeList.length) || 0
			}</div>
                        <div class="square-style-left-bottom"> ${
													cfg.workPatternName || '--'
												}</div>
                      </div>
                      <div class="square-style-right">
                        ${isCategory(cfg)}
                        ${
													(cfg.employeeList &&
														cfg.employeeList
															.map((item) => {
																if (item.fullName == 'TBC') {
																	return `<div class="square-style-right-bottom" style="color: #409eff">${item.fullName}</div>`;
																} else {
																	return `<div class="square-style-right-bottom" data-id='${
																		item.id
																	}'>${item.fullName}(${
																		item.workPatternName || '--'
																	})</div>`;
																}
															})
															.join('')) ||
													`<div class="square-style-right-bottom">--</div>`
												}
                      </div>
                    </div>
                  </div>
                  ${
										cfg.children && cfg.children.length
											? `<ul class="nodes">${generaHtml(
													cfg.children,
													level,
											  )}</ul>`
											: ''
									}
                </li>
              </div>
      `;
		});

		return kidHtml;
	}

	html += `
      <ul class="nodes">
        ${generaHtml(children, level)}
      </ul>
    `;

	return html;
};

const getTopHtml = (cfg) => {
	return `
    <div class="top-box-html">
      <div class="diamond-style">
        <div class="diamond-top">${cfg.employeeCount || 0} / ${
		cfg.headcount || 0
	}</div>
        <div class="diamond-bottom">${cfg.workPatternName || '--'}</div>
      </div>
      <div class="tox-box-diamond">
        <div class="diamond-title">
          ${
						isCn.value && cfg.nameZh
							? cfg.nameZh
							: isArabic.value && cfg.nameAr
							? cfg.nameAr
							: cfg.nameEn
					}
        </div>
        ${
					(cfg.leaderList &&
						cfg.leaderList
							.map((item) => {
								return `<div class="diamond-name">${item.fullName.replace(
									/(.{19})/g,
									'$1\n',
								)}(${item.workPatternName || '--'})</div>`;
							})
							.join('')) ||
					`<div class="diamond-name">--</div>`
				}
      </div>
    </div>`;
};
const generateSonTopHtml = (children, hasOuterLine = false) => {
	if (!children) return '';

	let level = 0;

	let html = '';

	function generaHtml(kids, level) {
		if (!kids) return '';
		level++;

		let kidHtml = '';
		kids.forEach((cfg, idx) => {
			kidHtml += `
              <div class="outermost">
                ${
									level === 1 && hasOuterLine
										? `
                    <div class="outermost-line">
                      ${
												idx === 0
													? '<div class="top-line0"></div>'
													: `<div class="top-line"></div>`
											}
                      <div class="bottom-line"></div>
                    </div>
                  `
										: ''
								}
                <li class="hierarchy" data-level='${level}' last-index='${
				kids.length - 1
			}' index='${idx}'>
                  <div class="square-wrap">
                    ${
											level > 1
												? `
                        <div class="line">
                          <div class="top-line"></div>
                          ${
														idx === kids.length - 1
															? ''
															: `<div class="bottom-line"></div>`
													}
                        </div>
                      `
												: ''
										}
                    <div class="node square-style">
                      <div class="square-style-left">
                        <div class="square-style-left-top">${
													cfg.employeeCount || 0
												} / ${
				(cfg.employeeList && cfg.employeeList.length) || 0
			}</div>
                        <div class="square-style-left-bottom"> ${
													cfg.workPatternName || '--'
												}</div>
                      </div>
                      <div class="square-style-right">
                        ${isCategory(cfg)}
                        ${
													(cfg.employeeList &&
														cfg.employeeList
															.map((item) => {
																if (item.fullName == 'TBC') {
																	return `<div class="square-style-right-bottom" style="color: #409eff">${item.fullName}</div>`;
																} else {
																	return `<div class="square-style-right-bottom" data-id="${
																		item.id
																	}">${item.fullName}(${
																		item.workPatternName || '--'
																	})</div>`;
																}
															})
															.join('')) ||
													`<div class="square-style-right-bottom">--</div>`
												}
                      </div>
                    </div>
                  </div>
                  ${
										cfg.children && cfg.children.length
											? `<ul class="nodes">${generaHtml(
													cfg.children,
													level,
											  )}</ul>`
											: ''
									}
                </li>
              </div>
      `;
		});

		return kidHtml;
	}

	html += `
      <ul class="nodes">
        ${generaHtml(children, level)}
      </ul>
    `;

	return html;
};

function calculateMaxChildren(data) {
	let maxChildrenCount = 0;

	for (let node of data) {
		let childrenCount = 0;

		// 递归计算子节点的数量
		if (node.children) {
			childrenCount += calculateMaxChildren(node.children);
		}

		// 计算当前节点及其子节点的子节点数量
		childrenCount += node.children ? node.children.length : 0;

		// 更新最大子节点数量
		maxChildrenCount = Math.max(maxChildrenCount, childrenCount);
	}

	return maxChildrenCount;
}
const methodChart = () => {
	G6.registerNode(
		'dom-node',
		{
			draw(cfg, group) {
				const container = document.createElement('div');
				if (cfg.level2 === 0) {
					container.innerHTML = `
              <div class="boxChartTop" id="${cfg.id}">
                ${getTopHtml(cfg)}
								${
									cfg.positionList && cfg.positionList.length > 0
										? `<div class="center-line"></div>`
										: ''
								}
                ${generateSonTopHtml(cfg.positionList, true)}
              </div>`;
				} else {
					container.innerHTML = `
              <div class="boxChart" id="${cfg.id}">
                <div class="circle-wrap">
                  <div class="outermost-line">
                    <div class="bottom-line"></div>
                  </div>
                  <div class="circle-style">${
										isCn.value && cfg.nameZh
											? cfg.nameZh
											: isArabic.value && cfg.nameAr
											? cfg.nameAr
											: cfg.nameEn
									}</div>
                </div>
                ${generateSonHtml(cfg.positionList, true)}
              </div>
            `;
				}
				// 将container插入隐藏容器以计算宽高
				const hiddenContainer = document.getElementById('hiddenContainer');
				hiddenContainer.appendChild(container);
				const width = container.clientWidth;
				const height = container.clientHeight;
				hiddenContainer.removeChild(container);

				const shape = group.addShape('dom', {
					attrs: {
						width,
						height: height + 10,
						x: 0,
						y: 0,
						html: container.innerHTML,
						size: [width, height],
					},
					draggable: true,
				});

				cfg.width = width;
				cfg.height = height;

				return shape;
			},
		},
		'dom',
	);

	G6.registerEdge('flow-line', {
		draw(cfg, group) {
			let startPoint = cfg.startPoint;
			let endPoint = cfg.endPoint;

			// 计算源节点底部中间位置
			const sourceModel = graph.findById(cfg.source).getModel();
			const sourceHeight = sourceModel.height;

			// 目标节点 这块可以优化,看看那位大佬帮忙优化一下
			const targetModel = graph.findById(cfg.target).getModel();
			const targetHeight = targetModel.height;
			const list =
				sourceModel.positionList && sourceModel.positionList.length > 0
					? calculateMaxChildren(sourceModel.positionList)
					: 0;
			console.log('计算', list);
			// 计算父节点底部中间位置
			let sourceX = startPoint.x;
			if (
				sourceModel.level2 === 0 &&
				sourceModel.positionList?.length > 0 &&
				list < 1
			) {
				console.log('1');
				sourceX = startPoint.x - 115;
			} else if (
				sourceModel.level2 === 0 &&
				sourceModel.positionList?.length <= 3 &&
				list <= 3
			) {
				console.log('2');
				sourceX = startPoint.x - 150;
			} else if (
				sourceModel.level2 === 0 &&
				sourceModel.positionList?.length > 3 &&
				list < 3
			) {
				console.log('3');
				sourceX = startPoint.x - 180;
			} else if (
				sourceModel.level2 === 0 &&
				sourceModel.positionList?.length < 4 &&
				list > 3
			) {
				console.log('4');
				sourceX = startPoint.x - list * 35;
			} else if (
				sourceModel.level2 === 0 &&
				sourceModel.positionList?.length === 4 &&
				list === 3
			) {
				console.log('5');
				sourceX = startPoint.x - list * 50;
			} else if (
				sourceModel.level2 === 0 &&
				sourceModel.positionList?.length <= 4 &&
				list >= 4
			) {
				console.log('6');
				sourceX = startPoint.x - list * 50;
			}

			const sourceY = startPoint.y + sourceHeight / 2;

			// 计算目标节点顶部中间位置
			const targetX = endPoint.x;
			const targetY = endPoint.y - targetHeight / 2;

			const isSafari = /^((?!chrome|android).)*safari/i.test(
				navigator.userAgent,
			);

			// 控制连接线的路径,使其从父节点底部中间连接到子节点顶部中间
			const path = [
				['M', sourceX, sourceY - 17],
				['L', sourceX, (sourceY + targetY) / 2],
				['L', targetX, (sourceY + targetY) / 2],
				['L', targetX, targetY],
			];

			// 绘制连接线
			const shape = group.addShape('path', {
				attrs: {
					path: path,
					stroke: '#e07572',
					lineWidth: 1.5,
				},
			});

			return shape;
		},
	});
};

const treeChart = () => {
	methodChart();
	const width = graphContainer.value.scrollWidth || 1740;
	const height = graphContainer.value.scrollHeight || 700;
	graph = new G6.TreeGraph({
		container: graphContainer.value,
		width,
		height,
		renderer: 'svg',
		fitView: true,
		linkCenter: true,
		modes: {
			default: ['drag-canvas', 'zoom-canvas'],
		},
		defaultNode: {
			type: 'dom-node',
		},
		defaultEdge: {
			type: 'flow-line',
			style: {
				stroke: '#e07572',
				lineWidth: 1,
			},
		},
		layout: {
			type: 'compactBox',
			direction: 'TB',
			getId: function getId(d) {
				return d.id;
			},
			getHeight: function getHeight(d) {
				return d.height || 200;
			},
			getWidth: function getWidth(d) {
				return d.width || 16;
			},
			// 每个节点的垂直间隙
			getVGap: function getVGap() {
				return 50;
			},
			// 每个节点的水平间隙
			getHGap: function getHGap() {
				return 70;
			},
		},
	});

	graph.read(props.treeData[0]);
	graph.clear();

	// 在首次渲染后获取节点实际宽高并重新布局
	setTimeout(() => {
		graph.read(props.treeData[0]);
		graph.fitView();
	}, 650);

	if (typeof window !== 'undefined')
		window.onresize = () => {
			if (!graph || graph.get('destroyed')) return;
			if (
				!graphContainer.value ||
				!graphContainer.value.scrollWidth ||
				!graphContainer.value.scrollHeight
			)
				return;
			graph.changeSize(
				graphContainer.value.scrollWidth,
				graphContainer.value.scrollHeight,
			);
		};
};
const context = ref();
const downImage = async () => {
	graph.fitView(); // 确保视图包含所有节点和边
	setTimeout(() => {
		domtoimage
			.toSvg(context.value)
			.then(function (dataUrl) {
				const link = document.createElement('a');
				link.href = dataUrl;
				link.download = `Org Chart-${props.treeData[0].nameEn}`;
				document.body.appendChild(link);
				link.click();
				document.body.removeChild(link);
			})
			.catch(function (error) {
				console.error('oops, something went wrong!', error);
			});
	}, 200);
};

const downPdf = () => {
	graph.fitView(); // 确保视图包含所有节点和边
	const target = context.value;
	console.log('target', target);

	let contentWidth = target.clientWidth; // 获得该容器的宽
	let contentHeight = target.clientHeight; // 获得该容器的高

	target.ownerDocument.defaultView.devicePixelRatio = 1.25;
	target.ownerDocument.defaultView.innerWidth = contentWidth;
	target.ownerDocument.defaultView.innerHeight = contentHeight;

	let opts = {
		scale: 4,
		width: contentWidth,
		height: contentHeight,
		useCORS: true,
		bgcolor: '#fff',
	};
	domtoimage
		.toPng(target, opts)
		.then(function (dataUrl) {
			var img = new Image();
			img.src = dataUrl;
			//一页pdf显示html页面生成的canvas高度;
			var pageHeight = (contentWidth / 592.28) * 841.89;
			//未生成pdf的html页面高度
			var leftHeight = contentHeight;
			//页面偏移
			var position = 0;
			//a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
			var imgWidth = 595.28;
			var imgHeight = (592.28 / contentWidth) * contentHeight;

			var pdf = new jsPDF('', 'pt', 'a4');

			//有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
			//当内容未超过pdf一页显示的范围,无需分页
			if (leftHeight < pageHeight) {
				pdf.addImage(dataUrl, 'PNG', 0, 0, imgWidth, imgHeight);
			} else {
				while (leftHeight > 0) {
					pdf.addImage(dataUrl, 'PNG', 0, position, imgWidth, imgHeight);
					leftHeight -= pageHeight;
					position -= 841.89;
					//避免添加空白页
					if (leftHeight > 0) {
						pdf.addPage();
					}
				}
			}

			pdf.save(`Org Chart-${props.treeData[0].nameEn}.pdf`);
		})
		.catch(function (error) {
			console.error('oops, something went wrong!', error);
		});
};
onUnmounted(() => {
	// 销毁图实例
	if (graph) {
		graph.clear();
		graph.destroy();
	}
});

const closeChart = () => {
	if (graph) {
		graph.clear();
		graph.destroy();
	}
};

onMounted(() => {
	Session.remove('employeeID');
	Session.remove('employeeParams');
	if (props.treeData && props.treeData.length) {
		treeChart();
	}
});
const clickHandler = (event) => {
	if (event.target.classList.contains('square-style-right-bottom')) {
		var dataId = event.target.getAttribute('data-id');
		if (dataId) {
			Session.set('employeeID', dataId);
			let params = {
				form: 'organization',
				employeeId: dataId,
				to: 'workInfo',
			};
			Session.set('employeeParams', params);
			router.push({ name: 'employDetails' });
		}
	}
};

document.body.addEventListener('click', clickHandler);
onBeforeUnmount(() => {
	document.body.removeEventListener('click', clickHandler);
});

defineExpose({
	treeChart,
	downImage,
	downPdf,
	closeChart,
});
</script>
<template>
	<div id="context" ref="context">
		<div class="use-cases">
			<div class="total-table">
				<div class="lattice">
					<div class="box">{{ $t('employee.organization.expat') }}</div>
					<div class="box">{{ props.treeData[0].expatCount }}</div>
				</div>
				<div class="lattice">
					<div class="box">{{ $t('employee.organization.local') }}</div>
					<div class="box">{{ props.treeData[0].localCount }}</div>
				</div>
				<div class="lattice">
					<div class="box">{{ $t('employee.organization.mixed') }}</div>
					<div class="box">{{ props.treeData[0].mixedCount }}</div>
				</div>
				<div class="lattice total">
					<div class="box">{{ $t('employee.organization.total') }}</div>
					<div class="box">{{ props.treeData[0].headcount }}</div>
				</div>
			</div>
			<div class="org-text">
				<div class="box positionGreen"></div>
				{{ $t('employee.organization.local') }}
				<div class="box positionOrange"></div>
				{{ $t('employee.organization.expat') }}
				<div class="box gradualChange"></div>
				{{ $t('employee.organization.mixed') }}
			</div>
		</div>
		<div id="graphContainer" ref="graphContainer"></div>
		<div
			id="hiddenContainer"
			style="visibility: hidden; position: absolute"
		></div>
	</div>
</template>
<style lang="scss">
.total-table {
	border: 1px solid #dae0e6;
	border-radius: 4px;
	.lattice {
		display: flex;
		color: #66809e;
		border-bottom: 1px solid #dae0e6;
		&:last-child {
			border-bottom: none;
		}
		.box {
			text-align: center;
			padding: 8px 16px;
			width: 80px;
			border-right: 1px solid #dae0e6;
			&:last-child {
				border-right: none;
			}
		}
	}
	.total {
		background-color: #f2f4f7;
	}
}
.use-cases {
	display: flex;
	justify-content: space-between;
	padding: 24px 24px 0px 24px;
	.org-text {
		display: flex;
		align-items: center;
		column-gap: 8px;
		margin-right: 8px;
		color: #66809e;
		.box {
			width: 20px;
			height: 20px;
			border-radius: 4px;
		}
		.positionGreen {
			background-color: #6fc9c2;
		}
		.positionOrange {
			background-color: #f2f09f;
		}
		.gradualChange {
			background: linear-gradient(to right, #f3ef9f, #6ecac1);
		}
	}
}

.boxChart {
	display: flex;
	flex-direction: column;
	align-items: center;
	user-select: none;
	height: 100%;

	.circle-wrap {
		display: flex;
		width: 100%;
		margin-bottom: -10px;
	}

	.circle-style {
		width: 100%;
		height: 80px;
		padding: 8px;
		border-radius: 48%;
		border: 1px solid #242424;
		background-color: #fff2cc;
		text-align: center;
		word-wrap: break-word;
		font-size: 20px;
		font-weight: bold;
		display: flex;
		justify-content: center;
		align-items: center;
	}

	.diamond-style {
		display: flex;
		flex-direction: column;

		.diamond-top {
			border: 1px solid #242424;
			border-right: none;
			width: 50px;
			height: 28px;
			line-height: 28px;
			text-align: center;
		}

		.diamond-bottom {
			border: 1px solid #242424;
			border-right: none;
			width: 50px;
			height: 26px;
			line-height: 28px;
			text-align: center;
		}
	}

	.nodes {
		list-style: none;

		.outermost {
			display: flex;
			overflow: hidden;

			.outermost-line {
				.top-line {
					width: 10px;
					height: 35px;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}

				.bottom-line {
					width: 0;
					height: 5000%;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}
			}
		}

		.hierarchy {
			padding-bottom: 20px;
			overflow: hidden;

			.square-wrap {
				display: flex;
				padding-top: 20px;

				&:last-child {
					margin-bottom: 0;
				}
			}
		}

		.square-wrap {
			.line {
				margin-top: -21px;

				.top-line {
					width: 10px;
					height: 35px;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}

				.bottom-line {
					width: 0;
					height: 5000%;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}
			}
		}

		.nodes {
			padding-left: 60px;
		}
	}

	.square-style {
		display: flex;
		background-color: #fff;

		&-left {
			display: flex;
			flex-direction: column;

			&-top {
				border: 1px solid #242424;
				border-right: none;
				width: 50px;
				padding: 8px;
				text-align: center;
			}

			&-bottom {
				border: 1px solid #242424;
				border-right: none;
				border-top: none;
				width: 50px;
				padding: 8px;
				text-align: center;
			}
		}

		&-right {
			width: 200px;
			height: auto;
			text-align: center;

			&-top {
				border: 1px solid #242424;
				padding: 8px;
			}

			&-bottom {
				border: 1px solid #242424;
				border-top: none;
				padding: 8px 0;
				cursor: pointer;
			}
		}
	}
}

.boxChartTop {
	display: flex;
	flex-direction: column;
	align-items: center;
	user-select: none;
	padding-right: 32px;

	.circle-wrap {
		display: flex;
		width: 100%;
		margin-left: -40px;
	}

	.center-top {
		width: 100%;
		display: flex;
		align-items: center;
	}

	.top-vertical {
		flex: 1;
		width: 0;
		height: 100%;
		border-color: rgba(217, 83, 79, 0.8);
		border-style: solid;
		border-width: 0 0 2px 2px;
		margin-left: 42.8%;
	}

	.diamond-style {
		display: flex;
		flex-direction: column;

		.diamond-top {
			border: 1px solid #242424;
			border-right: none;
			width: 50px;
			height: 28px;
			line-height: 28px;
			text-align: center;
		}

		.diamond-bottom {
			border: 1px solid #242424;
			border-right: none;
			border-top: none;
			width: 50px;
			height: 28px;
			line-height: 28px;
			text-align: center;
		}
	}

	.nodes {
		list-style: none;
		padding-left: 60px !important;

		.outermost {
			display: flex;
			overflow: hidden;

			.outermost-line {
				.top-line {
					width: 10px;
					height: 35px;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}

				.top-line0 {
					width: 10px;
					height: 35px;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 0;
				}

				.bottom-line {
					width: 0;
					height: 5000%;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}
			}
		}

		.hierarchy {
			padding-bottom: 21px;
			overflow: hidden;

			.square-wrap {
				display: flex;
				padding-top: 20px;

				&:last-child {
					margin-bottom: 0;
				}
			}
		}

		.square-wrap {
			.line {
				margin-top: -21px;

				.top-line {
					width: 10px;
					height: 35px;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}

				.bottom-line {
					width: 0;
					height: 5000%;
					border-color: rgba(217, 83, 79, 0.8);
					border-style: solid;
					border-width: 0 0 2px 2px;
				}
			}
		}

		.nodes {
			padding-left: 60px;
			// display: flex;
		}
	}

	.center-line {
		width: 0;
		height: 20px;
		border-color: rgba(217, 83, 79, 0.8);
		border-style: solid;
		border-width: 0 0 2px 2px;
		margin-bottom: -20px;
	}

	.square-style {
		display: flex;
		background-color: #fff;

		&-left {
			display: flex;
			flex-direction: column;

			&-top {
				border: 1px solid #242424;
				border-right: none;
				width: 50px;
				padding: 8px;
				text-align: center;
			}

			&-bottom {
				border: 1px solid #242424;
				border-right: none;
				border-top: none;
				width: 50px;
				padding: 8px;
				text-align: center;
			}
		}

		&-right {
			width: 200px;
			height: auto;
			text-align: center;

			&-top {
				border: 1px solid #242424;
				padding: 8px;
			}

			&-bottom {
				border: 1px solid #242424;
				border-top: none;
				padding: 8px 0;
				cursor: pointer;
			}
		}
	}

	.top-right {
		margin-left: -41px;
	}
}

.top-box-html {
	display: flex;
	width: 100%;
	margin-left: 135px;
	.tox-box-diamond {
		width: 200px;
		height: auto;
		text-align: center;

		.diamond-title {
			border: 1px solid #242424;
			background-color: #4f5b72;
			color: #fff;
			padding: 4px;
			min-height: 28px;
		}

		.diamond-name {
			border: 1px solid #242424;
			border-top: none;
			padding: 4px 0;
			min-height: 28px;
			background-color: #fff;
		}
	}
}

.positionGreen {
	background-color: #6fc9c2;
}

.positionOrange {
	background-color: #f2f09f;
}

.gradualChange {
	background: linear-gradient(to right, #f3ef9f, #6ecac1);
}
</style>

G6不能使用绝对定位,所以处理起来很麻烦。代码写的很繁琐,我已经尽力了。我项目使用了多语言,所以有$t()。如果大家有更好得处理方式可以优化。美少女写这个头发掉了一大把。后面产品这块需要改版,看了大概好像更难,欲哭无泪。。。

大概是这样子,不是最终版本,主要是线得问题。G6不能使用绝对定位就很头大。我想着用纯HTML写但是不能放大缩小然后随便拖。Html 放大也只是把浏览器放大(要禁止)

相关推荐
lovingsoft6 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly16 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝20 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦21 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡42 分钟前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队1 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端