src/views/basicData/product/index.vue
@@ -1,87 +1,60 @@
<template>
  <div class="app-container product-view">
    <div class="left">
      <div>
        <el-input
          v-model="search"
          style="width: 210px"
          placeholder="输入关键字进行搜索"
          @change="searchFilter"
          @clear="searchFilter"
          clearable
          prefix-icon="Search"
        />
        <el-button
          type="primary"
          @click="openProDia('addOne')"
          style="margin-left: 10px"
          >新增产品大类</el-button
        >
    <div class="main-content">
      <div class="search-section">
        <el-form :inline="true" :model="queryForm" class="search-form">
          <el-form-item label="产品名称">
            <el-input
              v-model="queryForm.productName"
              placeholder="请输入产品名称"
              clearable
              style="width: 200px"
              @keyup.enter="handleSearch"
            />
          </el-form-item>
          <el-form-item label="图纸编号">
            <el-input
              v-model="queryForm.model"
              placeholder="请输入图纸编号"
              clearable
              style="width: 200px"
              @keyup.enter="handleSearch"
            />
          </el-form-item>
          <el-form-item label="产品属性">
            <el-select
              v-model="queryForm.productType"
              placeholder="请选择产品属性"
              clearable
              style="width: 150px"
            >
              <el-option label="自制" :value="1" />
              <el-option label="外购" :value="2" />
              <el-option label="委外" :value="3" />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="handleSearch" icon="Search">
              搜索
            </el-button>
            <el-button @click="handleReset" icon="Refresh">
              重置
            </el-button>
          </el-form-item>
        </el-form>
        <div class="action-buttons">
          <el-button type="primary" @click="openModelDia('add')" icon="Plus">
            新增产品
          </el-button>
          <el-button type="success" @click="handleImport" icon="Upload">
            导入
          </el-button>
          <el-button type="danger" @click="handleDelete" icon="Delete" plain>
            删除
          </el-button>
        </div>
      </div>
      <div ref="containerRef">
        <el-tree
          ref="tree"
          v-loading="treeLoad"
          :data="list"
          @node-click="handleNodeClick"
          :expand-on-click-node="false"
          default-expand-all
          :default-expanded-keys="expandedKeys"
          :draggable="true"
          :filter-node-method="filterNode"
          :props="{ children: 'children', label: 'label' }"
          highlight-current
          node-key="id"
          style="
            height: calc(100vh - 190px);
            overflow-y: scroll;
            scrollbar-width: none;
          "
        >
          <template #default="{ node, data }">
            <div class="custom-tree-node">
              <span>{{ node.label }}</span>
              <div>
                <el-button
                  type="primary"
                  link
                  @click="openProDia('edit', data)"
                >
                  编辑
                </el-button>
                <el-button type="primary" link @click="openProDia('add', data)">
                  添加产品
                </el-button>
                <el-button
                  v-if="!node.childNodes.length"
                  style="margin-left: 4px"
                  type="danger"
                  link
                  @click="remove(node, data)"
                >
                  删除
                </el-button>
              </div>
            </div>
          </template>
        </el-tree>
      </div>
    </div>
    <div class="right">
      <div style="margin-bottom: 10px" v-if="isShowButton">
        <el-button type="primary" @click="openModelDia('add')">
          新增规格型号
        </el-button>
        <ImportExcel @uploadSuccess="getModelList" />
        <el-button
          type="danger"
          @click="handleDelete"
          style="margin-left: 10px"
          plain
        >
          删除
        </el-button>
      </div>
      <PIMTable
        rowKey="id"
        :column="tableColumn"
@@ -93,119 +66,219 @@
        @pagination="pagination"
      ></PIMTable>
    </div>
    <el-dialog v-model="productDia" title="产品" width="400px">
      <el-form
        :model="form"
        label-width="140px"
        label-position="top"
        :rules="rules"
        ref="formRef"
      >
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品名称:" prop="productName">
              <el-input
                v-model="form.productName"
                placeholder="请输入产品名称"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeProDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog
    <FormDialog
      v-model="modelDia"
      title="规格型号"
      width="400px"
      title="产品信息"
      width="500px"
      @close="closeModelDia"
      @confirm="submitModelForm"
    >
      <el-form
        :model="modelForm"
        label-width="140px"
        label-position="top"
        label-width="100px"
        :rules="modelRules"
        ref="modelFormRef"
      >
        <el-row>
          <el-col :span="24">
            <el-form-item label="规格型号:" prop="model">
              <el-input
                v-model="modelForm.model"
                placeholder="请输入规格型号"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="单位:" prop="unit">
              <el-input
                v-model="modelForm.unit"
                placeholder="请输入单位"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="产品名称" prop="productName">
          <el-input
            v-model="modelForm.productName"
            placeholder="请输入产品名称"
            clearable
            maxlength="50"
            show-word-limit
          />
        </el-form-item>
        <el-form-item label="图纸编号" prop="model">
          <el-input
            v-model="modelForm.model"
            placeholder="请输入图纸编号"
            clearable
          />
        </el-form-item>
        <el-form-item label="单位" prop="unit">
          <el-select
            v-model="modelForm.unit"
            placeholder="请选择单位"
            clearable
            style="width: 100%"
          >
            <el-option label="件" value="件" />
            <el-option label="套" value="套" />
            <el-option label="台" value="台" />
          </el-select>
        </el-form-item>
        <el-form-item label="产品属性" prop="productType">
          <el-select
            v-model="modelForm.productType"
            placeholder="请选择产品属性"
            clearable
            style="width: 100%"
          >
            <el-option label="自制" :value="1" />
            <el-option label="外购" :value="2" />
            <el-option label="委外" :value="3" />
          </el-select>
        </el-form-item>
        <el-form-item label="工艺路线" prop="routeId">
          <el-select
            v-model="modelForm.routeId"
            placeholder="请选择工艺路线"
            clearable
            style="width: 100%"
            filterable
          >
            <el-option
              v-for="item in processRouteList"
              :key="item.id"
              :label="item.processRouteName"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="上传图纸" prop="drawingFile">
          <el-upload
            v-model:file-list="drawingFileList"
            :action="upload.url"
            :headers="upload.headers"
            :data="upload.data"
            :on-success="handleDrawingUploadSuccess"
            :on-remove="handleDrawingRemove"
            :before-upload="handleDrawingBeforeUpload"
            :limit="1"
            accept=".pdf,.jpg,.jpeg,.png,.dwg"
            list-type="picture-card"
          >
            <el-icon class="avatar-uploader-icon"><Plus /></el-icon>
            <template #tip>
              <div class="el-upload__tip">
                支持 pdf、jpg、jpeg、png、dwg 格式,大小不超过 10MB
              </div>
            </template>
          </el-upload>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitModelForm">确认</el-button>
          <el-button @click="closeModelDia">取消</el-button>
    </FormDialog>
    <FormDialog
      v-model="importDia"
      title="产品导入"
      width="600px"
      @cancel="importDia = false"
      @confirm="submitImport"
    >
      <el-upload
        ref="importUploadRef"
        :limit="1"
        accept=".xlsx,.xls"
        :action="importUpload.url"
        :headers="importUpload.headers"
        :before-upload="importUpload.beforeUpload"
        :on-success="importUpload.onSuccess"
        :on-error="importUpload.onError"
        :on-progress="importUpload.onProgress"
        :on-change="importUpload.onChange"
        :auto-upload="false"
        drag
      >
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">
          将文件拖到此处,或<em>点击上传</em>
        </div>
      </template>
    </el-dialog>
        <template #tip>
          <div class="el-upload__tip">
            仅支持 xls/xlsx,大小不超过 10MB。
            <el-button link type="primary" @click="importTemplate">下载导入模板</el-button>
          </div>
        </template>
      </el-upload>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref } from "vue";
import { ref, reactive, onMounted } from "vue";
import { ElMessageBox } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { getToken } from "@/utils/auth.js";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
  addOrEditProduct,
  addOrEditProductModel,
  delProduct,
  delProductModel,
  modelListPage,
  productTreeList,
  productListPage,
  downloadTemplate,
} from "@/api/basicData/product.js";
import { listPage as getProcessRouteList } from "@/api/productionManagement/processRoute.js";
import ImportExcel from "./ImportExcel/index.vue";
const { proxy } = getCurrentInstance();
const tree = ref(null);
const containerRef = ref(null);
const importUploadRef = ref(null);
const productDia = ref(false);
const modelDia = ref(false);
const importDia = ref(false);
const modelOperationType = ref("");
const search = ref("");
const currentId = ref("");
const currentParentId = ref("");
const operationType = ref("");
const treeLoad = ref(false);
const list = ref([]);
const expandedKeys = ref([]);
const tableData = ref([]);
const tableLoading = ref(false);
const selectedRows = ref([]);
const modelFormRef = ref();
const processRouteList = ref([]);
const drawingFileList = ref([]);
const upload = reactive({
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  headers: { Authorization: "Bearer " + getToken() },
  data: { type: 13 },
});
const queryForm = reactive({
  productName: "",
  model: "",
  productType: null,
});
const page = reactive({
  current: 1,
  size: 50,
  total: 0,
});
const tableColumn = ref([
  {
    label: "规格型号",
    label: "产品名称",
    prop: "productName",
    minWidth: 200,
  },
  {
    label: "图纸编号",
    prop: "model",
    minWidth: 150,
  },
  {
    label: "工艺路线",
    prop: "routeName",
    minWidth: 150,
  },
  {
    label: "单位",
    prop: "unit",
    minWidth: 100,
  },
  {
    label: "产品属性",
    prop: "productType",
    width: 100,
    dataType: "tag",
    formatData: (v) => ({ "1": "自制", "2": "外购", "3": "委外" }[String(v)] ?? v),
    formatType: (v) => {
      const typeMap = { "1": "success", "2": "warning", "3": "info" };
      return typeMap[String(v)] || "info";
    },
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    width: 100,
    operation: [
      {
        name: "编辑",
@@ -217,137 +290,123 @@
    ],
  },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const isShowButton = ref(false);
const selectedRows = ref([]);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const data = reactive({
  form: {
    productName: "",
  },
  rules: {
    productName: [{ required: true, message: "请输入", trigger: "blur" }],
  },
  modelForm: {
    productName: "",
    model: "",
    unit: "",
    productType: null,
    routeId: null,
    drawingFile: "",
    tempFileIds: [],
    salesLedgerFiles: [],
  },
  modelRules: {
    model: [{ required: true, message: "请输入", trigger: "blur" }],
    unit: [{ required: true, message: "请输入", trigger: "blur" }],
    productName: [
      { required: true, message: "请输入产品名称", trigger: "blur" },
      { max: 50, message: "产品名称不能超过50个字符", trigger: "blur" },
    ],
    model: [{ required: true, message: "请输入图纸编号", trigger: "blur" }],
    unit: [{ required: true, message: "请选择单位", trigger: "change" }],
    productType: [{ required: true, message: "请选择产品属性", trigger: "change" }],
  },
});
const { form, rules, modelForm, modelRules } = toRefs(data);
// 查询产品树
const getProductTreeList = () => {
  treeLoad.value = true;
  productTreeList()
    .then((res) => {
      list.value = res;
      list.value.forEach((a) => {
        expandedKeys.value.push(a.label);
      });
      treeLoad.value = false;
    })
    .catch((err) => {
      treeLoad.value = false;
    });
};
// 过滤产品树
const searchFilter = () => {
  proxy.$refs.tree.filter(search.value);
};
// 打开产品弹框
const openProDia = (type, data) => {
  operationType.value = type;
  productDia.value = true;
  form.value.productName = "";
  if (type === "edit") {
    form.value.productName = data.productName;
  }
};
// 打开规格型号弹框
const openModelDia = (type, data) => {
  modelOperationType.value = type;
  modelDia.value = true;
  modelForm.value.model = "";
  modelForm.value.model = "";
  modelForm.value.id = "";
  if (type === "edit") {
    modelForm.value = { ...data };
  }
};
// 提交产品名称修改
const submitForm = () => {
  proxy.$refs.formRef.validate((valid) => {
    if (valid) {
      if (operationType.value === "add") {
        form.value.parentId = currentId.value;
        form.value.id = "";
      } else if (operationType.value === "addOne") {
        form.value.id = "";
        form.value.parentId = "";
      } else {
        form.value.id = currentId.value;
        form.value.parentId = "";
      }
      addOrEditProduct(form.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeProDia();
        getProductTreeList();
      });
const { modelForm, modelRules } = toRefs(data);
const importUpload = reactive({
  title: "产品导入",
  open: false,
  url: import.meta.env.VITE_APP_BASE_API + "/basic/product/import",
  headers: { Authorization: "Bearer " + getToken() },
  isUploading: false,
  beforeUpload: (file) => {
    const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
    const isLt10M = file.size / 1024 / 1024 < 10;
    if (!isExcel) {
      proxy.$modal.msgError("上传文件只能是 xlsx/xls 格式!");
      return false;
    }
  });
};
// 关闭产品弹框
const closeProDia = () => {
  proxy.$refs.formRef.resetFields();
  productDia.value = false;
};
// 删除产品
const remove = (node, data) => {
  let ids = [];
  ids.push(data.id);
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      tableLoading.value = true;
      delProduct(ids)
        .then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getProductTreeList();
        })
        .finally(() => {
          tableLoading.value = false;
        });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
// 选择产品
const handleNodeClick = (val, node, el) => {
  // 判断是否为叶子节点
  isShowButton.value = !(val.children && val.children.length > 0);
  // 只有叶子节点才执行以下逻辑
  currentId.value = val.id;
  currentParentId.value = val.parentId;
    if (!isLt10M) {
      proxy.$modal.msgError("上传文件大小不能超过 10MB!");
      return false;
    }
    return true;
  },
  onChange: (file, fileList) => {
    console.log('文件状态改变', file, fileList);
  },
  onProgress: (event, file, fileList) => {
    console.log('上传中...', event.percent);
  },
  onSuccess: (response, file, fileList) => {
    console.log('上传成功', response, file, fileList);
    importUpload.isUploading = false;
    if (response.code === 200) {
      proxy.$modal.msgSuccess("导入成功");
      importDia.value = false;
      if (importUploadRef.value) {
        importUploadRef.value.clearFiles();
      }
      getModelList();
    } else {
      proxy.$modal.msgError(response.msg || "导入失败");
    }
  },
  onError: (error, file, fileList) => {
    console.log('上传失败', error, file, fileList);
    importUpload.isUploading = false;
    proxy.$modal.msgError("导入失败");
  }
});
const handleSearch = () => {
  page.current = 1;
  getModelList();
};
// 提交规格型号修改
const handleReset = () => {
  queryForm.productName = "";
  queryForm.model = "";
  queryForm.productType = null;
  page.current = 1;
  getModelList();
};
const openModelDia = (type, data) => {
  modelOperationType.value = type;
  modelDia.value = true;
  modelForm.value.productName = "";
  modelForm.value.model = "";
  modelForm.value.id = "";
  modelForm.value.unit = "";
  modelForm.value.productType = null;
  modelForm.value.routeId = null;
  modelForm.value.drawingFile = "";
  modelForm.value.tempFileIds = [];
  modelForm.value.salesLedgerFiles = [];
  drawingFileList.value = [];
  if (type === "edit") {
    modelForm.value = { ...data };
    modelForm.value.tempFileIds = data.tempFileIds || [];
    modelForm.value.salesLedgerFiles = data.salesLedgerFiles || [];
    if (data.salesLedgerFiles && data.salesLedgerFiles.length > 0) {
      drawingFileList.value = data.salesLedgerFiles.map(file => ({
        name: file.name,
        url: file.url
      }));
    } else if (data.drawingFile) {
      drawingFileList.value = [{
        name: data.drawingFile.split('/').pop(),
        url: data.drawingFile
      }];
    }
  }
};
const submitModelForm = () => {
  proxy.$refs.modelFormRef.validate((valid) => {
  modelFormRef.value.validate((valid) => {
    if (valid) {
      modelForm.value.productId = currentId.value;
      addOrEditProductModel(modelForm.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeModelDia();
@@ -356,36 +415,39 @@
    }
  });
};
// 关闭型号弹框
const closeModelDia = () => {
  proxy.$refs.modelFormRef.resetFields();
  modelFormRef.value.resetFields();
  modelDia.value = false;
};
// 表格选择数据
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// 查询规格型号
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getModelList();
};
const getModelList = () => {
  tableLoading.value = true;
  modelListPage({
    id: currentId.value,
  productListPage({
    productName: queryForm.productName.trim(),
    model: queryForm.model.trim(),
    productType: queryForm.productType,
    current: page.current,
    size: page.size,
  }).then((res) => {
    console.log("res", res);
    tableData.value = res.records;
    page.total = res.total;
    tableData.value = res.data.records;
    page.total = res.data.total;
    tableLoading.value = false;
  }).catch(() => {
    tableLoading.value = false;
  });
};
// 删除规格型号
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
@@ -401,7 +463,7 @@
  })
    .then(() => {
      tableLoading.value = true;
      delProductModel(ids)
      delProduct(ids)
        .then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getModelList();
@@ -414,66 +476,167 @@
      proxy.$modal.msg("已取消");
    });
};
// 调用tree过滤方法 中文英过滤
const filterNode = (value, data, node) => {
  if (!value) {
    //如果数据为空,则返回true,显示所有的数据项
    return true;
const handleImport = () => {
  importDia.value = true;
  if (importUploadRef.value) {
    importUploadRef.value.clearFiles();
  }
  // 查询列表是否有匹配数据,将值小写,匹配英文数据
  let val = value.toLowerCase();
  return chooseNode(val, data, node); // 调用过滤二层方法
};
// 过滤父节点 / 子节点 (如果输入的参数是父节点且能匹配,则返回该节点以及其下的所有子节点;如果参数是子节点,则返回该节点的父节点。name是中文字符,enName是英文字符.
const chooseNode = (value, data, node) => {
  if (data.label.indexOf(value) !== -1) {
    return true;
  }
  const level = node.level;
  // 如果传入的节点本身就是一级节点就不用校验了
  if (level === 1) {
const submitImport = () => {
  importUploadRef.value.submit();
};
const importTemplate = () => {
  proxy.download("/basic/product/downloadTemplate", {}, "产品导入模板.xlsx");
};
const getProcessRouteListData = () => {
  getProcessRouteList({ current: 1, size: 1000 }).then((res) => {
    processRouteList.value = res.data.records || [];
  }).catch(() => {
    processRouteList.value = [];
  });
};
const handleDrawingBeforeUpload = (file) => {
  const isAllowed = [
    'application/pdf',
    'image/jpeg',
    'image/jpg',
    'image/png',
    'application/dwg'
  ].includes(file.type) || file.name.endsWith('.dwg');
  const isLt10M = file.size / 1024 / 1024 < 10;
  if (!isAllowed) {
    proxy.$modal.msgError("只能上传 pdf、jpg、jpeg、png、dwg 格式的文件!");
    return false;
  }
  // 先取当前节点的父节点
  let parentData = node.parent;
  // 遍历当前节点的父节点
  let index = 0;
  while (index < level - 1) {
    // 如果匹配到直接返回,此处name值是中文字符,enName是英文字符。判断匹配中英文过滤
    if (parentData.data.label.indexOf(value) !== -1) {
      return true;
    }
    // 否则的话再往上一层做匹配
    parentData = parentData.parent;
    index++;
  if (!isLt10M) {
    proxy.$modal.msgError("上传文件大小不能超过 10MB!");
    return false;
  }
  // 没匹配到返回false
  return false;
  return true;
};
getProductTreeList();
const handleDrawingUploadSuccess = (response, file, fileList) => {
  console.log('上传成功响应', response);
  console.log('response.data', response.data);
  if (response.code === 200) {
    modelForm.value.tempFileIds = [response.data?.tempId];
    modelForm.value.salesLedgerFiles = [{
      tempId: response.data?.tempId,
      originalName: response.data?.originalName || file.name,
      tempPath: response.data?.tempPath,
      type: response.data?.type || 13
    }];
    proxy.$modal.msgSuccess("上传成功");
  } else {
    proxy.$modal.msgError(response.msg || "上传失败");
  }
};
const handleDrawingRemove = (file) => {
  modelForm.value.tempFileIds = [];
  modelForm.value.salesLedgerFiles = [];
};
onMounted(() => {
  getModelList();
  getProcessRouteListData();
});
</script>
<style scoped>
.product-view {
  display: flex;
  padding: 20px;
  background: #f5f7fa;
  min-height: 100vh;
}
.left {
  width: 380px;
  padding: 16px;
.main-content {
  background: #ffffff;
  border-radius: 8px;
  padding: 24px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.right {
  width: calc(100% - 380px);
  padding: 16px;
  margin-left: 20px;
  background: #ffffff;
.search-section {
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid #e4e7ed;
}
.custom-tree-node {
  flex: 1;
.search-form {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  padding-right: 8px;
}
.search-form :deep(.el-form-item) {
  margin-bottom: 12px;
}
.action-buttons {
  display: flex;
  gap: 10px;
  margin-top: 16px;
}
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 148px;
  height: 148px;
  text-align: center;
  line-height: 148px;
}
:deep(.el-upload--picture-card) {
  width: 148px;
  height: 148px;
}
:deep(.el-upload-list__item) {
  width: 148px;
  height: 148px;
}
:deep(.el-upload__tip) {
  font-size: 12px;
  color: #909399;
  margin-top: 8px;
}
:deep(.el-dialog__body) {
  padding: 20px 24px;
}
:deep(.el-form-item__label) {
  font-weight: 500;
  color: #303133;
}
:deep(.el-input__inner) {
  border-radius: 4px;
}
:deep(.el-button) {
  border-radius: 4px;
  font-weight: 500;
}
:deep(.el-upload-dragger) {
  border-radius: 8px;
  border: 2px dashed #dcdfe6;
  transition: all 0.3s;
}
:deep(.el-upload-dragger:hover) {
  border-color: #409eff;
  background-color: #f5f7fa;
}
</style>