yxh
21 小时以前 3873d3c350c8c1edd7bfdc8afe08c9e78f56d968
src/views/project/fund/applyDetail/index.vue
@@ -12,7 +12,7 @@
          </el-col>
          <el-col :span="5">
            <el-form-item label="申请日期" prop="applyTime">
              <el-date-picker clearable :disabled="true" style="width: 100%" v-model="form.applyTime" type="date"
              <el-date-picker clearable :disabled="true" style="width: 100%" v-model="form.applyTime" type="date" format="yyyy-MM-dd"
                value-format="yyyy-MM-dd HH:mm:ss" placeholder="日   期">
              </el-date-picker>
            </el-form-item>
@@ -68,15 +68,7 @@
            <el-form-item label="捐献案例" prop="name">
              <el-input :disabled="true" v-model="form.donorname" placeholder="请输入捐献者姓名" />
            </el-form-item>
          </el-col>
          <el-col :span="4">
            <div style="margin-left: 10px;">
              <dict-tag
                :options="dict.type.sys_DonationStatus"
                :value="form.invoicecount"
              />
            </div>
          </el-col>
          </el-col>
          <el-col :span="10">
            <el-form-item label="申请说明" prop="remark">
              <el-input v-model="form.remark" placeholder="申请说明" />
@@ -795,6 +787,7 @@
import OrgSelecter from "@/views/project/components/orgselect";
import { getToken } from "@/utils/auth";
import { moneySum, moneyMul, moneySub, moneyRound } from "@/utils/money";
import { checkFileName, checkDetails } from "@/utils/fileNameCheck";
import CustomImageViewer from "@/components/CustomImageViewer"; // 根据你的路径调整
export default {
@@ -831,9 +824,10 @@
        return callback();
      }
      const regex = /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}([0-9]|X)$/;
      if (regex.test(value)) {
      } else {
        callback(new Error("身份证号码格式错误"));
      // 2026-10-19:格式不合法立即早退。原来写成 if/else 但 else 里没有 return,
      // 会继续往下走异步查重,导致 callback 被调用两次、"身份证号码格式错误"被后续结果覆盖。
      if (!regex.test(value)) {
        return callback(new Error("身份证号码格式错误"));
      }
      // 2. 调用后端接口进行重复性校验
@@ -966,6 +960,10 @@
      },
      dialogrules: {
        // 2026-10-19:账户名称(账户来源=从账户库 时的下拉)也是必输项
        accountname: [
          { required: true, message: "请选择账户名称", trigger: "change" }
        ],
        usertype: [
          { required: true, message: "请输入账户类型", trigger: "blur" }
        ],
@@ -975,6 +973,10 @@
        unitTaxNo: [{ required: true, message: "请输入税号", trigger: "blur" }],
        idcardno: [
          {
            // 2026-10-19:加 required 标记只为让 el-form-item 标出必输星号(Element 的 isRequired 只认 rule.required)。
            // 本规则带 validator,async-validator 的 getValidationMethod 会直接返回该 validator、跳过内建 required 检查,
            // 所以校验行为与改动前完全一致;另外**不能加 message**,否则会覆盖下面自定义 validator 的报错文案。
            required: true,
            // 账户类型选择"单位机构"时不需要身份证号
            validator: (rule, value, callback) => {
              if (this.hasUserType(this.accountselectform.usertype, "org")) {
@@ -1561,6 +1563,11 @@
    },
    /** 提交按钮 */
    submitForm: debounce(function () {
      // 2026-10-15:保存前附件文件名规范校验(仅新增单据;老单据的历史附件由后端按"只校验本次变更"放行)
      if (!this.checkDetailFileNames()) {
        return;
      }
      if (!this.randomnumber) {
        this.randomnumber = this.generateRandomString(9);
        this.form.randomnumber = this.randomnumber;
@@ -2343,6 +2350,11 @@
    },
    // 接受提醒立刻保存
    Savenow() {
      // 2026-10-15:保存前附件文件名规范校验(仅新增单据;老单据的历史附件由后端按"只校验本次变更"放行)
      if (!this.checkDetailFileNames()) {
        return;
      }
      if (!this.randomnumber) {
        this.randomnumber = this.generateRandomString(9);
        this.form.randomnumber = this.randomnumber;
@@ -2510,11 +2522,7 @@
                    } else {
                      this.$modal.closeLoading();
                      if (res.msg) {
                        this.$modal.msgError("操作失误");
                      } else {
                        this.$modal.msgError(res.msg);
                      }
                      this.$modal.msgError(res.msg || "操作失败");
                    }
                  });
                }
@@ -2590,6 +2598,23 @@
      });
      this.batchdialogVisible = true;
    },
    /**
     * 明细行是否为「空记录」:服务项目、专家、专家所在单位 是否都为空
     * 用于批量录入专家时判断"复用最后一行"还是"追加新行"
     */
    isBlankDetailRow(row) {
      if (!row) {
        return false;
      }
      const isEmpty = v =>
        v === null || v === undefined || String(v).trim() === "";
      return (
        isEmpty(row.itemid) &&
        isEmpty(row.beneficiaryname) &&
        isEmpty(row.unitno) &&
        isEmpty(row.unitname)
      );
    },
    /** 批量录入弹框:姓名/单位/工号/身份证后4位 命中判断 */
    personMatchKeyword(item, kw) {
      const idcard = String(item.idcrad || "");
@@ -2615,6 +2640,17 @@
          existed[String(row.beneficiaryname).trim()] = true;
        }
      });
      // 2026-10-22:修正"导入后表格多出一行空记录"。
      // 规则:先看最后一条记录,若它的"服务项目 / 专家 / 专家所在单位"都是空的,
      // 说明是一条空记录,就从这一行开始填;否则追加新行。
      let writeIndex = this.rbDetails.length;
      if (
        writeIndex > 0 &&
        this.isBlankDetailRow(this.rbDetails[writeIndex - 1])
      ) {
        writeIndex -= 1;
      }
      const duplicated = [];
      this.checkedCities.forEach(item => {
@@ -2674,7 +2710,13 @@
          annexfiles: null
        };
        this.rbDetails.push(rowData);
        if (writeIndex < this.rbDetails.length) {
          // 复用最后那条空记录
          this.rbDetails.splice(writeIndex, 1, rowData);
        } else {
          this.rbDetails.push(rowData);
        }
        writeIndex++;
      });
      for (let i = 0; i < this.rbDetails.length; i++) {
        this.rbDetails[i].orderno = i + 1;
@@ -2931,7 +2973,43 @@
      }
    },
    /**
     * 2026-10-15:上传时校验附件文件名(业务要求:不合格立即提示且不加入列表)。
     * 规则与后端 AttachmentNameValidator 同口径:必须含中文 / 去掉扩展名后主干 ≤20 字 /
     * 不含空格及 / \ : * ? " < > | # % 等特殊字符。
     */
    checkUploadFileName(file) {
      const raw = file && file.raw ? file.raw.name : file && file.name;
      const r = checkFileName(raw);
      if (!r.ok) {
        this.$modal.msgError(r.msg);
        return false;
      }
      return true;
    },
    /**
     * 2026-10-15:保存前校验整单明细附件文件名(含同一行内重名)。
     * 口径(业务确认):只校验"本次变更"的附件——老单据里已存在的不合规附件名不能把保存卡死
     * (否则老单据改一个金额都存不进去)。后端按 id 读旧记录放行历史附件;前端拿不到"哪些是历史"
     * 的权威信息,因此**仅新增单据**做保存前拦截,编辑老单据时新增的不合规附件由后端二次校验拦下。
     */
    checkDetailFileNames() {
      if (this.form && this.form.id) {
        return true;
      }
      const c = checkDetails(this.rbDetails);
      if (!c.ok) {
        this.$modal.msgError(c.msg);
        return false;
      }
      return true;
    },
    uploadSccess(response, file, fileList) {
      if (!this.checkUploadFileName(file)) {
        return;
      }
      this.rbDetails;
      const config = {
        headers: { Authorization: "Bearer " + this.ICDtoken }
@@ -3305,6 +3383,10 @@
    //专家/医疗机构/费用报销机构选择
    confirmSelectionDialog() {
      this.$refs["accountselectform"].validate(valid => {
        // 2026-10-19:原来没有判断 valid,校验失败也照样往下走、照样保存(星号/红字形同虚设)
        if (!valid) {
          return;
        }
        let tempIndex = this.accountselectform.index;
        if (this.selectionType == "account") {
          if (this.accountfrom == "1") {