yxh
2026-09-30 d21d7dd23f15a0ca395a4c9d1b3bb31330ff56c0
专家费校验
已修改10个文件
967 ■■■■■ 文件已修改
package.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/project/fund.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/documentfiles/index.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/externalperson/index.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/fund/applyDetail/index.vue 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/fund/expertFeeApply/index.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/fund/performancedetails/index.vue 388 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/fundcheck/expertFeeCheck/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/travelexpenseapply/travelexpensedetail/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vue.config.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -5,7 +5,7 @@
  "author": "浙江亥尔时信息科技有限公司",
  "license": "MIT",
  "scripts": {
    "dev": "vue-cli-service serve",
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "build:stage": "vue-cli-service build --mode staging",
    "preview": "node build/index.js --preview",
src/api/project/fund.js
@@ -160,6 +160,16 @@
  })
}
// 专家劳务费:保存前的费用控制校验(干跑,不落库)
// 返回 { errors: [阻断性信息], warnings: [提示信息], blocked: 是否阻断 }
export function validateExpertFee(data) {
  return request({
    url: '/project/fund/validateExpertFee',
    method: 'post',
    data: data
  })
}
// 伦理专家费打印
export function getdownloadLL(id) {
  return request({
src/views/project/documentfiles/index.vue
@@ -659,34 +659,100 @@
    download() {
      let list = this.fileList;
      if (list.length > 0) {
        let downUrl = list[0].url;
        const link = document.createElement("a");
        link.href = this.baseUrl + downUrl;
        link.setAttribute("download", this.getFileName(list[0].name));
        document.body.appendChild(link);
        link.click();
        this.saveFile(list[0].url, this.getFileName(list[0].name, list[0].url));
      } else {
        this.$modal.msgWarning("未获取到文件地址,请确认");
      }
    },
    handleDownload(row) {
      if (row.fileurl != "" && row.fileurl != null) {
        const link = document.createElement("a");
        link.href = this.baseUrl + row.fileurl;
        link.setAttribute("download", this.getFileName(row.fileurl));
        document.body.appendChild(link);
        link.click();
        this.saveFile(
          row.fileurl,
          this.getFileName(row.filename, row.fileurl, row.filename)
        );
      } else {
        this.$modal.msgWarning("未获取到文件地址,请确认");
      }
    },
    // 获取文件名称
    getFileName(name) {
      if (name.lastIndexOf("/") > -1) {
        return name.slice(name.lastIndexOf("/") + 1).toLowerCase();
      } else {
        return "";
    /**
     * 下载文件(必须带鉴权)。
     *
     * 背景:/profile/** 静态资源未在 SecurityConfig 中放行(anyRequest().authenticated()),
     * 而后端 TokenService 只从请求头 Authorization 取 token。
     * 因此用 <a href> 直接跳转下载不会携带 token,服务端返回 401,表现为“点下载没反应/下不到文件”。
     * 这里改为 fetch 带 Authorization 头取回文件流,再用 blob 本地另存。
     *
     * @param {string} fileUrl 文件地址(库中存的是 /profile/upload/... 相对路径)
     * @param {string} fileName 另存为的文件名
     */
    saveFile(fileUrl, fileName) {
      if (!fileUrl) {
        this.$modal.msgWarning("未获取到文件地址,请确认");
        return;
      }
      // 兼容历史数据中已是绝对地址的情况,避免重复拼接
      const isAbsolute = /^https?:\/\//i.test(fileUrl);
      const url = isAbsolute ? fileUrl : this.baseUrl + fileUrl;
      this.$modal.loading("正在下载文件,请耐心等待!");
      fetch(url, {
        method: "get",
        headers: { Authorization: "Bearer " + getToken() },
      })
        .then((response) => {
          if (!response.ok) {
            // 401/403/404 必须显式提示,否则会下到一个内容为错误页的文件
            throw new Error("HTTP " + response.status);
          }
          return response.blob();
        })
        .then((blob) => {
          const blobUrl = window.URL.createObjectURL(blob);
          const link = document.createElement("a");
          link.href = blobUrl;
          link.setAttribute("download", fileName || "下载文件");
          document.body.appendChild(link);
          link.click();
          document.body.removeChild(link);
          window.URL.revokeObjectURL(blobUrl);
        })
        .catch((err) => {
          console.error("文件下载失败:" + url, err);
          this.$modal.msgError("文件下载失败,请确认文件是否存在或登录是否过期");
        })
        .finally(() => {
          this.$modal.closeLoading();
        });
    },
    /**
     * 获取另存为的文件名。
     *
     * 优先级:带扩展名的原始文件名 > 文件地址末段 > 文档标题。
     * 之所以不直接用 form.filename:该字段在表单中可手工编辑(如“人体器官移植条例”),
     * 不带扩展名,直接用作另存名会导致系统无法识别打开方式。
     *
     * @param {string} storedName 原始文件名(如 form.filename 中存的 xxx.docx)
     * @param {string} fileUrl 文件地址
     * @param {string} title 备用名称(文档标题)
     */
    getFileName(storedName, fileUrl, title) {
      const hasExt = (name) => !!name && name.lastIndexOf(".") > -1;
      if (hasExt(storedName)) {
        return storedName;
      }
      const source = fileUrl || "";
      if (source.lastIndexOf("/") > -1) {
        const fromUrl = source.slice(source.lastIndexOf("/") + 1);
        if (fromUrl) {
          return fromUrl;
        }
      }
      return title || storedName || "下载文件";
    },
    //文件上传成功回调
src/views/project/externalperson/index.vue
@@ -47,9 +47,10 @@
    <el-table v-loading="loading" :data="externalpersonList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="账号类别" align="center" prop="usertype">
      <el-table-column label="账号类别" align="center" prop="usertype" width="220">
        <template slot-scope="scope">
          <dict-tag :options="dict.type.sys_UserType" :value="scope.row.usertype" />
          <dict-tag v-for="t in splitUserType(scope.row.usertype)" :key="t"
            :options="dict.type.sys_UserType" :value="t" />
        </template>
      </el-table-column>
      <el-table-column label="账号编号" align="center" prop="userno" />
@@ -90,7 +91,7 @@
      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
        <el-row>
          <el-col :span="12"><el-form-item label="账号类别" prop="usertype">
              <el-select v-model="form.usertype" placeholder="请选择账号类别">
              <el-select v-model="form.usertype" multiple collapse-tags placeholder="请选择账号类别(可多选)">
                <el-option v-for="dict in dict.type.sys_UserType" :key="dict.value" :label="dict.label"
                  :value="dict.value"></el-option>
              </el-select>
@@ -118,14 +119,14 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="form.usertype != 'org'">
        <el-row v-if="!isUnitForm">
          <el-col :span="24">
            <el-form-item label="身份证号" prop="idcardno">
              <el-input v-model="form.idcardno" placeholder="请输入身份证号" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="form.usertype != 'org'"><el-col :span="12">
        <el-row v-if="!isUnitForm"><el-col :span="12">
            <el-form-item label="性别" prop="sex">
              <el-select v-model="form.sex" placeholder="请选择性别">
                <el-option v-for="dict in dict.type.sys_user_sex" :key="dict.value" :label="dict.label"
@@ -140,7 +141,7 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="form.usertype != 'org'">
        <el-row v-if="!isUnitForm">
          <el-col :span="12">
            <el-form-item label="所在单位" prop="unitname">
              <el-select v-model="form.unitno" placeholder="所在单位" clearable filterable allow-create style="width: 100%">
@@ -272,7 +273,19 @@
          { required: true, message: "请输入账户名称", trigger: "blur" }
        ],
        idcardno: [
          { required: true, message: "请输入身份证号码", trigger: "blur" }
          {
            //账号类别为"单位机构"时无需身份证号
            validator: (rule, value, callback) => {
              if (this.hasUserType(this.form.usertype, "org")) {
                return callback();
              }
              if (!value || !String(value).trim()) {
                return callback(new Error("请输入身份证号码"));
              }
              return callback();
            },
            trigger: "blur"
          }
        ],
        bankcardno: [
          { required: true, message: "请输入银行账号", trigger: "blur" }
@@ -282,6 +295,12 @@
        ]
      },      
    };
  },
  computed: {
    /** 表单是否选择了"单位机构"(账号类别为多值,含 org 即为单位) */
    isUnitForm() {
      return this.hasUserType(this.form.usertype, "org");
    }
  },
  created() {
    this.getUserProfile();
@@ -296,6 +315,28 @@
        const managerList = ["admin", "048", "047"];
        this.ismanager = managerList.includes(res.data.userName);
      });
    },
    /** 人员类型(多值,逗号分隔)转数组 */
    splitUserType(usertype) {
      if (!usertype) {
        return [];
      }
      if (Array.isArray(usertype)) {
        return usertype;
      }
      return String(usertype)
        .split(",")
        .map(t => t.trim())
        .filter(t => t);
    },
    /** 人员类型数组转逗号分隔字符串 */
    joinUserType(usertype) {
      return Array.isArray(usertype) ? usertype.join(",") : usertype;
    },
    /** 人员类型是否包含指定类型(如 org = 单位机构) */
    hasUserType(usertype, code) {
      return this.splitUserType(usertype).indexOf(code) > -1;
    },
    /** 查询单位、人员账号列表 */
@@ -331,7 +372,7 @@
        userno: null,
        username: null,
        userstatus: 0,
        usertype: null,
        usertype: [],
        personnelunitno: null,
        delFlag: null,
        createBy: null,
@@ -383,6 +424,8 @@
      const id = row.id || this.ids;
      getExternalperson(id).then(response => {
        this.form = response.data;
        //账号类别为多值(逗号分隔),表单用数组回显
        this.form.usertype = this.splitUserType(response.data.usertype);
        this.open = true;
        this.title = "人员/单位账号信息修改";
      });
@@ -396,6 +439,8 @@
            if (res.organizationid == this.form.unitno)
              this.form.unitname = res.organizationname;
          });
          //账号类别为多值:提交前转回逗号分隔的字符串
          this.form.usertype = this.joinUserType(this.form.usertype);
          console.log(this.form);
          if (this.form.id != null) {
            updateExternalperson(this.form).then(response => {
@@ -405,15 +450,8 @@
              this.getList();
            });
          } else {
            //检查身份证唯一
            getInfoByIDCardNo(this.form.idcardno).then(res => {
              if (res.data.length > 1) {
                this.$modal.msgWarning("身份证号信息已被登记,请检查身份证号!"
                );
                return;
              }
              //保存信息
            //保存信息(单位账号没有身份证号,跳过身份证查重)
            const saveNew = () => {
              getOnlyCode(this.seqnoParams).then(res => {
                this.form.userno = res.data.currentvalue;
@@ -424,7 +462,21 @@
                  this.getList();
                });
              });
            };
            if (!this.form.idcardno || !String(this.form.idcardno).trim()) {
              saveNew();
              return;
            }
            //检查身份证唯一
            getInfoByIDCardNo(this.form.idcardno).then(res => {
              if (res.data.length > 1) {
                this.$modal.msgWarning("身份证号信息已被登记,请检查身份证号!"
                );
                return;
              }
              saveNew();
            });
          }
        }
src/views/project/fund/applyDetail/index.vue
@@ -229,8 +229,9 @@
              v-if="businessType == '1' || businessType == '2'">
              <template slot-scope="scope">
                <el-form-item label-width="0">
                  <el-input :disabled="userprofile.nickName != '陈慕华'" v-model.number="scope.row.idcardno"
                    @input="tableParValueChange1(scope.row)" placeholder="身份证号" />
                  <el-input :disabled="userprofile.nickName != '陈慕华' || scope.row.isunit == 1"
                    :placeholder="scope.row.isunit == 1 ? '单位无需填写' : '身份证号'"
                    v-model.number="scope.row.idcardno" @input="tableParValueChange1(scope.row)" />
                </el-form-item>
              </template>
            </el-table-column>
@@ -690,13 +691,19 @@
    </el-dialog>
    <!-- 批量选取 -->
    <el-dialog title="批量生成明细信息" :visible.sync="batchdialogVisible" width="30%">
    <el-dialog title="批量生成明细信息" :visible.sync="batchdialogVisible" width="35%">
      <div style="margin-bottom: 12px">
        <el-input v-model="personKeyword" clearable size="small" prefix-icon="el-icon-search"
          placeholder="输入 姓名 / 单位 / 工号 / 身份证后4位 筛查" style="width: 330px" />
        <span style="margin-left: 10px; color: #909399; font-size: 12px">共 {{ filteredCityOptions.length }} 人</span>
      </div>
      <div style="max-height: 300px; overflow-y: auto">
        <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">全选</el-checkbox>
        <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">全选(当前筛查结果)</el-checkbox>
        <div style="margin: 15px 0;"></div>
        <el-checkbox-group v-model="checkedCities" @change="handleCheckedCitiesChange">
          <el-checkbox border v-for="city in cityOptions" :label="city" :key="city.reportNo">{{ city.reportName
          }}</el-checkbox>
          <el-checkbox border v-for="city in filteredCityOptions" :label="city"
            :key="city.reportNo + '_' + city.reportName">{{ city.reportName
            }}{{ city.unitname ? " · " + city.unitname : "" }}</el-checkbox>
        </el-checkbox-group>
      </div>
@@ -736,6 +743,7 @@
  addorupdateFund,
  ManualCalculation,
  getdetailsByItemId,
  validateExpertFee,
  listbypower
} from "@/api/project/fund";
@@ -882,6 +890,8 @@
      // 网络请求头
      Networkheader: null,
      batchdialogVisible: false,
      //批量录入弹框的筛查关键字
      personKeyword: "",
      itemlist: [],
      cityOptions: [],
      checkAll: false,
@@ -964,7 +974,19 @@
        ],
        unitTaxNo: [{ required: true, message: "请输入税号", trigger: "blur" }],
        idcardno: [
          { required: true, message: '请输入身份证号', trigger: 'blur' },
          {
            // 账户类型选择"单位机构"时不需要身份证号
            validator: (rule, value, callback) => {
              if (this.hasUserType(this.accountselectform.usertype, "org")) {
                return callback();
              }
              if (!value || !String(value).trim()) {
                return callback(new Error("请输入身份证号"));
              }
              return callback();
            },
            trigger: "blur"
          },
          // 添加自定义校验规则
          { validator: validateIdCard, trigger: 'blur' }
        ],
@@ -1126,7 +1148,16 @@
  },
  //监听属性 类似于data概念
  computed: {},
  computed: {
    //批量录入弹框:按关键字(姓名/单位/工号/身份证后4位)筛查后的候选人
    filteredCityOptions() {
      const kw = (this.personKeyword || "").trim().toLowerCase();
      if (!kw) {
        return this.cityOptions;
      }
      return this.cityOptions.filter(item => this.personMatchKeyword(item, kw));
    }
  },
  //监控data中的数据变化
  watch: {},
  //方法集合
@@ -1539,6 +1570,31 @@
        if (valid) {
          this.formData = this.form;
          // 专家劳务费专用:9 条费用控制规则校验(校验不通过不允许保存)
          if (this.businessType == "1") {
            this.$modal.loading("正在校验专家劳务费用,请稍候!");
            this.checkExpertFeeRules("保存").then((allowed) => {
              this.$modal.closeLoading();
              if (allowed) {
                this.doSubmitForm();
              }
            });
            return;
          }
          this.doSubmitForm();
        }
      });
    }, 500),
    /**
     * 保存申请单的实际落库逻辑。
     *
     * 从 submitForm 中拆出,便于在专家劳务费规则校验通过后再执行;
     * 其他业务类型由 submitForm 直接调用,行为与拆分前一致。
     */
    doSubmitForm() {
          //保存前校验数据
          for (let k = 0; k < this.rbDetails.length; k++) {
            let tempDetail = this.rbDetails[k];
@@ -1736,9 +1792,84 @@
          //关闭窗口
          this.loading = false;
          // this.getfundList();
    },
    /**
     * 专家劳务费 9 条费用控制规则校验(提交前拦截)。
     *
     * 适用范围:businessType === '1'(专家劳务费申请单),与后端 applytype === '1' 对应。
     * 其他业务类型(伦理专家费、医学成本、办公费用)不校验,直接放行。
     *
     * 校验口径(同一捐献案例 infoid 下累计)由后端统一实现:
     *   errors   阻断型:提示违反的具体规则,不允许继续保存/审核
     *   warnings 提示型:仅提醒经办人处理(规则 5、6),需二次确认后才继续
     *
     * @param {string} action 当前动作,用于拼接提示文案(如“保存”“提交审核”)
     * @returns {Promise<boolean>} true 允许继续,false 阻止继续
     */
    checkExpertFeeRules(action) {
      if (this.businessType != "1") {
        return Promise.resolve(true);
        }
      // 后端按「案例内已有单据 + 本次提交明细」合并计算,确保尚未落库的明细也参与校验
      const payload = Object.assign({}, this.form, {
        applytype: this.form.applytype || "1",
        serviceFunddetails: this.rbDetails,
      });
    }, 500),
      // 排查用:能直接从控制台看到到底送了什么,以及后端判定了什么
      console.log("[专家费校验] 提交校验", action, {
        businessType: this.businessType,
        applytype: payload.applytype,
        infoid: payload.infoid,
        fundId: payload.id,
        明细: (this.rbDetails || []).map((d) => ({
          itemid: d.itemid,
          amount: d.amount,
          taxedamount: d.taxedamount,
          场景: d.servicesscopename,
        })),
      });
      return validateExpertFee(payload)
        .then((res) => {
          const errors = (res && res.errors) || [];
          const warnings = (res && res.warnings) || [];
          console.log("[专家费校验] 后端返回", { errors, warnings });
          // 1) 阻断型:列出违反的具体规则,不允许继续
          //    用弹窗而非轻提示:错误可能有多条,轻提示会挤成一行且很快消失
          if (errors.length > 0) {
            this.$modal.alertError(
              "专家劳务费用不符合以下规定,无法" +
                action +
                ":\n\n" +
                errors.map((msg, i) => i + 1 + ". " + msg).join("\n\n")
            );
            return false;
          }
          // 2) 提示型:需经办人确认后才能继续(当前规则 5、6 已改为硬拦截,此分支保留备用)
          if (warnings.length > 0) {
            const confirmText =
              "专家劳务费用存在以下问题,请确认是否继续" +
              action +
              "?\n\n" +
              warnings.map((msg, i) => i + 1 + ". " + msg).join("\n\n");
            return this.$modal
              .confirm(confirmText)
              .then(() => true)
              .catch(() => false);
          }
          return true;
        })
        .catch((err) => {
          // 接口异常时按“不阻断”处理,避免校验服务不可用导致完全无法报销;
          // 后端 addOrUpdateNew / checkFund 仍有同样的硬拦截兜底。
          console.error("专家劳务费规则校验接口调用失败", err);
          return true;
        });
    },
    checksubmit() {
      //检查是否收到纸质材料
@@ -1770,8 +1901,19 @@
        return;
      }
      // 专家劳务费专用:9 条费用控制规则校验(仅审核通过时拦截)
      const needExpertFeeCheck = this.checkObj.flowlevelone == 1;
      const expertFeePromise = needExpertFeeCheck
        ? this.checkExpertFeeRules("提交审核")
        : Promise.resolve(true);
      expertFeePromise.then((allowed) => {
        if (!allowed) {
          return;
        }
      this.Savenow();
      console.log("保存并审批");
      });
    },
    // 连续审批:获取当前业务类型下的待审批列表
@@ -1880,6 +2022,19 @@
      });
    },
    /** 人员类型(多值,逗号分隔)是否包含指定类型;org = 单位机构 */
    hasUserType(usertype, code) {
      if (!usertype) {
        return false;
      }
      return (
        String(usertype)
          .split(",")
          .map(t => t.trim())
          .indexOf(code) > -1
      );
    },
    /** 受益人/账户是否为"单位"(人员类型含 org) */
    //获取专家、单位机构的账户列表
    getAccountList() {
      listExternalperson().then(res => {
@@ -1888,7 +2043,7 @@
        for (let j = 0; j < res.rows.length; j++) {
          //单位账号列表
          if (res.rows[j].userstatus == "1") {
            if (res.rows[j].usertype == "org") {
            if (this.hasUserType(res.rows[j].usertype, "org")) {
              this.accountlist.push({
                id: res.rows[j].id,
                userno: res.rows[j].userno,
@@ -1899,6 +2054,7 @@
                bankcardno: res.rows[j].bankcardno,
                unitno: res.rows[j].unitno,
                unitname: res.rows[j].unitname,
                usertype: res.rows[j].usertype,
                userid:
                  res.rows[j].username + "(" + res.rows[j].bankcardno + ")"
              });
@@ -1913,6 +2069,7 @@
                bankcardno: res.rows[j].bankcardno,
                unitno: res.rows[j].unitno,
                unitname: res.rows[j].unitname,
                usertype: res.rows[j].usertype,
                userid:
                  res.rows[j].username + "(" + res.rows[j].idcardno + ")"
              });
@@ -2193,6 +2350,13 @@
      this.Savereminder = false;
      this.Reminderquantity = 0;
      this.totalquantity = 0;
      // 专家劳务费专用:9 条费用控制规则校验(校验不通过不允许落库)
      this.checkExpertFeeRules("保存").then((allowed) => {
        if (!allowed) {
          return;
        }
      this.$refs["form"].validate(valid => {
        if (valid) {
          this.formData = this.form;
@@ -2369,16 +2533,19 @@
          // this.getfundList();
        }
      });
      });
    },
    handleCheckAllChange(val) {
      this.checkedCities = val ? this.cityOptions : [];
      this.checkedCities = val ? this.filteredCityOptions.slice() : [];
      this.isIndeterminate = false;
    },
    handleCheckedCitiesChange(value) {
      let checkedCount = value.length;
      this.checkAll = checkedCount === this.cityOptions.length;
      this.checkAll =
        this.filteredCityOptions.length > 0 &&
        checkedCount === this.filteredCityOptions.length;
      this.isIndeterminate =
        checkedCount > 0 && checkedCount < this.cityOptions.length;
        checkedCount > 0 && checkedCount < this.filteredCityOptions.length;
    },
    // 手动计算
    Manualcalculationfn() {
@@ -2414,19 +2581,57 @@
    // 批量录入点击
    Batchentry() {
      // ethical
      this.personKeyword = "";
      this.checkedCities = [];
      this.checkAll = false;
      this.isIndeterminate = false;
      listReportname("ethical").then(res => {
        this.cityOptions = res.data;
      });
      this.batchdialogVisible = true;
    },
    /** 批量录入弹框:姓名/单位/工号/身份证后4位 命中判断 */
    personMatchKeyword(item, kw) {
      const idcard = String(item.idcrad || "");
      return (
        String(item.reportName || "")
          .toLowerCase()
          .indexOf(kw) > -1 ||
        String(item.unitname || "")
          .toLowerCase()
          .indexOf(kw) > -1 ||
        String(item.reportNo || "")
          .toLowerCase()
          .indexOf(kw) > -1 ||
        (idcard.length >= 4 && idcard.slice(-4).indexOf(kw) > -1)
      );
    },
    // 确认录入
    BatchentryFn() {
      // 明细中已有的人员姓名,用于重复提醒
      const existed = {};
      this.rbDetails.forEach(row => {
        if (row.beneficiaryname) {
          existed[String(row.beneficiaryname).trim()] = true;
        }
      });
      const duplicated = [];
      this.checkedCities.forEach(item => {
        const nameKey = String(item.reportName || "").trim();
        if (nameKey && existed[nameKey]) {
          duplicated.push(item.reportName);
          return;
        }
        if (nameKey) {
          existed[nameKey] = true;
        }
        let rowData = {
          orderno: null,
          fundid: null,
          beneficiaryname: item.reportName,
          beneficiaryno: item.reportNo,
          isunit: 0,
          unitname: item.unitname,
          unitno: null,
          uintuserno: null,
@@ -2471,11 +2676,17 @@
        this.rbDetails.push(rowData);
      });
      for (let i = 0; i < this.checkedCities.length + 1; i++) {
      for (let i = 0; i < this.rbDetails.length; i++) {
        this.rbDetails[i].orderno = i + 1;
      }
      this.calculateTax();
      this.batchdialogVisible = false;
      this.checkedCities = [];
      if (duplicated.length) {
        this.$modal.msgWarning(
          "以下人员在明细中已存在,已跳过重复添加:" + duplicated.join("、")
        );
      }
    },
    //新增一行
    addRow(rowIndex) {
@@ -2485,6 +2696,7 @@
        fundid: null,
        beneficiaryname: null,
        beneficiaryno: null,
        isunit: 0,
        unitname: null,
        itemdescribe: null,
        unitno: null,
@@ -3121,11 +3333,13 @@
                  this.rbDetails[tempIndex].phone = res.telephone;
                  if (res.usertype == "org") {
                    this.$set(this.rbDetails[tempIndex], "isunit", 1);
                    this.rbDetails[tempIndex].idcardno = "";
                    this.rbDetails[tempIndex].title = "";
                    this.rbDetails[tempIndex].unitno = "";
                    this.rbDetails[tempIndex].unitname = "";
                  } else {
                    this.$set(this.rbDetails[tempIndex], "isunit", 0);
                    this.rbDetails[tempIndex].idcardno = res.idcardno;
                    this.rbDetails[tempIndex].title = res.title;
                    this.rbDetails[tempIndex].unitno = parseInt(res.unitno);
@@ -3231,12 +3445,14 @@
                      this.rbDetails[tempIndex].bankcardno = res.bankcardno;
                      this.rbDetails[tempIndex].phone = res.telephone;
                      if (res.usertype == "org") {
                      if (this.hasUserType(res.usertype, "org")) {
                        this.$set(this.rbDetails[tempIndex], "isunit", 1);
                        this.rbDetails[tempIndex].idcardno = "";
                        this.rbDetails[tempIndex].title = "";
                        this.rbDetails[tempIndex].unitno = "";
                        this.rbDetails[tempIndex].unitname = "";
                      } else {
                        this.$set(this.rbDetails[tempIndex], "isunit", 0);
                        this.rbDetails[tempIndex].idcardno = res.idcardno;
                        this.rbDetails[tempIndex].title = res.title;
                        this.rbDetails[tempIndex].unitno = parseInt(res.unitno);
src/views/project/fund/expertFeeApply/index.vue
@@ -1112,7 +1112,8 @@
  getdownloadYX,
  getdownloadBX,
  addorupdateFund,
  getdetailsByItemId
  getdetailsByItemId,
  validateExpertFee
} from "@/api/project/fund";
import {
@@ -1825,21 +1826,37 @@
            // });
          }
          // 保存前先做专家劳务费 9 条费用控制校验(同一案例累计)
          validateExpertFee(this.form).then(valid => {
            const errors = (valid && valid.errors) || [];
            const warnings = (valid && valid.warnings) || [];
            // warnings(规则 5、6)仅提示,不阻断保存
            if (warnings.length > 0) {
              this.$modal.msgWarning(warnings.join(";"));
            }
            // errors 阻断保存,保留窗口让经办人先修正
            if (errors.length > 0) {
              this.$modal.msgError(errors.join(";"));
              this.loading = false;
              return;
            }
          addorupdateFund(this.form).then(response => {
            if (response.code === 200) {
              this.selectDonotor(this.curCase);
              this.$modal.msgSuccess("专家劳务费保存成功!");
            } else {
              this.$modal.msgError("专家劳务费保存失败:" + response.msg);
            }
          });
          //关闭窗口
          this.loading = false;
          this.dialogOpen = false;
          // this.getfundList();
          this.getList();
          this.selectDonotor(this.curCase);
                this.$modal.msgSuccess("专家劳务费保存成功!");
                this.getList();
              } else {
                this.$modal.msgError("专家劳务费保存失败:" + response.msg);
                this.loading = false;
              }
            });
          });
        }
      });
    },
src/views/project/fund/performancedetails/index.vue
@@ -115,7 +115,7 @@
                icon="el-icon-download"
                size="small"
                @click="Batchentry"
                >批量录入人员</el-button
                >批量导入绩效人员</el-button
              >
            </div>
          </el-col>
@@ -266,7 +266,7 @@
            <el-table-column
              prop="beneficiaryname"
              align="center"
              label="绩效人员或单位"
              label="绩效人员/单位"
              width="250"
            >
              <template slot-scope="scope">
@@ -283,13 +283,33 @@
              </template>
            </el-table-column>
            <el-table-column
              prop="isunit"
              align="center"
              label="人员类别"
              width="100"
            >
              <template slot-scope="scope">
                <el-tag
                  :type="scope.row.isunit == 1 ? 'warning' : 'success'"
                  size="mini"
                  >{{ scope.row.isunit == 1 ? "单位" : "个人" }}</el-tag
                >
              </template>
            </el-table-column>
            <el-table-column
              prop="idcardno"
              align="center"
              label="身份证号"
              width="200"
            >
              <template slot-scope="scope">
                <el-input v-model="scope.row.idcardno" placeholder="身份证号" />
                <el-input
                  v-model="scope.row.idcardno"
                  :disabled="scope.row.isunit == 1"
                  :placeholder="
                    scope.row.isunit == 1 ? '单位无需填写' : '身份证号'
                  "
                />
              </template>
            </el-table-column>
@@ -470,34 +490,43 @@
      :visible.sync="showSelectionDialog"
      :close-on-click-modal="false"
      :title="'绩效人员(含所在单位)账号选择'"
      width="500px"
      width="560px"
      style="text-align: center"
      v-loading="loading"
    >
      <el-form ref="funddetailForm" :model="funddetailForm">
        <el-row>
          <el-form-item label="请输入选择:" prop="unitno" label-width="120px">
          <el-form-item label="筛查人员:" prop="userKey" label-width="120px">
            <el-select
              v-model="funddetailForm.userName"
              v-model="funddetailForm.userKey"
              filterable
              remote
              reserve-keyword
              clearable
              allow-create
              default-first-option
              placeholder="请输入姓名或单位名称"
              :remote-method="remoteMethod"
              :loading="loadingperformance"
              :filter-method="filterPersonOption"
              placeholder="输入 姓名 / 单位 / 工号 / 身份证后4位 筛查,也可直接输入姓名"
              style="width: 100%"
            >
              <el-option
                v-for="item in optionsperformance"
                :key="item.value"
                :label="item.label"
                :value="item.value"
                :key="item.id"
                :label="personOptionLabel(item)"
                :value="item.id"
              >
                <span style="float: left">{{ item.username }}</span>
                <span style="float: right; color: #8492a6; font-size: 13px">
                  {{
                    item.isUnitPerson ? "单位" : "工号 " + (item.userno || "-")
                  }}{{ item.unitname ? " · " + item.unitname : "" }}
                </span>
              </el-option>
            </el-select>
          </el-form-item>
        </el-row>
        <el-row style="text-align: left; padding-left: 120px">
          <span style="color: #909399; font-size: 12px">
            候选为人员类型含「参与绩效」的人员;选中后身份证号、银行账号等自动带出,单位为单位的行不校验身份证号。
          </span>
        </el-row>
      </el-form>
      <span slot="footer" class="dialog-footer">
@@ -798,12 +827,25 @@
          <el-radio-button :label="70">年度绩效</el-radio-button>
        </el-radio-group>
      </div>
      <div style="margin-bottom: 12px; margin-left: 10px;">
        <el-input
          v-model="personKeyword"
          clearable
          size="small"
          prefix-icon="el-icon-search"
          placeholder="输入 姓名 / 单位 / 工号 / 身份证后4位 筛查"
          style="width: 330px"
        />
        <span style="margin-left: 10px; color: #909399; font-size: 12px">
          共 {{ filteredCityOptions.length }} 人
        </span>
      </div>
      <div style="max-height: 330px; overflow-y: auto">
        <el-checkbox
          :indeterminate="isIndeterminate"
          v-model="checkAll"
          @change="handleCheckAllChange"
          >全选</el-checkbox
          >全选(当前筛查结果)</el-checkbox
        >
        <div style="margin: 15px 0;"></div>
        <el-checkbox-group
@@ -812,10 +854,11 @@
        >
          <el-checkbox
            border
            v-for="city in cityOptions"
            v-for="city in filteredCityOptions"
            :label="city"
            :key="city.userNo"
            >{{ city.userName }}</el-checkbox
            :key="city.id"
            >{{ city.username }}{{ city.isUnitPerson ? "(单位)" : ""
            }}{{ city.unitname ? " · " + city.unitname : "" }}</el-checkbox
          >
        </el-checkbox-group>
      </div>
@@ -859,7 +902,6 @@
  listOrganization,
  getOrganization,
  listReportname,
  listperformance,
  listUser
} from "@/api/project/organization";
@@ -931,7 +973,10 @@
      feeUnitList: [],
      batchdialogVisible: false,
      itemlist: [],
      //绩效人员候选(人员类型含「参与绩效」的人员,来自 service_externalperson)
      cityOptions: [],
      //批量录入弹框的筛查关键字
      personKeyword: "",
      checkAll: false,
      checkedCities: [],
      Personnelforname: null,
@@ -1064,6 +1109,7 @@
      funddetailForm: {
        index: null,
        userKey: null,
        userName: null,
        beneficiaryname: null,
        beneficiaryno: null,
@@ -1199,7 +1245,16 @@
  },
  //监听属性 类似于data概念
  computed: {},
  computed: {
    //批量录入弹框:按关键字(姓名/单位/工号/身份证后4位)筛查后的候选人
    filteredCityOptions() {
      const kw = (this.personKeyword || "").trim().toLowerCase();
      if (!kw) {
        return this.cityOptions;
      }
      return this.cityOptions.filter(item => this.personMatchKeyword(item, kw));
    }
  },
  //监控data中的数据变化
  watch: {},
  //方法集合
@@ -1625,7 +1680,12 @@
              );
              return;
            }
            if (tempDetail.idcardno == null) {
            // 单位行(isunit=1)不需要身份证号
            if (
              tempDetail.isunit != 1 &&
              (tempDetail.idcardno == null ||
                String(tempDetail.idcardno).trim() === "")
            ) {
              this.$modal.msgWarning(
                "服务项目表中,第" +
                  (k + 1).toString() +
@@ -1653,6 +1713,22 @@
                return;
              }
            }
          }
          //明细重复校验:同一人员/单位不允许出现多行
          const detailNameMap = {};
          for (let k = 0; k < this.rbDetails.length; k++) {
            const nm = String(this.rbDetails[k].beneficiaryname || "").trim();
            if (!nm) {
              continue;
            }
            if (detailNameMap[nm]) {
              this.$modal.msgWarning(
                "服务项目表中,人员名称:" + nm + " 重复,请核对后再提交保存!"
              );
              return;
            }
            detailNameMap[nm] = true;
          }
          //处理明细
@@ -1866,7 +1942,7 @@
        this.personlist = res.rows;
        for (let j = 0; j < this.personlist.length; j++) {
          //获取费用报销机构列表
          if (this.personlist[j].usertype == "org") {
          if (this.hasUserType(this.personlist[j].usertype, "org")) {
            this.feeUnitList.push({
              id: this.personlist[j].id,
              reportNo: this.personlist[j].userno,
@@ -1877,7 +1953,7 @@
            });
          }
          //专家人员列表
          else if (res.rows[j].usertype == "expert") {
          else if (this.hasUserType(res.rows[j].usertype, "expert")) {
            this.expertlist.push({
              expertNo: this.personlist[j].id,
              exportName: this.personlist[j].username
@@ -2153,6 +2229,22 @@
            }
          }
          //明细重复校验:同一人员/单位不允许出现多行
          const detailNameMap = {};
          for (let k = 0; k < this.rbDetails.length; k++) {
            const nm = String(this.rbDetails[k].beneficiaryname || "").trim();
            if (!nm) {
              continue;
            }
            if (detailNameMap[nm]) {
              this.$modal.msgWarning(
                "服务项目表中,人员名称:" + nm + " 重复,请核对后再提交保存!"
              );
              return;
            }
            detailNameMap[nm] = true;
          }
          //处理明细
          for (let i = 0; i < this.rbDetails.length; i++) {
            let tempDetail = this.rbDetails[i];
@@ -2239,16 +2331,18 @@
        }
      });
    },
    // 全选
    // 全选(按当前筛查结果)
    handleCheckAllChange(val) {
      this.checkedCities = val ? this.cityOptions : [];
      this.checkedCities = val ? this.filteredCityOptions.slice() : [];
      this.isIndeterminate = false;
    },
    handleCheckedCitiesChange(value) {
      let checkedCount = value.length;
      this.checkAll = checkedCount === this.cityOptions.length;
      this.checkAll =
        this.filteredCityOptions.length > 0 &&
        checkedCount === this.filteredCityOptions.length;
      this.isIndeterminate =
        checkedCount > 0 && checkedCount < this.cityOptions.length;
        checkedCount > 0 && checkedCount < this.filteredCityOptions.length;
    },
    // 手动计算
    Manualcalculationfn() {
@@ -2273,18 +2367,107 @@
    },
    // 批量录入点击
    Batchentry() {
      // ethical
      this.listperformance();
      this.personKeyword = "";
      this.checkedCities = [];
      this.checkAll = false;
      this.isIndeterminate = false;
      this.batchdialogVisible = true;
    },
    /**
     * 载入绩效人员候选。
     * 改造前取自人员分组表(service_externalperson_group,group_id=1);
     * 2026-09-27 起改为「人员类型(UserType,可多选)含 参与绩效」的外围人员账户,
     * 不再依赖分组表。
     */
    listperformance() {
      listperformance(1).then(res => {
        this.cityOptions = res.data;
        this.listperformancea = [];
        this.listperformancea = this.cityOptions.map(item => {
          return { value: item.userName, label: item.userName };
      listExternalperson({
        usertype: "performance",
        pageNum: 1,
        pageSize: 10000
      }).then(res => {
        const rows = res.rows || [];
        this.cityOptions = rows
          //只保留启用账号(与改造前分组表接口的 userstatus=1 口径一致)。
          //注意:ServiceExternalperson 实体没有 del_flag 字段,接口不返回该字段,
          //不能用它做过滤,否则 undefined == 0 会把全部候选人滤空。
          .filter(
            item =>
              item.userstatus === undefined ||
              item.userstatus === null ||
              item.userstatus == 1
          )
          .map(item => {
            return Object.assign({}, item, {
              isUnitPerson: this.hasUserType(item.usertype, "org")
        });
      });
      });
    },
    /** 人员类型(多值,逗号分隔)是否包含指定类型 */
    hasUserType(usertype, code) {
      if (!usertype) {
        return false;
      }
      return (
        String(usertype)
          .split(",")
          .map(t => t.trim())
          .indexOf(code) > -1
      );
    },
    /** 关键字命中判断:姓名 / 单位 / 工号 / 身份证后4位 */
    personMatchKeyword(item, kw) {
      const idcard = String(item.idcardno || "");
      return (
        String(item.username || "")
          .toLowerCase()
          .indexOf(kw) > -1 ||
        String(item.unitname || "")
          .toLowerCase()
          .indexOf(kw) > -1 ||
        String(item.userno || "")
          .toLowerCase()
          .indexOf(kw) > -1 ||
        (idcard.length >= 4 && idcard.slice(-4).indexOf(kw) > -1)
      );
    },
    /** 识别人员类别:单位(人员类型含 org)还是个人 */
    isUnitPerson(item) {
      return !!(item && this.hasUserType(item.usertype, "org"));
    },
    /** 选择框下拉项文案:姓名(单位) */
    personOptionLabel(item) {
      return (
        (item.username || "") +
        (item.unitname ? "(" + item.unitname + ")" : "") +
        (item.isUnitPerson ? " - 单位" : "")
      );
    },
    /** 选择框候选:空关键字返回全部,否则按关键字筛查 */
    filterPersonCandidates(kw) {
      if (!kw) {
        return this.cityOptions.slice();
      }
      return this.cityOptions.filter(item => this.personMatchKeyword(item, kw));
    },
    /** 选择框本地筛查(姓名/单位/工号/身份证后4位) */
    filterPersonOption(query) {
      const kw = (query || "").trim().toLowerCase();
      this.optionsperformance = this.filterPersonCandidates(kw);
      // 已选中项要保留在下拉里,否则回显会空白
      const picked = this.getPickedPerson(this.funddetailForm.userKey);
      if (picked && this.optionsperformance.indexOf(picked) === -1) {
        this.optionsperformance = [picked].concat(this.optionsperformance);
      }
    },
    /** 按唯一键(人员 ID)取候选;手工输入的姓名返回 null */
    getPickedPerson(key) {
      if (key === null || key === undefined || key === "") {
        return null;
      }
      return (
        this.cityOptions.find(item => String(item.id) === String(key)) || null
      );
    },
    // 确认录入
    BatchentryFn() {
@@ -2292,23 +2475,42 @@
      if (!this.rbDetails[0].applytype) {
        this.rbDetails = [];
      }
      // 明细中已有的人员姓名,用于重复提醒(与伦理专家费页的判重口径一致)
      const existed = {};
      this.rbDetails.forEach(row => {
        if (row.beneficiaryname) {
          existed[String(row.beneficiaryname).trim()] = true;
        }
      });
      const duplicated = [];
      this.checkedCities.forEach(item => {
        const nameKey = String(item.username || "").trim();
        if (nameKey && existed[nameKey]) {
          duplicated.push(item.username);
          return;
        }
        if (nameKey) {
          existed[nameKey] = true;
        }
        const isunit = this.isUnitPerson(item);
        let rowData = {
          orderno: null,
          fundid: null,
          beneficiaryname: item.userName,
          beneficiaryno: item.usertNo,
          beneficiaryname: item.username,
          beneficiaryno: item.userno,
          isunit: isunit ? 1 : 0,
          unitname: item.unitname,
          unitno: null,
          unitno: item.unitno,
          uintuserno: null,
          title: item.title,
          idcardtype: null,
          idcardno: item.idcrad,
          idcardno: isunit ? "" : item.idcardno,
          sex: null,
          familyrelations: null,
          phone: null,
          depositbank: item.depositBank,
          bankcardno: item.bankCardNo,
          phone: item.telephone,
          depositbank: item.depositbank,
          bankcardno: item.bankcardno,
          branchbankname: item.branchbankname,
          annexbankcard: null,
          annexregistform: null,
@@ -2351,6 +2553,11 @@
      this.calculateTax();
      this.batchdialogVisible = false;
      this.checkedCities = [];
      if (duplicated.length) {
        this.$modal.msgWarning(
          "以下人员在明细中已存在,已跳过重复添加:" + duplicated.join("、")
        );
      }
      console.log(this.rowfeeItemTypes);
    },
    //新增一行
@@ -2361,6 +2568,7 @@
        fundid: null,
        beneficiaryname: null,
        beneficiaryno: null,
        isunit: 0,
        unitname: null,
        unitno: null,
        uintuserno: null,
@@ -2460,64 +2668,78 @@
        });
      //全部删除后添加一条新行
    },
    //专家/医疗机构/费用报销机构选择
    //绩效人员/单位选择:打开时预填当前行已选人员,避免"直接确定"把已有姓名清空
    ShowDetailDialog(spoce, showType) {
      this.spocewl = spoce;
      this.selectionType = showType;
      this.selectionType = this.experthear;
      this.expertfrom = "1";
      this.funddetailForm.index = spoce.$index;
      this.funddetailForm.beneficiaryno = spoce.row.beneficiaryno;
      this.funddetailForm.beneficiaryname = spoce.row.beneficiaryname;
      this.funddetailForm.unitno = spoce.row.unitno;
      this.funddetailForm.unitname = spoce.row.unitname;
      this.funddetailForm.expertname = "";
      this.funddetailForm.expertidcardno = "";
      this.funddetailForm.expertunitno = "";
      this.funddetailForm.expertunitname = "";
      this.funddetailForm.experttitle = "";
      this.funddetailForm.expertdepositbank = "";
      this.funddetailForm.expertbankcardno = "";
      //当前行已选人员的回填:先按人员编号匹配,匹配不到再按姓名匹配
      let cur =
        this.cityOptions.find(
          item => String(item.userno) === String(spoce.row.beneficiaryno)
        ) || null;
      if (!cur && spoce.row.beneficiaryname) {
        cur =
          this.cityOptions.find(
            item => item.username === spoce.row.beneficiaryname
          ) || null;
      }
      this.funddetailForm.userKey = cur
        ? cur.id
        : spoce.row.beneficiaryname || null;
      this.funddetailForm.userName = spoce.row.beneficiaryname || null;
      //打开时展示全量候选,便于直接浏览筛查
      this.optionsperformance = this.cityOptions.slice();
      this.showSelectionDialog = true;
    },
    //专家/医疗机构/费用报销机构选择
    /**
     * 确定选择:按唯一键(人员 ID)匹配候选,
     * 自动带出身份证号、银行账号、开户银行、电话、职称、人员编号、单位;
     * 单位(人员类型含 org)不写身份证号;手工输入的姓名不做带出。
     */
    ConfirmDetailDialog() {
      let tempIndex = this.funddetailForm.index;
      let tempExpertName = this.funddetailForm.userName;
      let singleDetail = this.rbDetails[tempIndex];
      const tempIndex = this.funddetailForm.index;
      const singleDetail = this.rbDetails[tempIndex];
      const key = this.funddetailForm.userKey;
      //从专家列表获取信息
      this.cityOptions.map(res => {
        if (tempExpertName == res.userName) {
          singleDetail.beneficiaryno = res.userno;
          singleDetail.bankcardno = res.bankCardNo;
          singleDetail.branchbankname = res.branchbankname;
          singleDetail.depositbank = res.depositBank;
          singleDetail.idcardno = res.idcardNo;
          singleDetail.phone = res.telephone;
          singleDetail.title = res.title;
          singleDetail.unitname = res.unitname;
          singleDetail.unitno = parseInt(res.unitno);
      if (key === null || key === undefined || key === "") {
        this.$modal.msgWarning("请先筛查并选择绩效人员,或直接输入人员姓名!");
        return;
        }
      });
      singleDetail.beneficiaryname = tempExpertName;
      this.rbDetails[tempIndex] = singleDetail;
      this.showSelectionDialog = false;
    },
    remoteMethod(query) {
      console.log(this.listperformancea);
      if (query !== "") {
        this.loadingperformance = true;
        setTimeout(() => {
          this.loadingperformance = false;
          this.optionsperformance = this.listperformancea.filter(item => {
            return item.label.toLowerCase().indexOf(query.toLowerCase()) > -1;
          });
        }, 200);
      const person = this.getPickedPerson(key);
      if (person) {
        // 命中人员库:自动带出全部信息
        const isunit = this.isUnitPerson(person);
        singleDetail.beneficiaryname = person.username;
        singleDetail.beneficiaryno = person.userno;
        singleDetail.isunit = isunit ? 1 : 0;
        singleDetail.idcardno = isunit ? "" : person.idcardno;
        singleDetail.bankcardno = person.bankcardno;
        singleDetail.branchbankname = person.branchbankname;
        singleDetail.depositbank = person.depositbank;
        singleDetail.phone = person.telephone;
        singleDetail.title = person.title;
        singleDetail.unitname = person.unitname;
        singleDetail.unitno = person.unitno;
      } else {
        this.optionsperformance = [];
        // 手工输入的人员:只写姓名,身份证号/银行账号由经办人填写
        singleDetail.beneficiaryname = String(key);
        singleDetail.isunit = 0;
        this.$modal.msgWarning(
          "人员库中未找到「" +
            key +
            "」,已按手工录入处理,请自行填写身份证号、银行账号等信息。"
        );
      }
      this.$set(this.rbDetails, tempIndex, singleDetail);
      this.showSelectionDialog = false;
    },
    handleUploadError() {},
    remove(file, fileList) {
src/views/project/fundcheck/expertFeeCheck/index.vue
@@ -421,7 +421,8 @@
  checkfund,
  listbypower,
  addFundSharedInfo,
  TotalcalculationFn
  TotalcalculationFn,
  validateExpertFee
} from "@/api/project/fund";
import {
  onelistFunds,
@@ -738,6 +739,27 @@
        this.$modal.msgError("请选择审核结论!");
        return;
      }
      // 审核“通过”前:先做专家劳务费 9 条费用控制校验(与后端拦截同口径)
      if (checkfundobj.flowconclusion == 1) {
        validateExpertFee({ id: this.form.id }).then(valid => {
          const errors = (valid && valid.errors) || [];
          const warnings = (valid && valid.warnings) || [];
          if (warnings.length > 0) {
            this.$modal.msgWarning(warnings.join(";"));
          }
          if (errors.length > 0) {
            this.$modal.msgError("不能审核通过:" + errors.join(";"));
            return;
          }
          this.doCheckfund(checkfundobj);
        });
        return;
      }
      // 不通过:无需校验限额
      this.doCheckfund(checkfundobj);
    },
    doCheckfund(checkfundobj) {
      checkfund(checkfundobj).then(res => {
        if (res.code == 200) {
          this.reset();
@@ -745,11 +767,8 @@
          this.getlistbypower();
          this.dialogOpen = false;
        } else {
          if (res.msg) {
            this.$modal.msgError("操作失误");
          } else {
            this.$modal.msgError(res.msg);
          }
          // 后端返回的具体原因(如超限额)优先展示
          this.$modal.msgError(res.msg || "操作失误");
        }
      });
    },
src/views/project/travelexpenseapply/travelexpensedetail/index.vue
@@ -2538,7 +2538,7 @@
            .map(item => ({ url: item, name: item }))
          : [];
        //审核状态
        this.form.recordstatus = response.data.recordstatus + "";
        this.form.recordstatus = this.toStatusText(response.data.recordstatus, "-1");
        let ids = response.data.id;
        let listFundflowparams = {
          fundid: ids,
@@ -2549,13 +2549,18 @@
        });
        //报销明细
        getReimbursementdetaillist(ids).then(res => {
          this.rbDetails = res.data;
        getReimbursementdetaillist(ids)
          .then(res => {
            this.rbDetails = this.normalizeDetailList(res);
          //合计费用
          // if (this.rbDetails.length > 0) this.sumTotalFee();
          for (let i = 0; i < this.rbDetails.length; i++) {
            this.sumRowFee(this.rbDetails[i]);
          }
          })
          .catch(err => {
            this.rbDetails = [];
            console.error("获取报销明细失败,id=" + ids, err);
        });
        //付款信息
@@ -2592,7 +2597,8 @@
      getReimbursement(id).then(response => {
        this.form = response.data;
        console.log(this.form.checkstatus, "checkstatus");
        this.form.checkstatus = this.form.checkstatus.toString();
        // checkstatus 在库中可为空(预审状态尚未产生时),直接 toString 会抛 TypeError 中断后续取数
        this.form.checkstatus = this.toStatusText(this.form.checkstatus, "-1");
        //票据
        this.fileListto = this.form.annexfiles
@@ -2619,15 +2625,21 @@
        });
        //报销明细
        getReimbursementdetaillist(ids).then(res => {
          this.rbDetails = res.data;
          console.log(this.rbDetails);
        getReimbursementdetaillist(ids)
          .then(res => {
            this.rbDetails = this.normalizeDetailList(res);
            console.log("报销明细行数:", this.rbDetails.length, this.rbDetails);
          //合计费用
          // if (this.rbDetails.length > 0) this.sumTotalFee();
          for (let i = 0; i < this.rbDetails.length; i++) {
            this.sumRowFee(this.rbDetails[i]);
          }
          })
          .catch(err => {
            // 取数失败时不再静默:置空并打出原因,避免“接口有数据但页面空白”无从排查
            this.rbDetails = [];
            console.error("获取报销明细失败,id=" + ids, err);
        });
        //付款信息
@@ -3251,6 +3263,45 @@
        });
    },
    /**
     * 状态类字段转字符串(接口可能返回 null)。
     *
     * 背景:checkstatus、recordstatus 等在库中可空,早期直接调用 .toString()
     * 会抛 TypeError 并中断其后的取数逻辑,表现为“接口已有数据但明细区空白”。
     *
     * @param {*} value 原始状态值
     * @param {string} fallback 为空时的兜底值
     * @returns {string}
     */
    toStatusText(value, fallback) {
      if (value === null || value === undefined || value === "") {
        return fallback;
      }
      return String(value);
    },
    /**
     * 统一从明细接口响应中取出数组。
     *
     * 正常为 AjaxResult 的 data 数组;若后端改为分页结构(total + rows),
     * res.data 将不是数组,直接绑定到 el-table 会导致“有数据但不显示”。
     *
     * @param {Object} res 接口原始响应
     * @returns {Array} 明细数组(异常时为空数组,不会让表格拿到非数组)
     */
    normalizeDetailList(res) {
      if (!res) {
        return [];
      }
      if (Array.isArray(res.data)) {
        return res.data;
      }
      if (Array.isArray(res.rows)) {
        return res.rows;
      }
      return [];
    },
    //计算一行合计
    //计算当前行报销内容费用合计(金额一律走精确运算)
    sumRowFee(row) {
vue.config.js
@@ -34,7 +34,7 @@
      // detail: https://cli.vuejs.org/config/#devserver-proxy
      [process.env.VUE_APP_BASE_API]: {
        target:`http://localhost:8080`,
        // target:`http://192.168.100.10:8081`,
        // target:`http://192.168.100.110:8080`,
        // target:`http://192.168.100.137:8080`,
        // target: `https://slb.hospitalstar.com:9093`,
        changeOrigin: true,