feat:优化在线问诊的患者选药显示信息,并且优化了交互,患者挂号卡片可以直接把药品导入至处方

This commit is contained in:
李琦
2026-07-19 16:10:55 +08:00
parent c1b71142df
commit 7656c3ac49
3 changed files with 502 additions and 58 deletions

View File

@@ -1,14 +1,19 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { Avatar, Button, Image } from 'ant-design-vue';
// 勿用 message 作 toast 名:会覆盖 props.message导致模板 message_type 全失效
import { Avatar, Button, Image, message as antdMessage } from 'ant-design-vue';
import { usePrescriptionStore } from '#/store/prescription';
import { getChatMessageRegisterInfoApi } from '#/views/business/chat/api';
import { useChatStore } from '#/views/business/chat/stores/chat';
import { sendMessage } from '#/views/business/chat/utils/request';
import { getDrugsByRegisterId as getDrugsByRegisterIdPharmacy } from '#/views/doctor/online-consultation/api/index';
import { getDrugsByRegisterId as getDrugsByRegisterIdClinic } from '#/views/doctor/online-consultation-clinic/api/index';
// 辅助函数:按需解析消息内容
const getParsedContent = (messageType, messageContent) => {
@@ -81,10 +86,22 @@ const chatUserStore = chatUseUserStore();
const userStore = useUserStore();
const chatStore = useChatStore();
const themeStore = useThemeStore();
const route = useRoute();
const prescriptionStore = usePrescriptionStore();
/** 诊所接诊模块用 clinic API / 存储前缀,药店用 pharmacy */
const isClinicModule = computed(() => {
return route.path.includes('online-consultation-clinic');
});
const getDrugsByRegisterId = computed(() => {
return isClinicModule.value ? getDrugsByRegisterIdClinic : getDrugsByRegisterIdPharmacy;
});
// 转接方查看抽屉相关
const showTransferDrawer = ref(false);
const transferRegisterId = ref(null);
/** 挂号卡「添加到处方单」请求中,防重复点击 */
const registerAddToRxLoading = ref(false);
const [RefusalOfTreatmentModals, RefusalOfTreatmentModalApi] = useVbenModal({
connectedComponent: RefusalOfTreatmentModal,
@@ -182,17 +199,109 @@ const registerCardDisplay = computed(() => {
return base;
});
const registerDrugNamesLine = computed(() => {
/**
* 挂号卡片药品行:药名 + 规格 + 数量 + 缩略图(对齐小程序,去掉底部「各 X 盒」)
*/
const registerCardDrugRows = computed(() => {
const pc = registerCardDisplay.value;
if (!pc || typeof pc !== 'object') return '';
if (!pc || typeof pc !== 'object') return [];
const fallbackQty =
pc.number != null && pc.number !== '' ? Number(pc.number) : 1;
const arr = pc.selected_western_drugs;
if (Array.isArray(arr) && arr.length) {
return arr.map((d) => d?.name).filter(Boolean).join('、');
return arr.map((d, idx) => {
const q = d?.quantity != null ? Number(d.quantity) : fallbackQty;
return {
key: String(d?.drug_id || d?.id || idx),
name: d?.name || '',
specification: d?.specification || d?.spec || d?.drug_spec || '',
image: d?.image || d?.drug_image || '',
quantity: q >= 1 ? q : 1,
};
});
}
if (pc.drug?.name) return pc.drug.name;
return '';
if (pc.drug?.name) {
return [
{
key: String(pc.drug.drug_id || pc.drug.id || 0),
name: pc.drug.name,
specification: pc.drug.specification || pc.drug.spec || '',
image: pc.drug.image || pc.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
},
];
}
return [];
});
/**
* 是否展示「添加到处方单」:有选药、接诊中、患者侧卡片(对齐小程序 canAddExperienceToRx && !isMine
*/
const canAddRegisterDrugsToRx = computed(() => {
return (
registerCardDrugRows.value.length > 0 &&
Number(registerCardDisplay.value?.status) === 2 &&
!props.isSent
);
});
/**
* 将本条挂号卡对应选药写入处方单(复用 getDrugsByRegisterId + prescriptionStore
*/
const handleAddRegisterDrugsToPrescription = async () => {
if (registerAddToRxLoading.value) return;
const cardRegisterId = registerCardDisplay.value?.id;
const registerId =
prescriptionStore.currentRegisterId ||
chatStore.currentFriend?.register_id ||
cardRegisterId;
if (!registerId) {
antdMessage.warning('请先接诊患者');
return;
}
if (!prescriptionStore.currentRegisterId) {
const storagePrefix = isClinicModule.value
? 'onlineConsultationClinic-'
: 'onlineConsultation-';
prescriptionStore.initializePrescription(registerId, false, storagePrefix);
}
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
antdMessage.warning('请先选择诊所');
return;
}
}
registerAddToRxLoading.value = true;
try {
const res = await getDrugsByRegisterId.value(registerId, storeId);
const drugList = res?.result || res?.data || res || [];
if (!drugList || drugList.length === 0) {
antdMessage.warning('未找到药品信息');
return;
}
let addedCount = 0;
for (const drugData of drugList) {
if (prescriptionStore.addProducts(drugData)) {
addedCount++;
}
}
if (addedCount > 0) {
antdMessage.success(`已成功添加 ${addedCount} 个药品到处方单`);
emit('open-prescription', registerId);
} else {
antdMessage.warning('所有药品已在处方中');
}
} catch (error) {
console.error('添加药品失败:', error);
antdMessage.error('添加药品失败,请重试');
} finally {
registerAddToRxLoading.value = false;
}
};
const isFollowUpDrugAssistant = computed(() => {
if (props.message.message_type !== 11) return false;
const pc = parsedContent.value;
@@ -617,9 +726,9 @@ const getSexText = (sex) => {
</div>
</div>
<!-- 主诉 / 所选药品 / 数量小程序挂号卡对齐 -->
<!-- 主诉 / 所选药品逐药缩略图 + 规格 + 数量对齐小程序 -->
<div
v-if="registerCardDisplay.chief_complaint || registerDrugNamesLine || (registerCardDisplay.number != null && registerCardDisplay.number !== '')"
v-if="registerCardDisplay.chief_complaint || registerCardDrugRows.length"
class="mt-3 space-y-2 rounded-lg border border-gray-100 p-3 text-sm dark:border-gray-600"
>
<div v-if="registerCardDisplay.chief_complaint">
@@ -628,19 +737,34 @@ const getSexText = (sex) => {
registerCardDisplay.chief_complaint
}}</span>
</div>
<div v-if="registerDrugNamesLine">
<span class="font-medium text-gray-600 dark:text-gray-300">所选药品</span>
<span class="text-gray-800 dark:text-gray-100">{{
registerDrugNamesLine
}}</span>
</div>
<div
v-if="registerCardDisplay.number != null && registerCardDisplay.number !== ''"
>
<span class="font-medium text-gray-600 dark:text-gray-300">数量</span>
<span class="text-gray-800 dark:text-gray-100"
> {{ registerCardDisplay.number }} </span
<div v-if="registerCardDrugRows.length" class="space-y-2">
<div class="font-medium text-gray-600 dark:text-gray-300">所选药品</div>
<div
v-for="row in registerCardDrugRows"
:key="row.key"
class="flex items-start gap-2"
>
<Image
v-if="row.image"
:src="row.image"
:width="36"
:height="36"
class="register-drug-thumb flex-shrink-0 overflow-hidden rounded"
:preview="{ src: row.image }"
/>
<div class="min-w-0 flex-1">
<div class="text-gray-800 dark:text-gray-100">{{ row.name || '药品' }}</div>
<div
v-if="row.specification"
class="text-xs text-gray-400 dark:text-gray-500"
>
{{ row.specification }}
</div>
</div>
<span class="flex-shrink-0 font-medium text-gray-700 dark:text-gray-200"
>×{{ row.quantity }}</span
>
</div>
</div>
</div>
@@ -680,14 +804,20 @@ const getSexText = (sex) => {
拒诊
</Button>
</div>
<!-- 查看详情按钮 -->
<!-- <div class="flex items-center justify-center mt-2">-->
<!-- <div class="flex items-center text-blue-500 dark:text-blue-300 hover:text-blue-600 dark:hover:text-blue-200 transition-colors cursor-pointer">-->
<!-- <span class="text-sm font-medium">查看详情</span>-->
<!-- <i class="fas fa-chevron-right ml-1 text-xs"></i>-->
<!-- </div>-->
<!-- </div>-->
<!-- 接诊中且有选药导入到处方单对齐小程序挂号卡 -->
<div v-if="canAddRegisterDrugsToRx" class="mt-2">
<Button
:loading="registerAddToRxLoading"
block
size="small"
type="primary"
ghost
@click.stop="handleAddRegisterDrugsToPrescription"
>
<i class="fas fa-plus-circle mr-1"></i>
添加到处方单
</Button>
</div>
</div>
<!-- 时间信息 -->

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, watch } from 'vue';
import { Descriptions, Button, Empty } from 'ant-design-vue';
import { computed, ref, watch } from 'vue';
import { Descriptions, Button, Empty, Image } from 'ant-design-vue';
import { getPatientDetail, reception } from '../api/index.ts';
import { message } from 'ant-design-vue';
import { sendMessage } from '#/views/business/chat/utils/request';
@@ -140,6 +140,56 @@ const getSexText = (sex) => {
return '未填写';
};
/**
* 就诊记录药品行:多选西药优先,否则单药;含缩略图/规格/数量(与医生端接诊面板一致)
*/
const getRegisterInfoDrugRows = (info) => {
if (!info || typeof info !== 'object') return [];
const fallbackQty =
info.number != null && info.number !== '' ? Number(info.number) : 1;
const arr = info.selected_western_drugs;
if (Array.isArray(arr) && arr.length) {
return arr.map((d, idx) => {
const q = d?.quantity != null ? Number(d.quantity) : fallbackQty;
return {
key: String(d?.drug_id || d?.id || idx),
name: d?.name || '',
specification: d?.specification || d?.spec || '',
image: d?.image || d?.drug_image || '',
quantity: q >= 1 ? q : 1,
};
});
}
if (info.drug?.name) {
return [
{
key: String(info.drug.drug_id || info.drug.id || 0),
name: info.drug.name,
specification: info.drug.specification || info.drug.spec || '',
image: info.drug.image || info.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
},
];
}
return [];
};
/** 预计算药品行,避免模板内重复调用且便于始终渲染图/规格/数量 */
const registerInfoViews = computed(() => {
const list = patientDetail.value?.register_infos;
if (!Array.isArray(list)) return [];
return list.map((info) => ({
info,
drugRows: getRegisterInfoDrugRows(info),
}));
});
/** 无图时占位首字 */
const drugThumbLetter = (name) => {
const s = String(name || '药').trim();
return s ? s.slice(0, 1) : '药';
};
// 进入聊天
const handleEnterChat = () => {
if (!props.patient) {
@@ -228,24 +278,53 @@ watch(
/>
</div>
<!-- 就诊记录 -->
<div v-if="patientDetail.register_infos && patientDetail.register_infos.length > 0" class="register-info-section">
<h3 class="section-title text-gray-900 dark:text-gray-100 border-b-2 border-blue-600 dark:border-blue-400">就诊记录</h3>
<!-- 就诊记录字色走 scoped不用 Tailwind 灰阶以免冲掉框架主题 -->
<div v-if="registerInfoViews.length > 0" class="register-info-section">
<h3 class="section-title border-b-2 border-blue-600 dark:border-blue-400">就诊记录</h3>
<div class="register-info-list">
<div
v-for="info in patientDetail.register_infos"
:key="info.id"
class="register-info-item bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-600"
v-for="view in registerInfoViews"
:key="view.info.id"
class="register-info-item"
>
<div v-if="info.illnessInfo" class="illness-info">
<strong>症状</strong>{{ info.illnessInfo }}
<div v-if="view.info.illnessInfo" class="illness-info">
<strong>症状</strong>{{ view.info.illnessInfo }}
</div>
<div v-if="info.drug" class="drug-info">
<strong>药品</strong>{{ info.drug.name }}
<div v-if="view.info.chief_complaint" class="illness-info">
<strong>主诉</strong>{{ view.info.chief_complaint }}
</div>
<div v-if="view.drugRows.length" class="drug-info">
<strong>药品</strong>
<div class="drug-rows">
<div
v-for="row in view.drugRows"
:key="row.key"
class="drug-row"
>
<Image
v-if="row.image"
:src="row.image"
:width="40"
:height="40"
class="drug-thumb"
:preview="{ src: row.image }"
/>
<div
v-else
class="drug-thumb-placeholder"
aria-hidden="true"
>{{ drugThumbLetter(row.name) }}</div>
<div class="drug-meta">
<div class="drug-name">{{ row.name || '药品' }}</div>
<div class="drug-spec">{{ row.specification || '暂无规格' }}</div>
</div>
<span class="drug-qty">×{{ row.quantity }}</span>
</div>
</div>
</div>
<div class="meta-info">
<span>就诊过{{ info.has_visited ? '是' : '否' }}</span>
<span style="margin-left: 16px;">使用过药品{{ info.has_used_drug ? '是' : '否' }}</span>
<span>就诊过{{ view.info.has_visited ? '是' : '否' }}</span>
<span style="margin-left: 16px;">使用过药品{{ view.info.has_used_drug ? '是' : '否' }}</span>
</div>
</div>
</div>
@@ -338,7 +417,8 @@ watch(
.register-info-item {
padding: 16px;
border-radius: 8px;
border: 1px solid;
border: 1px solid #e5e7eb;
background: #f9fafb;
}
.illness-info,
@@ -348,6 +428,68 @@ watch(
line-height: 1.6;
}
.drug-rows {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.drug-row {
display: flex;
align-items: flex-start;
gap: 8px;
}
.drug-thumb {
flex-shrink: 0;
overflow: hidden;
border-radius: 6px;
}
.drug-thumb-placeholder {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-size: 14px;
font-weight: 600;
color: #64748b;
background: #e2e8f0;
border-radius: 6px;
}
.drug-meta {
flex: 1;
min-width: 0;
}
/* 药品名/数量继承面板正文色,不写死灰阶以免覆盖主题 */
.drug-name,
.drug-qty {
color: inherit;
font-size: 14px;
line-height: 1.4;
}
.drug-name {
font-weight: 500;
}
.drug-qty {
flex-shrink: 0;
font-weight: 600;
}
.drug-spec {
margin-top: 2px;
font-size: 12px;
line-height: 1.4;
opacity: 0.65;
}
.illness-info strong,
.drug-info strong {
margin-right: 8px;
@@ -380,6 +522,11 @@ watch(
color: #f3f4f6;
}
.dark .register-info-item {
border-color: #4b5563;
background: #1f2937;
}
.dark .illness-info,
.dark .drug-info {
color: #cbd5e1;
@@ -390,6 +537,16 @@ watch(
color: #e2e8f0;
}
.dark .drug-name,
.dark .drug-qty {
color: inherit;
}
.dark .drug-thumb-placeholder {
color: #cbd5e1;
background: #334155;
}
.dark .meta-info {
color: #94a3b8;
}

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, watch } from 'vue';
import { Descriptions, Button, Empty } from 'ant-design-vue';
import { computed, ref, watch } from 'vue';
import { Descriptions, Button, Empty, Image } from 'ant-design-vue';
import { getPatientDetail, reception } from '../api/index.ts';
import { message } from 'ant-design-vue';
import { sendMessage } from '#/views/business/chat/utils/request';
@@ -139,6 +139,56 @@ const getSexText = (sex) => {
return '未填写';
};
/**
* 就诊记录药品行:多选西药优先,否则单药;含缩略图/规格/数量
*/
const getRegisterInfoDrugRows = (info) => {
if (!info || typeof info !== 'object') return [];
const fallbackQty =
info.number != null && info.number !== '' ? Number(info.number) : 1;
const arr = info.selected_western_drugs;
if (Array.isArray(arr) && arr.length) {
return arr.map((d, idx) => {
const q = d?.quantity != null ? Number(d.quantity) : fallbackQty;
return {
key: String(d?.drug_id || d?.id || idx),
name: d?.name || '',
specification: d?.specification || d?.spec || '',
image: d?.image || d?.drug_image || '',
quantity: q >= 1 ? q : 1,
};
});
}
if (info.drug?.name) {
return [
{
key: String(info.drug.drug_id || info.drug.id || 0),
name: info.drug.name,
specification: info.drug.specification || info.drug.spec || '',
image: info.drug.image || info.drug.drug_image || '',
quantity: fallbackQty >= 1 ? fallbackQty : 1,
},
];
}
return [];
};
/** 预计算药品行,避免模板内重复调用且便于始终渲染图/规格/数量 */
const registerInfoViews = computed(() => {
const list = patientDetail.value?.register_infos;
if (!Array.isArray(list)) return [];
return list.map((info) => ({
info,
drugRows: getRegisterInfoDrugRows(info),
}));
});
/** 无图时占位首字 */
const drugThumbLetter = (name) => {
const s = String(name || '药').trim();
return s ? s.slice(0, 1) : '药';
};
// 进入聊天
const handleEnterChat = () => {
if (!props.patient) {
@@ -228,24 +278,53 @@ watch(
/>
</div>
<!-- 就诊记录 -->
<div v-if="patientDetail.register_infos && patientDetail.register_infos.length > 0" class="register-info-section">
<h3 class="section-title text-gray-900 dark:text-gray-100 border-b-2 border-blue-600 dark:border-blue-400">就诊记录</h3>
<!-- 就诊记录字色走 scoped不用 Tailwind 灰阶以免冲掉框架主题 -->
<div v-if="registerInfoViews.length > 0" class="register-info-section">
<h3 class="section-title border-b-2 border-blue-600 dark:border-blue-400">就诊记录</h3>
<div class="register-info-list">
<div
v-for="info in patientDetail.register_infos"
:key="info.id"
class="register-info-item bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-600"
v-for="view in registerInfoViews"
:key="view.info.id"
class="register-info-item"
>
<div v-if="info.illnessInfo" class="illness-info">
<strong>症状</strong>{{ info.illnessInfo }}
<div v-if="view.info.illnessInfo" class="illness-info">
<strong>症状</strong>{{ view.info.illnessInfo }}
</div>
<div v-if="info.drug" class="drug-info">
<strong>药品</strong>{{ info.drug.name }}
<div v-if="view.info.chief_complaint" class="illness-info">
<strong>主诉</strong>{{ view.info.chief_complaint }}
</div>
<div v-if="view.drugRows.length" class="drug-info">
<strong>药品</strong>
<div class="drug-rows">
<div
v-for="row in view.drugRows"
:key="row.key"
class="drug-row"
>
<Image
v-if="row.image"
:src="row.image"
:width="40"
:height="40"
class="drug-thumb"
:preview="{ src: row.image }"
/>
<div
v-else
class="drug-thumb-placeholder"
aria-hidden="true"
>{{ drugThumbLetter(row.name) }}</div>
<div class="drug-meta">
<div class="drug-name">{{ row.name || '药品' }}</div>
<div class="drug-spec">{{ row.specification || '暂无规格' }}</div>
</div>
<span class="drug-qty">×{{ row.quantity }}</span>
</div>
</div>
</div>
<div class="meta-info">
<span>就诊过{{ info.has_visited ? '是' : '否' }}</span>
<span style="margin-left: 16px;">使用过药品{{ info.has_used_drug ? '是' : '否' }}</span>
<span>就诊过{{ view.info.has_visited ? '是' : '否' }}</span>
<span style="margin-left: 16px;">使用过药品{{ view.info.has_used_drug ? '是' : '否' }}</span>
</div>
</div>
</div>
@@ -337,7 +416,8 @@ watch(
.register-info-item {
padding: 16px;
border-radius: 8px;
border: 1px solid;
border: 1px solid #e5e7eb;
background: #f9fafb;
}
.illness-info,
@@ -347,6 +427,68 @@ watch(
line-height: 1.6;
}
.drug-rows {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.drug-row {
display: flex;
align-items: flex-start;
gap: 8px;
}
.drug-thumb {
flex-shrink: 0;
overflow: hidden;
border-radius: 6px;
}
.drug-thumb-placeholder {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-size: 14px;
font-weight: 600;
color: #64748b;
background: #e2e8f0;
border-radius: 6px;
}
.drug-meta {
flex: 1;
min-width: 0;
}
/* 药品名/数量继承面板正文色,不写死灰阶以免覆盖主题 */
.drug-name,
.drug-qty {
color: inherit;
font-size: 14px;
line-height: 1.4;
}
.drug-name {
font-weight: 500;
}
.drug-qty {
flex-shrink: 0;
font-weight: 600;
}
.drug-spec {
margin-top: 2px;
font-size: 12px;
line-height: 1.4;
opacity: 0.65;
}
.illness-info strong,
.drug-info strong {
margin-right: 8px;
@@ -379,6 +521,11 @@ watch(
color: #f3f4f6;
}
.dark .register-info-item {
border-color: #4b5563;
background: #1f2937;
}
.dark .illness-info,
.dark .drug-info {
color: #cbd5e1;
@@ -389,6 +536,16 @@ watch(
color: #e2e8f0;
}
.dark .drug-name,
.dark .drug-qty {
color: inherit;
}
.dark .drug-thumb-placeholder {
color: #cbd5e1;
background: #334155;
}
.dark .meta-info {
color: #94a3b8;
}