yxh
2026-10-03 d553a4be94d4f6ee162cc353a28b220403909cfc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
<!--
  医生工作台通用布局(叫号检查 / 设备领用 / 装机 三页共用)
 
  【解决什么问题】
  原先三个页面的功能按钮放在 `.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>