<!--
|
工作台顶部「工位切换 / 围观」下拉(主/围观模型的入口)
|
|
为什么放在这里:医生在工作台里干活时,最自然的诉求是"顺便看一眼别的诊室在做什么"。
|
下拉列出所有**已开放**工位,并标出相对我的身份:
|
|
· 我的工位 → 切回自己(可操作)
|
· 空闲 → 询问后入座(成为主人;服务端会自动释放我原来的工位)
|
· 已入座:张三 → **围观**(只读,不能改别人的数据)
|
|
围观只改"我在看哪个工位"(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>
|