From de4a1d478c988d6b2f1fd41011b144c03c996b96 Mon Sep 17 00:00:00 2001
From: chenhj <1263187585@qq.com>
Date: 星期五, 26 十二月 2025 09:40:03 +0800
Subject: [PATCH] Merge branch 'dev_JTWY' of http://114.132.189.42:9002/r/TianJin-product-management into dev_JTWY

---
 src/views/productionManagement/processRoute/ItemsForm.vue |  285 ++++++++++++++++++++++++++++++++++++++++
 src/views/productionManagement/processRoute/index.vue     |   27 +++
 src/views/basicData/product/ProductSelectDialog.vue       |   29 ++--
 src/api/productionManagement/processRouteItem.js          |   19 ++
 src/api/productionManagement/processRoute.js              |    2 
 src/api/productionManagement/productionProcess.js         |    8 +
 src/api/basicData/productModel.js                         |    9 +
 7 files changed, 357 insertions(+), 22 deletions(-)

diff --git a/src/api/basicData/productModel.js b/src/api/basicData/productModel.js
new file mode 100644
index 0000000..f048f9e
--- /dev/null
+++ b/src/api/basicData/productModel.js
@@ -0,0 +1,9 @@
+import request from "@/utils/request.js";
+
+export function productModelList(query) {
+    return request({
+        url: '/basic/product/pageModel',
+        method: 'get',
+        params: query
+    })
+}
\ No newline at end of file
diff --git a/src/api/productionManagement/processRoute.js b/src/api/productionManagement/processRoute.js
index 4348465..4d16775 100644
--- a/src/api/productionManagement/processRoute.js
+++ b/src/api/productionManagement/processRoute.js
@@ -1,4 +1,4 @@
-// 宸ュ簭椤甸潰鎺ュ彛
+// 宸ヨ壓璺嚎椤甸潰鎺ュ彛
 import request from "@/utils/request";
 
 // 鍒嗛〉鏌ヨ
diff --git a/src/api/productionManagement/processRouteItem.js b/src/api/productionManagement/processRouteItem.js
new file mode 100644
index 0000000..ad4861c
--- /dev/null
+++ b/src/api/productionManagement/processRouteItem.js
@@ -0,0 +1,19 @@
+// 宸ヨ壓璺嚎椤圭洰椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒楄〃鏌ヨ
+export function findProcessRouteItemList(query) {
+    return request({
+        url: "/processRouteItem/list",
+        method: "get",
+        params: query,
+    });
+}
+
+export function addOrUpdateProcessRouteItem(data) {
+    return request({
+        url: "/processRouteItem",
+        method: "post",
+        data: data,
+    });
+}
\ No newline at end of file
diff --git a/src/api/productionManagement/productionProcess.js b/src/api/productionManagement/productionProcess.js
index c5f1f23..e3cd929 100644
--- a/src/api/productionManagement/productionProcess.js
+++ b/src/api/productionManagement/productionProcess.js
@@ -10,6 +10,14 @@
   });
 }
 
+export function processList(query) {
+  return request({
+    url: "/productProcess/list",
+    method: "get",
+    params: query,
+  });
+}
+
 export function add(data) {
   return request({
     url: "/productProcess",
diff --git a/src/views/basicData/product/ProductSelectDialog.vue b/src/views/basicData/product/ProductSelectDialog.vue
index d4b0119..70d3f3e 100644
--- a/src/views/basicData/product/ProductSelectDialog.vue
+++ b/src/views/basicData/product/ProductSelectDialog.vue
@@ -37,8 +37,10 @@
         :data="tableData"
         height="420"
         highlight-current-row
-        @current-change="onCurrentChange"
+        row-key="id"
+        @selection-change="handleSelectionChange"
     >
+      <el-table-column type="selection" width="55" />
       <el-table-column type="index" label="#" width="60"/>
       <el-table-column prop="productName" label="浜у搧澶х被" min-width="160"/>
       <el-table-column prop="model" label="鍨嬪彿鍚嶇О" min-width="200"/>
@@ -60,7 +62,7 @@
 
     <template #footer>
       <el-button @click="close()">鍙栨秷</el-button>
-      <el-button type="primary" :disabled="!selectedRow" @click="onConfirm">
+      <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm">
         纭畾
       </el-button>
     </template>
@@ -70,7 +72,7 @@
 <script setup lang="ts">
 import {computed, onMounted, reactive, ref, watch} from "vue";
 import {ElMessage} from "element-plus";
-import {list} from '@/api/basicData/productModel'
+import {productModelList} from '@/api/basicData/productModel'
 
 export type ProductRow = {
   id: number;
@@ -83,10 +85,7 @@
   modelValue: boolean;
 }>();
 
-const emit = defineEmits<{
-  (e: "update:modelValue", v: boolean): void;
-  (e: "confirm", row: ProductRow): void; // 鎶婃暣琛屾暟鎹繑缁欑埗缁勪欢
-}>();
+const emit = defineEmits(['update:modelValue', 'confirm']);
 
 const visible = computed({
   get: () => props.modelValue,
@@ -106,14 +105,14 @@
 const loading = ref(false);
 const tableData = ref<ProductRow[]>([]);
 const total = ref(0);
-const selectedRow = ref<ProductRow | null>(null);
+const multipleSelection = ref<ProductRow[]>([])
 
 function close() {
   visible.value = false;
 }
 
-function onCurrentChange(row: ProductRow | null) {
-  selectedRow.value = row;
+const handleSelectionChange = (val: ProductRow[]) => {
+  multipleSelection.value = val
 }
 
 function onSearch() {
@@ -133,25 +132,25 @@
 }
 
 function onConfirm() {
-  if (!selectedRow.value) {
+  if (multipleSelection.value.length === 0) {
     ElMessage.warning("璇烽�夋嫨涓�鏉′骇鍝�");
     return;
   }
-  emit("confirm", selectedRow.value);
+  emit("confirm", multipleSelection.value);
   close();
 }
 
 async function loadData() {
   loading.value = true;
   try {
-    selectedRow.value = null; // 缈婚〉/鎼滅储鍚庢竻绌洪�夋嫨鏇寸鍚堥鏈�
-    const res = await list({
+    multipleSelection.value = []; // 缈婚〉/鎼滅储鍚庢竻绌洪�夋嫨鏇寸鍚堥鏈�
+    const res = await productModelList({
       productName: query.productName.trim(),
       model: query.model.trim(),
       pageNum: page.pageNum,
       pageSize: page.pageSize,
     });
-    tableData.value = res.list;
+    tableData.value = res.records;
     total.value = res.total;
   } finally {
     loading.value = false;
diff --git a/src/views/productionManagement/processRoute/ItemsForm.vue b/src/views/productionManagement/processRoute/ItemsForm.vue
new file mode 100644
index 0000000..525b205
--- /dev/null
+++ b/src/views/productionManagement/processRoute/ItemsForm.vue
@@ -0,0 +1,285 @@
+<template>
+  <div>
+    <el-dialog
+        v-model="isShow"
+        title="宸ヨ壓璺嚎椤圭洰"
+        width="800px"
+        @close="closeModal"
+    >
+      <el-button
+          type="primary"
+          @click="isShowProductSelectDialog = true"
+          class="mb5"
+          style="margin-bottom: 10px;"
+      >
+        閫夋嫨浜у搧
+      </el-button>
+
+      <el-table
+          ref="multipleTable"
+          v-loading="tableLoading"
+          border
+          :data="routeItems"
+          :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+          row-key="id"
+          tooltip-effect="dark"
+          class="lims-table"
+          style="cursor: move;"
+      >
+        <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+
+        <el-table-column
+            v-for="(item, index) in tableColumn"
+            :key="index"
+            :label="item.label"
+            :width="item.width"
+            show-overflow-tooltip
+        >
+          <template #default="scope" v-if="item.dataType === 'action'">
+            <el-button
+                v-for="(op, opIndex) in item.operation"
+                :key="opIndex"
+                :type="op.type"
+                :link="op.link"
+                size="small"
+                @click.stop="op.clickFun(scope.row)"
+            >
+              {{ op.name }}
+            </el-button>
+          </template>
+
+          <template #default="scope" v-else>
+            <template v-if="item.prop === 'processId'">
+              <el-select
+                  v-model="scope.row[item.prop]"
+                  style="width: 100%;"
+                  @mousedown.stop
+              >
+                <el-option
+                    v-for="process in processOptions"
+                    :key="process.id"
+                    :label="process.name"
+                    :value="process.id"
+                />
+              </el-select>
+            </template>
+            <template v-else>
+              {{ scope.row[item.prop] || '-' }}
+            </template>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="handleSubmit">纭</el-button>
+          <el-button @click="closeModal">鍙栨秷</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <ProductSelectDialog
+        v-model="isShowProductSelectDialog"
+        @confirm="handelSelectProducts"
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue";
+import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+import { findProcessRouteItemList, addOrUpdateProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
+import { processList } from "@/api/productionManagement/productionProcess.js";
+import Sortable from 'sortablejs';
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    required: true,
+    default: false
+  },
+  record: {
+    type: Object,
+    required: true,
+    default: () => ({})
+  }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+const processOptions = ref([]);
+const tableLoading = ref(false);
+const isShowProductSelectDialog = ref(false);
+const routeItems = ref([]);
+let sortable = null;
+const multipleTable = ref(null);
+
+const isShow = computed({
+  get() {
+    return props.visible;
+  },
+  set(val) {
+    emit('update:visible', val);
+  }
+});
+
+const tableColumn = ref([
+  { label: "浜у搧鍚嶇О", prop: "productName", width: 180 },
+  { label: "瑙勬牸鍚嶇О", prop: "model", width: 150 },
+  { label: "鍗曚綅", prop: "unit", width: 80 },
+  { label: "宸ュ簭鍚嶇О", prop: "processId", width: 180 },
+  {
+    dataType: "action",
+    label: "鎿嶄綔",
+    align: "center",
+    fixed: "right",
+    width: 100,
+    operation: [
+      {
+        name: "鍒犻櫎",
+        type: "danger",
+        link: true,
+        clickFun: (row) => {
+          const idx = routeItems.value.findIndex(item => item.id === row.id);
+          if (idx > -1) {
+            routeItems.value.splice(idx, 1);
+          }
+        }
+      }
+    ]
+  }
+]);
+
+const closeModal = () => {
+  isShow.value = false;
+};
+
+const handelSelectProducts = (products) => {
+  const newData = products.map(({ id, ...product }) => ({
+    ...product,
+    productModelId: id,
+    routeId: props.record.id,
+    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, // 鐢熸垚鏃犵壒娈婂瓧绗︾殑ID
+    processId: undefined
+  }));
+  routeItems.value.push(...newData);
+
+  nextTick(() => initSortable());
+};
+
+const findProcessRouteItems = () => {
+  tableLoading.value = true;
+  findProcessRouteItemList({ routeId: props.record.id })
+      .then(res => {
+        tableLoading.value = false;
+        routeItems.value = res.data.map(item => ({
+          ...item,
+          processId: item.processId === 0 ? undefined : item.processId
+        }));
+        nextTick(() => initSortable());
+      })
+      .catch(err => {
+        tableLoading.value = false;
+        console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+      });
+};
+
+const findProcessList = () => {
+  processList({})
+      .then(res => {
+        processOptions.value = res.data;
+      })
+      .catch(err => {
+        console.error("鑾峰彇宸ュ簭澶辫触锛�", err);
+      });
+};
+
+const { proxy } = getCurrentInstance() || {};
+
+const handleSubmit = () => {
+  if (routeItems.value.length === 0) {
+    proxy?.$modal?.msgError("璇锋坊鍔犺矾绾块」鐩�");
+    return;
+  }
+
+  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
+  if (hasEmptyProcess) {
+    proxy?.$modal?.msgError("璇蜂负鎵�鏈夐」鐩�夋嫨宸ュ簭");
+    return;
+  }
+
+  addOrUpdateProcessRouteItem({
+    routeId: props.record.id,
+    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
+  })
+      .then(res => {
+        isShow.value = false;
+        emit('completed');
+        proxy?.$modal?.msgSuccess("鎻愪氦鎴愬姛");
+      })
+      .catch(err => {
+        proxy?.$modal?.msgError(`鎻愪氦澶辫触锛�${err.msg || "缃戠粶寮傚父"}`);
+      });
+};
+
+const initSortable = () => {
+  if (sortable) {
+    sortable.destroy();
+    sortable = null;
+  }
+
+  if (!multipleTable.value) return;
+
+  const tbody = multipleTable.value.$el.querySelector('.el-table__body tbody') ||
+      multipleTable.value.$el.querySelector('.el-table__body-wrapper > table > tbody');
+  if (!tbody) return;
+
+  sortable = new Sortable(tbody, {
+    animation: 150,
+    ghostClass: 'sortable-ghost',
+    handle: '.el-table__row',
+    filter: '.el-button, .el-select',
+    onEnd: (evt) => {
+      const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
+      routeItems.value.splice(evt.newIndex, 0, moveItem);
+    }
+  });
+};
+
+onMounted(() => {
+  findProcessRouteItems();
+  findProcessList();
+});
+
+onUnmounted(() => {
+  if (sortable) {
+    sortable.destroy();
+  }
+});
+
+// 淇锛氭毚闇叉柟娉曟椂閬垮厤璇硶閿欒
+defineExpose({
+  closeModal,
+  handleSubmit,
+  isShow
+});
+</script>
+
+<style scoped>
+:deep(.sortable-ghost) {
+  opacity: 0.6;
+  background-color: #f5f7fa !important;
+}
+
+:deep(.el-table__row) {
+  transition: background-color 0.2s;
+}
+
+:deep(.el-table__row:hover) {
+  background-color: #f9fafc !important;
+}
+
+.mb5 {
+  margin-bottom: 5px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/processRoute/index.vue b/src/views/productionManagement/processRoute/index.vue
index 06a798d..7b53dfd 100644
--- a/src/views/productionManagement/processRoute/index.vue
+++ b/src/views/productionManagement/processRoute/index.vue
@@ -2,8 +2,8 @@
 	<div class="app-container">
 		<div class="search_form">
 			<el-form :model="searchForm" :inline="true">
-				<el-form-item label="闆朵欢鍚嶇О:">
-					<el-input v-model="searchForm.speculativeTradingName" placeholder="璇疯緭鍏�" clearable prefix-icon="Search"
+				<el-form-item label="瑙勬牸鍚嶇О:">
+					<el-input v-model="searchForm.model" placeholder="璇疯緭鍏�" clearable prefix-icon="Search"
 										style="width: 200px;"
 										@change="handleQuery" />
 				</el-form-item>
@@ -27,7 +27,7 @@
 				:tableLoading="tableLoading"
 				@pagination="pagination"
 				:total="page.total"
-			></PIMTable>
+			/>
 		</div>
 		<new-process
       v-if="isShowNewModal"
@@ -41,6 +41,14 @@
       :record="record"
       @completed="getList"
     />
+
+    <route-item-form
+        v-if="isShowItemModal"
+        v-model:visible="isShowItemModal"
+        :record="record"
+        @completed="getList"
+    />
+    RouteItemForm
 	</div>
 </template>
 
@@ -48,6 +56,7 @@
 import {onMounted, ref} from "vue";
 import NewProcess from "@/views/productionManagement/processRoute/New.vue";
 import EditProcess from "@/views/productionManagement/processRoute/Edit.vue";
+import RouteItemForm from "@/views/productionManagement/processRoute/ItemsForm.vue";
 import {listPage, del} from "@/api/productionManagement/processRoute.js";
 
 const data = reactive({
@@ -73,17 +82,17 @@
     width: 280,
     operation: [
       {
-        name: "璇︽儏",
+        name: "缂栬緫",
         type: "text",
         clickFun: (row) => {
           showEditModal(row);
         }
       },
       {
-        name: "缂栬緫",
+        name: "璺嚎椤圭洰",
         type: "text",
         clickFun: (row) => {
-          showEditModal(row);
+          showItemModal(row);
         }
       }
     ]
@@ -94,6 +103,7 @@
 const tableLoading = ref(false);
 const isShowNewModal = ref(false);
 const isShowEditModal = ref(false);
+const isShowItemModal = ref(false);
 const record = ref({});
 const page = reactive({
 	current: 1,
@@ -143,6 +153,11 @@
   record.value = row
 };
 
+const showItemModal = (row) => {
+  isShowItemModal.value = true
+  record.value = row
+};
+
 // 鍒犻櫎
 function handleDelete() {
   const ids = selectedRows.value.map((item) => item.id);

--
Gitblit v1.9.3