| | |
| | | <template> |
| | | <el-form :model="form" label-width="100px" :rules="formRules" ref="formRef"> |
| | | <el-form :model="form" label-width="120px" :rules="formRules" ref="formRef"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="设备名称" prop="deviceName"> |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="规格型号" prop="deviceModel"> |
| | | <el-input v-model="form.deviceModel" :disabled="(form.deviceModel != null && operationType === 'edit')" placeholder="请输入规格型号" /> |
| | | <el-input v-model="form.deviceModel" placeholder="请输入规格型号" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="设备品牌" prop="deviceBrand"> |
| | | <el-input v-model="form.deviceBrand" placeholder="请输入设备品牌" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="设备类型" prop="type"> |
| | | <el-select |
| | | v-model="form.type" |
| | | placeholder="请选择或输入设备类型" |
| | | clearable |
| | | filterable |
| | | allow-create |
| | | default-first-option |
| | | style="width: 100%" |
| | | @change="handleDeviceTypeChange" |
| | | > |
| | | <el-option |
| | | v-for="item in deviceTypeOptions" |
| | | :key="item" |
| | | :label="item" |
| | | :value="item" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="工序" prop="productProcessId"> |
| | | <el-select |
| | | v-model="form.productProcessId" |
| | | placeholder="请选择工序" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in processOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="启用折旧" prop="enableDepreciation"> |
| | | <el-switch v-model="form.enableDepreciation" :active-value="true" :inactive-value="false" /> |
| | | <el-form-item label="启用折旧" prop="isDepr"> |
| | | <el-switch v-model="form.isDepr" :active-value="1" :inactive-value="2" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12" v-if="form.isDepr === 1"> |
| | | <el-form-item label="每年折旧金额" prop="annualDepreciationAmount"> |
| | | <el-input-number |
| | | :step="0.01" |
| | | :min="0" |
| | | style="width: 100%" |
| | | v-model="form.annualDepreciationAmount" |
| | | placeholder="请输入每年折旧金额" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="数量" prop="number"> |
| | | <el-input-number :step="0.01" :min="0" style="width: 100%" |
| | | <el-input-number :min="1" style="width: 100%" |
| | | v-model="form.number" |
| | | disabled |
| | | placeholder="请输入数量" |
| | | @change="mathNum" |
| | | /> |
| | |
| | | @change="mathNum" |
| | | > |
| | | <el-option label="1" :value="1" /> |
| | | <el-option label="3" :value="3" /> |
| | | <el-option label="6" :value="6" /> |
| | | <el-option label="9" :value="9" /> |
| | | <el-option label="13" :value="13" /> |
| | | </el-select> |
| | | </el-form-item> |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="预计运行时间" prop="planRuntimeTime"> |
| | | <el-date-picker |
| | | style="width: 100%" |
| | | v-model="form.planRuntimeTime" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="请选择录入日期" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="设备图片" prop="deviceImage"> |
| | | <div v-if="displayedExistingProblems.length" class="problem-existing-wrap"> |
| | | <div v-for="img in displayedExistingProblems" :key="img.id" class="problem-thumb"> |
| | | <el-image :src="img.url" fit="cover" class="problem-thumb-img" @click="previewProblem(img.url)" /> |
| | | <el-icon class="problem-thumb-remove" @click.stop="markProblemRemove(img.id)"><Close /></el-icon> |
| | | </div> |
| | | </div> |
| | | <el-upload |
| | | class="repair-attachment-upload" |
| | | v-model:file-list="attachmentFileList" |
| | | drag |
| | | multiple |
| | | :auto-upload="false" |
| | | :limit="9" |
| | | accept="image/png,image/jpeg,image/jpg" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <el-icon class="repair-upload-icon"><UploadFilled /></el-icon> |
| | | <div class="el-upload__text">将文件拖到此处,或 <em>点击选择文件</em></div> |
| | | <template #tip> |
| | | <div class="el-upload__tip"> |
| | | 支持 png / jpg / jpeg,单张不超过 50MB,最多 9 张 |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import useFormData from "@/hooks/useFormData"; |
| | | // import useUserStore from "@/store/modules/user"; |
| | | import { getLedgerById } from "@/api/equipmentManagement/ledger"; |
| | | import { getLedgerById, getLedgerFileList, uploadLedgerFile, deleteLedgerFile } from "@/api/equipmentManagement/ledger"; |
| | | import { processList } from "@/api/productionManagement/productionProcess"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | calculateTaxIncludeTotalPrice, |
| | | calculateTaxExclusiveTotalPrice, |
| | | } from "@/utils/summarizeTable"; |
| | | import { ElMessage } from "element-plus"; |
| | | import {ref} from "vue"; |
| | | import { ref, onMounted, getCurrentInstance } from "vue"; |
| | | import { UploadFilled, Close } from "@element-plus/icons-vue"; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | defineOptions({ |
| | | name: "设备台账表单", |
| | | }); |
| | | const formRef = ref(null); |
| | | const operationType = ref(''); |
| | | // 设备类型固定选项 |
| | | const deviceTypeOptions = ref([ |
| | | '生产设备', |
| | | '办公设备', |
| | | '检测设备', |
| | | '运输设备', |
| | | '其他设备' |
| | | ]); |
| | | const processOptions = ref([]); |
| | | const formRules = { |
| | | deviceName: [{ required: true, trigger: "blur", message: "请输入" }], |
| | | deviceModel: [{ required: true, trigger: "blur", message: "请输入" }], |
| | | type: [{ required: true, trigger: "change", message: "请选择或输入设备类型" }], |
| | | supplierName: [{ required: true, trigger: "blur", message: "请输入" }], |
| | | unit: [{ required: true, trigger: "blur", message: "请输入" }], |
| | | number: [{ required: true, trigger: "blur", message: "请输入" }], |
| | | taxIncludingPriceUnit: [{ required: true, trigger: "blur", message: "请输入" }], |
| | | taxRate: [{ required: true, trigger: "change", message: "请输入" }], |
| | | planRuntimeTime: [{ required: true, trigger: "change", message: "请选择" }], |
| | | annualDepreciationAmount: [ |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (form.isDepr === 1 && (value === undefined || value === null || value === '')) { |
| | | callback(new Error('启用折旧时,请输入每年折旧金额')); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | trigger: "blur" |
| | | } |
| | | ], |
| | | } |
| | | |
| | | const { form, resetForm } = useFormData({ |
| | | deviceName: undefined, // 设备名称 |
| | | deviceModel: undefined, // 规格型号 |
| | | deviceBrand: undefined, // 设备品牌 |
| | | type: undefined, // 设备类型 |
| | | productProcessId: undefined, // 工序ID |
| | | supplierName: undefined, // 供应商 |
| | | storageLocation: undefined, // 存放位置 |
| | | enableDepreciation: false, // 是否启用折旧 |
| | | isDepr: 2, // 是否启用折旧 1-是 2-否 |
| | | annualDepreciationAmount: undefined, // 每年折旧金额 |
| | | unit: undefined, // 单位 |
| | | number: undefined, // 数量 |
| | | number: 1, // 数量 |
| | | taxIncludingPriceUnit: undefined, // 含税单价 |
| | | taxIncludingPriceTotal: undefined, // 含税总价 |
| | | taxRate: undefined, // 税率 |
| | | unTaxIncludingPriceTotal: undefined, // 不含税总价 |
| | | // createUser: useUserStore().nickName, // 录入人 |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 录入日期 |
| | | planRuntimeTime: dayjs().format("YYYY-MM-DD"), // 录入日期 |
| | | }); |
| | | |
| | | const attachmentFileList = ref([]); |
| | | const existingProblemImages = ref([]); |
| | | const pendingRemoveProblemIds = ref([]); |
| | | |
| | | const displayedExistingProblems = computed(() => |
| | | existingProblemImages.value.filter((img) => !pendingRemoveProblemIds.value.includes(img.id)) |
| | | ); |
| | | |
| | | const markProblemRemove = (fileId) => { |
| | | if (!pendingRemoveProblemIds.value.includes(fileId)) { |
| | | pendingRemoveProblemIds.value.push(fileId); |
| | | } |
| | | }; |
| | | |
| | | const loadForm = async (id) => { |
| | | if (id) { |
| | |
| | | form.deviceName = data.deviceName; |
| | | form.deviceModel = data.deviceModel; |
| | | form.deviceBrand = data.deviceBrand; |
| | | form.type = data.type; |
| | | const processName = data.productProcessName || data.processName || data.process; |
| | | form.productProcessId = data.productProcessId; |
| | | if (!form.productProcessId && processName) { |
| | | const matched = processOptions.value.find(item => item.label === processName); |
| | | form.productProcessId = matched?.value; |
| | | } |
| | | ensureProcessOptionExists(form.productProcessId, processName); |
| | | form.supplierName = data.supplierName; |
| | | form.storageLocation = data.storageLocation; |
| | | form.enableDepreciation = data.enableDepreciation; |
| | | form.isDepr = data.isDepr; |
| | | form.annualDepreciationAmount = data.annualDepreciationAmount; |
| | | form.unit = data.unit; |
| | | form.number = data.number; |
| | | form.number = 1; |
| | | form.taxIncludingPriceUnit = data.taxIncludingPriceUnit; |
| | | form.taxIncludingPriceTotal = data.taxIncludingPriceTotal; |
| | | form.taxRate = data.taxRate; |
| | | form.unTaxIncludingPriceTotal = data.unTaxIncludingPriceTotal; |
| | | form.createTime = data.createTime; |
| | | // 预计运行时间:后端返回后转为 YYYY-MM-DD 以便日期选择器正确展示 |
| | | if (data.planRuntimeTime) { |
| | | form.planRuntimeTime = dayjs(data.planRuntimeTime).format('YYYY-MM-DD'); |
| | | } else { |
| | | form.planRuntimeTime = undefined; |
| | | } |
| | | } |
| | | |
| | | existingProblemImages.value = []; |
| | | pendingRemoveProblemIds.value = []; |
| | | attachmentFileList.value = []; |
| | | if (id) { |
| | | try { |
| | | const res = await getLedgerFileList(id); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | existingProblemImages.value = list.map((item) => ({ |
| | | id: item.id, |
| | | name: item.name, |
| | | url: getFileAccessUrl(item), |
| | | })); |
| | | } catch { |
| | | existingProblemImages.value = []; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const ensureProcessOptionExists = (id, name) => { |
| | | if (!id) return; |
| | | if (!processOptions.value.some(item => item.value === id)) { |
| | | processOptions.value.push({ |
| | | value: id, |
| | | label: name || `${id}`, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const getProcessOptions = async () => { |
| | | try { |
| | | const { code, data } = await processList({}); |
| | | if (code === 200 && Array.isArray(data)) { |
| | | processOptions.value = data |
| | | .filter(item => item?.id !== undefined && item?.id !== null) |
| | | .map(item => ({ |
| | | value: item.id, |
| | | label: item?.name || `${item.id}`, |
| | | })); |
| | | ensureProcessOptionExists(form.productProcessId); |
| | | } |
| | | } catch (error) { |
| | | processOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | const handleDeviceTypeChange = (value) => { |
| | | // 如果输入的新值不在固定选项中,则添加到选项列表 |
| | | if (value && !deviceTypeOptions.value.includes(value)) { |
| | | deviceTypeOptions.value.push(value); |
| | | } |
| | | }; |
| | | |
| | | const mathNum = () => { |
| | | if (!form.taxIncludingPriceUnit) { |
| | | ElMessage.error("请输入单价"); |
| | | return; |
| | | } |
| | | if (!form.number) { |
| | | ElMessage.error("请输入数量"); |
| | | return; |
| | | } |
| | | form.taxIncludingPriceTotal = calculateTaxIncludeTotalPrice( |
| | |
| | | clearValidate(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getProcessOptions(); |
| | | }); |
| | | |
| | | const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); |
| | | const headers = ref({ Authorization: "Bearer " + getToken() }); |
| | | const ATTACH_MAX_MB = 50; |
| | | |
| | | const beforeUpload = (rawFile) => { |
| | | const okType = ["image/jpeg", "image/jpg", "image/png"].includes(rawFile.type); |
| | | if (!okType) { |
| | | ElMessage.error("只能上传 png / jpg / jpeg 图片"); |
| | | return false; |
| | | } |
| | | const maxBytes = ATTACH_MAX_MB * 1024 * 1024; |
| | | if (rawFile.size > maxBytes) { |
| | | ElMessage.error(`单个文件不能超过 ${ATTACH_MAX_MB}MB`); |
| | | return false; |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | const submitFiles = async (ledgerId) => { |
| | | for (const fileId of pendingRemoveProblemIds.value) { |
| | | await deleteLedgerFile(fileId); |
| | | } |
| | | pendingRemoveProblemIds.value = []; |
| | | |
| | | const rows = attachmentFileList.value || []; |
| | | const files = rows.map((f) => f.raw).filter(Boolean); |
| | | if (!files.length || ledgerId == null) return; |
| | | |
| | | for (const file of files) { |
| | | const fd = new FormData(); |
| | | fd.append("file", file); |
| | | fd.append("deviceLedgerId", String(ledgerId)); |
| | | // 16 is the FileNameType for EQUIPMENT_LEDGER that we added |
| | | fd.append("fileType", "16"); |
| | | const res = await uploadLedgerFile(fd); |
| | | if (res.code !== 200) { |
| | | throw new Error(res.msg || "附件上传失败"); |
| | | } |
| | | } |
| | | attachmentFileList.value = []; |
| | | }; |
| | | |
| | | const getFileAccessUrl = (file = {}) => { |
| | | let raw = file?.link || file?.url || ""; |
| | | if (String(raw).startsWith("http")) return raw; |
| | | const javaApi = getCurrentInstance()?.proxy?.javaApi || import.meta.env.VITE_APP_BASE_API || "/dev-api"; |
| | | let fileUrl = raw; |
| | | if (fileUrl.indexOf("\\") > -1) { |
| | | const lowerPath = fileUrl.toLowerCase(); |
| | | const uploadPathIndex = lowerPath.indexOf("uploadpath"); |
| | | if (uploadPathIndex > -1) { |
| | | fileUrl = fileUrl.substring(uploadPathIndex).replace(/\\/g, "/"); |
| | | } else { |
| | | fileUrl = fileUrl.replace(/\\/g, "/"); |
| | | } |
| | | } |
| | | fileUrl = fileUrl.replace(/^\/?uploadPath/, "/profile"); |
| | | if (fileUrl.startsWith("upload/")) { |
| | | fileUrl = "/profile/" + fileUrl; |
| | | } |
| | | if (!fileUrl.startsWith("http")) { |
| | | if (!fileUrl.startsWith("/")) fileUrl = "/" + fileUrl; |
| | | fileUrl = javaApi + fileUrl; |
| | | } |
| | | return fileUrl; |
| | | }; |
| | | |
| | | const previewProblem = (url) => { |
| | | window.open(url, "_blank"); |
| | | }; |
| | | |
| | | defineExpose({ |
| | | form, |
| | | loadForm, |
| | |
| | | clearValidate, |
| | | resetFormAndValidate, |
| | | formRef, |
| | | submitFiles, |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .repair-attachment-upload { |
| | | width: 100%; |
| | | :deep(.el-upload) { |
| | | width: 100%; |
| | | } |
| | | :deep(.el-upload-dragger) { |
| | | width: 100%; |
| | | padding: 24px 16px; |
| | | } |
| | | } |
| | | .repair-upload-icon { |
| | | font-size: 48px; |
| | | color: var(--el-color-primary); |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .problem-existing-wrap { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .problem-thumb { |
| | | position: relative; |
| | | width: 88px; |
| | | height: 88px; |
| | | border-radius: 6px; |
| | | overflow: hidden; |
| | | border: 1px solid var(--el-border-color); |
| | | } |
| | | |
| | | .problem-thumb-img { |
| | | width: 100%; |
| | | height: 100%; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .problem-thumb-remove { |
| | | position: absolute; |
| | | top: 2px; |
| | | right: 2px; |
| | | font-size: 16px; |
| | | color: #fff; |
| | | background: rgba(0, 0, 0, 0.45); |
| | | border-radius: 50%; |
| | | padding: 2px; |
| | | cursor: pointer; |
| | | } |
| | | </style> |