瀏覽代碼

新增计划布点的代码

695593266@qq.com 4 月之前
父節點
當前提交
4eb0af7038

+ 19 - 0
src/api/productionPlan/planDotLine.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request';
+
+//工序布点信息
+export async function getPlanDotLine(data) {
+  const res = await request.post('/aps/planrouting/detailList', data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}
+
+//保存工序布点信息
+export async function savePlanDotLine(data) {
+  const res = await request.post('/aps/planrouting/save', data);
+  if (res.data.code == 0) {
+    return res.data.data;
+  }
+  return Promise.reject(new Error(res.data.message));
+}

+ 274 - 0
src/views/bpm/processInstance/detail.vue

@@ -0,0 +1,274 @@
+<template>
+
+    <div class="app-container" style="padding: 15px">
+      <!-- 审批记录 -->
+      <el-card class="box-card" v-loading="tasksLoad">
+        <div slot="header" class="clearfix">
+          <span class="el-icon-picture-outline">审批记录</span>
+        </div>
+        <el-col :span="16" :offset="4">
+          <div class="block">
+            <el-timeline>
+              <el-timeline-item
+                v-for="(item, index) in tasks"
+                :key="index"
+                :icon="getTimelineItemIcon(item)"
+                :type="getTimelineItemType(item)"
+              >
+                <p style="font-weight: 700">任务:{{ item.name }}</p>
+                <el-card :body-style="{ padding: '10px' }">
+                  <label
+                    v-if="item.assigneeUser"
+                    style="font-weight: normal; margin-right: 30px"
+                  >
+                    审批人:{{ item.assigneeUser.nickname }}
+                    <el-tag type="info" size="mini">{{
+                      item.assigneeUser.deptName
+                    }}</el-tag>
+                  </label>
+                  <label style="font-weight: normal" v-if="item.createTime"
+                    >创建时间:</label
+                  >
+                  <label style="color: #8a909c; font-weight: normal">{{
+                    item.createTime
+                  }}</label>
+                  <label
+                    v-if="item.endTime"
+                    style="margin-left: 30px; font-weight: normal"
+                    >审批时间:</label
+                  >
+                  <label
+                    v-if="item.endTime"
+                    style="color: #8a909c; font-weight: normal"
+                  >
+                    {{ item.endTime }}</label
+                  >
+                  <label
+                    v-if="item.durationInMillis"
+                    style="margin-left: 30px; font-weight: normal"
+                    >耗时:</label
+                  >
+                  <label
+                    v-if="item.durationInMillis"
+                    style="color: #8a909c; font-weight: normal"
+                  >
+                    {{ getDateStar(item.durationInMillis) }}
+                  </label>
+                  <p v-if="item.reason">
+                    <el-tag :type="getTimelineItemType(item)">{{
+                      item.reason
+                    }}</el-tag>
+                  </p>
+                </el-card>
+              </el-timeline-item>
+            </el-timeline>
+          </div>
+        </el-col>
+      </el-card>
+
+      <!-- 高亮流程图 -->
+      <el-card class="box-card" v-loading="processInstanceLoading">
+        <div slot="header" class="clearfix">
+          <span class="el-icon-picture-outline">流程图</span>
+        </div>
+        <my-process-viewer
+          key="designer"
+          v-model="bpmnXML"
+          v-bind="bpmnControlForm"
+          :activityData="activityList"
+          :processInstanceData="processInstance"
+          :taskData="tasks"
+        />
+      </el-card>
+    </div>
+
+</template>
+
+<script>
+  import {
+    getProcessDefinitionBpmnXML,
+    getProcessInstance,
+    getTaskListByProcessInstanceId,
+    getActivityList
+  } from '@/api/bpm/index';
+  import store from '@/store';
+  import { getDate } from '@/utils/dateUtils';
+  import dictMixins from '@/mixins/dictMixins';
+  // import Vue from 'vue';
+
+  // 流程实例的详情页,可用于审批
+  export default {
+    name: 'ProcessInstanceDetail',
+    mixins: [dictMixins],
+    props: {
+      id: {
+        default: ''
+      }
+    },
+    components: {},
+    data() {
+      return {
+        // 遮罩层
+        processInstanceLoading: true,
+        dialogVisible: false,
+
+        // 流程实例
+        // id: undefined, // 流程实例的编号
+        processInstance: {},
+
+        // BPMN 数据
+        bpmnXML: null,
+        bpmnControlForm: {
+          prefix: 'flowable'
+        },
+        activityList: [],
+
+        // 审批记录
+        tasksLoad: true,
+        tasks: []
+      };
+    },
+    created() {
+
+      this.getDetail();
+    },
+    methods: {
+ 
+      /** 获得流程实例 */
+      getDetail() {
+        // 获得流程实例相关
+        this.processInstanceLoading = true;
+        getProcessInstance(this.id).then((response) => {
+          if (!response) {
+            this.$message.error('查询不到流程信息!');
+            return;
+          }
+          // 设置流程信息
+          this.processInstance = response;
+
+          // //将业务表单,注册为动态组件
+          // const path = this.processInstance.processDefinition.formCustomViewPath;
+          // Vue.component("async-biz-form-component", function (resolve) {
+          //   require([`@/views${path}`], resolve);
+          // });
+
+          // 加载流程图
+          getProcessDefinitionBpmnXML(
+            this.processInstance.processDefinition.id
+          ).then((response) => {
+            this.bpmnXML = response;
+          });
+          // 加载活动列表
+          getActivityList({
+            processInstanceId: this.processInstance.id
+          }).then((response) => {
+            console.log(response, 'response');
+            this.activityList = response;
+          });
+
+          // 取消加载中
+          this.processInstanceLoading = false;
+        });
+
+        // 获得流程任务列表(审批记录)
+        this.tasksLoad = true;
+        getTaskListByProcessInstanceId(this.id).then((response) => {
+          // 审批记录
+          this.tasks = [];
+          // 移除已取消的审批
+          response.forEach((task) => {
+            if (task.result !== 4) {
+              this.tasks.push(task);
+            }
+          });
+          // 排序,将未完成的排在前面,已完成的排在后面;
+          this.tasks.sort((a, b) => {
+            // 有已完成的情况,按照完成时间倒序
+            if (a.endTime && b.endTime) {
+              return b.endTime - a.endTime;
+            } else if (a.endTime) {
+              return 1;
+            } else if (b.endTime) {
+              return -1;
+              // 都是未完成,按照创建时间倒序
+            } else {
+              return b.createTime - a.createTime;
+            }
+          });
+
+          // 需要审核的记录
+          const userId = store.getters.userId;
+          this.tasks.forEach((task) => {
+            if (task.result !== 1 && task.result !== 6) {
+              // 只有待处理才需要
+              return;
+            }
+            if (!task.assigneeUser || task.assigneeUser.id !== userId) {
+              // 自己不是处理人
+              return;
+            }
+          });
+
+          // 取消加载中
+          this.tasksLoad = false;
+        });
+      },
+      getDateStar(ms) {
+        return getDate(ms);
+      },
+      getTimelineItemIcon(item) {
+        if (item.result === 1) {
+          return 'el-icon-time';
+        }
+        if (item.result === 2) {
+          return 'el-icon-check';
+        }
+        if (item.result === 3) {
+          return 'el-icon-close';
+        }
+        if (item.result === 4) {
+          return 'el-icon-remove-outline';
+        }
+        if (item.result === 5) {
+          return 'el-icon-back';
+        }
+        return '';
+      },
+      getTimelineItemType(item) {
+        if (item.result === 1) {
+          return 'primary';
+        }
+        if (item.result === 2) {
+          return 'success';
+        }
+        if (item.result === 3) {
+          return 'danger';
+        }
+        if (item.result === 4) {
+          return 'info';
+        }
+        if (item.result === 5) {
+          return 'warning';
+        }
+        if (item.result === 6) {
+          return 'default';
+        }
+        return '';
+      },
+      handleClose() {
+        this.dialogVisible = false;
+      }
+    }
+  };
+</script>
+
+<style lang="scss">
+  .my-process-designer {
+    height: calc(100vh - 200px);
+  }
+
+  .box-card {
+    width: 100%;
+    margin-bottom: 20px;
+  }
+</style>

+ 28 - 4
src/views/productionPlan/components/detail/plan.vue

@@ -130,9 +130,7 @@
           <div class="progress-box">
             <div class="gress">
               <el-progress
-                :percentage="
-                  +(((row.deliveryNum || 0) / row.contractNum) * 100).toFixed(2)
-                "
+                :percentage="getProgressPercentage(row)"
                 color="red"
                 :show-text="false"
                 text-color="#000"
@@ -176,7 +174,33 @@
         return this.infoData.productRequirementInfo || {};
       },
       salesOrderList() {
-        return this.infoData.salesOrderList[0] || {};
+        const list = this.infoData.salesOrderList || [];
+        return list[0] || {};
+      }
+    },
+    methods: {
+      normalizeNumber(value, fallback = 0) {
+        if (typeof value === 'number') {
+          return Number.isFinite(value) ? value : fallback;
+        }
+        if (typeof value === 'string') {
+          const parsed = Number(value.replace(/,/g, '').trim());
+          return Number.isFinite(parsed) ? parsed : fallback;
+        }
+        return fallback;
+      },
+      getProgressPercentage(row = {}) {
+        const deliveryNum = this.normalizeNumber(row.deliveryNum, 0);
+        const contractNum = this.normalizeNumber(row.contractNum, 0);
+        if (contractNum <= 0) {
+          return 0;
+        }
+        const percent = (deliveryNum / contractNum) * 100;
+        if (!Number.isFinite(percent)) {
+          return 0;
+        }
+        const bounded = Math.min(Math.max(percent, 0), 100);
+        return +bounded.toFixed(2);
       }
     },
     data() {

+ 343 - 212
src/views/productionPlan/components/planDotLine.vue

@@ -22,16 +22,19 @@
         <div v-else class="route-line">
           <div
             v-for="(item, index) in taskList"
-            :key="`route-${item._taskKey}`"
-            class="route-item"
+            :key="`route-seg-${item._taskKey}`"
+            class="route-segment"
           >
             <span
               class="route-node"
-              :class="{ 'route-node--done': isConfigured(item.dotLineConfig) }"
+              :class="{ 'route-node--done': isRouteItemDone(item) }"
             >
-              {{ item.name || item.taskName || `工艺${index + 1}` }}
+              {{ item.taskName || item.name || `工艺${index + 1}` }}
             </span>
-            <span v-if="index < taskList.length - 1" class="route-arrow"
+            <span
+              v-if="index < taskList.length - 1"
+              class="route-arrow"
+              aria-hidden="true"
               >→</span
             >
           </div>
@@ -48,7 +51,7 @@
             :data="taskList"
             border
             size="small"
-            :row-key="getRowKey"
+            row-key="_taskKey"
             max-height="420"
             class="config-table"
           >
@@ -57,35 +60,46 @@
               label="序号"
               width="48"
               align="center"
-              header-align="center"
             />
             <el-table-column
-              label="工序"
-              min-width="90"
+              label="工序名称"
+              min-width="100"
               show-overflow-tooltip
               class-name="task-name-cell"
               align="center"
-              header-align="center"
             >
               <template slot-scope="{ row, $index }">
                 <span class="task-name-text">{{
-                  row.name || row.taskName || `工艺${$index + 1}`
+                  row.taskName || row.name || `工艺${$index + 1}`
                 }}</span>
               </template>
             </el-table-column>
-            <el-table-column
-              label="班组"
-              min-width="130"
-              align="center"
-              header-align="center"
-            >
+            <el-table-column label="执行模式" min-width="100" align="center">
               <template slot-scope="{ row }">
                 <el-select
-                  v-model="row.dotLineConfig.teamId"
+                  v-model="row.executionType"
+                  placeholder="执行模式"
+                  clearable
+                  class="config-table-control"
+                >
+                  <el-option
+                    v-for="opt in executionTypeOptions"
+                    :key="opt.value"
+                    :label="opt.label"
+                    :value="opt.value"
+                  />
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column label="执行班组" min-width="130" align="center">
+              <template slot-scope="{ row }">
+                <el-select
+                  v-model="row.executionTeamId"
                   placeholder="请选择班组"
                   clearable
                   filterable
                   class="config-table-control"
+                  @change="onExecutionTeamChange(row)"
                 >
                   <el-option
                     v-for="team in teamOptions"
@@ -97,61 +111,37 @@
               </template>
             </el-table-column>
             <el-table-column
-              label="开始时间"
+              label="执行开始时间"
               min-width="168"
               align="center"
-              header-align="center"
             >
               <template slot-scope="{ row }">
                 <el-date-picker
-                  v-model="row.dotLineConfig.startTime"
+                  v-model="row.executionStartTime"
                   type="datetime"
                   value-format="yyyy-MM-dd HH:mm:ss"
-                  placeholder="开始时间"
+                  placeholder="执行开始时间"
                   class="config-table-control"
-                  @change="handleTimeChange(row, 'startTime')"
+                  @change="handleTimeChange(row, 'executionStartTime')"
                 />
               </template>
             </el-table-column>
             <el-table-column
-              label="结束时间"
+              label="执行结束时间"
               min-width="168"
               align="center"
-              header-align="center"
             >
               <template slot-scope="{ row }">
                 <el-date-picker
-                  v-model="row.dotLineConfig.endTime"
+                  v-model="row.executionEndTime"
                   type="datetime"
                   value-format="yyyy-MM-dd HH:mm:ss"
-                  placeholder="结束时间"
+                  placeholder="执行结束时间"
                   class="config-table-control"
-                  @change="handleTimeChange(row, 'endTime')"
+                  @change="handleTimeChange(row, 'executionEndTime')"
                 />
               </template>
             </el-table-column>
-            <el-table-column
-              label="类型"
-              min-width="100"
-              align="center"
-              header-align="center"
-            >
-              <template slot-scope="{ row }">
-                <el-select
-                  v-model="row.dotLineConfig.type"
-                  placeholder="请选择类型"
-                  clearable
-                  class="config-table-control"
-                >
-                  <el-option
-                    v-for="type in typeOptions"
-                    :key="type.value"
-                    :label="type.label"
-                    :value="type.value"
-                  />
-                </el-select>
-              </template>
-            </el-table-column>
           </el-table>
         </div>
       </div>
@@ -164,170 +154,282 @@
 </template>
 
 <script>
+  import {
+    getPlanDotLine,
+    savePlanDotLine
+  } from '@/api/productionPlan/planDotLine';
   import { getTaskListById } from '@/api/materialPlan';
   import { teamPage } from '@/api/mainData';
+
+  const EXEC_TYPE = Object.freeze({ HOMEMADE: 0, ENTRUST: 1, OUTSOURCE: 2 });
+
+  const EXEC_TYPE_OPTIONS = Object.freeze([
+    { label: '自制', value: EXEC_TYPE.HOMEMADE },
+    { label: '请托', value: EXEC_TYPE.ENTRUST },
+    { label: '委外', value: EXEC_TYPE.OUTSOURCE }
+  ]);
+
+  function toSafeNumber(val) {
+    if (val === '' || val == null) return undefined;
+    const n = Number(val);
+    return Number.isNaN(n) ? undefined : n;
+  }
+
+  function formatDateTime(val) {
+    if (val == null || val === '') return '';
+    const str = String(val).trim();
+    if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(str)) return str;
+    const d = new Date(val);
+    if (Number.isNaN(d.getTime())) return '';
+    const p = (n) => String(n).padStart(2, '0');
+    return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
+      d.getHours()
+    )}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
+  }
+
+  /** 比较两个时间字符串,结束 < 开始 则返回 true */
+  function isEndBeforeStart(startStr, endStr) {
+    if (!startStr || !endStr) return false;
+    const s = new Date(startStr).getTime();
+    const e = new Date(endStr).getTime();
+    return !Number.isNaN(s) && !Number.isNaN(e) && e < s;
+  }
+
   export default {
     data() {
       return {
         visible: false,
         dialogVisible: false,
-        title: '计划描点',
+        title: '计划点',
         taskList: [],
         teamOptions: [],
         currentPlan: null,
-        typeOptions: [
-          { label: '自制', value: '1' },
-          { label: '请托', value: '2' },
-          { label: '委外', value: '3' }
-        ],
-        // 与 typeOptions 一致:自制 / 请托 / 委外
-        TYPE_HOMEMADE: '1',
-        TYPE_ENTRUST: '2',
-        TYPE_OUTSOURCE: '3'
+        planRoutingPayload: null,
+        executionTypeOptions: EXEC_TYPE_OPTIONS
       };
     },
     methods: {
-      getRowKey(row) {
-        return row._taskKey;
-      },
       async open(data) {
         this.currentPlan = data || null;
+        this.planRoutingPayload = null;
         await Promise.all([this.loadTaskList(), this.loadTeamOptions()]);
-        this.fillSavedTaskConfig();
         this.visible = true;
         this.dialogVisible = true;
       },
+
       async loadTaskList() {
-        if (!this.currentPlan || !this.currentPlan.produceRoutingId) {
-          this.taskList = [];
+        this.taskList = [];
+        this.planRoutingPayload = null;
+        if (!this.currentPlan?.id) return;
+
+        let planData;
+        try {
+          planData = await getPlanDotLine({ planId: this.currentPlan.id });
+        } catch (e) {
+          this.$message.error(e.message || '获取工序布点信息失败');
           return;
         }
-        const list = await getTaskListById(this.currentPlan.produceRoutingId);
-        this.taskList = (list || []).map((item, index) => ({
+
+        this.planRoutingPayload = planData || {};
+        const details = planData?.detailList;
+
+        if (details?.length) {
+          this.taskList = [...details]
+            .sort((a, b) => (a.taskSort ?? 0) - (b.taskSort ?? 0))
+            .map((item, i) => this.normalizeDetailRow(item, i));
+          return;
+        }
+
+        if (!this.currentPlan.produceRoutingId) {
+          this.$message.warning('暂无布点明细且缺少工艺路线,无法加载工序');
+          return;
+        }
+        try {
+          const list = await getTaskListById(this.currentPlan.produceRoutingId);
+          this.taskList = (list || []).map((item, i) =>
+            this.normalizeRoutingTaskRow(item, i)
+          );
+        } catch (e) {
+          this.$message.error(e.message || '获取工序列表失败');
+        }
+      },
+
+      normalizeDetailRow(item, index) {
+        return {
           ...item,
-          _taskKey: item.id || item.taskId || item.sourceTaskId || index,
-          dotLineConfig: {
-            teamId: '',
-            startTime: '',
-            endTime: '',
-            type: this.TYPE_HOMEMADE
-          }
-        }));
+          _taskKey: item.id ?? `detail-${item.taskId ?? index}`,
+          executionStartTime: formatDateTime(item.executionStartTime),
+          executionEndTime: formatDateTime(item.executionEndTime),
+          executionType: toSafeNumber(item.executionType) ?? EXEC_TYPE.HOMEMADE,
+          executionTeamId: item.executionTeamId ?? '',
+          executionTeamLeader: item.executionTeamLeader ?? '',
+          executionTeamLeaderId: item.executionTeamLeaderId ?? '',
+          executionTeamName: item.executionTeamName ?? ''
+        };
+      },
+
+      normalizeRoutingTaskRow(item, index) {
+        const plan = this.currentPlan;
+        const taskId = item.taskId ?? item.sourceTaskId ?? item.id;
+        return {
+          ...item,
+          _taskKey: item.id ?? `new-${taskId ?? index}-${index}`,
+          taskId: taskId ?? undefined,
+          taskName: item.name ?? item.taskName,
+          taskCode: item.code ?? item.taskCode,
+          taskSort: item.taskSort ?? index + 1,
+          type: item.type ?? undefined,
+          sourceTaskId: item.sourceTaskId ?? taskId ?? index,
+          planId: plan.id,
+          planCode: plan.code,
+          routingId: plan.produceRoutingId,
+          routingName: plan.produceRoutingName ?? plan.routingName,
+          executionType: EXEC_TYPE.HOMEMADE,
+          executionTeamId: '',
+          executionTeamLeader: '',
+          executionTeamLeaderId: '',
+          executionTeamName: '',
+          executionStartTime: '',
+          executionEndTime: ''
+        };
+      },
+
+      onExecutionTeamChange(row) {
+        const team = this.teamOptions.find((t) => t.id === row.executionTeamId);
+        row.executionTeamName = team?.name ?? '';
+        row.executionTeamLeader = team?.leaderUserName ?? '';
+        row.executionTeamLeaderId = team?.leaderUserId ?? '';
       },
+
       async loadTeamOptions() {
         try {
-          const res = await teamPage({ size: 500 });
-          const list = res?.list || [];
-          this.teamOptions = list.map((team) => ({
-            id: team.id,
-            name: team.name
-          }));
-        } catch (error) {
+          const factoryId = this.$store.state.user.info.factoryId;
+          const res = await teamPage({ pageNum: 1, size: -1, factoryId });
+          this.teamOptions = (res?.list || []).map(
+            ({ id, name, leaderUserName, leaderUserId }) => ({
+              id,
+              name,
+              leaderUserName,
+              leaderUserId
+            })
+          );
+        } catch {
           this.teamOptions = [];
         }
       },
-      fillSavedTaskConfig() {
-        const savedList = this.currentPlan?.dotLineTaskList || [];
-        if (!savedList.length) {
-          return;
-        }
-        const map = {};
-        savedList.forEach((item) => {
-          map[item._taskKey] = item.dotLineConfig || {};
-        });
-        this.taskList = this.taskList.map((item) => {
-          const merged = {
-            ...item.dotLineConfig,
-            ...(map[item._taskKey] || {})
-          };
-          if (!merged.type) {
-            merged.type = this.TYPE_HOMEMADE;
-          }
-          return {
-            ...item,
-            dotLineConfig: merged
-          };
-        });
-      },
-      isConfigured(config) {
-        if (!config) {
-          return false;
-        }
-        return !!(
-          config.teamId ||
-          config.startTime ||
-          config.endTime ||
-          config.type
-        );
+
+      isRouteItemDone(row) {
+        return row.executionType != null && row.executionType !== '';
       },
-      handleTimeChange(item, changeKey) {
-        const cfg = item?.dotLineConfig || {};
-        if (!cfg.startTime || !cfg.endTime) {
-          return;
-        }
-        const start = new Date(cfg.startTime).getTime();
-        const end = new Date(cfg.endTime).getTime();
-        if (!Number.isNaN(start) && !Number.isNaN(end) && end < start) {
-          this.$message.warning('结束时间不能小于开始时间');
-          this.$set(cfg, changeKey, '');
+
+      handleTimeChange(row, changeKey) {
+        if (isEndBeforeStart(row.executionStartTime, row.executionEndTime)) {
+          this.$message.warning('执行结束时间不能小于执行开始时间');
+          this.$set(row, changeKey, '');
         }
       },
-      handleSave() {
-        const { TYPE_HOMEMADE, TYPE_ENTRUST, TYPE_OUTSOURCE } = this;
-        const taskLabel = (task) => task.name || task.taskName || '当前工艺';
 
-        for (const item of this.taskList) {
-          const cfg = item.dotLineConfig || {};
-          if (!cfg.type) {
-            this.$set(item.dotLineConfig, 'type', TYPE_HOMEMADE);
-          }
-        }
+      validateTaskList() {
+        const label = (t) => t.taskName || t.name || '当前工艺';
 
         for (const item of this.taskList) {
-          const cfg = item.dotLineConfig || {};
-          const type = cfg.type || TYPE_HOMEMADE;
+          if (item.executionType == null || item.executionType === '') {
+            this.$set(item, 'executionType', EXEC_TYPE.HOMEMADE);
+          }
+          const execType = Number(item.executionType);
 
-          if (type === TYPE_HOMEMADE) {
-            if (!cfg.teamId || !cfg.startTime || !cfg.endTime) {
-              this.$message.warning(
-                `${taskLabel(item)}类型为自制时,需填写班组、开始时间和结束时间`
-              );
-              return;
-            }
-            const start = new Date(cfg.startTime).getTime();
-            const end = new Date(cfg.endTime).getTime();
-            if (Number.isNaN(start) || Number.isNaN(end)) {
-              this.$message.warning(`${taskLabel(item)}开始时间或结束时间无效`);
-              return;
-            }
-            if (end < start) {
+          if (execType === EXEC_TYPE.HOMEMADE) {
+            if (
+              !item.executionTeamId ||
+              !item.executionStartTime ||
+              !item.executionEndTime
+            ) {
               this.$message.warning(
-                `${taskLabel(item)}结束时间不能小于开始时间`
+                `${label(
+                  item
+                )}执行模式为自制时,需填写执行班组、执行开始和结束时间`
               );
-              return;
-            }
-          } else if (type === TYPE_ENTRUST || type === TYPE_OUTSOURCE) {
-            if (cfg.startTime && cfg.endTime) {
-              const start = new Date(cfg.startTime).getTime();
-              const end = new Date(cfg.endTime).getTime();
-              if (!Number.isNaN(start) && !Number.isNaN(end) && end < start) {
-                this.$message.warning(
-                  `${taskLabel(item)}结束时间不能小于开始时间`
-                );
-                return;
-              }
+              return false;
             }
           }
+
+          if (
+            isEndBeforeStart(item.executionStartTime, item.executionEndTime)
+          ) {
+            this.$message.warning(
+              `${label(item)}执行结束时间不能小于执行开始时间`
+            );
+            return false;
+          }
         }
-        if (this.currentPlan) {
-          this.$set(this.currentPlan, 'dotLineTaskList', this.taskList);
-        }
-        this.$emit('save', {
-          plan: this.currentPlan,
-          taskList: this.taskList
+        return true;
+      },
+
+      buildSavePayload() {
+        const plan = this.currentPlan;
+        const head = this.planRoutingPayload || {};
+
+        const detailList = this.taskList.map((row) => {
+          const detail = {
+            executionEndTime: row.executionEndTime || undefined,
+            executionStartTime: row.executionStartTime || undefined,
+            executionTeamId: row.executionTeamId || undefined,
+            executionTeamLeader: row.executionTeamLeader || '',
+            executionTeamLeaderId: row.executionTeamLeaderId || '',
+            executionTeamName: row.executionTeamName || '',
+            executionType: toSafeNumber(row.executionType) ?? 0,
+            planCode: row.planCode || plan?.code,
+            planId: row.planId ?? plan?.id,
+            routingId:
+              row.routingId ?? plan?.produceRoutingId ?? head.routingId,
+            routingName:
+              row.routingName || plan?.produceRoutingName || head.routingName,
+            sourceTaskId: row.sourceTaskId ?? row.taskId,
+            taskCode: row.taskCode,
+            taskId: row.taskId,
+            taskName: row.taskName,
+            taskSort: row.taskSort,
+            type: toSafeNumber(row.type)
+          };
+          if (row.id != null) detail.id = row.id;
+          return detail;
         });
-        this.$message.success('保存成功');
-        this.onModalClose();
+
+        return {
+          detailList,
+          fileParam: head.fileParam ?? {},
+          id: head.id,
+          planCode: plan?.code ?? head.planCode,
+          planId: plan?.id ?? head.planId,
+          produceVersionId: head.produceVersionId,
+          produceVersionName: head.produceVersionName,
+          releaseTime: head.releaseTime,
+          routingCode: head.routingCode,
+          routingId: head.routingId ?? plan?.produceRoutingId,
+          routingName: head.routingName ?? plan?.produceRoutingName,
+          routingVersion: head.routingVersion
+        };
+      },
+
+      async handleSave() {
+        if (!this.validateTaskList()) return;
+
+        const payload = this.buildSavePayload();
+        const loading = this.$loading({ lock: true, text: '保存中...' });
+        try {
+          await savePlanDotLine(payload);
+          this.$emit('save', {
+            plan: this.currentPlan,
+            taskList: this.taskList,
+            payload
+          });
+          this.$message.success('保存成功');
+          this.onModalClose();
+        } catch (e) {
+          this.$message.error(e.message || '保存失败');
+        } finally {
+          loading.close();
+        }
       },
 
       onModalClose() {
@@ -335,15 +437,25 @@
         this.dialogVisible = false;
         this.taskList = [];
         this.currentPlan = null;
+        this.planRoutingPayload = null;
       }
     }
   };
 </script>
 
 <style lang="scss" scoped>
+  .plan-dot-line,
+  .top-route,
+  .config-panel,
+  .route-line {
+    width: 100%;
+    max-width: 100%;
+    min-width: 0;
+    box-sizing: border-box;
+  }
+
   .plan-dot-line {
     min-height: 360px;
-    display: block;
   }
 
   .top-route,
@@ -366,32 +478,37 @@
 
   .route-line {
     display: flex;
-    flex-wrap: wrap;
-    align-items: center;
-    row-gap: 4px;
+    flex-wrap: nowrap;
+    align-items: stretch;
     color: #303133;
   }
 
-  .route-item {
-    display: inline-flex;
+  .route-segment {
+    flex: 1 1 0;
+    min-width: 0;
+    display: flex;
     align-items: center;
-    font-size: 12px;
+    justify-content: center;
   }
 
   .route-node {
-    display: inline-flex;
+    flex: 0 1 auto;
+    display: flex;
     align-items: center;
     justify-content: center;
-    min-width: 38px;
-    height: 20px;
+    min-width: 0;
+    min-height: 22px;
     border-radius: 4px;
-    padding: 0 7px;
-    color: #ffffff;
+    padding: 2px 10px;
+    color: #fff;
     background: #909399;
-    font-size: 12px;
+    font-size: clamp(10px, 2.6vw, 12px);
     font-weight: 600;
-    line-height: 1;
-    box-sizing: border-box;
+    line-height: 1.2;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    text-align: center;
   }
 
   .route-node--done {
@@ -399,38 +516,52 @@
   }
 
   .route-arrow {
-    margin: 0 5px;
+    flex: 1 1 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
     color: #909399;
-    font-size: 12px;
+    font-size: clamp(14px, 2.5vw, 18px);
     line-height: 1;
   }
 
-  .task-config-table-wrap {
-    width: 100%;
-  }
-
-  .config-table ::v-deep .el-table th > .cell,
-  .config-table ::v-deep .el-table td > .cell {
-    font-size: 14px;
-    text-align: center;
-  }
-
-  .config-table ::v-deep .el-table__body .cell {
-    padding-left: 6px;
-    padding-right: 6px;
-  }
-
-  .config-table ::v-deep td.task-name-cell .task-name-text {
-    font-size: 14px;
-    font-weight: 500;
-    line-height: 1.4;
+  @media (max-width: 576px) {
+    .top-route,
+    .config-panel {
+      padding: 8px;
+    }
+    .route-node {
+      padding: 2px 4px;
+    }
+    .route-arrow {
+      font-size: 12px;
+    }
   }
 
-  .config-table ::v-deep .config-table-control {
-    width: 100% !important;
+  .task-config-table-wrap {
+    width: 100%;
   }
 
-  .config-table ::v-deep .config-table-control.el-date-editor {
-    max-width: 100%;
+  .config-table ::v-deep {
+    .el-table th > .cell,
+    .el-table td > .cell {
+      font-size: 14px;
+      text-align: center;
+    }
+    .el-table__body .cell {
+      padding-left: 6px;
+      padding-right: 6px;
+    }
+    td.task-name-cell .task-name-text {
+      font-size: 14px;
+      font-weight: 500;
+      line-height: 1.4;
+    }
+    .config-table-control {
+      width: 100% !important;
+    }
+    .config-table-control.el-date-editor {
+      max-width: 100%;
+    }
   }
 </style>

+ 36 - 31
src/views/productionPlan/detail.vue

@@ -14,6 +14,9 @@
             :planId="infoData.productionPlan && infoData.productionPlan.id"
           />
         </el-tab-pane> -->
+        <el-tab-pane label="流程信息" name="bpmDetail" v-if="processInstanceId">
+          <bpmDetail :id="processInstanceId" />
+        </el-tab-pane>
         <el-tab-pane
           label="生产详情表"
           name="productionDetail"
@@ -33,39 +36,41 @@
 </template>
 
 <script>
-import plan from './components/detail/plan.vue';
-import material from './components/detail/material.vue';
-import productionDetail from './components/detail/productionDetail.vue';
-// import prod from './components/detail/prod.vue';
-import { getProductPlanDetail } from '@/api/productionPlan/index';
-export default {
-  components: { plan, material, productionDetail },
-  data() {
-    return {
-      activeName: 'plan',
-      infoData: {}
-    };
-  },
-  created() {
-    this.getDetail();
-  },
-  computed: {
-    clientEnvironmentId() {
-      return this.$store.state.user.info.clientEnvironmentId;
-    }
-  },
-  methods: {
-    async getDetail() {
-      const data = await getProductPlanDetail(this.$route.query.id);
-   
-      this.infoData = data;
+  import plan from './components/detail/plan.vue';
+  import material from './components/detail/material.vue';
+  import productionDetail from './components/detail/productionDetail.vue';
+  // import prod from './components/detail/prod.vue';
+  import bpmDetail from '@/views/bpm/processInstance/detail.vue';
+  import { getProductPlanDetail } from '@/api/productionPlan/index';
+  export default {
+    components: { plan, material, productionDetail, bpmDetail },
+    data() {
+      return {
+        activeName: 'plan',
+        infoData: {},
+        processInstanceId: this.$route.query.processInstanceId
+      };
+    },
+    created() {
+      this.getDetail();
+    },
+    computed: {
+      clientEnvironmentId() {
+        return this.$store.state.user.info.clientEnvironmentId;
+      }
+    },
+    methods: {
+      async getDetail() {
+        const data = await getProductPlanDetail(this.$route.query.id);
+
+        this.infoData = data;
+      }
     }
-  }
-};
+  };
 </script>
 
 <style lang="scss" scoped>
-.pane-box {
-  padding: 20px 0;
-}
+  .pane-box {
+    padding: 20px 0;
+  }
 </style>

+ 10 - 25
src/views/productionPlan/index.vue

@@ -151,9 +151,9 @@
           >变更申请</el-button
         >
 
-        <el-button type="danger" size="mini" @click="planDotLine"
-          >计划点</el-button
-        >
+        <!-- <el-button type="danger" size="mini" @click="planDotLine"
+          >计划点</el-button
+        > -->
         <!-- <el-button
           class="my-btn"
           size="mini"
@@ -377,13 +377,8 @@
             拆批
           </el-link>
 
-          <el-link
-            type="primary"
-            :underline="false"
-            @click="process(row)"
-            v-if="row.processInstanceId"
-          >
-            流程
+          <el-link type="primary" :underline="false" @click="planDotLine(row)">
+            计划布点
           </el-link>
 
           <el-link
@@ -1311,19 +1306,8 @@
         }
       },
 
-      planDotLine() {
-        if (this.selection.length == 0) {
-          return this.$message.warning('请选择一个计划!');
-        }
-
-        if (this.selection[0].approvalStatus == 1) {
-          return this.$message.warning('该计划正在审核中!');
-        }
-
-        if (this.selection.length > 1) {
-          return this.$message.warning('计划描点只能选择一条计划!');
-        }
-        this.$refs.planDotLineRef.open(this.selection[0]);
+      planDotLine(row) {
+        this.$refs.planDotLineRef.open(row);
       },
 
       selectionFilter(row) {
@@ -1335,10 +1319,11 @@
         this.$refs.checkProductionPreparationsRef.open(item);
       },
 
-      goDetail({ id }) {
+      goDetail({ id, processInstanceId }) {
+        console.log(id, processInstanceId, 'id, processInstanceId');
         this.$router.push({
           path: '/productionPlan/detail',
-          query: { id }
+          query: { id, processInstanceId }
         });
       },