gaoluyang
2026-04-29 e40e1cf5d4aa99412ca3a87771b5d5a8ea5a105d
src/views/basicData/product/index.vue
@@ -64,7 +64,46 @@
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
      ></PIMTable>
      >
        <template #drawingFiles="{ row }">
          <div v-if="row.salesLedgerFiles && row.salesLedgerFiles.length" class="drawing-thumbs-list">
            <div
              v-for="(file, index) in row.salesLedgerFiles.slice(0, 3)"
              :key="index"
              class="drawing-thumb-item"
              @click="handlePreviewFile(file)"
            >
              <img
                v-if="isImageFile(file)"
                :src="getDrawingFileUrl(file)"
                class="drawing-thumb-img"
              />
              <div v-else class="drawing-thumb-placeholder">
                {{ getDrawingFileExtension(file).toUpperCase() }}
              </div>
            </div>
            <div v-if="row.salesLedgerFiles.length > 3" class="drawing-thumb-more">
              +{{ row.salesLedgerFiles.length - 3 }}
            </div>
          </div>
          <div v-else-if="row.drawingFile" class="drawing-thumbs-list">
            <div
              class="drawing-thumb-item"
              @click="handlePreviewDrawing(row.drawingFile)"
            >
              <img
                v-if="isDrawingImageFile(row.drawingFile)"
                :src="row.drawingFile"
                class="drawing-thumb-img"
              />
              <div v-else class="drawing-thumb-placeholder">
                {{ getFileExtensionFromUrl(row.drawingFile).toUpperCase() }}
              </div>
            </div>
          </div>
          <span v-else>-</span>
        </template>
      </PIMTable>
    </div>
    <FormDialog
@@ -144,10 +183,11 @@
            :data="upload.data"
            :on-success="handleDrawingUploadSuccess"
            :on-remove="handleDrawingRemove"
            :on-preview="handleDrawingPreview"
            :before-upload="handleDrawingBeforeUpload"
            :limit="1"
            :limit="5"
            accept=".pdf,.jpg,.jpeg,.png,.dwg"
            list-type="picture-card"
            list-type="text"
          >
            <el-icon class="avatar-uploader-icon"><Plus /></el-icon>
            <template #tip>
@@ -156,6 +196,39 @@
              </div>
            </template>
          </el-upload>
          <div v-if="drawingFileList.length" class="drawing-preview-list">
            <div
              v-for="file in drawingFileList"
              :key="file.uid || file.id || file.name"
              class="drawing-preview-card"
              @click="handleDrawingPreview(file)"
            >
              <img
                v-if="isImageFile(file)"
                :src="getDrawingFileUrl(file)"
                :alt="file.name"
                class="drawing-preview-image"
              />
              <div
                v-else
                class="drawing-preview-placeholder"
                :class="`is-${getDrawingFileExtension(file)}`"
              >
                {{ getDrawingFileExtension(file).toUpperCase() }}
              </div>
              <div class="drawing-preview-name">{{ file.name }}</div>
            </div>
          </div>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input
            v-model="modelForm.remark"
            placeholder="请输入备注"
            clearable
            type="textarea"
            :rows="3"
            show-word-limit
          />
        </el-form-item>
      </el-form>
    </FormDialog>
@@ -172,6 +245,7 @@
        :limit="1"
        accept=".xlsx,.xls"
        :action="importUpload.url"
        :http-request="importUpload.httpRequest"
        :headers="importUpload.headers"
        :before-upload="importUpload.beforeUpload"
        :on-success="importUpload.onSuccess"
@@ -193,15 +267,18 @@
        </template>
      </el-upload>
    </FormDialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import axios from "axios";
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 filePreview from "@/components/filePreview/index.vue";
import {
  addOrEditProductModel,
  delProduct,
@@ -209,10 +286,12 @@
  downloadTemplate,
} from "@/api/basicData/product.js";
import { listPage as getProcessRouteList } from "@/api/productionManagement/processRoute.js";
import { delLedgerFile } from "@/api/salesManagement/salesLedger.js";
import ImportExcel from "./ImportExcel/index.vue";
const { proxy } = getCurrentInstance();
const importUploadRef = ref(null);
const filePreviewRef = ref(null);
const modelDia = ref(false);
const importDia = ref(false);
@@ -259,6 +338,16 @@
    minWidth: 100,
  },
  {
    label: "工艺路线",
    prop: "routeName",
    minWidth: 100,
  },
  {
    label: "子项数量",
    prop: "subItemCount",
    minWidth: 100,
  },
  {
    label: "产品属性",
    prop: "productType",
    width: 100,
@@ -269,6 +358,29 @@
      return typeMap[String(v)] || "info";
    },
  },
  {
    label: "备注",
    prop: "remark",
    minWidth: 150,
    showOverflowTooltip: true,
  },
  {
    label: "图纸",
    prop: "salesLedgerFiles",
    minWidth: 200,
    dataType: "slot",
    slot: "drawingFiles",
  },
   {
      label: "创建时间",
      prop: "createTime",
      width: 120,
   },
   {
      label: "修改时间",
      prop: "updateTime",
      width: 120,
   },
  {
    dataType: "action",
    label: "操作",
@@ -288,6 +400,7 @@
const data = reactive({
  modelForm: {
    productId: null,
    productName: "",
    model: "",
    unit: "",
@@ -296,6 +409,7 @@
    drawingFile: "",
    tempFileIds: [],
    salesLedgerFiles: [],
    remark: "",
  },
  modelRules: {
    productName: [
@@ -309,12 +423,92 @@
});
const { modelForm, modelRules } = toRefs(data);
const createDefaultModelForm = () => ({
  productId: null,
  productName: "",
  model: "",
  unit: "",
  productType: null,
  routeId: null,
  drawingFile: "",
  remark: "",
  tempFileIds: [],
  salesLedgerFiles: [],
});
const downloadImportErrorFile = (blob, filename = "import-error.xlsx") => {
  const downloadElement = document.createElement("a");
  const href = window.URL.createObjectURL(blob);
  downloadElement.href = href;
  downloadElement.download = filename;
  document.body.appendChild(downloadElement);
  downloadElement.click();
  document.body.removeChild(downloadElement);
  window.URL.revokeObjectURL(href);
};
const tryParseJsonBlob = async (blob) => {
  try {
    const text = await blob.text();
    if (!text || !text.trim()) {
      return null;
    }
    return JSON.parse(text);
  } catch (_) {
    return null;
  }
};
const importUpload = reactive({
  title: "产品导入",
  open: false,
  url: import.meta.env.VITE_APP_BASE_API + "/basic/product/import",
  headers: { Authorization: "Bearer " + getToken() },
  isUploading: false,
  httpRequest: async (options) => {
    const { file, onProgress, onSuccess, onError } = options;
    importUpload.isUploading = true;
    const formData = new FormData();
    formData.append("file", file);
    try {
      const response = await axios({
        url: importUpload.url,
        method: "post",
        headers: {
          ...importUpload.headers,
          "Content-Type": "multipart/form-data",
        },
        data: formData,
        responseType: "blob",
        onUploadProgress: (progressEvent) => {
          const total = progressEvent.total || 1;
          const percent = Math.round((progressEvent.loaded * 100) / total);
          onProgress?.({ percent }, file);
        },
      });
      importUpload.isUploading = false;
      const blob = response.data;
      // Contract: success => empty response body; failure => binary error file.
      if (!blob || blob.size === 0) {
        onSuccess?.({ code: 200, msg: "import success" }, file);
        return;
      }
      const json = await tryParseJsonBlob(blob);
      if (json) {
        if (String(json.code) === "200" || json.success === true) {
          onSuccess?.(json, file);
        } else {
          onError?.(new Error(json.msg || json.message || "import failed"), file);
        }
        return;
      }
      downloadImportErrorFile(blob);
      onError?.(new Error("import failed, error file downloaded"), file);
    } catch (error) {
      importUpload.isUploading = false;
      onError?.(error, file);
    }
  },
  beforeUpload: (file) => {
    const isExcel = file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
    const isLt10M = file.size / 1024 / 1024 < 10;
@@ -337,7 +531,7 @@
  onSuccess: (response, file, fileList) => {
    console.log('上传成功', response, file, fileList);
    importUpload.isUploading = false;
    if (response.code === 200) {
    if (String(response?.code) === "200" || response?.success === true) {
      proxy.$modal.msgSuccess("导入成功");
      importDia.value = false;
      if (importUploadRef.value) {
@@ -371,21 +565,20 @@
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 = [];
  Object.assign(modelForm.value, createDefaultModelForm());
  drawingFileList.value = [];
  if (type === "edit") {
    modelForm.value = { ...data };
    Object.assign(modelForm.value, data);
    modelForm.value.tempFileIds = data.tempFileIds || [];
    modelForm.value.salesLedgerFiles = data.salesLedgerFiles || [];
    if (data.drawingFile) {
    // 处理图纸文件反显
    if (data.salesLedgerFiles && data.salesLedgerFiles.length > 0) {
      drawingFileList.value = data.salesLedgerFiles.map(file => ({
        id: file.id,  // 带上id用于删除时调用接口
        name: file.name,
        url: file.url
      }));
    } else if (data.drawingFile) {
      drawingFileList.value = [{
        name: data.drawingFile.split('/').pop(),
        url: data.drawingFile
@@ -397,7 +590,13 @@
const submitModelForm = () => {
  modelFormRef.value.validate((valid) => {
    if (valid) {
      addOrEditProductModel(modelForm.value).then((res) => {
      // 构建提交数据,确保 routeId 为空时传 null,同时清空 routeName
      const submitData = {
        ...modelForm.value,
        routeId: modelForm.value.routeId || 0,
        routeName: modelForm.value.routeId ? modelForm.value.routeName : null
      };
      addOrEditProductModel(submitData).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeModelDia();
        getModelList();
@@ -407,7 +606,9 @@
};
const closeModelDia = () => {
  modelFormRef.value.resetFields();
  modelFormRef.value?.resetFields();
  Object.assign(modelForm.value, createDefaultModelForm());
  drawingFileList.value = [];
  modelDia.value = false;
};
@@ -515,22 +716,96 @@
  console.log('上传成功响应', response);
  console.log('response.data', response.data);
  if (response.code === 200) {
    modelForm.value.tempFileIds = [response.data?.tempId];
    modelForm.value.salesLedgerFiles = [{
    file.url = response.data?.tempPath || file.url;
    file.name = response.data?.originalName || file.name;
    file.tempId = response.data?.tempId;
    // 支持多文件,追加到数组
    modelForm.value.tempFileIds.push(response.data?.tempId);
    modelForm.value.salesLedgerFiles.push({
      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 getDrawingFileUrl = (file) => {
  return file.url || file.response?.data?.tempPath || file.tempPath || "";
};
const getDrawingFileName = (file) => {
  return file.name || file.originalName || getDrawingFileUrl(file).split("/").pop() || "";
};
const getDrawingFileExtension = (file) => {
  const name = getDrawingFileName(file).split("?")[0];
  const nameParts = name.split(".");
  return nameParts.length > 1 ? nameParts.pop().toLowerCase() : "file";
};
const isImageFile = (file) => {
  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(getDrawingFileExtension(file));
};
const isDrawingImageFile = (url) => {
  if (!url) return false;
  const ext = url.split("?")[0].split(".").pop()?.toLowerCase();
  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext);
};
const getFileExtensionFromUrl = (url) => {
  if (!url) return "file";
  const cleanUrl = url.split("?")[0];
  const parts = cleanUrl.split(".");
  return parts.length > 1 ? parts.pop().toLowerCase() : "file";
};
const handleDrawingPreview = (file) => {
  const fileUrl = getDrawingFileUrl(file);
  if (!fileUrl) {
    return;
  }
  filePreviewRef.value?.open(fileUrl);
};
const handlePreviewFile = (file) => {
  const fileUrl = file.url || file.tempPath || "";
  if (!fileUrl) {
    return;
  }
  filePreviewRef.value?.open(fileUrl);
};
const handlePreviewDrawing = (drawingFile) => {
  if (!drawingFile) {
    return;
  }
  filePreviewRef.value?.open(drawingFile);
};
const handleDrawingRemove = (file) => {
  modelForm.value.tempFileIds = [];
  modelForm.value.salesLedgerFiles = [];
  // 如果是编辑模式下已存在的文件(带有id),调用删除接口
  if (file.id) {
    delLedgerFile({ id: file.id }).then(res => {
      if (res.code === 200) {
        proxy.$modal.msgSuccess("删除成功");
      }
    }).catch(err => {
      console.error("删除文件失败:", err);
    });
  }
  // 从数组中移除对应的文件
  const index = modelForm.value.salesLedgerFiles.findIndex(item =>
    item.tempId === file.response?.data?.tempId || item.tempId === file.tempId
  );
  if (index > -1) {
    modelForm.value.tempFileIds.splice(index, 1);
    modelForm.value.salesLedgerFiles.splice(index, 1);
  }
};
onMounted(() => {
@@ -579,20 +854,159 @@
.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 148px;
  height: 148px;
  width: 88px;
  height: 88px;
  text-align: center;
  line-height: 148px;
  line-height: 88px;
}
:deep(.el-upload--picture-card) {
  width: 148px;
  height: 148px;
:deep(.el-upload--text) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 88px;
  height: 88px;
  border: 1px dashed #dcdfe6;
  border-radius: 8px;
}
:deep(.el-upload-list__item) {
  width: 148px;
  height: 148px;
:deep(.el-upload-list--text) {
  margin-top: 8px;
}
.drawing-preview-list {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 12px;
}
.drawing-preview-card {
  width: 120px;
  cursor: pointer;
}
.drawing-preview-image,
.drawing-preview-placeholder {
  width: 120px;
  height: 120px;
  border: 1px solid #dcdfe6;
  border-radius: 8px;
  background: #f5f7fa;
}
.drawing-preview-image {
  object-fit: cover;
}
.drawing-files-list {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
}
.drawing-file-tag {
  cursor: pointer;
  max-width: 120px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.drawing-file-tag:hover {
  color: #409eff;
}
.drawing-file-link {
  color: #409eff;
  cursor: pointer;
  text-decoration: underline;
}
.drawing-file-link:hover {
  color: #66b1ff;
}
.drawing-thumbs-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.drawing-thumb-item {
  width: 50px;
  height: 50px;
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  border: 1px solid #dcdfe6;
  transition: all 0.2s;
}
.drawing-thumb-item:hover {
  border-color: #409eff;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
  transform: scale(1.05);
}
.drawing-thumb-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.drawing-thumb-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 600;
  color: #606266;
  background: #f5f7fa;
}
.drawing-thumb-more {
  width: 50px;
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: #909399;
  background: #f5f7fa;
  border-radius: 4px;
  border: 1px dashed #dcdfe6;
}
.drawing-preview-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: 600;
  color: #606266;
}
.drawing-preview-placeholder.is-pdf {
  color: #f56c6c;
  background: #fef0f0;
}
.drawing-preview-placeholder.is-dwg {
  color: #409eff;
  background: #ecf5ff;
}
.drawing-preview-name {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.4;
  color: #606266;
  word-break: break-all;
}
:deep(.el-upload__tip) {