|
|
@@ -0,0 +1,283 @@
|
|
|
+<template>
|
|
|
+ <div ref="containerRef" class="org-chart-container" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
|
+import { Graph, Node, Point } from '@antv/x6'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'OrgChart',
|
|
|
+ setup() {
|
|
|
+ const containerRef = ref(null)
|
|
|
+ let graph = null
|
|
|
+
|
|
|
+ // 注册节点和边的类型
|
|
|
+ const registerChartElements = () => {
|
|
|
+ Graph.registerNode(
|
|
|
+ 'org-node',
|
|
|
+ {
|
|
|
+ width: 180,
|
|
|
+ height: 60,
|
|
|
+ markup: [
|
|
|
+ {
|
|
|
+ tagName: 'rect',
|
|
|
+ selector: 'body',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ tagName: 'image',
|
|
|
+ selector: 'avatar',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ tagName: 'text',
|
|
|
+ selector: 'rank',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ tagName: 'text',
|
|
|
+ selector: 'name',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ attrs: {
|
|
|
+ body: {
|
|
|
+ refWidth: '100%',
|
|
|
+ refHeight: '100%',
|
|
|
+ fill: '#5F95FF',
|
|
|
+ stroke: '#5F95FF',
|
|
|
+ strokeWidth: 1,
|
|
|
+ rx: 10,
|
|
|
+ ry: 10,
|
|
|
+ pointerEvents: 'visiblePainted',
|
|
|
+ },
|
|
|
+ avatar: {
|
|
|
+ width: 48,
|
|
|
+ height: 48,
|
|
|
+ refX: 8,
|
|
|
+ refY: 6,
|
|
|
+ },
|
|
|
+ rank: {
|
|
|
+ refX: 0.9,
|
|
|
+ refY: 0.2,
|
|
|
+ fill: '#fff',
|
|
|
+ fontFamily: 'Courier New',
|
|
|
+ fontSize: 14,
|
|
|
+ textAnchor: 'end',
|
|
|
+ textDecoration: 'underline',
|
|
|
+ },
|
|
|
+ name: {
|
|
|
+ refX: 0.9,
|
|
|
+ refY: 0.6,
|
|
|
+ fill: '#fff',
|
|
|
+ fontFamily: 'Courier New',
|
|
|
+ fontSize: 14,
|
|
|
+ fontWeight: '800',
|
|
|
+ textAnchor: 'end',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ true
|
|
|
+ )
|
|
|
+
|
|
|
+ Graph.registerEdge(
|
|
|
+ 'org-edge',
|
|
|
+ {
|
|
|
+ zIndex: -1,
|
|
|
+ attrs: {
|
|
|
+ line: {
|
|
|
+ fill: 'none',
|
|
|
+ strokeLinejoin: 'round',
|
|
|
+ strokeWidth: 2,
|
|
|
+ stroke: '#A2B1C3',
|
|
|
+ sourceMarker: null,
|
|
|
+ targetMarker: null,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ true
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建节点
|
|
|
+ const createNode = (x, y, rank, name, image) => {
|
|
|
+ return graph.addNode({
|
|
|
+ x,
|
|
|
+ y,
|
|
|
+ shape: 'org-node',
|
|
|
+ attrs: {
|
|
|
+ avatar: {
|
|
|
+ opacity: 0.7,
|
|
|
+ // 'xlink:href': image,
|
|
|
+ },
|
|
|
+ rank: {
|
|
|
+ text: rank,
|
|
|
+ wordSpacing: '-5px',
|
|
|
+ letterSpacing: 0,
|
|
|
+ },
|
|
|
+ name: {
|
|
|
+ text: name,
|
|
|
+ fontSize: 13,
|
|
|
+ fontFamily: 'Arial',
|
|
|
+ letterSpacing: 0,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // 禁用节点拖动
|
|
|
+ movable: false,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建连线
|
|
|
+ const createLink = (source, target, vertices) => {
|
|
|
+ return graph.addEdge({
|
|
|
+ vertices,
|
|
|
+ source: {
|
|
|
+ cell: source,
|
|
|
+ },
|
|
|
+ target: {
|
|
|
+ cell: target,
|
|
|
+ },
|
|
|
+ shape: 'org-edge',
|
|
|
+ // 禁用连线拖动
|
|
|
+ connector: {
|
|
|
+ name: 'normal',
|
|
|
+ args: {
|
|
|
+ style: {
|
|
|
+ pointerEvents: 'none',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // 禁用连线调整
|
|
|
+ router: {
|
|
|
+ name: 'manhattan',
|
|
|
+ args: {
|
|
|
+ padding: 10,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ attrs: {
|
|
|
+ line: {
|
|
|
+ // 禁止选中连线
|
|
|
+ pointerEvents: 'none',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化图表
|
|
|
+ const initChart = () => {
|
|
|
+ if (!containerRef.value) return
|
|
|
+
|
|
|
+ // 创建图表实例
|
|
|
+ graph = new Graph({
|
|
|
+ container: containerRef.value,
|
|
|
+ connecting: {
|
|
|
+ anchor: 'orth',
|
|
|
+ },
|
|
|
+ // 禁用鼠标滚轮缩放
|
|
|
+ mousewheel: {
|
|
|
+ enabled: true,
|
|
|
+ },
|
|
|
+ // 禁用平移
|
|
|
+ panning: true,
|
|
|
+ // 禁用框选
|
|
|
+ selecting: {
|
|
|
+ enabled: false,
|
|
|
+ },
|
|
|
+ interacting: {
|
|
|
+ nodeMovable: false,
|
|
|
+ edgeMovable: false,
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ // 图表数据
|
|
|
+ const male =
|
|
|
+ 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*kUy8SrEDp6YAAAAAAAAAAAAAARQnAQ'
|
|
|
+ const female =
|
|
|
+ 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*f6hhT75YjkIAAAAAAAAAAAAAARQnAQ'
|
|
|
+
|
|
|
+ // 创建节点
|
|
|
+ const bart = createNode(300, 70, 'CEO', 'Bart Simpson', male)
|
|
|
+ const homer = createNode(90, 200, 'VP Marketing', 'Homer Simpson', male)
|
|
|
+ const marge = createNode(300, 200, 'VP Sales', 'Marge Simpson', female)
|
|
|
+ const lisa = createNode(500, 200, 'VP Production', 'Lisa Simpson', female)
|
|
|
+ const maggie = createNode(400, 350, 'Manager', 'Maggie Simpson', female)
|
|
|
+ const lenny = createNode(190, 350, 'Manager', 'Lenny Leonard', male)
|
|
|
+ const carl = createNode(190, 500, 'Manager', 'Carl Carlson', male)
|
|
|
+
|
|
|
+ // 创建连线
|
|
|
+ createLink(bart, marge, [
|
|
|
+ {
|
|
|
+ x: 385,
|
|
|
+ y: 180,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ createLink(bart, homer, [
|
|
|
+ {
|
|
|
+ x: 385,
|
|
|
+ y: 180,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ x: 175,
|
|
|
+ y: 180,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ createLink(bart, lisa, [
|
|
|
+ {
|
|
|
+ x: 385,
|
|
|
+ y: 180,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ x: 585,
|
|
|
+ y: 180,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ createLink(homer, lenny, [
|
|
|
+ {
|
|
|
+ x: 175,
|
|
|
+ y: 380,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ createLink(homer, carl, [
|
|
|
+ {
|
|
|
+ x: 175,
|
|
|
+ y: 530,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+ createLink(marge, maggie, [
|
|
|
+ {
|
|
|
+ x: 385,
|
|
|
+ y: 380,
|
|
|
+ },
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 调整视图以适应所有节点
|
|
|
+ graph.zoomToFit({ padding: 20, maxScale: 1 })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 组件挂载后初始化图表
|
|
|
+ onMounted(() => {
|
|
|
+ registerChartElements()
|
|
|
+ initChart()
|
|
|
+ })
|
|
|
+
|
|
|
+ // 组件卸载前清理图表
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ if (graph) {
|
|
|
+ graph.dispose()
|
|
|
+ graph = null
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return {
|
|
|
+ containerRef,
|
|
|
+ }
|
|
|
+ },
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.org-chart-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 600px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+</style>
|