| | |
| | | @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 |
| | |
| | | :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> |
| | |
| | | </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> |
| | |
| | | :limit="1" |
| | | accept=".xlsx,.xls" |
| | | :action="importUpload.url" |
| | | :http-request="importUpload.httpRequest" |
| | | :headers="importUpload.headers" |
| | | :before-upload="importUpload.beforeUpload" |
| | | :on-success="importUpload.onSuccess" |
| | |
| | | </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, |
| | |
| | | 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); |
| | |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "工艺路线", |
| | | prop: "routeName", |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "子项数量", |
| | | prop: "subItemCount", |
| | | minWidth: 100, |
| | | }, |
| | | { |
| | | label: "产品属性", |
| | | prop: "productType", |
| | | width: 100, |
| | |
| | | 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: "操作", |
| | |
| | | |
| | | const data = reactive({ |
| | | modelForm: { |
| | | productId: null, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | |
| | | drawingFile: "", |
| | | tempFileIds: [], |
| | | salesLedgerFiles: [], |
| | | remark: "", |
| | | }, |
| | | modelRules: { |
| | | productName: [ |
| | |
| | | }); |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | 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 |
| | |
| | | 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(); |
| | |
| | | }; |
| | | |
| | | const closeModelDia = () => { |
| | | modelFormRef.value.resetFields(); |
| | | modelFormRef.value?.resetFields(); |
| | | Object.assign(modelForm.value, createDefaultModelForm()); |
| | | drawingFileList.value = []; |
| | | modelDia.value = false; |
| | | }; |
| | | |
| | |
| | | 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(() => { |
| | |
| | | .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) { |