From 71a260afa397035d3844ab13f2fea5669f7b46ab Mon Sep 17 00:00:00 2001
From: 张诺 <zhang_12370@163.com>
Date: 星期二, 28 四月 2026 14:35:51 +0800
Subject: [PATCH] feat(productionManagement): 新增工单管理功能模块

---
 src/api/productionManagement/workOrder.js          |   27 ++
 src/views/productionManagement/workOrder/index.vue |  551 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 2 files changed, 546 insertions(+), 32 deletions(-)

diff --git a/src/api/productionManagement/workOrder.js b/src/api/productionManagement/workOrder.js
index 7e8bd86..84d344f 100644
--- a/src/api/productionManagement/workOrder.js
+++ b/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,
+  });
+}
diff --git a/src/views/productionManagement/workOrder/index.vue b/src/views/productionManagement/workOrder/index.vue
index c9accb9..18423af 100644
--- a/src/views/productionManagement/workOrder/index.vue
+++ b/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}琛屼笂鏈烘椂闂存牸寮忎笉姝g‘`);
+      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("鎶ュ伐鎴愬姛");

--
Gitblit v1.9.3