sinake
3 天以前 12ed29b3cd8471e6145cd847c6e33d6aa2f535ba
器官费用,发票管理修改
已修改3个文件
455 ■■■■■ 文件已修改
src/api/project/donorcharge.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/invoice/index.vue 387 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/unitCost/organfee/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/project/donorcharge.js
@@ -152,6 +152,15 @@
  })
}
// 器官费用汇总(应收/实收/未回款)
export function getOrganfeeSummary(query) {
  return request({
    url: '/project/donorchargeorgan/organfeeSummary',
    method: 'get',
    params: query
  })
}
//新增或修改捐献器官分配
export function saveDonorchargeorgan(data) {
src/views/project/invoice/index.vue
@@ -71,7 +71,87 @@
      ></right-toolbar>
    </el-row>
    <el-table v-loading="loading" border :data="invoiceList">
    <!-- 手机端:卡片列表 -->
    <div v-if="isMobile" class="invoice-cards" v-loading="loading">
      <div
        v-for="row in invoiceList"
        :key="row.id"
        class="invoice-card"
      >
        <div class="invoice-card__head">
          <span
            class="invoice-card__title"
            @click="handleUpdate(row)"
            >{{ row.invoiceName || "未命名发票" }}</span
          >
          <el-tag
            size="mini"
            :type="
              String(row.reimburseState) === '1' ? 'success' : 'info'
            "
          >
            {{ String(row.reimburseState) === "1" ? "已报销" : "未报销" }}
          </el-tag>
        </div>
        <div class="invoice-card__row">
          <span class="label">案例</span>
          <span class="value">{{ row.caseId || "-" }}</span>
        </div>
        <div class="invoice-card__row">
          <span class="label">金额</span>
          <span class="value">{{ amountText(row.invoiceAmount) }}</span>
        </div>
        <div class="invoice-card__row">
          <span class="label">日期</span>
          <span class="value">{{
            normalizeDate(row.occurDate) || row.occurDate || "-"
          }}</span>
        </div>
        <div class="invoice-card__row">
          <span class="label">号码</span>
          <span class="value">{{ row.invoiceNo || "-" }}</span>
        </div>
        <div class="invoice-card__row">
          <span class="label">报销ID</span>
          <span class="value">{{ row.reimburseId || "-" }}</span>
        </div>
        <div class="invoice-card__actions">
          <el-button
            type="text"
            icon="el-icon-view"
            @click="openPreview(row.invoiceUrl, row.invoiceName)"
            >预览</el-button
          >
          <el-button
            v-if="String(row.reimburseState) !== '1'"
            type="text"
            icon="el-icon-check"
            @click="handleReimburse(row)"
            v-hasPermi="['project:invoice:edit']"
            >已报销</el-button
          >
          <el-button
            type="text"
            icon="el-icon-edit"
            @click="handleUpdate(row)"
            v-hasPermi="['project:invoice:edit']"
            >修改</el-button
          >
          <el-button
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(row)"
            v-hasPermi="['project:invoice:remove']"
            >删除</el-button
          >
        </div>
      </div>
      <div v-if="!invoiceList.length && !loading" class="invoice-empty">
        暂无数据
      </div>
    </div>
    <el-table v-else v-loading="loading" border :data="invoiceList">
      <el-table-column type="index" label="序号" width="55" align="center" />
      <el-table-column label="案例" align="center" prop="caseId"  width="100">
        
@@ -121,16 +201,23 @@
        </template>
      </el-table-column>
      <el-table-column
        prop="reimburseId"
        label="报销ID"
        width="150"
        align="center"
        show-overflow-tooltip
      />
      <el-table-column
        label="操作"
        width="200"
        align="center"
        fixed="right"
        :fixed="isMobile ? false : 'right'"
      >
        <template slot-scope="scope">
          <el-button
            type="text"
            icon="el-icon-view"
            @click="openPreview(scope.row.invoiceUrl)"
            @click="openPreview(scope.row.invoiceUrl, scope.row.invoiceName)"
            >文件预览</el-button
          >
          <el-button
@@ -173,7 +260,9 @@
      :visible.sync="open"
      width="760px"
      append-to-body
      :fullscreen="isMobile"
      :close-on-click-modal="false"
      @closed="revokeThumbObjectUrl"
    >
      <!-- 第一步:选案例、用户,上传发票 -->
      <div v-if="mode === 'upload'">
@@ -368,6 +457,15 @@
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="报销ID" prop="reimburseId">
              <el-input
                v-model="form.reimburseId"
                maxlength="20"
                placeholder="请输入报销ID"
              />
            </el-form-item>
          </el-col>
          <el-col :span="24">
            <el-form-item label="备注" prop="remark">
              <el-input
@@ -386,17 +484,18 @@
                type="text"
                icon="el-icon-view"
                :disabled="!form.invoiceUrl"
                @click="openPreview(form.invoiceUrl)"
                @click="openPreview(form.invoiceUrl, form.invoiceName)"
                >文件预览</el-button
              >
              <el-image
              <img
                v-if="
                  form.invoiceUrl && getFileType(form.invoiceUrl) === 'image'
                "
                :src="form.invoiceUrl"
                :preview-src-list="[form.invoiceUrl]"
                fit="contain"
                :src="thumbObjectUrl || form.invoiceUrl"
                class="invoice-thumb"
                title="点击预览"
                @click="openPreview(form.invoiceUrl, form.invoiceName)"
                style="width: 400px;height: 240px;"
              />
            </el-form-item>
          </el-col>
@@ -424,27 +523,18 @@
    <el-dialog
      title="发票文件预览"
      :visible.sync="previewOpen"
      width="900px"
      width="1000px"
      append-to-body
      :fullscreen="isMobile"
      :destroy-on-close="true"
      @closed="handlePreviewClosed"
    >
      <div class="invoice-preview">
        <img
          v-if="previewType === 'image'"
          :src="previewUrl"
          class="preview-image"
      <div class="invoice-preview-wrap" v-loading="previewLoading">
        <attachment-preview-pane
          v-if="invoiceEmbeddedFile"
          :file="invoiceEmbeddedFile"
          style="flex: 1; min-width: 0;"
        />
        <iframe
          v-else-if="previewType === 'pdf'"
          :src="previewUrl"
          class="preview-frame"
        ></iframe>
        <div v-else class="preview-tip">
          该文件类型不支持在线预览,请
          <el-link type="primary" :href="previewUrl" target="_blank"
            >在新窗口打开</el-link
          >
        </div>
      </div>
    </el-dialog>
  </div>
@@ -503,11 +593,14 @@
      rules: {
       
      },
      // 文件预览
      // 文件预览(AttachmentPreviewPane 所需的 file 对象)
      previewOpen: false,
      previewUrl: "",
      // image / pdf / other
      previewType: "",
      previewLoading: false,
      invoiceEmbeddedFile: null,
      // 预览用的 blob 地址,关闭时释放
      previewObjectUrl: "",
      // 编辑弹窗缩略图的 blob 地址
      thumbObjectUrl: "",
      // 查询参数
      queryParams: {
        pageNum: 1,
@@ -521,6 +614,10 @@
    };
  },
  computed: {
    // 是否手机端(弹窗全屏、操作列不固定等适配)
    isMobile() {
      return this.$store.state.app.device === "mobile";
    },
    // 上传时携带的用户ID(当前登录用户)、案例ID(后端 uploadOCR 必传)
    uploadData() {
      return {
@@ -670,7 +767,7 @@
      this.afterUploadBatch();
    },
    /** 打开发票信息确认弹窗 */
    openEditForm(record) {
    async openEditForm(record) {
      this.form = {
        id: record.id,
        caseId: record.caseId,
@@ -685,11 +782,33 @@
        invoiceEndDate: this.normalizeDate(record.invoiceEndDate),
        sellerName: record.sellerName,
        reimburseState: record.reimburseState || "0",
        reimburseId: record.reimburseId,
        remark: record.remark
      };
      this.mode = "edit";
      this.title = "发票信息确认";
      this.open = true;
      this.refreshThumb();
    },
    /** 缩略图同样带 token 拉取,避免直接引用鉴权地址显示不出来 */
    async refreshThumb() {
      this.revokeThumbObjectUrl();
      const url = this.form.invoiceUrl;
      if (!url || this.getFileType(url) !== "image") {
        return;
      }
      const objectUrl = await this.loadPreviewBlob(url);
      if (objectUrl && this.form.invoiceUrl === url) {
        this.thumbObjectUrl = objectUrl;
      } else if (objectUrl) {
        URL.revokeObjectURL(objectUrl);
      }
    },
    revokeThumbObjectUrl() {
      if (this.thumbObjectUrl) {
        URL.revokeObjectURL(this.thumbObjectUrl);
        this.thumbObjectUrl = "";
      }
    },
    /** 修改按钮操作 */
    handleUpdate(row) {
@@ -697,15 +816,86 @@
        this.openEditForm(response.data);
      });
    },
    /** 文件预览:图片、PDF 弹窗内预览,其它类型提示新窗口打开 */
    openPreview(url) {
    /** 文件预览:带 token 拉取成 blob 再预览,兼容需要鉴权的 /profile 资源 */
    async openPreview(url, name) {
      if (!url) {
        this.$modal.msgWarning("该记录暂无发票文件");
        return;
      }
      this.previewUrl = url;
      this.previewType = this.getFileType(url);
      const type = this.getFileType(url);
      const fileName = name || this.getFileName(url);
      this.revokePreviewObjectUrl();
      this.invoiceEmbeddedFile = null;
      this.previewLoading = true;
      this.previewOpen = true;
      const objectUrl = await this.loadPreviewBlob(url);
      this.previewLoading = false;
      if (objectUrl) {
        this.previewObjectUrl = objectUrl;
        this.invoiceEmbeddedFile = { name: fileName, url: objectUrl, type };
      } else {
        // 拉取失败时退回原始地址,交给浏览器直接加载
        this.invoiceEmbeddedFile = { name: fileName, url: url, type };
      }
    },
    /** 依次尝试原始地址、接口前缀地址,带 token 拉取成 blob */
    async loadPreviewBlob(url) {
      const headers = { Authorization: "Bearer " + getToken() };
      const candidates = this.buildPreviewCandidates(url);
      for (let i = 0; i < candidates.length; i++) {
        try {
          const response = await fetch(candidates[i], { headers });
          if (!response.ok) {
            continue;
          }
          const blob = await response.blob();
          if (blob && blob.size > 0) {
            return URL.createObjectURL(blob);
          }
        } catch (e) {
          // 跨域/网络异常,继续尝试下一个地址
        }
      }
      return "";
    },
    /** 预览地址候选:原始地址 + 走接口前缀(/dev-api、/prod-api)的地址 */
    buildPreviewCandidates(url) {
      const list = [url];
      if (/^https?:\/\//i.test(url)) {
        const base = (process.env.VUE_APP_BASE_API || "").replace(/\/$/, "");
        let path = url.replace(/^https?:\/\/[^/]+/i, "");
        if (base && (path === base || path.indexOf(base + "/") === 0)) {
          path = path.slice(base.length);
        }
        if (base) {
          list.push(base + path);
        }
      }
      return list.filter((item, index) => list.indexOf(item) === index);
    },
    /** 关闭预览时释放 blob 地址 */
    handlePreviewClosed() {
      this.revokePreviewObjectUrl();
      this.invoiceEmbeddedFile = null;
    },
    revokePreviewObjectUrl() {
      if (this.previewObjectUrl) {
        URL.revokeObjectURL(this.previewObjectUrl);
        this.previewObjectUrl = "";
      }
    },
    /** 金额显示:保留两位小数 */
    amountText(value) {
      if (value === null || value === undefined || value === "") {
        return "0.00";
      }
      const num = Number(value);
      return Number.isFinite(num) ? num.toFixed(2) : value;
    },
    /** 从地址中取文件名(无发票名称时兜底) */
    getFileName(url) {
      const path = String(url || "").split("?")[0];
      return path.substring(path.lastIndexOf("/") + 1) || "发票文件";
    },
    /** 根据文件地址判断预览类型 */
    getFileType(url) {
@@ -783,28 +973,129 @@
  margin-left: 12px;
  border: 1px solid #ebeef5;
  vertical-align: middle;
  object-fit: contain;
  background: #f5f7fa;
  cursor: pointer;
}
.invoice-preview {
  min-height: 120px;
.invoice-preview-wrap {
  display: flex;
  width: 100%;
  height: 70vh;
}
  .preview-image {
    display: block;
    max-width: 100%;
    max-height: 70vh;
    margin: 0 auto;
/* 手机端卡片列表 */
.invoice-cards {
  min-height: 60px;
}
.invoice-card {
  padding: 12px;
  margin-bottom: 10px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
  &__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }
  .preview-frame {
  &__title {
    flex: 1;
    min-width: 0;
    margin-right: 8px;
    font-size: 15px;
    font-weight: 600;
    color: #303133;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  &__row {
    display: flex;
    align-items: center;
    margin-bottom: 6px;
    font-size: 13px;
    color: #606266;
    .label {
      flex: 0 0 40px;
      color: #909399;
    }
    .value {
      flex: 1;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
  &__actions {
    display: flex;
    justify-content: flex-end;
    margin-top: 8px;
    padding-top: 6px;
    border-top: 1px dashed #ebeef5;
    ::v-deep .el-button + .el-button {
      margin-left: 16px;
    }
  }
}
.invoice-empty {
  padding: 40px 0;
  text-align: center;
  font-size: 14px;
  color: #909399;
}
/* 手机端适配 */
@media (max-width: 992px) {
  .app-container {
    padding: 8px;
  }
  .opo-query-form {
    ::v-deep .el-form-item {
      display: flex;
      width: 100%;
      margin-right: 0;
      margin-bottom: 10px;
    }
    ::v-deep .el-form-item__content {
      flex: 1;
      min-width: 0;
    }
    ::v-deep .el-select,
    ::v-deep .el-input,
    ::v-deep .el-date-editor {
      width: 100% !important;
    }
  }
  /* 弹窗内两列表单在手机端变单列 */
  .el-col-12 {
    width: 100%;
    height: 70vh;
    border: none;
  }
  .preview-tip {
    padding: 40px 0;
    text-align: center;
    color: #909399;
  /* 全屏弹窗里的预览区占满剩余高度 */
  .invoice-preview-wrap {
    height: calc(100vh - 120px);
  }
  .invoice-thumb {
    display: block;
    margin: 8px 0 0;
  }
}
</style>
src/views/project/unitCost/organfee/index.vue
@@ -63,6 +63,16 @@
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="器官名称" prop="organname">
        <el-input
          v-model="queryParams.organname"
          placeholder="请输入器官名称"
          clearable
          size="small"
          style="width: 160px"
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="利用状态" prop="organState">
        <el-select
          v-model="queryParams.organState"
@@ -125,6 +135,8 @@
      v-loading="loading"
      border
      :data="organfeeList"
      show-summary
      :summary-method="getSummaries"
      highlight-current-row
      @current-change="handleCurrentChange"
    >
@@ -214,7 +226,7 @@
      />
      <el-table-column
        label="操作"
        width="210"
        width="230"
        align="center"
        fixed="right"
      >
@@ -539,7 +551,8 @@
  addDonorchargeorgan,
  updateDonorchargeorgan,
  delDonorchargeorgan,
  exportOrganfee
  exportOrganfee,
  getOrganfeeSummary
} from "@/api/project/donorcharge";
import { listOrganization } from "@/api/project/organization";
@@ -574,6 +587,12 @@
      showSearch: true,
      // 总条数
      total: 0,
      // 合计(应收/实收/未回款)
      summary: {
        organchargeTotal: 0,
        amountTotal: 0,
        unreceivedTotal: 0
      },
      // 器官费用列表
      organfeeList: [],
      // 表格当前选中行
@@ -632,6 +651,7 @@
        hospitalno: null,
        hospitalnoIsNull: false,
        donorname: null,
        organname: null,
        organState: null,
        payStatus: null,
        paymentid: null,
@@ -659,6 +679,40 @@
        .finally(() => {
          this.loading = false;
        });
      this.getSummary();
    },
    /** 查询合计(应收/实收/未回款),按当前查询条件统计全部结果 */
    getSummary() {
      getOrganfeeSummary(this.queryParams)
        .then(response => {
          const data = response.data || {};
          this.summary = {
            organchargeTotal: Number(data.organchargeTotal || 0),
            amountTotal: Number(data.amountTotal || 0),
            unreceivedTotal: Number(data.unreceivedTotal || 0)
          };
        })
        .catch(() => {});
    },
    /** 表格底部合计行 */
    getSummaries({ columns }) {
      const sums = [];
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = "合计";
          return;
        }
        if (column.property === "organcharge") {
          sums[index] = this.summary.organchargeTotal.toFixed(2);
        } else if (column.property === "amount") {
          sums[index] = this.summary.amountTotal.toFixed(2);
        } else if (column.label === "未回款金额") {
          sums[index] = this.summary.unreceivedTotal.toFixed(2);
        } else {
          sums[index] = "";
        }
      });
      return sums;
    },
    /** 导出按钮操作 */
    handleExport() {
@@ -884,6 +938,7 @@
      this.queryParams.hospitalnoIsNull = false;
      this.queryParams.hospitalno = null;
      this.queryParams.donorname = null;
      this.queryParams.organname = null;
      this.queryParams.organState = null;
      this.queryParams.payStatus = null;
      this.queryParams.starttime = null;