123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- <template>
- <div class="chart">
- <div class="chart-block">
- <div class="chart-ref" ref="ref_chart"></div>
- </div>
- <div class="tools">
- <div class="mini-map" ref="ref_miniMap"/>
- <div class="operations">
- <div class="zoom">
- <el-tooltip content="缩小" effect="light" placement="top" :show-arrow="false">
- <div class="__hover-bg" :class="{__disabled: state.zoom <= 0.25}" @click="graphZoom(state.zoom - 0.1)">
- <SvgIcon name="zoom-"/>
- </div>
- </el-tooltip>
- <el-dropdown :teleported="false" placement="top" :popper-options="{
- modifiers: [
- {
- name: 'offset',
- options: {
- offset: [0, 20],
- },
- },
- {
- name: 'arrow',
- },
- ],
- }">
- <div class="__hover-bg px-0.5">{{(state.zoom * 100).toFixed(0)}}%</div>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item @click="graphZoom(2)">200%</el-dropdown-item>
- <el-dropdown-item @click="graphZoom(1)">100%</el-dropdown-item>
- <el-dropdown-item @click="graphZoom(0.75)">75%</el-dropdown-item>
- <el-dropdown-item @click="graphZoom(0.5)">50%</el-dropdown-item>
- <el-dropdown-item divided @click="graphZoom(0)">自适应视图</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- <el-tooltip content="放大" effect="light" placement="top" :show-arrow="false">
- <div class="__hover-bg" :class="{__disabled: state.zoom >= 2}" @click="graphZoom(state.zoom + 0.1)">
- <SvgIcon name="zoom+"/>
- </div>
- </el-tooltip>
- </div>
- <div>
- <el-tooltip content="撤销" effect="light" placement="top" :show-arrow="false">
- <div class="__hover-bg" :class="{__disabled: !state.history.canUndo}" @click="onHistoryDo(-1)">
- <SvgIcon name="back"/>
- </div>
- </el-tooltip>
- <el-tooltip content="重做" effect="light" placement="top" :show-arrow="false">
- <div class="__hover-bg" :class="{__disabled: !state.history.canRedo}" @click="onHistoryDo(1)">
- <SvgIcon name="back" rotate="180"/>
- </div>
- </el-tooltip>
- <el-popover
- :show-arrow="false"
- :width="240"
- :popper-style="{
- padding: 0,
- }">
- <template #reference>
- <div class="__hover-bg">
- <SvgIcon name="history"/>
- </div>
- </template>
- <div class="">
- <div class="text-[18px] p-3 pb-1">变更历史</div>
- <div class="px-1">
- <template v-if="state.history.steps.length < 2">
- <div class="mt-2 h-[120px] flex flex-col justify-center items-center">
- <SvgIcon name="history" size="30" class="mb-3"/>
- 尚未更改任何内容
- </div>
- </template>
- <template v-else>
- <div class="mt-2 max-h-[300px] overflow-y-auto flex flex-col gap-1">
- <template v-for="(item, index) in state.history.steps">
- <div :class="`__hover-bg px-2 py-1.5 rounded-md text-[13px] ${state.history.current === index && 'bg-[#c8ceda33]'}`" @click="onHistoryDo(state.history.current - index)">
- {{MapGraphHistoryStep.get(item)}}
- (
- <template v-if="state.history.current < index">{{index - state.history.current}} 步后退</template>
- <template v-else-if="state.history.current > index">{{state.history.current - index}} 步前进</template>
- <template v-else>当前状态</template>
- )
- </div>
- </template>
- </div>
- <div class="bg-[#eaecf0] w-full h-[1px] my-2"/>
- <div class="__hover-bg px-2 py-1.5 rounded-md mb-2 text-[13px]" @click="onHistoryClear(state.history.steps[0])">清空历史记录</div>
- </template>
- </div>
- <div class="text-[12px] text-[#676f83] p-3">
- 提示<br/>
- 您的编辑操作将被跟踪并存储在您的设备上,直到您离开编辑器。此历史记录将在您离开编辑器时被清除。
- </div>
- </div>
- </el-popover>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import {createVNode, getCurrentInstance, nextTick, onMounted, reactive, ref, render, watch} from "vue";
- import {Graph, Path, Shape} from '@antv/x6'
- import {getNodeDefault, lineStyle} from "@/views/workflow/config";
- import nodeAdd from './node-add.vue'
- import {register} from "@antv/x6-vue-shape";
- import WorkflowNode from "./node-index.vue";
- import {handleEdge, handleNode} from "@/views/workflow/handle";
- import {Snapline} from '@antv/x6-plugin-snapline'
- import {ContextMenuTool} from "./context-menu-tool";
- import {useWorkflowStore} from "@/stores/modules/workflow";
- import {GraphHistoryStep, MapGraphHistoryStep, NodeType} from "@/views/workflow/types";
- import { MiniMap } from '@antv/x6-plugin-minimap'
- import { History } from '@antv/x6-plugin-history'
- import {v4} from "uuid";
- register({
- shape: 'workflow-node',
- component: WorkflowNode,
- })
- let edgeTimer: any = null
- Graph.registerPortLayout('start', (portsPositionArgs, elemBBox) => {
- return portsPositionArgs.map((_, index) => {
- return {
- position: {
- x: 3,
- y: 25 + 7,
- },
- }
- })
- })
- Graph.registerPortLayout('end', (portsPositionArgs, elemBBox) => {
- if (edgeTimer) {
- clearTimeout(edgeTimer)
- }
- edgeTimer = setTimeout(() => {
- initEdges()
- }, 100)
- return portsPositionArgs.map((_, index) => {
- return {
- position: {
- x: elemBBox.width + 11,
- y: 25 + 7,
- },
- }
- })
- })
- Graph.registerPortLayout('more', (portsPositionArgs, elemBBox) => {
- return portsPositionArgs.map((_, index) => {
- nextTick(() => {
- WorkflowStore.layoutPort(_.nodeId, _.portId)
- if (edgeTimer) {
- clearTimeout(edgeTimer)
- }
- edgeTimer = setTimeout(() => {
- initEdges()
- }, 100)
- })
- return {
- position: {
- x: elemBBox.width + 11,
- y: _.dy,
- },
- }
- })
- })
- Graph.registerConnector(
- 'algo-connector',
- (s, e) => {
- const offset = 6
- const deltaX = Math.abs(e.x - s.x)
- const control = Math.floor((deltaX / 3) * 2)
- const v1 = { x: s.x + offset + control, y: s.y }
- const v2 = { x: e.x - offset - control, y: e.y }
- return Path.normalize(
- `M ${s.x} ${s.y}
- L ${s.x} ${s.y}
- C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y}
- L ${e.x} ${e.y}
- `,
- )
- },
- true,
- )
- Graph.registerNodeTool('contextmenu', ContextMenuTool, true)
- Graph.registerEdgeTool('contextmenu', ContextMenuTool, true)
- const WorkflowStore = useWorkflowStore()
- const emits = defineEmits([])
- const props = defineProps({
- data: <any>{}
- })
- const {proxy}: any = getCurrentInstance()
- const state: any = reactive({
- graph: null,
- isPort: false,
- isInitEdges: false,
- zoom: 1,
- history: {
- canUndo: false,
- canRedo: false,
- steps: [],
- current: 0
- }
- })
- const ref_chart = ref()
- const ref_miniMap = ref()
- const initChart = () => {
- state.graph = new Graph({
- container: ref_chart.value,
- autoResize: true,
- panning: true,
- mousewheel: {
- enabled: true,
- factor: 1.05
- },
- scaling: {
- min: 0.25,
- max: 2
- },
- grid: {
- visible: true,
- type: 'doubleMesh',
- args: [
- {
- color: 'rgba(238,238,238,0.3)', // 主网格线颜色
- thickness: 1, // 主网格线宽度
- },
- {
- color: 'rgba(221,221,221,0.3)', // 次网格线颜色
- thickness: 1, // 次网格线宽度
- factor: 2, // 主次网格线间隔
- },
- ],
- },
- highlighting: {
- // 连接桩可以被连接时在连接桩外围围渲染一个包围框
- magnetAvailable: {
- name: 'stroke',
- args: {
- rx: 100,
- ry: 100,
- attrs: {
- fill: '#fff',
- stroke: 'rgba(var(--czr-main-color-rgb), 0.5)',
- 'stroke-width': 3,
- },
- },
- },
- // 连接桩吸附连线时在连接桩外围围渲染一个包围框
- magnetAdsorbed: {
- name: 'stroke',
- args: {
- rx: 100,
- ry: 100,
- attrs: {
- fill: '#fff',
- stroke: 'rgba(var(--czr-main-color-rgb), 1)',
- 'stroke-width': 3,
- },
- },
- },
- },
- connecting: {
- snap: { // 连线过程中自动吸附
- radius: 20,
- },
- allowBlank: false, // 是否允许连接到画布空白位置的点
- allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点
- allowNode: false, // 是否允许边连接到节点
- allowEdge: false, // 是否允许边链接到另一个边
- allowPort: ({sourcePort, targetPort}: any) => { // 是否允许边链接到连接桩
- return !sourcePort?.includes('start') && targetPort?.includes('start')
- },
- allowMulti: 'withPort', // 当设置为 'withPort' 时,在起始和终止节点的相同连接桩之间只允许创建一条边(即,起始和终止节点之间可以创建多条边,但必须要要链接在不同的连接桩上)
- highlight: true,
- // router: {
- // name: 'manhattan',
- // args: {
- // startDirections: ['right'],
- // endDirections: ['left'],
- // },
- // },
- connector: 'algo-connector',
- connectionPoint: 'anchor',
- anchor: 'center',
- // connector: {
- // name: 'rounded',
- // args: {
- // radius: 20,
- // },
- // },
- createEdge: (args) => {
- const id = v4()
- return new Shape.Edge({
- id,
- attrs: lineStyle,
- zIndex: -1,
- tools: [
- {
- name: 'contextmenu',
- args: {
- delFlag: GraphHistoryStep.EdgeDel,
- data: {id},
- }
- }
- ]
- })
- }
- },
- onPortRendered: (args: any) => {
- const selectors = args.contentSelectors
- const container = selectors && selectors.foContent
- if (container) {
- render(createVNode(nodeAdd, {
- port: args.port,
- node: args.node,
- graph: state.graph,
- }), container)
- }
- }
- })
- WorkflowStore.init(state.graph)
- initPlug()
- initWatch()
- initNodes()
- graphZoom(0)
- }
- const initNodes = () => {
- props.data.nodes.forEach(v => {
- state.graph.addNode(handleNode(v))
- })
- if (props.data.nodes.length === 0) {
- const node = getNodeDefault(NodeType.Root)
- state.graph.addNode(handleNode(node))
- }
- }
- const initEdges = () => {
- if (!state.isInitEdges) {
- props.data.edges.forEach(v => {
- const targetNode = state.graph.getCellById(v.target)
- targetNode.setData({
- edgeSource: v.port || v.source
- }, {deep: false})
- state.graph.addEdge(handleEdge(v))
- })
- state.isInitEdges = true
- onHistoryClear()
- }
- }
- const initWatch = () => {
- state.graph.on('node:click', ({ e, x, y, node, view, port }) => {
- setTimeout(() => {
- if (!state.isPort) {
- WorkflowStore.nodePanel(node)
- }
- }, 50)
- })
- state.graph.on('node:port:click', ({ e, x, y, node, view, port }) => {
- state.isPort = true;
- setTimeout(() => {
- state.isPort = false
- }, 100)
- })
- state.graph.on('scale', ({ sx, sy, ox, oy }) => {
- state.zoom = sx
- })
- state.graph.on('history:change', ({cmds, options}) => {
- if (state.isInitEdges) {
- state.history.canRedo = state.graph.canRedo()
- state.history.canUndo = state.graph.canUndo()
- if (options.name) {
- state.history.steps.unshift(options.name)
- }
- }
- })
- }
- const initPlug = () => {
- state.graph.use(
- new Snapline({
- enabled: true,
- clean: false,
- }),
- )
- state.graph.use(
- new MiniMap({
- container: ref_miniMap.value,
- width: 100,
- height: 70,
- padding: 6
- }),
- )
- state.graph.use(
- new History({
- enabled: true,
- beforeAddCommand: (event, args: any) => {
- if (
- args.key === 'ports' && args.options.propertyPath.includes('ports/items') ||
- args.key === 'attrs'
- ) {
- return false
- }
- }
- }),
- )
- }
- const graphZoom = (z) => {
- if (z) {
- state.graph.zoomTo(z)
- } else {
- state.graph.zoomToFit({ maxScale: 1 })
- }
- }
- const onHistoryDo = (step) => {
- if (step > 0) {
- for (let i = 1; i <= step; i++) {
- if (state.history.canRedo) {
- state.graph.redo()
- state.history.current -= 1
- }
- }
- } else {
- for (let i = -1; i >= step; i--) {
- if (state.history.canUndo) {
- state.graph.undo()
- state.history.current += 1
- }
- }
- }
- }
- const onHistoryClear = (first = '') => {
- state.graph.cleanHistory() // 初始化完成后清空历史队列
- state.history.steps = [first || GraphHistoryStep.Root]
- state.history.current = 0
- }
- watch(() => props.data, (n) => {
- if (n) {
- initChart()
- }
- }, {immediate: true})
- onMounted(() => {
- })
- defineExpose({
- toJSON: () => state.graph.toJSON()
- })
- </script>
- <style lang="scss" scoped>
- .chart {
- width: 100%;
- height: 100%;
- background-color: #f2f4f7;
- position: relative;
- .chart-block {
- width: 100%;
- height: 100%;
- .chart-ref {
- width: 100%;
- height: 100%;
- }
- }
- .tools {
- position: absolute;
- left: 20px;
- bottom: 20px;
- z-index: 2;
- .mini-map {
- position: absolute;
- bottom: calc(100% + 10px);
- }
- :deep(.operations) {
- display: flex;
- align-items: center;
- font-size: 14px;
- color: #676f83;
- gap: 10px;
- .el-popper__arrow {
- display: none;
- }
- >div {
- display: flex;
- align-items: center;
- background-color: rgba(255, 255, 255, 0.95);
- box-shadow: 0 0px 8px rgba(0, 0, 0, 0.1);
- border-radius: 6px;
- padding: 4px;
- >div {
- min-width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- }
- }
- }
- }
- </style>
|