yxh
yxh
7 天以前 09dc74e264b6262cbe41c9c8476e24705871d4a6
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
<!--
  工作台顶部「工位切换 / 围观」下拉(主/围观模型的入口)
 
  为什么放在这里:医生在工作台里干活时,最自然的诉求是"顺便看一眼别的诊室在做什么"。
  下拉列出所有**已开放**工位,并标出相对我的身份:
 
    · 我的工位      → 切回自己(可操作)
    · 空闲          → 询问后入座(成为主人;服务端会自动释放我原来的工位)
    · 已入座:张三   → **围观**(只读,不能改别人的数据)
 
  围观只改"我在看哪个工位"(roomStore.room),**不动我入座的工位**(roomStore.currentSeat);
  两者不同时 `getCanOperate` 为 false → 页面只读。服务端另有硬校验(doc_id 必须是我)。
-->
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { RoomApi, RoomVO } from '@/api/ecg/room'
import { DoctorApi } from '@/api/ecg/doctor'
import { useRoomStore } from '@/store/modules/room'
 
defineOptions({ name: 'BedSwitchSelect' })
 
const router = useRouter()
const roomStore = useRoomStore()
 
/**
 * 【本次需求】工位切换的范围控制:
 *   · 叫号检查页:`opTypes=[0]` → 下拉只列**检查类**工位;
 *   · 设备领用 / 装机页:`enabled=false` → 工位固定(= 当前查看的工位),不提供切换。
 * 三个页面通过 DoctorPageLayout 透传。
 */
const props = withDefaults(
  defineProps<{
    /** 是否允许切换工位(false 时只显示"固定工位"标签) */
    enabled?: boolean
    /** 可切换工位的作业类型过滤(null/空 = 不过滤);0检查 1领用 2装机 */
    opTypes?: number[] | null
  }>(),
  { enabled: true, opTypes: null }
)
 
const beds = ref<RoomVO[]>([])
const loading = ref(false)
let timer: number | undefined
 
const canOperate = computed(() => roomStore.getCanOperate)
const dropdownText = computed(() => {
  if (!roomStore.getIsSetRoom || !roomStore.getViewText) return '选择工位'
  const seatCount = roomStore.getMySeatCount
  const suffix = seatCount > 1 ? `(我的工位 ${seatCount} 个)` : ''
  const base = canOperate.value ? roomStore.getViewText : `围观 ${roomStore.getViewText}`
  return `${base}${suffix}`
})
 
const isSameBed = (a?: { roomId?: any; bedNo?: any } | null, b?: RoomVO | null) =>
  !!a && !!b && String(a.roomId) === String(b.roomId) && String(a.bedNo) === String(b.bedNo)
 
const isMySeat = (bed: RoomVO) => {
  // 【本需求·一人多工位】按"我的工位列表"判断,而不是单个 currentSeat
  const seats = roomStore.getMySeats || []
  if (seats.some((s) => isSameBed(s, bed))) return true
  return isSameBed(roomStore.getCurrentSeat, bed)
}
/**
 * 本页是否允许**出现/操作**该作业类型的工位。
 *
 * 【口径确认(2026-11-06)】每个工作台**只显示本台允许操作的作业类型与诊室**:
 *   · 叫号检查页 → opType=0(检查)
 *   · 领用 / 装机页 → 本页不提供切换(`enabled=false`,工位固定)
 * 因此该过滤同时作用于「我的工位 / 空闲工位 / 他人占用(围观·接管)」三类。
 */
const isOpAllowed = (bed: RoomVO) => {
  const types = props.opTypes
  if (!Array.isArray(types) || types.length === 0) return true
  return types.some((t) => Number(t) === Number(bed.opType))
}
 
/** 我的工位(下拉里"我的工位"分组,按 roomId+bedNo 去重;同样只保留本台允许的作业类型) */
const myBeds = computed(() => (roomStore.getMySeats || [])
  .map((s) => ({
    id: s.id ?? null,
    roomId: s.roomId ?? null,
    roomName: s.roomName ?? null,
    bedNo: s.bedNo ?? null,
    ip: s.ip ?? null,
    status: s.status ?? null,
    docId: s.docId ?? null,
    docName: s.docName ?? null,
    checkTypes: s.checkTypes ?? null,
    opType: s.opType ?? null
  } as RoomVO))
  .filter((b) => isOpAllowed(b)))
/** 其它工位(排除我的工位;`beds` 已在 loadBeds 里按作业类型过滤) */
const otherBeds = computed(() => beds.value.filter((b) => !isMySeat(b)))
 
 
/**
 * 按 `opTypes` 过滤工位(null/空 = 不过滤)。
 *
 * <p>【口径 2026-11-06 确认】每个工作台**只显示本台允许操作的作业类型与诊室**:
 * 叫号检查页只看 `opType=0`(检查);领用/装机页不提供切换(`enabled=false`)。
 * 非本页类型的工位整条不显示(连围观/接管入口也不给),避免"在检查页操作装机诊室"。
 */
const filterBeds = (all: RoomVO[]): RoomVO[] => {
  const types = props.opTypes
  if (!Array.isArray(types) || types.length === 0) return all
  return all.filter((b) => types.some((t) => Number(t) === Number(b.opType)))
}
 
/** 其它工位里的「空闲」工位(点击 = 入座) */
const freeBeds = computed(() => otherBeds.value.filter((b) => b.docId == null))
/** 其它工位里的「被他人占用」工位(可围观、可接管)—— `beds` 已按作业类型过滤 */
const occupiedBeds = computed(() => otherBeds.value.filter((b) => b.docId != null))
const isViewing = (bed: RoomVO) => isSameBed(roomStore.getRoom, bed)
 
/** 固定工位的只读文案(`enabled=false` 时显示) */
const fixedText = computed(() => {
  const text = roomStore.getViewText
  if (!text) return '未选择工位'
  return canOperate.value ? text : `围观 ${text}`
})
 
/** 每个工位的身份文案 */
const bedLabel = (bed: RoomVO) => {
  const name = `${bed.roomName || ''} · ${bed.bedNo || ''}`
  if (isMySeat(bed)) return `${name}(我的工位)`
  if (bed.docId == null) return `${name}(空闲,可入座)`
  return `${name}(已入座:${bed.docName || '其他医生'},只读围观)`
}
 
const loadBeds = async () => {
  try {
    loading.value = true
    const map: any = await RoomApi.getOpeningBedMap()
    const all: RoomVO[] = []
    for (const key in map || {}) {
      const arr = map[key]
      if (Array.isArray(arr)) all.push(...arr)
    }
    // 【口径 2026-11-06 确认】每个工作台只显示本台允许操作的作业类型与诊室:
    // 非本页类型(如检查页里的装机诊室 opType=2)**整条不显示**,到对应工作台/选工位页处理。
    beds.value = filterBeds(all)
    // 正在围观时顺便刷新"主人是否已离开"(只提示,不自动接管)
    const cur = roomStore.getRoom
    if (cur?.roomId != null && cur.bedNo) {
      // 注意:用**全量** all 去找当前工位(它可能不在过滤后的列表里)
      const latest = all.find((b) => isSameBed(cur, b))
      if (latest) {
        roomStore.refreshViewTargetState(latest)
      } else {
        // 正在看的工位已不在"已开放工位"列表里(被关闭/删除)→ 自动回到我自己的工位,
        // 否则页面会一直查一个不存在工位的数据(表现就是"患者列表一直是空的")。
        const seat = roomStore.getCurrentSeat
        if (seat?.seated && !isSameBed(cur, seat)) {
          console.warn('[BedSwitchSelect] 正在查看的工位已不可用,切回我的工位', cur)
          await roomStore.setRoomInfoAction({
            id: seat.id ?? null,
            roomId: seat.roomId ?? null,
            roomName: seat.roomName ?? null,
            bedNo: seat.bedNo ?? null,
            ip: seat.ip ?? null,
            status: seat.status ?? null,
            docId: seat.docId ?? null,
            docName: seat.docName ?? null,
            checkTypes: seat.checkTypes ?? null,
            opType: seat.opType ?? null
          } as RoomVO)
          ElMessage({
            message: `原查看的工位已不可用,已切回我的工位:${seat.roomName || ''} ${seat.bedNo || ''}`,
            type: 'warning',
            duration: 5000
          })
        }
      }
    }
  } catch (e) {
    console.warn('[BedSwitchSelect] 加载工位列表失败', e)
  } finally {
    loading.value = false
  }
}
 
/**
 * 【本需求】切换工位前检查"当前工位是否还有就诊中患者"。
 *
 * 为什么提示:叫号逻辑是「本工位已有就诊中患者就不再叫下一位」,
 * 医生切走后该患者会一直停在就诊中(大屏也一直显示),所以切换前要提醒。
 *
 * @returns true 表示可以继续切换(无就诊中患者,或用户确认切换)
 */
const confirmSwitchWhenOnstage = async (): Promise<boolean> => {
  const cur = roomStore.getRoom
  if (!cur || cur.roomId == null || !cur.bedNo || !roomStore.getCanOperate) {
    return true // 不是"自己的工位"(围观)→ 不提示
  }
  try {
    const list: any = await DoctorApi.getToBeCheckedPatientList({
      roomId: cur.roomId,
      bedNo: cur.bedNo
    } as any)
    const onstage = (Array.isArray(list) ? list : []).find((p: any) => Number(p?.status) === 30)
    if (!onstage) return true
    await ElMessageBox.confirm(
      `当前工位「${cur.roomName || ''} · ${cur.bedNo}」还有就诊中患者(${onstage.patName || onstage.patId})。\n` +
        `切换后该患者仍停留在「就诊中」,且本工位不会再叫下一位。是否继续切换?`,
      '切换工位',
      { confirmButtonText: '继续切换', cancelButtonText: '留在本工位', type: 'warning' }
    )
    return true
  } catch (e: any) {
    // 用户点"留在本工位"(ElMessageBox reject) → 不切换;查询异常 → 不阻断
    if (e === 'cancel' || e === 'close') return false
    console.warn('[BedSwitchSelect] 查询当前工位就诊中患者失败(不阻断切换)', e)
    return true
  }
}
 
/** 【本需求·接班自助接管】接管"别人占用"的工位 */
const takeOverBed = async (bed: RoomVO) => {
  const bedText = `${bed.roomName || ''} · ${bed.bedNo || ''}`
  const owner = bed.docName || '其他医生'
  try {
    await ElMessageBox.confirm(
      `工位「${bedText}」当前由「${owner}」占用。\n确认接管?接管后「${owner}」将被强制离座(其页面会提示重新就坐)。`,
      '接管工位',
      { confirmButtonText: '确认接管', cancelButtonText: '取消', type: 'warning' }
    )
  } catch {
    return
  }
  const ok = await roomStore.takeOverBed(bed.roomId, bed.bedNo)
  if (!ok) {
    ElMessage({ message: '接管失败,请稍后重试或联系管理员', type: 'error', duration: 4000 })
    return
  }
  ElMessage.success(`已接管工位:${bedText}`)
  await loadBeds()
}
 
/** 选中一个工位(也兼容"接管"这种伪命令) */
const onPick = async (cmd: any) => {
  // ⓪ 接管命令(下拉里"接管"那一项传的是 { takeover: bed })
  if (cmd && cmd.takeover) {
    await takeOverBed(cmd.takeover as RoomVO)
    return
  }
  const bed = cmd as RoomVO
  if (!bed) return
  // 点的是当前正在看的工位 → 无需切换
  if (isViewing(bed)) return
 
  // ① 我的工位(多工位之一)→ 只切换查看目标,不重入座、不释放其它工位
  if (isMySeat(bed)) {
    if (!(await confirmSwitchWhenOnstage())) return
    await roomStore.setRoomInfoAction(bed)
    ElMessage.success(`已切换到我的工位:${bed.roomName || ''} ${bed.bedNo || ''}`)
    return
  }
  // ② 空闲 → 询问后入座(多工位:**新增**一个我的工位,不释放其它工位)
  if (bed.docId == null) {
    try {
      await ElMessageBox.confirm(
        `入座到「${bed.roomName || ''} / ${bed.bedNo || ''}」?` +
          `(将新增一个"我的工位",不释放其它工位)`,
        '入座确认',
        { confirmButtonText: '入座', cancelButtonText: '取消', type: 'warning' }
      )
    } catch {
      return
    }
    if (!(await confirmSwitchWhenOnstage())) return
    const ok = await roomStore.seatRoom(bed)
    if (!ok) {
      ElMessage({ message: '入座失败:该工位可能已被他人占用,或状态不允许(已关闭)', type: 'warning', duration: 4000 })
    } else {
      await roomStore.fetchMySeat()
      ElMessage.success(`已入座:${bed.roomName || ''} ${bed.bedNo || ''}`)
    }
    return
  }
  // ③ 已被别人占用 → 围观(只读)
  if (!(await confirmSwitchWhenOnstage())) return
  await roomStore.setViewTargetAction(bed, bed.docName)
  ElMessage({
    message: `正在围观「${bed.roomName || ''} / ${bed.bedNo || ''}」(主人 ${bed.docName || '其他医生'}),只读查看`,
    type: 'info',
    duration: 4000
  })
}
 
onMounted(async () => {
  await loadBeds()
  // 30s 轮询:刷新占用状态,并发现"主人已离开"(只提示,不自动接管)
  timer = window.setInterval(loadBeds, 30000)
})
 
onBeforeUnmount(() => {
  if (timer) window.clearInterval(timer)
})
</script>
 
<template>
  <div class="bed-switch">
    <!--
      【本次需求】领用 / 装机页不提供切换:工位固定为"当前查看的工位",
      这里只显示一个只读标签;要换工位请到顶栏工位标签 → 选工位页。
    -->
    <el-tooltip v-if="!enabled" content="本工作台工位固定;如需查看其它工位,请点顶栏工位标签进入「选工位页」" placement="bottom">
      <el-tag :type="canOperate ? 'primary' : 'warning'" effect="plain" class="bed-switch__fixed">
        <Icon :icon="canOperate ? 'ep:office-building' : 'ep:view'" class="mr-4px" />
        工位:{{ fixedText }}
        <span class="bed-switch__fixed-tip">(固定)</span>
      </el-tag>
    </el-tooltip>
 
    <el-dropdown v-else trigger="click" :disabled="loading" @command="onPick">
      <el-button size="small" :type="canOperate ? 'primary' : 'warning'" plain>
        <Icon :icon="canOperate ? 'ep:office-building' : 'ep:view'" class="mr-4px" />
        {{ dropdownText }}
        <Icon icon="ep:arrow-down" class="ml-4px" />
      </el-button>
      <template #dropdown>
        <el-dropdown-menu>
          <!-- 【本需求·一人多工位】我的工位分组:可一键切换(不重入座、不释放其它工位) -->
          <el-dropdown-item v-if="myBeds.length > 0" disabled class="bed-switch__group">
            我的工位({{ myBeds.length }} 个)
          </el-dropdown-item>
          <el-dropdown-item
            v-for="bed in myBeds"
            :key="`mine-${bed.roomId}-${bed.bedNo}`"
            :command="bed"
            :class="{ 'is-current': isViewing(bed) }"
          >
            {{ bedLabel(bed) }}
          </el-dropdown-item>
 
          <!-- 空闲工位(点击 = 入座,新增一个"我的工位") -->
          <el-dropdown-item v-if="freeBeds.length > 0" disabled class="bed-switch__group">
            空闲工位(可入座)
          </el-dropdown-item>
          <el-dropdown-item
            v-for="bed in freeBeds"
            :key="`free-${bed.roomId}-${bed.bedNo}`"
            :command="bed"
            :class="{ 'is-current': isViewing(bed) }"
          >
            {{ bedLabel(bed) }}
          </el-dropdown-item>
 
          <!--
            【口径 2026-11-06】每个工作台只显示本台允许操作的作业类型/诊室:
            非本页作业类型的工位(如检查页里的领用/装机诊室)**整条不显示**,
            需要去对应工作台(领用/装机页)或「选工位页」查看。
          -->
          <!--
            【本需求】被他人占用的工位:**围观 / 接管 两个入口都保留**
              · 「围观」= 切换查看(只读),用于看别人叫号情况;
              · 「接管」= 上一班医生忘记离座时,把工位强制转给我。
          -->
          <el-dropdown-item v-if="occupiedBeds.length > 0" disabled class="bed-switch__group">
            他人占用(可围观 / 可接管)
          </el-dropdown-item>
          <template v-for="bed in occupiedBeds" :key="`occ-${bed.roomId}-${bed.bedNo}`">
            <el-dropdown-item :command="bed" :class="{ 'is-current': isViewing(bed) }">
              围观「{{ bed.roomName || '' }} · {{ bed.bedNo }}」({{ bed.docName || '其他医生' }})
            </el-dropdown-item>
            <el-dropdown-item :command="{ takeover: bed }" class="bed-switch__takeover">
              接管「{{ bed.roomName || '' }} · {{ bed.bedNo }}」({{ bed.docName || '其他医生' }})
            </el-dropdown-item>
          </template>
 
          <el-dropdown-item v-if="beds.length === 0" disabled>暂无已开放工位</el-dropdown-item>
          <el-dropdown-item divided @click="router.push({ path: '/roomselect' })">
            去「选工位页」…
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
 
    <!-- 主人已离开:只提示,不自动接管(按确认口径) -->
    <el-tag v-if="!canOperate && roomStore.viewTargetVacant" type="info" size="small" class="ml-8px">
      该工位现无人操作,如需操作请到选工位页入座
    </el-tag>
  </div>
</template>
 
<style scoped lang="scss">
.bed-switch {
  display: flex;
  align-items: center;
  flex: none;
}
 
/* 【本次需求】固定工位的只读标签(领用 / 装机页) */
.bed-switch__fixed {
  font-weight: 600;
}
 
.bed-switch__fixed-tip {
  margin-left: 2px;
  font-weight: 400;
  color: var(--el-text-color-secondary);
}
 
/* 【本需求·一人多工位】下拉分组标题 */
:deep(.el-dropdown-menu__item.bed-switch__group) {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  cursor: default;
}
 
/* 【本需求·接班自助接管】接管项用警示色,避免与"围观"混淆 */
:deep(.el-dropdown-menu__item.bed-switch__takeover) {
  color: var(--el-color-warning);
}
:deep(.el-dropdown-menu__item.is-current) {
  color: var(--el-color-primary);
  font-weight: 600;
}
</style>