Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory_web
This commit is contained in:
commit
349a650171
|
|
@ -188,6 +188,12 @@ export default {
|
||||||
return await http.get( `${config.API_URL}/wf/ticket/${id}/transitions/`);
|
return await http.get( `${config.API_URL}/wf/ticket/${id}/transitions/`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ticketAvailableActions: {
|
||||||
|
name: "获取当前用户可执行的工单操作",
|
||||||
|
req: async function(id){
|
||||||
|
return await http.get(`${config.API_URL}/wf/ticket/${id}/available_actions/`);
|
||||||
|
}
|
||||||
|
},
|
||||||
dutyAgg: {
|
dutyAgg: {
|
||||||
url: `${config.API_URL}/wf/ticket/duty_agg/`,
|
url: `${config.API_URL}/wf/ticket/duty_agg/`,
|
||||||
name: "待办审批聚合",
|
name: "待办审批聚合",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<BaseEdge
|
||||||
|
:id="id"
|
||||||
|
:path="edgePath"
|
||||||
|
:marker-end="markerEnd"
|
||||||
|
:style="style"
|
||||||
|
:interaction-width="20"
|
||||||
|
/>
|
||||||
|
<EdgeLabelRenderer v-if="label">
|
||||||
|
<div
|
||||||
|
class="dagre-edge-label nodrag nopan"
|
||||||
|
:style="{
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelPoint.x}px, ${labelPoint.y}px)`,
|
||||||
|
color: labelStyle && labelStyle.fill,
|
||||||
|
fontSize: labelStyle && labelStyle.fontSize,
|
||||||
|
fontWeight: labelStyle && labelStyle.fontWeight,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { BaseEdge, EdgeLabelRenderer } from "@vue-flow/core";
|
||||||
|
import { curveCatmullRom, line } from "d3";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DagreEdge",
|
||||||
|
components: { BaseEdge, EdgeLabelRenderer },
|
||||||
|
props: {
|
||||||
|
id: { type: String, required: true },
|
||||||
|
sourceX: { type: Number, required: true },
|
||||||
|
sourceY: { type: Number, required: true },
|
||||||
|
targetX: { type: Number, required: true },
|
||||||
|
targetY: { type: Number, required: true },
|
||||||
|
data: { type: Object, default: () => ({}) },
|
||||||
|
label: { type: [String, Number], default: "" },
|
||||||
|
markerEnd: { type: String, default: "" },
|
||||||
|
style: { type: Object, default: () => ({}) },
|
||||||
|
labelStyle: { type: Object, default: () => ({}) },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
points() {
|
||||||
|
const routed = this.data.dagrePoints || [];
|
||||||
|
const points = [
|
||||||
|
{ x: this.sourceX, y: this.sourceY },
|
||||||
|
...routed.slice(1, -1),
|
||||||
|
{ x: this.targetX, y: this.targetY },
|
||||||
|
];
|
||||||
|
const label = this.data.dagreLabel;
|
||||||
|
if (!label || points.some((point) => Math.abs(point.x - label.x) < 1 && Math.abs(point.y - label.y) < 1)) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
// TB 工艺图按 y 单调前进,把标签中心插入对应线段,确保线条从标签正中穿过。
|
||||||
|
const insertAt = points.findIndex((point, index) => index > 0 && point.y >= label.y);
|
||||||
|
points.splice(insertAt > 0 ? insertAt : Math.ceil(points.length / 2), 0, label);
|
||||||
|
return points;
|
||||||
|
},
|
||||||
|
edgePath() {
|
||||||
|
return (
|
||||||
|
line()
|
||||||
|
.x((point) => point.x)
|
||||||
|
.y((point) => point.y)
|
||||||
|
.curve(curveCatmullRom.alpha(0.8))(this.points) || ""
|
||||||
|
);
|
||||||
|
},
|
||||||
|
labelPoint() {
|
||||||
|
if (this.data.dagreLabel) return this.data.dagreLabel;
|
||||||
|
const middle = this.points[Math.floor(this.points.length / 2)];
|
||||||
|
return middle || { x: (this.sourceX + this.targetX) / 2, y: (this.sourceY + this.targetY) / 2 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dagre-edge-label {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 190px;
|
||||||
|
padding: 5px 9px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 1px 3px rgba(31, 45, 61, 0.08);
|
||||||
|
line-height: 1.25;
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,9 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mnode" :class="'mnode--' + (data.kind || 'mid')">
|
<div class="mnode" :class="'mnode--' + (data.kind || 'mid')">
|
||||||
<Handle type="target" :position="Position.Top" class="mnode__handle" />
|
<Handle type="target" :position="Position.Top" class="mnode__handle" />
|
||||||
|
<Handle
|
||||||
|
v-for="port in data.targetPorts || []"
|
||||||
|
:key="port.id"
|
||||||
|
:id="port.id"
|
||||||
|
type="target"
|
||||||
|
:position="Position.Top"
|
||||||
|
:style="{ left: port.left + '%' }"
|
||||||
|
class="mnode__handle mnode__route-handle"
|
||||||
|
/>
|
||||||
<span class="mnode__tag">{{ kindLabel }}</span>
|
<span class="mnode__tag">{{ kindLabel }}</span>
|
||||||
<div class="mnode__name" :title="data.label">{{ data.label }}</div>
|
<div class="mnode__name" :title="data.label">{{ data.label }}</div>
|
||||||
<Handle type="source" :position="Position.Bottom" class="mnode__handle" />
|
<Handle type="source" :position="Position.Bottom" class="mnode__handle" />
|
||||||
|
<Handle
|
||||||
|
v-for="port in data.sourcePorts || []"
|
||||||
|
:key="port.id"
|
||||||
|
:id="port.id"
|
||||||
|
type="source"
|
||||||
|
:position="Position.Bottom"
|
||||||
|
:style="{ left: port.left + '%' }"
|
||||||
|
class="mnode__handle mnode__route-handle"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -64,6 +82,10 @@ export default {
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.mnode__route-handle {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.mnode--raw {
|
.mnode--raw {
|
||||||
border-color: #f0c78a;
|
border-color: #f0c78a;
|
||||||
background: #fffaf2;
|
background: #fffaf2;
|
||||||
|
|
@ -95,12 +117,29 @@ export default {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.vue-flow__handle {
|
.vue-flow__handle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.vue-flow__handle::after {
|
||||||
|
content: "";
|
||||||
width: 9px;
|
width: 9px;
|
||||||
height: 9px;
|
height: 9px;
|
||||||
background: #fff;
|
|
||||||
border: 2px solid #409eff;
|
border: 2px solid #409eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
transition: width 0.15s, height 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
.vue-flow__handle:hover {
|
.vue-flow__handle:hover::after {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
background: #409eff;
|
background: #409eff;
|
||||||
|
box-shadow: 0 0 0 4px rgba(64, 158, 255, 0.18);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,114 @@ export const NODE_H = 54;
|
||||||
* dagre 自动布局,返回带 position 的 nodes
|
* dagre 自动布局,返回带 position 的 nodes
|
||||||
* @param {Array} nodes Vue Flow 节点(id 必须为字符串)
|
* @param {Array} nodes Vue Flow 节点(id 必须为字符串)
|
||||||
* @param {Array} edges Vue Flow 边
|
* @param {Array} edges Vue Flow 边
|
||||||
* @param {Object} opts { rankdir: TB|BT|LR|RL, nodeW, nodeH }
|
* @param {Object} opts { rankdir: TB|BT|LR|RL, nodeW, nodeH, nodesep, edgesep, ranksep, marginx, marginy }
|
||||||
*/
|
*/
|
||||||
export function layoutGraph(nodes, edges, opts = {}) {
|
export function layoutGraph(nodes, edges, opts = {}) {
|
||||||
const { rankdir = "TB", nodeW = NODE_W, nodeH = NODE_H } = opts;
|
return layoutGraphWithEdges(nodes, edges, opts).nodes;
|
||||||
const g = new dagre.graphlib.Graph();
|
}
|
||||||
g.setGraph({ rankdir, nodesep: 30, ranksep: 56, marginx: 20, marginy: 20 });
|
|
||||||
|
/**
|
||||||
|
* dagre 同时计算节点位置、边的绕行点和标签位置。
|
||||||
|
* Vue Flow 默认边只使用节点坐标,会让跨层边穿过节点;因此查看/编辑视图使用此完整结果。
|
||||||
|
*/
|
||||||
|
export function layoutGraphWithEdges(nodes, edges, opts = {}) {
|
||||||
|
const {
|
||||||
|
rankdir = "TB",
|
||||||
|
nodeW = NODE_W,
|
||||||
|
nodeH = NODE_H,
|
||||||
|
nodesep = 30,
|
||||||
|
edgesep = 20,
|
||||||
|
ranksep = 56,
|
||||||
|
marginx = 20,
|
||||||
|
marginy = 20,
|
||||||
|
} = opts;
|
||||||
|
const g = new dagre.graphlib.Graph({ multigraph: true });
|
||||||
|
g.setGraph({ rankdir, nodesep, edgesep, ranksep, marginx, marginy });
|
||||||
g.setDefaultEdgeLabel(() => ({}));
|
g.setDefaultEdgeLabel(() => ({}));
|
||||||
nodes.forEach((n) => g.setNode(n.id, { width: nodeW, height: nodeH }));
|
nodes.forEach((n) => g.setNode(n.id, { width: nodeW, height: nodeH }));
|
||||||
edges.forEach((e) => g.setEdge(e.source, e.target));
|
const parallelCount = new Map();
|
||||||
|
const edgeRefs = [];
|
||||||
|
edges.forEach((e, index) => {
|
||||||
|
const key = `${e.source}->${e.target}`;
|
||||||
|
const parallelIndex = parallelCount.get(key) || 0;
|
||||||
|
parallelCount.set(key, parallelIndex + 1);
|
||||||
|
// 参考原 dagre-d3:基础边跨 2 个 rank,同组边逐条增加长度。
|
||||||
|
const minlen = e.layoutMinlen || 2 + parallelIndex;
|
||||||
|
const name = `route-${index}`;
|
||||||
|
const labelWidth = e.label ? Math.min(190, Math.max(56, String(e.label).length * 13 + 20)) : 0;
|
||||||
|
g.setEdge(
|
||||||
|
e.source,
|
||||||
|
e.target,
|
||||||
|
{
|
||||||
|
minlen,
|
||||||
|
width: labelWidth,
|
||||||
|
height: e.label ? 30 : 0,
|
||||||
|
// 标签位于工序线本身;平行边已由独立锚点和 dagre edgesep 分开。
|
||||||
|
labelpos: "c",
|
||||||
|
labeloffset: 0,
|
||||||
|
},
|
||||||
|
name
|
||||||
|
);
|
||||||
|
edgeRefs.push({ v: e.source, w: e.target, name });
|
||||||
|
});
|
||||||
dagre.layout(g);
|
dagre.layout(g);
|
||||||
return nodes.map((n) => {
|
const laidOutNodes = nodes.map((n) => {
|
||||||
const gn = g.node(n.id);
|
const gn = g.node(n.id);
|
||||||
return { ...n, position: { x: gn.x - nodeW / 2, y: gn.y - nodeH / 2 } };
|
return { ...n, position: { x: gn.x - nodeW / 2, y: gn.y - nodeH / 2 } };
|
||||||
});
|
});
|
||||||
|
const laidOutEdges = edges.map((edge, index) => {
|
||||||
|
const ge = g.edge(edgeRefs[index]) || {};
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
data: {
|
||||||
|
...edge.data,
|
||||||
|
dagrePoints: ge.points || [],
|
||||||
|
dagreLabel: Number.isFinite(ge.x) && Number.isFinite(ge.y) ? { x: ge.x, y: ge.y } : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { nodes: laidOutNodes, edges: laidOutEdges };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为每条边分配独立的节点锚点,避免多条工序共用中心 Handle 产生重叠。
|
||||||
|
* 同一节点上的锚点按相邻节点横坐标排序,可同时减少分支交叉。
|
||||||
|
*/
|
||||||
|
export function assignEdgePorts(nodes, edges) {
|
||||||
|
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
||||||
|
const outgoing = new Map();
|
||||||
|
const incoming = new Map();
|
||||||
|
|
||||||
|
const routedEdges = edges.map((edge, index) => {
|
||||||
|
const sourceHandle = `route-out-${index}`;
|
||||||
|
const targetHandle = `route-in-${index}`;
|
||||||
|
if (!outgoing.has(edge.source)) outgoing.set(edge.source, []);
|
||||||
|
if (!incoming.has(edge.target)) incoming.set(edge.target, []);
|
||||||
|
outgoing.get(edge.source).push({ edgeIndex: index, handleId: sourceHandle, neighborId: edge.target });
|
||||||
|
incoming.get(edge.target).push({ edgeIndex: index, handleId: targetHandle, neighborId: edge.source });
|
||||||
|
return { ...edge, sourceHandle, targetHandle };
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCenterX = (id) => {
|
||||||
|
const node = nodeMap.get(id);
|
||||||
|
return node ? node.position.x + NODE_W / 2 : 0;
|
||||||
|
};
|
||||||
|
const makePorts = (items = []) =>
|
||||||
|
[...items]
|
||||||
|
.sort((a, b) => getCenterX(a.neighborId) - getCenterX(b.neighborId) || a.edgeIndex - b.edgeIndex)
|
||||||
|
.map((item, index, list) => ({
|
||||||
|
id: item.handleId,
|
||||||
|
left: ((index + 1) * 100) / (list.length + 1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const routedNodes = nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
sourcePorts: makePorts(outgoing.get(node.id)),
|
||||||
|
targetPorts: makePorts(incoming.get(node.id)),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { nodes: routedNodes, edges: routedEdges };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<el-table v-bind="$attrs" :data="tableData" :row-key="rowKey || undefined" :key="toggleIndex" ref="scTable"
|
<el-table v-bind="$attrs" :data="tableData" :row-key="rowKey || undefined" :key="toggleIndex" ref="scTable"
|
||||||
:height="height == 'auto' ? null : '100%'" :size="config.size" :border="config.border" :stripe="config.stripe"
|
:height="height == 'auto' ? null : '100%'" :size="config.size" :border="config.border" :stripe="config.stripe"
|
||||||
:summary-method="remoteSummary ? remoteSummaryMethod : summaryMethod" :expand-row-keys="rowKey ? expandRowKeys : undefined" @sort-change="sortChange"
|
:summary-method="remoteSummary ? remoteSummaryMethod : summaryMethod" :expand-row-keys="rowKey ? expandRowKeys : undefined" @sort-change="sortChange"
|
||||||
@filter-change="filterChange" @selection-change="selectionChange">
|
@filter-change="filterChange" @selection-change="selectionChange" @expand-change="handleExpandChange">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<template v-for="(item, index) in userColumn" :key="index">
|
<template v-for="(item, index) in userColumn" :key="index">
|
||||||
<el-table-column v-if="!item.hide" :column-key="item.prop" :label="item.label" :prop="item.prop"
|
<el-table-column v-if="!item.hide" :column-key="item.prop" :label="item.label" :prop="item.prop"
|
||||||
|
|
@ -563,16 +563,19 @@ export default {
|
||||||
},
|
},
|
||||||
// 控制展开/收起所有行
|
// 控制展开/收起所有行
|
||||||
toggleExpandAll() {
|
toggleExpandAll() {
|
||||||
const isExpandAll = this.expandRowKeys.length === this.tableData.length;
|
const rows = this.tableData || [];
|
||||||
|
const key = this.rowKey || "id";
|
||||||
|
const isExpandAll = rows.length > 0 && rows.every(item => this.expandRowKeys.includes(item[key]));
|
||||||
if (isExpandAll) {
|
if (isExpandAll) {
|
||||||
this.expandRowKeys = []; // 收起所有
|
this.expandRowKeys = []; // 收起所有
|
||||||
} else {
|
} else {
|
||||||
this.expandRowKeys = this.tableData.map(item => item.id); // 展开所有
|
this.expandRowKeys = rows.map(item => item[key]); // 展开所有
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 每次展开状态变化时更新 expandRowKeys
|
// 每次展开状态变化时更新 expandRowKeys
|
||||||
handleExpandChange(row, expandedRows) {
|
handleExpandChange(row, expandedRows) {
|
||||||
this.expandRowKeys = expandedRows.map(row => row.id); // 根据实际的唯一标识
|
const key = this.rowKey || "id";
|
||||||
|
this.expandRowKeys = expandedRows.map(item => item[key]); // 根据实际的唯一标识
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="flow-toolbar__left">
|
<div class="flow-toolbar__left">
|
||||||
<el-button v-if="!readonly" type="primary" icon="el-icon-plus" size="small" round @click="addStep">新增工序</el-button>
|
<el-button v-if="!readonly" type="primary" icon="el-icon-plus" size="small" round @click="addStep">新增工序</el-button>
|
||||||
<el-button icon="el-icon-magic-stick" size="small" round @click="relayout">自动布局</el-button>
|
<el-button icon="el-icon-magic-stick" size="small" round @click="relayout">自动布局</el-button>
|
||||||
<el-button icon="el-icon-full-screen" size="small" round @click="doFit">适应画布</el-button>
|
<el-button icon="el-icon-full-screen" size="small" round @click="fitToWidth">适应宽度</el-button>
|
||||||
<el-button icon="el-icon-circle-check" size="small" round @click="doValidate" :loading="validating">校验</el-button>
|
<el-button icon="el-icon-circle-check" size="small" round @click="doValidate" :loading="validating">校验</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-legend">
|
<div class="flow-legend">
|
||||||
|
|
@ -55,13 +55,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 画布 -->
|
<!-- 画布 -->
|
||||||
<div class="flow-canvas" @drop="onDrop" @dragover.prevent>
|
<div ref="canvas" class="flow-canvas" @drop="onDrop" @dragover.prevent>
|
||||||
<VueFlow
|
<VueFlow
|
||||||
:id="flowId"
|
:id="flowId"
|
||||||
:default-viewport="{ zoom: 0.9 }"
|
:default-viewport="{ zoom: 0.9 }"
|
||||||
:min-zoom="0.2"
|
:min-zoom="0.2"
|
||||||
:max-zoom="2"
|
:max-zoom="2"
|
||||||
:connection-radius="8"
|
:pan-on-scroll="true"
|
||||||
|
:zoom-on-scroll="false"
|
||||||
|
:connection-radius="56"
|
||||||
:connect-on-click="false"
|
:connect-on-click="false"
|
||||||
:nodes-draggable="true"
|
:nodes-draggable="true"
|
||||||
:nodes-connectable="!readonly"
|
:nodes-connectable="!readonly"
|
||||||
|
|
@ -78,6 +80,9 @@
|
||||||
<template #node-material="{ data }">
|
<template #node-material="{ data }">
|
||||||
<material-node :data="data" />
|
<material-node :data="data" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #edge-dagre="edgeProps">
|
||||||
|
<dagre-edge v-bind="edgeProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<Background :gap="18" pattern-color="#e6e9ef" />
|
<Background :gap="18" pattern-color="#e6e9ef" />
|
||||||
<Controls :show-interactive="false" />
|
<Controls :show-interactive="false" />
|
||||||
|
|
@ -108,8 +113,9 @@ import { VueFlow, useVueFlow } from "@vue-flow/core";
|
||||||
import { Background } from "@vue-flow/background";
|
import { Background } from "@vue-flow/background";
|
||||||
import { Controls } from "@vue-flow/controls";
|
import { Controls } from "@vue-flow/controls";
|
||||||
import routeForm from "@/views/mtm/route_form.vue";
|
import routeForm from "@/views/mtm/route_form.vue";
|
||||||
|
import DagreEdge from "./flow/DagreEdge.vue";
|
||||||
import MaterialNode from "./flow/MaterialNode.vue";
|
import MaterialNode from "./flow/MaterialNode.vue";
|
||||||
import { layoutGraph } from "./flow/dagreLayout";
|
import { assignEdgePorts, layoutGraphWithEdges, NODE_W } from "./flow/dagreLayout";
|
||||||
import "@vue-flow/core/dist/style.css";
|
import "@vue-flow/core/dist/style.css";
|
||||||
import "@vue-flow/core/dist/theme-default.css";
|
import "@vue-flow/core/dist/theme-default.css";
|
||||||
import "@vue-flow/controls/dist/style.css";
|
import "@vue-flow/controls/dist/style.css";
|
||||||
|
|
@ -119,7 +125,7 @@ let flowSeq = 0;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "xtFlowEditor",
|
name: "xtFlowEditor",
|
||||||
components: { VueFlow, Background, Controls, routeForm, MaterialNode },
|
components: { VueFlow, Background, Controls, routeForm, DagreEdge, MaterialNode },
|
||||||
props: {
|
props: {
|
||||||
// 工艺包 id
|
// 工艺包 id
|
||||||
routepack: { type: String, default: "" },
|
routepack: { type: String, default: "" },
|
||||||
|
|
@ -132,9 +138,9 @@ export default {
|
||||||
emits: ["changed"],
|
emits: ["changed"],
|
||||||
setup() {
|
setup() {
|
||||||
const flowId = "route-flow-editor-" + ++flowSeq;
|
const flowId = "route-flow-editor-" + ++flowSeq;
|
||||||
const { fitView, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes } =
|
const { setViewport, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes } =
|
||||||
useVueFlow(flowId);
|
useVueFlow(flowId);
|
||||||
return { flowId, fitView, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes };
|
return { flowId, setViewport, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes };
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -160,6 +166,8 @@ export default {
|
||||||
connectStartPos: null,
|
connectStartPos: null,
|
||||||
// 仅初次加载自动适配一次,避免编辑时视图跳动
|
// 仅初次加载自动适配一次,避免编辑时视图跳动
|
||||||
hasFitted: false,
|
hasFitted: false,
|
||||||
|
resizeObserver: null,
|
||||||
|
resizeTimer: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -180,6 +188,19 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadMaterials();
|
this.loadMaterials();
|
||||||
if (this.routepack) this.reload();
|
if (this.routepack) this.reload();
|
||||||
|
if (typeof ResizeObserver !== "undefined") {
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
|
this.resizeTimer = setTimeout(() => {
|
||||||
|
if (this.hasFitted) this.fitToWidth(false);
|
||||||
|
}, 80);
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(this.$refs.canvas);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/** 加载左侧物料面板列表 */
|
/** 加载左侧物料面板列表 */
|
||||||
|
|
@ -252,11 +273,11 @@ export default {
|
||||||
target: tid,
|
target: tid,
|
||||||
label: (r.process_name || "") + (locked ? " 🔒" : ""),
|
label: (r.process_name || "") + (locked ? " 🔒" : ""),
|
||||||
data: { route: r, locked },
|
data: { route: r, locked },
|
||||||
type: "smoothstep",
|
type: "dagre",
|
||||||
markerEnd: "arrowclosed",
|
markerEnd: "arrowclosed",
|
||||||
style: {
|
style: {
|
||||||
stroke: locked ? "#c0c4cc" : "#409eff",
|
stroke: locked ? "#c0c4cc" : "#409eff",
|
||||||
strokeWidth: locked ? 1.5 : 2,
|
strokeWidth: locked ? 1.7 : 2.2,
|
||||||
strokeDasharray: locked ? "5 4" : undefined,
|
strokeDasharray: locked ? "5 4" : undefined,
|
||||||
},
|
},
|
||||||
labelStyle: { fill: "#5b6370", fontSize: "12px", fontWeight: 500 },
|
labelStyle: { fill: "#5b6370", fontSize: "12px", fontWeight: 500 },
|
||||||
|
|
@ -290,11 +311,24 @@ export default {
|
||||||
this.seedNodes = this.seedNodes.filter((s) => !routeNodeIds.has(s.id));
|
this.seedNodes = this.seedNodes.filter((s) => !routeNodeIds.has(s.id));
|
||||||
|
|
||||||
const prevCount = this.nodes.length;
|
const prevCount = this.nodes.length;
|
||||||
this.edges = edges;
|
const routedGraph = this.layoutAndRoute(routeNodes.concat(this.seedNodes), edges);
|
||||||
this.nodes = layoutGraph(routeNodes.concat(this.seedNodes), edges);
|
this.nodes = routedGraph.nodes;
|
||||||
|
this.edges = routedGraph.edges;
|
||||||
this.syncStore();
|
this.syncStore();
|
||||||
// 节点增减后 dagre 会整体重排,重新适配视野,避免“看着是空白其实有节点”导致拉线误吸附
|
// 节点增减后 dagre 会整体重排,重新适配视野,避免“看着是空白其实有节点”导致拉线误吸附
|
||||||
if (this.hasFitted && this.nodes.length !== prevCount) this.doFit();
|
if (this.hasFitted && this.nodes.length !== prevCount) this.fitToWidth();
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 编辑与查看使用一致的 dagre-d3 布局参数,并保留边绕行点 */
|
||||||
|
layoutAndRoute(nodes, edges) {
|
||||||
|
const laidOutGraph = layoutGraphWithEdges(nodes, edges, {
|
||||||
|
nodesep: 40,
|
||||||
|
edgesep: 25,
|
||||||
|
ranksep: 20,
|
||||||
|
marginx: 80,
|
||||||
|
marginy: 10,
|
||||||
|
});
|
||||||
|
return assignEdgePorts(laidOutGraph.nodes, laidOutGraph.edges);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 把本地 nodes/edges 命令式同步到 Vue Flow store(先节点后边,避免边找不到端点) */
|
/** 把本地 nodes/edges 命令式同步到 Vue Flow store(先节点后边,避免边找不到端点) */
|
||||||
|
|
@ -306,15 +340,36 @@ export default {
|
||||||
|
|
||||||
/** 重新布局并适应画布 */
|
/** 重新布局并适应画布 */
|
||||||
relayout() {
|
relayout() {
|
||||||
this.nodes = layoutGraph([...this.nodes], this.edges);
|
const routedGraph = this.layoutAndRoute([...this.nodes], this.edges);
|
||||||
|
this.nodes = routedGraph.nodes;
|
||||||
|
this.edges = routedGraph.edges;
|
||||||
this.setNodes(this.nodes);
|
this.setNodes(this.nodes);
|
||||||
this.doFit();
|
this.setEdges(this.edges);
|
||||||
|
this.fitToWidth();
|
||||||
},
|
},
|
||||||
|
|
||||||
doFit() {
|
/** 按宽度保持节点可读,长流程通过滚轮或拖动画布纵向浏览 */
|
||||||
|
fitToWidth(animated = true) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
try {
|
try {
|
||||||
this.fitView({ padding: 0.25, maxZoom: 1, duration: 300 });
|
const canvas = this.$refs.canvas;
|
||||||
|
if (!canvas || !this.nodes.length) return;
|
||||||
|
const xs = this.nodes.map((n) => n.position.x);
|
||||||
|
const ys = this.nodes.map((n) => n.position.y);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
const maxX = Math.max(...xs) + NODE_W;
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const paddingX = 48;
|
||||||
|
const paddingTop = 36;
|
||||||
|
const graphWidth = Math.max(maxX - minX, NODE_W);
|
||||||
|
const zoom = Math.min(1, Math.max(0.2, (canvas.clientWidth - paddingX * 2) / graphWidth));
|
||||||
|
const x = (canvas.clientWidth - graphWidth * zoom) / 2 - minX * zoom;
|
||||||
|
const y = paddingTop - minY * zoom;
|
||||||
|
this.setViewport(
|
||||||
|
{ x, y, zoom },
|
||||||
|
{ duration: animated && this.hasFitted ? 200 : 0 }
|
||||||
|
);
|
||||||
|
this.hasFitted = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* noop */
|
/* noop */
|
||||||
}
|
}
|
||||||
|
|
@ -325,8 +380,7 @@ export default {
|
||||||
onNodesInit() {
|
onNodesInit() {
|
||||||
if (this.hasFitted) return;
|
if (this.hasFitted) return;
|
||||||
if (this.nodes.length === 0) return;
|
if (this.nodes.length === 0) return;
|
||||||
this.hasFitted = true;
|
this.fitToWidth();
|
||||||
this.doFit();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ============ 左侧面板拖拽入画布 ============ */
|
/* ============ 左侧面板拖拽入画布 ============ */
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<!-- 顶部工具栏:与 xtFlowEditor 视觉一致 -->
|
<!-- 顶部工具栏:与 xtFlowEditor 视觉一致 -->
|
||||||
<div class="flow-toolbar">
|
<div class="flow-toolbar">
|
||||||
<div class="flow-toolbar__left">
|
<div class="flow-toolbar__left">
|
||||||
<el-button icon="el-icon-full-screen" size="small" round @click="doFit">适应画布</el-button>
|
<el-button icon="el-icon-full-screen" size="small" round @click="fitToWidth">适应宽度</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-legend">
|
<div class="flow-legend">
|
||||||
<span class="lg lg--raw">原料</span>
|
<span class="lg lg--raw">原料</span>
|
||||||
|
|
@ -11,11 +11,13 @@
|
||||||
<span class="lg lg--final">成品</span>
|
<span class="lg lg--final">成品</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-canvas">
|
<div ref="canvas" class="flow-canvas">
|
||||||
<VueFlow
|
<VueFlow
|
||||||
:id="flowId"
|
:id="flowId"
|
||||||
:min-zoom="0.2"
|
:min-zoom="0.2"
|
||||||
:max-zoom="2"
|
:max-zoom="2"
|
||||||
|
:pan-on-scroll="true"
|
||||||
|
:zoom-on-scroll="false"
|
||||||
:nodes-draggable="false"
|
:nodes-draggable="false"
|
||||||
:nodes-connectable="false"
|
:nodes-connectable="false"
|
||||||
:elements-selectable="false"
|
:elements-selectable="false"
|
||||||
|
|
@ -24,6 +26,9 @@
|
||||||
<template #node-material="{ data }">
|
<template #node-material="{ data }">
|
||||||
<material-node :data="data" />
|
<material-node :data="data" />
|
||||||
</template>
|
</template>
|
||||||
|
<template #edge-dagre="edgeProps">
|
||||||
|
<dagre-edge v-bind="edgeProps" />
|
||||||
|
</template>
|
||||||
<Background :gap="18" pattern-color="#e6e9ef" />
|
<Background :gap="18" pattern-color="#e6e9ef" />
|
||||||
<Controls :show-interactive="false" />
|
<Controls :show-interactive="false" />
|
||||||
</VueFlow>
|
</VueFlow>
|
||||||
|
|
@ -39,8 +44,9 @@
|
||||||
import { VueFlow, useVueFlow } from "@vue-flow/core";
|
import { VueFlow, useVueFlow } from "@vue-flow/core";
|
||||||
import { Background } from "@vue-flow/background";
|
import { Background } from "@vue-flow/background";
|
||||||
import { Controls } from "@vue-flow/controls";
|
import { Controls } from "@vue-flow/controls";
|
||||||
|
import DagreEdge from "./flow/DagreEdge.vue";
|
||||||
import MaterialNode from "./flow/MaterialNode.vue";
|
import MaterialNode from "./flow/MaterialNode.vue";
|
||||||
import { layoutGraph } from "./flow/dagreLayout";
|
import { assignEdgePorts, layoutGraphWithEdges, NODE_W } from "./flow/dagreLayout";
|
||||||
import "@vue-flow/core/dist/style.css";
|
import "@vue-flow/core/dist/style.css";
|
||||||
import "@vue-flow/core/dist/theme-default.css";
|
import "@vue-flow/core/dist/theme-default.css";
|
||||||
import "@vue-flow/controls/dist/style.css";
|
import "@vue-flow/controls/dist/style.css";
|
||||||
|
|
@ -54,7 +60,7 @@ let viewerSeq = 0;
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
name: "xtFlowViewer",
|
name: "xtFlowViewer",
|
||||||
components: { VueFlow, Background, Controls, MaterialNode },
|
components: { VueFlow, Background, Controls, DagreEdge, MaterialNode },
|
||||||
props: {
|
props: {
|
||||||
nodes: { type: Array, default: () => [] },
|
nodes: { type: Array, default: () => [] },
|
||||||
edges: { type: Array, default: () => [] },
|
edges: { type: Array, default: () => [] },
|
||||||
|
|
@ -62,14 +68,17 @@ export default {
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const flowId = "xt-flow-viewer-" + ++viewerSeq;
|
const flowId = "xt-flow-viewer-" + ++viewerSeq;
|
||||||
const { fitView, setNodes, setEdges } = useVueFlow(flowId);
|
const { setViewport, setNodes, setEdges } = useVueFlow(flowId);
|
||||||
return { flowId, fitView, setNodes, setEdges };
|
return { flowId, setViewport, setNodes, setEdges };
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 首次 fit 不带动画,避免打开时视图跳动
|
// 首次 fit 不带动画,避免打开时视图跳动
|
||||||
hasFitted: false,
|
hasFitted: false,
|
||||||
rebuildPending: false,
|
rebuildPending: false,
|
||||||
|
renderedNodes: [],
|
||||||
|
resizeObserver: null,
|
||||||
|
resizeTimer: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -87,6 +96,19 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.rebuild();
|
this.rebuild();
|
||||||
|
if (typeof ResizeObserver !== "undefined") {
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
|
this.resizeTimer = setTimeout(() => {
|
||||||
|
if (this.hasFitted) this.fitToWidth(false);
|
||||||
|
}, 80);
|
||||||
|
});
|
||||||
|
this.resizeObserver.observe(this.$refs.canvas);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.resizeObserver) this.resizeObserver.disconnect();
|
||||||
|
clearTimeout(this.resizeTimer);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/** nodes/edges 通常被先后赋值,用微任务合并成一次 rebuild,避免双次 dagre 布局 */
|
/** nodes/edges 通常被先后赋值,用微任务合并成一次 rebuild,避免双次 dagre 布局 */
|
||||||
|
|
@ -104,19 +126,19 @@ export default {
|
||||||
const targetIds = new Set();
|
const targetIds = new Set();
|
||||||
const rawEdges = (this.edges || [])
|
const rawEdges = (this.edges || [])
|
||||||
.filter((e) => e.source != null && e.target != null)
|
.filter((e) => e.source != null && e.target != null)
|
||||||
.map((e) => {
|
.map((e, index) => {
|
||||||
const sid = String(e.source);
|
const sid = String(e.source);
|
||||||
const tid = String(e.target);
|
const tid = String(e.target);
|
||||||
sourceIds.add(sid);
|
sourceIds.add(sid);
|
||||||
targetIds.add(tid);
|
targetIds.add(tid);
|
||||||
return {
|
return {
|
||||||
id: e.id != null ? String(e.id) : sid + "-" + tid,
|
id: e.id != null ? String(e.id) : `${sid}-${tid}-${edgeIndex}`,
|
||||||
source: sid,
|
source: sid,
|
||||||
target: tid,
|
target: tid,
|
||||||
label: e.label || "",
|
label: e.label || "",
|
||||||
type: "smoothstep",
|
type: "dagre",
|
||||||
markerEnd: "arrowclosed",
|
markerEnd: "arrowclosed",
|
||||||
style: { stroke: "#409eff", strokeWidth: 2 },
|
style: { stroke: "#409eff", strokeWidth: 2.2 },
|
||||||
labelStyle: { fill: "#5b6370", fontSize: "12px", fontWeight: 500 },
|
labelStyle: { fill: "#5b6370", fontSize: "12px", fontWeight: 500 },
|
||||||
labelBgStyle: { fill: "#ffffff", stroke: "#e4e7ed", strokeWidth: 1 },
|
labelBgStyle: { fill: "#ffffff", stroke: "#e4e7ed", strokeWidth: 1 },
|
||||||
labelBgPadding: [7, 4],
|
labelBgPadding: [7, 4],
|
||||||
|
|
@ -137,19 +159,49 @@ export default {
|
||||||
const nodeIds = new Set(vNodes.map((n) => n.id));
|
const nodeIds = new Set(vNodes.map((n) => n.id));
|
||||||
const vEdges = rawEdges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
|
const vEdges = rawEdges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
|
||||||
|
|
||||||
|
const laidOutGraph = layoutGraphWithEdges(vNodes, vEdges, {
|
||||||
|
rankdir: this.rankdir,
|
||||||
|
nodesep: 40,
|
||||||
|
edgesep: 25,
|
||||||
|
ranksep: 20,
|
||||||
|
marginx: 80,
|
||||||
|
marginy: 10,
|
||||||
|
});
|
||||||
|
const routedGraph = assignEdgePorts(laidOutGraph.nodes, laidOutGraph.edges);
|
||||||
|
this.renderedNodes = routedGraph.nodes;
|
||||||
this.setEdges([]);
|
this.setEdges([]);
|
||||||
this.setNodes(layoutGraph(vNodes, vEdges, { rankdir: this.rankdir }));
|
this.setNodes(this.renderedNodes);
|
||||||
this.setEdges(vEdges);
|
this.setEdges(routedGraph.edges);
|
||||||
// 首次挂载交给 nodes-initialized(尺寸测量完成)再 fit;数据更新时尺寸已就绪,直接 fit
|
// 首次挂载交给 nodes-initialized(尺寸测量完成)再定位;数据更新时直接重新适配宽度
|
||||||
if (this.hasFitted) this.doFit();
|
if (this.hasFitted) this.fitToWidth();
|
||||||
},
|
},
|
||||||
onNodesInit() {
|
onNodesInit() {
|
||||||
this.doFit();
|
this.fitToWidth();
|
||||||
},
|
},
|
||||||
doFit() {
|
/**
|
||||||
|
* 只按宽度缩放,长流程不再为了塞进高度而缩成缩略图。
|
||||||
|
* 图从画布顶部开始展示,用户可拖动画布继续向下查看。
|
||||||
|
*/
|
||||||
|
fitToWidth(animated = true) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
try {
|
try {
|
||||||
this.fitView({ padding: 0.2, maxZoom: 1, duration: this.hasFitted ? 200 : 0 });
|
const canvas = this.$refs.canvas;
|
||||||
|
if (!canvas || !this.renderedNodes.length) return;
|
||||||
|
const xs = this.renderedNodes.map((n) => n.position.x);
|
||||||
|
const ys = this.renderedNodes.map((n) => n.position.y);
|
||||||
|
const minX = Math.min(...xs);
|
||||||
|
const maxX = Math.max(...xs) + NODE_W;
|
||||||
|
const minY = Math.min(...ys);
|
||||||
|
const paddingX = 48;
|
||||||
|
const paddingTop = 36;
|
||||||
|
const graphWidth = Math.max(maxX - minX, NODE_W);
|
||||||
|
const zoom = Math.min(1, Math.max(0.2, (canvas.clientWidth - paddingX * 2) / graphWidth));
|
||||||
|
const x = (canvas.clientWidth - graphWidth * zoom) / 2 - minX * zoom;
|
||||||
|
const y = paddingTop - minY * zoom;
|
||||||
|
this.setViewport(
|
||||||
|
{ x, y, zoom },
|
||||||
|
{ duration: animated && this.hasFitted ? 200 : 0 }
|
||||||
|
);
|
||||||
this.hasFitted = true;
|
this.hasFitted = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* noop */
|
/* noop */
|
||||||
|
|
|
||||||
|
|
@ -59,14 +59,9 @@
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="交接到工段">
|
<el-table-column label="产出库存归属">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag type="success" v-if="scope.row.into_wm_mgroup">
|
{{ wmScopes[scope.row.into_wm_scope] }}
|
||||||
是
|
|
||||||
</el-tag>
|
|
||||||
<!-- <el-icon v-if="scope.row.into_wm_mgroup" color="green">
|
|
||||||
<CircleCheckFilled />
|
|
||||||
</el-icon> -->
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="不合格品是否入库">
|
<el-table-column label="不合格品是否入库">
|
||||||
|
|
@ -211,6 +206,11 @@ export default {
|
||||||
20:'切分',
|
20:'切分',
|
||||||
30:'结合'
|
30:'结合'
|
||||||
},
|
},
|
||||||
|
wmScopes:{
|
||||||
|
10:'工段',
|
||||||
|
20:'部门',
|
||||||
|
30:'全局'
|
||||||
|
},
|
||||||
currentItem:{},
|
currentItem:{},
|
||||||
selectValue:[],
|
selectValue:[],
|
||||||
visible:false,
|
visible:false,
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,15 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="交接到工段">
|
<el-form-item label="产出库存归属">
|
||||||
<el-switch
|
<el-select v-model="form.into_wm_scope">
|
||||||
v-model="form.into_wm_mgroup"
|
<el-option
|
||||||
></el-switch>
|
v-for="item in wmScopeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
|
|
@ -133,7 +138,7 @@ const defaultForm = {
|
||||||
mtype:10,
|
mtype:10,
|
||||||
belong_dept: "",
|
belong_dept: "",
|
||||||
wpr_number_rule: "",
|
wpr_number_rule: "",
|
||||||
into_wm_mgroup: true,
|
into_wm_scope: 20,
|
||||||
store_notok: true,
|
store_notok: true,
|
||||||
has_ok_b_defect:false,
|
has_ok_b_defect:false,
|
||||||
};
|
};
|
||||||
|
|
@ -165,6 +170,11 @@ export default {
|
||||||
{name:'切分',value:20},
|
{name:'切分',value:20},
|
||||||
{name:'结合',value:30},
|
{name:'结合',value:30},
|
||||||
],
|
],
|
||||||
|
wmScopeOptions:[
|
||||||
|
{name:'工段',value:10},
|
||||||
|
{name:'部门',value:20},
|
||||||
|
{name:'全局',value:30},
|
||||||
|
],
|
||||||
visible: false,
|
visible: false,
|
||||||
isSaveing: false,
|
isSaveing: false,
|
||||||
deptOptions: [],
|
deptOptions: [],
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
ref="dialogForm"
|
ref="dialogForm"
|
||||||
:model="form"
|
:model="form"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
|
:disabled="mode === 'show'"
|
||||||
label-position="right"
|
label-position="right"
|
||||||
label-width="100px"
|
label-width="100px"
|
||||||
>
|
>
|
||||||
|
|
@ -197,7 +198,7 @@
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col>
|
<el-col>
|
||||||
<el-form-item label="工艺图纸">
|
<el-form-item label="工艺图纸">
|
||||||
<sc-upload-multiple v-model="fileurl" @imagesDel="imagesDel" @imagesChange="imagesChange" draggable :limit="9" tip="最多上传9个文件,单个文件不要超过10M,请上传图像格式文件"></sc-upload-multiple>
|
<sc-upload-multiple v-model="fileurl" @imagesDel="imagesDel" @imagesChange="imagesChange" draggable :disabled="mode === 'show'" :limit="9" tip="最多上传9个文件,单个文件不要超过10M,请上传图像格式文件"></sc-upload-multiple>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
@ -226,9 +227,10 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row v-if="project_code=='bxerp'&&mode=='edit'">
|
<el-row v-if="project_code=='bxerp'&&(mode=='edit'||mode=='show')">
|
||||||
<sc-form-table
|
<sc-form-table
|
||||||
hideDelete
|
hideDelete
|
||||||
|
:hideAdd="mode === 'show'"
|
||||||
ref="routematTable"
|
ref="routematTable"
|
||||||
:tableHeight="300"
|
:tableHeight="300"
|
||||||
v-model="routemats"
|
v-model="routemats"
|
||||||
|
|
@ -253,7 +255,7 @@
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="open" label="操作" width="125" align="center">
|
<el-table-column v-if="mode !== 'show'" prop="open" label="操作" width="125" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
text
|
text
|
||||||
|
|
@ -279,8 +281,8 @@
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<el-button type="primary" :loading="isSaveing" @click="submit">提交</el-button>
|
<el-button v-if="mode !== 'show'" type="primary" :loading="isSaveing" @click="submit">提交</el-button>
|
||||||
<el-button @click="visible = false">取消</el-button>
|
<el-button @click="visible = false">{{ mode === "show" ? "关闭" : "取消" }}</el-button>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
@ -347,7 +349,7 @@ export default {
|
||||||
fileurl_form:[],
|
fileurl_form:[],
|
||||||
paramsJsonFileurl:[],
|
paramsJsonFileurl:[],
|
||||||
materialOptions:[],
|
materialOptions:[],
|
||||||
titleMap: { add: "新增", edit: "编辑" },
|
titleMap: { add: "新增", edit: "编辑", show: "工序详情" },
|
||||||
setFiltersVisible: false,
|
setFiltersVisible: false,
|
||||||
routeId: "",
|
routeId: "",
|
||||||
processName: "",
|
processName: "",
|
||||||
|
|
@ -505,6 +507,9 @@ export default {
|
||||||
let that = this;
|
let that = this;
|
||||||
that.mode = mode;
|
that.mode = mode;
|
||||||
that.resetForm();
|
that.resetForm();
|
||||||
|
if (that.project_code == "bxerp" && (mode === "edit" || mode === "show")) {
|
||||||
|
that.getMaterialOptions();
|
||||||
|
}
|
||||||
if (that.prefill) that.setData(that.prefill);
|
if (that.prefill) that.setData(that.prefill);
|
||||||
that.visible = true;
|
that.visible = true;
|
||||||
that.$nextTick(() => {
|
that.$nextTick(() => {
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,9 @@
|
||||||
<div class="rs-card__title">工艺路线流程图</div>
|
<div class="rs-card__title">工艺路线流程图</div>
|
||||||
<el-tabs v-if="tabsTitle.length > 1" v-model="activeName" class="rs-tabs"
|
<el-tabs v-if="tabsTitle.length > 1" v-model="activeName" class="rs-tabs"
|
||||||
@tab-click="tabshandleClick">
|
@tab-click="tabshandleClick">
|
||||||
<el-tab-pane :label="item" v-for="item in tabsTitle" :key="item"></el-tab-pane>
|
<el-tab-pane :label="item" :name="item" v-for="item in tabsTitle" :key="item"></el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<xtFlowViewer v-if="limitedWatch" :nodes="nodes" :edges="edges" class="rs-flow__canvas"
|
<xtFlowViewer :nodes="nodes" :edges="edges" class="rs-flow__canvas">
|
||||||
style="min-height: 0">
|
|
||||||
</xtFlowViewer>
|
</xtFlowViewer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -33,7 +32,17 @@
|
||||||
<div class="rs-card rs-steps">
|
<div class="rs-card rs-steps">
|
||||||
<div class="rs-card__title">工序步骤</div>
|
<div class="rs-card__title">工序步骤</div>
|
||||||
<div class="rs-steps__table">
|
<div class="rs-steps__table">
|
||||||
<scTable ref="tables" :data="tableData" row-key="id" hidePagination hideDo stripe border>
|
<scTable
|
||||||
|
ref="tables"
|
||||||
|
:data="tableData"
|
||||||
|
row-key="id"
|
||||||
|
hidePagination
|
||||||
|
hideDo
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
class="rs-steps__clickable"
|
||||||
|
@row-click="showStepDetail"
|
||||||
|
>
|
||||||
<el-table-column label="排序" prop="sort" width="50">
|
<el-table-column label="排序" prop="sort" width="50">
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="工序" prop="process_name">
|
<el-table-column label="工序" prop="process_name">
|
||||||
|
|
@ -58,6 +67,13 @@
|
||||||
<el-tag v-if="scope.row.batch_bind" type="success">是</el-tag>
|
<el-tag v-if="scope.row.batch_bind" type="success">是</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" fixed="right" width="64">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link type="primary" :underline="false" @click.stop="showStepDetail(scope.row)">
|
||||||
|
查看
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</scTable>
|
</scTable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -66,16 +82,16 @@
|
||||||
<el-aside width="20%" v-if="form.ticket" class="rs-aside">
|
<el-aside width="20%" v-if="form.ticket" class="rs-aside">
|
||||||
<ticketd :ticket_="form.ticket_"></ticketd>
|
<ticketd :ticket_="form.ticket_"></ticketd>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
<route-form ref="routeDetail" :routepack="form.id || ''"></route-form>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { defineAsyncComponent } from "vue";
|
|
||||||
import ticketd from '@/views/wf/ticketd.vue'
|
import ticketd from '@/views/wf/ticketd.vue'
|
||||||
// 异步加载:Vue Flow 单独分包,仅打开详情时才下载
|
import xtFlowViewer from "@/components/xtFlowViewer.vue";
|
||||||
const xtFlowViewer = defineAsyncComponent(() => import(/* webpackPrefetch: true */ "@/components/xtFlowViewer.vue"));
|
import RouteForm from "./route_form.vue";
|
||||||
export default {
|
export default {
|
||||||
emits: ["success", "closed"],
|
emits: ["success", "closed"],
|
||||||
components: { ticketd, xtFlowViewer },
|
components: { ticketd, xtFlowViewer, RouteForm },
|
||||||
props: {
|
props: {
|
||||||
t_id: { type: String, default: null },
|
t_id: { type: String, default: null },
|
||||||
},
|
},
|
||||||
|
|
@ -104,7 +120,6 @@ export default {
|
||||||
tableData: [],
|
tableData: [],
|
||||||
project_code: "",
|
project_code: "",
|
||||||
activeName: "总图",
|
activeName: "总图",
|
||||||
limitedWatch: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
@ -124,6 +139,11 @@ export default {
|
||||||
setData(data) {
|
setData(data) {
|
||||||
let that = this;
|
let that = this;
|
||||||
Object.assign(that.form, data);
|
Object.assign(that.form, data);
|
||||||
|
that.nodes = [];
|
||||||
|
that.edges = [];
|
||||||
|
that.tabsTitle = ['总图'];
|
||||||
|
that.tabsData = {};
|
||||||
|
that.activeName = "总图";
|
||||||
that.getRoute(that.form.material);
|
that.getRoute(that.form.material);
|
||||||
that.getDEGdatas(data.id);
|
that.getDEGdatas(data.id);
|
||||||
this.getTabsList(data.id);
|
this.getTabsList(data.id);
|
||||||
|
|
@ -135,11 +155,10 @@ export default {
|
||||||
getDEGdatas(id) {
|
getDEGdatas(id) {
|
||||||
let that = this;
|
let that = this;
|
||||||
that.$API.mtm.routepack.dag.req(id).then((res) => {
|
that.$API.mtm.routepack.dag.req(id).then((res) => {
|
||||||
that.nodes = res.nodes;
|
|
||||||
that.edges = res.edges;
|
|
||||||
that.tabsData['总图'] = res;
|
that.tabsData['总图'] = res;
|
||||||
if (!that.limitedWatch) {
|
// 子图列表与总图并行加载;用户已切到子图时不能再被迟到的总图覆盖
|
||||||
that.limitedWatch = true;
|
if (that.activeName === "总图") {
|
||||||
|
that.showGraph(res);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -156,9 +175,16 @@ export default {
|
||||||
that.tabsData[res[key].name] = res[key];
|
that.tabsData[res[key].name] = res[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(that.tabsData);
|
const activeGraph = that.tabsData[that.activeName];
|
||||||
|
if (activeGraph) {
|
||||||
|
that.showGraph(activeGraph);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
showGraph(graph) {
|
||||||
|
this.nodes = graph?.nodes || [];
|
||||||
|
this.edges = graph?.edges || [];
|
||||||
|
},
|
||||||
//文件列表加载
|
//文件列表加载
|
||||||
getRoute(id) {
|
getRoute(id) {
|
||||||
this.$API.mtm.route.list
|
this.$API.mtm.route.list
|
||||||
|
|
@ -168,12 +194,15 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tabshandleClick(val) {
|
tabshandleClick(val) {
|
||||||
let that = this;
|
const graph = this.tabsData[val.props.name || val.props.label];
|
||||||
let label = val.props.label;
|
if (graph) {
|
||||||
that.$nextTick(() => {
|
this.showGraph(graph);
|
||||||
that.nodes = that.tabsData[label].nodes;
|
}
|
||||||
that.edges = that.tabsData[label].edges;
|
},
|
||||||
})
|
async showStepDetail(row) {
|
||||||
|
if (!row || !row.id) return;
|
||||||
|
const detail = await this.$API.mtm.route.item.req(row.id);
|
||||||
|
this.$refs.routeDetail.open("show").setData(detail);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -254,11 +283,19 @@ export default {
|
||||||
}
|
}
|
||||||
.rs-flow__canvas {
|
.rs-flow__canvas {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
.rs-tabs {
|
||||||
|
/* el-tabs 的空 content 默认会 flex-grow,多个流程图时会把下方画布挤出可视区 */
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
.rs-tabs :deep(.el-tabs__header) {
|
.rs-tabs :deep(.el-tabs__header) {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
.rs-tabs :deep(.el-tabs__content) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.rs-steps {
|
.rs-steps {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -270,6 +307,9 @@ export default {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
.rs-steps__clickable :deep(.el-table__row) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
/* 右侧工单/审批面板:与主体同底色,间距交给 ticketd 自身的内边距 */
|
/* 右侧工单/审批面板:与主体同底色,间距交给 ticketd 自身的内边距 */
|
||||||
.rs-aside {
|
.rs-aside {
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
|
<el-button
|
||||||
|
v-if="needAccept"
|
||||||
|
type="success"
|
||||||
|
:loading="isAccepting"
|
||||||
|
:disabled="isAccepting"
|
||||||
|
@click="acceptTicket"
|
||||||
|
style="margin-right: 2px"
|
||||||
|
>接单</el-button>
|
||||||
<span v-if="actionShow"><el-button
|
<span v-if="actionShow"><el-button
|
||||||
v-for="item in transitions"
|
v-for="item in transitions"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
|
@ -17,10 +25,9 @@
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, defineEmits,watch } from 'vue'
|
import { ref, defineEmits, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import API from '@/api';
|
import API from '@/api';
|
||||||
import TOOL from "@/utils/tool.js";
|
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -34,44 +41,53 @@ const props = defineProps({
|
||||||
|
|
||||||
const workflow = ref(null);
|
const workflow = ref(null);
|
||||||
const transitions = ref([]);
|
const transitions = ref([]);
|
||||||
let lastInitTicketId = null;
|
let lastInitKey = null;
|
||||||
const tryInit = () => {
|
let requestVersion = 0;
|
||||||
// 优先用 ticket_ 分支:等到 ticket_.id 出现再调
|
const getInitKey = () => {
|
||||||
if (props.ticket_ && props.ticket_.id) {
|
if (props.ticket_ && props.ticket_.id) {
|
||||||
if (lastInitTicketId !== props.ticket_.id) {
|
return [
|
||||||
lastInitTicketId = props.ticket_.id;
|
'ticket',
|
||||||
init();
|
props.ticket_.id,
|
||||||
|
props.ticket_.state,
|
||||||
|
JSON.stringify(props.ticket_.participant),
|
||||||
|
props.ticket_.update_time,
|
||||||
|
].join(':');
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 没有 ticket_,退化到 workflow_key 分支(创建工单场景)
|
|
||||||
if (props.workflow_key) {
|
if (props.workflow_key) {
|
||||||
if (lastInitTicketId !== '__wf_key__:' + props.workflow_key) {
|
return `workflow:${props.workflow_key}`;
|
||||||
lastInitTicketId = '__wf_key__:' + props.workflow_key;
|
|
||||||
init();
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const tryInit = () => {
|
||||||
|
const initKey = getInitKey();
|
||||||
|
if (initKey && lastInitKey !== initKey) {
|
||||||
|
lastInitKey = initKey;
|
||||||
|
init();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onMounted(() => { tryInit(); });
|
|
||||||
watch(() => props.ticket_, () => { tryInit(); }, { deep: true });
|
|
||||||
watch(() => props.workflow_key, () => { tryInit(); });
|
|
||||||
|
|
||||||
const ticketId = ref(null);
|
const ticketId = ref(null);
|
||||||
const actionShow = ref(false);
|
const actionShow = ref(false);
|
||||||
|
const needAccept = ref(false);
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
const currentRequestVersion = ++requestVersion;
|
||||||
actionShow.value = false;
|
actionShow.value = false;
|
||||||
|
needAccept.value = false;
|
||||||
transitions.value = [];
|
transitions.value = [];
|
||||||
if (props.ticket_ && props.ticket_.id) {
|
if (props.ticket_ && props.ticket_.id) {
|
||||||
ticketId.value = props.ticket_.id;
|
ticketId.value = props.ticket_.id;
|
||||||
const isParticipant =
|
const actionInfo = await API.wf.ticket.ticketAvailableActions.req(ticketId.value);
|
||||||
(props.ticket_.participant_type === 1 && props.ticket_.participant === currentUser.value) ||
|
if (currentRequestVersion !== requestVersion) {
|
||||||
(props.ticket_.participant_type === 2 && props.ticket_.participant.includes(currentUser.value))
|
return;
|
||||||
if (isParticipant) {
|
|
||||||
actionShow.value = true;
|
|
||||||
transitions.value = await API.wf.ticket.ticketTransitions.req(ticketId.value);
|
|
||||||
}
|
}
|
||||||
|
needAccept.value = actionInfo.need_accept;
|
||||||
|
actionShow.value = actionInfo.permission;
|
||||||
|
transitions.value = actionInfo.transitions;
|
||||||
}else if (props.workflow_key !=null && props.workflow_key != undefined) {
|
}else if (props.workflow_key !=null && props.workflow_key != undefined) {
|
||||||
let res = await API.wf.workflow.initkey.req(props.workflow_key);
|
let res = await API.wf.workflow.initkey.req(props.workflow_key);
|
||||||
|
if (currentRequestVersion !== requestVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
actionShow.value = true
|
actionShow.value = true
|
||||||
transitions.value = res.transitions;
|
transitions.value = res.transitions;
|
||||||
workflow.value = res.workflow
|
workflow.value = res.workflow
|
||||||
|
|
@ -79,11 +95,35 @@ const init = async () => {
|
||||||
ElMessage.error("缺少workflow_key或ticketId");
|
ElMessage.error("缺少workflow_key或ticketId");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const currentUser = ref(TOOL.data.get("USER_INFO").id)
|
// immediate watcher 注册时会同步执行回调,必须放在 init 初始化之后,
|
||||||
|
// 否则首次进入工艺步骤会在 const init 的暂时性死区内调用它。
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
props.ticket_?.id,
|
||||||
|
props.ticket_?.state,
|
||||||
|
props.ticket_?.participant,
|
||||||
|
props.ticket_?.update_time,
|
||||||
|
props.workflow_key,
|
||||||
|
],
|
||||||
|
tryInit,
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
const isSaveing = ref(false);
|
const isSaveing = ref(false);
|
||||||
|
const isAccepting = ref(false);
|
||||||
|
|
||||||
const emit = defineEmits(["success"]);
|
const emit = defineEmits(["success"]);
|
||||||
|
const acceptTicket = async () => {
|
||||||
|
isAccepting.value = true;
|
||||||
|
try {
|
||||||
|
await API.wf.ticket.ticketAccept.req(ticketId.value, {});
|
||||||
|
ElMessage.success("接单成功");
|
||||||
|
lastInitKey = null;
|
||||||
|
await init();
|
||||||
|
} finally {
|
||||||
|
isAccepting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleTransition = (item) => {
|
const handleTransition = (item) => {
|
||||||
currentTransitionId.value = item.id;
|
currentTransitionId.value = item.id;
|
||||||
if (item.attribute_type != 1) {
|
if (item.attribute_type != 1) {
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: space-between">
|
<div style="display: flex; justify-content: space-between">
|
||||||
<div>
|
<div>
|
||||||
|
<el-button
|
||||||
|
v-if="needAccept"
|
||||||
|
type="success"
|
||||||
|
:loading="acceptLoading"
|
||||||
|
:disabled="acceptLoading"
|
||||||
|
@click="handleAccept"
|
||||||
|
>接单</el-button
|
||||||
|
>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="addNode"
|
@click="addNode"
|
||||||
|
|
@ -628,6 +636,8 @@ export default {
|
||||||
dosOption: [],
|
dosOption: [],
|
||||||
handoverItem:{},
|
handoverItem:{},
|
||||||
submitLoading: false,
|
submitLoading: false,
|
||||||
|
acceptLoading: false,
|
||||||
|
needAccept: false,
|
||||||
userId: this.$TOOL.data.get("USER_INFO").id,
|
userId: this.$TOOL.data.get("USER_INFO").id,
|
||||||
isOwn: false,
|
isOwn: false,
|
||||||
isDuty: false,
|
isDuty: false,
|
||||||
|
|
@ -760,6 +770,7 @@ export default {
|
||||||
getticketItem() {
|
getticketItem() {
|
||||||
let that = this;
|
let that = this;
|
||||||
that.mainLoading = true;
|
that.mainLoading = true;
|
||||||
|
that.isDuty = false;
|
||||||
that.$API.wf.ticket.ticketItem.req(that.ticketId).then((res) => {
|
that.$API.wf.ticket.ticketItem.req(that.ticketId).then((res) => {
|
||||||
that.mainLoading = false;
|
that.mainLoading = false;
|
||||||
that.ticketDetail = res;
|
that.ticketDetail = res;
|
||||||
|
|
@ -791,11 +802,11 @@ export default {
|
||||||
if (this.ticketDetail.create_by == this.userId) {
|
if (this.ticketDetail.create_by == this.userId) {
|
||||||
this.isOwn = true;
|
this.isOwn = true;
|
||||||
}
|
}
|
||||||
let participant = this.ticketDetail.participant;
|
const participant = this.ticketDetail.participant;
|
||||||
if (
|
const participants = Array.isArray(participant)
|
||||||
participant == this.userId ||
|
? participant
|
||||||
participant.indexOf(this.userId) > -1
|
: [participant];
|
||||||
) {
|
if (participants.map(String).includes(String(this.userId))) {
|
||||||
this.isDuty = true;
|
this.isDuty = true;
|
||||||
}
|
}
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
|
@ -806,21 +817,37 @@ export default {
|
||||||
getBtns() {
|
getBtns() {
|
||||||
let that = this;
|
let that = this;
|
||||||
that.audit_imgs_show = false;
|
that.audit_imgs_show = false;
|
||||||
that.$API.wf.ticket.ticketTransitions.req(that.ticketId).then((res) => {
|
that.audit_work_show = false;
|
||||||
that.operationBtn = res;
|
that.needAccept = false;
|
||||||
|
that.$API.wf.ticket.ticketAvailableActions.req(that.ticketId).then((res) => {
|
||||||
|
that.operationBtn = res.transitions;
|
||||||
|
that.needAccept = res.need_accept;
|
||||||
|
if (res.permission || res.need_accept) {
|
||||||
|
that.isDuty = true;
|
||||||
|
}
|
||||||
console.log("operationBtn", that.operationBtn);
|
console.log("operationBtn", that.operationBtn);
|
||||||
if(res.length>0){
|
if(that.operationBtn.length>0){
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < that.operationBtn.length; i++) {
|
||||||
if(res[i].on_submit_func=="apps.opm.services.check_opl_audit_imgs"){
|
if(that.operationBtn[i].on_submit_func=="apps.opm.services.check_opl_audit_imgs"){
|
||||||
that.audit_imgs_show = true;
|
that.audit_imgs_show = true;
|
||||||
}
|
}
|
||||||
if(res[i].on_submit_func=="apps.opm.services.check_opl_work_imgs"){
|
if(that.operationBtn[i].on_submit_func=="apps.opm.services.check_opl_work_imgs"){
|
||||||
that.audit_work_show = true;
|
that.audit_work_show = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
handleAccept() {
|
||||||
|
this.acceptLoading = true;
|
||||||
|
this.$API.wf.ticket.ticketAccept.req(this.ticketId, {}).then(() => {
|
||||||
|
this.$message.success("接单成功");
|
||||||
|
this.getticketItem();
|
||||||
|
this.getBtns();
|
||||||
|
}).finally(() => {
|
||||||
|
this.acceptLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
//访客详情
|
//访客详情
|
||||||
getVisit() {
|
getVisit() {
|
||||||
this.$API.vm.visit.read.req(this.projectId).then((res) => {
|
this.$API.vm.visit.read.req(this.projectId).then((res) => {
|
||||||
|
|
|
||||||
|
|
@ -957,7 +957,7 @@ export default {
|
||||||
if(that.mlogItem.material_out_&&that.mlogItem.material_out_.tracking==10){
|
if(that.mlogItem.material_out_&&that.mlogItem.material_out_.tracking==10){
|
||||||
that.$refs.tableOut.refresh();
|
that.$refs.tableOut.refresh();
|
||||||
}else{
|
}else{
|
||||||
that.$refs.checkTable.refreshfun();
|
that.$refs.checkTable?.refreshfun?.();
|
||||||
}
|
}
|
||||||
//删除in记录后,out也要删除相应的记录
|
//删除in记录后,out也要删除相应的记录
|
||||||
});
|
});
|
||||||
|
|
@ -1060,7 +1060,7 @@ export default {
|
||||||
if(this.mlogItem.material_out_&&this.mlogItem.material_out_.tracking==10){
|
if(this.mlogItem.material_out_&&this.mlogItem.material_out_.tracking==10){
|
||||||
this.$refs.tableOut.refresh();
|
this.$refs.tableOut.refresh();
|
||||||
}else{
|
}else{
|
||||||
this.$refs.checkTable.refreshfun();
|
this.$refs.checkTable?.refreshfun?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleCheckSuccess() {
|
handleCheckSuccess() {
|
||||||
|
|
@ -1081,7 +1081,7 @@ export default {
|
||||||
if(this.mlogItem.material_out_&&this.mlogItem.material_out_.tracking==10){
|
if(this.mlogItem.material_out_&&this.mlogItem.material_out_.tracking==10){
|
||||||
this.$refs.tableOut.refresh();
|
this.$refs.tableOut.refresh();
|
||||||
}else{
|
}else{
|
||||||
this.$refs.checkTable.refreshfun();
|
this.$refs.checkTable?.refreshfun?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//编辑成功后的方法调用
|
//编辑成功后的方法调用
|
||||||
|
|
@ -1099,7 +1099,7 @@ export default {
|
||||||
if(this.mlogItem.material_out_&&this.mlogItem.material_out_.tracking==10){
|
if(this.mlogItem.material_out_&&this.mlogItem.material_out_.tracking==10){
|
||||||
this.$refs.tableOut.refresh();
|
this.$refs.tableOut.refresh();
|
||||||
}else{
|
}else{
|
||||||
this.$refs.checkTable.refreshfun();
|
this.$refs.checkTable?.refreshfun?.();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkDialogClose(){
|
checkDialogClose(){
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue