| | |
| | | <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> |
| | |
| | | </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> |
| | | |
| | |
| | | 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' }) |
| | |
| | | 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() // 消息弹窗 |
| | |
| | | patName: [{ required: true, message: '患者名不能为空', trigger: 'blur' }] |
| | | }) |
| | | |
| | | |
| | | const emit = defineEmits(['event_dev_install']) // 定义 success 事件,用于操作成功后的回调 |
| | | |
| | | const checkTypeStore = useCheckTypeStore() |
| | | |
| | |
| | | |
| | | /** 提交表单 */ |
| | | 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() |
| | | // 提交请求 |
| | |
| | | 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 |
| | | |
| | |
| | | const loading = ref(true) // 列表的加载中 |
| | | const list = ref<JobRecordVO[]>([]) // 列表的数据 |
| | | const total = ref(0) // 列表的总页数 |
| | | // 当前诊室信息(用于取诊室开展的检查项目做兜底,见 syncRoomCheckTypes) |
| | | const roomStore = useRoomStore() |
| | | |
| | | |
| | | const queryParams = reactive({ |
| | | pageNo: 1, |
| | | pageSize: 10, |
| | |
| | | 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) |
| | |
| | | }) |
| | | |
| | | </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> |