huminmin
5 小时以前 a215940f3d3f52a1404317c3bb88da2ff12c5fc0
src/views/productionManagement/productionOrder/Detail/index.vue
@@ -36,21 +36,36 @@
                  <div
                    class="step-title"
                    :class="{ selected: idx === selectedIndex }"
                    @click="selectProcess(idx)"
                    @click.stop="selectProcess(idx)"
                  >
                    {{ `${idx + 1}. ${p.processName || "-"}` }}
                  </div>
                </template>
                <template #description>
                  <div class="step-panel">
                    <div v-if="idx === active" class="current-progress">
                  <div
                    class="step-panel"
                    :class="{
                      'step-panel-selected': idx === selectedIndex,
                      'step-panel-current': idx === active,
                    }"
                    @click="selectProcess(idx)"
                  >
                    <span
                      v-if="p?.status"
                      class="step-status-badge"
                      :class="`step-status-badge-${p.status}`"
                    >
                      {{ statusTagText(p.status) }}
                    </span>
                    <div v-if="p.status !== 'wait'" class="current-progress">
                      <div class="current-progress-head">
                        <span class="current-progress-title">当前工序进度</span>
                        <!-- <span class="current-progress-value">{{ currentProcessPercentage }}%</span> -->
                        <span class="current-progress-title">工序进度</span>
                        <!-- <span class="current-progress-value">{{ processPercentage(p) }}%</span> -->
                      </div>
                      <el-progress
                        :percentage="currentProcessPercentage"
                        :status="currentProcessPercentage >= 100 ? 'success' : ''"
                        :percentage="processPercentage(p)"
                        :color="progressColor(processPercentage(p))"
                        :status="processPercentage(p) >= 100 ? 'success' : ''"
                        :stroke-width="10"
                      />
                    </div>
@@ -99,31 +114,83 @@
            </div>
            <div v-if="!selectedProcess" class="right-empty">
              点击左侧某个工序,右侧展示该工序的报工明细。
              暂无工序数据。
            </div>
            <div v-else class="right-content">
              <el-table :data="mockReports" border height="420">
              <el-table :data="reports" border height="420" v-loading="reportLoading">
                <el-table-column label="序号" type="index" width="60" align="center" />
                <el-table-column label="报工单号" prop="reportNo" min-width="140" show-overflow-tooltip />
                <el-table-column label="报工人员" prop="reportUser" min-width="120" show-overflow-tooltip />
                <el-table-column label="报工时间" prop="reportTime" min-width="160" show-overflow-tooltip />
                <el-table-column label="产出数量" prop="outputQty" min-width="110" />
                <el-table-column label="不良数量" prop="badQty" min-width="110" />
                <el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
                <el-table-column label="报工单号" prop="productNo" min-width="140" show-overflow-tooltip />
                <el-table-column label="报工人员" prop="nickName" min-width="120" show-overflow-tooltip />
                <el-table-column label="报工时间" prop="createTime" min-width="160" show-overflow-tooltip />
                <el-table-column label="产出数量" prop="quantity" min-width="110" />
                <el-table-column label="合格数量" prop="qualifiedQty" min-width="110" />
                <el-table-column label="不良数量" prop="scrapQty" min-width="110" />
                <el-table-column label="不合格处理" prop="dealResult" min-width="160" show-overflow-tooltip />
                <el-table-column label="操作" width="150" fixed="right">
                  <template #default="{ row }">
                    <el-button type="primary" link @click="viewReportRecord(row)">
                      生产记录
                    </el-button>
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </div>
        </div>
      </div>
    </el-card>
<!--    <el-dialog-->
<!--      v-model="reportRecordDialogVisible"-->
<!--      title="报工生产记录"-->
<!--      width="680px"-->
<!--      destroy-on-close-->
<!--    >-->
<!--      <div class="report-record-placeholder">-->
<!--        <div>报工单号:{{ currentReportRow?.reportNo || "-" }}</div>-->
<!--        <div>工序:{{ selectedProcess?.processName || "-" }}</div>-->
<!--        <div class="placeholder-tip">弹框内容待定(后续补充详细生产记录)。</div>-->
<!--      </div>-->
<!--      <template #footer>-->
<!--        <el-button @click="reportRecordDialogVisible = false">关闭</el-button>-->
<!--      </template>-->
<!--    </el-dialog>-->
    <CopperPrintingForm
        v-if="copperPrintingFormVisible"
        v-model:isShow="copperPrintingFormVisible"
        :isEdit="false"
        :row="currentReportRow"
        @refreshData="fetchReportsForProcess(selectedProcess.value)"/>
    <VoltageSortingForm
        v-if="voltageSortingFormVisible"
        v-model:isShow="voltageSortingFormVisible"
        :isEdit="false"
        :row="currentReportRow"
        @refreshData="fetchReportsForProcess(selectedProcess.value)"/>
    <GranulationForm
        v-if="granulationFormVisible"
        v-model:isShow="granulationFormVisible"
        :isEdit="false"
        :row="currentReportRow"
        @refreshData="fetchReportsForProcess(selectedProcess.value)"/>
    <Detail
        v-if="reportRecordDialogVisible"
        v-model:isShow="reportRecordDialogVisible"
        @refreshData="fetchReportsForProcess(selectedProcess.value)"
        :row="currentReportRow"/>
  </div>
</template>
<script setup>
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { computed, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { getByProductOrderId } from "@/api/productionManagement/workOrder.js";
import { getByProductWorkOrderId } from "@/api/productionManagement/productionProductMain.js";
const VoltageSortingForm = defineAsyncComponent(() => import("@/views/productionManagement/workOrder/components/VoltageSortingForm.vue"));
const CopperPrintingForm = defineAsyncComponent(() => import("@/views/productionManagement/workOrder/components/CopperPrintingForm.vue"));
const GranulationForm = defineAsyncComponent(() => import("@/views/productionManagement/workOrder/components/GranulationForm.vue"));
const Detail = defineAsyncComponent(() => import("@/views/productionManagement/productionReporting/components/Detail.vue"));
const route = useRoute();
const header = computed(() => ({
@@ -134,89 +201,166 @@
  specificationModel: route.query.specificationModel,
}));
// 模拟工序数据(后续用接口替换)
const processes = computed(() => [
  {
    processCode: "GX-001",
    processName: "备料",
    inputQty: 1000,
    outputQty: 980,
    qualifiedQty: 970,
    badQty: 10,
    status: "success",
  },
  {
    processCode: "GX-002",
    processName: "成型",
    inputQty: 980,
    outputQty: 960,
    qualifiedQty: 948,
    badQty: 12,
    status: "process",
  },
  {
    processCode: "GX-003",
    processName: "烘干",
    inputQty: 960,
    outputQty: 950,
    qualifiedQty: 948,
    badQty: 2,
    status: "wait",
  },
  {
    processCode: "GX-004",
    processName: "包装入库",
    inputQty: 950,
    outputQty: 920,
    qualifiedQty: 918,
    badQty: 2,
    status: "wait",
  },
]);
// 工序数据(接口替换)
const processes = ref([]);
const selectedIndex = ref(null);
const copperPrintingFormVisible = ref(false);
const voltageSortingFormVisible = ref(false);
const granulationFormVisible = ref(false);
const normalizeStatus = (statusText, completionStatus, inputQty, outputQty) => {
  const s = statusText === null || statusText === undefined ? "" : String(statusText).trim();
  // 按接口实际三种状态:已生成 / 生产中 / 待生产
  if (s.includes("生产中")) return "process";
  if (s.includes("待生产")) return "wait";
  if (s.includes("已生产")) return "success";
  // 兜底:仍按 completionStatus 做 0~100 判断
  const cs = Number(completionStatus);
  if (Number.isFinite(cs)) {
    if (cs >= 100) return "success";
    if (cs > 0) return "process";
    return "wait";
  }
  // 再兜底:用数量判断
  if (Number.isFinite(inputQty) && inputQty > 0 && Number.isFinite(outputQty) && outputQty >= inputQty) return "success";
  if (Number.isFinite(outputQty) && outputQty > 0) return "process";
  return "wait";
};
const normalizeProcess = (item) => {
  // 字段以接口约定为准(你给的截图字段映射)
  // 工序:completionStatus/statusText/processNo/scrapRate/planQuantity/completeQuantity/completeQty/scrapQty
  const inputQty = Number(item?.planQuantity ?? item?.inputQty ?? 0);
  const outputQty = Number(item?.completeQuantity ?? item?.outputQty ?? 0);
  const qualifiedQty = Number(item?.completeQty ?? item?.qualifiedQty ?? item?.goodQty ?? 0);
  const badQty = Number(item?.scrapQty ?? item?.badQty ?? item?.defectQty ?? 0);
  const completionStatus = Number(item?.completionStatus ?? 0);
  const scrapRate = Number(item?.scrapRate ?? NaN);
  const status = normalizeStatus(item?.statusText ?? item?.status ?? item?.workStatus ?? item?.processStatus ?? item?.state, completionStatus, inputQty, outputQty);
  return {
    processCode: item?.processNo ?? item?.processCode ?? item?.processWorkOrderCode ?? "",
    processName: item?.processName ?? item?.processWorkOrderName ?? item?.processNo ?? "",
    productWorkOrderId: item?.productWorkOrderId ?? item?.workOrderId ?? item?.id ?? null,
    inputQty: Number.isFinite(inputQty) ? inputQty : 0,
    outputQty: Number.isFinite(outputQty) ? outputQty : 0,
    qualifiedQty: Math.max(0, Number.isFinite(qualifiedQty) ? qualifiedQty : 0),
    badQty: Math.max(0, Number.isFinite(badQty) ? badQty : 0),
    completionStatus: Number.isFinite(completionStatus) ? completionStatus : 0,
    scrapRate: Number.isFinite(scrapRate) ? scrapRate : null,
    status,
  };
};
onMounted(async () => {
  const productOrderId = header.value?.orderId;
  if (!productOrderId) return;
  try {
    const res = await getByProductOrderId(productOrderId);
    const payload = res?.data;
    const list = Array.isArray(payload) ? payload : payload?.records || payload?.data || [];
    processes.value = list.map((it) => normalizeProcess(it));
  } catch (e) {
    console.error("获取工序工单列表失败:", e);
    processes.value = [];
  }
});
// 默认选中第一道序(接口数据就绪后仍可从 0 开始)
const selectedIndex = ref(0);
const selectProcess = (idx) => {
  selectedIndex.value = idx;
};
watch(
  () => (processes.value || []).length,
  (len) => {
    if (!len) return;
    if (selectedIndex.value >= len) selectedIndex.value = len - 1;
    if (selectedIndex.value < 0) selectedIndex.value = 0;
  }
);
const selectedProcess = computed(() => {
  if (selectedIndex.value === null || selectedIndex.value === undefined) return null;
  return (processes.value || [])[selectedIndex.value] || null;
  const list = processes.value || [];
  if (!list.length) return null;
  const raw = selectedIndex.value;
  const idx =
    raw === null || raw === undefined
      ? 0
      : Math.min(Math.max(0, raw), list.length - 1);
  return list[idx] || null;
});
// 模拟报工信息(后续用接口替换)
const mockReports = computed(() => {
  const p = selectedProcess.value;
  if (!p) return [];
  const code = p.processCode || "GX";
  return [
    {
      reportNo: `${code}-BG-0001`,
      reportUser: "张三",
      reportTime: "2026-03-14 09:20",
      outputQty: Math.floor((p.outputQty ?? 0) * 0.4),
      badQty: Math.floor((p.badQty ?? 0) * 0.4),
      remark: "正常报工",
    },
    {
      reportNo: `${code}-BG-0002`,
      reportUser: "李四",
      reportTime: "2026-03-14 13:45",
      outputQty: Math.floor((p.outputQty ?? 0) * 0.35),
      badQty: Math.floor((p.badQty ?? 0) * 0.35),
      remark: "设备调试后恢复",
    },
    {
      reportNo: `${code}-BG-0003`,
      reportUser: "王五",
      reportTime: "2026-03-14 17:10",
      outputQty: Math.max(0, (p.outputQty ?? 0) - Math.floor((p.outputQty ?? 0) * 0.75)),
      badQty: Math.max(0, (p.badQty ?? 0) - Math.floor((p.badQty ?? 0) * 0.75)),
      remark: "收尾",
    },
  ];
});
const reports = ref([]);
const reportLoading = ref(false);
const normalizeReport = (item) => {
  return {
    ...item,
    quantity:  Math.max(0, Number.isFinite(item.quantity) ? item.quantity : 0),
    scrapQty:  Math.max(0, Number.isFinite(item.scrapQty) ? item.scrapQty : 0),
    qualifiedQty:  Math.max(0, Number.isFinite(item.qualifiedQty) ? item.qualifiedQty : 0),
  };
};
const fetchReportsForProcess = async (p) => {
  if (!p) {
    reports.value = [];
    return;
  }
  const productWorkOrderId = p.productWorkOrderId ?? p.id ?? p.workOrderId ?? null;
  if (!productWorkOrderId) {
    reports.value = [];
    return;
  }
  reportLoading.value = true;
  try {
    const res = await getByProductWorkOrderId(productWorkOrderId);
    const payload = res?.data;
    const list = Array.isArray(payload)
      ? payload
      : payload?.records || payload?.data || payload?.list || [];
    reports.value = list.map((it) => normalizeReport(it));
  } catch (e) {
    console.error("获取报工信息失败:", e);
    reports.value = [];
  } finally {
    reportLoading.value = false;
  }
};
watch(
  () => selectedProcess.value,
  (val) => {
    fetchReportsForProcess(val);
  },
  { immediate: true }
);
const viewReportRecord = (row) => {
  if (!row?.productNo) return;
  currentReportRow.value = row;
  if (row.process ==='印铜' || row.process ==='印银') {
    copperPrintingFormVisible.value = true;
  } else if (row.process === '电压分选') {
    voltageSortingFormVisible.value = true;
  } else if (row.process === '造粒') {
    granulationFormVisible.value = true;
  } else {
    reportRecordDialogVisible.value = true;
  }
};
const reportRecordDialogVisible = ref(false);
const currentReportRow = ref(null);
const clampPercentage = (val) => {
  const n = Number(val);
@@ -226,30 +370,62 @@
  return Math.round(n);
};
const statusTagText = (status) => {
  if (status === "success") return "已生产";
  if (status === "process") return "生产中";
  if (status === "wait") return "待生产";
  return String(status ?? "");
};
// el-steps: active 为当前进行中的步骤下标(模拟)
const active = computed(() => {
  const list = processes.value || [];
  const idx = list.findIndex((p) => p.status === "process");
  return idx >= 0 ? idx : 0;
  // 激活状态为“待生产”的上一条
  const firstWaitIdx = list.findIndex((p) => p?.status === "wait");
  if (firstWaitIdx > 0) return firstWaitIdx - 1;
  // 如果没有待生产:
  // - 且最后一条是生产中:激活最后一条
  // - 否则:没有激活样式
  const lastIdx = list.length - 1;
  if (lastIdx >= 0 && list[lastIdx]?.status === "process") return lastIdx;
  return null;
});
const currentProcess = computed(() => {
  const list = processes.value || [];
  return list[active.value] || null;
});
// 当前工序进度:用产出/投入估算(UI 先跑通,后续按真实规则替换)
const currentProcessPercentage = computed(() => {
  const p = currentProcess.value;
// 工序进度:用产出/投入估算(UI 先跑通,后续按真实规则替换)
const processPercentage = (p) => {
  if (!p) return 0;
  // 优先使用接口字段 completionStatus(你给的截图“工序进度”)
  const cs = Number(p?.completionStatus ?? NaN);
  if (Number.isFinite(cs)) return clampPercentage(cs);
  // 兜底:用产出/投入估算
  const input = Number(p.inputQty ?? 0);
  const output = Number(p.outputQty ?? 0);
  if (!Number.isFinite(input) || input <= 0) return 0;
  return clampPercentage((output / input) * 100);
});
};
// 30/50/80/100 分段颜色:红/橙/蓝/绿
const progressColor = (percentage) => {
  const p = clampPercentage(percentage);
  if (p < 30) return "#f56c6c";
  if (p < 50) return "#e6a23c";
  if (p < 80) return "#409eff";
  return "#67c23a";
};
// 不良率:不良数量 / 产出数量(先按此口径,后续对接接口可调整)
const defectRateText = (p) => {
  // 优先使用接口字段 scrapRate(你给的截图“不良率”)
  const scrapRate = Number(p?.scrapRate ?? NaN);
  if (Number.isFinite(scrapRate)) {
    // 有些接口 scrapRate 可能是 0~1 或 0~100,这里做一个简单判断
    const percent = scrapRate <= 1 ? scrapRate * 100 : scrapRate;
    return `${percent.toFixed(2)}%`;
  }
  // 兜底:不良数量 / 产出数量
  const bad = Number(p?.badQty ?? 0);
  const output = Number(p?.outputQty ?? 0);
  if (!Number.isFinite(bad) || bad <= 0) return "0%";
@@ -327,13 +503,15 @@
    align-items: center;
    padding: 2px 6px;
    border-radius: 6px;
    transition: background-color 0.15s ease;
    transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
    &:hover {
      background: #f5f7fa;
      transform: translateX(2px);
    }
    &.selected {
      background: rgba(64, 158, 255, 0.12);
      background: rgba(64, 158, 255, 0.18);
      color: #409eff;
      font-weight: 700;
    }
  }
@@ -371,6 +549,8 @@
  }
  .step-panel {
    position: relative;
    cursor: pointer;
    background: #f6f8fb;
    border: 1px solid #ebeef5;
    border-radius: 10px;
@@ -378,6 +558,93 @@
    width: 100%;
    max-width: none;
    box-sizing: border-box;
    transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease,
      background 0.25s ease;
    &:hover {
      border-color: #c6e2ff;
      box-shadow: 0 4px 14px rgba(64, 158, 255, 0.12);
      transform: translateY(-1px);
    }
  }
  /* 被选中:沿用原「高亮」语义并加呼吸动画 */
  .step-panel-selected {
    background: #ecf5ff;
    border-color: #409eff;
    animation: step-panel-selected-pulse 2.2s ease-in-out infinite;
    &:hover {
      transform: translateY(-1px);
    }
  }
  @keyframes step-panel-selected-pulse {
    0%,
    100% {
      box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.22);
    }
    50% {
      box-shadow: 0 0 0 6px rgba(64, 158, 255, 0.12);
    }
  }
  /* 当前生产中的工序:橙色主题,与选中区分;若同时选中则以选中为主,仅保留角标 */
  .step-panel-current:not(.step-panel-selected) {
    background: #fdf6ec;
    border-color: #e6a23c;
    border-left: 4px solid #e6a23c;
    padding-left: 9px;
    &:hover {
      box-shadow: 0 4px 14px rgba(230, 162, 60, 0.18);
    }
  }
  .step-current-badge {
    position: absolute;
    top: 8px;
    right: 10px;
    z-index: 1;
    font-size: 11px;
    font-weight: 600;
    color: #b88230;
    background: rgba(230, 162, 60, 0.18);
    border: 1px solid rgba(230, 162, 60, 0.45);
    border-radius: 4px;
    padding: 2px 8px;
    line-height: 1.2;
    pointer-events: none;
  }
  .step-status-badge {
    position: absolute;
    top: 8px;
    right: 10px;
    z-index: 1;
    font-size: 11px;
    font-weight: 600;
    border-radius: 4px;
    padding: 2px 8px;
    line-height: 1.2;
    pointer-events: none;
    white-space: nowrap;
    border: 1px solid transparent;
  }
  .step-status-badge-success {
    color: #67c23a;
    background: rgba(103, 194, 58, 0.14);
    border-color: rgba(103, 194, 58, 0.35);
  }
  .step-status-badge-process {
    color: #b88230;
    background: rgba(230, 162, 60, 0.18);
    border-color: rgba(230, 162, 60, 0.45);
  }
  .step-status-badge-wait {
    color: #909399;
    background: rgba(144, 147, 153, 0.12);
    border-color: rgba(144, 147, 153, 0.35);
  }
  .right-panel {
@@ -439,6 +706,17 @@
    }
  }
  .report-record-placeholder {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-height: 120px;
    color: #303133;
    .placeholder-tip {
      color: #909399;
    }
  }
  .step-meta {
    display: flex;
    gap: 16px;