<!-- filename: ProcessGraph.vue author: liubin date: 2023-10-20 15:00:58 description: --> <template> <section class="process-graph"> <SearchBar :formConfigs="searchBarFormConfig" ref="search-bar" /> <div class="btns" style="text-align: right; position: absolute; top: 20px; right: 20px"> <el-button type="warning" @click="undo" plain v-if="allowUndo" :disabled="!allowUndo" icon="el-icon-back"> 撤销 </el-button> <el-button type="warning" @click="redo" plain v-if="allowRedo" :disabled="!allowRedo"> 下一步 <i class="el-icon-right el-icon--right"></i> </el-button> <el-button class="btn-refresh" @click="handleUpdateLayout" icon="el-icon-refresh"> 刷新布局</el-button> <el-button type="primary" plain class="btn-create" icon="el-icon-plus" @click="handleAdd"> 新建工序 </el-button> <el-button class="btn-edit" :disabled="currentDet == null" @click="handleEdit">编辑</el-button> </div> <div class="process-graph__panel" ref="panel"></div> <base-dialog :dialogTitle="title" :dialogVisible="open" width="35%" @close="cancel" @cancel="cancel" @confirm="submitForm"> <DialogForm v-if="open" ref="form" v-model="form" :rows="rows" /> </base-dialog> </section> </template> <script> import { Graph } from '@antv/x6'; import ProcessNode, { createProcessNode, CACHE_NAME, getSectionFrom } from './ProcessNode'; import DialogForm from '@/components/DialogForm'; // import { IdToName } from '@/utils' Graph.registerNode('process-node', ProcessNode,true); export default { name: 'ProcessGraph', components: { DialogForm }, props: {}, inject: ['getFlowId'], data() { return { allowRedo: false, allowUndo: false, graph: null, searchBarFormConfig: [{ label: '工序列表' }], title: '', open: false, form: { name: '', // 工序名称 sectionId: '', // 工段id remark: '', // 描述 }, rows: [ [ { input: true, label: '工序名称', prop: 'name', rules: [{ required: true, message: '不能为空', trigger: 'blur' }], }, ], [ { select: true, label: '工段', prop: 'sectionId', url: '/base/core-workshop-section/listAll', rules: [{ required: true, message: '不能为空', trigger: 'blur' }], bind: { filterable: true, }, cache: CACHE_NAME }, ], [ { textarea: true, label: '工序说明', prop: 'remark', }, ], ], updateUrl: '/extend/process-flow-det/update', deleteUrl: '/extend/process-flow-det/delete', addUrl: '/extend/process-flow-det/create', // pageUrl: '/extend/process-flow-det/get', infoUrl: '/extend/process-flow-view/getByFlowId', layout: { id: null, flowId: null, content: '', createTime: null }, currentDet: null, currentNode: null }; }, watch: { 'form.sectionId': { handler(id) { }, immediate: false, }, currentDet: { handler(val) { this.$emit('det-selected', val) }, deep: true, immediate: true } }, activated() { this.loadLayout().then(json => { this.initGraph(json) }) }, deactivated() { this.graph.dispose(); this.$nextTick(() => { this.resetLayout(); this.graph = null; }) }, computed: {}, methods: { initGraph(json) { const graph = new Graph({ container: this.$refs.panel, grid: { size: 10, visible: true, }, history: true, selecting: { className: 'my-select' }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowNode: false, allowPort: true, allowEdge: false, }, panning: true, // scroller: { // enabled: true, // pannable: true, // cursor: '', // width: 800, // height: 200 // }, mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'] } }); graph.fromJSON(json) this.graph = graph; this.$nextTick(() => { this.registerGraphEvents(); }) }, registerGraphEvents() { const reset = () => { const nodes = this.graph.getNodes(); const edges = this.graph.getEdges(); this.currentDet = null; this.currentNode = null; nodes.forEach(node => { node.attr('container/stroke', '#ccc'); }); edges.forEach(edge => { edge.attr('line/stroke', '#ccc') }) } this.graph.on('node:click', ({ e, x, y, node, view }) => { reset(); node.attr('container/stroke', '#0b58ff'); const { detId, detName, detDesc, processId, sectionId, sectionName } = node.attrs; this.currentDet = {} this.$set(this.currentDet, 'detId', detId.text) this.$set(this.currentDet, 'sectionId', sectionId.text) this.$set(this.currentDet, 'detName', detName.text) this.$set(this.currentDet, 'detDesc', detDesc.text) this.$set(this.currentDet, 'flowId', processId.text) this.$set(this.currentDet, 'sectionName', sectionName.text) this.currentNode = node }); this.graph.on('edge:click', ({ e, x, y, edge, view }) => { // console.log('edge clicked!', edge) reset(); edge.attr('line/stroke', '#0b58ff') }); this.graph.on('blank:click', ({ e, x, y }) => { reset(); }); this.graph.on('node:mouseenter', ({ node }) => { node.addTools({ name: 'button-remove', args: { x: '100%', y: 0, offset: { x: 0, y: 0 }, onClick: ({ e, cell, view }) => { this.$confirm( '确定删除这个工序吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ).then(async () => { const id = node.attrs.detId.text; const status = await this.handleDelete(id); if (status) { view.cell.remove(); } }).catch(err => { return; }) } } }) }); this.graph.on('node:mouseleave', ({ node }) => { node.removeTools(); }) }, resetLayout() { this.layout = { id: null, flowId: null, content: '', createTime: null } }, async loadLayout() { const flowId = this.$route.params.id; if (!flowId) return { cells: [] } const { code, data } = await this.info({ id: flowId }); if (code == 0) { if (data) { this.layout = data; return JSON.parse(data?.content) || { cells: [] }; } return { cells: [] }; } this.resetLayout(); return Promise.reject(this.infoUrl + ' 接口出错!'); }, handleToJson() { }, handleLoadJson() { }, handleDumpJson() { if (this.graph) { console.log(JSON.stringify(this.graph.toJSON(), null, 2)); } }, async handleUpdateLayout() { this.layout.content = JSON.stringify(this.graph.toJSON()); let code, data; console.table([this.layout, this.$route.params.id]) // 手动刷新布局 if (this.layout.id) { ({ code, data } = await this.http('/extend/process-flow-view/update', 'put', this.layout)); } else { this.layout.flowId = this.$route.params.id; ({ code, data } = await this.http('/extend/process-flow-view/create', 'post', this.layout)); } if (code == 0) { this.$modal.msgSuccess('布局已刷新!') } }, reset() { this.form = { name: '', // 工序名称 sectionId: '', // 工段id remark: '', // 描述 }; this.resetForm('form'); }, /** 取消按钮 */ cancel() { this.open = false; this.reset(); }, handleAdd() { this.reset(); this.open = true; this.title = '添加工序'; }, handleEdit() { this.form.name = this.currentDet.detName; this.form.sectionId = this.currentDet.sectionId; this.form.remark = this.currentDet.detDesc; this.form.id = this.currentDet.detId; this.title = '编辑工序'; this.$nextTick(() => { this.open = true; }) }, async handleDelete(id) { const { code, data } = await this.delete({ id }); debugger; if (code == 0) { this.$modal.msgSuccess('成功删除一个工序!'); return true; } return false; }, /** 提交按钮 */ submitForm() { this.$refs['form'].validate((valid) => { if (!valid) { return; } // 修改的提交 if (this.form.id != null) { this.updateProcess() .then((form) => { const { name, sectionId, remark } = form; getSectionFrom(sectionId).then(sectionName => { // 修改当前node的信息 this.currentNode.setAttrs({ detName: { text: name }, sectionId: { text: sectionId }, sectionName: { text: sectionName }, detDesc: { text: remark } }) }) }) .catch(err => { }); return; } this.createProcess() .then(({ id, name, sectionId, remark, flowId }) => { if (!id) return null; return createProcessNode({ flowId: flowId, name, sectionId, remark, id, }) }).then(node => { if (!node) { this.$modal.msgError('创建节点失败'); return; }; this.graph.addNode(node); }).catch(err => { return; }); }); }, updateProcess() { const flowId = this.getFlowId(); if (!flowId) { this.$modal.msgError('工艺ID不能为空'); return Promise.reject('工艺ID不能为空'); } return this.put({ flowId, ...this.form }) .then(({ code, data }) => { if (code == 0) { this.$modal.msgSuccess('修改成功'); } else { this.$modal.msgError('修改失败'); } const formCopy = { ...this.form } this.open = false; return formCopy; }); }, createProcess() { // const flowId = this.$route.params.id; const flowId = this.getFlowId(); // it also works if (!flowId) { this.$modal.msgError('工艺ID不能为空'); return Promise.reject('工艺ID不能为空'); } console.log('create process', this.form) // 添加的提交 return this.post({ flowId, ...this.form }).then( ({ code, data }) => { this.$modal.msgSuccess('新增成功'); this.open = false; // this.getList(); return { id: data, // 服务器返回的新建的工段id ...this.form, // 保存一份 this.form 副本,当 open->false 时 this.form 里的信息就清空了 flowId }; } ).catch(err => { this.$modal.msgError(err) }); }, put(payload) { return this.http(this.updateUrl, 'put', payload); }, post(payload) { return this.http(this.addUrl, 'post', payload); }, recv(payload) { return this.http(this.pageUrl, 'get', payload); }, info(payload) { return this.http(this.infoUrl, 'get', payload); }, delete({ id }) { return this.$axios({ url: this.deleteUrl + `?id=${id}`, method: 'delete', }); }, http(url, method, payload) { return this.$axios({ url, method, params: method === 'get' ? payload : null, data: method !== 'get' ? payload : null, }) }, }, }; </script> <style scoped lang="scss"> .process-graph { padding: 12px 20px 20px; background: #fff; border-radius: 8px; position: relative; } .process-graph__panel { height: 300px; } </style> <style> .x6-widget-selection-selected { border: 1px solid red; } .my-select { border: 1px solid red; } </style>