spring
7 小时以前 60501db2df54c0b5b80f19714c39d4b789da4cc6
fix: 合并军泰伟业 生产模块代码
已添加27个文件
已修改11个文件
7864 ■■■■ 文件已修改
src/api/basicData/productModel.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRoute.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteItem.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productProcessRoute.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productStructure.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProcess.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProductInput.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProductMain.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProductOutput.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionReporting.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/ProductSelectDialog.vue 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productManagement/productIdentifier/index.vue 1187 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/operationScheduling/components/formDia.vue 121 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/operationScheduling/index.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/Edit.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/ItemsForm.vue 531 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 503 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/StructureEdit.vue 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/index.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionDispatching/components/autoDispatchDia.vue 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionDispatching/components/formDia.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionDispatching/index.vue 464 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/ProcessRouteItemForm.vue 555 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 367 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/Input.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/Output.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/components/formDia.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 888 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 587 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
    })
}
src/api/productionManagement/processRoute.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
// å·¥è‰ºè·¯çº¿é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function listPage(query) {
  return request({
    url: "/processRoute/page",
    method: "get",
    params: query,
  });
}
export function add(data) {
  return request({
    url: "/processRoute",
    method: "post",
    data: data,
  });
}
export function del(ids) {
  return request({
    url: '/processRoute/' + ids,
    method: 'delete',
  })
}
export function update(data) {
  return request({
    url: '/processRoute',
    method: 'put',
    data: data,
  })
}
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,
    });
}
src/api/productionManagement/productProcessRoute.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
// å·¥è‰ºè·¯çº¿é¡¹ç›®é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ—表查询
export function findProductProcessRouteItemList(query) {
    return request({
        url: "/productProcessRoute/list",
        method: "get",
        params: query,
    });
}
export function addOrUpdateProductProcessRouteItem(data) {
    return request({
        url: "/productProcessRoute/updateRouteItem",
        method: "post",
        data: data,
    });
}
// åˆ é™¤å®¢æˆ·æ¡£æ¡ˆ
export function deleteRouteItem(ids) {
    return request({
        url: '/productProcessRoute/deleteRouteItem',
        method: 'delete',
        data: ids
    })
}
src/api/productionManagement/productStructure.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
// äº§å“ç»“构页面接口
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function queryList(id) {
  return request({
    url: "/productStructure/listByproductModelId/" + id,
    method: "get",
  });
}
export function add(data) {
  return request({
    url: "/productStructure",
    method: "post",
    data: data,
  });
}
src/api/productionManagement/productionOrder.js
@@ -9,6 +9,43 @@
    params: query,
  });
}
export function productOrderListPage(query) {
  return request({
    url: "/productOrder/page",
    method: "get",
    params: query,
  });
}
// èŽ·å–ç‚’æœºæ­£åœ¨å·¥ä½œé‡æ•°æ®
export function schedulingList(query) {
  return request({
    url: "/salesLedger/scheduling/list",
    method: "get",
    params: query,
  });
}
// ä¿å­˜ç‚’机设置
export function addSpeculatTrading(data) {
  return request({
    url: "/salesLedger/scheduling/addSpeculatTrading",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹ç‚’机设置
export function updateSpeculatTrading(data) {
  return request({
    url: "/salesLedger/scheduling/updateSpeculatTrading",
    method: "post",
    data: data,
  });
}
// ç”Ÿäº§æ´¾å·¥
export function productionDispatch(query) {
  return request({
@@ -16,4 +53,38 @@
    method: "post",
    data: query,
  });
}
// è‡ªåŠ¨æ´¾å·¥
export function productionDispatchList(query) {
  return request({
    url: "/salesLedger/scheduling/productionDispatchList",
    method: "post",
    data: query,
  });
}
// æŸ¥è¯¢æŸè€—率
export function getLossRate() {
  return request({
    url: "/salesLedger/scheduling/loss",
    method: "get",
  });
}
// æ–°å¢žæŸè€—率
export function addLossRate(data) {
  return request({
    url: "/salesLedger/scheduling/addLoss",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹æŸè€—率
export function updateLossRate(data) {
  return request({
    url: "/salesLedger/scheduling/updateLoss",
    method: "post",
    data: data,
  });
}
src/api/productionManagement/productionProcess.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
// å·¥åºé¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function listPage(query) {
  return request({
    url: "/productProcess/listPage",
    method: "get",
    params: query,
  });
}
export function processList(query) {
  return request({
    url: "/productProcess/list",
    method: "get",
    params: query,
  });
}
export function add(data) {
  return request({
    url: "/productProcess",
    method: "post",
    data: data,
  });
}
export function del(data) {
  return request({
    url: '/productProcess/batchDelete',
    method: 'delete',
    data: data,
  })
}
export function update(data) {
  return request({
    url: '/productProcess/update',
    method: 'put',
    data: data,
  })
}
// å·¥åºæŸ¥è¯¢
export function list() {
    return request({
        url: "/productProcess/list",
        method: "get",
    });
}
src/api/productionManagement/productionProductInput.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
// ç”Ÿäº§æŠ•入页面接口
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function productionProductInputListPage(query) {
    return request({
        url: "/productionProductInput/listPage",
        method: "get",
        params: query,
    });
}
src/api/productionManagement/productionProductMain.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
// ç”Ÿäº§æŠ¥å·¥é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function productionProductMainListPage(query) {
    return request({
        url: "/productionProductMain/listPage",
        method: "get",
        params: query,
    });
}
src/api/productionManagement/productionProductOutput.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
// ç”Ÿäº§äº§å‡ºé¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function productionProductOutputListPage(query) {
    return request({
        url: "/productionProductOutput/listPage",
        method: "get",
        params: query,
    });
}
src/api/productionManagement/productionReporting.js
@@ -32,4 +32,12 @@
    method: "post",
    data: query,
  });
}
}
// ç”Ÿäº§æŠ¥å·¥-删除
export function productionReportDelete(query) {
  return request({
    url: "/productionProductMain/delete",
    method: "delete",
    data: query,
  });
}
src/api/productionManagement/workOrder.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import request from "@/utils/request";
export function productWorkOrderPage(query) {
  return request({
    url: "/productWorkOrder/page",
    method: "get",
    params: query,
  });
}
export function updateProductWorkOrder(data) {
  return request({
    url: "/productWorkOrder/updateProductWorkOrder",
    method: "post",
    data: data,
  });
}
export function addProductMain(data) {
  return request({
    url: "/productionProductMain/addProductMain",
    method: "post",
    data: data,
  });
}
src/views/basicData/product/ProductSelectDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,163 @@
<template>
  <el-dialog
      v-model="visible"
      title="选择产品"
      width="900px"
      destroy-on-close
      :close-on-click-modal="false"
  >
    <el-form :inline="true" :model="query" class="mb-2">
      <el-form-item label="产品大类">
        <el-input
            v-model="query.productName"
            placeholder="输入产品大类"
            clearable
            @keyup.enter="onSearch"
        />
      </el-form-item>
      <el-form-item label="型号名称">
        <el-input
            v-model="query.model"
            placeholder="输入型号名称"
            clearable
            @keyup.enter="onSearch"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSearch">搜索</el-button>
        <el-button @click="onReset">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- åˆ—表 -->
    <el-table
        v-loading="loading"
        :data="tableData"
        height="420"
        highlight-current-row
        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"/>
      <el-table-column prop="unit" label="单位" min-width="160"/>
    </el-table>
    <div class="mt-3 flex justify-end">
      <el-pagination
          background
          layout="total, sizes, prev, pager, next, jumper"
          :total="total"
          v-model:page-size="page.pageSize"
          v-model:current-page="page.pageNum"
          :page-sizes="[10, 20, 50, 100]"
          @size-change="onPageChange"
          @current-change="onPageChange"
      />
    </div>
    <template #footer>
      <el-button @click="close()">取消</el-button>
      <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm">
        ç¡®å®š
      </el-button>
    </template>
  </el-dialog>
</template>
<script setup lang="ts">
import {computed, onMounted, reactive, ref, watch} from "vue";
import {ElMessage} from "element-plus";
import {productModelList} from '@/api/basicData/productModel'
export type ProductRow = {
  id: number;
  productName: string;
  model: string;
  unit?: string;
};
const props = defineProps<{
  modelValue: boolean;
}>();
const emit = defineEmits(['update:modelValue', 'confirm']);
const visible = computed({
  get: () => props.modelValue,
  set: (v) => emit("update:modelValue", v),
});
const query = reactive({
  productName: "",
  model: "",
});
const page = reactive({
  pageNum: 1,
  pageSize: 10,
});
const loading = ref(false);
const tableData = ref<ProductRow[]>([]);
const total = ref(0);
const multipleSelection = ref<ProductRow[]>([])
function close() {
  visible.value = false;
}
const handleSelectionChange = (val: ProductRow[]) => {
  multipleSelection.value = val
}
function onSearch() {
  page.pageNum = 1;
  loadData();
}
function onReset() {
  query.productName = "";
  query.model = "";
  page.pageNum = 1;
  loadData();
}
function onPageChange() {
  loadData();
}
function onConfirm() {
  if (multipleSelection.value.length === 0) {
    ElMessage.warning("请选择一条产品");
    return;
  }
  emit("confirm", multipleSelection.value);
  close();
}
async function loadData() {
  loading.value = true;
  try {
    multipleSelection.value = []; // ç¿»é¡µ/搜索后清空选择更符合预期
    const res = await productModelList({
      productName: query.productName.trim(),
      model: query.model.trim(),
      current: page.pageNum,
      size: page.pageSize,
    });
    tableData.value = res.records;
    total.value = res.total;
  } finally {
    loading.value = false;
  }
}
onMounted(() => {
  loadData()
})
</script>
src/views/productManagement/productIdentifier/index.vue
@@ -1,159 +1,231 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
             <!-- æœç´¢åŒºåŸŸ -->
       <el-row :gutter="20" class="search-row">
         <el-col :span="6">
           <el-input
             v-model="searchForm.productName"
             placeholder="请输入产品名称"
             clearable
             @keyup.enter="handleSearch"
           >
             <template #prefix>
               <el-icon><Search /></el-icon>
             </template>
           </el-input>
         </el-col>
         <el-col :span="6">
           <el-select v-model="searchForm.identifierType" placeholder="请选择标识类型" clearable>
             <el-option label="二维码" value="二维码"></el-option>
             <el-option label="防伪码" value="防伪码"></el-option>
           </el-select>
         </el-col>
         <el-col :span="6">
           <el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
             <el-option label="已生成" value="已生成"></el-option>
             <el-option label="已分配" value="已分配"></el-option>
             <el-option label="已使用" value="已使用"></el-option>
             <el-option label="已作废" value="已作废"></el-option>
           </el-select>
         </el-col>
         <el-col :span="6">
           <el-button type="primary" @click="handleSearch">搜索</el-button>
           <el-button @click="resetSearch">重置</el-button>
           <el-button style="float: right;" type="primary" @click="handleAdd">
             æ–°å¢žæ ‡è¯†
           </el-button>
         </el-col>
       </el-row>
      <!-- æœç´¢åŒºåŸŸ -->
      <el-row :gutter="20"
              class="search-row">
        <el-col :span="6">
          <el-input v-model="searchForm.productName"
                    placeholder="请输入产品名称"
                    clearable
                    @keyup.enter="handleSearch">
            <template #prefix>
              <el-icon>
                <Search />
              </el-icon>
            </template>
          </el-input>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.identifierType"
                     placeholder="请选择标识类型"
                     clearable>
            <el-option label="二维码"
                       value="二维码"></el-option>
            <el-option label="防伪码"
                       value="防伪码"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.status"
                     placeholder="请选择状态"
                     clearable>
            <el-option label="已生成"
                       value="已生成"></el-option>
            <el-option label="已分配"
                       value="已分配"></el-option>
            <el-option label="已使用"
                       value="已使用"></el-option>
            <el-option label="已作废"
                       value="已作废"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-button type="primary"
                     @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
          <el-button style="float: right;"
                     type="primary"
                     @click="handleAdd">
            æ–°å¢žæ ‡è¯†
          </el-button>
        </el-col>
      </el-row>
      <!-- äº§å“æ ‡è¯†åˆ—表 -->
      <el-table
        :data="filteredList"
        style="width: 100%"
        v-loading="loading"
        border
        stripe
        height="calc(100vh - 22em)"
      >
        <el-table-column prop="id" label="ID" width="80" align="center"/>
        <el-table-column prop="productName" label="产品名称" width="150" />
        <el-table-column prop="productCode" label="产品编码" width="120" />
        <el-table-column prop="batchNo" label="批次号" width="120" />
        <el-table-column prop="identifierType" label="标识类型" width="100">
      <el-table :data="filteredList"
                style="width: 100%"
                v-loading="loading"
                border
                stripe
                height="calc(100vh - 22em)">
        <el-table-column prop="id"
                         label="ID"
                         width="80"
                         align="center" />
        <el-table-column prop="productName"
                         label="产品名称"
                         width="150" />
        <el-table-column prop="productCode"
                         label="产品编码"
                         width="120" />
        <el-table-column prop="batchNo"
                         label="批次号"
                         width="120" />
        <el-table-column prop="identifierType"
                         label="标识类型"
                         width="100">
          <template #default="scope">
            <el-tag :type="getIdentifierTypeType(scope.row.identifierType)">
              {{ scope.row.identifierType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="identifierCode" label="标识码" />
        <el-table-column prop="status" label="状态" width="100">
        <el-table-column prop="identifierCode"
                         label="标识码" />
        <el-table-column prop="status"
                         label="状态"
                         width="100">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="generateTime" label="生成时间" width="160" />
        <el-table-column label="操作" fixed="right" align="center" width="280">
        <el-table-column prop="generateTime"
                         label="生成时间"
                         width="160" />
        <el-table-column label="操作"
                         fixed="right"
                         align="center"
                         width="280">
          <template #default="scope">
            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
            <el-button link type="success" @click="generateQRCode(scope.row)">生成二维码</el-button>
            <el-button link type="primary" @click="handleExport(scope.row)">导出</el-button>
            <el-button link type="primary" @click="handleReassign(scope.row)" v-if="scope.row.status === '已分配'">重新分配</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
            <el-button link
                       type="primary"
                       @click="handleView(scope.row)">查看</el-button>
            <el-button link
                       type="primary"
                       @click="handleEdit(scope.row)">编辑</el-button>
            <el-button link
                       type="success"
                       @click="generateQRCode(scope.row)">生成二维码</el-button>
            <el-button link
                       type="primary"
                       @click="handleExport(scope.row)">导出</el-button>
            <el-button link
                       type="primary"
                       @click="handleReassign(scope.row)"
                       v-if="scope.row.status === '已分配'">重新分配</el-button>
            <el-button link
                       type="danger"
                       @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        :total="pagination.total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="pagination.currentPage"
        :limit="pagination.pageSize"
        @pagination="handleCurrentChange"
      />
      <pagination :total="pagination.total"
                  layout="total, sizes, prev, pager, next, jumper"
                  :page="pagination.currentPage"
                  :limit="pagination.pageSize"
                  @pagination="handleCurrentChange" />
    </el-card>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
    <el-dialog v-model="dialogVisible"
               :title="dialogTitle"
               width="700px">
      <el-form :model="form"
               :rules="rules"
               ref="formRef"
               label-width="100px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="产品名称" prop="productName">
              <el-input v-model="form.productName" placeholder="请输入产品名称"></el-input>
            <el-form-item label="产品名称"
                          prop="productName">
              <el-input v-model="form.productName"
                        placeholder="请输入产品名称"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品编码" prop="productCode">
              <el-input v-model="form.productCode" placeholder="请输入产品编码"></el-input>
            <el-form-item label="产品编码"
                          prop="productCode">
              <el-input v-model="form.productCode"
                        placeholder="请输入产品编码"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="批次号" prop="batchNo">
              <el-input v-model="form.batchNo" placeholder="请输入批次号"></el-input>
            <el-form-item label="批次号"
                          prop="batchNo">
              <el-input v-model="form.batchNo"
                        placeholder="请输入批次号"></el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="标识类型" prop="identifierType">
              <el-select v-model="form.identifierType" placeholder="请选择标识类型" style="width: 100%">
                <el-option label="二维码" value="二维码"></el-option>
                <el-option label="防伪码" value="防伪码"></el-option>
            <el-form-item label="标识类型"
                          prop="identifierType">
              <el-select v-model="form.identifierType"
                         placeholder="请选择标识类型"
                         style="width: 100%">
                <el-option label="二维码"
                           value="二维码"></el-option>
                <el-option label="防伪码"
                           value="防伪码"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="生成数量" prop="quantity">
              <el-input-number v-model="form.quantity" :min="1" :max="10000" style="width: 100%"></el-input-number>
            <el-form-item label="生成数量"
                          prop="quantity">
              <el-input-number v-model="form.quantity"
                               :min="1"
                               :max="10000"
                               style="width: 100%"></el-input-number>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="状态" prop="status">
              <el-select v-model="form.status" placeholder="请选择状态" style="width: 100%">
                <el-option label="已生成" value="已生成"></el-option>
                <el-option label="已分配" value="已分配"></el-option>
                <el-option label="已使用" value="已使用"></el-option>
                <el-option label="已作废" value="已作废"></el-option>
            <el-form-item label="状态"
                          prop="status">
              <el-select v-model="form.status"
                         placeholder="请选择状态"
                         style="width: 100%">
                <el-option label="已生成"
                           value="已生成"></el-option>
                <el-option label="已分配"
                           value="已分配"></el-option>
                <el-option label="已使用"
                           value="已使用"></el-option>
                <el-option label="已作废"
                           value="已作废"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="备注" prop="remark">
              <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息" rows="3"></el-input>
            <el-form-item label="备注"
                          prop="remark">
              <el-input type="textarea"
                        v-model="form.remark"
                        placeholder="请输入备注信息"
                        rows="3"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="handleSubmit">ç¡® å®š</el-button>
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ ‡è¯†ç”Ÿæˆå¯¹è¯æ¡† -->
    <el-dialog v-model="generateDialogVisible" title="标识生成" width="500px">
    <el-dialog v-model="generateDialogVisible"
               title="标识生成"
               width="500px">
      <el-form label-width="100px">
        <el-form-item label="产品名称">
          <span>{{ currentProduct.productName }}</span>
@@ -167,30 +239,45 @@
        <el-form-item label="标识类型">
          <span>{{ currentProduct.identifierType }}</span>
        </el-form-item>
        <el-form-item label="生成数量" prop="generateQuantity">
          <el-input-number v-model="generateQuantity" :min="1" :max="10000" style="width: 100%"></el-input-number>
        <el-form-item label="生成数量"
                      prop="generateQuantity">
          <el-input-number v-model="generateQuantity"
                           :min="1"
                           :max="10000"
                           style="width: 100%"></el-input-number>
        </el-form-item>
        <el-form-item label="编码规则" prop="codeRule">
          <el-select v-model="codeRule" placeholder="请选择编码规则" style="width: 100%">
            <el-option label="产品编码+批次号+序号" value="产品编码+批次号+序号"></el-option>
            <el-option label="时间戳+随机数" value="时间戳+随机数"></el-option>
            <el-option label="自定义规则" value="自定义规则"></el-option>
        <el-form-item label="编码规则"
                      prop="codeRule">
          <el-select v-model="codeRule"
                     placeholder="请选择编码规则"
                     style="width: 100%">
            <el-option label="产品编码+批次号+序号"
                       value="产品编码+批次号+序号"></el-option>
            <el-option label="时间戳+随机数"
                       value="时间戳+随机数"></el-option>
            <el-option label="自定义规则"
                       value="自定义规则"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="自定义前缀" prop="customPrefix" v-if="codeRule === '自定义规则'">
          <el-input v-model="customPrefix" placeholder="请输入自定义前缀"></el-input>
        <el-form-item label="自定义前缀"
                      prop="customPrefix"
                      v-if="codeRule === '自定义规则'">
          <el-input v-model="customPrefix"
                    placeholder="请输入自定义前缀"></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="generateIdentifiers">生 æˆ</el-button>
          <el-button @click="generateDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="generateIdentifiers">生 æˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é‡æ–°åˆ†é…å¯¹è¯æ¡† -->
    <el-dialog v-model="reassignDialogVisible" title="重新分配标识" width="500px">
    <el-dialog v-model="reassignDialogVisible"
               title="重新分配标识"
               width="500px">
      <el-form label-width="100px">
        <el-form-item label="产品名称">
          <span>{{ currentProduct.productName }}</span>
@@ -198,26 +285,38 @@
        <el-form-item label="标识码">
          <span>{{ currentProduct.identifierCode }}</span>
        </el-form-item>
        <el-form-item label="新批次号" prop="newBatchNo">
          <el-input v-model="newBatchNo" placeholder="请输入新批次号"></el-input>
        <el-form-item label="新批次号"
                      prop="newBatchNo">
          <el-input v-model="newBatchNo"
                    placeholder="请输入新批次号"></el-input>
        </el-form-item>
        <el-form-item label="分配原因" prop="reassignReason">
          <el-input type="textarea" v-model="reassignReason" rows="3" placeholder="请输入重新分配原因"></el-input>
        <el-form-item label="分配原因"
                      prop="reassignReason">
          <el-input type="textarea"
                    v-model="reassignReason"
                    rows="3"
                    placeholder="请输入重新分配原因"></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="saveReassign">ç¡® å®š</el-button>
          <el-button @click="reassignDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="saveReassign">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- äºŒç»´ç é¢„览对话框 -->
    <el-dialog v-model="qrCodeDialogVisible" title="二维码预览" width="500px" center>
    <el-dialog v-model="qrCodeDialogVisible"
               title="二维码预览"
               width="500px"
               center>
      <div class="qr-preview-container">
        <div v-if="qrCodeUrl" class="qr-image-container">
          <img :src="qrCodeUrl" alt="二维码" class="qr-image" />
        <div v-if="qrCodeUrl"
             class="qr-image-container">
          <img :src="qrCodeUrl"
               alt="二维码"
               class="qr-image" />
          <div class="qr-info">
            <p><strong>产品名称:</strong>{{ currentQRProduct.productName }}</p>
            <p><strong>产品编码:</strong>{{ currentQRProduct.productCode }}</p>
@@ -226,29 +325,27 @@
            <p><strong>标识类型:</strong>{{ currentQRProduct.identifierType }}</p>
          </div>
        </div>
        <div v-else class="qr-loading">
          <el-icon class="is-loading"><Loading /></el-icon>
        <div v-else
             class="qr-loading">
          <el-icon class="is-loading">
            <Loading />
          </el-icon>
          <p>正在生成二维码...</p>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="qrCodeDialogVisible = false">关闭</el-button>
          <el-button
            v-if="qrCodeUrl"
            type="primary"
            @click="copyQRContent"
            icon="CopyDocument"
          >
          <el-button v-if="qrCodeUrl"
                     type="primary"
                     @click="copyQRContent"
                     icon="CopyDocument">
            å¤åˆ¶å†…容
          </el-button>
          <el-button
            v-if="qrCodeUrl"
            type="success"
            @click="downloadQRCode"
            icon="Download"
          >
          <el-button v-if="qrCodeUrl"
                     type="success"
                     @click="downloadQRCode"
                     icon="Download">
            ä¸‹è½½äºŒç»´ç 
          </el-button>
        </div>
@@ -258,451 +355,465 @@
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Loading, Download } from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
import QRCode from 'qrcode'
  import { ref, reactive, computed } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { Plus, Search, Loading, Download } from "@element-plus/icons-vue";
  import Pagination from "@/components/PIMTable/Pagination.vue";
  import QRCode from "qrcode";
// å“åº”式数据
const loading = ref(false)
const searchForm = reactive({
  productName: '',
  identifierType: '',
  status: ''
})
  // å“åº”式数据
  const loading = ref(false);
  const searchForm = reactive({
    productName: "",
    identifierType: "",
    status: "",
  });
const identifierList = ref([
  {
    id: 1,
    productName: '工业传感器A型',
    productCode: 'SENSOR001',
    batchNo: 'B202312001',
    identifierType: '二维码',
    identifierCode: 'QR_SENSOR001_B202312001_001',
    status: '已分配',
    generateTime: '2023-12-01 10:00:00',
    remark: '重要产品标识'
  },
  {
    id: 2,
    productName: '控制面板B型',
    productCode: 'PANEL002',
    batchNo: 'B202312002',
    identifierType: '防伪码',
    identifierCode: 'SEC_PANEL002_B202312002_001',
    status: '已生成',
    generateTime: '2023-12-02 14:30:00',
    remark: '常规产品标识'
  },
  {
    id: 3,
    productName: '数据采集器C型',
    productCode: 'COLLECTOR003',
    batchNo: 'B202312003',
    identifierType: '防伪码',
    identifierCode: 'SEC_COLLECTOR003_B202312003_001',
    status: '已使用',
    generateTime: '2023-12-03 09:15:00',
    remark: '测试产品标识'
  }
])
  const identifierList = ref([
    {
      id: 1,
      productName: "工业传感器A型",
      productCode: "SENSOR001",
      batchNo: "B202312001",
      identifierType: "二维码",
      identifierCode: "QR_SENSOR001_B202312001_001",
      status: "已分配",
      generateTime: "2023-12-01 10:00:00",
      remark: "重要产品标识",
    },
    {
      id: 2,
      productName: "控制面板B型",
      productCode: "PANEL002",
      batchNo: "B202312002",
      identifierType: "防伪码",
      identifierCode: "SEC_PANEL002_B202312002_001",
      status: "已生成",
      generateTime: "2023-12-02 14:30:00",
      remark: "常规产品标识",
    },
    {
      id: 3,
      productName: "数据采集器C型",
      productCode: "COLLECTOR003",
      batchNo: "B202312003",
      identifierType: "防伪码",
      identifierCode: "SEC_COLLECTOR003_B202312003_001",
      status: "已使用",
      generateTime: "2023-12-03 09:15:00",
      remark: "测试产品标识",
    },
  ]);
const pagination = reactive({
  total: 3,
  currentPage: 1,
  pageSize: 10
})
  const pagination = reactive({
    total: 3,
    currentPage: 1,
    pageSize: 10,
  });
const dialogVisible = ref(false)
const dialogTitle = ref('新增标识')
const form = reactive({
  productName: '',
  productCode: '',
  batchNo: '',
  identifierType: '',
  quantity: 1,
  status: '已生成',
  remark: ''
})
  const dialogVisible = ref(false);
  const dialogTitle = ref("新增标识");
  const form = reactive({
    productName: "",
    productCode: "",
    batchNo: "",
    identifierType: "",
    quantity: 1,
    status: "已生成",
    remark: "",
  });
const rules = {
  productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
  productCode: [{ required: true, message: '请输入产品编码', trigger: 'blur' }],
  batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }],
  identifierType: [{ required: true, message: '请选择标识类型', trigger: 'change' }],
  quantity: [{ required: true, message: '请输入生成数量', trigger: 'blur' }],
  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
  const rules = {
    productName: [{ required: true, message: "请输入产品名称", trigger: "blur" }],
    productCode: [{ required: true, message: "请输入产品编码", trigger: "blur" }],
    batchNo: [{ required: true, message: "请输入批次号", trigger: "blur" }],
    identifierType: [
      { required: true, message: "请选择标识类型", trigger: "change" },
    ],
    quantity: [{ required: true, message: "请输入生成数量", trigger: "blur" }],
    status: [{ required: true, message: "请选择状态", trigger: "change" }],
  };
const isEdit = ref(false)
const editId = ref(null)
const generateDialogVisible = ref(false)
const reassignDialogVisible = ref(false)
const currentProduct = ref({})
const generateQuantity = ref(1)
const codeRule = ref('')
const customPrefix = ref('')
const newBatchNo = ref('')
const reassignReason = ref('')
const formRef = ref()
  const isEdit = ref(false);
  const editId = ref(null);
  const generateDialogVisible = ref(false);
  const reassignDialogVisible = ref(false);
  const currentProduct = ref({});
  const generateQuantity = ref(1);
  const codeRule = ref("");
  const customPrefix = ref("");
  const newBatchNo = ref("");
  const reassignReason = ref("");
  const formRef = ref();
// äºŒç»´ç ç›¸å…³å˜é‡
const qrCodeDialogVisible = ref(false)
const qrCodeUrl = ref('')
const currentQRProduct = ref({})
  // äºŒç»´ç ç›¸å…³å˜é‡
  const qrCodeDialogVisible = ref(false);
  const qrCodeUrl = ref("");
  const currentQRProduct = ref({});
// è®¡ç®—属性
const filteredList = computed(() => {
  let list = identifierList.value
  if (searchForm.productName) {
    list = list.filter(item => item.productName.includes(searchForm.productName))
  }
  if (searchForm.identifierType) {
    list = list.filter(item => item.identifierType === searchForm.identifierType)
  }
  if (searchForm.status) {
    list = list.filter(item => item.status === searchForm.status)
  }
  return list
})
  // è®¡ç®—属性
  const filteredList = computed(() => {
    let list = identifierList.value;
    if (searchForm.productName) {
      list = list.filter(item =>
        item.productName.includes(searchForm.productName)
      );
    }
    if (searchForm.identifierType) {
      list = list.filter(
        item => item.identifierType === searchForm.identifierType
      );
    }
    if (searchForm.status) {
      list = list.filter(item => item.status === searchForm.status);
    }
    return list;
  });
// æ–¹æ³•
const getIdentifierTypeType = (type) => {
  const typeMap = {
    '二维码': 'success',
    '防伪码': 'warning'
  }
  return typeMap[type] || 'info'
}
  // æ–¹æ³•
  const getIdentifierTypeType = type => {
    const typeMap = {
      äºŒç»´ç : "success",
      é˜²ä¼ªç : "warning",
    };
    return typeMap[type] || "info";
  };
const getStatusType = (status) => {
  const statusMap = {
    '已生成': 'info',
    '已分配': 'primary',
    '已使用': 'success',
    '已作废': 'danger'
  }
  return statusMap[status] || 'info'
}
  const getStatusType = status => {
    const statusMap = {
      å·²ç”Ÿæˆ: "info",
      å·²åˆ†é…: "primary",
      å·²ä½¿ç”¨: "success",
      å·²ä½œåºŸ: "danger",
    };
    return statusMap[status] || "info";
  };
const handleSearch = () => {
  // æœç´¢é€»è¾‘已在computed中处理
}
  const handleSearch = () => {
    // æœç´¢é€»è¾‘已在computed中处理
  };
const resetSearch = () => {
  searchForm.productName = ''
  searchForm.identifierType = ''
  searchForm.status = ''
}
  const resetSearch = () => {
    searchForm.productName = "";
    searchForm.identifierType = "";
    searchForm.status = "";
  };
const handleAdd = () => {
  dialogTitle.value = '新增标识'
  isEdit.value = false
  form.productName = ''
  form.productCode = ''
  form.batchNo = ''
  form.identifierType = ''
  form.quantity = 1
  form.status = '已生成'
  form.remark = ''
  dialogVisible.value = true
}
  const handleAdd = () => {
    dialogTitle.value = "新增标识";
    isEdit.value = false;
    form.productName = "";
    form.productCode = "";
    form.batchNo = "";
    form.identifierType = "";
    form.quantity = 1;
    form.status = "已生成";
    form.remark = "";
    dialogVisible.value = true;
  };
const handleView = (row) => {
  // æŸ¥çœ‹æ ‡è¯†è¯¦æƒ…
  ElMessage.info('查看标识详情功能待实现')
}
  const handleView = row => {
    // æŸ¥çœ‹æ ‡è¯†è¯¦æƒ…
    ElMessage.info("查看标识详情功能待实现");
  };
const handleEdit = (row) => {
  dialogTitle.value = '编辑标识'
  isEdit.value = true
  editId.value = row.id
  Object.assign(form, row)
  dialogVisible.value = true
}
  const handleEdit = row => {
    dialogTitle.value = "编辑标识";
    isEdit.value = true;
    editId.value = row.id;
    Object.assign(form, row);
    dialogVisible.value = true;
  };
const handleExport = (row) => {
  // å¯¼å‡ºæ ‡è¯†
  ElMessage.success(`已导出标识: ${row.identifierCode}`)
}
  const handleExport = row => {
    // å¯¼å‡ºæ ‡è¯†
    ElMessage.success(`已导出标识: ${row.identifierCode}`);
  };
const handleReassign = (row) => {
  currentProduct.value = row
  newBatchNo.value = ''
  reassignReason.value = ''
  reassignDialogVisible.value = true
}
  const handleReassign = row => {
    currentProduct.value = row;
    newBatchNo.value = "";
    reassignReason.value = "";
    reassignDialogVisible.value = true;
  };
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该标识吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const index = identifierList.value.findIndex(item => item.id === row.id)
  const handleDelete = row => {
    ElMessageBox.confirm("确认删除该标识吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      const index = identifierList.value.findIndex(item => item.id === row.id);
      if (index > -1) {
        identifierList.value.splice(index, 1);
        pagination.total--;
        ElMessage.success("删除成功");
      }
    });
  };
  // ç”ŸæˆäºŒç»´ç 
  const generateQRCode = async row => {
    try {
      // æ£€æŸ¥å¿…要字段
      if (!row.productName || !row.productCode || !row.batchNo) {
        ElMessage.warning("产品信息不完整,无法生成二维码");
        return;
      }
      currentQRProduct.value = row;
      qrCodeUrl.value = "";
      qrCodeDialogVisible.value = true;
      // æž„建二维码内容
      let qrContent = "";
      if (row.identifierType === "二维码") {
        qrContent = `${row.productName}|${row.productCode}|${row.batchNo}|${row.identifierCode}`;
      } else if (row.identifierType === "防伪码") {
        // é˜²ä¼ªç æ ¼å¼ï¼šSEC_产品编码_批次号_时间戳_随机数
        const timestamp = Date.now();
        const random = Math.random().toString(36).substr(2, 8);
        qrContent = `SEC_${row.productCode}_${row.batchNo}_${timestamp}_${random}`;
      }
      // ç”ŸæˆäºŒç»´ç 
      qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
        width: 256,
        margin: 2,
        color: {
          dark: "#000000",
          light: "#FFFFFF",
        },
        errorCorrectionLevel: row.identifierType === "防伪码" ? "H" : "M",
      });
      ElMessage.success("二维码生成成功!");
    } catch (error) {
      console.error("生成二维码失败:", error);
      ElMessage.error("生成二维码失败:" + error.message);
      qrCodeDialogVisible.value = false;
    }
  };
  // ä¸‹è½½äºŒç»´ç 
  const downloadQRCode = () => {
    if (!qrCodeUrl.value) {
      ElMessage.warning("请先生成二维码");
      return;
    }
    const a = document.createElement("a");
    a.href = qrCodeUrl.value;
    a.download = `${currentQRProduct.value.productName}_${
      currentQRProduct.value.identifierType
    }_${new Date().getTime()}.png`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    ElMessage.success("下载成功!");
  };
  // å¤åˆ¶äºŒç»´ç å†…容
  const copyQRContent = async () => {
    if (!currentQRProduct.value) {
      ElMessage.warning("没有可复制的内容");
      return;
    }
    try {
      let content = "";
      if (currentQRProduct.value.identifierType === "二维码") {
        content = `${currentQRProduct.value.productName}|${currentQRProduct.value.productCode}|${currentQRProduct.value.batchNo}|${currentQRProduct.value.identifierCode}`;
      } else if (currentQRProduct.value.identifierType === "防伪码") {
        const timestamp = Date.now();
        const random = Math.random().toString(36).substr(2, 8);
        content = `SEC_${currentQRProduct.value.productCode}_${currentQRProduct.value.batchNo}_${timestamp}_${random}`;
      }
      await navigator.clipboard.writeText(content);
      ElMessage.success("内容已复制到剪贴板");
    } catch (error) {
      // é™çº§æ–¹æ¡ˆ
      const textArea = document.createElement("textarea");
      textArea.value = content;
      document.body.appendChild(textArea);
      textArea.select();
      document.execCommand("copy");
      document.body.removeChild(textArea);
      ElMessage.success("内容已复制到剪贴板");
    }
  };
  const generateIdentifiers = () => {
    if (!codeRule.value) {
      ElMessage.warning("请选择编码规则");
      return;
    }
    // ç”Ÿæˆæ ‡è¯†çš„逻辑
    const newIdentifiers = [];
    for (let i = 1; i <= generateQuantity.value; i++) {
      let identifierCode = "";
      if (codeRule.value === "产品编码+批次号+序号") {
        identifierCode = `${currentProduct.value.productCode}_${
          currentProduct.value.batchNo
        }_${String(i).padStart(3, "0")}`;
      } else if (codeRule.value === "时间戳+随机数") {
        identifierCode = `TS_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
      } else if (codeRule.value === "自定义规则") {
        identifierCode = `${customPrefix.value || "CUSTOM"}_${Date.now()}_${i}`;
      }
      newIdentifiers.push({
        id: Math.max(...identifierList.value.map(item => item.id)) + i,
        productName: currentProduct.value.productName,
        productCode: currentProduct.value.productCode,
        batchNo: currentProduct.value.batchNo,
        identifierType: currentProduct.value.identifierType,
        identifierCode: identifierCode,
        status: "已生成",
        generateTime: new Date().toLocaleString(),
        remark: "批量生成",
      });
    }
    identifierList.value.push(...newIdentifiers);
    pagination.total += newIdentifiers.length;
    ElMessage.success(`成功生成 ${newIdentifiers.length} ä¸ªæ ‡è¯†`);
    generateDialogVisible.value = false;
  };
  const saveReassign = () => {
    if (!newBatchNo.value) {
      ElMessage.warning("请输入新批次号");
      return;
    }
    const index = identifierList.value.findIndex(
      item => item.id === currentProduct.value.id
    );
    if (index > -1) {
      identifierList.value.splice(index, 1)
      pagination.total--
      ElMessage.success('删除成功')
      identifierList.value[index].batchNo = newBatchNo.value;
      identifierList.value[index].status = "已分配";
      ElMessage.success("标识重新分配成功");
      reassignDialogVisible.value = false;
    }
  })
}
  };
// ç”ŸæˆäºŒç»´ç 
const generateQRCode = async (row) => {
  try {
    // æ£€æŸ¥å¿…要字段
    if (!row.productName || !row.productCode || !row.batchNo) {
      ElMessage.warning('产品信息不完整,无法生成二维码')
      return
    }
    currentQRProduct.value = row
    qrCodeUrl.value = ''
    qrCodeDialogVisible.value = true
    // æž„建二维码内容
    let qrContent = ''
    if (row.identifierType === '二维码') {
      qrContent = `${row.productName}|${row.productCode}|${row.batchNo}|${row.identifierCode}`
    } else if (row.identifierType === '防伪码') {
      // é˜²ä¼ªç æ ¼å¼ï¼šSEC_产品编码_批次号_时间戳_随机数
      const timestamp = Date.now()
      const random = Math.random().toString(36).substr(2, 8)
      qrContent = `SEC_${row.productCode}_${row.batchNo}_${timestamp}_${random}`
    }
    // ç”ŸæˆäºŒç»´ç 
    qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
      width: 256,
      margin: 2,
      color: {
        dark: '#000000',
        light: '#FFFFFF'
      },
      errorCorrectionLevel: row.identifierType === '防伪码' ? 'H' : 'M'
    })
    ElMessage.success('二维码生成成功!')
  } catch (error) {
    console.error('生成二维码失败:', error)
    ElMessage.error('生成二维码失败:' + error.message)
    qrCodeDialogVisible.value = false
  }
}
  const handleSubmit = () => {
    formRef.value.validate(valid => {
      if (valid) {
        if (isEdit.value) {
          // ç¼–辑
          const index = identifierList.value.findIndex(
            item => item.id === editId.value
          );
          if (index > -1) {
            identifierList.value[index] = { ...form, id: editId.value };
            ElMessage.success("编辑成功");
          }
        } else {
          // æ–°å¢ž
          const newId =
            Math.max(...identifierList.value.map(item => item.id)) + 1;
// ä¸‹è½½äºŒç»´ç 
const downloadQRCode = () => {
  if (!qrCodeUrl.value) {
    ElMessage.warning('请先生成二维码')
    return
  }
  const a = document.createElement('a')
  a.href = qrCodeUrl.value
  a.download = `${currentQRProduct.value.productName}_${currentQRProduct.value.identifierType}_${new Date().getTime()}.png`
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  ElMessage.success('下载成功!')
}
          // æ ¹æ®æ ‡è¯†ç±»åž‹ç”Ÿæˆä¸åŒçš„æ ‡è¯†ç 
          let identifierCode = "";
          if (form.identifierType === "二维码") {
            identifierCode = `QR_${form.productCode}_${form.batchNo}_001`;
          } else if (form.identifierType === "防伪码") {
            identifierCode = `SEC_${form.productCode}_${form.batchNo}_001`;
          }
// å¤åˆ¶äºŒç»´ç å†…容
const copyQRContent = async () => {
  if (!currentQRProduct.value) {
    ElMessage.warning('没有可复制的内容')
    return
  }
  try {
    let content = ''
    if (currentQRProduct.value.identifierType === '二维码') {
      content = `${currentQRProduct.value.productName}|${currentQRProduct.value.productCode}|${currentQRProduct.value.batchNo}|${currentQRProduct.value.identifierCode}`
    } else if (currentQRProduct.value.identifierType === '防伪码') {
      const timestamp = Date.now()
      const random = Math.random().toString(36).substr(2, 8)
      content = `SEC_${currentQRProduct.value.productCode}_${currentQRProduct.value.batchNo}_${timestamp}_${random}`
    }
    await navigator.clipboard.writeText(content)
    ElMessage.success('内容已复制到剪贴板')
  } catch (error) {
    // é™çº§æ–¹æ¡ˆ
    const textArea = document.createElement('textarea')
    textArea.value = content
    document.body.appendChild(textArea)
    textArea.select()
    document.execCommand('copy')
    document.body.removeChild(textArea)
    ElMessage.success('内容已复制到剪贴板')
  }
}
const generateIdentifiers = () => {
  if (!codeRule.value) {
    ElMessage.warning('请选择编码规则')
    return
  }
  // ç”Ÿæˆæ ‡è¯†çš„逻辑
  const newIdentifiers = []
  for (let i = 1; i <= generateQuantity.value; i++) {
    let identifierCode = ''
    if (codeRule.value === '产品编码+批次号+序号') {
      identifierCode = `${currentProduct.value.productCode}_${currentProduct.value.batchNo}_${String(i).padStart(3, '0')}`
    } else if (codeRule.value === '时间戳+随机数') {
      identifierCode = `TS_${Date.now()}_${Math.floor(Math.random() * 1000)}`
    } else if (codeRule.value === '自定义规则') {
      identifierCode = `${customPrefix.value || 'CUSTOM'}_${Date.now()}_${i}`
    }
    newIdentifiers.push({
      id: Math.max(...identifierList.value.map(item => item.id)) + i,
      productName: currentProduct.value.productName,
      productCode: currentProduct.value.productCode,
      batchNo: currentProduct.value.batchNo,
      identifierType: currentProduct.value.identifierType,
      identifierCode: identifierCode,
      status: '已生成',
      generateTime: new Date().toLocaleString(),
      remark: '批量生成'
    })
  }
  identifierList.value.push(...newIdentifiers)
  pagination.total += newIdentifiers.length
  ElMessage.success(`成功生成 ${newIdentifiers.length} ä¸ªæ ‡è¯†`)
  generateDialogVisible.value = false
}
const saveReassign = () => {
  if (!newBatchNo.value) {
    ElMessage.warning('请输入新批次号')
    return
  }
  const index = identifierList.value.findIndex(item => item.id === currentProduct.value.id)
  if (index > -1) {
    identifierList.value[index].batchNo = newBatchNo.value
    identifierList.value[index].status = '已分配'
    ElMessage.success('标识重新分配成功')
    reassignDialogVisible.value = false
  }
}
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (isEdit.value) {
        // ç¼–辑
        const index = identifierList.value.findIndex(item => item.id === editId.value)
        if (index > -1) {
          identifierList.value[index] = { ...form, id: editId.value }
          ElMessage.success('编辑成功')
          identifierList.value.push({
            ...form,
            id: newId,
            identifierCode: identifierCode,
            generateTime: new Date().toLocaleString(),
          });
          pagination.total++;
          ElMessage.success("新增成功");
        }
             } else {
         // æ–°å¢ž
         const newId = Math.max(...identifierList.value.map(item => item.id)) + 1
         // æ ¹æ®æ ‡è¯†ç±»åž‹ç”Ÿæˆä¸åŒçš„æ ‡è¯†ç 
         let identifierCode = ''
         if (form.identifierType === '二维码') {
           identifierCode = `QR_${form.productCode}_${form.batchNo}_001`
         } else if (form.identifierType === '防伪码') {
           identifierCode = `SEC_${form.productCode}_${form.batchNo}_001`
         }
         identifierList.value.push({
           ...form,
           id: newId,
           identifierCode: identifierCode,
           generateTime: new Date().toLocaleString()
         })
         pagination.total++
         ElMessage.success('新增成功')
       }
      dialogVisible.value = false
    }
  })
}
        dialogVisible.value = false;
      }
    });
  };
const handleCurrentChange = (val) => {
  pagination.currentPage = val.page
  pagination.pageSize = val.limit
}
  const handleCurrentChange = val => {
    pagination.currentPage = val.page;
    pagination.pageSize = val.limit;
  };
</script>
<style scoped>
.search-row {
  margin-bottom: 20px;
}
  .search-row {
    margin-bottom: 20px;
  }
.quick-actions-row {
  margin-bottom: 20px;
}
  .quick-actions-row {
    margin-bottom: 20px;
  }
.quick-actions-row .el-alert {
  margin-bottom: 0;
}
  .quick-actions-row .el-alert {
    margin-bottom: 0;
  }
.quick-actions-row .el-alert p {
  margin: 5px 0;
  font-size: 14px;
  line-height: 1.5;
}
  .quick-actions-row .el-alert p {
    margin: 5px 0;
    font-size: 14px;
    line-height: 1.5;
  }
/* äºŒç»´ç é¢„览样式 */
.qr-preview-container {
  text-align: center;
  padding: 20px;
}
  /* äºŒç»´ç é¢„览样式 */
  .qr-preview-container {
    text-align: center;
    padding: 20px;
  }
.qr-image-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}
  .qr-image-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
  }
.qr-image {
  max-width: 100%;
  height: auto;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
  .qr-image {
    max-width: 100%;
    height: auto;
    border: 2px solid #e0e0e0;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
.qr-info {
  text-align: left;
  background: #f8f9fa;
  padding: 15px;
  border-radius: 8px;
  min-width: 300px;
}
  .qr-info {
    text-align: left;
    background: #f8f9fa;
    padding: 15px;
    border-radius: 8px;
    min-width: 300px;
  }
.qr-info p {
  margin: 8px 0;
  color: #666;
  font-size: 14px;
}
  .qr-info p {
    margin: 8px 0;
    color: #666;
    font-size: 14px;
  }
.qr-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 15px;
  padding: 40px 0;
}
  .qr-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 15px;
    padding: 40px 0;
  }
.qr-loading .el-icon {
  font-size: 32px;
  color: #409EFF;
}
  .qr-loading .el-icon {
    font-size: 32px;
    color: #409eff;
  }
.qr-loading p {
  color: #666;
  margin: 0;
}
  .qr-loading p {
    color: #666;
    margin: 0;
  }
</style>
src/views/productionManagement/operationScheduling/components/formDia.vue
@@ -8,20 +8,47 @@
    >
      <el-button type="primary" @click="addRow" style="margin-bottom: 10px;">新增</el-button>
            <span style="font-size: 18px;margin-left: 10px">待排产数量:{{pendingNum}}</span>
<!--            <div style="margin-bottom: 10px; margin-left: 10px;">-->
<!--                <el-form-item label="领用:" style="margin-bottom: 0;">-->
<!--                    <el-input v-model="receive" placeholder="请输入领用" style="width: 200px;" />-->
<!--                </el-form-item>-->
<!--            </div>-->
      <el-table :data="tableData" border style="width: 100%" :summary-method="summarizeMainTable" show-summary :row-key="row => row.id">
        <el-table-column label="序号" width="60">
        <el-table-column label="序号" width="60" align="center">
          <template #default="scope">
            {{ scope.$index + 1 }}
          </template>
        </el-table-column>
        <el-table-column label="工序" prop="process">
        <el-table-column label="工序" prop="process" width="150">
          <template #default="scope">
                        <el-input v-model="scope.row.process" placeholder="请输入工序" />
          </template>
        </el-table-column>
        <el-table-column label="单位" prop="unit">
        <el-table-column label="产线" prop="productionLine" width="150">
          <template #default="scope">
            <el-select
              v-model="scope.row.productionLine"
              placeholder="选择产线"
              style="width: 100%;"
              clearable
            >
              <el-option
                v-for="line in productionLines"
                :key="line.value"
                :label="line.label"
                :value="line.value"
              />
            </el-select>
          </template>
        </el-table-column>
        <el-table-column label="单位" prop="unit" width="90">
          <template #default="scope">
            <el-input v-model="scope.row.unit" placeholder="请输入单位" />
          </template>
        </el-table-column>
        <el-table-column label="口味/品名/规格" prop="type" width="150">
          <template #default="scope">
            <el-input v-model="scope.row.type" placeholder="请输入" />
          </template>
        </el-table-column>
        <el-table-column label="排产数量" width="200" prop="schedulingNum">
@@ -50,17 +77,20 @@
                        />
          </template>
        </el-table-column>
        <el-table-column label="排产日期" prop="schedulingDate">
        <el-table-column label="排产日期" prop="schedulingDate" width="200">
          <template #default="scope">
            <el-date-picker v-model="scope.row.schedulingDate" type="date" placeholder="选择日期" style="width: 100%;" value-format="YYYY-MM-DD" format="YYYY-MM-DD"/>
          </template>
        </el-table-column>
        <el-table-column label="排产人" prop="schedulingUserId">
        <el-table-column label="排产人" prop="schedulingUserId" width="150">
          <template #default="scope">
                        <el-select
                            v-model="scope.row.schedulingUserId"
                            placeholder="选择人员"
                            style="width: 100%;"
              filterable
              default-first-option
              :reserve-keyword="false"
                        >
                            <el-option
                                v-for="user in userList"
@@ -69,6 +99,11 @@
                                :value="user.userId"
                            />
                        </el-select>
          </template>
        </el-table-column>
        <el-table-column label="备注" prop="remark" width="200">
          <template #default="scope">
            <el-input v-model="scope.row.remark" placeholder="请输入备注" />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80">
@@ -88,7 +123,7 @@
</template>
<script setup>
import {ref} from "vue";
import {ref, getCurrentInstance} from "vue";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import {processScheduling} from "@/api/productionManagement/operationScheduling.js";
const { proxy } = getCurrentInstance()
@@ -97,33 +132,56 @@
const dialogFormVisible = ref(false);
const operationType = ref('')
const tableData = ref([
    { process: '', schedulingDate: '', schedulingNum: '', schedulingUserId: '', workHours: '', unit: '' }
]);
const tableData = ref([]);
const unitFromRow = ref('');
const idFromRow = ref('');
const pendingNum = ref('');
const specificationModelFromRow = ref('');
const pendingNum = ref(0);
const userList = ref([])
const receive = ref('')
const sunqianUserId = ref('')
// äº§çº¿é€‰é¡¹
const productionLines = ref([
  { label: '产线1', value: '产线1' },
  { label: '产线2', value: '产线2' },
  { label: '产线3', value: '产线3' },
  { label: '产线4', value: '产线4' }
])
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
    pendingNum.value = row?.pendingNum ?? 0;
    unitFromRow.value = row?.unit ?? '';
    idFromRow.value = row?.id ?? '';
    specificationModelFromRow.value = row?.specificationModel ?? '';
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
        // æ‰¾åˆ°å­™å€©çš„用户ID并设置为默认值
        const sunqianUser = userList.value.find(user => user.nickName === '孙倩');
        if (sunqianUser) {
            sunqianUserId.value = sunqianUser.userId;
        }
        // åœ¨ç”¨æˆ·åˆ—表加载完成后创建行数据,并将产线数据带入
        tableData.value = [createRow(row)];
    });
    pendingNum.value = row.pendingNum
  if (row && row.unit !== undefined) {
    unitFromRow.value = row.unit;
    idFromRow.value = row.id;
    tableData.value.forEach(item => {
      item.unit = row.unit;
      item.id = row.id;
    });
  } else {
    unitFromRow.value = '';
  }
}
const createRow = (row) => ({
    id: idFromRow.value,
    process: '包装',
    schedulingDate: '',
    schedulingNum: null,
    schedulingUserId: sunqianUserId.value, // é»˜è®¤è®¾ç½®ä¸ºå­™å€©çš„用户ID
    workHours: null,
    unit: unitFromRow.value,
    remark: '',
    type: specificationModelFromRow.value,
    productionLine: row?.productionLine ?? '', // ä»Žè¡Œæ•°æ®ä¸­èŽ·å–äº§çº¿ä¿¡æ¯
});
const submitForm = () => {
    // 1. æ£€æŸ¥æ¯ä¸€è¡Œæ˜¯å¦å¡«å†™å®Œæ•´
    for (let i = 0; i < tableData.value.length; i++) {
@@ -134,7 +192,8 @@
            row.schedulingNum === '' || row.schedulingNum === null ||
            !row.schedulingUserId ||
            row.workHours === '' || row.workHours === null ||
            !row.unit
            !row.unit ||
            !row.productionLine
        ) {
            proxy.$modal.msgError(`第${i + 1}行数据未填写完整`);
            return;
@@ -148,7 +207,15 @@
        proxy.$modal.msgError('排产数量合计不能超过待排产数量');
        return;
    }
    processScheduling(tableData.value).then((res) => {
    // 3. å°† receive å­—段添加到每条数据中,并移除 loss å­—段
    const submitData = tableData.value.map(row => {
        const { loss, ...rest } = row;
        return {
            ...rest,
            receive: receive.value
        };
    });
    processScheduling(submitData).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeDia();
    })
@@ -159,6 +226,12 @@
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  receive.value = '';
  tableData.value = [];
  unitFromRow.value = '';
  idFromRow.value = '';
  specificationModelFromRow.value = '';
  pendingNum.value = 0;
  emit('close')
};
defineExpose({
@@ -166,7 +239,7 @@
});
const addRow = () => {
  tableData.value.push({ id: idFromRow.value, process: '', unit: unitFromRow.value, schedulingNum: '', workHours: '', schedulingDate: '', schedulingUserId: '' });
  tableData.value.push(createRow());
};
const removeRow = (index) => {
  tableData.value.splice(index, 1);
src/views/productionManagement/operationScheduling/index.vue
@@ -7,11 +7,16 @@
                                        style="width: 200px;"
                                        @change="handleQuery" />
                </el-form-item>
                <el-form-item label="项目名称:">
                    <el-input v-model="searchForm.projectName" placeholder="请输入" clearable prefix-icon="Search"
                <el-form-item label="合同号:">
                    <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search"
                                        style="width: 200px;"
                                        @change="handleQuery" />
                </el-form-item>
<!--                <el-form-item label="项目名称:">-->
<!--                    <el-input v-model="searchForm.projectName" placeholder="请输入" clearable prefix-icon="Search"-->
<!--                                        style="width: 200px;"-->
<!--                                        @change="handleQuery" />-->
<!--                </el-form-item>-->
                <el-form-item label="派工日期:">
                    <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                                                    placeholder="请选择" clearable @change="changeDaterange" />
@@ -60,10 +65,12 @@
const data = reactive({
    searchForm: {
        staffName: "",
        customerName: "",
        salesContractNo: "",
        status: 1,
        entryDate: null, // å½•入日期
        entryDateStart: undefined,
        entryDateEnd: undefined,
        entryDate: [dayjs().format("YYYY-MM-DD"), dayjs().format("YYYY-MM-DD")], // å½•入日期,默认当天
        entryDateStart: dayjs().format("YYYY-MM-DD"),
        entryDateEnd: dayjs().format("YYYY-MM-DD"),
    },
});
const { searchForm } = toRefs(data);
@@ -105,21 +112,21 @@
        prop: "salesContractNo",
        width: 200,
    },
    {
        label: "客户合同号",
        prop: "customerContractNo",
        width: 200,
    },
    // {
    //     label: "客户合同号",
    //     prop: "customerContractNo",
    //     width: 200,
    // },
    {
        label: "客户名称",
        prop: "customerName",
        width: 200,
    },
    {
        label: "项目名称",
        prop: "projectName",
        width:300
    },
    // {
    //     label: "项目名称",
    //     prop: "projectName",
    //     width:300
    // },
    {
        label: "产品大类",
        prop: "productCategory",
@@ -131,6 +138,16 @@
        width: 150,
    },
    {
        label: "绑定机器",
        prop: "speculativeTradingName",
        width: 220,
    },
    // {
    //     label: "产线",
    //     prop: "productionLine",
    //     width: 220,
    // },
    {
        label: "单位",
        prop: "unit",
    },
src/views/productionManagement/processRoute/Edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,186 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="编辑工艺路线"
        width="400"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="产品大类:"
            prop="productId"
            :rules="[
                {
                required: true,
                message: '请选择产品大类',
              }
            ]"
        >
          <el-tree-select
              v-model="formState.productId"
              placeholder="请选择"
              clearable
              check-strictly
              @change="getModels"
              :data="productOptions"
              :render-after-expand="false"
              style="width: 100%"
          />
        </el-form-item>
        <el-form-item
            label="规格型号:"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择规格型号',
              }
            ]"
        >
          <el-select
              v-model="formState.productModelId"
              placeholder="请选择"
              clearable
          >
            <el-option
                v-for="item in productModelsOptions"
                :key="item.id"
                :label="item.model"
                :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, computed, getCurrentInstance, onMounted} from "vue";
import {update} from "@/api/productionManagement/processRoute.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
  record: {
    type: Object,
    required: true,
  }
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
let { proxy } = getCurrentInstance()
const productModelsOptions = ref([])
const productOptions = ref([])
const closeModal = () => {
  isShow.value = false;
};
const setFormData = () => {
  formState.value = props.record
}
const getProductOptions = () => {
  productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
  });
};
const getModels = (value) => {
  formState.value.productModelId = undefined;
  productModelsOptions.value = [];
  if (value) {
    modelList({ id: value }).then((res) => {
      productModelsOptions.value = res;
    });
  }
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
      return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹çš„label
    }
    if (nodes[i].children && nodes[i].children.length > 0) {
      const foundNode = findNodeById(nodes[i].children, productId);
      if (foundNode) {
        return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œç›´æŽ¥è¿”å›žï¼ˆå·²ç»æ˜¯label字符串)
      }
    }
  }
  return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
};
function convertIdToValue(data) {
  return data.map((item) => {
    const { id, children, ...rest } = item;
    const newItem = {
      ...rest,
      value: id, // å°† id æ”¹ä¸º value
    };
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      update(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
    }
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
onMounted(() => {
  getProductOptions()
  getModels(props.record.productId)
  nextTick(() => {
    setFormData()
  });
})
</script>
src/views/productionManagement/processRoute/ItemsForm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,531 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="工艺路线项目"
        width="800px"
        @close="closeModal"
    >
      <div class="operate-button">
        <el-button
            type="primary"
            @click="isShowProductSelectDialog = true"
            class="mb5"
            style="margin-bottom: 10px;"
        >
          é€‰æ‹©äº§å“
        </el-button>
        <el-switch
            v-model="isTable"
            inline-prompt
            active-text="表格"
            inactive-text="列表"
            @change="handleViewChange"
        />
      </div>
      <el-table
          v-if="isTable"
          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="序号" width="60">
          <template #default="scope">
            {{ scope.$index + 1 }}
          </template>
        </el-table-column>
        <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>
      <!-- ä½¿ç”¨æ™®é€šdiv替代el-steps -->
      <div
          v-else
          ref="stepsContainer"
          class="mb5 custom-steps"
          style="padding: 10px 0; display: flex; flex-wrap: nowrap; gap: 20px; align-items: flex-start;"
      >
        <div
            v-for="(item, index) in routeItems"
            :key="item.id"
            class="custom-step draggable-step"
            :data-id="item.id"
            style="cursor: move; flex: 0 0 auto; min-width: 220px;"
        >
          <div class="step-content">
            <div class="step-number">{{ index + 1 }}</div>
            <el-card
                :header="item.productName"
                class="step-card"
                style="cursor: move;"
            >
              <div class="step-card-content">
                <p>{{ item.model }}</p>
                <p>{{ item.unit }}</p>
                <el-select
                    v-model="item.processId"
                    style="width: 100%;"
                    @mousedown.stop
                >
                  <el-option
                      v-for="process in processOptions"
                      :key="process.id"
                      :label="process.name"
                      :value="process.id"
                  />
                </el-select>
              </div>
              <template #footer>
                <div class="step-card-footer">
                  <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">删除</el-button>
                </div>
              </template>
            </el-card>
          </div>
        </div>
      </div>
      <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 tableSortable = null;
let stepsSortable = null;
const multipleTable = ref(null);
const stepsContainer = ref(null);
const isTable = ref(true);
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) {
            removeItem(idx)
          }
        }
      }
    ]
  }
]);
const removeItem = (index) => {
  routeItems.value.splice(index, 1);
  nextTick(() => initSortable());
};
const removeItemByID = (id) => {
  const idx = routeItems.value.findIndex(item => item.id === id);
  if (idx > -1) {
    routeItems.value.splice(idx, 1);
    nextTick(() => initSortable());
  }
};
const closeModal = () => {
  isShow.value = false;
};
const handelSelectProducts = (products) => {
  destroySortable();
  const newData = products.map(({ id, ...product }) => ({
    ...product,
    productModelId: id,
    routeId: props.record.id,
    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
    processId: undefined
  }));
  console.log('选择产品前数组:', routeItems.value);
  routeItems.value.push(...newData);
  routeItems.value = [...routeItems.value];
  console.log('选择产品后数组:', routeItems.value);
  // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
  nextTick(() => {
    // å¼ºåˆ¶é‡æ–°æ¸²æŸ“组件
    if (proxy?.$forceUpdate) {
      proxy.$forceUpdate();
    }
    const temp = [...routeItems.value];
    routeItems.value = [];
    nextTick(() => {
      routeItems.value = temp;
      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
        }));
        // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
        nextTick(() => {
          setTimeout(() => initSortable(), 100);
        });
      })
      .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 = () => {
  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 destroySortable = () => {
  if (tableSortable) {
    tableSortable.destroy();
    tableSortable = null;
  }
  if (stepsSortable) {
    stepsSortable.destroy();
    stepsSortable = null;
  }
};
const initSortable = () => {
  destroySortable();
  if (isTable.value) {
    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;
    tableSortable = new Sortable(tbody, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      handle: '.el-table__row',
      filter: '.el-button, .el-select',
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序,与表格模式保持一致
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
        console.log('排序后数组:', routeItems.value);
      }
    });
  } else {
    if (!stepsContainer.value) return;
    // ä¿®æ”¹ï¼šç›´æŽ¥ä½¿ç”¨stepsContainer.value作为拖拽容器
    const stepsList = stepsContainer.value;
    if (!stepsList) {
      console.warn('未找到步骤条拖拽容器');
      return;
    }
    // ä¿®æ”¹ï¼šç®€åŒ–拖拽配置
    stepsSortable = new Sortable(stepsList, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      draggable: '.draggable-step', // å¯æ‹–拽元素
      handle: '.draggable-step, .step-card', // æ‹–拽手柄
      filter: '.el-button, .el-select, .el-input', // è¿‡æ»¤æŒ‰é’®/选择器
      forceFallback: true,
      fallbackClass: 'sortable-fallback',
      preventOnFilter: true,
      scroll: true,
      scrollSensitivity: 30,
      scrollSpeed: 10,
      bubbleScroll: true,
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
      }
    });
    // è°ƒè¯•:打印容器和实例,确认绑定成功
    console.log('步骤条拖拽容器:', stepsList);
    console.log('Sortable实例:', stepsSortable);
  }
};
const handleViewChange = () => {
  destroySortable();
  // å»¶è¿Ÿåˆå§‹åŒ–,确保视图切换后DOM完全渲染
  nextTick(() => {
    setTimeout(() => initSortable(), 100);
  });
};
onMounted(() => {
  findProcessRouteItems();
  findProcessList();
});
onUnmounted(() => {
  destroySortable();
});
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;
}
:deep(.el-card__footer){
  padding: 0 !important;
}
.operate-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤æ¡å®¹å™¨æ ·å¼ */
.custom-steps {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 20px;
  min-height: 100px;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤é¡¹æ ·å¼ */
.custom-step {
  cursor: move !important;
  padding: 8px;
  position: relative;
  transition: all 0.2s ease;
  flex: 0 0 auto;
  min-width: 220px;
  touch-action: none;
}
/* æ‹–拽悬浮样式,提示可拖拽 */
.custom-step:hover {
  background-color: rgba(64, 158, 255, 0.05);
  transform: translateY(-2px);
}
.sortable-ghost {
  opacity: 0.4;
  background-color: #f5f7fa !important;
  border: 2px dashed #409eff;
  margin: 10px;
  transform: scale(1.02);
}
.sortable-fallback {
  opacity: 0.9;
  background-color: #f5f7fa;
  border: 1px solid #409eff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform: rotate(2deg);
  margin: 10px;
}
.step-card {
  cursor: move !important;
  transition: box-shadow 0.2s ease;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: auto;
  margin: 10px;
  height: 240px;
}
.step-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.step-content {
  width: 220px;
  user-select: none;
}
.step-card-content {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.step-card-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 10px;
}
/* è‡ªå®šä¹‰åºå·æ ·å¼ä¼˜åŒ– */
.step-number {
  font-weight: bold;
  text-align: center;
  width: 36px;
  height: 36px;
  line-height: 36px;
  margin: 0 auto 10px;
  background: #409eff;
  color: #fff;
  border-radius: 50%;
  font-size: 14px;
}
</style>
src/views/productionManagement/processRoute/New.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="新增工艺路线"
        width="400"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="产品大类:"
            prop="productId"
            :rules="[
                {
                required: true,
                message: '请选择产品大类',
              }
            ]"
        >
          <el-tree-select
              v-model="formState.productId"
              placeholder="请选择"
              clearable
              check-strictly
              @change="getModels"
              :data="productOptions"
              :render-after-expand="false"
              style="width: 100%"
          />
        </el-form-item>
        <el-form-item
            label="规格型号:"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择规格型号',
              }
            ]"
        >
          <el-select
              v-model="formState.productModelId"
              placeholder="请选择"
              clearable
          >
            <el-option
                v-for="item in productModelsOptions"
                :key="item.id"
                :label="item.model"
                :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, computed, getCurrentInstance, onMounted} from "vue";
import {add} from "@/api/productionManagement/processRoute.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  productId: undefined,
  productModelId: undefined,
  description: '',
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
const productModelsOptions = ref([])
const productOptions = ref([])
let { proxy } = getCurrentInstance()
const closeModal = () => {
  isShow.value = false;
};
const getProductOptions = () => {
  productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
  });
};
const getModels = (value) => {
  formState.value.productId = undefined;
  formState.value.productModelId = undefined;
  productModelsOptions.value = [];
  if (value) {
    formState.value.productId = findNodeById(productOptions.value, value) || undefined;
    modelList({ id: value }).then((res) => {
      productModelsOptions.value = res;
    });
  }
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
      return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹çš„label
    }
    if (nodes[i].children && nodes[i].children.length > 0) {
      const foundNode = findNodeById(nodes[i].children, productId);
      if (foundNode) {
        return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œç›´æŽ¥è¿”å›žï¼ˆå·²ç»æ˜¯label字符串)
      }
    }
  }
  return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
};
function convertIdToValue(data) {
  return data.map((item) => {
    const { id, children, ...rest } = item;
    const newItem = {
      ...rest,
      value: id, // å°† id æ”¹ä¸º value
    };
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      add(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
    }
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
onMounted(() => {
  getProductOptions()
})
</script>
src/views/productionManagement/processRoute/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm" :inline="true">
        <el-form-item label="规格名称:">
          <el-input v-model="searchForm.model" placeholder="请输入" clearable prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </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" @click="handleDelete" :disabled="selectedRows.length === 0" plain>删除工艺路线</el-button>
      </div>
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
      />
    </div>
    <new-process
        v-if="isShowNewModal"
        v-model:visible="isShowNewModal"
        @completed="getList"
    />
    <edit-process
        v-if="isShowEditModal"
        v-model:visible="isShowEditModal"
        :record="record"
        @completed="getList"
    />
    <route-item-form
        v-if="isShowItemModal"
        v-model:visible="isShowItemModal"
        :record="record"
        @completed="getList"
    />
  </div>
</template>
<script setup>
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";
import { useRouter } from 'vue-router'
const router = useRouter()
const data = reactive({
  searchForm: {
    model: "",
  },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
  {
    label: "工艺路线编号",
    prop: "processRouteCode",
  },
  {
    label: "产品名称",
    prop: "productName",
  },
  {
    label: "规格名称",
    prop: "model",
  },
  {
    label: "描述",
    prop: "description",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 280,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          showEditModal(row);
        }
      },
      {
        name: "路线项目",
        type: "text",
        clickFun: (row) => {
          showItemModal(row);
        }
      }
    ]
  }
]);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const isShowNewModal = ref(false);
const isShowEditModal = ref(false);
const isShowItemModal = ref(false);
const record = ref({});
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
});
const { proxy } = getCurrentInstance()
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
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.value, ...page };
  params.entryDate = undefined
  listPage(params).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records.map(item => ({
      ...item,
    }));
    page.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æ‰“开新增弹框
const showNewModal = () => {
  isShowNewModal.value = true
};
const showEditModal = (row) => {
  isShowEditModal.value = true
  record.value = row
};
const showItemModal = (row) => {
  router.push({
    path: '/productionManagement/processRouteItem',
    query: {
      id: row.id
    }
  })
};
// åˆ é™¤
function handleDelete() {
  const ids = selectedRows.value.map((item) => item.id);
  proxy.$modal
      .confirm('是否确认删除已勾选的数据项?')
      .then(function () {
        return del(ids);
      })
      .then(() => {
        getList();
        proxy.$modal.msgSuccess("删除成功");
      })
      .catch(() => {});
}
onMounted(() => {
  getList();
});
</script>
<style scoped></style>
src/views/productionManagement/processRoute/processRouteItem/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,503 @@
<template>
  <div class="app-container">
    <div class="operate-button">
      <div style="margin-bottom: 15px;">
        <el-button
            type="primary"
            @click="isShowProductSelectDialog = true"
        >
          é€‰æ‹©äº§å“
        </el-button>
        <el-button type="primary" @click="handleSubmit">确认</el-button>
      </div>
      <el-switch
          v-model="isTable"
          inline-prompt
          active-text="表格"
          inactive-text="列表"
          @change="handleViewChange"
      />
    </div>
    <el-table
        v-if="isTable"
        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="序号" width="60">
        <template #default="scope">
          {{ scope.$index + 1 }}
        </template>
      </el-table-column>
      <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>
    <!-- ä½¿ç”¨æ™®é€šdiv替代el-steps -->
    <div
        v-else
        ref="stepsContainer"
        class="mb5 custom-steps"
    >
      <div
          v-for="(item, index) in routeItems"
          :key="item.id"
          class="custom-step draggable-step"
          :data-id="item.id"
          style="cursor: move; flex: 0 0 auto; min-width: 220px;"
      >
        <div class="step-content">
          <div class="step-number">{{ index + 1 }}</div>
          <el-card
              :header="item.productName"
              class="step-card"
              style="cursor: move;"
          >
            <div class="step-card-content">
              <p>{{ item.model }}</p>
              <p>{{ item.unit }}</p>
              <el-select
                  v-model="item.processId"
                  style="width: 100%;"
                  @mousedown.stop
              >
                <el-option
                    v-for="process in processOptions"
                    :key="process.id"
                    :label="process.name"
                    :value="process.id"
                />
              </el-select>
            </div>
            <template #footer>
              <div class="step-card-footer">
                <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">删除</el-button>
              </div>
            </template>
          </el-card>
        </div>
      </div>
    </div>
    <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';
import { useRoute, useRouter } from 'vue-router'
const processOptions = ref([]);
const tableLoading = ref(false);
const isShowProductSelectDialog = ref(false);
const routeItems = ref([]);
let tableSortable = null;
let stepsSortable = null;
const multipleTable = ref(null);
const stepsContainer = ref(null);
const isTable = ref(true);
const route = useRoute()
const router = useRouter()
const routeId = computed({
  get() {
    return route.query.id;
  },
  set(val) {
    emit('update:router', val)
  }
});
const tableColumn = ref([
  { label: "产品名称", prop: "productName"},
  { label: "规格名称", prop: "model" },
  { label: "单位", prop: "unit" },
  { label: "工序名称", prop: "processId", width: 200 },
  {
    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) {
            removeItem(idx)
          }
        }
      }
    ]
  }
]);
const removeItem = (index) => {
  routeItems.value.splice(index, 1);
  nextTick(() => initSortable());
};
const removeItemByID = (id) => {
  const idx = routeItems.value.findIndex(item => item.id === id);
  if (idx > -1) {
    routeItems.value.splice(idx, 1);
    nextTick(() => initSortable());
  }
};
const handelSelectProducts = (products) => {
  destroySortable();
  const newData = products.map(({ id, ...product }) => ({
    ...product,
    productModelId: id,
    routeId: routeId.value,
    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
    processId: undefined
  }));
  console.log('选择产品前数组:', routeItems.value);
  routeItems.value.push(...newData);
  routeItems.value = [...routeItems.value];
  console.log('选择产品后数组:', routeItems.value);
  // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
  nextTick(() => {
    // å¼ºåˆ¶é‡æ–°æ¸²æŸ“组件
    if (proxy?.$forceUpdate) {
      proxy.$forceUpdate();
    }
    const temp = [...routeItems.value];
    routeItems.value = [];
    nextTick(() => {
      routeItems.value = temp;
      initSortable();
    });
  });
};
const findProcessRouteItems = () => {
  tableLoading.value = true;
  findProcessRouteItemList({ routeId: routeId.value })
      .then(res => {
        tableLoading.value = false;
        routeItems.value = res.data.map(item => ({
          ...item,
          processId: item.processId === 0 ? undefined : item.processId
        }));
        // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
        nextTick(() => {
          setTimeout(() => initSortable(), 100);
        });
      })
      .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 = () => {
  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
  if (hasEmptyProcess) {
    proxy?.$modal?.msgError("请为所有项目选择工序");
    return;
  }
  addOrUpdateProcessRouteItem({
    routeId: routeId.value,
    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
  })
      .then(res => {
        router.push({
          path: '/productionManagement/processRoute',
        })
        proxy?.$modal?.msgSuccess("提交成功");
      })
      .catch(err => {
        proxy?.$modal?.msgError(`提交失败:${err.msg || "网络异常"}`);
      });
};
const destroySortable = () => {
  if (tableSortable) {
    tableSortable.destroy();
    tableSortable = null;
  }
  if (stepsSortable) {
    stepsSortable.destroy();
    stepsSortable = null;
  }
};
const initSortable = () => {
  destroySortable();
  if (isTable.value) {
    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;
    tableSortable = new Sortable(tbody, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      handle: '.el-table__row',
      filter: '.el-button, .el-select',
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序,与表格模式保持一致
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
        console.log('排序后数组:', routeItems.value);
      }
    });
  } else {
    if (!stepsContainer.value) return;
    // ä¿®æ”¹ï¼šç›´æŽ¥ä½¿ç”¨stepsContainer.value作为拖拽容器
    const stepsList = stepsContainer.value;
    if (!stepsList) {
      console.warn('未找到步骤条拖拽容器');
      return;
    }
    // ä¿®æ”¹ï¼šç®€åŒ–拖拽配置
    stepsSortable = new Sortable(stepsList, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      draggable: '.draggable-step', // å¯æ‹–拽元素
      handle: '.draggable-step, .step-card', // æ‹–拽手柄
      filter: '.el-button, .el-select, .el-input', // è¿‡æ»¤æŒ‰é’®/选择器
      forceFallback: true,
      fallbackClass: 'sortable-fallback',
      preventOnFilter: true,
      scroll: true,
      scrollSensitivity: 30,
      scrollSpeed: 10,
      bubbleScroll: true,
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
      }
    });
    // è°ƒè¯•:打印容器和实例,确认绑定成功
    console.log('步骤条拖拽容器:', stepsList);
    console.log('Sortable实例:', stepsSortable);
  }
};
const handleViewChange = () => {
  destroySortable();
  // å»¶è¿Ÿåˆå§‹åŒ–,确保视图切换后DOM完全渲染
  nextTick(() => {
    setTimeout(() => initSortable(), 100);
  });
};
onMounted(() => {
  findProcessRouteItems();
  findProcessList();
});
onUnmounted(() => {
  destroySortable();
});
defineExpose({
  handleSubmit,
});
</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;
}
:deep(.el-card__footer){
  padding: 0 !important;
}
.operate-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤æ¡å®¹å™¨æ ·å¼ */
.custom-steps {
  min-height: 100px;
  padding: 10px 0;
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  align-items: flex-start;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤é¡¹æ ·å¼ */
.custom-step {
  cursor: move !important;
  padding: 8px;
  position: relative;
  transition: all 0.2s ease;
  flex: 0 0 auto;
  min-width: 220px;
  touch-action: none;
}
/* æ‹–拽悬浮样式,提示可拖拽 */
.custom-step:hover {
  background-color: rgba(64, 158, 255, 0.05);
  transform: translateY(-2px);
}
.sortable-ghost {
  opacity: 0.4;
  background-color: #f5f7fa !important;
  border: 2px dashed #409eff;
  margin: 10px;
  transform: scale(1.02);
}
.sortable-fallback {
  opacity: 0.9;
  background-color: #f5f7fa;
  border: 1px solid #409eff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform: rotate(2deg);
  margin: 10px;
}
.step-card {
  cursor: move !important;
  transition: box-shadow 0.2s ease;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: auto;
  margin: 10px;
  height: 260px;
}
.step-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.step-content {
  width: 245px;
  user-select: none;
}
.step-card-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 140px;
}
.step-card-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 10px;
}
/* è‡ªå®šä¹‰åºå·æ ·å¼ä¼˜åŒ– */
.step-number {
  font-weight: bold;
  text-align: center;
  width: 36px;
  height: 36px;
  line-height: 36px;
  margin: 0 auto 10px;
  background: #409eff;
  color: #fff;
  border-radius: 50%;
  font-size: 14px;
}
</style>
src/views/productionManagement/productStructure/Detail/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,293 @@
<template>
  <div class="app-container">
    <el-button v-if="dataValue.isEdit"
               type="primary"
               @click="addItem"
               style="margin-bottom: 10px">添加
    </el-button>
    <el-button v-if="!dataValue.isEdit"
               type="primary"
               @click="dataValue.isEdit = true"
               style="margin-bottom: 10px">编辑
    </el-button>
    <el-button v-if="dataValue.isEdit"
               type="primary"
               @click="cancelEdit"
               style="margin-bottom: 10px">取消
    </el-button>
    <el-button type="primary"
               :loading="dataValue.loading"
               @click="submit"
               :disabled="!dataValue.isEdit"
               style="margin-bottom: 10px">确认
    </el-button>
    <el-table
        :data="tableData"
        border
        :preserve-expanded-content="false"
        style="width: 100%"
    >
      <el-table-column type="expand">
        <template #default="props">
          <el-form ref="form"
                   :model="dataValue">
            <el-table :data="dataValue.dataList"
                      style="width: 100%">
              <el-table-column prop="productName"
                               label="产品"/>
              <el-table-column prop="model"
                               label="规格">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :prop="`dataList.${$index}.model`"
                                :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-select v-model="row.model"
                               placeholder="请选择规格"
                               clearable
                               :disabled="!dataValue.isEdit"
                               style="width: 100%"
                               @visible-change="(v) => { if (v) openDialog($index) }">
                      <el-option v-if="row.model"
                                 :label="row.model"
                                 :value="row.model" />
                    </el-select>
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="processId"
                               label="消耗工序">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.processId`"
                                :rules="[{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
                               filterable
                               clearable
                               style="width: 100%"
                               :disabled="!dataValue.isEdit">
                      <el-option v-for="item in dataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
                                 :value="item.id" />
                    </el-select>
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unitQuantity"
                               label="单位产出所需数量">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.unitQuantity`"
                                :rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.unitQuantity"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="demandedQuantity"
                               label="需求总量">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.demandedQuantity`"
                                :rules="[{ required: true, message: '请输入需求总量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.demandedQuantity"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unit"
                               label="单位">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.unit`"
                                :rules="[{ required: true, message: '请输入单位', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input v-model="row.unit"
                              placeholder="请输入单位"
                              clearable
                              :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="diskQuantity"
                               label="盘数(盘)">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.diskQuantity`"
                                :rules="[{ required: true, message: '请输入盘数', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.diskQuantity"
                                     :min="0"
                                     :precision="0"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column label="操作" fixed="right" width="100">
                <template #default="{ row, $index }">
                  <el-button type="danger"
                             text
                             @click="dataValue.dataList.splice($index, 1)">删除
                  </el-button>
                </template>
              </el-table-column>
            </el-table>
          </el-form>
        </template>
      </el-table-column>
      <el-table-column label="产品编码" prop="productCode" />
      <el-table-column label="产品名称" prop="productName" />
      <el-table-column label="规格型号" prop="model" />
      <el-table-column label="单位" prop="unit" />
    </el-table>
    <product-select-dialog v-if="dataValue.showProductDialog"
                           v-model:model-value="dataValue.showProductDialog"
                           @confirm="handleProduct" />
  </div>
</template>
<script setup lang="ts">
import {
  computed,
  defineAsyncComponent,
  defineComponent,
  onMounted,
  reactive,
  ref,
} from "vue";
import { queryList, add } from "@/api/productionManagement/productStructure.js";
import { list } from "@/api/productionManagement/productionProcess";
import { ElMessage } from "element-plus";
import {useRoute, useRouter} from "vue-router";
defineComponent({
  name: "StructureEdit",
});
const ProductSelectDialog = defineAsyncComponent(
    () => import("@/views/basicData/product/ProductSelectDialog.vue")
);
const form = ref();
const route = useRoute()
const router = useRouter()
const routeId = computed({
  get() {
    return route.query.id;
  },
  set(val) {
    emit('update:router', val)
  }
});
const dataValue = reactive({
  dataList: [],
  productOptions: [],
  processOptions: [],
  showProductDialog: false,
  currentRowIndex: null,
  loading: false,
  isEdit: false,
});
const tableData = reactive([
  {
    productName: "",
    model: "",
    unit: "",
    productCode: "",
  }
])
const openDialog = index => {
  dataValue.currentRowIndex = index;
  dataValue.showProductDialog = true;
};
const fetchData = async () => {
  const { data } = await queryList(routeId.value);
  tableData[0].productName = data.productName;
  tableData[0].model = data.model;
  tableData[0].unit = data.unit;
  tableData[0].productCode = data.productCode;
  dataValue.dataList = data.productStructureList;
};
const fetchProcessOptions = async () => {
  const { data } = await list(routeId.value);
  dataValue.processOptions = data;
};
const handleProduct = row => {
  if (row?.length > 1) {
    ElMessage.error("只能选择一个产品");
  }
  dataValue.dataList[dataValue.currentRowIndex].productName =
      row[0].productName;
  dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
  dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
  dataValue.showProductDialog = false;
};
const submit = () => {
  form.value
      .validate(valid => {
        dataValue.loading = true;
        if (valid) {
          add({
            parentId: routeId.value,
            productStructureList: dataValue.dataList || [],
          }).then(res => {
            router.push({
              path: '/productionManagement/productionManagement/productStructure/index',
            })
            ElMessage.success("保存成功");
            dataValue.loading = false;
          });
        }
      })
      .finally(() => {
        dataValue.loading = false;
      });
};
const addItem = () => {
  dataValue.dataList.push({
    productName: "",
    productId: "",
    model: undefined,
    productModelId: undefined,
    processId: "",
    unitQuantity: 0,
    demandedQuantity: 0,
    unit: "",
    diskQuantity: 0,
  });
};
const cancelEdit = () => {
  dataValue.isEdit = false;
  dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
};
onMounted(() => {
  fetchData();
  fetchProcessOptions();
});
</script>
src/views/productionManagement/productStructure/StructureEdit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,311 @@
<template>
  <el-dialog v-model="visible"
             title="结构"
             width="1200"
             close-on-click-modal
             @close="visible = false">
    <el-button v-if="dataValue.isEdit"
               type="primary"
               @click="addItem"
               style="margin-bottom: 10px">添加
    </el-button>
    <el-button v-if="!dataValue.isEdit"
               type="primary"
               @click="dataValue.isEdit = true"
               style="margin-bottom: 10px">编辑
    </el-button>
    <el-button v-if="dataValue.isEdit"
               type="primary"
               @click="cancelEdit"
               style="margin-bottom: 10px">取消
    </el-button>
    <el-table
        :data="tableData"
        border
        :preserve-expanded-content="false"
        style="width: 100%"
    >
      <el-table-column type="expand">
        <template #default="props">
          <el-form ref="form"
                   :model="dataValue">
            <el-table :data="dataValue.dataList"
                      style="width: 100%">
              <el-table-column prop="productName"
                               label="产品"
                               width="150" />
              <el-table-column prop="model"
                               label="规格"
                               width="150">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                                :prop="`dataList.${$index}.model`"
                                :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-select v-model="row.model"
                               placeholder="请选择产品"
                               clearable
                               :disabled="!dataValue.isEdit"
                               style="width: 100%"
                               @visible-change="(v) => { if (v) openDialog($index) }">
                      <el-option v-if="row.model"
                                 :label="row.model"
                                 :value="row.model" />
                    </el-select>
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="processId"
                               label="消耗工序"
                               width="150">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.processId`"
                                :rules="[{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
                               filterable
                               clearable
                               style="width: 100%"
                               :disabled="!dataValue.isEdit">
                      <el-option v-for="item in dataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
                                 :value="item.id" />
                    </el-select>
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unitQuantity"
                               label="单位产出所需数量"
                               width="150">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.unitQuantity`"
                                :rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.unitQuantity"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="demandedQuantity"
                               label="需求总量"
                               width="150">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.demandedQuantity`"
                                :rules="[{ required: true, message: '请输入需求总量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.demandedQuantity"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unit"
                               label="单位"
                               width="150">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.unit`"
                                :rules="[{ required: true, message: '请输入单位', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input v-model="row.unit"
                              placeholder="请输入单位"
                              clearable
                              :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="diskQuantity"
                               label="盘数(盘)"
                               width="150">
                <template #default="{ row, $index }">
                  <el-form-item :prop="`dataList.${$index}.diskQuantity`"
                                :rules="[{ required: true, message: '请输入盘数', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.diskQuantity"
                                     :min="0"
                                     :precision="0"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column label="操作">
                <template #default="{ row, $index }">
                  <el-button type="danger"
                             text
                             @click="dataValue.dataList.splice($index, 1)">删除
                  </el-button>
                </template>
              </el-table-column>
            </el-table>
          </el-form>
        </template>
      </el-table-column>
      <el-table-column label="产品编码" prop="productCode" />
      <el-table-column label="产品名称" prop="productName" />
      <el-table-column label="规格型号" prop="model" />
      <el-table-column label="单位" prop="unit" />
    </el-table>
    <product-select-dialog v-if="dataValue.showProductDialog"
                           v-model:model-value="dataValue.showProductDialog"
                           @confirm="handleProduct" />
    <template #footer>
      <div class="dialog-footer">
        <el-button type="primary"
                   :loading="dataValue.loading"
                   @click="submit"
                   :disabled="!dataValue.isEdit">
          ç¡®è®¤
        </el-button>
        <el-button @click="visible = false">取消</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup lang="ts">
  import {
    computed,
    defineAsyncComponent,
    defineComponent,
    onMounted,
    reactive,
    ref,
  } from "vue";
  import { queryList, add } from "@/api/productionManagement/productStructure.js";
  import { list } from "@/api/productionManagement/productionProcess";
  import { ElMessage } from "element-plus";
  defineComponent({
    name: "StructureEdit",
  });
  const ProductSelectDialog = defineAsyncComponent(
    () => import("@/views/basicData/product/ProductSelectDialog.vue")
  );
  const form = ref();
  const props = defineProps({
    showModel: {
      type: Boolean,
      default: false,
    },
    record: {
      type: Object,
      required: true,
    },
  });
  const emits = defineEmits(["update:showModel"]);
  const visible = computed({
    get() {
      return props.showModel;
    },
    set(val) {
      emits("update:showModel", val);
    },
  });
  const dataValue = reactive({
    dataList: [],
    productOptions: [],
    processOptions: [],
    showProductDialog: false,
    currentRowIndex: null,
    loading: false,
    isEdit: false,
  });
  const tableData = [
    {
      productName: props.record.productName,
      model: props.record.model,
      unit: props.record.unit,
      productCode: props.record.productCode,
    }
  ]
  const openDialog = index => {
    dataValue.currentRowIndex = index;
    dataValue.showProductDialog = true;
  };
  const fetchData = async () => {
    const { data } = await queryList(props.record.id);
    dataValue.dataList = data;
  };
  const fetchProcessOptions = async () => {
    const { data } = await list(props.record.id);
    dataValue.processOptions = data;
  };
  const handleProduct = row => {
    if (row?.length > 1) {
      ElMessage.error("只能选择一个产品");
    }
    dataValue.dataList[dataValue.currentRowIndex].productName =
      row[0].productName;
    dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
    dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
    dataValue.showProductDialog = false;
  };
  const submit = () => {
    form.value
      .validate(valid => {
        dataValue.loading = true;
        if (valid) {
          add({
            parentId: props.record.id,
            productStructureList: dataValue.dataList || [],
          }).then(res => {
            ElMessage.success("保存成功");
            visible.value = false;
            dataValue.loading = false;
          });
        }
      })
      .finally(() => {
        dataValue.loading = false;
      });
  };
  const addItem = () => {
    dataValue.dataList.push({
      productName: "",
      productId: "",
      model: undefined,
      productModelId: undefined,
      processId: "",
      unitQuantity: 0,
      demandedQuantity: 0,
      unit: "",
      diskQuantity: 0,
    });
  };
  const cancelEdit = () => {
    dataValue.isEdit = false;
    dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
  };
  onMounted(() => {
    fetchData();
    fetchProcessOptions();
  });
</script>
src/views/productionManagement/productStructure/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
<template>
  <div class="app-container">
    <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
    >
      <template #detail="{row}">
        <el-button
            type="primary"
            text
            @click="showDetail(row.id)">{{ row.productName }}
        </el-button>
      </template>
    </PIMTable>
    <StructureEdit v-if="showEdit" v-model:show-model="showEdit" :record="currentRow"/>
  </div>
</template>
<script setup>
import {ref} from "vue";
import {productModelList} from "@/api/basicData/productModel.js";
import { useRouter } from 'vue-router'
const router = useRouter()
const StructureEdit = defineAsyncComponent(() => import('@/views/productionManagement/productStructure/StructureEdit.vue'))
const tableColumn = ref([
  {
    label: "产品编码",
    prop: "productCode",
    slot: "detail"
  },
  {
    label: "产品名称",
    prop: "productName",
    dataType: 'slot',
    slot: "detail"
  },
  {
    label: "规格型号",
    prop: "model",
  },
  {
    label: "单位",
    prop: "unit",
  }
]);
const tableData = ref([]);
const tableLoading = ref(false);
const showEdit = ref(false);
const selectedRows = ref([]);
const currentRow = ref({});
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const data = reactive({
  form: {
    productName: "",
  },
  rules: {
    productName: [{required: true, message: "请输入", trigger: "blur"}],
  },
  modelForm: {
    otherModel: '',
    model: "",
    unit: "",
    speculativeTradingName: [],
  },
});
const {form, rules} = toRefs(data);
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æŸ¥è¯¢è§„格型号
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getModelList();
};
const showDetail = (id) => {
  router.push({
    path: '/productionManagement/productStructureDetail',
    query: {
      id: id
    }
  })
}
const getModelList = () => {
  tableLoading.value = true;
  productModelList({
    current: page.current,
    size: page.size,
  }).then((res) => {
    tableData.value = res.records;
    page.total = res.total;
    tableLoading.value = false;
  });
};
onMounted(() => {
  getModelList();
})
</script>
src/views/productionManagement/productionCosting/index.vue
@@ -14,6 +14,15 @@
                    clearable
                    prefix-icon="Search"
                />
                <span class="search_title ml10">合同号:</span>
                <el-input
                    v-model="searchForm.salesContractNo"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search"
                />
                <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
                >搜索</el-button
                >
@@ -61,21 +70,21 @@
        prop: "salesContractNo",
        width: 220,
    },
    {
        label: "客户合同号",
        prop: "customerContractNo",
        width: 250,
    },
    // {
    //     label: "客户合同号",
    //     prop: "customerContractNo",
    //     width: 250,
    // },
    {
        label: "客户名称",
        prop: "customerName",
        width: 250,
    },
    {
        label: "项目名称",
        prop: "projectName",
        width:300
    },
    // {
    //     label: "项目名称",
    //     prop: "projectName",
    //     width:300
    // },
    {
        label: "产品大类",
        prop: "productCategory",
@@ -121,6 +130,7 @@
const data = reactive({
    searchForm: {
        schedulingUserName: "",
        salesContractNo: "",
        entryDate: [
            dayjs().format("YYYY-MM-DD"),
            dayjs().add(1, "day").format("YYYY-MM-DD"),
src/views/productionManagement/productionDispatching/components/autoDispatchDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,153 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="自动派工"
        width="80%"
        @close="closeDia"
    >
      <el-form :model="form" label-width="140px" label-position="top" ref="formRef">
        <el-divider content-position="left">派工列表</el-divider>
        <el-table
            :data="dispatchList"
            border
            style="width: 100%; margin-top: 20px;"
            :row-class-name="tableRowClassName"
        >
          <el-table-column label="序号" type="index" width="60" align="center" />
          <el-table-column label="合同号" prop="salesContractNo" width="200" />
          <el-table-column label="客户名称" prop="customerName" width="200" />
          <!-- <el-table-column label="项目名称" prop="projectName" width="250" /> -->
          <el-table-column label="产品大类" prop="productCategory" width="150" />
          <el-table-column label="规格型号" prop="specificationModel" width="200" />
          <el-table-column label="绑定机器" prop="speculativeTradingName" width="120" />
          <el-table-column label="总数量" prop="quantity" width="100" align="right" />
          <el-table-column label="已排产" prop="schedulingNum" width="100" align="right" fixed="right" />
          <el-table-column label="待排产" prop="pendingQuantity" width="100" align="right" fixed="right" />
          <el-table-column label="本次排产" width="150" align="center" fixed="right">
            <template #default="{ row }">
              <el-input-number
                  v-model="row.schedulingNum"
                  :min="0"
                  :max="row.pendingQuantity"
                  :step="1"
                  :precision="0"
                  size="small"
                  style="width: 120px"
                  @change="(value) => changeCurrentNum(value, row)"
              />
            </template>
          </el-table-column>
        </el-table>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认派工</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, toRefs, computed} from "vue";
import {productionDispatch, productionDispatchList} from "@/api/productionManagement/productionOrder.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const data = reactive({
  form: {},
  dispatchList: [], // æ´¾å·¥åˆ—表数据
});
const { form, dispatchList } = toRefs(data);
// è¡¨æ ¼è¡Œæ ·å¼
const tableRowClassName = ({ rowIndex }) => {
  if (rowIndex % 2 === 1) {
    return 'even-row'
  }
  return ''
}
// ä¿®æ”¹æœ¬æ¬¡æŽ’产数量
const changeCurrentNum = (value, row) => {
  if (value > row.pendingQuantity) {
    row.schedulingNum = row.pendingQuantity
    proxy.$modal.msgWarning('排产数量不可大于待排产数量')
  }
}
// æ‰“开弹框
const openDialog = (type, rows) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  // å¤„理传入的数据
  dispatchList.value = rows.map(row => ({
    ...row,
    schedulingNum: 0, // åˆå§‹åŒ–本次排产数量为0
    pendingQuantity: (Number(row.quantity) || 0) - (Number(row.schedulingNum) || 0) // è®¡ç®—待排产数量
  }))
}
// æäº¤è¡¨å•
const submitForm = () => {
  // æ£€æŸ¥æ˜¯å¦æœ‰æŽ’产数据
  const hasSchedulingData = dispatchList.value.some(item => item.schedulingNum > 0)
  if (!hasSchedulingData) {
    proxy.$modal.msgWarning('请至少为一条记录设置排产数量')
    return
  }
  // æž„造提交数据 - ç›´æŽ¥ä¼ é€’数组,不过滤
  const submitData = dispatchList.value
  console.log('提交自动派工数据:', submitData)
  // è°ƒç”¨API(这里需要根据实际接口调整)
  productionDispatchList(submitData).then(res => {
    proxy.$modal.msgSuccess(res.msg);
    closeDia();
  }).catch(err => {
    proxy.$modal.msgError("派工失败");
    console.error('派工失败:', err);
  })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  dispatchList.value = []
  emit('close')
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
:deep(.even-row) {
  background-color: #fafafa;
}
:deep(.el-table .cell) {
  padding: 8px 12px;
}
:deep(.el-table th) {
  background-color: #f5f7fa;
  color: #606266;
  font-weight: 600;
}
</style>
src/views/productionManagement/productionDispatching/components/formDia.vue
@@ -7,7 +7,7 @@
        @close="closeDia"
    >
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
        <el-row :gutter="30">
        <!-- <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectName">
              <el-input v-model="form.projectName" placeholder="请输入" clearable disabled/>
@@ -16,6 +16,18 @@
          <el-col :span="12">
            <el-form-item label="产品大类:" prop="productCategory">
              <el-input v-model="form.productCategory" placeholder="请输入" clearable disabled/>
            </el-form-item>
          </el-col>
        </el-row> -->
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="specificationModel">
              <el-input v-model="form.specificationModel" placeholder="请输入" clearable disabled/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="绑定机器:" prop="speculativeTradingName">
              <el-input v-model="form.speculativeTradingName" placeholder="自动获取" clearable disabled/>
            </el-form-item>
          </el-col>
        </el-row>
@@ -46,6 +58,11 @@
                            />
                        </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品大类:" prop="productCategory">
              <el-input v-model="form.productCategory" placeholder="请输入" clearable disabled/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
                    <el-col :span="12">
@@ -54,6 +71,9 @@
                                v-model="form.schedulingUserId"
                                placeholder="选择人员"
                                style="width: 100%;"
                filterable
                default-first-option
                :reserve-keyword="false"
                            >
                                <el-option
                                    v-for="user in userList"
@@ -105,11 +125,13 @@
  form: {
        projectName: "",
        productCategory: "",
        specificationModel: "", // è§„格型号
        quantity: "",
        schedulingNum: "",
        schedulingUserId: "",
        schedulingDate: "",
        pendingQuantity: "",
        speculativeTradingName: "", // ç»‘定机器名称
  },
  rules: {
        schedulingNum: [{ required: true, message: "请输入", trigger: "blur" },],
@@ -121,6 +143,7 @@
const userList = ref([])
const userStore = useUserStore()
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
src/views/productionManagement/productionDispatching/index.vue
@@ -1,5 +1,32 @@
<template>
    <div class="app-container">
        <!-- ç‚’机1-4 å±•示(总量 / æ­£åœ¨ç”Ÿäº§é‡ / ç©ºä½™é‡ï¼‰ -->
        <div class="machines-grid">
            <div v-for="machine in machines" :key="machine.id" class="machine-card">
                <div class="machine-title">{{ machine.name }}</div>
                <div class="machine-metrics">
                <div class="machine-control">
                    <span>总量(kg):</span>
                    <el-input-number v-model="machineData[machine.name].workLoad" :min="0" :step="1" size="small" />
                </div>
                <div><span> é¢„计投入量(kg):</span><span>{{ machineData[machine.name].currentWorkLoad }}</span></div>
                <div><span>空余工作量(kg):</span><span>{{ machineData[machine.name].vacant }}</span></div>
            </div>
            </div>
            <div class="save-button-container">
                <div class="loss-rate-container">
                    <span class="loss-rate-label">损耗率(%):</span>
                    <el-select v-model="rate" placeholder="请选择损耗率" style="width: 120px" size="small">
                        <el-option label="6" :value="6" />
                        <el-option label="7" :value="7" />
                        <el-option label="8" :value="8" />
                        <el-option label="9" :value="9" />
                        <el-option label="10" :value="10" />
                    </el-select>
                </div>
                <el-button type="primary" @click="saveMachineTotals" size="small">保存设置</el-button>
            </div>
        </div>
        <div class="search_form">
            <div>
                <span class="search_title">客户名称:</span>
@@ -11,24 +38,40 @@
                    clearable
                    prefix-icon="Search"
                />
                <span class="search_title ml10">项目名称:</span>
                <span class="search_title ml10">合同号:</span>
                <el-input
                    v-model="searchForm.projectName"
                    v-model="searchForm.salesContractNo"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search"
                />
<!--                <span class="search_title ml10">项目名称:</span>-->
<!--                <el-input-->
<!--                    v-model="searchForm.projectName"-->
<!--                    style="width: 240px"-->
<!--                    placeholder="请输入"-->
<!--                    @change="handleQuery"-->
<!--                    clearable-->
<!--                    prefix-icon="Search"-->
<!--                />-->
                <span class="search_title ml10">录入日期:</span>
                <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                                                placeholder="请选择" clearable @change="changeDaterange" />
                                        placeholder="请选择" clearable @change="changeDaterange" />
                <el-checkbox
                    style="margin-left: 10px"
                    v-model="searchForm.status"
                    label="不显示待排数量为0"
                    @change="handleQuery"
                />
                <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
            </div>
            <div>
                <el-button type="primary" @click="openForm('add')">生产派工</el-button>
                <el-button @click="handleOut">导出</el-button>
            </div>
            <el-button type="primary" @click="openForm('add')">生产派工</el-button>
            <el-button type="success" @click="openAutoDispatch">自动派工</el-button>
            <el-button @click="handleOut">导出</el-button>
        </div>
        </div>
        <div class="table_list">
            <PIMTable
@@ -44,23 +87,27 @@
            ></PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
        <auto-dispatch-dia ref="autoDispatchDia" @close="handleQuery"></auto-dispatch-dia>
    </div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick, computed, watch} from "vue";
import FormDia from "@/views/productionManagement/productionDispatching/components/formDia.vue";
import AutoDispatchDia from "@/views/productionManagement/productionDispatching/components/autoDispatchDia.vue";
import dayjs from "dayjs";
import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
import {schedulingListPage, schedulingList, addSpeculatTrading, updateSpeculatTrading, getLossRate, addLossRate, updateLossRate} from "@/api/productionManagement/productionOrder.js";
import { ElMessageBox } from "element-plus";
const data = reactive({
    searchForm: {
        customerName: "",
        salesContractNo: "",
        projectName: "",
        entryDate: null, // å½•入日期
        entryDateStart: undefined,
        entryDateEnd: undefined,
        status: true,
        entryDate: [dayjs().format("YYYY-MM-DD"), dayjs().format("YYYY-MM-DD")], // å½•入日期,默认当天
        entryDateStart: dayjs().format("YYYY-MM-DD"),
        entryDateEnd: dayjs().format("YYYY-MM-DD"),
    },
});
const { searchForm } = toRefs(data);
@@ -71,19 +118,9 @@
        width: 220,
    },
    {
        label: "客户合同号",
        prop: "customerContractNo",
        width: 250,
    },
    {
        label: "客户名称",
        prop: "customerName",
        width: 250,
    },
    {
        label: "项目名称",
        prop: "projectName",
        width:300
    },
    {
        label: "产品大类",
@@ -93,7 +130,12 @@
    {
        label: "规格型号",
        prop: "specificationModel",
        width: 220,
        width: 120,
    },
    {
        label: "绑定机器",
        prop: "speculativeTradingName",
        width: 160,
    },
    {
        label: "单位",
@@ -104,6 +146,32 @@
        label: "录入日期",
        prop: "entryDate",
        width: 120,
    },
    {
        label: "状态",
        prop: "status",
        dataType: "tag",
        formatType: (params) => {
            if (params == '生产中') {
                return "warning";
            } else if (params == '未开始') {
                return "danger";
            } else {
                return "success";
            }
        },
    },
    {
        label: "生产进度",
        prop: "progress",
        formatData: (cellValue) => {
            // å¦‚果值为空或undefined,显示空字符串
            if (cellValue === null || cellValue === undefined || cellValue === '') {
                return '';
            }
            // ç›´æŽ¥åœ¨æ•°å­—后面添加百分号
            return `${cellValue}%`;
        }
    },
    {
        label: "数量",
@@ -118,6 +186,7 @@
        label: "待排数量",
        prop: "pendingQuantity",
        width: 100,
        fixed: 'right',
    },
]);
const tableData = ref([]);
@@ -129,7 +198,140 @@
    total: 0,
});
const formDia = ref()
const autoDispatchDia = ref()
const { proxy } = getCurrentInstance()
// ç‚’机数据
const machineData = reactive({
    "炒机1": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
    "炒机2": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
    "炒机3": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
    "炒机4": { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
})
// ç‚’机配置数组
const machines = [
    { id: 1, name: '炒机1' },
    { id: 2, name: '炒机2' },
    { id: 3, name: '炒机3' },
    { id: 4, name: '炒机4' }
]
// ä¿å­˜ç‚’机总量设置
const saveMachineTotals = () => {
    // éªŒè¯æŸè€—率是否已选择
    if (rate.value === null || rate.value === undefined || isNaN(rate.value)) {
        proxy.$message.warning('请选择损耗率');
        return;
    }
    // æž„造保存数据数组,使用machines数组循环构建
    const saveData = machines.map(machine => {
        const saveItem = {
            name: machine.name, // ç‚’机名称
            workLoad: machineData[machine.name].workLoad,  // æ€»é‡
            currentWorkLoad: machineData[machine.name].currentWorkLoad, //  é¢„计投入量
            vacant: machineData[machine.name].vacant   // ç©ºä½™é‡
        };
        // å¦‚果是修改操作,需要传递id字段
        if (hasQueryData.value) {
            const queryData = getMachineQueryData(machine.id);
            if (queryData && queryData.id) {
                saveItem.id = queryData.id;
            }
        }
        return saveItem;
    });
    // æž„造损耗率数据
    const rateData = {
        rate: rate.value
    };
    // å¦‚果有ID,说明是修改操作
    if (rateId.value) {
        rateData.id = rateId.value;
    }
    // æ ¹æ®æ˜¯å¦æœ‰æŸ¥è¯¢æ•°æ®å†³å®šè°ƒç”¨æ–°å¢žæŽ¥å£è¿˜æ˜¯ä¿®æ”¹æŽ¥å£
    const saveApi = hasQueryData.value ? updateSpeculatTrading : addSpeculatTrading;
    const successMessage = hasQueryData.value ? '炒机设置修改成功' : '炒机设置新增成功';
    // æ ¹æ®æ˜¯å¦æœ‰ID决定调用新增接口还是修改接口
    const rateApi = rateId.value ? updateLossRate : addLossRate;
    const rateSuccessMessage = rateId.value ? '损耗率修改成功' : '损耗率新增成功';
    // å¹¶è¡Œè°ƒç”¨ä¸¤ä¸ªæŽ¥å£
    Promise.all([
        saveApi(saveData),
        rateApi(rateData)
    ]).then(([saveRes, rateRes]) => {
        proxy.$message.success(successMessage);
        proxy.$message.success(rateSuccessMessage);
        // ä¿å­˜æˆåŠŸåŽï¼Œè®¾ç½®hasQueryData为true,下次保存将调用修改接口
        if (!hasQueryData.value) {
            hasQueryData.value = true;
        }
        // å¦‚果返回了ID,保存起来
        if (rateRes && rateRes.data && rateRes.data.id) {
            rateId.value = rateRes.data.id;
        }
        // ä¿å­˜æˆåŠŸåŽé‡æ–°è°ƒç”¨æŸ¥è¯¢é¡µé¢
        getList();
    }).catch(err => {
        proxy.$message.error('保存失败');
        console.error('保存失败:', err);
    });
}
// èŽ·å–ç‚’æœºæŸ¥è¯¢æ•°æ®
const machineQueryData = ref([]);
const getMachineQueryData = (machineId) => {
    return machineQueryData.value.find(item => item.id === machineId);
};
const getMachineIndex = (item) => {
    // å…¼å®¹å¤šç§å­—段命名,返回 1-4 ä¹‹ä¸€ï¼Œå¦åˆ™è¿”回 0(未知)
    const candidates = [item.machineId, item.machineNo, item.machine, item.deviceNo, item.deviceId]
    for (const v of candidates) {
        if (v === undefined || v === null) continue
        const n = Number(String(v).replace(/[^\d]/g, "")) // æŠ½å–æ•°å­—
        if ([1,2,3,4].includes(n)) return n
    }
    return 0
}
const computeTodaySummary = () => {
    const todayStr = dayjs().format("YYYY-MM-DD")
    // é‡ç½®æ‰€æœ‰ç‚’机数据
    machines.forEach(machine => {
        machineData[machine.name] = { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
    })
    tableData.value.forEach(item => {
        // ä»…统计当天
        const isToday = dayjs(item.entryDate).format("YYYY-MM-DD") === todayStr
        if (!isToday) return
        // ä½¿ç”¨æ­£ç¡®çš„字段名:workLoad(炒机工作量), currentWorkLoad(炒机正在工作量)
        const workLoad = Number(item.workLoad) || 0
        const currentWorkLoad = Number(item.currentWorkLoad) || 0
        const machineName = item.speculativeTradingName || '炒机1'
        if (machineData[machineName]) {
            machineData[machineName].workLoad += workLoad
            machineData[machineName].currentWorkLoad += currentWorkLoad
            machineData[machineName].vacant = machineData[machineName].workLoad - machineData[machineName].currentWorkLoad
        }
    })
}
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
@@ -137,6 +339,56 @@
    page.current = 1;
    getList();
};
// æ˜¯å¦æœ‰æŸ¥è¯¢æ•°æ®
const hasQueryData = ref(false)
// æŸè€—率
const rate = ref(6)
// æŸè€—率ID
const rateId = ref(null)
// èŽ·å–ç‚’æœºæ­£åœ¨å·¥ä½œé‡æ•°æ®
const getMachineProductionData = () => {
    schedulingList().then((res) => {
        // å¤„理炒机正在工作量数据
        if (res.data && Array.isArray(res.data)) {
            // è®¾ç½®æ˜¯å¦æœ‰æŸ¥è¯¢æ•°æ®
            hasQueryData.value = res.data.length > 0
            // ä¿å­˜æŸ¥è¯¢æ•°æ®åˆ°machineQueryData
            machineQueryData.value = res.data;
            // é‡ç½®æ‰€æœ‰ç‚’机数据
            machines.forEach(machine => {
                machineData[machine.name] = { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
            });
            // éåŽ†æ•°æ®ï¼Œæ ¹æ®æŸ¥è¯¢è¿”å›žçš„æ•°æ®ç»“æž„å¤„ç†
            res.data.forEach(item => {
                // æ ¹æ®name字段确定炒机
                const machineName = item.name || '炒机1';
                if (machineData[machineName]) {
                    // å¦‚果查询数据中有workLoad,则初始化炒机总量
                    if (item.workLoad !== null && item.workLoad !== undefined) {
                        machineData[machineName].workLoad = Number(item.workLoad) || 0;
                    }
                    // å¦‚果查询数据中有currentWorkLoad,则设置正在工作量
                    if (item.currentWorkLoad !== null && item.currentWorkLoad !== undefined) {
                        machineData[machineName].currentWorkLoad = Number(item.currentWorkLoad) || 0;
                    }
                    // è®¡ç®—空余工作量
                    machineData[machineName].vacant = machineData[machineName].workLoad - machineData[machineName].currentWorkLoad;
                }
            });
        }
    }).catch(err => {
        console.error('获取炒机正在工作量数据失败:', err);
    });
};
const changeDaterange = (value) => {
    if (value) {
        searchForm.value.entryDateStart = value[0];
@@ -165,9 +417,33 @@
            pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0)
        }));
        page.total = res.data.total;
        computeTodaySummary()
        // åŒæ—¶èŽ·å–ç‚’æœºæ­£åœ¨å·¥ä½œé‡æ•°æ®
        getMachineProductionData();
        // èŽ·å–æŸè€—çŽ‡æ•°æ®
        getLossRateData();
    }).catch(() => {
        tableLoading.value = false;
    })
};
// èŽ·å–æŸè€—çŽ‡æ•°æ®
const getLossRateData = () => {
    getLossRate().then((res) => {
        const data = res.data || res;
        if (data && data.rate !== undefined && data.rate !== null) {
            rate.value = Number(data.rate); // ç¡®ä¿è½¬æ¢ä¸ºæ•°å­—
            rateId.value = data.id || null;
        } else {
            rate.value = 6;
            rateId.value = null;
        }
    }).catch(err => {
        console.error('获取损耗率数据失败:', err);
        rate.value = 6;
        rateId.value = null;
    });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
@@ -189,6 +465,26 @@
    })
};
// æ‰“开自动派工弹框
const openAutoDispatch = () => {
    if (selectedRows.value.length === 0) {
        proxy.$message.error("请选择至少一条数据");
        return;
    }
    // è¿‡æ»¤æŽ‰å¾…排产数量为0的数据
    const validRows = selectedRows.value.filter(row => row.pendingQuantity > 0);
    if (validRows.length === 0) {
        proxy.$message.warning("选中的数据无需派工");
        return;
    }
    nextTick(() => {
        autoDispatchDia.value?.openDialog('auto', validRows)
    })
};
// å¯¼å‡º
const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
@@ -206,7 +502,129 @@
onMounted(() => {
    getList();
    getLossRateData();
});
</script>
<style scoped></style>
<style scoped>
.summary-bar{
    display: flex;
    gap: 16px;
    margin: 10px 0 16px 0;
}
.summary-item{
    background: #f5f7fa;
    border: 1px solid #ebeef5;
    border-radius: 6px;
    padding: 10px 16px;
    min-width: 160px;
}
.summary-label{
    color: #909399;
    font-size: 12px;
    margin-bottom: 6px;
}
.summary-value{
    color: #303133;
    font-size: 20px;
    font-weight: 600;
}
.summary-control{
    display: flex;
    align-items: center;
    height: 28px;
}
.machines-grid{
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 16px;
    margin-bottom: 20px;
    padding: 16px;
    background: #f8f9fa;
    border-radius: 8px;
    border: 1px solid #e9ecef;
}
.machine-card{
    border: 1px solid #dee2e6;
    border-radius: 8px;
    padding: 16px;
    background: #fff;
    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
    transition: all 0.3s ease;
}
.machine-card:hover{
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.machine-title{
    font-weight: 600;
    font-size: 16px;
    margin-bottom: 12px;
    color: #2c3e50;
    text-align: center;
    padding-bottom: 8px;
    border-bottom: 2px solid #3498db;
}
.machine-metrics{
    display: flex;
    flex-direction: column;
    gap: 10px;
    color: #495057;
}
.machine-control{
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 0;
    border-bottom: 1px solid #f1f3f4;
}
.machine-control span{
    font-size: 14px;
    white-space: nowrap;
    color: #6c757d;
    font-weight: 500;
}
.machine-metrics > div:not(.machine-control) {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 4px 0;
    font-size: 14px;
}
.machine-metrics > div:not(.machine-control) span:first-child {
    color: #6c757d;
}
.machine-metrics > div:not(.machine-control) span:last-child {
    font-weight: 600;
    color: #2c3e50;
}
.save-button-container{
    grid-column: 1 / -1;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 16px;
    margin-top: 16px;
    padding-top: 16px;
    border-top: 1px solid #e9ecef;
}
.loss-rate-container{
    display: flex;
    align-items: center;
    gap: 8px;
}
.loss-rate-label{
    font-size: 14px;
    color: #6c757d;
    font-weight: 500;
    white-space: nowrap;
}
</style>
src/views/productionManagement/productionOrder/ProcessRouteItemForm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,555 @@
<template>
  <div>
    <el-dialog v-model="isShow"
               title="工艺路线项目"
               width="800px"
               @close="closeModal">
      <div class="operate-button">
        <el-button type="primary"
                   @click="isShowProductSelectDialog = true"
                   class="mb5"
                   style="margin-bottom: 10px;">
          é€‰æ‹©äº§å“
        </el-button>
        <el-switch v-model="isTable"
                   inline-prompt
                   active-text="表格"
                   inactive-text="列表"
                   @change="handleViewChange" />
      </div>
      <el-table v-if="isTable"
                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="序号"
                         width="60">
          <template #default="scope">
            {{ scope.$index + 1 }}
          </template>
        </el-table-column>
        <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>
      <!-- ä½¿ç”¨æ™®é€šdiv替代el-steps -->
      <div v-else
           ref="stepsContainer"
           class="mb5 custom-steps"
           style="padding: 10px 0; display: flex; flex-wrap: nowrap; gap: 20px; align-items: flex-start;">
        <div v-for="(item, index) in routeItems"
             :key="item.id"
             class="custom-step draggable-step"
             :data-id="item.id"
             style="cursor: move; flex: 0 0 auto; min-width: 220px;">
          <div class="step-content">
            <div class="step-number">{{ index + 1 }}</div>
            <el-card :header="item.productName"
                     class="step-card"
                     style="cursor: move;">
              <div class="step-card-content">
                <p>{{ item.model }}</p>
                <p>{{ item.unit }}</p>
                <el-select v-model="item.processId"
                           style="width: 100%;"
                           @mousedown.stop>
                  <el-option v-for="process in processOptions"
                             :key="process.id"
                             :label="process.name"
                             :value="process.id" />
                </el-select>
              </div>
              <template #footer>
                <div class="step-card-footer">
                  <el-button type="danger"
                             link
                             size="small"
                             @click.stop="removeItemByID(item.id)">删除</el-button>
                </div>
              </template>
            </el-card>
          </div>
        </div>
      </div>
      <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 {
    findProductProcessRouteItemList,
    addOrUpdateProductProcessRouteItem,
    deleteRouteItem,
  } from "@/api/productionManagement/productProcessRoute.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 tableSortable = null;
  let stepsSortable = null;
  const multipleTable = ref(null);
  const stepsContainer = ref(null);
  const isTable = ref(true);
  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 => {
            console.log(row.id, "删除");
            const dragSortx = routeItems.value.findIndex(
              item => item.dragSort === row.dragSort
            );
            const idx = routeItems.value.findIndex(item => item.id === row.id);
            console.log(idx, "idx");
            if (row.id) {
              deleteRouteItemByIds({ id: row.id }, idx);
            } else {
              removeItem(dragSortx);
            }
          },
        },
      ],
    },
  ]);
  const removeItem = index => {
    console.log("软删除", index);
    routeItems.value.splice(index, 1);
    updateDragSort();
    nextTick(() => initSortable());
  };
  const removeItemByID = id => {
    const idx = routeItems.value.findIndex(item => item.id === id);
    if (idx > -1) {
      routeItems.value.splice(idx, 1);
      updateDragSort();
      nextTick(() => initSortable());
    }
  };
  const deleteRouteItemByIds = (ids, index) => {
    deleteRouteItem(ids).then(res => {
      routeItems.value.splice(index, 1);
      updateDragSort();
      nextTick(() => initSortable());
    });
  };
  const closeModal = () => {
    isShow.value = false;
  };
  const updateDragSort = () => {
    routeItems.value.forEach((item, index) => {
      item.dragSort = index + 1;
    });
    routeItems.value = [...routeItems.value];
    console.log("更新后的数组:", routeItems.value);
  };
  const handelSelectProducts = products => {
    destroySortable();
    // è®¡ç®—æ–°çš„dragSort值起始点
    const maxDragSort =
      routeItems.value.length > 0
        ? Math.max(...routeItems.value.map(item => item.dragSort || 0))
        : 0;
    const newData = products.map(({ id, ...product }, index) => ({
      ...product,
      productModelId: id,
      routeId: props.record.id,
      // id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
      processId: undefined,
      dragSort: maxDragSort + index + 1,
    }));
    console.log("选择产品前数组:", routeItems.value);
    routeItems.value.push(...newData);
    updateDragSort();
    console.log("选择产品后数组:", routeItems.value);
    // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
    nextTick(() => {
      // å¼ºåˆ¶é‡æ–°æ¸²æŸ“组件
      if (proxy?.$forceUpdate) {
        proxy.$forceUpdate();
      }
      const temp = [...routeItems.value];
      routeItems.value = [];
      nextTick(() => {
        routeItems.value = temp;
        initSortable();
      });
    });
  };
  const findProcessRouteItems = () => {
    tableLoading.value = true;
    findProductProcessRouteItemList({ orderId: props.record.id })
      .then(res => {
        tableLoading.value = false;
        routeItems.value = res.data.map(item => ({
          ...item,
          processId: item.processId === 0 ? undefined : item.processId,
        }));
        // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
        nextTick(() => {
          setTimeout(() => initSortable(), 100);
        });
      })
      .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 = () => {
    const hasEmptyProcess = routeItems.value.some(item => !item.processId);
    if (hasEmptyProcess) {
      proxy?.$modal?.msgError("请为所有项目选择工序");
      return;
    }
    addOrUpdateProductProcessRouteItem({
      routeId: props.record.id,
      processRouteItem: routeItems.value,
    })
      .then(res => {
        isShow.value = false;
        emit("completed");
        proxy?.$modal?.msgSuccess("提交成功");
      })
      .catch(err => {
        proxy?.$modal?.msgError(`提交失败:${err.msg || "网络异常"}`);
      });
  };
  const destroySortable = () => {
    if (tableSortable) {
      tableSortable.destroy();
      tableSortable = null;
    }
    if (stepsSortable) {
      stepsSortable.destroy();
      stepsSortable = null;
    }
  };
  const initSortable = () => {
    destroySortable();
    if (isTable.value) {
      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;
      tableSortable = new Sortable(tbody, {
        animation: 150,
        ghostClass: "sortable-ghost",
        handle: ".el-table__row",
        filter: ".el-button, .el-select",
        onEnd: evt => {
          if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex])
            return;
          // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序,与表格模式保持一致
          const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
          routeItems.value.splice(evt.newIndex, 0, moveItem);
          updateDragSort();
          console.log("排序后数组:", routeItems.value);
        },
      });
    } else {
      if (!stepsContainer.value) return;
      // ä¿®æ”¹ï¼šç›´æŽ¥ä½¿ç”¨stepsContainer.value作为拖拽容器
      const stepsList = stepsContainer.value;
      if (!stepsList) {
        console.warn("未找到步骤条拖拽容器");
        return;
      }
      // ä¿®æ”¹ï¼šç®€åŒ–拖拽配置
      stepsSortable = new Sortable(stepsList, {
        animation: 150,
        ghostClass: "sortable-ghost",
        draggable: ".draggable-step", // å¯æ‹–拽元素
        handle: ".draggable-step, .step-card", // æ‹–拽手柄
        filter: ".el-button, .el-select, .el-input", // è¿‡æ»¤æŒ‰é’®/选择器
        forceFallback: true,
        fallbackClass: "sortable-fallback",
        preventOnFilter: true,
        scroll: true,
        scrollSensitivity: 30,
        scrollSpeed: 10,
        bubbleScroll: true,
        onEnd: evt => {
          if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex])
            return;
          // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序
          const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
          routeItems.value.splice(evt.newIndex, 0, moveItem);
          updateDragSort();
        },
      });
      // è°ƒè¯•:打印容器和实例,确认绑定成功
      console.log("步骤条拖拽容器:", stepsList);
      console.log("Sortable实例:", stepsSortable);
    }
  };
  const handleViewChange = () => {
    destroySortable();
    // å»¶è¿Ÿåˆå§‹åŒ–,确保视图切换后DOM完全渲染
    nextTick(() => {
      setTimeout(() => initSortable(), 100);
    });
  };
  onMounted(() => {
    findProcessRouteItems();
    findProcessList();
  });
  onUnmounted(() => {
    destroySortable();
  });
  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;
  }
  :deep(.el-card__footer) {
    padding: 0 !important;
  }
  .operate-button {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  /* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤æ¡å®¹å™¨æ ·å¼ */
  .custom-steps {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 20px;
    min-height: 100px;
  }
  /* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤é¡¹æ ·å¼ */
  .custom-step {
    cursor: move !important;
    padding: 8px;
    position: relative;
    transition: all 0.2s ease;
    flex: 0 0 auto;
    min-width: 220px;
    touch-action: none;
  }
  /* æ‹–拽悬浮样式,提示可拖拽 */
  .custom-step:hover {
    background-color: rgba(64, 158, 255, 0.05);
    transform: translateY(-2px);
  }
  .sortable-ghost {
    opacity: 0.4;
    background-color: #f5f7fa !important;
    border: 2px dashed #409eff;
    margin: 10px;
    transform: scale(1.02);
  }
  .sortable-fallback {
    opacity: 0.9;
    background-color: #f5f7fa;
    border: 1px solid #409eff;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transform: rotate(2deg);
    margin: 10px;
  }
  .step-card {
    cursor: move !important;
    transition: box-shadow 0.2s ease;
    user-select: none;
    -webkit-user-select: none;
    pointer-events: auto;
    margin: 10px;
    height: 240px;
  }
  .step-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
  .step-content {
    width: 220px;
    user-select: none;
  }
  .step-card-content {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .step-card-footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    padding: 10px;
  }
  /* è‡ªå®šä¹‰åºå·æ ·å¼ä¼˜åŒ– */
  .step-number {
    font-weight: bold;
    text-align: center;
    width: 36px;
    height: 36px;
    line-height: 36px;
    margin: 0 auto 10px;
    background: #409eff;
    color: #fff;
    border-radius: 50%;
    font-size: 14px;
  }
</style>
src/views/productionManagement/productionOrder/index.vue
@@ -1,198 +1,193 @@
<template>
    <div class="app-container">
        <div class="search_form">
            <div>
                <span class="search_title">客户名称:</span>
                <el-input
                    v-model="searchForm.customerName"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search"
                />
                <span class="search_title ml10">项目名称:</span>
                <el-input
                    v-model="searchForm.projectName"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search"
                />
                <span class="search_title ml10">录入日期:</span>
                <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                                                placeholder="请选择" clearable @change="changeDaterange" />
                <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
                >搜索</el-button
                >
            </div>
            <div>
                <el-button @click="handleOut">导出</el-button>
            </div>
        </div>
        <div class="table_list">
            <PIMTable
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination"
            ></PIMTable>
        </div>
    </div>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="客户名称:">
          <el-input v-model="searchForm.customerName"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="合同号:">
          <el-input v-model="searchForm.salesContractNo"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="产品名称:">
          <el-input v-model="searchForm.productCategory"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="规格:">
          <el-input v-model="searchForm.specificationModel"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
        </el-form-item>
      </el-form>
      <div>
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
    </div>
    <process-route-item-form v-if="isShowItemModal"
                             v-model:visible="isShowItemModal"
                             :record="record"
                             @completed="getList" />
  </div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import { ElMessageBox } from "element-plus";
import dayjs from "dayjs";
import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
const { proxy } = getCurrentInstance();
  import { onMounted, ref } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
  const { proxy } = getCurrentInstance();
  import ProcessRouteItemForm from "@/views/productionManagement/productionOrder/ProcessRouteItemForm.vue";
const tableColumn = ref([
    {
        label: "录入日期",
        prop: "entryDate",
        width: 120,
    },
    {
        label: "合同号",
        prop: "salesContractNo",
        width: 220,
    },
    {
        label: "客户合同号",
        prop: "customerContractNo",
        width: 250,
    },
    {
        label: "客户名称",
        prop: "customerName",
        width: 250,
    },
    {
        label: "项目名称",
        prop: "projectName",
        width:300
    },
    {
        label: "付款状态",
        prop: "status",
        dataType: "tag",
        formatType: (params) => {
            if (params == '未完成') {
                return "danger";
            } else if (params == '已完成') {
                return "success";
            } else {
                return null;
            }
        },
    },
    {
        label: "产品大类",
        prop: "productCategory",
        width: 160,
    },
    {
        label: "规格型号",
        prop: "specificationModel",
        width: 220,
    },
    {
        label: "单位",
        prop: "unit",
        width:90
    },
    {
        label: "数量",
        prop: "quantity",
    },
    {
        label: "排产数量",
        prop: "schedulingNum",
        width: 100,
    },
    {
        label: "完工数量",
        prop: "successNum",
        width: 100,
    },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const page = reactive({
    current: 1,
    size: 100,
    total: 0,
});
  const tableColumn = ref([
    {
      label: "生产订单号",
      prop: "npsNo",
    },
    {
      label: "销售合同号",
      prop: "salesContractNo",
    },
    {
      label: "客户名称",
      prop: "customerName",
    },
    {
      label: "产品名称",
      prop: "productCategory",
    },
    {
      label: "规格",
      prop: "specificationModel",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 200,
      operation: [
        {
          name: "工艺路线",
          type: "text",
          clickFun: row => {
            showRouteItemModal(row);
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
const data = reactive({
    searchForm: {
        customerName: "",
        projectName: "",
        entryDate: null, // å½•入日期
        entryDateStart: undefined,
        entryDateEnd: undefined,
    },
});
const { searchForm } = toRefs(data);
  const data = reactive({
    searchForm: {
      customerName: "",
      salesContractNo: "",
      projectName: "",
      productCategory: "",
      specificationModel: "",
    },
  });
  const { searchForm } = toRefs(data);
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
    page.current = 1;
    getList();
};
const pagination = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const changeDaterange = (value) => {
    if (value) {
        searchForm.value.entryDateStart = value[0];
        searchForm.value.entryDateEnd = value[1];
    } else {
        searchForm.value.entryDateStart = undefined;
        searchForm.value.entryDateEnd = undefined;
    }
    handleQuery();
};
const getList = () => {
    tableLoading.value = true;
    // æž„造一个新的对象,不包含entryDate字段
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined
    schedulingListPage(params).then((res) => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
    }).catch(() => {
        tableLoading.value = false;
    })
};
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const changeDaterange = value => {
    if (value) {
      searchForm.value.entryDateStart = value[0];
      searchForm.value.entryDateEnd = value[1];
    } else {
      searchForm.value.entryDateStart = undefined;
      searchForm.value.entryDateEnd = undefined;
    }
    handleQuery();
  };
  const getList = () => {
    tableLoading.value = true;
    // æž„造一个新的对象,不包含entryDate字段
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined;
    productOrderListPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
// å¯¼å‡º
const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            proxy.download("/salesLedger/scheduling/export", {}, "生产订单.xlsx");
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
  const isShowItemModal = ref(false);
  const record = ref({});
  const showRouteItemModal = row => {
    isShowItemModal.value = true;
    record.value = row;
  };
onMounted(() => {
    getList();
});
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/salesLedger/scheduling/export", {}, "生产订单.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const handleConfirmRoute = () => {};
  onMounted(() => {
    getList();
  });
</script>
<style scoped lang="scss"></style>
src/views/productionManagement/productionProcess/Edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="编辑工序"
        width="400"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="工序名称:"
            prop="name"
            :rules="[
                {
                required: true,
                message: '请输入工序名称',
              },
              {
                max: 100,
                message: '最多100个字符',
              }
            ]">
          <el-input v-model="formState.name" />
        </el-form-item>
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formState.remark" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from "vue";
import {update} from "@/api/productionManagement/productionProcess.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
  record: {
    type: Object,
    required: true,
  }
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  id: props.record.id,
  name: props.record.name,
  remark: props.record.remark,
  salaryQuota: props.record.salaryQuota,
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
let { proxy } = getCurrentInstance()
const closeModal = () => {
  isShow.value = false;
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      update(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
    }
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
</script>
src/views/productionManagement/productionProcess/New.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="新增工序"
        width="400"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="工序名称:"
            prop="name"
            :rules="[
                {
                required: true,
                message: '请输入工序名称',
              },
              {
                max: 100,
                message: '最多100个字符',
              }
            ]">
          <el-input v-model="formState.name" />
        </el-form-item>
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formState.remark" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from "vue";
import {add} from "@/api/productionManagement/productionProcess.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  name: '',
  remark: '',
  salaryQuota:  '',
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
let { proxy } = getCurrentInstance()
const closeModal = () => {
  isShow.value = false;
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      add(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
    }
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
</script>
src/views/productionManagement/productionProcess/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,218 @@
<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-input v-model="searchForm.no"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </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"
                   @click="handleDelete"
                   :disabled="selectedRows.length === 0"
                   plain>删除工序</el-button>
      </div>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total"></PIMTable>
    </div>
    <new-process v-if="isShowNewModal"
                 v-model:visible="isShowNewModal"
                 @completed="getList" />
    <edit-process v-if="isShowEditModal"
                  v-model:visible="isShowEditModal"
                  :record="record"
                  @completed="getList" />
  </div>
</template>
<script setup>
  import { onMounted, ref } from "vue";
  import NewProcess from "@/views/productionManagement/productionProcess/New.vue";
  import EditProcess from "@/views/productionManagement/productionProcess/Edit.vue";
  import { listPage, del } from "@/api/productionManagement/productionProcess.js";
  const data = reactive({
    searchForm: {
      name: "",
      no: "",
    },
  });
  const { searchForm } = toRefs(data);
  const tableColumn = ref([
    {
      label: "工序名称",
      prop: "name",
    },
    {
      label: "工序编号",
      prop: "no",
    },
    {
      label: "工资定额",
      prop: "salaryQuota",
    },
    {
      label: "备注",
      prop: "remark",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 280,
      operation: [
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            showEditModal(row);
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const isShowNewModal = ref(false);
  const isShowEditModal = ref(false);
  const record = ref({});
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const { proxy } = getCurrentInstance();
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  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.value, ...page };
    params.entryDate = undefined;
    listPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records.map(item => ({
          ...item,
        }));
        page.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // æ‰“开新增弹框
  const showNewModal = () => {
    isShowNewModal.value = true;
  };
  const showEditModal = row => {
    isShowEditModal.value = true;
    record.value = row;
  };
  // åˆ é™¤
  function handleDelete() {
    const no = selectedRows.value.map(item => item.no);
    const ids = selectedRows.value.map(item => item.id);
    if (no.length > 2) {
      proxy.$modal
        .confirm(
          '是否确认删除工序编号为"' +
            no[0] +
            "、" +
            no[1] +
            '"等' +
            no.length +
            "条数据项?"
        )
        .then(function () {
          return del(ids);
        })
        .then(() => {
          getList();
          proxy.$modal.msgSuccess("删除成功");
        })
        .catch(() => {});
    } else {
      proxy.$modal
        .confirm('是否确认删除工序编号为"' + no + '"的数据项?')
        .then(function () {
          return del(ids);
        })
        .then(() => {
          getList();
          proxy.$modal.msgSuccess("删除成功");
        })
        .catch(() => {});
    }
  }
  // å¯¼å‡º
  // const handleOut = () => {
  //     ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
  //         confirmButtonText: "确认",
  //         cancelButtonText: "取消",
  //         type: "warning",
  //     })
  //         .then(() => {
  //             proxy.download("/salesLedger/scheduling/exportTwo", {}, "工序排产.xlsx");
  //         })
  //         .catch(() => {
  //             proxy.$modal.msg("已取消");
  //         });
  // };
  onMounted(() => {
    getList();
  });
</script>
<style scoped></style>
src/views/productionManagement/productionReporting/Input.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="投入"
        @close="closeModal"
    >
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="data"
          :page="page"
          :tableLoading="tableLoading"
          @pagination="pagination"
      ></PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="closeModal">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, computed, onMounted} from "vue";
import { productionProductInputListPage } from "@/api/productionManagement/productionProductInput";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
  productionProductMainId: {
    type: Number,
    required: true,
  },
});
const emit = defineEmits(['update:visible', 'completed']);
const page = reactive({
  current: 1,
  size: 100,
  total: 0
});
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  fetchData();
};
const tableLoading = ref(false);
const tableColumn = [
  {
    label: '报工单号',
    prop: 'productNo',
  },
  {
    label: '产品型号',
    prop: 'model',
  },
  {
    label: '投入数量',
    prop: 'quantity',
  },
]
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
const data = ref([])
const closeModal = () => {
  isShow.value = false;
};
const fetchData = () => {
  tableLoading.value = true;
  const params = { productMainId: props.productionProductMainId, ...page };
  productionProductInputListPage(params).then(res => {
    tableLoading.value = false;
    data.value = res.data.records;
    page.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
defineExpose({
  closeModal,
  isShow,
});
onMounted(() => {
  fetchData()
})
</script>
src/views/productionManagement/productionReporting/Output.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
<template>
  <div>
    <el-dialog v-model="isShow"
               title="产出"
               @close="closeModal">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="data"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="closeModal">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { productionProductOutputListPage } from "@/api/productionManagement/productionProductOutput.js";
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
    productionProductMainId: {
      type: Number,
      required: true,
    },
  });
  const emit = defineEmits(["update:visible", "completed"]);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    fetchData();
  };
  const tableLoading = ref(false);
  const tableColumn = [
    {
      label: "报工单号",
      prop: "productNo",
    },
    {
      label: "产品型号",
      prop: "model",
    },
    {
      label: "产出数量",
      prop: "quantity",
    },
  ];
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const data = ref([]);
  const closeModal = () => {
    isShow.value = false;
  };
  const fetchData = () => {
    tableLoading.value = true;
    const params = { productMainId: props.productionProductMainId, ...page };
    productionProductOutputListPage(params)
      .then(res => {
        tableLoading.value = false;
        data.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  defineExpose({
    closeModal,
    isShow,
  });
  onMounted(() => {
    fetchData();
  });
</script>
src/views/productionManagement/productionReporting/components/formDia.vue
@@ -13,8 +13,15 @@
              <el-input v-model="form.schedulingNum" placeholder="请输入" clearable disabled/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="本次生产数量:" prop="finishedNum">
                    <el-col :span="12">
                        <el-form-item label="待生产数量:" prop="pendingNum">
                            <el-input v-model="form.pendingNum" placeholder="请输入" clearable disabled/>
                        </el-form-item>
                    </el-col>
        </el-row>
        <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="本次生产数量:" prop="finishedNum">
                            <el-input-number
                                v-model="form.finishedNum"
                                placeholder="请输入"
@@ -25,13 +32,18 @@
                                style="width: 100%"
                                @change="changeNum"
                            />
                        </el-form-item>
                    </el-col>
          <el-col :span="12">
            <el-form-item label="单价(元):" prop="unitPrice">
              <el-input v-model="form.unitPrice" placeholder="请输入" clearable @input="calculateTotalPrice"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="待生产数量:" prop="pendingNum">
              <el-input v-model="form.pendingNum" placeholder="请输入" clearable disabled/>
            <el-form-item label="总价(元):" prop="totalPrice">
              <el-input v-model="form.totalPrice" placeholder="请输入" clearable disabled/>
            </el-form-item>
          </el-col>
        </el-row>
@@ -42,6 +54,9 @@
                                v-model="form.schedulingUserId"
                                placeholder="选择人员"
                                style="width: 100%;"
                filterable
                default-first-option
                :reserve-keyword="false"
                            >
                                <el-option
                                    v-for="user in userList"
@@ -95,6 +110,8 @@
        finishedNum: "",
        schedulingUserId: "",
        schedulingDate: "",
        unitPrice: "",
        totalPrice: "",
  },
  rules: {
        schedulingNum: [{ required: true, message: "请输入", trigger: "blur" },],
@@ -118,6 +135,19 @@
        proxy.$modal.msgWarning('本次生产数量不可大于排产数量')
    }
    form.value.pendingNum = form.value.schedulingNum - form.value.finishedNum;
    calculateTotalPrice();
}
// è®¡ç®—总价
const calculateTotalPrice = () => {
    const quantity = Number(form.value.finishedNum ?? 0);
    const unitPrice = Number(form.value.unitPrice ?? 0);
    if (quantity > 0 && unitPrice > 0) {
        form.value.totalPrice = (quantity * unitPrice).toFixed(2);
    } else {
        form.value.totalPrice = '0.00';
    }
}
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
src/views/productionManagement/productionReporting/index.vue
@@ -1,427 +1,481 @@
<template>
    <div class="app-container">
        <div class="search_form">
            <el-form :model="searchForm" :inline="true">
                <el-form-item label="客户名称:">
                    <el-input v-model="searchForm.customerName" placeholder="请输入" clearable prefix-icon="Search"
                                        style="width: 200px;"
                                        @change="handleQuery" />
                </el-form-item>
                <el-form-item label="项目名称:">
                    <el-input v-model="searchForm.projectName" placeholder="请输入" clearable prefix-icon="Search"
                                        style="width: 200px;"
                                        @change="handleQuery" />
                </el-form-item>
                <el-form-item label="排产日期:">
                    <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                                                    placeholder="请选择" clearable @change="changeDaterange" />
                </el-form-item>
                <el-form-item label="状态:">
                    <el-select v-model="searchForm.status" placeholder="请选择状态" style="width: 140px" clearable>
                        <el-option label="待生产" :value="1"></el-option>
                        <el-option label="已报工" :value="3"></el-option>
                        <el-option label="生产中" :value="2"></el-option>
                    </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="openForm('add')">生产报工</el-button>
                <el-button @click="handleOut">导出</el-button>
            </div>
            <PIMTable
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                :expandRowKeys="expandedRowKeys"
                @expand-change="expandChange"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total"
            >
                <template #expand="{ row }">
                    <el-table
                        :data="expandData"
                        border
                        show-summary
                        :summary-method="summarizeMainTable"
                        v-loading="childrenLoading"
                    >
                        <el-table-column
                            align="center"
                            label="序号"
                            type="index"
                            width="60"
                        />
                        <el-table-column label="本次生产数量" prop="finishedNum" align="center" width="400">
                            <template #default="scope">
                                <el-input-number :step="0.01" :min="0" style="width: 100%"
                                                                 v-model="scope.row.finishedNum"
                                                                 :disabled="!scope.row.editType"
                                                                 :precision="2"
                                                                 placeholder="请输入"
                                                                 clearable
                                                                 @change="changeNum(scope.row)"
                                />
                            </template>
                        </el-table-column>
<!--                        <el-table-column label="待生产数量" prop="pendingNum" width="240" align="center"></el-table-column>-->
                        <el-table-column label="生产人" prop="schedulingUserId" width="400">
                            <template #default="scope">
                                <el-select
                                    v-model="scope.row.schedulingUserId"
                                    placeholder="选择人员"
                                    :disabled="!scope.row.editType"
                                    style="width: 100%;"
                                >
                                    <el-option
                                        v-for="user in userList"
                                        :key="user.userId"
                                        :label="user.nickName"
                                        :value="user.userId"
                                    />
                                </el-select>
                            </template>
                        </el-table-column>
                        <el-table-column label="生产日期" prop="schedulingDate" width="400">
                            <template #default="scope">
                                <el-date-picker
                                    v-model="scope.row.schedulingDate"
                                    type="date"
                                    :disabled="!scope.row.editType"
                                    placeholder="请选择日期"
                                    value-format="YYYY-MM-DD"
                                    format="YYYY-MM-DD"
                                    clearable
                                    style="width: 100%"
                                />
                            </template>
                        </el-table-column>
                        <el-table-column label="操作" width="60">
                            <template #default="scope">
                                <el-button
                                    link
                                    type="primary"
                                    size="small"
                                    @click="changeEditType(scope.row)"
                                    v-if="!scope.row.editType"
                                >编辑</el-button
                                >
                                <el-button
                                    link
                                    type="primary"
                                    size="small"
                                    @click="saveReceiptPayment(scope.row)"
                                    v-if="scope.row.editType"
                                >保存</el-button
                                >
                            </template>
                        </el-table-column>
                    </el-table>
                </template>
            </PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
    </div>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="报工人员名称:">
          <el-input v-model="searchForm.nickName"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="工单号:">
          <el-input v-model="searchForm.workOrderNo"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="工单状态:">
          <el-select v-model="searchForm.workOrderStatus"
                     placeholder="请选择工单状态"
                     style="width: 140px"
                     clearable>
            <el-option label="待确认"
                       :value="1"></el-option>
            <el-option label="待生产"
                       :value="2"></el-option>
            <el-option label="生产中"
                       :value="3"></el-option>
            <el-option label="已生产"
                       :value="4"></el-option>
          </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="openForm('add')">生产报工</el-button> -->
        <el-button @click="handleOut">导出</el-button>
      </div>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                :expandRowKeys="expandedRowKeys"
                @expand-change="expandChange"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total">
        <template #expand="{ row }">
          <el-table :data="expandData"
                    border
                    show-summary
                    :summary-method="summarizeMainTable"
                    v-loading="childrenLoading">
            <el-table-column align="center"
                             label="序号"
                             type="index"
                             width="60" />
            <el-table-column label="本次生产数量"
                             prop="finishedNum"
                             align="center"
                             width="400">
              <template #default="scope">
                <el-input-number :step="0.01"
                                 :min="0"
                                 style="width: 100%"
                                 v-model="scope.row.finishedNum"
                                 :disabled="!scope.row.editType"
                                 :precision="2"
                                 placeholder="请输入"
                                 clearable
                                 @change="changeNum(scope.row)" />
              </template>
            </el-table-column>
            <!--                        <el-table-column label="待生产数量" prop="pendingNum" width="240" align="center"></el-table-column>-->
            <el-table-column label="生产人"
                             prop="schedulingUserId"
                             width="400">
              <template #default="scope">
                <el-select v-model="scope.row.schedulingUserId"
                           placeholder="选择人员"
                           :disabled="!scope.row.editType"
                           style="width: 100%;">
                  <el-option v-for="user in userList"
                             :key="user.userId"
                             :label="user.nickName"
                             :value="user.userId" />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="生产日期"
                             prop="schedulingDate"
                             width="400">
              <template #default="scope">
                <el-date-picker v-model="scope.row.schedulingDate"
                                type="date"
                                :disabled="!scope.row.editType"
                                placeholder="请选择日期"
                                value-format="YYYY-MM-DD"
                                format="YYYY-MM-DD"
                                clearable
                                style="width: 100%" />
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             width="60">
              <template #default="scope">
                <el-button link
                           type="primary"
                           size="small"
                           @click="changeEditType(scope.row)"
                           v-if="!scope.row.editType"
                           :disabled="scope.row.parentStatus === 3">编辑</el-button>
                <el-button link
                           type="primary"
                           size="small"
                           @click="saveReceiptPayment(scope.row)"
                           v-if="scope.row.editType">保存</el-button>
              </template>
            </el-table-column>
          </el-table>
        </template>
      </PIMTable>
    </div>
    <form-dia ref="formDia"
              @close="handleQuery"></form-dia>
    <input-modal v-if="isShowInput"
                 v-model:visible="isShowInput"
                 :production-product-main-id="isShowingId" />
    <output-modal v-if="isShowOutput"
                  v-model:visible="isShowOutput"
                  :production-product-main-id="isShowingId" />
  </div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import FormDia from "@/views/productionManagement/productionReporting/components/formDia.vue";
import {staffJoinDel, staffJoinListPage} from "@/api/personnelManagement/onboarding.js";
import {ElMessageBox} from "element-plus";
import dayjs from "dayjs";
import {
    productionReportUpdate,
    workListPage,
    workListPageById
} from "@/api/productionManagement/productionReporting.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
  import { onMounted, ref } from "vue";
  import FormDia from "@/views/productionManagement/productionReporting/components/formDia.vue";
  import { ElMessageBox } from "element-plus";
  import {
    productionReportUpdate,
    workListPageById,
    productionReportDelete,
  } from "@/api/productionManagement/productionReporting.js";
  import { productionProductMainListPage } from "@/api/productionManagement/productionProductMain.js";
  import { userListNoPageByTenantId } from "@/api/system/user.js";
  import InputModal from "@/views/productionManagement/productionReporting/Input.vue";
  import OutputModal from "@/views/productionManagement/productionReporting/Output.vue";
const data = reactive({
    searchForm: {
        staffName: "",
        entryDate: null, // å½•入日期
        entryDateStart: undefined,
        entryDateEnd: undefined,
    },
});
const { searchForm } = toRefs(data);
const expandedRowKeys = ref([]);
const expandData = ref([]);
const userList = ref([])
const tableColumn = ref([
    {
        type: "expand",
        dataType: "slot",
        slot: "expand",
    },
    {
        label: "状态",
        prop: "status",
        dataType: "tag",
        formatData: (params) => {
            if (params == 3) {
                return "已报工";
            } else if (params == 1) {
                return "待生产";
            } else {
                return '生产中';
            }
        },
        formatType: (params) => {
            if (params == 3) {
                return "success";
            } else if (params == 1) {
                return "primary";
            } else {
                return 'warning';
            }
        },
    },
    {
        label: "排产日期",
        prop: "schedulingDate",
        width: 120,
    },
    {
        label: "排产人",
        prop: "schedulingUserName",
    },
    {
        label: "合同号",
        prop: "salesContractNo",
        width: 200,
    },
    {
        label: "客户合同号",
        prop: "customerContractNo",
        width: 200,
    },
    {
        label: "客户名称",
        prop: "customerName",
        width: 200,
    },
    {
        label: "项目名称",
        prop: "projectName",
        width:300
    },
    {
        label: "产品大类",
        prop: "productCategory",
        width: 150,
    },
    {
        label: "规格型号",
        prop: "specificationModel",
        width: 150,
    },
    {
        label: "单位",
        prop: "unit",
    },
    {
        label: "工序",
        prop: "process",
    },
    {
        label: "排产数量",
        prop: "schedulingNum",
        width: 100,
    },
    {
        label: "生产数量",
        prop: "finishedNum",
        width: 100,
    },
    {
        label: "待生产数量",
        prop: "pendingFinishNum",
        width: 100,
    },
]);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const childrenLoading = ref(false);
const page = reactive({
    current: 1,
    size: 100,
    total: 0,
});
const formDia = ref()
const { proxy } = getCurrentInstance()
  const data = reactive({
    searchForm: {
      nickName: "",
      workOrderNo: "",
      workOrderStatus: "",
    },
  });
  const { searchForm } = toRefs(data);
  const expandedRowKeys = ref([]);
  const expandData = ref([]);
  const userList = ref([]);
  const tableColumn = ref([
    {
      label: "报工单号",
      prop: "productNo",
      width: 120,
    },
    {
      label: "报工人员",
      prop: "nickName",
      width: 120,
    },
    {
      label: "工单编号",
      prop: "workOrderNo",
      width: 120,
    },
    {
      label: "报工状态",
      prop: "status",
      dataType: "tag",
      formatData: params => {
        if (params == 3) {
          return "已报工";
        } else if (params == 1) {
          return "待生产";
        } else {
          return "生产中";
        }
      },
      formatType: params => {
        if (params == 3) {
          return "success";
        } else if (params == 1) {
          return "primary";
        } else {
          return "warning";
        }
      },
    },
    {
      label: "工单状态",
      prop: "workOrderStatus",
      dataType: "tag",
      formatData: params => {
        switch (params) {
          case "1":
            return "待确认";
          case "2":
            return "待生产";
          case "3":
            return "生产中";
          case "4":
            return "已生产";
          default:
            return "";
        }
      },
      formatType: params => {
        switch (params) {
          case "1":
            return "primary";
          case "2":
            return "info";
          case "3":
            return "warning";
          case "4":
            return "success";
          default:
            return "";
        }
      },
    },
    {
      label: "生产时间",
      prop: "createTime",
      width: 120,
      formatData: params => {
        const date = new Date(params);
        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
          2,
          "0"
        )}-${String(date.getDate()).padStart(2, "0")}`;
      },
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 230,
      operation: [
        {
          name: "查看投入",
          type: "text",
          clickFun: row => {
            showInput(row);
          },
        },
        {
          name: "查看产出",
          type: "text",
          clickFun: row => {
            showOutput(row);
          },
        },
        {
          name: "删除",
          type: "danger",
          clickFun: row => {
            deleteReport(row);
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const childrenLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const formDia = ref();
  const { proxy } = getCurrentInstance();
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
    page.current = 1;
    getList();
};
const changeDaterange = (value) => {
    if (value) {
        searchForm.value.entryDateStart = value[0];
        searchForm.value.entryDateEnd = value[1];
    } else {
        searchForm.value.entryDateStart = undefined;
        searchForm.value.entryDateEnd = undefined;
    }
    handleQuery();
};
const pagination = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined
    expandedRowKeys.value = []
    workListPage(params).then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records.map(item => ({
            ...item,
            pendingFinishNum: (Number(item.schedulingNum) || 0) - (Number(item.finishedNum) || 0)
        }));
        page.total = res.data.total;
    }).catch(err => {
        tableLoading.value = false;
    })
};
// å±•开行
const expandChange = (row, expandedRows) => {
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    if (expandedRows.length > 0) {
        nextTick(() => {
            expandedRowKeys.value = [];
            try {
                childrenLoading.value = true;
                workListPageById({ id: row.id }).then((res) => {
                    childrenLoading.value = false;
                    const index = tableData.value.findIndex((item) => item.id === row.id);
                    if (index > -1) {
                        expandData.value = res.data.map(item => ({
                            ...item,
                            pendingNum: (Number(item.schedulingNum) || 0) - (Number(item.finishedNum) || 0),
                            parentStatus: row.status // æ–°å¢žçˆ¶è¡¨çŠ¶æ€
                        }));
                    }
                    expandedRowKeys.value.push(row.id);
                });
            } catch (error) {
                childrenLoading.value = false;
                console.log(error);
            }
        })
    } else {
        expandedRowKeys.value = [];
    }
};
const changeNum = (row) => {
    // æ‰¾åˆ°çˆ¶è¡¨æ ¼æ•°æ®
    const parentRow = tableData.value.find(item => item.id === expandedRowKeys.value[0]);
    // è®¡ç®—所有子表格 finishedNum çš„æ€»å’Œ
    const totalFinishedNum = expandData.value.reduce((sum, item) => sum + (Number(item.finishedNum) || 0), 0);
    // çˆ¶è¡¨æ ¼çš„æŽ’产数量
    const schedulingNum = parentRow ? Number(parentRow.schedulingNum) : 0;
    if (totalFinishedNum > schedulingNum) {
        // å›žé€€æœ¬æ¬¡è¾“å…¥
        row.finishedNum = schedulingNum - (totalFinishedNum - Number(row.finishedNum));
        proxy.$modal.msgWarning('所有本次生产数量之和不可大于排产数量');
    }
    row.pendingNum = row.schedulingNum - row.finishedNum;
}
// ç¼–辑修改状态
const changeEditType = (row) => {
    row.editType = !row.editType;
};
// ä¿å­˜è®°å½•
const saveReceiptPayment = (row) => {
    productionReportUpdate(row).then((res) => {
        row.editType = !row.editType;
        getList();
        proxy.$modal.msgSuccess("提交成功");
    });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
};
const summarizeMainTable = (param) => {
    return proxy.summarizeTable(param, [
        "finishedNum"
    ]);
};
// æ‰“开弹框
const openForm = (type, row) => {
    if (selectedRows.value.length !== 1) {
        proxy.$message.error("请选择一条数据");
        return;
    }
    if (selectedRows.value[0].pendingFinishNum == 0) {
        proxy.$message.warning("无需再报工");
        return;
    }
    nextTick(() => {
        const rowInfo = type === 'add' ? selectedRows.value[0] : row
        formDia.value?.openDialog(type, rowInfo)
    })
};
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const changeDaterange = value => {
    if (value) {
      searchForm.value.entryDateStart = value[0];
      searchForm.value.entryDateEnd = value[1];
    } else {
      searchForm.value.entryDateStart = undefined;
      searchForm.value.entryDateEnd = undefined;
    }
    handleQuery();
  };
  const deleteReport = row => {
    ElMessageBox.confirm("确定删除该报工吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      productionReportDelete({ id: row.id }).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        } else {
          ElMessageBox.alert(res.msg || "删除失败", "提示", {
            confirmButtonText: "确定",
          });
        }
      });
    });
  };
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined;
    expandedRowKeys.value = [];
    productionProductMainListPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records.map(item => ({
          ...item,
          pendingFinishNum:
            (Number(item.schedulingNum) || 0) - (Number(item.finishedNum) || 0),
        }));
        page.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  // å±•开行
  const expandChange = (row, expandedRows) => {
    userListNoPageByTenantId().then(res => {
      userList.value = res.data;
    });
    if (expandedRows.length > 0) {
      nextTick(() => {
        expandedRowKeys.value = [];
        try {
          childrenLoading.value = true;
          workListPageById({ id: row.id }).then(res => {
            childrenLoading.value = false;
            const index = tableData.value.findIndex(item => item.id === row.id);
            if (index > -1) {
              expandData.value = res.data.map(item => ({
                ...item,
                pendingNum:
                  (Number(item.schedulingNum) || 0) -
                  (Number(item.finishedNum) || 0),
                parentStatus: row.status, // æ–°å¢žçˆ¶è¡¨çŠ¶æ€
              }));
            }
            expandedRowKeys.value.push(row.id);
          });
        } catch (error) {
          childrenLoading.value = false;
          console.log(error);
        }
      });
    } else {
      expandedRowKeys.value = [];
    }
  };
  const changeNum = row => {
    // æ‰¾åˆ°çˆ¶è¡¨æ ¼æ•°æ®
    const parentRow = tableData.value.find(
      item => item.id === expandedRowKeys.value[0]
    );
    // è®¡ç®—所有子表格 finishedNum çš„æ€»å’Œ
    const totalFinishedNum = expandData.value.reduce(
      (sum, item) => sum + (Number(item.finishedNum) || 0),
      0
    );
    // çˆ¶è¡¨æ ¼çš„æŽ’产数量
    const schedulingNum = parentRow ? Number(parentRow.schedulingNum) : 0;
// åˆ é™¤
const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
        ids = selectedRows.value.map((item) => item.id);
    } else {
        proxy.$modal.msgWarning("请选择数据");
        return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            staffJoinDel(ids).then((res) => {
                proxy.$modal.msgSuccess("删除成功");
                getList();
            });
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
// å¯¼å‡º
const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            proxy.download("/salesLedger/work/export", {}, "生产报工.xlsx");
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
onMounted(() => {
    getList();
});
    if (totalFinishedNum > schedulingNum) {
      // å›žé€€æœ¬æ¬¡è¾“å…¥
      row.finishedNum =
        schedulingNum - (totalFinishedNum - Number(row.finishedNum));
      proxy.$modal.msgWarning("所有本次生产数量之和不可大于排产数量");
    }
    row.pendingNum = row.schedulingNum - row.finishedNum;
  };
  // ç¼–辑修改状态
  const changeEditType = row => {
    row.editType = !row.editType;
  };
  // ä¿å­˜è®°å½•
  const saveReceiptPayment = row => {
    productionReportUpdate(row).then(res => {
      row.editType = !row.editType;
      getList();
      proxy.$modal.msgSuccess("提交成功");
    });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const summarizeMainTable = param => {
    return proxy.summarizeTable(param, ["finishedNum"]);
  };
  // æ‰“开弹框
  const openForm = (type, row) => {
    if (selectedRows.value.length !== 1) {
      proxy.$message.error("请选择一条数据");
      return;
    }
    if (selectedRows.value[0].pendingFinishNum == 0) {
      proxy.$message.warning("无需再报工");
      return;
    }
    nextTick(() => {
      const rowInfo = type === "add" ? selectedRows.value[0] : row;
      formDia.value?.openDialog(type, rowInfo);
    });
  };
  // æ‰“开投入模态框
  const isShowInput = ref(false);
  const isShowingId = ref(0);
  const showInput = row => {
    isShowInput.value = true;
    isShowingId.value = row.id;
  };
  // æ‰“开产出模态框
  const isShowOutput = ref(false);
  const showOutput = row => {
    isShowOutput.value = true;
    isShowingId.value = row.id;
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/salesLedger/work/export", {}, "生产报工.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  onMounted(() => {
    getList();
  });
</script>
<style scoped></style>
src/views/productionManagement/workOrder/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,587 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div class="search-row">
        <div class="search-item">
          <span class="search_title">工单编号:</span>
          <el-input v-model="searchForm.workOrderNo"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search" />
        </div>
        <div class="search-item">
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
        </div>
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
    </div>
    <el-dialog v-model="editDialogVisible"
               title="编辑时间"
               width="500px">
      <el-form :model="editrow"
               label-width="120px">
        <el-form-item label="计划开始时间">
          <el-date-picker v-model="editrow.planStartTime"
                          type="date"
                          placeholder="请选择"
                          value-format="YYYY-MM-DD"
                          style="width: 300px" />
        </el-form-item>
        <el-form-item label="计划结束时间">
          <el-date-picker v-model="editrow.planEndTime"
                          type="date"
                          placeholder="请选择"
                          value-format="YYYY-MM-DD"
                          style="width: 300px" />
        </el-form-item>
        <el-form-item label="实际开始时间">
          <el-date-picker v-model="editrow.actualStartTime"
                          type="date"
                          placeholder="请选择"
                          value-format="YYYY-MM-DD"
                          style="width: 300px" />
        </el-form-item>
        <el-form-item label="实际结束时间">
          <el-date-picker v-model="editrow.actualEndTime"
                          type="date"
                          placeholder="请选择"
                          value-format="YYYY-MM-DD"
                          style="width: 300px" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     @click="handleUpdate">确定</el-button>
          <el-button @click="editDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    <el-dialog v-model="transferCardVisible"
               title="流转卡"
               width="1000px">
      <div class="transfer-card-title">工单流转卡</div>
      <div class="transfer-card-container">
        <div class="transfer-card-info">
          <div class="info-group">
            <div class="info-item">
              <span class="info-label">工单编号</span>
              <span class="info-value">{{ transferCardRowData.workOrderNo }}</span>
            </div>
            <!-- <div class="info-item">
              <span class="info-label">产品编号</span>
              <span class="info-value">{{ transferCardRowData.productNo }}</span>
            </div> -->
            <div class="info-item">
              <span class="info-label">产品名称</span>
              <span class="info-value">{{ transferCardRowData.productName }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">产品规格</span>
              <span class="info-value">{{ transferCardRowData.model }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">工单状态</span>
              <span class="info-value">{{
                transferCardRowData.status === 1 ? '待确认' :
                transferCardRowData.status === 2 ? '待生产' :
                transferCardRowData.status === 3 ? '生产中' :
                transferCardRowData.status === 4 ? '已生产' :
                transferCardRowData.status
              }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">计划开始时间</span>
              <span class="info-value">{{ transferCardRowData.planStartTime }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">计划结束时间</span>
              <span class="info-value">{{ transferCardRowData.planEndTime }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">备注</span>
              <span class="info-value">{{ transferCardRowData.remark }}</span>
            </div>
          </div>
          <div class="info-group">
            <div class="info-item">
              <span class="info-label">&nbsp;</span>
              <span class="info-value">&nbsp;</span>
            </div>
            <div class="info-item">
              <span class="info-label">计划数量</span>
              <span class="info-value">{{ transferCardRowData.planQuantity }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">良品数量</span>
              <span class="info-value">0</span>
            </div>
            <div class="info-item">
              <span class="info-label">不良品数</span>
              <span class="info-value">0</span>
            </div>
            <div class="info-item">
              <span class="info-label">实际开始时间</span>
              <span class="info-value">{{ transferCardRowData.actualStartTime }}</span>
            </div>
            <div class="info-item">
              <span class="info-label">实际结束时间</span>
              <span class="info-value">{{ transferCardRowData.actualEndTime }}</span>
            </div>
          </div>
        </div>
        <div class="transfer-card-qr">
          <div class="qr-container">
            <img :src="transferCardQrUrl"
                 alt="流转卡二维码"
                 style="width: 200px; height: 200px;" />
            <!-- <div class="qr-tip"
                 style="margin-top: 10px; text-align: center;">流转卡二维码</div> -->
          </div>
        </div>
      </div>
      <div class="print-button-container"
           style=" text-align: center;
      margin-bottom: 40px;">
        <el-button type="primary"
                   style="margin-top: 20px;"
                   @click="printTransferCard">打印流转卡</el-button>
      </div>
    </el-dialog>
    <el-dialog v-model="reportDialogVisible"
               title="报工"
               width="500px">
      <el-form :model="reportForm"
               label-width="120px">
        <el-form-item label="待生产数量">
          <el-input v-model="reportForm.planQuantity"
                    readonly
                    style="width: 300px" />
        </el-form-item>
        <el-form-item label="本次生产数量">
          <el-input v-model.number="reportForm.quantity"
                    type="number"
                    min="1"
                    style="width: 300px"
                    placeholder="请输入本次生产数量" />
        </el-form-item>
        <el-form-item label="班组信息">
          <el-input v-model="reportForm.userName"
                    style="width: 300px"
                    readonly
                    placeholder="请输入班组信息" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     @click="handleReport">确定</el-button>
          <el-button @click="reportDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { onMounted, ref } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import {
    productWorkOrderPage,
    updateProductWorkOrder,
    addProductMain,
  } from "@/api/productionManagement/workOrder.js";
  import { getUserProfile } from "@/api/system/user.js";
  import QRCode from "qrcode";
  import { getCurrentInstance, reactive, toRefs } from "vue";
  const { proxy } = getCurrentInstance();
  const tableColumn = ref([
    {
      label: "工单编号",
      prop: "workOrderNo",
      width: "140",
    },
    {
      label: "生产订单号",
      prop: "productOrderNpsNo",
      width: "140",
    },
    {
      label: "产品名称",
      prop: "productName",
      width: "140",
    },
    {
      label: "规格",
      prop: "model",
    },
    {
      label: "单位",
      prop: "unit",
    },
    {
      label: "工序名称",
      prop: "processName",
    },
    {
      label: "待生产数量",
      prop: "planQuantity",
      width: "140",
    },
    {
      label: "计划生产数量",
      prop: "quantity",
      width: "140",
    },
    {
      label: "计划开始时间",
      prop: "planStartTime",
      width: "140",
    },
    {
      label: "计划结束时间",
      prop: "planEndTime",
      width: "140",
    },
    {
      label: "实际开始时间",
      prop: "actualStartTime",
      width: "140",
    },
    {
      label: "实际结束时间",
      prop: "actualEndTime",
      width: "140",
    },
    {
      label: "操作",
      width: "200",
      align: "center",
      dataType: "action",
      fixed: "right",
      operation: [
        {
          name: "编辑",
          clickFun: row => {
            handleEdit(row);
          },
        },
        {
          name: "流转卡",
          clickFun: row => {
            showTransferCard(row);
          },
        },
        {
          name: "报工",
          clickFun: row => {
            showReportDialog(row);
          },
          disabled: row => row.planQuantity <= 0,
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const qrCodeUrl = ref("");
  const qrRowData = ref(null);
  const editDialogVisible = ref(false);
  const transferCardVisible = ref(false);
  const transferCardData = ref([]);
  const transferCardQrUrl = ref("");
  const transferCardRowData = ref(null);
  const reportDialogVisible = ref(false);
  const reportForm = reactive({
    planQuantity: 0,
    quantity: 0,
    userName: "",
    workOrderId: "",
    reportWork: "",
    productProcessRouteItemId: "",
    userId: "",
    productMainId: null,
  });
  const currentReportRowData = ref(null);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const data = reactive({
    searchForm: {
      workOrderNo: "",
    },
  });
  const { searchForm } = toRefs(data);
  let editrow = ref(null);
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  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.value, ...page };
    productWorkOrderPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  const showTransferCard = async row => {
    transferCardRowData.value = row;
    const qrContent =
      proxy.javaApi + "/work-order?orderRow=" + JSON.stringify(row);
    console.log(qrContent, "qrContent");
    transferCardQrUrl.value = await QRCode.toDataURL(qrContent);
    transferCardVisible.value = true;
  };
  const printTransferCard = () => {
    window.print();
  };
  const handleEdit = row => {
    editrow.value = JSON.parse(JSON.stringify(row));
    editDialogVisible.value = true;
  };
  const handleUpdate = () => {
    updateProductWorkOrder(editrow.value)
      .then(res => {
        proxy.$modal.msgSuccess("提交成功");
        editDialogVisible.value = false;
        getList();
      })
      .catch(() => {
        ElMessageBox.alert("修改失败", "提示", {
          confirmButtonText: "确定",
        });
      });
  };
  const showReportDialog = row => {
    currentReportRowData.value = row;
    reportForm.planQuantity = row.planQuantity;
    reportForm.quantity = row.quantity;
    reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
    reportForm.workOrderId = row.id;
    reportForm.reportWork = row.reportWork;
    reportForm.productMainId = row.productMainId;
    // èŽ·å–å½“å‰ç™»å½•ç”¨æˆ·ä¿¡æ¯
    reportDialogVisible.value = true;
  };
  const handleReport = () => {
    if (reportForm.planQuantity <= 0) {
      ElMessageBox.alert("待生产数量为0,无法报工", "提示", {
        confirmButtonText: "确定",
      });
      return;
    }
    if (!reportForm.quantity || reportForm.quantity <= 0) {
      ElMessageBox.alert("请输入有效的本次生产数量", "提示", {
        confirmButtonText: "确定",
      });
      return;
    }
    if (reportForm.quantity > reportForm.planQuantity) {
      ElMessageBox.alert("本次生产数量不能超过待生产数量", "提示", {
        confirmButtonText: "确定",
      });
      return;
    }
    // console.log(reportForm);
    addProductMain(reportForm).then(res => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess("报工成功");
        reportDialogVisible.value = false;
        getList();
      } else {
        ElMessageBox.alert(res.msg || "报工失败", "提示", {
          confirmButtonText: "确定",
        });
      }
    });
  };
  onMounted(() => {
    getList();
    getUserProfile()
      .then(res => {
        if (res.code === 200) {
          reportForm.userName = res.data.userName;
          reportForm.userId = res.data.userId;
        }
      })
      .catch(err => {
        console.error("获取用户信息失败", err);
      });
  });
</script>
<style scoped lang="scss">
  .search_form {
    margin-bottom: 20px;
    .search-row {
      display: flex;
      gap: 20px;
      align-items: center;
      .search-item {
        display: flex;
        align-items: center;
        gap: 10px;
      }
    }
  }
  .transfer-card-title {
    font-size: 24px;
    font-weight: bold;
    text-align: center;
    margin-bottom: 20px;
  }
  .transfer-card-container {
    display: flex;
    gap: 20px;
    height: 350px;
    .transfer-card-info {
      flex: 1;
      overflow: auto;
      .info-group {
        width: 50%;
        float: left;
      }
      .info-item {
        display: flex;
        margin-bottom: 15px;
        .info-label {
          width: 120px;
          font-weight: bold;
          margin-right: 20px;
        }
        .info-value {
          flex: 1;
        }
      }
    }
    .transfer-card-qr {
      width: 240px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: flex-start;
    }
  }
</style>
<style  lang="scss">
  @media print {
    @page {
      size: landscape;
    }
    body * {
      visibility: hidden;
    }
    .el-dialog__wrapper,
    .el-dialog,
    .el-dialog__body,
    .transfer-card-title,
    .transfer-card-container,
    .transfer-card-container *,
    .info-item,
    .info-label,
    .info-value {
      visibility: visible;
    }
    .print-button-container {
      visibility: hidden;
    }
    .el-dialog__wrapper {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      margin: 0;
    }
    .el-dialog {
      width: 100% !important;
      max-width: 800px;
      margin: 0 auto !important;
    }
    .el-dialog__header,
    .el-dialog__footer {
      display: none;
    }
    .el-dialog__body {
      padding: 20px;
    }
    .transfer-card-container {
      height: auto;
      display: flex;
      gap: 20px;
    }
    .transfer-card-info {
      flex: 1;
      .info-group {
        width: 100%;
        float: none;
        margin-bottom: 20px;
      }
      .info-item {
        display: flex;
        margin-bottom: 10px;
        .info-label {
          width: 100px;
          font-weight: bold;
          margin-right: 15px;
          white-space: nowrap;
        }
        .info-value {
          flex: 1;
          word-break: break-word;
        }
      }
    }
    .transfer-card-qr {
      width: 160px;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: flex-start;
    }
    .qr-container img {
      width: 140px !important;
      height: 140px !important;
    }
  }
</style>