张诺
9 小时以前 71a260afa397035d3844ab13f2fea5669f7b46ab
feat(productionManagement): 新增工单管理功能模块

- 实现工单列表展示功能,支持工单编号搜索查询
- 添加工单时间编辑对话框,支持计划和实际开始结束时间修改
- 集成工单流转卡功能,显示工单基本信息和二维码
已修改2个文件
578 ■■■■■ 文件已修改
src/api/productionManagement/workOrder.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 551 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js
@@ -33,3 +33,30 @@
    responseType: "blob",
  });
}
export function addProductionMachineRecord(data) {
  return request({
    url: "/productionMachineRecord/add",
    method: "post",
    data
  });
}
// /productionMachineRecord/listPage
export function productionMachineRecordListPage(data) {
  return request({
    url: "/productionMachineRecord/listPage",
    method: "get",
      params: data
  });
}
// 删除生产排产
// /productionMachineRecord/delete
export function deleteProductionMachineRecord(query) {
  return request({
    url: "/productionMachineRecord/delete",
    method: "delete",
        data: query,
  });
}
src/views/productionManagement/workOrder/index.vue
@@ -264,32 +264,6 @@
          </el-col>
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-date-picker
                  v-model="reportForm.startTime"
                  type="datetime"
                  value-format="YYYY-MM-DD HH:mm:ss"
                  format="YYYY-MM-DD HH:mm:ss"
                  placeholder="请选择开始时间"
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束时间" prop="endTime">
              <el-date-picker
                  v-model="reportForm.endTime"
                  type="datetime"
                  value-format="YYYY-MM-DD HH:mm:ss"
                  format="YYYY-MM-DD HH:mm:ss"
                  placeholder="请选择结束时间"
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="审核人" prop="auditUserId">
              <el-select
                  v-model="reportForm.auditUserId"
@@ -351,6 +325,112 @@
        </span>
      </template>
    </el-dialog>
    <el-dialog v-model="scheduleDialogVisible"
               :title="`生产排产(工单编号:${currentReportRowData?.workOrderNo || '-'})`"
               width="1000px"
               :close-on-click-modal="false">
      <div class="schedule-panel">
        <el-row style="margin-bottom: 12px;">
          <el-col>
            <el-button type="primary" plain :disabled="scheduleLoading || scheduleSaving" @click="addScheduleRow">
              新增一行
            </el-button>
          </el-col>
        </el-row>
        <el-table :data="scheduleRows" border style="width: 100%" v-loading="scheduleLoading">
          <el-table-column type="index" label="序号" width="70" align="center" />
          <el-table-column label="本次上机机台" min-width="220">
            <template #default="{ row }">
              <el-select
                  v-model="row.deviceId"
                  placeholder="请选择机台"
                  filterable
                  clearable
                  style="width: 100%"
                  :disabled="scheduleSaving"
                  @change="val => handleScheduleDeviceChange(val, row)"
              >
                <el-option
                    v-for="item in deviceOptions"
                    :key="item.id"
                    :label="item.deviceName"
                    :value="String(item.id)"
                />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column label="本次上机人" min-width="220">
            <template #default="{ row }">
              <el-select
                  v-model="row.userIds"
                  placeholder="请选择上机人"
                  filterable
                  multiple
                  clearable
                  collapse-tags
                  style="width: 100%"
                  :disabled="scheduleSaving"
                  @change="val => handleScheduleUserChange(val, row)"
              >
                <el-option
                    v-for="user in row.userOptions"
                    :key="user.userId"
                    :label="user.nickName"
                    :value="String(user.userId)"
                />
              </el-select>
            </template>
          </el-table-column>
          <el-table-column label="本次上机时间" min-width="240">
            <template #default="{ row }">
              <el-date-picker
                  v-model="row.startTime"
                  type="datetime"
                  value-format="YYYY-MM-DD HH:mm:ss"
                  format="YYYY-MM-DD HH:mm:ss"
                  placeholder="请选择上机时间"
                  style="width: 100%"
                  :disabled="scheduleSaving"
              />
            </template>
          </el-table-column>
          <el-table-column label="操作" width="110" align="center">
            <template #default="{ row }">
              <el-button
                  link
                  type="danger"
                  :loading="row.deleting"
                  :disabled="scheduleSaving"
                  @click="removeScheduleRow(row)"
              >
                删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <Pagination
            v-show="schedulePage.total > 0"
            style="margin-top: 12px"
            :total="schedulePage.total"
            :page="schedulePage.current"
            :limit="schedulePage.size"
            @pagination="handleSchedulePagination"
        />
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" :loading="scheduleSaving" @click="handleSaveSchedule">保存排产</el-button>
          <el-button :disabled="scheduleSaving" @click="scheduleDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    <FilesDia ref="workOrderFilesRef"/>
  </div>
</template>
@@ -359,12 +439,16 @@
import {onMounted, ref, nextTick, computed} from "vue";
import {deepClone} from "@/utils/index.js"
import {ElMessageBox, ElMessage} from "element-plus";
import Pagination from "@/components/PIMTable/Pagination.vue";
import dayjs from "dayjs";
import {
  productWorkOrderPage,
  updateProductWorkOrder,
  addProductMain,
  downProductWorkOrder,
  addProductionMachineRecord,
  productionMachineRecordListPage,
  deleteProductionMachineRecord
} from "@/api/productionManagement/workOrder.js";
import {getUserProfile, userListNoPageByTenantId} from "@/api/system/user.js";
import QRCode from "qrcode";
@@ -424,7 +508,6 @@
  const candidateIds = [
    row.reportUserIds,
    row.reportWorkerIds,
    row.userIds,
    row.userIdList,
    row.reportUserId,
    row.userId,
@@ -466,6 +549,398 @@
const canOperateByReportWorker = computed(() => {
  return (row) => isCurrentUserReportWorker(row);
});
const isRowScheduled = (row) => {
  const ids = normalizeArray(row?.userIds)
    .map((val) => String(val))
    .filter(Boolean);
  if (!ids.length) return false;
  return ids.some((val) => val !== "0");
};
const buildBaseScheduleUsersByRow = (row) => {
  if (!row) return [];
  if (Array.isArray(row?.reportWorkerList) && row.reportWorkerList.length > 0) {
    const mapped = row.reportWorkerList
      .map((item) => {
        const userId = String(item?.userId ?? item?.id ?? "").trim();
        const nickName = String(item?.userName ?? item?.nickName ?? "").trim();
        return { userId, nickName: nickName || userId };
      })
      .filter((item) => item.userId);
    const uniq = new Map();
    mapped.forEach((item) => uniq.set(String(item.userId), item));
    return Array.from(uniq.values());
  }
  const configuredIds = [
    row.reportUserIds,
    row.reportWorkerIds,
    row.userIdList,
    row.reportUserId,
    row.userId,
  ]
    .flatMap((v) => normalizeArray(v))
    .map((v) => String(v).trim())
    .filter(Boolean);
  if (configuredIds.length > 0) {
    const uniqIds = Array.from(new Set(configuredIds));
    return uniqIds.map((id) => {
      const user = userTeamOptions.value.find((u) => String(u.userId) === String(id));
      return { userId: String(id), nickName: user?.nickName || String(id) };
    });
  }
  return userTeamOptions.value.map((u) => ({
    userId: String(u.userId),
    nickName: u.nickName,
  }));
};
const resolveScheduleUserName = (userId) => {
  const uid = String(userId ?? "").trim();
  if (!uid) return "";
  const inBase = baseScheduleUsers.value.find((u) => String(u.userId) === uid);
  if (inBase?.nickName) return inBase.nickName;
  const inTeam = userTeamOptions.value.find((u) => String(u.userId) === uid);
  return inTeam?.nickName || uid;
};
const buildScheduleUserOptionsByDeviceId = (deviceId) => {
  const device = deviceOptions.value.find((item) => String(item.id) === String(deviceId));
  const operatorIds = device?.operatorId
    ? String(device.operatorId)
      .split(/[,,;;\s]+/g)
      .map((id) => id.trim())
      .filter(Boolean)
    : [];
  if (!operatorIds.length) {
    return [...baseScheduleUsers.value];
  }
  return baseScheduleUsers.value.filter((user) =>
    operatorIds.includes(String(user.userId))
  );
};
const createScheduleRow = (preset = {}) => {
  const deviceId =
    preset?.deviceId === null || preset?.deviceId === undefined
      ? ""
      : String(preset.deviceId);
  const userOptions = deviceId
    ? buildScheduleUserOptionsByDeviceId(deviceId)
    : [...baseScheduleUsers.value];
  const userIds = normalizeArray(preset?.userIds)
    .map((val) => String(val))
    .filter(Boolean)
    .filter((uid) => userOptions.some((user) => String(user.userId) === String(uid)));
  return {
    id: preset?.id ?? "",
    deviceId,
    deviceName: preset?.deviceName ?? "",
    userIds,
    userOptions,
    startTime: preset?.startTime ?? dayjs().format("YYYY-MM-DD HH:mm:ss"),
    deleting: false,
  };
};
const addScheduleRow = (preset) => {
  if (preset) {
    scheduleRows.value.push(createScheduleRow(preset));
    return;
  }
  const first = scheduleRows.value[0] || {};
  const deviceId = first.deviceId || currentReportRowData.value?.deviceId || "";
  const userOptions = deviceId ? buildScheduleUserOptionsByDeviceId(deviceId) : [...baseScheduleUsers.value];
  const defaultUserId = userOptions[0]?.userId ? String(userOptions[0].userId) : "";
  scheduleRows.value.push(
    createScheduleRow({
      id: "",
      deviceId,
      deviceName: first.deviceName || currentReportRowData.value?.deviceName || "",
      userIds: defaultUserId ? [defaultUserId] : [],
      startTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    })
  );
};
const refreshScheduleRows = async () => {
  const workOrderRow = currentReportRowData.value;
  if (!workOrderRow?.id) {
    schedulePage.current = 1;
    schedulePage.total = 0;
    scheduleRows.value = [createScheduleRow({})];
    return;
  }
  scheduleLoading.value = true;
  try {
    const res = await productionMachineRecordListPage({
      workOrderId: workOrderRow.id,
      current: schedulePage.current,
      size: schedulePage.size,
    });
    const records = Array.isArray(res?.data?.records) ? res.data.records : [];
    const apiTotal = Number(res?.data?.total);
    schedulePage.total =
      Number.isFinite(apiTotal) && apiTotal > 0
        ? apiTotal
        : records.length;
    const lastPage = Math.max(1, Math.ceil((schedulePage.total || 0) / schedulePage.size));
    if (schedulePage.current > lastPage) {
      schedulePage.current = lastPage;
      await refreshScheduleRows();
      return;
    }
    const rows = buildScheduleRowsFromRecords(records);
    scheduleRows.value = rows.length > 0 ? rows : [createScheduleRow({})];
  } catch (error) {
    console.error("获取排产记录失败", error);
    schedulePage.total = 0;
    scheduleRows.value = [createScheduleRow({})];
    ElMessage.error("获取排产记录失败");
  } finally {
    scheduleLoading.value = false;
  }
};
const removeScheduleRow = async (row) => {
  if (!row) return;
  if (!row.id) {
    scheduleRows.value = scheduleRows.value.filter((item) => item !== row);
    if (!scheduleRows.value.length) {
      scheduleRows.value = [createScheduleRow({})];
    }
    return;
  }
  try {
    await ElMessageBox.confirm("确定删除这条排产记录吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    });
  } catch {
    return;
  }
  row.deleting = true;
  try {
    const res = await deleteProductionMachineRecord([row.id]);
    if (res?.code !== undefined && res.code !== 200) {
      ElMessage.error(res?.msg || "删除失败");
      return;
    }
    ElMessage.success("删除成功");
    await refreshScheduleRows();
    getList();
  } catch (error) {
    console.error("删除排产记录失败", error);
    ElMessage.error("删除失败,请重试");
  } finally {
    row.deleting = false;
  }
};
const handleScheduleUserChange = (userIds, row) => {
  row.userIds = normalizeArray(userIds).map((val) => String(val)).filter(Boolean);
};
const handleScheduleDeviceChange = (deviceId, row) => {
  const device = deviceOptions.value.find((item) => String(item.id) === String(deviceId));
  row.deviceId = deviceId === null || deviceId === undefined ? "" : String(deviceId);
  row.deviceName = device?.deviceName || "";
  row.userOptions = row.deviceId ? buildScheduleUserOptionsByDeviceId(row.deviceId) : [...baseScheduleUsers.value];
  row.userIds = normalizeArray(row.userIds)
    .map((uid) => String(uid))
    .filter((uid) => row.userOptions.some((user) => String(user.userId) === String(uid)));
};
const handleSchedulePagination = ({page, limit}) => {
  schedulePage.current = page;
  schedulePage.size = limit;
  refreshScheduleRows();
};
const validateScheduleRows = () => {
  if (!scheduleRows.value.length) {
    ElMessage.warning("请至少添加一条上机信息");
    return false;
  }
  for (let index = 0; index < scheduleRows.value.length; index += 1) {
    const row = scheduleRows.value[index];
    if (!row.deviceId) {
      ElMessage.warning(`第${index + 1}行请选择机台`);
      return false;
    }
    if (!Array.isArray(row.userIds) || row.userIds.length === 0) {
      ElMessage.warning(`第${index + 1}行请选择上机人`);
      return false;
    }
    if (!row.startTime) {
      ElMessage.warning(`第${index + 1}行请选择上机时间`);
      return false;
    }
    if (!dayjs(row.startTime).isValid()) {
      ElMessage.warning(`第${index + 1}行上机时间格式不正确`);
      return false;
    }
  }
  return true;
};
const buildMachineRecordPayload = (workOrderRow, scheduleRow, sortIndex = 0) => {
  const processId =
    workOrderRow?.processId ??
    workOrderRow?.productProcessRouteItemId ??
    reportForm.productProcessRouteItemId;
  const operatorIds = normalizeArray(scheduleRow?.userIds)
    .map((val) => String(val).trim())
    .filter(Boolean)
    .join(",");
  const nickName = normalizeArray(scheduleRow?.userIds)
    .map((uid) => resolveScheduleUserName(uid))
    .filter(Boolean)
    .join(",");
  const payload = {
    workOrderId: workOrderRow?.id,
    processId,
    machineId: scheduleRow.deviceId ? Number(scheduleRow.deviceId) : undefined,
    deviceName: scheduleRow.deviceName,
    operatorId: operatorIds || undefined,
    nickName: nickName || "",
    machineStartTime: scheduleRow.startTime,
    reportStatus: false,
    remark: `排产序号:${sortIndex + 1}`,
  };
  if (scheduleRow.id) {
    payload.id = scheduleRow.id;
  }
  return payload;
};
const mapMachineRecordToScheduleRow = (record) => {
  const id = record?.id ?? "";
  const deviceId = record?.machineId ?? record?.deviceId ?? "";
  const deviceName = record?.deviceName ?? record?.machineName ?? "";
  const startTime = record?.machineStartTime ?? record?.startTime ?? "";
  const userIds = normalizeArray(record?.operatorId ?? record?.operatorIds ?? record?.userId)
    .map((val) => String(val))
    .filter(Boolean);
  return createScheduleRow({
    id,
    deviceId: deviceId === null || deviceId === undefined ? "" : String(deviceId),
    deviceName,
    userIds,
    startTime,
  });
};
const buildScheduleRowsFromRecords = (records) => {
  const list = Array.isArray(records) ? records : [];
  const grouped = new Map();
  list.forEach((record) => {
    const row = mapMachineRecordToScheduleRow(record);
    const key = `${row.deviceId}__${row.startTime}__${row.deviceName}`;
    if (!grouped.has(key)) {
      grouped.set(key, row);
      return;
    }
    const existing = grouped.get(key);
    existing.ids = Array.from(new Set([existing.id, row.id].filter(Boolean)));
    existing.userIds = Array.from(
      new Set([...(existing?.userIds || []), ...(row?.userIds || [])].map((v) => String(v)))
    ).filter(Boolean);
    if (!existing.deviceName && row.deviceName) existing.deviceName = row.deviceName;
  });
  return Array.from(grouped.values()).sort(
    (a, b) => dayjs(a.startTime).valueOf() - dayjs(b.startTime).valueOf()
  );
};
const openScheduleDialog = async (row) => {
  currentReportRowData.value = row;
  baseScheduleUsers.value = buildBaseScheduleUsersByRow(row);
  userTemp.value = [...baseScheduleUsers.value];
  schedulePage.current = 1;
  scheduleRows.value = [];
  scheduleDialogVisible.value = true;
  await refreshScheduleRows();
};
const handleSaveSchedule = async () => {
  if (scheduleSaving.value) return;
  if (!validateScheduleRows()) return;
  const workOrderRow = currentReportRowData.value;
  if (!workOrderRow?.id) {
    ElMessage.warning("缺少工单信息,无法保存排产");
    return;
  }
  const sortedRows = [...scheduleRows.value].sort((a, b) => dayjs(a.startTime).valueOf() - dayjs(b.startTime).valueOf());
  scheduleSaving.value = true;
  try {
    const productionMachineRecord = sortedRows.map((scheduleRow, index) =>
      buildMachineRecordPayload(workOrderRow, scheduleRow, index)
    );
    const res = await addProductionMachineRecord({productionMachineRecord});
    if (res?.code !== undefined && res.code !== 200) {
      ElMessage.error(res?.msg || "保存排产失败");
      return;
    }
    proxy.$modal.msgSuccess("排产已保存");
    await refreshScheduleRows();
    getList();
  } catch (error) {
    console.error("保存排产失败", error);
    ElMessage.error("保存排产失败,请重试");
  } finally {
    scheduleSaving.value = false;
  }
};
const tableColumn = ref([
  {
@@ -568,6 +1043,12 @@
          showReportDialog(row);
        },
      },
      {
        name: "生产排产",
        clickFun: row => {
          openScheduleDialog(row);
        },
      },
      // {
      //   name:"审核",
      //   color: "#f56c6c",
@@ -581,13 +1062,21 @@
]);
const tableData = ref([]);
const tableLoading = ref(false);
const qrCodeUrl = ref("");
const qrRowData = ref(null);
const scheduleRows = ref([]);
const scheduleLoading = ref(false);
const scheduleSaving = ref(false);
const schedulePage = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const editDialogVisible = ref(false);
const transferCardVisible = ref(false);
const transferCardData = ref([]);
const transferCardQrUrl = ref("");
const scheduleDialogVisible = ref(false);
const transferCardRowData = ref(null);
const baseScheduleUsers = ref([]);
const userTemp = ref([]);
const reportDialogVisible = ref(false);
const auditDialogVisible = ref(false);
const auditRowData = ref(null);
@@ -1013,8 +1502,6 @@
      quantity: quantity,
      scrapQty: scrapQty,
    };
    // console.log(submitData);
    addProductMain(submitData).then(res => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess("报工成功");