<!--
|
医生工作台通用布局(叫号检查 / 设备领用 / 装机 三页共用)
|
|
【解决什么问题】
|
原先三个页面的功能按钮放在 `.right-bottom { position: absolute; bottom: 50px }`,
|
**脱离文档流**,因此"已完成患者列表"变长后会直接铺到按钮底下、把
|
「叫号 / 过号 / 重叫 / 暂停 / 撤销叫号」挡住;
|
且外层容器没有确定高度,中间区无法产生滚动,内容只能一路把页面撑长。
|
|
【布局契约】——纵向三段 + 右侧栏
|
┌────────────────────────────────────────────┐
|
│ #header 标题栏(固定高度,不滚动) │ flex: none
|
├──────────────────────────────┬─────────────┤
|
│ #default 主体(滚动区) │ #aside │ 仅中间区与右栏各自滚动
|
│ · 患者信息(由各面板内部 │ (独立滚动)│
|
│ 设为 sticky,钉在顶部) │ │
|
│ · 已完成记录列表(滚动) │ │
|
├──────────────────────────────┴─────────────┤
|
│ #footer 功能按钮(固定底部,永不被遮挡) │ flex: none
|
└────────────────────────────────────────────┘
|
|
【关键实现点】
|
1. 根元素 `height:100%; display:flex; flex-direction:column; overflow:hidden`
|
—— 让"整页"有确定高度,中间区才可能滚动(而不是把页面撑长)。
|
2. 中间区必须有 `min-height: 0`。flex 子项默认 `min-height: auto`
|
会**拒绝收缩到内容高度以下**,从而 `overflow-y:auto` 永不生效 ——
|
这是本类布局最常见的坑,别删。
|
3. `#footer` 在**正常文档流**中(不再用 position:absolute),
|
带不透明背景与上边框,内容永远不会透过来或盖住它。
|
4. 根元素**自带不透明背景**:各面板内 sticky 的患者信息若不透明,
|
滚动内容会从它与边缘的缝隙透出,故整页统一底色。
|
|
【插槽】
|
header 标题栏(可选)
|
default 主体内容(滚动区)
|
aside 右侧栏(可选;独立滚动,如患者队列)
|
footer 底部功能按钮(可选;固定在页面底部)
|
-->
|
<script setup lang="ts">
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
defineOptions({ name: 'DoctorPageLayout' })
|
|
withDefaults(
|
defineProps<{
|
/** 右侧栏宽度 */
|
asideWidth?: string
|
}>(),
|
{
|
asideWidth: '400px'
|
}
|
)
|
|
/**
|
* 【关键】不依赖 CSS `height: 100%` 的解析,改为**实测父容器可用高度**。
|
*
|
* 背景(真实故障):原先靠 `.doctor-page { height: 100% }` 撑起高度。
|
* 该写法要求**父级有确定高度**,而本组件挂在 `AppView` 的 `<section>` 下,
|
* 那条链上任何一环(`el-scrollbar__view` 的 99%、`AppView` 的 `min-height` 等)
|
* 一旦不成立,`height:100%` 就会退化成 `auto` ——
|
* 于是 `.doctor-page` **随内容长高**,flex 的 `flex:none` footer 被推到视口下方,
|
* 用户必须滚动页面才能看到 footer。这正是用户反馈的现象。
|
*
|
* 解决:用 `ResizeObserver` 量出**父元素的实际内容高度**,以内联 style 直接给根元素
|
* 设定像素高度。这样无论外层是 flex、block 还是百分比链,
|
* 我们的三段式布局都拿到一个**确定的**高度,footer 必然贴在容器底部、无需滚动。
|
*
|
* 若父级高度不可测(罕见),`height` 保持 undefined,
|
* 此时仍回退到 CSS 的 `height: 100%`,不会比原来更差。
|
*/
|
const rootRef = ref<HTMLElement>()
|
const measuredHeight = ref<string | undefined>(undefined)
|
|
let ro: ResizeObserver | undefined
|
let rafId = 0
|
|
const measure = () => {
|
const el = rootRef.value
|
const parent = el?.parentElement
|
if (!el || !parent) return
|
|
const cs = getComputedStyle(parent)
|
const inner =
|
parent.clientHeight -
|
parseFloat(cs.paddingTop || '0') -
|
parseFloat(cs.paddingBottom || '0')
|
|
// 父级高度不可测(auto/0)时保留 CSS 回退,不设内联高度
|
measuredHeight.value = inner > 0 ? `${inner}px` : undefined
|
}
|
|
/** 用 rAF 合并连续触发,避免 ResizeObserver 循环告警 */
|
const scheduleMeasure = () => {
|
if (rafId) cancelAnimationFrame(rafId)
|
rafId = requestAnimationFrame(() => {
|
rafId = 0
|
measure()
|
})
|
}
|
|
onMounted(() => {
|
measure()
|
const parent = rootRef.value?.parentElement
|
if (parent && typeof ResizeObserver !== 'undefined') {
|
ro = new ResizeObserver(scheduleMeasure)
|
ro.observe(parent)
|
} else {
|
window.addEventListener('resize', scheduleMeasure)
|
}
|
})
|
|
onBeforeUnmount(() => {
|
if (rafId) cancelAnimationFrame(rafId)
|
ro?.disconnect()
|
ro = undefined
|
window.removeEventListener('resize', scheduleMeasure)
|
})
|
</script>
|
|
<template>
|
<div ref="rootRef" class="doctor-page" :style="{ height: measuredHeight }">
|
<!-- 标题栏:固定高度,不滚动 -->
|
<div v-if="$slots.header" class="doctor-page__header">
|
<slot name="header"></slot>
|
</div>
|
|
<!-- 主体:占据剩余全部高度;内部只让"内容区"和"右栏"各自滚动 -->
|
<div class="doctor-page__body">
|
<div class="doctor-page__content">
|
<slot></slot>
|
</div>
|
|
<div
|
v-if="$slots.aside"
|
class="doctor-page__aside"
|
:style="{ width: asideWidth, flex: `0 0 ${asideWidth}` }"
|
>
|
<slot name="aside"></slot>
|
</div>
|
</div>
|
|
<!--
|
功能按钮:正常文档流中的固定底部栏,永不被内容遮挡。
|
需求:`自动`、`停止候诊` 与 `叫号/过号/重叫/暂停/撤销叫号` **合成一组**,
|
一起**靠右**(对着右侧候诊表),不要被拆散到左边/中间。
|
-->
|
<div v-if="$slots.footer" class="doctor-page__footer">
|
<div class="doctor-page__footer-right">
|
<slot name="footer"></slot>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped lang="scss">
|
.doctor-page {
|
display: flex;
|
flex-direction: column;
|
/*
|
兜底高度:仅在 `measuredHeight` 未测到时生效(见 script 中的说明)。
|
真正的确定性高度来自 ResizeObserver 实测后写入的行内 style ——
|
因为 `height: 100%` 依赖父级有确定高度,这条链在外层布局变化时会失效,
|
导致根元素随内容长高、把 footer 推到视口下方(用户反馈的现象)。
|
*/
|
height: 100%;
|
overflow: hidden;
|
/* 不透明底色:面板内 sticky 的患者信息与边缘缝隙不会透出滚动内容 */
|
background-color: var(--el-bg-color);
|
}
|
|
/* ---------- 标题栏 ---------- */
|
.doctor-page__header {
|
flex: none;
|
height: 40px;
|
padding: 0 12px;
|
overflow: hidden;
|
background-color: var(--el-color-primary-light-7);
|
}
|
|
/* ---------- 主体 ---------- */
|
.doctor-page__body {
|
display: flex;
|
flex: 1;
|
/* ⚠️ 关键:不加 min-height:0,flex 子项不会收缩,overflow 不生效 */
|
min-height: 0;
|
}
|
|
/*
|
内容区:**自身不滚动**(需求:滚动条要长在「已完成记录列表」上,而不是中间区整体)。
|
因此这里 flex 纵向排列 + overflow-y:auto:
|
· 患者信息(面板里的 el-form)flex:none,固定在上方;
|
· 记录列表(.records-host)flex:1;记录列表**自己滚动**
|
(各面板内 `.records-wrap :deep(.el-card__body) { max-height: calc(100% - 545px); overflow-y:auto }`
|
配合 `.el-table__header-wrapper { position: sticky }` 固定表头)。
|
|
这里的 `overflow-y: auto` 只在**空间实在不足**(患者信息 + 按钮把高度吃光)时生效,
|
作为兜底;正常情况不会出现滚动条。
|
注意必须保留 min-height:0,否则 flex 子项不会收缩,记录列表拿不到受限高度。
|
*/
|
.doctor-page__content {
|
display: flex;
|
flex: 1;
|
flex-direction: column;
|
min-width: 0;
|
min-height: 0;
|
padding: 6px 10px;
|
overflow-y: auto;
|
overflow-x: hidden;
|
border: solid var(--el-color-primary-light-7);
|
}
|
|
/* ---------- 右侧栏 ---------- */
|
.doctor-page__aside {
|
flex: none;
|
/* 允许右栏自身收缩,配合 overflow 独立滚动 */
|
min-width: 0;
|
min-height: 0;
|
/* 右栏独立滚动,长列表不会把底部按钮顶走 */
|
overflow-y: auto;
|
border: solid var(--el-color-primary-light-7);
|
border-left: none;
|
}
|
|
/* ---------- 底部功能按钮 ---------- */
|
.doctor-page__footer {
|
display: flex;
|
flex: none;
|
/* 需求:所有控件「一行显示」且整组靠右 —— 不允许换行,否则 footer 变两行、
|
会占掉记录区高度(原先 flex-wrap:wrap 在 1366 宽下就会折行)。 */
|
flex-wrap: nowrap;
|
align-items: center;
|
/* 整组靠右:右侧不留 padding,使按钮组与右侧 #aside(候诊表)右边线对齐 */
|
justify-content: flex-end;
|
padding: 4px 0 4px 8px;
|
/* 固定底部:有背景/上边框,内容不会透过来 */
|
background-color: var(--el-bg-color);
|
border-top: 1px solid var(--el-border-color);
|
}
|
|
/*
|
右侧控件组:`自动` 复选框 + 功能按钮 + `停止候诊` 开关 **合成一组**,
|
一起靠右。用 gap 统一间距(并清掉 Element Plus 给相邻 el-button 的
|
默认 margin-left:12px,否则会与 gap 叠加导致间距不均)。
|
*/
|
.doctor-page__footer-right {
|
display: flex;
|
flex: none;
|
gap: 8px;
|
align-items: center;
|
white-space: nowrap;
|
|
:deep(.el-button + .el-button) {
|
margin-left: 0;
|
}
|
}
|
</style>
|