From 899cf3d156b845ec610aae7352ab4b8332f32037 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期三, 01 四月 2026 17:31:32 +0800
Subject: [PATCH] fix: 设备备件与设备维修、设备保养关联起来

---
 src/views/equipmentManagement/spareParts/index.vue           |  292 +++++++++++++++++++++++++-----------
 src/views/equipmentManagement/repair/Modal/MaintainModal.vue |  115 ++++++++++++++
 src/api/equipmentManagement/sparePartsUsage.js               |   36 ++++
 3 files changed, 352 insertions(+), 91 deletions(-)

diff --git a/src/api/equipmentManagement/sparePartsUsage.js b/src/api/equipmentManagement/sparePartsUsage.js
new file mode 100644
index 0000000..9fca9d3
--- /dev/null
+++ b/src/api/equipmentManagement/sparePartsUsage.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+/**
+ * 澶囦欢棰嗙敤璁板綍 - 鍒嗛〉鏌ヨ
+ * params: { current, size, sparePartId?, sparePartName?, source?, deviceId?, startTime?, endTime? }
+ */
+export const getSparePartsUsagePage = (params) => {
+  return request({
+    url: "/sparePartsUsage/listPage",
+    method: "get",
+    params,
+  });
+};
+
+/**
+ * 澶囦欢棰嗙敤璁板綍 - 鏂板
+ * data 绀轰緥锛�
+ * {
+ *   source: "repair" | "upkeep" | "manual",
+ *   sourceId?: number | string,
+ *   deviceId?: number | string,
+ *   deviceName?: string,
+ *   operatorId?: number | string,
+ *   operator?: string,
+ *   useTime?: string, // YYYY-MM-DD HH:mm:ss
+ *   items: [{ sparePartId: number|string, qty: number }]
+ * }
+ */
+export const addSparePartsUsage = (data) => {
+  return request({
+    url: "/sparePartsUsage/add",
+    method: "post",
+    data,
+  });
+};
+
diff --git a/src/views/equipmentManagement/repair/Modal/MaintainModal.vue b/src/views/equipmentManagement/repair/Modal/MaintainModal.vue
index 496b072..b0b09f0 100644
--- a/src/views/equipmentManagement/repair/Modal/MaintainModal.vue
+++ b/src/views/equipmentManagement/repair/Modal/MaintainModal.vue
@@ -32,23 +32,61 @@
           style="width: 100%"
         />
       </el-form-item>
+      <el-form-item label="璁惧澶囦欢">
+        <el-select v-model="form.sparePartsIds" :loading="loadingSparePartOptions" placeholder="璇烽�夋嫨璁惧澶囦欢" multiple filterable>
+          <el-option
+              v-for="item in sparePartOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item v-if="selectedSpareParts.length" label="棰嗙敤鏁伴噺">
+        <div style="width: 100%">
+          <div
+            v-for="item in selectedSpareParts"
+            :key="item.id"
+            style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;"
+          >
+            <div style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
+              {{ item.name }}
+              <span v-if="item.quantity !== null && item.quantity !== undefined" style="color: #909399;">
+                锛堝簱瀛橈細{{ item.quantity }}锛�
+              </span>
+            </div>
+            <el-input-number
+              v-model="sparePartQtyMap[item.id]"
+              :min="1"
+              :max="item.quantity !== null && item.quantity !== undefined ? Number(item.quantity) : undefined"
+              :step="1"
+              controls-position="right"
+              style="width: 180px"
+            />
+          </div>
+        </div>
+      </el-form-item>
     </el-form>
   </FormDialog>
 </template>
 
 <script setup>
+import { computed, getCurrentInstance, nextTick, ref } from "vue";
 import FormDialog from "@/components/Dialog/FormDialog.vue";
 import { addMaintain } from "@/api/equipmentManagement/repair";
 import useFormData from "@/hooks/useFormData";
 import useUserStore from "@/store/modules/user";
 import dayjs from "dayjs";
 import { ElMessage } from "element-plus";
+import { getSparePartsList } from "@/api/equipmentManagement/spareParts";
 
 defineOptions({
   name: "缁翠慨妯℃�佹",
 });
 
 const emits = defineEmits(["ok"]);
+const { proxy } = getCurrentInstance();
 
 // 淇濆瓨鎶ヤ慨璁板綍鐨刬d
 const repairId = ref();
@@ -61,6 +99,16 @@
   maintenanceResult: undefined, // 缁翠慨缁撴灉
   maintenanceTime: undefined, // 缁翠慨鏃ユ湡
   status: 0,
+  sparePartsIds: [],
+});
+const sparePartOptions = ref([])
+const loadingSparePartOptions = ref(true)
+const sparePartQtyMap = ref({})
+
+const selectedSpareParts = computed(() => {
+  const ids = Array.isArray(form.sparePartsIds) ? form.sparePartsIds : [];
+  const set = new Set(ids.map((i) => String(i)));
+  return (sparePartOptions.value || []).filter((p) => set.has(String(p.id)));
 });
 
 const setForm = (data) => {
@@ -71,16 +119,59 @@
       ? dayjs(data.maintenanceTime).format("YYYY-MM-DD HH:mm:ss")
       : dayjs().format("YYYY-MM-DD HH:mm:ss");
   form.status = 1; // 榛樿鐘舵�佷负瀹岀粨
+  // multiple 閫夋嫨鍣ㄨ姹傛暟缁勶紱鍚庣甯歌繑鍥� "1,2,3"
+  if (Array.isArray(data?.sparePartsIds)) {
+    form.sparePartsIds = data.sparePartsIds.map((v) => Number(v)).filter((v) => Number.isFinite(v));
+  } else if (typeof data?.sparePartsIds === "string") {
+    form.sparePartsIds = data.sparePartsIds
+      .split(",")
+      .map((s) => Number(String(s).trim()))
+      .filter((v) => Number.isFinite(v));
+  } else if (typeof data?.sparePartsIds === "number") {
+    form.sparePartsIds = [data.sparePartsIds];
+  } else {
+    form.sparePartsIds = [];
+  }
 };
 
 const sendForm = async () => {
   loading.value = true;
   try {
-    const { code } = await addMaintain({ id: repairId.value, ...form });
+    // 棰嗙敤鏁伴噺鏍¢獙
+    if (Array.isArray(form.sparePartsIds) && form.sparePartsIds.length > 0) {
+      for (const partId of form.sparePartsIds) {
+        const qty = Number(sparePartQtyMap.value?.[partId]);
+        if (!Number.isFinite(qty) || qty <= 0) {
+          proxy?.$modal?.msgError?.("璇峰~鍐欏浠堕鐢ㄦ暟閲�");
+          return;
+        }
+        const part = sparePartOptions.value.find((p) => String(p.id) === String(partId));
+        const stock = part?.quantity;
+        if (stock !== null && stock !== undefined && Number.isFinite(Number(stock))) {
+          if (qty > Number(stock)) {
+            proxy?.$modal?.msgError?.(`澶囦欢銆�${part?.name || ""}銆嶉鐢ㄦ暟閲忎笉鑳借秴杩囧簱瀛橈紙${stock}锛塦);
+            return;
+          }
+        }
+      }
+    }
+    const data = {
+      id: repairId.value,
+      ...form,
+      sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
+      sparePartsQty: form.sparePartsIds
+        ? form.sparePartsIds.map((id) => sparePartQtyMap.value?.[id] ?? 1).join(",")
+        : "",
+      sparePartsUseList: form.sparePartsIds
+        ? form.sparePartsIds.map((id) => ({ id, quantity: sparePartQtyMap.value?.[id] ?? 1 }))
+        : [],
+    }
+    const { code } = await addMaintain(data);
     if (code == 200) {
       ElMessage.success("缁翠慨鎴愬姛");
       emits("ok");
       resetForm();
+      sparePartQtyMap.value = {};
       visible.value = false;
     }
   } finally {
@@ -88,13 +179,34 @@
   }
 };
 
+const fetchSparePartOptions = () => {
+  loadingSparePartOptions.value = true;
+  // 鍜屽浠剁鐞嗛〉涓�鑷达細/spareParts/listPage 鈫� res.data.records
+  getSparePartsList({ current: 1, size: 1000 })
+    .then((res) => {
+      if (res.code === 200) {
+        sparePartOptions.value = res?.data?.records || [];
+      } else {
+        sparePartOptions.value = [];
+      }
+    })
+    .catch(() => {
+      sparePartOptions.value = [];
+    })
+    .finally(() => {
+      loadingSparePartOptions.value = false;
+    });
+}
+
 const handleCancel = () => {
   resetForm();
+  sparePartQtyMap.value = {};
   visible.value = false;
 };
 
 const handleClose = () => {
   resetForm();
+  sparePartQtyMap.value = {};
   visible.value = false;
 };
 
@@ -103,6 +215,7 @@
   visible.value = true;
   await nextTick();
   setForm(row);
+  fetchSparePartOptions()
 };
 
 defineExpose({
diff --git a/src/views/equipmentManagement/spareParts/index.vue b/src/views/equipmentManagement/spareParts/index.vue
index 4a48d28..823071f 100644
--- a/src/views/equipmentManagement/spareParts/index.vue
+++ b/src/views/equipmentManagement/spareParts/index.vue
@@ -1,107 +1,144 @@
 <template>
   <div class="spare-part-category">
-		<div class="search_form">
-			<el-form :inline="true" :model="queryParams" class="search-form">
-				<el-form-item label="澶囦欢鍚嶇О">
-					<el-input
-						v-model="queryParams.name"
-						placeholder="璇疯緭鍏ュ浠跺悕绉�"
-						clearable
-						style="width: 240px"
-					/>
-				</el-form-item>
-				<el-form-item>
-					<el-button type="primary" @click="handleQuery">鏌ヨ</el-button>
-					<el-button @click="resetQuery">閲嶇疆</el-button>
-				</el-form-item>
-			</el-form>
-			<div>
-				<el-button type="primary" @click="addCategory" >鏂板</el-button>
-			</div>
-		</div>
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+      <el-tab-pane label="澶囦欢鍒楄〃" name="list">
+        <div class="search_form">
+          <el-form :inline="true" :model="queryParams" class="search-form">
+            <el-form-item label="澶囦欢鍚嶇О">
+              <el-input
+                v-model="queryParams.name"
+                placeholder="璇疯緭鍏ュ浠跺悕绉�"
+                clearable
+                style="width: 240px"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleQuery">鏌ヨ</el-button>
+              <el-button @click="resetQuery">閲嶇疆</el-button>
+            </el-form-item>
+          </el-form>
+          <div>
+            <el-button type="primary" @click="addCategory">鏂板</el-button>
+          </div>
+        </div>
 
-    <PIMTable
-        rowKey="id"
-        :column="columns"
-        :tableData="renderTableData"
-        :tableLoading="loading"
-        :page="pagination"
-        :isShowPagination="true"
-        @pagination="handleSizeChange"
-    >
-      <template #status="{ row }">
-        <el-tag type="success" size="small">{{ row.status }}</el-tag>
-      </template>
-    </PIMTable>
-    
-    <el-dialog title="鍒嗙被绠$悊" v-model="dialogVisible" width="60%">
-      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
-        <el-form-item label="璁惧" prop="deviceLedgerIds">
-          <el-select
-            v-model="form.deviceLedgerIds"
-            placeholder="璇烽�夋嫨璁惧"
-            filterable
-            default-first-option
-            :reserve-keyword="false"
-            multiple
-            style="width: 100%"
-          >
-            <el-option
-              v-for="(item, index) in deviceOptions"
-              :key="index"
-              :label="item.deviceName"
-              :value="item.id"
-            ></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="澶囦欢鍚嶇О" prop="name">
-          <el-input v-model="form.name"></el-input>
-        </el-form-item>
-        <el-form-item label="澶囦欢缂栧彿" prop="sparePartsNo">
-          <el-input v-model="form.sparePartsNo"></el-input>
-        </el-form-item>
-        <el-form-item label="鏁伴噺" prop="quantity">
-          <el-input type="number" v-model="form.quantity"></el-input>
-        </el-form-item>
-        <el-form-item label="鐘舵��" prop="status">
-          <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
-            <el-option label="姝e父" value="姝e父"></el-option>
-            <el-option label="绂佺敤" value="绂佺敤"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="鎻忚堪" prop="description">
-          <el-input v-model="form.description"></el-input>
-        </el-form-item>
-        <el-form-item label="浠锋牸" prop="price">
-          <el-input-number
-            v-model="form.price"
-            placeholder="璇疯緭鍏ヤ环鏍�"
-            :min="0"
-            :step="0.01"
-            :precision="2"
-            style="width: 100%"
-          ></el-input-number>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button @click="dialogVisible = false" :disabled="formLoading">鍙栨秷</el-button>
-          <el-button type="primary" @click="submitForm" :loading="formLoading">纭畾</el-button>
-        </span>
-      </template>
-    </el-dialog>
+        <PIMTable
+          rowKey="id"
+          :column="columns"
+          :tableData="renderTableData"
+          :tableLoading="loading"
+          :page="pagination"
+          :isShowPagination="true"
+          @pagination="handleSizeChange"
+        >
+          <template #status="{ row }">
+            <el-tag type="success" size="small">{{ row.status }}</el-tag>
+          </template>
+        </PIMTable>
+
+        <el-dialog title="鍒嗙被绠$悊" v-model="dialogVisible" width="60%">
+          <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+            <el-form-item label="璁惧" prop="deviceLedgerIds">
+              <el-select
+                v-model="form.deviceLedgerIds"
+                placeholder="璇烽�夋嫨璁惧"
+                filterable
+                default-first-option
+                :reserve-keyword="false"
+                multiple
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="(item, index) in deviceOptions"
+                  :key="index"
+                  :label="item.deviceName"
+                  :value="item.id"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="澶囦欢鍚嶇О" prop="name">
+              <el-input v-model="form.name"></el-input>
+            </el-form-item>
+            <el-form-item label="澶囦欢缂栧彿" prop="sparePartsNo">
+              <el-input v-model="form.sparePartsNo"></el-input>
+            </el-form-item>
+            <el-form-item label="鏁伴噺" prop="quantity">
+              <el-input type="number" v-model="form.quantity"></el-input>
+            </el-form-item>
+            <el-form-item label="鐘舵��" prop="status">
+              <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
+                <el-option label="姝e父" value="姝e父"></el-option>
+                <el-option label="绂佺敤" value="绂佺敤"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="鎻忚堪" prop="description">
+              <el-input v-model="form.description"></el-input>
+            </el-form-item>
+            <el-form-item label="浠锋牸" prop="price">
+              <el-input-number
+                v-model="form.price"
+                placeholder="璇疯緭鍏ヤ环鏍�"
+                :min="0"
+                :step="0.01"
+                :precision="2"
+                style="width: 100%"
+              ></el-input-number>
+            </el-form-item>
+          </el-form>
+          <template #footer>
+            <span class="dialog-footer">
+              <el-button @click="dialogVisible = false" :disabled="formLoading">鍙栨秷</el-button>
+              <el-button type="primary" @click="submitForm" :loading="formLoading">纭畾</el-button>
+            </span>
+          </template>
+        </el-dialog>
+      </el-tab-pane>
+
+      <el-tab-pane label="澶囦欢棰嗙敤璁板綍" name="usage">
+        <div class="search_form">
+          <el-form :inline="true" :model="usageQuery" class="search-form">
+            <el-form-item label="澶囦欢鍚嶇О">
+              <el-input v-model="usageQuery.sparePartName" placeholder="璇疯緭鍏ュ浠跺悕绉�" clearable style="width: 240px" />
+            </el-form-item>
+            <el-form-item label="鏉ユ簮">
+              <el-select v-model="usageQuery.source" placeholder="璇烽�夋嫨" clearable style="width: 200px">
+                <el-option label="缁翠慨" value="repair" />
+                <el-option label="淇濆吇" value="upkeep" />
+              </el-select>
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" @click="handleUsageQuery">鏌ヨ</el-button>
+              <el-button @click="resetUsageQuery">閲嶇疆</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+
+        <PIMTable
+          rowKey="rowKey"
+          :column="usageColumns"
+          :tableData="usageTableData"
+          :tableLoading="usageLoading"
+          :page="usagePagination"
+          :isShowPagination="true"
+          @pagination="handleUsagePageChange"
+        />
+      </el-tab-pane>
+    </el-tabs>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, reactive, watch } from 'vue';
+import { ref, computed, onMounted, reactive } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { getSparePartsList, addSparePart, editSparePart, delSparePart } from "@/api/equipmentManagement/spareParts";
 import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import { getSparePartsUsagePage } from "@/api/equipmentManagement/sparePartsUsage";
 
 // 鍔犺浇鐘舵��
 const loading = ref(false);
 const formLoading = ref(false);
+const activeTab = ref("list");
 // 瀵硅瘽妗嗘樉绀虹姸鎬�
 const dialogVisible = ref(false);
 // 缂栬緫 ID
@@ -126,6 +163,35 @@
   size: 10,
   total: 0
 });
+
+// 澶囦欢棰嗙敤璁板綍
+const usageLoading = ref(false);
+const usageQuery = reactive({
+  sparePartName: "",
+  source: "",
+});
+const usagePagination = reactive({
+  current: 1,
+  size: 10,
+  total: 0,
+});
+const usageTableData = ref([]);
+const usageColumns = ref([
+  { label: "鏉ユ簮", prop: "sourceText" },
+  { label: "鍗曟嵁/璁板綍ID", prop: "sourceId" },
+  { label: "璁惧鍚嶇О", prop: "deviceName" },
+  { label: "澶囦欢鍚嶇О", prop: "sparePartName" },
+  { label: "棰嗙敤鏁伴噺", prop: "qty" },
+  { label: "鎿嶄綔浜�", prop: "operator" },
+  { label: "鏃堕棿", prop: "time" },
+]);
+
+const handleTabChange = async (name) => {
+  if (name === "usage") {
+    usagePagination.current = 1;
+    await fetchUsageData();
+  }
+};
 const columns = ref([
   {
     label: "璁惧鍚嶇О",
@@ -268,6 +334,52 @@
   }
 }
 
+const fetchUsageData = async () => {
+  usageLoading.value = true;
+  try {
+    const res = await getSparePartsUsagePage({
+      current: usagePagination.current,
+      size: usagePagination.size,
+      sparePartName: usageQuery.sparePartName || undefined,
+      source: usageQuery.source || undefined,
+    });
+    if (res?.code === 200) {
+      const records = res?.data?.records || [];
+      usagePagination.total = res?.data?.total || 0;
+      usageTableData.value = records.map((r, idx) => ({
+        rowKey: r.id ?? `${usagePagination.current}-${idx}`,
+        ...r,
+        sourceText:
+          r.source === "缁翠慨" ? "缁翠慨" :
+          r.source === "淇濆吇" ? "淇濆吇" :
+          r.source === "manual" ? "鎵嬪伐" :
+          (r.source || "-"),
+      }));
+    } else {
+      usagePagination.total = 0;
+      usageTableData.value = [];
+    }
+  } finally {
+    usageLoading.value = false;
+  }
+};
+
+const handleUsageQuery = () => {
+  usagePagination.current = 1;
+  fetchUsageData();
+};
+const resetUsageQuery = () => {
+  usageQuery.sparePartName = "";
+  usageQuery.source = "";
+  usagePagination.current = 1;
+  fetchUsageData();
+};
+const handleUsagePageChange = (obj) => {
+  usagePagination.current = obj.page;
+  usagePagination.size = obj.limit;
+  fetchUsageData();
+};
+
 // 鏌ヨ
 const handleQuery = () => {
   pagination.current = 1;

--
Gitblit v1.9.3