spring
7 小时以前 5e94a2858a40bc32f3b0369172e16ce17597714a
fix: 同步军泰伟业生产模块变更
已添加2个文件
已修改23个文件
已删除1个文件
3676 ■■■■■ 文件已修改
src/api/inventoryManagement/stockIn.js 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockManage.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockOut.js 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRoute.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteItem.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productBom.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productProcessRoute.js 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productStructure.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProcess.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PageHeader/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/QRCodeGenerator/index.vue 419 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/Edit.vue 220 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 173 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 1077 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/index.vue 299 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/ProcessRouteItemForm.vue 555 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/Input.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockIn.js
@@ -9,6 +9,50 @@
    });
};
// æŸ¥è¯¢ç”Ÿäº§å…¥åº“信息列表
export const getStockInPageByProduction = (params) => {
    return request({
        url: "/stockin/listPageByProduction",
        method: "get",
        params,
    });
};
// æŸ¥è¯¢ç”Ÿäº§å…¥åº“信息列表
export const getStockInPageByProductProduction = (params) => {
    return request({
        url: "/stockin/listPageByProductProduction",
        method: "get",
        params,
    });
};
// å‡ºåº“台账-查询自定义入库信息列表
export const getStockInPageByCustom = (params) => {
    return request({
        url: "/stockmanagement/listPageByCustom",
        method: "get",
        params,
    });
};
// å…¥åº“管理-查询自定义入库信息列表
export const getInPageByCustom = (params) => {
    return request({
        url: "/stockin/listPageByCustom",
        method: "get",
        params,
    });
};
// å‡ºåº“台账-查询生产出库信息列表
export const getStockInPageByProduct = (params) => {
    return request({
        url: "/stockmanagement/listPageByProduct",
        method: "get",
        params,
    });
};
// ä¿®æ”¹å…¥åº“存信息
export const updateStockIn = (data) => {
    return request({
@@ -26,6 +70,14 @@
        data,
    });
};
// ä¿®æ”¹ææ–™åº“存信息
export const updateManagementByCustom = (data) => {
    return request({
        url: "/stockin/updateManagementByCustom ",
        method: "post",
        data,
    });
};
// æ–°å¢žå•†å“å…¥åº“信息
export function addSutockIn(data) {
@@ -36,6 +88,32 @@
    })
}
// æ–°å¢žè‡ªå®šä¹‰å…¥åº“信息
export function addStockInCustom(data) {
    return request({
        url: '/stockin/addCustom',
        method: 'post',
        data: data
    })
}
// ç¼–辑自定义入库信息
export function updateStockInCustom(data) {
    return request({
        url: '/stockin/updateCustom',
        method: 'post',
        data: data
    })
}
// ç¼–辑成品入库信息
export function updateProduct(data) {
    return request({
        url: '/stockin/update',
        method: 'post',
        data: data
    })
}
// åˆ é™¤å…¥åº“信息
export function delStockIn(ids) {
    return request({
@@ -45,6 +123,15 @@
    })
}
// åˆ é™¤è‡ªå®šä¹‰å…¥åº“信息
export function delStockInCustom(ids) {
    return request({
        url: '/stockin/delteCustom',
        method: 'post',
        data: ids
    })
}
// å¯¼å‡ºå…¥åº“信息
export function exportStockIn(query) {
    return request({
src/api/inventoryManagement/stockManage.js
@@ -9,6 +9,31 @@
    });
};
// æŸ¥è¯¢ç”Ÿäº§å…¥åº“库存信息列表
export const getStockManagePageByProduction = (params) => {
    return request({
        url: "/stockin/listPageCopyByProduction",
        method: "get",
        params,
    });
};
// æŸ¥è¯¢æˆå“åº“存信息列表
export const getStockManageProduction = (params) => {
    return request({
        url: "/stockin/listPageProductionStock",
        method: "get",
        params,
    });
};
// æŸ¥è¯¢è‡ªå®šä¹‰å…¥åº“库存信息列表
export const getStockManagePageByCustom = (params) => {
    return request({
        url: "/stockin/listPageCopyByCustom",
        method: "get",
        params,
    });
};
// ä¿®æ”¹åº“存信息
export const updateStockManage = (data) => {
@@ -38,7 +63,7 @@
    })
}
//出库接口
// å‡ºåº“管理-领用接口
export const stockOut = (data) => {
    return request({
        url: '/stockmanagement/stockout',
src/api/inventoryManagement/stockOut.js
@@ -1,9 +1,18 @@
import request from "@/utils/request";
//查询出库列表
// å‡ºåº“台账-采购出库查询出库列表
export const getStockOutPage = (params) => {
    return request({
        url: "/stockmanagement/listPage",
        method: "get",
        params,
    });
};
// å‡ºåº“台账-生产出库查询出库列表
export const getStockOutSemiProductPage = (params) => {
    return request({
        url: "/stockmanagement/listPageBySemiProduct",
        method: "get",
        params,
    });
@@ -33,15 +42,5 @@
        url: '/stockmanagement/del',
        method: 'post',
        data: ids
    })
}
//导出出库信息
export const exportStockOut = (query) => {
    return request({
        url: '/stockmanagement/export',
        method: 'get',
        params: query,
        responseType: 'blob'
    })
}
src/api/productionManagement/processRoute.js
@@ -32,3 +32,11 @@
    data: data,
  })
}
// èŽ·å–è¯¦æƒ…
export function getById(id) {
  return request({
    url: `/processRoute/${id}`,
    method: 'get',
  })
}
src/api/productionManagement/processRouteItem.js
@@ -17,3 +17,22 @@
        data: data,
    });
}
// æŽ’序接口
export function sortProcessRouteItem(data) {
  return request({
    url: "/processRouteItem/sort",
    method: "post",
    data: data,
  });
}
// æ‰¹é‡åˆ é™¤æŽ¥å£
export function batchDeleteProcessRouteItem(ids) {
  // å°†id数组转换为逗号分隔的字符串,拼接到URL后面
  const idsStr = Array.isArray(ids) ? ids.join(",") : ids;
  return request({
    url: `/processRouteItem/batchDelete/${idsStr}`,
    method: "delete",
  });
}
src/api/productionManagement/productBom.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
// äº§å“BOM页面接口
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢
export function listPage(query) {
  return request({
    url: "/productBom/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢ž
export function add(data) {
  return request({
    url: "/productBom/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹
export function update(data) {
  return request({
    url: "/productBom/update",
    method: "put",
    data: data,
  });
}
// æ‰¹é‡åˆ é™¤
export function batchDelete(ids) {
  return request({
    url: "/productBom/batchDelete",
    method: "delete",
    data: ids,
  });
}
// æ ¹æ®äº§å“åž‹å·ID查询BOM
export function getByModel(productModelId) {
  return request({
    url: "/productBom/getByModel",
    method: "get",
    params: { productModelId },
  });
}
src/api/productionManagement/productProcessRoute.js
@@ -18,11 +18,37 @@
    });
}
// åˆ é™¤å®¢æˆ·æ¡£æ¡ˆ
export function deleteRouteItem(ids) {
// ç”Ÿäº§è®¢å•下:新增工艺路线项目
export function addRouteItem(data) {
    return request({
        url: '/productProcessRoute/deleteRouteItem',
        method: 'delete',
        data: ids
    })
    url: "/productProcessRoute/addRouteItem",
    method: "post",
    data,
  });
}
// èŽ·å–ç”Ÿäº§è®¢å•å…³è”çš„å·¥è‰ºè·¯çº¿ä¸»ä¿¡æ¯
export function listMain(orderId) {
  return request({
    url: "/productProcessRoute/listMain",
    method: "get",
    params: { orderId },
  });
}
// åˆ é™¤å·¥è‰ºè·¯çº¿é¡¹ç›®ï¼ˆè·¯ç”±åŽæ‹¼æŽ¥ id)
export function deleteRouteItem(id) {
  return request({
    url: `/productProcessRoute/deleteRouteItem/${id}`,
    method: "delete",
  });
}
// ç”Ÿäº§è®¢å•下:排序工艺路线项目
export function sortRouteItem(data) {
  return request({
    url: "/productProcessRoute/sortRouteItem",
    method: "post",
    data,
  });
}
src/api/productionManagement/productStructure.js
@@ -4,7 +4,7 @@
// åˆ†é¡µæŸ¥è¯¢
export function queryList(id) {
  return request({
    url: "/productStructure/listByproductModelId/" + id,
    url: "/productStructure/listBybomId/" + id,
    method: "get",
  });
}
src/api/productionManagement/productionOrder.js
@@ -10,7 +10,6 @@
  });
}
export function productOrderListPage(query) {
  return request({
    url: "/productOrder/page",
@@ -19,6 +18,33 @@
  });
}
// ç”Ÿäº§è®¢å•-按产品型号查询可用工艺路线列表
export function listProcessRoute(query) {
  return request({
    url: "/productOrder/listProcessRoute",
    method: "get",
    params: query,
  });
}
// ç”Ÿäº§è®¢å•-绑定工艺路线
export function bindingRoute(data) {
  return request({
    url: "/productOrder/bindingRoute",
    method: "post",
    data,
  });
}
// ç”Ÿäº§è®¢å•-查询产品结构列表
export function listProcessBom(query) {
  return request({
    url: "/productOrder/listProcessBom",
    method: "get",
    params: query,
  });
}
// èŽ·å–ç‚’æœºæ­£åœ¨å·¥ä½œé‡æ•°æ®
export function schedulingList(query) {
  return request({
src/api/productionManagement/productionProcess.js
@@ -49,3 +49,21 @@
        method: "get",
    });
}
// å¯¼å…¥æ•°æ®
export function importData(data) {
  return request({
    url: "/productProcess/importData",
    method: "post",
    data: data,
  });
}
// ä¸‹è½½æ¨¡æ¿
export function downloadTemplate() {
  return request({
    url: "/productProcess/downloadTemplate",
    method: "post",
    responseType: "blob",
  });
}
src/components/PageHeader/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
<template>
  <div class="page-header-wrapper">
    <el-page-header @back="handleBack" :content="content">
      <template #icon v-if="$slots.icon">
        <slot name="icon"></slot>
      </template>
      <template #title v-if="$slots.title">
        <slot name="title"></slot>
      </template>
      <template #content v-if="$slots.content">
        <slot name="content"></slot>
      </template>
      <template #extra>
        <slot name="extra">
          <slot name="right-button"></slot>
        </slot>
      </template>
    </el-page-header>
  </div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const props = defineProps({
  content: {
    type: String,
    default: ''
  }
})
const emit = defineEmits(['back'])
const router = useRouter()
const handleBack = () => {
  emit('back')
  // é»˜è®¤è¿”回到上一级
  router.back()
}
</script>
<style scoped>
.page-header-wrapper {
  margin-bottom: 16px;
}
.page-header-wrapper :deep(.el-page-header__extra) {
  display: flex;
  align-items: center;
  gap: 8px;
}
</style>
src/components/QRCodeGenerator/index.vue
@@ -1,70 +1,79 @@
<template>
  <div class="qr-code-generator">
    <!-- äºŒç»´ç ç”Ÿæˆè¡¨å• -->
    <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="qr-form">
    <el-form :model="form"
             :rules="rules"
             ref="formRef"
             label-width="120px"
             class="qr-form">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="标识类型" prop="type">
            <el-select v-model="form.type" placeholder="请选择标识类型" style="width: 100%">
              <el-option label="二维码" value="qrcode"></el-option>
              <el-option label="防伪码" value="security"></el-option>
          <el-form-item label="标识类型"
                        prop="type">
            <el-select v-model="form.type"
                       placeholder="请选择标识类型"
                       style="width: 100%">
              <el-option label="二维码"
                         value="qrcode"></el-option>
              <el-option label="防伪码"
                         value="security"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="内容" prop="content">
            <el-input
              v-model="form.content"
          <el-form-item label="内容"
                        prop="content">
            <el-input v-model="form.content"
              placeholder="请输入要编码的内容"
              :type="form.type === 'security' ? 'textarea' : 'text'"
              :rows="form.type === 'security' ? 3 : 1"
            ></el-input>
                      :rows="form.type === 'security' ? 3 : 1"></el-input>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="尺寸" prop="size">
            <el-input-number
              v-model="form.size"
          <el-form-item label="尺寸"
                        prop="size">
            <el-input-number v-model="form.size"
              :min="100" 
              :max="500" 
              :step="50"
              style="width: 100%"
            ></el-input-number>
                             style="width: 100%"></el-input-number>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="边距" prop="margin">
            <el-input-number
              v-model="form.margin"
          <el-form-item label="边距"
                        prop="margin">
            <el-input-number v-model="form.margin"
              :min="0" 
              :max="10" 
              :step="1"
              style="width: 100%"
            ></el-input-number>
                             style="width: 100%"></el-input-number>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="前景色" prop="foregroundColor">
            <el-color-picker v-model="form.foregroundColor" style="width: 100%"></el-color-picker>
          <el-form-item label="前景色"
                        prop="foregroundColor">
            <el-color-picker v-model="form.foregroundColor"
                             style="width: 100%"></el-color-picker>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="背景色" prop="backgroundColor">
            <el-color-picker v-model="form.backgroundColor" style="width: 100%"></el-color-picker>
          <el-form-item label="背景色"
                        prop="backgroundColor">
            <el-color-picker v-model="form.backgroundColor"
                             style="width: 100%"></el-color-picker>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="24">
          <el-form-item>
            <el-button type="primary" @click="generateCode" :loading="generating">
            <el-button type="primary"
                       @click="generateCode"
                       :loading="generating">
              ç”Ÿæˆ{{ form.type === 'qrcode' ? '二维码' : '防伪码' }}
            </el-button>
            <el-button @click="resetForm">重置</el-button>
@@ -72,18 +81,17 @@
        </el-col>
      </el-row>
    </el-form>
    <!-- ç”Ÿæˆçš„码显示区域 -->
    <div v-if="generatedCodeUrl" class="code-display">
    <div v-if="generatedCodeUrl"
         class="code-display">
      <el-divider content-position="center">
        {{ form.type === 'qrcode' ? '生成的二维码' : '生成的防伪码' }}
      </el-divider>
      <div class="code-container">
        <div class="code-image">
          <img :src="generatedCodeUrl" :alt="form.type === 'qrcode' ? '二维码' : '防伪码'" />
          <img :src="generatedCodeUrl"
               :alt="form.type === 'qrcode' ? '二维码' : '防伪码'" />
        </div>
        <div class="code-info">
          <p><strong>内容:</strong>{{ form.content }}</p>
          <p><strong>类型:</strong>{{ form.type === 'qrcode' ? '二维码' : '防伪码' }}</p>
@@ -91,60 +99,71 @@
          <p><strong>生成时间:</strong>{{ generateTime }}</p>
        </div>
      </div>
      <div class="code-actions">
        <el-button type="success" @click="downloadCode" icon="Download">
        <el-button type="success"
                   @click="downloadCode"
                   icon="Download">
          ä¸‹è½½å›¾ç‰‡
        </el-button>
        <el-button type="primary" @click="copyToClipboard" icon="CopyDocument">
        <el-button type="primary"
                   @click="copyToClipboard"
                   icon="CopyDocument">
          å¤åˆ¶å†…容
        </el-button>
        <el-button @click="printCode" icon="Printer">
        <el-button @click="printCode"
                   icon="Printer">
          æ‰“印
        </el-button>
      </div>
    </div>
    <!-- æ‰¹é‡ç”Ÿæˆå¯¹è¯æ¡† -->
    <el-dialog v-model="batchDialogVisible" title="批量生成" width="600px">
      <el-form :model="batchForm" label-width="120px">
    <el-dialog v-model="batchDialogVisible"
               title="批量生成"
               width="600px">
      <el-form :model="batchForm"
               label-width="120px">
        <el-form-item label="生成数量">
          <el-input-number v-model="batchForm.quantity" :min="1" :max="100" style="width: 100%"></el-input-number>
          <el-input-number v-model="batchForm.quantity"
                           :min="1"
                           :max="100"
                           style="width: 100%"></el-input-number>
        </el-form-item>
        <el-form-item label="前缀">
          <el-input v-model="batchForm.prefix" placeholder="请输入前缀,如:PROD_"></el-input>
          <el-input v-model="batchForm.prefix"
                    placeholder="请输入前缀,如:PROD_"></el-input>
        </el-form-item>
        <el-form-item label="起始编号">
          <el-input-number v-model="batchForm.startNumber" :min="1" style="width: 100%"></el-input-number>
          <el-input-number v-model="batchForm.startNumber"
                           :min="1"
                           style="width: 100%"></el-input-number>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="generateBatchCodes">开始生成</el-button>
          <el-button @click="batchDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="generateBatchCodes">开始生成</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ‰¹é‡ç”Ÿæˆç»“æžœ -->
    <div v-if="batchCodes.length > 0" class="batch-results">
    <div v-if="batchCodes.length > 0"
         class="batch-results">
      <el-divider content-position="center">批量生成结果</el-divider>
      <div class="batch-grid">
        <div
          v-for="(code, index) in batchCodes"
        <div v-for="(code, index) in batchCodes"
          :key="index" 
          class="batch-item"
        >
          <img :src="code.url" :alt="code.content" />
             class="batch-item">
          <img :src="code.url"
               :alt="code.content" />
          <p class="batch-content">{{ code.content }}</p>
          <el-button size="small" @click="downloadSingleCode(code)">下载</el-button>
          <el-button size="small"
                     @click="downloadSingleCode(code)">下载</el-button>
        </div>
      </div>
      <div class="batch-actions">
        <el-button type="success" @click="downloadAllCodes">下载全部</el-button>
        <el-button type="success"
                   @click="downloadAllCodes">下载全部</el-button>
        <el-button @click="clearBatchCodes">清空结果</el-button>
      </div>
    </div>
@@ -152,145 +171,146 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import QRCode from 'qrcode'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Download, CopyDocument, Printer } from '@element-plus/icons-vue'
  import { ref, reactive, computed, onMounted } from "vue";
  import QRCode from "qrcode";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { Download, CopyDocument, Printer } from "@element-plus/icons-vue";
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'QRCodeGenerator'
})
    name: "QRCodeGenerator",
  });
// è¡¨å•数据
const form = reactive({
  type: 'qrcode',
  content: '',
    type: "qrcode",
    content: "",
  size: 200,
  margin: 2,
  foregroundColor: '#000000',
  backgroundColor: '#FFFFFF'
})
    foregroundColor: "#000000",
    backgroundColor: "#FFFFFF",
  });
// è¡¨å•验证规则
const rules = {
  type: [{ required: true, message: '请选择标识类型', trigger: 'change' }],
  content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
}
    type: [{ required: true, message: "请选择标识类型", trigger: "change" }],
    content: [{ required: true, message: "请输入内容", trigger: "blur" }],
  };
// å“åº”式数据
const formRef = ref()
const generating = ref(false)
const generatedCodeUrl = ref('')
const generateTime = ref('')
const batchDialogVisible = ref(false)
  const formRef = ref();
  const generating = ref(false);
  const generatedCodeUrl = ref("");
  const generateTime = ref("");
  const batchDialogVisible = ref(false);
const batchForm = reactive({
  quantity: 10,
  prefix: '',
  startNumber: 1
})
const batchCodes = ref([])
    prefix: "",
    startNumber: 1,
  });
  const batchCodes = ref([]);
// ç”ŸæˆäºŒç»´ç æˆ–防伪码
const generateCode = async () => {
  try {
    await formRef.value.validate()
      await formRef.value.validate();
    
    if (!form.content.trim()) {
      ElMessage.warning('请输入要编码的内容')
      return
        ElMessage.warning("请输入要编码的内容");
        return;
    }
    
    generating.value = true
      generating.value = true;
    
    if (form.type === 'qrcode') {
      if (form.type === "qrcode") {
      // ç”ŸæˆäºŒç»´ç 
      generatedCodeUrl.value = await QRCode.toDataURL(form.content, {
        width: form.size,
        margin: form.margin,
        color: {
          dark: form.foregroundColor,
          light: form.backgroundColor
            light: form.backgroundColor,
        },
        errorCorrectionLevel: 'M'
      })
          errorCorrectionLevel: "M",
        });
    } else {
      // ç”Ÿæˆé˜²ä¼ªç ï¼ˆä½¿ç”¨äºŒç»´ç æŠ€æœ¯ï¼Œä½†å†…容格式不同)
      const securityContent = generateSecurityCode(form.content)
        const securityContent = generateSecurityCode(form.content);
      generatedCodeUrl.value = await QRCode.toDataURL(securityContent, {
        width: form.size,
        margin: form.margin,
        color: {
          dark: form.foregroundColor,
          light: form.backgroundColor
            light: form.backgroundColor,
        },
        errorCorrectionLevel: 'H' // é˜²ä¼ªç ä½¿ç”¨æœ€é«˜çº é”™çº§åˆ«
      })
          errorCorrectionLevel: "H", // é˜²ä¼ªç ä½¿ç”¨æœ€é«˜çº é”™çº§åˆ«
        });
    }
    
    generateTime.value = new Date().toLocaleString()
    ElMessage.success('生成成功!')
      generateTime.value = new Date().toLocaleString();
      ElMessage.success("生成成功!");
  } catch (error) {
    console.error('生成失败:', error)
    ElMessage.error('生成失败:' + error.message)
      console.error("生成失败:", error);
      ElMessage.error("生成失败:" + error.message);
  } finally {
    generating.value = false
      generating.value = false;
  }
}
  };
// ç”Ÿæˆé˜²ä¼ªç å†…容
const generateSecurityCode = (content) => {
  const timestamp = Date.now()
  const random = Math.random().toString(36).substr(2, 8)
  return `SEC_${content}_${timestamp}_${random}`
}
  const generateSecurityCode = content => {
    const timestamp = Date.now();
    const random = Math.random().toString(36).substr(2, 8);
    return `SEC_${content}_${timestamp}_${random}`;
  };
// ä¸‹è½½ç”Ÿæˆçš„码
const downloadCode = () => {
  if (!generatedCodeUrl.value) {
    ElMessage.warning('请先生成码')
    return
      ElMessage.warning("请先生成码");
      return;
  }
  
  const a = document.createElement('a')
  a.href = generatedCodeUrl.value
  a.download = `${form.type === 'qrcode' ? '二维码' : '防伪码'}_${new Date().getTime()}.png`
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  ElMessage.success('下载成功!')
}
    const a = document.createElement("a");
    a.href = generatedCodeUrl.value;
    a.download = `${
      form.type === "qrcode" ? "二维码" : "防伪码"
    }_${new Date().getTime()}.png`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    ElMessage.success("下载成功!");
  };
// å¤åˆ¶å†…容到剪贴板
const copyToClipboard = async () => {
  try {
    await navigator.clipboard.writeText(form.content)
    ElMessage.success('内容已复制到剪贴板')
      await navigator.clipboard.writeText(form.content);
      ElMessage.success("内容已复制到剪贴板");
  } catch (error) {
    // é™çº§æ–¹æ¡ˆ
    const textArea = document.createElement('textarea')
    textArea.value = form.content
    document.body.appendChild(textArea)
    textArea.select()
    document.execCommand('copy')
    document.body.removeChild(textArea)
    ElMessage.success('内容已复制到剪贴板')
      const textArea = document.createElement("textarea");
      textArea.value = form.content;
      document.body.appendChild(textArea);
      textArea.select();
      document.execCommand("copy");
      document.body.removeChild(textArea);
      ElMessage.success("内容已复制到剪贴板");
  }
}
  };
// æ‰“印码
const printCode = () => {
  if (!generatedCodeUrl.value) {
    ElMessage.warning('请先生成码')
    return
      ElMessage.warning("请先生成码");
      return;
  }
  
  const printWindow = window.open('', '_blank')
    const printWindow = window.open("", "_blank");
  printWindow.document.write(`
    <html>
      <head>
        <title>打印${form.type === 'qrcode' ? '二维码' : '防伪码'}</title>
          <title>打印${form.type === "qrcode" ? "二维码" : "防伪码"}</title>
        <style>
          body { text-align: center; padding: 20px; }
          img { max-width: 100%; height: auto; }
@@ -298,144 +318,149 @@
        </style>
      </head>
      <body>
        <h2>${form.type === 'qrcode' ? '二维码' : '防伪码'}</h2>
        <img src="${generatedCodeUrl.value}" alt="${form.type === 'qrcode' ? '二维码' : '防伪码'}" />
          <h2>${form.type === "qrcode" ? "二维码" : "防伪码"}</h2>
          <img src="${generatedCodeUrl.value}" alt="${
      form.type === "qrcode" ? "二维码" : "防伪码"
    }" />
        <div class="info">
          <p><strong>内容:</strong>${form.content}</p>
          <p><strong>生成时间:</strong>${generateTime.value}</p>
        </div>
      </body>
    </html>
  `)
  printWindow.document.close()
  printWindow.print()
}
    `);
    printWindow.document.close();
    printWindow.print();
  };
// é‡ç½®è¡¨å•
const resetForm = () => {
  formRef.value.resetFields()
  generatedCodeUrl.value = ''
  generateTime.value = ''
  batchCodes.value = []
}
    formRef.value.resetFields();
    generatedCodeUrl.value = "";
    generateTime.value = "";
    batchCodes.value = [];
  };
// æ‰¹é‡ç”Ÿæˆ
const generateBatchCodes = async () => {
  if (!batchForm.prefix.trim()) {
    ElMessage.warning('请输入前缀')
    return
      ElMessage.warning("请输入前缀");
      return;
  }
  
  batchCodes.value = []
  generating.value = true
    batchCodes.value = [];
    generating.value = true;
  
  try {
    for (let i = 0; i < batchForm.quantity; i++) {
      const number = batchForm.startNumber + i
      const content = `${batchForm.prefix}${number.toString().padStart(6, '0')}`
        const number = batchForm.startNumber + i;
        const content = `${batchForm.prefix}${number
          .toString()
          .padStart(6, "0")}`;
      
      let codeUrl
      if (form.type === 'qrcode') {
        let codeUrl;
        if (form.type === "qrcode") {
        codeUrl = await QRCode.toDataURL(content, {
          width: form.size,
          margin: form.margin,
          color: {
            dark: form.foregroundColor,
            light: form.backgroundColor
          }
        })
              light: form.backgroundColor,
            },
          });
      } else {
        const securityContent = generateSecurityCode(content)
          const securityContent = generateSecurityCode(content);
        codeUrl = await QRCode.toDataURL(securityContent, {
          width: form.size,
          margin: form.margin,
          color: {
            dark: form.foregroundColor,
            light: form.backgroundColor
          }
        })
              light: form.backgroundColor,
            },
          });
      }
      
      batchCodes.value.push({
        content,
        url: codeUrl
      })
          url: codeUrl,
        });
    }
    
    ElMessage.success(`批量生成完成,共生成 ${batchForm.quantity} ä¸ªç `)
    batchDialogVisible.value = false
      ElMessage.success(`批量生成完成,共生成 ${batchForm.quantity} ä¸ªç `);
      batchDialogVisible.value = false;
  } catch (error) {
    console.error('批量生成失败:', error)
    ElMessage.error('批量生成失败:' + error.message)
      console.error("批量生成失败:", error);
      ElMessage.error("批量生成失败:" + error.message);
  } finally {
    generating.value = false
      generating.value = false;
  }
}
  };
// ä¸‹è½½å•个批量生成的码
const downloadSingleCode = (code) => {
  const a = document.createElement('a')
  a.href = code.url
  a.download = `${code.content}.png`
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
}
  const downloadSingleCode = code => {
    const a = document.createElement("a");
    a.href = code.url;
    a.download = `${code.content}.png`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  };
// ä¸‹è½½æ‰€æœ‰æ‰¹é‡ç”Ÿæˆçš„码
const downloadAllCodes = async () => {
  if (batchCodes.value.length === 0) {
    ElMessage.warning('没有可下载的码')
    return
      ElMessage.warning("没有可下载的码");
      return;
  }
  
  try {
    // ä½¿ç”¨JSZip打包下载
    const JSZip = await import('jszip')
    const zip = new JSZip.default()
      const JSZip = await import("jszip");
      const zip = new JSZip.default();
    
    batchCodes.value.forEach((code, index) => {
      // å°†base64转换为blob
      const base64Data = code.url.split(',')[1]
      const byteCharacters = atob(base64Data)
      const byteNumbers = new Array(byteCharacters.length)
        const base64Data = code.url.split(",")[1];
        const byteCharacters = atob(base64Data);
        const byteNumbers = new Array(byteCharacters.length);
      for (let i = 0; i < byteCharacters.length; i++) {
        byteNumbers[i] = byteCharacters.charCodeAt(i)
          byteNumbers[i] = byteCharacters.charCodeAt(i);
      }
      const byteArray = new Uint8Array(byteNumbers)
        const byteArray = new Uint8Array(byteNumbers);
      
      zip.file(`${code.content}.png`, byteArray)
    })
        zip.file(`${code.content}.png`, byteArray);
      });
    
    const content = await zip.generateAsync({ type: 'blob' })
    const a = document.createElement('a')
    a.href = URL.createObjectURL(content)
    a.download = `批量${form.type === 'qrcode' ? '二维码' : '防伪码'}_${new Date().getTime()}.zip`
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(a.href)
      const content = await zip.generateAsync({ type: "blob" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(content);
      a.download = `批量${
        form.type === "qrcode" ? "二维码" : "防伪码"
      }_${new Date().getTime()}.zip`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(a.href);
    
    ElMessage.success('批量下载完成!')
      ElMessage.success("批量下载完成!");
  } catch (error) {
    console.error('批量下载失败:', error)
    ElMessage.error('批量下载失败,请逐个下载')
      console.error("批量下载失败:", error);
      ElMessage.error("批量下载失败,请逐个下载");
  }
}
  };
// æ¸…空批量生成结果
const clearBatchCodes = () => {
  batchCodes.value = []
}
    batchCodes.value = [];
  };
// æš´éœ²æ–¹æ³•给父组件
defineExpose({
  generateCode,
  downloadCode,
  resetForm,
  form
})
    form,
  });
</script>
<style scoped>
src/main.js
@@ -52,6 +52,8 @@
import DictTag from "@/components/DictTag";
// è¡¨æ ¼ç»„ä»¶
import PIMTable from "@/components/PIMTable/PIMTable.vue";
// é¡µé¢å¤´éƒ¨ç»„ä»¶
import PageHeader from "@/components/PageHeader/index.vue";
import { getToken } from "@/utils/auth";
import {
@@ -93,6 +95,7 @@
app.component("RightToolbar", RightToolbar);
app.component("Editor", Editor);
app.component("PIMTable", PIMTable);
app.component("PageHeader", PageHeader);
app.use(router);
app.use(store);
src/views/productionManagement/processRoute/Edit.vue
@@ -8,46 +8,45 @@
    >
      <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="规格型号:"
            label="产品名称"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择规格型号',
                message: '请选择产品',
                trigger: 'change',
              }
            ]"
        >
          <el-button type="primary" @click="showProductSelectDialog = true">
            {{ formState.productName && formState.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item
            label="BOM"
            prop="bomId"
            :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]"
        >
          <el-select
              v-model="formState.productModelId"
              placeholder="请选择"
              v-model="formState.bomId"
              placeholder="请选择BOM"
              clearable
              :disabled="!formState.productModelId || bomOptions.length === 0"
              style="width: 100%"
          >
            <el-option
                v-for="item in productModelsOptions"
                v-for="item in bomOptions"
                :key="item.id"
                :label="item.model"
                :label="item.bomNo || `BOM-${item.id}`"
                :value="item.id"
            />
          </el-select>
@@ -57,6 +56,13 @@
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog
          v-model="showProductSelectDialog"
          @confirm="handleProductSelect"
          single
      />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
@@ -68,9 +74,10 @@
</template>
<script setup>
import {ref, computed, getCurrentInstance, onMounted} from "vue";
import {ref, computed, getCurrentInstance, onMounted, nextTick, watch} from "vue";
import {update} from "@/api/productionManagement/processRoute.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {getByModel} from "@/api/productionManagement/productBom.js";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
const props = defineProps({
  visible: {
@@ -87,7 +94,14 @@
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({});
const formState = ref({
  productId: undefined,
  productModelId: undefined,
  productName: "",
  productModelName: "",
  bomId: undefined,
  description: '',
});
const isShow = computed({
  get() {
@@ -98,66 +112,111 @@
  },
});
const showProductSelectDialog = ref(false);
const bomOptions = ref([]);
let { proxy } = getCurrentInstance()
const productModelsOptions = ref([])
const productOptions = ref([])
const closeModal = () => {
  isShow.value = false;
};
// è®¾ç½®è¡¨å•数据
const setFormData = () => {
  formState.value = props.record
  if (props.record) {
    formState.value = {
      ...props.record,
      productId: props.record.productId,
      productModelId: props.record.productModelId,
      productName: props.record.productName || "",
      // æ³¨æ„ï¼šrecord中的字段是model,需要映射到productModelName
      productModelName: props.record.model || props.record.productModelName || "",
      bomId: props.record.bomId,
      description: props.record.description || '',
    };
    // å¦‚果有产品型号ID,加载BOM列表
    if (props.record.productModelId) {
      loadBomList(props.record.productModelId);
    }
  }
}
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;
    });
// åŠ è½½BOM列表
const loadBomList = async (productModelId) => {
  if (!productModelId) {
    bomOptions.value = [];
    return;
  }
  try {
    const res = await getByModel(productModelId);
    // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
    let bomList = [];
    if (Array.isArray(res)) {
      bomList = res;
    } else if (res && res.data) {
      bomList = Array.isArray(res.data) ? res.data : [res.data];
    } else if (res && typeof res === 'object') {
      bomList = [res];
    }
    bomOptions.value = bomList;
  } catch (error) {
    console.error("加载BOM列表失败:", error);
    bomOptions.value = [];
  }
};
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);
// äº§å“é€‰æ‹©å¤„理
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    // å…ˆæŸ¥è¯¢BOM列表(必选)
    try {
      const res = await getByModel(product.id);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === 'object') {
        bomList = [res];
    }
    return newItem;
  });
      if (bomList.length > 0) {
        formState.value.productModelId = product.id;
        formState.value.productName = product.productName;
        formState.value.productModelName = product.model;
        // å¦‚果当前选择的BOM不在新列表中,则重置BOM选择
        const currentBomExists = bomList.some(bom => bom.id === formState.value.bomId);
        if (!currentBomExists) {
          formState.value.bomId = undefined;
}
        bomOptions.value = bomList;
        showProductSelectDialog.value = false;
        // è§¦å‘表单验证更新
        proxy.$refs["formRef"]?.validateField('productModelId');
      } else {
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
    }
  }
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
      }
      if (!formState.value.bomId) {
        proxy.$modal.msgError("请选择BOM");
        return;
      }
      update(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
@@ -176,11 +235,18 @@
});
onMounted(() => {
  getProductOptions()
  getModels(props.record.productId)
// ç›‘听弹窗打开,初始化表单数据
watch(() => props.visible, (visible) => {
  if (visible && props.record) {
  nextTick(() => {
    setFormData()
      setFormData();
  });
})
  }
}, { immediate: true });
onMounted(() => {
  if (props.visible && props.record) {
    setFormData();
  }
});
</script>
src/views/productionManagement/processRoute/New.vue
@@ -8,46 +8,45 @@
    >
      <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="规格型号:"
            label="产品名称"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择规格型号',
                message: '请选择产品',
                trigger: 'change',
              }
            ]"
        >
          <el-button type="primary" @click="showProductSelectDialog = true">
            {{ formState.productName && formState.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item
            label="BOM"
            prop="bomId"
            :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]"
        >
          <el-select
              v-model="formState.productModelId"
              placeholder="请选择"
              v-model="formState.bomId"
              placeholder="请选择BOM"
              clearable
              :disabled="!formState.productModelId || bomOptions.length === 0"
              style="width: 100%"
          >
            <el-option
                v-for="item in productModelsOptions"
                v-for="item in bomOptions"
                :key="item.id"
                :label="item.model"
                :label="item.bomNo || `BOM-${item.id}`"
                :value="item.id"
            />
          </el-select>
@@ -57,6 +56,13 @@
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog
          v-model="showProductSelectDialog"
          @confirm="handleProductSelect"
          single
      />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
@@ -68,9 +74,10 @@
</template>
<script setup>
import {ref, computed, getCurrentInstance, onMounted} from "vue";
import {ref, computed, getCurrentInstance} from "vue";
import {add} from "@/api/productionManagement/processRoute.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {getByModel} from "@/api/productionManagement/productBom.js";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
const props = defineProps({
  visible: {
@@ -85,6 +92,9 @@
const formState = ref({
  productId: undefined,
  productModelId: undefined,
  productName: "",
  productModelName: "",
  bomId: undefined,
  description: '',
});
@@ -97,66 +107,73 @@
  },
});
const productModelsOptions = ref([])
const productOptions = ref([])
const showProductSelectDialog = ref(false);
const bomOptions = ref([]);
let { proxy } = getCurrentInstance()
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  formState.value = {
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
    bomId: undefined,
    description: '',
  };
  bomOptions.value = [];
  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);
// äº§å“é€‰æ‹©å¤„理
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    // å…ˆæŸ¥è¯¢BOM列表(必选)
    try {
      const res = await getByModel(product.id);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === 'object') {
        bomList = [res];
    }
    return newItem;
  });
      if (bomList.length > 0) {
        formState.value.productModelId = product.id;
        formState.value.productName = product.productName;
        formState.value.productModelName = product.model;
        formState.value.bomId = undefined; // é‡ç½®BOM选择
        bomOptions.value = bomList;
        showProductSelectDialog.value = false;
        // è§¦å‘表单验证更新
        proxy.$refs["formRef"]?.validateField('productModelId');
      } else {
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
}
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
    }
  }
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
      }
      if (!formState.value.bomId) {
        proxy.$modal.msgError("请选择BOM");
        return;
      }
      add(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
@@ -174,8 +191,4 @@
  handleSubmit,
  isShow,
});
onMounted(() => {
  getProductOptions()
})
</script>
src/views/productionManagement/processRoute/index.vue
@@ -80,6 +80,10 @@
    prop: "model",
  },
  {
    label: "BOM编号",
    prop: "bomNo",
  },
  {
    label: "描述",
    prop: "description",
  },
@@ -166,7 +170,13 @@
  router.push({
    path: '/productionManagement/processRouteItem',
    query: {
      id: row.id
      id: row.id,
      processRouteCode: row.processRouteCode || '',
      productName: row.productName || '',
      model: row.model || '',
      bomNo: row.bomNo || '',
      description: row.description || '',
      type: 'route',
    }
  })
};
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -1,111 +1,168 @@
<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>
    <PageHeader content="工艺路线项目" />
      <el-switch
          v-model="isTable"
          inline-prompt
          active-text="表格"
          inactive-text="列表"
          @change="handleViewChange"
      />
    <!-- å·¥è‰ºè·¯çº¿ä¿¡æ¯å±•示 -->
    <el-card v-if="routeInfo.processRouteCode" class="route-info-card" shadow="hover">
      <div class="route-info">
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">工艺路线编号</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.processRouteCode }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">产品名称</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.productName || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">规格名称</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.model || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">BOM编号</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.bomNo || '-' }}</span>
          </div>
        </div>
        <div class="info-item full-width" v-if="routeInfo.description">
          <div class="info-label-wrapper">
            <span class="info-label">描述</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.description }}</span>
          </div>
        </div>
      </div>
    </el-card>
    <!-- è¡¨æ ¼è§†å›¾ -->
    <div v-if="viewMode === 'table'" class="section-header">
      <div class="section-title">工艺路线项目列表</div>
      <div class="section-actions">
        <el-button
            icon="Grid"
            @click="toggleView"
            style="margin-right: 10px;"
        >
          å¡ç‰‡è§†å›¾
        </el-button>
        <el-button type="primary" @click="handleAdd">新增</el-button>
      </div>
    </div>
    <el-table
        v-if="isTable"
        ref="multipleTable"
        v-if="viewMode === 'table'"
        ref="tableRef"
        v-loading="tableLoading"
        border
        :data="routeItems"
        :data="tableData"
        :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">
      <el-table-column align="center" label="序号" width="60" type="index" />
      <el-table-column label="工序名称" prop="processId" width="200">
        <template #default="scope">
          {{ scope.$index + 1 }}
          {{ getProcessName(scope.row.processId) || '-' }}
        </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>
      <el-table-column label="产品名称" prop="productName" min-width="160" />
      <el-table-column label="规格名称" prop="model" min-width="140" />
      <el-table-column label="单位" prop="unit" width="100" />
      <el-table-column label="操作" align="center" fixed="right" width="150">
        <template #default="scope">
          <el-button type="primary" link size="small" @click="handleEdit(scope.row)">编辑</el-button>
          <el-button type="danger" link size="small" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- ä½¿ç”¨æ™®é€šdiv替代el-steps -->
    <!-- å¡ç‰‡è§†å›¾ -->
    <template v-else>
      <div class="section-header">
        <div class="section-title">工艺路线项目列表</div>
        <div class="section-actions">
          <el-button
              icon="Menu"
              @click="toggleView"
              style="margin-right: 10px;"
          >
            è¡¨æ ¼è§†å›¾
          </el-button>
          <el-button type="primary" @click="handleAdd">新增</el-button>
        </div>
      </div>
      <div v-loading="tableLoading" class="card-container">
    <div
        v-else
        ref="stepsContainer"
        class="mb5 custom-steps"
            ref="cardsContainer"
            class="cards-wrapper"
    >
      <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;"
            v-for="(item, index) in tableData"
            :key="item.id || index"
            class="process-card"
            :data-index="index"
      >
        <div class="step-content">
          <div class="step-number">{{ index + 1 }}</div>
          <el-card
              :header="item.productName"
              class="step-card"
              style="cursor: move;"
          <!-- åºå·åœ†åœˆ -->
          <div class="card-header">
            <div class="card-number">{{ index + 1 }}</div>
            <div class="card-process-name">{{ getProcessName(item.processId) || '-' }}</div>
          </div>
          <!-- äº§å“ä¿¡æ¯ -->
          <div class="card-content">
            <div v-if="item.productName" class="product-info">
              <div class="product-name">{{ item.productName }}</div>
              <div v-if="item.model" class="product-model">
                {{ item.model }}
                <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> -->
              </div>
            </div>
            <div v-else class="product-info empty">暂无产品信息</div>
          </div>
          <!-- æ“ä½œæŒ‰é’® -->
          <div class="card-footer">
            <el-button type="primary" link size="small" @click="handleEdit(item)">编辑</el-button>
            <el-button type="danger" link size="small" @click="handleDelete(item)">删除</el-button>
          </div>
        </div>
      </div>
      </div>
    </template>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
        v-model="dialogVisible"
        :title="operationType === 'add' ? '新增工艺路线项目' : '编辑工艺路线项目'"
        width="500px"
        @close="closeDialog"
          >
            <div class="step-card-content">
              <p>{{ item.model }}</p>
              <p>{{ item.unit }}</p>
      <el-form
          ref="formRef"
          :model="form"
          :rules="rules"
          label-width="120px"
      >
        <el-form-item label="工序" prop="processId">
              <el-select
                  v-model="item.processId"
                  style="width: 100%;"
                  @mousedown.stop
              v-model="form.processId"
              placeholder="请选择工序"
              clearable
              style="width: 100%"
              >
                <el-option
                    v-for="process in processOptions"
@@ -114,20 +171,37 @@
                    :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>
        </el-form-item>
        <el-form-item label="产品名称" prop="productModelId">
          <el-button type="primary" @click="showProductSelectDialog = true">
            {{ form.productName && form.model
              ? `${form.productName} - ${form.model}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="单位" prop="unit">
          <el-input
              v-model="form.unit"
              :placeholder="form.productModelId ? '根据选择的产品自动带出' : '请先选择产品'"
              clearable
              :disabled="true"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="closeDialog">取消</el-button>
        <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¯¹è¯æ¡† -->
    <ProductSelectDialog
        v-model="isShowProductSelectDialog"
        @confirm="handelSelectProducts"
        v-model="showProductSelectDialog"
        @confirm="handleProductSelect"
        single
    />
  </div>
</template>
@@ -135,178 +209,285 @@
<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 { findProcessRouteItemList, addOrUpdateProcessRouteItem, sortProcessRouteItem, batchDeleteProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
import { findProductProcessRouteItemList, deleteRouteItem, addRouteItem, addOrUpdateProductProcessRouteItem, sortRouteItem } from "@/api/productionManagement/productProcessRoute.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);
import { useRoute } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import Sortable from 'sortablejs'
const route = useRoute()
const router = useRouter()
const routeId = computed({
  get() {
    return route.query.id;
  },
const { proxy } = getCurrentInstance() || {};
  set(val) {
    emit('update:router', val)
  }
const routeId = computed(() => route.query.id);
const orderId = computed(() => route.query.orderId);
const pageType = computed(() => route.query.type);
const tableLoading = ref(false);
const tableData = ref([]);
const dialogVisible = ref(false);
const operationType = ref('add'); // add | edit
const formRef = ref(null);
const submitLoading = ref(false);
const cardsContainer = ref(null);
const tableRef = ref(null);
const viewMode = ref('table'); // table | card
const routeInfo = ref({
  processRouteCode: '',
  productName: '',
  model: '',
  bomNo: '',
  description: ''
});
const processOptions = ref([]);
const showProductSelectDialog = ref(false);
let tableSortable = null;
let cardSortable = null;
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完全渲染
// åˆ‡æ¢è§†å›¾
const toggleView = () => {
  viewMode.value = viewMode.value === 'table' ? 'card' : 'table';
  // åˆ‡æ¢è§†å›¾åŽé‡æ–°åˆå§‹åŒ–拖拽排序
  nextTick(() => {
    // å¼ºåˆ¶é‡æ–°æ¸²æŸ“组件
    if (proxy?.$forceUpdate) {
      proxy.$forceUpdate();
    }
    const temp = [...routeItems.value];
    routeItems.value = [];
    nextTick(() => {
      routeItems.value = temp;
      initSortable();
    });
  });
};
const findProcessRouteItems = () => {
const form = ref({
  id: undefined,
  routeId: routeId.value,
  processId: undefined,
  productModelId: undefined,
  productName: "",
  model: "",
  unit: "",
});
const rules = {
  processId: [{ required: true, message: '请选择工序', trigger: 'change' }],
  productModelId: [{ required: true, message: '请选择产品', trigger: 'change' }],
};
// æ ¹æ®å·¥åºID获取工序名称
const getProcessName = (processId) => {
  if (!processId) return '';
  const process = processOptions.value.find(p => p.id === processId);
  return process ? process.name : '';
};
// èŽ·å–åˆ—è¡¨
const getList = () => {
  tableLoading.value = true;
  findProcessRouteItemList({ routeId: routeId.value })
  const listPromise =
    pageType.value === "order"
      ? findProductProcessRouteItemList({ orderId: orderId.value })
      : findProcessRouteItemList({ routeId: routeId.value });
  listPromise
      .then(res => {
      tableData.value = res.data || [];
        tableLoading.value = false;
        routeItems.value = res.data.map(item => ({
          ...item,
          processId: item.processId === 0 ? undefined : item.processId
        }));
        // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
      // åˆ—表加载完成后初始化拖拽排序
        nextTick(() => {
          setTimeout(() => initSortable(), 100);
        initSortable();
        });
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
      proxy?.$modal?.msgError("获取列表失败");
      });
};
const findProcessList = () => {
// èŽ·å–å·¥åºåˆ—è¡¨
const getProcessList = () => {
  processList({})
      .then(res => {
        processOptions.value = res.data;
      processOptions.value = res.data || [];
      })
      .catch(err => {
        console.error("获取工序失败:", err);
      });
};
const { proxy } = getCurrentInstance() || {};
// èŽ·å–å·¥è‰ºè·¯çº¿è¯¦æƒ…ï¼ˆä»Žè·¯ç”±å‚æ•°èŽ·å–ï¼‰
const getRouteInfo = () => {
  routeInfo.value = {
    processRouteCode: route.query.processRouteCode || '',
    productName: route.query.productName || '',
    model: route.query.model || '',
    bomNo: route.query.bomNo || '',
    description: route.query.description || ''
  };
};
const handleSubmit = () => {
  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
  if (hasEmptyProcess) {
    proxy?.$modal?.msgError("请为所有项目选择工序");
    return;
  }
// æ–°å¢ž
const handleAdd = () => {
  operationType.value = 'add';
  resetForm();
  dialogVisible.value = true;
};
  addOrUpdateProcessRouteItem({
// ç¼–辑
const handleEdit = (row) => {
  operationType.value = 'edit';
  form.value = {
    id: row.id,
    routeId: routeId.value,
    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
    processId: row.processId,
    productModelId: row.productModelId,
    productName: row.productName || "",
    model: row.model || "",
    unit: row.unit || "",
  };
  dialogVisible.value = true;
};
// åˆ é™¤
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该工艺路线项目?', '提示', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning'
  })
      .then(res => {
        router.push({
          path: '/productionManagement/processRoute',
    .then(() => {
      // ç”Ÿäº§è®¢å•下使用 productProcessRoute çš„删除接口(路由后拼接 id),其它情况使用工艺路线项目批量删除接口
      const deletePromise =
        pageType.value === 'order'
          ? deleteRouteItem(row.id)
          : batchDeleteProcessRouteItem([row.id]);
      deletePromise
        .then(() => {
          proxy?.$modal?.msgSuccess('删除成功');
          getList();
        })
        proxy?.$modal?.msgSuccess("提交成功");
        .catch(() => {
          proxy?.$modal?.msgError('删除失败');
        });
      })
      .catch(err => {
        proxy?.$modal?.msgError(`提交失败:${err.msg || "网络异常"}`);
    .catch(() => {});
};
// äº§å“é€‰æ‹©
const handleProductSelect = (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    form.value.productModelId = product.id;
    form.value.productName = product.productName;
    form.value.model = product.model;
    form.value.unit = product.unit || "";
    showProductSelectDialog.value = false;
    // è§¦å‘表单验证
    formRef.value?.validateField('productModelId');
  }
};
// æäº¤
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      submitLoading.value = true;
      if (operationType.value === 'add') {
        // æ–°å¢žï¼šä¼ å•个对象,包含dragSort字段
        // dragSort = å½“前列表长度 + 1,表示新增记录排在最后
        const dragSort = tableData.value.length + 1;
        const isOrderPage = pageType.value === 'order';
        const addPromise = isOrderPage
          ? addRouteItem({
              productOrderId: orderId.value,
              productRouteId: routeId.value,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
              dragSort,
            })
          : addOrUpdateProcessRouteItem({
              routeId: routeId.value,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
              dragSort,
            });
        addPromise
          .then(() => {
            proxy?.$modal?.msgSuccess('新增成功');
            closeDialog();
            getList();
          })
          .catch(() => {
            proxy?.$modal?.msgError('新增失败');
          })
          .finally(() => {
            submitLoading.value = false;
          });
      } else {
        // ç¼–辑:生产订单下使用 productProcessRoute/updateRouteItem,其它情况使用工艺路线项目更新接口
        const isOrderPage = pageType.value === 'order';
        const updatePromise = isOrderPage
          ? addOrUpdateProductProcessRouteItem({
              id: form.value.id,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
            })
          : addOrUpdateProcessRouteItem({
              routeId: routeId.value,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
              id: form.value.id,
            });
        updatePromise
          .then(() => {
            proxy?.$modal?.msgSuccess('修改成功');
            closeDialog();
            getList();
          })
          .catch(() => {
            proxy?.$modal?.msgError('修改失败');
          })
          .finally(() => {
            submitLoading.value = false;
          });
      }
    }
      });
};
const destroySortable = () => {
  if (tableSortable) {
    tableSortable.destroy();
    tableSortable = null;
  }
  if (stepsSortable) {
    stepsSortable.destroy();
    stepsSortable = null;
  }
// é‡ç½®è¡¨å•
const resetForm = () => {
  form.value = {
    id: undefined,
    routeId: routeId.value,
    processId: undefined,
    productModelId: undefined,
    productName: "",
    model: "",
    unit: "",
  };
  formRef.value?.resetFields();
};
// å…³é—­å¼¹çª—
const closeDialog = () => {
  dialogVisible.value = false;
  resetForm();
};
// åˆå§‹åŒ–拖拽排序
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 (viewMode.value === 'table') {
    // è¡¨æ ¼è§†å›¾çš„æ‹–拽排序
    if (!tableRef.value) return;
    const tbody = tableRef.value.$el.querySelector('.el-table__body tbody') ||
        tableRef.value.$el.querySelector('.el-table__body-wrapper > table > tbody');
    if (!tbody) return;
    tableSortable = new Sortable(tbody, {
@@ -315,189 +496,381 @@
      handle: '.el-table__row',
      filter: '.el-button, .el-select',
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        if (evt.oldIndex === evt.newIndex || !tableData.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);
        // é‡æ–°æŽ’序数组
        const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
        tableData.value.splice(evt.newIndex, 0, moveItem);
        // è®¡ç®—新的序号(dragSort从1开始)
        const newIndex = evt.newIndex;
        const dragSort = newIndex + 1;
        // è°ƒç”¨æŽ’序接口
        if (moveItem.id) {
          const isOrderPage = pageType.value === 'order';
          const sortPromise = isOrderPage
            ? sortRouteItem({
                id: moveItem.id,
                dragSort: dragSort
              })
            : sortProcessRouteItem({
                id: moveItem.id,
                dragSort: dragSort
              });
          sortPromise
            .then(() => {
              // æ›´æ–°æ‰€æœ‰è¡Œçš„dragSort
              tableData.value.forEach((item, index) => {
                if (item.id) {
                  item.dragSort = index + 1;
                }
              });
              proxy?.$modal?.msgSuccess('排序成功');
            })
            .catch((err) => {
              // æŽ’序失败,恢复原数组
              tableData.value.splice(newIndex, 1);
              tableData.value.splice(evt.oldIndex, 0, moveItem);
              proxy?.$modal?.msgError('排序失败');
              console.error("排序失败:", err);
            });
        }
      }
    });
  } else {
    if (!stepsContainer.value) return;
    // å¡ç‰‡è§†å›¾çš„æ‹–拽排序
    if (!cardsContainer.value) return;
    // ä¿®æ”¹ï¼šç›´æŽ¥ä½¿ç”¨stepsContainer.value作为拖拽容器
    const stepsList = stepsContainer.value;
    if (!stepsList) {
      console.warn('未找到步骤条拖拽容器');
      return;
    }
    // ä¿®æ”¹ï¼šç®€åŒ–拖拽配置
    stepsSortable = new Sortable(stepsList, {
    cardSortable = new Sortable(cardsContainer.value, {
      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,
      handle: '.process-card',
      filter: '.el-button',
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        if (evt.oldIndex === evt.newIndex || !tableData.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];
      }
        // é‡æ–°æŽ’序数组
        const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
        tableData.value.splice(evt.newIndex, 0, moveItem);
        // è®¡ç®—新的序号(dragSort从1开始)
        const newIndex = evt.newIndex;
        const dragSort = newIndex + 1;
        // è°ƒç”¨æŽ’序接口
        if (moveItem.id) {
          const isOrderPage = pageType.value === 'order';
          const sortPromise = isOrderPage
            ? sortRouteItem({
                id: moveItem.id,
                dragSort: dragSort
              })
            : sortProcessRouteItem({
                id: moveItem.id,
                dragSort: dragSort
    });
    // è°ƒè¯•:打印容器和实例,确认绑定成功
    console.log('步骤条拖拽容器:', stepsList);
    console.log('Sortable实例:', stepsSortable);
          sortPromise
            .then(() => {
              // æ›´æ–°æ‰€æœ‰è¡Œçš„dragSort
              tableData.value.forEach((item, index) => {
                if (item.id) {
                  item.dragSort = index + 1;
                }
              });
              proxy?.$modal?.msgSuccess('排序成功');
            })
            .catch((err) => {
              // æŽ’序失败,恢复原数组
              tableData.value.splice(newIndex, 1);
              tableData.value.splice(evt.oldIndex, 0, moveItem);
              proxy?.$modal?.msgError('排序失败');
              console.error("排序失败:", err);
            });
        }
      }
    });
  }
};
const handleViewChange = () => {
  destroySortable();
  // å»¶è¿Ÿåˆå§‹åŒ–,确保视图切换后DOM完全渲染
  nextTick(() => {
    setTimeout(() => initSortable(), 100);
  });
// é”€æ¯æ‹–拽排序
const destroySortable = () => {
  if (tableSortable) {
    tableSortable.destroy();
    tableSortable = null;
  }
  if (cardSortable) {
    cardSortable.destroy();
    cardSortable = null;
  }
};
onMounted(() => {
  findProcessRouteItems();
  findProcessList();
  getRouteInfo();
  getList();
  getProcessList();
});
onUnmounted(() => {
  destroySortable();
});
defineExpose({
  handleSubmit,
});
</script>
<style scoped>
.card-container {
  padding: 20px 0;
}
.cards-wrapper {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  padding: 10px 0;
  min-height: 200px;
}
.cards-wrapper::-webkit-scrollbar {
  height: 8px;
}
.cards-wrapper::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 4px;
}
.cards-wrapper::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 4px;
}
.cards-wrapper::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}
.process-card {
  flex-shrink: 0;
  width: 220px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 16px;
  display: flex;
  flex-direction: column;
  cursor: move;
  transition: all 0.3s;
}
.process-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform: translateY(-2px);
}
.card-header {
  text-align: center;
  margin-bottom: 12px;
}
.card-number {
  width: 36px;
  height: 36px;
  line-height: 36px;
  border-radius: 50%;
  background: #409eff;
  color: #fff;
  font-weight: bold;
  font-size: 16px;
  margin: 0 auto 8px;
}
.card-process-name {
  font-size: 14px;
  color: #333;
  font-weight: 500;
  word-break: break-all;
}
.card-content {
  flex: 1;
  margin-bottom: 12px;
  min-height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.product-info {
  font-size: 13px;
  color: #666;
  text-align: center;
  width: 100%;
}
.product-info.empty {
  color: #999;
  text-align: center;
  padding: 20px 0;
}
.product-name {
  margin-bottom: 6px;
  word-break: break-all;
  line-height: 1.5;
  text-align: center;
}
.product-model {
  color: #909399;
  font-size: 12px;
  word-break: break-all;
  line-height: 1.5;
  text-align: center;
}
.product-unit {
  margin-left: 4px;
  color: #409eff;
}
.card-footer {
  display: flex;
  justify-content: space-around;
  padding-top: 12px;
  border-top: 1px solid #f0f0f0;
}
.card-footer .el-button {
  padding: 0;
  font-size: 12px;
}
:deep(.sortable-ghost) {
  opacity: 0.6;
  opacity: 0.5;
  background-color: #f5f7fa !important;
}
:deep(.sortable-drag) {
  opacity: 0.8;
}
/* è¡¨æ ¼è§†å›¾æ ·å¼ */
:deep(.el-table__row) {
  transition: background-color 0.2s;
  cursor: move;
}
:deep(.el-table__row:hover) {
  background-color: #f9fafc !important;
}
:deep(.el-card__footer){
  padding: 0 !important;
/* åŒºåŸŸæ ‡é¢˜æ ·å¼ */
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}
.operate-button {
.section-title {
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  padding-left: 12px;
  position: relative;
  margin-bottom: 0;
}
.section-title::before {
  content: '';
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 16px;
  background: #409eff;
  border-radius: 2px;
}
.section-actions {
  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;
/* å·¥è‰ºè·¯çº¿ä¿¡æ¯å¡ç‰‡æ ·å¼ */
.route-info-card {
  margin-bottom: 20px;
  border: 1px solid #e4e7ed;
  background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
  border-radius: 8px;
  overflow: hidden;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤é¡¹æ ·å¼ */
.custom-step {
  cursor: move !important;
  padding: 8px;
  position: relative;
  transition: all 0.2s ease;
  flex: 0 0 auto;
  min-width: 220px;
  touch-action: none;
.route-info {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px;
  padding: 4px;
}
/* æ‹–拽悬浮样式,提示可拖拽 */
.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 {
.info-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 140px;
  background: #ffffff;
  border-radius: 6px;
  padding: 14px 16px;
  border: 1px solid #f0f2f5;
  transition: all 0.3s ease;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.step-card-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 10px;
.info-item:hover {
  border-color: #409eff;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
  transform: translateY(-1px);
}
/* è‡ªå®šä¹‰åºå·æ ·å¼ä¼˜åŒ– */
.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;
.info-item.full-width {
  grid-column: 1 / -1;
}
.info-label-wrapper {
  margin-bottom: 8px;
}
.info-label {
  display: inline-block;
  color: #909399;
  font-size: 12px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  padding: 2px 0;
  position: relative;
}
.info-label::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 20px;
  height: 2px;
  background: linear-gradient(90deg, #409eff, transparent);
  border-radius: 1px;
}
.info-value-wrapper {
  flex: 1;
}
.info-value {
  display: block;
  color: #303133;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.5;
  word-break: break-all;
}
</style>
src/views/productionManagement/productStructure/Detail/index.vue
@@ -1,30 +1,32 @@
<template>
  <div class="app-container">
    <el-button v-if="dataValue.isEdit"
    <PageHeader content="产品结构详情">
      <template #right-button>
        <el-button v-if="dataValue.isEdit && !isOrderPage"
               type="primary"
               @click="addItem"
               style="margin-bottom: 10px">添加
                   @click="addItem">添加
    </el-button>
    <el-button v-if="!dataValue.isEdit"
        <el-button v-if="!dataValue.isEdit && !isOrderPage"
               type="primary"
               @click="dataValue.isEdit = true"
               style="margin-bottom: 10px">编辑
                   @click="dataValue.isEdit = true">编辑
    </el-button>
    <el-button v-if="dataValue.isEdit"
        <el-button v-if="dataValue.isEdit && !isOrderPage"
               type="primary"
               @click="cancelEdit"
               style="margin-bottom: 10px">取消
                   @click="cancelEdit">取消
    </el-button>
    <el-button type="primary"
        <el-button v-if="!isOrderPage"
                   type="primary"
               :loading="dataValue.loading"
               @click="submit"
               :disabled="!dataValue.isEdit"
               style="margin-bottom: 10px">确认
                   :disabled="!dataValue.isEdit">确认
    </el-button>
      </template>
    </PageHeader>
    <el-table
        :data="tableData"
        border
        :preserve-expanded-content="false"
        :default-expand-all="true"
        style="width: 100%"
    >
      <el-table-column type="expand">
@@ -120,25 +122,10 @@
                  </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"
                  <el-button v-if="dataValue.isEdit"
                             type="danger"
                             text
                             @click="dataValue.dataList.splice($index, 1)">删除
                  </el-button>
@@ -148,10 +135,9 @@
          </el-form>
        </template>
      </el-table-column>
      <el-table-column label="产品编码" prop="productCode" />
      <el-table-column label="BOM编号" prop="bomNo" />
      <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"
@@ -170,6 +156,7 @@
  ref,
} from "vue";
import { queryList, add } from "@/api/productionManagement/productStructure.js";
import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
import { list } from "@/api/productionManagement/productionProcess";
import { ElMessage } from "element-plus";
import {useRoute, useRouter} from "vue-router";
@@ -195,6 +182,13 @@
  }
});
// ä»Žè·¯ç”±å‚数获取产品信息
const routeBomNo = computed(() => route.query.bomNo || '');
const routeProductName = computed(() => route.query.productName || '');
const routeProductModelName = computed(() => route.query.productModelName || '');
const routeOrderId = computed(() => route.query.orderId);
const pageType = computed(() => route.query.type);
const isOrderPage = computed(() => pageType.value === 'order' && routeOrderId.value);
const dataValue = reactive({
  dataList: [],
@@ -210,8 +204,7 @@
  {
    productName: "",
    model: "",
    unit: "",
    productCode: "",
    bomNo: "",
  }
])
@@ -221,12 +214,15 @@
};
const fetchData = async () => {
  if (isOrderPage.value) {
    // è®¢å•情况:使用订单的产品结构接口
    const { data } = await listProcessBom({ orderId: routeOrderId.value });
    dataValue.dataList = data || [];
  } else {
    // éžè®¢å•情况:使用原来的接口
  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;
    dataValue.dataList = data || [];
  }
};
const fetchProcessOptions = async () => {
@@ -242,6 +238,7 @@
      row[0].productName;
  dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
  dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
  dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || "";
  dataValue.showProductDialog = false;
};
@@ -251,7 +248,7 @@
        dataValue.loading = true;
        if (valid) {
          add({
            parentId: routeId.value,
            bomId: routeId.value,
            productStructureList: dataValue.dataList || [],
          }).then(res => {
            router.push({
@@ -277,7 +274,6 @@
    unitQuantity: 0,
    demandedQuantity: 0,
    unit: "",
    diskQuantity: 0,
  });
};
@@ -287,6 +283,16 @@
};
onMounted(() => {
  // ä»Žè·¯ç”±å‚数回显数据
  tableData[0].productName = routeProductName.value;
  tableData[0].model = routeProductModelName.value;
  tableData[0].bomNo = routeBomNo.value;
  // è®¢å•情况下禁用编辑
  if (isOrderPage.value) {
    dataValue.isEdit = false;
  }
  fetchData();
  fetchProcessOptions();
});
src/views/productionManagement/productStructure/index.vue
@@ -1,5 +1,9 @@
<template>
  <div class="app-container">
    <div style="text-align: right; margin-bottom: 10px;">
      <el-button type="primary" @click="handleAdd">新增</el-button>
      <el-button type="danger" plain @click="handleBatchDelete" :disabled="selectedRows.length === 0">删除</el-button>
    </div>
    <PIMTable
        rowKey="id"
        :column="tableColumn"
@@ -14,100 +18,323 @@
        <el-button
            type="primary"
            text
            @click="showDetail(row.id)">{{ row.productName }}
            @click="showDetail(row)">{{ row.bomNo }}
        </el-button>
      </template>
    </PIMTable>
    <StructureEdit v-if="showEdit" v-model:show-model="showEdit" :record="currentRow"/>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
        v-model="dialogVisible"
        :title="operationType === 'add' ? '新增BOM' : '编辑BOM'"
        width="600px"
        @close="closeDialog"
    >
      <el-form
          ref="formRef"
          :model="form"
          :rules="rules"
          label-width="120px"
      >
        <el-form-item label="产品名称" prop="productModelId">
          <el-button type="primary" @click="showProductSelectDialog = true">
            {{ form.productName || '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="版本号" prop="version">
          <el-input v-model="form.version" placeholder="请输入版本号" clearable />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input
              v-model="form.remark"
              type="textarea"
              :rows="3"
              placeholder="请输入备注"
              clearable
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="closeDialog">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¼¹çª— -->
    <ProductSelectDialog
        v-model="showProductSelectDialog"
        @confirm="handleProductSelect"
        single
    />
  </div>
</template>
<script setup>
import {ref} from "vue";
import {productModelList} from "@/api/basicData/productModel.js";
import { ref, reactive, toRefs, onMounted, getCurrentInstance, defineAsyncComponent } from "vue";
import { listPage, add, update, batchDelete } from "@/api/productionManagement/productBom.js";
import { useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
const router = useRouter()
const { proxy } = getCurrentInstance()
const StructureEdit = defineAsyncComponent(() => import('@/views/productionManagement/productStructure/StructureEdit.vue'))
const tableColumn = ref([
  {
    label: "产品编码",
    prop: "productCode",
    slot: "detail"
    label: "BOM编号",
    prop: "bomNo",
    dataType: 'slot',
    slot: "detail",
    minWidth: 140
  },
  {
    label: "产品名称",
    prop: "productName",
    dataType: 'slot',
    slot: "detail"
    minWidth: 160
  },
  {
    label: "规格型号",
    prop: "model",
    prop: "productModelName",
    minWidth: 140
  },
  {
    label: "单位",
    prop: "unit",
    label: "版本号",
    prop: "version",
    width: 100
  },
  {
    label: "备注",
    prop: "remark",
    minWidth: 160
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 150,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          handleEdit(row)
        }
      },
      {
        name: "删除",
        type: "danger",
        link: true,
        clickFun: (row) => {
          handleDelete(row)
        }
      }
    ]
  }
]);
const tableData = ref([]);
const tableLoading = ref(false);
const showEdit = ref(false);
const selectedRows = ref([]);
const currentRow = ref({});
const dialogVisible = ref(false);
const operationType = ref('add'); // add | edit
const formRef = ref(null);
const showProductSelectDialog = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const data = reactive({
  form: {
    id: undefined,
    productName: "",
    productModelName: "",
    productModelId: "",
    remark: "",
    version: ""
  },
  rules: {
    productName: [{required: true, message: "请输入", trigger: "blur"}],
  },
  modelForm: {
    otherModel: '',
    model: "",
    unit: "",
    speculativeTradingName: [],
  },
    productModelId: [{ required: true, message: "请选择产品", trigger: "change" }],
    version: [{ required: true, message: "请输入版本号", trigger: "blur" }]
  }
});
const {form, rules} = toRefs(data);
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æŸ¥è¯¢è§„格型号
// åˆ†é¡µ
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getModelList();
  getList();
};
const showDetail = (id) => {
  router.push({
    path: '/productionManagement/productStructureDetail',
    query: {
      id: id
    }
  })
}
const getModelList = () => {
// æŸ¥è¯¢åˆ—表
const getList = () => {
  tableLoading.value = true;
  productModelList({
  listPage({
    current: page.current,
    size: page.size,
  }).then((res) => {
    tableData.value = res.records;
    page.total = res.total;
  })
    .then((res) => {
      const records = res?.data?.records || [];
      tableData.value = records;
      page.total = res?.data?.total || 0;
    })
    .catch((err) => {
      console.error("获取列表失败:", err);
    })
    .finally(() => {
    tableLoading.value = false;
  });
};
onMounted(() => {
  getModelList();
// æ–°å¢ž
const handleAdd = () => {
  operationType.value = 'add';
  Object.assign(form.value, {
    id: undefined,
    productName: "",
    productModelName: "",
    productModelId: "",
    remark: "",
    version: ""
  });
  dialogVisible.value = true;
};
// ç¼–辑
const handleEdit = (row) => {
  operationType.value = 'edit';
  Object.assign(form.value, {
    id: row.id,
    productName: row.productName || "",
    productModelName: row.productModelName || "",
    productModelId: row.productModelId || "",
    remark: row.remark || "",
    version: row.version || ""
  });
  dialogVisible.value = true;
};
// åˆ é™¤ï¼ˆå•条)
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该BOM?', '提示', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning'
})
    .then(() => {
      batchDelete([row.id])
        .then(() => {
          proxy.$modal.msgSuccess('删除成功');
          getList();
        })
        .catch(() => {
          proxy.$modal.msgError('删除失败');
        });
    })
    .catch(() => {});
};
// æ‰¹é‡åˆ é™¤
const handleBatchDelete = () => {
  if (!selectedRows.value.length) {
    proxy.$modal.msgWarning('请选择数据');
    return;
  }
  const ids = selectedRows.value.map(item => item.id);
  ElMessageBox.confirm('选中的内容将被删除,是否确认删除?', '删除提示', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning'
  })
    .then(() => {
      batchDelete(ids)
        .then(() => {
          proxy.$modal.msgSuccess('删除成功');
          getList();
        })
        .catch(() => {
          proxy.$modal.msgError('删除失败');
        });
    })
    .catch(() => {});
};
// äº§å“é€‰æ‹©
const handleProductSelect = (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    form.value.productModelId = product.id;
    form.value.productName = product.productName;
    form.value.productModelName = product.model;
  }
  showProductSelectDialog.value = false;
};
// æäº¤è¡¨å•
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const payload = { ...form.value };
      if (operationType.value === 'add') {
        add(payload)
          .then(() => {
            proxy.$modal.msgSuccess('新增成功');
            closeDialog();
            getList();
          })
          .catch(() => {
            proxy.$modal.msgError('新增失败');
          });
      } else {
        update(payload)
          .then(() => {
            proxy.$modal.msgSuccess('修改成功');
            closeDialog();
            getList();
          })
          .catch(() => {
            proxy.$modal.msgError('修改失败');
          });
      }
    }
  });
};
// å…³é—­å¼¹çª—
const closeDialog = () => {
  dialogVisible.value = false;
  formRef.value?.resetFields();
};
// æŸ¥çœ‹è¯¦æƒ…
const showDetail = (row) => {
  router.push({
    path: '/productionManagement/productStructureDetail',
    query: {
      id: row.id,
      bomNo: row.bomNo || '',
      productName: row.productName || '',
      productModelName: row.productModelName || ''
    }
  });
};
onMounted(() => {
  getList();
});
</script>
src/views/productionManagement/productionOrder/ProcessRouteItemForm.vue
ÎļþÒÑɾ³ý
src/views/productionManagement/productionOrder/index.vue
@@ -8,7 +8,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="合同号:">
@@ -16,7 +16,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="产品名称:">
@@ -24,7 +24,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item label="规格:">
@@ -32,7 +32,7 @@
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    style="width: 160px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item>
@@ -50,12 +50,41 @@
                :tableData="tableData"
                :page="page"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
                @pagination="pagination">
        <template #completionStatus="{ row }">
          <el-progress
            :percentage="toProgressPercentage(row?.completionStatus)"
            :color="progressColor(toProgressPercentage(row?.completionStatus))"
            :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''"
          />
        </template>
      </PIMTable>
    </div>
    <process-route-item-form v-if="isShowItemModal"
                             v-model:visible="isShowItemModal"
                             :record="record"
                             @completed="getList" />
    <el-dialog v-model="bindRouteDialogVisible"
               title="绑定工艺路线"
               width="500px">
      <el-form label-width="90px">
        <el-form-item label="工艺路线">
          <el-select v-model="bindForm.routeId"
                     placeholder="请选择工艺路线"
                     style="width: 100%;"
                     :loading="bindRouteLoading">
            <el-option v-for="item in routeOptions"
                       :key="item.id"
                       :label="`${item.processRouteCode || ''}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="bindRouteDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary"
                     :loading="bindRouteSaving"
                     @click="handleBindRouteConfirm">ç¡® è®¤</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
@@ -63,30 +92,75 @@
  import { onMounted, ref } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import { productOrderListPage } from "@/api/productionManagement/productionOrder.js";
  import { useRouter } from "vue-router";
  import {
    productOrderListPage,
    listProcessRoute,
    bindingRoute,
    listProcessBom,
  } from "@/api/productionManagement/productionOrder.js";
  import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
  const { proxy } = getCurrentInstance();
  import ProcessRouteItemForm from "@/views/productionManagement/productionOrder/ProcessRouteItemForm.vue";
  const router = useRouter();
  const tableColumn = ref([
    {
      label: "生产订单号",
      prop: "npsNo",
      width: '120px',
    },
    {
      label: "销售合同号",
      prop: "salesContractNo",
      width: '150px',
    },
    {
      label: "客户名称",
      prop: "customerName",
      width: '200px',
    },
    {
      label: "产品名称",
      prop: "productCategory",
      width: '120px',
    },
    {
      label: "规格",
      prop: "specificationModel",
      width: '120px',
    },
    {
      label: "工艺路线编号",
      prop: "processRouteCode",
      width: '140px',
    },
    {
      label: "需求数量",
      prop: "quantity",
    },
    {
      label: "完成数量",
      prop: "completeQuantity",
    },
    {
      dataType: "slot",
      label: "完成进度",
      prop: "completionStatus",
      slot: "completionStatus",
      width: 180,
    },
    {
      label: "开始日期",
      prop: "startTime",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
      width: 120,
    },
    {
      label: "结束日期",
      prop: "endTime",
      formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
      width: 120,
    },
    {
      dataType: "action",
@@ -100,6 +174,21 @@
          type: "text",
          clickFun: row => {
            showRouteItemModal(row);
          },
        },
        {
          name: "绑定工艺路线",
          type: "text",
          showHide: row => !row.processRouteCode,
          clickFun: row => {
            openBindRouteDialog(row);
          },
        },
        {
          name: "产品结构",
          type: "text",
          clickFun: row => {
            showProductStructure(row);
          },
        },
      ],
@@ -123,6 +212,77 @@
    },
  });
  const { searchForm } = toRefs(data);
  const toProgressPercentage = val => {
    const n = Number(val);
    if (!Number.isFinite(n)) return 0;
    if (n <= 0) return 0;
    if (n >= 100) return 100;
    return Math.round(n);
  };
  // 30/50/80/100 åˆ†æ®µé¢œè‰²ï¼šçº¢/橙/蓝/绿
  const progressColor = percentage => {
    const p = toProgressPercentage(percentage);
    if (p < 30) return "#f56c6c";
    if (p < 50) return "#e6a23c";
    if (p < 80) return "#409eff";
    return "#67c23a";
  };
  // ç»‘定工艺路线弹框
  const bindRouteDialogVisible = ref(false);
  const bindRouteLoading = ref(false);
  const bindRouteSaving = ref(false);
  const routeOptions = ref([]);
  const bindForm = reactive({
    orderId: null,
    routeId: null,
  });
  const openBindRouteDialog = async row => {
    bindForm.orderId = row.id;
    bindForm.routeId = null;
    bindRouteDialogVisible.value = true;
    routeOptions.value = [];
    if (!row.productModelId) {
      proxy.$modal.msgWarning("当前订单缺少产品型号,无法查询工艺路线");
      bindRouteDialogVisible.value = false;
      return;
    }
    bindRouteLoading.value = true;
    try {
      const res = await listProcessRoute({ productModelId: row.productModelId });
      routeOptions.value = res.data || [];
    } catch (e) {
      console.error("获取工艺路线列表失败:", e);
      proxy.$modal.msgError("获取工艺路线列表失败");
    } finally {
      bindRouteLoading.value = false;
    }
  };
  const handleBindRouteConfirm = async () => {
    if (!bindForm.routeId) {
      proxy.$modal.msgWarning("请选择工艺路线");
      return;
    }
    bindRouteSaving.value = true;
    try {
      await bindingRoute({
        id: bindForm.orderId,
        routeId: bindForm.routeId,
      });
      proxy.$modal.msgSuccess("绑定成功");
      bindRouteDialogVisible.value = false;
      getList();
    } catch (e) {
      console.error("绑定工艺路线失败:", e);
      proxy.$modal.msgError("绑定工艺路线失败");
    } finally {
      bindRouteSaving.value = false;
    }
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
@@ -161,11 +321,46 @@
      });
  };
  const isShowItemModal = ref(false);
  const record = ref({});
  const showRouteItemModal = row => {
    isShowItemModal.value = true;
    record.value = row;
  const showRouteItemModal = async row => {
    const orderId = row.id;
    try {
      const res = await getOrderProcessRouteMain(orderId);
      const data = res.data || {};
      if (!data || !data.id) {
        proxy.$modal.msgWarning("未找到关联的工艺路线");
        return;
      }
      router.push({
        path: "/productionManagement/processRouteItem",
        query: {
          id: data.id,
          processRouteCode: data.processRouteCode || "",
          productName: data.productName || "",
          model: data.model || "",
          bomNo: data.bomNo || "",
          description: data.description || "",
          orderId,
          type: "order",
        },
      });
    } catch (e) {
      console.error("获取工艺路线主信息失败:", e);
      proxy.$modal.msgError("获取工艺路线信息失败");
    }
  };
  const showProductStructure = row => {
    router.push({
      path: "/productionManagement/productStructureDetail",
      query: {
        id: row.id,
        bomNo: row.bomNo || "",
        productName: row.productCategory || "",
        productModelName: row.specificationModel || "",
        orderId: row.id,
        type: "order",
      },
    });
  };
  // å¯¼å‡º
@@ -176,7 +371,7 @@
      type: "warning",
    })
      .then(() => {
        proxy.download("/salesLedger/scheduling/export", {}, "生产订单.xlsx");
        proxy.download("/productOrder/export", {...searchForm.value}, "生产订单.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -190,4 +385,7 @@
  });
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.search_form{
  align-items: start;
}</style>
src/views/productionManagement/productionProcess/Edit.vue
@@ -22,6 +22,9 @@
            ]">
          <el-input v-model="formState.name" />
        </el-form-item>
        <el-form-item label="工序编号" prop="no">
          <el-input v-model="formState.no"  />
        </el-form-item>
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
        </el-form-item>
@@ -40,7 +43,7 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance } from "vue";
import { ref, computed, getCurrentInstance, watch } from "vue";
import {update} from "@/api/productionManagement/productionProcess.js";
const props = defineProps({
@@ -61,6 +64,7 @@
const formState = ref({
  id: props.record.id,
  name: props.record.name,
  no: props.record.no,
  remark: props.record.remark,
  salaryQuota: props.record.salaryQuota,
});
@@ -74,6 +78,32 @@
  },
});
// ç›‘听 record å˜åŒ–,更新表单数据
watch(() => props.record, (newRecord) => {
  if (newRecord && isShow.value) {
    formState.value = {
      id: newRecord.id,
      name: newRecord.name || '',
      no: newRecord.no || '',
      remark: newRecord.remark || '',
      salaryQuota: newRecord.salaryQuota || '',
    };
  }
}, { immediate: true, deep: true });
// ç›‘听弹窗打开,重新初始化表单数据
watch(() => props.visible, (visible) => {
  if (visible && props.record) {
    formState.value = {
      id: props.record.id,
      name: props.record.name || '',
      no: props.record.no || '',
      remark: props.record.remark || '',
      salaryQuota: props.record.salaryQuota || '',
    };
  }
});
let { proxy } = getCurrentInstance()
const closeModal = () => {
src/views/productionManagement/productionProcess/New.vue
@@ -22,6 +22,9 @@
            ]">
          <el-input v-model="formState.name" />
        </el-form-item>
        <el-form-item label="工序编号" prop="no">
          <el-input v-model="formState.no"  />
        </el-form-item>
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
        </el-form-item>
src/views/productionManagement/productionProcess/index.vue
@@ -30,6 +30,7 @@
           class="mb10">
        <el-button type="primary"
                   @click="showNewModal">新增工序</el-button>
        <el-button type="info" plain @click="handleImport">导入</el-button>
        <el-button type="danger"
                   @click="handleDelete"
                   :disabled="selectedRows.length === 0"
@@ -52,14 +53,29 @@
                  v-model:visible="isShowEditModal"
                  :record="record"
                  @completed="getList" />
    <ImportDialog
      ref="importDialogRef"
      v-model="importDialogVisible"
      title="导入工序"
      :action="importAction"
      :headers="importHeaders"
      :auto-upload="false"
      :on-success="handleImportSuccess"
      :on-error="handleImportError"
      @confirm="handleImportConfirm"
      @download-template="handleDownloadTemplate"
      @close="handleImportClose"
    />
  </div>
</template>
<script setup>
  import { onMounted, ref } from "vue";
  import { onMounted, ref, reactive, toRefs, getCurrentInstance } 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";
  import ImportDialog from "@/components/Dialog/ImportDialog.vue";
  import { listPage, del, importData, downloadTemplate } from "@/api/productionManagement/productionProcess.js";
  import { getToken } from "@/utils/auth";
  const data = reactive({
    searchForm: {
@@ -70,13 +86,14 @@
  const { searchForm } = toRefs(data);
  const tableColumn = ref([
    {
      label: "工序名称",
      prop: "name",
    },
    {
      label: "工序编号",
      prop: "no",
    },
    {
      label: "工序名称",
      prop: "name",
    },
    {
      label: "工资定额",
      prop: "salaryQuota",
@@ -84,6 +101,10 @@
    {
      label: "备注",
      prop: "remark",
    },
     {
      label: "更新时间",
      prop: "updateTime",
    },
    {
      dataType: "action",
@@ -108,12 +129,18 @@
  const isShowNewModal = ref(false);
  const isShowEditModal = ref(false);
  const record = ref({});
  const importDialogVisible = ref(false);
  const importDialogRef = ref(null);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
  const { proxy } = getCurrentInstance();
  // å¯¼å…¥ç›¸å…³é…ç½®
  const importAction = import.meta.env.VITE_APP_BASE_API + "/productProcess/importData";
  const importHeaders = { Authorization: "Bearer " + getToken() };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
@@ -195,6 +222,63 @@
    }
  }
  // å¯¼å…¥
  const handleImport = () => {
    importDialogVisible.value = true;
  };
  // ç¡®è®¤å¯¼å…¥
  const handleImportConfirm = () => {
    if (importDialogRef.value) {
      importDialogRef.value.submit();
    }
  };
  // å¯¼å…¥æˆåŠŸ
  const handleImportSuccess = (response) => {
    if (response.code === 200) {
      proxy.$modal.msgSuccess("导入成功");
      importDialogVisible.value = false;
      if (importDialogRef.value) {
        importDialogRef.value.clearFiles();
      }
      getList();
    } else {
      proxy.$modal.msgError(response.msg || "导入失败");
    }
  };
  // å¯¼å…¥å¤±è´¥
  const handleImportError = (error) => {
    proxy.$modal.msgError("导入失败:" + (error.message || "未知错误"));
  };
  // å…³é—­å¯¼å…¥å¼¹çª—
  const handleImportClose = () => {
    if (importDialogRef.value) {
      importDialogRef.value.clearFiles();
    }
  };
  // ä¸‹è½½æ¨¡æ¿
  const handleDownloadTemplate = async () => {
    try {
      const res = await downloadTemplate();
      const blob = new Blob([res], {
        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      });
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = "工序导入模板.xlsx";
      link.click();
      window.URL.revokeObjectURL(url);
      proxy.$modal.msgSuccess("模板下载成功");
    } catch (error) {
      proxy.$modal.msgError("模板下载失败");
    }
  };
  // å¯¼å‡º
  // const handleOut = () => {
  //     ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
src/views/productionManagement/productionReporting/Input.vue
@@ -59,13 +59,21 @@
    prop: 'productNo',
  },
  {
    label: '产品型号',
    label: '投入产品名称',
    prop: 'productName',
  },
  {
    label: '投入产品型号',
    prop: 'model',
  },
  {
    label: '投入数量',
    prop: 'quantity',
  },
  {
    label: '单位',
    prop: 'unit',
  },
]
const isShow = computed({
src/views/productionManagement/productionReporting/index.vue
@@ -115,7 +115,7 @@
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             width="60">
                             >
              <template #default="scope">
                <el-button link
                           type="primary"
@@ -139,9 +139,6 @@
    <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>
@@ -157,7 +154,6 @@
  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: {
@@ -187,92 +183,52 @@
      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: "salesContractNo",
      width: 120,
    },
    {
      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: "productName",
      width: 120,
    },
    {
      label: "生产时间",
      label: "产品规格型号",
      prop: "productModelName",
      width: 120,
    },
    {
      label: "产出数量",
      prop: "quantity",
      width: 120,
    },
    // {
    //   label: "报废数量",
    //   prop: "scrapQuantity",
    //   width: 120,
    // },
    {
      label: "单位",
      prop: "unit",
      width: 120,
    },
    {
      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);
          },
        },
        {
@@ -449,13 +405,6 @@
  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;
  };