zhangwencui
2 天以前 8e70dcadbe5a0fe2526e019607eaf8fcd8f1fc88
src/views/basicData/product/index.vue
@@ -2,37 +2,57 @@
  <div class="app-container product-view">
    <div class="left">
      <div>
        <el-input
            v-model="search"
            style="width: 240px"
            placeholder="输入关键字进行搜索"
            @change="searchFilter"
            @clear="searchFilter"
            clearable
            prefix-icon="Search"
        />
        <el-button type="primary" @click="openProDia('add')" style="margin-left: 10px">新增产品</el-button>
        <el-input v-model="search"
                  style="width: 210px"
                  placeholder="输入关键字进行搜索"
                  @input="searchFilter"
                  @change="searchFilter"
                  @clear="searchFilter"
                  clearable
                  prefix-icon="Search" />
        <el-button type="primary"
                   @click="openProDia('addOne')"
                   style="margin-left: 10px">新增产品大类</el-button>
      </div>
      <div>
        <el-tree ref="tree" v-loading="treeLoad" :data="list"
                 :default-expanded-keys="expandedKeys" :draggable="true" :filter-node-method="filterNode"
                 :props="{ children: 'children', label: 'label' }" highlight-current node-key="label"
                 style="height: calc(100vh - 190px);overflow-y: scroll;scrollbar-width: none;">
      <div ref="containerRef">
        <el-tree ref="tree"
                 v-loading="treeLoad"
                 :data="list"
                 @node-click="handleNodeClick"
                 :expand-on-click-node="false"
                 :default-expanded-keys="expandedKeys"
                 :filter-node-method="filterNode"
                 :props="{ children: 'children', label: 'label' }"
                 highlight-current
                 node-key="id"
                 class="product-tree-scroll"
                 style="height: calc(100vh - 190px); overflow-y: auto">
          <template #default="{ node, data }">
            <div class="custom-tree-node">
              <span>{{ node.label }}</span>
              <span class="tree-node-content">
                <el-icon class="orange-icon">
                  <component :is="data.children && data.children.length > 0
                  ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
                </el-icon>
                <span class="tree-node-label">{{ data.label }}</span>
              </span>
              <div>
                <el-button type="primary" link @click="openProDia('edit', data)">
                <el-button type="primary"
                           link
                           @click="openProDia('edit', data)">
                  编辑
                </el-button>
                <!-- 修改此处 -->
                <el-button
                    v-if="!node.childNodes.length"
                    style="margin-left: 4px"
                    type="danger"
                    link
                    @click="remove(node, data)"
                >
                <el-button type="primary"
                           link
                           @click="openProDia('add', data)"
                           :disabled="node.level >= 3">
                  添加产品
                </el-button>
                <el-button v-if="!node.childNodes.length"
                           style="margin-left: 4px"
                           type="danger"
                           link
                           @click="remove(node, data)">
                  删除
                </el-button>
              </div>
@@ -42,27 +62,134 @@
      </div>
    </div>
    <div class="right">
      <div style="margin-bottom: 10px">
        <el-button type="primary" @click="modelDia = true">新增规格型号</el-button>
        <el-button type="danger" @click="handleDelete" style="margin-left: 10px" plain>删除</el-button>
      <div style="margin-bottom: 10px"
           v-if="isShowButton">
        <el-button type="primary"
                   @click="openModelDia('add')">
          新增规格型号
        </el-button>
        <ImportExcel :product-id="currentId"
                     @uploadSuccess="getModelList" />
        <el-button type="danger"
                   @click="handleDelete"
                   style="margin-left: 10px"
                   plain>
          删除
        </el-button>
      </div>
      <PIMTable :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" :handleSelectionChange="handleSelectionChange"
                :tableLoading="tableLoading" @pagination="pagination" :total="total"></PIMTable>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
    </div>
    <el-dialog :visible.sync="productDia" title="产品" width="400px">
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
    <el-dialog v-model="productDia"
               title="产品"
               width="400px"
               @keydown.enter.prevent>
      <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 label="产品名称:"
                          prop="productName">
              <el-input v-model="form.productName"
                        placeholder="请输入产品名称"
                        maxlength="20"
                        show-word-limit
                        clearable
                        @keydown.enter.prevent />
            </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 type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeProDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog v-model="modelDia"
               title="规格型号"
               width="400px"
               @close="closeModelDia"
               @keydown.enter.prevent>
      <el-form :model="modelForm"
               label-width="140px"
               label-position="top"
               :rules="modelRules"
               ref="modelFormRef">
        <el-row>
          <el-col :span="24">
            <el-form-item label="规格型号:"
                          prop="model">
              <el-input v-model="modelForm.model"
                        placeholder="请输入规格型号"
                        clearable
                        @keydown.enter.prevent />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="厚度:"
                          prop="thickness">
              <el-input v-model="modelForm.thickness"
                        placeholder="请输入厚度"
                        clearable
                        @keydown.enter.prevent
                        @blur="modelForm.thickness = formatThicknessTo15(modelForm.thickness)" />
            </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
                        @keydown.enter.prevent />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitModelForm">确认</el-button>
          <el-button @click="closeModelDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- 二维码对话框 -->
    <el-dialog v-model="qrCodeDialog"
               title="产品二维码"
               width="400px">
      <div class="qrcode-container">
        <img v-if="qrCodeUrl"
             :src="qrCodeUrl"
             class="qrcode-image" />
        <div v-else
             class="loading">生成中...</div>
      </div>
      <div style="text-align: center;">
        {{ qrCodeName }}
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="qrCodeDialog = false">关闭</el-button>
          <el-button type="primary"
                     @click="saveQrCodeAsImage"
                     :disabled="!qrCodeUrl">保存为图片</el-button>
        </div>
      </template>
    </el-dialog>
@@ -70,192 +197,470 @@
</template>
<script setup>
import {ref} from "vue";
import {listCustomer} from "@/api/basicData/customerFile.js";
import {ElMessageBox} from "element-plus";
import {productTreeList} from "@/api/basicData/product.js";
const { proxy } = getCurrentInstance()
  import { ref, getCurrentInstance, toRefs, reactive } from "vue";
  import { ElMessageBox } from "element-plus";
  import QRCode from "qrcode";
  import { saveAs } from "file-saver";
  import {
    addOrEditProduct,
    addOrEditProductModel,
    delProduct,
    delProductModel,
    modelListPage,
    productTreeList,
  } from "@/api/basicData/product.js";
  import ImportExcel from "./ImportExcel/index.vue";
const search = ref('')
const treeLoad = ref(false)
const list = ref([])
const expandedKeys = ref([])
const tableColumn = ref([
  {
    label: '规格型号',
    prop: 'model',
  },
  {
    label: '单位',
    prop: 'model',
  },
  {
    dataType: "action",
    label: "操作",
    align: 'center',
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm('edit', row);
  const { proxy } = getCurrentInstance();
  const tree = ref(null);
  const containerRef = ref(null);
  const productDia = ref(false);
  const modelDia = ref(false);
  const qrCodeDialog = ref(false);
  const qrCodeUrl = ref("");
  const currentProductId = ref("");
  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 tableColumn = ref([
    {
      label: "规格型号",
      prop: "model",
    },
    {
      label: "厚度",
      prop: "thickness",
      // 列表展示时统一保留 15 位小数
      formatData: val => formatThicknessTo15(val),
    },
    {
      label: "单位",
      prop: "unit",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      operation: [
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            openModelDia("edit", row);
          },
        },
      },
    ],
  },
])
const tableData = ref([])
const tableLoading = ref(false)
const total = ref(0)
const selectedRows = ref([])
const page = reactive({
  current: 1,
  size: 10,
})
const productDia = ref(false)
const modelDia = ref(false)
const data = reactive({
  form: {
    productName: '',
  },
  rules: {
    productName: [{ required: true, message: "请输入", trigger: "blur" }],
  }
})
const { form, rules } = 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) => {
  productDia.value = true
  console.log('openEditDia', data)
}
const submitForm = () => {
}
const closeProDia = () => {
  proxy.$refs.formRef.resetFields();
  productDia.value = false;
}
const remove = (value) => {
        {
          name: "生成二维码",
          type: "text",
          clickFun: row => {
            generateQrcode(row);
          },
        },
      ],
    },
  ]);
  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" },
        { max: 20, message: "产品名称不能超过20个字符", trigger: "blur" },
      ],
    },
    modelForm: {
      model: "",
      thickness: "",
    },
    modelRules: {
      model: [{ required: true, message: "请输入", trigger: "blur" }],
      thickness: [{ required: true, message: "请输入", trigger: "blur" }],
    },
  });
  const { form, rules, modelForm, modelRules } = toRefs(data);
}
// 表格选择数据
const handleSelectionChange = (selection) => {
  selectedRows.value = selection
}
// 删除
const handleDelete = () => {
  let ids = []
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map(item => item.id);
  } else {
    proxy.$modal.msgWarning('请选择数据')
    return
  }
  ElMessageBox.confirm(
      '选中的内容将被删除,是否确认删除?',
      '删除提示', {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type: 'warning',
      }
  ).then(() => {
    tableLoading.value = true
    delCustomer(ids).then(res => {
      proxy.$modal.msgSuccess("删除成功")
      getList()
    }).finally(() => {
      tableLoading.value = false
    })
  }).catch(() => {
    proxy.$modal.msg("已取消")
  })
}
// 查询规格型号
const pagination = ({ current, limit }) => {
  page.current = current;
  page.size = limit;
  getList()
}
const getList = () => {
  tableLoading.value = true
  listCustomer({...searchForm.value, ...page}).then(res => {
    tableLoading.value = false
    tableData.value = res.rows
    total.value = res.total
  })
}
// 调用tree过滤方法 中文英过滤
const filterNode = (value, data, node) => {
  if (!value) {    //如果数据为空,则返回true,显示所有的数据项
    return true
  }
  // 查询列表是否有匹配数据,将值小写,匹配英文数据
  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) {
    return false
  }
  // 先取当前节点的父节点
  let parentData = node.parent
  // 遍历当前节点的父节点
  let index = 0
  while (index < level - 1) {
    // 如果匹配到直接返回,此处name值是中文字符,enName是英文字符。判断匹配中英文过滤
    if (parentData.data.label.indexOf(value) !== -1) {
      return true
  // 把厚度格式化成固定 15 位小数(用于展示/提交)
  const formatThicknessTo15 = val => {
    if (val === null || val === undefined) return "";
    const s = String(val).trim();
    if (s === "") return "";
    const n = Number(s);
    if (Number.isNaN(n)) return s;
    return n.toFixed(15);
  };
  // 查询产品树
  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;
    }
    // 否则的话再往上一层做匹配
    parentData = parentData.parent
    index++
  }
  // 没匹配到返回false
  return false
}
getProductTreeList()
  };
  // 打开规格型号弹框
  const openModelDia = (type, data) => {
    modelOperationType.value = type;
    modelDia.value = true;
    modelForm.value.model = "";
    modelForm.value.id = "";
    modelForm.value.thickness = "";
    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 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 fatherName = ref("");
  // 选择产品
  const handleNodeClick = (val, node, el) => {
    // 判断是否为叶子节点
    isShowButton.value = !(val.children && val.children.length > 0);
    // 只有叶子节点才执行以下逻辑
    currentId.value = val.id;
    currentParentId.value = val.parentId;
    fatherName.value = val.label;
    getModelList();
  };
  // 提交规格型号修改
  const submitModelForm = () => {
    proxy.$refs.modelFormRef.validate(valid => {
      if (valid) {
        modelForm.value.productId = currentId.value;
        modelForm.value.thickness = formatThicknessTo15(
          modelForm.value.thickness
        );
        addOrEditProductModel(modelForm.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeModelDia();
          getModelList();
        });
      }
    });
  };
  // 关闭型号弹框
  const closeModelDia = () => {
    proxy.$refs.modelFormRef.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,
      current: page.current,
      size: page.size,
    }).then(res => {
      console.log("res", res);
      tableData.value = res.records;
      page.total = res.total;
      tableLoading.value = false;
    });
  };
  // 删除规格型号
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        tableLoading.value = true;
        delProductModel(ids)
          .then(res => {
            proxy.$modal.msgSuccess("删除成功");
            getModelList();
          })
          .finally(() => {
            tableLoading.value = false;
          });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // 调用tree过滤方法 中文英过滤
  const filterNode = (value, data, node) => {
    if (!value) {
      //如果数据为空,则返回true,显示所有的数据项
      return true;
    }
    // 关键字与节点 label 均按小写比较,英文模糊匹配不区分大小写(如 LOW / low 可匹配 Low-E)
    const val = String(value).trim().toLowerCase();
    if (!val) return true;
    return chooseNode(val, data, node); // 调用过滤二层方法
  };
  // 过滤父节点 / 子节点 (如果输入的参数是父节点且能匹配,则返回该节点以及其下的所有子节点;如果参数是子节点,则返回该节点的父节点。name是中文字符,enName是英文字符.
  const chooseNode = (value, data, node) => {
    const labelLower = String(data.label ?? "").toLowerCase();
    if (labelLower.indexOf(value) !== -1) {
      return true;
    }
    const level = node.level;
    // 如果传入的节点本身就是一级节点就不用校验了
    if (level === 1) {
      return false;
    }
    // 先取当前节点的父节点
    let parentData = node.parent;
    // 遍历当前节点的父节点
    let index = 0;
    while (index < level - 1) {
      // 如果匹配到直接返回;与 filterNode 一致,标签与关键字均按小写比较,英文模糊匹配不区分大小写
      const parentLabelLower = String(parentData.data.label ?? "").toLowerCase();
      if (parentLabelLower.indexOf(value) !== -1) {
        return true;
      }
      // 否则的话再往上一层做匹配
      parentData = parentData.parent;
      index++;
    }
    // 没匹配到返回false
    return false;
  };
  const qrCodeName = ref("");
  // 生成二维码
  const generateQrcode = async row => {
    try {
      currentProductId.value = row.id;
      qrCodeName.value = fatherName.value + "-" + row.model;
      // 使用row.id生成二维码
      const qrCodeData = JSON.stringify({
        id: row.id,
        productName: fatherName.value,
        model: row.model,
        unit: row.unit,
      });
      // 生成二维码URL
      qrCodeUrl.value = await QRCode.toDataURL(qrCodeData, {
        width: 300,
        margin: 1,
      });
      // 打开二维码对话框
      qrCodeDialog.value = true;
    } catch (error) {
      console.error("生成二维码失败:", error);
      proxy.$modal.msgError("生成二维码失败");
    }
  };
  // 保存二维码为图片
  const saveQrCodeAsImage = () => {
    if (!qrCodeUrl.value) return;
    try {
      // 从Data URL创建Blob
      const blob = dataURLToBlob(qrCodeUrl.value);
      // 使用file-saver保存图片
      saveAs(blob, `${qrCodeName.value}.png`);
      proxy.$modal.msgSuccess("保存成功");
    } catch (error) {
      console.error("保存图片失败:", error);
      proxy.$modal.msgError("保存图片失败");
    }
  };
  // 将Data URL转换为Blob
  const dataURLToBlob = dataURL => {
    const arr = dataURL.split(",");
    const mime = arr[0].match(/:(.*?);/)[1];
    const bstr = atob(arr[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], { type: mime });
  };
  getProductTreeList();
</script>
<style scoped>
.product-view {
  display: flex;
}
.left {
  width: 380px;
  padding: 16px;
  background: #ffffff;
}
.right {
  width: calc(100% - 380px);
  padding: 16px;
  margin-left: 20px;
  background: #ffffff;
}
.custom-tree-node {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  padding-right: 8px;
}
</style>
  .product-view {
    display: flex;
  }
  .left {
    width: 450px;
    min-width: 450px;
    padding: 16px;
    background: #ffffff;
  }
  .right {
    flex: 1;
    min-width: 0;
    padding: 16px;
    margin-left: 20px;
    background: #ffffff;
  }
  .custom-tree-node {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 14px;
    padding-right: 8px;
  }
  .tree-node-content {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    height: 100%;
    overflow: hidden;
  }
  .tree-node-content .orange-icon {
    flex-shrink: 0;
  }
  .tree-node-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .orange-icon {
    color: orange;
    font-size: 18px;
    margin-right: 8px; /* 图标与文字之间加点间距 */
  }
  .product-tree-scroll {
    scrollbar-width: thin;
    scrollbar-color: #c0c4cc #f5f7fa;
  }
  .product-tree-scroll::-webkit-scrollbar {
    width: 8px;
  }
  .product-tree-scroll::-webkit-scrollbar-track {
    background: #f5f7fa;
    border-radius: 4px;
  }
  .product-tree-scroll::-webkit-scrollbar-thumb {
    background: #c0c4cc;
    border-radius: 4px;
  }
  .product-tree-scroll::-webkit-scrollbar-thumb:hover {
    background: #909399;
  }
  /* 二维码样式 */
  .qrcode-container {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20px;
    min-height: 300px;
  }
  .qrcode-image {
    max-width: 100%;
    height: auto;
  }
  .loading {
    font-size: 16px;
    color: #606266;
  }
</style>