Files
his/openhis-ui-vue3/src/components/PatientList/index.vue

696 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>