Browse Source

BOM管理

lxf 1 year ago
parent
commit
085f3f0c3a

+ 40 - 46
src/assets/styles/element-ui.scss

@@ -69,7 +69,7 @@
 // dropdown
 .el-dropdown-menu {
   a {
-    display: block
+    display: block;
   }
 }
 
@@ -83,76 +83,70 @@
   box-sizing: content-box;
 }
 
-.el-menu--collapse
-  > div
-  > .el-submenu
-  > .el-submenu__title
-  .el-submenu__icon-arrow {
+.el-menu--collapse > div > .el-submenu > .el-submenu__title .el-submenu__icon-arrow {
   display: none;
 }
 
-.el-dropdown .el-dropdown-link{
+.el-dropdown .el-dropdown-link {
   color: var(--el-color-primary) !important;
 }
 
-
-.el-form .el-form-item__label{
-	font-weight: 400!important;
+.el-form .el-form-item__label {
+  font-weight: 400 !important;
 }
-.el-dialog{
-	background: #EEEEEE!important;
+.el-dialog {
+  background: #eeeeee !important;
 }
-.el-dialog__header{
-	background: #EEEEEE;
-	padding-bottom: 14px;
+.el-dialog__header {
+  background: #eeeeee;
+  padding-bottom: 14px;
 }
-.el-dialog__body{
-	background: #fff;
-  padding: 20px 30px!important;
+.el-dialog__body {
+  background: #fff;
+  padding: 20px 30px !important;
 }
-.el-dialog__footer{
-	background: #fff;
-	text-align: center!important;
-	padding: 40px 0;
+.el-dialog__footer {
+  background: #fff;
+  text-align: center !important;
+  padding: 40px 0;
 }
 
-.el-loading-mask{
-  z-index: 1000!important;
+.el-loading-mask {
+  z-index: 1000 !important;
 }
 
-.el-form-item{
+.el-form-item {
   // padding: 0!important;
 }
 
-.el-form-item__label{
-  height: 22px!important;
+.el-form-item__label {
+  height: 32px !important;
+  line-height: 32px !important;
 }
 
-.el-tree-node__content{
+.el-tree-node__content {
   height: 40px !important;
-  font-size: 12px!important;
-  color: #666!important;
+  font-size: 12px !important;
+  color: #666 !important;
 }
 
-.el-tree-node:focus>.el-tree-node__content{
-  background-color: #EFF6FF!important;
-  
+.el-tree-node:focus > .el-tree-node__content {
+  background-color: #eff6ff !important;
 }
 
-.is-current{
-  background-color: #EFF6FF!important;
+.is-current {
+  background-color: #eff6ff !important;
 }
 
-.el-input.is-disabled .el-input__wrapper{
-  background: #fafafa!important;
-  
+.el-input.is-disabled .el-input__wrapper {
+  background: #fafafa !important;
+}
+.el-input.is-disabled .el-input__inner {
+  -webkit-text-fill-color: #606266 !important;
+  color: #606266 !important;
 }
-.el-input.is-disabled .el-input__inner{
-  -webkit-text-fill-color:#606266!important;
-  color:#606266!important;
+.el-textarea.is-disabled .el-textarea__inner {
+  background: #fafafa !important;
+  -webkit-text-fill-color: #606266 !important;
+  color: #606266 !important;
 }
-.el-textarea.is-disabled .el-textarea__inner{
-  background: #fafafa!important;
-  -webkit-text-fill-color:#606266!important;
-  color:#606266!important;
-}

+ 1 - 1
src/components/Editor/index.vue

@@ -121,7 +121,7 @@ defineExpose({
 </script>
 <style scoped lang="scss">
 :deep(.ql-editor) {
-  min-height: 180px;
+  min-height: 300px;
 }
 :deep(.ql-formats) {
   height: 21px;

+ 10 - 6
src/components/byForm/index.vue

@@ -64,8 +64,9 @@
           :clearable="i.clearable ? i.clearable : false"
           :filterable="i.filterable ? true : false"
           :style="i.style"
-          :readonly="i.readonly ? i.readonly : false">
-          <el-option :label="j.title || j.name || j.label" :value="j.id || j.value" v-for="j in i.data" :key="j.id"> </el-option>
+          :readonly="i.readonly ? i.readonly : false"
+          style="width: 100%">
+          <el-option :label="j.dictValue || j.title || j.name || j.label" :value="j.dictKey || j.id || j.value" v-for="j in i.data" :key="j.id"> </el-option>
         </el-select>
         <el-tree-select
           v-model="formData[i.prop]"
@@ -81,7 +82,8 @@
           :placeholder="i.placeholder || $t('common.pleaseSelect')"
           :disabled="i.disabled ? i.disabled : false"
           check-strictly
-          :style="i.style" />
+          :style="i.style"
+          style="width: 100%" />
         <el-date-picker
           v-model="formData[i.prop]"
           :readonly="i.readonly ? i.readonly : false"
@@ -91,7 +93,8 @@
           @change="(e) => commonsEmit(e, i)"
           :disabled="i.disabled ? i.disabled : false"
           :format="i.format ? i.format : dateFormatInit(i.itemType)"
-          :value-format="i.format ? i.format : dateFormatInit(i.itemType)" />
+          :value-format="i.format ? i.format : dateFormatInit(i.itemType)"
+          style="width: 100%" />
         <el-switch
           :disabled="i.disabled ? i.disabled : false"
           v-else-if="i.type == 'switch'"
@@ -401,8 +404,9 @@ loadInit();
 .form-title {
   font-size: 14px;
   font-weight: bold;
-  margin-top: 22px;
-  color: #333333;
+  color: #6c88f1;
+  width: 120px;
+  border-bottom: 1px solid #6c88f1;
 }
 .by-form .el-form--inline .el-form-item {
   margin-right: 0px;

+ 4 - 9
src/components/byTable/index.vue

@@ -66,6 +66,9 @@
               {{ getValue(scope, item) }}
             </div>
           </template>
+          <template #default="props" v-else>
+            <slot :name="item.attrs.slot" :item="props.row" v-if="item.attrs.slot"> 插槽占位符 </slot>
+          </template>
         </el-table-column>
       </el-table>
       <el-row v-if="!hidePagination" class="table-pagination" justify="end" type="flex">
@@ -211,10 +214,7 @@ export default defineComponent({
       return Object.assign({}, params, props.pagination);
     });
     const getActionList = computed(() => {
-      return props.actionList
-        .slice()
-        .reverse()
-        .filter((it) => it.text);
+      return props.actionList.slice().filter((it) => it.text);
     });
     const getValue = (scope, configItem) => {
       const prop = configItem.attrs.prop;
@@ -564,11 +564,6 @@ export default defineComponent({
   overflow-y: auto;
   line-height: 1;
 }
-::v-deep(.tableHeader th) {
-  background-color: #edf0f5;
-  height: 35px;
-  padding: 0;
-}
 ::v-deep(.el-button--small) {
   padding: 5px 17px;
 }

+ 619 - 0
src/components/makeBOM/index.vue

@@ -0,0 +1,619 @@
+<template>
+  <div style="height: calc(100vh - 114px); overflow-y: auto; overflow-x: hidden">
+    <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="submit">
+      <template #bomClassifyId>
+        <div style="width: 100%">
+          <el-cascader
+            v-model="formData.data.bomClassifyId"
+            :options="classifyList"
+            :props="{ checkStrictly: true, value: 'id', label: 'name', emitPath: false }"
+            clearable
+            style="width: 100%" />
+        </div>
+      </template>
+      <template #specification>
+        <div style="width: 100%">
+          <div>
+            <el-icon style="cursor: pointer; transform: translateY(4px); font-size: 24px; margin-left: 10px" @click="addSpecification"><Plus /></el-icon>
+          </div>
+          <el-table :data="formData.data.bomSpecList" :row-style="{ height: '35px' }" header-row-class-name="tableHeader">
+            <el-table-column label="图片" align="center" width="100">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.imgFile'">
+                  <el-upload
+                    class="avatar-uploader"
+                    action="https://winfaster.obs.cn-south-1.myhuaweicloud.com"
+                    :data="uploadData"
+                    :show-file-list="false"
+                    :on-success="
+                      (response, uploadFile) => {
+                        return handleSuccess(uploadFile, $index);
+                      }
+                    "
+                    :before-upload="uploadFile">
+                    <el-image v-if="row.imgFile && row.imgFile.fileUrl" :src="row.imgFile.fileUrl" fit="scale-down" class="avatar" />
+                    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+                  </el-upload>
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="品名" min-width="220">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.name'" :rules="rules.name" :inline-message="true" style="width: 100%">
+                  <el-input v-model="row.name" placeholder="请输入品名" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="品号" min-width="160">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.code'" :rules="rules.code" :inline-message="true" style="width: 100%">
+                  <el-input v-model="row.code" placeholder="请输入品号" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="颜色" width="120">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.colour'" style="width: 100%">
+                  <el-input v-model="row.colour" placeholder="请输入颜色" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="成本价" width="100">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.costPrice'" style="width: 100%">
+                  <el-input-number
+                    onmousewheel="return false;"
+                    v-model="row.costPrice"
+                    placeholder="请输入"
+                    style="width: 100%"
+                    :controls="false"
+                    :min="0"
+                    :precision="2" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="对内销售价 (含税)" width="140">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.internalSellingPrice'" style="width: 100%">
+                  <el-input-number
+                    onmousewheel="return false;"
+                    v-model="row.internalSellingPrice"
+                    placeholder="请输入"
+                    style="width: 100%"
+                    :controls="false"
+                    :min="0"
+                    :precision="2" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="对外销售价 (含税)" width="140">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.externalSellingPrice'" style="width: 100%">
+                  <el-input-number
+                    onmousewheel="return false;"
+                    v-model="row.externalSellingPrice"
+                    placeholder="请输入"
+                    style="width: 100%"
+                    :controls="false"
+                    :min="0"
+                    :precision="2" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="安全库存" width="100">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.safetyStock'" style="width: 100%">
+                  <el-input-number
+                    onmousewheel="return false;"
+                    v-model="row.safetyStock"
+                    placeholder="请输入"
+                    style="width: 100%"
+                    :controls="false"
+                    :min="0"
+                    :precision="0" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="单品尺寸(长宽高,cm)" align="center" width="280">
+              <template #default="{ row }">
+                <el-form-item style="width: 100%">
+                  <el-row>
+                    <el-col :span="8">
+                      <el-input-number
+                        onmousewheel="return false;"
+                        v-model="row.length"
+                        placeholder="请输入长"
+                        style="width: 100%"
+                        :controls="false"
+                        :min="0"
+                        :precision="2" />
+                    </el-col>
+                    <el-col :span="8">
+                      <el-input-number
+                        onmousewheel="return false;"
+                        v-model="row.width"
+                        placeholder="请输入宽"
+                        style="width: 100%"
+                        :controls="false"
+                        :min="0"
+                        :precision="2" />
+                    </el-col>
+                    <el-col :span="8">
+                      <el-input-number
+                        onmousewheel="return false;"
+                        v-model="row.height"
+                        placeholder="请输入高"
+                        style="width: 100%"
+                        :controls="false"
+                        :min="0"
+                        :precision="2" />
+                    </el-col>
+                  </el-row>
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="净重(g)" width="100">
+              <template #default="{ row, $index }">
+                <el-form-item :prop="'bomSpecList.' + $index + '.netWeight'" style="width: 100%">
+                  <el-input-number
+                    onmousewheel="return false;"
+                    v-model="row.netWeight"
+                    placeholder="请输入"
+                    style="width: 100%"
+                    :controls="false"
+                    :min="0"
+                    :precision="2" />
+                </el-form-item>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="80">
+              <template #default="{ $index }">
+                <el-button type="primary" @click="clickDelete($index)" text>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </template>
+      <template #mainImgFile>
+        <div style="width: 100%">
+          <div style="color: #aaaaaa">图片不超过5M,支持JPEG、JPG、PNG格式;</div>
+          <div style="color: #aaaaaa">建议主图大于640*640,主题鲜明、图片清晰、提升买家满意度;</div>
+          <el-upload
+            class="avatar-uploader-main"
+            action="https://winfaster.obs.cn-south-1.myhuaweicloud.com"
+            :data="uploadMainData"
+            :show-file-list="false"
+            :on-success="handleMainSuccess"
+            :before-upload="uploadMainFile">
+            <el-image
+              v-if="formData.data.mainImgFile && formData.data.mainImgFile.fileUrl"
+              :src="formData.data.mainImgFile.fileUrl"
+              fit="scale-down"
+              class="avatar" />
+            <el-icon v-else class="avatar-uploader-main-icon"><Plus /></el-icon>
+          </el-upload>
+        </div>
+      </template>
+      <template #detailText>
+        <div style="width: 100%">
+          <div v-if="props.detailStatus">
+            <div v-html="getStyle(formData.data.detailText)"></div>
+          </div>
+          <Editor v-else :value="formData.data.detailText" @updateValue="updateValue" ref="editor" />
+        </div>
+      </template>
+    </byForm>
+    <div style="width: 100%; text-align: center; margin: 10px">
+      <el-button v-if="props.detailStatus" @click="clickCancel()" size="large">关 闭</el-button>
+      <el-button v-if="!props.detailStatus" @click="clickCancel()" size="large">取 消</el-button>
+      <el-button type="primary" @click="submitForm()" v-if="!props.detailStatus" :disabled="btnDisabled" size="large">确 定</el-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import byForm from "@/components/byForm/index";
+import { ElMessage } from "element-plus";
+import Editor from "@/components/Editor/index.vue";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(["clickCancel"]);
+const submit = ref(null);
+const formOption = reactive({
+  inline: true,
+  labelWidth: "120px",
+  itemWidth: 100,
+  rules: [],
+  labelPosition: "right",
+});
+const formData = reactive({
+  data: {
+    detailText: "",
+    mainImgFile: {},
+    bomSpecList: [
+      {
+        imgFile: {},
+        name: "",
+        code: "",
+        colour: "",
+        costPrice: undefined,
+        internalSellingPrice: undefined,
+        externalSellingPrice: undefined,
+        safetyStock: undefined,
+        length: undefined,
+        width: undefined,
+        height: undefined,
+        netWeight: undefined,
+      },
+    ],
+  },
+});
+const formConfig = computed(() => {
+  return [
+    {
+      type: "title",
+      title: "基本信息",
+      label: "",
+    },
+    {
+      type: "slot",
+      prop: "bomClassifyId",
+      slotName: "bomClassifyId",
+      label: "类目",
+      itemWidth: 50,
+    },
+    {
+      type: "input",
+      prop: "code",
+      label: "品号",
+      itemType: "text",
+      itemWidth: 50,
+    },
+    {
+      type: "input",
+      prop: "name",
+      label: "名称",
+      itemType: "text",
+    },
+    {
+      type: "title",
+      title: "材质特征",
+      label: "",
+    },
+    {
+      type: "select",
+      label: "项目小类",
+      prop: "itemSubclass",
+      data: proxy.useUserStore().allDict["bom_itemSubclass"],
+      itemWidth: 50,
+    },
+    {
+      type: "select",
+      label: "种类",
+      prop: "species",
+      data: proxy.useUserStore().allDict["bom_species"],
+      itemWidth: 50,
+    },
+    {
+      type: "input",
+      prop: "material",
+      label: "材质",
+      itemType: "text",
+      itemWidth: 50,
+    },
+    {
+      type: "input",
+      prop: "modelNumber",
+      label: "型号",
+      itemType: "text",
+      itemWidth: 50,
+    },
+    {
+      type: "select",
+      label: "正面纹路",
+      prop: "frontGrain",
+      data: proxy.useUserStore().allDict["bom_frontGrain"],
+      itemWidth: 50,
+    },
+    {
+      type: "select",
+      label: "背面纹路",
+      prop: "reverseGrain",
+      data: proxy.useUserStore().allDict["bom_reverseGrain"],
+      itemWidth: 50,
+    },
+    {
+      type: "input",
+      prop: "unit",
+      label: "单位",
+      itemType: "text",
+      itemWidth: 50,
+    },
+    {
+      type: "input",
+      prop: "logo",
+      label: "LOGO",
+      itemType: "text",
+      itemWidth: 50,
+    },
+    {
+      type: "select",
+      label: "色层",
+      prop: "chromatophore",
+      data: proxy.useUserStore().allDict["bom_chromatophore"],
+      itemWidth: 50,
+    },
+    {
+      type: "select",
+      label: "售价体系",
+      prop: "sellingPriceSystem",
+      data: proxy.useUserStore().allDict["bom_sellingPriceSystem"],
+      itemWidth: 50,
+    },
+    {
+      type: "select",
+      label: "角度",
+      prop: "angle",
+      data: proxy.useUserStore().allDict["bom_angle"],
+      itemWidth: 50,
+    },
+    {
+      type: "input",
+      prop: "formula",
+      label: "配方",
+      itemType: "text",
+      itemWidth: 50,
+    },
+    {
+      type: "select",
+      label: "压纹工艺",
+      prop: "embossingProcess",
+      data: proxy.useUserStore().allDict["bom_embossingProcess"],
+      itemWidth: 50,
+    },
+    {
+      type: "slot",
+      slotName: "specification",
+      label: "规格",
+    },
+    {
+      type: "title",
+      title: "图片介绍",
+      label: "",
+    },
+    {
+      type: "slot",
+      slotName: "mainImgFile",
+      label: "主图",
+    },
+    {
+      type: "slot",
+      slotName: "detailText",
+      label: "详情描述",
+    },
+  ];
+});
+const rules = ref({
+  bomClassifyId: [{ required: true, message: "请选择类目", trigger: "change" }],
+  name: [{ required: true, message: "请输入品名", trigger: "blur" }],
+  code: [{ required: true, message: "请输入品号", trigger: "blur" }],
+});
+const classifyList = ref([]);
+const getBomClassify = () => {
+  proxy.post("/bomClassify/tree", {}).then((res) => {
+    if (res && res.length > 0) {
+      classifyList.value = res.map((item) => {
+        return {
+          ...item,
+          disabled: true,
+        };
+      });
+    }
+  });
+};
+getBomClassify();
+const addSpecification = () => {
+  if (!props.detailStatus) {
+    if (formData.data.bomSpecList && formData.data.bomSpecList.length > 0) {
+      formData.data.bomSpecList.push({
+        imgFile: [],
+        name: "",
+        code: "",
+        colour: "",
+        costPrice: undefined,
+        internalSellingPrice: undefined,
+        externalSellingPrice: undefined,
+        safetyStock: undefined,
+        length: undefined,
+        width: undefined,
+        height: undefined,
+        netWeight: undefined,
+      });
+    } else {
+      formData.data.bomSpecList = [
+        {
+          imgFile: [],
+          name: "",
+          code: "",
+          colour: "",
+          costPrice: undefined,
+          internalSellingPrice: undefined,
+          externalSellingPrice: undefined,
+          safetyStock: undefined,
+          length: undefined,
+          width: undefined,
+          height: undefined,
+          netWeight: undefined,
+        },
+      ];
+    }
+  }
+};
+const uploadData = ref({});
+const uploadFile = async (file) => {
+  const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
+  uploadData.value = res.uploadBody;
+  file.id = res.id;
+  file.fileName = res.fileName;
+  file.fileUrl = res.fileUrl;
+  return true;
+};
+const handleSuccess = (uploadFile, index) => {
+  formData.data.bomSpecList[index].imgFile = {
+    id: uploadFile.raw.id,
+    fileName: uploadFile.raw.fileName,
+    fileUrl: uploadFile.raw.fileUrl,
+  };
+};
+const clickDelete = (index) => {
+  formData.data.bomSpecList.splice(index, 1);
+};
+const editor = ref(null);
+const updateValue = (val) => {
+  formData.data.detailText = val;
+};
+const uploadMainData = ref({});
+const uploadMainFile = async (file) => {
+  const res = await proxy.post("/fileInfo/getSing", { fileName: file.name });
+  uploadMainData.value = res.uploadBody;
+  file.id = res.id;
+  file.fileName = res.fileName;
+  file.fileUrl = res.fileUrl;
+  return true;
+};
+const handleMainSuccess = (response, uploadFile) => {
+  formData.data.mainImgFile = {
+    id: uploadFile.raw.id,
+    fileName: uploadFile.raw.fileName,
+    fileUrl: uploadFile.raw.fileUrl,
+  };
+};
+const btnDisabled = ref(false);
+const submitForm = () => {
+  submit.value.handleSubmit(() => {
+    if (formData.data.bomSpecList && formData.data.bomSpecList.length > 0) {
+      let type = "add";
+      if (formData.data.id) {
+        type = "edit";
+      }
+      btnDisabled.value = true;
+      proxy.post("/bom/" + type, formData.data).then(
+        () => {
+          ElMessage({
+            message: type == "add" ? "添加成功" : "编辑成功",
+            type: "success",
+          });
+          emit("clickCancel", true);
+        },
+        (err) => {
+          console.log(err);
+          btnDisabled.value = false;
+        }
+      );
+    } else {
+      return ElMessage("请添加规格");
+    }
+  });
+};
+const clickCancel = () => {
+  emit("clickCancel", false);
+};
+const props = defineProps({
+  rowData: Object,
+  detailStatus: Boolean,
+});
+onMounted(() => {
+  formOption.disabled = props.detailStatus;
+  if (props.rowData && props.rowData.id) {
+    proxy.post("/bom/detail", { id: props.rowData.id }).then((res) => {
+      for (var text in res) {
+        formData.data[text] = res[text];
+      }
+      if (!props.detailStatus) {
+        editor.value.changeHtml(formData.data.detailText);
+      }
+      let list = [props.rowData.id];
+      if (res.bomSpecList && res.bomSpecList.length > 0) {
+        list = list.concat(res.bomSpecList.map((item) => item.id));
+      }
+      proxy.post("/fileInfo/getList", { businessIdList: list }).then((fileObj) => {
+        if (fileObj) {
+          if (fileObj[props.rowData.id] && fileObj[props.rowData.id].length > 0) {
+            formData.data.mainImgFile = fileObj[props.rowData.id][0];
+          }
+          if (formData.data.bomSpecList && formData.data.bomSpecList.length > 0) {
+            for (let i = 0; i < formData.data.bomSpecList.length; i++) {
+              if (fileObj[formData.data.bomSpecList[i].id] && fileObj[formData.data.bomSpecList[i].id].length > 0) {
+                formData.data.bomSpecList[i].imgFile = fileObj[formData.data.bomSpecList[i].id][0];
+              }
+            }
+          }
+        }
+      });
+    });
+  }
+});
+const getStyle = (text) => {
+  if (text) {
+    return text.replace(/\n|\r\n/g, "<br>");
+  } else {
+    return "";
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.avatar-uploader .avatar {
+  width: 50px;
+  height: 50px;
+  display: block;
+  background-color: black;
+}
+.avatar-uploader .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+.avatar-uploader .el-upload:hover {
+  border-color: var(--el-color-primary);
+}
+.el-icon.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 50px;
+  height: 50px;
+  text-align: center;
+  border: 1px dashed var(--el-border-color);
+}
+::v-deep(.el-input-number .el-input__inner) {
+  text-align: left;
+}
+.avatar-uploader-main .avatar {
+  width: 148px;
+  height: 148px;
+  display: block;
+  background-color: black;
+}
+.avatar-uploader-main .el-upload {
+  border: 1px dashed var(--el-border-color);
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  transition: var(--el-transition-duration-fast);
+}
+.avatar-uploader-main .el-upload:hover {
+  border-color: var(--el-color-primary);
+}
+.el-icon.avatar-uploader-main-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 148px;
+  height: 148px;
+  text-align: center;
+  border: 1px dashed var(--el-border-color);
+}
+</style>

+ 7 - 1
src/layout/index.vue

@@ -56,7 +56,8 @@ function setLayout() {
   settingRef.value.openSetting();
 }
 const getAllDict = () => {
-  proxy.useUserStore()
+  proxy
+    .useUserStore()
     .allDictMap()
     .then(() => {})
     .catch(() => {});
@@ -122,4 +123,9 @@ getAllDict();
   overflow-y: auto;
   overflow-x: hidden;
 }
+::v-deep(.tableHeader th) {
+  background-color: #edf0f5;
+  height: 35px;
+  padding: 0;
+}
 </style>

+ 2 - 2
src/main.js

@@ -29,7 +29,7 @@ import { useDict } from "@/utils/dict";
 import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from "@/utils/ruoyi";
 
 import {
-  dictDataEcho,
+  dictKeyValue,
   dictValueLabel,
   moneyFormat,
   calculationWeek,
@@ -70,7 +70,7 @@ app.config.globalProperties.selectDictLabel = selectDictLabel;
 app.config.globalProperties.selectDictLabels = selectDictLabels;
 app.config.globalProperties.t = i18n.global.t;
 //字典回显
-app.config.globalProperties.dictDataEcho = dictDataEcho;
+app.config.globalProperties.dictKeyValue = dictKeyValue;
 app.config.globalProperties.dictValueLabel = dictValueLabel;
 app.config.globalProperties.moneyFormat = moneyFormat;
 app.config.globalProperties.calculationWeek = calculationWeek;

+ 3 - 3
src/utils/util.js

@@ -6,10 +6,10 @@ import JsPDF from "jspdf";
 import * as toEnglish from "./ACapital.js";
 
 //根据value值回显字典label值
-export function dictDataEcho(value, arr) {
+export function dictKeyValue(value, arr) {
   if (value && arr) {
     value = value + "";
-    const current = arr.find((x) => x.dictKey === value);
+    const current = arr.find((x) => x.dictKey == value + "");
     if (current != undefined && current.dictValue) {
       return current.dictValue;
     }
@@ -21,7 +21,7 @@ export function dictDataEcho(value, arr) {
 //根据value值回显字典label值
 export function dictValueLabel(value, arr) {
   if ((value || value === 0) && arr) {
-    const current = arr.filter((x) => x.value == value);
+    const current = arr.filter((x) => x.value == value + "");
     if (current && current.length > 0) {
       return current[0].label;
     }

+ 10 - 10
src/views/group/BOM/classification/index.vue

@@ -12,7 +12,7 @@
       :action-list="[
         {
           text: '新增',
-          action: () => openModal(),
+          action: () => clickModal(),
         },
       ]"
       @get-list="getList"
@@ -47,7 +47,7 @@
       </byForm>
       <template #footer>
         <el-button @click="openDialog = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitForm()" v-loading="btnLoading" size="large">确 定</el-button>
+        <el-button type="primary" @click="submitForm()" :disabled="btnDisabled" size="large">确 定</el-button>
       </template>
     </el-dialog>
   </el-card>
@@ -144,11 +144,11 @@ const rules = ref({
   name: [{ required: true, message: "请输入分类名称", trigger: "blur" }],
   code: [{ required: true, message: "请输入分类编码", trigger: "blur" }],
 });
-const btnLoading = ref(false);
-const openModal = () => {
+const btnDisabled = ref(false);
+const clickModal = () => {
   modalType.value = "add";
   formData.data = {};
-  btnLoading.value = false;
+  btnDisabled.value = false;
   openDialog.value = true;
 };
 const addChildNode = (row) => {
@@ -156,12 +156,12 @@ const addChildNode = (row) => {
   formData.data = {
     parentId: row.id,
   };
-  btnLoading.value = false;
+  btnDisabled.value = false;
   openDialog.value = true;
 };
 const submitForm = () => {
   submit.value.handleSubmit(() => {
-    btnLoading.value = true;
+    btnDisabled.value = true;
     proxy.post("/bomClassify/" + modalType.value, formData.data).then(
       () => {
         ElMessage({
@@ -169,12 +169,12 @@ const submitForm = () => {
           type: "success",
         });
         openDialog.value = false;
-        btnLoading.value = false;
+        btnDisabled.value = false;
         getList();
       },
       (err) => {
         console.log(err);
-        btnLoading.value = false;
+        btnDisabled.value = false;
       }
     );
   });
@@ -189,7 +189,7 @@ const clickUpdate = (row) => {
     sort: row.sort,
     remark: row.remark,
   };
-  btnLoading.value = false;
+  btnDisabled.value = false;
   openDialog.value = true;
 };
 const clickDelete = (row) => {

+ 331 - 134
src/views/group/BOM/management/index.vue

@@ -19,8 +19,6 @@
       <el-col :span="20">
         <el-card class="box-card">
           <byTable
-            :hideTable="true"
-            :hidePagination="true"
             :source="sourceList.data"
             :pagination="sourceList.pagination"
             :config="config"
@@ -29,46 +27,78 @@
             highlight-current-row
             :action-list="[
               {
-                text: '新增',
-                action: () => openModal(),
+                text: '添加BOM',
+                action: () => clickModal(),
+              },
+              {
+                text: '操作日志',
+                action: () => viewLogs(),
               },
             ]"
             @get-list="getList"
             @clickReset="clickReset">
+            <template #typeExpand="{ item }">
+              <div style="width: 100%">
+                <el-table
+                  :data="item.bomSpecList"
+                  size="small"
+                  :row-style="{ height: '35px' }"
+                  :cell-style="{ padding: '0' }"
+                  header-row-class-name="tableHeader"
+                  :show-header="false">
+                  <el-table-column width="100" align="center" />
+                  <el-table-column label="设计图" width="100">
+                    <template #default="{ row }">
+                      <div v-if="row.mainImgUrl">
+                        <img
+                          style="width: 32px; height: 32px; object-fit: contain; vertical-align: middle; border: none; cursor: pointer; margin-bottom: 8px"
+                          :src="row.mainImgUrl"
+                          @click="openFile(row.mainImgUrl)" />
+                      </div>
+                    </template>
+                  </el-table-column>
+                  <el-table-column label="品号" prop="name" min-width="240" />
+                  <el-table-column label="品名" prop="code" width="200" />
+                  <el-table-column label="单品尺寸(L*W*H)" width="240">
+                    <template #default="{ row }">
+                      <span>{{ `${row.length} * ${row.width} * ${row.height}` }}</span>
+                    </template>
+                  </el-table-column>
+                  <!-- <el-table-column label="操作" prop="weight" width="80" v-if="selectStatus">
+                    <template slot-scope="sonScope">
+                      <div>
+                        <el-button type="text" @click="selectBOM(scope.row, sonScope.$index)" v-db-click>选择</el-button>
+                      </div>
+                    </template>
+                  </el-table-column> -->
+                </el-table>
+              </div>
+            </template>
+            <template #name="{ item }">
+              <div>
+                <a style="color: #409eff; cursor: pointer; word-break: break-all" @click="clickName(item)">{{ item.name }}</a>
+              </div>
+            </template>
           </byTable>
-          <el-table :data="sourceList.data" row-key="id" default-expand-all>
-            <el-table-column prop="name" label="分类名称" min-width="180" />
-            <el-table-column prop="sort" label="排序" align="center" width="100" />
-            <el-table-column label="操作" align="center" width="180">
-              <template #default="{ row }">
-                <div>
-                  <el-button type="primary" @click="addChildNode(row)" text>添加子节点</el-button>
-                  <el-button type="primary" @click="clickUpdate(row)" v-if="row.parentId" text>编辑</el-button>
-                  <el-button type="primary" @click="clickDelete(row)" v-if="row.parentId" text>删除</el-button>
-                </div>
-              </template>
-            </el-table-column>
-          </el-table>
         </el-card>
       </el-col>
     </el-row>
 
-    <el-dialog :title="modalType == 'add' ? '新增分类' : '编辑分类'" v-if="openDialog" v-model="openDialog" width="400">
-      <!-- <byForm :formConfig="formConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="submit">
-        <template #parentId>
-          <div style="width: 100%">
-            <el-cascader
-              v-model="formData.data.parentId"
-              :options="sourceList.data"
-              :props="{ checkStrictly: true, value: 'id', label: 'name', emitPath: false }"
-              clearable
-              style="width: 100%" />
-          </div>
-        </template>
-      </byForm> -->
+    <el-dialog :title="modalTitle" v-if="openDialog" v-model="openDialog" width="90%">
+      <MakeBOM :rowData="rowData" :detailStatus="detailStatus" @clickCancel="clickCancel"></MakeBOM>
+    </el-dialog>
+
+    <el-dialog title="操作日志" v-if="openLogs" v-model="openLogs" width="50%">
+      <byTable
+        :source="logsList.data"
+        :pagination="logsList.pagination"
+        :config="configLogs"
+        :loading="loadingLogs"
+        highlight-current-row
+        @get-list="getLogsList">
+      </byTable>
       <template #footer>
-        <el-button @click="openDialog = false" size="large">取 消</el-button>
-        <el-button type="primary" @click="submitForm()" v-loading="btnLoading" size="large">确 定</el-button>
+        <el-button @click="openLogs = false" size="large">关 闭</el-button>
       </template>
     </el-dialog>
   </div>
@@ -76,8 +106,8 @@
 
 <script setup>
 import byTable from "@/components/byTable/index";
-import byForm from "@/components/byForm/index";
 import { ElMessage, ElMessageBox } from "element-plus";
+import MakeBOM from "@/components/makeBOM/index";
 
 const { proxy } = getCurrentInstance();
 const filterTree = ref("");
@@ -97,13 +127,22 @@ const filterNodeMethod = (value, data) => {
   return data.name.includes(value);
 };
 const handleNodeClick = (val) => {
-  console.log(val);
+  getList({ bomClassifyId: val.id });
 };
 const sourceList = ref({
   data: [],
   pagination: {
     total: 0,
+    pageNum: 1,
+    pageSize: 10,
     name: "",
+    code: "",
+    species: "",
+    chromatophore: "",
+    frontGrain: "",
+    reverseGrain: "",
+    colour: "",
+    bomClassifyId: "",
   },
 });
 const loading = ref(false);
@@ -112,7 +151,12 @@ const searchConfig = computed(() => {
     {
       type: "input",
       prop: "name",
-      label: "分类名称",
+      label: "品名",
+    },
+    {
+      type: "input",
+      prop: "code",
+      label: "品号",
     },
     {
       type: "select",
@@ -120,123 +164,187 @@ const searchConfig = computed(() => {
       dictKey: "bom_species",
       label: "种类",
     },
+    {
+      type: "select",
+      prop: "chromatophore",
+      dictKey: "bom_chromatophore",
+      label: "色层",
+    },
+    {
+      type: "select",
+      prop: "embossingProcess",
+      dictKey: "bom_embossingProcess",
+      label: "压纹工艺",
+    },
+    {
+      type: "select",
+      prop: "frontGrain",
+      dictKey: "bom_frontGrain",
+      label: "正面纹路",
+    },
+    {
+      type: "select",
+      prop: "reverseGrain",
+      dictKey: "bom_reverseGrain",
+      label: "背面纹路",
+    },
+    {
+      type: "input",
+      prop: "colour",
+      label: "颜色",
+    },
   ];
 });
 const config = computed(() => {
-  return [];
-});
-const getList = async (req) => {
-  sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
-  loading.value = true;
-  proxy.post("/bomClassify/tree", sourceList.value.pagination).then((res) => {
-    sourceList.value.data = res;
-    setTimeout(() => {
-      loading.value = false;
-    }, 200);
-  });
-};
-getList();
-const clickReset = () => {
-  treeCategory.setCurrentKey(null);
-  getList({ name: "" });
-};
-const modalType = ref("add");
-const submit = ref(null);
-const openDialog = ref(false);
-const formOption = reactive({
-  inline: true,
-  labelWidth: 100,
-  itemWidth: 100,
-  rules: [],
-});
-const formData = reactive({
-  data: {},
-});
-const formConfig = computed(() => {
   return [
     {
-      type: "slot",
-      prop: "parentId",
-      slotName: "parentId",
-      label: "上级分类",
+      type: "expand",
+      attrs: {
+        label: " ",
+        slot: "typeExpand",
+        width: 50,
+      },
     },
     {
-      type: "input",
-      prop: "name",
-      label: "分类名称",
-      itemType: "text",
+      attrs: {
+        label: "群组品名",
+        slot: "name",
+        "min-width": 240,
+      },
     },
     {
-      type: "input",
-      prop: "code",
-      label: "分类编码",
-      itemType: "text",
+      attrs: {
+        label: "项目小类",
+        prop: "itemSubclass",
+        width: 140,
+      },
+      render(val) {
+        return proxy.dictKeyValue(val, proxy.useUserStore().allDict["bom_itemSubclass"]);
+      },
     },
     {
-      type: "number",
-      prop: "sort",
-      label: "排序",
-      precision: 0,
+      attrs: {
+        label: "种类",
+        prop: "species",
+        width: 140,
+      },
+      render(val) {
+        return proxy.dictKeyValue(val, proxy.useUserStore().allDict["bom_species"]);
+      },
     },
     {
-      type: "input",
-      prop: "remark",
-      label: "备注",
-      itemType: "textarea",
+      attrs: {
+        label: "色层",
+        prop: "chromatophore",
+        width: 100,
+      },
+      render(val) {
+        return proxy.dictKeyValue(val, proxy.useUserStore().allDict["bom_chromatophore"]);
+      },
+    },
+    {
+      attrs: {
+        label: "压纹工艺",
+        prop: "embossingProcess",
+        width: 140,
+      },
+      render(val) {
+        return proxy.dictKeyValue(val, proxy.useUserStore().allDict["bom_embossingProcess"]);
+      },
+    },
+    {
+      attrs: {
+        label: "正面纹路",
+        prop: "frontGrain",
+        width: 140,
+      },
+      render(val) {
+        return proxy.dictKeyValue(val, proxy.useUserStore().allDict["bom_frontGrain"]);
+      },
+    },
+    {
+      attrs: {
+        label: "背面纹路",
+        prop: "reverseGrain",
+        width: 140,
+      },
+      render(val) {
+        return proxy.dictKeyValue(val, proxy.useUserStore().allDict["bom_reverseGrain"]);
+      },
+    },
+    {
+      attrs: {
+        label: "操作",
+        width: 120,
+        align: "center",
+        fixed: "right",
+      },
+      renderHTML(row) {
+        return [
+          {
+            attrs: {
+              label: "编辑",
+              type: "primary",
+              text: true,
+            },
+            el: "button",
+            click() {
+              clickUpdate(row);
+            },
+          },
+          {
+            attrs: {
+              label: "删除",
+              type: "primary",
+              text: true,
+            },
+            el: "button",
+            click() {
+              clickDelete(row);
+            },
+          },
+        ];
+      },
     },
   ];
 });
-const rules = ref({
-  parentId: [{ required: true, message: "请选择上级分类", trigger: "change" }],
-  name: [{ required: true, message: "请输入分类名称", trigger: "blur" }],
-  code: [{ required: true, message: "请输入分类编码", trigger: "blur" }],
-});
-const btnLoading = ref(false);
-const openModal = () => {
-  modalType.value = "add";
-  formData.data = {};
-  btnLoading.value = false;
-  openDialog.value = true;
+const getList = async (req, status) => {
+  if (status) {
+    sourceList.value.pagination = {
+      pageNum: sourceList.value.pagination.pageNum,
+      pageSize: sourceList.value.pagination.pageSize,
+    };
+  } else {
+    sourceList.value.pagination = { ...sourceList.value.pagination, ...req };
+  }
+  loading.value = true;
+  proxy.post("/bom/page", sourceList.value.pagination).then((res) => {
+    sourceList.value.data = res.rows;
+    sourceList.value.pagination.total = res.total;
+    setTimeout(() => {
+      loading.value = false;
+    }, 200);
+  });
 };
-const addChildNode = (row) => {
-  modalType.value = "add";
-  formData.data = {
-    parentId: row.id,
-  };
-  btnLoading.value = false;
-  openDialog.value = true;
+getList();
+const clickReset = () => {
+  treeCategory.value.setCurrentKey(null);
+  getList("", true);
 };
-const submitForm = () => {
-  submit.value.handleSubmit(() => {
-    btnLoading.value = true;
-    proxy.post("/bomClassify/" + modalType.value, formData.data).then(
-      () => {
-        ElMessage({
-          message: modalType.value == "add" ? "添加成功" : "编辑成功",
-          type: "success",
-        });
-        openDialog.value = false;
-        btnLoading.value = false;
-        getList();
-      },
-      (err) => {
-        console.log(err);
-        btnLoading.value = false;
-      }
-    );
-  });
+const modalTitle = ref("添加BOM");
+const openDialog = ref(false);
+const rowData = ref({});
+const detailStatus = ref(false);
+const clickModal = () => {
+  modalTitle.value = "添加BOM";
+  rowData.value = {};
+  detailStatus.value = false;
+  openDialog.value = true;
 };
 const clickUpdate = (row) => {
-  modalType.value = "edit";
-  formData.data = {
-    id: row.id,
-    parentId: row.parentId,
-    name: row.name,
-    code: row.code,
-    sort: row.sort,
-    remark: row.remark,
-  };
-  btnLoading.value = false;
+  modalTitle.value = "编辑BOM";
+  rowData.value = row;
+  detailStatus.value = false;
   openDialog.value = true;
 };
 const clickDelete = (row) => {
@@ -245,12 +353,101 @@ const clickDelete = (row) => {
     cancelButtonText: "取消",
     type: "warning",
   }).then(() => {
-    proxy.post("/bomClassify/delete", { id: row.id }).then(() => {
+    proxy.post("/bom/delete", { id: row.id }).then(() => {
       ElMessage({ message: "删除成功", type: "success" });
       getList();
     });
   });
 };
+const clickCancel = (status) => {
+  openDialog.value = false;
+  if (status) {
+    getList();
+  }
+};
+const openFile = (path) => {
+  window.open(path);
+};
+const clickName = (row) => {
+  modalTitle.value = "BOM详情";
+  rowData.value = row;
+  detailStatus.value = true;
+  openDialog.value = true;
+};
+const openLogs = ref(false);
+const loadingLogs = ref(false);
+const logsList = ref({
+  data: [],
+  pagination: {
+    total: 0,
+    pageNum: 1,
+    pageSize: 10,
+  },
+});
+const type = ref([
+  { dictKey: "1", dictValue: "新增" },
+  { dictKey: "2", dictValue: "修改" },
+  { dictKey: "3", dictValue: "删除" },
+]);
+const configLogs = computed(() => {
+  return [
+    {
+      attrs: {
+        label: "操作时间",
+        prop: "createTime",
+        width: 160,
+        align: "center",
+      },
+    },
+    {
+      attrs: {
+        label: "操作人",
+        prop: "operator",
+        align: "center",
+      },
+    },
+    {
+      attrs: {
+        label: "BOM品号",
+        prop: "code",
+        align: "center",
+      },
+    },
+    {
+      attrs: {
+        label: "行为",
+        prop: "type",
+        width: 100,
+        align: "center",
+      },
+      render(val) {
+        return proxy.dictKeyValue(val, type.value);
+      },
+    },
+  ];
+});
+const viewLogs = () => {
+  logsList.value.data = [];
+  logsList.value.pagination.total = 0;
+  openLogs.value = true;
+  getLogsList({ pageNum: 1, pageSize: 10 });
+};
+const getLogsList = async (req) => {
+  logsList.value.pagination = { ...logsList.value.pagination, ...req };
+  loadingLogs.value = true;
+  proxy.post("/bomOperatingLog/page", logsList.value.pagination).then((res) => {
+    logsList.value.data = res.rows;
+    logsList.value.pagination.total = res.total;
+    setTimeout(() => {
+      loadingLogs.value = false;
+    }, 200);
+  });
+};
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+:deep(.el-dialog) {
+  margin-top: 10px !important;
+  margin-bottom: 10px !important;
+}
+</style>