yxh
2026-10-03 d553a4be94d4f6ee162cc353a28b220403909cfc
src/views/ecg/doctor/components/RoutinePanel.vue
@@ -1,157 +1,95 @@
<!-- 常规检查 医生界面-->
<template>
  <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
  <el-form class="patient-compact-form" ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
    <!-- ===== 编辑区(需要录入的字段)===== -->
    <el-row :gutter="20">
      <el-col :span="6">
        <el-form-item label="患者编号" prop="patId">
          <el-input v-model="formData.patId" placeholder="请输入患者编号" @input="getRentInfoByPatId" />
          <!--
          <Qrcode v-if="formData.patId !== undefined && formData.patId !== ''" :text="formData.patId" logo="/logo.gif" width=100 />
          <Barcode v-if="displayBarCode" :value="displayBarCode" :width="100" />
          <el-button @click="copyBarcode()">复制</el-button>
          -->
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="患者名称" prop="patName">
          <el-input v-model="formData.patName" placeholder="请输入患者名称" />
        </el-form-item>
      </el-col><el-col :span="6">
        <el-form-item label="性别">
          {{ genderText }}
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="联系电话" prop="episodeId">
          <!-- 修正:原读 `patDetails.mobile`(queue 表的 JSON 快照,只含 source/mobile/wardDesc/bedNo 四字段)。
               而 setPatient 已把后端从 appointment 回填的**平铺字段** `patMobile` 赋进 formData,
               故应以平铺字段优先,patDetails 仅作旧数据回退。 -->
          {{ formData.patMobile || formData?.patDetails?.mobile || '-' }}
        </el-form-item>
      </el-col>
    </el-row>
    <el-row>
      <el-col :span="6">
        <el-form-item label="就诊编号" prop="episodeId">
          <el-input v-model="formData.episodeId" placeholder="请输入患者就诊编号" />
        </el-form-item>
      </el-col><el-col :span="6">
        <el-form-item label="出生日期">
          {{ fmtDate(formData.patBirthday) }}
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="年龄">
          {{ ageText }}
        </el-form-item>
      </el-col><el-col :span="6">
        <el-form-item label="开单医师">
          {{ formData.doctor }}
        </el-form-item>
      </el-col>
    </el-row>
    <el-row><el-col :span="6">
        <el-form-item label="申请单号" prop="applyNo">
          <el-input v-model="formData.applyNo" placeholder="请输入申请单号" />
        </el-form-item>
      </el-col>
      <el-col :span="18">
        <el-form-item label="科室病区" prop="patDetails">
          <!-- 修正:来源与病区/床号原读 `patDetails.*`(queue 表的 JSON 快照,可能滞后且常缺病区),
               改为**平铺字段优先**(后端已从 appointment 回填 patSrc/patWardDesc/patBedNo),
               patDetails 仅作旧数据回退。 -->
          <dict-tag v-if="formData.patSrc ?? formData.patDetails?.source"
                    :type="DICT_TYPE.ECG_PAT_SOURCE"
                    :value="formData.patSrc ?? formData.patDetails?.source" />
          {{ formData.patWardDesc || formData?.patDetails?.wardDesc || '-' }}
          - {{ formData.patBedNo || formData?.patDetails?.bedNo || '-' }}
        </el-form-item></el-col>
       <el-col :span="6">
        <el-form-item label="检查项目" prop="">
          {{ formData?.checkType && getCheckTypeName(formData?.checkType) }}
        </el-form-item>
      </el-col>
        <el-col :span="6">
        <el-form-item label="患者编号" prop="patId">
          <el-input v-model="formData.patId" placeholder="请输入患者编号" @input="getRentInfoByPatId" />
          <Barcode v-if="displayBarCode" :value="displayBarCode" :width="100" />
          <el-button @click="copyBarcode()">复制</el-button>
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="患者姓名" prop="patName">
          <el-input v-model="formData.episodeId" placeholder="请输入患者姓名" />
        </el-form-item>
      </el-col>
    </el-row>
    <el-row>
      <el-col :span="6">
        <el-form-item label="开单日期">
          {{ fmtDate(formData.doctorTime) }}
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="距今天数">
          {{ daysFromOrder }}
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="医嘱号">
          <!-- 修正:此处原先绑定的是 `formData.applyNo`(申请单号),
               与「医嘱号」是**两个不同的标识**:
                 申请单号 apply_no           = HIS root 2.16.156.10011.1.24,例 2126091700011
                 医嘱号   doctor_advice_no   = HIS root 2.16.156.10011.1.28,例 112663505
               原写法是把申请单号冒充成医嘱号,属于错误展示。
               现改为读后端新采集的 doctorAdviceNo(该节点规范中非必须项,历史数据为空 → 显示 '-')。 -->
          {{ formData.doctorAdviceNo || '-' }}
        </el-form-item>
      </el-col><el-col :span="6">
        <el-form-item label="登记时间">
          {{ fmtDate(formData.registerDate) }}
        </el-form-item>
      </el-col>
    </el-row>
    <!-- 需求1:补齐本面板原先缺失的两项(排队序号、状态),使 18 项齐全 -->
    <el-row>
      <el-col :span="6">
        <el-form-item label="排队序号">
          {{ formData?.seqNum ?? formData?.bookSeqNum ?? '-' }}
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="状态">
          <dict-tag v-if="formData?.status !== undefined && formData?.status !== null"
                    :type="DICT_TYPE.ECG_QUEUE_STATUS" :value="formData.status" />
          <span v-else>-</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row>
    <!-- ===== 患者信息(需求1:18 项统一展示,**一行四列**)=====
         原先本面板把 18 项手写在多行 el-col(span=6/18 混排),
         现统一改用 PatientInfoBlock 组件,与预约签到/设备领用/装机/拆机各页**口径一致**。 -->
    <PatientInfoBlock :data="formData" check-type-field="checkType" :span="6" />
    <el-row>
      <el-col :span="6">
        <el-form-item label="检查时间" prop="rentTime">
          <el-date-picker v-model="formData.rentTime" type="date" value-format="x" placeholder="选择时间" />
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="检查项目" prop="">
          {{ formData?.checkType && getCheckTypeName(formData?.checkType) }}
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="多耐药" prop="tolerance">
          <!-- 需求6:统一走字典 ecg_tolerance(0=无 1=多耐 2=多重耐药);
               NULL 按"无"显示(Q12)。原先硬编码"否/是/未知"三分支,与字典重复且文案不符。 -->
          <dict-tag :type="DICT_TYPE.ECG_TOLERANCE" :value="formData.tolerance ?? 0" />
        </el-form-item>
      </el-col>
      <el-col :span="6">
      </el-col>
      <el-col :span="18">
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formData.remark" placeholder="请输入备注" />
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
  <el-button @click="submitForm" type="primary" :disabled="formLoading">检查完成</el-button>
  <el-button @click="cancelInstall" :disabled="formLoading">放弃检查</el-button>
  <el-button @click="resetForm()">重 置</el-button>
  <el-button @click="yddownload()" :loading="yddownloadLoading" :disabled="yddownloadLoading">平板运动心电图知情同意书</el-button>
  <el-button @click="sgdownload()" :loading="sgdownloadLoading" :disabled="sgdownloadLoading">食管心脏电生理诊疗知情同意书</el-button>
  <el-button v-if="needAutoFeeConfirm(formData.checkType)" @click="feeConfirm" :disabled="formLoading">确费</el-button>
  <el-button v-if="needAutoFeeConfirm(formData.checkType)" @click="feeCancel" :disabled="formLoading">取消确费</el-button>
  <div class="patient-actions">
  <el-button size="small" @click="submitForm" type="primary" :disabled="formLoading">检查完成</el-button>
  <el-button size="small" @click="cancelInstall" :disabled="formLoading">放弃检查</el-button>
  <el-button size="small" @click="resetForm()">重 置</el-button>
  <el-button size="small" @click="yddownload()" :loading="yddownloadLoading" :disabled="yddownloadLoading">平板运动心电图知情同意书</el-button>
  <el-button size="small" @click="sgdownload()" :loading="sgdownloadLoading" :disabled="sgdownloadLoading">食管心脏电生理诊疗知情同意书</el-button>
  <el-button size="small" v-if="needAutoFeeConfirm(formData.checkType)" @click="feeConfirm" :disabled="formLoading">确费</el-button>
  <el-button size="small" v-if="needAutoFeeConfirm(formData.checkType)" @click="feeCancel" :disabled="formLoading">取消确费</el-button>
  </div>
  <!-- 医生装机列表 -->
  <ContentWrap>
    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" @row-click="onRowClick">
      <!--      <el-table-column label="id" align="center" prop="id" />-->
  <!--
    已完成记录列表。
    滚动方式:**中间内容区自身滚动**(.doctor-page__content 为 overflow-y:auto),
    患者信息用 sticky 固定在顶部;底部 footer 由 DoctorPageLayout 固定,无需滚动即可见。
    (曾尝试让 el-table 内部滚动以固定表头,但本项目 Element Plus 2.7.0 下
       height/maxHeight 均未生效,已回退,详见《部署清单》5.3.15。)
  -->
  <div class="records-host">
    <ContentWrap class="records-wrap">
      <el-table
        v-loading="loading"
        :data="list"
        :stripe="true"
        :show-overflow-tooltip="true"
        @row-click="onRowClick"
      >
      <!--
        需求11(修正):把「撤销」放在**已完成检查记录列表的第一列**里。
        原先放在表格上方的独立按钮行,位置不对 —— 现改为该列的**表头插槽**,
        使其成为表格的第一列(且只出现一次,作用于最近一位已完成患者,
        语义与后端 lastFinishedPatient 一致)。
      -->
      <el-table-column label="撤销" align="center" width="90px" fixed="left">
        <template #header>
          <el-button
            link
            type="warning"
            :disabled="undoDisabled"
            :loading="undoLoading"
            @click="$emit('event_undo_finish')"
          >
            撤销
          </el-button>
        </template>
      </el-table-column>
      <el-table-column label="检查状态" align="center" prop="lost">
        <template #default="scope">
          <span v-if="scope.row.jobType === 60">已检查</span>
@@ -190,11 +128,16 @@
        </template>
      </el-table-column>
-->
    </el-table>
      </el-table>
    </ContentWrap>
    <!-- 分页 -->
    <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
      @pagination="getList" />
  </ContentWrap>
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
  </div>
</template>
<script setup lang="ts">
@@ -212,6 +155,15 @@
import { Barcode } from '@/components/Barcode'
import { useCheckTypeStore } from '@/store/modules/checkType'
import { getCheckTypeName, needAutoFeeConfirm } from '../../../../utils/checkTypeFormatter'
import { useRoomStore } from '@/store/modules/room'
// ⚠️ 必须显式 import:`unplugin-vue-components` 的 globs 只覆盖
//    `src/components/**`(见 build/vite/index.ts 的 Components({ globs: [...] })),
//    因此 src/components/ 下的 Dialog、ContentWrap、Pagination 等可自动引入,
//    但 **src/views/** 下的本组件必须手写 import**。
//    若漏写,Vue 会把它当成「未知标签」**静默不渲染**(只有控制台 warning),
//    而 vite build 与 ESLint 都不会报错 —— 实测因改造时误删本行 import,
//    导致「叫号检查」页面患者信息**整块消失**。
import PatientInfoBlock from '@/views/ecg/doctor/components/PatientInfoBlock.vue'
/** 设备装机 表单 */
defineComponent({ name: 'RoutinePanel' })
@@ -225,10 +177,24 @@
    type: [String, Number],
    required: false,
    default: null
  },
  /**
   * 需求11:记录列表第一列的「撤销」按钮状态,由父页面控制
   * (撤销动作请求后端、需要 loading 与禁用态,逻辑保留在父页面)。
   */
  undoDisabled: {
    type: Boolean,
    required: false,
    default: false
  },
  undoLoading: {
    type: Boolean,
    required: false,
    default: false
  }
})
const emit = defineEmits(['event_routine']) // 定义 success 事件,用于操作成功后的回调
const emit = defineEmits(['event_routine', 'event_undo_finish'])
const checkTypeStore = useCheckTypeStore()
@@ -486,6 +452,10 @@
const loading = ref(true) // 列表的加载中
const list = ref<JobRecordVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
// 当前诊室信息(用于取诊室开展的检查项目做兜底,见 syncRoomCheckTypes)
const roomStore = useRoomStore()
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
@@ -498,10 +468,37 @@
  jobTime: [curDayStart(), curDayEnd()],
  summary: undefined,
  remark: undefined,
  createTime: []
  createTime: [],
  // ========================================================================
  // 按**当前诊室开展的检查项目**隔离已完成记录。
  //
  // 背景:`job_record` 表只有 doc_id + check_type,**没有 room_id**;
  // 而 /ecg/job-record/doctor-page 是强制按登录医生查"自己的记录"。
  // 于是同一医生在多个诊室坐诊时(如 1号门诊 ct=[100,1100]、6号门诊 ct=[400] 平板运动心电),
  // 叫号检查页会把**多个诊室的记录混在一起**显示 —— 正是用户反馈的现象。
  //
  // 由各页面(Routine.vue / index.vue / DevInstall.vue)在 onMounted 时
  // 用当前诊室的 checkTypes 赋值,并在每次查询前同步,确保切诊室后立即生效。
  // ========================================================================
  checkTypes: undefined as number[] | undefined
})
/**
 * 把「当前诊室开展的检查项目」同步到查询条件,实现已完成记录按诊室隔离。
 * 见 queryParams.checkTypes 的说明。
 */
const syncRoomCheckTypes = () => {
  // 优先用页面传入的诊室信息;为空时回退到 store(更可靠,见 TitlePanel 同处说明)
  const fromProp = props.room?.checkTypes
  const types = (Array.isArray(fromProp) && fromProp.length > 0)
    ? fromProp
    : roomStore.room?.checkTypes
  queryParams.checkTypes = (Array.isArray(types) && types.length > 0) ? [...types] : undefined
}
/** 查询列表 */
const getList = async () => {
  // 查询前同步当前诊室开展的检查项目,保证只显示本诊室的记录
  syncRoomCheckTypes()
  loading.value = true
  try {
    const data = await JobRecordApi.getJobRecordDoctorPage(queryParams)
@@ -516,8 +513,36 @@
  if (cur !== null) getSelectedRent(cur.rentId)
}
/**
 * 点击「已完成检查记录」的某一行:只把**该条记录**的信息合入表单。
 *
 * ⚠️ 原实现是 `formData.value = await DevRentApi.getDevRent(rentId)` —— **整个替换**。
 * 而实测 `GET /ecg/dev-rent/get` 的响应里,除 `patName` 外的患者字段**全是 null**:
 *   {"patName":"…","patGender":null,"patBirthday":null,"registerDate":null,"doctor":null,
 *    "patMobile":null,"patPhone":null,"patWardDesc":null,"patBedNo":null,"outpatientNo":null,
 *    "patIdentityId":null,"doctorAdviceNo":null,"tolerance":null}
 * 原因:`dev_rent` 表**不存**这些字段,它们是由 `DevRentServiceImpl` 从 appointment
 * 回填的;而 `/dev-rent/get` 这条路径**没有做回填**。
 *
 * 后果:医生一点记录行,PatientInfoBlock 里除姓名外的信息**全部变成 '-'**,
 * 表现为"患者信息缺失"。
 *
 * 修复:**保留当前患者信息**,只用返回的记录字段覆盖(仅当返回值为非空时)。
 */
const getSelectedRent = async (rentId) => {
  formData.value = await DevRentApi.getDevRent(rentId)
  const record = await DevRentApi.getDevRent(rentId)
  if (!record) return
  const merged = { ...formData.value } as Record<string, any>
  Object.keys(record as Record<string, any>).forEach((k) => {
    const v = (record as Record<string, any>)[k]
    // 只覆盖"确有值"的字段,避免用 null 冲掉已回填好的患者信息
    if (v !== null && v !== undefined && v !== '') {
      merged[k] = v
    }
  })
  // 记录里可能完全没有患者字段,此时保留原患者信息
  if (!merged.patName) merged.patName = formData.value.patName
  formData.value = merged as typeof formData.value
}
const cancelInstall = async () => {
@@ -603,3 +628,72 @@
  getList()
})
</script>
<style scoped lang="scss">
/* ==========================================================================
   需求:表单内容挤占了下方记录表格的空间 —— 压缩行间距。
   Element Plus 默认 `.el-form-item { margin-bottom: 18px }`;
   本页表单含 18 项患者信息(4 列 = 5 行)+ 9 项设备信息,
   仅间距就要上百 px,把 el-table 挤到看不见。
   这里统一压到 4px。注意 :deep 是必需的(el-form-item 是本组件的子组件渲染)。
   ========================================================================== */
.patient-compact-form :deep(.el-form-item) {
  margin-bottom: 4px;
}
/* ==========================================================================
   患者信息固定在内容区顶部。
   内容区自身不滚动(滚动条在记录表格上),因此**不需要 position:sticky** ——
   直接作为 flex 的固定项(flex:none)即可始终留在顶部。
   背景不透明,保证与下方记录表格视觉分离。
   ========================================================================== */
:deep(.doctor-page__content) > .patient-compact-form {
  flex: none;
  padding-bottom: 4px;
  background-color: var(--el-bg-color);
  border-bottom: 1px solid var(--el-border-color-lighter);
}
/* ==========================================================================
   患者信息固定在内容区顶部。
   内容区(.doctor-page__content)自身滚动,所以这里必须用 position:sticky
   才能让 18 项患者信息在记录列表滚动时保持可见。
   背景不透明,避免滚动内容从表单项间隙透出。
   ========================================================================== */
:deep(.doctor-page__content) > .patient-compact-form {
  position: sticky;
  top: 0;
  z-index: 3;
  padding-bottom: 4px;
  background-color: var(--el-bg-color);
  border-bottom: 1px solid var(--el-border-color-lighter);
}
/* ==========================================================================
   患者操作功能块:位于「患者信息」与「已完成记录列表」之间,独立成块。
   需求:各功能按钮(检查完成 / 放弃检查 / 重置 / 知情同意书 …)**排在一行**。
   做法:flex + gap,并 **禁止换行**(flex-wrap:nowrap);
   按钮用 size="small" 以容纳全部按钮;即使宽度不足也不折行。
   上下边框 + 底色,使它与患者信息、记录列表在视觉上分区。
   ========================================================================== */
.patient-actions {
  display: flex;
  flex: none;
  flex-wrap: nowrap;
  gap: 8px;
  align-items: center;
  /* 上下留白 + 分隔线,形成独立功能块 */
  padding: 6px 4px;
  margin-bottom: 6px;
  background-color: var(--el-bg-color);
  border-top: 1px solid var(--el-border-color-lighter);
  border-bottom: 1px solid var(--el-border-color-lighter);
  /* Element Plus 给相邻 el-button 默认加了 margin-left:12px,会与 gap 叠加,清掉 */
  :deep(.el-button + .el-button) {
    margin-left: 0;
  }
}
</style>