696 lines
17 KiB
Vue
696 lines
17 KiB
Vue
<template>
|
||
<div class="patientList-container" :class="{ 'patientList-container-unexpand': !currentExpand }">
|
||
<div
|
||
class="patientList-operate"
|
||
:class="{ 'patientList-operate-unexpand': !currentExpand }"
|
||
v-if="currentExpand"
|
||
>
|
||
<el-input
|
||
class="patientList-search-input"
|
||
placeholder="床号/住院号/姓名"
|
||
v-model="searchKeyword"
|
||
@keyup.enter="handleSearch"
|
||
:prefix-icon="Search"
|
||
/>
|
||
<el-button class="icon-btn" circle @click="handleRefresh" type="text" plain>
|
||
<el-icon icon-class="Refresh" size="24" :class="{ 'is-rotating': refreshing }">
|
||
<Refresh />
|
||
</el-icon>
|
||
</el-button>
|
||
</div>
|
||
|
||
<transition name="patient-list-toggle" mode="out-in">
|
||
<div key="expanded" class="patientList-list" v-if="currentExpand">
|
||
<div class="patient-cards" v-loading="isLoading">
|
||
<template v-if="filteredCardData && filteredCardData.length > 0">
|
||
<el-scrollbar ref="expandScrollbarRef" class="patient-cards-scrollbar">
|
||
<div
|
||
class="patient-card"
|
||
v-for="item in filteredCardData"
|
||
:key="item.encounterId"
|
||
:id="item.encounterId"
|
||
@click="handleItemClick(item)"
|
||
:class="{ actived: activeCardId === item.encounterId }"
|
||
>
|
||
<div class="patient-card-header">
|
||
<div class="header-top">
|
||
<div class="bed-container">
|
||
<div class="bed">
|
||
<div class="bed-info">
|
||
<div v-if="item.houseName" class="house-name">{{ item.houseName }}</div>
|
||
<div class="bed-name">{{ item.bedName || '未分床' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-space>
|
||
<el-tag
|
||
v-if="item.statusEnum_enumText"
|
||
size="small"
|
||
class="payer-tag-status"
|
||
effect="light"
|
||
:style="getStatusStyle(item.statusEnum)"
|
||
>
|
||
{{ item.statusEnum_enumText }}
|
||
</el-tag>
|
||
<el-tag
|
||
v-if="item.contractName"
|
||
size="small"
|
||
class="payer-tag"
|
||
effect="light"
|
||
>
|
||
{{ item.contractName }}
|
||
</el-tag>
|
||
</el-space>
|
||
</div>
|
||
|
||
<div class="header-bottom">
|
||
<span class="bus-no">住院号:{{ item.busNo || '-' }}</span>
|
||
<span class="insurance-type" v-if="item.insutype_dictText">
|
||
险种类型:{{ item.insutype_dictText }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="doctor-parent-line" />
|
||
|
||
<div class="patient-card-body">
|
||
<div class="personal-info-container">
|
||
<div class="name-container">
|
||
<div class="name">
|
||
<el-text :text="item.patientName" tclass="name" width="auto">
|
||
{{ item.patientName || '-' }}
|
||
</el-text>
|
||
</div>
|
||
<div class="age">
|
||
<el-tag
|
||
size="small"
|
||
class="age-tag"
|
||
effect="plain"
|
||
:class="{ 'age-tag-female': item.genderEnum_enumText === '女性' }"
|
||
>
|
||
{{ item.genderEnum_enumText || '-' }}
|
||
<span v-if="item.age"> · {{ item.age }}</span>
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-scrollbar>
|
||
</template>
|
||
<el-empty v-else description="暂无数据" />
|
||
</div>
|
||
</div>
|
||
<div
|
||
key="collapsed"
|
||
class="patientList-list"
|
||
v-else
|
||
v-loading="isLoading"
|
||
:class="{ 'patientList-list-unexpand': !currentExpand }"
|
||
>
|
||
<el-scrollbar ref="contractScrollbarRef" class="patient-cards-scrollbar">
|
||
<template v-if="filteredCardData && filteredCardData.length > 0">
|
||
<el-tooltip
|
||
v-for="item in filteredCardData"
|
||
:show-after="200"
|
||
:key="item.encounterId"
|
||
:show-arrow="true"
|
||
placement="right"
|
||
effect="light"
|
||
:offset="4"
|
||
>
|
||
<template #content>
|
||
<div class="card-tooltip">
|
||
<div class="card-tooltip-main">
|
||
<span class="card-tooltip-bed">{{ item.bedName }}</span>
|
||
<span class="card-tooltip-name">{{ item.patientName }}</span>
|
||
</div>
|
||
<div class="card-tooltip-sex">
|
||
<span class="card-tooltip-sex-text">
|
||
{{ item.genderEnum_enumText || '-' }}
|
||
<span v-if="item.age"> · {{ item.age }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div>
|
||
<div
|
||
class="card-small"
|
||
:class="{ 'patient-active': activeCardId === item.encounterId }"
|
||
@click="handleSmallCardClick(item)"
|
||
:key="item.encounterId"
|
||
>
|
||
{{ item.bedName }}
|
||
</div>
|
||
<div class="patient-card-small-border"></div>
|
||
</div>
|
||
</el-tooltip>
|
||
</template>
|
||
<el-empty v-else description="暂无数据" :image-size="50" />
|
||
</el-scrollbar>
|
||
</div>
|
||
</transition>
|
||
|
||
<div
|
||
class="patientList-toggle-btn-wrap"
|
||
:class="{ 'patientList-toggle-btn-wrap-unexpand': !currentExpand }"
|
||
>
|
||
<el-button class="icon-btn" circle @click="updateExpand">
|
||
<el-icon class="svg-sty-menu" size="24">
|
||
<Expand v-if="!currentExpand" />
|
||
<Fold v-if="currentExpand" />
|
||
</el-icon>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import {computed, ref, watch} from 'vue';
|
||
import {Expand, Fold, Refresh, Search} from '@element-plus/icons-vue';
|
||
import type {ElScrollbar} from 'element-plus';
|
||
|
||
interface PatientCardItem {
|
||
encounterId: string;
|
||
bedName?: string;
|
||
busNo?: string;
|
||
patientName?: string;
|
||
genderEnum_enumText?: string;
|
||
age?: number | string;
|
||
contractName?: string;
|
||
[key: string]: any;
|
||
}
|
||
|
||
interface Props {
|
||
// 过滤后的卡片数据
|
||
filteredCardData?: PatientCardItem[];
|
||
// 当前激活的卡片ID
|
||
activeCardId?: string;
|
||
// 是否展开(不传则为不受控模式,组件内部自己管理)
|
||
expand?: boolean;
|
||
// 加载状态
|
||
loading?: boolean;
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
filteredCardData: () => [],
|
||
activeCardId: '',
|
||
expand: undefined,
|
||
loading: false,
|
||
});
|
||
|
||
interface Emits {
|
||
(e: 'item-click', item: PatientCardItem): void;
|
||
(e: 'search', keyword: string): void;
|
||
(e: 'refresh'): void;
|
||
(e: 'update:expand', value: boolean): void;
|
||
}
|
||
|
||
const emit = defineEmits<Emits>();
|
||
|
||
const searchKeyword = ref<string>('');
|
||
const refreshing = ref<boolean>(false);
|
||
const refreshLoading = ref<boolean>(false);
|
||
|
||
const internalExpand = ref<boolean>(true);
|
||
|
||
const isControlled = computed<boolean>(() => props.expand !== undefined);
|
||
|
||
const currentExpand = computed<boolean>(() => {
|
||
return isControlled.value ? props.expand! : internalExpand.value;
|
||
});
|
||
|
||
const isLoading = computed<boolean>(() => {
|
||
return props.loading || refreshLoading.value;
|
||
});
|
||
|
||
const handleItemClick = (item: PatientCardItem): void => {
|
||
emit('item-click', item);
|
||
};
|
||
|
||
const handleSmallCardClick = (item: PatientCardItem): void => {
|
||
emit('item-click', item);
|
||
};
|
||
|
||
const handleSearch = (): void => {
|
||
emit('search', searchKeyword.value);
|
||
};
|
||
|
||
const handleRefresh = (): void => {
|
||
if (refreshing.value) return;
|
||
refreshing.value = true;
|
||
refreshLoading.value = true;
|
||
emit('refresh');
|
||
setTimeout(() => {
|
||
refreshing.value = false;
|
||
}, 600);
|
||
|
||
setTimeout(() => {
|
||
if (!props.loading) {
|
||
refreshLoading.value = false;
|
||
}
|
||
}, 500);
|
||
};
|
||
|
||
const updateExpand = (): void => {
|
||
const newValue = !currentExpand.value;
|
||
if (isControlled.value) {
|
||
emit('update:expand', newValue);
|
||
} else {
|
||
internalExpand.value = newValue;
|
||
}
|
||
};
|
||
|
||
// 根据状态枚举值返回文本颜色
|
||
const getStatusColor = (statusEnum?: number): string => {
|
||
if (statusEnum === undefined || statusEnum === null) {
|
||
return '';
|
||
}
|
||
|
||
switch (statusEnum) {
|
||
case 2: // REGISTERED - 待入科
|
||
return '#E6A23C'; // 橙色
|
||
case 3: // AWAITING_DISCHARGE - 待出院
|
||
return '#F56C6C'; // 红色
|
||
case 4: // DISCHARGED_FROM_HOSPITAL - 待出院结算
|
||
return '#909399'; // 灰色
|
||
case 5: // ADMITTED_TO_THE_HOSPITAL - 已入院
|
||
return '#67C23A'; // 绿色
|
||
case 6: // PENDING_TRANSFER - 待转科
|
||
return '#409EFF'; // 蓝色
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
// 根据状态枚举值返回带透明背景的样式
|
||
const getStatusStyle = (statusEnum?: number): Record<string, string> => {
|
||
const color = getStatusColor(statusEnum);
|
||
if (!color) {
|
||
return {};
|
||
}
|
||
|
||
// 不同状态对应的半透明背景色
|
||
let backgroundColor = '';
|
||
switch (statusEnum) {
|
||
case 2: // 橙色
|
||
backgroundColor = 'rgba(230, 162, 60, 0.12)';
|
||
break;
|
||
case 3: // 红色
|
||
backgroundColor = 'rgba(245, 108, 108, 0.12)';
|
||
break;
|
||
case 4: // 灰色
|
||
backgroundColor = 'rgba(144, 147, 153, 0.12)';
|
||
break;
|
||
case 5: // 绿色
|
||
backgroundColor = 'rgba(103, 194, 58, 0.12)';
|
||
break;
|
||
case 6: // 蓝色
|
||
backgroundColor = 'rgba(64, 158, 255, 0.12)';
|
||
break;
|
||
default:
|
||
backgroundColor = 'rgba(148, 163, 184, 0.12)';
|
||
}
|
||
|
||
return {
|
||
color,
|
||
backgroundColor,
|
||
borderColor: 'transparent',
|
||
};
|
||
};
|
||
|
||
// 保险类型映射表
|
||
const insuranceTypeMap: Record<number, string> = {
|
||
310: '职工基本医疗保险',
|
||
320: '公务员医疗补助',
|
||
330: '大额医疗费用补助',
|
||
340: '离休人员医疗保障',
|
||
350: '一至六级残废军人医疗补助',
|
||
360: '老红军医疗保障',
|
||
370: '企业补充医疗保险',
|
||
380: '新型农村合作医疗',
|
||
390: '城乡居民基本医疗保险',
|
||
391: '城镇居民基本医疗保险',
|
||
392: '城乡居民大病医疗保险',
|
||
399: '其他特殊人员医疗保障',
|
||
410: '长期照护保险',
|
||
};
|
||
|
||
watch(
|
||
() => props.loading,
|
||
(newLoading) => {
|
||
if (!newLoading && refreshLoading.value) {
|
||
refreshLoading.value = false;
|
||
}
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.patient-card {
|
||
width: 100%;
|
||
overflow: hidden;
|
||
background-color: #fff;
|
||
border-radius: 6px;
|
||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||
box-shadow: 0 2px 6px 0 rgba(15, 35, 52, 0.12);
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease;
|
||
|
||
&:hover {
|
||
box-shadow: 0 4px 12px rgba(15, 35, 52, 0.18);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
&.actived {
|
||
background-color: rgba(7, 155, 140, 0.06);
|
||
border-color: var(--el-color-primary);
|
||
box-shadow: 0 0 0 1px rgba(7, 155, 140, 0.3), 0 4px 14px rgba(7, 155, 140, 0.25);
|
||
}
|
||
|
||
.patient-card-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10px 12px 4px;
|
||
|
||
.header-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
|
||
.bed-container {
|
||
display: flex;
|
||
flex: 1;
|
||
align-items: center;
|
||
min-width: 0;
|
||
|
||
.bed {
|
||
flex-grow: 0;
|
||
flex-shrink: 1;
|
||
min-width: 0;
|
||
|
||
.bed-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
line-height: 1.4;
|
||
|
||
.house-name {
|
||
color: #1f2933;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
white-space: normal;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.bed-name {
|
||
color: #1f2933;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
white-space: normal;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.payer-tag {
|
||
max-width: 120px;
|
||
font-size: 12px;
|
||
border-radius: 999px;
|
||
font-weight: bolder;
|
||
}
|
||
.payer-tag-status {
|
||
font-weight: bolder;
|
||
border-radius: 999px;
|
||
}
|
||
}
|
||
|
||
.header-bottom {
|
||
margin-top: 4px;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 2px;
|
||
color: #6b7280;
|
||
font-size: 12px;
|
||
|
||
.bus-no {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.insurance-type {
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
.doctor-parent-line {
|
||
margin: 6px 12px 0;
|
||
border-bottom: 1px dashed #e5e7eb;
|
||
}
|
||
|
||
.patient-card-body {
|
||
padding: 8px 12px 10px;
|
||
|
||
.personal-info-container {
|
||
display: block;
|
||
|
||
.name-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
width: 100%;
|
||
|
||
.name {
|
||
color: #111827;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.age {
|
||
flex-shrink: 0;
|
||
|
||
.age-tag {
|
||
border-radius: 999px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.age-tag-female {
|
||
border-color: rgb(255, 55, 158);
|
||
color: rgb(255, 126, 184);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.patientList-container {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
border-right: 1px solid #ebeef5;
|
||
background-color: #ffffff;
|
||
width: 240px;
|
||
min-width: 240px;
|
||
&-unexpand {
|
||
width: 84px;
|
||
min-width: 84px;
|
||
}
|
||
.patientList-operate {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 44px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
flex: none;
|
||
padding: 0 0 0 8px;
|
||
|
||
&-unexpand {
|
||
justify-content: space-around;
|
||
}
|
||
}
|
||
|
||
.patientList-list {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
height: 0;
|
||
width: 240px;
|
||
&-unexpand {
|
||
width: 84px;
|
||
}
|
||
|
||
.search-operate {
|
||
padding: 0 8px;
|
||
height: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
flex: none;
|
||
}
|
||
|
||
.patient-cards {
|
||
flex: 1;
|
||
padding: 0 8px;
|
||
|
||
overflow: hidden;
|
||
|
||
:deep(.patient-cards-scrollbar) {
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
.el-scrollbar__bar {
|
||
width: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-small {
|
||
height: 44px;
|
||
padding: 0 10px 0 12px;
|
||
overflow: hidden;
|
||
font-size: 14px;
|
||
line-height: 44px;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
border-right: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.patient-active {
|
||
background-color: #e6f7ff;
|
||
font-weight: 600;
|
||
color: var(--el-color-primary);
|
||
}
|
||
|
||
.patient-card-small-border {
|
||
display: block;
|
||
width: 100%;
|
||
height: 2px;
|
||
background-color: #f1faff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.patientList-toggle-btn-wrap {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 4px 16px 8px;
|
||
&-unexpand {
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
.patient-list-toggle-enter-active,
|
||
.patient-list-toggle-leave-active {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.patient-list-toggle-enter-from,
|
||
.patient-list-toggle-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-8px);
|
||
}
|
||
|
||
.card-tooltip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-width: 140px;
|
||
max-width: 220px;
|
||
padding: 6px 10px;
|
||
border-radius: 6px;
|
||
background-color: #ffffff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.card-tooltip-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.card-tooltip-bed {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.card-tooltip-name {
|
||
font-size: 13px;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.card-tooltip-sex {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.card-tooltip-sex-text {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.svg-gray {
|
||
fill: var(--hip-color-text-unit);
|
||
}
|
||
|
||
.icon-btn {
|
||
border: none;
|
||
background-color: transparent;
|
||
box-shadow: none;
|
||
padding: 4px;
|
||
}
|
||
|
||
.is-rotating {
|
||
animation: patient-refresh-rotate 0.6s linear;
|
||
}
|
||
|
||
@keyframes patient-refresh-rotate {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
:deep(.scrollbar) {
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
.el-scrollbar__bar {
|
||
width: 0;
|
||
}
|
||
}
|
||
|
||
.f-16 {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.f-14 {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.empty-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
|
||
.empty-text-sty {
|
||
margin-top: 0;
|
||
}
|
||
}
|
||
</style>
|