优化功能
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
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:
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user