优化功能
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
李琦
2026-01-22 14:53:29 +08:00
parent e33f2f1edc
commit 5ba5ec65c8
13 changed files with 2373 additions and 25 deletions

View File

@@ -16,7 +16,8 @@ import {
getProcessRuleList,
getProductListDoctorReception,
} from '#/views/doctor/doctor-reception/api';
import { getRegisterStoreInfo } from '#/views/doctor/online-consultation/api';
import { getRegisterStoreInfo as getRegisterStoreInfoPharmacy } from '#/views/doctor/online-consultation/api';
import { getRegisterStoreInfo as getRegisterStoreInfoClinic } from '#/views/doctor/online-consultation-clinic/api';
import traditionalJson from '#/views/business/chat/config/traditional.json';
export const usePrescriptionStore = defineStore('prescription', () => {
@@ -85,10 +86,13 @@ export const usePrescriptionStore = defineStore('prescription', () => {
const selectedStoreId = ref<number | null>(null); // 用户选择的诊所ID
const sendMode = ref(0); // 0=默认使用医生当前诊所1=自定义(使用挂号诊所)
// 存储前缀(用于区分在线复诊-药店和在线复诊-诊所)
const storagePrefix = ref('onlineConsultation-'); // 默认前缀
// 获取localStorage key按类型分开存储
const getStorageKey = (category?: number) => {
const cat = category ?? activeCategory.value;
return `onlineConsultation-prescriptionData_${cat}_${currentRegisterId.value}`;
return `${storagePrefix.value}prescriptionData_${cat}_${currentRegisterId.value}`;
};
// 计算属性
@@ -198,24 +202,28 @@ export const usePrescriptionStore = defineStore('prescription', () => {
// 完整初始化(包含患者信息,仅用于 PrescriptionModal
const initializePrescription = async (
registerId: number | string,
shouldFetchPatientInfo: boolean = false
shouldFetchPatientInfo: boolean = false,
prefix: string = 'onlineConsultation-'
) => {
// 设置存储前缀
storagePrefix.value = prefix;
// 设置当前注册ID
currentRegisterId.value = registerId;
// 恢复之前保存的 activeCategory
const savedCategory = localStorage.getItem(
`onlineConsultation-activeCategory${registerId}`
`${storagePrefix.value}activeCategory${registerId}`
);
if (savedCategory) {
activeCategory.value = Number.parseInt(savedCategory);
} else {
// 检查哪个存储键有数据
const chineseData = localStorage.getItem(
`onlineConsultation-prescriptionData_1_${registerId}`
`${storagePrefix.value}prescriptionData_1_${registerId}`
);
const westData = localStorage.getItem(
`onlineConsultation-prescriptionData_2_${registerId}`
`${storagePrefix.value}prescriptionData_2_${registerId}`
);
if (chineseData && JSON.parse(chineseData).length > 0) {
activeCategory.value = 1;
@@ -245,25 +253,28 @@ export const usePrescriptionStore = defineStore('prescription', () => {
};
// 轻量级初始化(仅用于 WesternModal 等子组件)
const initializeForModal = async (registerId: number | string) => {
const initializeForModal = async (registerId: number | string, prefix: string = 'onlineConsultation-') => {
console.log('轻量级初始化registerId:', registerId);
// 设置存储前缀
storagePrefix.value = prefix;
// 设置当前注册ID
currentRegisterId.value = registerId;
// 恢复之前保存的 activeCategory
const savedCategory = localStorage.getItem(
`onlineConsultation-activeCategory${registerId}`
`${storagePrefix.value}activeCategory${registerId}`
);
if (savedCategory) {
activeCategory.value = Number.parseInt(savedCategory);
} else {
// 检查哪个存储键有数据
const chineseData = localStorage.getItem(
`onlineConsultation-prescriptionData_1_${registerId}`
`${storagePrefix.value}prescriptionData_1_${registerId}`
);
const westData = localStorage.getItem(
`onlineConsultation-prescriptionData_2_${registerId}`
`${storagePrefix.value}prescriptionData_2_${registerId}`
);
if (chineseData && JSON.parse(chineseData).length > 0) {
activeCategory.value = 1;
@@ -348,7 +359,12 @@ export const usePrescriptionStore = defineStore('prescription', () => {
return;
}
try {
const res = await getRegisterStoreInfo(Number(currentRegisterId.value));
// 根据storagePrefix判断使用哪个API
const getRegisterStoreInfoFn = storagePrefix.value === 'onlineConsultationClinic-'
? getRegisterStoreInfoClinic
: getRegisterStoreInfoPharmacy;
const res = await getRegisterStoreInfoFn(Number(currentRegisterId.value));
registerStoreInfo.value = res;
// 默认选择挂号诊所
@@ -863,7 +879,7 @@ export const usePrescriptionStore = defineStore('prescription', () => {
// 保存 Tab 类型
localStorage.setItem(
`onlineConsultation-activeCategory${currentRegisterId.value}`,
`${storagePrefix.value}activeCategory${currentRegisterId.value}`,
categoryValue.toString(),
);
activeCategory.value = categoryValue;

View File

@@ -1,5 +1,6 @@
<script setup>
import { ref, provide, nextTick, onMounted, onUnmounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import { message, Modal } from 'ant-design-vue';
import { PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
@@ -12,11 +13,31 @@ import { useUserStore } from '../stores/user.ts';
import { sendMessage } from '../utils/request.ts';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
import { endConsultation } from '#/views/doctor/online-consultation/api/index';
// 根据路由判断是在线复诊-药店还是在线复诊-诊所
const route = useRoute();
const isClinicModule = computed(() => {
return route.path.includes('online-consultation-clinic');
});
// 动态导入对应的API和组件
import { endConsultation as endConsultationPharmacy } from '#/views/doctor/online-consultation/api/index';
import { endConsultation as endConsultationClinic } from '#/views/doctor/online-consultation-clinic/api/index';
import QuickReplyBubblesPharmacy from '#/views/doctor/online-consultation/components/QuickReplyBubbles.vue';
import QuickReplyBubblesClinic from '#/views/doctor/online-consultation-clinic/components/QuickReplyBubbles.vue';
// 根据模块选择对应的API和组件
const endConsultation = computed(() => {
return isClinicModule.value ? endConsultationClinic : endConsultationPharmacy;
});
const QuickReplyBubbles = computed(() => {
return isClinicModule.value ? QuickReplyBubblesClinic : QuickReplyBubblesPharmacy;
});
import CustomTextarea from './CustomTextarea.vue';
import EmojiPicker from './EmojiPicker.vue';
import FileUploadPreview from './FileUploadPreview.vue';
import QuickReplyBubbles from '#/views/doctor/online-consultation/components/QuickReplyBubbles.vue';
// 定义 props
const props = defineProps({
@@ -338,7 +359,9 @@ const handleAddPatientProductsToPrescription = async () => {
if (!prescriptionStore.currentRegisterId) {
const registerId = chatStore.currentFriend?.register_id;
if (registerId) {
prescriptionStore.initializePrescription(registerId, false);
// 根据模块选择对应的存储前缀
const storagePrefix = isClinicModule.value ? 'onlineConsultationClinic-' : 'onlineConsultation-';
prescriptionStore.initializePrescription(registerId, false, storagePrefix);
} else {
message.warning('请先接诊患者');
return;
@@ -448,7 +471,9 @@ const handleEndConsultation = () => {
cancelText: '取消',
onOk: async () => {
try {
await endConsultation({
// 使用计算属性获取对应的endConsultation函数
const endConsultationFn = endConsultation.value;
await endConsultationFn({
register_id: registerId,
doctor_id: userStore.currentUser?.doctor_id || '39',
reason: '医生主动结束'
@@ -469,7 +494,8 @@ const handleEndConsultation = () => {
<template>
<div class="message-input-container flex-shrink-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<!-- 快速回复气泡仅在线复诊场景显示 -->
<QuickReplyBubbles
<component
:is="QuickReplyBubbles"
v-if="chatStore.currentFriend"
:doctor-id="'32'"
@select-reply="handleQuickReplySelect"

View File

@@ -103,10 +103,14 @@ const [Modal, modalApi] = useVbenModal({
modalData.value = modalApi.getData<Record<string, any>>();
if (modalData.value?.registerId) {
// 获取存储前缀,默认为在线复诊-药店
const storagePrefix = modalData.value?.storagePrefix || 'onlineConsultation-';
// 使用完整初始化,包含患者信息(传递 true 以获取患者信息)
await prescriptionStore.initializePrescription(
modalData.value.registerId,
true,
storagePrefix,
);
prescriptionStore.loadFromLocalStorage();
// 获取挂号诊所信息
@@ -271,6 +275,9 @@ const togglePatientInfo = () => {
// 打开西药模态框
const openWesternModal = () => {
// 获取存储前缀从modalData或使用默认值
const storagePrefix = modalData.value?.storagePrefix || 'onlineConsultation-';
// 判断是否为简单产品类型3,5,6,7
const simpleProductTypes = [3, 5, 6, 7];
if (simpleProductTypes.includes(prescriptionStore.activeCategory)) {
@@ -282,7 +289,7 @@ const openWesternModal = () => {
getCurrentDrugs: () => {
// 这里可以添加获取当前药品的逻辑
},
storagePrefix: 'onlineConsultation-', // 在线复诊加前缀
storagePrefix: storagePrefix, // 使用动态前缀
});
SimpleProductModalApi.open();
} else {
@@ -294,7 +301,7 @@ const openWesternModal = () => {
getCurrentDrugs: () => {
// 这里可以添加获取当前药品的逻辑
},
storagePrefix: 'onlineConsultation-', // 在线复诊加前缀
storagePrefix: storagePrefix, // 使用动态前缀
});
WesternDrugModalApi.open();
}

View File

@@ -0,0 +1,102 @@
import { requestClient } from '#/api/request';
const prefix = 'online-consultation-clinic/';
/**
* 获取默认医生ID
*/
export async function getDefaultDoctorId() {
return requestClient.get<any>(`${prefix}get-default-doctor-id`);
}
/**
* 创建或获取聊天房间
* @param data
*/
export async function getOrCreateChatRoom(data: any) {
return requestClient.post<any>(`${prefix}get-or-create-chat-room`, data);
}
/**
* 结束在线复诊
* @param data
*/
export async function endConsultation(data: any) {
return requestClient.post<any>(`${prefix}end-consultation`, data);
}
/**
* 获取快速回复模板列表
* @param data
*/
export async function getQuickReplyList(data: any) {
return requestClient.get<any>(`${prefix}get-quick-reply-list`, { params: data });
}
/**
* 创建快速回复模板
* @param data
*/
export async function createQuickReply(data: any) {
return requestClient.post<any>(`${prefix}create-quick-reply`, data);
}
/**
* 更新快速回复模板
* @param data
*/
export async function updateQuickReply(data: any) {
return requestClient.post<any>(`${prefix}update-quick-reply`, data);
}
/**
* 删除快速回复模板
* @param data
*/
export async function deleteQuickReply(data: any) {
return requestClient.post<any>(`${prefix}delete-quick-reply`, data);
}
/**
* 获取在线复诊患者列表
* @param type 类型1=当前待接诊2=历史(已接诊)
*/
export async function getPatientList(type: number) {
return requestClient.get<any>(`${prefix}get-patient-list`, { params: { type } });
}
/**
* 获取患者详情
* @param roomId 房间ID可选如果没有room_id但有registerId可以通过registerId查询
* @param registerId 挂号ID可选
*/
export async function getPatientDetail(roomId?: string, registerId?: number) {
const params: any = {};
if (roomId) {
params.room_id = roomId;
}
if (registerId) {
params.register_id = registerId;
}
return requestClient.get<any>(`${prefix}get-patient-detail`, { params });
}
/**
* 接诊
* @param registerId 挂号ID
*/
export async function reception(registerId: number) {
return requestClient.post<any>(`${prefix}reception`, {
register_id: registerId
});
}
/**
* 获取挂号诊所信息
* @param registerId 挂号ID
*/
export async function getRegisterStoreInfo(registerId: number) {
return requestClient.get<any>(`${prefix}get-register-store-info`, {
params: { register_id: registerId }
});
}

View File

@@ -0,0 +1,192 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
content: {
type: Object,
required: true,
},
});
// 解析内容
const endData = computed(() => {
if (typeof props.content === 'string') {
try {
return JSON.parse(props.content);
} catch {
return {};
}
}
return props.content;
});
</script>
<template>
<div class="end-consultation-card">
<!-- 卡片头部 -->
<div class="card-header">
<div class="header-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="header-title">本次问诊已结束</div>
</div>
<!-- 卡片内容 -->
<div class="card-body">
<!-- 结束原因 -->
<div v-if="endData.end_reason" class="info-row">
<div class="info-icon">
<i class="fas fa-comment-alt"></i>
</div>
<div class="info-content">
<div class="info-label">结束原因</div>
<div class="info-value">{{ endData.end_reason }}</div>
</div>
</div>
<!-- 结束时间 -->
<div v-if="endData.end_time" class="info-row">
<div class="info-icon">
<i class="fas fa-clock"></i>
</div>
<div class="info-content">
<div class="info-label">结束时间</div>
<div class="info-value">{{ endData.end_time }}</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.end-consultation-card {
width: 100%;
min-width: 350px;
max-width: 450px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 16px;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.3s ease;
}
.end-consultation-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #64748b 0%, #475569 100%);
color: white;
}
.header-icon {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.header-title {
flex: 1;
font-size: 17px;
font-weight: 600;
letter-spacing: 0.5px;
}
/* 卡片内容 */
.card-body {
padding: 20px;
}
.info-row {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 16px;
background: white;
border-radius: 12px;
margin-bottom: 12px;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row:hover {
border-color: #64748b;
background: #f8fafc;
}
.info-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
font-size: 14px;
flex-shrink: 0;
}
.info-content {
flex: 1;
min-width: 0;
}
.info-label {
font-size: 12px;
color: #64748b;
margin-bottom: 4px;
font-weight: 500;
}
.info-value {
font-size: 14px;
color: #1e293b;
line-height: 1.6;
word-break: break-word;
}
/* 暗黑模式适配 */
.dark .end-consultation-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #334155;
}
.dark .card-header {
background: linear-gradient(135deg, #475569 0%, #334155 100%);
}
.dark .info-row {
background: #1e293b;
border-color: #334155;
}
.dark .info-row:hover {
background: #334155;
}
.dark .info-value {
color: #e2e8f0;
}
.dark .info-icon {
background: linear-gradient(135deg, #334155 0%, #1e293b 100%);
color: #94a3b8;
}
</style>

View File

@@ -0,0 +1,372 @@
<script setup>
import { computed, ref } from 'vue';
import { Button, message } from 'ant-design-vue';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
const props = defineProps({
content: {
type: Object,
required: true,
},
});
const prescriptionStore = usePrescriptionStore();
const addingDrug = ref(false);
// 解析内容
const experienceData = computed(() => {
if (typeof props.content === 'string') {
try {
return JSON.parse(props.content);
} catch {
return {};
}
}
return props.content;
});
const hasVisitedText = computed(() => {
return experienceData.value.has_visited === '1' || experienceData.value.has_visited === 1
? '已就诊过'
: '未就诊过';
});
const hasUsedDrugText = computed(() => {
return experienceData.value.has_used_drug === '1' || experienceData.value.has_used_drug === 1
? '已使用过药物'
: '未使用过药物';
});
// 添加药品到处方单
const handleAddToPrescription = async () => {
if (!experienceData.value.drug_name) {
message.warning('该就诊经历中没有药品信息');
return;
}
// 检查处方store是否已初始化
if (!prescriptionStore.currentRegisterId) {
message.warning('请先接诊患者');
return;
}
addingDrug.value = true;
try {
// 通过药品名称搜索药品
// 如果myStoreId为0尝试使用myStoreList的第一个
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
}
}
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: experienceData.value.drug_name,
});
if (!res || res.length === 0) {
message.warning(`未找到药品:${experienceData.value.drug_name}`);
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('该药品已在处方中');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已将${drugData.drug.drug_name}添加到处方单`);
}
} catch (error) {
console.error('添加药品失败:', error);
message.error('添加药品失败,请重试');
} finally {
addingDrug.value = false;
}
};
</script>
<template>
<div class="patient-experience-card">
<!-- 卡片头部 -->
<div class="card-header">
<div class="header-icon">
<i class="fas fa-notes-medical"></i>
</div>
<div class="header-title">患者就诊经历</div>
<div class="header-tags">
<span :class="['tag', hasVisitedText === '已就诊过' ? 'tag-success' : 'tag-warning']">
{{ hasVisitedText }}
</span>
<span :class="['tag', hasUsedDrugText === '已使用过药物' ? 'tag-info' : 'tag-default']">
{{ hasUsedDrugText }}
</span>
</div>
</div>
<!-- 卡片内容 -->
<div class="card-body">
<!-- 症状描述 -->
<div v-if="experienceData.symptom_description" class="info-row">
<div class="info-icon">
<i class="fas fa-stethoscope"></i>
</div>
<div class="info-content">
<div class="info-label">症状描述</div>
<div class="info-value">{{ experienceData.symptom_description }}</div>
</div>
</div>
<!-- 病情信息 -->
<div v-if="experienceData.illness_info" class="info-row">
<div class="info-icon">
<i class="fas fa-file-medical"></i>
</div>
<div class="info-content">
<div class="info-label">病情信息</div>
<div class="info-value">{{ experienceData.illness_info }}</div>
</div>
</div>
<!-- 使用药物 -->
<div v-if="experienceData.drug_name" class="info-row drug-row">
<div class="info-icon">
<i class="fas fa-pills"></i>
</div>
<div class="info-content">
<div class="info-label">使用药物</div>
<div class="info-value drug-name">{{ experienceData.drug_name }}</div>
</div>
</div>
</div>
<!-- 卡片底部操作 -->
<div v-if="experienceData.drug_name" class="card-footer">
<Button
type="primary"
:loading="addingDrug"
@click="handleAddToPrescription"
class="add-btn"
>
<i class="fas fa-plus-circle mr-1"></i>
添加药品到处方单
</Button>
</div>
</div>
</template>
<style scoped>
.patient-experience-card {
width: 100%;
min-width: 420px;
max-width: 520px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 16px;
border: 1px solid #e2e8f0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.3s ease;
}
.patient-experience-card:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
}
.header-icon {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.header-title {
flex: 1;
font-size: 17px;
font-weight: 600;
letter-spacing: 0.5px;
}
.header-tags {
display: flex;
gap: 8px;
}
.tag {
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.tag-success {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
}
.tag-warning {
background: rgba(251, 191, 36, 0.2);
color: #fcd34d;
}
.tag-info {
background: rgba(96, 165, 250, 0.2);
color: #93c5fd;
}
.tag-default {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.8);
}
/* 卡片内容 */
.card-body {
padding: 20px;
}
.info-row {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 16px;
background: white;
border-radius: 12px;
margin-bottom: 12px;
border: 1px solid #e2e8f0;
transition: all 0.2s ease;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-row:hover {
border-color: #3b82f6;
background: #f8fafc;
}
.info-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #3b82f6;
font-size: 14px;
flex-shrink: 0;
}
.drug-row .info-icon {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
color: #d97706;
}
.info-content {
flex: 1;
min-width: 0;
}
.info-label {
font-size: 12px;
color: #64748b;
margin-bottom: 4px;
font-weight: 500;
}
.info-value {
font-size: 14px;
color: #1e293b;
line-height: 1.6;
word-break: break-word;
}
.drug-name {
color: #d97706;
font-weight: 600;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
padding: 4px 12px;
border-radius: 6px;
display: inline-block;
}
/* 卡片底部 */
.card-footer {
padding: 16px 20px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: flex-end;
}
.add-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s ease;
}
.add-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* 暗黑模式适配 */
.dark .patient-experience-card {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: #334155;
}
.dark .info-row {
background: #1e293b;
border-color: #334155;
}
.dark .info-row:hover {
background: #334155;
}
.dark .info-value {
color: #e2e8f0;
}
.dark .card-footer {
background: #0f172a;
border-color: #334155;
}
</style>

View File

@@ -0,0 +1,336 @@
<script setup>
import { ref, watch } from 'vue';
import { Descriptions, Button, Empty } 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';
import { useUserStore } from '#/views/business/chat/stores/user';
const userStore = useUserStore();
const props = defineProps({
patient: {
type: Object,
default: null
},
doctorId: {
type: String,
default: '39' // 改为39与后端保持一致虽然不再使用但保持一致性
}
});
const emit = defineEmits(['reception-success', 'enter-chat']);
const patientDetail = ref(null);
const loading = ref(false);
const receptionLoading = ref(false);
// 获取患者详情
const fetchPatientDetail = async () => {
// 至少需要 room_id 或 register_id 其中之一
if (!props.patient || (!props.patient.room_id && !props.patient.register_id)) {
patientDetail.value = null;
return;
}
loading.value = true;
try {
// 如果患者数据中有register_id传递它来查询对应的就诊记录
let registerId;
if (props.patient.register_id) {
const id = Number(props.patient.register_id);
if (!isNaN(id) && id > 0) {
registerId = id;
}
}
// 支持只有 register_id 没有 room_id 的情况
const res = await getPatientDetail(props.patient.room_id || '', registerId);
// 确保正确解析返回数据
const data = res?.result || res?.data || res || null;
patientDetail.value = data;
} catch (error) {
console.error('获取患者详情失败:', error);
message.error('获取患者详情失败,请重试');
patientDetail.value = null;
} finally {
loading.value = false;
}
};
// 接诊
const handleReception = async () => {
if (!props.patient) {
message.error('患者信息不存在');
return;
}
if (!props.patient.register_id) {
message.error('缺少挂号ID');
return;
}
receptionLoading.value = true;
try {
// 传递挂号ID
const res = await reception(props.patient.register_id);
// res 已经是 data.result 的内容,无需判断 code请求封装会自动判断失败会抛出异常
message.success('接诊成功');
const receptionResult = {
...props.patient,
register_id: res.register_id || props.patient.register_id,
status: 2, // 接诊中
};
emit('reception-success', receptionResult);
await fetchPatientDetail();
} catch (error) {
console.error('接诊失败:', error);
message.error('接诊失败,请重试');
} finally {
receptionLoading.value = false;
}
};
// 获取性别文本
const getSexText = (sex) => {
if (sex === 1) return '男';
if (sex === 2) return '女';
return '未填写';
};
// 进入聊天
const handleEnterChat = () => {
if (!props.patient) {
message.warning('患者信息不存在');
return;
}
if (!props.patient.room_id) {
message.warning('患者尚未创建聊天房间,请先接诊');
return;
}
emit('enter-chat', props.patient);
};
// 监听患者变化
watch(
() => props.patient,
(newPatient) => {
if (newPatient) {
fetchPatientDetail();
} else {
patientDetail.value = null;
}
},
{ immediate: true }
);
</script>
<template>
<div class="patient-info-panel bg-white dark:bg-gray-900">
<div v-if="loading" class="loading-state">
<text>加载中...</text>
</div>
<div v-else-if="!patientDetail && !patient" class="empty-state">
<Empty description="请选择患者" />
</div>
<div v-else-if="patientDetail" class="patient-content">
<!-- 患者信息头部标题和进入聊天按钮 -->
<div class="patient-header">
<h2 class="patient-title">患者信息</h2>
<Button
v-if="patient && patient.room_id"
type="primary"
@click="handleEnterChat"
class="enter-chat-btn"
>
进入聊天
</Button>
</div>
<!-- 患者基本信息 -->
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
bordered
class="patient-descriptions"
>
<Descriptions.Item label="患者姓名">
{{ patientDetail.user_patient?.name || patientDetail.user?.nick_name || '未知' }}
</Descriptions.Item>
<Descriptions.Item label="患者年龄">
{{ patientDetail.user_patient?.age || 0 }}
</Descriptions.Item>
<Descriptions.Item label="患者性别">
{{ getSexText(patientDetail.user_patient?.sex) }}
</Descriptions.Item>
<Descriptions.Item label="手机号">
{{ patientDetail.user_patient?.mobile || patientDetail.user?.mobile || '未填写' }}
</Descriptions.Item>
<Descriptions.Item v-if="patientDetail.register_order?.order_no" label="订单号">
{{ patientDetail.register_order.order_no }}
</Descriptions.Item>
</Descriptions>
<!-- 就诊记录 -->
<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>
<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"
>
<div v-if="info.illnessInfo" class="illness-info">
<strong>症状</strong>{{ info.illnessInfo }}
</div>
<div v-if="info.drug" class="drug-info">
<strong>药品</strong>{{ info.drug.name }}
</div>
<div class="meta-info">
<span>就诊过{{ info.has_visited ? '是' : '否' }}</span>
<span style="margin-left: 16px;">使用过药品{{ info.has_used_drug ? '是' : '否' }}</span>
</div>
</div>
</div>
</div>
<!-- 接诊按钮 -->
<div v-if="!patientDetail.is_recepted" class="action-section border-t border-gray-200 dark:border-gray-700">
<Button
type="primary"
:loading="receptionLoading"
size="large"
block
@click="handleReception"
>
接诊
</Button>
</div>
<div v-else class="recepted-info bg-green-50 dark:bg-green-900/20">
<text class="recepted-text text-green-600 dark:text-green-400">已接诊</text>
</div>
</div>
</div>
</template>
<style scoped>
.patient-info-panel {
height: 100%;
overflow-y: auto;
padding: 20px;
}
.loading-state,
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.patient-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.patient-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.patient-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.enter-chat-btn {
flex-shrink: 0;
}
.patient-descriptions {
margin-bottom: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 8px;
}
.register-info-section {
margin-top: 24px;
}
.register-info-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.register-info-item {
padding: 16px;
border-radius: 8px;
border: 1px solid;
}
.illness-info,
.drug-info {
margin-bottom: 8px;
font-size: 14px;
line-height: 1.6;
}
.illness-info strong,
.drug-info strong {
margin-right: 8px;
}
.meta-info {
margin-top: 8px;
font-size: 12px;
}
.action-section {
margin-top: 24px;
padding-top: 24px;
}
.recepted-info {
margin-top: 24px;
padding: 16px;
text-align: center;
border-radius: 8px;
}
.recepted-text {
font-size: 16px;
font-weight: 500;
}
/* 暗色模式适配 */
.dark .patient-title {
color: #f3f4f6;
}
.dark .illness-info,
.dark .drug-info {
color: #cbd5e1;
}
.dark .illness-info strong,
.dark .drug-info strong {
color: #e2e8f0;
}
.dark .meta-info {
color: #94a3b8;
}
</style>

View File

@@ -0,0 +1,319 @@
<script setup>
import { ref, watch, onMounted } from 'vue';
import { RadioButton, RadioGroup, Button, message } from 'ant-design-vue';
import { ReloadOutlined } from '@ant-design/icons-vue';
import { getPatientList } from '../api/index.ts';
import { useChatStore } from '#/views/business/chat/stores/chat';
const chatStore = useChatStore();
const props = defineProps({
doctorId: {
type: String,
default: '32'
}
});
const emit = defineEmits(['select-patient']);
const listType = ref(1); // 1=当前2=历史
const patients = ref([]);
const selectedPatientId = ref(null);
const loading = ref(false);
// 获取患者列表
const fetchPatientList = async () => {
loading.value = true;
try {
const res = await getPatientList(listType.value);
// 确保正确解析返回数据jok函数返回格式为 { code: 0, message: "...", result: [...] }
const data = res?.result || res?.data || res || [];
patients.value = Array.isArray(data) ? data : [];
} catch (error) {
console.error('获取患者列表失败:', error);
patients.value = [];
} finally {
loading.value = false;
}
};
// 获取患者的唯一标识符
const getPatientUniqueId = (patient) => {
if (!patient) return null;
return patient.register_id || patient.room_id || patient.id || null;
};
// 切换列表类型
const handleTypeChange = () => {
selectedPatientId.value = null;
fetchPatientList();
};
// 选择患者
const selectPatient = (patient) => {
selectedPatientId.value = getPatientUniqueId(patient);
emit('select-patient', patient);
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
1: '待接诊',
2: '接诊中',
3: '已结束',
};
return statusMap[status] || '未知';
};
// 获取状态样式类
const getStatusClass = (status) => {
const classMap = {
1: 'status-pending',
2: 'status-consulting',
3: 'status-ended',
};
return classMap[status] || '';
};
// 格式化时间
const formatTime = (time) => {
if (!time) return '';
const date = new Date(time * 1000 || time);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return date.toLocaleDateString('zh-CN');
};
onMounted(() => {
fetchPatientList();
// 定时刷新列表每10分钟
setInterval(fetchPatientList, 600000);
});
// 监听列表类型变化
watch(listType, handleTypeChange);
// 监听新挂号通知,自动刷新患者列表
watch(
() => chatStore.newRegisterNotification,
(newNotification) => {
if (newNotification && listType.value === 1) {
// 只在"当前"列表时自动刷新
message.info('有新的挂号,正在刷新列表...');
fetchPatientList();
}
},
{ deep: true }
);
// 暴露方法供父组件调用
defineExpose({
fetchPatientList
});
</script>
<template>
<div class="patient-list-container bg-white dark:bg-gray-900">
<div class="list-header border-b border-gray-200 dark:border-gray-700">
<div class="header-top">
<RadioGroup v-model:value="listType" class="type-switch" @change="handleTypeChange">
<RadioButton :value="1">当前</RadioButton>
<RadioButton :value="2">历史</RadioButton>
</RadioGroup>
<Button
type="text"
:loading="loading"
@click="fetchPatientList"
class="refresh-btn"
>
<template #icon>
<ReloadOutlined />
</template>
刷新
</Button>
</div>
</div>
<div v-if="loading" class="loading-state">
<text>加载中...</text>
</div>
<div v-else-if="patients.length === 0" class="empty-state">
<text>暂无患者</text>
</div>
<div v-else class="patient-list">
<div
v-for="(patient, index) in patients"
:key="`patient-${getPatientUniqueId(patient) || index}`"
:class="['patient-card', 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800', { 'active border-l-4 border-l-blue-600 bg-blue-50 dark:bg-blue-900/20': selectedPatientId === getPatientUniqueId(patient) }]"
@click.stop="selectPatient(patient)"
>
<div class="patient-info">
<h3 class="patient-name">
{{ patient.user_patient?.name || patient.user?.nick_name || '未知患者' }}
</h3>
<p class="patient-phone">{{ patient.user_patient?.mobile || patient.user?.mobile || '' }}</p>
<p v-if="patient.order_no" class="order-no">订单号{{ patient.order_no }}</p>
<div class="patient-meta">
<span :class="['status', getStatusClass(patient.status)]">
{{ getStatusText(patient.status) }}
</span>
<span class="time text-gray-500 dark:text-gray-400">{{ formatTime(patient.last_message_time) }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.patient-list-container {
height: 100%;
display: flex;
flex-direction: column;
}
.list-header {
padding: 16px;
}
.header-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.type-switch {
flex: 1;
display: flex;
}
.refresh-btn {
flex-shrink: 0;
}
.type-switch :deep(.ant-radio-button-wrapper) {
flex: 1;
text-align: center;
}
.loading-state,
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #666;
}
.patient-list {
flex: 1;
overflow-y: auto;
}
.patient-card {
padding: 16px;
cursor: pointer;
transition: all 0.3s;
}
.patient-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.patient-name {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.patient-phone {
font-size: 14px;
margin: 0;
}
.order-no {
font-size: 12px;
margin: 4px 0 0 0;
}
.patient-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
}
.status {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background: #fef0f0;
color: #f56c6c;
}
.status-consulting {
background: #f0f9eb;
color: #67c23a;
}
.status-ended {
background: #f4f4f5;
color: #909399;
}
.time {
font-size: 12px;
}
/* 暗色模式适配 */
.dark .patient-name {
color: #e2e8f0;
}
.dark .patient-phone {
color: #94a3b8;
}
.dark .order-no {
color: #94a3b8;
}
.dark .status-pending {
background: rgba(245, 108, 108, 0.2);
color: #f87171;
}
.dark .status-consulting {
background: rgba(103, 194, 58, 0.2);
color: #4ade80;
}
.dark .status-ended {
background: rgba(144, 147, 153, 0.2);
color: #9ca3af;
}
.dark .loading-state,
.dark .empty-state {
color: #94a3b8;
}
</style>

View File

@@ -0,0 +1,147 @@
<script setup>
import { computed } from 'vue';
import { Card, Image, Button } from 'ant-design-vue';
const props = defineProps({
content: {
type: Object,
required: true,
},
});
const emit = defineEmits(['click-product']);
// 解析内容
const productData = computed(() => {
if (typeof props.content === 'string') {
try {
return JSON.parse(props.content);
} catch {
return {};
}
}
return props.content;
});
const handleClick = () => {
emit('click-product', productData.value);
};
</script>
<template>
<Card class="product-card" :bordered="true" style="max-width: 300px">
<div class="product-content" @click="handleClick">
<div v-if="productData.image" class="product-image">
<Image :src="productData.image" :preview="false" style="width: 100%; height: 150px; object-fit: cover;" />
</div>
<div class="product-info">
<div class="product-name">{{ productData.drug_name || '商品名称' }}</div>
<div v-if="productData.specification" class="product-spec">
规格{{ productData.specification }}
</div>
<div v-if="productData.function" class="product-function">
功效{{ productData.function }}
</div>
<div class="product-price">
<span class="price-label">价格</span>
<span class="price-value">¥{{ productData.price || '0.00' }}</span>
</div>
</div>
</div>
</Card>
</template>
<style scoped>
.product-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.product-content {
padding: 8px;
}
.product-image {
margin-bottom: 8px;
border-radius: 4px;
overflow: hidden;
}
.product-info {
padding: 0 4px;
}
.product-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.product-spec,
.product-function {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.product-price {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #eee;
}
.price-label {
font-size: 12px;
color: #666;
}
.price-value {
font-size: 18px;
font-weight: 600;
color: #ff4d4f;
margin-left: 4px;
}
/* 暗色模式适配 */
.dark .product-card {
background: #1e293b;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.dark .product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.dark .product-name {
color: #e2e8f0;
}
.dark .product-spec,
.dark .product-function {
color: #94a3b8;
}
.dark .product-price {
border-top-color: #334155;
}
.dark .price-label {
color: #94a3b8;
}
.dark .price-value {
color: #f87171;
}
</style>

View File

@@ -0,0 +1,255 @@
<script setup>
import { ref, onMounted, watch } from 'vue';
import { Button, Popconfirm } from 'ant-design-vue';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import {
getQuickReplyList,
createQuickReply,
updateQuickReply,
deleteQuickReply
} from '../api/index';
const props = defineProps({
doctorId: {
type: String,
default: '32',
},
});
const emit = defineEmits(['select-reply']);
const quickReplies = ref([]);
const showAddModal = ref(false);
const editingItem = ref(null);
const newContent = ref('');
// 加载快速回复列表
const loadQuickReplies = async () => {
try {
const res = await getQuickReplyList({ doctor_id: props.doctorId });
quickReplies.value = res || [];
} catch (error) {
console.error('加载快速回复失败:', error);
}
};
// 选择快速回复
const handleSelectReply = (content) => {
emit('select-reply', content);
};
// 添加快速回复
const handleAddReply = async () => {
if (!newContent.value.trim()) {
return;
}
try {
await createQuickReply({
doctor_id: props.doctorId,
content: newContent.value.trim(),
sort_order: quickReplies.value.length,
});
newContent.value = '';
showAddModal.value = false;
await loadQuickReplies();
} catch (error) {
console.error('添加快速回复失败:', error);
}
};
// 编辑快速回复
const handleEditReply = async (item) => {
editingItem.value = item;
newContent.value = item.content;
showAddModal.value = true;
};
// 更新快速回复
const handleUpdateReply = async () => {
if (!newContent.value.trim() || !editingItem.value) {
return;
}
try {
await updateQuickReply({
id: editingItem.value.id,
doctor_id: props.doctorId,
content: newContent.value.trim(),
});
newContent.value = '';
editingItem.value = null;
showAddModal.value = false;
await loadQuickReplies();
} catch (error) {
console.error('更新快速回复失败:', error);
}
};
// 删除快速回复
const handleDeleteReply = async (id) => {
try {
await deleteQuickReply({
id,
doctor_id: props.doctorId,
});
await loadQuickReplies();
} catch (error) {
console.error('删除快速回复失败:', error);
}
};
// 取消编辑
const handleCancel = () => {
newContent.value = '';
editingItem.value = null;
showAddModal.value = false;
};
onMounted(() => {
loadQuickReplies();
});
watch(() => props.doctorId, () => {
loadQuickReplies();
});
</script>
<template>
<div class="quick-reply-bubbles bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="bubbles-container">
<div
v-for="item in quickReplies"
:key="item.id"
class="reply-bubble bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20"
@click="handleSelectReply(item.content)"
>
<span class="bubble-text">{{ item.content }}</span>
<div class="bubble-actions" @click.stop>
<Button
v-if="item.doctor_id !== '0' && item.doctor_id !== 0"
type="text"
size="small"
@click="handleEditReply(item)"
>
<template #icon><EditOutlined /></template>
</Button>
<Popconfirm
v-if="item.doctor_id !== '0' && item.doctor_id !== 0"
title="确定删除这条快速回复吗?"
@confirm="handleDeleteReply(item.id)"
>
<Button
type="text"
size="small"
danger
>
<template #icon><DeleteOutlined /></template>
</Button>
</Popconfirm>
</div>
</div>
<div
v-if="!showAddModal"
class="add-bubble bg-white dark:bg-gray-700 border-dashed border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 hover:text-blue-500 dark:hover:text-blue-400"
@click="showAddModal = true"
>
<PlusOutlined />
<span>添加常用语</span>
</div>
<div v-else class="add-modal bg-white dark:bg-gray-700 border-blue-500 dark:border-blue-400">
<input
v-model="newContent"
type="text"
placeholder="输入常用语"
class="add-input bg-transparent placeholder-gray-400 dark:placeholder-gray-500"
@keyup.enter="editingItem ? handleUpdateReply() : handleAddReply()"
@keyup.esc="handleCancel"
/>
<div class="add-actions">
<Button size="small" @click="handleCancel">取消</Button>
<Button
type="primary"
size="small"
@click="editingItem ? handleUpdateReply() : handleAddReply()"
>
{{ editingItem ? '更新' : '添加' }}
</Button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.quick-reply-bubbles {
padding: 8px 12px;
}
.bubbles-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.reply-bubble {
position: relative;
display: inline-flex;
align-items: center;
padding: 6px 12px;
border: 1px solid;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s;
}
.reply-bubble:hover .bubble-actions {
display: flex;
}
.bubble-text {
font-size: 13px;
}
.bubble-actions {
display: none;
margin-left: 8px;
gap: 4px;
}
.add-bubble {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px dashed;
border-radius: 16px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.add-modal {
display: flex;
align-items: center;
gap: 8px;
padding: 4px;
border: 1px solid;
border-radius: 16px;
}
.add-input {
flex: 1;
padding: 4px 8px;
border: none;
outline: none;
font-size: 13px;
}
.add-actions {
display: flex;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,576 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useVbenModal } from '@vben/common-ui';
import { Button, Modal, message } from 'ant-design-vue';
import ChatArea from '#/views/business/chat/components/ChatArea.vue';
import ConnectionStatus from '#/views/business/chat/components/ConnectionStatus.vue';
import EmptyState from '#/views/business/chat/components/EmptyState.vue';
import PrescriptionModal from '#/views/business/chat/components/PrescriptionModal.vue';
import { useChatStore } from '#/views/business/chat/stores/chat.ts';
import { useUserStore } from '#/views/business/chat/stores/user';
import { usePrescriptionStore } from '#/store/prescription';
import PatientList from './components/PatientList.vue';
import PatientInfoPanel from './components/PatientInfoPanel.vue';
import { endConsultation } from './api/index.ts';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
const route = useRoute();
const router = useRouter();
const chatStore = useChatStore();
const userStore = useUserStore();
const prescriptionStore = usePrescriptionStore();
// 使用 VbenModal 管理处方模态框
const [PrescriptionModalComponent, prescriptionModalApi] = useVbenModal({
connectedComponent: PrescriptionModal,
});
// 当前患者信息
const currentPatient = ref(null);
const registerId = ref(null);
// 左侧列表显示状态
const leftShow = ref(true);
// 当前选中的患者
const selectedPatient = ref(null);
// PatientList 组件的 ref
const patientListRef = ref(null);
// 视图模式:'list' = 列表模式(显示患者列表和信息),'chat' = 聊天模式(显示聊天界面)
const viewMode = ref('list');
// 获取左侧列表显示状态
function getLeftShow() {
const localStorageLeftShow = localStorage.getItem('onlineConsultationClinicLeftShow');
leftShow.value = localStorageLeftShow !== 'false';
}
getLeftShow();
// 监听leftShow存入缓存
watch(
() => leftShow.value,
(enable) => {
localStorage.setItem('onlineConsultationClinicLeftShow', enable);
},
{ immediate: true }
);
// 从路由参数获取registerId
onMounted(() => {
const id = route.params.id || route.query.registerId;
if (id) {
registerId.value = id;
// 初始化处方store但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(id, false, 'onlineConsultationClinic-');
}
});
// 监听当前好友变化,更新患者信息
watch(
() => chatStore.currentFriend,
(friend) => {
if (friend) {
currentPatient.value = friend;
// 更新registerId
if (friend.registerId) {
registerId.value = friend.registerId;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(friend.registerId, false, 'onlineConsultationClinic-');
}
}
},
{ immediate: true }
);
// 显示开方模块
const openPrescriptionModule = (id) => {
const targetRegisterId = id || registerId.value;
if (!targetRegisterId) {
console.error('registerId不能为空');
return;
}
prescriptionModalApi.setData({
registerId: targetRegisterId,
onPrescriptionSent: handlePrescriptionSent,
storagePrefix: 'onlineConsultationClinic-',
});
prescriptionModalApi.open();
};
// 处理处方发送完成事件
const handlePrescriptionSent = () => {
// 可以在这里添加其他逻辑,如刷新聊天记录等
};
// 处理选择患者
const handleSelectPatient = (patient) => {
selectedPatient.value = patient;
// 如果是已接诊的患者status=2直接进入聊天模式否则显示患者信息
if (patient.status === 2) {
viewMode.value = 'chat';
} else {
viewMode.value = 'list';
}
// 设置当前好友,以便聊天功能正常工作
if (patient && patient.room_id) {
// 设置聊天好友信息
const friendInfo = {
id: patient.user_id || patient.user?.id,
room_id: patient.room_id,
register_id: patient.register_id,
nick_name: patient.user_patient?.name || patient.user?.nick_name || '患者',
avatar: patient.user?.avatar || '',
un_read_count: 0,
};
chatStore.setCurrentFriend(friendInfo);
// 切换到该好友并加载消息(如果已接诊)
if (friendInfo.room_id && userStore.currentUser?.doctor_id && patient.status === 2) {
chatStore.switchFriend(friendInfo, userStore.currentUser.doctor_id);
}
// 更新registerId
if (patient.register_id) {
registerId.value = patient.register_id;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(patient.register_id, false, 'onlineConsultationClinic-');
}
}
};
// 处理进入聊天
const handleEnterChat = (patient) => {
// 切换到聊天模式
viewMode.value = 'chat';
// 设置当前好友,以便聊天功能正常工作
if (patient && patient.room_id) {
// 设置聊天好友信息参考handleSelectPatient的逻辑
const friendInfo = {
id: patient.user_id || patient.user?.id,
room_id: patient.room_id,
register_id: patient.register_id,
nick_name: patient.user_patient?.name || patient.user?.nick_name || '患者',
avatar: patient.user?.avatar || '',
un_read_count: 0,
};
chatStore.setCurrentFriend(friendInfo);
// 切换到该好友并加载消息
if (friendInfo.room_id && userStore.currentUser?.id) {
chatStore.switchFriend(friendInfo, userStore.currentUser.id);
}
// 更新registerId
if (patient.register_id) {
registerId.value = patient.register_id;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(patient.register_id, false, 'onlineConsultationClinic-');
}
}
};
// 处理接诊成功
const handleReceptionSuccess = (patient) => {
// 更新 selectedPatient
selectedPatient.value = {
...selectedPatient.value,
...patient,
status: 2, // 接诊中
};
// 接诊成功后,切换到聊天模式
viewMode.value = 'chat';
// 确保 room_id 存在
if (!patient.room_id && selectedPatient.value?.room_id) {
patient.room_id = selectedPatient.value.room_id;
}
// 设置当前好友,以便聊天功能正常工作
if (patient && patient.room_id) {
// 设置聊天好友信息参考handleSelectPatient的逻辑
const friendInfo = {
id: patient.user_id || patient.user?.id,
room_id: patient.room_id,
register_id: patient.register_id,
nick_name: patient.user_patient?.name || patient.user?.nick_name || '患者',
avatar: patient.user?.avatar || '',
un_read_count: 0,
};
chatStore.setCurrentFriend(friendInfo);
// 切换到该好友并加载消息
if (friendInfo.room_id && userStore.currentUser?.doctor_id) {
chatStore.switchFriend(friendInfo, userStore.currentUser.doctor_id);
}
}
// 更新registerId
if (patient.register_id) {
registerId.value = patient.register_id;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(patient.register_id, false, 'onlineConsultationClinic-');
}
};
// 结束接诊
const handleEndConsultation = () => {
if (!selectedPatient.value) {
message.warning('请先选择患者');
return;
}
if (!selectedPatient.value.register_id) {
message.warning('患者信息不完整,无法结束接诊');
return;
}
const patientName = selectedPatient.value.user_patient?.name ||
selectedPatient.value.user?.nick_name ||
'患者';
Modal.confirm({
title: '确认结束接诊',
content: `确定要结束${patientName}的复诊吗?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await endConsultation({
register_id: selectedPatient.value.register_id,
doctor_id: userStore.currentUser?.doctor_id || '39',
reason: '医生主动结束'
});
// res 已经是 result 内容,无需判断 code
message.success('结束接诊成功');
// 清空选中的患者
selectedPatient.value = null;
// 切换到列表模式
viewMode.value = 'list';
// 刷新患者列表
patientListRef.value?.fetchPatientList();
} catch (error) {
console.error('结束接诊失败:', error);
message.error('结束接诊失败,请重试');
}
}
});
};
// 处理结束接诊成功事件(从 ChatArea 传递过来)
const handleEndConsultationSuccess = () => {
// 清空选中的患者
selectedPatient.value = null;
// 切换到列表模式
viewMode.value = 'list';
// 刷新患者列表
patientListRef.value?.fetchPatientList();
};
// 将患者购买的药品加入处方单
const handleAddPatientProductsToPrescription = async () => {
// 检查处方store是否已初始化
if (!prescriptionStore.currentRegisterId) {
message.warning('请先接诊患者');
return;
}
// 从聊天记录中筛选出所有 message_type=11 的消息(患者就诊经历)
const productMessages = chatStore.messages.filter(
(msg) => msg.message_type === 11
);
if (productMessages.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取诊所ID
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
}
}
let addedCount = 0;
let skippedCount = 0;
const errors = [];
try {
// 遍历每条商品消息,添加到处方单
for (const msg of productMessages) {
try {
// 解析消息内容
let productData;
try {
productData = typeof msg.message_content === 'string'
? JSON.parse(msg.message_content)
: msg.message_content;
} catch (e) {
console.error('解析就诊经历消息失败:', e);
continue;
}
// 获取药品名称(从就诊经历中提取 drug_name
const drugName = productData.drug_name;
if (!drugName) {
skippedCount++;
continue;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
errors.push(`未找到药品:${drugName}`);
skippedCount++;
continue;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
skippedCount++;
continue;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
addedCount++;
}
} catch (error) {
console.error('添加药品失败:', error);
errors.push(`添加药品失败:${error.message || '未知错误'}`);
}
}
// 显示结果消息
if (addedCount > 0) {
message.success(`已成功添加 ${addedCount} 个药品到处方单`);
// 打开处方模态框
prescriptionModalApi.setData({
registerId: prescriptionStore.currentRegisterId,
onPrescriptionSent: handlePrescriptionSent,
storagePrefix: 'onlineConsultationClinic-',
});
prescriptionModalApi.open();
} else if (skippedCount > 0) {
// 如果所有药品都无法添加,尝试添加最新的那个
// 按时间排序取最新的药品created_at 或 timestamp 最大的)
const sortedMessages = [...productMessages].sort((a, b) => {
const timeA = a.created_at ? new Date(a.created_at).getTime() : (a.timestamp || 0);
const timeB = b.created_at ? new Date(b.created_at).getTime() : (b.timestamp || 0);
return timeB - timeA; // 降序排列,最新的在前
});
if (sortedMessages.length > 0) {
const latestMessage = sortedMessages[0];
try {
// 解析最新消息的内容
let productData;
try {
productData = typeof latestMessage.message_content === 'string'
? JSON.parse(latestMessage.message_content)
: latestMessage.message_content;
} catch (e) {
console.error('解析商品消息失败:', e);
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取药品名称
const drugName = productData.drug_name || productData.name;
if (!drugName) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已成功添加 ${drugData.drug.drug_name} 到处方单`);
// 打开处方模态框
prescriptionModalApi.setData({
registerId: prescriptionStore.currentRegisterId,
onPrescriptionSent: handlePrescriptionSent,
storagePrefix: 'onlineConsultationClinic-',
});
prescriptionModalApi.open();
} else {
message.warning('当前聊天没有可添加的药品');
}
} catch (error) {
console.error('添加最新药品失败:', error);
message.warning('当前聊天没有可添加的药品');
}
} else {
message.warning('当前聊天没有可添加的药品');
}
}
// 如果有错误,显示错误信息
if (errors.length > 0) {
console.warn('添加药品时的错误:', errors);
}
} catch (error) {
console.error('批量添加药品失败:', error);
message.error('添加药品失败,请重试');
}
};
</script>
<template>
<div
class="chat-box-body flex bg-gradient-to-br from-blue-900 via-purple-900 to-pink-300 p-5"
style="height: 95vh"
>
<!-- 主容器 -->
<div
:class="leftShow ? 'container-box-grid' : 'container-box'"
class="z-10 mx-auto flex flex-1 overflow-hidden rounded-2xl bg-white/90 shadow-2xl backdrop-blur-sm"
>
<!-- 连接状态指示器 -->
<ConnectionStatus />
<!-- 处方模态框 - 使用 VbenModal 模式 -->
<PrescriptionModalComponent />
<!-- 左侧患者列表 -->
<div class="left-panel-wrapper">
<Button
:class="leftShow ? 'w-full mb-2' : 'sticky top-5 left-5'"
type="primary"
@click="leftShow = !leftShow"
>
{{ leftShow ? '收起患者列表' : '展开患者列表' }}
</Button>
<PatientList
ref="patientListRef"
v-if="leftShow"
:doctor-id="userStore.currentUser?.doctor_id"
@select-patient="handleSelectPatient"
class="patient-list-panel"
/>
</div>
<!-- 右侧内容区域 -->
<div class="right-content-area">
<!-- 列表模式显示患者信息和接诊 -->
<template v-if="viewMode === 'list'">
<PatientInfoPanel
:patient="selectedPatient"
:doctor-id="userStore.currentUser?.doctor_id"
@reception-success="handleReceptionSuccess"
@enter-chat="handleEnterChat"
class="patient-info-panel"
/>
</template>
<!-- 聊天模式显示聊天界面 -->
<template v-else>
<div class="min-w-0 flex-1 min-h-0">
<ChatArea
v-if="chatStore.currentFriend"
@open-prescription="openPrescriptionModule"
@end-consultation-success="handleEndConsultationSuccess"
/>
<EmptyState v-else />
</div>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.container-box {
min-height: 90vh;
display: flex;
flex-direction: column;
}
.container-box-grid {
min-height: 90vh;
display: grid;
grid-template-columns: 300px 1fr;
gap: 0;
}
.left-panel-wrapper {
border-right: 1px solid #e4e7ed;
padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.patient-list-panel {
flex: 1;
overflow: hidden;
}
.right-content-area {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.patient-info-panel {
flex: 1;
overflow-y: auto;
}
.dark .left-panel-wrapper {
border-right-color: #334155;
background: #1e293b;
}
.dark .container-box-grid {
background: #1f2937;
}
</style>

View File

@@ -635,7 +635,7 @@ async function handleSave() {
switch (prescriptionType.value) {
case 'west': {
const drugs = drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
select_number: drug.select_number || 1,
@@ -664,7 +664,7 @@ async function handleSave() {
}
case 'chinese': {
const drugs = drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
price: drug.price || 0,
@@ -690,7 +690,7 @@ async function handleSave() {
}
case 'granular': {
const drugs = drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
price: drug.price || 0,

View File

@@ -663,7 +663,7 @@ async function handleSave() {
case 'west': {
// 构建西药药品数据
const drugs = drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
select_number: drug.select_number || 1,
@@ -692,7 +692,7 @@ async function handleSave() {
case 'chinese': {
// 构建中药药品数据
const drugs = drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
price: drug.price || 0,
@@ -718,7 +718,7 @@ async function handleSave() {
case 'granular': {
// 构建颗粒药药品数据
const drugs = drugList.value.map((drug) => ({
drug_id: drug.drug_id || drug.id,
drug_id: drug.id,
drug_name: drug.drug_name,
number: drug.number || 1,
price: drug.price || 0,