From 1b475b840978b0b98c7cd29a00c87971513e3426 Mon Sep 17 00:00:00 2001
From: 吴羡 <16976746+Xian-TT3@user.noreply.gitee.com>
Date: 星期五, 09 十月 2026 10:13:17 +0800
Subject: [PATCH] feat(生产排产): 新增车间、班组、排产管理模块并联动工单报工;修复空path菜单导致的路由注册报错

---
 src/permission.js                                            |    3 
 index.html                                                   |    4 
 src/views/productionScheduling/teamManage/index.vue          |  375 ++++++++++++++++
 src/views/productionScheduling/workshopManage/index.vue      |  308 +++++++++++++
 src/store/modules/permission.js                              |   20 
 src/views/productionScheduling/schedulingManage/index.vue    |  462 ++++++++++++++++++++
 src/api/productionScheduling/workshop.js                     |   42 +
 src/api/productionScheduling/scheduling.js                   |   43 +
 src/api/productionScheduling/team.js                         |   43 +
 src/views/productionManagement/workOrderManagement/index.vue |   59 +
 10 files changed, 1,337 insertions(+), 22 deletions(-)

diff --git a/index.html b/index.html
index 274352d..34df730 100644
--- a/index.html
+++ b/index.html
@@ -11,9 +11,9 @@
     <link rel="icon" href="/favicon.ico" />
     <title>%VITE_APP_TITLE%</title>
     <!-- 楂樺痉鍦板浘API -->
-    <script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
+    <script type="text/javascript" defer src="https://webapi.amap.com/maps?v=2.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
     <!-- 楂樺痉鍦板浘鎼滅储鎻掍欢 -->
-    <script type="text/javascript" src="https://webapi.amap.com/loca?v=2.0.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
+    <script type="text/javascript" defer src="https://webapi.amap.com/loca?v=2.0.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
     <!--[if lt IE 11
       ]><script>
         window.location.href = "/html/ie.html";
diff --git a/src/api/productionScheduling/scheduling.js b/src/api/productionScheduling/scheduling.js
new file mode 100644
index 0000000..cf9a376
--- /dev/null
+++ b/src/api/productionScheduling/scheduling.js
@@ -0,0 +1,43 @@
+// 鐢熶骇鎺掔彮鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+  return request({
+    url: "/productionScheduling/page",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ鐝粍浠婃棩鎺掔彮
+export function today(teamId) {
+  return request({
+    url: "/productionScheduling/today",
+    method: "get",
+    params: { teamId },
+  });
+}
+
+export function add(data) {
+  return request({
+    url: "/productionScheduling",
+    method: "post",
+    data: data,
+  });
+}
+
+export function update(data) {
+  return request({
+    url: "/productionScheduling",
+    method: "put",
+    data: data,
+  });
+}
+
+export function del(ids) {
+  return request({
+    url: `/productionScheduling/${ids}`,
+    method: "delete",
+  });
+}
diff --git a/src/api/productionScheduling/team.js b/src/api/productionScheduling/team.js
new file mode 100644
index 0000000..7c5f89f
--- /dev/null
+++ b/src/api/productionScheduling/team.js
@@ -0,0 +1,43 @@
+// 鐝粍绠$悊鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+  return request({
+    url: "/productionTeam/page",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ鍏ㄩ儴鐝粍锛堜笅鎷夌敤锛屽彲鎸夎溅闂磋繃婊わ級
+export function listAll(workshopId) {
+  return request({
+    url: "/productionTeam/listAll",
+    method: "get",
+    params: { workshopId },
+  });
+}
+
+export function add(data) {
+  return request({
+    url: "/productionTeam",
+    method: "post",
+    data: data,
+  });
+}
+
+export function update(data) {
+  return request({
+    url: "/productionTeam",
+    method: "put",
+    data: data,
+  });
+}
+
+export function del(ids) {
+  return request({
+    url: `/productionTeam/${ids}`,
+    method: "delete",
+  });
+}
diff --git a/src/api/productionScheduling/workshop.js b/src/api/productionScheduling/workshop.js
new file mode 100644
index 0000000..74a57a2
--- /dev/null
+++ b/src/api/productionScheduling/workshop.js
@@ -0,0 +1,42 @@
+// 杞﹂棿绠$悊鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+  return request({
+    url: "/workshop/page",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ鍏ㄩ儴杞﹂棿锛堜笅鎷夌敤锛�
+export function listAll() {
+  return request({
+    url: "/workshop/listAll",
+    method: "get",
+  });
+}
+
+export function add(data) {
+  return request({
+    url: "/workshop",
+    method: "post",
+    data: data,
+  });
+}
+
+export function update(data) {
+  return request({
+    url: "/workshop",
+    method: "put",
+    data: data,
+  });
+}
+
+export function del(ids) {
+  return request({
+    url: `/workshop/${ids}`,
+    method: "delete",
+  });
+}
diff --git a/src/permission.js b/src/permission.js
index 9bf4a91..239fb67 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -36,7 +36,8 @@
           usePermissionStore().generateRoutes().then(accessRoutes => {
             // 鏍规嵁roles鏉冮檺鐢熸垚鍙闂殑璺敱琛�
             accessRoutes.forEach(route => {
-              if (!isHttp(route.path)) {
+              // 鍚庣鑿滃崟鏁版嵁缂哄け path 鏃讹紙濡備粎濉� https:// 鐨勫唴閾捐彍鍗曪級璺宠繃锛岄伩鍏� addRoute 鎶涢敊鍗℃鏁翠釜鐧诲綍娴佺▼
+              if (route.path && !isHttp(route.path)) {
                 router.addRoute(route) // 鍔ㄦ�佹坊鍔犲彲璁块棶璺敱琛�
               }
             })
diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js
index 6a16d9d..30035e6 100644
--- a/src/store/modules/permission.js
+++ b/src/store/modules/permission.js
@@ -36,9 +36,10 @@
         return new Promise(resolve => {
           // 鍚戝悗绔姹傝矾鐢辨暟鎹�
           getRouters().then(res => {
-            const sdata = JSON.parse(JSON.stringify(res.data))
-            const rdata = JSON.parse(JSON.stringify(res.data))
-            const defaultData = JSON.parse(JSON.stringify(res.data))
+            const data = filterBadRoutes(JSON.parse(JSON.stringify(res.data)))
+            const sdata = JSON.parse(JSON.stringify(data))
+            const rdata = JSON.parse(JSON.stringify(data))
+            const defaultData = JSON.parse(JSON.stringify(data))
             const sidebarRoutes = filterAsyncRouter(sdata)
             const rewriteRoutes = filterAsyncRouter(rdata, false, true)
             const defaultRoutes = filterAsyncRouter(defaultData)
@@ -55,6 +56,19 @@
     }
   })
 
+// 鍓旈櫎鍚庣杩斿洖鐨勬棤 path 鑿滃崟锛堝鍦板潃鍙~ https:// 鐨勫唴閾捐彍鍗曪級锛岄伩鍏� addRoute 涓庝晶杈规爮娓叉煋鎶ラ敊
+function filterBadRoutes(routes) {
+  return routes.filter(route => {
+    if (!route.path) {
+      return false
+    }
+    if (route.children && route.children.length) {
+      route.children = filterBadRoutes(route.children)
+    }
+    return true
+  })
+}
+
 // 閬嶅巻鍚庡彴浼犳潵鐨勮矾鐢卞瓧绗︿覆锛岃浆鎹负缁勪欢瀵硅薄
 function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
   return asyncRouterMap.filter(route => {
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
index 6def04c..822c2aa 100644
--- a/src/views/productionManagement/workOrderManagement/index.vue
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -142,17 +142,21 @@
                     @input="handleScrapQtyInput" />
         </el-form-item>
         <el-form-item label="鐝粍淇℃伅">
-          <el-select v-model="reportForm.userId"
+          <el-select v-model="reportForm.teamId"
                      style="width: 300px"
                      placeholder="璇烽�夋嫨鐝粍淇℃伅"
                      clearable
                      filterable
-                     @change="handleUserChange">
-            <el-option v-for="user in userOptions"
-                       :key="user.userId"
-                       :label="user.nickName"
-                       :value="user.userId" />
+                     @change="handleTeamChange">
+            <el-option v-for="team in teamOptions"
+                       :key="team.id"
+                       :label="team.teamName"
+                       :value="team.id" />
           </el-select>
+          <div v-if="teamScheduleText"
+               style="width: 300px; margin-top: 5px; color: #909399; font-size: 12px; line-height: 1.5;">
+            {{ teamScheduleText }}
+          </div>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -172,12 +176,15 @@
   import { onMounted, ref, nextTick } from "vue";
   import { ElMessageBox } from "element-plus";
   import dayjs from "dayjs";
+  import { useDict } from "@/utils/dict.js";
   import {
     productWorkOrderPage,
     addProductMain,
     downProductWorkOrder,
   } from "@/api/productionManagement/workOrder.js";
-  import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+  import { getUserProfile } from "@/api/system/user.js";
+  import { listAll as teamListAll } from "@/api/productionScheduling/team.js";
+  import { today as getTeamToday } from "@/api/productionScheduling/scheduling.js";
   import QRCode from "qrcode";
   import { getCurrentInstance, reactive, toRefs } from "vue";
   import FilesDia from "./components/filesDia.vue";
@@ -292,7 +299,9 @@
   const reportDialogVisible = ref(false);
   const workOrderFilesRef = ref(null);
   const reportFormRef = ref(null);
-  const userOptions = ref([]);
+  const teamOptions = ref([]);
+  const teamScheduleText = ref("");
+  const { shift_type, schedule_status } = useDict("shift_type", "schedule_status");
   const reportForm = reactive({
     planQuantity: 0,
     quantity: null,
@@ -302,6 +311,7 @@
     reportWork: "",
     productProcessRouteItemId: "",
     userId: "",
+    teamId: null,
     productMainId: null,
   });
 
@@ -529,6 +539,8 @@
     reportForm.productMainId = row.productMainId;
     reportForm.scrapQty =
       row.scrapQty !== undefined && row.scrapQty !== null ? row.scrapQty : null;
+    reportForm.teamId = null;
+    teamScheduleText.value = "";
     nextTick(() => {
       reportFormRef.value?.clearValidate();
     });
@@ -629,18 +641,33 @@
     });
   };
 
-  const handleUserChange = val => {
-    const user = userOptions.value.find(item => item.userId === val);
-    reportForm.userName = user ? user.nickName : "";
+  const handleTeamChange = val => {
+    teamScheduleText.value = "";
+    if (!val) return;
+    // 閫変腑鐝粍鍚庯紝鎶ュ伐浜洪粯璁や负鐝粍闀�
+    const team = teamOptions.value.find(item => item.id === val);
+    if (team && team.leaderUserId) {
+      reportForm.userId = team.leaderUserId;
+      reportForm.userName = team.leaderName;
+    }
+    // 灞曠ず璇ョ彮缁勪粖鏃ユ帓鐝俊鎭�
+    getTeamToday(val).then(res => {
+      const schedule = res.data;
+      if (schedule && schedule.shiftType !== null && schedule.shiftType !== undefined) {
+        const shift = shift_type.value.find(item => item.value === String(schedule.shiftType));
+        const status = schedule_status.value.find(item => item.value === String(schedule.status));
+        teamScheduleText.value = `浠婃棩鎺掔彮锛�${shift ? shift.label : ""}${status ? "锛�" + status.label + "锛�" : ""}`;
+      } else {
+        teamScheduleText.value = "浠婃棩鏈帓鐝�";
+      }
+    });
   };
 
   onMounted(() => {
     getList();
-    // 鑾峰彇鐢ㄦ埛鍒楄〃
-    userListNoPageByTenantId().then(res => {
-      if (res.code === 200) {
-        userOptions.value = res.data;
-      }
+    // 鑾峰彇鐝粍鍒楄〃
+    teamListAll().then(res => {
+      teamOptions.value = res.data || [];
     });
   });
 </script>
diff --git a/src/views/productionScheduling/schedulingManage/index.vue b/src/views/productionScheduling/schedulingManage/index.vue
new file mode 100644
index 0000000..d21f8e0
--- /dev/null
+++ b/src/views/productionScheduling/schedulingManage/index.vue
@@ -0,0 +1,462 @@
+<template>
+  <div class="app-container">
+    <div class="search_form">
+      <el-form :model="searchForm"
+               :inline="true">
+        <el-form-item label="宸ヤ綔鏃ユ湡:">
+          <el-date-picker v-model="searchForm.dateRange"
+                          type="daterange"
+                          range-separator="鑷�"
+                          start-placeholder="寮�濮嬫棩鏈�"
+                          end-placeholder="缁撴潫鏃ユ湡"
+                          format="YYYY-MM-DD"
+                          value-format="YYYY-MM-DD"
+                          style="width: 260px;"
+                          @change="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鎵�灞炶溅闂�:">
+          <el-select v-model="searchForm.workshopId"
+                     placeholder="璇烽�夋嫨"
+                     clearable
+                     style="width: 180px;"
+                     @change="handleSearchWorkshopChange">
+            <el-option v-for="item in workshopOptions"
+                       :key="item.id"
+                       :label="item.name"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐝粍:">
+          <el-select v-model="searchForm.teamId"
+                     placeholder="璇烽�夋嫨"
+                     clearable
+                     style="width: 180px;"
+                     @change="handleQuery">
+            <el-option v-for="item in searchTeamOptions"
+                       :key="item.id"
+                       :label="item.teamName"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐘舵��:">
+          <el-select v-model="searchForm.status"
+                     placeholder="璇烽�夋嫨"
+                     clearable
+                     style="width: 140px;"
+                     @change="handleQuery">
+            <el-option v-for="item in schedule_status"
+                       :key="item.value"
+                       :label="item.label"
+                       :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鎼滅储</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="table_list">
+      <div style="text-align: right"
+           class="mb10">
+        <el-button type="primary"
+                   @click="showNewModal">鏂板鎺掔彮</el-button>
+        <el-button type="danger"
+                   plain
+                   :disabled="selectedRows.length === 0"
+                   @click="handleDelete">鍒犻櫎鎺掔彮</el-button>
+      </div>
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                :tableLoading="tableLoading"
+                @selection-change="handleSelectionChange"
+                @pagination="pagination">
+        <template #shiftType="{ row }">
+          <el-tag :type="shiftTag(row.shiftType)">
+            {{ shiftLabel(row.shiftType) }}
+          </el-tag>
+        </template>
+        <template #status="{ row }">
+          <el-tag :type="statusTag(row.status)">
+            {{ statusLabel(row.status) }}
+          </el-tag>
+        </template>
+      </PIMTable>
+    </div>
+
+    <!-- 鏂板/缂栬緫寮圭獥 -->
+    <el-dialog v-model="dialogVisible"
+               :title="form.id ? '缂栬緫鎺掔彮' : '鏂板鎺掔彮'"
+               width="600px">
+      <el-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="100px">
+        <el-form-item label="宸ヤ綔鏃ユ湡"
+                      prop="workDate">
+          <el-date-picker v-model="form.workDate"
+                          type="date"
+                          placeholder="璇烽�夋嫨宸ヤ綔鏃ユ湡"
+                          format="YYYY-MM-DD"
+                          value-format="YYYY-MM-DD"
+                          style="width: 100%;" />
+        </el-form-item>
+        <el-form-item label="鐝"
+                      prop="shiftType">
+          <el-select v-model="form.shiftType"
+                     placeholder="璇烽�夋嫨鐝"
+                     style="width: 100%;">
+            <el-option v-for="item in shift_type"
+                       :key="item.value"
+                       :label="item.label"
+                       :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鎵�灞炶溅闂�"
+                      prop="workshopId">
+          <el-select v-model="form.workshopId"
+                     placeholder="璇烽�夋嫨鎵�灞炶溅闂�"
+                     style="width: 100%;"
+                     @change="handleFormWorkshopChange">
+            <el-option v-for="item in workshopOptions"
+                       :key="item.id"
+                       :label="item.name"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐝粍"
+                      prop="teamId">
+          <el-select v-model="form.teamId"
+                     placeholder="璇烽�夋嫨鐝粍"
+                     style="width: 100%;">
+            <el-option v-for="item in formTeamOptions"
+                       :key="item.id"
+                       :label="item.teamName"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐘舵��"
+                      prop="status">
+          <el-select v-model="form.status"
+                     placeholder="璇烽�夋嫨鐘舵��"
+                     style="width: 100%;">
+            <el-option v-for="item in schedule_status"
+                       :key="item.value"
+                       :label="item.label"
+                       :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="澶囨敞"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    maxlength="500" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button type="primary"
+                     @click="handleSubmit">纭畾</el-button>
+          <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import {
+    onMounted,
+    ref,
+    reactive,
+    computed,
+    getCurrentInstance,
+  } from "vue";
+  import dayjs from "dayjs";
+  import { useDict } from "@/utils/dict.js";
+  import {
+    listPage,
+    add,
+    update,
+    del,
+  } from "@/api/productionScheduling/scheduling.js";
+  import { listAll as workshopListAll } from "@/api/productionScheduling/workshop.js";
+  import { listAll as teamListAll } from "@/api/productionScheduling/team.js";
+
+  const { proxy } = getCurrentInstance();
+
+  // 瀛楀吀
+  const { shift_type, schedule_status } = useDict("shift_type", "schedule_status");
+
+  const findDictItem = (dict, val) => {
+    return dict.value.find(item => item.value === String(val));
+  };
+  const shiftLabel = val => {
+    const item = findDictItem(shift_type, val);
+    return item ? item.label : val;
+  };
+  const shiftTag = val => {
+    const item = findDictItem(shift_type, val);
+    return item ? item.elTagType : "info";
+  };
+  const statusLabel = val => {
+    const item = findDictItem(schedule_status, val);
+    return item ? item.label : val;
+  };
+  const statusTag = val => {
+    const item = findDictItem(schedule_status, val);
+    return item ? item.elTagType : "info";
+  };
+
+  const tableColumn = ref([
+    {
+      label: "宸ヤ綔鏃ユ湡",
+      prop: "workDate",
+      width: "120",
+    },
+    {
+      label: "鐝",
+      prop: "shiftType",
+      dataType: "slot",
+      slot: "shiftType",
+      width: "100",
+    },
+    {
+      label: "鎵�灞炶溅闂�",
+      prop: "workshopName",
+      width: "140",
+    },
+    {
+      label: "鐝粍",
+      prop: "teamName",
+      width: "140",
+    },
+    {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "slot",
+      slot: "status",
+      width: "100",
+    },
+    {
+      label: "澶囨敞",
+      prop: "remark",
+    },
+    {
+      label: "鏇存柊鏃堕棿",
+      prop: "updateTime",
+      width: "160",
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 100,
+      operation: [
+        {
+          name: "缂栬緫",
+          type: "text",
+          clickFun: row => {
+            showEditModal(row);
+          },
+        },
+      ],
+    },
+  ]);
+  const tableData = ref([]);
+  const selectedRows = ref([]);
+  const tableLoading = ref(false);
+  const dialogVisible = ref(false);
+  const formRef = ref(null);
+  const workshopOptions = ref([]);
+  const allTeams = ref([]);
+  const page = reactive({
+    current: 1,
+    size: 100,
+    total: 0,
+  });
+
+  const data = reactive({
+    searchForm: {
+      dateRange: [],
+      workshopId: null,
+      teamId: null,
+      status: null,
+    },
+  });
+  const { searchForm } = data;
+
+  const form = reactive({
+    id: null,
+    workDate: "",
+    shiftType: "",
+    workshopId: null,
+    teamId: null,
+    status: "1",
+    remark: "",
+  });
+
+  const rules = {
+    workDate: [{ required: true, message: "璇烽�夋嫨宸ヤ綔鏃ユ湡", trigger: "change" }],
+    shiftType: [{ required: true, message: "璇烽�夋嫨鐝", trigger: "change" }],
+    workshopId: [{ required: true, message: "璇烽�夋嫨鎵�灞炶溅闂�", trigger: "change" }],
+    teamId: [{ required: true, message: "璇烽�夋嫨鐝粍", trigger: "change" }],
+  };
+
+  // 鎼滅储鐝粍涓嬫媺锛氭寜鎵�閫夎溅闂磋仈鍔�
+  const searchTeamOptions = computed(() => {
+    if (searchForm.workshopId) {
+      return allTeams.value.filter(item => item.workshopId === searchForm.workshopId);
+    }
+    return allTeams.value;
+  });
+
+  // 寮圭獥鐝粍涓嬫媺锛氭寜鎵�閫夎溅闂磋仈鍔�
+  const formTeamOptions = computed(() => {
+    if (form.workshopId) {
+      return allTeams.value.filter(item => item.workshopId === form.workshopId);
+    }
+    return allTeams.value;
+  });
+
+  /** 鎼滅储鎸夐挳鎿嶄綔 */
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  const handleSearchWorkshopChange = () => {
+    searchForm.teamId = null;
+    handleQuery();
+  };
+
+  const handleFormWorkshopChange = () => {
+    form.teamId = null;
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  const getList = () => {
+    tableLoading.value = true;
+    const { dateRange, ...rest } = searchForm;
+    const params = {
+      ...rest,
+      ...(dateRange && dateRange.length === 2
+        ? { startDate: dateRange[0], endDate: dateRange[1] }
+        : {}),
+      ...page,
+    };
+    listPage(params)
+      .then(res => {
+        tableLoading.value = false;
+        tableData.value = res.data.records;
+        page.total = res.data.total;
+      })
+      .catch(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  // 琛ㄦ牸閫夋嫨鏁版嵁
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  const resetForm = () => {
+    form.id = null;
+    form.workDate = dayjs().format("YYYY-MM-DD");
+    form.shiftType = "";
+    form.workshopId = null;
+    form.teamId = null;
+    form.status = "1";
+    form.remark = "";
+  };
+
+  // 鎵撳紑鏂板寮规
+  const showNewModal = () => {
+    resetForm();
+    dialogVisible.value = true;
+  };
+
+  const showEditModal = row => {
+    resetForm();
+    form.id = row.id;
+    form.workDate = row.workDate;
+    form.shiftType = row.shiftType !== null && row.shiftType !== undefined ? String(row.shiftType) : "";
+    form.workshopId = row.workshopId;
+    form.teamId = row.teamId;
+    form.status = row.status !== null && row.status !== undefined ? String(row.status) : "1";
+    form.remark = row.remark;
+    dialogVisible.value = true;
+  };
+
+  const handleSubmit = () => {
+    formRef.value.validate(valid => {
+      if (!valid) return;
+      const payload = {
+        id: form.id,
+        workDate: form.workDate,
+        shiftType: form.shiftType !== "" ? Number(form.shiftType) : null,
+        workshopId: form.workshopId,
+        teamId: form.teamId,
+        status: form.status !== "" ? Number(form.status) : null,
+        remark: form.remark,
+      };
+      const request = payload.id ? update(payload) : add(payload);
+      request.then(() => {
+        proxy.$modal.msgSuccess(payload.id ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+        dialogVisible.value = false;
+        getList();
+      });
+    });
+  };
+
+  // 鎵归噺鍒犻櫎
+  const handleDelete = () => {
+    const rows = selectedRows.value;
+    const ids = rows.map(item => item.id);
+    const firstDesc = `${rows[0].workDate} ${rows[0].teamName || ""}`;
+    proxy.$modal
+      .confirm(
+        `鏄惁纭鍒犻櫎"${firstDesc}"${rows.length > 1 ? `绛�${rows.length}鏉℃暟鎹甡 : ""}锛焋
+      )
+      .then(function () {
+        return del(ids.join(","));
+      })
+      .then(() => {
+        getList();
+        proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+      })
+      .catch(() => {});
+  };
+
+  const getWorkshopOptions = () => {
+    workshopListAll().then(res => {
+      workshopOptions.value = res.data || [];
+    });
+  };
+
+  const getTeamOptions = () => {
+    teamListAll().then(res => {
+      allTeams.value = res.data || [];
+    });
+  };
+
+  onMounted(() => {
+    getList();
+    getWorkshopOptions();
+    getTeamOptions();
+  });
+</script>
+
+<style scoped></style>
diff --git a/src/views/productionScheduling/teamManage/index.vue b/src/views/productionScheduling/teamManage/index.vue
new file mode 100644
index 0000000..2eefe67
--- /dev/null
+++ b/src/views/productionScheduling/teamManage/index.vue
@@ -0,0 +1,375 @@
+<template>
+  <div class="app-container">
+    <div class="search_form">
+      <el-form :model="searchForm"
+               :inline="true">
+        <el-form-item label="鐝粍鍚嶇О:">
+          <el-input v-model="searchForm.teamName"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    prefix-icon="Search"
+                    style="width: 200px;"
+                    @change="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鎵�灞炶溅闂�:">
+          <el-select v-model="searchForm.workshopId"
+                     placeholder="璇烽�夋嫨"
+                     clearable
+                     style="width: 200px;"
+                     @change="handleQuery">
+            <el-option v-for="item in workshopOptions"
+                       :key="item.id"
+                       :label="item.name"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐘舵��:">
+          <el-select v-model="searchForm.status"
+                     placeholder="璇烽�夋嫨"
+                     clearable
+                     style="width: 160px;"
+                     @change="handleQuery">
+            <el-option v-for="item in statusOptions"
+                       :key="item.value"
+                       :label="item.label"
+                       :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鎼滅储</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="table_list">
+      <div style="text-align: right"
+           class="mb10">
+        <el-button type="primary"
+                   @click="showNewModal">鏂板鐝粍</el-button>
+        <el-button type="danger"
+                   plain
+                   :disabled="selectedRows.length === 0"
+                   @click="handleDelete">鍒犻櫎鐝粍</el-button>
+      </div>
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                :tableLoading="tableLoading"
+                @selection-change="handleSelectionChange"
+                @pagination="pagination">
+        <template #status="{ row }">
+          <el-tag :type="row.status === 0 ? 'success' : 'info'">
+            {{ row.status === 0 ? "鍚敤" : "鍋滅敤" }}
+          </el-tag>
+        </template>
+      </PIMTable>
+    </div>
+
+    <!-- 鏂板/缂栬緫寮圭獥 -->
+    <el-dialog v-model="dialogVisible"
+               :title="form.id ? '缂栬緫鐝粍' : '鏂板鐝粍'"
+               width="600px">
+      <el-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="100px">
+        <el-form-item label="鐝粍鍚嶇О"
+                      prop="teamName">
+          <el-input v-model="form.teamName"
+                    placeholder="璇疯緭鍏ョ彮缁勫悕绉�"
+                    maxlength="100" />
+        </el-form-item>
+        <el-form-item label="鐝粍缂栫爜"
+                      prop="teamCode">
+          <el-input v-model="form.teamCode"
+                    placeholder="璇疯緭鍏ョ彮缁勭紪鐮�"
+                    maxlength="50" />
+        </el-form-item>
+        <el-form-item label="鎵�灞炶溅闂�"
+                      prop="workshopId">
+          <el-select v-model="form.workshopId"
+                     placeholder="璇烽�夋嫨鎵�灞炶溅闂�"
+                     style="width: 100%;">
+            <el-option v-for="item in workshopOptions"
+                       :key="item.id"
+                       :label="item.name"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鐝粍闀�"
+                      prop="leaderUserId">
+          <el-select v-model="form.leaderUserId"
+                     placeholder="璇烽�夋嫨鐝粍闀�"
+                     clearable
+                     filterable
+                     style="width: 100%;"
+                     @change="handleLeaderChange">
+            <el-option v-for="item in userOptions"
+                       :key="item.userId"
+                       :label="item.nickName"
+                       :value="item.userId" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鑱旂郴鐢佃瘽"
+                      prop="contactPhone">
+          <el-input v-model="form.contactPhone"
+                    placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+                    maxlength="20" />
+        </el-form-item>
+        <el-form-item label="鐘舵��"
+                      prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="item in statusOptions"
+                      :key="item.value"
+                      :value="item.value">{{ item.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="澶囨敞"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    maxlength="500" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button type="primary"
+                     @click="handleSubmit">纭畾</el-button>
+          <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { onMounted, ref, reactive, getCurrentInstance } from "vue";
+  import {
+    listPage,
+    add,
+    update,
+    del,
+  } from "@/api/productionScheduling/team.js";
+  import { listAll as workshopListAll } from "@/api/productionScheduling/workshop.js";
+  import { userListNoPageByTenantId } from "@/api/system/user.js";
+
+  const { proxy } = getCurrentInstance();
+
+  const statusOptions = [
+    { label: "鍚敤", value: 0 },
+    { label: "鍋滅敤", value: 1 },
+  ];
+
+  const tableColumn = ref([
+    {
+      label: "鐝粍鍚嶇О",
+      prop: "teamName",
+    },
+    {
+      label: "鐝粍缂栫爜",
+      prop: "teamCode",
+      width: "120",
+    },
+    {
+      label: "鎵�灞炶溅闂�",
+      prop: "workshopName",
+      width: "140",
+    },
+    {
+      label: "鐝粍闀�",
+      prop: "leaderName",
+      width: "100",
+    },
+    {
+      label: "鑱旂郴鐢佃瘽",
+      prop: "contactPhone",
+      width: "140",
+    },
+    {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "slot",
+      slot: "status",
+      width: "90",
+    },
+    {
+      label: "澶囨敞",
+      prop: "remark",
+    },
+    {
+      label: "鏇存柊鏃堕棿",
+      prop: "updateTime",
+      width: "160",
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 100,
+      operation: [
+        {
+          name: "缂栬緫",
+          type: "text",
+          clickFun: row => {
+            showEditModal(row);
+          },
+        },
+      ],
+    },
+  ]);
+  const tableData = ref([]);
+  const selectedRows = ref([]);
+  const tableLoading = ref(false);
+  const dialogVisible = ref(false);
+  const formRef = ref(null);
+  const workshopOptions = ref([]);
+  const userOptions = ref([]);
+  const page = reactive({
+    current: 1,
+    size: 100,
+    total: 0,
+  });
+
+  const data = reactive({
+    searchForm: {
+      teamName: "",
+      workshopId: null,
+      status: null,
+    },
+  });
+  const { searchForm } = data;
+
+  const form = reactive({
+    id: null,
+    teamName: "",
+    teamCode: "",
+    workshopId: null,
+    leaderUserId: null,
+    leaderName: "",
+    contactPhone: "",
+    status: 0,
+    remark: "",
+  });
+
+  const rules = {
+    teamName: [{ required: true, message: "璇疯緭鍏ョ彮缁勫悕绉�", trigger: "blur" }],
+    workshopId: [{ required: true, message: "璇烽�夋嫨鎵�灞炶溅闂�", trigger: "change" }],
+  };
+
+  /** 鎼滅储鎸夐挳鎿嶄綔 */
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  const getList = () => {
+    tableLoading.value = true;
+    const params = { ...searchForm, ...page };
+    listPage(params)
+      .then(res => {
+        tableLoading.value = false;
+        tableData.value = res.data.records;
+        page.total = res.data.total;
+      })
+      .catch(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  // 琛ㄦ牸閫夋嫨鏁版嵁
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  const resetForm = () => {
+    form.id = null;
+    form.teamName = "";
+    form.teamCode = "";
+    form.workshopId = null;
+    form.leaderUserId = null;
+    form.leaderName = "";
+    form.contactPhone = "";
+    form.status = 0;
+    form.remark = "";
+  };
+
+  // 鎵撳紑鏂板寮规
+  const showNewModal = () => {
+    resetForm();
+    dialogVisible.value = true;
+  };
+
+  const showEditModal = row => {
+    resetForm();
+    Object.assign(form, JSON.parse(JSON.stringify(row)));
+    dialogVisible.value = true;
+  };
+
+  const handleLeaderChange = val => {
+    const user = userOptions.value.find(item => item.userId === val);
+    form.leaderName = user ? user.nickName : "";
+  };
+
+  const handleSubmit = () => {
+    formRef.value.validate(valid => {
+      if (!valid) return;
+      const request = form.id ? update(form) : add(form);
+      request.then(() => {
+        proxy.$modal.msgSuccess(form.id ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+        dialogVisible.value = false;
+        getList();
+      });
+    });
+  };
+
+  // 鎵归噺鍒犻櫎
+  const handleDelete = () => {
+    const names = selectedRows.value.map(item => item.teamName);
+    const ids = selectedRows.value.map(item => item.id);
+    proxy.$modal
+      .confirm(
+        `鏄惁纭鍒犻櫎鐝粍"${names[0]}"${names.length > 1 ? `绛�${names.length}鏉℃暟鎹甡 : ""}锛焋
+      )
+      .then(function () {
+        return del(ids.join(","));
+      })
+      .then(() => {
+        getList();
+        proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+      })
+      .catch(() => {});
+  };
+
+  const getWorkshopOptions = () => {
+    workshopListAll().then(res => {
+      workshopOptions.value = res.data || [];
+    });
+  };
+
+  const getUserOptions = () => {
+    userListNoPageByTenantId().then(res => {
+      if (res.code === 200) {
+        userOptions.value = res.data;
+      }
+    });
+  };
+
+  onMounted(() => {
+    getList();
+    getWorkshopOptions();
+    getUserOptions();
+  });
+</script>
+
+<style scoped></style>
diff --git a/src/views/productionScheduling/workshopManage/index.vue b/src/views/productionScheduling/workshopManage/index.vue
new file mode 100644
index 0000000..c015514
--- /dev/null
+++ b/src/views/productionScheduling/workshopManage/index.vue
@@ -0,0 +1,308 @@
+<template>
+  <div class="app-container">
+    <div class="search_form">
+      <el-form :model="searchForm"
+               :inline="true">
+        <el-form-item label="杞﹂棿鍚嶇О:">
+          <el-input v-model="searchForm.name"
+                    placeholder="璇疯緭鍏�"
+                    clearable
+                    prefix-icon="Search"
+                    style="width: 200px;"
+                    @change="handleQuery" />
+        </el-form-item>
+        <el-form-item label="鐘舵��:">
+          <el-select v-model="searchForm.status"
+                     placeholder="璇烽�夋嫨"
+                     clearable
+                     style="width: 160px;"
+                     @change="handleQuery">
+            <el-option v-for="item in statusOptions"
+                       :key="item.value"
+                       :label="item.label"
+                       :value="item.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary"
+                     @click="handleQuery">鎼滅储</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div class="table_list">
+      <div style="text-align: right"
+           class="mb10">
+        <el-button type="primary"
+                   @click="showNewModal">鏂板杞﹂棿</el-button>
+        <el-button type="danger"
+                   plain
+                   :disabled="selectedRows.length === 0"
+                   @click="handleDelete">鍒犻櫎杞﹂棿</el-button>
+      </div>
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                :tableLoading="tableLoading"
+                @selection-change="handleSelectionChange"
+                @pagination="pagination">
+        <template #status="{ row }">
+          <el-tag :type="row.status === 0 ? 'success' : 'info'">
+            {{ row.status === 0 ? "鍚敤" : "鍋滅敤" }}
+          </el-tag>
+        </template>
+      </PIMTable>
+    </div>
+
+    <!-- 鏂板/缂栬緫寮圭獥 -->
+    <el-dialog v-model="dialogVisible"
+               :title="form.id ? '缂栬緫杞﹂棿' : '鏂板杞﹂棿'"
+               width="600px">
+      <el-form ref="formRef"
+               :model="form"
+               :rules="rules"
+               label-width="100px">
+        <el-form-item label="杞﹂棿鍚嶇О"
+                      prop="name">
+          <el-input v-model="form.name"
+                    placeholder="璇疯緭鍏ヨ溅闂村悕绉�"
+                    maxlength="100" />
+        </el-form-item>
+        <el-form-item label="杞﹂棿缂栫爜"
+                      prop="code">
+          <el-input v-model="form.code"
+                    placeholder="璇疯緭鍏ヨ溅闂寸紪鐮�"
+                    maxlength="50" />
+        </el-form-item>
+        <el-form-item label="璐熻矗浜�"
+                      prop="principal">
+          <el-input v-model="form.principal"
+                    placeholder="璇疯緭鍏ヨ礋璐d汉"
+                    maxlength="50" />
+        </el-form-item>
+        <el-form-item label="鑱旂郴鐢佃瘽"
+                      prop="contactPhone">
+          <el-input v-model="form.contactPhone"
+                    placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+                    maxlength="20" />
+        </el-form-item>
+        <el-form-item label="鐘舵��"
+                      prop="status">
+          <el-radio-group v-model="form.status">
+            <el-radio v-for="item in statusOptions"
+                      :key="item.value"
+                      :value="item.value">{{ item.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="澶囨敞"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    maxlength="500" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button type="primary"
+                     @click="handleSubmit">纭畾</el-button>
+          <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+  import { onMounted, ref, reactive, getCurrentInstance } from "vue";
+  import {
+    listPage,
+    add,
+    update,
+    del,
+  } from "@/api/productionScheduling/workshop.js";
+
+  const { proxy } = getCurrentInstance();
+
+  const statusOptions = [
+    { label: "鍚敤", value: 0 },
+    { label: "鍋滅敤", value: 1 },
+  ];
+
+  const tableColumn = ref([
+    {
+      label: "杞﹂棿鍚嶇О",
+      prop: "name",
+    },
+    {
+      label: "杞﹂棿缂栫爜",
+      prop: "code",
+      width: "120",
+    },
+    {
+      label: "璐熻矗浜�",
+      prop: "principal",
+      width: "100",
+    },
+    {
+      label: "鑱旂郴鐢佃瘽",
+      prop: "contactPhone",
+      width: "140",
+    },
+    {
+      label: "鐘舵��",
+      prop: "status",
+      dataType: "slot",
+      slot: "status",
+      width: "90",
+    },
+    {
+      label: "澶囨敞",
+      prop: "remark",
+    },
+    {
+      label: "鏇存柊鏃堕棿",
+      prop: "updateTime",
+      width: "160",
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 100,
+      operation: [
+        {
+          name: "缂栬緫",
+          type: "text",
+          clickFun: row => {
+            showEditModal(row);
+          },
+        },
+      ],
+    },
+  ]);
+  const tableData = ref([]);
+  const selectedRows = ref([]);
+  const tableLoading = ref(false);
+  const dialogVisible = ref(false);
+  const formRef = ref(null);
+  const page = reactive({
+    current: 1,
+    size: 100,
+    total: 0,
+  });
+
+  const data = reactive({
+    searchForm: {
+      name: "",
+      status: null,
+    },
+  });
+  const { searchForm } = data;
+
+  const form = reactive({
+    id: null,
+    name: "",
+    code: "",
+    principal: "",
+    contactPhone: "",
+    status: 0,
+    remark: "",
+  });
+
+  const rules = {
+    name: [{ required: true, message: "璇疯緭鍏ヨ溅闂村悕绉�", trigger: "blur" }],
+  };
+
+  /** 鎼滅储鎸夐挳鎿嶄綔 */
+  const handleQuery = () => {
+    page.current = 1;
+    getList();
+  };
+
+  const pagination = obj => {
+    page.current = obj.page;
+    page.size = obj.limit;
+    getList();
+  };
+
+  const getList = () => {
+    tableLoading.value = true;
+    const params = { ...searchForm, ...page };
+    listPage(params)
+      .then(res => {
+        tableLoading.value = false;
+        tableData.value = res.data.records;
+        page.total = res.data.total;
+      })
+      .catch(() => {
+        tableLoading.value = false;
+      });
+  };
+
+  // 琛ㄦ牸閫夋嫨鏁版嵁
+  const handleSelectionChange = selection => {
+    selectedRows.value = selection;
+  };
+
+  const resetForm = () => {
+    form.id = null;
+    form.name = "";
+    form.code = "";
+    form.principal = "";
+    form.contactPhone = "";
+    form.status = 0;
+    form.remark = "";
+  };
+
+  // 鎵撳紑鏂板寮规
+  const showNewModal = () => {
+    resetForm();
+    dialogVisible.value = true;
+  };
+
+  const showEditModal = row => {
+    resetForm();
+    Object.assign(form, JSON.parse(JSON.stringify(row)));
+    dialogVisible.value = true;
+  };
+
+  const handleSubmit = () => {
+    formRef.value.validate(valid => {
+      if (!valid) return;
+      const request = form.id ? update(form) : add(form);
+      request.then(() => {
+        proxy.$modal.msgSuccess(form.id ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+        dialogVisible.value = false;
+        getList();
+      });
+    });
+  };
+
+  // 鎵归噺鍒犻櫎
+  const handleDelete = () => {
+    const names = selectedRows.value.map(item => item.name);
+    const ids = selectedRows.value.map(item => item.id);
+    proxy.$modal
+      .confirm(
+        `鏄惁纭鍒犻櫎杞﹂棿"${names[0]}"${names.length > 1 ? `绛�${names.length}鏉℃暟鎹甡 : ""}锛焋
+      )
+      .then(function () {
+        return del(ids.join(","));
+      })
+      .then(() => {
+        getList();
+        proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+      })
+      .catch(() => {});
+  };
+
+  onMounted(() => {
+    getList();
+  });
+</script>
+
+<style scoped></style>

--
Gitblit v1.9.3