gongchunyi
2026-06-22 492802e4fc1b371ba21a2a490c8dcd67d7c8b29c
src/views/equipmentManagement/ledger/Form.vue
@@ -1,5 +1,5 @@
<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">
@@ -8,12 +8,51 @@
      </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">
@@ -32,14 +71,26 @@
        </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"
          />
@@ -81,7 +132,9 @@
            @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>
@@ -114,53 +167,137 @@
          />
        </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) {
@@ -171,26 +308,88 @@
    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(
@@ -216,6 +415,81 @@
  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,
@@ -223,5 +497,58 @@
  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>