yxh
2026-10-03 d553a4be94d4f6ee162cc353a28b220403909cfc
src/views/ecg/doctor/components/DevInstallPanel.vue
@@ -1,54 +1,72 @@
<template>
  <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
    <el-form-item label="检查项目" prop="">
      {{ getCheckTypeName(formData.checkType) }}
    </el-form-item>
  <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="">
          {{ getCheckTypeName(formData.checkType) }}
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <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-form-item label="患者名称" prop="patName">
          <el-input v-model="formData.patName" placeholder="请输入患者名称" />
        </el-form-item>
        <el-form-item label="患者描述" prop="patDetails">
          <dict-tag :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>
        <!-- 需求1:患者信息 18 项统一展示(后端已从预约单平铺回填,setPatient 已赋值到 formData) -->
        <PatientInfoBlock :data="formData" check-type-field="checkType" :span="12" />
        <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="12">
      <el-col :span="6">
        <el-form-item label="设备编号" prop="devId">
          <el-input v-model="formData.devId" placeholder="请输入设备编号" @input="getDevInfo" />
        </el-form-item>
        <el-form-item label="设备描述" prop="">
          {{ devInfo?.brand || '-' }} {{ devInfo?.model || '-' }} {{ tranlateDevState(devInfo?.state) }}
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formData.remark" placeholder="请输入备注" />
        </el-form-item>
        <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-row>
    <!-- ===== 患者信息(需求1:18 项统一展示,一行四列)=====
         后端 DevRentRespVO 已从预约单平铺回填,setPatient 已赋值到 formData -->
    <PatientInfoBlock :data="formData" check-type-field="checkType" :span="6" />
    <!-- ===== 设备信息(一行四列)===== -->
    <DeviceInfoBlock :data="formData" :device="devInfo" time-label-mode="install" :span="6">
      <template #rentTime>
        <el-date-picker v-model="formData.rentTime" type="date" value-format="x" placeholder="选择装机时间" />
      </template>
    </DeviceInfoBlock>
    <el-form-item label="备注" prop="remark">
      <el-input v-model="formData.remark" placeholder="请输入备注" />
    </el-form-item>
  </el-form>
  <el-button @click="submitForm" type="primary" :disabled="formLoading">装机完成</el-button>
  <el-button @click="resetForm()">重 置</el-button>
  <el-button @click="cancelInstall" :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="resetForm()">重 置</el-button>
  <el-button size="small" @click="cancelInstall" :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" />
  <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 === 10">"已装机"</span>
@@ -86,10 +104,15 @@
        </template>
      </el-table-column>-->
    </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>
@@ -106,9 +129,11 @@
import { useCheckTypeStore } from "@/store/modules/checkType";
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { tranlateDevState } from "../../../../utils/statusFormatter";
import { getCheckTypeName } from "../../../../utils/checkTypeFormatter";
import { getCheckTypeName, needDevReady } from "../../../../utils/checkTypeFormatter";
import { useRoomStore } from '@/store/modules/room'
// 需求1:患者信息 18 项统一展示块
import PatientInfoBlock from "@/views/ecg/doctor/components/PatientInfoBlock.vue";
import DeviceInfoBlock from "@/views/ecg/doctor/components/DeviceInfoBlock.vue";
/** 设备装机 表单 */
defineComponent({ name: 'DevInstallPanel' })
@@ -117,8 +142,22 @@
  room: {
    type: Object as PropType<RoomBedVO>,
    required: true
  },
  /** 需求11:记录列表第一列「撤销」按钮的禁用态(由父页面控制) */
  undoDisabled: {
    type: Boolean,
    required: false,
    default: false
  },
  /** 需求11:记录列表第一列「撤销」按钮的 loading(由父页面控制) */
  undoLoading: {
    type: Boolean,
    required: false,
    default: false
  }
})
const emit = defineEmits(['event_dev_install', 'event_undo_finish'])
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
@@ -154,8 +193,6 @@
  patName: [{ required: true, message: '患者名不能为空', trigger: 'blur' }]
})
const emit = defineEmits(['event_dev_install']) // 定义 success 事件,用于操作成功后的回调
const checkTypeStore = useCheckTypeStore()
@@ -193,6 +230,17 @@
/** 提交表单 */
const submitForm = async () => {
  // 【守卫】装机受理的是「已领用」患者,其检查项目必须属于需要设备领用的流程。
  // 与 DevReadyPanel 同一守卫:`check_type.need_dev_ready` 在后端从未被校验,
  // 人工把常规心电图这类患者拉进来会造成"检查类患者出现在设备流程"的混乱。
  if (formData.value.checkType != null && !needDevReady(formData.value.checkType)) {
    const ctName = getCheckTypeName(formData.value.checkType) || String(formData.value.checkType)
    message.warning(
      `「${ctName}」不需要设备领用,不能办理装机。请确认是否应到「叫号检查」页面处理该患者。`
    )
    return
  }
  // 校验表单
  await formRef.value.validate()
  // 提交请求
@@ -297,7 +345,16 @@
  if (data == null)
    return
  formData.value = data  //不能整个赋值, 默认装机时间被清空
  // ⚠️ 原实现是整体替换 formData,会把患者信息冲成 null:
  //   实测 GET /ecg/dev-rent/get 的响应里,除 patName 外的患者字段全是 null
  //   (dev_rent 表不存这些列,且该路径未从 appointment 回填)。
  //   故改为「只覆盖非空字段」,保留已回填的患者信息(PatientInfoBlock 依赖)。
  const _merged = { ...formData.value } as Record<string, any>
  Object.keys(data as Record<string, any>).forEach((k) => {
    const v = (data as Record<string, any>)[k]
    if (v !== null && v !== undefined && v !== '') _merged[k] = v
  })
  formData.value = _merged as typeof formData.value  //不能整个赋值, 默认装机时间被清空
  if (formData.value == null)
    return
@@ -315,6 +372,10 @@
const loading = ref(true) // 列表的加载中
const list = ref<JobRecordVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
// 当前诊室信息(用于取诊室开展的检查项目做兜底,见 syncRoomCheckTypes)
const roomStore = useRoomStore()
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
@@ -327,10 +388,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)
@@ -415,3 +503,72 @@
})
</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>